THE C++ PROGRAMMING LANGUAGE

64 views
Skip to first unread message

AxeL

unread,
Oct 8, 2006, 6:18:41 PM10/8/06
to InfoAb
THE C++ PROGRAMMING
LANGUAGE


BJARNE STROUSTRUP


ADDISON-WESLEY
PUBLISHING COMPANY
1986

PREFATA
===========


C++ este un limbaj de programare cu scop universal. El
contine facilitati flexibile si eficiente pentru a definii tipuri
noi. Programatorul poate partitiona o aplicatie in bucati
manevrabile prin definiri de tipuri noi, care corespund mai bine
la conceptele aplicatiei. Aceasta tehnica de construire a progra-
mului se numeste adesea abstractizare de date. Obiectele unui
anumit tip definit de utilizator contin informatie de tip. Astfel
de obiecte pot fi folosite convenabil in contextul in care tipul
lor poate fi determinat in momentul compilarii. Programele utili-
zind obiecte de astfel de tipuri se numesc adesea bazate pe
obiecte. Cind se utilizeaza bine, aceste tehnici conduc la
programe mai scurte, mai usor de inteles si mai usor de a le
menine.
Conceptul cheie din C++ este clasa. O clasa este un tip
utilizator. Clasele furnizeaza ascunderea datelor, garantarea
initializarii datelor, conversii de tip implicite pentru tipuri
de date utilizator, tipuri dinamice, gestionarea memoriei contro-
late de utilizator si mecanismul de supraincarcare a operatori-
lor. C++ furnizeaza facilitati mai bune pentru verificarea tipu-
lui si pentru exprimarea modularitatii, decit o face limbajul C.
De asemenea contine imbunatatiri care sint direct inrudite cu
clasele, incluzind constantele simbolice, substitutia in linie a
functiilor, argumente implicite ale functiilor care se suprain-
carca, operatori de gestionare a memoriei libere si un tip refe-
rinta.

OBSERVATII PENTRU CITITOR
===========================

Structura cartii
----------------

Capitolul 1 este o trecere in revista rapida a caracteris-
ticilor majore ale lui C++. La inceput sint caracteristici comune
cu C. Cea de-a doua jumatate descrie facilitatile lui C++ pentru
a definii tipuri noi.
Capitolele 2, 3 si 4 descriu caracteristicile lui C++ care
nu sint implicate in definirea de tipuri noi. Deci se descrie
subsetul lui C++ care este in esenta C. Informatia completa se da
in manualul de referinta.
Capitolele 5, 6, 7 descriu facilitatile lui C++ pentru a
definii tipuri noi, trasaturi care nu fac parte din C. Capitolul
5 prezinta conceptul de clasa, aratind cum obiectele de tip
utilizator, pot fi initializate, accesate si in final eliminate.
Capitolul 6 explica cum se pot definii operatorii unari si binari
pentru un tip definit de utilizator, cum se pot face conversatii
intre tipurile definite de utilizator si cum se specifica modul
de creare, stergere si copiere a unei valori a unui tip definit
de utilizator. Capitolul 7 descrie conceptul de clasa derivata,
care permite programatorului sa construiasca clase mai complexe
din unele mai simple, pentru a furniza interfete alternative
pentru o clasa si a minui obiecte intr-o maniera eficienta si in
deplina protectie, in contextul in care tipurile lor nu pot fi
cunoscute la compilare.
Capitolul 8 prezinta clasele ostream si istream furnizate
pentru intrari si iesiri din biblioteca standard. Acest capitol
prezinta o facilitate care este un exemplu real de utilizare a
lui C++.
In final este inclus manualul de referinta C++.

Trimiterile se descriu astfel:

&2.3.4 -> capitolul 2, sectiunea 3.4;
&r8.5.5 -> trimitere in partea de referinta.

Exercitiile sint gradate astfel:

(*1) - exercitiul necesita 5 minute;
(*2) - exercitiul necesita o ora;
(*3) - exercitiul necesita o zi.

Observatii de proiectare
------------------------

C++ nu are tipuri de date de nivel inalt si nici operatii
primitive de nivel inalt. De exemplu, nu exista tipul matrice cu
un operator de inversare sau tipul sir cu operatorul de concate-
nare. Daca un utilizator doreste un astfel de tip, el poate fi
definit. Defapt, definirea unui tip nou cu scopuri generale sau
specific aplicative este scopul cel mai important al limbajului
C++. Un tip definit de utilizator difera de unul predefinit numai
in modul de definire si nu si in modul in care este utilizat.

Note istorice
-------------

C++ a aparut in vara anului 1983 (C cu clase). Incercari au
fost facute inca din 1980. C++ inseamna C incrementat. C++ are ca
scop principal scrierea de programe bune mai usor si mai placut
pentru programatorul individual.
O sursa de inspiratie a fost Simula 67; conceptul de clasa a
fost imprumutat de aici.
C si ANSI C sint foarte apropiate pentru a fi un subset a
lui C++. C++ a fost dezvoltat din C si cu foarte mici exceptii C
ramine un subset a lui C++.

Observatii filozofice
---------------------

Un limbaj de programare serveste la doua scopuri inrudite:
el furnizeaza un mijloc pentru programator de a specifica actiuni
de executat si un set de concepte pentru programator care sa fie
utilizate cind se gindeste in legatura cu ceea ce poate fi facut.
Primul aspect in mod ideal cere un limbaj ce este "strins
legat de masina" asa incit toate aspectele unei masini sa fie
manevrate simplu si eficient intr-un mod care sa fie rezonabil de
clar pentru programator. Limbajul C initial a fost proiectat
avind acest lucru in minte.
Cel de al doilea aspect in mod ideal cere un limbaj care
este "strins legat de problema de rezolvat", asa ca, conceptele
unei solutii sa poata fi exprimate direct si concis. Facilitatile
adaugate la C pentru a crea C++ initial au fost proiectate avind
acest lucru in minte.
Legatura dintre limbajul in care noi gindim programul si
dintre cel in care ne imaginam problemele si solutiile este
foarte strinsa. Din acest motiv, restringerea caracteristicilor
cu scopul de a elimina erorile programatorului este cel mai
periculos. Tot asa cu limbajele naturale, exista un beneficiu
mare din faptul ca sint cel putin bilingve. Limbajul furnizeaza
programatorului un set de instrumente conceptuale: daca acestea
sint inadecvate pentru un task, ele pur si simplu vor fi igno-
rate. De exemplu, restringind serios conceptul de pointer, pur si
simplu se forteaza programatorul ca sa utilizeze un vector plus
aritmetica intreaga pentru a implementa structuri, pointeri, etc.
Un proiect bun si absenta erorilor nu poate fi garantata
numai prin caracteristicile limbajului.
Sistemul tipurilor ar trebui sa fie in special util pentru
task-uri netriviale.

Ginduri despre programare in C++
--------------------------------

Ideal sarcina de concepere a unui program este impartita in
3 stadii: primul consta in intelegerea clara a problemei, apoi
identificare conceptelor cheie implicate intr-o solutie si in
final exprimarea solutiei printr-un program. Totusi, detaliile
problemei si conceptele unei solutii adesea devin clar intelese
numai prin efortul de a le exprima intr-un program; acesta este
motivul alegerii limbajului de programare.
In cele mai multe aplicatii exista concepte care nu sint
reprezentate usor intr-un program nici printr-un tip fundamental
si nici printr-o functie fara date statice asociate. Dindu-se un
astfel de concept, se declara o clasa pentru a-l reprezenta in
program. O clasa este un tip; adica, ea specifica cum obiectele
din clasa se dezvolta: cum se creaza, cum pot fi manipulate, cum
se anihileaza. O clasa de asemenea specifica cum se reprezinta
obiectele, dar la un stadiu mai initial al proiectarii programu-
lui aceasta nu trebuie sa fie o conceptie majora. Cheia scrierii
unui program bun este de a proiecta clasele in asa fel incit
fiecare, in mod clar, sa reprezinte un singur concept. Adesea
aceasta inseamna ca programatorul trebuie sa se concetreze asupra
problemelor: cum se creaza obiectele din aceasta clasa? se poate
ca obiectele din aceasta clasa sa fie copiate si/sau distruse? ce
operatii pot fi facute cu astfel de obiecte? Daca nu sint raspun-
suri bune la astfel de intrebari, conceptul probabil ca nu a fost
clar definit si va trebui sa ne mai gindim asupra lui.
Conceptele cu care este mai usor sa ne ocupam sint cele care
au un formalism matematic traditional: numere de toate felurile,
multimi, forme geometrice, etc.. Se cuvine sa fie biblioteci
standard de clase care sa reprezinte astfel de concepte.
Unul dintre cele mai puternice instrumente intelectuale
pentru tratarea complexitatilor este ordonarea ierarhica; adica
organizarea conceptelor inrudite intr-o structura de arbore cu
cel mai general concept in radacina. In C++ clasele derivate
reprezinta o astfel de structura. Un program poate fi adesea
organizat ca o multime de arbori.

Reguli
------

Iata citeva reguli care trebuiesc considerate cind invatam
C++.

[1] Cind programam, noi cream o reprezentare concreta a ideilor
ce constituie solutia noastra la o anumita problema. Structura
programului reflecta acele idei atit de direct cit este posibil:

[a] Daca noi putem sa ne gindim la "el" ca la o idee separa-

ta, sa-l facem o clasa.
[b] Daca noi putem sa ne gindim la "el" ca la o entitate

separata, sa-l facem obiect al unei anumite clase.
[c] Daca doua clase au ceva seminificativ in comun, aceasta
se face o clasa de baza. Majoritatea claselor din pro-
gramul nostru vor avea ceva in comun: au o clasa de baza

universala si ea trebuie proiectata cu multa atentie.

[2] Cind noi definim o clasa care nu implementeaza o entitate
matematica ca o matrice sau un numar complex sau un tip de nivel
inferior ca o lista inlantuita:

[a] Sa nu se utilizeze date globale.
[b] Sa nu se utilizeze functii globale (care nu sint

membri).
[c] Sa nu se utilizeze membri ale datelor publice.
[d] Sa nu se utilizeze frati, exceptind cazul in care ei se
folosesc pentru a elimina [a], [b] sau [c].
[e] Sa nu se faca acces direct la membri de date a altor

obiecte.
[f] Sa nu se puna un tip 'cimp' intr-o clasa; sa se utili
zeze functii virtuale.
[g] Sa nu se utilizeze functii inline; exceptind cazul unei
optimizari semnificative.


CUPRINS
=======


NUME PAG.
=================================================================

CAP.1 === "TUTORUL" LUI C++ 1
1.1. Introducere 1
1.1.1. Iesire 1
1.1.2. Compilare 1
1.1.3. Intrare 1
1.2. Comentariu 2
1.3. Tipuri si Declaratii 2
1.3.1. Tipuri fundamentale 2
1.3.2. Tipuri derivate 3
1.4. Expresii si Instructiuni 3
1.5. Functii 5
1.6. Structura programului 6
1.7. Clase 7
1.8. Operatorul overloading 8
1.9. Referinte 9
1.10. Constructori 10
1.11. Vectori 11
1.12. Expandare inline 13
1.13. Clase derivate 13
1.14. Mai mult despre operatori 15
1.15. Prieteni (Friends) 17
1.16. Vectori generici 18
1.17. Vectori polimorfici 18
1.18. Functii virtuale 20

CAP.2 === DECLARATII SI CONSTANTE 21
2.1. Declaratii 21
2.1.1. Domeniu 22
2.1.2. Obiecte si Lvalori 24
2.1.3. Durata de viata 24
2.2. Nume 25
2.3. Tipuri 25
2.3.1. Tipuri fundamentale 26
2.3.2. Conversia implicita de tip 27
2.3.3. Tipuri derivate 28
2.3.4. Void 29
2.3.5. Pointeri 30
2.3.6. Vectori 31
2.3.7. Pointeri si Vectori 32
2.3.8. Structuri 34
2.3.9. Echivalenta tipurilor 36


NUME PAG.
=================================================================
2.3.10. Referinte 36
2.3.11. Registrii 39
2.4. Constante 40
2.4.1. Constante intregi 40
2.4.2. Constante in flotanta 41
2.4.3. Constante caracter 41
2.4.4. Siruri 42
2.4.5. Zero 43
2.4.6. Const 43
2.4.7. Enumerari 45
2.5. Salvarea spatiului 46
2.5.1. Cimpuri 46
2.5.2. Reuniuni 47
2.6. Exercitii 49

CAP.3 === EXPRESII SI INSTRUCTIUNI 51
3.1. Un calculator de birou 51
3.1.1. Analizorul 52
3.1.2. Functia de intrare 56
3.1.3. Tabela de simboluri 58
3.1.4. Tratarea erorilor 60
3.1.5. Driverul 61
3.1.6. Argumentele liniei de comanda 62
3.2. Sumar de operatori 63
3.2.1. Paranteze rotunde 65
3.2.2. Ordinea de evaluare 66
3.2.3. Incrementare si Decrementare 67
3.2.4. Operatori logici pe biti 68
3.2.5. Conversia tipului 69
3.2.6. Memoria libera 70
3.3. Sumarul instructiunilor 73
3.3.1. Teste 74
3.3.2. Goto 76
3.4. Comentarii si Decalari 77
3.5. Exercitii 79

CAP.4 === FUNCTII SI FISIERE 83
4.1. Introducere 83
4.2. Linkare 84
4.3. Fisiere antet 86
4.3.1. Fisier antet unic 88
4.3.2. Fisiere antet multiple 90
4.3.3. Ascunderea datelor 92
4.4. Fisiere si Module 93
4.5. Cum se construieste o biblioteca 93
4.6. Functii 95
4.6.1. Declaratii de functii 95
4.6.2. Definitii de functii 95
4.6.3. Transferul argumentelor 96
4.6.4. Valoarea returnata 97
4.6.5. Argumente vector 98
4.6.6. Argumente implicite 99
4.6.7. Nume de functii supraincarcate 100

NUME PAG.
=================================================================
4.6.8. Numar nespecificat de argumente 102
4.6.9. Pointer spre functie 104
4.7. Macrouri 107
4.8. Exercitii 110

CAP.5 === CLASE 113
5.1. Introducere si privire generala 113
5.2. Clase si Membri 114
5.2.1. Functii membru 114
5.2.2. Clase 115
5.2.3. Autoreferinta 116
5.2.4. Initializare 118
5.2.5. Curatire (Stergere) 120
5.2.6. In linie 121
5.3. Interfete si Implementari 121
5.3.1. Implementari alternative 122
5.3.2. O clasa completa 125
5.4. Prieteni si Reuniuni 128
5.4.1. Prieteni 128
5.4.2. Calificarea numelor membre 131
5.4.3. Clase imbricate 131
5.4.4. Membri statici 132
5.4.5. Pointeri spre membri 133
5.4.6. Structuri si Reuniuni 134
5.5. Constructori si Destructori 137
5.5.1. Goluri 137
5.5.2. Memoria statica 138
5.5.3. Memoria libera 139
5.5.4. Obiectele clasei de membri 140
5.5.5. Vectori si Obiecte clasa 142
5.5.6. Obiecte mici 143
5.5.7. Goluri 144
5.5.8. Obiecte de dimensiune variabila 145
5.6. Exercitii 147

CAP.6 === OPERATOR SUPRAINCARCAT 149
6.1. Introducere 149
6.2. Functiile operator 150
6.2.1. Operatori binari si unari 151
6.2.2. Sensul predefinit al operatorilor 151
6.2.3. Operatori si Tipuri definite de
utilizatori 152
6.3. Conversia de tip definita de utilizator 152
6.3.1. Constructori 153
6.3.2. Operatori de conversie 154
6.3.3. Ambiguitati 155
6.4. Constante 157
6.5. Obiecte mari 157
6.6. Asignare si Initializare 158
6.7. Indexare 160
6.8. Apelul unei functii 162
6.9. O clasa sir 163
6.10. Prieteni si Membri 166

NUME PAG.
=================================================================
6.11. Goluri 167
6.12. Exercitii 168

CAP.7 === CLASE DERIVATE 171
7.1. Introducere 171
7.2. Clase derivate 172
7.2.1. Derivare 172
7.2.2. Functii membru 173
7.2.3. Vizibilitate 175
7.2.4. Pointeri 176
7.2.5. Ierarhizarea claselor 177
7.2.6. Constructori si Destructori 178
7.2.7. Cimpuri de tip 179
7.2.8. Functii virtuale 181
7.3. Interfete alternative 183
7.3.1. O interfata 183
7.3.2. O implementare 184
7.3.3. Cum sa o folosim 186
7.3.4. Tratarea erorilor 187
7.3.5. Clase generice 189
7.3.6. Interfete restrictive 190
7.4. Adaugarea la o clasa 191
7.5. Liste eterogene 193
7.6. Un program complet 193
7.6.1. Controlul ecranului 193
7.6.2. Biblioteca de figuri 196
7.6.3. Programul de aplicatie 198
7.7. Memoria libera 200
7.8. Exercitii 202

CAP.8 === STREAMS 205
8.1. Introducere 205
8.2. Iesire 206
8.2.1. Iesirea tipurilor predefinite 206
8.2.2. Iesirea tipurilor definite de
utilizator 207
8.2.3. Citeva detalii de implementare 208
8.2.4. Iesire formatata 209
8.2.5. O functie de iesire virtuala 212
8.3. Fisiere si Streamuri 213
8.3.1. Initializarea streamurilor de
iesire 213
8.3.2. Inchiderea streamurilor de iesire 214
8.3.3. Deschiderea fisierelor 214
8.3.4. Copierea streamurilor 215
8.4. Intrari 215
8.4.1. Introducerea tipurilor predefi-
nite 216
8.4.2. Starile streamului 217
8.4.3. Introducerea tipurilor definite
de utilizator 218
8.4.4. Initializarea streamurilor de
intrare 219

NUME PAG.
=================================================================
8.5. Manipularea sirurilor 220
8.6. Blocare in bufer 221
8.7. Eficienta 223
8.8. Exercitii 224

MANUAL DE REFERINTA 227
1. Introducere 227
2. Conventii lexicale 227
2.1. Comentarii 227
2.2. Identificatori (Nume) 227
2.3. Cuvinte cheie 228
2.4. Constante 228
2.4.1. Constante intregi 228
2.4.2. Constante long explicite 228
2.4.3. Constante caracter 229
2.4.4. Constante flotante 229
2.4.5. Constante enumerative 229
2.4.6. Constante declarate 229
2.5. Siruri 230
2.6. Caracteristici hardware 230
3. Notatia sintactica 230
4. Nume si Tipuri 231
4.1. Domenii 231
4.2. Definitii 232
4.3. Linkare 232
4.4. Clase de memorie 232
4.5. Tipuri fundamentale 232
4.6. Tipuri derivate 233
5. Obiecte si Lvalori 233
6. Conversii 234
6.1. Caractere si Intregi 234
6.2. Flotante in simpla si dubla pre-
cizie 234
6.3. Flotante si Intregi 234
6.4. Pointeri si Intregi 235
6.5. Intregi fara semn 235
6.6. Conversii aritmetice 235
6.7. Conversii de pointeri 236
6.8. Conversie de referinta 236
7. Expresii 236
7.1. Expresii primare 237
7.2. Operatori unari 239
7.2.1. Incrementare si Decrementare 239
7.2.2. Sizeof 240
7.2.3. Conversie explicita de tip 240
7.2.4. Memoria libera 241
7.3. Operatori multiplicatori 242
7.4. Operatori aditivi 242
7.5. Operatori de deplasare 243
7.6. Operatori relationali 243
7.7. Operatori de egalitate 244
7.8. Operatorul SI pe biti 244
7.9. Operatorul SAU-EXCLUSIV pe biti 244

NUME PAG.
=================================================================
7.10. Operatorul SAU-INCLUSIV pe biti 244
7.11. Operatorul logic SI 244
7.12. Operatorul logic SAU 245
7.13. Operator conditional 245
7.14. Operatori de asignare 245
7.15. Operatorul virgula 246
7.16. Operatori de supraincarcare 246
7.16.1. Operatori unari 247
7.16.2. Operatori binari 247
7.16.3. Operatori speciali 247
8. Declaratii 247
8.1. Specificatori de clasa de memorie 248
8.2. Specificatori de tip 249
8.3. Declaratori 250
8.4. Intelesul ( sensul ) declaratorilor 251
8.4.1. Exemple 253
8.4.2. Tablouri, Pointeri si Indici 254
8.5. Declaratii de clasa 255
8.5.1. Membri statici 256
8.5.2. Functii membru 257
8.5.3. Clase derivate 258
8.5.4. Functii virtuale 259
8.5.5. Constructori 259
8.5.6. Conversii 260
8.5.7. Destructori 261
8.5.8. Memoria libera 261
8.5.9. Vizibilitatea numelor membri 262
8.5.10. Prieteni 263
8.5.11. Functii operator 264
8.5.12. Structuri 264
8.5.13. Reuniuni 264
8.5.14. Cimpuri de biti 265
8.5.15. Clase imbricate 265
8.6. Initializare 266
8.6.1. Liste initializatoare 266
8.6.2. Obiecte de clasa 267
8.6.3. Referinte 268
8.6.4. Tablouri de caractere 269
8.7. Nume de tip 269
8.8. Typedef 270
8.9. Nume de functii supraincarcate 271
8.10. Declaratii de enumerare 272
8.11. Declaratia ASM 273
9. Instructiuni 273
9.1. Instructiunea expresie 273
9.2. Instructiunea compusa (blocul) 273
9.3. Instructiunea conditionala 274
9.4. Instructiunea WHILE 274
9.5. Instructiunea DO 274
9.6. Instructiunea FOR 274
9.7. Instructiunea SWITCH 275
9.8. Instructiunea BREAK 276
9.9. Instructiunea CONTINUE 276

NUME PAG.
=================================================================
9.10. Instructiunea RETURN 276
9.11. Instructiunea GOTO 277
9.12. Instructiunea etichetata 277
9.13. Instructiunea NULL 277
9.14. Instructiunea declarativa 277
10. Definitii de functii 278
11. Linii de control ale compilatorului 279
11.1. Substitutia de siruri 280
11.2. Incluziune de fisiere 280
11.3. Compilarea conditionata 281
11.4. Linie de control 281
12. Expresii constante 282
13. Consideratii de portabilitate 282
14. Sumar de sintaxa 283
14.1. Expresii 283
14.2. Declaratii 284
14.3. Instructiuni 286
14.4. Definitii externe 286
14.5. Preprocesor 287
15. Diferente fata de C 287
15.1. Extensii 287
15.2. Sumar de incompatibilitati 288
15.3. Anacronisme 288


CAPITOLUL 1
===========


"TUTORUL" LUI C++
=================


1.1 Introducere
-----------

1.1.1 Iesire
------

#include <stream.h>
main()
{
cout << "Hello, world\n";
}

#include <stream.h> - include declaratii pentru facilitatile standard
de
intrare/iesire aflate in stream.h.
Operatorul << scrie cel de al doilea operand al sau peste primul.


1.1.2 Compilare
---------
Se apeleaza cu litere mari CC. Daca programul este in fisierul
hello.c,
atunci se compileaza si se executa ca mai jos:

$CC hello.c
$a.out
Hello, world
$


1.1.3 Intrare
-------
#include <stream.h>
main() //converteste inch in cm
{int inch = 0;
cout << "inches";
cin >> inch;
cout << inch;
cout << "in=";
cout << inch*2.54;
cout << "cm\n";
}

Exemplu de executie
$a.out
inches = 12
12 in = 30.48 cm
$

Ultimii 4 operatori pot fi scrisi:
cout << inch << "in=" << inch*2.54 << "cm\n";


1.2 Comentariu
----------
Incepe prin /* si se termina prin */.
Comentariu poate incepe prin // si se termina la sfirsitul =" <<
inch*2.54 << "cm\n";
trateaza corect cele 4 valori de iesire care sint diferite.
C++ are citeva tipuri de baza si diferite moduri de a crea altele
noi.


1.3.1 Tipuri fundamentale
-------------------
char short int long float double
sizeof(char) <= sizeof(short) <= sizeof(int) <=
sizeof(long) <= sizeof(float) <= sizeof(double)
const float pi = 3.14;
const char plus = '+';
Operatori aritmetici:
+ - (unari si binari ambii)
* / %
Operatori de comparare ca in C.
double d = 1; int i = 1;
d = d + i; i = d + i;


1.3.2 Tipuri derivate
---------------
* -> pointer
*const -> pointer constant
& -> adresa
[] -> vector
() -> functie

char* p;
char *const q;
char v[10];
char c;
//......
p = &c; // p pointeaza spre c

1.4 Expresii si Instructiuni
------------------------

~ &(si) ^ | << >> se aplica la intregi
= op=
x = sqrt (a = 3*x)
++ --

Cea mai frecventa forma a unei instructiuni este o instructiune
expresie;
ea consta dintr-o expresie urmata de un punct si virgula.

a = b*3+c;
cout << "go go go";
lseek(fd, 0, 2);


Instructiunea VIDA:
;


Blocuri:

{
a = b + 2;
b++;
}


Instructiunea IF:

#include <stream.h>
main() //converteste din inch in cm si invers
{
const float fac = 2.54;
float x, in, cm;
char ch = 0;
cout << "enter lenght:";
cin >> x >> ch;
if(ch=='i')
{ //inch
in = x;
cm = x*fac;
}
else
if(ch=='c')
{ //cm
in = x/fac;
cm = x;
}
else
in = cm = 0;
cout << in << "in=" << cm << "cm\n";
}


Instructiunea SWITCH:

switch(ch)
{
case 'i': in = x;
cm = x*fac;
break;
case 'c': in = x/fac;
cm = x;
break;
default: in = cm = 0;
break;
}

Instructiunea WHILE:

while(*p!=0)
{
*q = *p;
q = q+1;
p = p+1;
}
*q = 0;
while(*p)
*q++ = *p++;
*q = 0;
while(*q++ = *p++);

Instructiunea FOR:

for(int i=0; i<10; i++)
q[i] = p[i];

Declaratii:

for(int i=1; i<MAX; i++)
{
int t = v[i-1];
v[i-1] = v[i];
v[i] = t;
}

1.5 Functii
-------

O functie este o parte denumita a programului care poate fi
apelata din
alte parti ale programului atit de des, cit este nevoie.
extern float pow(float, int);
// pow este definita in alta parte
main()
{for(int i=0; i<10; i++)
cout << pow(2, 1) << "\n";
pow(12.3, "abcd") //este eroare
}
float pow(float x, int n)
{
if(n<0)
error("expresie negativ pentru pow");
switch(n)
{
case 0: return 1;
case 1: return x;
default: return x*pow(x, n-1);
}
}

overload pow;
int pow(int, int);
double pow(double, double);
//.......
x = pow(2, 10);
y = pow(2.0, 10.0);

Declaratia overload pow informeaza compilatorul ca se
intentioneaza sa se
foloseasca numele pow pentru mai mult decit o singura functie.

Daca o functie nu returneaza o valoare trebuie sa se declare void:

void swap(int* p, int* q)
{
int t = *p;
*p = *q;
*q = t;
}


1.6 Structura programului
---------------------

Un nume care se utilizeaza ca sa refere acelasi lucru in doua
fisiere
sursa trebuie sa fie declarat ca extern:

extern double sqrt(double);
extern istream cin;

Este bine ca aceste declaratii sa se plaseze intr-un fisier si
apoi
acesta sa se includa. De exemplu, daca declaratia pentru sqrt() este in

math.h
atunci putem scrie:

#include <math.h>
//........
x = sqrt(4);

Daca este intre paranteze unghiulare se include de obicei din
/usr/include/CC. Altfel se folosesc ghilimele.

#include "math1.h"
#include "/usr/bs/math2.h"


Mai jos un sir se defineste intr-un fisier si se scrie in altul.

//header.h
extern char* prog_name;
extern void f();

Fisierul main.c este programul principal:

#include "header.h"
char* prog_name = "silly, but complete";
main()
{
f();
}

si fisierul f.c imprima sirul:

#include <stream.h>
#include "header.h"
void f(){ cout << prog_name << "\n"; }
La executie se obtine textul:

$CC main.c f.c -o silly
$silly
silly, but complete
$


1.7 Clase
-----
Sa vedem cum putem defini tipul ostream. Pentru a simplifica
aceasta
sarcina, presupunem ca s-a definit tipul streambuf pentru buferarea
caracterelor. Un streambuf este in realitate definit in <stream.h> unde

se
gaseste de asemenea definitia reala a lui ostream.
Definitia tipului utilizator (numit clasa in C++) contine o
specificatie
a datei necesare pentru a reprezenta un obiect de acest tip si o
multime de
operatii pentru a manevra astfel de obiecte. Definitia are doua parti:
o parte
privata ce pastreaza informatia care poate fi utilizata numai de
implementatorul ei si o parte publica ce reprezinta o interfata cu
utilizatorul:
class ostream{
streambuf* buf;
int state;
public:
void put(char*);
void put(long);
void put(double);
};

Declaratiile dupa eticheta public specifica interfata;
utilizatorul poate
apela cele 3 functii put(). Declaratiile ce se gasesc inaintea
etichetei
public specifica reprezentarea unui obiect al clasei ostream. Numele
buf si
state pot fi utilizate numai prin functiile put() declarate in partea
public.
O clasa defineste un tip si nu un obiect data, asa ca pentru a
utiliza un
ostream noi trebuie sa declaram unul (in acelasi mod in care noi
declaram
variabilele de tip int):

ostream my_out;

Presupunind ca my_out a fost deja initializat in mod
corespunzator, el
poate fi utilizat acum astfel:

my_out.put("Hello, world\n");

Operatorul se foloseste pentru a selecta un membru al clasei
pentru un
obiect dat al acelei clase. Aici functia membru put() se apeleaza
pentru
obiectul my_out.

Functia poate fi declarata astfel:

void ostream::put(char* p)
{
while(*p)
buf.sputc(*p++);
}

unde sputc() este o functie care pune un caracter in streambuf.
Prefixul
ostream este necesar pentru a distinge put() a lui ostream de alte
apeluri ale
lui put().
Pentru a apela o functie membru, un obiect al clasei trebuie sa
fie
specificat. In functia membru, acest obiect poate fi implicit
referentiat asa
cum se face in ostream::put() de mai sus; in fiecare apel, buf se
refera la
membrul buf al obiectului pentru care se apeleaza functia.
Este de asemenea posibil sa ne referim explicit la acel obiect
printr-un
pointer numit this. Intr-o functie membru al unei clase X, acesta este
implicit declarat ca X* (pointer spre X) si initializat cu un pointer
spre
obiectul pentru care functia este apelata. Definitia lui ostream::put()

ar
putea fi scrisa astfel:
void ostream::put(char* p)
{
while(*p)
this->buf.sputc(*p++);
}

Operatorul -> se utilizeaza pentru a selecta un membru al unui
obiect.

1.8 Operatorul overloading
----------------------

Clasa reala ostream defineste operatorul << pentru a-l face
convenabil sa
scrie diferite obiecte cu o singura instructiune.
Pentru a defini @, unde @ este orice operator C++ pentru un tip
definit
de utilizator, noi definim o functie numita operator@ care are
argumente de
tip corespunzator. De exemplu:

class ostream{ //........
ostream operator<<(char*);
};
ostream ostream::operator<<(char* p)
{
while(*p)
buf.sputc(*p++);
return *this;
}

defineste operatorul <<, ca membru al clasei ostream, asa ca s<<p se
interpreteaza ca s.operator<<(p) cind s este un ostream si p este un
pointer
spre caractere. Operatorul << este binar, dar functia operator<<(char*)

pare
la prima vedere sa aiba un singur argument; el totusi are de asemenea
argumentul standard implicit this.
Revenind din ostream ni se permite sa aplicam << la rezultatul
unei
operatii de iesire. De exemplu, s<<p<<q se interpreteaza
(s.operator<<(p)).operator<<(q). Acesta este modul in care operatiile
sint
furnizate pentru tipuri predefinite.
Utilizind setul de operatii furnizate cu membri publici ai clasei
ostream, noi putem acum defini << pentru tipuri utilizator cum ar fi
cel
complex, fara a modifica declaratia clasei ostream:

ostream operator<<(ostream s, complex z)
// un complex are doua parti: real si imaginar
// se scrie un complex ca (real, imag)
{
return s << "(" << z.real << "," << z.imag << ")";
}

Intrucit operator<<(ostream, complex) nu este o functie membru,
este
necesar sa explicitam argumentele ca sa fie binare.
Se vor scrie valorile in ordine corecta caci <<, ca si majoritatea
operatorilor C++ se grupeaza de la stinga la dreapta; adica a<<b<<c
inseamna
(a<<b)<<c.
Compilatorul cunoaste diferenta dintre functiile membru si
nemembru cind
interpreteaza operatorii. De exemplu, daca z este o variabila complexa,

s<<z
va fi expandata utilizind apelul functiei standard (nemembru)
operator<<(s,z).

1.9 Referinte
---------

Ultima versiune a lui ostream din nefericire contine o eroare
serioasa.
Problema este ca ostream este copiat de doua ori pentru fiecare
utilizare a
lui <<: odata ca un argument si odata ca valoare returnata. Aceasta
lasa
starea nemodificata dupa fiecare apel. Este nevoie de o facilitate
pentru a
pasa un pointer la ostream mai degraba decit sa se paseze insasi
ostream.
Aceasta se poate realiza utilizind referintele. O referinta
actioneaza ca
un nume pentru un obiect; T& inseamna referinta la T. O referinta
trebuie
initializata si devine un nume alternativa pentru obiectul cu care este
initializat. De exemplu:
ostream& s1 = my_out;
ostream& s2 = cout;
Referintele s1 si my_out pot fi utilizate acum in acelasi mod si cu
acelasi
inteles. De exemplu, atribuirea:
s1 = s2;
copiaza obiectul referit prin s2 (adica cout) in obiectul referit prin
s1
(adica my_out). Membri se selecteaza utilizind operatorul punct:
s1.put("don't use ->");
si daca utilizam operatorul adresa, primim adresa obiectului referit:

&s1 == &my_out

Prima utilizare evidenta a referintei este ca sa ne asiguram ca
adresa
unui obiect, mai degraba decit obiectul insusi, este pasata la o
functie de
iesire (aceasta se numeste in anumite limbaje apel prin referinta):

ostream& operator<<(ostream& s, complex z)
{
return s << "(" << z.real << "," << z.imag << ")";
}

Corpul functiei este neschimbat dar asignarea facuta lui s va
afecta acum
obiectul dat ca argument. In acest caz, returnindu-se o referinta de
asemenea
se imbunatateste eficienta, intru- cit modul evident de implementare a
unei
referinte este un pointer si un pointer este mai ieftin sa fie
transferat
decit o structura mare.
Referintele sint de asemenea esentiale pentru definirea sirurilor
de
intrare deoarece operatorului input i se da variabila in care se
citeste ca
operand. Daca referintele nu sint utilizate, utilizatorul ar trebui sa
paseze
pointeri expliciti functiilor de intrare:

class istream{ //........
int state;
public:
istream& operator>>(char&);
istream& operator>>(char*);
istream& operator>>(int&);
istream& operator>>(long&);
//........
};

Sa observam ca se folosesc doua operatii separate pentru a citi
intr-o
zona long si intr-o zona int si numai una pentru scriere. Motivul este
ca un
int poate fi convertit spre long prin regulile implicite de conversie.


1.10 Constructori
------------
Definirea lui ostream ca si clasa, face ca datele membru sa fie
private.
Numai o functie membru poate accesa membri privati, asa ca noi trebuie
sa
furnizam una pentru initializare. O astfel de functie se numeste
constructor
si se distinge avind acelasi nume ca si al clasei lui:
class ostream{ //.......
ostream(streambuf*);
ostream(int size, char* s); };
Aici se furnizeaza doi constructori. Unul ia un streambuf pentru
o
iesire reala iar celalalt ia o dimensiune si un pointer spre caractere
pentru
formatarea sirului. Intr-o declaratie, argumentul lista necesar pentru
un
constructor se adauga la nume. Noi putem declara acum streamuri astfel:
ostream my_out(&some_stream_buffer);
char xx[256];
ostream xx_stream(256,xx);
Declaratia lui my_out seteaza nu numai cantitatea corespunzatoare

de
memorie ci de asemenea apeleaza si constructorul
ostream::ostream(streambuf*)
pentru a-l initializa cu argumentul &some_stream_buffer, care este un
pointer
spre un obiect potrivit al clasei streambuf. Declaratia functiei
xx_stream()
se trateaza similar, dar utilizeaza celalalt constructor. Declarind
construc-
tori pentru o clasa nu furnizam numai un mod de a initializa obiecte,
ci de
asemenea se asigura ca toate obiectele clasei vor fi initializate. Cind

s-a
declarat un constructor pentru o clasa, nu este posibil sa se declare o
variabila a acelei clase fara a apela un constructor. Daca o clasa are
un
constructor care nu ia argumente, acel constructor va fi apelat daca nu

se da
nici un argument in declaratie.


1.11 Vectori
-------

Conceptul de vector construit in C++ a fost proiectat pentru a
permite o
eficienta maxima la executie si memorie minima.
Este de asemenea, mai ales cind se utilizeaza impreuna cu
pointerii, un
instrument puternic pentru construirea unor facilitati de nivel inalt.
Noi
putem, totusi, sa ne plingem de faptul ca dimensiunea unui vector
trebuie sa
fie specificata ca o constanta, ca nu exista verificarea depasirii
limitelor
vectorilor, etc.. Un raspuns la aceste plingeri este: noi insine putem
programa acest lucru. Sa vedem daca acesta este un raspuns rezonabil,
cu alte
cuvinte, sa testam facilitatile de abstractizare ale lui C++ incercind
sa
furnizam aceste caracteristici pentru tipurile de vectori proiectati de

noi si
sa observam dificultatile implicate, costurile implicate si comoditatea
utilizarii tipurilor de vectori rezultate.

class vector{
int* v;
int sz;
public:
vector(int); //constructor
~vector(); //destructor
int size(){return sz;}
void set_size(int);
int& operator[](int);
int& elem(int i){return v[i];}
};

Functia size() returneaza numarul de elemente al vectorului;
adica,
indicii trebuie sa fie in domeniul 0..size()-1. Functia set_size() este
furnizata pentru a schimba acea dimensiune, elem() furnizeaza acces la
membri
fara a verifica indexul, iar operator[] furnizeaza acces cu verificarea
limitelor.
Ideea este de a avea clasa ca o structura de dimensiune fixa care
controleaza accesul la memoria reala a vectorului, care este alocata
prin
constructorul vectorului utilizind operatorul new de alocare de
memorie.

vector::vector(int s)
{if(s<=0)
error("bad vector size");
sz = s;
v = new int[s];
}
Noi putem declara vectori foarte asemanator cu vectorii care sint
construiti in limbajul insusi:

vector v1(100);
vector v2(nelem*2-4);

Operatia de acces poate fi definita ca:

int& vector::operator[](int i)
{
if(i<0 || sz<=i)
error("vector index out of range");
return v[i];
}

Returnind o referinta se asigura ca notatia [] poate fi utilizata
de
ambele parti a unei atribuiri:

v1[x] = v2[y];

Functia ~vector() este un destructor; adica este o functie
declarata
pentru a fi apelata implicit cind obiectul unei clase iese in afara
domeniului. Destructorul pentru o clasa C se numeste ~C. Daca noi o
definim
astfel:
vector::~vector()
{
delete v;
}

ea va fi utilizata pentru a sterge operatorul si pentru a dezaloca
spatiul
alocat prin constructor, asa ca atunci cind un vector iese afara din
domeniu,
tot spatiul lui este eliberat si poate fi reutilizat.

1.12 Expandare inline
----------------

O functie membru nu este mai costisitoare la apel decit o functie
nemembru cu acelasi numar de argumente (sa ne amintim ca o functie
membru
totdeauna are cel putin un argument), iar apelul functiilor C++ este
aproximativ tot atit de eficient ca si in alte limbaje. Totusi, pentru
functiile extrem de mici, apelul poate sa iasa in evidenta. Daca este
asa, noi
am putea dori sa specificam o functie care sa expandeze in linie. In
caz
afirmativ, compilatorul va genera cod propriu pentru functie in locul
apelului. Semanticile apelului ramin neschimbate. De exemplu, daca
size() si
elem() sint substituite in linie:
vector s(100);
//..........
i = s.size();
x = elem(i-1);
este echivalent cu
i = 100;
x = s.v[i-1];

Compilatorul este destul de abil pentru a genera un cod care este
tot
atit de bun ca si cel care se obtine direct prin macro expandare.
Evident,
compilatorul are nevoie uneori sa foloseasca variabile temporare si
alte
citeva abilitati pentru a prezerva semanticile.
Noi dorim o indicatie a faptului ca functia se expandeaza inline
care sa
preceada definitia ei. Aceasta este cuvintul cheie inline sau pentru o
functie
membru, pur si simplu prin includerea definitiei functiei in
declaratiile
clasei, asa cum s-a facut pentru size() si elem() in exemplul
precedent.
Cind se utilizeaza bine, functiile inline maresc simultan viteza
de
executie si descresc dimensiunea codului obiect. Totusi, functiile
inline din
declaratiile clasei pot incetini compilarea, asa ca ele trebuie sa fie
eliminate cind ele nu sint necesare. Pentru ca substitutia inline sa
fie un
beneficiu semnificativ pentru o functie, functia trebuie sa fie foarte
mica.

1.13 Clase derivate
--------------

Acum sa definim un vector pentru care un utilizator poate defini
limitele
indexului.

class vec:public vector{
int low, high;
public:
vec(int,int);
int& elem(int);
int& operator[](int);
};
Definind vec ca public vector inseamna inainte de toate ca un vec
este un
vector. Adica tipul vec are toate proprietatile tipului vector si in
plus cele
specific declarate pentru el. Clasa vector se spune ca este clasa de
baza
pentru vec si invers vec se spune ca este derivat din vector.
Clasa vec modifica clasa vector furnizind un constructor diferit
care
cere utilizatorului sa specifice cele doua limite ale indexului in
schimbul
dimensiunii si produce accesul propriu functiilor elem(int) si
operator[](int).elem() a lui vec se exprima usor in termenii lui elem()

al lui
vector:

int& vec::elem(int i){ return vector::elem(i-low); }

Scopul operatorului :: este de a elimina o recursivitate infinita
calculind vec::elem() din el insusi. Unarul :: se poate folosi pentru a

ne
referi la nume nelocale. Ar fi rezonabil sa declaram vec::elem()
inline din
motive de eficienta, dar nu este necesar, sau este posibil sa-l scriem
asa ca
el sa utilizeze direct membrul privat V al clasei vector. Functiile
unei clase
derivate nu au nici un acces special la membri privati ai clasei de
baza
propri. Constructorul poate fi scris astfel:

vec::vec(int lb, int hb) : (hb-lb+1)
{
if(hb-lb < 0)
hb = lb;
low = lb;
high = hb;
}

Constructia: (hb-lb+1) se utilizeaza pentru a specifica lista argument
pentru
constructorul clasei de baza vector::vector(). Acest constructor se
apeleaza
inaintea corpului lui vec::vec(). Iata un mic exemplu care poate fi
executat
daca se compileaza cu
restul declaratiilor lui vector:

#include <stream.h>

void error(char* p)
{
cerr << p << "\n"; // cerr is the error output stream
exit(1);
}


void vector::set_size(int)
{
/* dummy */
}


int& vec::operator[](int i)
{
if(i<low || high<i)
error("vec index out of range");
return elem(i);
}


main()
{
vector a(10);
for(int i=0; i<a.size(); i++)
{
a[i] = i;
cout << a[i] << " ";
}
cout << "\n";
vec b(10, 19);
for(i=0; i<b.size(); i++)
b[i+10] = a[i];
for(i=0; i<b.size(); i++)
cout << b[i+10] << " ";
cout << "\n";
}


Acesta produce:


0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9


1.14 Mai mult despre operatori
-------------------------

O alta directie a dezvoltarii este de a furniza vectori cu
operatii:

class Vec::public vector{
public:
Vec(int s) : (s){ }
Vec(Vec&);
~Vec(){ }
void operator=(Vec&);
void operator*=(Vec&);
void operator*=(int);
//......
};

Observam modul in care constructorul pentru clasa derivata
Vec::Vec()
este definit pentru a transfera argumentele lui la constructorul pentru

clasa
de baza vector::vector(). Operatorul de atribuire este supraincarcat si

poate
fi definit astfel:

void Vec::operator=(Vec& a)
{
int s = size();
if(s != a.size())
error("bad vector size for =");
for(int i=0; i<s; i++)
elem(i) = a.elem(i);
}

Atribuirea de Vec-uri acum copiaza elemente, in timp ce atribuirea

de
vectori copiaza pur si simplu structura care controleaza accesul la
elemente.
Totusi, ultima se intimpla cind se copiaza un vector fara utilizarea
explicita
a operatorului de atribuire:

(1) cind un vector este initializat prin atribuirea unui alt
vector;
(2) cind un vector se paseaza ca argument;
(3) cind un vector se paseaza ca valoare returnata de la o
functie.

Pentru a cistiga control in aceste cazuri pentru vectorii Vec, noi
definim
constructorul Vec(Vec&):

Vec::Vec(Vec& a) : (a.size())
{
int sz = a.size();
for(int i=0; i<sz; i++)
elem(i) = a.elem(i);
}

Acest constructor initializeaza un Vec ca o copie a altuia si va fi
apelat in
cazurile mentionate precedent. Pentru operatori de forma = si +=,
expresia din
stinga este evident speciala si se pare natural ca ei sa se
implementeze ca
operatii asupra obiectelor notate prin acea expresie. In particular,
este
posibil pentru ei sa se schimbe valoarea primului lor operand. Pentru
operatori de forma + si -, operandul sting nu necesita o atentie
speciala. Noi
am putea, de exemplu, sa transferam ambele argumente prin valoare si
totusi sa
capatam o implementare corecta a adunarii vectorilor. Vectorii pot fi
mari,
asa ca, pentru a elimina copierea, operanzii lui + se transfera
operatorului
operator+() prin referinta:

Vec operator+(Vec& a, Vec& b)
{
int s = a.size();
if(s != b.size())
error("bad vector size for +");
Vec sum(s);
for(int i=0; i<s; i++)
sum.elem(i) = a.elem(i) + b.elem(i);
return sum;
}

Iata un mic exemplu care poate fi executat daca se compileaza cu
declaratiile de vector prezentate anterior:

#include <stream.h>

void error(char* p)
{cerr << p << "\n";
exit(1);
}
void vector::set_size(int){ /*...*/ }
void vec::operator[](int i){ /*...*/ }

main()
{Vec a(10);
Vec b(10);
for(int i=0; i<a.size(); i++)
a[i] = i;
b = a;
Vec c = a+b;
for(i=0; i<c.size(); i++)
cout << c[i] << "\n";
}


1.15 Prieteni (Friends)
------------------

Functia operator+() nu opereaza direct asupra reprezentarii unui
vector;
intr-adevar, nu ar putea, deoarece nu este un membru. Totusi, uneori
este de
dorit ca sa se admita ca functii nemembru sa aiba acces la partea
privata a
unui obiect de clasa. De exemplu, neexistind functia cu acces
"neverificat",
vector:: elem(), noi ar trebui sa fortam verificarea indexului i fata
de
limitele vectorului de trei ori de fiecare data cind se executa ciclul.
Aceasta problema a fost eliminata aici, dar ea este tipica, asa ca
exista un
mecanism pentru o clasa care sa accepte accesul la partea sa privata
pentru o
functie nemembru.
O declaratie a unei functii precedate prin cuvintul cheie friend
este pur
si simplu plasata in declaratia clasei. De exemplu, dindu-se:

class Vec; // Vec este un nume de clasa
class vector{
friend Vec operator+(Vec, Vec);
//...........
};

noi putem scrie:

Vec operator+(Vec a, Vec b)
{
int s = a.size();
if(s != b.size())
error("bad vector size for +");
Vec& sum = *new Vec(s);
int* sp = sum.v;
int* ap = a.v;
int* bp = b.v;
while(s--)
*sp++ = *ap++ + *bp++;
return sum;
}

Un aspect particular util al mecanismului de prieten (friend) este

ca o
functie poate fi prieten a doua sau mai multe clase. Pentru a vedea
aceasta,
sa consideram definirea unui vector si a unei matrici si apoi definirea
functiei de inmultire.

1.16 Vectori generici
----------------

Noi am dori, de exemplu, unul din acei vectori pentru tipul
matrice pe
care l-am definit. Din nefericire, C++ nu furnizeaza o facilitate
pentru a
defini o clasa vector cu tipul elementelor ca argument. Un mod de a
proceda ar
fi sa se copieze atit definitia clasei cit si functiile membru. Acest
lucru nu
este ideal, dar adesea este acceptabil. Noi putem utiliza macroprocesor

pentru
a mecaniza acel task. De exemplu, clasa vector este o versiune
simplificata a
unei clase care poate fi gasita intr-un fisier header standard. Noi am
putea
scrie:

#include <vector.h>
declare(vector, int);
main()
{
vector (int)vv(10);
vv[2] = 3;
vv[10] = 4; //eroare de rang
}

Fisierul vector.h defineste macrouri asa ca declare(vector, int) se
expandeaza
spre declaratia unei clase vector foarte asemanatoare cu cea definita,
iar
implement(vector, int) se expandeaza spre definitiile functiilor acelei
clase.Intrucit implement(vec- tor, int) se expandeaza in definitii de
functii,
el poate fi utilizat numai odata intr-un program, in timp ce
declare(vector,
int) trebuie sa fie utilizat odata in fiecare fisier care manipuleaza
acest
fel de vectori intregi.
declare(vector, int);
//......
implement(vector, char);

da un tip (separat) "vector de caractere".

1.17 Vectori polimorfici
-------------------
O alta varianta ar fi ca sa definim vectorul nostru si cu alte
clase
container in termenii unor pointeri la obiectele unei anumite clase:
class common{ /*........*/};
class vector{
common** v;
//......
public:
cvector(int);
common*& elem(int);
common*& operator[](int);
//......
};

Sa observam ca deoarece pointerii si nu obiectele insasi sint
memorati
intr-un astfel de vector, un obiect poate fi "in" diferiti astfel de
vectori
in acelasi timp. Aceasta este o caracteristica foarte utila pentru
clasele
container de felul vectorilor, listelor inlantuite, multimilor, etc..
Mai mult
decit atit, un pointer la o clasa derivata poate fi atribuit la un
pointer
spre clasa ei de baza, asa ca cvector de mai sus poate fi utilizat
pentru a
pastra pointeri spre obiectele tuturor claselor derivate din common. De
exemplu:

class apple : public common{ /*...*/ };
class orange : public common{ /*...*/ };
class apple_vector : public cvector{
public:
cvector fruitbowl(100);
//......
apple aa;
orange oo;
fruitbowl[0] = &aa;
fruitbowl[1] = &oo;
};

Totusi, tipul exact al unui obiect intr-o astfel de clasa
container nu
mai este cunoscut de compilator. De exemplu, in exemplul precedent noi
stim ca
un element al vectorului este un common, dar este un apple sau un
orange ? In
mod obisnuit, tipul exact trebuie sa fie descoperit mai tirziu pentru a

putea
utiliza corect obiectul. Pentru a face aceasta, noi trebuie sau sa
memoram o
anumita forma a tipului de informatie in obiectul insusi sau sa ne
asiguram ca
numai obiectele unui tip dat se pun in container. Ultima varianta este
atinsa
trivial utilizind o clasa derivata. De exemplu, noi am putea face un
vector de
pointeri apple:
class apple_vector : public cvector{
public:
apple*& elem(int i)
{ return (apple*&)cvector::elem(i); }
//......... };
utilizind notatia de type_casting.
common*& (o referinta la pointer spre common) returnat prin
cvector::elem
spre apple*&. Aceasta utilizare a claselor derivate furnizeaza o
alternativa a
claselor generice. Este putin mai greu sa scriem in acest fel (daca nu
sint
utilizate macrouri asa incit clasele derivate sa fie de fapt utilizate
pentru
a implementa clase generice), dar are avantajul ca toate clasele
derivate au
in comun o singura copie a functiilor clasei de baza.
Pentru o clasa generica de felul lui vector(type), trebuie sa se
faca o
noua copie a acelor functii (prin implement()) pentru fiecare tip nou
utilizat.
Alternativa de a memora identificatorul tipului in fiecare obiect
ne
conduce spre un stil de programare adesea referit ca bazat sau orientat

spre
obiect.

1.18 Functii virtuale
----------------
Sa consideram scrierea unui program pentru afisarea formelor pe un

ecran.
Atributele comune ale formelor se reprezinta prin clasa shape, atribute
specificate prin clase derivate specifice:
class shape{point center;
color col;
//.......
public:
void move(point to)
{
center = to;
draw();
}
point where(){ return center; }
virtual void draw();
virtual void rotate(int);
//........
};
Functiile care pot fi definite fara cunostinte despre forma
specifica (de
exemplu move si where), pot fi declarate in mod obisnuit. Restul se
declara
virtual, adica se vor defini intr-o clasa derivata. De exemplu:
ch;
int count = 1;
char* name = "Bjarne";
struct complex{ float re,im; }
complex cvar;
extern complex sqrt(complex);
extern int error_number;
typedef complex point;
float real(complex* p){ return p->re; };
const double pi = 3.1415926535897932385;
struct user;
Majoritatea acestor declaratii sint de asemenea si definitii;
adica ele
definesc o entitate pentru numele la care se refera. Pentru ch, count
si cvar,
aceasta entitate este o cantitate corespunzatoare de memorie care sa se
utilizeze ca o variabila. Pentru real, entitatea este o functie
specifica.
Pentru constanta pi entitatea este o valoare 3.1415... . Pentru
complex,
entitatea este un tip nou. Pentru point, entitatea este tipul complex
asa ca
point devine un sinonim pentru complex. Numai declaratiile
extern complex sqrt(complex);
extern int error_number;
struct user;

nu sint si definitii. Adica, entitatile la care se refera ele trebuie
sa fie
definita altundeva. Codul pentru functia sqrt() trebuie sa fie
specificat
printr-o anumita alta declaratie, memoria pentru variabila error_number

de tip
intreg trebuie sa fie alocata printr-o anumita alta declaratie a lui
error_number, iar o anumita alta declaratie a tipului user trebuie sa
defineasca cum arata acel tip. Trebuie totdeauna sa fie exact o
definitie
pentru fiecare nume dintr-un program C++, dar pot fi multe declaratii
si toate
declaratiile trebuie sa fie compatibile cu tipul entitatii referite,
asa ca
fragmentul de mai jos are doua erori:
int count;
int count; // error : redefinition
extern int error_number;
extern short error_number; // error : type mismatch

Anumite definitii specifica o "valoare" pentru entitatile pe care
le
definesc ele:
struct complex{ float re,im; };
typedef complex point;
float real(complex* p){ return p->re };
const double pi=3.1415926535897932385;
Pentru tipuri, functii si constante "valoarea" este permanenta.
Pentru
tipuri de date neconstante valoarea initiala poate fi schimbata
ulterior:

int count = 1;
char* name = "Bjarne";
//................
count = 2;
name = "Marian";

Numai definitia
char ch;
nu specifica o valoare. Orice declaratie ce specifica o valoare este o
definitie.


2.1.1 Domeniu
-------

O declaratie introduce un nume intr-un domeniu; adica un nume
poate fi
utilizat numai intr-o parte specifica a textului programului. Pentru un

nume
declarat intr-o functie (adesea numit nume local), domeniul lui se
extinde din
punctul declaratiei pina la sfirsitul blocului in care apare declaratia

lui.
Pentru un nume care nu este intr-o functie sau intr-o clasa (adesea
numit nume
global), domeniul se extinde din punctul declaratiei pina la sfirsitul
fisierului in care apare declaratia lui. Un nume poate fi redefinit
intr-un
bloc pentru a referi o entitate diferita in blocul respectiv. Dupa
iesirea din
bloc numele isi reia intelesul lui precedent. De exemplu:

int x; //global x
f()
{
int x; //local x. Ascunde globalul x
x = 1; //asignarea la x local
{
int x; //ascunde primul local x
x = 2; //asignarea la cel de al doilea local
}
x = 3; //asignarea la primul local x
}
int* p = &x; //ia adresa globalului x

Ascunderea numelor este inevitabila cind se scriu programe mari.
Totusi,
un cititor poate usor sa nu observe ca un nume a fost ascuns si erorile
datorate acestui fapt sint foarte dificil de gasit. In consecinta ar
trebui
minimizat numarul numelor ascunse. Utilizarea numelor de felul lui i si

x
pentru variabile globale sau locale in functii mari poate sa ne conduca

la
erori.
Este posibil sa se utilizeze un nume global ascuns utilizind
operatorul
de rezolutie a domeniului "::". De exemplu:
int x;
f()
{int x = 1; // ascunde globalul x
::x = 2; // asigneaza lui x global
}
Nu exista un mod de a utiliza un nume local ascuns.
Domeniul unui nume incepe in punctul declaratiei lui; aceasta
inseamna ca
un nume poate fi utilizat chiar pentru a specifica valoarea lui
initiala. De
exemplu:

int x;
f(){ int x = x; }

Aceasta este legal dar este fara sens.
Este posibil sa utilizam un singur nume pentru a ne referi la doua
obiecte diferite intr-un bloc fara a utiliza operatorul "::". De
exemplu:

int x = 11;
f()
{
int y = x; // global x
int x = 22;
y = x; // local x
}

Variabila y este initializata cu 11, valoarea globalului x, iar
apoi i se
atribuie valoarea 22 a variabilei locale x.
Numele argumentelor unei functii se considera declarate in blocul
cel mai
exterior functiei, deci
f(int x)
{
int x; // eroare
}
eroare, deoarece x este declarat de doua ori in acelasi domeniu.


2.1.2 Obiecte si Lvalori
-----------------

Se pot aloca si utiliza "variabile" ce nu au nume si este posibil
sa se
faca atribuiri la expresii care arata straniu (de exemplu *p[a+10]=7).
In consecinta, exista nevoia de nume pentru "ceva aflat in
memorie". Iata
ghilimelele corespunzatoare pentru a face referire la manualul C++: "Un

obiect
este o regiune de memorie; o lvaloare este o expresie care refera un
obiect"
(&r.5). Cuvintul lvalue original a fost desemnat pentru a insemna "ceva

ce
poate fi in partea stinga a unei atribuiri". Totusi, nu orice lvalue
poate fi
utilizata in partea stinga a unei atribuiri; se poate avea o lvaloare
care
face referire la o constanta (vezi &2.4).

2.1.3 Durata de viata
---------------
Daca programatorul nu specifica altfel, un obiect este creat cind
definitia lui este intilnita si distrus cind numele lui iese afara din
domeniu. Obiectele cu nume globale se creeaza si se initializeaza numai

odata
si "traiesc" pina cind se termina programul. Obiectele definite
printr-o
declaratie cu cuvintul cheie static se comporta la fel. De exemplu,
(directiva
#include <stream.h> a fost lasata afara din exemplele din acest capitol

pentru
a face economie de spatiu. Este necesar sa fie prezenta pentru
exemplele care
produc iesiri):

int a = 1;
void f()
{
int b = 1; //se initializeaza la fiecare apel a lui f()
static int c = 1; //se initializeaza numai odata
cout << "a=" << a++ << "b=" << b++ <<"c=" <<c++ <<"\n";
}

main()
{
while(a < 4)
f();
}

produce iesirea:
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3

O variabila statica care nu este explicit initializata este
initializata
cu zero (&2.4.5).
Utilizind operatorii new si delete, programatorul poate crea
obiecte a
caror durata de viata poate fi controlata direct (&3.2.4).

2.2 Nume
----

Un nume (identificator) consta dintr-un sir de litere si cifre.
Primul
caracter trebuie sa fie litera. Caracterul subliniere _ se considera a
fi o
litera. C++ nu impune limite asupra numarului de caractere dintr-un
nume, dar
anumite implementari nu sint sub controlul scriitorului de
compilatoare (in
particular, incarcatorul). Anumite medii de executie sint de asemenea
nece-
sare pentru a extinde sau restringe setul de caractere acceptat intr-un
identificator; extensiile (de exemplu, cele care admit caracterul $
intr-un
nume) produc programe neportabile. Un cuvint cheie C++ (vezi &r.2.3)
nu poate
fi utilizat ca un nume. Exemple de nume:

hello this_is_a_most_unusually_long_name
DEFINED fo0 bAr u_name HorseSence
var0 var1 CLASS _class ___

Exemple de siruri de caractere care nu pot fi utilizate ca
identificatori:
012 a fool $sys class 3var
pay.dul foo-bar .name if

Literele mari si mici sint distincte, asa ca Count si count sint
nume
diferite, dar nu este indicat sa se aleaga nume care difera numai putin

unul
de altul. Numele care incep cu subliniere se utilizeaza de obicei
pentru
facilitati in mediul de executie
si de aceea nu se recomanda sa se utilizeze astfel de nume in
programele
aplicative.
Cind compilatorul citeste un program, el totdeauna cauta cel mai
lung sir
care poate forma un sir, asa ca var10 este un singur nume si nu numele
var
urmat de numarul 10, iar elseif un singur nume, nu cuvintul cheie else
urmat
de if.

2.3 Tipuri
------

Orice nume (identificator) dintr-un program C++ are un tip asociat

cu el.
Acest tip determina ce operatii pot fi aplicate asupra numelui (adica
la
entitatea referita prin nume) si cum se interpreteaza aceste operatii.
De
exemplu:

int error_number;
float real(complex* p);

Intrucit error_number este declarat sa fie int, lui i se pot face
atribuiri,
poate fi folosit in expresii aritmetice, etc..
Functia real, pe de alta parte, poate fi aplicata cu adresa unui
complex
ca parametru al ei. Este posibil sa se ia adresa oricaruia din ei.
Anumite
nume, cum ar fi int si complex, sint nume de tipuri. Un nume de tip
este
utilizat pentru a specifica tipul unui alt nume intr-o declaratie.
Singura
alta operatie asupra unui nume de tip este sizeof (pentru a determina
cantita-
tea de memorie necesara pentru a pastra un obiect de acel tip) si new
(pentru
alocare de memorie libera pentru obiectele de tipul respectiv). De
exemplu:

main()
{
int* p = new int;
cout << "sizeof(int) =" << sizeof(int) << "\n";
}

Un nume de tip poate fi utilizat ca sa specifice explicit
conversia de la
un tip la altul (&3.2.4). De exemplu:

float f;
char* p;
long ll = long(p); // converteste p spre long
int i = int(f); // converteste f spre int

2.3.1 Tipuri fundamentale
-------------------

C++ are un set de tipuri fundamentale ce corespund la cele mai
comune
unitati de memorie ale calculatoarelor si la cele mai fundamentale
moduri de
utilizare ale lor.
char
short int
int
long int , pentru a reprezenta intregi de diferite dimensiuni;
float
double ,pentru a reprezenta numere in flotanta;
unsigned char
unsigned short int
unsigned int
unsigned long int ,pentru a reprezenta intregi fara semn,

valori
logice, vectori de biti, etc..
Pentru o notatie mai compacta, int poate fi eliminat dintr-o
combinatie
de multicuvinte (de exemplu short este de fapt short int) fara a
schimba
intelesul; astfel long inseamna long int iar unsigned inseamna unsigned

int.
In general, cind un tip este omis intr-o declaratie, se presupune ca
s-a omis
int. De exemplu:

const a = 1;
static x;

fiecare defineste un obiect de tip int.
Intregul de tip caracter este cel mai potrivit pentru a mentine si
manipula caractere pe un calculator dat; acest tip este de obicei pe 8
biti.
Dimensiunile obiectelor din C++ sint exprimate in termeni multipli ai
dimensiunii lui char, asa ca, prin definitie sizeof(char) = 1.
Depinzind de
hardware, un char este un intreg cu sau fara semn. Tipul caracter fara
semn
este sigur totdeauna fara semn (unsigned char) si utilizindu-l produce
programe mai portabile, dar poate sa fie mai putin eficient decit daca
este
folosit ca tip char obisnuit.
Motivul pentru a funiza mai multe tipuri de intregi, mai multe
tipuri de
intregi fara semn si mai multe tipuri de flotante este pentru a permite
programatorului sa utilizeze avantajos caracteristicile hardware. Pe
multe
masini exista diferente semnificative in cerintele de memorie, timpul
de acces
la memorie si viteza de calcul dintre diferite varietati a tipurilor
funda-
mentale. Cunoscind o masina, de obicei este usor a alege, de exemplu,
tipul de
intreg potrivit pentru o variabila particulara. A scrie cod de nivel
inferior
portabil cu adevarat este foarte greu. Ceea ce este garantat in
legatura cu
dimensiunile tipurilor fundamentale este:

sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
sizeof(float) <= sizeof(double)

Cu toate acestea, de obicei este rezonabil sa presupunem ca tipul
char
poate pastra intregi in intervalul 0..127 (el poate totdeauna pastra un
caracter din setul de caractere al masinii), ca un short si un int au
cel
putin 16 biti, ca un int este apropiat de o dimensiune potrivita pentru
aritmetica intregilor si ca un long are cel putin 24 de biti. A
presupune mai
mult este hazardos si chiar aceste reguli implicite nu se aplica
universal (o
tabela de caracteristici hardware pentru citeva masini se poate vedea
in
&r.2.6).
Tipurile de intregi fara semn sint ideale pentru a utiliza memoria

ca un
vector pe biti. Utilizarea unui intreg fara semn in locul unui int
pentru a
cistiga un bit in plus pentru a reprezenta intregi pozitivi aproape
totdeauna
nu este o idee buna. Incercarea de a ne asigura ca anumite valori sint
pozitive prin declararea variabilelor de tip unsigned va fi ignorata
prin
reguli implicite de conversie. De exemplu:

unsigned surprise = -1;

este legal (dar compilatorul va face un avertisment despre el).


2.3.2 Conversie implicita de tip
--------------------------

Tipurile fundamentale pot fi amestecate liber in expresii. Oricind

este
posibil, valorile se convertesc asa ca sa nu se piarda informatie
(regula
exacta poata fi gasita in &r.6.6).
Exista cazuri in care informatia se poate pierde sau chiar
distruge.
Atribuirea unei valori de un tip la o variabila de un alt tip cu biti
mai
putini in reprezentarea ei este in mod necesar o sursa potentiala de
erori. De
exemplu, sa presupunem ca secventa urmatoare se executa pe o masina in
care
intregii se reprezinta in complement fata de doi si caracterele pe 8
biti:

int i1 = 256 +255;
char ch = i1; //ch == 255
int i2 = ch; //i2 == ?

Un bit (cel mai semnificativ) este pierdut in atribuirea ch = i1 si ch
va
pastra toti bitii 1 (adica 8 biti de 1); deci nu exista o cale ca
acesta sa
poata deveni 511 cind se atribuie lui i2! Dar care ar putea fi valoarea

lui i2
? Pe VAX, unde un caracter este cu semn, raspunsul este 255. C++ nu are

un
mecanism la executie care sa detecteze un astfel de tip de problema,
iar
detectarea la compilare este prea dificila in general, asa ca
programatorul
trebuie sa fie atent la acest fapt.

2.3.3 Tipuri derivate
---------------

Din tipurile fundamentale (si din tipurile definite de utilizator)

se
pot deriva alte tipuri folosind operatorii de declaratie:
* pointer
& adresa
[] vector
() functie

si mecanismul de definitie de structura. De exemplu:

int* a;
float v[10];
char* p[20]; //vector de 20 de pointeri spre caractere
void f(int);
struct str{
short length;
char* p;
};

Regulile de compunere a tipurilor utilizind acesti operatori se
explica
in detaliu in &r8.3.4. Ideea de baza este ca declara-
rea unui tip derivat oglindeste utilizarea lui. De exemplu:

int v[10]; //declara un vector
i = v[3]; //utilizeaza un element al vectorului
int* p; //declaratie de pointer
i = *p; //utilizeaza obiectul spre care se pointeaza

Toate problemele in intelegerea notatiei pentru tipuri derivate
apar din
cauza faptului ca * si & sint operatori prefix iar [] si () sint
postfix, asa
ca parantezele trebuie sa fie utilizate pentru a exprima tipuri in care
precedenta operatorilor este incomoda. De exemplu deoarece [] are o
prioritate
mai mare decit *:

int *v[10]; //vectori de pointeri
int (*p)[10] //pointer spre vector

Poate fi plicticos sa utilizam o declaratie pentru fiecare nume pe

care
vrem sa-l introducem intr-un program, mai ales daca tipurile lor sint
identice. Este posibil sa declaram diferite nume intr-o singura
declaratie; in
locul unui singur nume, declaratia pur si simplu contine o lista de
nume
separate prin virgula. De exemplu, se pot declara doi intregi astfel:
int x, y; //int x; int y;

Cind declaram tipuri derivate, trebuie sa observam ca operatorii
se
aplica numai la nume individuale (si nu la orice alte nume din aceeasi
declaratie). De exemplu:
int* p, y; //int *p; int y; nu int *y;
int x, *p; //int x; int *p;
int v[10], *p; //int v[10]; int *p;

Opinia autorului este ca astfel de constructii fac un program mai
putin
lizibil si ar trebui eliminate.

2.3.4 Void
----

Tipul void se comporta sintactic ca un tip fundamental. El poate
totusi,
sa fie utilizat numai ca parte a unui tip derivat; nu exista obiecte de

tip
void. Este folosit pentru a specifica ca o functie nu returneaza o
valoare sau
ca tip de baza pentru pointeri spre obiecte de tip necunoscut.
void f(); //f nu returneaza o valoare
void* pv; //pointer spre un obiect de tip necunoscut

Un pointer spre orice tip poate fi atribuit la o variabila de tip
void*.
Pentru inceput acesta nu pare prea util, deoarece un pointer void* nu
poate
fi indirectat dar aceasta restrictie este exact ceea ce face ca tipul
void*
sa fie util. El se utilizeaza in primul rind pentru a transfera la
functii
pointeri despre care nu se poate face presupunere asupra tipului
obiectului
spre care pointeza si pentru a returna obiecte fara tip dintr-o
functie.
Pentru a utiliza un astfel de obiect, trebuie sa se utilizeze
conversia
explicita de tip. Astfel de functii de obicei exista la cel mai
inferior
nivel al sistemului unde se manipuleaza resurse hardware reale. De
exemplu:
void* allocate(int size);
void deallocate(void*);
f()
{int* pi = (int*)allocate(10 * sizeof(int));
char* pc = (char*)allocate(10);
//....
deallocate(pi);
deallocate(pc);
}

2.3.5 Pointeri
--------

Pentru cele mai multe tipuri T, T* este tipul pointer spre T. Adica

o
variabila de tipul T* poate pastra adresa unui obiect de tipul T.
Pentru
pointeri spre vectori si pointeri spre functii exista notatii mai
complicate:

int* pi;
char** cpp; //pointer spre pointer spre caractere
int (*vp)[10] //pointer spre vector de 10 elemente
int (*fp)(char,char*) //pointer spre o functie care are ca
//parametru (char, char*) si retur-
//neaza un int

Operatia fundamentala asupra unui pointer este indirectarea, adica
referirea la un obiect pointat printr-un pointer spre el. Operatorul de
indirectare este unarul * (prefixat). De exemplu:

char c1 = 'a';
char* p = &c1; //p pastreaza adresa lui c1
char c2 = *p; //c2 = 'a'

Variabila spre care pointeaza p este c1 si valoarea pastrata in c1 este

'a',
asa ca valoarea lui *p atribuita lui c2 este 'a'.
Este posibil sa se faca unele operatii aritmetice cu pointerii.
Iata de
exemplu o functie care calculeaza numarul de caractere dintr-un sir
(nesocotind 0 care termina sirul):

int strlen(char* p)
{
int i = 0;
while(*p++)
i++;
return i;
}

Un alt mod de a gasi lungimea este ca la inceput sa gasim
sfirsitul
sirului si apoi sa scadem adresa inceputului sirului din adresa
sfirsitului:

int strlen(char* p)
{
char* q = p;
while(*q++);
return(q-p-1);
}

Pointerii spre functii pot fi extrem de utili; ei se discuta in
(&4.6.7).


2.3.6 Vectori
-------

Pentru un tip T, T[size] este tipul "vector de size elemente de
tip T".
Elementele sint indexate de la 0 la size-1. De exemplu:
float v[3]; // un vector de 3 flotante: v[0],v[1],v[2]
int a[2][5]; // doi vectori de 5 intregi
char* vpc[32]; // vectori de 32 de pointeri spre caractere

Un ciclu pentru a scrie valori intregi pentru caracterele mici ar

putea
fi scris astfel:
extern int strlen(char*);
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
main()
{int sz = strlen(alpha);
for(int i=0; i<sz; i++)
{
char ch = alpha[i];
cout << "'" << chr(ch) << "'" << "=" << ch << "=0"
<< oct(ch) << "=0x" << hex(ch) << "\n";
}
}
Functia chr() returneaza reprezentarea sub forma de caracter a unui
intreg
mic; de exemplu, chr(80) este "P" pe o masina care utilizeaza setul de
caractere ASCII. Functia oct() produce o reprezentare octala a
argumentului
sau intreg, iar hex() produce o reprezentare hexazecimala a
argumentului sau
intreg; chr(), oct() si hex() sint declarate in <stream.h>.
Functia strlen() a fost utilizata pentru a numara caracterele din
alpha
(vezi &2.4.4). Cind se utilizeaza setul de caractere ASCII, iesirea va

arata
astfel:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63

Sa observam ca nu este necesar sa se specifice dimensiunea
vectorului
alpha; compilatorul calculeaza numarul de caractere din sirul de
caractere
specificat ca initializator. Utilizind un sir ca un initializator
pentru un
vector de caractere este convenabil, dar din nefericire este unica
utilizare a
sirurilor. Nu exista o atribuire similara a unui sir la un vector. De
exemplu:

char v[9];
v = "a string"; // error

este o eroare deoarece atribuirea nu este definita pentru vectori.
Evident sirurile sint potrivite numai pentru a initializa vectori
de
caractere; pentru alte tipuri trebuie sa se utilizeze o notatie mai
laborioasa. Aceasta notatie poate fi de asemenea utilizata pentru
vectori de
caractere. De exemplu:

int v1[] = {1,2,3,4};
int v2[] = {'a','b','c','d'};
char v3[] = {1,2,3,4};
char v4[] = {'a','b','c','d'};


Observam ca v4 este un vector de 4 (nu 5) caractere; nu este
terminat
printr-un zero, asa cum cer prin conventie toate rutinele de
biblioteca.
Aceasta notatie este de asemenea restrin sa la obiecte statice.
Tablourile multidimensionale sint reprezentate ca vectori de
vectori si
notind cu virgula pentru a separa limitele ca in alte limbaje de
programare se
obtine la compilare o eroare deoarece virgula (,) este un operator de
succesiune (vezi &3.2.2). De exemplu, sa incercam:

int bad[5,2]; // error
int v[5][2]; //correct
int bad = v[5,2]; // error
int good = v[4][1]; // correct

O declaratie char v[2][5]; declara un vector cu doua elemente; fiecare
din ele
este un vector de tip char [5]. In exemplul urmator, primul din acei
vectori
este initializat cu primele 5 litere iar cel de al doilea cu primele 5
cifre:

char v[2][5] = {'a','b','c','d','e','0','1','2','3','4'};
main()
{
for(int i=0; i<2; i++)
{
for(int j=0; j<2; j++)
cout << "v[" << i << "][" << j
<< "]=" chr(v[i][j]) << " ";
cout << "\n";
}
}
va produce:
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e
v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4

2.3.7 Pointeri si Vectori
-------------------

In C++, pointerii si vectorii sint foarte strinsi legati. Numele
unui
vector poate de asemenea, sa fie utilizat ca un pointer spre primul sau
element, asa ca exemplul cu alfabetul ar putea fi scris astfel:

char alpha[] = "abcdefghijklmnopqrstuvwxyz";
char* p = alpha;
char ch;
while(ch = *p++);
cout << chr(ch) << "=" << ch
<< "=0" << oct(ch) << "\n";

Declaratia lui p ar putea de asemenea sa fie scrisa:

char* p = &alpha[0];

Aceasta echivalenta este utilizata extensiv in apelurile de
functii, in
care un argument vector este totdeauna pasat ca un pointer la primul
element
al vectorului; astfel in acest exemplu:
extern int strlen(char*);
char v[] = "Annemarie";
char* p = v;
strlen(p);
strlen(v);
este transferata aceeasi valoare la strlen in ambele apeluri.
Rezultatul aplicarii operatorilor +, -, ++, -- la pointeri depinde

de
tipul obiectului spre care pointeaza pointerul. Cind un operator
aritmetic se
aplica la un pointer spre un tip T, p este presupus ca pointeaza spre
un
element al vectorului de obiecte de tip T; p+1 inseamna elementul
urmator al
acelui vector iar p-1 elementul precedent. Aceasta implica faptul ca
valoarea
lui p+1 va fi cu sizeof(T) mai mare decit valoarea lui p. De exemplu:

main()
{
char cv[10];
int iv[10];
char* pc = cv;
int* pi = iv;
cout << "char*" << long(pc+1) - long(pc) << "\n";
cout << "int*" << long(pi+1) - long(pi) << "\n";
}

va produce:

char* 1
int* 4

deoarece caracterele ocupa un octet fiecare si intregii ocupa fiecare 4

octeti
pe masina mea. Valorile pointer au fost convertite spre long inainte de

a face
scaderea utilizind conversia explicita de tip (&3.2.5). Ele au fost
convertite
spre long si nu spre tipul int deoarece exista masini pe care un
pointer nu
incape intr-un int (adica sizeof(int) < sizeof(char*)).
Scaderea de pointeri este definita numai cind ambii pointeri
pointeaza
spre elemente ale aceluiasi vector (desi limbajul nu are un mod de a se
asigura ca acest lucru este adevarat). Cind se scade un pointer
dintr-un
altul, rezultatul este numarul de elemente al vectorului dintre cei doi
pointeri (un intreg). Se poate adauga un intreg la un pointer sau

scadea un intreg
dintr-un pointer; in ambele cazuri rezultatul este o valoare pointer.
Daca
acea valoare nu pointeaza spre un element al aceluiasi vector, ca si
vectorul
initial, rezultatul utilizarii valorii respective este nedefinit. De
exemplu:

int v1[10];
int v2[10];
int i = &v1[5] - &v1[3]; // 2
i = &v1[5] - &v2[3]; // rezultat nedefinit
int* p = v2 + 2; // p==&v2[2]
p = v2 - 2; // *p nedefinit

2.3.8 Structuri
---------

Un vector este un agregat de elemente de un acelasi tip; o
structura este
un agregat de elemente de orice tip. De exemplu:

struct address{char* name; // "Jim Dandy"
long number; // 61
char* street; // "South St"
char* town; // "New Providence"
char state[2]; // 'N' 'J'
int zip; // 7974
};

defineste un tip nou numit address care consta din elementele de care
avem
nevoie pentru a trimite o scrisoare la cineva (address nu este in
general
destul pentru a gestiona toate scrisorile, dar este suficient pentru un
exemplu). Sa observam punct-virgula de la sfirsit este unul din foarte
putinele locuri din C++ unde este necesar sa o avem dupa o acolada
inchisa,
asa ca lumea este inclinata sa o uite.
Variabilele de tip adresa pot fi declarate exact ca si alte
variabile,
iar elementele individuale pot fi accesate utilizind operatorul
'.'(punct). De
exemplu:

address jd;
jd.name = "Jim Dandy";
jd.number = 61;

Notatia utilizata pentru initializarea vectorilor poate de
asemenea sa
fie utilizata pentru variabile de tip structura. De exemplu:

address jd = {"Jim Dandy",61,"South St","New Providence",
{'N','J'},7974};

Utilizind un constructor (&5.2.4) este de obicei mai bine. Sa
observam ca
jd.state nu poate fi initializat prin sirul "NJ". Sirurile sint
terminate prin
caracterul '\0' asa ca "NJ" are trei caractere, adica unul in plus
decit ar
incapea in jd.state.
Obiectele structura sint adesea accesate prin pointeri folosind
operatorul ->. De exemplu:
void print_addr(address* p)
{
cout << p->name << "\n" << p->number << " " << p->street
<< "\n" << p->town << "\n" << chr(p->state[0])
<< chr(p->state[1]) << " " << p->zip << "\n";
}

Obiectele de tip structura pot fi atribuite, pasate ca si
argumente la
functie si returnate ca rezultat al unei functii. De exemplu:
address current;
address set_current(address next)
{
address prev = current;
current = next;
return prev;
}
Alte operatii plauzibile cum ar fi compararea (== si !=) nu sint
definite. Cu toate acestea, utilizatorul poate defini astfel de
operatori
(vezi cap. 6).
Nu este posibil sa se calculeze dimensiunea unui obiect de tip
structura
pur si simplu insumind membri ei. Motivul pentru aceasta este ca multe
masini
necesita ca obiecte de un anumit tip sa fie alocate numai la anumite
adrese
(un exemplu tipic este faptul ca un intreg trebuie sa fie alocat la o
adresa
de cuvint) sau pur si simplu pentru a trata astfel de obiecte mult mai
eficient. Aceasta conduce spre "goluri" in structuri. De exemplu (pe
masina
mea): sizeof(address) este 24 si nu 22 cit ne-am astepta.
Sa observam ca numele unui tip devine disponibil pentru utilizare
imediat
dupa ce el a fost intilnit si nu numai dupa declararea completa. De
exemplu:

struct link{
link* previsious;
link* successor;
};

Nu este posibil sa se declare obiecte noi de tip structura pina
cind nu
s-a terminat complet declaratia, deci
struct no_good{ no_goog member; };

este o eroare (compilatorul nu este in stare sa determine dimensiunea
lui
no_good). Pentru a permite ca doua (sau mai multe) tipuri structura sa
se
refere unul la altul, pur si simplu se admite ca sa se declare ca un
nume este
numele unui tip structura. De exemplu:
struct list; // to be defined later
struct link{
link* pre;
link* suc;
list* member_of;
};
struct list{ link* head; };

Fara prima declaratie a lui list, declaratia lui link ar produce o

eroare
sintactica.


2.3.9 Echivalenta tipurilor
---------------------

Doua tipuri structura sint diferite chiar daca ele au aceeasi
membri. De
exemplu:
struct s1{ int a; };
struct s2{ int a; };

sint doua tipuri diferite, asa ca
s1 x;
s2 y = x; // error: type mismatch

Tipurile structura sint de asemenea diferite de tipurile
fundamentale,
asa ca:
s1 x;
int i = x; // error: type mismatch
Exista un mecanism pentru a declara un nume nou pentru un tip,
fara a
introduce un obiect nou. O declaratie prefixata prin cuvintul cheie
typedef
declara nu o noua variabila de un tip dat, ci un nume nou pentru tip.
De
exemplu:
typedef char* pchar;
pchar p1,p2;
char* p3 = p1; Aceasta poate fi o prescurtare convenabila.


2.3.10 Referinte
---------

O referinta este un nume pentru un obiect. Prima utilizare a
referintelor
este aceea de a specifica operatiile pentru tipuri definite de
utilizator (ele
se discuta in cap. 6). Ele pot fi de asemenea utile ca argumente de
functii.
Notatia X& inseamna referinta la X. De exemplu:
int i = 1;
int& r = i; // r si i acum se refera la acelasi obiect
int x = r; // x = 1
r = 2; // i = 2

O referinta trebuie sa fie utilizata (trebuie sa fie ceva pentru
ce este
el nume). Sa observam ca initializarea unei referinte este ceva cit se
poate
de diferit de atribuirea la ea.
In ciuda aparentelor, nici un operator nu opereaza asupra unei
referinte.
De exemplu:
int ii = 0;
int& rr = ii;
rr++; // ii se incrementeaza cu 1
este legal, dar r++ nu incrementeaza referinta rr; ++ se aplica la un
int,
care se intimpla sa fie ii. In consecinta, valoarea referintei nu poate

fi
schimbata dupa initializare; ea totdeauna se refera la obiectul cu care

a fost
initializata pentru a-l denumi. Pentru a primi un pointer spre obiectul

notat
prin referinta rr, se poate scrie &rr.
Implementarea unei referinte este un pointer (constant) care este
indirectat de fiecare data cind el este utilizat. Aceasta face
initializarea
unei referinte trivial cind initializatorul este o lvaloare (un obiect

la
care se poate lua adresa vezi &r5). Cu toate acestea, initializatorul
pentru
T& este necesar sa nu fie o lvaloare sau chiar de tip T. In astfel de
cazuri:
[1] Intii, se aplica conversia de tip daca este necesar
(vezi &r6.6.8 si &r8.5.6);
[2] Apoi valoarea rezultat este plasata intr-o variabila
temporara;
[3] In final, adresa acestuia se utilizeaza ca valoare a
initializatorului.

Consideram declaratia:
double& dr = 1;

Interpretarea acesteia este:
double* drp; // referinta reprezentata printr-un pointer
double temp;
temp = double(1);
drp = &temp;
O referinta poate fi utilizata pentru a implementa o functie care
se
presupune ca schimba valoarea argumentelor sale.
int x = 1;
void incr(int& aa){ aa++; }
incr(x); // x = 2;

Semantica transferului de argumente se defineste ca si pentru
initializare, asa ca atunci cind este apelata functia de mai sus
argumentul aa
a lui incr() devine un alt nume pentru x. Cu toate acestea, pentru a
avea un
program mai lizibil este cel mai bine sa eliminam functiile care isi
modifica
argumentele. Este adesea preferabil sa se returneze o valoare dintr-o
functie
in mod explicit sau sa se returneze un pointer spre argument.
int x = 1;
int next(int p){ return p+1; }
x = next(x); // x = 2
void inc(int* p){ (*p)++; }
inc(&x); // x = 3
Referintele pot fi de asemenea utilizate pentru a defini functii care
pot fi
utilizate atit in parttea stinga cit si in partea dreapta a unei
atribuiri.
Din nou, multe din cele mai interesante utilizari ale referintei se
afla in
proiectarea tipurilor netriviale definite de utilizator. Ca de exemplu,

sa
definim un tablou asociativ simplu. Intii noi definim struct pair prin:
struct pair{ char* name; int val; };

Ideea de baza este ca un sir are o valoare intreaga asociata cu
el. Este
usor sa se defineasca o functie, find() care mentine o data structurata

ce
consta dintr-o pereche pentru fiecare sir diferit ce a fost prezentat.
O
implementare foarte simpla (dar ineficienta) ar fi urmatoarea:
const large = 1024;
static pair vec[large+1];
pair* find(char* p)
/* mentinerea unui set de "pair": se cauta p, se returneaza
"pair"-ul respectiv daca se gaseste, altfel se returneaza
un "pair" neutilizat */
{
for(int i=0; vec[i].name; i++)
if(strcmp(p,vec[i].name)==0)
return &vec[i];
if(i==large)
return &vec[large-1];
return &vec[i];
}

Aceasta functie poate fi utilizata prin functia value() care
implementeaza un tablou de intregi indexat prin siruri de caractere:
int& value(char* p)
{
pair* res = find(p);
if(res->name=='\0') // aici spre negasit:initializare
{
res->name=new char[strlen(p)+1];
strcpy(res->name,p);
res->val = 0; // valoarea initiala: 0
}
return res_val;
}
Pentru un parametru sir dat, value() gaseste obiectul intreg
respectiv
(nu valoarea intregului corespunzator); ea returneaza o referinta la
el.
Aceasta s-ar putea utiliza astfel:

const MAX = 256; //mai mare decit cel mai mare cuvint
main() //numara aparitiilor fiecarui cuvint de la intrare
{
char buf[MAX];
while(cin >> buf)
value(buf++);
for(int i=0; vec[i].name; i++)
cout << vec[i].name << ":" << vec[i].val << "\n";
}

Fiecare pas al ciclului citeste un cuvint de la intrarea standard
cin in
buf (vezi cap.8), iar apoi se pune la zi contorul asociat cu el prin
find().
In final tabela rezultata de cuvinte diferite de la intrare, fiecare cu
numarul sau de aparitii, este imprimat. De exemplu, dindu-se intrarea

aa bb
bb aa aa bb aa aa
programul va produce:
aa : 5
bb : 3

Este usor sa se perfectioneze aceasta intr-un tip de tablou
asociativ
propriu folosind o clasa cu operatorul de selectie [] (vezi &6.7).

2.3.11 Registrii
---------

Pe orice arhitectura de masina obiectele (mici) pot fi accesate
mai rapid
cind se plaseaza intr-un registru. Ideal, compilatorul va determina
strategia
optima pentru a utiliza orice registru disponibil pe masina pe care se
compileaza programul. Totusi, acest task nu este trivial, asa ca uneori

este
util ca programatorul sa dea compilatorului aceasta informatie. Aceasta

se
face declarind un obiect registru. De exemplu:
register int i;
register point cursor;
register char* p;

Declaratiile de registrii ar trebui utilizate numai cind eficienta

este
intr-adevar importanta. Declarind fiecare variabila ca variabila
registru se
va ingreuna textul programului si se poate chiar marii dimensiunea
codului si
timpul de executie (de obicei sint necesare instructiuni de a incarca
un
obiect si de a memora un obiect dintr-un registru). Nu este posibil sa
se ia
adresa unui nume declarat ca registru si nici nu poate fi global un
astfel de
nume.


2.4 Constante
---------
C++ furnizeaza o notatie pentru valorile de tipuri fundamentale:
constante caracter, constatante intregi si constante in virgula
flotanta. In
plus, zero (0) poate fi utilizat ca si o constanta pentru orice tip de
pointer, iar sirurile de caractere sint constante de tip char[]. Este
posibil,
de asemenea, sa se specifice constante simbolice. O constanta simbolica

este
un nume a carui valoare nu poate fi schimbata in domeniul ei de
existenta. In
C++ exista trei feluri de constante simbolice:

(1) oricarei valori de orice tip i se poate da un nume si sa
fie folosita ca o consatnta adaugind cuvintul cheie
const la definitia ei;
(2) un set de constante intregi poate fi definit ca o enume-
rare;
(3) orice nume de vector sau functie este o constanta.


2.4.1 Constante intregi
-----------------

Constantele intregi pot fi de patru feluri: zecimale, octale,
hexazecimale si constante caracter. Constantele zecimale sint cele mai
frecvent utilizate si arata asa cum ne asteptam noi:

0 1234 976 12345678901234567890

Tipul unei constante zecimale este int cu conditia ca ea sa incapa
intr-un int, altfel ea este long. Compilatorul se cuvine sa avertizeze
asupra
constantelor care sint prea lungi ca sa fie reprezentate in calculator.
O constanta care incepe cu zero urmat de x (0x) este hexazecimal,
iar o
constanta care incepe cu zero urmat de o cifra este in octal.
Exemple de constante octale:

0 02 077 0123

Exemple de constante in hexazecimal:

0x0 0x2 0x38 0x53

Literele a, b, c, d, e si f sau echivalentele lor in litere mari
se
utilizeaza pentru a reprezenta 10, 11, 12, 13, 14 si respectiv 15.
Notatiile
octale si hexazecimale sint mai folosi-
lositoare pentru a exprima structuri pe biti; utilizind aceste notatii
pentru
a exprima numere adevarate putem ajunge la surprize. De exemplu, pe
o
masina pe care un int se reprezinta ca un intreg prin complement fata
de 2 pe
16 biti, intregul 0xffff este numarul negativ -1; daca s-ar folosi mai
multi
biti pentru a reprezenta un int, atunci acesta ar fi 65535.


2.4.2 Constante in flotanta
---------------------
O constanta in flotanta este de tip double. Din nou compilatorul
da un
avertisment despre constante flotante care sint prea mari pentru a
putea fi
reprezentate. Iata citeva constante in virgula flotanta:
1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15

Sa observam ca nu poate apare un spatiu in mijlocul unei constante
flotante. De exemplu:
65.43 e-21
nu este o constanta flotanta ci 4 lexicuri:
65.43 e - 21
si va cauza eroare sintactica.
Daca noi dorim o constanta de tip float, noi putem defini una de
forma
(&2.4.6):

const float pi8 = 3.14159265;


2.4.3 Constante caracter
------------------

Desi C++ nu are un tip caracter separat pentru date, ci mai
degraba un
tip intreg care poate pastra un caracter, trebuie sa avem o notatie
speciala
si convenabila pentru caractere.
O constanta caracter este un caracter inclus intre caracterele
apostrof:
de exemplu 'a' si '0'. Astfel de constante caracter sint constante
simbolice
adevarate pentru valorile intregi ale caracterelor din setul de
caractere al
masinii pe care se executa programul C++ (care nu este in mod necesar
acelasi
set de caractere ca si cel utilizat de calculatorul pe care se
compileaza
programul). Astfel, daca noi executam programul pe o masina care
utilizeaza
setul de caractere ASCII, valoarea '0' este 48; daca masina utilizeaza
setul
EBCDIC, el este 240. Utilizind constantele caracter in locul notatiei
zecimale
programele devin mai portabile. Citeva caractere au de asemenea notatii
standard in care se utilizeaza caracterul backslash (\):

'\b' backspace
'\f' formfeed
'\n' newline
'\r' cariage return
'\t' horizontal tab
'\v' vertical tab
'\\' backslash
'\'' simple quote
'\"' double quote
'\0' null, the integer value 0
Acestea sint caractere singulare in ciuda aparentei. Este posibil,

de
asemenea sa reprezentam un caracter printr-un numar octal de o cifra,
doua sau
trei (\ urmat de cifre octale) sau de un numar hexazecimal de una, doua

sau
trei cifre(\x urmat de cifre hexazecimale). De exemplu:

'\6' '\x6' 6 ASCII ack
'\60' '\x30' 48 ASCII '0'
'\137' '\x05f' 95 ASCII '-'

Aceasta face posibil ca sa se reprezinte fiecare caracter din
setul
caracterelor masina si in particular pentru a include astfel de
caractere in
siruri de caractere (vezi sectiunea urmatoare). Utilizind o notatie
numerica
pentru un caracter, programul respectiv nu mai este portabil pentru
masini cu
seturi diferite de caractere.


2.4.4 Siruri
------

Un sir constant este o secventa de caractere inclusa intre
ghilimele:

"this is a string"

Orice sir constant contine cu un caracter mai mult decit cele care

apar
in sir; ele toate se termina prin caracterul nul '\0', cu valoarea 0.
De
exemplu:

sizeof("asdf")==5;

Tipul unui sir este "vector de un numar corespunzator de
caractere", asa
ca "asdf" este de tipul char[5]. Sirul vid se scrie "" (si are tipul
char[1]).
Sa observam ca pentru orice sir s, strlen(s) == sizeof(s) - 1 deoarece
strlen() nu numara zeroul terminal.
Conventia backslash pentru reprezentarea caracterelor negrafice
pot de
asemenea sa fie utilizate intr-un sir: aceasta face posibil sa se
reprezinte
ghilimelele si insusi caracterul backslash intr-un sir. Cel mai
frecvent
astfel de caracter este pe de parte caracterul '\n'. De exemplu:

cout << "beep at end of message\007\n";

unde 7 este valoarea ASCII a caracterului bel.
Nu este posibil sa avem un caracter newline "real" intr-un sir:

"this is not a string
but a syntax error"

cu toate acestea, un backslash urmat imediat de un newline poate apare
intr-un
sir: ambele vor fi ignorate. De exemplu:
cout << "this is\
ok"
va scrie
this is ok

Este posibil ca sa avem caracterul nul intr-un sir, dar
majoritatea
programelor nu vor suspecta ca dupa el mai sint caractere. De exemplu,
sirul
"asdf\000hjkl" va fi tratat ca "asdf" prin functii standard cum ar fi
strcpy()
si strlen().
Cind se include o constanta numerica intr-un sir folosind notatia
octala
sau hexazecimala, este totdeauna bine sa se utilizeze trei cifre pentru

numar.
Notatia este destul de greu de utilizat fara sa apara probleme cind
caracterul
dupa o constanta de acest fel este o cifra. Consideram exemplele:

char v1[]="a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9'
char v2[]="a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9'
char v3[]="a\xfad\127"; // 'a' '\xfad' '\127'

Sa observam ca o notatie cu doua cifre hexazecimale nu este
suficienta pe
masini cu 9 biti pe byte.


2.4.5 Zero
----

Zero (0) poate fi folosit ca o constanta de tip intreg, flotant
sau
pointer.
Nici un obiect nu este alocat cu adresa zero. Tipul lui zero va fi
determinat de context. Toti bitii de o dimensiune potrivita sint zero.


2.4.6 Const
-----

Cuvintul cheie const poate fi adaugat la declaratia unui obiect
pentru a
face acel obiect o constanta in loc de variabila. De exemplu:

const int model = 145;
const int v[] = {1, 2, 3, 4};

Deoarece la un astfel de obiect nu i se poate atribui o valoare,
el
trebuie sa fie initializat. Declarind ceva ca este constant ne asiguram

ca
valoarea lui nu va fi schimbata in domeniul lui:

model = 165; // error
model++; // error

Sa observam ca const modifica un tip; adica el restringe modul in
care un
obiect poate fi utilizat, in loc sa specifice cum se aloca constanta.
Este, de
exemplu, perfect rezonabil si uneori util sa declaram o functie care
returneaza o constanta:

const char* peek(int i){ return private[i]; }
O functie de aceasta forma ar putea fi utilizata pentru a permite
cuiva
sa citeasca un sir care nu poate fi alterat.
Cu toate acestea, un compilator poate avea avantaje de pe urma
unui
obiect care este o constanta in diferite moduri. Cel mai evident este
faptul
ca de obicei nu este nevoie ca sa fie alocata memorie pentru constanta
deoarece compilatorul cunoaste valoarea lui. Mai mult decit atit,
initializatorul pentru o constanta este adesea (dar nu totdeauna) o
expresie
constanta; daca este asa, ea poate fi evaluata la compilare. Cu toate
acestea,
de obicei este necesar sa se aloce memorie pentru un vector de
constante
deoarece compilatorul nu poate in general sa defineasca care elemente
ale
vectorului sint referite in expresii. Pe multe masini, totusi, o
implementare
eficienta poate fi atinsa chiar in acest caz plasind vectori de
constante in
memorii read_only.
Cind utilizam un pointer, sint implicate doua obiecte; pointerul
insusi
si obiectul spre care se face pointarea.
"Prefixind" o declaratie a unui pointer cu const se construieste
obiectul
ca o constanta, nu si pointerul. De exemplu:

const char* pc = "asdf"; // pointer spre o constanta
pc[3] = 'a'; // eroare
pc = "ghjk"; // ok

Pentru a declara ca pointerul insusi este o constanta si nu
obiectul spre
care pointeaza, se foloseste operatorul *const:
char *const cp = "asdf"; // pointer constant
cp[3] = 'a'; // ok
cp = "ghjk"; // eroare

Pentru a face ca sa fie constante atit obiectele, cit si pointerul

spre
ele, trebuie ca ambele sa fie declarate ca si constante. De exemplu:
const char *const cpe = "asdf"; // pointer constant spre
// constanta
cpc[3] = 'a'; // eroare
cpc = "ghjk"; // eroare

Un obiect care este o constanta cind este accesat printr-un
pointer poate
fi variabila cind este accesat in alt mod. Aceasta este util mai ales
pentru
argumentele functiilor. Declarind un pointer_argument ca si const,
functiei i
se interzice sa modifice obiectul spre care pointeaza pointerul
respectiv. De
exemplu:

char* strcpy(char* p,const char* q);//nu poate modifica pe *q

Se poate atribui adresa unei variabile la un pointer spre o
constanta
deoarece nu se intimpla nimic rau prin aceasta. Cu toate acestea,
adresa unei
constante nu se poate atribui la un pointer fara restrictii deoarece
aceasta
ar permite sa schimbe valoarea obiectului. De exemplu:

int a = 1;
const c = 2;
const* p1 = &c; // ok
const* p2 = &a; // ok
int* p3 = &c; // eroare
*p3 = 7; // schimba valoarea lui

De obicei, daca tipul este omis intr-o declaratie, se alege int ca
implicit.

2.4.7 Enumerari
---------

O alta posibilitate pentru a defini constante intregi, care este
adesea
mai convenabil decit utilizind const, este enumerarea. De exemplu:

enum {ASM, AUTO, BREAK};

defineste trei constante intregi, numite enumeratori si atribuie valori

la
acestia. Deoarece valorile enumerator sint atribuite crescator de la
zero,
aceasta este echivalent cu scrierea:

const ASM = 0;
const AUTO = 1;
const BREAK = 2;

O enumerare poate fi definita. De exemplu:

enum keyword {ASM,AUTO,BREAK};
Numele enumerarii devine un sinonim pentru int, nu un nou tip. De
exemplu:
keyword key;
switch(key)
{
case ASM: // se face ceva
break;
case BREAK:// face ceva
break;
}
va conduce la un avertisment deoarece numai doua valori au fost tratate

din
cele trei.
Valorile pot fi de asemenea date explicit enumeratorilor. De
exemplu:

enum int16 {
sign = 0100000,
most_significant = 0400000,
last_significant = 1
};
Aceste valori nu este necesar sa fie distincte, crescatoare sau
pozitive.


2.5 Salvarea spatiului
------------------

Cind programam aplicatii netriviale, invariabil vine vremea cind
dorim
mai mult spatiu de memorie decit este disponibil sau ne putem permite.
Exista
doua moduri de a obtine mai mult spatiu in afara de cel care este
disponibil:

[1] Sa se puna mai mult de un obiect mic intr-un octet;
[2] Sa se utilizeze acelasi spatiu pentru a pastra diferite
obiecte in momente diferite.

Prima metoda poate fi realizata folosind cimpurile, iar cea de a doua
folosind
reuniunile. Aceste constructii se descriu in sectiunile urmatoare.
Deoarece
utilizarea lor tipica este pentru a optimiza pur si simplu un program
si
deoarece ele sint adesea cele mai neportabile parti ale programului,
programatorul trebuie sa gindeasca de doua ori inainte de a le utiliza.

Adesea
o conceptie mai buna este sa schimbe modul in care se gestioneaza
datele; de
exemplu, sa se insiste mai mult asupra memoriei alocate dinamic
(&3.2.6) si
mai putin asupra memoriei prealocate static.


2.5.1 Cimpuri
-------

Se pare extravagant ca sa se utilizeze un caracter pentru a
reprezenta o
variabila binara, de exemplu un comutator on/off, dar tipul char este
cel mai
mic obiect care poate fi alocat independent in C++. Este posibil,
totusi, sa
se inmanuncheze impreuna diferite astfel de variabile foarte mici ca si
cimpuri intr-o structura. Un membru se defineste a fi un cimp
specificind
numarul de biti pe care ii ocupa, dupa numele lui. Se admit si cimpuri
nedenumite; ele nu afecteaza sensul cimpurilor denumite, dar pot fi
utilizate
pentru a face o aranjare mai buna insa dependenta de masina:
struct sreg{
unsigned enable : 1;
unsigned page : 3;
unsigned : 1; //neutilizat
unsigned mode : 2;
unsigned : 4; //neutilizat
unsigned access : 1;
unsigned length : 1;
unsigned non_resident : 1;
};

Aceasta se intimpla sa fie aranjarea bitilor la registru de stare
0 la
DEC PDP11/45. Un cimp trebuie sa fie de tip intreg si se utilizeaza ca
alti
intregi exceptind faptul ca nu este posibil sa se ia adresa unui cimp.

In
modulul kernel al unui sistem de operare sau in debugger, tipul sreg ar

putea
fi utilizat astfel:
sreg* sr0 = (sreg*)0777572;
//........
if(sr0->access) //access violation
{//clean up the mess
sr0->access = 0;
}

Cu toate acestea, utilizind cimpuri pentru a putea impacheta
diferite
variabile intr-un singur octet nu neaparat se salveaza spatiu. Se
salveaza
spatiu la date, dar dimensiunea codului rezultat din manipularea
acestor
variabile se mareste pe majoritatea masinilor. Programele se stie ca se
scurteaza semnificativ cind variabilele binare se convertesc de la
cimpuri
binare la caractere! Mai mult decit atit, de obicei este mai rapid sa
se faca
acces la char sau int decit pentru a face acces la un cimp.

2.5.2 Reuniuni
--------
Sa consideram o tabela de simboluri in care o intrare pastreaza un

nume
si o valoare, iar valoarea este sau un sir sau un intreg:
struct entry{
char* name;
char type;
char* string_value; //se utilizeaza daca type == 's'
int int_value; //se utilizeaza daca type == 'i'
};
void print_entry(entry* p)
{switch(p->type)
{
case 's': cout << p->string_value;
break;
case 'i': cout << p->int_value;
break;
default : cerr << "type corrupted\n";
break;
}
}
Deoarece string_value si int_value nu pot fi utilizate in acelasi
timp,
evident se pierde spatiu. Se poate recupera usor specificind ca ambii
ar
trebui sa fie membri ai unei reuniuni, ca mai jos:
struct entry{
char* name;
char type;
union{
char* string_value; //used if type =='s'
int int_value; //used if type =='i'
};
};
Aceasta lasa tot codul care foloseste pe entry neschimbat, dar
asigura
faptul ca atunci cind entry se aloca, string_value si int_value sa aiba
aceeasi adresa. Aceasta implica, ca toti membri unei reuniuni sa aiba
in comun
acelasi spatiu care permite pastrarea celui mai mare membru.
Utilizind reuniunea in asa fel ca totdeauna sa folosim membrul
care a
fost pastrat in ea, se obtine o optimizare pura. Cu toate acestea, in
programe
mari, nu este usor sa se asigure ca o reuniune se utilizeaza numai in
acest
mod si se pot introduce erori subtile. Este posibil sa se incapsuleze o
reuniune in asa fel incit corespondenta intre tipul cimp si tipurile
membrilor
unei reuniuni sa fie garantat ca este corecta (&5.4.6).
Reuniunile sint uneori utilizate pentru "conversie de tip"
(aceasta se
face in principiu prin programe introdu-se in limbaj in afara
facilitatilor de
conversie a tipului, unde este necesar sa fie facuta). De exemplu, pe
VAX
acestea convertesc un int in int* pur si simplu prin echivalenta de
biti.
struct fudge{
union{
int i;
int* p;
};
};
fudge a;
a.i = 4096;
int* p = a.p; //bad usage

Cu toate acestea, aceasta nu este o conversie reala; pe anumite
masini un
int si un int* nu ocupa acelasi spatiu, iar pe altele nici un intreg nu

poate
avea o adresa impara. O astfel de utilizare a unei reuniuni nu este
portabila
si exista un mod explicit si portabil de a specifica aceasta conversie
(&3.2.5).
Reuniunile sint ocazional utilizate in mod deliberat pentru a
elimina
conversia de tip. Am putea, de exemplu, utiliza un fudge pentru a gasi
reprezentarea pointerului 0:
fudge.p = 0;
int i = fudge.i; // i nu este necesar sa fie 0

Este de asemenea posibil sa se dea un nume unei reuniuni; adica ea
formeaza un tip in adevaratul sens al lui. De exemplu, fudge ar putea
fi
declarata astfel:
union fudge{
int i;
int* p;
};
si folosita exact ca inainte. Reuniunile numite au de asemenea,
utilizari
proprii (vezi &5.4.6).

2.6 Exercitii
---------

1. (*1). Sa se execute programul "Hello, world" (&1.1.1).

2. (*1). Pentru fiecare din declaratiile din (&2.1) sa se faca
urmatoarele: daca o declaratie nu este o definitie, sa se scrie o
definitie
pentru ea. Daca o declaratie este o definitie, sa se scrie o
declaratie
pentru ea, care nu este de asemenea o definitie.

3. (*1). Sa se scrie declaratii pentru urmatoarele: un
pointer spre
un caracter; un vector de 10 intregi; o referinta spre un vector de 10
intregi; un pointer spre un vector de siruri de caractere; un pointer
spre un
pointer la un caracter; o constanta intreaga; un pointer spre o
constanta
intreaga; un pointer constant spre un intreg. Sa se initializeze
fiecare din
ei.

4. (*1.5). Sa se scrie un program care imprima dimensiunea
tipurilor fundamentale si a pointerului. Sa se utilizeze operatorul
sizeof.

5. (*1.5). Sa se scrie un program care imprima literele
'a'..'z' si
cifrele '0'..'9' si valorile lor intregi. Sa se faca acelasi lucru
pentru alte
caractere imprimabile. Sa se faca acelasi lucru, dar utilizind notatia
hexazecimala.

6. (*1). Sa se imprime bitii care se folosesc pentru a
reprezenta
pointerul 0 pe sistemul d-voastra (&2.5.2).

7. (*1.5). Sa se scrie o functie care imprima exponentul si
mantisa unui parametru in dubla precizie.

8. (*2). Care sint valorile cele mai mari si cele mai mici pe
sistemul
d-voastra pentru tipurile urmatoare: char, short, int, long, float,
double,
unsigned, char*, int* si void* ? Exista mai multe restrictii asupra
valorilor
? De exemplu, poate int* sa aiba o valoare impara ? Care este cadrajul
obiectelor de acele tipuri ? De exemplu poate un int sa aiba o adresa
impara ?

9. (*1). Care este cel mai lung nume local pe care il puteti
utiliza
intr-un program C++ pe sistemul d-voastra ? Care este cel mai lung nume

extern
pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ?
Exista
vreo restrictie asupra caracterelor pe care le puteti utiliza intr-un
nume ?

10. (*2). Definiti pe unu astfel:
const one = 1;
Incercati sa schimbati valoarea lui one la doi. Definiti pe num prin:
const
num[] = {1,2}; Incercati sa schimbati valoarea lui num[1] la 2.

11. (*1). Scrieti o functie care permuta doi intregi. Sa se
utilizeze
int* ca tip al argumentului. Scrieti o alta functie de permutare care
utilizeaza int& ca tip de argument.
12. (*1). Care este dimensiunea vectorului str in exemplul
urmator:
char str[] = "a short string";
Care este lungimea sirului "a short string"?

13. (*1.5). Sa se defineasca o tabela de nume continind
numele
fiecarei luni din an si numarul de zile din fiecare luna. Sa se scrie
tabela.
Sa se faca aceasta de doua ori: odata utilizind un vector pentru nume
si un
vector pentru numarul de zile si odata utilizind un vector de
structuri,
fiecare structura pastrind numele lunii si numarul de zile din ea.

14. (*1). Sa se utilizeze typedef pentru a defini tipurile:
unsigned
char, constant unsigned char, pointer spre intreg, pointer spre pointer

spre
char, pointer spre vector de caractere, vector de 7 pointeri intregi,
pointer
spre un vector de 7 pointeri intregi, vector de 8 vectori de 7 pointeri
intregi.


CAPITOLUL 3
-----------

EXPRESII SI INSTRUCTIUNI
------------------------


C++ are un set mic, dar flexibil, de tipuri de instructiuni pentru
controlul programului si un set bogat de operatori pentru manipularea
datelor.
Un singur exemplu complex introduce cele mai frecvente facilitati
utilizate.
Dupa aceea sint rezumate expresiile si conversiile explicite de tip si
este
prezentata in detaliu utilizarea memoriei libere. Apoi sint rezumate
in-
structiunile, iar in final se discuta stilul de decalare si comentare a
textului.

3.1 Un calculator de birou
----------------------

Instructiunile si expresiile se introduc prin prezentarea
programului
calculatorului de birou care furnizeaza cele patru operatii aritmetice
standard ca operatori infix asupra numerelor flotante. Utilizatorul
poate, de
asemenea, defini variabile. De exemplu, dindu-se intrarea:

r = 2.5
area = pi * r * r

(pi este predefinit), programul calculator va scrie:

2.5
19.635

unde 2.5 este rezultatul primei linii de intrare, iar 19.635 este
rezultatul
celei de a doua.

Calculatorul consta din patru parti principale: un analizor, o
functie de
intrare, o tabela de simboluri si un driver. In realitate este un
compilator
miniatura cu un analizor care face analiza sintactica, functia de
intrare
realizind intrarea si analiza lexicala, tabela de simboluri pastrind
informatia permanenta, iar driverul facind initializarea, iesirea si
tratind
erorile. Exista multe facilitati care pot fi adaugate la acest
calculator
pentru a-l face mai util, dar codul este destul de lung intrucit are
200 de
linii si cele mai multe facilitati noi ar adauga cod fara a furniza
aspecte
noi in utilizarea lui C++.


3.1.1 Analizorul
----------

Iata o gramatica pentru limbajul acceptat de calculator:

program:
END //END este sfirsitul intrarii
expr_list END
expr_list:
expression PRINT //PRINT este '\n' sau ;
expresion PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER //numar in flotanta din C++
NAME //nume din C++ fara subliniat
NAME = expression
_primary
(expression)

Cu alte cuvinte, un program este un sir de linii. Fiecare linie
consta
din una sau mai multe expresii separate prin punct- virgula. Unitatile
de baza
ale unei expresii sint numere, nume si operatorii *, /, +, - (atit unar

cit si
binar) si =. Numele nu trebuie sa fie declarate inainte sa fie
utilizate.
Stilul analizei sintactice utilizate este de obicei numit analiza
descendenta recursiva. Este o tehnica top-down directa. Intr-un limbaj
cum
este C++ in care apelurile de functii sint relativ ieftine, aceasta
este o
tehnica eficienta. Pentru fiecare productie din gramatica exista o
functie
care apeleaza alte functii. Simbolurile terminale (de exemplu END,
NUMBER, +
si -) se recunosc prin analizorul lexical, get_token(), iar simbolurile
neterminale sint recunoscute prin functiile analizorului sintactic
expr(),
term() si prim(). De indata ce ambii operanzi ai unei (sub)expresii
sint
cunoscuti, ei se evalueaza. Intr-un compilator real se genereaza codul
in
acest punct.
Analizorul utilizeaza o functie get_token() pentru a obtine o
intrare.
Valoarea ultimului apel a lui get_token() poate fi gasita in variabila
curr_tok. Aceasta este o valoare de enumerare de tip token_value:

enum token_value{
NAME, NUMBER, END, PLUS = '+', MINUS = '-',
MUL = '*', DIV = '/', PRINT =';',
ASSIGN = '=', LP = '(', RP = ')'
};
token_value curr_tok;

Fiecare functie a analizorului presupune ca get_token() a fost
apelat
astfel incit curr_tok sa pastreaze tokenul (lexicul) urmator de
analizat.
Aceasta permite analizorului sa vada un lexic inainte si obliga fiecare
functie a analizorului sa citeasca totdeauna un lexic in plus fata de
cele pe
care le utilizeaza productia pe care o trateaza ea. Fiecare functie a
analizorului evalueaza expresia ei si returneaza o valoare. Functia
expr()
trateaza adunarea si scaderea. Ea consta dintr-un singur ciclu care
cauta
termeni de adunat sau scazut:

double expr()
{
double left = term();
for(;;) //ciclu infinit
switch(curr_tok)
{
case PLUS : get_token(); //salt peste '+'
left += term();
bre_token() trebuie sa fie declarate
inainte de
expr().
Capitolul patru discuta cum sa se organizeze un program ca un set
de
fisiere. Cu o singura exceptie, declaratiile pentru acest exemplu de
calculator de birou pot fi ordonate in asa fel incit fiecare este
declarata
exact o data inainte de a fi utilizata. Exceptie face expr(), care
apeleaza
term(), care apeleaza prim(), care la rindul ei apeleaza expr(). Acest
ciclu
trebuie sa fie intrerupt cumva. O declaratie:

double expr();

inaintea definitiei lui prim() va fi nimerita.
Functia term() trateaza inmultirea si impartirea:

double term() //inmultire si impartire
{
double left = prim();
for(;;)
switch(curr_tok)
{
case MUL: get_token(); //sare peste '*'
left *= prim();
break;
case DIV: get_token(); //sare peste '/'
double d = prim();
if(d == 0)
return error("divide by 0");
left /= d;
break;
default:
return left;
}
}

Testul pentru a ne asigura ca nu se face impartirea prin zero este
necesar deoarece rezultatul in acest caz nu este definit. Functia
error(char*)
este descrisa mai tirziu. Variabila d este introdusa in program acolo
unde
este nevoie de ea si este initializata imediat. In multe limbaje, o
declaratie
poate apare numai in antetul unui bloc. Aceasta restrictie poate
conduce la
erori. Foarte frecvent o variabila locala neinitializata este pur si
simplu o
indicatie de un stil rau. Exceptii sint variabilele care se
initializeaza prin
operatii de intrare si variabilele de tip vector sau structura care nu
pot fi
initializate convenabil printr-o atribuire simpla. Sa observam ca =
este
operatorul de asignare, iar == este operatorul de comparare.
Functia prim() trateaza un primar; deoarece este la un nivel mai
inferior
in ierarhia de apeluri, ea face un pic mai multa "munca" si nu mai este
necesar sa cicleze.
double prim()
{switch(curr_tok)
{case NUMBER: get_token(); //constanta in flotanta
return number_value;
case NAME : if(get_token() == ASSIGN)
{
name* n = insert(name_string);
get_token();
n->value = expr();
return n->value;
}
return look(name_string)->value;
case MINUS : get_token(); //minus unar
return _prim();
case LP : get_token();
double e = expr();
if(curr_tok != RP)
return error(") expected");
get_token();
return e;
case END : return 1;
default : return error("primary expected");
}
}

Cind se4 gaseste un NUMBER (adica o constanta flotanta), se
returneaza
valoarea ei. Rutina de intrare get_token() plaseaza valoarea in
variabila
globala number_value. Utilizarea unei variabile globale intr-un program

indica
adesea ca structura nu este cit se poate de "curata", ca un anumit fel
de
optimizare a fost aplicat. Asa este aici; un lexic in mod tipic consta

din
doua parti: o valoare care specifica tipul lexicului (token_value in
acest
program) si (cind este nevoie) valoarea lexicului. Aici exista numai o
singura
variabila simpla curr_tok, asa ca este nevoie de variabila globala
number_value pentru a pastra valoarea ultimului NUMBER citit. Aceasta
functioneaza deoarece calculatorul totdeauna utilizeaza un numar in
calcul
inainte de a citi un alt numar de intrare.
In acelasi mod in care valoarea ultimului NUMBER intilnit este
tinut in
number_value, reprezentarea sirului de caractere a ultimului NAME
intilnit
este tinut in name_string. Inainte de a face ceva unui nume, inainte
calculatorul trebuie sa vada daca el este asignat sau numai utilizat.
In
ambele cazuri se consulta tabela de simboluri. Tabela este prezentata
in
&3.1.3. Aici trebuie sa observam ca ea contine intrari de forma:
struct name{
char* string;
name* next;
double value;
};
unde next se utilizeaza numai de functiile care mentin tabela:
name* look(char*);
name* insert(char*);

Ambele returneaza un pointer la un nume care corespunde la
parametrul sir
de caractere. Functia look() semnaleaza daca numele nu a fost definit.
Aceasta
inseamna ca in calculator un nume poate fi utilizat fara o declaratie
prealabila, dar prima lui utilizare trebuie sa fie partea stinga a unei
atribuiri.

3.1.2 Functia de intrare
------------------
Citirea intrarii este adesea cea mai incurcata parte a unui
program.
Motivul este faptul ca daca un program trebuie sa comunice cu o
persoana, el
trebuie sa invinga capriciile, conventiile si erorile unei persoane sau

a mai
multora. Incercarea de a forta persoana sa se comporte intr-o maniera
mai
convenabila pentru masina este adesea, pe drept cuvint, considerata
ofensiva.
Sarcina unei rutine de intrare de nivel inferior este de a citi
caractere unul
dupa altul si sa compuna unitati de nivel mai inalt. Aici intrarea de
nivel
inferior se face cu get_token().
Regulile pentru intrarile in calculator au fost deliberat alese
asa ca sa
fie ceva incomod pentru sirul de functii care le manevreaza. Modificari
neimportante in definitiile unitatilor ar face pe get_token() foarte
simpla.
Prima problema este aceea ca, caracterul newline '\n' este
semnificativ
pentru calculator, dar sirul de functii de intrare il considera un
caracter
whitespace. Adica, pentru acele functii, '\n' este un terminator de
unitate
lexicala. Pentru a invinge aceasta trebuie examinate spatiile albe
(spaces,
tab, etc):
char ch;
do{ //sare peste spatiile albe, exceptind '\n'
if(!cin.get(ch))
return curr_tok = END;
}while(ch!='\n' && isspace(ch));

Apelul cin.get(ch) citeste un singur caracter din sirul de la
intrarea
standard in ch. Testul if(!cin.get(ch)) esueaza daca nici un caracter
nu poate
fi citit de la intrare (din cin). In acest caz se returneaza END pentru

a
termina sesiunea de calcul. Operatorul ! (not) se utilizeaza intrucit
get()
returneaza o valoare nenula in caz de succes. Functia isspace() din
<ctype.h>
furnizeaza testul standard pentru spatiu alb (&8.4.1). Functia
isspace(c)
returneaza o valoare nenula daca c este un caracter alb, zero altfel.
Testul
este implementat ca o tabela de cautare, astfel, utilizind isspace este

mai
rapid decit daca s-ar testa individual caracterele spatiu alb. Acelasi
lucru
se aplica la functiile isalpha(), isdigit() si isalnum() utilizate in
get_token().
Dupa ce s-a facut avans peste caracterele albe, se utilizeaza
caracterul
urmator pentru a determina ce fel de unitate lexicala incepe in sirul
de
intrare. Sa ne oprim la niste cazuri separate inainte de a prezenta
functia
completa. Expresiile terminatoare '\n' si ';' sint tratate astfel:
switch(ch)
{
case ';' :
case '\n' : cinn >> WS; //sare peste caractere albe
return curr_tok = PRINT;
}
Saltul peste caractere albe (din nou) nu este necesar, dar daca ar

trebui
s-ar repeta apeluri ale lui get_token().
WS este un obiect de spatiu alb declarat in <stream.h>. El este
utilizat
numai pentru a indeparta spatiile albe. O eroare la intrare sau la
sfirsitul
intrarii nu va fi detectata pina la apelul urmator a lui get_token().
Sa observam modul in care diferite etichete ale lui case pot fi
utilizate
pentru un singur sir de instructiuni care trateaza acele cazuri. Se
returneaza
unitatea PRINT si se pune in curr_tok in ambele cazuri. Numerele se
trateaza
astfel:
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.': cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
Scrierea etichetelor orizontal in loc de vertical, in general, nu
este o
idee buna deoarece este mai greu de citit, dar nu este nimerit in cazul

de
fata sa avem o linie pentru fiecare cifra. Deoarece operatorul >> este
deja
definit pentru a citi constante in virgula flotanta dubla precizie,
codul este
trivial. Intii caracterul initial (o cifra sau un punct) se pune inapoi

in cin
si apoi constanta poate fi citita in number_value.
Un nume, care este un lexic de tip NAME, este definit ca o litera
care
este posibil sa fie urmata de litere sau cifre:

if(isalpha(ch))
{char* p = name_string;
*p++ = ch;
while(cin.get(ch) && isalnum(ch))
*p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok = NAME;
}
Aceasta construieste un sir terminat cu zero in name_string.
Functiile
isalpha() si isalnum() sint furnizate in <ctype.h>, isalnum(c) este
diferit
de zero daca c este o litera sau o cifra si zero altfel.
Iata in final functia de intrare completa:
token_value get_token()
{char ch;
do{ //sare peste spatiile albe exceptind '\n'
if(!cin.get(ch))
return curr_tok = END;
}while(ch != '\n' && isspace(ch));
switch(ch)
{
case ';' :
case '\n': cin >> WS; //salt peste spatii albe
return curr_tok = PRINT;
case '*':
case '/':
case '+':
case '(':
case ')':
case '=': return curr_tok = ch;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.': cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
default : //NAME, NAME= sau eroare
if(isalpha(ch))
{
char* p = name_string;
*p++ = ch;
while(cin.get(ch) && isalnum(ch))
*p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok = NAME;
}
error("bad token");
return curr_tok = PRINT;
}
}
Intrucit token_value al unui operator a fost definit prin valoarea
intreaga a operatorului, toate alternativele (case) pentru operator se
trateaza trivial.

3.1.3 Tabela de simboluri
-------------------

O singura functie are acces la tabela de simboluri:

name* look(char* p, int ins = 0);

Cel de al doilea argument indica daca sirul de caractere presupus
trebuie
sa fie in prealabil inserat sau nu.
Folosirea argumentului implicit da pentru look posibilitatea
convenabila
de a scrie look("sqrt2") in loc de look("sqrt2", 0), adica se
doreste
cautare, nu inserare. Pentru a obtine notatii convenabile pentru
inserare se
defineste o a doua functie:

inline name* insert(char* s){ return look(s, 1); }

Asa cum s-a mentionat inainte, intrarile in tabela sint de forma:

struct name{
char* string;
name* next;
double value;
};

Elementul next se utilizeaza pentru a face inlantuirea numerelor in
tabela.
Tabela insasi este pur si simplu un vector de pointeri spre
obiecte de
tip nume:

const TBLSZ = 23;
name* table[TBLSZ];

Deoarece toate obiectele statice sint implicit initializate cu
zero,
aceasta declaratie triviala a lui table asigura de asemenea si
initializarea.
Pentru a gasi o intrare pentru un nume din tabela, look()
utilizeaza un
cod hash simplu (numele cu acelasi cod hash se inlantuie):

int ii = 0;
char* pp = p;
while(*pp)
ii = ii << 1 ^ *p++;
if(ii < 0)
ii = -ii;
ii %= TBLSZ;

Fiecare caracter din sirul de intrare p este "adaugat" la ii ("suma"
caracterelor precedente) printr-un sau exclusiv. Un bit din x^y este
setat
daca si numai daca bitii corespunzatori din operanzii x si y sint
diferiti.
Inainte de a face un sau exclusiv, ii se deplaseaza cu un bit in stinga

pentru
a elimina utilizarea numai a unui octet din el. Aceasta se poate
exprima
astfel:

ii <<= 1;
ii ^= *pp++;

Utilizarea lui ^ este mai rapida decit a lui +. Deplasarea este
esentiala
pentru a obtine un cod hash rezonabil in ambele cazuri. Instructiunile:
if(ii < 0)
ii = -ii;
ii %= TBLSZ;
asigura ca ii sa fie in domeniul 0 ... TBLSZ - 1, (% este opera torul
modulo, numit si rest).
Iata functia completa:
extern int strlen(const char*);
extern int strcmp(const char*, const char*);
extern char* strcpy(const char*, const char*);
name* look(char* p, int ins = 0)
{int ii = 0; //hash
char* pp = p;
while(*pp)
ii = ii << 1 ^ *pp++;
if(ii < 0)
ii = -ii;
ii %= TBLSZ;
for(name* n = table[ii]; n; n = n->next) //cautare
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name; //inserare
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->next = table[ii];
table[ii] = nn;
return nn;
}
Dupa ce codul hash a fost calculat in ii, numele este gasit
printr-o
cautare simpla prin intermediul cimpurilor next. Fiecare nume este
verificat
folosind functia strcmp() de comparare a sirurilor. Daca este gasit, se
returneaza numele lui; altfel se adauga un nume nou.
Adaugarea unui nume implica crearea unui obiect cu numele nou
intr-o zona
de memorie libera folosind operatorul new (vezi &3.2.6), initializarea
si
adaugarea lui la lista de nume. Adaugarea se face punind noul nume in
capul
listei deoarece aceasta se poate face fara a testa daca exista sau nu o

lista.
Sirul de caractere care alcatuieste numele trebuie si el pastrat intr-o

zona
libera. Functia strlen() se foloseste pentru a gasi cit de multa
memorie este
necesara, operatorul new pentru a aloca memorie, iar functia strcpy()
pentru a
copia sirul in zona respectiva.

3.1.4 Tratarea erorilor
-----------------
Intrucit programul este atit de simplu, tratarea erorilor nu este
o
preocupare majora. Functia eroare pur si simplu numara erorile, scrie
un mesaj
de eroare si returneaza:
int no_of_errors;
double error(char* s)
{cerr << "error: " << s << "\n";
no_of_errors++;
return 1;
}
Motivul pentru care se returneaza o valoare este faptul ca erorile

de
obicei apar in mijlocul evaluarii unei expresii, asa ca ar trebui sau
sa se
faca un abandon al acelei evaluari sau sa se returneze o valoare care
in
continuare sa fie putin probabil sa cauzeze erori. Ultima varianta este
adecvata pentru acest calculator simplu. Daca get_token() ar tine
numerele de
linie, error() ar putea informa utilizatorul aproximativ asupra locului

unde a
aparut eroarea. Aceasta ar fi util la o folosire interactiva a
calculatorului.
Adesea un program trebuie sa fie terminat dupa o eroare deoarece
nu
exista o cale adecvata care sa permita continuarea executiei. Acest
lucru se
poate face apelind functia exit(), care la inceput videaza lucrurile de

tipul
fisierelor de iesire (&8.3.2) dupa care se termina programul iar
valoarea
returnata de el este argumentul lui exit(). Un mod mai drastic de
terminare a
programului este apelul lui abort() care termina imediat sau imediat
dupa
pastrarea undeva a informatiei pentru debugger (vidaj de memorie).

3.1.5 Driverul
--------

Cu toate bucatile programului construite noi avem nevoie numai de
un
driver care sa initializeze si sa porneasca tot procesul. In acest
exemplu
simplu functia main() poate fi construita astfel:

int main() //insereaza nume predefinite
{
insert("pi")->value = 3.1415926535897932385;
insert("e")->value = 2.7182818284590452354;
while(cin)
{
get_token();
if(curr_tok == END)
break;
if(curr_tok == PRINT)
continue;
cout << expr() << "\n";
}
return no_of_errors;
}

Prin conventie, main() returneaza zero daca programul se termina
normal
si altfel, o valoare diferita de zero, asa ca returnarea numarului de
erori se
potriveste bine cu aceasta conventie.
Aici singurele initializari sint numerele predefinite pentru "pi"
si "e"
care se insereaza in tabela de simboluri.
Sarcina primordiala a ciclului principal este sa citeasca expresii

si sa
scrie raspunsul. Aceasta se obtine prin linia:

cout << expr() << "\n";

Testind pe cin la fiecare pas al ciclului se asigura ca programul
sa se
termine daca ceva merge rau in sirul de intrare iar testul pentru END
asigura
ca ciclul sa se termine corect cind get_token() intilneste sfirsitul de
fisier. O instructiune break provoaca iesirea din instructiunea switch
sau din
ciclul care o contine (adica o instructiune for, while sau do). Testul
pentru
PRINT (adica pentru '\n' si ';') elibereaza pe expr() de necesitatea de

a
prelucra expresii vide. O instructiune continue este echivalenta cu
trecerea
la sfirsitul ciclului, asa ca in acest caz:
while(cin)
{ //............
if(curr_tok == PRINT)
continue;
cout << expr() << "\n";
}
este echivalent cu :
while(cin)
{
//............
if(curr_tok == PRINT)
goto end_of_loop;
cout << expr() << "\n";
end_of_loop : ;
}
(ciclurile se descriu in detaliu in &r9).

3.1.6 Argumentele liniei de comanda
-----------------------------

Dupa ce programul a fost scris si testat, am observat ca tastarea
expresiilor la intrarea standard a fost adesea mai mult decit necesar,
deoarece in mod frecvent a trebuit sa se evalueze o singura expresie.
Daca
este posibil ca aceasta expresie sa fie prezentata ca un argument al
liniei de
comanda, atunci multe accese cheie ar fi fost eliminate.
Asa cum s-a mentionat in prealabil, un program incepe prin apelul
lui
main(). Cind aceasta s-a facut, main() primeste doua argumente, care
specifica
numarul de argumente si care de obicei se numeste argc si un vector de
argumente, care de obicei se numeste argv. Argumentele sint siruri de
caractere, asa ca tipul lui argv este char *[argc]. Numele unui program
(intrucit el apare pe linia de comanda) se paseaza ca argv[0], asa ca
argc
este intotdeauna cel putin 1. De exemplu, pentru comanda:
dc 150/1.1934
argumentele au aceste valori:
argc 2
argv[0] "dc"
argv[1] "150/1.1934"
Nu este dificil sa fie pastrata linia de comanda ca argument. Problema
este
cum sa se foloseasca fara a face reprogramare. In acest caz, este
trivial
intrucit un sir de intrare poate fi limitat la un sir de caractere in
loc de
un fisier (&8.5). De exemplu, cin poate fi facut sa citeasca caractere
dintr-un sir in loc de intrarea standard:
int main(int argc, char* argv[])
{switch(argc)
{case 1: break; //citeste din intrarea standard
case 2: cin = *new istream(strlen(argv[1]),argv[1]);
break;
default: error("too many arguments");
return 1;
}
// ca inainte
}
Programul este neschimbat exceptind adaugarea argumentelor la
main() si
utilizarea lor in instructiunea switch.
S-ar putea usor modifica main() pentru a accepta diferite
argumente in
linia de comanda, dar acest lucru nu este necesar, deoarece diferite
expresii
pot fi pasate ca un singur argument:
dc "rate=1.1934;150/rate;19.75/rate217/rate"
Ghilimelele sint necesare aici din cauza ca ';' este separator de
comenzi in
sistemul UNIX.

3.2 Sumar de operatori
------------------
Operatorii C++ sint descrisi sistematic si complet in &r7.
Aici,
este un sumar al lor si niste exemple. Fiecare operator este urmat de
unul sau
mai multe nume utilizate in comun pentru el si de un exemplu de
utilizare a
lui. In aceste exemple class_name este numele unei clase, member este
un nume
al unui membru, un object este o expresie care produce un obiect, un
pointer
este o expresie care produce un pointer, o expr este o expresie, iar o
lvalue
este o expresie ce noteaza un obiect neconstant. Un type poate fi un
nume de
tip general complet (cu *, (), etc.) numai cind el apare in paranteze.
Altfel
exista restrictii.
Operatorii unari si operatorii de atribuire se asociaza de la
dreapta;
toti ceilalti se asociaza de la stinga.
Adica
a = b = c inseamna a = (b = c),
a + b + c inseamna (a + b) + c,
iar
*p++ inseamna *(p++), nu (*p)++.
-----------------------------------------------------------------
| SUMAR DE OPERATORI |
-----------------------------------------------------------------
| :: domeniu de existenta class_name::member |
| :: global ::name |
-----------------------------------------------------------------
| -> selectare de membru pointer->member |
| [] indexare pointer[expr] |
| () apel de functie expr(expr_list) |
| () constructie de valoare type(expr_list) |
| sizeof dimensiunea unui obiect sizeof expr |
| sizeof dimensiunea unui tip sizeof(type) |
-----------------------------------------------------------------
| ++ increment postfixat lvalue++ |
| ++ increment prefixat ++lvalue |
| -- decrement postfixat lvalue-- |
| -- decrement prefixat --lvalue |
| ~ complement ~expr |
| ! negare !expr |
| - minus unar -expr |
| + plus unar +expr |
| & adresa &lvalue |
| * indirectare *expr |
| new creaza(aloca) new type |
| delete distruge(dealoca) delete pointer |
| delete[] distruge un vector delete[expr]pointer|
| (type) conversie de tip (type)expr |
-----------------------------------------------------------------
| * inmultire expr * expr |
| / impartire expr / expr |
| % modulo(rest) expr % expr |
-----------------------------------------------------------------
| + adunare(plus) expr + expr |
| - scadere(minus) expr - expr |
-----------------------------------------------------------------
| << deplasare stinga expr << expr |
| >> deplasare dreapta expr >> expr |
-----------------------------------------------------------------
| < mai mic expr < expr |
| <= mai mic sau egal expr <= expr |
| > mai mare expr > expr |
| >= mai mare sau egal expr >= expr |
-----------------------------------------------------------------
| == egal expr == expr |
| != diferit expr != expr |
-----------------------------------------------------------------
| & si pe biti expr & expr |
-----------------------------------------------------------------
-----------------------------------------------------------------
| ^ sau exclusiv pe biti expr ^ expr |
-----------------------------------------------------------------
| | sau pe biti expr | expr |
-----------------------------------------------------------------
| && si logic expr && expr |
-----------------------------------------------------------------
| || sau logic expr || expr |
-----------------------------------------------------------------
| ? : if aritmetic expr ? expr : expr |
-----------------------------------------------------------------
| = asignare simpla lvalue = expr |
| *= inmultire si asignare lvalue *= expr |
| /= impartire si asignare lvalue /= expr |
| %= modulo si asignare lvalue %= expr |
| += adunare si asignare lvalue += expr |
| -= scadere si asignare lvalue -= expr |
| <<= deplasare stinga si asignare lvalue <<= expr |
| >>= deplasare dreapta si asignare lvalue >>= expr |
| &= si pe biti si asignare lvalue &= expr |
| |= sau pe biti si asignare lvalue |= expr |
| ^= sau exclusiv pe biti si asignare lvalue ^= expr |
-----------------------------------------------------------------
| , virgula(succesiune) expr, expr |
-----------------------------------------------------------------
Fiecare dreptunghi contine operatori cu aceeasi prioritate. Un
operator
are o prioritate mai mare decit operatorii aflati in dreptunghiuri
inferioare.
De exemplu:
a + b * c
inseamna
a + (b * c)
deoarece * are prioritate mai mare decit +, iar a + b - c inseamna (a +

b) - c
deoarece + si - au aceeasi prioritate, dar operatorii + si - sint
asociati de
la stinga spre dreapta.

3.2.1 Paranteze rotunde
-----------------

Parantezele rotunde sint suprasolicitate in sintaxa lui C++. Ele
au un
numar mare de utilizari: includ argumentele in apelurile de functii,
includ
tipul intr-o conversie de tip, includ nume de tipuri pentru a nota
functii si,
de asemenea, pentru a rezolva conflictul prioritatilor intr-o expresie.

Din
fericire, ultimul caz nu este necesar foarte frecvent deoarece regulile

cu
nivelele de prioritate si de asociativitate sint astfel definite ca
expresiile
sa "functioneze" asa cum ne asteptam (adica sa re flecte utilizarile
cele mai
frecvente). De exemplu:

if(i <= 0 || max < i)
//..........

are intelesul obisnuit. Cu toate acestea, parantezele ar trebui
utilizate ori
de cite ori un programator este in dubiu despre acele reguli:

if((i <= 0)||(max < i))
//..........

Utilizarea parantezelor este mai frecventa cind subexpresiile sint

mai
complicate; dar subexpresiile complicate sint o sursa de erori, asa ca
daca
simtim nevoia de a folosii paranteze am putea sa descompunem expresiile
utilizind variabile auxiliare. Exista, de asemenea, cazuri cind
prioritatea
operatorilor nu conduce la o interpretare "evidenta". De exemplu:

if(i & mask == 0)
//..........

nu aplica o masca la i si apoi testeaza daca rezultatul este zero.
Intrucit ==
are o prioritate mai mare decit &, expresia este interpretata ca: i &
(mask
== 0). In acest caz parantezele sint importante:

if((i & mask) == 0)
//..........

De asemenea, poate fi util sa observam ca secventa de mai jos nu
functioneaza in modul in care s-ar astepta un utilizator naiv:

if(0 <= a <= 99)
//.........

este legal, dar se interpreteaza ca:

(0 <= a) <= 99

si rezultatul primei comparatii este 0 sau 1 si nu a (daca a
diferit de
1). Pentru a testa daca a este in domeniul 0..99 se poate folosi:

if(0 <= a && a <= 99)
//..........


3.2.2 Ordinea de evaluare
-------------------

Ordinea de evaluare a subexpresiilor intr-o expresie este
nedefinita. De
exemplu:

int i = 1;
v[i] = i++;

poate fi evaluata sau ca v[1] = 1, sau ca v[2] = 1. Un cod mai bun se
poate
genera in absenta restrictiilor asupra ordinii de evaluare a
expresiilor. Ar
fi mai bine daca compilatorul ne-ar avertiza despre astfel de
ambiguitati;majoritatea compilatoarelor nu fac acest lucru.
Operatorii && si || garanteaza faptul ca operandul lor sting se
evalueaza
inaintea celui drept. De exemplu, b = (a = 2, a + 1) atribuie lui b
valoarea
3. Exemple de utilizare a lui && si || se dau in paragraful &3.3.1. Sa
observam ca operatorul virgula este logic diferit de virgula folosita
pentru a
separa argumente intr-un apel de functie. Sa consideram:

f1(v[i], i++); //doua argumente
f2((v[i], i++)); //un argument

Apelul lui f1 are doua argumente, v[i] si i++, iar ordinea de
evaluare a
expresiilor argument este nedefinita. Ordinea de evaluare a expresiilor
argument este neportabila si nu este precizata. Apelul lui f2 are un
singur
argument si anume expresia (v[i], i++).
Parantezele nu pot fi utilizate pentru a forta ordinea de
evaluare;
a*(b/c) poate fi evaluata ca (a*b)/c deoarece * si / au aceeasi
precedenta.
Cind ordinea de evaluare este importanta, se pot introduce variabile
temporare. De exemplu:
(t = b / c, a * t)


3.2.3 Incrementare si Decrementare
----------------------------

Operatorul ++ se utilizeaza pentru a exprima o incrementare
directa in
schimbul exprimarii ei folosind o combinatie intre adunare si
atribuire. Prin
definitie, ++lvalue inseamna: lvalue += 1 care din nou inseamna
lvalue =
lvalue + 1 cu conditia ca lvalue sa nu aiba efecte secundare. Expresia
care
noteaza obiectul de incrementat se evalueaza o singura data.
Decrementarea
este exprimata similar prin operatorul --. Operatorii ++ si -- pot fi
utilizati ambii atit prefix cit si postfix. Valoarea lui ++x este noua
valoare
a lui x (adica cea incrementata). De exemplu y = ++x este echivalent cu

y = (x
+= 1). Valoarea lui x++ este valoarea veche a lui x. De exemplu y=x++
este
echivalent cu y = (t=x, x+=1, t), unde t este o variabila de acelasi
tip cu x.
Operatorii de incrementare sint utili mai ales pentru a incrementa

si
decrementa variabile in cicluri. De exemplu se poate copia un sir
terminat cu
zero astfel:

inline void cpy(char* p, const char* q){while(*p++ = *q++);}

Sa ne amintim ca incrementind si decrementind pointeri, ca si
adunarea
sau scaderea dintr-un pointer, opereaza in termenii elementelor
vectorului
spre care pointeaza pointerul in cauza; p++ face ca p sa pointeze spre
elementul urmator. Pentru un pointer de tip T*, are loc prin definitie:
long(p + 1) == long(p) + sizeof(T);


3.2.4 Operatori logici pe biti
-------------------------

Operatorii logici pe biti &, |, ^, ~, >> si << se aplica la
intregi;
adica obiecte de tip char, short, int, long si corespunzatoarele lor
fara semn
(unsigned), iar rezultatele lor sint de asemenea intregi.
O utilizare tipica a operatorilor logici pe biti este de a
implementa
seturi mici (vectori de biti). In acest caz fiecare bit al unui intreg
fara
semn reprezinta numai un membru al setului, iar numarul de biti
limiteaza
numarul de membri. Operatorul binar & este interpretat ca intersectie,
| ca
reuniune si ^ ca diferenta. O enumerare poate fi utilizata pentru a
numi
membri u. Se utilizeaza operatorul |= deoarece sirul ar putea
fi deformat deja (adica state == _bad) asa ca: cin.state = _eof ar

fi
sters conditia
respectiva. Se poate gasi modul in care difera doua stari astfel:

state_value diff = cin.state ^ cout.state;

Pentru tipul stream_state o astfel de diferenta nu este foarte
folositoare, dar pentru alte tipuri similare ea este mai utila. De
exemplu, sa
consideram compararea unui vector de biti care reprezinta setul de
intreruperi
de prelucrat cu un altul care reprezinta setul de intreruperi ce
asteapta sa
fie prelucrat.
Sa observam ca utilizind cimpurile (&2.5.1) se obtine o
prescurtare
convenabila pentru a deplasa masca si a extrage cimpuri de biti
dintr-un
cuvint. Aceasta se poate face, evident, utilizind operatorii logici pe
biti.
De exemplu, se pot extrage 16 biti din mijlocul unui int de 32 de biti
astfel:
unsigned short middle(int a){ return (a >> 8) & 0xffff; }
Sa nu se faca confuzie intre operatorii logici pe biti cu cei logici
&&, || si
!. Acestia din urma returneaza sau 0 sau 1 si ei sint in primul rind
utili
pentru a scrie teste in if, while sau for (&3.3.1). De exemplu !0
(negatia
lui 0) are valoarea 1, in timp ce ~0 (complementul lui zero) reprezinta
valoarea -1 (toti biti sint unu).

3.2.5 Conversia tipului
-----------------
Uneori este necesar sa se converteasca o valoare de un tip spre o
valoare
de un alt tip. O conversie de tip explicit produce o valoare de un tip
dat
pentru o valoare a unui alt tip. De exemplu:
float r = float(1);
converteste valoarea 1 spre valoarea flotanta 1.0 inainte de a face
atribuirea. Rezultatul unei conversii de un tip nu este o lvalue deci
nu i se
poate face o asignare (numai daca tipul este un tip referinta).
Exista doua notatii pentru conversia explicita a tipului: notatia
traditionala din C (de exemplu (double)) si notatia functionala
(double(a)).
Notatia functionala nu poate fi folosita pentru tipuri care nu au un
nume
simplu. De exemplu, pentru a converti o valoare spre un pointer se
poate
folosi notatia din C:
char* p = (char*)0777;
sau sa se defineasca un nume de tip nou:
typedef char* pchar;
char* p = pchar(0777);
Dupa parerea mea, notatia functionala este preferabila pentru exemple
netriviale. Sa consideram aceste doua exemple echivalente:
pname n2 = pbase(n1->tp)->b_name;
pname n3 = ((pbase)n2->tp)->b_name;
Intrucit operatorul -> are prioritate mai mare decit (tip), ultima
expresie se
interpreteaza astfel:
((pbase)(n2->tp))->b_name
Utilizind explicit conversia de tip asupra tipurilor pointer este
posibil sa
avem pretentia ca un obiect sa aiba orice tip. De exemplu:
any_type* p = (any_type*)&some_object;
va permite ca some_object sa fie tratat ca any_type prin p.
Cind o conversie de tip nu este necesara ea trebuie eliminata.
Programele
care utilizeaza multe conversii explicite sint mai greu de inteles
decit
programele care nu le utilizeaza. Totusi, astfel de programe sint mai
usor de
inteles decit programele care pur si simplu nu utilizeaza tipuri
pentru a
reprezenta concepte de nivel mai inalt (de exemplu, un program care
opereaza
cu un registru de periferic folosind deplasari si mascari de intregi in

loc de
a defini o structura corespunzatoare si o operatie cu ea; vezi &2.5.2).

Mai
mult decit atit, corectitudinea unei conversii explicite de tip depinde

adesea
in mod esential de intelegerea de catre programator a modului in care
diferite
tipuri de obiecte sint tratate in limbaj si foarte adesea de detaliile
de
implementare. De exemplu:
int i = 1; char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// sizeof(int) < sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// char* se reprezinta diferit de int*
Pe multe masini nu se va intimpla nimic rau, dar pe altele
rezultatul va
fi dezastruos. In cel mai bun caz, un astfel de cod nu este portabil.
De
obicei este gresit sa presupunem ca pointerii la diferite structuri au
aceeasi
reprezentare. Mai mult decit atit, orice pointer poate fi asignat la un

void*
(fara un tip explicit de conversie) si un void* poate fi convertit
explicit la
un pointer de orice tip.
In C++, conversia explicita de tip nu este necesara in multe
cazuri in
care in C este necesara. In multe programe conversia explicita de tip
poate fi
complet eliminata, iar in multe alte programe utilizarea ei poate fi
localizata in citeva rutine.

3.2.6 Memoria libera
--------------

Un obiect denumit este sau static sau automatic (vezi &2.1.3). Un
obiect
static se aloca cind incepe programul si exista pe durata executiei
programului! Un obiect automatic se aloca de fiecare data cind se intra

in
blocul lui si este eliminat numai cind se iese din bloc. Adesea este
util sa
se creeze un obiect nou care exista numai cit timp este nevoie de el.
In
particular, adesea este util sa se creeze un obiect care poate fi
utilizat
dupa ce se revine dintr-o functie in care el a fost creat. Operatorul
new
creaza astfel de obiecte, iar operatorul delete poate fi folosit pentru

a le
distruge mai tirziu. Obiectele alocate prin new se spune ca sint in
memoria
libera. Astfel de obiecte sint de exemplu nodurile unui arbore sau a
unei
liste inlantuite care sint parte a unei sructuri de date mai mari a
carei
dimensiune nu poate fi cunoscuta la compilare.Sa consideram modul in
care s-ar
putea scrie un compilator in stilul folosit la calculatorul de birou.
Functiile de analiza sintactica ar putea construi o reprezentare sub
forma de
arbore a expresiilor, care sa fie utilizata de generatorul de cod. De
exemplu:
struct enode{
token_value oper;
enode* left;
enode* right;
};
enode* expr()
{
enode* left = term();

for(;;)
switch(curr_tok)
{
case PLUS :
case MINUS: get_token();
enode* n = new enode;
n->oper = curr_tok;
n->left = left;
n->right = term();
left = n;
break;
default : return left;
}
}

Un generator de cod ar putea utiliza arborele rezultat astfel:

void generate(enode* n)
{
switch(n->oper)
{
case PLUS: //face ceva potrivit starii curente
delete n;
}
}

Un obiect creat prin new exista pina cind este distrus explicit
prin
delete dupa care spatiul ocupat de el poate fi reutilizat prin new. Nu
exista
"colectarea rezidurilor". Operatorul delete se poate aplica numai la un
pointer returnat de new sau la zero. Aplicarea lui delete la zero nu
are nici
un efect. Se pot, de asemenea, crea vectori de obiecte prin intermediul

lui
new. De exemplu:

char* save_string(char* p)
{
char* s = new char[strlen(p)+1];
strcpy(s, p);
return s;
}

Sa observam ca pentru a dealoca spatiul alocat prin new, delete
trebuie
sa fie capabil sa determine dimensiunea obiectului alocat. De exemplu:
int main(int argc, char* argv[])
{
if(argc < 2)
exit(1);
char* p = save_string(argv[1]);
delete p;
}

Aceasta implica faptul ca un obiect alocat utilizind implementarea
standard prin new va ocupa putin mai mult spatiu decit un obiect static

(de
obicei un cuvint in plus).
Este de asemenea, posibil sa se specifice dimensiunea unui vector
explicit intr-o operatie de stergere. De exemplu:

int main(int argc, char* argv[])
{
if(argc < 2)
exit(1);
int size = strlen(argv[1])+1;
char* p = save_string(argv[1]);
delete[size] p;
}

Dimensiunea vectorului furnizata de utilizator se ignora exceptind

unele
tipuri definite de utilizator (&5.5.5).
Operatorii de memorie libera se implementeaza prin functiile
(&r7.2.3):

void* operator new(long);
void operator delete(void*);
Implementarea standard a lui new nu initializeaza obiectul
returnat. Ce
se intimpla daca new nu gaseste memorie de alocat. Intrucit chiar
memoria
virtuala este finita, uneori se poate intimpla acest lucru; o cerere de

forma:

char* p = new char[100000000];

de obicei va cauza probleme. Cind new esueaza, ea apeleaza functia spre

care
pointeaza pointerul _new_handler (pointerii spre functii vor fi
discutati in
&4.6.9). Noi putem seta acel pointer direct sau sa utilizam functia
set_new_handler(). De exemplu:

#include <stream.h>

void out_of_store()
{
cerr << "operator new failed: out of store\n";
exit(1);
}

typedef void (*PF)(); //pointer spre tipul functiei
extern PF set_new_handler(PF);

main()
{
set_new_handler(&out_of_store);
char* p = new char[100000000];
cout << "done, p= " << long(p) << "\n";
}

de obicei niciodata nu va scrie done dar in schimb va produce:

operator new failed: out of store

Un _new_handler ar putea face ceva mai destept decit pur si simplu

sa
termine programul. Daca noi stim cum lucreaza new si delete, de
exemplu,
deoarece noi furnizam operatorii nostri proprii new() si delete(),
handlerul
ar putea astepta sa gaseasca memorie pentru new. Cu alte cuvinte, un
utilizator ar putea furniza un colector de reziduri, redind in
utilizare
zonele sterse. Aceasta evident nu este o sarcina pentru un incepator.
Din motive istorice, new pur si simplu returneaza pointerul 0 daca

el nu
gaseste destula memorie si nu a fost specificat un _new_handler. De
exemplu:

#include <stream.h>
main()
{
char* p = new char[100000000];
cout << "done, p= " << long(p) << "\n";
}

va produce
done, p= 0

Noi am avertizat! Sa observam ca furnizind _new_handler se
verifica
depasirea memoriei pentru orice utilizare a lui new in program
(exceptind
cazul cind utilizatorul furnizeaza rutine separate pentru tratarea
alocarii
obiectelor de tipuri specifice definite de utilizator; vezi &5.5.6).
3.3 Sumarul instructiunilor
-----------------------

Instructiunile C++ sint descrise sistematic si complet in &r.9. Cu

toate
acestea, dam mai jos un rezumat si citeva exemple.
Sintaxa instructiunilor:
statement:
declaration
{
statement_list_opt
}
expression_opt;
if(expression) statement
if(expression) statement else statement
switch(expression) statement
while(expression) statement
do statement while(expression);
for(statement expression_opt; expression_opt)
statement
case constant_expression: statement
default: statement
break;
continue;
return expression_opt;
goto identifier;
identifier: statement
statement_list:
statement
statement statement_list
Sa observam ca o declaratie este o instructiune si ca nu exista
nici o
instructiune de atribuire sau de apel; atribuirea si apelul functiei se
trateaza ca expresii.

3.3.1 Teste
-----

O valoare poate fi testata sau printr-o instructiune if sau
printr-o
instructiune switch:
if(expression) statement
if(expression) statement else statement
switch(expression) statement
Nu exista in C++ tipul boolean separat.
Operatorii de comparare == != < <= > >= returneaza valoarea 1

daca
compararea este adevarata si 0 altfel. Nu este ceva iesit din comun ca
sa
consideram ca true se defineste ca 1 si false ca 0.
Intr-o instructiune if se executa prima (sau singura) instructiune

daca
expresia este diferita de zero si altfel se executa cea de a doua
instructiune
(daca este prezenta). Aceasta implica faptul ca orice expresie intreaga

poate
fi utilizata ca o conditie. In particular, daca a este un intreg:
if(a)
//........
este echivalent cu
if(a != 0)
//........
Operatorii logici &&, || si ! sint cei mai utilizati in
conditii.
Operatorii && si || nu vor evalua cel de al doilea argument al lor
numai daca
este necesar. De exemplu:

if(p && 1 < p->count)
//........

intii testeaza ca p nu este nul si numai daca este asa se testeaza 1 <
p->count.
Anumite instructiuni if simple pot fi inlocuite convenabil
inlocuindu-le
prin expresii if aritmetice. De exemplu:

if(a <= b)
max = b;
else
max = a;

este mai bine sa fie exprimat prin

max = (a<=b) ? b:a;

Parantezele in jurul conditiei nu sint necesare, dar codul este
mai usor
de citit cind sint utilizate.
Anumite instructiuni switch simple pot fi scrise prin mai multe
instructiuni if. De exemplu:

switch(val)
{
case 1: f();
break;
case 2: g();
break;
default: h();
break;
}

se poate scrie

if(val==1)
f();
else if(val==2)
g();
else h();

Intelesul este acelasi, dar prima versiune (cu switch) este de
preferat
din cauza ca natura operatiei (testul unei valori fata de un set de
constante)
este explicita in acest caz. Aceasta face ca instructiunea switch sa
fie mai
usor de citit.
Sa avem grija ca un case al unui switch trebuie terminat cumva
daca nu
dorim ca executia sa continue cu case-ul urmator. De exemplu:
switch(val)
{
case 1: cout << "case 1\n";
case 2: cout << "case 2\n";
default: cout << "default: case not found\n";
}
cu val == 1 va imprima
case 1
case 2
default: case not found
spre marea surprindere a neinitiatilor. Cel mai frecvent mod de
intrerupere al
unui case este terminarea prin break, dar se poate adesea folosi o
instructiune return sau goto. De exemplu:
switch(val)
{case 0: cout << "case 0\n";
case 1: cout << "case 1\n";
return;
case 2: cout << "case 2\n";
goto case 1;
default: cout << "default: case not found\n";
return;
}

Apelat cu val == 2, produce

case 2
case 1

Sa observam ca o scriere de forma
goto case 1;
este o eroare sintactica.

3.3.2 Goto
----

C++ are faimoasa instructiune goto.

goto identifier;
identifier: statement

Are putine utilizari in limbajele de nivel inalt, dar poate fi
foarte
util cind un program C++ este generat printr-un program in loc ca
programul sa
fie scris direct de catre o persoana; de exemplu, goto-urile pot fi
utilizate
intr-un analizor generat dintr-o gramatica printr-un generator de
analizoare.
Goto poate fi, de asemenea, important in acele cazuri cind
eficienta
optimala este esentiala, de exemplu, in ciclul interior al unei
aplicatii de
timp real.
Una din putinele utilizari bune ale lui goto este iesirea dintr-un

ciclu
imbricat sau switch (instructiunea break intrerupe numai ciclul sau
switch-ul
cel mai interior care o contine). De exemplu:
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
if(nm[i][j] == a)
goto found;
// not found
//...........
found:
// nm[i][j] == a;
Exista de asemenea instructiunea continue, care transfera
controlul la
sfirsitul instructiunii ciclice, asa cum s-a explicat in &3.1.5.

3.4 Comentarii si Decalari
----------------------

Utilizarea judicioasa a comentariilor si utilizarea consistenta a
decalarilor poate face sarcina citirii si intelegerii unui program mai
placuta. Exista diferite stiluri ale decalarilor. Autorul nu vede
motive
fundamentale pentru a prefera un stil fata de altul (deci, ca multi
altii, eu
am preferintele mele). Acelasi lucru se aplica si la stilurile de
comentare.
Comentariile pot fi omise, dar atunci citirea programului va fi
serios
afectata. Compilatorul nu intelege continutul unui comentariu, asa ca
nu
exista nici o cale de a ne asigura ca un comentariu:

[1] este de neinteles;
[2] descrie programul;
[3] este pus la zi.

Multe programe contin comentarii care sint incomprehensibile,
ambigue si
chiar eronate. Comentariile rele pot fi mai rele decit daca nu ar
exista.
Daca ceva poate fi exprimat in limbajul insusi, ar trebui sa fie
mentionat in el, nu numai intr-un comentariu. Aceasta remarca este
intarita de
comentariile de mai jos:

//variabila "v" trebuie sa fie initializata
//variabila "v" trebuie sa fie folosita numai de functia "f()"
//apeleaza functia "init()" inainte de a apela
//orice alta functie din acest fisier
//apeleaza functia "cleanup()" la sfirsitul programului
//sa nu se utilizeze functia "wierd()"
//functia "f()" are doua argumente

Astfel de comentarii pot adesea sa fie interpretate ca necesare
printr-o
utilizare corespunzatoare a lui C++. De exemplu, s-ar putea utiliza
regulile
de linkere (&4.2) si vizibilitate, initializare si curatire pentru
clase (vezi
&5.5.2) pentru a face exemplele precedente redondante.
Odata ce a fost afirmat ceva clar in limbaj, nu ar trebui
mentionat a
doua oara intr-un comentariu. De exemplu:

a = b+c // a devine b+c
count++ // se incrementeaza count

Astfel de comentarii sint mai rele decit redondanta: ele maresc
cantitatea de text pe care trebuie sa o citeasca programatorul si ele
adesea
fac mai obscura structura programatorului.
Preferintele autorului sint pentru:

[1] Un comentariu pentru fiecare fisier sursa care sa afirme
ce declaratii din el se utilizeaza in comun, scopuri
generale pentru mentinere, etc.
[2] Un comentariu pentru fiecare functie netriviala care sa
indice scopul ei, algoritmul utilizat (daca nu este
evident) si poate ceva despre mediul de executie al ei.
[3] Citeva comentarii in locurile unde codul nu este evident
si/sau neportabil.
[4] Foarte mici alternative else.

De exemplu:

// tbl.c: Implementarea tabelei de simboluri
/* Eliminare Gauss prin pivotare
partiala. Vezi Ralston:...pg...
*/
//swap() presupune utilizarea stivei la un AT&T 3B20.
/*********************************
Copyright (c) 1984 AT&T. Inc.
All rights reserved
*********************************/

Un set de comentarii bine ales si bine scris este o parte
esentiala a
unui program bun. Scrierea de comentarii bune poate fi tot atit de
dificil ca
si scrierea programului insusi.
Sa observam, de asemenea, ca daca se folosesc comentariile cu //
intr-o
functie, atunci orice parte a acelei functii poate fi comentata
utilizind
stilul de comentarii /*...*/ si viceversa.

3.5 Exercitii
---------

1. (*1). Sa se scrie instructiunea urmatoare ca o instruc-
tiune while echivalenta:

for(i = 0; i < max_length; i++)
if(input_line[i] == '?')
quest_count++;

Sa se rescrie utilizind un pointer ca si variabila de control; adica
asa ca
testul sa fie unul de forma *p == '?'.

2. (*1). Sa se includa complet in paranteze expresiile
urmatoare:

a = b + c * d << 2 & 8
a & 077 != 3
a == b || a == c && c < 5
c = x != 0
0 <= i < 7
f(1, 2) + 3
a = -1+ +b-- -5
a = b == c++
a = b = c = 0
a[4][2] *= *b ? c : *d * 2
a - b, c = d

3. (*2). Sa se gaseasca 5 constructii C++ diferite pentru care
sensul
este nedefinit.

4. (*2). Sa se gaseasca 10 exemple de cod C++ neportabile.

5. (*1). Ce se intimpla daca se face o impartire cu zero pe
sistemul d-voastra? Ce se intimpla in cazul unei depasiri superioare
sau
inferioare.

6. (*1). Sa se includa complet in paranteze expresiile
urmatoare:

*p++
*--p
+++a--
(int*)->m
*p.m
*a[i]

7. (*2). Sa se scrie functiile strlen() care returneaza
lungimea unui sir, strcpy() care copiaza un sir in altul si strcmp()
care
compara doua siruri. Sa se considere ce tipuri de argumente si ce
tipuri se
cuvine sa se returneze, apoi sa se compare cu versiunile standard asa
cum sint
declarate in <string.h>.


8. (*1). Vedeti cum reactioneaza compilatorul d-voastra la aceste

erori:

a := b+1;
if(a = 3)
//.....
if(a & 077 == 0)
//.....

9. (*2). Sa se scrie o functie cat() care are doua argumente de
tip sir
si returneaza un sir care este concatenarea argumentelor. Sa se
utilizeze new
pentru a gasi memorie pentru rezultat. Sa se scrie o functie rev() care

are un
argument de tip sir si reutilizeaza caracterele din el. Adica, dupa
rev(p),
ultimul caracter a lui p va fi primul, etc.

10. (*2). Ce face exemplul urmator?

void send(register* to, register* from, register count)
{register n = (count+7)/8;
switch(count%8)
{
case 0: do{
*to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
}while(--n > 0);
}
}
De ce ar vrea cineva sa scrie un astfel de program?

11. (*2). Sa se scrie o functie atoi() care are ca argument un sir

ce
contine cifre si returneaza int-ul corespunzator. De exemplu,
atoi("123") este
123. Sa se modifice atoi() pentru a trata sirurile octale din C++ si in

plus
si cele hexazecimale. Sa se modifice atoi() pentru a trata caracterele
C++
utilizate intr-o notatie de constanta. Sa se scrie o functie itoa()
care
creaza un sir pornind de la un argument intreg.

12. (*2). Sa se rescrie get_token() (&3.1.2) asa ca sa
citeasca o
linie la un moment dat intr-un buffer si apoi sa compuna unitatile
citind
caracterele din buffer.

13. (*2). Sa se adauge functii de forma sqrt(), log() si sin()
la
calculatorul de birou din &3.1. Sa se predefineasca numele si apelul
functiilor printr-un vector de pointeri spre functii. Sa nu se uite sa

se
verifice argumentele dintr-o functie call.
14. (*3). Sa se permita unui utilizator sa defineasca
functii in
calculatorul de birou:
Scop: Sa se defineasca o functie ca un sir de operatii exact asa
cum un
utilizator ar trebui sa o scrie. Un astfel de sir poate fi memorat sau
ca un
sir de caractere sau ca o lista de unitati. Apoi se citeste si se
executa
acele operatii cind functia este apelata. Daca noi dorim ca o functie
utilizator sa aiba argumente, noi trebuie sa inventam o notatie pentru
aceasta.

15. (*1.5). Sa se converteasca calculatorul de birou pentru a
utiliza un
simbol structura in loc sa se utilizeze variabilele statice name_string

si
number_value:

struct symbol{
token_value tok;
union{
double number_value;
char* name_string;
};
};

16. (*2.5). Sa se scrie un program care elimina comentariile de
tip C++
din program. Adica, citeste din cin si elimina atit comentariile de
forma //,
cit si cele de forma /*..*/ si scrie rezultatul in cout. Trebuie sa
avem grija
de // si /*..*/ din comentarii, siruri si constante caracter.

CAPITOLUL 4
===========


FUNCTII SI FISIERE
==================

Toate programele netriviale sint alcatuite din diferite unitati
compilate
separat (conventional, numite fisiere). Acest capitol descrie cum se
compileaza functiile separat, cum se pot apela una pe alta, cum
functiile
compilate separat pot utiliza date in comun si cum tipurile utilizate
in
diferite fisiere ale programului pot fi tinute consistent
(necontradictoriu).Functiile se discuta in anumite detalii; aceasta
include
transferul de argumente, argumente implicite, nume de functii care se
suprain-
carca, pointeri spre functii si desigur, declaratii si definitii de
functii.
In final sint prezentate macrourile.

4.1. Introducere
-----------

A avea un program complet intr-un fisier este de obicei imposibil
deoarece codul pentru bibliotecile standard si de sistem sint in alta
parte.
Mai mult decit atit, avind fiecare utilizator codul sau intr-un singur
fisier
este ceva care este atit impractic cit si inconvenient. Modul in care
este
organizat un program in fisiere poate ajuta cititorul sa inteleaga
structura
unui program si sa permita compilatorului sa impuna acea structura.
Intrucit
unitatea de compilare este un fisier, tot fisierul trebuie sa fie
recompilat
ori de cite ori s-a facut in el o schimbare.
Pentru un program dimensionat chiar moderat, timpul petrecut
pentru
recompilare poate fi redus semnificativ partitionind programul in
fisiere
dimensionate potrivit.
Sa consideram exemplul cu calculatorul de birou. A fost prezentat
ca un
singur fisier sursa. Daca il tastam, noi fara indoiala avem niste
probleme
minore in obtinerea declaratiilor in ordine corecta si cel putin o
declaratie
trebuie utilizata pentru a permite compilatorului sa trateze functiile
mutual
recursive expr(), term() si prim(). Textul amintit are patru parti
(analizor
lexical, analizor sintactic, tabela de simboluri si un driver), dar
aceasta
nu se reflecta in nici un fel in cod. In realitate calculatorul nu a
fost
scris in acest fel. Acesta nu este modul de a o face; chiar daca toate
consideratiile metodologiei de programare, mentinere si eficienta
compilarii
au fost deconsiderate pentru acest program, autorul totusi va
partitiona
acest program de 200 de linii in mai multe fisiere pur si simplu pentru

a face
sarcina programarii mai placuta.
Un program care consta din mai multe parti compilate separat
trebuie sa
fie consistent (necontradictoriu) in utilizarea numelor si tipurilor in

exact
acelasi mod ca si un program care consta dintr-un singur fisier sursa.
In
principiu, aceasta se poate asigura prin linker. Linkerul este
programul care
leaga partile compilate separat. Un linker uneori este numit (gresit)
incarcator; linkerul UNIX-ului se numeste ld. Cu toate acestea linkerul
disponibil pe majoritatea sistemelor este prevazut cu putine facilitati

care
sa verifice consistenta modulelor compilate separat.
Programatorul poate compensa lipsa acestor facilitati ale
linkerului
furnizind informatii de tip suplimentare (declaratii). Un program poate

fi
realizat consistent asigurind ca declaratiile prezentate in compilari
separate
sa fie consistente. C++ a fost definit ca un instrument care sa
incurajeze
astfel de compilari cu declaratii explicite si este prevazut un linker
care sa
verifice consistenta modulelor respective. Un astfel de linker se spune

ca
face o linkare explicita. In cazul limbajului C nu se realizeaza o
linkare
explicita ci numai una implicita si ea este adesea saraca in testarea
consistentei modulelor linkate.

4.2. Link-editare
------------

Daca nu se stabileste altfel, un nume care nu este local la o
functie sau
clasa trebuie sa refere acelasi tip, valoare, functie sau obiect in
orice
parte compilata separat a programului. Deci exista numai un tip,
valoare,
functie sau obiect nelocal atasat la un nume intr-un program. De
exemplu,
consideram doua fisiere:
// file1.c:
int a = 1;
int f(){/* face ceva */}
// file2.c:
extern int a;
int f();
void g(){a = f();}

'a' si f() utilizati in file2.c sint cele definite in file1.c. Cuvintul

cheie
extern indica faptul ca declaratia lui a in file2.c este (chiar) o
declaratie
si nu o definitie. Daca 'a' ar fi fost initializata, extern ar fi fost
pur si
simplu ignorata deoarece o declaratie cu initializator este totdeauna o
definitie. Un obiect trebuie sa fie definit exact odata intr-un
program. Poate
fi declarat de mai multe ori, dar tipul trebuie sa coincida exact. De
exemplu:
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
Exista trei erori: 'a' este definit de doua ori (int a: este o
definitie
insemnind int a = 0); 'b' este declarat de doua ori cu diferite tipuri;

'c'
este declarat de doua ori dar nu este definit. Aceste tipuri de erori
(erori
de linkare) nu pot fi detectate cu un compilator care analizeaza odata

numai
un fisier. Ele sint, totusi, detectate la linkare.
Programul urmator nu este in C++ (chiar daca el este in C):
// file1.c:
int a;
int f(){return a;}

// file2.c:
int a;
int g(){return f();}
Intii, file2.c nu este C++ deoarece f() nu a fost declarat, asa ca,
compilarea
va esua. In al doilea rind programul nu se va putea linka deoarece 'a'
este
definit de doua ori.
Un nume poate fi local la un fisier declarindu-l static. De
exemplu:

// file1.c
static int a = 6;
static int f(){/*.......*/}

// file2.c
static int a = 7;
static int f(){/*.......*/}

Intrucit fiecare 'a' si f() este declarat static, programul
rezultat este
corect. Fiecare fisier are pe 'a' si f() propriu. Cind variabilele si
functiile sint declarate static explicit, un fragment de program este
mai usor
de inteles (nu trebuie sa ne uitam in alta parte). Utilizind static
pentru
functii putem avea, de asemenea, un efect benefic asupra cantitatii de
functii
utilizate si dind compilatorului informatii care pot fi utilizate in
ideea
realizarii unor optimizari.
Consideram aceste doua fisiere:

// file1.c
const a = 7;
inline int f(){/*.......*/}
struct s{int a, b;};

// file2.c
const a = 7;
inline int f(){/*........*/}
struct s{int a, b;};

Daca se aplica regula a "exact unei definitii" la constante,
functii
inline si definitii de tip in acelasi mod in care se aplica la functii
si
variabile, file1.c si file2.c nu pot fi parte ale aceluiasi program
C++. Dar
daca este asa, cum pot doua fisiere sa utilizeze aceleasi tipuri si
constante?
Raspunsul scurt este ca tipurile, constantele, etc. pot fi definite de
atitea
ori de cit este de necesar cu conditia ca ele sa fie definite identic.
Raspunsul complet este intr-o anumita masura mai complicat (asa cum se
explica
in sectiunea urmatoare).

4.3. Fisiere antet
-------------

Tipurile in toate declaratiile aceluiasi obiect trebuie sa fie
consistente. Un mod de a atinge acest lucru ar fi de a furniza
facilitatile de
verificare de tip linkerului, dar deoarece multe linkere au fost
proiectate in
1950 ele nu pot fi schimbate din motive practice. Este usor a schimba
un
linker, dar facind aceasta si scriind un program care depinde de
imbunatatirile facute, cum mai poate fi acest program transferat
portabil pe
alte calculatoare ?
O alta conceptie este de a asigura ca,codul supus compilarii sa
fie sau
consistent sau sa contina chei care sa permita compilatorului sa
detecteze
inconvenientele. O metoda imperfecta dar simpla de a atinge consistenta

pentru
declaratii in diferite fisiere este de a include fisiere antet, care
sa
contina informatii de interfata din fisierele sursa care contin cod
executabil
si/sau definitii de date.
Mecanismul #incse poate folosi comanda:

CC -E file.c

pentru a prelucra fisierul file.c in acelasi mod ca si cind CC ar fi
inainte
de a incepe compilarea propriu-zisa. Pentru a include fisiere standard,

se
utilizeaza parantezele unghiulare in locul ghilimelelor. De exemplu:
#include <stream.h> // din directorul include standard
#include "myheader.h" // din directorul curent
Avantajul lui "<", ">" este faptul ca numele real al directorului
standard pentru include nu este construit in program.
Un spatiu este semnificativ intr-o directiva include:

#include < stream.h > // nu va gasi stream.h

Ar fi extravagant sa se recompileze un fisier de fiecare data cind

este
inclus undeva, dar timpul necesar pentru a compila un astfel de fisier
de
obicei nu difera mult de timpul necesar pentru a citi o anumita forma
precompilata a lui. Motivul este ca textul programului este o
reprezentare cit
se poate de compacta a programului si ca fisierele incluse, de obicei,
contin
numai declaratii si nu un cod care trebuie sa fie analizat extensiv de
catre
compilator.
Regula urmatoare despre ce poate si ce nu poate fi plasat intr-un
fisier
antet nu este o cerinta a limbajului, ci pur si simplu o sugestie
despre un
mod rezonabil de a utiliza mecanismul #include.
Un fisier antet poate contine:

-----------------------------------------------------------------
|Definitii de tip struct point{int x, y;}; |
|Declaratii de functii extern int strlen{const char*}; |
|Definitii de functii inline inline char get(){return *p++;}; |
|Declaratii de date extern int a; |
|Definitii de constante const float pi = 3.141593; |
|Enumerari enum bool {false, true}; |
|Directive #include #include <signal.h> |
|Macro definitii #define Case break; case |
|Comentarii /* check for end of file */ |
-----------------------------------------------------------------

Dar niciodata nu contine:
-----------------------------------------------------------------
|Definitii de functii ordinare char get(){return *p++;} |
|Definitii de date int a; |
|Definitii de agregate constante const tbl[] = {/*...*/}; |
-----------------------------------------------------------------

In sistemul UNIX, fisierele antet sint cu extensia convenabila .h.
Fisierele care contin definitii de functii si date vor avea extensia
.c. De
aceea ele sint frecvent referite ca "fisiere.h" si respectiv
"fisiere.c".
Macrourile se descriu in &4.7. Sa observam ca macrourile sint pe
departe mai
putin utile in C++ decit in C, deoarece C++ are constructia const in
limbaj
pentru a defini constante inline.
Motivul de a admite definirea de constante simple si nu si a
agregatelor
constante in fisierele.h este pragmatic. In principiu exista o singura
problema in admiterea copiilor definitiilor de variabile (chiar si
definitiile
functiilor pot fi copiate). Cu toate acestea, este foarte dificil
pentru un
linker vechi sa verifice identitatea constantelor netriviale si sa
elimine
duplicatele nenecesare. Mai mult decit atit, cazurile simple sint pe
departe
mai frecvente si de aceea mai importante pentru generarea de cod.

4.3.1. Fisier antet unic
-----------------

Cea mai simpla solutie la problema partitionarii unui program in
diferite
fisiere este de a pune definitiile de functii si date intr-un numar
potrivit
de fisiere sursa si de a declara tipurile necesare pentru a comunica,
intr-un
singur fisier antet care este inclus de toate celelalte fisiere. Pentru
programul calculator putem folosi fisiere.c : lex.c, sgn.c, table.c,
main.c si
un fisier antet dc.h, care contine declaratiile fiecarui nume utilizat
in mai
mult decit un fisier.c:
//dc.h declaratii comune pentru programul calculator
#include <stream.h>
enum token_value
{
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
};
extern int no_of_errors;
extern double error(char* s);
extern token_value get_token();
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern double expr();
extern double term();
extern double prim();
struct name{
char* string;
name* next;
double value;
};
extern name* look(char* p, int ins = 0);
inline name* insert(char* s){return look(s, 1);}
Codul real al lui lex.c va arata astfel:

//lex.c : analiza de intrare si analiza lexicala
#include "dc.h"
#include <ctype.h>
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }

Sa observam ca, utilizind fisierele antet in acest fel se asigura
ca
fiecare declaratie a unui obiect definit de utilizator intr-un fisier
antet va
fi intr-un anumit punct inclus fisierul in care el este definit. De
exemplu,
cind compilam lex.c, compilatorul va intilni:

extern token_value get_token();
// ...
token_value get_token() { /* ... */ }
Aceasta asigura ca, compilatorul va detecta orice inconsis- tenta
in
tipurile specificate pentru un nume. De exemplu, daca get_token() a
fost
declarat sa returneze o valoare de tip token_value, dar este definit sa
returneze un int, atunci compilarea lui lex.c va esua, cu eroare de
neconcordanta de tip.
Fisierul sgn.c va arata astfel:

//sgn.c : analiza sintactica si evolutiva
#include "dc.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }

Fisierul table.c va arata astfel :

//table.c : tabela de simboluri si lookup
#include "dc.h"
extern char* strcmp(const char*, const char*);
extern char* strcpy(char*, const char*);
extern int strlen(const char*);
const TBLSZ = 23;
name table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }

Sa observam ca table.c declara el insusi functiile standard de
manipulare
a sirurilor, asa ca nu exista modificari de consistenta asupra acestor
declaratii. Este aproape totdeauna mai bine sa se includa un fisier
antet
decit sa se declare un nume extern intr-un fisier.c. Aceasta ar putea
implica
sa se includa "prea mult", dar aceasta nu afecteaza serios timpul
necesar
pentru compilare si de obicei va economisi timp pentru programator. Ca
un
exemplu al acestui fapt sa observam cum se redeclara strlen() din nou
in
main.c (de mai jos). Aceasta este o sursa potentiala de erori intrucit
compilatorul nu poate verifica consistenta celor doua declaratii.
Evident,
aceasta problema s-ar putea elimina daca fiecare declaratie externa
s-ar plasa
in dc.h.
Aceasta neglijenta a fost lasata in program din cauza ca este foarte
frecventa
in programele C si conduce la erori care insa nu sint greu de depistat.

In final, fisierul main.c va arata astfel:

//main.c: initializare ciclu principal si tratarea erorilor
#include "dc.h"
int no_of_errors;
double error(char* s) { /* ... */ }
extern int strlen(const char*);
main(int argc, char* argv[]){//...}

Exista un caz important in care dimensiunea fisierelor antet
devine o
pacoste serioasa. Un set de fisiere antet si o biblioteca pot fi
utilizate
pentru a extinde limbajul cu un set de tipuri generale si specifice
aplicatiei (vezi capitolele 5-8). In astfel de cazuri, nu este iesit
din comun
sa gasim mii de linii ale fisierelor antet la inceputul fiecarui fisier

care
se compileaza. Continutul acelor fisiere este de obicei "inghetat" si
se
schimba foarte rar.
O tehnica pentru a incepe compilarea cu continutul acestor fisiere

antet
poate fi de mare utilitate. Intr-un sens, se poate crea un anumit
limbaj cu un
anumit sens special cu ajutorul compilatorului existent. Nu exista
proceduri
standard pentru a crea un astfel de sistem de compilare.

4.3.2 Fisiere antet multiple
----------------------

Stilul unui singur fisier antet pentru un program partitionat este

mult
mai util cind programul este mic si partile lui nu se intentioneaza sa
se
utilizeze separat. Apoi, nu este o situatie serioasa faptul ca nu este
posibil
sa se determine care declaratii se plaseaza in fisierul antet si pentru

ce
motiv. Comentariile pot fi de ajutor. O alternativa este sa lasam ca
fiecare
parte a unui program sa aiba fisierul antet propriu care defineste
facilitatile pe care le furnizeaza el. Fiecare fisier.c are atunci un
fisier.h
corespunzator si fiecare fisier.c include fisierul.h propriu (care
specifica
ce furnizeaza el) si de asemenea pot fi si alte fisiere.h (care
specifica de
ce are el nevoie).
Considerind aceasta organizare pentru calculator, noi observam ca
error()
este utilizata exact ca fiecare functie din program si ea insasi
utilizeaza
numai <stream.h>. Aceasta este tipic pentru functiile error() si
implica
faptul ca error() ar trebui sa fie separata de main():
//error.h: trateaza erorile
extern int no_errors;
extern double error(char* s);

//error.c
#include <stream.h>
#include "error.h"
int no_of_errors;
double error(char* s) { /* ... */ }

In acest stil de utilizare a fisierelor antet, un fisier.h si un
fisierul.c pot fi vazute ca un modul in care fisierul.h specifica o
interfata
si fisierul.c specifica implementarea.
Tabela de simboluri este independenta de restul, exceptind
utilizarea
functiei error(). Aceasta se poate face acum explicit:

//table.h : declaratiile tabelei de simboluri
struct name{
char* string;
name* next;
double value;
};

extern name* look(char* p, int ins = 0);
inline name* insert(char* s){return look(s, 1);}

//table.c : definitiile tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h"
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }

Sa observam ca declaratiile functiilor de manipulare a sirurilor
sint
incluse in <string.h>. Aceasta elimina o alta sursa potentiala de
erori.

//lex.h: declaratii pentru intrare si analiza lexicala
enum token_value{
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
};
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern token_value get_token();

Aceasta interfata cu analizorul lexical este cit se poate de
incurcata.
Lipsa unui tip propriu de lexic arata necesitatea de a prezenta
utilizatorului
pe get_token() cu bufferele de lexicuri reale number_value si
name_string.

//lex.c : definitiile pentru intrare si analiza lexicala
#include <stream.h>
#include <ctype.h>
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }

Interfata cu analizorul sintactic este curata:

//syn.h : declaratii pentru analiza sintactica si evoluare
#include "error.h"
#include "lex.h"
#include "syn.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }


Programul principal este pe cit de uzual pe atit de trivial:
#include <stream.h>
#include <lex.h>
#include <syn.h>
#include <table.h>
#include <string.h>
main(int argc, char* argv[]) { /* ... */ }
Cit de multe fisiere antet sa se utilizeze intr-un program depinde de
multi
factori. Multi dintre acestia au de a face mai mult cu modul de tratare

al
fisierelor pe sistemul dumneavoastra, decit cu C++. De exemplu, daca
editorul
nu are facilitati de a cauta in acelasi timp in mai multe fisiere,
utilizarea
multor fisiere antet devine mai putin atractiva. Analog, daca deschide-

rea si
citirea a 10 fisiere de 50 de linii fiecare este substantial mai
costisitor
decit citirea unui singur fisier de 500 de linii. Noi trebuie sa gidim
de doua
ori inainte de a folosi stilul fisierelor antet multiple pentru un
program
mic. Un sfat: un set de 10 fisiere antet plus fisierele standard antet
este de
obicei ceva normal de gestionat. Totusi, daca partitionati declaratiile

unui
program mare in fisiere antet de dimensiuni logic minime (punind
fiecare
declaratie de structura intr-un fisier propriu, etc.), atunci ve-ti
ajunge
usor la sute de fisiere greu de gestionat.

4.3.3 Ascunderea datelor
------------------
Utilizind fisierele antet, un utilizator poate defini explicit
interfetele pentru a asigura utilizarea consistenta a tipurilor
dintr-un
program. Cu toate acestea, un utilizator poate ocoli interfata
furnizata
printr-un fisier antet inserind declaratiile externe in fisierele.c.
Sa observam ca stilul urmator de legatura nu este recomandat:
//file1.c : "extern" nu se utilizeaza
int a = 7;
const c = 8;
void f(long) { /* ... */ }
//file2.c : "extern" in fisierul.c
extern int a;
extern const c;
extern f(int);
int g(){ return f(a+c); }
Intrucit declaratiile extern din file2.c nu sint incluse cu definitiile

din
file1.c compilatorul nu poate verifica consistenta acestui program. In
consecinta, daca incarcatorul nu este mai destept decit de obicei, cele

doua
erori din acest program va trebui sa le gaseasca programatorul.
Un utilizator poate proteja un fisier impotriva unei astfel de
legaturi
indisciplinate declarind ca static acele nume care nu se intentioneaza
sa se
utilizeze global. Astfel, ele au ca dome- niu fisierul respectiv si
sint
interzise pentru alte parti din program. De exemplu:
//table.c : definitia tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h"
const TBLSZ = 23;
static name* table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }
Aceasta va asigura ca toate accesele la table sa se faca prin look().
Nu este
necesar sa se "ascunda" constanta TBLSZ.


4.4 Fisiere si Module
-----------------

In sectiunea precedenta fisierele.c si .h definesc impreuna o
parte a
programului. Fisierul.h este interfata utilizata de alte parti ale
programului; fisierul.c specifica implementarea.
O astfel de entitate este numita, adesea, modul. Numai numele de
care are
nevoie sa le cunoasca utilizatorul se fac disponibile iar restul sint
ascunse.
Aceasta proprietate se numeste adesea ascunderea datelor, chiar daca
data este
numai unul din lucrurile ce se pot ascunde. Acest tip de modul
furnizeaza o
flexibilitate mare. De exemplu, o implementare poate consta din unul
sau mai
multe fisiere.c si diferite interfete ce pot fi furnizate sub forma de
fisiere.h. Informatia pe care un utilizator nu este necesar sa o
cunoasca este
ascunsa in fisierul.c. Daca se considera ca utilizatorul nu trebuie sa
stie
exact ce contine fisierul.c, atunci el nu trebuie sa fie disponibil in
sursa.
Fisierele de tip .obj sint suficiente.
Este uneori o problema ca aceasta flexibilitate sa fie atinsa fara

o
structura formala. Limbajul insusi nu recunoaste un astfel de modul ca
o
entitate si nu exista nici o cale ca, compilatorul sa faca distinctie
intre
fisierele.h care definesc nume ce sa fie utilizate de alte module
(exportate)
de fisierele.h folosite pentru a declara nume din alte module
(importate).
Alta data, poate fi o problema ca un modul sa defineasca un set de
obiecte si nu un nou tip. De exemplu, modulul table defineste o tabela;

daca
noi dorim doua tabele, nu exista un mod trivial de a furniza celalalt
tabel
utilizind aceasta idee de module. Capitolul 5 prezinta o solutie a
acestei
probleme.
Fiecare obiect alocat static este implicit initializat cu zero,
iar alte
valori (constante) pot fi specificate de programator. Aceasta este doar

o
forma primitiva de initializare. Din fericire, utilizind clasele, se
poate
specifica un cod care sa fie executat pentru initializare inainte de a
face
orice utilizare a modulului si de asemenea se poate executa cod pentru
anulare
(curatire) dupa ultima utilizare a modulului. (vezi &5.5.2).

4.5 Cum se construieste o biblioteca
--------------------------------

Fraze de genul "pune in biblioteca" si "gaseste intr-o anumita
biblioteca" se utilizeaza des (in aceasta carte si in alta parte), dar
ce
inseamna acest lucru pentru un program C++ ?
Din nefericire, raspunsul depinde de sistemul de operare utilizat.
Aceasta sectiune explica cum se face si se utilizeaza o biblioteca in
versiunea 8 a sistemului UNIX. Alte sisteme furni zeaza facilitati
similare.
O biblioteca, in principiu, este o multime de fisiere.o obtinute
prin
compilarea unui set de fisiere.c. De obicei exista unul sau mai multe
fisiere.h care contin declaratii necesare pentru a utiliza acele
fisiere.o. Ca
un exemplu, sa consideram ca avem de furnizat (in mod convenabil) un
set de
functii matematice pentru o multime nespecificata de utilizatori.
Fisierul
antet ar putea arata astfel:
extern double sqrt(double); //subset al lui <math.h>
extern double cos(double);
extern double exp(double);
extern double log(double);
iar definitiile acestor functii vor fi memorate in fisierele sqrt.c,
sin.c,
cos.c, exp.c si respectiv log.c.
O biblioteca numita math.a poate fi facuta astfel:

$cc -c math.c sin.c cos.c exp.c log.c
$ar cr math.a sqrt.o sin.o cos.o exp.o log.o
$ranlib math.a

Fisierele sursa se compileaza intii obtinindu-se fisiere obiect
echivalente. Se utilizeaza apoi comanda ar pentru a face o arhiva
numita
math.a. In final arhiva respectiva este indexata pentru un acces mai
rapid.
Daca sistemul dumneavoastra nu are comanda ranlib, atunci probabil ca
nu aveti
nevoie de ea; sa va uitati in manualul de operare pentru detalii.
Biblioteca
poate fi utilizata astfel:

$cc myprog.c math.a

Acum, care este avantajul utilizarii lui math.a in loc de a
utiliza
direct fisierele.o? De exemplu:
$ myprog.c sqrt.o sin.o cos.o exp.o log.o

Pentru majoritatea programelor, gasirea setului corect de
fisiere.o nu
este un lucru trivial. In exemplul de mai sus, ele au fost toate
incluse, dar
daca functiile din myprog.c apeleaza numai functiile sqrt() si cos()
atunci
pare ca ar fi suficient:
$cc myprog.c sqrt.o cos.o

Acest lucru nu este tocmai asa deoarece cos.c utilizeaza sin.c.
Linkerul apelat de comanda cc ca sa foloseasca un fisier.a (in
acest caz
math.a) stie sa extraga numai fisierele.o necesare, din multimea care a

fost
utilizata pentru a crea fisierul.a.
Cu alte cuvinte, folosind o biblioteca, se pot include multe
definitii
folosind un singur nume (inclusiv definitii de functii si variabile
utilizate
de functii interne pe care utilizatorul nu le-a vazut niciodata) si in
acelasi
timp se asigura numai un numar minim de definitii include.

4.6 Functii
-------
Modul tipic de a face ceva intr-un program C++ este de a apela o
functie
care sa faca lucrul respectiv. Definirea unei functii este o cale de a
specifica cum sa se faca o operatie. O functie nu poate fi apelata daca

ea nu
este declarata.

4.6.1 Declaratii de functii
---------------------
O declaratie de functie da un nume functiei, tipul valorii
returnate
(daca returneaza vreuna) de functie, numarul si tipurile argumentelor
care
trebuie furnizate in apelul unei functii. De exemplu:
extern double sqrt(double);
extern elem* next_elem();
extern char* strcpy(char* to, const char* from);
extern void exit(int);
Semantica transferului de argumente este identica cu semantica
initializarii. Tipurile argumentelor se verifica si se fac conversii
implicite
ale tipurilor argumentelor cind este necesar. De exemplu, dindu-se
declaratiile precedente:
doublesr2 = sqrt(2); va apela corect functia sqrt() cu valoarea
2.0.
O declaratie de functie poate contine nume de argumente. Acest
lucru
poate fi un ajutor pentru cititor, dar compilatorul ignora pur si
simplu
astfel de nume.

4.6.2 Definitii de functii
--------------------

Fiecare functie care este apelata intr-un program trebuie sa fie
definita
undeva (o singura data). O definitie de functie este o declaratie de
functie
in care este prezent corpul functiei. De exemplu:
extern void swap(int*, int*); //o declaratie
void swap(int* p, int* q) //o definitie
{
int t = *p;
*p = *q;
*q = t;
}

O functie poate fi declarata inline pentru a elimina apelul
functiei
suprapunind-o peste el (&1.12), iar argumentele pot fi declarate
register
pentru a furniza un acces mai rapid la ele (&2.3.11). Ambele
caracteristici
pot fi eliminate si ele ar trebui sa fie eliminate ori de cite ori
exista
dubii in legatura cu utilitatea folosirii lor.

4.6.3 Transferul argumentelor
-----------------------

Cind se apeleaza o functie se rezerva memorie pentru argumentele
formale
si fiecare argument formal se initializeaza prin argumentele efective
corespunzatoare. Semantica transferului de parametri este identica cu
semantica initializarii. In parti- cular se verifica tipul unui
argument
efectiv cu tipul argumentului formal corespunzator si se fac toate
conversiile
de tip standard si definite de utilizator. Exista reguli speciale
pentru
transferul vectorilor (&4.6.5), o facilitate pentru transferul
neverificat al
argumentelor (&4.6.8) si o facilitate pentru specificarea argumentelor
implicite (&4.6.6). Consideram:
void f(int val, int& ref)
{
val++;
ref++;
}
Cind se apeleaza f(), val++ mareste o copie locala a primului sau
argument, in
timp ce ref++ incrementeaza cel de al doilea argument efectiv. De
exemplu:
int i = 1;
int j = 1;
f(i, j);
va incrementa pe j dar nu si pe i. Primul argument i este pasat prin
valoare,
iar cel de al doilea prin referinta. Asa cum s-a mentionat in &2.3.10,
folosind functii care modifica argumentele apelate prin referinta se
pot face
programe greu de citit si in general ar trebui eliminate (dar vezi &6.5

si
&8.4). Totusi, este mult mai eficient ca un obiect mare sa fie
transferat prin
refe- rinta in loc sa fie transferat prin valoare. In acest caz,
argumentul ar
putea fi declarat const pentru a indica faptul ca referinta se
utilizeaza
numai din motive de eficienta iar functia apelata nu poate schimba
valoarea
obiectului:
void f(const large& arg)
{ //valoarea lui arg nu poate fi schimbata }

Analog, declarind un argument pointer const, cititorul este
avertizat ca
valoarea obiectului spre care pointeaza acel argument nu se schimba
prin
functia respectiva. De exemplu :
extern int strlen(const char*); //din <string.h>
extern char* strcpy(char* to, const char* from);
extern int strcmp(const char*, const char*);

Importanta acestei practici creste cu dimensiunea programului. Sa
observam ca semantica transferului de argumente este diferita de
semantica
asignarii. Acest lucru este important pentru argumentele const, pentru
argumentele referinta si pentru argumentele unor tipuri definite de
utilizator
(&6.6).

4.6.4 Valoarea returnata
------------------

O valoare poate fi (si trebuie) returnata dintr-o functie care nu
este
declarata void. Valoarea returnata se specifica printr-o instructiune
return.
De exemplu:
int fact(int n)
{
return (n>1) ? n*fact(n-1) : 1;
}

Pot fi mai multe instructiuni return intr-o functie:
int fact(int n)
{
if(n > 1)
return n*fact(n-1);
else
return 1;
}

Ca si semantica transferului de argumente, semantica valorii
returnate de
o functie este identica cu semantica initializarii. O instructiune
return se
considera ca initializeaza o variabila de tipul returnat. Tipul
expresiei
returnate se verifica cu tipul valorii returnate de functie si la
nevoie se
fac toate conversiile de tip standard sau definite de utilizator. De
exemplu:
double f()
{ // ...
return 1; //se converteste spre double(1)
}

De fiecare data cind se apeleaza o functie se creaza o copie noua
pentru
argumentele si variabilele automatice ale ei. Memoria este eliberata la
revenirea din functie, asa ca nu este indicat sa se returneze un
pointer spre
o variabila locala. Continutul locatiei spre care se face pointarea se
va
schimba imprevizibil:
int* f()
{
int local = 1;
// ...
return &local; //nu se face asa ceva
}

Din fericire, compilatorul avertizeaza asupra unor astfel de
valori
returnate. Iata un alt exemplu:
int& f()
{
return 1; //nu se face asa ceva
}

4.6.5 Argumente vector
----------------

Daca se utilizeaza un vector ca un argument de functie, se
transfera un
pointer spre primul sau element. De exemplu:
int strlen(const char*);
void f()
{
char v[] = "a vector";
strlen(v);
strlen("Nicholas");
}

Cu alte cuvinte, un argument de tip T[] va fi convertit spre T*
cind este
transferat. Rezulta ca o asignare la un element al argumentului vector
schimba
valoarea elementului argumentului respectiv. Cu alte cuvinte, vectorii
difera
de alte tipuri prin aceea ca vectorul nu este pasat prin valoare (si
nici nu
poate fi pasat prin valoare). Dimensiunea unui vector nu este
disponibila in
functia apelata. Aceasta poate fi o pacoste, dar exista dife- rite
moduri de
tratare a acestei probleme. Sirurile se termina prin zero, asa ca
dimensiunea
lor se poate calcula usor. Pentru alte tipuri de vectori se poate
transfera un
al doilea argument care contine dimensiunea sau un tip care contine un
pointer
si un indicator de lungime in locul vectorului (&11.11). De exemplu:

void compute1(int* vec_ptr, int vec_size); //un mod
struct vec{ //un alt mod
int* ptr;
int size;
};
void compute2(vec v);

Tablourile multidimensionale sint mai ciudate, dar adesea pot fi
utilizati vectori de pointeri in locul lor si nu au nevoie de o tratare
speciala. De exemplu:
char* day[] = {"mon","tue","wed","thu","fri","sat","sun"};
Cu toate acestea consideram definirea unei functii care
manipuleaza o
matrice bidimensionala. Daca dimensiunile sint cunoscute la compilare,
nu
exista nici o problema:

void print_m34(int m[3][4])
{
for(int i=0; i<3; i++)
{
for(int j=0; j<4; j++)
cout << " " << m[i][j];
cout << "\n";
}
}

Cazul dificil apare cind trebuie pasate ambele dimensiuni.
"Solutia
evidenta" pur si simplu nu functioneaza:
void print_mij(int m[][], int dim1, int dim2) //eroare
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << m[i][j]; //surpriza
cout << "\n";
}
}

In primul rind, argumentul m[][] este ilegal deoarece trebuie sa
fie
cunoscuta dimensiunea a doua a tabloului pentru a gasi locatia unui
element.
In al doilea rind, expresia m[i][j] este corect interpretata ca
*(*(m+i)+j),
dar aceasta este impro- babil ca este ce a dorit programatorul. O
solutie
corecta este:
void print_mij(int** m, int dim1, int dim2)
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << ((int*)m)[i*dim2+j]; //obscur
cout << "\n";
}
}

Expresia utilizata pentru a face acces la elementele tabloului
este
echivalenta cu cea generata de compilator cind cunoaste ultima
dimensiune. Se
poate introduce o variabila auxiliara pentru a face codul mai putin
obscur:
int* v = (int*)m;
v[i*dim2+j];


4.6.6 Argumente implicite
-------------------

O functie necesita adesea mai multe argumente in general, decit
este
nevoie in cazul cel mai simplu sau in cazul cel mai frecvent. De
exemplu,
biblioteca stream are o functie hex() care produce un sir ce contine
reprezentarea hexazecimala a unui intreg. Un al doilea intreg se
foloseste
pentru a specifica numarul de caractere disponibile pentru
reprezentarea
primului argument. Daca numarul de caractere este prea mic pentru a
reprezenta
intregul, apare trunchierea; daca este prea mare, sirul este completat
cu
spatii. Adesea, programatorul nu se intereseaza despre numarul de
caractere
necesare pentru a reprezenta intregul atita timp cit exista spatiu
suficient,
asa ca argumentul al doilea este 0 pentru a indica faptul ca la
conversie sa
se utilizeze "exact atitea caractere cite sint necesare". Pentru a
elimina
apelurile de forma hex(i, 0), functia se declara astfel:

extern char* hex(long, int = 0);

Initializarea pentru cel de al doilea parametru inseamna ca acesta

este
un parametru implicit. Adica, daca numai un argument este prezent
intr-un
apel, cel de al doilea este utilizat impli- cit. De exemplu:

cout << "**" << hex(31) << hex(32, 3) << "**";

se interpreteaza astfel:
cout << "**" << hex(31, 0) << hex(32, 3) << "**";

si va imprima:
**1f 20**

Un argument implicit se verifica din punct de vedere al tipului in
momentul declararii functiei si este evaluat in momentul apelului. Este
posibil sa se furnizeze argumente implicite numai pentru argumente din
ultimele pozitii, asa ca:
int f(int, int = 0, char* = 0); //ok
int g(int = 0, int = 0, char*); //error
int h(int = 0, int, char* = 0); //error

Sa observam ca in acest caz spatiul dintre * si = este
semnificativ (*=
este operatorul de asignare):
int nasty(char *= 0); //syntax error


4.6.7 Nume de functii supraincarcate
------------------------------

Adesea este o idee buna de a da la diferite functii nume diferite,

dar
cind niste functii fac acelasi lucru asupra obiectelor de tipuri
diferite,
poate fi mai convenabil sa le dam acelasi nume. Utilizarea aceluiasi
nume
pentru operatii diferite pentru tipuri diferite se numeste
supraincarcare.
Tehnica este deja utilizata pentru operatii de baza in C++; exista un
singur
nume pentru adunare (+), dar el poate fi utilizat pentru a aduna valori

de
tipuri intregi, in flotant si pointeri. Aceasta idee se extinde simplu
pentru
a trata operatii definite de programator, adica functii. Pentru a
proteja
programatorul de reutilizarea accidentala a unui nume, un nume poate fi
utilizat pentru mai multe functii numai daca este declarat la inceput
ca fiind
supraincarcat. De exemplu:
overload print;
void print(int);
void print(char*);
La compilare singurul lucru pe care functiile il au in comun este
numele.
Probabil ca intr-un anumit sens functiile sint similare, dar limbajul

nu are
restrictii asupra lor. Astfel numele supraincarcat al functiilor sint
in
primul rind o conventie de notatie. Aceasta conventie este
semnificativa
pentru functii cu nume conventionale, cum ar fi sqrt, print si open.
Cind un
nume este semantic semnificativ, cum ar fi operatorii +, * si << (&6.2)

si in
cazul constructorilor (&5.2.4 si &6.3.1), aceasta facilitate devine
esentiala.
Cind este apelata o functie f() supraincarcata, compilatorul trebuie sa

stie
care functie este apelata dintre cele cu numele f. Aceasta se face prin

compa-
rarea tipurilor argumentelor efective cu tipurile argumentelor formale
a
tuturor functiilor numite f. Gasirea functiei care sa fie apelata se
face in
trei pasi separati:

[1] Cauta o corespondenta exacta si daca exista se utilizea-
za functia respectiva;
[2] Cauta o corespondenta utilizind conversii predefinite si
utilizeaze informatie nu se aplica, raminind int spre long,
int spre double,
zero spre long, zero spre double si conversia de pointeri; zero spre
pointer,
pointer spre void* si pointer spre clasa derivata pentru a pointa spre
baza
clasei (&7.2.4). Iata un exemplu in care este necesara conversia:
overload print(double), print(long);
void f(int a){print(a);}

Aici a poate fi imprimat sau ca double sau ca long. Ambiguitatea poate
fi
rezolvata utilizind tipul de conversie explicita (sau print(long(a))
sau
print(double(a))).
Dindu-se aceste reguli, se poate asigura ca cel mai simplu
algoritm
(functie) va fi utilizat, cind eficienta sau precizia calcului difera
semnificativ pentru tipurile implicite. De exemplu:
overload pow;
int pow(int, int);
double pow(double, double); //din <math.h>
complex pow(double, complex); //din <complex.h>
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);

Procesul de gasire a corespondentei ignora unsigned si const.

4.6.8 Numar nespecificat de argumente
-------------------------------

Pentru anumite functii nu este posibil sa se specifice numarul si
tipul
tuturor argumentelor asteptate intr-un apel. O astfel de functie se
declara
terminind lista argumentelor din declaratie prin trei puncte (...) care
inseamna ca " pot fi mai multe argumente". De exemplu:

int printf(char* ...);

Aceasta specifica faptul ca un apel a lui printf trebuie sa aiba
cel
putin un argument de tip char* si poate sa aiba sau nu si altele. De
exemplu:

printf("Hello, word\n");
printf("My name is %s %s\n", first_name, second_name);
printf("%d + %d = %d\n", 2, 3, 5);

O astfel de functie trebuie sa se refere la o informatie care nu
este
disponibila compilatorului cind se interpreteaza lista de argumente. In

cazul
functiei printf(), primul argument este un sir de format care contine o
succesiune de caractere speciale care permite ca printf() sa trateze
corect
celelalte argumente: %s inseamna "se asteapta un argument de tip char*"

iar %d
inseamna "asteapta un argument int". Cu toate acestea, compilatorul nu
stie
aceasta, asa ca el nu se poate asigura ca argumentele asteptate sa
existe in
realitate sau ca un argument este un tip propriu. De exemplu:

printf("My name is %s %s\n", 2);

se va compila si in cel mai bun caz se va scrie la executie ceva
straniu.
Evident daca un argument nu a fost declarat, compilatorul nu are
informatia necesara pentru a face verificarea standard de tip si de a
face
eventual o conversie de tip. In acest caz, char sau short se transfera
ca int,
iar float ca double. Aceasta nu este in mod necesar ceea ce a vrut
utilizatorul.
Utilizarea la extrema a celor trei puncte conduce la
imposibilitatea de a
verifica argumentele, lasind programatorului deschisa problema aceasta.

Un
program bine proiectat necesita cel putin citeva functii pentru care
tipurile
argumentelor nu sint specificate complet. Functiile supraincarcate si
functiile care utilizeaza argumente implicite pot fi utilizate avind
grija ca
verificarea tipului sa se faca ori de cite ori se utilizeaza argumente
de tip
nespecificat. Numai cind atit numarul de argu mente cit si tipul
argumentelor
variaza este necesar sa se foloseasca trei puncte. Cea mai frecventa
utilizare
a celor trei puncte este de a specifica o interfata cu functiile de
biblioteca
ale lui C care sint definite fara a fi disponibile alternativele
posibile:

extern int fprintf(FILE*, char* ...); din <stdin.h>
extern int execl(char* ...); din <system.h>
extern int abort(...); din <libc.h>

Un set de macrouri standard disponibile pentru a avea acces la
argumente
nespecificate in astfel de functii pot fi gasite in <stdargs.h>. Sa
consideram
scrierea unei functii eroare care are un argument intreg ce indica
nivelul de
eroare, urmat de un numar arbitrar de siruri. Ideea este de a compune
mesajul
de eroare pasind fiecare cuvint ca un argument de tip sir separat:

void error(int ...);
main(int argc, char* argv[])
{switch(argc)
{case 1: error(0, argv[0], 0);
break;
case 2: error(0, argv[0], argv[1], 0);
break;
default: error(1, argv[0], "with", dec(argc-1),
"arguments", 0);
}
}

Functia eroare ar putea fi definita astfel:
#include <stdargs.h>
void error(int n ...)
// "n" urmat de o lista de char* s terminata prin zero
{
va_list ap;
va_start(ap, n); //arg startup
for(;;)
{
char* p = va_arg(ap, char*);
if(p == 0)
break;
cerr << p << " ";
}
va_end(ap); //curatirea argumentelor
cerr << "\n";
if(n)
exit(n);
}
Intii se defineste va_list care este initializata prin apelul lui
va_start().
Macroul va_start ia numele lui va_list si numele ultimului argument
formal ca
argumente. Macroul va_arg() se utilizeaza pentru a alege argumentul
nedenumit in ordine. La fiecare apel programatorul trebuie sa furnizeze

un
tip; va_arg() presupune ca argumentul efectiv de acel tip a fost pasat,

dar de
obicei nu exista o cale de a asigura aceasta. Inainte de a reveni
dintr-o
functie in care s-a utilizat va_start(), trebuie apelata va_end().
Motivul
este ca va_start() poate modifica stiva in asa fel ca revenirea nu se
va mai
realiza cu succes: va_end() reface stiva la forma necesara revenirii
corecte.

4.6.9 Pointer spre functie
--------------------

Exista numai doua lucruri care pot fi facute cu o functie: apelul
ei si
sa se ia adresa ei. Pointerul obtinut functiei poate fi apoi utilizat
pentru a
apela functia. De exemplu:
void error(char* p){/*...*/}
void (*efct)(char*); //pointer spre functie
void f()
{efct = &error; //efct pointeaza spre error
(*efct)("error"); //apelul lui error prin efct
}

Pentru a apela o functie printr-un pointer (de exemplu efct) intii
trebuie sa i se atribuie pointerului adresa functiei res- pective.
Intrucit
operatorul () de apel de functie are prioritate mai mare decit
operatorul *,
nu se poate scrie apelul prin *efct("error") caci aceasta inseamna
*(efct("error")), ceea ce este o eroare de tip. Acelasi lucru se aplica

la
sintaxa declaratiei (vezi de asemenea &7.3.4).
Sa observam ca pointerii spre functii au tipurile argumentelor
declarate
ca si functiile insasi. In asignarea de pointeri, tipul functiei
trebuie sa
corespunda exact. De exemplu:

void (*pf)(char*); //pointer spre void(char*);
void f1(char*); //void(char*);
int f2(char*); //int(char*);
void f3(int*); //void(int*);

void f()
{
pf = &f1; //ok
pf = &f2; //eroare: tipul valorii returnate
// este eronat
pf = &f3; //eroare: argument de tip eronat
(*pf)("asdf"); //ok
(*pf)(1); //eroare: tip de argument eronat
int i = (*pf)("qwer"); //eroare: void se asigneaza la int
}

Regulile pentru pasarea argumentelor sint aceleasi atit pentru
apelurile
directe la o functie cit si pentru apelurile la o functie printr-un
parametru.

Adesea este convenabil sa se defineasca un nume pentru tipul unui
pointer
spre o functie pentru a elimina utilizarea tot timpul a unei sintaxe
neevidente. De exemplu:

typedef int (*SIG_TYP)(); //din <signal.h>
typedef void (*SIG_ARG_TYP)();
SIG_TYP signal(int, SIG_ARG_TYP);

Adesea este util un vector de pointeri spre functii. De exemplu,
sistemul
de meniuri pentru editorul bazat pe "mouse" se implementeaza utilizind
vectori
de pointeri spre functii ce reprezinta operatii. Sistemul nu poate fi
descris
aici in detaliu dar ideea generala este aceasta:
typedef void (*PF)();
PF edit_ops[]={cut, paste, snarf, search}; //op. de editare
PF file_ops[]={open, reshape, close, write};//tratarea fis.

Definirea si initializarea pointerilor care definesc acti- unile
selectate dintr-un meniu asociat cu butoanele mouse-ului:
PF* button2 = edit_ops;
PF* button3 = file_ops;

Intr-o implementare completa, este necesara mai multa informatie
pentru a
defini fiecare element. De exemplu, un sir care specifica textul de
afisat
trebuie sa fie pastrat undeva. Pe masura ce se utilizeaza sistemul,
sensul
butoanelor mouse se schimba frecvent cu contextul. Astfel de schimbari
se
realizeaza (partial) schimbind valoarea pointerilor de butoane. Cind un
utilizator selecteaza un meniu, cum ar fi elementul 3 pentru butonul 2,

se
executa operatia asociata: (*button2[3])();
Un mod de a cistiga o apreciere a puterii expresive a poin-
terilor spre
functii este incercarea de a scrie cod fara ele. Un meniu poate fi
modificat
la executie inserind functii noi intr-o tabela operator. Este de
asemenea usor
sa se construiasca meniuri noi la executie.
Pointerii spre functii pot fi utilizati sa furnizeze rutine care
pot fi
aplicate la obiecte de tipuri diferite:
typedef int (*CFT)(char*, char*);
int sort(char* base, unsigned n, int sz, CFT cmp)
/* Sorteaza cele n elemente ale vectorului "base" in ordine
crescatoare utilizind functia de comparare spre care
pointeaza "cmp". Elementele sint de dimensiune "sz".
Algoritm foarte ineficient: bubble sort.
*/
{
for(int i = 0; i < n-1; i++)
for(int j = n-1; i < j; j--)
{
char* pj = base+j*sz; //b[j]
char* pj1 = pj-sz; //b[j-1]
if((*cmp)(pj, pj1) < 0) //swap b[j] and b[j-1]
for(int k = 0; k < sz; k++)
{
char temp = pj[k];
pj[k] = pj1[k];
pj1[k] = temp;
}
}
}
Rutina de sortare nu cunoaste tipul obiectelor pe care le
sorteaza, ci
numai numarul de elemente (dimensiunea vectorului), dimensiunea
fiecarui
element si functia de apelat pentru a face compararea. Tipul lui sort()

ales
este acelasi cu tipul rutinei qsort() din biblioteca C standard.
Programele
reale utilizeaza qsort(). Intrucit sort() nu returneaza o valoare, ar
trebui
declarata cu void, dar tipul void nu a fost introdus in C cind a fost
definit
qsort(). Analog, ar fi mai onest sa se foloseasca void* in loc de char*

ca tip
de argument. O astfel de functie sort() ar putea fi utilizata pentru a
sorta o
tabela de forma:
struct user{char* name;
char* id;
int dept;
};
typedef user* Puser;
user heads[]={"McIlroy M.D.", "doug", 11271,
"Aho A.V.", "ava", 11272,
"Weinberger P.J.", "pjw", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275,
"Kernighan B.W.", "bwk", 11276
};

void print_id(Puser v, int n)
{for(int i = 0; i < n; i++)
cout << v[i].name << "\t" << v[i].id << "\t"
<< v[i].dept << "\n";
}
Pentru a putea face sortarea, intii trebuie sa definim functia de
comparare potrivita. O functie de comparare trebuie sa returneze o
valoare
negativa daca primul ei argument este mai mic decit al doilea, zero
daca sint
egale si un numar pozitiv altfel:
int cmp1(char* p, char* q) //se compara sirurile nume
{
return strcmp(Puser(p)->name, Puser(q)->name);
}

int cmp2(char* p, char* q) //se compara numerele dept
{
return Puser(p)->dept - Puser(q)->dept;
}
Programul acesta sorteaza si imprima:
main()
{
sort((char*)heads, 6, sizeof(user), cmp1);
print_id(heads, 6) //in ordine alfabetica
cout << "\n";
sort((char*)heads, 6, sizeof(user), cmp2);
print_id(heads, 6); //in ordinea numerelor de departament
}

Este posibil sa se ia adresa unei functii inline si de asemenea sa

se ia
adresa unei functii supraincarcate (&r8.9).


4.7 Macrouri
--------

Macrourile se definesc in &r11. Ele sint foarte importante in C,
dar
sint pe departe mai putin utilizate in C++. Prima regula despre ele
este: sa
nu fie utilizate daca nu trebuie. S-a observat ca aproape fiecare macro
demonstreaza o fisura fie in limbajul de programare, fie in program.
Daca
doriti sa folositi macrouri va rog sa cititi foarte atent manualul de
referinta pentru implementarea preprocesorului C pe care il folositi.
Un macro simplu se defineste astfel:

#define name restul liniei

Cind name se intilneste ca o unitate lexicala, el este inlocuit prin
restul
liniei. De exemplu:

named = name

va fi expandat prin:

named = restul liniei

Un macro poate fi definit, de asemenea, prin argumente. De
exemplu:

#define mac(a, b) argunent1: a argument2: b

Cind se utilizeaza mac, cele doua siruri de argumente trebuie sa fie
prezente.
Ele vor inlocui pe a si b cind se expandeaza mac(). De exemplu:

expanded = mac(foo bar, yuc yuk)

va fi expandat in:

expanded = argument1: foo bar argument2: yuk yuk

Macrourile manipuleaza siruri si stiu putin despre sintaxa lui C++

si
nimic despre tipurile si regulile de existenta ale lui C++.
Compilatorul
vede numai formele expandate ale unui macro, asa ca o eroare intr-un
macro va
fi propagata cind macroul se expandeaza. Aceasta conduce la mesaje de
eroare
obscure, ele nefiind descoperite in definitia macroului. Iata citeva
macrouri
plauzibile:

#define case break;case
#define nl <<"\n"
#define forever for(;;)
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

Iata citeva macrouri complete necesare:

#define PI 3.141593
#define BEGIN {
#define END }

Iata citeva macrouri periculoase:

#define SQUARE(a) a*a
#define INCR_xx (xx)++
#define DISP = 4

Pentru a vedea de ce sint periculoase, sa incercam sa expandam:

int xx = 0; //numarator global
void f()
{
int xx = 0; //variabila locala
xx = SQUARE(xx+2); //xx=xx+2*xx+2
INCR_xx; //incrementeaza localul xx
if(a-DISP == b) //a-= 4==b
{
//....
}
}

Daca noi dorim sa utilizam un macro trebuie sa utilizam operatorul

de
rezolutie a domeniului "::" cind dorim sa facem referinte la nume
globale
(&2.1.1) si sa includem in paranteze aparitiile numelor argumente ale
macrourilor (vezi MIN de mai sus).
Sa se observe diferenta efectelor de expandare a acestor doua
macrouri:

#define m1(a) something(a) // comentariu serios
#define m2(a) something(a) /* comentariu serios */


De exemplu:

int a = m1(1) + 2;
int b = m2(1) + 2;

se vor expanda in

int a = something(1) // comentariu serios + 2 ;
int b = something(1) /* comentariu serios */ + 2;

Utilizind macrouri, noi putem proiecta limbajul nostru propriu; el

va fi
probabil mult mai incomprehensibil decit altele. Mai mult decit atit,
preprocesorul C este un macroprocesor foarte simplu. Cind noi incercam
sa
facem ceva netrivial, noi probabil gasim sau ca este imposibil sau ceva
nejustificat de greu de realizat (dar vezi &7.3.5).

4.8 Exercitii
---------

1. (*1). Sa se scrie declaratii pentru: o functie care are ca
argumente
un pointer spre caractere si referinta la un intreg si nu returneaza
nici o
valoare; un pointer spre o astfel de functie; o functie care are un
astfel de
pointer ca argument; o functie care returneaza un astfel de pointer. Sa

se
scrie definitia unei functii care are un astfel de pointer ca argument
si
returneaza argumentul ei ca valoare. Sa se utilizeze typedef.

2. (*1). Ce semnifica linia de mai jos? La ce ar fi buna ea?
typedef int(rifii&)(int, int);

3. (*1.5). Sa se scrie un program ca "Hello, world" care ia
un nume din linia de comanda si care scrie "Hello, numele respectiv".
Sa se
modifice acest program pentru a lua oricite nume ca argumente si sa se
scrie
Hello la fiecare.

4. (*1.5). Sa se scrie un program care citeste un numar
arbitrar de fisiere a caror nume se dau ca argumente in linia de
comanda si le
scrie unul dupa altul in cout. Acest program se poate numi cat deoarece
concateneaza fisierele respective.

5. (*2). Sa se converteasca un program mic C intr-un program
C++. Sa se modifice fisierele antet pentru a declara toate fun- ctiile
apelate
si sa declare tipul fiecarui argument. Sa se inlo- cuiasca #define prin

enum,
const sau inline unde este posibil. Sa se elimine declaratiile extern
din
fisierele C si sa se converteasca in sintaxa definitiilor de functii
din C++.
Sa se inlocuiasca apelurile lui malloc() si free() cu new si delete. Sa

se
elimine conversiile de tip explicit necesare.

6. (*2). Sa se implementeze sort() (&4.6.9) utilizind un
algoritm de sortare mai eficient.

7. (*2). Sa consideram definitia lui struct tnode din &r8.5.
Sa se scrie functia pentru introducerea unui cuvint nou intr-un arbore
de
tnode noduri. Sa se scrie o functie care listeaza arborele de tnode
noduri. Sa
se scrie o functie care listeaza arborele respectiv in ordine
alfabetica a
cuvintelor pe care le contine. Sa se modifice tnode astfel incit sa
contina
numai un pointer spre un cuvint de lungime arbitrara memorat in memoria

libera
folosind new. Sa se modifice functiile pentru a putea utiliza noua
definitie a
lui tnode.

8. (*2). Sa se scrie un modul care implementeaza o stiva.
Fisierul.h
trebuie sa declare functiile push(), pop() si orice alte functii
potrivite. Un
fisier.c defineste functiile si datele necesare de a fi pastrate pe
stiva.

9. (*2). Sa cunoasteti fisierele antet standard. Sa se lis- teze
fisierele din /usr/include si /usr/include/cc (sau orice alte fisiere

antet
standard pastrate de sistemul d-voastra). Cititi tot ce pare a fi
interesant.
10. (*2). Sa se scrie o functie ce inverseaza un tablou
bidimensional.

11. (*2). Sa se scrie un program care citeste din cin si
scrie caracterele in cout codificat. Codul lui c poate fi c^key[i],
unde key
este un sir pasat ca argument al liniei de comanda. Programul
utilizeaza
caracterele din key intr-o maniera ciclica pina cind au fost citite
toate
caracterele de la intrare. Recodificarea textului cu aceeasi cheie
produce
textul original. Daca nu exista nici o cheie (s-a pasat sirul vid),
atunci nu
se face nici o codificare.

12. (*3). Sa se scrie un program care ajuta la descifrarea
mesajelor
codificate cu metoda descrisa mai sus fara a cunoaste cheia. A se
consulta
David Kahn: The code-breakers, Macmillan, 1967, New York, pp 207-213.

13. (*3). Sa se scrie o functie error care are un format
asemanator cu
printf, continind %s, %c si %d si un numar arbitrar de argumente. Sa nu

se
foloseasca printf(). A se consulta &8.2.4 daca nu se cunoaste sensul
lui %s
etc. Sa se utilizeze <stdargs.h>.

14. (*1). Cum am alege nume pentru tipuri de pointeri spre
functii definite prin typedef?

15. (*2). Analizati niste programe pentru a avea o idee
despre
diversitatea stilurilor numelor utilizate in realitate. Cum se
utilizeaza
literele mari? Cum se utilizeaza sublinierea? Cind se utilizeaza nume
scurte
ca x si i?

16. (*1). Ce este gresit in macrodefinitiile de mai jos?

#define PI = 3.141593;
#define MAX(a, b) a > B ? a : b
#define fac(a) (a) * fac((a) - 1)

17. (*3). Sa se scrie un macroprocesor care defineste si
expandeaza
macrouri simple (asa cum face macroprocesorul C). Sa citeasca din cin
si sa
scrie in cout. La inceput sa nu se incerce sa se trateze macrouri cu
argumente. Calculatorul de birou (&3.1) contine o tabela de simboluri
si un
analizor lexical pe care noi l-am putea modifica.

CAPITOLUL 5
===========


CLASE
=====

Acest capitol descrie facilitatile pentru a defini tipuri noi
pentru care
accesul la date este restrins la un set specific de functii de acces.
Sint
explicitate modurile in care o data structurata poate fi protejata,
initializata, accesata si in final eliminata. Exemplele includ clase
simple
utilizate pentru gestiunea tabelei de simboluri, manipularea stivei,
manipularea multimilor si implementarea unei reuniuni "incapsulate".

5.1 Introducere si privire generala
-------------------------------

Scopul conceptului de clasa C++ este de a furniza programatorului
un
instrument pentru a crea tipuri noi care pot fi folo- site tot atit de
convenabil ca si tipurile predefinite. Ideal, un tip definit de
utilizator ar
trebui sa nu difere de tipurile predefinite in modul in care sint
utilizate,
ci numai in modul in care sint create.
Un tip este reprezentarea concreta a unei idei (concept). De
exemplu,
tipul float din C++ cu operatiile +, -, *, etc., furnizeaza o versiune
restrinsa dar concreta a conceptului matematic de numar real. Motivul
de a
desemna un tip nou este de a furniza o definitie concreta si specifica
a
conceptului care nu are un corespondent direct si evident intre
tipurile
predefinite. De exemplu, cineva poate furniza tipul "trunk_module"
intr-un
program ce se ocupa cu telefoanele sau tipul "list_of_paragraphs"
pentru un
program de procesare de text.
Un program care furnizeaza tipuri care sint strins legate de
conceptele
aplicatiei este de obicei mai usor de inteles si mai usor de modificat
decit
un program care nu face asa ceva. Un set de tipuri definite de
utilizator bine
ales face un program mai concis; el de asemenea permite compilatorului
sa
detecteze utilizari ilegale ale obiectelor care altfel nu ar fi
detectate pina
in momentul in care nu se testeaza efectiv programul.
Ideea fundamentala in definirea unui tip nou este de a separa
detaliile
incidentale ale implementarii (de exemplu, aranjamentul datelor
utilizate
pentru a memora un obiect al tipu-lui) de proprietatile esentiale ale
utilizarii lui corecte (de exemplu, lista completa de functii care pot
avea
acces la date). O astfel de separare poate fi exprimata prin
canalizarea
tuturor utilizarilor datelor structurii si a rutinelor de memorare
interna
printr-o interfata specifica.
Acest capitol consta din 4 parti separate:

&5.2 Clase si Membri. Aceasta sectiune introduce notiunea de
baza: tip
definit de utilizator numita clasa.
Accesul la obiectele unei clase se poate restringe la un set de
functii
declarate ca o parte a clasei; astfel de functii se numesc functii
membru.
Obiectele unei clase pot fi create si initializate prin functii membru
declarate in mod specific pentru acest scop: astfel de functii se
numesc
constructori. O functie membru poate fi declarata pentru a "sterge" un
astfel
de obiect al unei clase cind el este distrus; o astfel de functie se
numeste
destructor.

&5.3 Interfete si Implementari. Aceasta sectiune prezinta doua
exemple
de modul in care pot fi proiectate, implementate si utilizate clasele.

&5.4 Prieteni si Reuniuni. Aceasta sectiune prezinta multe
detalii
suplimentare despre clase. Arata cum se face accesul la partile private

ale
unei clase si cum se poate admite accesul pentru o functie care nu este

membru
al acelei clase. O astfel de functie se numeste prieten. Aceasta
sectiune de
asemenea arata cum se defineste o reuniune distinctiva.

&5.5 Constructori si Destructori. Un obiect poate fi creat ca un
obiect
automatic, static sau in memoria libera. Un obiect, de asemenea, poate
fi un
membru al unui anumit agregat (o clasa sau un vector), care la rindul
lui
poate fi alocat in una din cele 3 moduri indicate mai sus. Utilizarea
constructorilor si destructorilor se explica in detaliu.


5.2 Clase si Membri
---------------

Clasa este un tip definit de utilizator. Aceasta sectiune
introduce
facilitatile de baza pentru a defini o clasa, crearea obiectelor unei
clase,
manipularea acestor obiecte si in final stergerea acestor obiecte dupa
utilizare.

5.2.1 Functii membru
--------------

Sa consideram implementarea conceptului de data utilizind o
structura
pentru a defini reprezentarea unei date si un set de functii pentru
manipularea variabilelor de acest tip:

struct date{ int month, day, year; };
date today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(date*);
Nu exista conexiuni explicite intre functii si tipul datei. O
astfel de
conexiune se poate stabilii declarind functiile ca membri:
struct date{int month, day, year;
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

Functiile declarate in acest fel se numesc functii membru si pot
fi
invocate numai pentru o variabila specifica de tipul corespunzator
utilizind
sintaxa standard pentru accesul la membri unei structuri. De exemplu:
date today;
date my_birthday;
void f()
{my_birthday.set(30, 12, 1950);
today.set(18, 1, 1985);
my_birthday.print();
today.next();
}

Intrucit diferite structuri pot avea functii membru cu ace-
lasi nume, trebuie sa se specifice numele structurii cind se
defineste o functie membru:

void date::next()
{
if(++day > 28)
{
//do the hard part
}
}

Intr-o functie membru, numele membrilor pot fi folosite fara o

referire explicita la un obiect. In acest caz, numele se refera la
acel
membru al obiectului pentru care a fost apelata functia.


5.2.2 Clase
-----

Declaratia lui date din subsectiunea precedenta furnizeaza un set
de
functii pentru manipularea unei variabile de tip date, dar nu specifica

faptul
ca acele functii ar trebui sa fie singurele care sa aiba acces la
obiectele de
tip date. Aceasta restrictie poate fi exprimata utilizind o clasa in
locul
unei structuri:
class date{
int month, day, year;
public:
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

Eticheta public separa corpul clasei in doua parti. Numele din
prima
parte, private, pot fi utilizate numai de functiile membre. Partea a

doua,
public, constituie interfata cu obiectele clasei. O structura (struct)

este
pur si simplu o clasa cu toti membri publici, asa ca functiile membru
se
definesc si se utilizeaza exact ca inainte. De exemplu:
void date::print() //print folosind notatia US
{
cout << month << "/" << day << "/" << year;
}
Cu toate acestea, functiile care nu sint membru nu pot folosi membri
privati
ai clasei date. De exemplu:

void backdate()
{
today.day--; //eroare
}

Exista citeva beneficii in urma restringerii accesului, la o data
structurata, la o lista de functii declarata explicit. Orice eroare
care
face ca date sa aiba o valoare ilegala (de exemplu December 36, 1985)
trebuie sa fie cauzata de codul unei functii membru, asa ca primul
stadiu al
depanarii, localizarea, este rezolvat inainte ca programul sa se
execute.
Acesta este un caz special al observatiei generale ca orice
schimbare
in comportarea tipului date poate, si trebuie sa fie efectuata prin
schimbarea
membrilor lui. Un alt avantaj este ca un utilizator de un astfel de
tip este
necesar numai sa examineze definitia functiilor membru pentru a invata
utilizarea lui.
Protectia datelor private se bazeaza pe restrictia utilizarii
numelor
membru ale clasei. Se poate trece peste aceasta prin manipularea de
adrese si
conversie explicita de tip, dar aceasta evident este un fel de
inselatorie.

5.2.3 Autoreferinta
-------------

Intr-o functie membru, ne putem referi direct la membri unui
obiect
pentru care functia membru este apelata. De exemplu:
class x{
int m;
public:
int readm(){ return m; }
x aa;
x bb;
void f()
{
int a = aa.readm();
int b = bb.readm();
//.......
}
}
In primul apel al membrului readm(), m se refera la aa.m iar in cel de
al
doilea la bb.m.
Un pointer la obiectul pentru care o functie membru este apelata
constituie un membru ascuns pentru functie. Argumentul implicit poate
fi
referit explicit prin this. In orice functie a unei clase x, pointerul
this
este declarat implicit ca:

x* this;

si este initializat ca sa pointeze spre obiectul pentru care functia
membru
este apelata. Intrucit this este un cuvint cheie el nu poate fi
declarat
explicit. Clasa x ar putea fi declarata explicit astfel:
class x{ int m;
public: int readm(){ return this->m; }
};
Utilizarea lui this cind ne referim la membri nu este
necesara; utilizarea majora a lui this este pentru a scrie functii
membru care
manipuleaza direct pointerii. Un exemplu tipic pentru this este o
functie care
insereaza o legatura intr-o lista dublu inlantuita:

class dlink{
dlink* pre; //legatura precedenta
dlink* suc; //legatura urmator
public:
void append(dlink*);
//........
};

void dlink::append(dlink* p)
{
p->suc = suc; //adica p->suc = this->suc
p->pre = this; //utilizarea explicita a lui this
suc->pre = p; //adica, this->suc->pre = p;
suc = p; //adica, this->suc = p
}

dlink* list_head;

void f(dlink* a, dlink* b)
{
//.......
list_head->append(a);
list_head->append(b);
}

Legaturile de aceasta natura generala sint baza pentru cla- sele
lista
descrise in capitolul 7. Pentru a adauga o legatura la o lista, trebuie

puse
la zi obiectele spre care pointeaza this, pre si suc. Ele toate sint de

tip
dlink, asa ca functia membru dlink::append() poate sa faca acces la
ele.
Unitatea de protectie in C++ este clasa, nu un obiect individual al
unei
clase.

5.2.4 Initializare
------------

Utilizarea functiilor de felul set_data() pentru a furniza
initializarea
pentru obiectele clasei nu este eleganta si este inclinata spre erori.
Intrucit nicaieri nu se afirma ca un obiect trebuie initializat, un
programator poate uita sa faca acest lucru sau (adesea cu rezultate
dezastruoase) sa faca acest lucru de doua ori. O conceptie mai buna
este de a
permite programatorului sa declare o functie cu scopul explicit de a
initializa obiecte. Deoarece o astfel de functie construieste valori de

un tip
dat, ea se numeste constructor. Un constructor se recunoaste deoarece
are
acelasi nume ca si clasa insasi. De exemplu:

class date{
//......
date(int, int, int);
};
Cind o clasa are un constructor, toate obiectele acelei clase vor
fi
initializate. Daca constructorul cere argumente, ele pot fi furnizate:
date today = date(23, 6, 1983);
date xmas(25, 12, 0); //forma prescurtata
date my_birthday: //ilegal, lipseste initializarea

Este adesea util sa se furnizeze diferite moduri de initializare a
obiectelor unei clase. Aceasta se poate face furnizind diferiti
constructori.
De exemplu:

class date{
int month, day, year;
public:
//........
date(int, int, int); //zi luna an
date(char*); //date reprezxista un motiv de a
obliga utilizarea cuvintului overload care sa ne protejeze impotriva
unei
reutilizari accidentale a unui nume.
Proliferarea constructorilor in exemplul date este tipica. Cind se
proiecteaza o clasa exista totdeauna tentatia de a furniza "totul"
deoarece se
crede ca este mai usor sa se furnizeze o trasatura chiar in cazul in
care
cineva o vrea sau din cauza ca ea arata frumos si apoi sa se decida ce
este in
realitate necesar. Ultima varianta necesita un timp mai mare de
gindire, dar
de obicei conduce la programe mai mici si mai comprehensibile. Un mod
de a
reduce numarul de functii inrudite este de a utiliza argumentele
implicite. In
date, fiecarui argument i se poate da o valoare implicita care se
interpreteaza: "implicit ia data curenta".
class date{
int month, day, year;
public:
//..........
date(int d=0, int m=0, int y=0);
date(char*); //date reprezentat ca sir
};
date::date(int d, int m, int y)
{day = d ? d : today.day;
month = m ? m : today.month;
year = y ? y : today.year;
//verifica faptul ca date este valida
//..........
}
Cind se utilizeaza o valoare pentru un argument pentru a indica
"ia
valoarea implicita", valoarea aleasa trebuie sa fie in afara setului
posibil
de valori pentru argument. Pentru zi si luna este clar acest lucru, dar
valoarea zero pentru an poate sa nu fie o alegere evidenta. Din
fericire nu
exista anul zero in calendarul european. 1AD(year == 1) vine imediat
dupa
1BC(year == -1), dar aceasta probabil ar fi prea subtil pentru un
program
real.

Un obiect al unei clase fara constructori poate fi initializat
atribuindu-i un alt obiect al acelei clase. Aceasta se poate face, de
asemenea, cind constructorii au fost declarati. De exemplu:
date d = today; //initializare prin asignare

In esenta, exista un constructor implicit ca o copie de biti a
obiectelor
din aceeasi clasa. Daca nu este dorit acest constructor implicit pentru

clasa
X, el poate fi redefinit prin constructorul denumit X(X&) (aceasta se
va
discuta mai departe in &6.6).

5.2.5 Curatire (stergere)
-------------------

Mai frecvent este cazul in care un tip definit de utilizator
are un
constructor pentru a asigura initializarea proprie. Multe tipuri
necesita, de
asemenea, un destructor, care sa asigure stergerea obiectelor de un
tip.
Numele destructorului pentru clasa X este ~X() ("complementul
constructorului"). In particular, multe clase utilizeaza memoria libera

(vezi
&3.2.6) ce se aloca printr-un constructor si se dealoca printr-un
destructor.
De exemplu, iata un tip de stiva conventionala care a fost complet
eliberata
de tratarea erorilor pentru a o prescurta:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){top = s = new char[size=sz];}
~char_stack(){ delete s; }
void push(char c){ *top++ = c; }
char pop(){ return *--top; }
};

Cind char_stack iese in afara domeniului, se apeleaza destructorul:

void f()
{
char_stack s1(100);
char_stack s2(200);
s1.push('a');
s2.push(s1.pop());
char ch = s2.pop();
cout << chr(ch) << "\n";
}

Cind f() este apelata, constructorul char_stack va fi apelat
pentru s1 ca
sa aloce un vector de 100 de caractere si pentru s2 pentru a aloca un
vector
de 200 de caractere; la revenirea din f(), acesti doi vectori vor fi
eliminati.


5.2.6 "Inline"
--------

Cind programam folosind clasele, este foarte frecvent sa utilizam
multe
functii mici. In esenta, o functie este realizata unde un program
structurat,
in mod traditional, ar avea un anumit mod tipic de utilizare a unei
date
structurate; ceea ce a fost o conventie devine un standard recunoscut
prin
compilator. Aceasta poate conduce la ineficiente teribile deoarece
costul
apelului unei functii este inca mai inalt decit citeva referinte la
me-
morie necesare pentru corpul unei functii triviale.
Facilitatile functiilor "in linie" au fost proiectate pentru a
trata
aceasta problema. O functie membru definita (nu numai declarata) in
declaratia
de clasa se considera ca fiind in linie. Aceasta inseamna de exemplu,
ca,
codul generat pentru functiile care utilizeaza char_stack-ul prezentat
mai sus
nu contine nici un apel de functie exceptind cele utilizate pentru a
implementa operatiile de iesire. Cu alte cuvinte, nu exista un cost de
timp
mai mic decit cel luat in seama cind proiectam o clasa; chiar si cele
mai
costisitoare operatii pot fi realizate eficient. Aceasta observatie
invalideaza motivele cele mai frecvent utilizate in favoarea utilizarii
membrilor publici ai datelor. O functie mem- bru poate, de asemenea, sa

fie
declarata inline in afara declaratiei de clasa. De exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char pop();
//......
}

inline char char_stack::pop()
{
return *--top;
}

5.3 Interfete si Implementari
-------------------------

Ce face o clasa buna? Ceva ce are un set mic si bine definit de
operatori. Ceva ce poate fi vazut ca o "cutie neagra" manipulata
exclusiv prin
acel set de operatii. Ceva a carei reprezentare reala ar putea fi
conceputa sa
fie modificata fara a afecta modul de utilizare a acelui set de
operatii.
Containerele de toate felurile furnizeaza exemple evidente:
tabele,
multimi, liste, vectori, dictionare, etc.. O astfel de clasa va avea o
operatie de inserare, care de obicei va avea de asemenea operatii
pentru a
verifica daca un membru specific a fost inserat, poate va avea operatii

pentru
sortarea membrilor, poate va avea operatii pentru examinarea tuturor
membrilor
intr-o anumita ordine si in final ar putea, de asemenea, sa aiba o
operatie
pentru eliminarea unui membru. Clasele container de obicei au
constructori si
destructori.
Ascunderea datelor si o interfata bine definita pot fi de asemenea
obtinute prin conceptul de modul (vezi de exemplu, &4.4: fisiere ca
module).
Cu toate acestea, o clasa este un tip; pentru a o utiliza, trebuie sa
se
creeze obiecte ale clasei respective si se pot crea atit de multe
astfel de
obiecte cite sint necesare. Un modul este el insusi un obiect; pentru
a-l
utiliza, cineva este necesar sa-l initializeze si exista exact un
astfel de
obiect.


5.3.1 Implementari alternative
------------------------

Atita timp cit declaratia partii publice a unei clase si
declaratia
functiilor membru ramin neschimbate, implementarea unei clase poate fi
schimbata fara a afecta utilizatorii ei. Sa consideram o tabela de
simboluri
de felul celei utilizate pentru calculatorul de birou din capitolul 3.
Este o
tabela de nume:

struct name{
char* string;
name* next;
double value;
};

Iata o versiune a clasei tabela:
//file table.h:
class table{
name* tbl;
public:
table(){tbl = 0;}
name* look(char*, int=0);
name* insert(char* s){return look(s, 1);}
};
Aceasta tabela difera de cea definita in capitolul 3 prin aceea ca este

un tip
propriu. Se pot declara mai multe tabele, putem avea un pointer spre o
tabela,
etc.. De exemplu:

#include "table.h"
table globals;
table keywords;
table* locals;
main()
{locals = new table;
//.........
}

Iata o implementare a lui table::look() utilizind o cautare
liniara prin
lista inlantuita de nume din tabela:
#include <string.h>
name* table::look(char* p, int ins)
{for(name* n = tbl; n; n = n->next)
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name;
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->value = 1;
nn->next = tbl;
tbl = nn;
return nn;
}

Acum consideram o inlantuire a clasei utilizind cautarea prin
hashing asa
cum s-a facut in exemplul cu calculatorul de birou. Este insa mai
dificil sa
facem acest lucru din cauza restrictiei ca, codul scris folosind
versiunea de
clasa table de mai jos, sa nu se schimbe.
class table{name** tbl;
int size;
public:
table(int sz=15);
~table();
name* look(char*, int=0);
name* insert(char* s){return look(s, 1);}
};
Structura datelor si constructorul s-au schimbat pentru a reflecta

nevoia
pentru o dimensiune specifica a tabelei cind se utilizeaza hashingul.
Prevazind constructorul cu un argument implicit ne asiguram ca, codul
vechi
care nu a specificat dimen- siunea unei tabele este inca corect.
Argumentele
implicite sint foarte utile in situatii cind vrem sa schimbam o clasa
fara a
afecta codul vechi. Constructorul si destructorul acum gestioneaza
crearea si
stergerea tabelelor de hashing:
table::table(int sz)
{
if(sz < 0)
error("negative table size");
tbl = new name*[size=sz];
for(int i=0; i < sz; i++)
tbl[i] = 0;
}

table::~table()
{for(int i=0; i < size; i++)
{name* nx;
for(name* n=tbl[i]; n; n=nx)
{
nx = n->next;
delete n->string;
delete n;
}
}
delete tbl;
}
O versiune mai simpla si mai clara a lui table::~table() se poate
obtine
declarind un destructor pentru class name. Functia lookup este aproape
identica cu cea utilizata in exemplul cu calculatorul de birou
(&3.1.3):
name* table::look(char* p, int ins)
{
int ii = 0;
char* pp = p;
while(*pp)
ii == ii << 1 ^ *pp++;
if(ii < 0)
ii = -ii;
ii %= size;
for(name* n = tbl[ii]; n; n = n->next)
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name;
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->value = 1;
nn->next = tbl[ii];
tbl[ii] = nn;
return nn;
}
Evident, functiile membru ale unei clase trebuie sa fie recompilate ori

de
cite ori se face o schimbare in declaratia de clasa. Ideal, o astfel de
schimbare nu ar trebui sa afecteze de loc utilizatorii unei clase. Din
nefericire, nu este asa. Pentru a aloca o variabila de clasa,
compilatorul are
nevoie sa cunoasca dimensiunea unui obiect al clasei. Daca dimensiunea
unui
astfel de obiect este schimbata, fisierele care contin utilizari ale
clasei
trebuie sa fie recompilate. Softwarul care determina setul minim de
fisiere ce
necesita sa fie recompilate dupa o schimbare a declaratiei de clasa
poate fi
(si a fost) scris, dar nu este inca utilizat pe scara larga.
Noi ne putem intreba, de ce nu a fost proiectat C++ in asa fel ca
recompilarea utilizatorilor unei clase sa fie necesara dupa o schimbare

in
partea privata? Si de ce trebuie sa fie prezenta partea privata in
declaratia
de clasa? Cu alte cuvinte, intrucit utilizatorii unei clase nu sint
admisi
sa aiba acces la membri privati, de ce declaratiile lor trebuie sa fie
prezente in fisierele antet ale utilizatorului? Raspunsul este
eficienta. Pe
multe sisteme, atit procesul de compilare cit si secventa de operatii
care
implementeaza apelul unei functii sint mai simple cind dimensiunea
obiectelor
automatice (obiecte pe stiva) se cunoaste la compilare.
Aceasta problema ar putea fi eliminata reprezentind fiecare obiect

al
clasei ca un pointer spre obiectul "real". Intrucit toti acesti
pointeri ar
avea aceeasi dimensiune, iar alocarea obiectelor "reale" ar putea fi
definita
intr-un fisier unde este disponibila partea privata, acest fapt ar
putea
rezolva problema. Cu toate acestea, aceasta solutie impune referirea la

o
memorie suplimentara cind se face acces la membri unei clase si mai
rau ar
implica cel putin un apel al alocatorului si dealocatorului de memorie
pentru
fiecare apel de functie cu un obiect automatic al clasei. De asemenea
s-ar
face implementarea unei functii membru inline care sa faca acces la
date
private fezabile. Mai mult decit atit, o astfel de schimbare ar face
imposibila linkarea impreuna a fragmentelor de programe C++ si C
(deoarece un
compilator C ar trata diferit o structura fata de un compilator C++).
Aceasta
este nepotrivit in C++.


5.3.2 O clasa completa
----------------
Programarea fara ascunderea datelor (folosind structuri) necesita
mai
putina bataie de cap decit programarea cu ascunderea de date (utilizind
clase). Se poate defini o structura fara prea mare bataie de cap, dar
cind
definim o clasa noi trebuie sa ne concentram sa furnizam un set complet

de
operatii pentru tipul nou; aceasta este o deplasare importanta in
domeniul
utilizarii. Timpul cheltuit in proiectarea unui nou tip este de obicei
recu-
perat de multe ori in dezvoltarea si testarea unui program. Iata un
exemplu de
tip complet, intset, care furnizeaza conceptul de "multime de intregi".
class intset{int cursize, maxsize;
int* x;
public:
intset(int m, int n); //cel putin m intregi
//in 1..n
~intset();
int member(int t); //este "t" un membru?
void insert(int t); //adauga "t" la multime
void iterate(int& i){i = 0;}
int ok(int& i){return i < cursize;}
int next(int& i){return x[i++];}
};
Pentru a testa aceasta clasa noi putem crea si apoi imprima un set de
intregi
aleatori. Un astfel de set ar putea constitui niste numere de loterie.
Acest
set simplu ar putea fi utilizat pentru a verifica un sir de intregi
punind
in evidenta duplicatele, dar pentru majoritatea aplicatiilor tipul set
ar
trebui sa fie putin mai migalos elaborat. Ca totdeauna sint posibile
erori:
#include <stream.h>
void error(char* s)
{cerr << "set: " << s << "\n";
exit(1);
}
Clasa intset se utilizeaza in functia main() care asteapta doua
argumente
intregi. Primul argument specifica numarul de numere aleatoare de
generat. Cel
de al doilea argument specifica domeniul intregilor aleatori care se
asteapta:
main(int argc, char* argv[])
{
if(argc != 3)
error("No arguments expected");
int count = 0;
int m = atoi(argv[1]); //numarul elementelor multimii
int n = atoi(argv[2]); //in domeniul 1..n
intset s(m, n);
while(cout < m)
{
int t = randint(n);
if(s.member(t) == 0)
{
s.insert(t);
count++;
}
}
print_in_order(&s);
}
Motivul ca argumentul numarator argc sa fie 3 pentru un program care
cere 2
argumente este faptul ca numele programului este totdeauna pasat ca
argv[0].
Functia:

extern int atoi(char*);

este o functie standard de biblioteca pentru covertirea reprezentarii
sub
forma de sir a unui intreg in forma lui interna binara.
Numerele aleatoare se genereaza utilizind functia standard rand():

extern int rand(); //nu este prea aleatoare
int randint(int n) //in domeniul 1..n
{
int r = rand();
if(r < 0)
r = -r;
return 1 + r % n;
}
Detaliile de implementare ale unei clase ar trebui sa fie de un
interes
mai mic pentru un utilizator, dar aici sint in orice caz si functiile
membru.
Constructorul aloca un vector intreg de dimensiune maxima a multimii
specificate, iar destructorul o dealoca:

intset::intset(int m, int n) //cel mult m intregi in 1..n
{
if(m < 1 || n < m)
error("illegal intset size");
cursize = 0;
maxsize = m;
x = new int[maxsize];
}

intset::~intset(){delete x;}

Intregii se insereaza asa ca ei sa fie tinuti in ordine cresca- toare
in
multime:

void intset::insert(int t)
{
if(++cursize > maxsize)
error("too many elements");
int i = cursize-1;
x[i] = t;
while(i > 0 && x[i-1] > x[i])
{
int t = x[i]; //permuta x[i] si x[i-1]
x[i] = x[i-1];
x[i-1] = t;
i--;
}
}

Se foloseste o cautare binara pentru a gasi un membru:

int intset::member(int t) //cautare binara
{
int l = 0;
int n = cursize-1;
while(l <= n)
{
int m = (l+n)/2;
if(t < x[m])
n = m-1;
else
if(t > x[m])
l = m+1;
else
return 1; //gasit
}
return 0; //negasit
}

In final, intrucit reprezentarea unei clase intset este ascunsa
utilizatorului, noi trebuie sa furnizam un set de ope- ratii care
permit
utilizatorului sa itereze prin multime intr-o anumita ordine. O
multime nu
este ordonata intrinsec, asa ca noi nu putem furniza pur si simplu un
mod de
accesare la vector (miine, eu ma pot gindi sa reimplementez intset ca o

lista
inlantuita).
Se furnizeaza trei functii: iterate() pentru a initializa o
iteratie,
ok() pentru a verifica daca exista un membru urmator si next() pentru a

obtine
membrul urmator:

class intset{
//.........
void iterate(int& i){i = 0;}
int ok(int& i){return i < cursize;}
int next(int& i){return x[i++];}
};

Pentru a permite ca aceste trei operatii sa coopereze si sa
reaminteasca
cit de departe a progresat iteratia, utilizatorul trebuie sa furnizeze
un
argument intreg. Intrucit argumentele sint pastrate intr-o lista
sortata,
implementarea lor este triviala. Acum poate fi definita functia
print_in_order:

void print_in_order(intset* set)
{
int var;
set->iterate(var);
while(set->ok(var))
cout << set->next(var) << "\n";
}


O alta varianta de a furniza un iterator se prezinta in &6.8.

5.4 Prieteni si Reuniuni
--------------------
Aceasta sectiune descrie inca citeva facilitati relativ la clase.
Se
prezinta un mod de a acorda acces functiilor membre la membri privati.
Se
descrie cum se pot rezolva conflictele numelor membre, cum se pot
imbrica
declaratiile de clase si cum pot fi eliminate imbricarile nedorite. De
asemenea se discuta cum pot fi obiectele unei clase divizate intre
membri ei
si cum se pot utiliza pointerii spre membri. In final exista un
exemplu care
arata cum se poate proiecta o reuniune discriminatorie.

5.4.1 Prieteni
--------
Presupunem ca noi trebuie sa definim doua clase, vector si
matrix.
Fiecare din ele ascunde reprezentarea ei si furnizeaza un set complet
de
operatii pentru manipularea obiectelor ei. Acum sa definim o functie

care
inmulteste o matrice cu un vector. Pentru simplificare, presupunem ca
un
vector are patru elemente, cu indicii 0..3 si ca o matrice are 4
vectori
indexati cu 0..3. Presupunem de asemenea, ca elementele unui vector
sint
accesate printr-o functie elem() care verifica indexul si ca matrix are

o
functie similara. O conceptie este de a defini o functie globala
multiply() de
forma:
vector multiply(matrix& m, vector& v)
{vector r;
for(int i=0; i<3; i++)
{
//r[i] = m[i] * v;
r.elem(i) = 0;
for(int j=0; j<3; j++)
r.elem(i) += m.elem(i, j) * v.elem(j);
}
return r;
}
Aceasta este intr-un anumit mod "natural" sa se faca asa, dar este
ineficient. De fiecare data cind se apeleaza multiply(), elem() se
apeleaza de
4*(1+4*3) ori.
Acum, daca noi facem ca multiply() sa fie membru al clasei vector,

noi am
putea sa ne dispensam de verificarea indicilor cind se face acces la un
element al vectorului si daca noi facem ca multiply() sa fie membru al
clasei
matrix, noi am putea sa ne dispensam de verificarea indicilor cind se
face
acces la elementul unei matrici. Cu toate acestea, o functie nu poate
fi
membru pentru doua clase. Ceea ce este necesar este o constructie a
limbajului
care sa asigure unei functii accesul la partea privata a unei clase. O
functie
nemembru la care i se permite accesul la partea privata a unei clase
se
numeste prieten al clasei. O fun- ctie devine prieten al unei clase
printr-o
declaratie de prieten in clasa respectiva. De exemplu:
class matrix;
class vector{float v[4];
//........
friend vector multiply(matrix&, vector&);
};
class matrix{vector v[4];
//........
friend vector multiply(matrix&, vector&);
};
Nu este nimic special in legatura cu o functie prieten exceptind
dreptul
de acces la partea privata a unei clase. In particular, o functie
prieten nu
are un pointer this (numai daca este o functie membru). O declaratie
friend
este o declaratie reala. Ea introduce numele functiei in domeniul
cel mai
extern al unui program si il verifica fata de alte declaratii ale
lui. O
declaratie friend poate fi plasata sau in partea privata sau in partea
publica
a unei declaratii de clasa; nu are importanta unde se introduce.
Functia
multiply poate acum sa fie scrisa utilizind direct elementele
vectorilor si
matricilor:

vector multiply(matrix& m, vector& v)
{
vector r;
for(int i=0; i<3; i++)
{
//r[i] = m[i]*v;
r.v[i] = 0;
for(int j=0; j<3; j++)
r.v[i] += m.v[i][j] * v.v[j];
}
return r;
}

Exista moduri de a trata aceasta problema particulara de eficienta

fara a
utiliza mecanismul friend (se poate defini operatia de inmultire pentru
vectori si sa se defineasca multiply() folosind-o pe aceasta). Cu toate
acestea, exista multe probleme care sint mult mai usor de rezolvat dind
posibilitatea unei functii care nu este membru al unei clase sa faca
acces la
partea privata a acelei clase. Capitolul 6 contine multe exemple de
utilizare
a prietenilor. Meritele relative ale functiilor prietene si membre va
fi
discutata mai tirziu.
O functie membru a unei clase poate fi prieten al alteia. De
exemplu:

class x{
//........
void f();
};

class y{
//........
friend void x::f();
};

Nu este ceva iesit din comun ca toate functiile unei clase sa fie
pritene
ale alteia. Exista chiar o prescurtare pentru acest fapt:

class x{
friend class y;
//........
};

Aceasta declaratie, friend, face ca toate functiile membre ale clasei y

sa fie
prietene ale clasei x.

5.4.2 Calificarea numelor de membri
-----------------------------

Ocazional, este util sa se faca distinctie explicita intre numele
membre
ale unei clase si alte nume. Se poate folosi operatorul de rezolutie a
domeniului "::":

class x{
int m;
public:
int readm(){ return x::m; }
void setm(int m){ x::m = m; }
};

In x::setm() numele argument m ascunde membrul m, asa ca membrul
ar putea
sa fie referit numai utilizind numele calificator al lui, x::m.
Operandul
sting a lui :: trebuie sa fie numele unei clase.
Un nume prefixat prin :: trebuie sa fie un nume global. Aceasta
este in
particular util pentru a permite nume populare cum ar fi read, put si
open sa
fie folosite pentru nume de fun- ctii membru fara a pierde abilitatea
de a se
face referire la versiunea nemembru. De exemplu:
class my_file{ //..........
public:
int open(char*, char*);
};

int my_file::open(char* name, char* spec)
{ //...........
if(::open(name, flag))
{
//utilizeaza open() din UNIX(2)
//..........
}
//...........
}


5.4.3 Clase imbricate
---------------

Declaratiile de clasa pot fi imbricate. De exemplu:
class set{
struct setmem{
int mem;
setmem* next;
setmem(int m,setmem* n){ mem=m; next=n; }
};
setmem* first;
public:
set(){first = 0;}
insert(int m){first = new setmem(m, first);}
//.......
};

Daca clasa imbricata nu este foarte simpla, astfel de declaratii
sint
foarte incurcate. Mai mult decit atit, clasele imbricate sint mai mult
o
facilitate in notatie, intrucit o clasa imbricata nu este ascunsa in
domeniul
clasei care o include din punct de vedere lexical:
class set{
struct setmem{
int mem;
setmem* next;
setmem(int m, setmem* n);
};
//.......
};

setmem::setmem(int m, setmem* n)
{mem = m;
next = n;
}
setmem m1(1, 0);

Constructorii de forma set::setmem::setmem() nu sint necesari si
nici
legali. Singurul mod de ascundere a numelui unei clase este prin
utilizarea
tehnicii de fisiere_module (&4.4).
Clasele netriviale este bine sa fie declarate separat:
class setmem{
friend class set; //acces numai prin membri
//lui set
int mem;
setmem* next;
setmem(int m, setmem* n){ mem=m; next=n; }
};

class set{
setmem* first;
public:
set(){ first = 0; }
insert(int m){ first = new setmem(m, first); }
};


5.4.4 Membri statici
--------------
O clasa este un tip, nu un obiect data si fiecare obiect al clasei

are
copia lui proprie a membrilor date ai clasei. Cu toate acestea, unele
tipuri
sint implementate mai elegant daca toate obiectele acelui tip au in
comun
unele date. Este preferabil ca o astfel de data comuna sa fie
declarata ca
parte a clasei. De exemplu, pentru a gestiona taskuri intr-un sistem de
operare, este adesea utila o lista a tuturor taskurilor:
class task{//........
task* next;
static task* task_chain;
void schedule(int);
void wait(event);
//........
};
Declarind membrul task_chain ca static se asigura ca va fi numai o

copie
a lui, nu o copie pentru fiecare obiect task. Este inca in domeniul
clasei
task si poate fi accesat "din afara" numai daca a fost declarat public.

In
acest caz, numele lui tre- buie sa fie calificat prin numele clasei
sale:
task::task_chain

Intr-o functie membru, se poate face referire prin task_chain.
Utilizarea
membrilor statici ai clasei poate reduce considerabil necesarul de
memorie
pentru variabilele globale.


5.4.5 Pointeri spre membri
--------------------

Este posibil sa se ia adresa unui membru al unei clase. A lua

adresa
unei functii membru este adesea util intrucit tehnicile si motivele
pentru a
utiliza pointeri la functii prezentate in &4.6.9 se aplica in mod egal
si la
functii membru. Totusi exista un defect curent in limbaj: nu este
posibil sa
se exprime tipul pointerului obtinut dintr-o astfel de operatie. In
consecinta
trebuie sa folosim trucuri folosind avantajele din implementarea
curenta.
Exemplul de mai jos nu este garantat ca fun- ctioneaza si utilizarea
lui
trebuie localizata in asa fel incit sa poata fi usor convertit spre a
utiliza
constructiile propri ale limbajului. Trucul folosit este acela de a
avea
avantajul faptului ca this este implementat curent ca primul argument
(ascuns)
al unei functii membru.

#include <stream.h>
struct cl{
char* val;
void print(int x){ cout << val << x << "/n"; }
cl(char* v){val = v;}
};

//"se ia" tipul functiilor membru:

typedef void (*PROC)(void*, int);

main()
{cl z1("z1 ");
cl z2("z2 ");
PROC pf1 = PROC(&z1.print);
PROC pf2 = PROC(&z2.print);
z1.print(1);
(*pf1)(&z1, 2);
z2.print(3);
(*pf2)(&z2, 4);
}
In multe cazuri, functiile virtuale (vezi capitolul 7) pot fi
utilizate
cind altfel s-ar utiliza pointeri spre functii.
Versiunile ulterioare de C++ vor suporta un concept de pointer
spre un
membru: cl::* inseamna "pointer spre un membru a lui cl". De exemplu:
typedef void(cl::*PROC)(int);
PROC pf1=&cl::print;//nu este nevoie de conversie explicita
PROC pf2 = &cl::print;
Operatorii . si -> se utilizeaza pentru un pointer spre o functie
membru.
De exemplu:

(z1.*pf1)(2);
((&z2)->*pf2)(4);


5.4.6 Structuri si Reuniuni
---------------------


Prin definitie o structura este pur si simplu o clasa cu toti
membri
publici, adica:

struct s{ ...
este pur si simplu o prescurtare pentru:

class{
public: ...

Structurile se folosesc cind ascunderea datelor este nepotrivita.

O reuniune numita se defineste ca o structura in care fiecare
membru are
aceeasi adresa (vezi &r8.5.13). Daca se stie ca numai un membru al unei
structuri va avea o valoare utila la un moment dat, o reuniune poate
salva
spatiu. De exemplu, se poate defini o reuniune pentru a pastra
unitatile
lexicale dintr-un compilator C:

union tok_val{
char* p; //sir
char v[8]; //identificator (maxim 8 caractere)
long i; //valori intregi
double d; //valori flotante
};
Problema este ca, in general compilatorul nu poate sa stie care
membru
este utilizat in fiecare moment, asa ca nu poate fi testat tipul. De
exemplu:

void strange(int i)
{
tok_val x;
if(i)
x.p = "2";
else
x.d = 2;
sqrt(x.d); //eroare daca i != 0
}

Mai mult decit atit, o reuniune definita in acest fel poate fi
initializata. De exemplu:

tok_val curr_val = 12; //eroare: se atribuie int la tok_val


este ilegal. Se pot utiliza constructori care sa trateze corect aceasta
problema:

union tok_value{
char* p; //sir
char v[8]; //identificator
long i; //valori intregi
double d; //valori flotante
tok_value(char*) //trebuie sa decida intre
//p si v
tok_value(int ii){i = ii;}
tok_value(double dd){d == dd;}
};

Aceasta trateaza cazurile in care tipurile membru pot fi rezolvate prin

reguli
pentru nume de functii supraincarcate (vezi &4.6.7 si &6.3.3). De
exemplu:

void f()
{
tok_val a = 10; //a.i = 10
tok_val b = 10.0; //b.d = 10.0
}

Cind acest lucru nu este posibil (pentru tipurile char* si char[8], int

si
char, etc.), membrul propriu poate fi gasit numai examinind
initializatorul la
momentul executiei sau furnizind un extra argument. De exemplu:

tok_val::tok_val(char* pp)
{
if(strlen(pp) <= 8)
strncpy(v, pp, 8); //sir scurt
else
p = pp; //sir lung
public:
tok_val(char* pp);
tok_val(long ii){ i=ii; tag='I'; }
tok_val(double dd){ d=dd; tag='D'; }
long& ival(){ check('I', "ival"); return i; }
double& fval(){check('D', "fval"); return d; }
char*& sval(){ check('S', "sval"); return p; }
char* id(){ check('N', "id"); return v; }
};

Constructorul utilizeaza functia strncpy pentru a copia un sir
scurt;
strncpy() aminteste de strcpy(), ea avind un al treilea argument care
defineste numarul de caractere ce se copiaza.

tok_val::tok_val(char* pp)
{
if(strlen(pp) <= 8)
{ //sir scurt
tag = 'N';
strncpy(v, pp, 8); //copiaza 8 caractere
}
else
{
tag = 'S';
p = pp; //se pastreaza numai pointerul
}
}


Tipul tok_val poate fi folosit astfel:


void f()
{
tok_val t1("short"); //asignare la v
tok_val t2("long string"); //asignare la p
char s[8];
strncpy(s, t1.id(), 8); //ok
strncpy(s, t2.id(), 8); //testul va esua
}

5.5 Constructori si Destructori
---------------------------

Cind o clasa are un constructor, el este apelat ori de cite ori se

creaza
un obiect al acelei clase. Cind o clasa are un destructor, el este
apelat ori
de cite ori este distrus un obiect al acelei clase. Obiectele pot fi
create
ca:
[1] Un obiect automatic: se creaza de fiecare data cind se
intilneste declaratia lui la executia programului si
este distrus de fiecare data cind se iese din blocul in
care el a aparut;
[2] Un obiect static: se creaza o data la pornirea programu-
lui si se distruge o data cu terminarea programului;
[3] Un obiect in memoria libera: este creat folosind
operatorul new si distrus folosind operatorul delete;
[4] Un obiect membru: ca membru al unei clase ori ca un
element de vector.

Un obiect poate de asemenea, sa fie construit intr-o expresie prin
folosirea explicita a unui constructor (&6.4), caz in care el este un
obiect
automatic. In subsectiunile care urmeaza se presupune ca obiectele sint

ale
unei clase cu un constructor si un destructor. Ca exemplu se utilizeaza

clasa
table din &5.3.


5.5.1 Goluri
------

Daca x si y sint obiecte ale clasei cl, x=y inseamna copie- rea
bitilor
lui y in x (&2.3.8). Avind asignarea interpretata in acest fel noi
putem sa
ajungem la surprize (uneori nedorite) cind folosim obiecte ale unei
clase
pentru care a fost definit un constructor si un destructor. De exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){top=s=new char[size=sz];}
~char_stack(){delete s;} //destructor
void push(char c){*top++=c;}
char pop(){return *--top;}
};
void h()
{
char_stack s1(100);
char_stack s2 = s1; //apar probleme
char_stack s3(99);
s3 = s2; //apar probleme
}

Aici constructorul char_stack::char_stack() se apeleaza de doua ori:
pentru s1
si s3. Nu se apeleaza pentru s2 deoarece variabila s2 a fost
initializata prin
atribuire. Totusi, destructorul char_stack::~char_stack() se apeleaza
de trei
ori: pentru s1, s2 si s3. Mai mult decit atit, interpretarea implicita
a
atribuirii ca si copiere de biti face ca s1, s2 si s3 sa contina
fiecare la
sfirsitul lui h() un pointer spre vectorul de caractere alocat in
memoria
libera cind a fost creat s1. Nu va ramine nici un pointer spre vectorul

de
caractere alocate cind a fost creat s3. Astfel de anomalii pot fi
eliminate
asa cum se va vedea in capitolul 6.

5.5.2 Memoria statica
---------------

Consideram:

table tbl1(100);
void f(){ static table tbl2(200); }

main()
{
f();
}

Aici, constructorul table::table() asa cum a fost definit in
&5.3.1 va fi
apelat de doua ori: o data pentru tbl1 si o data pentru tbl2.
Destructorul
table::~table() va fi apelat de asemenea de doua ori: pentru a elimina
tbl1 si
tbl2 dupa iesirea din main().
Constructorii pentru obiecte globale statice intr-un fisier se
executa in
ordinea in care apar declaratiile; destructorul se apeleaza in ordine
inversa.
Daca un constructor pentru un obiect local static este apelat, el se
apeleaza
dupa ce au fost apelati constructorii pentru obiectele statice globale
care il
preced.
Argumentele pentru constructorii de obiecte statice trebuie sa fie
expresii constante:

void g(int a)
{
static table t(a); //eroare
}

Traditional, executia lui main() a fost vazuta ca executia
programului.
Aceasta nu a fost niciodata asa, nici chiar in C, dar numai alocind un
obiect
static al unei clase cu un constructor si/sau un destructor
programatorul
poate sa aiba un mod evident si simplu de a specifica cod de
executat
inainte si/sau dupa apelul lui main.
Apelind constructori si destructori pentru obiecte statice se
realizeaza
functii extrem de importante in C++. Este modul de a asigura
initializari
propri si de a curata structuri de date din biblioteci. Consideram
<stream.h>.
De unde vin cin, cout si cerr? Unde au fost ele initializate? Si ce
este mai
important, intrucit sirurile de iesire pastreaza zone tampon interne de
caractere, cum se videaza aceste zone tampon? Raspunsul simplu si clar
este
acela ca activitatea se face prin constructori si des- tructori
corespunzatori
inainte si dupa executia lui main(). Exista alternative de a utiliza
constructori si destructori pentru initializarea si stergerea
facilitatilor de
biblioteca.
Daca un program se termina utilizind functia exit(), se vor apela
destructorii pentru obiectele statice, dar daca, programul se termina
folosind
abort(), ei nu vor fi apelati. Sa observam ca aceasta implica faptul ca

exit()
nu termina programul imediat. Apelind exit() intr-un destructor se
poate
ajunge la o recursivitate infinita.
Uneori, cind noi proiectam o biblioteca, este necesar sau pur si
simplu
convenabil sa inventam un tip cu un constructor si un destructor cu
singurul
scop al initializarii si stergerii. Un astfel de tip va fi folosit
numai o
data: sa aloce un obiect static prin apelul constructorului.


5.5.3 Memoria libera
--------------

Fie:
main()
{
table* p = new table(100);
table* q = new table(200);
delete p;
delete p; //probabil o eroare
}

Constructorul table::table() va fi apelat de doua ori si la fel si
destructorul table::~table(). Este bine de amintit ca C++ nu ofera
garantie ca
un destructor este apelat vreodata pentru un obiect creat folosind new.
Programul precedent nu il sterge pe q, dar pe p il sterge de doua ori.
In
functie de tipul lui p si q, programatorul poate sau nu sa considere
aceasta
ca o eroare. Ne- stergind un obiect de obicei nu este o eroare, ci
numai o
pierdere de spatiu. Stergind p de doua ori este de obicei o eroare
serioasa.
Un rezultat frecvent al aplicarii lui delete de doua ori la acelasi
pointer
este un ciclu infinit in rutina de gestionare a memoriei libere, dar
comportamentul in acest caz nu este specificat prin definitia
limbajului si
depinde de implementare.
Utilizatorul poate defini o implementare noua pentru operatorii
new si
delete (vezi &3.2.6). Este de asemenea posibil sa se specifice modul in

care
interactioneaza constructorul si destructorul cu operatorii new si
delete
(vezi &5.5.6).

5.5.4 Obiectele clasei ca membri
---------------------------
(clase de obiecte ca membri)

Consideram:

class classdef{
table members;
int no_of_members;
//...........
classdef(int size);
~classdef();
};

Intentia este clara; aceea ca classdef sa contina o tabela de
members de
dimensiune size si problema este de a obtine constructorul
table::table()
apelat cu argumentul size. Se poate face astfel:
classdef::classdef(int size)
:members(size)
{
no_of_members = size;
//...........
}

Argumentele pentru un constructor membru (table::table()) se
plaseaza in
definitia (nu in declaratia) constructorului clasei care il contine
(aici
classdef::classdef()). Constructorul membru este apoi apelat inaintea
corpului
constructorului care specifica lista argumentelor lui.

Daca sint mai multi membri ce necesita liste de argumente pentru
constructori, ei pot fi specificati in mod analog. De exemplu:

class classdef{
table members;
table friends;
int no_of_members;
//..........
classdef(int size);
~classdef();
};


Lista de argumente pentru membri se separa prin virgula (nu prin
doua
puncte), iar listele initializatorilor pentru membri pot fi prezentate

in
orice ordine:

classdef::classdef(int size)
:friends(size), members(size)
{
no_of_members = size;
//...........
}

Ordinea in care se apeleaza constructorii nu este specificata, asa

ca nu
se recomanda ca lista argumentelor sa fie cu efecte secundare:

classdef::classdef(int size)
:friends(size = size/2), members(size) //stil rau
{
no_of_members = size;
//...........
}

Daca un constructor pentru un membru nu necesita argumente, atunci

nu
este necesar sa se specifice nici o lista de argumente. De exemplu,
intrucit
table::table() a fost definit cu argumentul implicit 15, ceea ce
urmeaza este
corect:

classdef::classdef(int size)
:members(size)
{
no_of_members = size;
//...........
}

si dimensiunea lui friends table va fi 15.
Cind o clasa care contine clase (de exemplu classdef) se distruge,

intii
se executa corpul destructorului propriu acelei clase si apoi se
executa
destructorii membrilor.

Consideram varianta traditionala de a avea clase ca membri si
anume
aceea de a avea membri pointeri si ai initializa pe acestia intr-un
constructor:

class classdef{
table* members;
table* friends;
int no_of_members;
//............
classdef(int size);
~classdef();
};
classdef::classdef(int size)
{
members = new table(size);
friends = new table; //dimensiune implicita
no_of_members = size;
//...........
}

Intrucit tabelele au fost create folosind new, ele trebuie sa fie
distruse utilizind delete:

classdef::~classdef()
{//...........
delete members;
delete friends;
}
Obiectele create separat ca acestea pot fi utile, dar sa observam
ca
members si friends pointeaza spre obiecte separate care cer o alocare
si o
dealocare fiecare. Mai mult decit atit, un pointer plus un obiect in
memoria
libera ia mai mult spatiu decit un obiect membru.


5.5.5 Vectori si Obiecte clasa
------------------------

Pentru a declara un vector de obiecte ale unei clase cu un
constructor
acea clasa trebuie sa aiba un constructor care sa poata fi apelat fara
o lista
de argumente. Nici argumentele implicite nu pot fi utilizate. De
exemplu:
table tblvec[10];

este o eroare deoarece table::table() necesita un argument intreg. Nu
exista
nici un mod de a specifica argumente pentru un constructor intr-o
declaratie
de vector. Pentru a permite declararea vectorilor de tabele, ar putea
fi
modificata declaratia clasei table (&5.3.1) astfel:
class table{
//.........
void init(int sz); //ca si constructorul vechi
public:
table(int sz){init(sz);} //ca inainte dar nu
//exista valoare implicita
table(){init(15);} //implicit
//.........
};

Destructorul trebuie apelat pentru fiecare element al unui vector
cind se
distruge acel vector. Aceasta se face implicit pentru vectori care nu
sint
alocati utilizind new. Cu toate acestea, aceasta nu se poate face
implicit
pentru vectori din memoria libera deoarece compilatorul nu poate face
distinctie dintre pointerul spre un singur obiect de un pointer spre
primul
element al unui vector de obiecte. De exemplu:
void f()
{
table* t1 = new table;
table* t2 = new table[10];
delete t1; //o tabela
delete t2; //apar probleme: 10 tabele
}


In acest caz programatorul trebuie sa furnizeze dimensiunea
vectorului:
void g(int sz)
{table* t1 = new table;
table* t2 = new table[sz];
delete t1;
delete[sz] t2; }
Dar de ce nu poate compilatorul sa deduca numarul de elemente din
cantitatea
de memorie alocata? Deoarece alocatorul de memorie libera nu este o
parte a
limbajului si ar putea fi furnizata de programator.


5.5.6 Obiecte mici
------------

Cind se utilizeaza multe obiecte mici alocate in memoria libera,
noi
putem sa aflam ca programul consuma timp considerabil pentru alocare si
dealocare de astfel de obiecte. O solutie este de a furniza un alocator

cu
scopuri generale mai bun si o a doua este ca proiectarea unei clase sa
nu se
faca pentru a fi gestionata in memoria libera, definind constructori si
destructori.
Sa consideram clasa name folosita in exemplul table. Ea ar putea
fi
definita astfel:
struct name{char* string;
name* next;
double value;
name(char*, double, name*);
~name();
};
Programatorul poate avea avantaje din faptul ca alocarea si dealocarea
obiectelor unui tip poate fi facuta pe departe mai eficient (in timp si
spatiu) decit cu o implementare generala prin new si delete. Ideea
generala
este de a prealoca "felii" de obiecte de tip name si de a le lega intre

ele,
reducind alocarea si dealocarea la operatii simple asupra listelor
inlantuite.
Variabila nfree este antetul unei liste de nume neutilizate.
const NALL = 128;
name* nfree;
Alocatorul utilizat prin operatorul new pastreaza dimensiunea unui
obiect
impreuna cu obiectul pentru ca operatorul delete sa functioneze corect.

Aceste
spatii suplimentare se elimina simplu la un alocator specific unui tip.

De
exemplu, alocatorul urmator utilizeaza 16 octeti pentru a memora un
name la
masina mea, in timp ce alocatorul general foloseste 20. Iata cum se
poate face
aceasta:
name::name(char* s, double v, name* n)
{register name* p = nfree //prima alocare
if(p)
nfree = p->next;
else
{name* q = (name*)new char[NALL * sizeof(name)];
for(p = nfree = &q[NALL-1]; q<p; p--)
p->next = p-1;
(p+1)->next = 0;
}
this = p;
string = s; //initializare
value = v;
next = n;
}

Atribuirea la this informeaza compilatorul ca programatorul a luat
controlul si ca mecanismul implicit de alocare de memorie nu trebuie sa

fie
utilizat. Constructorul name::name() trateaza cazul in care numele este

alocat
numai prin new, dar pentru multe tipuri acesta este de obicei cazul;
&5.5.8
explica cum se scrie un constructor pentru a trata atit memoria libera,

cit si
alte tipuri de alocari.
Sa observam ca spatiul nu ar putea fi alocat pur si simplu astfel:

name* q = new name[NALL];

intrucit aceasta ar cauza o recursivitate infinita cind new apeleaza
name::name().
Dealocarea este de obicei triviala:

name::~name()
{
next = nfree;
nfree = this;
this = 0;
}

Atribuind 0 la this intr-un destructor se asigura ca nu se va
utiliza
destructorul standard.

5.5.7 Goluri
------

Cind se face o atribuire la this intr-un constructor, valoarea lui

this
este nedefinita pina la acea atribuire. O referinta la un membru
inaintea
acelei atribuiri este de aceea nedefinita si probabil cauzeaza un
destructor.
Compilatorul curent nu incearca sa asigure ca o atribuire la this sa
apara pe
orice cale a executiei:
mytype::mytype(int i)
{if(i) this = mytype_alloc(); //asignare la membri
};
se va aloca si nu se va aloca nici un obiect cind i == 0.
Este posibil pentru un constructor sa se determine daca el a fost
apelat
de new sau nu. Daca a fost apelat prin new, pointerul this are valoarea

zero
la intrare, altfel this pointeaza spre spatiul deja alocat pentru
obiect (de
exemplu pe stiva). De aceea este usor sa se scrie un constructor care
aloca
memorie daca (si numai daca) a fost apelat prin new. De exemplu:

mytype::mytype(int i)
{
if(this == 0)
this = mytype_alloc(); //asignare la membri
};

Nu exista o facilitate echivalenta care sa permita unui destructor

sa
decida daca obiectele lui au fost create folosind new si nici o
facilitate
care sa permita sa se decida daca el a fost apelat prin delete sau
printr-un
obiect din afara domeniului. Daca cunoasterea acestui lucru este
importanta,
utilizatorul poate memora undeva informatii corespunzatoare pe care sa
le
citeasca destructorul. O alta varianta este ca utilizatorul sa se
asigure ca
obiectele acelei clase sint numai alocate in mod corespunzator. Daca
prima
problema este tratata, ultima este neinteresanta.
Daca implementatorul unei clase este de asemenea numai
utilizatorul ei,
este rezonabil sa se simplifice clasa bazindu-ne pe presupunerile
despre
utilizarea ei. Cind o clasa este proiectata pentru o utilizare larga,
astfel
de presupuneri este adesea mai bine sa fie eliminate.


5.5.8 Obiecte de dimensiune variabila
-------------------------------

Luind controlul asupra alocarii si dealocarii, utilizatorul poate
de
asemenea, construi obiecte a caror dimensiune nu este determinata la
momentul
compilarii. Exemplele precedente de implementare a claselor container
vector,
stack, insert si table ca dimensionate fix, acceseaza direct structuri
care
contin pointeri spre dimensiunea reala. Aceasta implica faptul ca sint
necesare doua operatii de creare de astfel de obiecte in memoria libera

si ca
orice acces la informatiile memorate va implica o indirectare
suplimentara. De
exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){ top=s=new char[size=sz]; }
~char_stack(){ delete s; } //destructor
void push(char c){ *top++=c; }
char pop(){ return *--top; }
};


Daca fiecare obiect al unei clase este alocat in memoria libera,
aceasta
nu este necesar. Iata o alternativa:

class char_stack{
int size;
char* top;
char s[1];
public:
char_stack(int sz);
void push(char c){ *top++=c; }
char pop(){ return *--top; }
};

char_stack::char_stack(int sz)
{
if(this)
error("stack not on free store");
if(sz<1)
error("stack size < 1");
this = (char_stack*)new char[sizeof(char_stack)+sz-1];
size = sz;
top = s;
}

Observam ca un destructor nu mai este necesar, intrucit delete
poate
elibera spatiul utilizat de char_stack fara vreun ajutor din partea
programatorului.


5.6 Exercitii
---------

1. (*1). Sa se modifice calculatorul de birou din capitolul
3 pentru a utiliza clasa table.

2. (*1). Sa se proiecteze tnode (&r8.5) ca o clasa cu con-
sructori, destructori, etc.. Sa se defineasca un arbore de tnodes
ca o clasa cu constructori, destructori, etc..

3. (*1). Sa se modifice clasa intset (&5.3.2) intr-o multime
de siruri.

4. (*1). Sa se modifice clasa intset intr-o multime de noduri

unde
node este o structura pe care sa o definiti.

5. (*3). Se defineste o clasa pentru analizarea, memorarea,
evaluarea si imprimarea expresiilor aritmetice simple care constau din
constante intregi si operatiile '+', '-', '*' si '/'. Interfata publica

ar
trebui sa arate astfel:

class expr{
//.........
public:
expr(char*);
int eval();
void print();
};
Argumentul sir pentru constructorul expr::expr() este expresia.
Functia
expr::eval() returneaza valoarea expresiei, iar expr::print() imprima
reprezentarea expresiei la cout. Un program ar putea arata astfel:

expr x("123/4+123*4-3");
cout << "x = " << x.eval() << "\n";
x.print();

Sa se defineasca expr class de doua ori: o data utilizind o lista
inlantuita de noduri si o data utilizind un sir de caractere. Sa se
experimenteze diferite moduri de imprimare a expre- siei: cu paranteze
complete, notatie postfix, cod de asamblare, etc..

6. (*1). Sa se defineasca o clasa char_queue asa ca interfata
publica
sa nu depinda de reprezentare. Sa se implementeze char_queue: (1) ca o
lista
inlantuita si (2) ca un vector.

7. (*2). Sa se defineasca o clasa histograma care tine seama
de numerele dintr-un anumit interval specificat ca argumente la
constructorul
histogramei. Sa se furnizeze functii pentru a imprima histograme. Sa se
trateze domeniul valorilor. Recomandare: <task.h>.

8. (*2). Sa se defineasca niste clase pentru a furniza numere
aleatoare de o anumita distributie. Fiecare clasa are un constructor
care
specifica parametri pentru distributie si o functie draw care
returneaza
valoarea "urmatoare". Recomandare: <task.h>. Vezi de asemenea clasa
intset.

9. (*2). Sa se rescrie exemplul date (&5.2.2) exemplul
char_stack
(&5.2.5) si exemplul intset (&5.3.2) fara a utiliza functii membru
(nici chiar
constructori si destructori). Sa se utilizeze numai class si friend. Sa

se
testeze versiunile noi. Sa se compare cu versiunile care utilizeaza
functiile
membru.

10. (*3). Sa se proiecteze o clasa pentru o tabela de simbo-
luri si o clasa de intrare in tabela de simboluri pentru un anumit
limbaj. Sa
aruncam o privire la compilatorul limbajului respectiv pentru a vedea
cum
arata tabela de simboluri reala.

11. (*2). Sa se modifice clasa expresie din exercitiul 5 pentru

a
trata variabile si operatorul de asignare =. Sa se foloseasca clasa
tabela de
simboluri din exercitiul 10.

12. (*1). Fiind dat programul:

#include <stream.h>
main()
{ cout << "Hello, word\n";
}
sa se modifice pentru a avea la iesire:

Initialize
Hello, world
Clean up

Sa nu se modifice functia main().


CAPITOLUL 6
===========


OPERATOR SUPRAINCARCAT
======================


Acest capitol descrie mecanismul pentru operatorul de
supraincarcare
furnizat de C++. Un programator poate defini un sens pentru operatori
cind se
aplica la obiectele unei clase specifice; in plus se pot defini fata de
operatiile aritmetice, logice si relationale, apelul () si indexarea []

si
atit initializarea cit si asignarea pot fi redefinite. Se pot defini
conversii
de tip implicite si explicite intre cele definite de utilizator si
tipurile de
baza. Se arata cum se defineste o clasa pentru care un obiect nu poate
fi
copiat sau distrus exceptind functiile specifice definite de
utilizator.

6.1 Introducere
-----------

Programele adesea manipuleaza obiecte care sint reprezentari
concrete ale
conceptelor abstracte. De exemplu, datele de tip int din C++, impreuna
cu
operatorii +, -, *, /, etc., furnizeaza o implementare (restrictiva) a
conceptului matematic de intregi. Astfel de concepte de obicei includ
un set
de operatori care reprezinta operatiile de baza asupra obiectelor
intr-un mod
concis, convenabil si conventional. Din nefericire, numai foarte putine

astfel
de concepte pot fi suportate direct prin limbajul de programare. De
exemplu,
ideile de aritmetica complexa, algebra matricilor, semnale logice si
sirurile
receptionate nu au un suport direct in C++. Clasele furnizeaza o
facilitate
pentru a specifica o reprezentare a obiectelor neprimitive in C++
impre- una
cu

Text Attachment [ Scan and Save to Computer | Save to Yahoo! Briefcase
]

Plain Text Attachment [ Scan and Save to Computer | Save to Yahoo!
Briefcase ]


THE C++

PROGRAMMING LANGUAGE


BJARNE STROUSTRUP


ADDISON-WESLEY
PUBLISHING COMPANY
1986

PREFATA
===========


C++ este un limbaj de programare cu scop universal. El
contine facilitati flexibile si eficiente pentru a definii tipuri
noi. Programatorul poate partitiona o aplicatie in bucati
manevrabile prin definiri de tipuri noi, care corespund mai bine
la conceptele aplicatiei. Aceasta tehnica de construire a progra-
mului se numeste adesea abstractizare de date. Obiectele unui
anumit tip definit de utilizator contin informatie de tip. Astfel
de obiecte pot fi folosite convenabil in contextul in care tipul
lor poate fi determinat in momentul compilarii. Programele utili-
zind obiecte de astfel de tipuri se numesc adesea bazate pe
obiecte. Cind se utilizeaza bine, aceste tehnici conduc la
programe mai scurte, mai usor de inteles si mai usor de a le
menine.
Conceptul cheie din C++ este clasa. O clasa este un tip
utilizator. Clasele furnizeaza ascunderea datelor, garantarea
initializarii datelor, conversii de tip implicite pentru tipuri
de date utilizator, tipuri dinamice, gestionarea memoriei contro-
late de utilizator si mecanismul de supraincarcare a operatori-
lor. C++ furnizeaza facilitati mai bune pentru verificarea tipu-
lui si pentru exprimarea modularitatii, decit o face limbajul C.
De asemenea contine imbunatatiri care sint direct inrudite cu
clasele, incluzind constantele simbolice, substitutia in linie a
functiilor, argumente implicite ale functiilor care se suprain-
carca, operatori de gestionare a memoriei libere si un tip refe-
rinta.

OBSERVATII PENTRU CITITOR
===========================

Structura cartii
----------------

Capitolul 1 este o trecere in revista rapida a caracteris-
ticilor majore ale lui C++. La inceput sint caracteristici comune
cu C. Cea de-a doua jumatate descrie facilitatile lui C++ pentru
a definii tipuri noi.
Capitolele 2, 3 si 4 descriu caracteristicile lui C++ care
nu sint implicate in definirea de tipuri noi. Deci se descrie
subsetul lui C++ care este in esenta C. Informatia completa se da
in manualul de referinta.
Capitolele 5, 6, 7 descriu facilitatile lui C++ pentru a
definii tipuri noi, trasaturi care nu fac parte din C. Capitolul
5 prezinta conceptul de clasa, aratind cum obiectele de tip
utilizator, pot fi initializate, accesate si in final eliminate.
Capitolul 6 explica cum se pot definii operatorii unari si binari
pentru un tip definit de utilizator, cum se pot face conversatii
intre tipurile definite de utilizator si cum se specifica modul
de creare, stergere si copiere a unei valori a unui tip definit
de utilizator. Capitolul 7 descrie conceptul de clasa derivata,
care permite programatorului sa construiasca clase mai complexe
din unele mai simple, pentru a furniza interfete alternative
pentru o clasa si a minui obiecte intr-o maniera eficienta si in
deplina protectie, in contextul in care tipurile lor nu pot fi
cunoscute la compilare.
Capitolul 8 prezinta clasele ostream si istream furnizate
pentru intrari si iesiri din biblioteca standard. Acest capitol
prezinta o facilitate care este un exemplu real de utilizare a
lui C++.
In final este inclus manualul de referinta C++.

Trimiterile se descriu astfel:

&2.3.4 -> capitolul 2, sectiunea 3.4;
&r8.5.5 -> trimitere in partea de referinta.

Exercitiile sint gradate astfel:

(*1) - exercitiul necesita 5 minute;
(*2) - exercitiul necesita o ora;
(*3) - exercitiul necesita o zi.

Observatii de proiectare
------------------------

C++ nu are tipuri de date de nivel inalt si nici operatii
primitive de nivel inalt. De exemplu, nu exista tipul matrice cu
un operator de inversare sau tipul sir cu operatorul de concate-
nare. Daca un utilizator doreste un astfel de tip, el poate fi
definit. Defapt, definirea unui tip nou cu scopuri generale sau
specific aplicative este scopul cel mai important al limbajului
C++. Un tip definit de utilizator difera de unul predefinit numai
in modul de definire si nu si in modul in care este utilizat.

Note istorice
-------------

C++ a aparut in vara anului 1983 (C cu clase). Incercari au
fost facute inca din 1980. C++ inseamna C incrementat. C++ are ca
scop principal scrierea de programe bune mai usor si mai placut
pentru programatorul individual.
O sursa de inspiratie a fost Simula 67; conceptul de clasa a
fost imprumutat de aici.
C si ANSI C sint foarte apropiate pentru a fi un subset a
lui C++. C++ a fost dezvoltat din C si cu foarte mici exceptii C
ramine un subset a lui C++.

Observatii filozofice
---------------------

Un limbaj de programare serveste la doua scopuri inrudite:
el furnizeaza un mijloc pentru programator de a specifica actiuni
de executat si un set de concepte pentru programator care sa fie
utilizate cind se gindeste in legatura cu ceea ce poate fi facut.
Primul aspect in mod ideal cere un limbaj ce este "strins
legat de masina" asa incit toate aspectele unei masini sa fie
manevrate simplu si eficient intr-un mod care sa fie rezonabil de
clar pentru programator. Limbajul C initial a fost proiectat
avind acest lucru in minte.
Cel de al doilea aspect in mod ideal cere un limbaj care
este "strins legat de problema de rezolvat", asa ca, conceptele
unei solutii sa poata fi exprimate direct si concis. Facilitatile
adaugate la C pentru a crea C++ initial au fost proiectate avind
acest lucru in minte.
Legatura dintre limbajul in care noi gindim programul si
dintre cel in care ne imaginam problemele si solutiile este
foarte strinsa. Din acest motiv, restringerea caracteristicilor
cu scopul de a elimina erorile programatorului este cel mai
periculos. Tot asa cu limbajele naturale, exista un beneficiu
mare din faptul ca sint cel putin bilingve. Limbajul furnizeaza
programatorului un set de instrumente conceptuale: daca acestea
sint inadecvate pentru un task, ele pur si simplu vor fi igno-
rate. De exemplu, restringind serios conceptul de pointer, pur si
simplu se forteaza programatorul ca sa utilizeze un vector plus
aritmetica intreaga pentru a implementa structuri, pointeri, etc.
Un proiect bun si absenta erorilor nu poate fi garantata
numai prin caracteristicile limbajului.
Sistemul tipurilor ar trebui sa fie in special util pentru
task-uri netriviale.

Ginduri despre programare in C++
--------------------------------

Ideal sarcina de concepere a unui program este impartita in
3 stadii: primul consta in intelegerea clara a problemei, apoi
identificare conceptelor cheie implicate intr-o solutie si in
final exprimarea solutiei printr-un program. Totusi, detaliile
problemei si conceptele unei solutii adesea devin clar intelese
numai prin efortul de a le exprima intr-un program; acesta este
motivul alegerii limbajului de programare.
In cele mai multe aplicatii exista concepte care nu sint
reprezentate usor intr-un program nici printr-un tip fundamental
si nici printr-o functie fara date statice asociate. Dindu-se un
astfel de concept, se declara o clasa pentru a-l reprezenta in
program. O clasa este un tip; adica, ea specifica cum obiectele
din clasa se dezvolta: cum se creaza, cum pot fi manipulate, cum
se anihileaza. O clasa de asemenea specifica cum se reprezinta
obiectele, dar la un stadiu mai initial al proiectarii programu-
lui aceasta nu trebuie sa fie o conceptie majora. Cheia scrierii
unui program bun este de a proiecta clasele in asa fel incit
fiecare, in mod clar, sa reprezinte un singur concept. Adesea
aceasta inseamna ca programatorul trebuie sa se concetreze asupra
problemelor: cum se creaza obiectele din aceasta clasa? se poate
ca obiectele din aceasta clasa sa fie copiate si/sau distruse? ce
operatii pot fi facute cu astfel de obiecte? Daca nu sint raspun-
suri bune la astfel de intrebari, conceptul probabil ca nu a fost
clar definit si va trebui sa ne mai gindim asupra lui.
Conceptele cu care este mai usor sa ne ocupam sint cele care
au un formalism matematic traditional: numere de toate felurile,
multimi, forme geometrice, etc.. Se cuvine sa fie biblioteci
standard de clase care sa reprezinte astfel de concepte.
Unul dintre cele mai puternice instrumente intelectuale
pentru tratarea complexitatilor este ordonarea ierarhica; adica
organizarea conceptelor inrudite intr-o structura de arbore cu
cel mai general concept in radacina. In C++ clasele derivate
reprezinta o astfel de structura. Un program poate fi adesea
organizat ca o multime de arbori.

Reguli
------

Iata citeva reguli care trebuiesc considerate cind invatam
C++.

[1] Cind programam, noi cream o reprezentare concreta a ideilor
ce constituie solutia noastra la o anumita problema. Structura
programului reflecta acele idei atit de direct cit este posibil:

[a] Daca noi putem sa ne gindim la "el" ca la o idee separa-

ta, sa-l facem o clasa.
[b] Daca noi putem sa ne gindim la "el" ca la o entitate

separata, sa-l facem obiect al unei anumite clase.
[c] Daca doua clase au ceva seminificativ in comun, aceasta
se face o clasa de baza. Majoritatea claselor din pro-
gramul nostru vor avea ceva in comun: au o clasa de baza

universala si ea trebuie proiectata cu multa atentie.

[2] Cind noi definim o clasa care nu implementeaza o entitate
matematica ca o matrice sau un numar complex sau un tip de nivel
inferior ca o lista inlantuita:

[a] Sa nu se utilizeze date globale.
[b] Sa nu se utilizeze functii globale (care nu sint

membri).
[c] Sa nu se utilizeze membri ale datelor publice.
[d] Sa nu se utilizeze frati, exceptind cazul in care ei se
folosesc pentru a elimina [a], [b] sau [c].
[e] Sa nu se faca acces direct la membri de date a altor

obiecte.
[f] Sa nu se puna un tip 'cimp' intr-o clasa; sa se utili
zeze functii virtuale.
[g] Sa nu se utilizeze functii inline; exceptind cazul unei
optimizari semnificative.


CUPRINS
=======


NUME PAG.
=================================================================

CAP.1 === "TUTORUL" LUI C++ 1
1.1. Introducere 1
1.1.1. Iesire 1
1.1.2. Compilare 1
1.1.3. Intrare 1
1.2. Comentariu 2
1.3. Tipuri si Declaratii 2
1.3.1. Tipuri fundamentale 2
1.3.2. Tipuri derivate 3
1.4. Expresii si Instructiuni 3
1.5. Functii 5
1.6. Structura programului 6
1.7. Clase 7
1.8. Operatorul overloading 8
1.9. Referinte 9
1.10. Constructori 10
1.11. Vectori 11
1.12. Expandare inline 13
1.13. Clase derivate 13
1.14. Mai mult despre operatori 15
1.15. Prieteni (Friends) 17
1.16. Vectori generici 18
1.17. Vectori polimorfici 18
1.18. Functii virtuale 20

CAP.2 === DECLARATII SI CONSTANTE 21
2.1. Declaratii 21
2.1.1. Domeniu 22
2.1.2. Obiecte si Lvalori 24
2.1.3. Durata de viata 24
2.2. Nume 25
2.3. Tipuri 25
2.3.1. Tipuri fundamentale 26
2.3.2. Conversia implicita de tip 27
2.3.3. Tipuri derivate 28
2.3.4. Void 29
2.3.5. Pointeri 30
2.3.6. Vectori 31
2.3.7. Pointeri si Vectori 32
2.3.8. Structuri 34
2.3.9. Echivalenta tipurilor 36


NUME PAG.
=================================================================
2.3.10. Referinte 36
2.3.11. Registrii 39
2.4. Constante 40
2.4.1. Constante intregi 40
2.4.2. Constante in flotanta 41
2.4.3. Constante caracter 41
2.4.4. Siruri 42
2.4.5. Zero 43
2.4.6. Const 43
2.4.7. Enumerari 45
2.5. Salvarea spatiului 46
2.5.1. Cimpuri 46
2.5.2. Reuniuni 47
2.6. Exercitii 49

CAP.3 === EXPRESII SI INSTRUCTIUNI 51
3.1. Un calculator de birou 51
3.1.1. Analizorul 52
3.1.2. Functia de intrare 56
3.1.3. Tabela de simboluri 58
3.1.4. Tratarea erorilor 60
3.1.5. Driverul 61
3.1.6. Argumentele liniei de comanda 62
3.2. Sumar de operatori 63
3.2.1. Paranteze rotunde 65
3.2.2. Ordinea de evaluare 66
3.2.3. Incrementare si Decrementare 67
3.2.4. Operatori logici pe biti 68
3.2.5. Conversia tipului 69
3.2.6. Memoria libera 70
3.3. Sumarul instructiunilor 73
3.3.1. Teste 74
3.3.2. Goto 76
3.4. Comentarii si Decalari 77
3.5. Exercitii 79

CAP.4 === FUNCTII SI FISIERE 83
4.1. Introducere 83
4.2. Linkare 84
4.3. Fisiere antet 86
4.3.1. Fisier antet unic 88
4.3.2. Fisiere antet multiple 90
4.3.3. Ascunderea datelor 92
4.4. Fisiere si Module 93
4.5. Cum se construieste o biblioteca 93
4.6. Functii 95
4.6.1. Declaratii de functii 95
4.6.2. Definitii de functii 95
4.6.3. Transferul argumentelor 96
4.6.4. Valoarea returnata 97
4.6.5. Argumente vector 98
4.6.6. Argumente implicite 99
4.6.7. Nume de functii supraincarcate 100

NUME PAG.
=================================================================
4.6.8. Numar nespecificat de argumente 102
4.6.9. Pointer spre functie 104
4.7. Macrouri 107
4.8. Exercitii 110

CAP.5 === CLASE 113
5.1. Introducere si privire generala 113
5.2. Clase si Membri 114
5.2.1. Functii membru 114
5.2.2. Clase 115
5.2.3. Autoreferinta 116
5.2.4. Initializare 118
5.2.5. Curatire (Stergere) 120
5.2.6. In linie 121
5.3. Interfete si Implementari 121
5.3.1. Implementari alternative 122
5.3.2. O clasa completa 125
5.4. Prieteni si Reuniuni 128
5.4.1. Prieteni 128
5.4.2. Calificarea numelor membre 131
5.4.3. Clase imbricate 131
5.4.4. Membri statici 132
5.4.5. Pointeri spre membri 133
5.4.6. Structuri si Reuniuni 134
5.5. Constructori si Destructori 137
5.5.1. Goluri 137
5.5.2. Memoria statica 138
5.5.3. Memoria libera 139
5.5.4. Obiectele clasei de membri 140
5.5.5. Vectori si Obiecte clasa 142
5.5.6. Obiecte mici 143
5.5.7. Goluri 144
5.5.8. Obiecte de dimensiune variabila 145
5.6. Exercitii 147

CAP.6 === OPERATOR SUPRAINCARCAT 149
6.1. Introducere 149
6.2. Functiile operator 150
6.2.1. Operatori binari si unari 151
6.2.2. Sensul predefinit al operatorilor 151
6.2.3. Operatori si Tipuri definite de
utilizatori 152
6.3. Conversia de tip definita de utilizator 152
6.3.1. Constructori 153
6.3.2. Operatori de conversie 154
6.3.3. Ambiguitati 155
6.4. Constante 157
6.5. Obiecte mari 157
6.6. Asignare si Initializare 158
6.7. Indexare 160
6.8. Apelul unei functii 162
6.9. O clasa sir 163
6.10. Prieteni si Membri 166

NUME PAG.
=================================================================
6.11. Goluri 167
6.12. Exercitii 168

CAP.7 === CLASE DERIVATE 171
7.1. Introducere 171
7.2. Clase derivate 172
7.2.1. Derivare 172
7.2.2. Functii membru 173
7.2.3. Vizibilitate 175
7.2.4. Pointeri 176
7.2.5. Ierarhizarea claselor 177
7.2.6. Constructori si Destructori 178
7.2.7. Cimpuri de tip 179
7.2.8. Functii virtuale 181
7.3. Interfete alternative 183
7.3.1. O interfata 183
7.3.2. O implementare 184
7.3.3. Cum sa o folosim 186
7.3.4. Tratarea erorilor 187
7.3.5. Clase generice 189
7.3.6. Interfete restrictive 190
7.4. Adaugarea la o clasa 191
7.5. Liste eterogene 193
7.6. Un program complet 193
7.6.1. Controlul ecranului 193
7.6.2. Biblioteca de figuri 196
7.6.3. Programul de aplicatie 198
7.7. Memoria libera 200
7.8. Exercitii 202

CAP.8 === STREAMS 205
8.1. Introducere 205
8.2. Iesire 206
8.2.1. Iesirea tipurilor predefinite 206
8.2.2. Iesirea tipurilor definite de
utilizator 207
8.2.3. Citeva detalii de implementare 208
8.2.4. Iesire formatata 209
8.2.5. O functie de iesire virtuala 212
8.3. Fisiere si Streamuri 213
8.3.1. Initializarea streamurilor de
iesire 213
8.3.2. Inchiderea streamurilor de iesire 214
8.3.3. Deschiderea fisierelor 214
8.3.4. Copierea streamurilor 215
8.4. Intrari 215
8.4.1. Introducerea tipurilor predefi-
nite 216
8.4.2. Starile streamului 217
8.4.3. Introducerea tipurilor definite
de utilizator 218
8.4.4. Initializarea streamurilor de
intrare 219

NUME PAG.
=================================================================
8.5. Manipularea sirurilor 220
8.6. Blocare in bufer 221
8.7. Eficienta 223
8.8. Exercitii 224

MANUAL DE REFERINTA 227
1. Introducere 227
2. Conventii lexicale 227
2.1. Comentarii 227
2.2. Identificatori (Nume) 227
2.3. Cuvinte cheie 228
2.4. Constante 228
2.4.1. Constante intregi 228
2.4.2. Constante long explicite 228
2.4.3. Constante caracter 229
2.4.4. Constante flotante 229
2.4.5. Constante enumerative 229
2.4.6. Constante declarate 229
2.5. Siruri 230
2.6. Caracteristici hardware 230
3. Notatia sintactica 230
4. Nume si Tipuri 231
4.1. Domenii 231
4.2. Definitii 232
4.3. Linkare 232
4.4. Clase de memorie 232
4.5. Tipuri fundamentale 232
4.6. Tipuri derivate 233
5. Obiecte si Lvalori 233
6. Conversii 234
6.1. Caractere si Intregi 234
6.2. Flotante in simpla si dubla pre-
cizie 234
6.3. Flotante si Intregi 234
6.4. Pointeri si Intregi 235
6.5. Intregi fara semn 235
6.6. Conversii aritmetice 235
6.7. Conversii de pointeri 236
6.8. Conversie de referinta 236
7. Expresii 236
7.1. Expresii primare 237
7.2. Operatori unari 239
7.2.1. Incrementare si Decrementare 239
7.2.2. Sizeof 240
7.2.3. Conversie explicita de tip 240
7.2.4. Memoria libera 241
7.3. Operatori multiplicatori 242
7.4. Operatori aditivi 242
7.5. Operatori de deplasare 243
7.6. Operatori relationali 243
7.7. Operatori de egalitate 244
7.8. Operatorul SI pe biti 244
7.9. Operatorul SAU-EXCLUSIV pe biti 244

NUME PAG.
=================================================================
7.10. Operatorul SAU-INCLUSIV pe biti 244
7.11. Operatorul logic SI 244
7.12. Operatorul logic SAU 245
7.13. Operator conditional 245
7.14. Operatori de asignare 245
7.15. Operatorul virgula 246
7.16. Operatori de supraincarcare 246
7.16.1. Operatori unari 247
7.16.2. Operatori binari 247
7.16.3. Operatori speciali 247
8. Declaratii 247
8.1. Specificatori de clasa de memorie 248
8.2. Specificatori de tip 249
8.3. Declaratori 250
8.4. Intelesul ( sensul ) declaratorilor 251
8.4.1. Exemple 253
8.4.2. Tablouri, Pointeri si Indici 254
8.5. Declaratii de clasa 255
8.5.1. Membri statici 256
8.5.2. Functii membru 257
8.5.3. Clase derivate 258
8.5.4. Functii virtuale 259
8.5.5. Constructori 259
8.5.6. Conversii 260
8.5.7. Destructori 261
8.5.8. Memoria libera 261
8.5.9. Vizibilitatea numelor membri 262
8.5.10. Prieteni 263
8.5.11. Functii operator 264
8.5.12. Structuri 264
8.5.13. Reuniuni 264
8.5.14. Cimpuri de biti 265
8.5.15. Clase imbricate 265
8.6. Initializare 266
8.6.1. Liste initializatoare 266
8.6.2. Obiecte de clasa 267
8.6.3. Referinte 268
8.6.4. Tablouri de caractere 269
8.7. Nume de tip 269
8.8. Typedef 270
8.9. Nume de functii supraincarcate 271
8.10. Declaratii de enumerare 272
8.11. Declaratia ASM 273
9. Instructiuni 273
9.1. Instructiunea expresie 273
9.2. Instructiunea compusa (blocul) 273
9.3. Instructiunea conditionala 274
9.4. Instructiunea WHILE 274
9.5. Instructiunea DO 274
9.6. Instructiunea FOR 274
9.7. Instructiunea SWITCH 275
9.8. Instructiunea BREAK 276
9.9. Instructiunea CONTINUE 276

NUME PAG.
=================================================================
9.10. Instructiunea RETURN 276
9.11. Instructiunea GOTO 277
9.12. Instructiunea etichetata 277
9.13. Instructiunea NULL 277
9.14. Instructiunea declarativa 277
10. Definitii de functii 278
11. Linii de control ale compilatorului 279
11.1. Substitutia de siruri 280
11.2. Incluziune de fisiere 280
11.3. Compilarea conditionata 281
11.4. Linie de control 281
12. Expresii constante 282
13. Consideratii de portabilitate 282
14. Sumar de sintaxa 283
14.1. Expresii 283
14.2. Declaratii 284
14.3. Instructiuni 286
14.4. Definitii externe 286
14.5. Preprocesor 287
15. Diferente fata de C 287
15.1. Extensii 287
15.2. Sumar de incompatibilitati 288
15.3. Anacronisme 288


CAPITOLUL 1
===========


"TUTORUL" LUI C++
=================


1.1 Introducere
-----------

1.1.1 Iesire
------

#include <stream.h>
main()
{
cout << "Hello, world\n";
}

#include <stream.h> - include declaratii pentru facilitatile standard
de
intrare/iesire aflate in stream.h.
Operatorul << scrie cel de al doilea operand al sau peste primul.


1.1.2 Compilare
---------
Se apeleaza cu litere mari CC. Daca programul este in fisierul
hello.c,
atunci se compileaza si se executa ca mai jos:

$CC hello.c
$a.out
Hello, world
$


1.1.3 Intrare
-------
#include <stream.h>
main() //converteste inch in cm
{int inch = 0;
cout << "inches";
cin >> inch;
cout << inch;
cout << "in=";
cout << inch*2.54;
cout << "cm\n";
}

Exemplu de executie
$a.out
inches = 12
12 in = 30.48 cm
$

Ultimii 4 operatori pot fi scrisi:
cout << inch << "in=" << inch*2.54 << "cm\n";


1.2 Comentariu
----------
Incepe prin /* si se termina prin */.
Comentariu poate incepe prin // si se termina la sfirsitul =" <<
inch*2.54 << "cm\n";
trateaza corect cele 4 valori de iesire care sint diferite.
C++ are citeva tipuri de baza si diferite moduri de a crea altele
noi.


1.3.1 Tipuri fundamentale
-------------------
char short int long float double
sizeof(char) <= sizeof(short) <= sizeof(int) <=
sizeof(long) <= sizeof(float) <= sizeof(double)
const float pi = 3.14;
const char plus = '+';
Operatori aritmetici:
+ - (unari si binari ambii)
* / %
Operatori de comparare ca in C.
double d = 1; int i = 1;
d = d + i; i = d + i;


1.3.2 Tipuri derivate
---------------
* -> pointer
*const -> pointer constant
& -> adresa
[] -> vector
() -> functie

char* p;
char *const q;
char v[10];
char c;
//......
p = &c; // p pointeaza spre c

1.4 Expresii si Instructiuni
------------------------

~ &(si) ^ | << >> se aplica la intregi
= op=
x = sqrt (a = 3*x)
++ --

Cea mai frecventa forma a unei instructiuni este o instructiune
expresie;
ea consta dintr-o expresie urmata de un punct si virgula.

a = b*3+c;
cout << "go go go";
lseek(fd, 0, 2);


Instructiunea VIDA:
;


Blocuri:

{
a = b + 2;
b++;
}


Instructiunea IF:

#include <stream.h>
main() //converteste din inch in cm si invers
{
const float fac = 2.54;
float x, in, cm;
char ch = 0;
cout << "enter lenght:";
cin >> x >> ch;
if(ch=='i')
{ //inch
in = x;
cm = x*fac;
}
else
if(ch=='c')
{ //cm
in = x/fac;
cm = x;
}
else
in = cm = 0;
cout << in << "in=" << cm << "cm\n";
}


Instructiunea SWITCH:

switch(ch)
{
case 'i': in = x;
cm = x*fac;
break;
case 'c': in = x/fac;
cm = x;
break;
default: in = cm = 0;
break;
}

Instructiunea WHILE:

while(*p!=0)
{
*q = *p;
q = q+1;
p = p+1;
}
*q = 0;
while(*p)
*q++ = *p++;
*q = 0;
while(*q++ = *p++);

Instructiunea FOR:

for(int i=0; i<10; i++)
q[i] = p[i];

Declaratii:

for(int i=1; i<MAX; i++)
{
int t = v[i-1];
v[i-1] = v[i];
v[i] = t;
}

1.5 Functii
-------

O functie este o parte denumita a programului care poate fi
apelata din
alte parti ale programului atit de des, cit este nevoie.
extern float pow(float, int);
// pow este definita in alta parte
main()
{for(int i=0; i<10; i++)
cout << pow(2, 1) << "\n";
pow(12.3, "abcd") //este eroare
}
float pow(float x, int n)
{
if(n<0)
error("expresie negativ pentru pow");
switch(n)
{
case 0: return 1;
case 1: return x;
default: return x*pow(x, n-1);
}
}

overload pow;
int pow(int, int);
double pow(double, double);
//.......
x = pow(2, 10);
y = pow(2.0, 10.0);

Declaratia overload pow informeaza compilatorul ca se
intentioneaza sa se
foloseasca numele pow pentru mai mult decit o singura functie.

Daca o functie nu returneaza o valoare trebuie sa se declare void:

void swap(int* p, int* q)
{
int t = *p;
*p = *q;
*q = t;
}


1.6 Structura programului
---------------------

Un nume care se utilizeaza ca sa refere acelasi lucru in doua
fisiere
sursa trebuie sa fie declarat ca extern:

extern double sqrt(double);
extern istream cin;

Este bine ca aceste declaratii sa se plaseze intr-un fisier si
apoi
acesta sa se includa. De exemplu, daca declaratia pentru sqrt() este in

math.h
atunci putem scrie:

#include <math.h>
//........
x = sqrt(4);

Daca este intre paranteze unghiulare se include de obicei din
/usr/include/CC. Altfel se folosesc ghilimele.

#include "math1.h"
#include "/usr/bs/math2.h"


Mai jos un sir se defineste intr-un fisier si se scrie in altul.

//header.h
extern char* prog_name;
extern void f();

Fisierul main.c este programul principal:

#include "header.h"
char* prog_name = "silly, but complete";
main()
{
f();
}

si fisierul f.c imprima sirul:

#include <stream.h>
#include "header.h"
void f(){ cout << prog_name << "\n"; }
La executie se obtine textul:

$CC main.c f.c -o silly
$silly
silly, but complete
$


1.7 Clase
-----
Sa vedem cum putem defini tipul ostream. Pentru a simplifica
aceasta
sarcina, presupunem ca s-a definit tipul streambuf pentru buferarea
caracterelor. Un streambuf este in realitate definit in <stream.h> unde

se
gaseste de asemenea definitia reala a lui ostream.
Definitia tipului utilizator (numit clasa in C++) contine o
specificatie
a datei necesare pentru a reprezenta un obiect de acest tip si o
multime de
operatii pentru a manevra astfel de obiecte. Definitia are doua parti:
o parte
privata ce pastreaza informatia care poate fi utilizata numai de
implementatorul ei si o parte publica ce reprezinta o interfata cu
utilizatorul:
class ostream{
streambuf* buf;
int state;
public:
void put(char*);
void put(long);
void put(double);
};

Declaratiile dupa eticheta public specifica interfata;
utilizatorul poate
apela cele 3 functii put(). Declaratiile ce se gasesc inaintea
etichetei
public specifica reprezentarea unui obiect al clasei ostream. Numele
buf si
state pot fi utilizate numai prin functiile put() declarate in partea
public.
O clasa defineste un tip si nu un obiect data, asa ca pentru a
utiliza un
ostream noi trebuie sa declaram unul (in acelasi mod in care noi
declaram
variabilele de tip int):

ostream my_out;

Presupunind ca my_out a fost deja initializat in mod
corespunzator, el
poate fi utilizat acum astfel:

my_out.put("Hello, world\n");

Operatorul se foloseste pentru a selecta un membru al clasei
pentru un
obiect dat al acelei clase. Aici functia membru put() se apeleaza
pentru
obiectul my_out.

Functia poate fi declarata astfel:

void ostream::put(char* p)
{
while(*p)
buf.sputc(*p++);
}

unde sputc() este o functie care pune un caracter in streambuf.
Prefixul
ostream este necesar pentru a distinge put() a lui ostream de alte
apeluri ale
lui put().
Pentru a apela o functie membru, un obiect al clasei trebuie sa
fie
specificat. In functia membru, acest obiect poate fi implicit
referentiat asa
cum se face in ostream::put() de mai sus; in fiecare apel, buf se
refera la
membrul buf al obiectului pentru care se apeleaza functia.
Este de asemenea posibil sa ne referim explicit la acel obiect
printr-un
pointer numit this. Intr-o functie membru al unei clase X, acesta este
implicit declarat ca X* (pointer spre X) si initializat cu un pointer
spre
obiectul pentru care functia este apelata. Definitia lui ostream::put()

ar
putea fi scrisa astfel:
void ostream::put(char* p)
{
while(*p)
this->buf.sputc(*p++);
}

Operatorul -> se utilizeaza pentru a selecta un membru al unui
obiect.

1.8 Operatorul overloading
----------------------

Clasa reala ostream defineste operatorul << pentru a-l face
convenabil sa
scrie diferite obiecte cu o singura instructiune.
Pentru a defini @, unde @ este orice operator C++ pentru un tip
definit
de utilizator, noi definim o functie numita operator@ care are
argumente de
tip corespunzator. De exemplu:

class ostream{ //........
ostream operator<<(char*);
};
ostream ostream::operator<<(char* p)
{
while(*p)
buf.sputc(*p++);
return *this;
}

defineste operatorul <<, ca membru al clasei ostream, asa ca s<<p se
interpreteaza ca s.operator<<(p) cind s este un ostream si p este un
pointer
spre caractere. Operatorul << este binar, dar functia operator<<(char*)

pare
la prima vedere sa aiba un singur argument; el totusi are de asemenea
argumentul standard implicit this.
Revenind din ostream ni se permite sa aplicam << la rezultatul
unei
operatii de iesire. De exemplu, s<<p<<q se interpreteaza
(s.operator<<(p)).operator<<(q). Acesta este modul in care operatiile
sint
furnizate pentru tipuri predefinite.
Utilizind setul de operatii furnizate cu membri publici ai clasei
ostream, noi putem acum defini << pentru tipuri utilizator cum ar fi
cel
complex, fara a modifica declaratia clasei ostream:

ostream operator<<(ostream s, complex z)
// un complex are doua parti: real si imaginar
// se scrie un complex ca (real, imag)
{
return s << "(" << z.real << "," << z.imag << ")";
}

Intrucit operator<<(ostream, complex) nu este o functie membru,
este
necesar sa explicitam argumentele ca sa fie binare.
Se vor scrie valorile in ordine corecta caci <<, ca si majoritatea
operatorilor C++ se grupeaza de la stinga la dreapta; adica a<<b<<c
inseamna
(a<<b)<<c.
Compilatorul cunoaste diferenta dintre functiile membru si
nemembru cind
interpreteaza operatorii. De exemplu, daca z este o variabila complexa,

s<<z
va fi expandata utilizind apelul functiei standard (nemembru)
operator<<(s,z).

1.9 Referinte
---------

Ultima versiune a lui ostream din nefericire contine o eroare
serioasa.
Problema este ca ostream este copiat de doua ori pentru fiecare
utilizare a
lui <<: odata ca un argument si odata ca valoare returnata. Aceasta
lasa
starea nemodificata dupa fiecare apel. Este nevoie de o facilitate
pentru a
pasa un pointer la ostream mai degraba decit sa se paseze insasi
ostream.
Aceasta se poate realiza utilizind referintele. O referinta
actioneaza ca
un nume pentru un obiect; T& inseamna referinta la T. O referinta
trebuie
initializata si devine un nume alternativa pentru obiectul cu care este
initializat. De exemplu:
ostream& s1 = my_out;
ostream& s2 = cout;
Referintele s1 si my_out pot fi utilizate acum in acelasi mod si cu
acelasi
inteles. De exemplu, atribuirea:
s1 = s2;
copiaza obiectul referit prin s2 (adica cout) in obiectul referit prin
s1
(adica my_out). Membri se selecteaza utilizind operatorul punct:
s1.put("don't use ->");
si daca utilizam operatorul adresa, primim adresa obiectului referit:

&s1 == &my_out

Prima utilizare evidenta a referintei este ca sa ne asiguram ca
adresa
unui obiect, mai degraba decit obiectul insusi, este pasata la o
functie de
iesire (aceasta se numeste in anumite limbaje apel prin referinta):

ostream& operator<<(ostream& s, complex z)
{
return s << "(" << z.real << "," << z.imag << ")";
}

Corpul functiei este neschimbat dar asignarea facuta lui s va
afecta acum
obiectul dat ca argument. In acest caz, returnindu-se o referinta de
asemenea
se imbunatateste eficienta, intru- cit modul evident de implementare a
unei
referinte este un pointer si un pointer este mai ieftin sa fie
transferat
decit o structura mare.
Referintele sint de asemenea esentiale pentru definirea sirurilor
de
intrare deoarece operatorului input i se da variabila in care se
citeste ca
operand. Daca referintele nu sint utilizate, utilizatorul ar trebui sa
paseze
pointeri expliciti functiilor de intrare:

class istream{ //........
int state;
public:
istream& operator>>(char&);
istream& operator>>(char*);
istream& operator>>(int&);
istream& operator>>(long&);
//........
};

Sa observam ca se folosesc doua operatii separate pentru a citi
intr-o
zona long si intr-o zona int si numai una pentru scriere. Motivul este
ca un
int poate fi convertit spre long prin regulile implicite de conversie.


1.10 Constructori
------------
Definirea lui ostream ca si clasa, face ca datele membru sa fie
private.
Numai o functie membru poate accesa membri privati, asa ca noi trebuie
sa
furnizam una pentru initializare. O astfel de functie se numeste
constructor
si se distinge avind acelasi nume ca si al clasei lui:
class ostream{ //.......
ostream(streambuf*);
ostream(int size, char* s); };
Aici se furnizeaza doi constructori. Unul ia un streambuf pentru
o
iesire reala iar celalalt ia o dimensiune si un pointer spre caractere
pentru
formatarea sirului. Intr-o declaratie, argumentul lista necesar pentru
un
constructor se adauga la nume. Noi putem declara acum streamuri astfel:
ostream my_out(&some_stream_buffer);
char xx[256];
ostream xx_stream(256,xx);
Declaratia lui my_out seteaza nu numai cantitatea corespunzatoare

de
memorie ci de asemenea apeleaza si constructorul
ostream::ostream(streambuf*)
pentru a-l initializa cu argumentul &some_stream_buffer, care este un
pointer
spre un obiect potrivit al clasei streambuf. Declaratia functiei
xx_stream()
se trateaza similar, dar utilizeaza celalalt constructor. Declarind
construc-
tori pentru o clasa nu furnizam numai un mod de a initializa obiecte,
ci de
asemenea se asigura ca toate obiectele clasei vor fi initializate. Cind

s-a
declarat un constructor pentru o clasa, nu este posibil sa se declare o
variabila a acelei clase fara a apela un constructor. Daca o clasa are
un
constructor care nu ia argumente, acel constructor va fi apelat daca nu

se da
nici un argument in declaratie.


1.11 Vectori
-------

Conceptul de vector construit in C++ a fost proiectat pentru a
permite o
eficienta maxima la executie si memorie minima.
Este de asemenea, mai ales cind se utilizeaza impreuna cu
pointerii, un
instrument puternic pentru construirea unor facilitati de nivel inalt.
Noi
putem, totusi, sa ne plingem de faptul ca dimensiunea unui vector
trebuie sa
fie specificata ca o constanta, ca nu exista verificarea depasirii
limitelor
vectorilor, etc.. Un raspuns la aceste plingeri este: noi insine putem
programa acest lucru. Sa vedem daca acesta este un raspuns rezonabil,
cu alte
cuvinte, sa testam facilitatile de abstractizare ale lui C++ incercind
sa
furnizam aceste caracteristici pentru tipurile de vectori proiectati de

noi si
sa observam dificultatile implicate, costurile implicate si comoditatea
utilizarii tipurilor de vectori rezultate.

class vector{
int* v;
int sz;
public:
vector(int); //constructor
~vector(); //destructor
int size(){return sz;}
void set_size(int);
int& operator[](int);
int& elem(int i){return v[i];}
};

Functia size() returneaza numarul de elemente al vectorului;
adica,
indicii trebuie sa fie in domeniul 0..size()-1. Functia set_size() este
furnizata pentru a schimba acea dimensiune, elem() furnizeaza acces la
membri
fara a verifica indexul, iar operator[] furnizeaza acces cu verificarea
limitelor.
Ideea este de a avea clasa ca o structura de dimensiune fixa care
controleaza accesul la memoria reala a vectorului, care este alocata
prin
constructorul vectorului utilizind operatorul new de alocare de
memorie.

vector::vector(int s)
{if(s<=0)
error("bad vector size");
sz = s;
v = new int[s];
}
Noi putem declara vectori foarte asemanator cu vectorii care sint
construiti in limbajul insusi:

vector v1(100);
vector v2(nelem*2-4);

Operatia de acces poate fi definita ca:

int& vector::operator[](int i)
{
if(i<0 || sz<=i)
error("vector index out of range");
return v[i];
}

Returnind o referinta se asigura ca notatia [] poate fi utilizata
de
ambele parti a unei atribuiri:

v1[x] = v2[y];

Functia ~vector() este un destructor; adica este o functie
declarata
pentru a fi apelata implicit cind obiectul unei clase iese in afara
domeniului. Destructorul pentru o clasa C se numeste ~C. Daca noi o
definim
astfel:
vector::~vector()
{
delete v;
}

ea va fi utilizata pentru a sterge operatorul si pentru a dezaloca
spatiul
alocat prin constructor, asa ca atunci cind un vector iese afara din
domeniu,
tot spatiul lui este eliberat si poate fi reutilizat.

1.12 Expandare inline
----------------

O functie membru nu este mai costisitoare la apel decit o functie
nemembru cu acelasi numar de argumente (sa ne amintim ca o functie
membru
totdeauna are cel putin un argument), iar apelul functiilor C++ este
aproximativ tot atit de eficient ca si in alte limbaje. Totusi, pentru
functiile extrem de mici, apelul poate sa iasa in evidenta. Daca este
asa, noi
am putea dori sa specificam o functie care sa expandeze in linie. In
caz
afirmativ, compilatorul va genera cod propriu pentru functie in locul
apelului. Semanticile apelului ramin neschimbate. De exemplu, daca
size() si
elem() sint substituite in linie:
vector s(100);
//..........
i = s.size();
x = elem(i-1);
este echivalent cu
i = 100;
x = s.v[i-1];

Compilatorul este destul de abil pentru a genera un cod care este
tot
atit de bun ca si cel care se obtine direct prin macro expandare.
Evident,
compilatorul are nevoie uneori sa foloseasca variabile temporare si
alte
citeva abilitati pentru a prezerva semanticile.
Noi dorim o indicatie a faptului ca functia se expandeaza inline
care sa
preceada definitia ei. Aceasta este cuvintul cheie inline sau pentru o
functie
membru, pur si simplu prin includerea definitiei functiei in
declaratiile
clasei, asa cum s-a facut pentru size() si elem() in exemplul
precedent.
Cind se utilizeaza bine, functiile inline maresc simultan viteza
de
executie si descresc dimensiunea codului obiect. Totusi, functiile
inline din
declaratiile clasei pot incetini compilarea, asa ca ele trebuie sa fie
eliminate cind ele nu sint necesare. Pentru ca substitutia inline sa
fie un
beneficiu semnificativ pentru o functie, functia trebuie sa fie foarte
mica.

1.13 Clase derivate
--------------

Acum sa definim un vector pentru care un utilizator poate defini
limitele
indexului.

class vec:public vector{
int low, high;
public:
vec(int,int);
int& elem(int);
int& operator[](int);
};
Definind vec ca public vector inseamna inainte de toate ca un vec
este un
vector. Adica tipul vec are toate proprietatile tipului vector si in
plus cele
specific declarate pentru el. Clasa vector se spune ca este clasa de
baza
pentru vec si invers vec se spune ca este derivat din vector.
Clasa vec modifica clasa vector furnizind un constructor diferit
care
cere utilizatorului sa specifice cele doua limite ale indexului in
schimbul
dimensiunii si produce accesul propriu functiilor elem(int) si
operator[](int).elem() a lui vec se exprima usor in termenii lui elem()

al lui
vector:

int& vec::elem(int i){ return vector::elem(i-low); }

Scopul operatorului :: este de a elimina o recursivitate infinita
calculind vec::elem() din el insusi. Unarul :: se poate folosi pentru a

ne
referi la nume nelocale. Ar fi rezonabil sa declaram vec::elem()
inline din
motive de eficienta, dar nu este necesar, sau este posibil sa-l scriem
asa ca
el sa utilizeze direct membrul privat V al clasei vector. Functiile
unei clase
derivate nu au nici un acces special la membri privati ai clasei de
baza
propri. Constructorul poate fi scris astfel:

vec::vec(int lb, int hb) : (hb-lb+1)
{
if(hb-lb < 0)
hb = lb;
low = lb;
high = hb;
}

Constructia: (hb-lb+1) se utilizeaza pentru a specifica lista argument
pentru
constructorul clasei de baza vector::vector(). Acest constructor se
apeleaza
inaintea corpului lui vec::vec(). Iata un mic exemplu care poate fi
executat
daca se compileaza cu
restul declaratiilor lui vector:

#include <stream.h>

void error(char* p)
{
cerr << p << "\n"; // cerr is the error output stream
exit(1);
}


void vector::set_size(int)
{
/* dummy */
}


int& vec::operator[](int i)
{
if(i<low || high<i)
error("vec index out of range");
return elem(i);
}


main()
{
vector a(10);
for(int i=0; i<a.size(); i++)
{
a[i] = i;
cout << a[i] << " ";
}
cout << "\n";
vec b(10, 19);
for(i=0; i<b.size(); i++)
b[i+10] = a[i];
for(i=0; i<b.size(); i++)
cout << b[i+10] << " ";
cout << "\n";
}


Acesta produce:


0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9


1.14 Mai mult despre operatori
-------------------------

O alta directie a dezvoltarii este de a furniza vectori cu
operatii:

class Vec::public vector{
public:
Vec(int s) : (s){ }
Vec(Vec&);
~Vec(){ }
void operator=(Vec&);
void operator*=(Vec&);
void operator*=(int);
//......
};

Observam modul in care constructorul pentru clasa derivata
Vec::Vec()
este definit pentru a transfera argumentele lui la constructorul pentru

clasa
de baza vector::vector(). Operatorul de atribuire este supraincarcat si

poate
fi definit astfel:

void Vec::operator=(Vec& a)
{
int s = size();
if(s != a.size())
error("bad vector size for =");
for(int i=0; i<s; i++)
elem(i) = a.elem(i);
}

Atribuirea de Vec-uri acum copiaza elemente, in timp ce atribuirea

de
vectori copiaza pur si simplu structura care controleaza accesul la
elemente.
Totusi, ultima se intimpla cind se copiaza un vector fara utilizarea
explicita
a operatorului de atribuire:

(1) cind un vector este initializat prin atribuirea unui alt
vector;
(2) cind un vector se paseaza ca argument;
(3) cind un vector se paseaza ca valoare returnata de la o
functie.

Pentru a cistiga control in aceste cazuri pentru vectorii Vec, noi
definim
constructorul Vec(Vec&):

Vec::Vec(Vec& a) : (a.size())
{
int sz = a.size();
for(int i=0; i<sz; i++)
elem(i) = a.elem(i);
}

Acest constructor initializeaza un Vec ca o copie a altuia si va fi
apelat in
cazurile mentionate precedent. Pentru operatori de forma = si +=,
expresia din
stinga este evident speciala si se pare natural ca ei sa se
implementeze ca
operatii asupra obiectelor notate prin acea expresie. In particular,
este
posibil pentru ei sa se schimbe valoarea primului lor operand. Pentru
operatori de forma + si -, operandul sting nu necesita o atentie
speciala. Noi
am putea, de exemplu, sa transferam ambele argumente prin valoare si
totusi sa
capatam o implementare corecta a adunarii vectorilor. Vectorii pot fi
mari,
asa ca, pentru a elimina copierea, operanzii lui + se transfera
operatorului
operator+() prin referinta:

Vec operator+(Vec& a, Vec& b)
{
int s = a.size();
if(s != b.size())
error("bad vector size for +");
Vec sum(s);
for(int i=0; i<s; i++)
sum.elem(i) = a.elem(i) + b.elem(i);
return sum;
}

Iata un mic exemplu care poate fi executat daca se compileaza cu
declaratiile de vector prezentate anterior:

#include <stream.h>

void error(char* p)
{cerr << p << "\n";
exit(1);
}
void vector::set_size(int){ /*...*/ }
void vec::operator[](int i){ /*...*/ }

main()
{Vec a(10);
Vec b(10);
for(int i=0; i<a.size(); i++)
a[i] = i;
b = a;
Vec c = a+b;
for(i=0; i<c.size(); i++)
cout << c[i] << "\n";
}


1.15 Prieteni (Friends)
------------------

Functia operator+() nu opereaza direct asupra reprezentarii unui
vector;
intr-adevar, nu ar putea, deoarece nu este un membru. Totusi, uneori
este de
dorit ca sa se admita ca functii nemembru sa aiba acces la partea
privata a
unui obiect de clasa. De exemplu, neexistind functia cu acces
"neverificat",
vector:: elem(), noi ar trebui sa fortam verificarea indexului i fata
de
limitele vectorului de trei ori de fiecare data cind se executa ciclul.
Aceasta problema a fost eliminata aici, dar ea este tipica, asa ca
exista un
mecanism pentru o clasa care sa accepte accesul la partea sa privata
pentru o
functie nemembru.
O declaratie a unei functii precedate prin cuvintul cheie friend
este pur
si simplu plasata in declaratia clasei. De exemplu, dindu-se:

class Vec; // Vec este un nume de clasa
class vector{
friend Vec operator+(Vec, Vec);
//...........
};

noi putem scrie:

Vec operator+(Vec a, Vec b)
{
int s = a.size();
if(s != b.size())
error("bad vector size for +");
Vec& sum = *new Vec(s);
int* sp = sum.v;
int* ap = a.v;
int* bp = b.v;
while(s--)
*sp++ = *ap++ + *bp++;
return sum;
}

Un aspect particular util al mecanismului de prieten (friend) este

ca o
functie poate fi prieten a doua sau mai multe clase. Pentru a vedea
aceasta,
sa consideram definirea unui vector si a unei matrici si apoi definirea
functiei de inmultire.

1.16 Vectori generici
----------------

Noi am dori, de exemplu, unul din acei vectori pentru tipul
matrice pe
care l-am definit. Din nefericire, C++ nu furnizeaza o facilitate
pentru a
defini o clasa vector cu tipul elementelor ca argument. Un mod de a
proceda ar
fi sa se copieze atit definitia clasei cit si functiile membru. Acest
lucru nu
este ideal, dar adesea este acceptabil. Noi putem utiliza macroprocesor

pentru
a mecaniza acel task. De exemplu, clasa vector este o versiune
simplificata a
unei clase care poate fi gasita intr-un fisier header standard. Noi am
putea
scrie:

#include <vector.h>
declare(vector, int);
main()
{
vector (int)vv(10);
vv[2] = 3;
vv[10] = 4; //eroare de rang
}

Fisierul vector.h defineste macrouri asa ca declare(vector, int) se
expandeaza
spre declaratia unei clase vector foarte asemanatoare cu cea definita,
iar
implement(vector, int) se expandeaza spre definitiile functiilor acelei
clase.Intrucit implement(vec- tor, int) se expandeaza in definitii de
functii,
el poate fi utilizat numai odata intr-un program, in timp ce
declare(vector,
int) trebuie sa fie utilizat odata in fiecare fisier care manipuleaza
acest
fel de vectori intregi.
declare(vector, int);
//......
implement(vector, char);

da un tip (separat) "vector de caractere".

1.17 Vectori polimorfici
-------------------
O alta varianta ar fi ca sa definim vectorul nostru si cu alte
clase
container in termenii unor pointeri la obiectele unei anumite clase:
class common{ /*........*/};
class vector{
common** v;
//......
public:
cvector(int);
common*& elem(int);
common*& operator[](int);
//......
};

Sa observam ca deoarece pointerii si nu obiectele insasi sint
memorati
intr-un astfel de vector, un obiect poate fi "in" diferiti astfel de
vectori
in acelasi timp. Aceasta este o caracteristica foarte utila pentru
clasele
container de felul vectorilor, listelor inlantuite, multimilor, etc..
Mai mult
decit atit, un pointer la o clasa derivata poate fi atribuit la un
pointer
spre clasa ei de baza, asa ca cvector de mai sus poate fi utilizat
pentru a
pastra pointeri spre obiectele tuturor claselor derivate din common. De
exemplu:

class apple : public common{ /*...*/ };
class orange : public common{ /*...*/ };
class apple_vector : public cvector{
public:
cvector fruitbowl(100);
//......
apple aa;
orange oo;
fruitbowl[0] = &aa;
fruitbowl[1] = &oo;
};

Totusi, tipul exact al unui obiect intr-o astfel de clasa
container nu
mai este cunoscut de compilator. De exemplu, in exemplul precedent noi
stim ca
un element al vectorului este un common, dar este un apple sau un
orange ? In
mod obisnuit, tipul exact trebuie sa fie descoperit mai tirziu pentru a

putea
utiliza corect obiectul. Pentru a face aceasta, noi trebuie sau sa
memoram o
anumita forma a tipului de informatie in obiectul insusi sau sa ne
asiguram ca
numai obiectele unui tip dat se pun in container. Ultima varianta este
atinsa
trivial utilizind o clasa derivata. De exemplu, noi am putea face un
vector de
pointeri apple:
class apple_vector : public cvector{
public:
apple*& elem(int i)
{ return (apple*&)cvector::elem(i); }
//......... };
utilizind notatia de type_casting.
common*& (o referinta la pointer spre common) returnat prin
cvector::elem
spre apple*&. Aceasta utilizare a claselor derivate furnizeaza o
alternativa a
claselor generice. Este putin mai greu sa scriem in acest fel (daca nu
sint
utilizate macrouri asa incit clasele derivate sa fie de fapt utilizate
pentru
a implementa clase generice), dar are avantajul ca toate clasele
derivate au
in comun o singura copie a functiilor clasei de baza.
Pentru o clasa generica de felul lui vector(type), trebuie sa se
faca o
noua copie a acelor functii (prin implement()) pentru fiecare tip nou
utilizat.
Alternativa de a memora identificatorul tipului in fiecare obiect
ne
conduce spre un stil de programare adesea referit ca bazat sau orientat

spre
obiect.

1.18 Functii virtuale
----------------
Sa consideram scrierea unui program pentru afisarea formelor pe un

ecran.
Atributele comune ale formelor se reprezinta prin clasa shape, atribute
specificate prin clase derivate specifice:
class shape{point center;
color col;
//.......
public:
void move(point to)
{
center = to;
draw();
}
point where(){ return center; }
virtual void draw();
virtual void rotate(int);
//........
};
Functiile care pot fi definite fara cunostinte despre forma
specifica (de
exemplu move si where), pot fi declarate in mod obisnuit. Restul se
declara
virtual, adica se vor defini intr-o clasa derivata. De exemplu:
ch;
int count = 1;
char* name = "Bjarne";
struct complex{ float re,im; }
complex cvar;
extern complex sqrt(complex);
extern int error_number;
typedef complex point;
float real(complex* p){ return p->re; };
const double pi = 3.1415926535897932385;
struct user;
Majoritatea acestor declaratii sint de asemenea si definitii;
adica ele
definesc o entitate pentru numele la care se refera. Pentru ch, count
si cvar,
aceasta entitate este o cantitate corespunzatoare de memorie care sa se
utilizeze ca o variabila. Pentru real, entitatea este o functie
specifica.
Pentru constanta pi entitatea este o valoare 3.1415... . Pentru
complex,
entitatea este un tip nou. Pentru point, entitatea este tipul complex
asa ca
point devine un sinonim pentru complex. Numai declaratiile
extern complex sqrt(complex);
extern int error_number;
struct user;

nu sint si definitii. Adica, entitatile la care se refera ele trebuie
sa fie
definita altundeva. Codul pentru functia sqrt() trebuie sa fie
specificat
printr-o anumita alta declaratie, memoria pentru variabila error_number

de tip
intreg trebuie sa fie alocata printr-o anumita alta declaratie a lui
error_number, iar o anumita alta declaratie a tipului user trebuie sa
defineasca cum arata acel tip. Trebuie totdeauna sa fie exact o
definitie
pentru fiecare nume dintr-un program C++, dar pot fi multe declaratii
si toate
declaratiile trebuie sa fie compatibile cu tipul entitatii referite,
asa ca
fragmentul de mai jos are doua erori:
int count;
int count; // error : redefinition
extern int error_number;
extern short error_number; // error : type mismatch

Anumite definitii specifica o "valoare" pentru entitatile pe care
le
definesc ele:
struct complex{ float re,im; };
typedef complex point;
float real(complex* p){ return p->re };
const double pi=3.1415926535897932385;
Pentru tipuri, functii si constante "valoarea" este permanenta.
Pentru
tipuri de date neconstante valoarea initiala poate fi schimbata
ulterior:

int count = 1;
char* name = "Bjarne";
//................
count = 2;
name = "Marian";

Numai definitia
char ch;
nu specifica o valoare. Orice declaratie ce specifica o valoare este o
definitie.


2.1.1 Domeniu
-------

O declaratie introduce un nume intr-un domeniu; adica un nume
poate fi
utilizat numai intr-o parte specifica a textului programului. Pentru un

nume
declarat intr-o functie (adesea numit nume local), domeniul lui se
extinde din
punctul declaratiei pina la sfirsitul blocului in care apare declaratia

lui.
Pentru un nume care nu este intr-o functie sau intr-o clasa (adesea
numit nume
global), domeniul se extinde din punctul declaratiei pina la sfirsitul
fisierului in care apare declaratia lui. Un nume poate fi redefinit
intr-un
bloc pentru a referi o entitate diferita in blocul respectiv. Dupa
iesirea din
bloc numele isi reia intelesul lui precedent. De exemplu:

int x; //global x
f()
{
int x; //local x. Ascunde globalul x
x = 1; //asignarea la x local
{
int x; //ascunde primul local x
x = 2; //asignarea la cel de al doilea local
}
x = 3; //asignarea la primul local x
}
int* p = &x; //ia adresa globalului x

Ascunderea numelor este inevitabila cind se scriu programe mari.
Totusi,
un cititor poate usor sa nu observe ca un nume a fost ascuns si erorile
datorate acestui fapt sint foarte dificil de gasit. In consecinta ar
trebui
minimizat numarul numelor ascunse. Utilizarea numelor de felul lui i si

x
pentru variabile globale sau locale in functii mari poate sa ne conduca

la
erori.
Este posibil sa se utilizeze un nume global ascuns utilizind
operatorul
de rezolutie a domeniului "::". De exemplu:
int x;
f()
{int x = 1; // ascunde globalul x
::x = 2; // asigneaza lui x global
}
Nu exista un mod de a utiliza un nume local ascuns.
Domeniul unui nume incepe in punctul declaratiei lui; aceasta
inseamna ca
un nume poate fi utilizat chiar pentru a specifica valoarea lui
initiala. De
exemplu:

int x;
f(){ int x = x; }

Aceasta este legal dar este fara sens.
Este posibil sa utilizam un singur nume pentru a ne referi la doua
obiecte diferite intr-un bloc fara a utiliza operatorul "::". De
exemplu:

int x = 11;
f()
{
int y = x; // global x
int x = 22;
y = x; // local x
}

Variabila y este initializata cu 11, valoarea globalului x, iar
apoi i se
atribuie valoarea 22 a variabilei locale x.
Numele argumentelor unei functii se considera declarate in blocul
cel mai
exterior functiei, deci
f(int x)
{
int x; // eroare
}
eroare, deoarece x este declarat de doua ori in acelasi domeniu.


2.1.2 Obiecte si Lvalori
-----------------

Se pot aloca si utiliza "variabile" ce nu au nume si este posibil
sa se
faca atribuiri la expresii care arata straniu (de exemplu *p[a+10]=7).
In consecinta, exista nevoia de nume pentru "ceva aflat in
memorie". Iata
ghilimelele corespunzatoare pentru a face referire la manualul C++: "Un

obiect
este o regiune de memorie; o lvaloare este o expresie care refera un
obiect"
(&r.5). Cuvintul lvalue original a fost desemnat pentru a insemna "ceva

ce
poate fi in partea stinga a unei atribuiri". Totusi, nu orice lvalue
poate fi
utilizata in partea stinga a unei atribuiri; se poate avea o lvaloare
care
face referire la o constanta (vezi &2.4).

2.1.3 Durata de viata
---------------
Daca programatorul nu specifica altfel, un obiect este creat cind
definitia lui este intilnita si distrus cind numele lui iese afara din
domeniu. Obiectele cu nume globale se creeaza si se initializeaza numai

odata
si "traiesc" pina cind se termina programul. Obiectele definite
printr-o
declaratie cu cuvintul cheie static se comporta la fel. De exemplu,
(directiva
#include <stream.h> a fost lasata afara din exemplele din acest capitol

pentru
a face economie de spatiu. Este necesar sa fie prezenta pentru
exemplele care
produc iesiri):

int a = 1;
void f()
{
int b = 1; //se initializeaza la fiecare apel a lui f()
static int c = 1; //se initializeaza numai odata
cout << "a=" << a++ << "b=" << b++ <<"c=" <<c++ <<"\n";
}

main()
{
while(a < 4)
f();
}

produce iesirea:
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3

O variabila statica care nu este explicit initializata este
initializata
cu zero (&2.4.5).
Utilizind operatorii new si delete, programatorul poate crea
obiecte a
caror durata de viata poate fi controlata direct (&3.2.4).

2.2 Nume
----

Un nume (identificator) consta dintr-un sir de litere si cifre.
Primul
caracter trebuie sa fie litera. Caracterul subliniere _ se considera a
fi o
litera. C++ nu impune limite asupra numarului de caractere dintr-un
nume, dar
anumite implementari nu sint sub controlul scriitorului de
compilatoare (in
particular, incarcatorul). Anumite medii de executie sint de asemenea
nece-
sare pentru a extinde sau restringe setul de caractere acceptat intr-un
identificator; extensiile (de exemplu, cele care admit caracterul $
intr-un
nume) produc programe neportabile. Un cuvint cheie C++ (vezi &r.2.3)
nu poate
fi utilizat ca un nume. Exemple de nume:

hello this_is_a_most_unusually_long_name
DEFINED fo0 bAr u_name HorseSence
var0 var1 CLASS _class ___

Exemple de siruri de caractere care nu pot fi utilizate ca
identificatori:
012 a fool $sys class 3var
pay.dul foo-bar .name if

Literele mari si mici sint distincte, asa ca Count si count sint
nume
diferite, dar nu este indicat sa se aleaga nume care difera numai putin

unul
de altul. Numele care incep cu subliniere se utilizeaza de obicei
pentru
facilitati in mediul de executie
si de aceea nu se recomanda sa se utilizeze astfel de nume in
programele
aplicative.
Cind compilatorul citeste un program, el totdeauna cauta cel mai
lung sir
care poate forma un sir, asa ca var10 este un singur nume si nu numele
var
urmat de numarul 10, iar elseif un singur nume, nu cuvintul cheie else
urmat
de if.

2.3 Tipuri
------

Orice nume (identificator) dintr-un program C++ are un tip asociat

cu el.
Acest tip determina ce operatii pot fi aplicate asupra numelui (adica
la
entitatea referita prin nume) si cum se interpreteaza aceste operatii.
De
exemplu:

int error_number;
float real(complex* p);

Intrucit error_number este declarat sa fie int, lui i se pot face
atribuiri,
poate fi folosit in expresii aritmetice, etc..
Functia real, pe de alta parte, poate fi aplicata cu adresa unui
complex
ca parametru al ei. Este posibil sa se ia adresa oricaruia din ei.
Anumite
nume, cum ar fi int si complex, sint nume de tipuri. Un nume de tip
este
utilizat pentru a specifica tipul unui alt nume intr-o declaratie.
Singura
alta operatie asupra unui nume de tip este sizeof (pentru a determina
cantita-
tea de memorie necesara pentru a pastra un obiect de acel tip) si new
(pentru
alocare de memorie libera pentru obiectele de tipul respectiv). De
exemplu:

main()
{
int* p = new int;
cout << "sizeof(int) =" << sizeof(int) << "\n";
}

Un nume de tip poate fi utilizat ca sa specifice explicit
conversia de la
un tip la altul (&3.2.4). De exemplu:

float f;
char* p;
long ll = long(p); // converteste p spre long
int i = int(f); // converteste f spre int

2.3.1 Tipuri fundamentale
-------------------

C++ are un set de tipuri fundamentale ce corespund la cele mai
comune
unitati de memorie ale calculatoarelor si la cele mai fundamentale
moduri de
utilizare ale lor.
char
short int
int
long int , pentru a reprezenta intregi de diferite dimensiuni;
float
double ,pentru a reprezenta numere in flotanta;
unsigned char
unsigned short int
unsigned int
unsigned long int ,pentru a reprezenta intregi fara semn,

valori
logice, vectori de biti, etc..
Pentru o notatie mai compacta, int poate fi eliminat dintr-o
combinatie
de multicuvinte (de exemplu short este de fapt short int) fara a
schimba
intelesul; astfel long inseamna long int iar unsigned inseamna unsigned

int.
In general, cind un tip este omis intr-o declaratie, se presupune ca
s-a omis
int. De exemplu:

const a = 1;
static x;

fiecare defineste un obiect de tip int.
Intregul de tip caracter este cel mai potrivit pentru a mentine si
manipula caractere pe un calculator dat; acest tip este de obicei pe 8
biti.
Dimensiunile obiectelor din C++ sint exprimate in termeni multipli ai
dimensiunii lui char, asa ca, prin definitie sizeof(char) = 1.
Depinzind de
hardware, un char este un intreg cu sau fara semn. Tipul caracter fara
semn
este sigur totdeauna fara semn (unsigned char) si utilizindu-l produce
programe mai portabile, dar poate sa fie mai putin eficient decit daca
este
folosit ca tip char obisnuit.
Motivul pentru a funiza mai multe tipuri de intregi, mai multe
tipuri de
intregi fara semn si mai multe tipuri de flotante este pentru a permite
programatorului sa utilizeze avantajos caracteristicile hardware. Pe
multe
masini exista diferente semnificative in cerintele de memorie, timpul
de acces
la memorie si viteza de calcul dintre diferite varietati a tipurilor
funda-
mentale. Cunoscind o masina, de obicei este usor a alege, de exemplu,
tipul de
intreg potrivit pentru o variabila particulara. A scrie cod de nivel
inferior
portabil cu adevarat este foarte greu. Ceea ce este garantat in
legatura cu
dimensiunile tipurilor fundamentale este:

sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
sizeof(float) <= sizeof(double)

Cu toate acestea, de obicei este rezonabil sa presupunem ca tipul
char
poate pastra intregi in intervalul 0..127 (el poate totdeauna pastra un
caracter din setul de caractere al masinii), ca un short si un int au
cel
putin 16 biti, ca un int este apropiat de o dimensiune potrivita pentru
aritmetica intregilor si ca un long are cel putin 24 de biti. A
presupune mai
mult este hazardos si chiar aceste reguli implicite nu se aplica
universal (o
tabela de caracteristici hardware pentru citeva masini se poate vedea
in
&r.2.6).
Tipurile de intregi fara semn sint ideale pentru a utiliza memoria

ca un
vector pe biti. Utilizarea unui intreg fara semn in locul unui int
pentru a
cistiga un bit in plus pentru a reprezenta intregi pozitivi aproape
totdeauna
nu este o idee buna. Incercarea de a ne asigura ca anumite valori sint
pozitive prin declararea variabilelor de tip unsigned va fi ignorata
prin
reguli implicite de conversie. De exemplu:

unsigned surprise = -1;

este legal (dar compilatorul va face un avertisment despre el).


2.3.2 Conversie implicita de tip
--------------------------

Tipurile fundamentale pot fi amestecate liber in expresii. Oricind

este
posibil, valorile se convertesc asa ca sa nu se piarda informatie
(regula
exacta poata fi gasita in &r.6.6).
Exista cazuri in care informatia se poate pierde sau chiar
distruge.
Atribuirea unei valori de un tip la o variabila de un alt tip cu biti
mai
putini in reprezentarea ei este in mod necesar o sursa potentiala de
erori. De
exemplu, sa presupunem ca secventa urmatoare se executa pe o masina in
care
intregii se reprezinta in complement fata de doi si caracterele pe 8
biti:

int i1 = 256 +255;
char ch = i1; //ch == 255
int i2 = ch; //i2 == ?

Un bit (cel mai semnificativ) este pierdut in atribuirea ch = i1 si ch
va
pastra toti bitii 1 (adica 8 biti de 1); deci nu exista o cale ca
acesta sa
poata deveni 511 cind se atribuie lui i2! Dar care ar putea fi valoarea

lui i2
? Pe VAX, unde un caracter este cu semn, raspunsul este 255. C++ nu are

un
mecanism la executie care sa detecteze un astfel de tip de problema,
iar
detectarea la compilare este prea dificila in general, asa ca
programatorul
trebuie sa fie atent la acest fapt.

2.3.3 Tipuri derivate
---------------

Din tipurile fundamentale (si din tipurile definite de utilizator)

se
pot deriva alte tipuri folosind operatorii de declaratie:
* pointer
& adresa
[] vector
() functie

si mecanismul de definitie de structura. De exemplu:

int* a;
float v[10];
char* p[20]; //vector de 20 de pointeri spre caractere
void f(int);
struct str{
short length;
char* p;
};

Regulile de compunere a tipurilor utilizind acesti operatori se
explica
in detaliu in &r8.3.4. Ideea de baza este ca declara-
rea unui tip derivat oglindeste utilizarea lui. De exemplu:

int v[10]; //declara un vector
i = v[3]; //utilizeaza un element al vectorului
int* p; //declaratie de pointer
i = *p; //utilizeaza obiectul spre care se pointeaza

Toate problemele in intelegerea notatiei pentru tipuri derivate
apar din
cauza faptului ca * si & sint operatori prefix iar [] si () sint
postfix, asa
ca parantezele trebuie sa fie utilizate pentru a exprima tipuri in care
precedenta operatorilor este incomoda. De exemplu deoarece [] are o
prioritate
mai mare decit *:

int *v[10]; //vectori de pointeri
int (*p)[10] //pointer spre vector

Poate fi plicticos sa utilizam o declaratie pentru fiecare nume pe

care
vrem sa-l introducem intr-un program, mai ales daca tipurile lor sint
identice. Este posibil sa declaram diferite nume intr-o singura
declaratie; in
locul unui singur nume, declaratia pur si simplu contine o lista de
nume
separate prin virgula. De exemplu, se pot declara doi intregi astfel:
int x, y; //int x; int y;

Cind declaram tipuri derivate, trebuie sa observam ca operatorii
se
aplica numai la nume individuale (si nu la orice alte nume din aceeasi
declaratie). De exemplu:
int* p, y; //int *p; int y; nu int *y;
int x, *p; //int x; int *p;
int v[10], *p; //int v[10]; int *p;

Opinia autorului este ca astfel de constructii fac un program mai
putin
lizibil si ar trebui eliminate.

2.3.4 Void
----

Tipul void se comporta sintactic ca un tip fundamental. El poate
totusi,
sa fie utilizat numai ca parte a unui tip derivat; nu exista obiecte de

tip
void. Este folosit pentru a specifica ca o functie nu returneaza o
valoare sau
ca tip de baza pentru pointeri spre obiecte de tip necunoscut.
void f(); //f nu returneaza o valoare
void* pv; //pointer spre un obiect de tip necunoscut

Un pointer spre orice tip poate fi atribuit la o variabila de tip
void*.
Pentru inceput acesta nu pare prea util, deoarece un pointer void* nu
poate
fi indirectat dar aceasta restrictie este exact ceea ce face ca tipul
void*
sa fie util. El se utilizeaza in primul rind pentru a transfera la
functii
pointeri despre care nu se poate face presupunere asupra tipului
obiectului
spre care pointeza si pentru a returna obiecte fara tip dintr-o
functie.
Pentru a utiliza un astfel de obiect, trebuie sa se utilizeze
conversia
explicita de tip. Astfel de functii de obicei exista la cel mai
inferior
nivel al sistemului unde se manipuleaza resurse hardware reale. De
exemplu:
void* allocate(int size);
void deallocate(void*);
f()
{int* pi = (int*)allocate(10 * sizeof(int));
char* pc = (char*)allocate(10);
//....
deallocate(pi);
deallocate(pc);
}

2.3.5 Pointeri
--------

Pentru cele mai multe tipuri T, T* este tipul pointer spre T. Adica

o
variabila de tipul T* poate pastra adresa unui obiect de tipul T.
Pentru
pointeri spre vectori si pointeri spre functii exista notatii mai
complicate:

int* pi;
char** cpp; //pointer spre pointer spre caractere
int (*vp)[10] //pointer spre vector de 10 elemente
int (*fp)(char,char*) //pointer spre o functie care are ca
//parametru (char, char*) si retur-
//neaza un int

Operatia fundamentala asupra unui pointer este indirectarea, adica
referirea la un obiect pointat printr-un pointer spre el. Operatorul de
indirectare este unarul * (prefixat). De exemplu:

char c1 = 'a';
char* p = &c1; //p pastreaza adresa lui c1
char c2 = *p; //c2 = 'a'

Variabila spre care pointeaza p este c1 si valoarea pastrata in c1 este

'a',
asa ca valoarea lui *p atribuita lui c2 este 'a'.
Este posibil sa se faca unele operatii aritmetice cu pointerii.
Iata de
exemplu o functie care calculeaza numarul de caractere dintr-un sir
(nesocotind 0 care termina sirul):

int strlen(char* p)
{
int i = 0;
while(*p++)
i++;
return i;
}

Un alt mod de a gasi lungimea este ca la inceput sa gasim
sfirsitul
sirului si apoi sa scadem adresa inceputului sirului din adresa
sfirsitului:

int strlen(char* p)
{
char* q = p;
while(*q++);
return(q-p-1);
}

Pointerii spre functii pot fi extrem de utili; ei se discuta in
(&4.6.7).


2.3.6 Vectori
-------

Pentru un tip T, T[size] este tipul "vector de size elemente de
tip T".
Elementele sint indexate de la 0 la size-1. De exemplu:
float v[3]; // un vector de 3 flotante: v[0],v[1],v[2]
int a[2][5]; // doi vectori de 5 intregi
char* vpc[32]; // vectori de 32 de pointeri spre caractere

Un ciclu pentru a scrie valori intregi pentru caracterele mici ar

putea
fi scris astfel:
extern int strlen(char*);
char alpha[] = "abcdefghijklmnopqrstuvwxyz";
main()
{int sz = strlen(alpha);
for(int i=0; i<sz; i++)
{
char ch = alpha[i];
cout << "'" << chr(ch) << "'" << "=" << ch << "=0"
<< oct(ch) << "=0x" << hex(ch) << "\n";
}
}
Functia chr() returneaza reprezentarea sub forma de caracter a unui
intreg
mic; de exemplu, chr(80) este "P" pe o masina care utilizeaza setul de
caractere ASCII. Functia oct() produce o reprezentare octala a
argumentului
sau intreg, iar hex() produce o reprezentare hexazecimala a
argumentului sau
intreg; chr(), oct() si hex() sint declarate in <stream.h>.
Functia strlen() a fost utilizata pentru a numara caracterele din
alpha
(vezi &2.4.4). Cind se utilizeaza setul de caractere ASCII, iesirea va

arata
astfel:
'a' = 97 = 0141 = 0x61
'b' = 98 = 0142 = 0x62
'c' = 99 = 0143 = 0x63

Sa observam ca nu este necesar sa se specifice dimensiunea
vectorului
alpha; compilatorul calculeaza numarul de caractere din sirul de
caractere
specificat ca initializator. Utilizind un sir ca un initializator
pentru un
vector de caractere este convenabil, dar din nefericire este unica
utilizare a
sirurilor. Nu exista o atribuire similara a unui sir la un vector. De
exemplu:

char v[9];
v = "a string"; // error

este o eroare deoarece atribuirea nu este definita pentru vectori.
Evident sirurile sint potrivite numai pentru a initializa vectori
de
caractere; pentru alte tipuri trebuie sa se utilizeze o notatie mai
laborioasa. Aceasta notatie poate fi de asemenea utilizata pentru
vectori de
caractere. De exemplu:

int v1[] = {1,2,3,4};
int v2[] = {'a','b','c','d'};
char v3[] = {1,2,3,4};
char v4[] = {'a','b','c','d'};


Observam ca v4 este un vector de 4 (nu 5) caractere; nu este
terminat
printr-un zero, asa cum cer prin conventie toate rutinele de
biblioteca.
Aceasta notatie este de asemenea restrin sa la obiecte statice.
Tablourile multidimensionale sint reprezentate ca vectori de
vectori si
notind cu virgula pentru a separa limitele ca in alte limbaje de
programare se
obtine la compilare o eroare deoarece virgula (,) este un operator de
succesiune (vezi &3.2.2). De exemplu, sa incercam:

int bad[5,2]; // error
int v[5][2]; //correct
int bad = v[5,2]; // error
int good = v[4][1]; // correct

O declaratie char v[2][5]; declara un vector cu doua elemente; fiecare
din ele
este un vector de tip char [5]. In exemplul urmator, primul din acei
vectori
este initializat cu primele 5 litere iar cel de al doilea cu primele 5
cifre:

char v[2][5] = {'a','b','c','d','e','0','1','2','3','4'};
main()
{
for(int i=0; i<2; i++)
{
for(int j=0; j<2; j++)
cout << "v[" << i << "][" << j
<< "]=" chr(v[i][j]) << " ";
cout << "\n";
}
}
va produce:
v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e
v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4

2.3.7 Pointeri si Vectori
-------------------

In C++, pointerii si vectorii sint foarte strinsi legati. Numele
unui
vector poate de asemenea, sa fie utilizat ca un pointer spre primul sau
element, asa ca exemplul cu alfabetul ar putea fi scris astfel:

char alpha[] = "abcdefghijklmnopqrstuvwxyz";
char* p = alpha;
char ch;
while(ch = *p++);
cout << chr(ch) << "=" << ch
<< "=0" << oct(ch) << "\n";

Declaratia lui p ar putea de asemenea sa fie scrisa:

char* p = &alpha[0];

Aceasta echivalenta este utilizata extensiv in apelurile de
functii, in
care un argument vector este totdeauna pasat ca un pointer la primul
element
al vectorului; astfel in acest exemplu:
extern int strlen(char*);
char v[] = "Annemarie";
char* p = v;
strlen(p);
strlen(v);
este transferata aceeasi valoare la strlen in ambele apeluri.
Rezultatul aplicarii operatorilor +, -, ++, -- la pointeri depinde

de
tipul obiectului spre care pointeaza pointerul. Cind un operator
aritmetic se
aplica la un pointer spre un tip T, p este presupus ca pointeaza spre
un
element al vectorului de obiecte de tip T; p+1 inseamna elementul
urmator al
acelui vector iar p-1 elementul precedent. Aceasta implica faptul ca
valoarea
lui p+1 va fi cu sizeof(T) mai mare decit valoarea lui p. De exemplu:

main()
{
char cv[10];
int iv[10];
char* pc = cv;
int* pi = iv;
cout << "char*" << long(pc+1) - long(pc) << "\n";
cout << "int*" << long(pi+1) - long(pi) << "\n";
}

va produce:

char* 1
int* 4

deoarece caracterele ocupa un octet fiecare si intregii ocupa fiecare 4

octeti
pe masina mea. Valorile pointer au fost convertite spre long inainte de

a face
scaderea utilizind conversia explicita de tip (&3.2.5). Ele au fost
convertite
spre long si nu spre tipul int deoarece exista masini pe care un
pointer nu
incape intr-un int (adica sizeof(int) < sizeof(char*)).
Scaderea de pointeri este definita numai cind ambii pointeri
pointeaza
spre elemente ale aceluiasi vector (desi limbajul nu are un mod de a se
asigura ca acest lucru este adevarat). Cind se scade un pointer
dintr-un
altul, rezultatul este numarul de elemente al vectorului dintre cei doi
pointeri (un intreg). Se poate adauga un intreg la un pointer sau

scadea un intreg
dintr-un pointer; in ambele cazuri rezultatul este o valoare pointer.
Daca
acea valoare nu pointeaza spre un element al aceluiasi vector, ca si
vectorul
initial, rezultatul utilizarii valorii respective este nedefinit. De
exemplu:

int v1[10];
int v2[10];
int i = &v1[5] - &v1[3]; // 2
i = &v1[5] - &v2[3]; // rezultat nedefinit
int* p = v2 + 2; // p==&v2[2]
p = v2 - 2; // *p nedefinit

2.3.8 Structuri
---------

Un vector este un agregat de elemente de un acelasi tip; o
structura este
un agregat de elemente de orice tip. De exemplu:

struct address{char* name; // "Jim Dandy"
long number; // 61
char* street; // "South St"
char* town; // "New Providence"
char state[2]; // 'N' 'J'
int zip; // 7974
};

defineste un tip nou numit address care consta din elementele de care
avem
nevoie pentru a trimite o scrisoare la cineva (address nu este in
general
destul pentru a gestiona toate scrisorile, dar este suficient pentru un
exemplu). Sa observam punct-virgula de la sfirsit este unul din foarte
putinele locuri din C++ unde este necesar sa o avem dupa o acolada
inchisa,
asa ca lumea este inclinata sa o uite.
Variabilele de tip adresa pot fi declarate exact ca si alte
variabile,
iar elementele individuale pot fi accesate utilizind operatorul
'.'(punct). De
exemplu:

address jd;
jd.name = "Jim Dandy";
jd.number = 61;

Notatia utilizata pentru initializarea vectorilor poate de
asemenea sa
fie utilizata pentru variabile de tip structura. De exemplu:

address jd = {"Jim Dandy",61,"South St","New Providence",
{'N','J'},7974};

Utilizind un constructor (&5.2.4) este de obicei mai bine. Sa
observam ca
jd.state nu poate fi initializat prin sirul "NJ". Sirurile sint
terminate prin
caracterul '\0' asa ca "NJ" are trei caractere, adica unul in plus
decit ar
incapea in jd.state.
Obiectele structura sint adesea accesate prin pointeri folosind
operatorul ->. De exemplu:
void print_addr(address* p)
{
cout << p->name << "\n" << p->number << " " << p->street
<< "\n" << p->town << "\n" << chr(p->state[0])
<< chr(p->state[1]) << " " << p->zip << "\n";
}

Obiectele de tip structura pot fi atribuite, pasate ca si
argumente la
functie si returnate ca rezultat al unei functii. De exemplu:
address current;
address set_current(address next)
{
address prev = current;
current = next;
return prev;
}
Alte operatii plauzibile cum ar fi compararea (== si !=) nu sint
definite. Cu toate acestea, utilizatorul poate defini astfel de
operatori
(vezi cap. 6).
Nu este posibil sa se calculeze dimensiunea unui obiect de tip
structura
pur si simplu insumind membri ei. Motivul pentru aceasta este ca multe
masini
necesita ca obiecte de un anumit tip sa fie alocate numai la anumite
adrese
(un exemplu tipic este faptul ca un intreg trebuie sa fie alocat la o
adresa
de cuvint) sau pur si simplu pentru a trata astfel de obiecte mult mai
eficient. Aceasta conduce spre "goluri" in structuri. De exemplu (pe
masina
mea): sizeof(address) este 24 si nu 22 cit ne-am astepta.
Sa observam ca numele unui tip devine disponibil pentru utilizare
imediat
dupa ce el a fost intilnit si nu numai dupa declararea completa. De
exemplu:

struct link{
link* previsious;
link* successor;
};

Nu este posibil sa se declare obiecte noi de tip structura pina
cind nu
s-a terminat complet declaratia, deci
struct no_good{ no_goog member; };

este o eroare (compilatorul nu este in stare sa determine dimensiunea
lui
no_good). Pentru a permite ca doua (sau mai multe) tipuri structura sa
se
refere unul la altul, pur si simplu se admite ca sa se declare ca un
nume este
numele unui tip structura. De exemplu:
struct list; // to be defined later
struct link{
link* pre;
link* suc;
list* member_of;
};
struct list{ link* head; };

Fara prima declaratie a lui list, declaratia lui link ar produce o

eroare
sintactica.


2.3.9 Echivalenta tipurilor
---------------------

Doua tipuri structura sint diferite chiar daca ele au aceeasi
membri. De
exemplu:
struct s1{ int a; };
struct s2{ int a; };

sint doua tipuri diferite, asa ca
s1 x;
s2 y = x; // error: type mismatch

Tipurile structura sint de asemenea diferite de tipurile
fundamentale,
asa ca:
s1 x;
int i = x; // error: type mismatch
Exista un mecanism pentru a declara un nume nou pentru un tip,
fara a
introduce un obiect nou. O declaratie prefixata prin cuvintul cheie
typedef
declara nu o noua variabila de un tip dat, ci un nume nou pentru tip.
De
exemplu:
typedef char* pchar;
pchar p1,p2;
char* p3 = p1; Aceasta poate fi o prescurtare convenabila.


2.3.10 Referinte
---------

O referinta este un nume pentru un obiect. Prima utilizare a
referintelor
este aceea de a specifica operatiile pentru tipuri definite de
utilizator (ele
se discuta in cap. 6). Ele pot fi de asemenea utile ca argumente de
functii.
Notatia X& inseamna referinta la X. De exemplu:
int i = 1;
int& r = i; // r si i acum se refera la acelasi obiect
int x = r; // x = 1
r = 2; // i = 2

O referinta trebuie sa fie utilizata (trebuie sa fie ceva pentru
ce este
el nume). Sa observam ca initializarea unei referinte este ceva cit se
poate
de diferit de atribuirea la ea.
In ciuda aparentelor, nici un operator nu opereaza asupra unei
referinte.
De exemplu:
int ii = 0;
int& rr = ii;
rr++; // ii se incrementeaza cu 1
este legal, dar r++ nu incrementeaza referinta rr; ++ se aplica la un
int,
care se intimpla sa fie ii. In consecinta, valoarea referintei nu poate

fi
schimbata dupa initializare; ea totdeauna se refera la obiectul cu care

a fost
initializata pentru a-l denumi. Pentru a primi un pointer spre obiectul

notat
prin referinta rr, se poate scrie &rr.
Implementarea unei referinte este un pointer (constant) care este
indirectat de fiecare data cind el este utilizat. Aceasta face
initializarea
unei referinte trivial cind initializatorul este o lvaloare (un obiect

la
care se poate lua adresa vezi &r5). Cu toate acestea, initializatorul
pentru
T& este necesar sa nu fie o lvaloare sau chiar de tip T. In astfel de
cazuri:
[1] Intii, se aplica conversia de tip daca este necesar
(vezi &r6.6.8 si &r8.5.6);
[2] Apoi valoarea rezultat este plasata intr-o variabila
temporara;
[3] In final, adresa acestuia se utilizeaza ca valoare a
initializatorului.

Consideram declaratia:
double& dr = 1;

Interpretarea acesteia este:
double* drp; // referinta reprezentata printr-un pointer
double temp;
temp = double(1);
drp = &temp;
O referinta poate fi utilizata pentru a implementa o functie care
se
presupune ca schimba valoarea argumentelor sale.
int x = 1;
void incr(int& aa){ aa++; }
incr(x); // x = 2;

Semantica transferului de argumente se defineste ca si pentru
initializare, asa ca atunci cind este apelata functia de mai sus
argumentul aa
a lui incr() devine un alt nume pentru x. Cu toate acestea, pentru a
avea un
program mai lizibil este cel mai bine sa eliminam functiile care isi
modifica
argumentele. Este adesea preferabil sa se returneze o valoare dintr-o
functie
in mod explicit sau sa se returneze un pointer spre argument.
int x = 1;
int next(int p){ return p+1; }
x = next(x); // x = 2
void inc(int* p){ (*p)++; }
inc(&x); // x = 3
Referintele pot fi de asemenea utilizate pentru a defini functii care
pot fi
utilizate atit in parttea stinga cit si in partea dreapta a unei
atribuiri.
Din nou, multe din cele mai interesante utilizari ale referintei se
afla in
proiectarea tipurilor netriviale definite de utilizator. Ca de exemplu,

sa
definim un tablou asociativ simplu. Intii noi definim struct pair prin:
struct pair{ char* name; int val; };

Ideea de baza este ca un sir are o valoare intreaga asociata cu
el. Este
usor sa se defineasca o functie, find() care mentine o data structurata

ce
consta dintr-o pereche pentru fiecare sir diferit ce a fost prezentat.
O
implementare foarte simpla (dar ineficienta) ar fi urmatoarea:
const large = 1024;
static pair vec[large+1];
pair* find(char* p)
/* mentinerea unui set de "pair": se cauta p, se returneaza
"pair"-ul respectiv daca se gaseste, altfel se returneaza
un "pair" neutilizat */
{
for(int i=0; vec[i].name; i++)
if(strcmp(p,vec[i].name)==0)
return &vec[i];
if(i==large)
return &vec[large-1];
return &vec[i];
}

Aceasta functie poate fi utilizata prin functia value() care
implementeaza un tablou de intregi indexat prin siruri de caractere:
int& value(char* p)
{
pair* res = find(p);
if(res->name=='\0') // aici spre negasit:initializare
{
res->name=new char[strlen(p)+1];
strcpy(res->name,p);
res->val = 0; // valoarea initiala: 0
}
return res_val;
}
Pentru un parametru sir dat, value() gaseste obiectul intreg
respectiv
(nu valoarea intregului corespunzator); ea returneaza o referinta la
el.
Aceasta s-ar putea utiliza astfel:

const MAX = 256; //mai mare decit cel mai mare cuvint
main() //numara aparitiilor fiecarui cuvint de la intrare
{
char buf[MAX];
while(cin >> buf)
value(buf++);
for(int i=0; vec[i].name; i++)
cout << vec[i].name << ":" << vec[i].val << "\n";
}

Fiecare pas al ciclului citeste un cuvint de la intrarea standard
cin in
buf (vezi cap.8), iar apoi se pune la zi contorul asociat cu el prin
find().
In final tabela rezultata de cuvinte diferite de la intrare, fiecare cu
numarul sau de aparitii, este imprimat. De exemplu, dindu-se intrarea

aa bb
bb aa aa bb aa aa
programul va produce:
aa : 5
bb : 3

Este usor sa se perfectioneze aceasta intr-un tip de tablou
asociativ
propriu folosind o clasa cu operatorul de selectie [] (vezi &6.7).

2.3.11 Registrii
---------

Pe orice arhitectura de masina obiectele (mici) pot fi accesate
mai rapid
cind se plaseaza intr-un registru. Ideal, compilatorul va determina
strategia
optima pentru a utiliza orice registru disponibil pe masina pe care se
compileaza programul. Totusi, acest task nu este trivial, asa ca uneori

este
util ca programatorul sa dea compilatorului aceasta informatie. Aceasta

se
face declarind un obiect registru. De exemplu:
register int i;
register point cursor;
register char* p;

Declaratiile de registrii ar trebui utilizate numai cind eficienta

este
intr-adevar importanta. Declarind fiecare variabila ca variabila
registru se
va ingreuna textul programului si se poate chiar marii dimensiunea
codului si
timpul de executie (de obicei sint necesare instructiuni de a incarca
un
obiect si de a memora un obiect dintr-un registru). Nu este posibil sa
se ia
adresa unui nume declarat ca registru si nici nu poate fi global un
astfel de
nume.


2.4 Constante
---------
C++ furnizeaza o notatie pentru valorile de tipuri fundamentale:
constante caracter, constatante intregi si constante in virgula
flotanta. In
plus, zero (0) poate fi utilizat ca si o constanta pentru orice tip de
pointer, iar sirurile de caractere sint constante de tip char[]. Este
posibil,
de asemenea, sa se specifice constante simbolice. O constanta simbolica

este
un nume a carui valoare nu poate fi schimbata in domeniul ei de
existenta. In
C++ exista trei feluri de constante simbolice:

(1) oricarei valori de orice tip i se poate da un nume si sa
fie folosita ca o consatnta adaugind cuvintul cheie
const la definitia ei;
(2) un set de constante intregi poate fi definit ca o enume-
rare;
(3) orice nume de vector sau functie este o constanta.


2.4.1 Constante intregi
-----------------

Constantele intregi pot fi de patru feluri: zecimale, octale,
hexazecimale si constante caracter. Constantele zecimale sint cele mai
frecvent utilizate si arata asa cum ne asteptam noi:

0 1234 976 12345678901234567890

Tipul unei constante zecimale este int cu conditia ca ea sa incapa
intr-un int, altfel ea este long. Compilatorul se cuvine sa avertizeze
asupra
constantelor care sint prea lungi ca sa fie reprezentate in calculator.
O constanta care incepe cu zero urmat de x (0x) este hexazecimal,
iar o
constanta care incepe cu zero urmat de o cifra este in octal.
Exemple de constante octale:

0 02 077 0123

Exemple de constante in hexazecimal:

0x0 0x2 0x38 0x53

Literele a, b, c, d, e si f sau echivalentele lor in litere mari
se
utilizeaza pentru a reprezenta 10, 11, 12, 13, 14 si respectiv 15.
Notatiile
octale si hexazecimale sint mai folosi-
lositoare pentru a exprima structuri pe biti; utilizind aceste notatii
pentru
a exprima numere adevarate putem ajunge la surprize. De exemplu, pe
o
masina pe care un int se reprezinta ca un intreg prin complement fata
de 2 pe
16 biti, intregul 0xffff este numarul negativ -1; daca s-ar folosi mai
multi
biti pentru a reprezenta un int, atunci acesta ar fi 65535.


2.4.2 Constante in flotanta
---------------------
O constanta in flotanta este de tip double. Din nou compilatorul
da un
avertisment despre constante flotante care sint prea mari pentru a
putea fi
reprezentate. Iata citeva constante in virgula flotanta:
1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15

Sa observam ca nu poate apare un spatiu in mijlocul unei constante
flotante. De exemplu:
65.43 e-21
nu este o constanta flotanta ci 4 lexicuri:
65.43 e - 21
si va cauza eroare sintactica.
Daca noi dorim o constanta de tip float, noi putem defini una de
forma
(&2.4.6):

const float pi8 = 3.14159265;


2.4.3 Constante caracter
------------------

Desi C++ nu are un tip caracter separat pentru date, ci mai
degraba un
tip intreg care poate pastra un caracter, trebuie sa avem o notatie
speciala
si convenabila pentru caractere.
O constanta caracter este un caracter inclus intre caracterele
apostrof:
de exemplu 'a' si '0'. Astfel de constante caracter sint constante
simbolice
adevarate pentru valorile intregi ale caracterelor din setul de
caractere al
masinii pe care se executa programul C++ (care nu este in mod necesar
acelasi
set de caractere ca si cel utilizat de calculatorul pe care se
compileaza
programul). Astfel, daca noi executam programul pe o masina care
utilizeaza
setul de caractere ASCII, valoarea '0' este 48; daca masina utilizeaza
setul
EBCDIC, el este 240. Utilizind constantele caracter in locul notatiei
zecimale
programele devin mai portabile. Citeva caractere au de asemenea notatii
standard in care se utilizeaza caracterul backslash (\):

'\b' backspace
'\f' formfeed
'\n' newline
'\r' cariage return
'\t' horizontal tab
'\v' vertical tab
'\\' backslash
'\'' simple quote
'\"' double quote
'\0' null, the integer value 0
Acestea sint caractere singulare in ciuda aparentei. Este posibil,

de
asemenea sa reprezentam un caracter printr-un numar octal de o cifra,
doua sau
trei (\ urmat de cifre octale) sau de un numar hexazecimal de una, doua

sau
trei cifre(\x urmat de cifre hexazecimale). De exemplu:

'\6' '\x6' 6 ASCII ack
'\60' '\x30' 48 ASCII '0'
'\137' '\x05f' 95 ASCII '-'

Aceasta face posibil ca sa se reprezinte fiecare caracter din
setul
caracterelor masina si in particular pentru a include astfel de
caractere in
siruri de caractere (vezi sectiunea urmatoare). Utilizind o notatie
numerica
pentru un caracter, programul respectiv nu mai este portabil pentru
masini cu
seturi diferite de caractere.


2.4.4 Siruri
------

Un sir constant este o secventa de caractere inclusa intre
ghilimele:

"this is a string"

Orice sir constant contine cu un caracter mai mult decit cele care

apar
in sir; ele toate se termina prin caracterul nul '\0', cu valoarea 0.
De
exemplu:

sizeof("asdf")==5;

Tipul unui sir este "vector de un numar corespunzator de
caractere", asa
ca "asdf" este de tipul char[5]. Sirul vid se scrie "" (si are tipul
char[1]).
Sa observam ca pentru orice sir s, strlen(s) == sizeof(s) - 1 deoarece
strlen() nu numara zeroul terminal.
Conventia backslash pentru reprezentarea caracterelor negrafice
pot de
asemenea sa fie utilizate intr-un sir: aceasta face posibil sa se
reprezinte
ghilimelele si insusi caracterul backslash intr-un sir. Cel mai
frecvent
astfel de caracter este pe de parte caracterul '\n'. De exemplu:

cout << "beep at end of message\007\n";

unde 7 este valoarea ASCII a caracterului bel.
Nu este posibil sa avem un caracter newline "real" intr-un sir:

"this is not a string
but a syntax error"

cu toate acestea, un backslash urmat imediat de un newline poate apare
intr-un
sir: ambele vor fi ignorate. De exemplu:
cout << "this is\
ok"
va scrie
this is ok

Este posibil ca sa avem caracterul nul intr-un sir, dar
majoritatea
programelor nu vor suspecta ca dupa el mai sint caractere. De exemplu,
sirul
"asdf\000hjkl" va fi tratat ca "asdf" prin functii standard cum ar fi
strcpy()
si strlen().
Cind se include o constanta numerica intr-un sir folosind notatia
octala
sau hexazecimala, este totdeauna bine sa se utilizeze trei cifre pentru

numar.
Notatia este destul de greu de utilizat fara sa apara probleme cind
caracterul
dupa o constanta de acest fel este o cifra. Consideram exemplele:

char v1[]="a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9'
char v2[]="a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9'
char v3[]="a\xfad\127"; // 'a' '\xfad' '\127'

Sa observam ca o notatie cu doua cifre hexazecimale nu este
suficienta pe
masini cu 9 biti pe byte.


2.4.5 Zero
----

Zero (0) poate fi folosit ca o constanta de tip intreg, flotant
sau
pointer.
Nici un obiect nu este alocat cu adresa zero. Tipul lui zero va fi
determinat de context. Toti bitii de o dimensiune potrivita sint zero.


2.4.6 Const
-----

Cuvintul cheie const poate fi adaugat la declaratia unui obiect
pentru a
face acel obiect o constanta in loc de variabila. De exemplu:

const int model = 145;
const int v[] = {1, 2, 3, 4};

Deoarece la un astfel de obiect nu i se poate atribui o valoare,
el
trebuie sa fie initializat. Declarind ceva ca este constant ne asiguram

ca
valoarea lui nu va fi schimbata in domeniul lui:

model = 165; // error
model++; // error

Sa observam ca const modifica un tip; adica el restringe modul in
care un
obiect poate fi utilizat, in loc sa specifice cum se aloca constanta.
Este, de
exemplu, perfect rezonabil si uneori util sa declaram o functie care
returneaza o constanta:

const char* peek(int i){ return private[i]; }
O functie de aceasta forma ar putea fi utilizata pentru a permite
cuiva
sa citeasca un sir care nu poate fi alterat.
Cu toate acestea, un compilator poate avea avantaje de pe urma
unui
obiect care este o constanta in diferite moduri. Cel mai evident este
faptul
ca de obicei nu este nevoie ca sa fie alocata memorie pentru constanta
deoarece compilatorul cunoaste valoarea lui. Mai mult decit atit,
initializatorul pentru o constanta este adesea (dar nu totdeauna) o
expresie
constanta; daca este asa, ea poate fi evaluata la compilare. Cu toate
acestea,
de obicei este necesar sa se aloce memorie pentru un vector de
constante
deoarece compilatorul nu poate in general sa defineasca care elemente
ale
vectorului sint referite in expresii. Pe multe masini, totusi, o
implementare
eficienta poate fi atinsa chiar in acest caz plasind vectori de
constante in
memorii read_only.
Cind utilizam un pointer, sint implicate doua obiecte; pointerul
insusi
si obiectul spre care se face pointarea.
"Prefixind" o declaratie a unui pointer cu const se construieste
obiectul
ca o constanta, nu si pointerul. De exemplu:

const char* pc = "asdf"; // pointer spre o constanta
pc[3] = 'a'; // eroare
pc = "ghjk"; // ok

Pentru a declara ca pointerul insusi este o constanta si nu
obiectul spre
care pointeaza, se foloseste operatorul *const:
char *const cp = "asdf"; // pointer constant
cp[3] = 'a'; // ok
cp = "ghjk"; // eroare

Pentru a face ca sa fie constante atit obiectele, cit si pointerul

spre
ele, trebuie ca ambele sa fie declarate ca si constante. De exemplu:
const char *const cpe = "asdf"; // pointer constant spre
// constanta
cpc[3] = 'a'; // eroare
cpc = "ghjk"; // eroare

Un obiect care este o constanta cind este accesat printr-un
pointer poate
fi variabila cind este accesat in alt mod. Aceasta este util mai ales
pentru
argumentele functiilor. Declarind un pointer_argument ca si const,
functiei i
se interzice sa modifice obiectul spre care pointeaza pointerul
respectiv. De
exemplu:

char* strcpy(char* p,const char* q);//nu poate modifica pe *q

Se poate atribui adresa unei variabile la un pointer spre o
constanta
deoarece nu se intimpla nimic rau prin aceasta. Cu toate acestea,
adresa unei
constante nu se poate atribui la un pointer fara restrictii deoarece
aceasta
ar permite sa schimbe valoarea obiectului. De exemplu:

int a = 1;
const c = 2;
const* p1 = &c; // ok
const* p2 = &a; // ok
int* p3 = &c; // eroare
*p3 = 7; // schimba valoarea lui

De obicei, daca tipul este omis intr-o declaratie, se alege int ca
implicit.

2.4.7 Enumerari
---------

O alta posibilitate pentru a defini constante intregi, care este
adesea
mai convenabil decit utilizind const, este enumerarea. De exemplu:

enum {ASM, AUTO, BREAK};

defineste trei constante intregi, numite enumeratori si atribuie valori

la
acestia. Deoarece valorile enumerator sint atribuite crescator de la
zero,
aceasta este echivalent cu scrierea:

const ASM = 0;
const AUTO = 1;
const BREAK = 2;

O enumerare poate fi definita. De exemplu:

enum keyword {ASM,AUTO,BREAK};
Numele enumerarii devine un sinonim pentru int, nu un nou tip. De
exemplu:
keyword key;
switch(key)
{
case ASM: // se face ceva
break;
case BREAK:// face ceva
break;
}
va conduce la un avertisment deoarece numai doua valori au fost tratate

din
cele trei.
Valorile pot fi de asemenea date explicit enumeratorilor. De
exemplu:

enum int16 {
sign = 0100000,
most_significant = 0400000,
last_significant = 1
};
Aceste valori nu este necesar sa fie distincte, crescatoare sau
pozitive.


2.5 Salvarea spatiului
------------------

Cind programam aplicatii netriviale, invariabil vine vremea cind
dorim
mai mult spatiu de memorie decit este disponibil sau ne putem permite.
Exista
doua moduri de a obtine mai mult spatiu in afara de cel care este
disponibil:

[1] Sa se puna mai mult de un obiect mic intr-un octet;
[2] Sa se utilizeze acelasi spatiu pentru a pastra diferite
obiecte in momente diferite.

Prima metoda poate fi realizata folosind cimpurile, iar cea de a doua
folosind
reuniunile. Aceste constructii se descriu in sectiunile urmatoare.
Deoarece
utilizarea lor tipica este pentru a optimiza pur si simplu un program
si
deoarece ele sint adesea cele mai neportabile parti ale programului,
programatorul trebuie sa gindeasca de doua ori inainte de a le utiliza.

Adesea
o conceptie mai buna este sa schimbe modul in care se gestioneaza
datele; de
exemplu, sa se insiste mai mult asupra memoriei alocate dinamic
(&3.2.6) si
mai putin asupra memoriei prealocate static.


2.5.1 Cimpuri
-------

Se pare extravagant ca sa se utilizeze un caracter pentru a
reprezenta o
variabila binara, de exemplu un comutator on/off, dar tipul char este
cel mai
mic obiect care poate fi alocat independent in C++. Este posibil,
totusi, sa
se inmanuncheze impreuna diferite astfel de variabile foarte mici ca si
cimpuri intr-o structura. Un membru se defineste a fi un cimp
specificind
numarul de biti pe care ii ocupa, dupa numele lui. Se admit si cimpuri
nedenumite; ele nu afecteaza sensul cimpurilor denumite, dar pot fi
utilizate
pentru a face o aranjare mai buna insa dependenta de masina:
struct sreg{
unsigned enable : 1;
unsigned page : 3;
unsigned : 1; //neutilizat
unsigned mode : 2;
unsigned : 4; //neutilizat
unsigned access : 1;
unsigned length : 1;
unsigned non_resident : 1;
};

Aceasta se intimpla sa fie aranjarea bitilor la registru de stare
0 la
DEC PDP11/45. Un cimp trebuie sa fie de tip intreg si se utilizeaza ca
alti
intregi exceptind faptul ca nu este posibil sa se ia adresa unui cimp.

In
modulul kernel al unui sistem de operare sau in debugger, tipul sreg ar

putea
fi utilizat astfel:
sreg* sr0 = (sreg*)0777572;
//........
if(sr0->access) //access violation
{//clean up the mess
sr0->access = 0;
}

Cu toate acestea, utilizind cimpuri pentru a putea impacheta
diferite
variabile intr-un singur octet nu neaparat se salveaza spatiu. Se
salveaza
spatiu la date, dar dimensiunea codului rezultat din manipularea
acestor
variabile se mareste pe majoritatea masinilor. Programele se stie ca se
scurteaza semnificativ cind variabilele binare se convertesc de la
cimpuri
binare la caractere! Mai mult decit atit, de obicei este mai rapid sa
se faca
acces la char sau int decit pentru a face acces la un cimp.

2.5.2 Reuniuni
--------
Sa consideram o tabela de simboluri in care o intrare pastreaza un

nume
si o valoare, iar valoarea este sau un sir sau un intreg:
struct entry{
char* name;
char type;
char* string_value; //se utilizeaza daca type == 's'
int int_value; //se utilizeaza daca type == 'i'
};
void print_entry(entry* p)
{switch(p->type)
{
case 's': cout << p->string_value;
break;
case 'i': cout << p->int_value;
break;
default : cerr << "type corrupted\n";
break;
}
}
Deoarece string_value si int_value nu pot fi utilizate in acelasi
timp,
evident se pierde spatiu. Se poate recupera usor specificind ca ambii
ar
trebui sa fie membri ai unei reuniuni, ca mai jos:
struct entry{
char* name;
char type;
union{
char* string_value; //used if type =='s'
int int_value; //used if type =='i'
};
};
Aceasta lasa tot codul care foloseste pe entry neschimbat, dar
asigura
faptul ca atunci cind entry se aloca, string_value si int_value sa aiba
aceeasi adresa. Aceasta implica, ca toti membri unei reuniuni sa aiba
in comun
acelasi spatiu care permite pastrarea celui mai mare membru.
Utilizind reuniunea in asa fel ca totdeauna sa folosim membrul
care a
fost pastrat in ea, se obtine o optimizare pura. Cu toate acestea, in
programe
mari, nu este usor sa se asigure ca o reuniune se utilizeaza numai in
acest
mod si se pot introduce erori subtile. Este posibil sa se incapsuleze o
reuniune in asa fel incit corespondenta intre tipul cimp si tipurile
membrilor
unei reuniuni sa fie garantat ca este corecta (&5.4.6).
Reuniunile sint uneori utilizate pentru "conversie de tip"
(aceasta se
face in principiu prin programe introdu-se in limbaj in afara
facilitatilor de
conversie a tipului, unde este necesar sa fie facuta). De exemplu, pe
VAX
acestea convertesc un int in int* pur si simplu prin echivalenta de
biti.
struct fudge{
union{
int i;
int* p;
};
};
fudge a;
a.i = 4096;
int* p = a.p; //bad usage

Cu toate acestea, aceasta nu este o conversie reala; pe anumite
masini un
int si un int* nu ocupa acelasi spatiu, iar pe altele nici un intreg nu

poate
avea o adresa impara. O astfel de utilizare a unei reuniuni nu este
portabila
si exista un mod explicit si portabil de a specifica aceasta conversie
(&3.2.5).
Reuniunile sint ocazional utilizate in mod deliberat pentru a
elimina
conversia de tip. Am putea, de exemplu, utiliza un fudge pentru a gasi
reprezentarea pointerului 0:
fudge.p = 0;
int i = fudge.i; // i nu este necesar sa fie 0

Este de asemenea posibil sa se dea un nume unei reuniuni; adica ea
formeaza un tip in adevaratul sens al lui. De exemplu, fudge ar putea
fi
declarata astfel:
union fudge{
int i;
int* p;
};
si folosita exact ca inainte. Reuniunile numite au de asemenea,
utilizari
proprii (vezi &5.4.6).

2.6 Exercitii
---------

1. (*1). Sa se execute programul "Hello, world" (&1.1.1).

2. (*1). Pentru fiecare din declaratiile din (&2.1) sa se faca
urmatoarele: daca o declaratie nu este o definitie, sa se scrie o
definitie
pentru ea. Daca o declaratie este o definitie, sa se scrie o
declaratie
pentru ea, care nu este de asemenea o definitie.

3. (*1). Sa se scrie declaratii pentru urmatoarele: un
pointer spre
un caracter; un vector de 10 intregi; o referinta spre un vector de 10
intregi; un pointer spre un vector de siruri de caractere; un pointer
spre un
pointer la un caracter; o constanta intreaga; un pointer spre o
constanta
intreaga; un pointer constant spre un intreg. Sa se initializeze
fiecare din
ei.

4. (*1.5). Sa se scrie un program care imprima dimensiunea
tipurilor fundamentale si a pointerului. Sa se utilizeze operatorul
sizeof.

5. (*1.5). Sa se scrie un program care imprima literele
'a'..'z' si
cifrele '0'..'9' si valorile lor intregi. Sa se faca acelasi lucru
pentru alte
caractere imprimabile. Sa se faca acelasi lucru, dar utilizind notatia
hexazecimala.

6. (*1). Sa se imprime bitii care se folosesc pentru a
reprezenta
pointerul 0 pe sistemul d-voastra (&2.5.2).

7. (*1.5). Sa se scrie o functie care imprima exponentul si
mantisa unui parametru in dubla precizie.

8. (*2). Care sint valorile cele mai mari si cele mai mici pe
sistemul
d-voastra pentru tipurile urmatoare: char, short, int, long, float,
double,
unsigned, char*, int* si void* ? Exista mai multe restrictii asupra
valorilor
? De exemplu, poate int* sa aiba o valoare impara ? Care este cadrajul
obiectelor de acele tipuri ? De exemplu poate un int sa aiba o adresa
impara ?

9. (*1). Care este cel mai lung nume local pe care il puteti
utiliza
intr-un program C++ pe sistemul d-voastra ? Care este cel mai lung nume

extern
pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ?
Exista
vreo restrictie asupra caracterelor pe care le puteti utiliza intr-un
nume ?

10. (*2). Definiti pe unu astfel:
const one = 1;
Incercati sa schimbati valoarea lui one la doi. Definiti pe num prin:
const
num[] = {1,2}; Incercati sa schimbati valoarea lui num[1] la 2.

11. (*1). Scrieti o functie care permuta doi intregi. Sa se
utilizeze
int* ca tip al argumentului. Scrieti o alta functie de permutare care
utilizeaza int& ca tip de argument.
12. (*1). Care este dimensiunea vectorului str in exemplul
urmator:
char str[] = "a short string";
Care este lungimea sirului "a short string"?

13. (*1.5). Sa se defineasca o tabela de nume continind
numele
fiecarei luni din an si numarul de zile din fiecare luna. Sa se scrie
tabela.
Sa se faca aceasta de doua ori: odata utilizind un vector pentru nume
si un
vector pentru numarul de zile si odata utilizind un vector de
structuri,
fiecare structura pastrind numele lunii si numarul de zile din ea.

14. (*1). Sa se utilizeze typedef pentru a defini tipurile:
unsigned
char, constant unsigned char, pointer spre intreg, pointer spre pointer

spre
char, pointer spre vector de caractere, vector de 7 pointeri intregi,
pointer
spre un vector de 7 pointeri intregi, vector de 8 vectori de 7 pointeri
intregi.


CAPITOLUL 3
-----------

EXPRESII SI INSTRUCTIUNI
------------------------


C++ are un set mic, dar flexibil, de tipuri de instructiuni pentru
controlul programului si un set bogat de operatori pentru manipularea
datelor.
Un singur exemplu complex introduce cele mai frecvente facilitati
utilizate.
Dupa aceea sint rezumate expresiile si conversiile explicite de tip si
este
prezentata in detaliu utilizarea memoriei libere. Apoi sint rezumate
in-
structiunile, iar in final se discuta stilul de decalare si comentare a
textului.

3.1 Un calculator de birou
----------------------

Instructiunile si expresiile se introduc prin prezentarea
programului
calculatorului de birou care furnizeaza cele patru operatii aritmetice
standard ca operatori infix asupra numerelor flotante. Utilizatorul
poate, de
asemenea, defini variabile. De exemplu, dindu-se intrarea:

r = 2.5
area = pi * r * r

(pi este predefinit), programul calculator va scrie:

2.5
19.635

unde 2.5 este rezultatul primei linii de intrare, iar 19.635 este
rezultatul
celei de a doua.

Calculatorul consta din patru parti principale: un analizor, o
functie de
intrare, o tabela de simboluri si un driver. In realitate este un
compilator
miniatura cu un analizor care face analiza sintactica, functia de
intrare
realizind intrarea si analiza lexicala, tabela de simboluri pastrind
informatia permanenta, iar driverul facind initializarea, iesirea si
tratind
erorile. Exista multe facilitati care pot fi adaugate la acest
calculator
pentru a-l face mai util, dar codul este destul de lung intrucit are
200 de
linii si cele mai multe facilitati noi ar adauga cod fara a furniza
aspecte
noi in utilizarea lui C++.


3.1.1 Analizorul
----------

Iata o gramatica pentru limbajul acceptat de calculator:

program:
END //END este sfirsitul intrarii
expr_list END
expr_list:
expression PRINT //PRINT este '\n' sau ;
expresion PRINT expr_list
expression:
expression + term
expression - term
term
term:
term / primary
term * primary
primary
primary:
NUMBER //numar in flotanta din C++
NAME //nume din C++ fara subliniat
NAME = expression
_primary
(expression)

Cu alte cuvinte, un program este un sir de linii. Fiecare linie
consta
din una sau mai multe expresii separate prin punct- virgula. Unitatile
de baza
ale unei expresii sint numere, nume si operatorii *, /, +, - (atit unar

cit si
binar) si =. Numele nu trebuie sa fie declarate inainte sa fie
utilizate.
Stilul analizei sintactice utilizate este de obicei numit analiza
descendenta recursiva. Este o tehnica top-down directa. Intr-un limbaj
cum
este C++ in care apelurile de functii sint relativ ieftine, aceasta
este o
tehnica eficienta. Pentru fiecare productie din gramatica exista o
functie
care apeleaza alte functii. Simbolurile terminale (de exemplu END,
NUMBER, +
si -) se recunosc prin analizorul lexical, get_token(), iar simbolurile
neterminale sint recunoscute prin functiile analizorului sintactic
expr(),
term() si prim(). De indata ce ambii operanzi ai unei (sub)expresii
sint
cunoscuti, ei se evalueaza. Intr-un compilator real se genereaza codul
in
acest punct.
Analizorul utilizeaza o functie get_token() pentru a obtine o
intrare.
Valoarea ultimului apel a lui get_token() poate fi gasita in variabila
curr_tok. Aceasta este o valoare de enumerare de tip token_value:

enum token_value{
NAME, NUMBER, END, PLUS = '+', MINUS = '-',
MUL = '*', DIV = '/', PRINT =';',
ASSIGN = '=', LP = '(', RP = ')'
};
token_value curr_tok;

Fiecare functie a analizorului presupune ca get_token() a fost
apelat
astfel incit curr_tok sa pastreaze tokenul (lexicul) urmator de
analizat.
Aceasta permite analizorului sa vada un lexic inainte si obliga fiecare
functie a analizorului sa citeasca totdeauna un lexic in plus fata de
cele pe
care le utilizeaza productia pe care o trateaza ea. Fiecare functie a
analizorului evalueaza expresia ei si returneaza o valoare. Functia
expr()
trateaza adunarea si scaderea. Ea consta dintr-un singur ciclu care
cauta
termeni de adunat sau scazut:

double expr()
{
double left = term();
for(;;) //ciclu infinit
switch(curr_tok)
{
case PLUS : get_token(); //salt peste '+'
left += term();
bre_token() trebuie sa fie declarate
inainte de
expr().
Capitolul patru discuta cum sa se organizeze un program ca un set
de
fisiere. Cu o singura exceptie, declaratiile pentru acest exemplu de
calculator de birou pot fi ordonate in asa fel incit fiecare este
declarata
exact o data inainte de a fi utilizata. Exceptie face expr(), care
apeleaza
term(), care apeleaza prim(), care la rindul ei apeleaza expr(). Acest
ciclu
trebuie sa fie intrerupt cumva. O declaratie:

double expr();

inaintea definitiei lui prim() va fi nimerita.
Functia term() trateaza inmultirea si impartirea:

double term() //inmultire si impartire
{
double left = prim();
for(;;)
switch(curr_tok)
{
case MUL: get_token(); //sare peste '*'
left *= prim();
break;
case DIV: get_token(); //sare peste '/'
double d = prim();
if(d == 0)
return error("divide by 0");
left /= d;
break;
default:
return left;
}
}

Testul pentru a ne asigura ca nu se face impartirea prin zero este
necesar deoarece rezultatul in acest caz nu este definit. Functia
error(char*)
este descrisa mai tirziu. Variabila d este introdusa in program acolo
unde
este nevoie de ea si este initializata imediat. In multe limbaje, o
declaratie
poate apare numai in antetul unui bloc. Aceasta restrictie poate
conduce la
erori. Foarte frecvent o variabila locala neinitializata este pur si
simplu o
indicatie de un stil rau. Exceptii sint variabilele care se
initializeaza prin
operatii de intrare si variabilele de tip vector sau structura care nu
pot fi
initializate convenabil printr-o atribuire simpla. Sa observam ca =
este
operatorul de asignare, iar == este operatorul de comparare.
Functia prim() trateaza un primar; deoarece este la un nivel mai
inferior
in ierarhia de apeluri, ea face un pic mai multa "munca" si nu mai este
necesar sa cicleze.
double prim()
{switch(curr_tok)
{case NUMBER: get_token(); //constanta in flotanta
return number_value;
case NAME : if(get_token() == ASSIGN)
{
name* n = insert(name_string);
get_token();
n->value = expr();
return n->value;
}
return look(name_string)->value;
case MINUS : get_token(); //minus unar
return _prim();
case LP : get_token();
double e = expr();
if(curr_tok != RP)
return error(") expected");
get_token();
return e;
case END : return 1;
default : return error("primary expected");
}
}

Cind se4 gaseste un NUMBER (adica o constanta flotanta), se
returneaza
valoarea ei. Rutina de intrare get_token() plaseaza valoarea in
variabila
globala number_value. Utilizarea unei variabile globale intr-un program

indica
adesea ca structura nu este cit se poate de "curata", ca un anumit fel
de
optimizare a fost aplicat. Asa este aici; un lexic in mod tipic consta

din
doua parti: o valoare care specifica tipul lexicului (token_value in
acest
program) si (cind este nevoie) valoarea lexicului. Aici exista numai o
singura
variabila simpla curr_tok, asa ca este nevoie de variabila globala
number_value pentru a pastra valoarea ultimului NUMBER citit. Aceasta
functioneaza deoarece calculatorul totdeauna utilizeaza un numar in
calcul
inainte de a citi un alt numar de intrare.
In acelasi mod in care valoarea ultimului NUMBER intilnit este
tinut in
number_value, reprezentarea sirului de caractere a ultimului NAME
intilnit
este tinut in name_string. Inainte de a face ceva unui nume, inainte
calculatorul trebuie sa vada daca el este asignat sau numai utilizat.
In
ambele cazuri se consulta tabela de simboluri. Tabela este prezentata
in
&3.1.3. Aici trebuie sa observam ca ea contine intrari de forma:
struct name{
char* string;
name* next;
double value;
};
unde next se utilizeaza numai de functiile care mentin tabela:
name* look(char*);
name* insert(char*);

Ambele returneaza un pointer la un nume care corespunde la
parametrul sir
de caractere. Functia look() semnaleaza daca numele nu a fost definit.
Aceasta
inseamna ca in calculator un nume poate fi utilizat fara o declaratie
prealabila, dar prima lui utilizare trebuie sa fie partea stinga a unei
atribuiri.

3.1.2 Functia de intrare
------------------
Citirea intrarii este adesea cea mai incurcata parte a unui
program.
Motivul este faptul ca daca un program trebuie sa comunice cu o
persoana, el
trebuie sa invinga capriciile, conventiile si erorile unei persoane sau

a mai
multora. Incercarea de a forta persoana sa se comporte intr-o maniera
mai
convenabila pentru masina este adesea, pe drept cuvint, considerata
ofensiva.
Sarcina unei rutine de intrare de nivel inferior este de a citi
caractere unul
dupa altul si sa compuna unitati de nivel mai inalt. Aici intrarea de
nivel
inferior se face cu get_token().
Regulile pentru intrarile in calculator au fost deliberat alese
asa ca sa
fie ceva incomod pentru sirul de functii care le manevreaza. Modificari
neimportante in definitiile unitatilor ar face pe get_token() foarte
simpla.
Prima problema este aceea ca, caracterul newline '\n' este
semnificativ
pentru calculator, dar sirul de functii de intrare il considera un
caracter
whitespace. Adica, pentru acele functii, '\n' este un terminator de
unitate
lexicala. Pentru a invinge aceasta trebuie examinate spatiile albe
(spaces,
tab, etc):
char ch;
do{ //sare peste spatiile albe, exceptind '\n'
if(!cin.get(ch))
return curr_tok = END;
}while(ch!='\n' && isspace(ch));

Apelul cin.get(ch) citeste un singur caracter din sirul de la
intrarea
standard in ch. Testul if(!cin.get(ch)) esueaza daca nici un caracter
nu poate
fi citit de la intrare (din cin). In acest caz se returneaza END pentru

a
termina sesiunea de calcul. Operatorul ! (not) se utilizeaza intrucit
get()
returneaza o valoare nenula in caz de succes. Functia isspace() din
<ctype.h>
furnizeaza testul standard pentru spatiu alb (&8.4.1). Functia
isspace(c)
returneaza o valoare nenula daca c este un caracter alb, zero altfel.
Testul
este implementat ca o tabela de cautare, astfel, utilizind isspace este

mai
rapid decit daca s-ar testa individual caracterele spatiu alb. Acelasi
lucru
se aplica la functiile isalpha(), isdigit() si isalnum() utilizate in
get_token().
Dupa ce s-a facut avans peste caracterele albe, se utilizeaza
caracterul
urmator pentru a determina ce fel de unitate lexicala incepe in sirul
de
intrare. Sa ne oprim la niste cazuri separate inainte de a prezenta
functia
completa. Expresiile terminatoare '\n' si ';' sint tratate astfel:
switch(ch)
{
case ';' :
case '\n' : cinn >> WS; //sare peste caractere albe
return curr_tok = PRINT;
}
Saltul peste caractere albe (din nou) nu este necesar, dar daca ar

trebui
s-ar repeta apeluri ale lui get_token().
WS este un obiect de spatiu alb declarat in <stream.h>. El este
utilizat
numai pentru a indeparta spatiile albe. O eroare la intrare sau la
sfirsitul
intrarii nu va fi detectata pina la apelul urmator a lui get_token().
Sa observam modul in care diferite etichete ale lui case pot fi
utilizate
pentru un singur sir de instructiuni care trateaza acele cazuri. Se
returneaza
unitatea PRINT si se pune in curr_tok in ambele cazuri. Numerele se
trateaza
astfel:
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.': cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
Scrierea etichetelor orizontal in loc de vertical, in general, nu
este o
idee buna deoarece este mai greu de citit, dar nu este nimerit in cazul

de
fata sa avem o linie pentru fiecare cifra. Deoarece operatorul >> este
deja
definit pentru a citi constante in virgula flotanta dubla precizie,
codul este
trivial. Intii caracterul initial (o cifra sau un punct) se pune inapoi

in cin
si apoi constanta poate fi citita in number_value.
Un nume, care este un lexic de tip NAME, este definit ca o litera
care
este posibil sa fie urmata de litere sau cifre:

if(isalpha(ch))
{char* p = name_string;
*p++ = ch;
while(cin.get(ch) && isalnum(ch))
*p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok = NAME;
}
Aceasta construieste un sir terminat cu zero in name_string.
Functiile
isalpha() si isalnum() sint furnizate in <ctype.h>, isalnum(c) este
diferit
de zero daca c este o litera sau o cifra si zero altfel.
Iata in final functia de intrare completa:
token_value get_token()
{char ch;
do{ //sare peste spatiile albe exceptind '\n'
if(!cin.get(ch))
return curr_tok = END;
}while(ch != '\n' && isspace(ch));
switch(ch)
{
case ';' :
case '\n': cin >> WS; //salt peste spatii albe
return curr_tok = PRINT;
case '*':
case '/':
case '+':
case '(':
case ')':
case '=': return curr_tok = ch;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
case '.': cin.putback(ch);
cin >> number_value;
return curr_tok = NUMBER;
default : //NAME, NAME= sau eroare
if(isalpha(ch))
{
char* p = name_string;
*p++ = ch;
while(cin.get(ch) && isalnum(ch))
*p++ = ch;
cin.putback(ch);
*p = 0;
return curr_tok = NAME;
}
error("bad token");
return curr_tok = PRINT;
}
}
Intrucit token_value al unui operator a fost definit prin valoarea
intreaga a operatorului, toate alternativele (case) pentru operator se
trateaza trivial.

3.1.3 Tabela de simboluri
-------------------

O singura functie are acces la tabela de simboluri:

name* look(char* p, int ins = 0);

Cel de al doilea argument indica daca sirul de caractere presupus
trebuie
sa fie in prealabil inserat sau nu.
Folosirea argumentului implicit da pentru look posibilitatea
convenabila
de a scrie look("sqrt2") in loc de look("sqrt2", 0), adica se
doreste
cautare, nu inserare. Pentru a obtine notatii convenabile pentru
inserare se
defineste o a doua functie:

inline name* insert(char* s){ return look(s, 1); }

Asa cum s-a mentionat inainte, intrarile in tabela sint de forma:

struct name{
char* string;
name* next;
double value;
};

Elementul next se utilizeaza pentru a face inlantuirea numerelor in
tabela.
Tabela insasi este pur si simplu un vector de pointeri spre
obiecte de
tip nume:

const TBLSZ = 23;
name* table[TBLSZ];

Deoarece toate obiectele statice sint implicit initializate cu
zero,
aceasta declaratie triviala a lui table asigura de asemenea si
initializarea.
Pentru a gasi o intrare pentru un nume din tabela, look()
utilizeaza un
cod hash simplu (numele cu acelasi cod hash se inlantuie):

int ii = 0;
char* pp = p;
while(*pp)
ii = ii << 1 ^ *p++;
if(ii < 0)
ii = -ii;
ii %= TBLSZ;

Fiecare caracter din sirul de intrare p este "adaugat" la ii ("suma"
caracterelor precedente) printr-un sau exclusiv. Un bit din x^y este
setat
daca si numai daca bitii corespunzatori din operanzii x si y sint
diferiti.
Inainte de a face un sau exclusiv, ii se deplaseaza cu un bit in stinga

pentru
a elimina utilizarea numai a unui octet din el. Aceasta se poate
exprima
astfel:

ii <<= 1;
ii ^= *pp++;

Utilizarea lui ^ este mai rapida decit a lui +. Deplasarea este
esentiala
pentru a obtine un cod hash rezonabil in ambele cazuri. Instructiunile:
if(ii < 0)
ii = -ii;
ii %= TBLSZ;
asigura ca ii sa fie in domeniul 0 ... TBLSZ - 1, (% este opera torul
modulo, numit si rest).
Iata functia completa:
extern int strlen(const char*);
extern int strcmp(const char*, const char*);
extern char* strcpy(const char*, const char*);
name* look(char* p, int ins = 0)
{int ii = 0; //hash
char* pp = p;
while(*pp)
ii = ii << 1 ^ *pp++;
if(ii < 0)
ii = -ii;
ii %= TBLSZ;
for(name* n = table[ii]; n; n = n->next) //cautare
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name; //inserare
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->next = table[ii];
table[ii] = nn;
return nn;
}
Dupa ce codul hash a fost calculat in ii, numele este gasit
printr-o
cautare simpla prin intermediul cimpurilor next. Fiecare nume este
verificat
folosind functia strcmp() de comparare a sirurilor. Daca este gasit, se
returneaza numele lui; altfel se adauga un nume nou.
Adaugarea unui nume implica crearea unui obiect cu numele nou
intr-o zona
de memorie libera folosind operatorul new (vezi &3.2.6), initializarea
si
adaugarea lui la lista de nume. Adaugarea se face punind noul nume in
capul
listei deoarece aceasta se poate face fara a testa daca exista sau nu o

lista.
Sirul de caractere care alcatuieste numele trebuie si el pastrat intr-o

zona
libera. Functia strlen() se foloseste pentru a gasi cit de multa
memorie este
necesara, operatorul new pentru a aloca memorie, iar functia strcpy()
pentru a
copia sirul in zona respectiva.

3.1.4 Tratarea erorilor
-----------------
Intrucit programul este atit de simplu, tratarea erorilor nu este
o
preocupare majora. Functia eroare pur si simplu numara erorile, scrie
un mesaj
de eroare si returneaza:
int no_of_errors;
double error(char* s)
{cerr << "error: " << s << "\n";
no_of_errors++;
return 1;
}
Motivul pentru care se returneaza o valoare este faptul ca erorile

de
obicei apar in mijlocul evaluarii unei expresii, asa ca ar trebui sau
sa se
faca un abandon al acelei evaluari sau sa se returneze o valoare care
in
continuare sa fie putin probabil sa cauzeze erori. Ultima varianta este
adecvata pentru acest calculator simplu. Daca get_token() ar tine
numerele de
linie, error() ar putea informa utilizatorul aproximativ asupra locului

unde a
aparut eroarea. Aceasta ar fi util la o folosire interactiva a
calculatorului.
Adesea un program trebuie sa fie terminat dupa o eroare deoarece
nu
exista o cale adecvata care sa permita continuarea executiei. Acest
lucru se
poate face apelind functia exit(), care la inceput videaza lucrurile de

tipul
fisierelor de iesire (&8.3.2) dupa care se termina programul iar
valoarea
returnata de el este argumentul lui exit(). Un mod mai drastic de
terminare a
programului este apelul lui abort() care termina imediat sau imediat
dupa
pastrarea undeva a informatiei pentru debugger (vidaj de memorie).

3.1.5 Driverul
--------

Cu toate bucatile programului construite noi avem nevoie numai de
un
driver care sa initializeze si sa porneasca tot procesul. In acest
exemplu
simplu functia main() poate fi construita astfel:

int main() //insereaza nume predefinite
{
insert("pi")->value = 3.1415926535897932385;
insert("e")->value = 2.7182818284590452354;
while(cin)
{
get_token();
if(curr_tok == END)
break;
if(curr_tok == PRINT)
continue;
cout << expr() << "\n";
}
return no_of_errors;
}

Prin conventie, main() returneaza zero daca programul se termina
normal
si altfel, o valoare diferita de zero, asa ca returnarea numarului de
erori se
potriveste bine cu aceasta conventie.
Aici singurele initializari sint numerele predefinite pentru "pi"
si "e"
care se insereaza in tabela de simboluri.
Sarcina primordiala a ciclului principal este sa citeasca expresii

si sa
scrie raspunsul. Aceasta se obtine prin linia:

cout << expr() << "\n";

Testind pe cin la fiecare pas al ciclului se asigura ca programul
sa se
termine daca ceva merge rau in sirul de intrare iar testul pentru END
asigura
ca ciclul sa se termine corect cind get_token() intilneste sfirsitul de
fisier. O instructiune break provoaca iesirea din instructiunea switch
sau din
ciclul care o contine (adica o instructiune for, while sau do). Testul
pentru
PRINT (adica pentru '\n' si ';') elibereaza pe expr() de necesitatea de

a
prelucra expresii vide. O instructiune continue este echivalenta cu
trecerea
la sfirsitul ciclului, asa ca in acest caz:
while(cin)
{ //............
if(curr_tok == PRINT)
continue;
cout << expr() << "\n";
}
este echivalent cu :
while(cin)
{
//............
if(curr_tok == PRINT)
goto end_of_loop;
cout << expr() << "\n";
end_of_loop : ;
}
(ciclurile se descriu in detaliu in &r9).

3.1.6 Argumentele liniei de comanda
-----------------------------

Dupa ce programul a fost scris si testat, am observat ca tastarea
expresiilor la intrarea standard a fost adesea mai mult decit necesar,
deoarece in mod frecvent a trebuit sa se evalueze o singura expresie.
Daca
este posibil ca aceasta expresie sa fie prezentata ca un argument al
liniei de
comanda, atunci multe accese cheie ar fi fost eliminate.
Asa cum s-a mentionat in prealabil, un program incepe prin apelul
lui
main(). Cind aceasta s-a facut, main() primeste doua argumente, care
specifica
numarul de argumente si care de obicei se numeste argc si un vector de
argumente, care de obicei se numeste argv. Argumentele sint siruri de
caractere, asa ca tipul lui argv este char *[argc]. Numele unui program
(intrucit el apare pe linia de comanda) se paseaza ca argv[0], asa ca
argc
este intotdeauna cel putin 1. De exemplu, pentru comanda:
dc 150/1.1934
argumentele au aceste valori:
argc 2
argv[0] "dc"
argv[1] "150/1.1934"
Nu este dificil sa fie pastrata linia de comanda ca argument. Problema
este
cum sa se foloseasca fara a face reprogramare. In acest caz, este
trivial
intrucit un sir de intrare poate fi limitat la un sir de caractere in
loc de
un fisier (&8.5). De exemplu, cin poate fi facut sa citeasca caractere
dintr-un sir in loc de intrarea standard:
int main(int argc, char* argv[])
{switch(argc)
{case 1: break; //citeste din intrarea standard
case 2: cin = *new istream(strlen(argv[1]),argv[1]);
break;
default: error("too many arguments");
return 1;
}
// ca inainte
}
Programul este neschimbat exceptind adaugarea argumentelor la
main() si
utilizarea lor in instructiunea switch.
S-ar putea usor modifica main() pentru a accepta diferite
argumente in
linia de comanda, dar acest lucru nu este necesar, deoarece diferite
expresii
pot fi pasate ca un singur argument:
dc "rate=1.1934;150/rate;19.75/rate217/rate"
Ghilimelele sint necesare aici din cauza ca ';' este separator de
comenzi in
sistemul UNIX.

3.2 Sumar de operatori
------------------
Operatorii C++ sint descrisi sistematic si complet in &r7.
Aici,
este un sumar al lor si niste exemple. Fiecare operator este urmat de
unul sau
mai multe nume utilizate in comun pentru el si de un exemplu de
utilizare a
lui. In aceste exemple class_name este numele unei clase, member este
un nume
al unui membru, un object este o expresie care produce un obiect, un
pointer
este o expresie care produce un pointer, o expr este o expresie, iar o
lvalue
este o expresie ce noteaza un obiect neconstant. Un type poate fi un
nume de
tip general complet (cu *, (), etc.) numai cind el apare in paranteze.
Altfel
exista restrictii.
Operatorii unari si operatorii de atribuire se asociaza de la
dreapta;
toti ceilalti se asociaza de la stinga.
Adica
a = b = c inseamna a = (b = c),
a + b + c inseamna (a + b) + c,
iar
*p++ inseamna *(p++), nu (*p)++.
-----------------------------------------------------------------
| SUMAR DE OPERATORI |
-----------------------------------------------------------------
| :: domeniu de existenta class_name::member |
| :: global ::name |
-----------------------------------------------------------------
| -> selectare de membru pointer->member |
| [] indexare pointer[expr] |
| () apel de functie expr(expr_list) |
| () constructie de valoare type(expr_list) |
| sizeof dimensiunea unui obiect sizeof expr |
| sizeof dimensiunea unui tip sizeof(type) |
-----------------------------------------------------------------
| ++ increment postfixat lvalue++ |
| ++ increment prefixat ++lvalue |
| -- decrement postfixat lvalue-- |
| -- decrement prefixat --lvalue |
| ~ complement ~expr |
| ! negare !expr |
| - minus unar -expr |
| + plus unar +expr |
| & adresa &lvalue |
| * indirectare *expr |
| new creaza(aloca) new type |
| delete distruge(dealoca) delete pointer |
| delete[] distruge un vector delete[expr]pointer|
| (type) conversie de tip (type)expr |
-----------------------------------------------------------------
| * inmultire expr * expr |
| / impartire expr / expr |
| % modulo(rest) expr % expr |
-----------------------------------------------------------------
| + adunare(plus) expr + expr |
| - scadere(minus) expr - expr |
-----------------------------------------------------------------
| << deplasare stinga expr << expr |
| >> deplasare dreapta expr >> expr |
-----------------------------------------------------------------
| < mai mic expr < expr |
| <= mai mic sau egal expr <= expr |
| > mai mare expr > expr |
| >= mai mare sau egal expr >= expr |
-----------------------------------------------------------------
| == egal expr == expr |
| != diferit expr != expr |
-----------------------------------------------------------------
| & si pe biti expr & expr |
-----------------------------------------------------------------
-----------------------------------------------------------------
| ^ sau exclusiv pe biti expr ^ expr |
-----------------------------------------------------------------
| | sau pe biti expr | expr |
-----------------------------------------------------------------
| && si logic expr && expr |
-----------------------------------------------------------------
| || sau logic expr || expr |
-----------------------------------------------------------------
| ? : if aritmetic expr ? expr : expr |
-----------------------------------------------------------------
| = asignare simpla lvalue = expr |
| *= inmultire si asignare lvalue *= expr |
| /= impartire si asignare lvalue /= expr |
| %= modulo si asignare lvalue %= expr |
| += adunare si asignare lvalue += expr |
| -= scadere si asignare lvalue -= expr |
| <<= deplasare stinga si asignare lvalue <<= expr |
| >>= deplasare dreapta si asignare lvalue >>= expr |
| &= si pe biti si asignare lvalue &= expr |
| |= sau pe biti si asignare lvalue |= expr |
| ^= sau exclusiv pe biti si asignare lvalue ^= expr |
-----------------------------------------------------------------
| , virgula(succesiune) expr, expr |
-----------------------------------------------------------------
Fiecare dreptunghi contine operatori cu aceeasi prioritate. Un
operator
are o prioritate mai mare decit operatorii aflati in dreptunghiuri
inferioare.
De exemplu:
a + b * c
inseamna
a + (b * c)
deoarece * are prioritate mai mare decit +, iar a + b - c inseamna (a +

b) - c
deoarece + si - au aceeasi prioritate, dar operatorii + si - sint
asociati de
la stinga spre dreapta.

3.2.1 Paranteze rotunde
-----------------

Parantezele rotunde sint suprasolicitate in sintaxa lui C++. Ele
au un
numar mare de utilizari: includ argumentele in apelurile de functii,
includ
tipul intr-o conversie de tip, includ nume de tipuri pentru a nota
functii si,
de asemenea, pentru a rezolva conflictul prioritatilor intr-o expresie.

Din
fericire, ultimul caz nu este necesar foarte frecvent deoarece regulile

cu
nivelele de prioritate si de asociativitate sint astfel definite ca
expresiile
sa "functioneze" asa cum ne asteptam (adica sa re flecte utilizarile
cele mai
frecvente). De exemplu:

if(i <= 0 || max < i)
//..........

are intelesul obisnuit. Cu toate acestea, parantezele ar trebui
utilizate ori
de cite ori un programator este in dubiu despre acele reguli:

if((i <= 0)||(max < i))
//..........

Utilizarea parantezelor este mai frecventa cind subexpresiile sint

mai
complicate; dar subexpresiile complicate sint o sursa de erori, asa ca
daca
simtim nevoia de a folosii paranteze am putea sa descompunem expresiile
utilizind variabile auxiliare. Exista, de asemenea, cazuri cind
prioritatea
operatorilor nu conduce la o interpretare "evidenta". De exemplu:

if(i & mask == 0)
//..........

nu aplica o masca la i si apoi testeaza daca rezultatul este zero.
Intrucit ==
are o prioritate mai mare decit &, expresia este interpretata ca: i &
(mask
== 0). In acest caz parantezele sint importante:

if((i & mask) == 0)
//..........

De asemenea, poate fi util sa observam ca secventa de mai jos nu
functioneaza in modul in care s-ar astepta un utilizator naiv:

if(0 <= a <= 99)
//.........

este legal, dar se interpreteaza ca:

(0 <= a) <= 99

si rezultatul primei comparatii este 0 sau 1 si nu a (daca a
diferit de
1). Pentru a testa daca a este in domeniul 0..99 se poate folosi:

if(0 <= a && a <= 99)
//..........


3.2.2 Ordinea de evaluare
-------------------

Ordinea de evaluare a subexpresiilor intr-o expresie este
nedefinita. De
exemplu:

int i = 1;
v[i] = i++;

poate fi evaluata sau ca v[1] = 1, sau ca v[2] = 1. Un cod mai bun se
poate
genera in absenta restrictiilor asupra ordinii de evaluare a
expresiilor. Ar
fi mai bine daca compilatorul ne-ar avertiza despre astfel de
ambiguitati;majoritatea compilatoarelor nu fac acest lucru.
Operatorii && si || garanteaza faptul ca operandul lor sting se
evalueaza
inaintea celui drept. De exemplu, b = (a = 2, a + 1) atribuie lui b
valoarea
3. Exemple de utilizare a lui && si || se dau in paragraful &3.3.1. Sa
observam ca operatorul virgula este logic diferit de virgula folosita
pentru a
separa argumente intr-un apel de functie. Sa consideram:

f1(v[i], i++); //doua argumente
f2((v[i], i++)); //un argument

Apelul lui f1 are doua argumente, v[i] si i++, iar ordinea de
evaluare a
expresiilor argument este nedefinita. Ordinea de evaluare a expresiilor
argument este neportabila si nu este precizata. Apelul lui f2 are un
singur
argument si anume expresia (v[i], i++).
Parantezele nu pot fi utilizate pentru a forta ordinea de
evaluare;
a*(b/c) poate fi evaluata ca (a*b)/c deoarece * si / au aceeasi
precedenta.
Cind ordinea de evaluare este importanta, se pot introduce variabile
temporare. De exemplu:
(t = b / c, a * t)


3.2.3 Incrementare si Decrementare
----------------------------

Operatorul ++ se utilizeaza pentru a exprima o incrementare
directa in
schimbul exprimarii ei folosind o combinatie intre adunare si
atribuire. Prin
definitie, ++lvalue inseamna: lvalue += 1 care din nou inseamna
lvalue =
lvalue + 1 cu conditia ca lvalue sa nu aiba efecte secundare. Expresia
care
noteaza obiectul de incrementat se evalueaza o singura data.
Decrementarea
este exprimata similar prin operatorul --. Operatorii ++ si -- pot fi
utilizati ambii atit prefix cit si postfix. Valoarea lui ++x este noua
valoare
a lui x (adica cea incrementata). De exemplu y = ++x este echivalent cu

y = (x
+= 1). Valoarea lui x++ este valoarea veche a lui x. De exemplu y=x++
este
echivalent cu y = (t=x, x+=1, t), unde t este o variabila de acelasi
tip cu x.
Operatorii de incrementare sint utili mai ales pentru a incrementa

si
decrementa variabile in cicluri. De exemplu se poate copia un sir
terminat cu
zero astfel:

inline void cpy(char* p, const char* q){while(*p++ = *q++);}

Sa ne amintim ca incrementind si decrementind pointeri, ca si
adunarea
sau scaderea dintr-un pointer, opereaza in termenii elementelor
vectorului
spre care pointeaza pointerul in cauza; p++ face ca p sa pointeze spre
elementul urmator. Pentru un pointer de tip T*, are loc prin definitie:
long(p + 1) == long(p) + sizeof(T);


3.2.4 Operatori logici pe biti
-------------------------

Operatorii logici pe biti &, |, ^, ~, >> si << se aplica la
intregi;
adica obiecte de tip char, short, int, long si corespunzatoarele lor
fara semn
(unsigned), iar rezultatele lor sint de asemenea intregi.
O utilizare tipica a operatorilor logici pe biti este de a
implementa
seturi mici (vectori de biti). In acest caz fiecare bit al unui intreg
fara
semn reprezinta numai un membru al setului, iar numarul de biti
limiteaza
numarul de membri. Operatorul binar & este interpretat ca intersectie,
| ca
reuniune si ^ ca diferenta. O enumerare poate fi utilizata pentru a
numi
membri u. Se utilizeaza operatorul |= deoarece sirul ar putea
fi deformat deja (adica state == _bad) asa ca: cin.state = _eof ar

fi
sters conditia
respectiva. Se poate gasi modul in care difera doua stari astfel:

state_value diff = cin.state ^ cout.state;

Pentru tipul stream_state o astfel de diferenta nu este foarte
folositoare, dar pentru alte tipuri similare ea este mai utila. De
exemplu, sa
consideram compararea unui vector de biti care reprezinta setul de
intreruperi
de prelucrat cu un altul care reprezinta setul de intreruperi ce
asteapta sa
fie prelucrat.
Sa observam ca utilizind cimpurile (&2.5.1) se obtine o
prescurtare
convenabila pentru a deplasa masca si a extrage cimpuri de biti
dintr-un
cuvint. Aceasta se poate face, evident, utilizind operatorii logici pe
biti.
De exemplu, se pot extrage 16 biti din mijlocul unui int de 32 de biti
astfel:
unsigned short middle(int a){ return (a >> 8) & 0xffff; }
Sa nu se faca confuzie intre operatorii logici pe biti cu cei logici
&&, || si
!. Acestia din urma returneaza sau 0 sau 1 si ei sint in primul rind
utili
pentru a scrie teste in if, while sau for (&3.3.1). De exemplu !0
(negatia
lui 0) are valoarea 1, in timp ce ~0 (complementul lui zero) reprezinta
valoarea -1 (toti biti sint unu).

3.2.5 Conversia tipului
-----------------
Uneori este necesar sa se converteasca o valoare de un tip spre o
valoare
de un alt tip. O conversie de tip explicit produce o valoare de un tip
dat
pentru o valoare a unui alt tip. De exemplu:
float r = float(1);
converteste valoarea 1 spre valoarea flotanta 1.0 inainte de a face
atribuirea. Rezultatul unei conversii de un tip nu este o lvalue deci
nu i se
poate face o asignare (numai daca tipul este un tip referinta).
Exista doua notatii pentru conversia explicita a tipului: notatia
traditionala din C (de exemplu (double)) si notatia functionala
(double(a)).
Notatia functionala nu poate fi folosita pentru tipuri care nu au un
nume
simplu. De exemplu, pentru a converti o valoare spre un pointer se
poate
folosi notatia din C:
char* p = (char*)0777;
sau sa se defineasca un nume de tip nou:
typedef char* pchar;
char* p = pchar(0777);
Dupa parerea mea, notatia functionala este preferabila pentru exemple
netriviale. Sa consideram aceste doua exemple echivalente:
pname n2 = pbase(n1->tp)->b_name;
pname n3 = ((pbase)n2->tp)->b_name;
Intrucit operatorul -> are prioritate mai mare decit (tip), ultima
expresie se
interpreteaza astfel:
((pbase)(n2->tp))->b_name
Utilizind explicit conversia de tip asupra tipurilor pointer este
posibil sa
avem pretentia ca un obiect sa aiba orice tip. De exemplu:
any_type* p = (any_type*)&some_object;
va permite ca some_object sa fie tratat ca any_type prin p.
Cind o conversie de tip nu este necesara ea trebuie eliminata.
Programele
care utilizeaza multe conversii explicite sint mai greu de inteles
decit
programele care nu le utilizeaza. Totusi, astfel de programe sint mai
usor de
inteles decit programele care pur si simplu nu utilizeaza tipuri
pentru a
reprezenta concepte de nivel mai inalt (de exemplu, un program care
opereaza
cu un registru de periferic folosind deplasari si mascari de intregi in

loc de
a defini o structura corespunzatoare si o operatie cu ea; vezi &2.5.2).

Mai
mult decit atit, corectitudinea unei conversii explicite de tip depinde

adesea
in mod esential de intelegerea de catre programator a modului in care
diferite
tipuri de obiecte sint tratate in limbaj si foarte adesea de detaliile
de
implementare. De exemplu:
int i = 1; char* pc = "asdf";
int* pi = &i;
i = (int)pc;
pc = (char*)i; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// sizeof(int) < sizeof(char*)
pi = (int*)pc;
pc = (char*)pi; // nu se recomanda: pc s-ar putea sa-si
// schimbe valoarea. Pe anumite masini
// char* se reprezinta diferit de int*
Pe multe masini nu se va intimpla nimic rau, dar pe altele
rezultatul va
fi dezastruos. In cel mai bun caz, un astfel de cod nu este portabil.
De
obicei este gresit sa presupunem ca pointerii la diferite structuri au
aceeasi
reprezentare. Mai mult decit atit, orice pointer poate fi asignat la un

void*
(fara un tip explicit de conversie) si un void* poate fi convertit
explicit la
un pointer de orice tip.
In C++, conversia explicita de tip nu este necesara in multe
cazuri in
care in C este necesara. In multe programe conversia explicita de tip
poate fi
complet eliminata, iar in multe alte programe utilizarea ei poate fi
localizata in citeva rutine.

3.2.6 Memoria libera
--------------

Un obiect denumit este sau static sau automatic (vezi &2.1.3). Un
obiect
static se aloca cind incepe programul si exista pe durata executiei
programului! Un obiect automatic se aloca de fiecare data cind se intra

in
blocul lui si este eliminat numai cind se iese din bloc. Adesea este
util sa
se creeze un obiect nou care exista numai cit timp este nevoie de el.
In
particular, adesea este util sa se creeze un obiect care poate fi
utilizat
dupa ce se revine dintr-o functie in care el a fost creat. Operatorul
new
creaza astfel de obiecte, iar operatorul delete poate fi folosit pentru

a le
distruge mai tirziu. Obiectele alocate prin new se spune ca sint in
memoria
libera. Astfel de obiecte sint de exemplu nodurile unui arbore sau a
unei
liste inlantuite care sint parte a unei sructuri de date mai mari a
carei
dimensiune nu poate fi cunoscuta la compilare.Sa consideram modul in
care s-ar
putea scrie un compilator in stilul folosit la calculatorul de birou.
Functiile de analiza sintactica ar putea construi o reprezentare sub
forma de
arbore a expresiilor, care sa fie utilizata de generatorul de cod. De
exemplu:
struct enode{
token_value oper;
enode* left;
enode* right;
};
enode* expr()
{
enode* left = term();

for(;;)
switch(curr_tok)
{
case PLUS :
case MINUS: get_token();
enode* n = new enode;
n->oper = curr_tok;
n->left = left;
n->right = term();
left = n;
break;
default : return left;
}
}

Un generator de cod ar putea utiliza arborele rezultat astfel:

void generate(enode* n)
{
switch(n->oper)
{
case PLUS: //face ceva potrivit starii curente
delete n;
}
}

Un obiect creat prin new exista pina cind este distrus explicit
prin
delete dupa care spatiul ocupat de el poate fi reutilizat prin new. Nu
exista
"colectarea rezidurilor". Operatorul delete se poate aplica numai la un
pointer returnat de new sau la zero. Aplicarea lui delete la zero nu
are nici
un efect. Se pot, de asemenea, crea vectori de obiecte prin intermediul

lui
new. De exemplu:

char* save_string(char* p)
{
char* s = new char[strlen(p)+1];
strcpy(s, p);
return s;
}

Sa observam ca pentru a dealoca spatiul alocat prin new, delete
trebuie
sa fie capabil sa determine dimensiunea obiectului alocat. De exemplu:
int main(int argc, char* argv[])
{
if(argc < 2)
exit(1);
char* p = save_string(argv[1]);
delete p;
}

Aceasta implica faptul ca un obiect alocat utilizind implementarea
standard prin new va ocupa putin mai mult spatiu decit un obiect static

(de
obicei un cuvint in plus).
Este de asemenea, posibil sa se specifice dimensiunea unui vector
explicit intr-o operatie de stergere. De exemplu:

int main(int argc, char* argv[])
{
if(argc < 2)
exit(1);
int size = strlen(argv[1])+1;
char* p = save_string(argv[1]);
delete[size] p;
}

Dimensiunea vectorului furnizata de utilizator se ignora exceptind

unele
tipuri definite de utilizator (&5.5.5).
Operatorii de memorie libera se implementeaza prin functiile
(&r7.2.3):

void* operator new(long);
void operator delete(void*);
Implementarea standard a lui new nu initializeaza obiectul
returnat. Ce
se intimpla daca new nu gaseste memorie de alocat. Intrucit chiar
memoria
virtuala este finita, uneori se poate intimpla acest lucru; o cerere de

forma:

char* p = new char[100000000];

de obicei va cauza probleme. Cind new esueaza, ea apeleaza functia spre

care
pointeaza pointerul _new_handler (pointerii spre functii vor fi
discutati in
&4.6.9). Noi putem seta acel pointer direct sau sa utilizam functia
set_new_handler(). De exemplu:

#include <stream.h>

void out_of_store()
{
cerr << "operator new failed: out of store\n";
exit(1);
}

typedef void (*PF)(); //pointer spre tipul functiei
extern PF set_new_handler(PF);

main()
{
set_new_handler(&out_of_store);
char* p = new char[100000000];
cout << "done, p= " << long(p) << "\n";
}

de obicei niciodata nu va scrie done dar in schimb va produce:

operator new failed: out of store

Un _new_handler ar putea face ceva mai destept decit pur si simplu

sa
termine programul. Daca noi stim cum lucreaza new si delete, de
exemplu,
deoarece noi furnizam operatorii nostri proprii new() si delete(),
handlerul
ar putea astepta sa gaseasca memorie pentru new. Cu alte cuvinte, un
utilizator ar putea furniza un colector de reziduri, redind in
utilizare
zonele sterse. Aceasta evident nu este o sarcina pentru un incepator.
Din motive istorice, new pur si simplu returneaza pointerul 0 daca

el nu
gaseste destula memorie si nu a fost specificat un _new_handler. De
exemplu:

#include <stream.h>
main()
{
char* p = new char[100000000];
cout << "done, p= " << long(p) << "\n";
}

va produce
done, p= 0

Noi am avertizat! Sa observam ca furnizind _new_handler se
verifica
depasirea memoriei pentru orice utilizare a lui new in program
(exceptind
cazul cind utilizatorul furnizeaza rutine separate pentru tratarea
alocarii
obiectelor de tipuri specifice definite de utilizator; vezi &5.5.6).
3.3 Sumarul instructiunilor
-----------------------

Instructiunile C++ sint descrise sistematic si complet in &r.9. Cu

toate
acestea, dam mai jos un rezumat si citeva exemple.
Sintaxa instructiunilor:
statement:
declaration
{
statement_list_opt
}
expression_opt;
if(expression) statement
if(expression) statement else statement
switch(expression) statement
while(expression) statement
do statement while(expression);
for(statement expression_opt; expression_opt)
statement
case constant_expression: statement
default: statement
break;
continue;
return expression_opt;
goto identifier;
identifier: statement
statement_list:
statement
statement statement_list
Sa observam ca o declaratie este o instructiune si ca nu exista
nici o
instructiune de atribuire sau de apel; atribuirea si apelul functiei se
trateaza ca expresii.

3.3.1 Teste
-----

O valoare poate fi testata sau printr-o instructiune if sau
printr-o
instructiune switch:
if(expression) statement
if(expression) statement else statement
switch(expression) statement
Nu exista in C++ tipul boolean separat.
Operatorii de comparare == != < <= > >= returneaza valoarea 1

daca
compararea este adevarata si 0 altfel. Nu este ceva iesit din comun ca
sa
consideram ca true se defineste ca 1 si false ca 0.
Intr-o instructiune if se executa prima (sau singura) instructiune

daca
expresia este diferita de zero si altfel se executa cea de a doua
instructiune
(daca este prezenta). Aceasta implica faptul ca orice expresie intreaga

poate
fi utilizata ca o conditie. In particular, daca a este un intreg:
if(a)
//........
este echivalent cu
if(a != 0)
//........
Operatorii logici &&, || si ! sint cei mai utilizati in
conditii.
Operatorii && si || nu vor evalua cel de al doilea argument al lor
numai daca
este necesar. De exemplu:

if(p && 1 < p->count)
//........

intii testeaza ca p nu este nul si numai daca este asa se testeaza 1 <
p->count.
Anumite instructiuni if simple pot fi inlocuite convenabil
inlocuindu-le
prin expresii if aritmetice. De exemplu:

if(a <= b)
max = b;
else
max = a;

este mai bine sa fie exprimat prin

max = (a<=b) ? b:a;

Parantezele in jurul conditiei nu sint necesare, dar codul este
mai usor
de citit cind sint utilizate.
Anumite instructiuni switch simple pot fi scrise prin mai multe
instructiuni if. De exemplu:

switch(val)
{
case 1: f();
break;
case 2: g();
break;
default: h();
break;
}

se poate scrie

if(val==1)
f();
else if(val==2)
g();
else h();

Intelesul este acelasi, dar prima versiune (cu switch) este de
preferat
din cauza ca natura operatiei (testul unei valori fata de un set de
constante)
este explicita in acest caz. Aceasta face ca instructiunea switch sa
fie mai
usor de citit.
Sa avem grija ca un case al unui switch trebuie terminat cumva
daca nu
dorim ca executia sa continue cu case-ul urmator. De exemplu:
switch(val)
{
case 1: cout << "case 1\n";
case 2: cout << "case 2\n";
default: cout << "default: case not found\n";
}
cu val == 1 va imprima
case 1
case 2
default: case not found
spre marea surprindere a neinitiatilor. Cel mai frecvent mod de
intrerupere al
unui case este terminarea prin break, dar se poate adesea folosi o
instructiune return sau goto. De exemplu:
switch(val)
{case 0: cout << "case 0\n";
case 1: cout << "case 1\n";
return;
case 2: cout << "case 2\n";
goto case 1;
default: cout << "default: case not found\n";
return;
}

Apelat cu val == 2, produce

case 2
case 1

Sa observam ca o scriere de forma
goto case 1;
este o eroare sintactica.

3.3.2 Goto
----

C++ are faimoasa instructiune goto.

goto identifier;
identifier: statement

Are putine utilizari in limbajele de nivel inalt, dar poate fi
foarte
util cind un program C++ este generat printr-un program in loc ca
programul sa
fie scris direct de catre o persoana; de exemplu, goto-urile pot fi
utilizate
intr-un analizor generat dintr-o gramatica printr-un generator de
analizoare.
Goto poate fi, de asemenea, important in acele cazuri cind
eficienta
optimala este esentiala, de exemplu, in ciclul interior al unei
aplicatii de
timp real.
Una din putinele utilizari bune ale lui goto este iesirea dintr-un

ciclu
imbricat sau switch (instructiunea break intrerupe numai ciclul sau
switch-ul
cel mai interior care o contine). De exemplu:
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
if(nm[i][j] == a)
goto found;
// not found
//...........
found:
// nm[i][j] == a;
Exista de asemenea instructiunea continue, care transfera
controlul la
sfirsitul instructiunii ciclice, asa cum s-a explicat in &3.1.5.

3.4 Comentarii si Decalari
----------------------

Utilizarea judicioasa a comentariilor si utilizarea consistenta a
decalarilor poate face sarcina citirii si intelegerii unui program mai
placuta. Exista diferite stiluri ale decalarilor. Autorul nu vede
motive
fundamentale pentru a prefera un stil fata de altul (deci, ca multi
altii, eu
am preferintele mele). Acelasi lucru se aplica si la stilurile de
comentare.
Comentariile pot fi omise, dar atunci citirea programului va fi
serios
afectata. Compilatorul nu intelege continutul unui comentariu, asa ca
nu
exista nici o cale de a ne asigura ca un comentariu:

[1] este de neinteles;
[2] descrie programul;
[3] este pus la zi.

Multe programe contin comentarii care sint incomprehensibile,
ambigue si
chiar eronate. Comentariile rele pot fi mai rele decit daca nu ar
exista.
Daca ceva poate fi exprimat in limbajul insusi, ar trebui sa fie
mentionat in el, nu numai intr-un comentariu. Aceasta remarca este
intarita de
comentariile de mai jos:

//variabila "v" trebuie sa fie initializata
//variabila "v" trebuie sa fie folosita numai de functia "f()"
//apeleaza functia "init()" inainte de a apela
//orice alta functie din acest fisier
//apeleaza functia "cleanup()" la sfirsitul programului
//sa nu se utilizeze functia "wierd()"
//functia "f()" are doua argumente

Astfel de comentarii pot adesea sa fie interpretate ca necesare
printr-o
utilizare corespunzatoare a lui C++. De exemplu, s-ar putea utiliza
regulile
de linkere (&4.2) si vizibilitate, initializare si curatire pentru
clase (vezi
&5.5.2) pentru a face exemplele precedente redondante.
Odata ce a fost afirmat ceva clar in limbaj, nu ar trebui
mentionat a
doua oara intr-un comentariu. De exemplu:

a = b+c // a devine b+c
count++ // se incrementeaza count

Astfel de comentarii sint mai rele decit redondanta: ele maresc
cantitatea de text pe care trebuie sa o citeasca programatorul si ele
adesea
fac mai obscura structura programatorului.
Preferintele autorului sint pentru:

[1] Un comentariu pentru fiecare fisier sursa care sa afirme
ce declaratii din el se utilizeaza in comun, scopuri
generale pentru mentinere, etc.
[2] Un comentariu pentru fiecare functie netriviala care sa
indice scopul ei, algoritmul utilizat (daca nu este
evident) si poate ceva despre mediul de executie al ei.
[3] Citeva comentarii in locurile unde codul nu este evident
si/sau neportabil.
[4] Foarte mici alternative else.

De exemplu:

// tbl.c: Implementarea tabelei de simboluri
/* Eliminare Gauss prin pivotare
partiala. Vezi Ralston:...pg...
*/
//swap() presupune utilizarea stivei la un AT&T 3B20.
/*********************************
Copyright (c) 1984 AT&T. Inc.
All rights reserved
*********************************/

Un set de comentarii bine ales si bine scris este o parte
esentiala a
unui program bun. Scrierea de comentarii bune poate fi tot atit de
dificil ca
si scrierea programului insusi.
Sa observam, de asemenea, ca daca se folosesc comentariile cu //
intr-o
functie, atunci orice parte a acelei functii poate fi comentata
utilizind
stilul de comentarii /*...*/ si viceversa.

3.5 Exercitii
---------

1. (*1). Sa se scrie instructiunea urmatoare ca o instruc-
tiune while echivalenta:

for(i = 0; i < max_length; i++)
if(input_line[i] == '?')
quest_count++;

Sa se rescrie utilizind un pointer ca si variabila de control; adica
asa ca
testul sa fie unul de forma *p == '?'.

2. (*1). Sa se includa complet in paranteze expresiile
urmatoare:

a = b + c * d << 2 & 8
a & 077 != 3
a == b || a == c && c < 5
c = x != 0
0 <= i < 7
f(1, 2) + 3
a = -1+ +b-- -5
a = b == c++
a = b = c = 0
a[4][2] *= *b ? c : *d * 2
a - b, c = d

3. (*2). Sa se gaseasca 5 constructii C++ diferite pentru care
sensul
este nedefinit.

4. (*2). Sa se gaseasca 10 exemple de cod C++ neportabile.

5. (*1). Ce se intimpla daca se face o impartire cu zero pe
sistemul d-voastra? Ce se intimpla in cazul unei depasiri superioare
sau
inferioare.

6. (*1). Sa se includa complet in paranteze expresiile
urmatoare:

*p++
*--p
+++a--
(int*)->m
*p.m
*a[i]

7. (*2). Sa se scrie functiile strlen() care returneaza
lungimea unui sir, strcpy() care copiaza un sir in altul si strcmp()
care
compara doua siruri. Sa se considere ce tipuri de argumente si ce
tipuri se
cuvine sa se returneze, apoi sa se compare cu versiunile standard asa
cum sint
declarate in <string.h>.


8. (*1). Vedeti cum reactioneaza compilatorul d-voastra la aceste

erori:

a := b+1;
if(a = 3)
//.....
if(a & 077 == 0)
//.....

9. (*2). Sa se scrie o functie cat() care are doua argumente de
tip sir
si returneaza un sir care este concatenarea argumentelor. Sa se
utilizeze new
pentru a gasi memorie pentru rezultat. Sa se scrie o functie rev() care

are un
argument de tip sir si reutilizeaza caracterele din el. Adica, dupa
rev(p),
ultimul caracter a lui p va fi primul, etc.

10. (*2). Ce face exemplul urmator?

void send(register* to, register* from, register count)
{register n = (count+7)/8;
switch(count%8)
{
case 0: do{
*to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
}while(--n > 0);
}
}
De ce ar vrea cineva sa scrie un astfel de program?

11. (*2). Sa se scrie o functie atoi() care are ca argument un sir

ce
contine cifre si returneaza int-ul corespunzator. De exemplu,
atoi("123") este
123. Sa se modifice atoi() pentru a trata sirurile octale din C++ si in

plus
si cele hexazecimale. Sa se modifice atoi() pentru a trata caracterele
C++
utilizate intr-o notatie de constanta. Sa se scrie o functie itoa()
care
creaza un sir pornind de la un argument intreg.

12. (*2). Sa se rescrie get_token() (&3.1.2) asa ca sa
citeasca o
linie la un moment dat intr-un buffer si apoi sa compuna unitatile
citind
caracterele din buffer.

13. (*2). Sa se adauge functii de forma sqrt(), log() si sin()
la
calculatorul de birou din &3.1. Sa se predefineasca numele si apelul
functiilor printr-un vector de pointeri spre functii. Sa nu se uite sa

se
verifice argumentele dintr-o functie call.
14. (*3). Sa se permita unui utilizator sa defineasca
functii in
calculatorul de birou:
Scop: Sa se defineasca o functie ca un sir de operatii exact asa
cum un
utilizator ar trebui sa o scrie. Un astfel de sir poate fi memorat sau
ca un
sir de caractere sau ca o lista de unitati. Apoi se citeste si se
executa
acele operatii cind functia este apelata. Daca noi dorim ca o functie
utilizator sa aiba argumente, noi trebuie sa inventam o notatie pentru
aceasta.

15. (*1.5). Sa se converteasca calculatorul de birou pentru a
utiliza un
simbol structura in loc sa se utilizeze variabilele statice name_string

si
number_value:

struct symbol{
token_value tok;
union{
double number_value;
char* name_string;
};
};

16. (*2.5). Sa se scrie un program care elimina comentariile de
tip C++
din program. Adica, citeste din cin si elimina atit comentariile de
forma //,
cit si cele de forma /*..*/ si scrie rezultatul in cout. Trebuie sa
avem grija
de // si /*..*/ din comentarii, siruri si constante caracter.

CAPITOLUL 4
===========


FUNCTII SI FISIERE
==================

Toate programele netriviale sint alcatuite din diferite unitati
compilate
separat (conventional, numite fisiere). Acest capitol descrie cum se
compileaza functiile separat, cum se pot apela una pe alta, cum
functiile
compilate separat pot utiliza date in comun si cum tipurile utilizate
in
diferite fisiere ale programului pot fi tinute consistent
(necontradictoriu).Functiile se discuta in anumite detalii; aceasta
include
transferul de argumente, argumente implicite, nume de functii care se
suprain-
carca, pointeri spre functii si desigur, declaratii si definitii de
functii.
In final sint prezentate macrourile.

4.1. Introducere
-----------

A avea un program complet intr-un fisier este de obicei imposibil
deoarece codul pentru bibliotecile standard si de sistem sint in alta
parte.
Mai mult decit atit, avind fiecare utilizator codul sau intr-un singur
fisier
este ceva care este atit impractic cit si inconvenient. Modul in care
este
organizat un program in fisiere poate ajuta cititorul sa inteleaga
structura
unui program si sa permita compilatorului sa impuna acea structura.
Intrucit
unitatea de compilare este un fisier, tot fisierul trebuie sa fie
recompilat
ori de cite ori s-a facut in el o schimbare.
Pentru un program dimensionat chiar moderat, timpul petrecut
pentru
recompilare poate fi redus semnificativ partitionind programul in
fisiere
dimensionate potrivit.
Sa consideram exemplul cu calculatorul de birou. A fost prezentat
ca un
singur fisier sursa. Daca il tastam, noi fara indoiala avem niste
probleme
minore in obtinerea declaratiilor in ordine corecta si cel putin o
declaratie
trebuie utilizata pentru a permite compilatorului sa trateze functiile
mutual
recursive expr(), term() si prim(). Textul amintit are patru parti
(analizor
lexical, analizor sintactic, tabela de simboluri si un driver), dar
aceasta
nu se reflecta in nici un fel in cod. In realitate calculatorul nu a
fost
scris in acest fel. Acesta nu este modul de a o face; chiar daca toate
consideratiile metodologiei de programare, mentinere si eficienta
compilarii
au fost deconsiderate pentru acest program, autorul totusi va
partitiona
acest program de 200 de linii in mai multe fisiere pur si simplu pentru

a face
sarcina programarii mai placuta.
Un program care consta din mai multe parti compilate separat
trebuie sa
fie consistent (necontradictoriu) in utilizarea numelor si tipurilor in

exact
acelasi mod ca si un program care consta dintr-un singur fisier sursa.
In
principiu, aceasta se poate asigura prin linker. Linkerul este
programul care
leaga partile compilate separat. Un linker uneori este numit (gresit)
incarcator; linkerul UNIX-ului se numeste ld. Cu toate acestea linkerul
disponibil pe majoritatea sistemelor este prevazut cu putine facilitati

care
sa verifice consistenta modulelor compilate separat.
Programatorul poate compensa lipsa acestor facilitati ale
linkerului
furnizind informatii de tip suplimentare (declaratii). Un program poate

fi
realizat consistent asigurind ca declaratiile prezentate in compilari
separate
sa fie consistente. C++ a fost definit ca un instrument care sa
incurajeze
astfel de compilari cu declaratii explicite si este prevazut un linker
care sa
verifice consistenta modulelor respective. Un astfel de linker se spune

ca
face o linkare explicita. In cazul limbajului C nu se realizeaza o
linkare
explicita ci numai una implicita si ea este adesea saraca in testarea
consistentei modulelor linkate.

4.2. Link-editare
------------

Daca nu se stabileste altfel, un nume care nu este local la o
functie sau
clasa trebuie sa refere acelasi tip, valoare, functie sau obiect in
orice
parte compilata separat a programului. Deci exista numai un tip,
valoare,
functie sau obiect nelocal atasat la un nume intr-un program. De
exemplu,
consideram doua fisiere:
// file1.c:
int a = 1;
int f(){/* face ceva */}
// file2.c:
extern int a;
int f();
void g(){a = f();}

'a' si f() utilizati in file2.c sint cele definite in file1.c. Cuvintul

cheie
extern indica faptul ca declaratia lui a in file2.c este (chiar) o
declaratie
si nu o definitie. Daca 'a' ar fi fost initializata, extern ar fi fost
pur si
simplu ignorata deoarece o declaratie cu initializator este totdeauna o
definitie. Un obiect trebuie sa fie definit exact odata intr-un
program. Poate
fi declarat de mai multe ori, dar tipul trebuie sa coincida exact. De
exemplu:
// file1.c:
int a = 1;
int b = 1;
extern int c;
// file2.c:
int a;
extern double b;
extern int c;
Exista trei erori: 'a' este definit de doua ori (int a: este o
definitie
insemnind int a = 0); 'b' este declarat de doua ori cu diferite tipuri;

'c'
este declarat de doua ori dar nu este definit. Aceste tipuri de erori
(erori
de linkare) nu pot fi detectate cu un compilator care analizeaza odata

numai
un fisier. Ele sint, totusi, detectate la linkare.
Programul urmator nu este in C++ (chiar daca el este in C):
// file1.c:
int a;
int f(){return a;}

// file2.c:
int a;
int g(){return f();}
Intii, file2.c nu este C++ deoarece f() nu a fost declarat, asa ca,
compilarea
va esua. In al doilea rind programul nu se va putea linka deoarece 'a'
este
definit de doua ori.
Un nume poate fi local la un fisier declarindu-l static. De
exemplu:

// file1.c
static int a = 6;
static int f(){/*.......*/}

// file2.c
static int a = 7;
static int f(){/*.......*/}

Intrucit fiecare 'a' si f() este declarat static, programul
rezultat este
corect. Fiecare fisier are pe 'a' si f() propriu. Cind variabilele si
functiile sint declarate static explicit, un fragment de program este
mai usor
de inteles (nu trebuie sa ne uitam in alta parte). Utilizind static
pentru
functii putem avea, de asemenea, un efect benefic asupra cantitatii de
functii
utilizate si dind compilatorului informatii care pot fi utilizate in
ideea
realizarii unor optimizari.
Consideram aceste doua fisiere:

// file1.c
const a = 7;
inline int f(){/*.......*/}
struct s{int a, b;};

// file2.c
const a = 7;
inline int f(){/*........*/}
struct s{int a, b;};

Daca se aplica regula a "exact unei definitii" la constante,
functii
inline si definitii de tip in acelasi mod in care se aplica la functii
si
variabile, file1.c si file2.c nu pot fi parte ale aceluiasi program
C++. Dar
daca este asa, cum pot doua fisiere sa utilizeze aceleasi tipuri si
constante?
Raspunsul scurt este ca tipurile, constantele, etc. pot fi definite de
atitea
ori de cit este de necesar cu conditia ca ele sa fie definite identic.
Raspunsul complet este intr-o anumita masura mai complicat (asa cum se
explica
in sectiunea urmatoare).

4.3. Fisiere antet
-------------

Tipurile in toate declaratiile aceluiasi obiect trebuie sa fie
consistente. Un mod de a atinge acest lucru ar fi de a furniza
facilitatile de
verificare de tip linkerului, dar deoarece multe linkere au fost
proiectate in
1950 ele nu pot fi schimbate din motive practice. Este usor a schimba
un
linker, dar facind aceasta si scriind un program care depinde de
imbunatatirile facute, cum mai poate fi acest program transferat
portabil pe
alte calculatoare ?
O alta conceptie este de a asigura ca,codul supus compilarii sa
fie sau
consistent sau sa contina chei care sa permita compilatorului sa
detecteze
inconvenientele. O metoda imperfecta dar simpla de a atinge consistenta

pentru
declaratii in diferite fisiere este de a include fisiere antet, care
sa
contina informatii de interfata din fisierele sursa care contin cod
executabil
si/sau definitii de date.
Mecanismul #incse poate folosi comanda:

CC -E file.c

pentru a prelucra fisierul file.c in acelasi mod ca si cind CC ar fi
inainte
de a incepe compilarea propriu-zisa. Pentru a include fisiere standard,

se
utilizeaza parantezele unghiulare in locul ghilimelelor. De exemplu:
#include <stream.h> // din directorul include standard
#include "myheader.h" // din directorul curent
Avantajul lui "<", ">" este faptul ca numele real al directorului
standard pentru include nu este construit in program.
Un spatiu este semnificativ intr-o directiva include:

#include < stream.h > // nu va gasi stream.h

Ar fi extravagant sa se recompileze un fisier de fiecare data cind

este
inclus undeva, dar timpul necesar pentru a compila un astfel de fisier
de
obicei nu difera mult de timpul necesar pentru a citi o anumita forma
precompilata a lui. Motivul este ca textul programului este o
reprezentare cit
se poate de compacta a programului si ca fisierele incluse, de obicei,
contin
numai declaratii si nu un cod care trebuie sa fie analizat extensiv de
catre
compilator.
Regula urmatoare despre ce poate si ce nu poate fi plasat intr-un
fisier
antet nu este o cerinta a limbajului, ci pur si simplu o sugestie
despre un
mod rezonabil de a utiliza mecanismul #include.
Un fisier antet poate contine:

-----------------------------------------------------------------
|Definitii de tip struct point{int x, y;}; |
|Declaratii de functii extern int strlen{const char*}; |
|Definitii de functii inline inline char get(){return *p++;}; |
|Declaratii de date extern int a; |
|Definitii de constante const float pi = 3.141593; |
|Enumerari enum bool {false, true}; |
|Directive #include #include <signal.h> |
|Macro definitii #define Case break; case |
|Comentarii /* check for end of file */ |
-----------------------------------------------------------------

Dar niciodata nu contine:
-----------------------------------------------------------------
|Definitii de functii ordinare char get(){return *p++;} |
|Definitii de date int a; |
|Definitii de agregate constante const tbl[] = {/*...*/}; |
-----------------------------------------------------------------

In sistemul UNIX, fisierele antet sint cu extensia convenabila .h.
Fisierele care contin definitii de functii si date vor avea extensia
.c. De
aceea ele sint frecvent referite ca "fisiere.h" si respectiv
"fisiere.c".
Macrourile se descriu in &4.7. Sa observam ca macrourile sint pe
departe mai
putin utile in C++ decit in C, deoarece C++ are constructia const in
limbaj
pentru a defini constante inline.
Motivul de a admite definirea de constante simple si nu si a
agregatelor
constante in fisierele.h este pragmatic. In principiu exista o singura
problema in admiterea copiilor definitiilor de variabile (chiar si
definitiile
functiilor pot fi copiate). Cu toate acestea, este foarte dificil
pentru un
linker vechi sa verifice identitatea constantelor netriviale si sa
elimine
duplicatele nenecesare. Mai mult decit atit, cazurile simple sint pe
departe
mai frecvente si de aceea mai importante pentru generarea de cod.

4.3.1. Fisier antet unic
-----------------

Cea mai simpla solutie la problema partitionarii unui program in
diferite
fisiere este de a pune definitiile de functii si date intr-un numar
potrivit
de fisiere sursa si de a declara tipurile necesare pentru a comunica,
intr-un
singur fisier antet care este inclus de toate celelalte fisiere. Pentru
programul calculator putem folosi fisiere.c : lex.c, sgn.c, table.c,
main.c si
un fisier antet dc.h, care contine declaratiile fiecarui nume utilizat
in mai
mult decit un fisier.c:
//dc.h declaratii comune pentru programul calculator
#include <stream.h>
enum token_value
{
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
};
extern int no_of_errors;
extern double error(char* s);
extern token_value get_token();
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern double expr();
extern double term();
extern double prim();
struct name{
char* string;
name* next;
double value;
};
extern name* look(char* p, int ins = 0);
inline name* insert(char* s){return look(s, 1);}
Codul real al lui lex.c va arata astfel:

//lex.c : analiza de intrare si analiza lexicala
#include "dc.h"
#include <ctype.h>
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }

Sa observam ca, utilizind fisierele antet in acest fel se asigura
ca
fiecare declaratie a unui obiect definit de utilizator intr-un fisier
antet va
fi intr-un anumit punct inclus fisierul in care el este definit. De
exemplu,
cind compilam lex.c, compilatorul va intilni:

extern token_value get_token();
// ...
token_value get_token() { /* ... */ }
Aceasta asigura ca, compilatorul va detecta orice inconsis- tenta
in
tipurile specificate pentru un nume. De exemplu, daca get_token() a
fost
declarat sa returneze o valoare de tip token_value, dar este definit sa
returneze un int, atunci compilarea lui lex.c va esua, cu eroare de
neconcordanta de tip.
Fisierul sgn.c va arata astfel:

//sgn.c : analiza sintactica si evolutiva
#include "dc.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }

Fisierul table.c va arata astfel :

//table.c : tabela de simboluri si lookup
#include "dc.h"
extern char* strcmp(const char*, const char*);
extern char* strcpy(char*, const char*);
extern int strlen(const char*);
const TBLSZ = 23;
name table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }

Sa observam ca table.c declara el insusi functiile standard de
manipulare
a sirurilor, asa ca nu exista modificari de consistenta asupra acestor
declaratii. Este aproape totdeauna mai bine sa se includa un fisier
antet
decit sa se declare un nume extern intr-un fisier.c. Aceasta ar putea
implica
sa se includa "prea mult", dar aceasta nu afecteaza serios timpul
necesar
pentru compilare si de obicei va economisi timp pentru programator. Ca
un
exemplu al acestui fapt sa observam cum se redeclara strlen() din nou
in
main.c (de mai jos). Aceasta este o sursa potentiala de erori intrucit
compilatorul nu poate verifica consistenta celor doua declaratii.
Evident,
aceasta problema s-ar putea elimina daca fiecare declaratie externa
s-ar plasa
in dc.h.
Aceasta neglijenta a fost lasata in program din cauza ca este foarte
frecventa
in programele C si conduce la erori care insa nu sint greu de depistat.

In final, fisierul main.c va arata astfel:

//main.c: initializare ciclu principal si tratarea erorilor
#include "dc.h"
int no_of_errors;
double error(char* s) { /* ... */ }
extern int strlen(const char*);
main(int argc, char* argv[]){//...}

Exista un caz important in care dimensiunea fisierelor antet
devine o
pacoste serioasa. Un set de fisiere antet si o biblioteca pot fi
utilizate
pentru a extinde limbajul cu un set de tipuri generale si specifice
aplicatiei (vezi capitolele 5-8). In astfel de cazuri, nu este iesit
din comun
sa gasim mii de linii ale fisierelor antet la inceputul fiecarui fisier

care
se compileaza. Continutul acelor fisiere este de obicei "inghetat" si
se
schimba foarte rar.
O tehnica pentru a incepe compilarea cu continutul acestor fisiere

antet
poate fi de mare utilitate. Intr-un sens, se poate crea un anumit
limbaj cu un
anumit sens special cu ajutorul compilatorului existent. Nu exista
proceduri
standard pentru a crea un astfel de sistem de compilare.

4.3.2 Fisiere antet multiple
----------------------

Stilul unui singur fisier antet pentru un program partitionat este

mult
mai util cind programul este mic si partile lui nu se intentioneaza sa
se
utilizeze separat. Apoi, nu este o situatie serioasa faptul ca nu este
posibil
sa se determine care declaratii se plaseaza in fisierul antet si pentru

ce
motiv. Comentariile pot fi de ajutor. O alternativa este sa lasam ca
fiecare
parte a unui program sa aiba fisierul antet propriu care defineste
facilitatile pe care le furnizeaza el. Fiecare fisier.c are atunci un
fisier.h
corespunzator si fiecare fisier.c include fisierul.h propriu (care
specifica
ce furnizeaza el) si de asemenea pot fi si alte fisiere.h (care
specifica de
ce are el nevoie).
Considerind aceasta organizare pentru calculator, noi observam ca
error()
este utilizata exact ca fiecare functie din program si ea insasi
utilizeaza
numai <stream.h>. Aceasta este tipic pentru functiile error() si
implica
faptul ca error() ar trebui sa fie separata de main():
//error.h: trateaza erorile
extern int no_errors;
extern double error(char* s);

//error.c
#include <stream.h>
#include "error.h"
int no_of_errors;
double error(char* s) { /* ... */ }

In acest stil de utilizare a fisierelor antet, un fisier.h si un
fisierul.c pot fi vazute ca un modul in care fisierul.h specifica o
interfata
si fisierul.c specifica implementarea.
Tabela de simboluri este independenta de restul, exceptind
utilizarea
functiei error(). Aceasta se poate face acum explicit:

//table.h : declaratiile tabelei de simboluri
struct name{
char* string;
name* next;
double value;
};

extern name* look(char* p, int ins = 0);
inline name* insert(char* s){return look(s, 1);}

//table.c : definitiile tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h"
const TBLSZ = 23;
name* table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }

Sa observam ca declaratiile functiilor de manipulare a sirurilor
sint
incluse in <string.h>. Aceasta elimina o alta sursa potentiala de
erori.

//lex.h: declaratii pentru intrare si analiza lexicala
enum token_value{
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
};
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern token_value get_token();

Aceasta interfata cu analizorul lexical este cit se poate de
incurcata.
Lipsa unui tip propriu de lexic arata necesitatea de a prezenta
utilizatorului
pe get_token() cu bufferele de lexicuri reale number_value si
name_string.

//lex.c : definitiile pentru intrare si analiza lexicala
#include <stream.h>
#include <ctype.h>
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }

Interfata cu analizorul sintactic este curata:

//syn.h : declaratii pentru analiza sintactica si evoluare
#include "error.h"
#include "lex.h"
#include "syn.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }


Programul principal este pe cit de uzual pe atit de trivial:
#include <stream.h>
#include <lex.h>
#include <syn.h>
#include <table.h>
#include <string.h>
main(int argc, char* argv[]) { /* ... */ }
Cit de multe fisiere antet sa se utilizeze intr-un program depinde de
multi
factori. Multi dintre acestia au de a face mai mult cu modul de tratare

al
fisierelor pe sistemul dumneavoastra, decit cu C++. De exemplu, daca
editorul
nu are facilitati de a cauta in acelasi timp in mai multe fisiere,
utilizarea
multor fisiere antet devine mai putin atractiva. Analog, daca deschide-

rea si
citirea a 10 fisiere de 50 de linii fiecare este substantial mai
costisitor
decit citirea unui singur fisier de 500 de linii. Noi trebuie sa gidim
de doua
ori inainte de a folosi stilul fisierelor antet multiple pentru un
program
mic. Un sfat: un set de 10 fisiere antet plus fisierele standard antet
este de
obicei ceva normal de gestionat. Totusi, daca partitionati declaratiile

unui
program mare in fisiere antet de dimensiuni logic minime (punind
fiecare
declaratie de structura intr-un fisier propriu, etc.), atunci ve-ti
ajunge
usor la sute de fisiere greu de gestionat.

4.3.3 Ascunderea datelor
------------------
Utilizind fisierele antet, un utilizator poate defini explicit
interfetele pentru a asigura utilizarea consistenta a tipurilor
dintr-un
program. Cu toate acestea, un utilizator poate ocoli interfata
furnizata
printr-un fisier antet inserind declaratiile externe in fisierele.c.
Sa observam ca stilul urmator de legatura nu este recomandat:
//file1.c : "extern" nu se utilizeaza
int a = 7;
const c = 8;
void f(long) { /* ... */ }
//file2.c : "extern" in fisierul.c
extern int a;
extern const c;
extern f(int);
int g(){ return f(a+c); }
Intrucit declaratiile extern din file2.c nu sint incluse cu definitiile

din
file1.c compilatorul nu poate verifica consistenta acestui program. In
consecinta, daca incarcatorul nu este mai destept decit de obicei, cele

doua
erori din acest program va trebui sa le gaseasca programatorul.
Un utilizator poate proteja un fisier impotriva unei astfel de
legaturi
indisciplinate declarind ca static acele nume care nu se intentioneaza
sa se
utilizeze global. Astfel, ele au ca dome- niu fisierul respectiv si
sint
interzise pentru alte parti din program. De exemplu:
//table.c : definitia tabelei de simboluri
#include "error.h"
#include <string.h>
#include "table.h"
const TBLSZ = 23;
static name* table[TBLSZ];
name* look(char* p, int ins) { /* ... */ }
Aceasta va asigura ca toate accesele la table sa se faca prin look().
Nu este
necesar sa se "ascunda" constanta TBLSZ.


4.4 Fisiere si Module
-----------------

In sectiunea precedenta fisierele.c si .h definesc impreuna o
parte a
programului. Fisierul.h este interfata utilizata de alte parti ale
programului; fisierul.c specifica implementarea.
O astfel de entitate este numita, adesea, modul. Numai numele de
care are
nevoie sa le cunoasca utilizatorul se fac disponibile iar restul sint
ascunse.
Aceasta proprietate se numeste adesea ascunderea datelor, chiar daca
data este
numai unul din lucrurile ce se pot ascunde. Acest tip de modul
furnizeaza o
flexibilitate mare. De exemplu, o implementare poate consta din unul
sau mai
multe fisiere.c si diferite interfete ce pot fi furnizate sub forma de
fisiere.h. Informatia pe care un utilizator nu este necesar sa o
cunoasca este
ascunsa in fisierul.c. Daca se considera ca utilizatorul nu trebuie sa
stie
exact ce contine fisierul.c, atunci el nu trebuie sa fie disponibil in
sursa.
Fisierele de tip .obj sint suficiente.
Este uneori o problema ca aceasta flexibilitate sa fie atinsa fara

o
structura formala. Limbajul insusi nu recunoaste un astfel de modul ca
o
entitate si nu exista nici o cale ca, compilatorul sa faca distinctie
intre
fisierele.h care definesc nume ce sa fie utilizate de alte module
(exportate)
de fisierele.h folosite pentru a declara nume din alte module
(importate).
Alta data, poate fi o problema ca un modul sa defineasca un set de
obiecte si nu un nou tip. De exemplu, modulul table defineste o tabela;

daca
noi dorim doua tabele, nu exista un mod trivial de a furniza celalalt
tabel
utilizind aceasta idee de module. Capitolul 5 prezinta o solutie a
acestei
probleme.
Fiecare obiect alocat static este implicit initializat cu zero,
iar alte
valori (constante) pot fi specificate de programator. Aceasta este doar

o
forma primitiva de initializare. Din fericire, utilizind clasele, se
poate
specifica un cod care sa fie executat pentru initializare inainte de a
face
orice utilizare a modulului si de asemenea se poate executa cod pentru
anulare
(curatire) dupa ultima utilizare a modulului. (vezi &5.5.2).

4.5 Cum se construieste o biblioteca
--------------------------------

Fraze de genul "pune in biblioteca" si "gaseste intr-o anumita
biblioteca" se utilizeaza des (in aceasta carte si in alta parte), dar
ce
inseamna acest lucru pentru un program C++ ?
Din nefericire, raspunsul depinde de sistemul de operare utilizat.
Aceasta sectiune explica cum se face si se utilizeaza o biblioteca in
versiunea 8 a sistemului UNIX. Alte sisteme furni zeaza facilitati
similare.
O biblioteca, in principiu, este o multime de fisiere.o obtinute
prin
compilarea unui set de fisiere.c. De obicei exista unul sau mai multe
fisiere.h care contin declaratii necesare pentru a utiliza acele
fisiere.o. Ca
un exemplu, sa consideram ca avem de furnizat (in mod convenabil) un
set de
functii matematice pentru o multime nespecificata de utilizatori.
Fisierul
antet ar putea arata astfel:
extern double sqrt(double); //subset al lui <math.h>
extern double cos(double);
extern double exp(double);
extern double log(double);
iar definitiile acestor functii vor fi memorate in fisierele sqrt.c,
sin.c,
cos.c, exp.c si respectiv log.c.
O biblioteca numita math.a poate fi facuta astfel:

$cc -c math.c sin.c cos.c exp.c log.c
$ar cr math.a sqrt.o sin.o cos.o exp.o log.o
$ranlib math.a

Fisierele sursa se compileaza intii obtinindu-se fisiere obiect
echivalente. Se utilizeaza apoi comanda ar pentru a face o arhiva
numita
math.a. In final arhiva respectiva este indexata pentru un acces mai
rapid.
Daca sistemul dumneavoastra nu are comanda ranlib, atunci probabil ca
nu aveti
nevoie de ea; sa va uitati in manualul de operare pentru detalii.
Biblioteca
poate fi utilizata astfel:

$cc myprog.c math.a

Acum, care este avantajul utilizarii lui math.a in loc de a
utiliza
direct fisierele.o? De exemplu:
$ myprog.c sqrt.o sin.o cos.o exp.o log.o

Pentru majoritatea programelor, gasirea setului corect de
fisiere.o nu
este un lucru trivial. In exemplul de mai sus, ele au fost toate
incluse, dar
daca functiile din myprog.c apeleaza numai functiile sqrt() si cos()
atunci
pare ca ar fi suficient:
$cc myprog.c sqrt.o cos.o

Acest lucru nu este tocmai asa deoarece cos.c utilizeaza sin.c.
Linkerul apelat de comanda cc ca sa foloseasca un fisier.a (in
acest caz
math.a) stie sa extraga numai fisierele.o necesare, din multimea care a

fost
utilizata pentru a crea fisierul.a.
Cu alte cuvinte, folosind o biblioteca, se pot include multe
definitii
folosind un singur nume (inclusiv definitii de functii si variabile
utilizate
de functii interne pe care utilizatorul nu le-a vazut niciodata) si in
acelasi
timp se asigura numai un numar minim de definitii include.

4.6 Functii
-------
Modul tipic de a face ceva intr-un program C++ este de a apela o
functie
care sa faca lucrul respectiv. Definirea unei functii este o cale de a
specifica cum sa se faca o operatie. O functie nu poate fi apelata daca

ea nu
este declarata.

4.6.1 Declaratii de functii
---------------------
O declaratie de functie da un nume functiei, tipul valorii
returnate
(daca returneaza vreuna) de functie, numarul si tipurile argumentelor
care
trebuie furnizate in apelul unei functii. De exemplu:
extern double sqrt(double);
extern elem* next_elem();
extern char* strcpy(char* to, const char* from);
extern void exit(int);
Semantica transferului de argumente este identica cu semantica
initializarii. Tipurile argumentelor se verifica si se fac conversii
implicite
ale tipurilor argumentelor cind este necesar. De exemplu, dindu-se
declaratiile precedente:
doublesr2 = sqrt(2); va apela corect functia sqrt() cu valoarea
2.0.
O declaratie de functie poate contine nume de argumente. Acest
lucru
poate fi un ajutor pentru cititor, dar compilatorul ignora pur si
simplu
astfel de nume.

4.6.2 Definitii de functii
--------------------

Fiecare functie care este apelata intr-un program trebuie sa fie
definita
undeva (o singura data). O definitie de functie este o declaratie de
functie
in care este prezent corpul functiei. De exemplu:
extern void swap(int*, int*); //o declaratie
void swap(int* p, int* q) //o definitie
{
int t = *p;
*p = *q;
*q = t;
}

O functie poate fi declarata inline pentru a elimina apelul
functiei
suprapunind-o peste el (&1.12), iar argumentele pot fi declarate
register
pentru a furniza un acces mai rapid la ele (&2.3.11). Ambele
caracteristici
pot fi eliminate si ele ar trebui sa fie eliminate ori de cite ori
exista
dubii in legatura cu utilitatea folosirii lor.

4.6.3 Transferul argumentelor
-----------------------

Cind se apeleaza o functie se rezerva memorie pentru argumentele
formale
si fiecare argument formal se initializeaza prin argumentele efective
corespunzatoare. Semantica transferului de parametri este identica cu
semantica initializarii. In parti- cular se verifica tipul unui
argument
efectiv cu tipul argumentului formal corespunzator si se fac toate
conversiile
de tip standard si definite de utilizator. Exista reguli speciale
pentru
transferul vectorilor (&4.6.5), o facilitate pentru transferul
neverificat al
argumentelor (&4.6.8) si o facilitate pentru specificarea argumentelor
implicite (&4.6.6). Consideram:
void f(int val, int& ref)
{
val++;
ref++;
}
Cind se apeleaza f(), val++ mareste o copie locala a primului sau
argument, in
timp ce ref++ incrementeaza cel de al doilea argument efectiv. De
exemplu:
int i = 1;
int j = 1;
f(i, j);
va incrementa pe j dar nu si pe i. Primul argument i este pasat prin
valoare,
iar cel de al doilea prin referinta. Asa cum s-a mentionat in &2.3.10,
folosind functii care modifica argumentele apelate prin referinta se
pot face
programe greu de citit si in general ar trebui eliminate (dar vezi &6.5

si
&8.4). Totusi, este mult mai eficient ca un obiect mare sa fie
transferat prin
refe- rinta in loc sa fie transferat prin valoare. In acest caz,
argumentul ar
putea fi declarat const pentru a indica faptul ca referinta se
utilizeaza
numai din motive de eficienta iar functia apelata nu poate schimba
valoarea
obiectului:
void f(const large& arg)
{ //valoarea lui arg nu poate fi schimbata }

Analog, declarind un argument pointer const, cititorul este
avertizat ca
valoarea obiectului spre care pointeaza acel argument nu se schimba
prin
functia respectiva. De exemplu :
extern int strlen(const char*); //din <string.h>
extern char* strcpy(char* to, const char* from);
extern int strcmp(const char*, const char*);

Importanta acestei practici creste cu dimensiunea programului. Sa
observam ca semantica transferului de argumente este diferita de
semantica
asignarii. Acest lucru este important pentru argumentele const, pentru
argumentele referinta si pentru argumentele unor tipuri definite de
utilizator
(&6.6).

4.6.4 Valoarea returnata
------------------

O valoare poate fi (si trebuie) returnata dintr-o functie care nu
este
declarata void. Valoarea returnata se specifica printr-o instructiune
return.
De exemplu:
int fact(int n)
{
return (n>1) ? n*fact(n-1) : 1;
}

Pot fi mai multe instructiuni return intr-o functie:
int fact(int n)
{
if(n > 1)
return n*fact(n-1);
else
return 1;
}

Ca si semantica transferului de argumente, semantica valorii
returnate de
o functie este identica cu semantica initializarii. O instructiune
return se
considera ca initializeaza o variabila de tipul returnat. Tipul
expresiei
returnate se verifica cu tipul valorii returnate de functie si la
nevoie se
fac toate conversiile de tip standard sau definite de utilizator. De
exemplu:
double f()
{ // ...
return 1; //se converteste spre double(1)
}

De fiecare data cind se apeleaza o functie se creaza o copie noua
pentru
argumentele si variabilele automatice ale ei. Memoria este eliberata la
revenirea din functie, asa ca nu este indicat sa se returneze un
pointer spre
o variabila locala. Continutul locatiei spre care se face pointarea se
va
schimba imprevizibil:
int* f()
{
int local = 1;
// ...
return &local; //nu se face asa ceva
}

Din fericire, compilatorul avertizeaza asupra unor astfel de
valori
returnate. Iata un alt exemplu:
int& f()
{
return 1; //nu se face asa ceva
}

4.6.5 Argumente vector
----------------

Daca se utilizeaza un vector ca un argument de functie, se
transfera un
pointer spre primul sau element. De exemplu:
int strlen(const char*);
void f()
{
char v[] = "a vector";
strlen(v);
strlen("Nicholas");
}

Cu alte cuvinte, un argument de tip T[] va fi convertit spre T*
cind este
transferat. Rezulta ca o asignare la un element al argumentului vector
schimba
valoarea elementului argumentului respectiv. Cu alte cuvinte, vectorii
difera
de alte tipuri prin aceea ca vectorul nu este pasat prin valoare (si
nici nu
poate fi pasat prin valoare). Dimensiunea unui vector nu este
disponibila in
functia apelata. Aceasta poate fi o pacoste, dar exista dife- rite
moduri de
tratare a acestei probleme. Sirurile se termina prin zero, asa ca
dimensiunea
lor se poate calcula usor. Pentru alte tipuri de vectori se poate
transfera un
al doilea argument care contine dimensiunea sau un tip care contine un
pointer
si un indicator de lungime in locul vectorului (&11.11). De exemplu:

void compute1(int* vec_ptr, int vec_size); //un mod
struct vec{ //un alt mod
int* ptr;
int size;
};
void compute2(vec v);

Tablourile multidimensionale sint mai ciudate, dar adesea pot fi
utilizati vectori de pointeri in locul lor si nu au nevoie de o tratare
speciala. De exemplu:
char* day[] = {"mon","tue","wed","thu","fri","sat","sun"};
Cu toate acestea consideram definirea unei functii care
manipuleaza o
matrice bidimensionala. Daca dimensiunile sint cunoscute la compilare,
nu
exista nici o problema:

void print_m34(int m[3][4])
{
for(int i=0; i<3; i++)
{
for(int j=0; j<4; j++)
cout << " " << m[i][j];
cout << "\n";
}
}

Cazul dificil apare cind trebuie pasate ambele dimensiuni.
"Solutia
evidenta" pur si simplu nu functioneaza:
void print_mij(int m[][], int dim1, int dim2) //eroare
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << m[i][j]; //surpriza
cout << "\n";
}
}

In primul rind, argumentul m[][] este ilegal deoarece trebuie sa
fie
cunoscuta dimensiunea a doua a tabloului pentru a gasi locatia unui
element.
In al doilea rind, expresia m[i][j] este corect interpretata ca
*(*(m+i)+j),
dar aceasta este impro- babil ca este ce a dorit programatorul. O
solutie
corecta este:
void print_mij(int** m, int dim1, int dim2)
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << ((int*)m)[i*dim2+j]; //obscur
cout << "\n";
}
}

Expresia utilizata pentru a face acces la elementele tabloului
este
echivalenta cu cea generata de compilator cind cunoaste ultima
dimensiune. Se
poate introduce o variabila auxiliara pentru a face codul mai putin
obscur:
int* v = (int*)m;
v[i*dim2+j];


4.6.6 Argumente implicite
-------------------

O functie necesita adesea mai multe argumente in general, decit
este
nevoie in cazul cel mai simplu sau in cazul cel mai frecvent. De
exemplu,
biblioteca stream are o functie hex() care produce un sir ce contine
reprezentarea hexazecimala a unui intreg. Un al doilea intreg se
foloseste
pentru a specifica numarul de caractere disponibile pentru
reprezentarea
primului argument. Daca numarul de caractere este prea mic pentru a
reprezenta
intregul, apare trunchierea; daca este prea mare, sirul este completat
cu
spatii. Adesea, programatorul nu se intereseaza despre numarul de
caractere
necesare pentru a reprezenta intregul atita timp cit exista spatiu
suficient,
asa ca argumentul al doilea este 0 pentru a indica faptul ca la
conversie sa
se utilizeze "exact atitea caractere cite sint necesare". Pentru a
elimina
apelurile de forma hex(i, 0), functia se declara astfel:

extern char* hex(long, int = 0);

Initializarea pentru cel de al doilea parametru inseamna ca acesta

este
un parametru implicit. Adica, daca numai un argument este prezent
intr-un
apel, cel de al doilea este utilizat impli- cit. De exemplu:

cout << "**" << hex(31) << hex(32, 3) << "**";

se interpreteaza astfel:
cout << "**" << hex(31, 0) << hex(32, 3) << "**";

si va imprima:
**1f 20**

Un argument implicit se verifica din punct de vedere al tipului in
momentul declararii functiei si este evaluat in momentul apelului. Este
posibil sa se furnizeze argumente implicite numai pentru argumente din
ultimele pozitii, asa ca:
int f(int, int = 0, char* = 0); //ok
int g(int = 0, int = 0, char*); //error
int h(int = 0, int, char* = 0); //error

Sa observam ca in acest caz spatiul dintre * si = este
semnificativ (*=
este operatorul de asignare):
int nasty(char *= 0); //syntax error


4.6.7 Nume de functii supraincarcate
------------------------------

Adesea este o idee buna de a da la diferite functii nume diferite,

dar
cind niste functii fac acelasi lucru asupra obiectelor de tipuri
diferite,
poate fi mai convenabil sa le dam acelasi nume. Utilizarea aceluiasi
nume
pentru operatii diferite pentru tipuri diferite se numeste
supraincarcare.
Tehnica este deja utilizata pentru operatii de baza in C++; exista un
singur
nume pentru adunare (+), dar el poate fi utilizat pentru a aduna valori

de
tipuri intregi, in flotant si pointeri. Aceasta idee se extinde simplu
pentru
a trata operatii definite de programator, adica functii. Pentru a
proteja
programatorul de reutilizarea accidentala a unui nume, un nume poate fi
utilizat pentru mai multe functii numai daca este declarat la inceput
ca fiind
supraincarcat. De exemplu:
overload print;
void print(int);
void print(char*);
La compilare singurul lucru pe care functiile il au in comun este
numele.
Probabil ca intr-un anumit sens functiile sint similare, dar limbajul

nu are
restrictii asupra lor. Astfel numele supraincarcat al functiilor sint
in
primul rind o conventie de notatie. Aceasta conventie este
semnificativa
pentru functii cu nume conventionale, cum ar fi sqrt, print si open.
Cind un
nume este semantic semnificativ, cum ar fi operatorii +, * si << (&6.2)

si in
cazul constructorilor (&5.2.4 si &6.3.1), aceasta facilitate devine
esentiala.
Cind este apelata o functie f() supraincarcata, compilatorul trebuie sa

stie
care functie este apelata dintre cele cu numele f. Aceasta se face prin

compa-
rarea tipurilor argumentelor efective cu tipurile argumentelor formale
a
tuturor functiilor numite f. Gasirea functiei care sa fie apelata se
face in
trei pasi separati:

[1] Cauta o corespondenta exacta si daca exista se utilizea-
za functia respectiva;
[2] Cauta o corespondenta utilizind conversii predefinite si
utilizeaze informatie nu se aplica, raminind int spre long,
int spre double,
zero spre long, zero spre double si conversia de pointeri; zero spre
pointer,
pointer spre void* si pointer spre clasa derivata pentru a pointa spre
baza
clasei (&7.2.4). Iata un exemplu in care este necesara conversia:
overload print(double), print(long);
void f(int a){print(a);}

Aici a poate fi imprimat sau ca double sau ca long. Ambiguitatea poate
fi
rezolvata utilizind tipul de conversie explicita (sau print(long(a))
sau
print(double(a))).
Dindu-se aceste reguli, se poate asigura ca cel mai simplu
algoritm
(functie) va fi utilizat, cind eficienta sau precizia calcului difera
semnificativ pentru tipurile implicite. De exemplu:
overload pow;
int pow(int, int);
double pow(double, double); //din <math.h>
complex pow(double, complex); //din <complex.h>
complex pow(complex, int);
complex pow(complex, double);
complex pow(complex, complex);

Procesul de gasire a corespondentei ignora unsigned si const.

4.6.8 Numar nespecificat de argumente
-------------------------------

Pentru anumite functii nu este posibil sa se specifice numarul si
tipul
tuturor argumentelor asteptate intr-un apel. O astfel de functie se
declara
terminind lista argumentelor din declaratie prin trei puncte (...) care
inseamna ca " pot fi mai multe argumente". De exemplu:

int printf(char* ...);

Aceasta specifica faptul ca un apel a lui printf trebuie sa aiba
cel
putin un argument de tip char* si poate sa aiba sau nu si altele. De
exemplu:

printf("Hello, word\n");
printf("My name is %s %s\n", first_name, second_name);
printf("%d + %d = %d\n", 2, 3, 5);

O astfel de functie trebuie sa se refere la o informatie care nu
este
disponibila compilatorului cind se interpreteaza lista de argumente. In

cazul
functiei printf(), primul argument este un sir de format care contine o
succesiune de caractere speciale care permite ca printf() sa trateze
corect
celelalte argumente: %s inseamna "se asteapta un argument de tip char*"

iar %d
inseamna "asteapta un argument int". Cu toate acestea, compilatorul nu
stie
aceasta, asa ca el nu se poate asigura ca argumentele asteptate sa
existe in
realitate sau ca un argument este un tip propriu. De exemplu:

printf("My name is %s %s\n", 2);

se va compila si in cel mai bun caz se va scrie la executie ceva
straniu.
Evident daca un argument nu a fost declarat, compilatorul nu are
informatia necesara pentru a face verificarea standard de tip si de a
face
eventual o conversie de tip. In acest caz, char sau short se transfera
ca int,
iar float ca double. Aceasta nu este in mod necesar ceea ce a vrut
utilizatorul.
Utilizarea la extrema a celor trei puncte conduce la
imposibilitatea de a
verifica argumentele, lasind programatorului deschisa problema aceasta.

Un
program bine proiectat necesita cel putin citeva functii pentru care
tipurile
argumentelor nu sint specificate complet. Functiile supraincarcate si
functiile care utilizeaza argumente implicite pot fi utilizate avind
grija ca
verificarea tipului sa se faca ori de cite ori se utilizeaza argumente
de tip
nespecificat. Numai cind atit numarul de argu mente cit si tipul
argumentelor
variaza este necesar sa se foloseasca trei puncte. Cea mai frecventa
utilizare
a celor trei puncte este de a specifica o interfata cu functiile de
biblioteca
ale lui C care sint definite fara a fi disponibile alternativele
posibile:

extern int fprintf(FILE*, char* ...); din <stdin.h>
extern int execl(char* ...); din <system.h>
extern int abort(...); din <libc.h>

Un set de macrouri standard disponibile pentru a avea acces la
argumente
nespecificate in astfel de functii pot fi gasite in <stdargs.h>. Sa
consideram
scrierea unei functii eroare care are un argument intreg ce indica
nivelul de
eroare, urmat de un numar arbitrar de siruri. Ideea este de a compune
mesajul
de eroare pasind fiecare cuvint ca un argument de tip sir separat:

void error(int ...);
main(int argc, char* argv[])
{switch(argc)
{case 1: error(0, argv[0], 0);
break;
case 2: error(0, argv[0], argv[1], 0);
break;
default: error(1, argv[0], "with", dec(argc-1),
"arguments", 0);
}
}

Functia eroare ar putea fi definita astfel:
#include <stdargs.h>
void error(int n ...)
// "n" urmat de o lista de char* s terminata prin zero
{
va_list ap;
va_start(ap, n); //arg startup
for(;;)
{
char* p = va_arg(ap, char*);
if(p == 0)
break;
cerr << p << " ";
}
va_end(ap); //curatirea argumentelor
cerr << "\n";
if(n)
exit(n);
}
Intii se defineste va_list care este initializata prin apelul lui
va_start().
Macroul va_start ia numele lui va_list si numele ultimului argument
formal ca
argumente. Macroul va_arg() se utilizeaza pentru a alege argumentul
nedenumit in ordine. La fiecare apel programatorul trebuie sa furnizeze

un
tip; va_arg() presupune ca argumentul efectiv de acel tip a fost pasat,

dar de
obicei nu exista o cale de a asigura aceasta. Inainte de a reveni
dintr-o
functie in care s-a utilizat va_start(), trebuie apelata va_end().
Motivul
este ca va_start() poate modifica stiva in asa fel ca revenirea nu se
va mai
realiza cu succes: va_end() reface stiva la forma necesara revenirii
corecte.

4.6.9 Pointer spre functie
--------------------

Exista numai doua lucruri care pot fi facute cu o functie: apelul
ei si
sa se ia adresa ei. Pointerul obtinut functiei poate fi apoi utilizat
pentru a
apela functia. De exemplu:
void error(char* p){/*...*/}
void (*efct)(char*); //pointer spre functie
void f()
{efct = &error; //efct pointeaza spre error
(*efct)("error"); //apelul lui error prin efct
}

Pentru a apela o functie printr-un pointer (de exemplu efct) intii
trebuie sa i se atribuie pointerului adresa functiei res- pective.
Intrucit
operatorul () de apel de functie are prioritate mai mare decit
operatorul *,
nu se poate scrie apelul prin *efct("error") caci aceasta inseamna
*(efct("error")), ceea ce este o eroare de tip. Acelasi lucru se aplica

la
sintaxa declaratiei (vezi de asemenea &7.3.4).
Sa observam ca pointerii spre functii au tipurile argumentelor
declarate
ca si functiile insasi. In asignarea de pointeri, tipul functiei
trebuie sa
corespunda exact. De exemplu:

void (*pf)(char*); //pointer spre void(char*);
void f1(char*); //void(char*);
int f2(char*); //int(char*);
void f3(int*); //void(int*);

void f()
{
pf = &f1; //ok
pf = &f2; //eroare: tipul valorii returnate
// este eronat
pf = &f3; //eroare: argument de tip eronat
(*pf)("asdf"); //ok
(*pf)(1); //eroare: tip de argument eronat
int i = (*pf)("qwer"); //eroare: void se asigneaza la int
}

Regulile pentru pasarea argumentelor sint aceleasi atit pentru
apelurile
directe la o functie cit si pentru apelurile la o functie printr-un
parametru.

Adesea este convenabil sa se defineasca un nume pentru tipul unui
pointer
spre o functie pentru a elimina utilizarea tot timpul a unei sintaxe
neevidente. De exemplu:

typedef int (*SIG_TYP)(); //din <signal.h>
typedef void (*SIG_ARG_TYP)();
SIG_TYP signal(int, SIG_ARG_TYP);

Adesea este util un vector de pointeri spre functii. De exemplu,
sistemul
de meniuri pentru editorul bazat pe "mouse" se implementeaza utilizind
vectori
de pointeri spre functii ce reprezinta operatii. Sistemul nu poate fi
descris
aici in detaliu dar ideea generala este aceasta:
typedef void (*PF)();
PF edit_ops[]={cut, paste, snarf, search}; //op. de editare
PF file_ops[]={open, reshape, close, write};//tratarea fis.

Definirea si initializarea pointerilor care definesc acti- unile
selectate dintr-un meniu asociat cu butoanele mouse-ului:
PF* button2 = edit_ops;
PF* button3 = file_ops;

Intr-o implementare completa, este necesara mai multa informatie
pentru a
defini fiecare element. De exemplu, un sir care specifica textul de
afisat
trebuie sa fie pastrat undeva. Pe masura ce se utilizeaza sistemul,
sensul
butoanelor mouse se schimba frecvent cu contextul. Astfel de schimbari
se
realizeaza (partial) schimbind valoarea pointerilor de butoane. Cind un
utilizator selecteaza un meniu, cum ar fi elementul 3 pentru butonul 2,

se
executa operatia asociata: (*button2[3])();
Un mod de a cistiga o apreciere a puterii expresive a poin-
terilor spre
functii este incercarea de a scrie cod fara ele. Un meniu poate fi
modificat
la executie inserind functii noi intr-o tabela operator. Este de
asemenea usor
sa se construiasca meniuri noi la executie.
Pointerii spre functii pot fi utilizati sa furnizeze rutine care
pot fi
aplicate la obiecte de tipuri diferite:
typedef int (*CFT)(char*, char*);
int sort(char* base, unsigned n, int sz, CFT cmp)
/* Sorteaza cele n elemente ale vectorului "base" in ordine
crescatoare utilizind functia de comparare spre care
pointeaza "cmp". Elementele sint de dimensiune "sz".
Algoritm foarte ineficient: bubble sort.
*/
{
for(int i = 0; i < n-1; i++)
for(int j = n-1; i < j; j--)
{
char* pj = base+j*sz; //b[j]
char* pj1 = pj-sz; //b[j-1]
if((*cmp)(pj, pj1) < 0) //swap b[j] and b[j-1]
for(int k = 0; k < sz; k++)
{
char temp = pj[k];
pj[k] = pj1[k];
pj1[k] = temp;
}
}
}
Rutina de sortare nu cunoaste tipul obiectelor pe care le
sorteaza, ci
numai numarul de elemente (dimensiunea vectorului), dimensiunea
fiecarui
element si functia de apelat pentru a face compararea. Tipul lui sort()

ales
este acelasi cu tipul rutinei qsort() din biblioteca C standard.
Programele
reale utilizeaza qsort(). Intrucit sort() nu returneaza o valoare, ar
trebui
declarata cu void, dar tipul void nu a fost introdus in C cind a fost
definit
qsort(). Analog, ar fi mai onest sa se foloseasca void* in loc de char*

ca tip
de argument. O astfel de functie sort() ar putea fi utilizata pentru a
sorta o
tabela de forma:
struct user{char* name;
char* id;
int dept;
};
typedef user* Puser;
user heads[]={"McIlroy M.D.", "doug", 11271,
"Aho A.V.", "ava", 11272,
"Weinberger P.J.", "pjw", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275,
"Kernighan B.W.", "bwk", 11276
};

void print_id(Puser v, int n)
{for(int i = 0; i < n; i++)
cout << v[i].name << "\t" << v[i].id << "\t"
<< v[i].dept << "\n";
}
Pentru a putea face sortarea, intii trebuie sa definim functia de
comparare potrivita. O functie de comparare trebuie sa returneze o
valoare
negativa daca primul ei argument este mai mic decit al doilea, zero
daca sint
egale si un numar pozitiv altfel:
int cmp1(char* p, char* q) //se compara sirurile nume
{
return strcmp(Puser(p)->name, Puser(q)->name);
}

int cmp2(char* p, char* q) //se compara numerele dept
{
return Puser(p)->dept - Puser(q)->dept;
}
Programul acesta sorteaza si imprima:
main()
{
sort((char*)heads, 6, sizeof(user), cmp1);
print_id(heads, 6) //in ordine alfabetica
cout << "\n";
sort((char*)heads, 6, sizeof(user), cmp2);
print_id(heads, 6); //in ordinea numerelor de departament
}

Este posibil sa se ia adresa unei functii inline si de asemenea sa

se ia
adresa unei functii supraincarcate (&r8.9).


4.7 Macrouri
--------

Macrourile se definesc in &r11. Ele sint foarte importante in C,
dar
sint pe departe mai putin utilizate in C++. Prima regula despre ele
este: sa
nu fie utilizate daca nu trebuie. S-a observat ca aproape fiecare macro
demonstreaza o fisura fie in limbajul de programare, fie in program.
Daca
doriti sa folositi macrouri va rog sa cititi foarte atent manualul de
referinta pentru implementarea preprocesorului C pe care il folositi.
Un macro simplu se defineste astfel:

#define name restul liniei

Cind name se intilneste ca o unitate lexicala, el este inlocuit prin
restul
liniei. De exemplu:

named = name

va fi expandat prin:

named = restul liniei

Un macro poate fi definit, de asemenea, prin argumente. De
exemplu:

#define mac(a, b) argunent1: a argument2: b

Cind se utilizeaza mac, cele doua siruri de argumente trebuie sa fie
prezente.
Ele vor inlocui pe a si b cind se expandeaza mac(). De exemplu:

expanded = mac(foo bar, yuc yuk)

va fi expandat in:

expanded = argument1: foo bar argument2: yuk yuk

Macrourile manipuleaza siruri si stiu putin despre sintaxa lui C++

si
nimic despre tipurile si regulile de existenta ale lui C++.
Compilatorul
vede numai formele expandate ale unui macro, asa ca o eroare intr-un
macro va
fi propagata cind macroul se expandeaza. Aceasta conduce la mesaje de
eroare
obscure, ele nefiind descoperite in definitia macroului. Iata citeva
macrouri
plauzibile:

#define case break;case
#define nl <<"\n"
#define forever for(;;)
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

Iata citeva macrouri complete necesare:

#define PI 3.141593
#define BEGIN {
#define END }

Iata citeva macrouri periculoase:

#define SQUARE(a) a*a
#define INCR_xx (xx)++
#define DISP = 4

Pentru a vedea de ce sint periculoase, sa incercam sa expandam:

int xx = 0; //numarator global
void f()
{
int xx = 0; //variabila locala
xx = SQUARE(xx+2); //xx=xx+2*xx+2
INCR_xx; //incrementeaza localul xx
if(a-DISP == b) //a-= 4==b
{
//....
}
}

Daca noi dorim sa utilizam un macro trebuie sa utilizam operatorul

de
rezolutie a domeniului "::" cind dorim sa facem referinte la nume
globale
(&2.1.1) si sa includem in paranteze aparitiile numelor argumente ale
macrourilor (vezi MIN de mai sus).
Sa se observe diferenta efectelor de expandare a acestor doua
macrouri:

#define m1(a) something(a) // comentariu serios
#define m2(a) something(a) /* comentariu serios */


De exemplu:

int a = m1(1) + 2;
int b = m2(1) + 2;

se vor expanda in

int a = something(1) // comentariu serios + 2 ;
int b = something(1) /* comentariu serios */ + 2;

Utilizind macrouri, noi putem proiecta limbajul nostru propriu; el

va fi
probabil mult mai incomprehensibil decit altele. Mai mult decit atit,
preprocesorul C este un macroprocesor foarte simplu. Cind noi incercam
sa
facem ceva netrivial, noi probabil gasim sau ca este imposibil sau ceva
nejustificat de greu de realizat (dar vezi &7.3.5).

4.8 Exercitii
---------

1. (*1). Sa se scrie declaratii pentru: o functie care are ca
argumente
un pointer spre caractere si referinta la un intreg si nu returneaza
nici o
valoare; un pointer spre o astfel de functie; o functie care are un
astfel de
pointer ca argument; o functie care returneaza un astfel de pointer. Sa

se
scrie definitia unei functii care are un astfel de pointer ca argument
si
returneaza argumentul ei ca valoare. Sa se utilizeze typedef.

2. (*1). Ce semnifica linia de mai jos? La ce ar fi buna ea?
typedef int(rifii&)(int, int);

3. (*1.5). Sa se scrie un program ca "Hello, world" care ia
un nume din linia de comanda si care scrie "Hello, numele respectiv".
Sa se
modifice acest program pentru a lua oricite nume ca argumente si sa se
scrie
Hello la fiecare.

4. (*1.5). Sa se scrie un program care citeste un numar
arbitrar de fisiere a caror nume se dau ca argumente in linia de
comanda si le
scrie unul dupa altul in cout. Acest program se poate numi cat deoarece
concateneaza fisierele respective.

5. (*2). Sa se converteasca un program mic C intr-un program
C++. Sa se modifice fisierele antet pentru a declara toate fun- ctiile
apelate
si sa declare tipul fiecarui argument. Sa se inlo- cuiasca #define prin

enum,
const sau inline unde este posibil. Sa se elimine declaratiile extern
din
fisierele C si sa se converteasca in sintaxa definitiilor de functii
din C++.
Sa se inlocuiasca apelurile lui malloc() si free() cu new si delete. Sa

se
elimine conversiile de tip explicit necesare.

6. (*2). Sa se implementeze sort() (&4.6.9) utilizind un
algoritm de sortare mai eficient.

7. (*2). Sa consideram definitia lui struct tnode din &r8.5.
Sa se scrie functia pentru introducerea unui cuvint nou intr-un arbore
de
tnode noduri. Sa se scrie o functie care listeaza arborele de tnode
noduri. Sa
se scrie o functie care listeaza arborele respectiv in ordine
alfabetica a
cuvintelor pe care le contine. Sa se modifice tnode astfel incit sa
contina
numai un pointer spre un cuvint de lungime arbitrara memorat in memoria

libera
folosind new. Sa se modifice functiile pentru a putea utiliza noua
definitie a
lui tnode.

8. (*2). Sa se scrie un modul care implementeaza o stiva.
Fisierul.h
trebuie sa declare functiile push(), pop() si orice alte functii
potrivite. Un
fisier.c defineste functiile si datele necesare de a fi pastrate pe
stiva.

9. (*2). Sa cunoasteti fisierele antet standard. Sa se lis- teze
fisierele din /usr/include si /usr/include/cc (sau orice alte fisiere

antet
standard pastrate de sistemul d-voastra). Cititi tot ce pare a fi
interesant.
10. (*2). Sa se scrie o functie ce inverseaza un tablou
bidimensional.

11. (*2). Sa se scrie un program care citeste din cin si
scrie caracterele in cout codificat. Codul lui c poate fi c^key[i],
unde key
este un sir pasat ca argument al liniei de comanda. Programul
utilizeaza
caracterele din key intr-o maniera ciclica pina cind au fost citite
toate
caracterele de la intrare. Recodificarea textului cu aceeasi cheie
produce
textul original. Daca nu exista nici o cheie (s-a pasat sirul vid),
atunci nu
se face nici o codificare.

12. (*3). Sa se scrie un program care ajuta la descifrarea
mesajelor
codificate cu metoda descrisa mai sus fara a cunoaste cheia. A se
consulta
David Kahn: The code-breakers, Macmillan, 1967, New York, pp 207-213.

13. (*3). Sa se scrie o functie error care are un format
asemanator cu
printf, continind %s, %c si %d si un numar arbitrar de argumente. Sa nu

se
foloseasca printf(). A se consulta &8.2.4 daca nu se cunoaste sensul
lui %s
etc. Sa se utilizeze <stdargs.h>.

14. (*1). Cum am alege nume pentru tipuri de pointeri spre
functii definite prin typedef?

15. (*2). Analizati niste programe pentru a avea o idee
despre
diversitatea stilurilor numelor utilizate in realitate. Cum se
utilizeaza
literele mari? Cum se utilizeaza sublinierea? Cind se utilizeaza nume
scurte
ca x si i?

16. (*1). Ce este gresit in macrodefinitiile de mai jos?

#define PI = 3.141593;
#define MAX(a, b) a > B ? a : b
#define fac(a) (a) * fac((a) - 1)

17. (*3). Sa se scrie un macroprocesor care defineste si
expandeaza
macrouri simple (asa cum face macroprocesorul C). Sa citeasca din cin
si sa
scrie in cout. La inceput sa nu se incerce sa se trateze macrouri cu
argumente. Calculatorul de birou (&3.1) contine o tabela de simboluri
si un
analizor lexical pe care noi l-am putea modifica.

CAPITOLUL 5
===========


CLASE
=====

Acest capitol descrie facilitatile pentru a defini tipuri noi
pentru care
accesul la date este restrins la un set specific de functii de acces.
Sint
explicitate modurile in care o data structurata poate fi protejata,
initializata, accesata si in final eliminata. Exemplele includ clase
simple
utilizate pentru gestiunea tabelei de simboluri, manipularea stivei,
manipularea multimilor si implementarea unei reuniuni "incapsulate".

5.1 Introducere si privire generala
-------------------------------

Scopul conceptului de clasa C++ este de a furniza programatorului
un
instrument pentru a crea tipuri noi care pot fi folo- site tot atit de
convenabil ca si tipurile predefinite. Ideal, un tip definit de
utilizator ar
trebui sa nu difere de tipurile predefinite in modul in care sint
utilizate,
ci numai in modul in care sint create.
Un tip este reprezentarea concreta a unei idei (concept). De
exemplu,
tipul float din C++ cu operatiile +, -, *, etc., furnizeaza o versiune
restrinsa dar concreta a conceptului matematic de numar real. Motivul
de a
desemna un tip nou este de a furniza o definitie concreta si specifica
a
conceptului care nu are un corespondent direct si evident intre
tipurile
predefinite. De exemplu, cineva poate furniza tipul "trunk_module"
intr-un
program ce se ocupa cu telefoanele sau tipul "list_of_paragraphs"
pentru un
program de procesare de text.
Un program care furnizeaza tipuri care sint strins legate de
conceptele
aplicatiei este de obicei mai usor de inteles si mai usor de modificat
decit
un program care nu face asa ceva. Un set de tipuri definite de
utilizator bine
ales face un program mai concis; el de asemenea permite compilatorului
sa
detecteze utilizari ilegale ale obiectelor care altfel nu ar fi
detectate pina
in momentul in care nu se testeaza efectiv programul.
Ideea fundamentala in definirea unui tip nou este de a separa
detaliile
incidentale ale implementarii (de exemplu, aranjamentul datelor
utilizate
pentru a memora un obiect al tipu-lui) de proprietatile esentiale ale
utilizarii lui corecte (de exemplu, lista completa de functii care pot
avea
acces la date). O astfel de separare poate fi exprimata prin
canalizarea
tuturor utilizarilor datelor structurii si a rutinelor de memorare
interna
printr-o interfata specifica.
Acest capitol consta din 4 parti separate:

&5.2 Clase si Membri. Aceasta sectiune introduce notiunea de
baza: tip
definit de utilizator numita clasa.
Accesul la obiectele unei clase se poate restringe la un set de
functii
declarate ca o parte a clasei; astfel de functii se numesc functii
membru.
Obiectele unei clase pot fi create si initializate prin functii membru
declarate in mod specific pentru acest scop: astfel de functii se
numesc
constructori. O functie membru poate fi declarata pentru a "sterge" un
astfel
de obiect al unei clase cind el este distrus; o astfel de functie se
numeste
destructor.

&5.3 Interfete si Implementari. Aceasta sectiune prezinta doua
exemple
de modul in care pot fi proiectate, implementate si utilizate clasele.

&5.4 Prieteni si Reuniuni. Aceasta sectiune prezinta multe
detalii
suplimentare despre clase. Arata cum se face accesul la partile private

ale
unei clase si cum se poate admite accesul pentru o functie care nu este

membru
al acelei clase. O astfel de functie se numeste prieten. Aceasta
sectiune de
asemenea arata cum se defineste o reuniune distinctiva.

&5.5 Constructori si Destructori. Un obiect poate fi creat ca un
obiect
automatic, static sau in memoria libera. Un obiect, de asemenea, poate
fi un
membru al unui anumit agregat (o clasa sau un vector), care la rindul
lui
poate fi alocat in una din cele 3 moduri indicate mai sus. Utilizarea
constructorilor si destructorilor se explica in detaliu.


5.2 Clase si Membri
---------------

Clasa este un tip definit de utilizator. Aceasta sectiune
introduce
facilitatile de baza pentru a defini o clasa, crearea obiectelor unei
clase,
manipularea acestor obiecte si in final stergerea acestor obiecte dupa
utilizare.

5.2.1 Functii membru
--------------

Sa consideram implementarea conceptului de data utilizind o
structura
pentru a defini reprezentarea unei date si un set de functii pentru
manipularea variabilelor de acest tip:

struct date{ int month, day, year; };
date today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(date*);
Nu exista conexiuni explicite intre functii si tipul datei. O
astfel de
conexiune se poate stabilii declarind functiile ca membri:
struct date{int month, day, year;
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

Functiile declarate in acest fel se numesc functii membru si pot
fi
invocate numai pentru o variabila specifica de tipul corespunzator
utilizind
sintaxa standard pentru accesul la membri unei structuri. De exemplu:
date today;
date my_birthday;
void f()
{my_birthday.set(30, 12, 1950);
today.set(18, 1, 1985);
my_birthday.print();
today.next();
}

Intrucit diferite structuri pot avea functii membru cu ace-
lasi nume, trebuie sa se specifice numele structurii cind se
defineste o functie membru:

void date::next()
{
if(++day > 28)
{
//do the hard part
}
}

Intr-o functie membru, numele membrilor pot fi folosite fara o

referire explicita la un obiect. In acest caz, numele se refera la
acel
membru al obiectului pentru care a fost apelata functia.


5.2.2 Clase
-----

Declaratia lui date din subsectiunea precedenta furnizeaza un set
de
functii pentru manipularea unei variabile de tip date, dar nu specifica

faptul
ca acele functii ar trebui sa fie singurele care sa aiba acces la
obiectele de
tip date. Aceasta restrictie poate fi exprimata utilizind o clasa in
locul
unei structuri:
class date{
int month, day, year;
public:
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};

Eticheta public separa corpul clasei in doua parti. Numele din
prima
parte, private, pot fi utilizate numai de functiile membre. Partea a

doua,
public, constituie interfata cu obiectele clasei. O structura (struct)

este
pur si simplu o clasa cu toti membri publici, asa ca functiile membru
se
definesc si se utilizeaza exact ca inainte. De exemplu:
void date::print() //print folosind notatia US
{
cout << month << "/" << day << "/" << year;
}
Cu toate acestea, functiile care nu sint membru nu pot folosi membri
privati
ai clasei date. De exemplu:

void backdate()
{
today.day--; //eroare
}

Exista citeva beneficii in urma restringerii accesului, la o data
structurata, la o lista de functii declarata explicit. Orice eroare
care
face ca date sa aiba o valoare ilegala (de exemplu December 36, 1985)
trebuie sa fie cauzata de codul unei functii membru, asa ca primul
stadiu al
depanarii, localizarea, este rezolvat inainte ca programul sa se
execute.
Acesta este un caz special al observatiei generale ca orice
schimbare
in comportarea tipului date poate, si trebuie sa fie efectuata prin
schimbarea
membrilor lui. Un alt avantaj este ca un utilizator de un astfel de
tip este
necesar numai sa examineze definitia functiilor membru pentru a invata
utilizarea lui.
Protectia datelor private se bazeaza pe restrictia utilizarii
numelor
membru ale clasei. Se poate trece peste aceasta prin manipularea de
adrese si
conversie explicita de tip, dar aceasta evident este un fel de
inselatorie.

5.2.3 Autoreferinta
-------------

Intr-o functie membru, ne putem referi direct la membri unui
obiect
pentru care functia membru este apelata. De exemplu:
class x{
int m;
public:
int readm(){ return m; }
x aa;
x bb;
void f()
{
int a = aa.readm();
int b = bb.readm();
//.......
}
}
In primul apel al membrului readm(), m se refera la aa.m iar in cel de
al
doilea la bb.m.
Un pointer la obiectul pentru care o functie membru este apelata
constituie un membru ascuns pentru functie. Argumentul implicit poate
fi
referit explicit prin this. In orice functie a unei clase x, pointerul
this
este declarat implicit ca:

x* this;

si este initializat ca sa pointeze spre obiectul pentru care functia
membru
este apelata. Intrucit this este un cuvint cheie el nu poate fi
declarat
explicit. Clasa x ar putea fi declarata explicit astfel:
class x{ int m;
public: int readm(){ return this->m; }
};
Utilizarea lui this cind ne referim la membri nu este
necesara; utilizarea majora a lui this este pentru a scrie functii
membru care
manipuleaza direct pointerii. Un exemplu tipic pentru this este o
functie care
insereaza o legatura intr-o lista dublu inlantuita:

class dlink{
dlink* pre; //legatura precedenta
dlink* suc; //legatura urmator
public:
void append(dlink*);
//........
};

void dlink::append(dlink* p)
{
p->suc = suc; //adica p->suc = this->suc
p->pre = this; //utilizarea explicita a lui this
suc->pre = p; //adica, this->suc->pre = p;
suc = p; //adica, this->suc = p
}

dlink* list_head;

void f(dlink* a, dlink* b)
{
//.......
list_head->append(a);
list_head->append(b);
}

Legaturile de aceasta natura generala sint baza pentru cla- sele
lista
descrise in capitolul 7. Pentru a adauga o legatura la o lista, trebuie

puse
la zi obiectele spre care pointeaza this, pre si suc. Ele toate sint de

tip
dlink, asa ca functia membru dlink::append() poate sa faca acces la
ele.
Unitatea de protectie in C++ este clasa, nu un obiect individual al
unei
clase.

5.2.4 Initializare
------------

Utilizarea functiilor de felul set_data() pentru a furniza
initializarea
pentru obiectele clasei nu este eleganta si este inclinata spre erori.
Intrucit nicaieri nu se afirma ca un obiect trebuie initializat, un
programator poate uita sa faca acest lucru sau (adesea cu rezultate
dezastruoase) sa faca acest lucru de doua ori. O conceptie mai buna
este de a
permite programatorului sa declare o functie cu scopul explicit de a
initializa obiecte. Deoarece o astfel de functie construieste valori de

un tip
dat, ea se numeste constructor. Un constructor se recunoaste deoarece
are
acelasi nume ca si clasa insasi. De exemplu:

class date{
//......
date(int, int, int);
};
Cind o clasa are un constructor, toate obiectele acelei clase vor
fi
initializate. Daca constructorul cere argumente, ele pot fi furnizate:
date today = date(23, 6, 1983);
date xmas(25, 12, 0); //forma prescurtata
date my_birthday: //ilegal, lipseste initializarea

Este adesea util sa se furnizeze diferite moduri de initializare a
obiectelor unei clase. Aceasta se poate face furnizind diferiti
constructori.
De exemplu:

class date{
int month, day, year;
public:
//........
date(int, int, int); //zi luna an
date(char*); //date reprezxista un motiv de a
obliga utilizarea cuvintului overload care sa ne protejeze impotriva
unei
reutilizari accidentale a unui nume.
Proliferarea constructorilor in exemplul date este tipica. Cind se
proiecteaza o clasa exista totdeauna tentatia de a furniza "totul"
deoarece se
crede ca este mai usor sa se furnizeze o trasatura chiar in cazul in
care
cineva o vrea sau din cauza ca ea arata frumos si apoi sa se decida ce
este in
realitate necesar. Ultima varianta necesita un timp mai mare de
gindire, dar
de obicei conduce la programe mai mici si mai comprehensibile. Un mod
de a
reduce numarul de functii inrudite este de a utiliza argumentele
implicite. In
date, fiecarui argument i se poate da o valoare implicita care se
interpreteaza: "implicit ia data curenta".
class date{
int month, day, year;
public:
//..........
date(int d=0, int m=0, int y=0);
date(char*); //date reprezentat ca sir
};
date::date(int d, int m, int y)
{day = d ? d : today.day;
month = m ? m : today.month;
year = y ? y : today.year;
//verifica faptul ca date este valida
//..........
}
Cind se utilizeaza o valoare pentru un argument pentru a indica
"ia
valoarea implicita", valoarea aleasa trebuie sa fie in afara setului
posibil
de valori pentru argument. Pentru zi si luna este clar acest lucru, dar
valoarea zero pentru an poate sa nu fie o alegere evidenta. Din
fericire nu
exista anul zero in calendarul european. 1AD(year == 1) vine imediat
dupa
1BC(year == -1), dar aceasta probabil ar fi prea subtil pentru un
program
real.

Un obiect al unei clase fara constructori poate fi initializat
atribuindu-i un alt obiect al acelei clase. Aceasta se poate face, de
asemenea, cind constructorii au fost declarati. De exemplu:
date d = today; //initializare prin asignare

In esenta, exista un constructor implicit ca o copie de biti a
obiectelor
din aceeasi clasa. Daca nu este dorit acest constructor implicit pentru

clasa
X, el poate fi redefinit prin constructorul denumit X(X&) (aceasta se
va
discuta mai departe in &6.6).

5.2.5 Curatire (stergere)
-------------------

Mai frecvent este cazul in care un tip definit de utilizator
are un
constructor pentru a asigura initializarea proprie. Multe tipuri
necesita, de
asemenea, un destructor, care sa asigure stergerea obiectelor de un
tip.
Numele destructorului pentru clasa X este ~X() ("complementul
constructorului"). In particular, multe clase utilizeaza memoria libera

(vezi
&3.2.6) ce se aloca printr-un constructor si se dealoca printr-un
destructor.
De exemplu, iata un tip de stiva conventionala care a fost complet
eliberata
de tratarea erorilor pentru a o prescurta:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){top = s = new char[size=sz];}
~char_stack(){ delete s; }
void push(char c){ *top++ = c; }
char pop(){ return *--top; }
};

Cind char_stack iese in afara domeniului, se apeleaza destructorul:

void f()
{
char_stack s1(100);
char_stack s2(200);
s1.push('a');
s2.push(s1.pop());
char ch = s2.pop();
cout << chr(ch) << "\n";
}

Cind f() este apelata, constructorul char_stack va fi apelat
pentru s1 ca
sa aloce un vector de 100 de caractere si pentru s2 pentru a aloca un
vector
de 200 de caractere; la revenirea din f(), acesti doi vectori vor fi
eliminati.


5.2.6 "Inline"
--------

Cind programam folosind clasele, este foarte frecvent sa utilizam
multe
functii mici. In esenta, o functie este realizata unde un program
structurat,
in mod traditional, ar avea un anumit mod tipic de utilizare a unei
date
structurate; ceea ce a fost o conventie devine un standard recunoscut
prin
compilator. Aceasta poate conduce la ineficiente teribile deoarece
costul
apelului unei functii este inca mai inalt decit citeva referinte la
me-
morie necesare pentru corpul unei functii triviale.
Facilitatile functiilor "in linie" au fost proiectate pentru a
trata
aceasta problema. O functie membru definita (nu numai declarata) in
declaratia
de clasa se considera ca fiind in linie. Aceasta inseamna de exemplu,
ca,
codul generat pentru functiile care utilizeaza char_stack-ul prezentat
mai sus
nu contine nici un apel de functie exceptind cele utilizate pentru a
implementa operatiile de iesire. Cu alte cuvinte, nu exista un cost de
timp
mai mic decit cel luat in seama cind proiectam o clasa; chiar si cele
mai
costisitoare operatii pot fi realizate eficient. Aceasta observatie
invalideaza motivele cele mai frecvent utilizate in favoarea utilizarii
membrilor publici ai datelor. O functie mem- bru poate, de asemenea, sa

fie
declarata inline in afara declaratiei de clasa. De exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char pop();
//......
}

inline char char_stack::pop()
{
return *--top;
}

5.3 Interfete si Implementari
-------------------------

Ce face o clasa buna? Ceva ce are un set mic si bine definit de
operatori. Ceva ce poate fi vazut ca o "cutie neagra" manipulata
exclusiv prin
acel set de operatii. Ceva a carei reprezentare reala ar putea fi
conceputa sa
fie modificata fara a afecta modul de utilizare a acelui set de
operatii.
Containerele de toate felurile furnizeaza exemple evidente:
tabele,
multimi, liste, vectori, dictionare, etc.. O astfel de clasa va avea o
operatie de inserare, care de obicei va avea de asemenea operatii
pentru a
verifica daca un membru specific a fost inserat, poate va avea operatii

pentru
sortarea membrilor, poate va avea operatii pentru examinarea tuturor
membrilor
intr-o anumita ordine si in final ar putea, de asemenea, sa aiba o
operatie
pentru eliminarea unui membru. Clasele container de obicei au
constructori si
destructori.
Ascunderea datelor si o interfata bine definita pot fi de asemenea
obtinute prin conceptul de modul (vezi de exemplu, &4.4: fisiere ca
module).
Cu toate acestea, o clasa este un tip; pentru a o utiliza, trebuie sa
se
creeze obiecte ale clasei respective si se pot crea atit de multe
astfel de
obiecte cite sint necesare. Un modul este el insusi un obiect; pentru
a-l
utiliza, cineva este necesar sa-l initializeze si exista exact un
astfel de
obiect.


5.3.1 Implementari alternative
------------------------

Atita timp cit declaratia partii publice a unei clase si
declaratia
functiilor membru ramin neschimbate, implementarea unei clase poate fi
schimbata fara a afecta utilizatorii ei. Sa consideram o tabela de
simboluri
de felul celei utilizate pentru calculatorul de birou din capitolul 3.
Este o
tabela de nume:

struct name{
char* string;
name* next;
double value;
};

Iata o versiune a clasei tabela:
//file table.h:
class table{
name* tbl;
public:
table(){tbl = 0;}
name* look(char*, int=0);
name* insert(char* s){return look(s, 1);}
};
Aceasta tabela difera de cea definita in capitolul 3 prin aceea ca este

un tip
propriu. Se pot declara mai multe tabele, putem avea un pointer spre o
tabela,
etc.. De exemplu:

#include "table.h"
table globals;
table keywords;
table* locals;
main()
{locals = new table;
//.........
}

Iata o implementare a lui table::look() utilizind o cautare
liniara prin
lista inlantuita de nume din tabela:
#include <string.h>
name* table::look(char* p, int ins)
{for(name* n = tbl; n; n = n->next)
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name;
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->value = 1;
nn->next = tbl;
tbl = nn;
return nn;
}

Acum consideram o inlantuire a clasei utilizind cautarea prin
hashing asa
cum s-a facut in exemplul cu calculatorul de birou. Este insa mai
dificil sa
facem acest lucru din cauza restrictiei ca, codul scris folosind
versiunea de
clasa table de mai jos, sa nu se schimbe.
class table{name** tbl;
int size;
public:
table(int sz=15);
~table();
name* look(char*, int=0);
name* insert(char* s){return look(s, 1);}
};
Structura datelor si constructorul s-au schimbat pentru a reflecta

nevoia
pentru o dimensiune specifica a tabelei cind se utilizeaza hashingul.
Prevazind constructorul cu un argument implicit ne asiguram ca, codul
vechi
care nu a specificat dimen- siunea unei tabele este inca corect.
Argumentele
implicite sint foarte utile in situatii cind vrem sa schimbam o clasa
fara a
afecta codul vechi. Constructorul si destructorul acum gestioneaza
crearea si
stergerea tabelelor de hashing:
table::table(int sz)
{
if(sz < 0)
error("negative table size");
tbl = new name*[size=sz];
for(int i=0; i < sz; i++)
tbl[i] = 0;
}

table::~table()
{for(int i=0; i < size; i++)
{name* nx;
for(name* n=tbl[i]; n; n=nx)
{
nx = n->next;
delete n->string;
delete n;
}
}
delete tbl;
}
O versiune mai simpla si mai clara a lui table::~table() se poate
obtine
declarind un destructor pentru class name. Functia lookup este aproape
identica cu cea utilizata in exemplul cu calculatorul de birou
(&3.1.3):
name* table::look(char* p, int ins)
{
int ii = 0;
char* pp = p;
while(*pp)
ii == ii << 1 ^ *pp++;
if(ii < 0)
ii = -ii;
ii %= size;
for(name* n = tbl[ii]; n; n = n->next)
if(strcmp(p, n->string) == 0)
return n;
if(ins == 0)
error("name not found");
name* nn = new name;
nn->string = new char[strlen(p) + 1];
strcpy(nn->string, p);
nn->value = 1;
nn->next = tbl[ii];
tbl[ii] = nn;
return nn;
}
Evident, functiile membru ale unei clase trebuie sa fie recompilate ori

de
cite ori se face o schimbare in declaratia de clasa. Ideal, o astfel de
schimbare nu ar trebui sa afecteze de loc utilizatorii unei clase. Din
nefericire, nu este asa. Pentru a aloca o variabila de clasa,
compilatorul are
nevoie sa cunoasca dimensiunea unui obiect al clasei. Daca dimensiunea
unui
astfel de obiect este schimbata, fisierele care contin utilizari ale
clasei
trebuie sa fie recompilate. Softwarul care determina setul minim de
fisiere ce
necesita sa fie recompilate dupa o schimbare a declaratiei de clasa
poate fi
(si a fost) scris, dar nu este inca utilizat pe scara larga.
Noi ne putem intreba, de ce nu a fost proiectat C++ in asa fel ca
recompilarea utilizatorilor unei clase sa fie necesara dupa o schimbare

in
partea privata? Si de ce trebuie sa fie prezenta partea privata in
declaratia
de clasa? Cu alte cuvinte, intrucit utilizatorii unei clase nu sint
admisi
sa aiba acces la membri privati, de ce declaratiile lor trebuie sa fie
prezente in fisierele antet ale utilizatorului? Raspunsul este
eficienta. Pe
multe sisteme, atit procesul de compilare cit si secventa de operatii
care
implementeaza apelul unei functii sint mai simple cind dimensiunea
obiectelor
automatice (obiecte pe stiva) se cunoaste la compilare.
Aceasta problema ar putea fi eliminata reprezentind fiecare obiect

al
clasei ca un pointer spre obiectul "real". Intrucit toti acesti
pointeri ar
avea aceeasi dimensiune, iar alocarea obiectelor "reale" ar putea fi
definita
intr-un fisier unde este disponibila partea privata, acest fapt ar
putea
rezolva problema. Cu toate acestea, aceasta solutie impune referirea la

o
memorie suplimentara cind se face acces la membri unei clase si mai
rau ar
implica cel putin un apel al alocatorului si dealocatorului de memorie
pentru
fiecare apel de functie cu un obiect automatic al clasei. De asemenea
s-ar
face implementarea unei functii membru inline care sa faca acces la
date
private fezabile. Mai mult decit atit, o astfel de schimbare ar face
imposibila linkarea impreuna a fragmentelor de programe C++ si C
(deoarece un
compilator C ar trata diferit o structura fata de un compilator C++).
Aceasta
este nepotrivit in C++.


5.3.2 O clasa completa
----------------
Programarea fara ascunderea datelor (folosind structuri) necesita
mai
putina bataie de cap decit programarea cu ascunderea de date (utilizind
clase). Se poate defini o structura fara prea mare bataie de cap, dar
cind
definim o clasa noi trebuie sa ne concentram sa furnizam un set complet

de
operatii pentru tipul nou; aceasta este o deplasare importanta in
domeniul
utilizarii. Timpul cheltuit in proiectarea unui nou tip este de obicei
recu-
perat de multe ori in dezvoltarea si testarea unui program. Iata un
exemplu de
tip complet, intset, care furnizeaza conceptul de "multime de intregi".
class intset{int cursize, maxsize;
int* x;
public:
intset(int m, int n); //cel putin m intregi
//in 1..n
~intset();
int member(int t); //este "t" un membru?
void insert(int t); //adauga "t" la multime
void iterate(int& i){i = 0;}
int ok(int& i){return i < cursize;}
int next(int& i){return x[i++];}
};
Pentru a testa aceasta clasa noi putem crea si apoi imprima un set de
intregi
aleatori. Un astfel de set ar putea constitui niste numere de loterie.
Acest
set simplu ar putea fi utilizat pentru a verifica un sir de intregi
punind
in evidenta duplicatele, dar pentru majoritatea aplicatiilor tipul set
ar
trebui sa fie putin mai migalos elaborat. Ca totdeauna sint posibile
erori:
#include <stream.h>
void error(char* s)
{cerr << "set: " << s << "\n";
exit(1);
}
Clasa intset se utilizeaza in functia main() care asteapta doua
argumente
intregi. Primul argument specifica numarul de numere aleatoare de
generat. Cel
de al doilea argument specifica domeniul intregilor aleatori care se
asteapta:
main(int argc, char* argv[])
{
if(argc != 3)
error("No arguments expected");
int count = 0;
int m = atoi(argv[1]); //numarul elementelor multimii
int n = atoi(argv[2]); //in domeniul 1..n
intset s(m, n);
while(cout < m)
{
int t = randint(n);
if(s.member(t) == 0)
{
s.insert(t);
count++;
}
}
print_in_order(&s);
}
Motivul ca argumentul numarator argc sa fie 3 pentru un program care
cere 2
argumente este faptul ca numele programului este totdeauna pasat ca
argv[0].
Functia:

extern int atoi(char*);

este o functie standard de biblioteca pentru covertirea reprezentarii
sub
forma de sir a unui intreg in forma lui interna binara.
Numerele aleatoare se genereaza utilizind functia standard rand():

extern int rand(); //nu este prea aleatoare
int randint(int n) //in domeniul 1..n
{
int r = rand();
if(r < 0)
r = -r;
return 1 + r % n;
}
Detaliile de implementare ale unei clase ar trebui sa fie de un
interes
mai mic pentru un utilizator, dar aici sint in orice caz si functiile
membru.
Constructorul aloca un vector intreg de dimensiune maxima a multimii
specificate, iar destructorul o dealoca:

intset::intset(int m, int n) //cel mult m intregi in 1..n
{
if(m < 1 || n < m)
error("illegal intset size");
cursize = 0;
maxsize = m;
x = new int[maxsize];
}

intset::~intset(){delete x;}

Intregii se insereaza asa ca ei sa fie tinuti in ordine cresca- toare
in
multime:

void intset::insert(int t)
{
if(++cursize > maxsize)
error("too many elements");
int i = cursize-1;
x[i] = t;
while(i > 0 && x[i-1] > x[i])
{
int t = x[i]; //permuta x[i] si x[i-1]
x[i] = x[i-1];
x[i-1] = t;
i--;
}
}

Se foloseste o cautare binara pentru a gasi un membru:

int intset::member(int t) //cautare binara
{
int l = 0;
int n = cursize-1;
while(l <= n)
{
int m = (l+n)/2;
if(t < x[m])
n = m-1;
else
if(t > x[m])
l = m+1;
else
return 1; //gasit
}
return 0; //negasit
}

In final, intrucit reprezentarea unei clase intset este ascunsa
utilizatorului, noi trebuie sa furnizam un set de ope- ratii care
permit
utilizatorului sa itereze prin multime intr-o anumita ordine. O
multime nu
este ordonata intrinsec, asa ca noi nu putem furniza pur si simplu un
mod de
accesare la vector (miine, eu ma pot gindi sa reimplementez intset ca o

lista
inlantuita).
Se furnizeaza trei functii: iterate() pentru a initializa o
iteratie,
ok() pentru a verifica daca exista un membru urmator si next() pentru a

obtine
membrul urmator:

class intset{
//.........
void iterate(int& i){i = 0;}
int ok(int& i){return i < cursize;}
int next(int& i){return x[i++];}
};

Pentru a permite ca aceste trei operatii sa coopereze si sa
reaminteasca
cit de departe a progresat iteratia, utilizatorul trebuie sa furnizeze
un
argument intreg. Intrucit argumentele sint pastrate intr-o lista
sortata,
implementarea lor este triviala. Acum poate fi definita functia
print_in_order:

void print_in_order(intset* set)
{
int var;
set->iterate(var);
while(set->ok(var))
cout << set->next(var) << "\n";
}


O alta varianta de a furniza un iterator se prezinta in &6.8.

5.4 Prieteni si Reuniuni
--------------------
Aceasta sectiune descrie inca citeva facilitati relativ la clase.
Se
prezinta un mod de a acorda acces functiilor membre la membri privati.
Se
descrie cum se pot rezolva conflictele numelor membre, cum se pot
imbrica
declaratiile de clase si cum pot fi eliminate imbricarile nedorite. De
asemenea se discuta cum pot fi obiectele unei clase divizate intre
membri ei
si cum se pot utiliza pointerii spre membri. In final exista un
exemplu care
arata cum se poate proiecta o reuniune discriminatorie.

5.4.1 Prieteni
--------
Presupunem ca noi trebuie sa definim doua clase, vector si
matrix.
Fiecare din ele ascunde reprezentarea ei si furnizeaza un set complet
de
operatii pentru manipularea obiectelor ei. Acum sa definim o functie

care
inmulteste o matrice cu un vector. Pentru simplificare, presupunem ca
un
vector are patru elemente, cu indicii 0..3 si ca o matrice are 4
vectori
indexati cu 0..3. Presupunem de asemenea, ca elementele unui vector
sint
accesate printr-o functie elem() care verifica indexul si ca matrix are

o
functie similara. O conceptie este de a defini o functie globala
multiply() de
forma:
vector multiply(matrix& m, vector& v)
{vector r;
for(int i=0; i<3; i++)
{
//r[i] = m[i] * v;
r.elem(i) = 0;
for(int j=0; j<3; j++)
r.elem(i) += m.elem(i, j) * v.elem(j);
}
return r;
}
Aceasta este intr-un anumit mod "natural" sa se faca asa, dar este
ineficient. De fiecare data cind se apeleaza multiply(), elem() se
apeleaza de
4*(1+4*3) ori.
Acum, daca noi facem ca multiply() sa fie membru al clasei vector,

noi am
putea sa ne dispensam de verificarea indicilor cind se face acces la un
element al vectorului si daca noi facem ca multiply() sa fie membru al
clasei
matrix, noi am putea sa ne dispensam de verificarea indicilor cind se
face
acces la elementul unei matrici. Cu toate acestea, o functie nu poate
fi
membru pentru doua clase. Ceea ce este necesar este o constructie a
limbajului
care sa asigure unei functii accesul la partea privata a unei clase. O
functie
nemembru la care i se permite accesul la partea privata a unei clase
se
numeste prieten al clasei. O fun- ctie devine prieten al unei clase
printr-o
declaratie de prieten in clasa respectiva. De exemplu:
class matrix;
class vector{float v[4];
//........
friend vector multiply(matrix&, vector&);
};
class matrix{vector v[4];
//........
friend vector multiply(matrix&, vector&);
};
Nu este nimic special in legatura cu o functie prieten exceptind
dreptul
de acces la partea privata a unei clase. In particular, o functie
prieten nu
are un pointer this (numai daca este o functie membru). O declaratie
friend
este o declaratie reala. Ea introduce numele functiei in domeniul
cel mai
extern al unui program si il verifica fata de alte declaratii ale
lui. O
declaratie friend poate fi plasata sau in partea privata sau in partea
publica
a unei declaratii de clasa; nu are importanta unde se introduce.
Functia
multiply poate acum sa fie scrisa utilizind direct elementele
vectorilor si
matricilor:

vector multiply(matrix& m, vector& v)
{
vector r;
for(int i=0; i<3; i++)
{
//r[i] = m[i]*v;
r.v[i] = 0;
for(int j=0; j<3; j++)
r.v[i] += m.v[i][j] * v.v[j];
}
return r;
}

Exista moduri de a trata aceasta problema particulara de eficienta

fara a
utiliza mecanismul friend (se poate defini operatia de inmultire pentru
vectori si sa se defineasca multiply() folosind-o pe aceasta). Cu toate
acestea, exista multe probleme care sint mult mai usor de rezolvat dind
posibilitatea unei functii care nu este membru al unei clase sa faca
acces la
partea privata a acelei clase. Capitolul 6 contine multe exemple de
utilizare
a prietenilor. Meritele relative ale functiilor prietene si membre va
fi
discutata mai tirziu.
O functie membru a unei clase poate fi prieten al alteia. De
exemplu:

class x{
//........
void f();
};

class y{
//........
friend void x::f();
};

Nu este ceva iesit din comun ca toate functiile unei clase sa fie
pritene
ale alteia. Exista chiar o prescurtare pentru acest fapt:

class x{
friend class y;
//........
};

Aceasta declaratie, friend, face ca toate functiile membre ale clasei y

sa fie
prietene ale clasei x.

5.4.2 Calificarea numelor de membri
-----------------------------

Ocazional, este util sa se faca distinctie explicita intre numele
membre
ale unei clase si alte nume. Se poate folosi operatorul de rezolutie a
domeniului "::":

class x{
int m;
public:
int readm(){ return x::m; }
void setm(int m){ x::m = m; }
};

In x::setm() numele argument m ascunde membrul m, asa ca membrul
ar putea
sa fie referit numai utilizind numele calificator al lui, x::m.
Operandul
sting a lui :: trebuie sa fie numele unei clase.
Un nume prefixat prin :: trebuie sa fie un nume global. Aceasta
este in
particular util pentru a permite nume populare cum ar fi read, put si
open sa
fie folosite pentru nume de fun- ctii membru fara a pierde abilitatea
de a se
face referire la versiunea nemembru. De exemplu:
class my_file{ //..........
public:
int open(char*, char*);
};

int my_file::open(char* name, char* spec)
{ //...........
if(::open(name, flag))
{
//utilizeaza open() din UNIX(2)
//..........
}
//...........
}


5.4.3 Clase imbricate
---------------

Declaratiile de clasa pot fi imbricate. De exemplu:
class set{
struct setmem{
int mem;
setmem* next;
setmem(int m,setmem* n){ mem=m; next=n; }
};
setmem* first;
public:
set(){first = 0;}
insert(int m){first = new setmem(m, first);}
//.......
};

Daca clasa imbricata nu este foarte simpla, astfel de declaratii
sint
foarte incurcate. Mai mult decit atit, clasele imbricate sint mai mult
o
facilitate in notatie, intrucit o clasa imbricata nu este ascunsa in
domeniul
clasei care o include din punct de vedere lexical:
class set{
struct setmem{
int mem;
setmem* next;
setmem(int m, setmem* n);
};
//.......
};

setmem::setmem(int m, setmem* n)
{mem = m;
next = n;
}
setmem m1(1, 0);

Constructorii de forma set::setmem::setmem() nu sint necesari si
nici
legali. Singurul mod de ascundere a numelui unei clase este prin
utilizarea
tehnicii de fisiere_module (&4.4).
Clasele netriviale este bine sa fie declarate separat:
class setmem{
friend class set; //acces numai prin membri
//lui set
int mem;
setmem* next;
setmem(int m, setmem* n){ mem=m; next=n; }
};

class set{
setmem* first;
public:
set(){ first = 0; }
insert(int m){ first = new setmem(m, first); }
};


5.4.4 Membri statici
--------------
O clasa este un tip, nu un obiect data si fiecare obiect al clasei

are
copia lui proprie a membrilor date ai clasei. Cu toate acestea, unele
tipuri
sint implementate mai elegant daca toate obiectele acelui tip au in
comun
unele date. Este preferabil ca o astfel de data comuna sa fie
declarata ca
parte a clasei. De exemplu, pentru a gestiona taskuri intr-un sistem de
operare, este adesea utila o lista a tuturor taskurilor:
class task{//........
task* next;
static task* task_chain;
void schedule(int);
void wait(event);
//........
};
Declarind membrul task_chain ca static se asigura ca va fi numai o

copie
a lui, nu o copie pentru fiecare obiect task. Este inca in domeniul
clasei
task si poate fi accesat "din afara" numai daca a fost declarat public.

In
acest caz, numele lui tre- buie sa fie calificat prin numele clasei
sale:
task::task_chain

Intr-o functie membru, se poate face referire prin task_chain.
Utilizarea
membrilor statici ai clasei poate reduce considerabil necesarul de
memorie
pentru variabilele globale.


5.4.5 Pointeri spre membri
--------------------

Este posibil sa se ia adresa unui membru al unei clase. A lua

adresa
unei functii membru este adesea util intrucit tehnicile si motivele
pentru a
utiliza pointeri la functii prezentate in &4.6.9 se aplica in mod egal
si la
functii membru. Totusi exista un defect curent in limbaj: nu este
posibil sa
se exprime tipul pointerului obtinut dintr-o astfel de operatie. In
consecinta
trebuie sa folosim trucuri folosind avantajele din implementarea
curenta.
Exemplul de mai jos nu este garantat ca fun- ctioneaza si utilizarea
lui
trebuie localizata in asa fel incit sa poata fi usor convertit spre a
utiliza
constructiile propri ale limbajului. Trucul folosit este acela de a
avea
avantajul faptului ca this este implementat curent ca primul argument
(ascuns)
al unei functii membru.

#include <stream.h>
struct cl{
char* val;
void print(int x){ cout << val << x << "/n"; }
cl(char* v){val = v;}
};

//"se ia" tipul functiilor membru:

typedef void (*PROC)(void*, int);

main()
{cl z1("z1 ");
cl z2("z2 ");
PROC pf1 = PROC(&z1.print);
PROC pf2 = PROC(&z2.print);
z1.print(1);
(*pf1)(&z1, 2);
z2.print(3);
(*pf2)(&z2, 4);
}
In multe cazuri, functiile virtuale (vezi capitolul 7) pot fi
utilizate
cind altfel s-ar utiliza pointeri spre functii.
Versiunile ulterioare de C++ vor suporta un concept de pointer
spre un
membru: cl::* inseamna "pointer spre un membru a lui cl". De exemplu:
typedef void(cl::*PROC)(int);
PROC pf1=&cl::print;//nu este nevoie de conversie explicita
PROC pf2 = &cl::print;
Operatorii . si -> se utilizeaza pentru un pointer spre o functie
membru.
De exemplu:

(z1.*pf1)(2);
((&z2)->*pf2)(4);


5.4.6 Structuri si Reuniuni
---------------------


Prin definitie o structura este pur si simplu o clasa cu toti
membri
publici, adica:

struct s{ ...
este pur si simplu o prescurtare pentru:

class{
public: ...

Structurile se folosesc cind ascunderea datelor este nepotrivita.

O reuniune numita se defineste ca o structura in care fiecare
membru are
aceeasi adresa (vezi &r8.5.13). Daca se stie ca numai un membru al unei
structuri va avea o valoare utila la un moment dat, o reuniune poate
salva
spatiu. De exemplu, se poate defini o reuniune pentru a pastra
unitatile
lexicale dintr-un compilator C:

union tok_val{
char* p; //sir
char v[8]; //identificator (maxim 8 caractere)
long i; //valori intregi
double d; //valori flotante
};
Problema este ca, in general compilatorul nu poate sa stie care
membru
este utilizat in fiecare moment, asa ca nu poate fi testat tipul. De
exemplu:

void strange(int i)
{
tok_val x;
if(i)
x.p = "2";
else
x.d = 2;
sqrt(x.d); //eroare daca i != 0
}

Mai mult decit atit, o reuniune definita in acest fel poate fi
initializata. De exemplu:

tok_val curr_val = 12; //eroare: se atribuie int la tok_val


este ilegal. Se pot utiliza constructori care sa trateze corect aceasta
problema:

union tok_value{
char* p; //sir
char v[8]; //identificator
long i; //valori intregi
double d; //valori flotante
tok_value(char*) //trebuie sa decida intre
//p si v
tok_value(int ii){i = ii;}
tok_value(double dd){d == dd;}
};

Aceasta trateaza cazurile in care tipurile membru pot fi rezolvate prin

reguli
pentru nume de functii supraincarcate (vezi &4.6.7 si &6.3.3). De
exemplu:

void f()
{
tok_val a = 10; //a.i = 10
tok_val b = 10.0; //b.d = 10.0
}

Cind acest lucru nu este posibil (pentru tipurile char* si char[8], int

si
char, etc.), membrul propriu poate fi gasit numai examinind
initializatorul la
momentul executiei sau furnizind un extra argument. De exemplu:

tok_val::tok_val(char* pp)
{
if(strlen(pp) <= 8)
strncpy(v, pp, 8); //sir scurt
else
p = pp; //sir lung
public:
tok_val(char* pp);
tok_val(long ii){ i=ii; tag='I'; }
tok_val(double dd){ d=dd; tag='D'; }
long& ival(){ check('I', "ival"); return i; }
double& fval(){check('D', "fval"); return d; }
char*& sval(){ check('S', "sval"); return p; }
char* id(){ check('N', "id"); return v; }
};

Constructorul utilizeaza functia strncpy pentru a copia un sir
scurt;
strncpy() aminteste de strcpy(), ea avind un al treilea argument care
defineste numarul de caractere ce se copiaza.

tok_val::tok_val(char* pp)
{
if(strlen(pp) <= 8)
{ //sir scurt
tag = 'N';
strncpy(v, pp, 8); //copiaza 8 caractere
}
else
{
tag = 'S';
p = pp; //se pastreaza numai pointerul
}
}


Tipul tok_val poate fi folosit astfel:


void f()
{
tok_val t1("short"); //asignare la v
tok_val t2("long string"); //asignare la p
char s[8];
strncpy(s, t1.id(), 8); //ok
strncpy(s, t2.id(), 8); //testul va esua
}

5.5 Constructori si Destructori
---------------------------

Cind o clasa are un constructor, el este apelat ori de cite ori se

creaza
un obiect al acelei clase. Cind o clasa are un destructor, el este
apelat ori
de cite ori este distrus un obiect al acelei clase. Obiectele pot fi
create
ca:
[1] Un obiect automatic: se creaza de fiecare data cind se
intilneste declaratia lui la executia programului si
este distrus de fiecare data cind se iese din blocul in
care el a aparut;
[2] Un obiect static: se creaza o data la pornirea programu-
lui si se distruge o data cu terminarea programului;
[3] Un obiect in memoria libera: este creat folosind
operatorul new si distrus folosind operatorul delete;
[4] Un obiect membru: ca membru al unei clase ori ca un
element de vector.

Un obiect poate de asemenea, sa fie construit intr-o expresie prin
folosirea explicita a unui constructor (&6.4), caz in care el este un
obiect
automatic. In subsectiunile care urmeaza se presupune ca obiectele sint

ale
unei clase cu un constructor si un destructor. Ca exemplu se utilizeaza

clasa
table din &5.3.


5.5.1 Goluri
------

Daca x si y sint obiecte ale clasei cl, x=y inseamna copie- rea
bitilor
lui y in x (&2.3.8). Avind asignarea interpretata in acest fel noi
putem sa
ajungem la surprize (uneori nedorite) cind folosim obiecte ale unei
clase
pentru care a fost definit un constructor si un destructor. De exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){top=s=new char[size=sz];}
~char_stack(){delete s;} //destructor
void push(char c){*top++=c;}
char pop(){return *--top;}
};
void h()
{
char_stack s1(100);
char_stack s2 = s1; //apar probleme
char_stack s3(99);
s3 = s2; //apar probleme
}

Aici constructorul char_stack::char_stack() se apeleaza de doua ori:
pentru s1
si s3. Nu se apeleaza pentru s2 deoarece variabila s2 a fost
initializata prin
atribuire. Totusi, destructorul char_stack::~char_stack() se apeleaza
de trei
ori: pentru s1, s2 si s3. Mai mult decit atit, interpretarea implicita
a
atribuirii ca si copiere de biti face ca s1, s2 si s3 sa contina
fiecare la
sfirsitul lui h() un pointer spre vectorul de caractere alocat in
memoria
libera cind a fost creat s1. Nu va ramine nici un pointer spre vectorul

de
caractere alocate cind a fost creat s3. Astfel de anomalii pot fi
eliminate
asa cum se va vedea in capitolul 6.

5.5.2 Memoria statica
---------------

Consideram:

table tbl1(100);
void f(){ static table tbl2(200); }

main()
{
f();
}

Aici, constructorul table::table() asa cum a fost definit in
&5.3.1 va fi
apelat de doua ori: o data pentru tbl1 si o data pentru tbl2.
Destructorul
table::~table() va fi apelat de asemenea de doua ori: pentru a elimina
tbl1 si
tbl2 dupa iesirea din main().
Constructorii pentru obiecte globale statice intr-un fisier se
executa in
ordinea in care apar declaratiile; destructorul se apeleaza in ordine
inversa.
Daca un constructor pentru un obiect local static este apelat, el se
apeleaza
dupa ce au fost apelati constructorii pentru obiectele statice globale
care il
preced.
Argumentele pentru constructorii de obiecte statice trebuie sa fie
expresii constante:

void g(int a)
{
static table t(a); //eroare
}

Traditional, executia lui main() a fost vazuta ca executia
programului.
Aceasta nu a fost niciodata asa, nici chiar in C, dar numai alocind un
obiect
static al unei clase cu un constructor si/sau un destructor
programatorul
poate sa aiba un mod evident si simplu de a specifica cod de
executat
inainte si/sau dupa apelul lui main.
Apelind constructori si destructori pentru obiecte statice se
realizeaza
functii extrem de importante in C++. Este modul de a asigura
initializari
propri si de a curata structuri de date din biblioteci. Consideram
<stream.h>.
De unde vin cin, cout si cerr? Unde au fost ele initializate? Si ce
este mai
important, intrucit sirurile de iesire pastreaza zone tampon interne de
caractere, cum se videaza aceste zone tampon? Raspunsul simplu si clar
este
acela ca activitatea se face prin constructori si des- tructori
corespunzatori
inainte si dupa executia lui main(). Exista alternative de a utiliza
constructori si destructori pentru initializarea si stergerea
facilitatilor de
biblioteca.
Daca un program se termina utilizind functia exit(), se vor apela
destructorii pentru obiectele statice, dar daca, programul se termina
folosind
abort(), ei nu vor fi apelati. Sa observam ca aceasta implica faptul ca

exit()
nu termina programul imediat. Apelind exit() intr-un destructor se
poate
ajunge la o recursivitate infinita.
Uneori, cind noi proiectam o biblioteca, este necesar sau pur si
simplu
convenabil sa inventam un tip cu un constructor si un destructor cu
singurul
scop al initializarii si stergerii. Un astfel de tip va fi folosit
numai o
data: sa aloce un obiect static prin apelul constructorului.


5.5.3 Memoria libera
--------------

Fie:
main()
{
table* p = new table(100);
table* q = new table(200);
delete p;
delete p; //probabil o eroare
}

Constructorul table::table() va fi apelat de doua ori si la fel si
destructorul table::~table(). Este bine de amintit ca C++ nu ofera
garantie ca
un destructor este apelat vreodata pentru un obiect creat folosind new.
Programul precedent nu il sterge pe q, dar pe p il sterge de doua ori.
In
functie de tipul lui p si q, programatorul poate sau nu sa considere
aceasta
ca o eroare. Ne- stergind un obiect de obicei nu este o eroare, ci
numai o
pierdere de spatiu. Stergind p de doua ori este de obicei o eroare
serioasa.
Un rezultat frecvent al aplicarii lui delete de doua ori la acelasi
pointer
este un ciclu infinit in rutina de gestionare a memoriei libere, dar
comportamentul in acest caz nu este specificat prin definitia
limbajului si
depinde de implementare.
Utilizatorul poate defini o implementare noua pentru operatorii
new si
delete (vezi &3.2.6). Este de asemenea posibil sa se specifice modul in

care
interactioneaza constructorul si destructorul cu operatorii new si
delete
(vezi &5.5.6).

5.5.4 Obiectele clasei ca membri
---------------------------
(clase de obiecte ca membri)

Consideram:

class classdef{
table members;
int no_of_members;
//...........
classdef(int size);
~classdef();
};

Intentia este clara; aceea ca classdef sa contina o tabela de
members de
dimensiune size si problema este de a obtine constructorul
table::table()
apelat cu argumentul size. Se poate face astfel:
classdef::classdef(int size)
:members(size)
{
no_of_members = size;
//...........
}

Argumentele pentru un constructor membru (table::table()) se
plaseaza in
definitia (nu in declaratia) constructorului clasei care il contine
(aici
classdef::classdef()). Constructorul membru este apoi apelat inaintea
corpului
constructorului care specifica lista argumentelor lui.

Daca sint mai multi membri ce necesita liste de argumente pentru
constructori, ei pot fi specificati in mod analog. De exemplu:

class classdef{
table members;
table friends;
int no_of_members;
//..........
classdef(int size);
~classdef();
};


Lista de argumente pentru membri se separa prin virgula (nu prin
doua
puncte), iar listele initializatorilor pentru membri pot fi prezentate

in
orice ordine:

classdef::classdef(int size)
:friends(size), members(size)
{
no_of_members = size;
//...........
}

Ordinea in care se apeleaza constructorii nu este specificata, asa

ca nu
se recomanda ca lista argumentelor sa fie cu efecte secundare:

classdef::classdef(int size)
:friends(size = size/2), members(size) //stil rau
{
no_of_members = size;
//...........
}

Daca un constructor pentru un membru nu necesita argumente, atunci

nu
este necesar sa se specifice nici o lista de argumente. De exemplu,
intrucit
table::table() a fost definit cu argumentul implicit 15, ceea ce
urmeaza este
corect:

classdef::classdef(int size)
:members(size)
{
no_of_members = size;
//...........
}

si dimensiunea lui friends table va fi 15.
Cind o clasa care contine clase (de exemplu classdef) se distruge,

intii
se executa corpul destructorului propriu acelei clase si apoi se
executa
destructorii membrilor.

Consideram varianta traditionala de a avea clase ca membri si
anume
aceea de a avea membri pointeri si ai initializa pe acestia intr-un
constructor:

class classdef{
table* members;
table* friends;
int no_of_members;
//............
classdef(int size);
~classdef();
};
classdef::classdef(int size)
{
members = new table(size);
friends = new table; //dimensiune implicita
no_of_members = size;
//...........
}

Intrucit tabelele au fost create folosind new, ele trebuie sa fie
distruse utilizind delete:

classdef::~classdef()
{//...........
delete members;
delete friends;
}
Obiectele create separat ca acestea pot fi utile, dar sa observam
ca
members si friends pointeaza spre obiecte separate care cer o alocare
si o
dealocare fiecare. Mai mult decit atit, un pointer plus un obiect in
memoria
libera ia mai mult spatiu decit un obiect membru.


5.5.5 Vectori si Obiecte clasa
------------------------

Pentru a declara un vector de obiecte ale unei clase cu un
constructor
acea clasa trebuie sa aiba un constructor care sa poata fi apelat fara
o lista
de argumente. Nici argumentele implicite nu pot fi utilizate. De
exemplu:
table tblvec[10];

este o eroare deoarece table::table() necesita un argument intreg. Nu
exista
nici un mod de a specifica argumente pentru un constructor intr-o
declaratie
de vector. Pentru a permite declararea vectorilor de tabele, ar putea
fi
modificata declaratia clasei table (&5.3.1) astfel:
class table{
//.........
void init(int sz); //ca si constructorul vechi
public:
table(int sz){init(sz);} //ca inainte dar nu
//exista valoare implicita
table(){init(15);} //implicit
//.........
};

Destructorul trebuie apelat pentru fiecare element al unui vector
cind se
distruge acel vector. Aceasta se face implicit pentru vectori care nu
sint
alocati utilizind new. Cu toate acestea, aceasta nu se poate face
implicit
pentru vectori din memoria libera deoarece compilatorul nu poate face
distinctie dintre pointerul spre un singur obiect de un pointer spre
primul
element al unui vector de obiecte. De exemplu:
void f()
{
table* t1 = new table;
table* t2 = new table[10];
delete t1; //o tabela
delete t2; //apar probleme: 10 tabele
}


In acest caz programatorul trebuie sa furnizeze dimensiunea
vectorului:
void g(int sz)
{table* t1 = new table;
table* t2 = new table[sz];
delete t1;
delete[sz] t2; }
Dar de ce nu poate compilatorul sa deduca numarul de elemente din
cantitatea
de memorie alocata? Deoarece alocatorul de memorie libera nu este o
parte a
limbajului si ar putea fi furnizata de programator.


5.5.6 Obiecte mici
------------

Cind se utilizeaza multe obiecte mici alocate in memoria libera,
noi
putem sa aflam ca programul consuma timp considerabil pentru alocare si
dealocare de astfel de obiecte. O solutie este de a furniza un alocator

cu
scopuri generale mai bun si o a doua este ca proiectarea unei clase sa
nu se
faca pentru a fi gestionata in memoria libera, definind constructori si
destructori.
Sa consideram clasa name folosita in exemplul table. Ea ar putea
fi
definita astfel:
struct name{char* string;
name* next;
double value;
name(char*, double, name*);
~name();
};
Programatorul poate avea avantaje din faptul ca alocarea si dealocarea
obiectelor unui tip poate fi facuta pe departe mai eficient (in timp si
spatiu) decit cu o implementare generala prin new si delete. Ideea
generala
este de a prealoca "felii" de obiecte de tip name si de a le lega intre

ele,
reducind alocarea si dealocarea la operatii simple asupra listelor
inlantuite.
Variabila nfree este antetul unei liste de nume neutilizate.
const NALL = 128;
name* nfree;
Alocatorul utilizat prin operatorul new pastreaza dimensiunea unui
obiect
impreuna cu obiectul pentru ca operatorul delete sa functioneze corect.

Aceste
spatii suplimentare se elimina simplu la un alocator specific unui tip.

De
exemplu, alocatorul urmator utilizeaza 16 octeti pentru a memora un
name la
masina mea, in timp ce alocatorul general foloseste 20. Iata cum se
poate face
aceasta:
name::name(char* s, double v, name* n)
{register name* p = nfree //prima alocare
if(p)
nfree = p->next;
else
{name* q = (name*)new char[NALL * sizeof(name)];
for(p = nfree = &q[NALL-1]; q<p; p--)
p->next = p-1;
(p+1)->next = 0;
}
this = p;
string = s; //initializare
value = v;
next = n;
}

Atribuirea la this informeaza compilatorul ca programatorul a luat
controlul si ca mecanismul implicit de alocare de memorie nu trebuie sa

fie
utilizat. Constructorul name::name() trateaza cazul in care numele este

alocat
numai prin new, dar pentru multe tipuri acesta este de obicei cazul;
&5.5.8
explica cum se scrie un constructor pentru a trata atit memoria libera,

cit si
alte tipuri de alocari.
Sa observam ca spatiul nu ar putea fi alocat pur si simplu astfel:

name* q = new name[NALL];

intrucit aceasta ar cauza o recursivitate infinita cind new apeleaza
name::name().
Dealocarea este de obicei triviala:

name::~name()
{
next = nfree;
nfree = this;
this = 0;
}

Atribuind 0 la this intr-un destructor se asigura ca nu se va
utiliza
destructorul standard.

5.5.7 Goluri
------

Cind se face o atribuire la this intr-un constructor, valoarea lui

this
este nedefinita pina la acea atribuire. O referinta la un membru
inaintea
acelei atribuiri este de aceea nedefinita si probabil cauzeaza un
destructor.
Compilatorul curent nu incearca sa asigure ca o atribuire la this sa
apara pe
orice cale a executiei:
mytype::mytype(int i)
{if(i) this = mytype_alloc(); //asignare la membri
};
se va aloca si nu se va aloca nici un obiect cind i == 0.
Este posibil pentru un constructor sa se determine daca el a fost
apelat
de new sau nu. Daca a fost apelat prin new, pointerul this are valoarea

zero
la intrare, altfel this pointeaza spre spatiul deja alocat pentru
obiect (de
exemplu pe stiva). De aceea este usor sa se scrie un constructor care
aloca
memorie daca (si numai daca) a fost apelat prin new. De exemplu:

mytype::mytype(int i)
{
if(this == 0)
this = mytype_alloc(); //asignare la membri
};

Nu exista o facilitate echivalenta care sa permita unui destructor

sa
decida daca obiectele lui au fost create folosind new si nici o
facilitate
care sa permita sa se decida daca el a fost apelat prin delete sau
printr-un
obiect din afara domeniului. Daca cunoasterea acestui lucru este
importanta,
utilizatorul poate memora undeva informatii corespunzatoare pe care sa
le
citeasca destructorul. O alta varianta este ca utilizatorul sa se
asigure ca
obiectele acelei clase sint numai alocate in mod corespunzator. Daca
prima
problema este tratata, ultima este neinteresanta.
Daca implementatorul unei clase este de asemenea numai
utilizatorul ei,
este rezonabil sa se simplifice clasa bazindu-ne pe presupunerile
despre
utilizarea ei. Cind o clasa este proiectata pentru o utilizare larga,
astfel
de presupuneri este adesea mai bine sa fie eliminate.


5.5.8 Obiecte de dimensiune variabila
-------------------------------

Luind controlul asupra alocarii si dealocarii, utilizatorul poate
de
asemenea, construi obiecte a caror dimensiune nu este determinata la
momentul
compilarii. Exemplele precedente de implementare a claselor container
vector,
stack, insert si table ca dimensionate fix, acceseaza direct structuri
care
contin pointeri spre dimensiunea reala. Aceasta implica faptul ca sint
necesare doua operatii de creare de astfel de obiecte in memoria libera

si ca
orice acces la informatiile memorate va implica o indirectare
suplimentara. De
exemplu:

class char_stack{
int size;
char* top;
char* s;
public:
char_stack(int sz){ top=s=new char[size=sz]; }
~char_stack(){ delete s; } //destructor
void push(char c){ *top++=c; }
char pop(){ return *--top; }
};


Daca fiecare obiect al unei clase este alocat in memoria libera,
aceasta
nu este necesar. Iata o alternativa:

class char_stack{
int size;
char* top;
char s[1];
public:
char_stack(int sz);
void push(char c){ *top++=c; }
char pop(){ return *--top; }
};

char_stack::char_stack(int sz)
{
if(this)
error("stack not on free store");
if(sz<1)
error("stack size < 1");
this = (char_stack*)new char[sizeof(char_stack)+sz-1];
size = sz;
top = s;
}

Observam ca un destructor nu mai este necesar, intrucit delete
poate
elibera spatiul utilizat de char_stack fara vreun ajutor din partea
programatorului.


5.6 Exercitii
---------

1. (*1). Sa se modifice calculatorul de birou din capitolul
3 pentru a utiliza clasa table.

2. (*1). Sa se proiecteze tnode (&r8.5) ca o clasa cu con-
sructori, destructori, etc.. Sa se defineasca un arbore de tnodes
ca o clasa cu constructori, destructori, etc..

3. (*1). Sa se modifice clasa intset (&5.3.2) intr-o multime
de siruri.

4. (*1). Sa se modifice clasa intset intr-o multime de noduri

unde
node este o structura pe care sa o definiti.

5. (*3). Se defineste o clasa pentru analizarea, memorarea,
evaluarea si imprimarea expresiilor aritmetice simple care constau din
constante intregi si operatiile '+', '-', '*' si '/'. Interfata publica

ar
trebui sa arate astfel:

class expr{
//.........
public:
expr(char*);
int eval();
void print();
};
Argumentul sir pentru constructorul expr::expr() este expresia.
Functia
expr::eval() returneaza valoarea expresiei, iar expr::print() imprima
reprezentarea expresiei la cout. Un program ar putea arata astfel:

expr x("123/4+123*4-3");
cout << "x = " << x.eval() << "\n";
x.print();

Sa se defineasca expr class de doua ori: o data utilizind o lista
inlantuita de noduri si o data utilizind un sir de caractere. Sa se
experimenteze diferite moduri de imprimare a expre- siei: cu paranteze
complete, notatie postfix, cod de asamblare, etc..

6. (*1). Sa se defineasca o clasa char_queue asa ca interfata
publica
sa nu depinda de reprezentare. Sa se implementeze char_queue: (1) ca o
lista
inlantuita si (2) ca un vector.

7. (*2). Sa se defineasca o clasa histograma care tine seama
de numerele dintr-un anumit interval specificat ca argumente la
constructorul
histogramei. Sa se furnizeze functii pentru a imprima histograme. Sa se
trateze domeniul valorilor. Recomandare: <task.h>.

8. (*2). Sa se defineasca niste clase pentru a furniza numere
aleatoare de o anumita distributie. Fiecare clasa are un constructor
care
specifica parametri pentru distributie si o functie draw care
returneaza
valoarea "urmatoare". Recomandare: <task.h>. Vezi de asemenea clasa
intset.

9. (*2). Sa se rescrie exemplul date (&5.2.2) exemplul
char_stack
(&5.2.5) si exemplul intset (&5.3.2) fara a utiliza functii membru
(nici chiar
constructori si destructori). Sa se utilizeze numai class si friend. Sa

se
testeze versiunile noi. Sa se compare cu versiunile care utilizeaza
functiile
membru.

10. (*3). Sa se proiecteze o clasa pentru o tabela de simbo-
luri si o clasa de intrare in tabela de simboluri pentru un anumit
limbaj. Sa
aruncam o privire la compilatorul limbajului respectiv pentru a vedea
cum
arata tabela de simboluri reala.

11. (*2). Sa se modifice clasa expresie din exercitiul 5 pentru

a
trata variabile si operatorul de asignare =. Sa se foloseasca clasa
tabela de
simboluri din exercitiul 10.

12. (*1). Fiind dat programul:

#include <stream.h>
main()
{ cout << "Hello, word\n";
}
sa se modifice pentru a avea la iesire:

Initialize
Hello, world
Clean up

Sa nu se modifice functia main().


CAPITOLUL 6
===========


OPERATOR SUPRAINCARCAT
======================


Acest capitol descrie mecanismul pentru operatorul de
supraincarcare
furnizat de C++. Un programator poate defini un sens pentru operatori
cind se
aplica la obiectele unei clase specifice; in plus se pot defini fata de
operatiile aritmetice, logice si relationale, apelul () si indexarea []

si
atit initializarea cit si asignarea pot fi redefinite. Se pot defini
conversii
de tip implicite si explicite intre cele definite de utilizator si
tipurile de
baza. Se arata cum se defineste o clasa pentru care un obiect nu poate
fi
copiat sau distrus exceptind functiile specifice definite de
utilizator.

6.1 Introducere
-----------

Programele adesea manipuleaza obiecte care sint reprezentari
concrete ale
conceptelor abstracte. De exemplu, datele de tip int din C++, impreuna
cu
operatorii +, -, *, /, etc., furnizeaza o implementare (restrictiva) a
conceptului matematic de intregi. Astfel de concepte de obicei includ
un set
de operatori care reprezinta operatiile de baza asupra obiectelor
intr-un mod
concis, convenabil si conventional. Din nefericire, numai foarte putine

astfel
de concepte pot fi suportate direct prin limbajul de programare. De
exemplu,
ideile de aritmetica complexa, algebra matricilor, semnale logice si
sirurile
receptionate nu au un suport direct in C++. Clasele furnizeaza o
facilitate
pentru a specifica o reprezentare a obiectelor neprimitive in C++
impre- una
cu

Plain Text Attachment [ Scan and Save to Computer | Save to Yahoo!
Briefcase ]

#include <stdio.h>
long int f3(int x)
{
return (x*x*x);
}
void main()
{
int a;
scanf("%d",&a);
printf("%ld",f3(a));
if (f3(a)>1000)
printf("E mare");
else
printf(" emai mic");
}

Attachments
Attachment scanning provided by:

Files:
POO.doc POO.doc (158k) [Preview] Scan and Save to Computer - Save to
Yahoo! Briefcase
Delete Reply Forward Spam Move...
Previous | Next | Back to Messages Save Message Text | Full Headers
Check Mail Compose
Search Mail Search the Web
Move Options

* [New Folder]
* adrese

Forward Options

* As Inline Text
* As Attachment

Reply Options

* Reply To Sender
* Reply To Everyone

Mail Shortcuts

* Check Mail Ctrl+shift+C
* Compose Ctrl+shift+P

* Folders Ctrl+shift+F
* Advanced Search Ctrl+shift+S

* Options
* Help Ctrl+shift+H

Address Book Shortcuts

* Add Contact
* Add Category

* View Contacts
* View Lists

* Quickbuilder
* Import Contacts
* Synchronize

* Addresses Options
* Addresses Help

Calendar Shortcuts

* Add Event
* Add Task
* Add Birthday

* Day
* Week
* Month
* Year

* Event List
* Reminders
* Tasks

* Sharing
* Synchronize

* Calendar Options
* Calendar Help

Notepad Shortcuts

* Add Note
* Add Folder

* View Notes

* Notepad Options
* Notepad Help

Advanced Search

* Advanced Search

Copyright © 1994-2006 Yahoo! Inc. All rights reserved. Terms of
Service - Copyright/IP Policy - Guidelines - Ad Feedback
NOTICE: We collect personal information on this site.
To learn more about how we use your information, see our Privacy Policy

Reply all
Reply to author
Forward
0 new messages