You are on page 1of 282

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 programului 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 utilizind 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
controlate de utilizator si mecanismul de supraincarcare a operatorilor. C++
furnizeaza facilitati mai bune pentru verificarea tipului 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 supraincarca, operatori de gestionare a memoriei libere si un tip
referinta.

OBSERVATII PENTRU CITITOR


===========================

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

Capitolul 1 este o trecere in revista rapida a caracteristicilor 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 concatenare. 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 ignorate. 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 separata, 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 utilizeze 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 liniei respective.

1.3 Tipuri si Declaratii


Fiecare nume si fiecare expresie are un tip care determina operatiile
care pot fi facute asupra lor.
O declaratie este o instructiune care introduce un nume intr-un program. O declaratie
specifica un tip pentru acel nume. Un tip defineste utilizarea numelui sau a unei
expresii. Operatiile de forma +, -, * si / se definesc pentru intregi. Dupa ce s-a inclus
stream.h, un int poate fi cel de al doilea operand pentru << cind primul argument este
ostream.
Tipul unui obiect determina nu numai care operatii pot fi aplicate asupra lui, ci de
asemenea intelesul acelor operatii. De exemplu, instructiunea:
cout << inch << "in=" << 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 constructori 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:
class circle : public shape{
int radius; public:
void draw();
void rotate(int i){}
//.......
};
Acum daca shape_vec este un vector de forme, noi putem scrie:
for(int i=0; i<no_of_shapes; i++)
shape_vec[i].rotate(45); pentru a roti (si redesena) toate formele cu 45 de grade.
Acest stil este deosebit de util in programe interactive cind obiectele de tipuri diferite
sint tratate uniform de catre softwareul de baza.

CAPITOLUL 2

DECLARATII SI CONSTANTE

2.1 Declaratii

Exemple de declaratii:
char 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 necesare 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 cantitatea 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 fundamentale. 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 declararea 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 //returneaza 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.10Referinte

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.11Registrii

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 enumerare;
(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 folosilositoare 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 instructiunile, 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(); break;
case MINUS: get_token(); //salt peste '-'
left -= term(); break;
default: return left;
}
}

Aceasta functie in realitate nu face ea insasi foarte mult. Intr-o maniera tipica pentru
functii de nivel mai inalt dintr-un program mare, ea apeleaza alte functii pentru a face
"greul". Sa observam ca o expresie de forma 2 - 3 + 4 se evalueaza ca (2 - 3) + 4, asa
cum se specifica in gramatica.
Notatia curioasa for(;;) este modul standard de a specifica un ciclu infinit. O
alternativa este while(1). Instructiunea switch se executa repetat pina cind nu se mai
gaseste + sau - si in acest caz se executa instructiunea return din default.
Operatorii += si -= se utilizeaza pentru a trata adunarea si scaderea.
left = left + term();
left = left - term();

ar putea fi utilizate fara a schimba intelesul programului. Cu toate acestea


left += term();
left -= term();

sint nu numai mai scurte, dar exprima direct operatia intentionata. Pentru un
operator binar @, o expresie x @= y inseamna x = x @ y si se aplica la operatorii
binari:

+ - * / % & | ^ << >>

asa ca sint posibili urmatorii operatori de atribuire:

= += -= *= /= %= &= |= ^= <<= >>=


Fiecare este un lexic separat, asa ca a + = 1; este o eroare din cauza spatiului dintre +
si =. (% este modulo sau restul impartirii, &, |, ^ sint operatorii logici pe biti and, or
si xor, << si >> sint deplasari stinga si dreapta).
Functiile term() si get_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, include 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 unui astfel de set. Iata
un mic exemplu imprumutat din implementarea (nu interfata utilizator) lui
<stream.h>:
enum state_value{_good = 0, _eof = 1, _fail = 2, _bad = 4 };
Definirea lui _good nu este necesara. Eu numai am dorit sa existe un nume adecvat
pentru starea in care nu sint probleme. Starea unui sir poate fi resetata astfel:
cout.state = _good;
Se poate testa daca un sir a fost deformat sau o operatie a esuat, ca mai jos:
if(cout.state & (_bad | _fail)) //nu este bine
Parantezele sint necesare deoarece & are o precedenta mai mare decit |.
O functie care intilneste sfirsitul intrarii poate sa indice acest lucru astfel: cin.state |=
_eof. 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 instructiune 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 supraincarca,
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++. Dardaca 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 #include este o facilitate extrem de simpla de manipulare a textului
sursa pentru a culege fragmente de programe sursa impreuna intr-o singura unitate
(fisier) pentru compilare. Directiva:
#include "to_be_included"
inlocuieste linia in care apare #include cu continutul fisierului "to_be_included".
Continutul ar trebui sa fie text sursa C++ intrucit compilatorul va proceda la citirea
lui. Adesea, incluziunea este gestionata printr-un program separat numit preprocesor
C, apelat de CC pentru a transforma fisierul sursa prezentat de programator intr-un
fisier fara a include directivele inainte de a incepe compilarea propriuzisa. O alta
varianta este ca, compilatorul sa gestioneze aceste directive pe masura ce ele apar in
textul sursa. Daca programatorul vrea sa vada efectul directivelor include, se 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 cart 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 inconsistenta 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 deschiderea 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 referinta 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 spree 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 improbabil 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 compararea
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 utilizeaza functia respectiva;


[2] Cauta o corespondenta utilizind conversii predefinite si utilizeaza o functie gasita
in acest fel;
[3] Cauta o corespondenta folosind conversiile definite de utilizator (&6.3) si daca
exista un set de conversii unic, se utilizeaza functia gasita. De exemplu:
overload print(double), print(int); void f()
{
print(1);
print(1.0);
}
Regula de corespondenta exacta va face ca f() sa scrie pe 1 ca un intreg, iar pe 1.0 ca
un numar flotant. Zero, char sau short sint fiecare o corespondenta exacta pentru un
argument int. Analog, un float este o corespondenta exacta pentru double.
Pentru argumentele functiilor cu nume supraincarcate, regulile de conversie standard
(&r.6.6) nu se aplica complet. Conversiile care pot distruge 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 actiunile 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 pointerilor spree 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 listeze 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 folosite 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 tipului) 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 acelasi 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 clasele 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 reprezentate ca sir
date(int); //zi, luna si anul curent
date(); //data curenta
};
Constructorii respecta aceleasi reguli pentru tipurile de argumente ca si celelalte
functii supraincarcate (&4.6.7). Atita timp cit constructorii difera suficient in tipurile
argumentelor lor compilatorul le poate selecta corect, unul pentru fiecare utilizare:
date today(4); date july4("july 4, 1983"); date guy("5 Nov");
date now; //initializare implicita
Sa observam ca functiile membru pot fi supraincarcate fara a utiliza explicit cuvintul
cheie overload. Intrucit lista completa a functiilor membru apare in declaratia de
clasa si adesea este scurta, nu exista 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 memorie 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 recuperat 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 crescatoare 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
}
Astfel de cazuri este mai bine sa fie eliminate. Utilizind constructorii nu putem
preveni utilizarea eronata a unui tok_val prin atribuirea unei valori la un tip si apoi
utilizarea ei ca fiind de alt tip. Aceasta problema poate fi rezolvata incluzind
reuniunea intr-o clasa care tine seama de tipul valorii memorate.
class tok_val{
char tag;
union{
char* p; char v[8]; long i; double d;
};
int check(char t, char* s)
{
if(tag != t)
{
error(s);
return 0;
}
return 1;
}
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 programului 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 copierea 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 consructori, 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 simboluri 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++ impreuna cu un set de operatii care pot fi efectuate cu astfel de
obiecte. Definind operatori care sa opereze asupra obiectelor unei clase, uneori se
permite unui programator sa furnizeze o notatie mai conventionala si mai
convenabila pentru a manipula obiectele unei clase, decit s-ar putea realiza utilizind
numai notatia functionala de baza. De exemplu:
class complex{
double re, im;
public: complex(double r, double i){re=r; im=i;}
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};

defineste o implementare simpla a conceptului de numere comlexe, unde un numar


este reprezentat printr-o pereche de numere flotante in dubla precizie manipulate
(exclusiv) prin operatorii + si *. Programatorul furnizeaza un inteles pentru + si *
definind functiile denumite operator+ si operator*. De exemplu, dind b si c de tip
complex, b+c inseamna (prin definitie) operator+(b, c). Este posibil acum sa se
aproximeze interpretarea conventionala a expresiilor complexe. De exemplu:
void f()
{
complex a = complex(1, 3.1);
complex b = complex(1.2, 2);
complex c = b;
a = b+c;
b = b+c*a;
c = a*b+complex(1, 2);
}

6.2 Functiile operator

Functiile care definesc intelesul pentru operatorii urmatori pot fi declarate:

+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
/= << >> >>= <<= == != <= >= &&
|| ++ -- [] () new delete
Ultimii patru sint pentru indexare (&6.7), apel de functie (&6.8), alocare de
memorie libera si dealocare de memorie libera (&3.2.6). Nu este posibil sa se
schimbe precedenta acestor operatori si nici sintaxa expresiei nu poate fi schimbata.
De exemplu, nu este posibil sa se defineasca un operator unar % sau unul binar !. Nu
este posibil sa se defineasca operatori noi, dar noi putem utiliza notatia de apel de
functie cind acest set de operatori nu este adecvat. De exemplu, vom utiliza pow() si
nu **. Aceste restrictii s-ar parea sa fie suparatoare, dar reguli mai flexibile pot foarte
usor sa conduca la ambiguitati. De exemplu, definind un operator ** care sa insemne
exponentiala, expresia a**p se poate interpreta atit ca a*(*p) cit si (a)**(p).
Numele unei functii operator este cuvintul cheie operator urmat de operatorul insusi
(de exemplu operator<<). O functie operator se declara si poate fi apelata ca orice
alta functie; utilizarea unui operator este numai o prescurtare pentru un apel explicit a
functiei operator. De exemplu:
void f(complex a, complex b)
{
complex c = a+b; //prescurtare
complex d = operator+(a, b); //apel explicit
}

6.2.1 Operatori binari si unari

Un operator binar poate fi definit sau printr-o functie membru care are un argument
sau printr-o functie prieten care are doua argumente. Astfel pentru orice operator
binar @, aa@bb poate fi interpretat sau ca aa.operator@(bb) sau ca operator@(aa,
bb). Daca ambii sint definiti, aa@bb este o eroare. Un operator unar, prefix sau postfix,
poate fi definit fie ca o functie membru fara argumente, fie ca o functie prieten cu un
argument. Astfel pentru un operator unar @, atit aa@ cit si @aa pot fi interpretate
sau ca aa.operator@() sau ca operator@(aa). Daca ambele sint definite, aa@ si @aa
sint erori. Consideram exemplele:
class X{ //prieteni
friend X operator-(X); //minus unar
friend X operator-(X,X); //minus binar
friend X operator-(); //eroare:nu exista operand
friend X operator-(X,X,X);//eroare: ternar
//membri
X* operator&(); //unar & (adresa lui)
X operator&(X); //binar & (si)
X operator&(X,X); //eroare: ternar
};
Cind sint supraincarcati operatorii ++ si --, nu este posibil sa se faca distinctie intre
aplicatia postfix si cea prefix.

6.2.2 Sensul predefinit al operatorilor

Nu se face nici o presupunere despre sensul unui operator definit de utilizator. In


particular, intrucit supraincarcarea lui = nu se presupune ca implementeaza atribuirea
la primul operand al lui; nu se face nici un test pentru a asigura ca acel operand este o
lvalue (&r6). Sensurile unor operatori predefiniti se definesc astfel incit sa fie
echivalente cu anumite combinatii de alti operatori asupra acelorasi argumente. De
exemplu, daca a este un intreg, ++a inseamna a+=1, care la rindul ei inseamna
a=a+1. Astfel de relatii nu au loc pentru operatorii definiti de utilizator, numai daca
se intimpla ca utilizatorul sa le defineasca in acel fel. De exemplu, definitia lui
operator++() pentru un tip complex nu poate fi dedusa din definitiile
complex::operator+() si complex::operator=().
Din cauza unui accident istoric, operatorii = si & au sensuri predefinite cind se aplica
la obiectele unei clase. Nu exista un mod elegant de a "nedefini" acesti doi operatori.
Ei pot totusi sa fie dezactivati pentru o clasa X. Se poate, de exemplu, declara
X::operator&() fara a furniza o definitie pentru el. Daca undeva se ia adresa unui
obiect al clasei X, linkerul va detecta o lipsa de definitie. Pe anumite sisteme,
linkerul este atit de "destept" incit el se descurca cind o fun- ctie neutilizata nu este
definita. Pe astfel de sisteme aceasta tehnica nu poate fi utilizata. O alta alternativa
este de a defini X::operator&() asa ca sa dea la executie o eroare.

6.2.3 Operatori si Tipuri definite de utilizatori

O functie operator trebuie sau sa fie un membru sau sa aiba cel putin un argument
obiect al unei clase (functiile care redefinesc operatorii new si delete nu sint
necesare). Aceasta regula asigura ca un utilizator sa nu poata schimba sensul oricarei
expresii care nu implica un tip de data definit de utilizator. In particular, nu este
posibil sa se defineasca o functie operator care sa opereze exclusiv asupra pointerilor.
O functie operator care intentioneaza sa accepte un tip de baza ca primul sau operand
nu poate fi o functie membru. De exemplu, sa consideram adaugarea unei variabile
complexe aa la intregul 2: aa+2 poate cu o functie membru corespunzatoare sa fie
interpretata ca aa.operator+(2), dar 2+aa nu poate fi, intrucit nu exista nici o clasa int
pentru care sa se defineasca + ca sa insemne 2.operator+(aa). Chiar daca ar fi, ar fi
necesare doua functii membru diferite care sa trateze 2+aa si aa+2. Deoarece
compilatorul nu cunoaste intelesul lui + definit de utilizator, el nu poate presupune ca
el este comutativ si sa interpreteze 2+aa ca aa+2. Acest exemplu se trateaza trivial
cind se utilizeaza functii friends.
Toate functiile operator sint prin definitie supraincarcate. O functie operator
furnizeaza un inteles nou pentru un operator in plus fata de definitia predefinita si pot
fi functii operator diferite cu acelasi nume atita timp cit ele difera suficient prin tipul
argumentelor lor (&4.6.7).

6.3 Conversia de tip definita de utilizator

Implementarea numerelor complexe prezentata in introducere este prea restrictiva ca


sa placa cuiva, asa ca ea trebuie extinsa. Aceasta este mai mult o repetitie triviala a
tehnicilor prezentate anterior.
class complex {
double re, im; public:
complex(double r, double i){re=r; im=i;}
friend complex operator+(complex, complex);
friend complex operator+(complex, double);
friend complex operator+(double, complex);
friend complex operator-(complex, complex);
friend complex operator-(complex, double);
friend complex operator-(double, complex);
complex operator-(); //unar -
friend complex operator*(complex, complex);
friend complex operator*(complex, double);
friend complex operator*(double, complex);
// ...
};

Acum, cu aceasta declaratie a lui complex noi putem scrie:


void f()
{
complex a(1, 1), b(2, 2), c(3, 3), d(4, 4), e(5, 5);
a = -b-c; b = c*2.0*c; c = (d+e)*a;
}

Totusi, scrierea unei functii pentru fiecare combinatie dintre complex si double ca si
pentru operator*() de mai sus, este o tendinta de nesuportat. Mai mult decit atit, o
facilitate realista pentru aritmetica complexa trebuie sa furnizeze cel putin o duzina
de astfel de functii; vezi de exemplu, tipul complex asa cum este el declarat in
<complex.h>.
6.3.1 Constructori

O varianta de a utiliza diferite functii supraincarcate este de a declara un constructor


care dindu-i-se un double creaza un complex. De exemplu:
class complex{
// ...
complex(double r)
{re=r; im=0;}
};

Un constructor care cere un singur argument nu poate fi apelat explicit:


complex z1 = complex(23);
complex z2 = 23;

Atit z1 cit si z2 vor fi initializate apelind complex(23, 0). Un constructor este o


prescriptie pentru a crea o valoare a unui tip dat. Cind se astea ta o valoare de un tip
si cind o astfel de valoare poate fi creata printr-un constructor dind valoarea de
asignat, se poate utiliza constructorul. De exemplu, clasa complex ar putea fi
declarata astfel:
class complex{
double re, im;
public: complex(double r, double i=0){re=r; im=i;} friend complex operator+
(complex, complex); friend complex operator*(complex, complex);
};

iar operatiile care implica variabilele complexe si constantele intregi vor fi legale. O
constanta intreaga va fi interpretata ca un complex cu partea imaginara zero. De
exemplu, a=b*2 inseamna:
a = operator*(b, complex(double(2), double(0)))
O conversie definita de utilizator se aplica implicit numai daca ea este unica
(&6.3.3).
Un obiect construit prin utilizarea implicita sau explicita a unui constructor este
automatic si va fi distrus la prima ocazie; de obicei imediat dupa instructiunea care l-
a creat.

6.3.2 Operatori de conversie


Utilizarea unui constructor care sa specifice conversia de tip este convenabil, dar are
implicatii care pot fi nedorite:
[1] Nu pot fi conversii implicite de la un tip definit de utilizator spre un tip de baza
(intrucit tipurile de baza nu sint clase);
[2] Nu este posibil sa se specifice o conversie de la un tip nou la unul vechi fara a
modifica declaratia pentru cel vechi.
[3] Nu este posibil sa avem un constructor cu un singur argument fara a avea de
asemenea o conversie.
Ultima restrictie se pare ca nu este o problema serioasa si primele doua probleme pot
fi acoperite definind un operator de conversie pentru tipul sursa. O functie membru
X::operatorT(), unde T este un nume de tip, defineste o conversie de la X la T. De
exemplu, se poate defini un tip tiny care are valori in dome- niul 0..63, dar care se
poate utiliza combinat cu intregi in operatiile aritmetice:
class tiny{
char v;
int assign(int i)
{return v=(i&~63) ? (error("range error"),0):i;}
public:
tiny(int i){ assign(i); }
tiny(tiny& t){ v=t.v; }
int operator=(tiny& t){ return v=t.v; }
int operator=(int i){ return assign(i); }
operator int(){ return v; }
};

Domeniul este verificat ori de cite ori este initializat un tiny printr-un int si ori de cite
ori un int este asignat la un tiny. Un tiny poate fi asignat la un altul fara a verifica
domeniul. Pentru a permite operatiile uzuale cu intregi asupra variabilelor tiny,
tiny::operator int(), defineste conversii implicite de la tiny spre int. Ori de cite ori
apare un tiny unde este necesar un int se utilizeaza int-ul potrivit. De exemplu:
void main(void)
{
tiny c1 = 2;
tiny c2 = 62;
tiny c3 = c2-c1; //c3=60
tiny c4 = c3; //nu se face verificarea domeniului
int i = c1+c2; //i=64
c1 = c2+2*c1; //eroare de domeniu c1=0 (nu 66)
c2 = c1-i; //eroare de domeniu: c2=0
c3 = c2; //nu se face verificare(nu este necesar)
}

Un vector de tip tiny pare sa fie mai util intrucit el de asemenea salveaza spatiu;
operatorul de indexare [] poate fi folosit sa faca, ca un astfel de tip sa fie util. O alta
utilizare a operatorilor de conversie definiti de utilizator sint tipurile ce furnizeaza
reprezentari nestandard de numere (aritmetica in baza 100, aritmetica in virgula fixa,
reprezentare BCD, etc.);acestea de obicei vor implica redefinirea
operatorilor + si *.
Functiile de conversie par sa fie mai utile mai ales pentru tratarea structurilor de date
cind citirea este triviala (implementate printr-un operator de conversie), in timp ce
atribuirea si initializarea sint mai putin triviale.
Tipurile istream si ostream sint legate de o functie de conversie care sa faca posibile
instructiuni de forma:
while(cin >> x)
cout << x;
Operatia de intrare de mai sus returneaza un istream&. Aceasta valoare se
converteste implicit spre o valoare care indica starea lui cin si apoi aceasta valoare
poate fi testata de while (&8.4.2). Totusi, nu este o idee buna sa se defineasca o
conversie implicita de la un tip la altul astfel incit sa se piarda informatie prin
conversie.

6.3.3 Ambiguitati

asignare (sau initializare) la un obiect al unei clase X este legala daca, sau valoarea
care se asigneaza este un X sau exista o conversie unica a valorii asignate spre tipul
X.
Intr-un astfel de caz, o valoare a tipului cerut poate fi construita prin utilizarea
repetata a constructorilor sau a operatorilor de conversie.Aceasta trebuie sa fie tratata
printr-o utilizare explicita; numai un nivel de conversie implicita definita de utilizator
este legal! In anumite cazuri, o valoare a tipului cerut poate fi construita in mai mult
decit un mod. Astfel de cazuri sint ilegale. De exemplu:
class x{/*...*/ x(int); x(char*);}; class y{/*...*/ y(int);}; class z{/*...*/ z(x);};
overload f;
x f(x);
y f(y);
z g(z);
f(1); //ilegal: este ambiguu f(x(1)) sau f(y(1))f(x(1)); f(y(1)); g("asdf");
//ilegal: g(z(x("asdf"))) g(z("asdf"));
Conversiile definite de utilizator sint considerate numai daca un apel nu se rezolva
fara ele. De exemplu:
class x{
/*...*/
x(int);
};

overload h(double), h(x);


h(1);

Apelul ar putea fi interpretat ca h(double(1)) sau h(x(1)) si va apare ilegal potrivit


regulii de unicitate. Cu toate acestea, prima interpretare utilizeaza numai o conversie
standard si va fi aleasa conform regulii prezentate in &4.6.7.
Regulile pentru conversie nu sint nici cel mai simplu de implementat si nici cel mai
simplu de documentat. Sa consideram cerinta ca o conversie trebuie sa fie unica
pentru a fi legala. O conceptie mai simpla ar admite ca,compilatorul sa utilizeze orice
conversie pe care el o poate gasi; astfel nu ar fi necesar sa consideram toate
conversiile posibile inainte de a declara o expresie legala. Din nefericire, aceasta ar
insemna ca sensul unui program depinde de conversiile care au fost gasite. De fapt,
sensul unui program ar fi intr-un anumit mod dependent de ordinea declararii
conversiilor. Intrucit acestea adesea vor rezida in fisiere sursa diferite (scrise de
diferiti programatori), sensul unui program ar depinde de ordinea in care partile lui
s-ar interclasa. Alternativ, conversiile implicite ar fi nepermise. Nimic nu ar putea fi
mai simplu, dar aceasta regula conduce sau la o utilizare neeleganta a interfetelor sau
la o explozie a functiilor supraincarcate asa cum se vede in clasa complex din
sectiunea precedenta. O conceptie mai generala ar fi luarea in considerare a intregii
informatii de tip disponibile si considerarea tuturor conversiilor posibile. De
exemplu, utilizind declaratiile precedente, aa=f(1) ar putea fi tratata din cauza ca
tipul lui aa determina o interpretare unica. Daca aa este un x, f(x(1)) este singurul
care produce pe x necesar in asignari; daca aa este un y, va fi folosit in schimb
f(y(1)). Cea mai generala conceptie ar acoperi de asemenea pe g("asdf") deoarece
g(z(x("asdf"))) este o interpretare unica.
Problema cu aceasta conceptie este ca ea cere o analiza extensiva a unei expresii
complete pentru a determina interpretarea fiecarui operator si apel de functie.
Aceasta conduce spre o compilare inceata si de asemenea spre interpretari si mesaje
de eroare surprinzatoare deoarece compilatorul considera conversiile definite in
biblioteci, etc. Cu aceasta conceptie compilatorul tine seama de mai multa informatie
decit se asteapta programatorul ca sa cunoasca.

6.4 Constante
Nu este posibil sa se defineasca constante de tip clasa in sensul ca 1.2 si 12e3 sint
constante de tip double. Totusi constantele de tip predefinit pot fi utilizate daca in
schimb, functiile membru ale unei clase se utilizeaza ca sa furnizeze o interpretare
pentru ele. Constructorii care au un singur argument furnizeaza un mecanism general
pentru acest lucru. Cind constructorii sint simpli si se substituie inline, este cit se
poate de rezonabil sa interpretam apelurile constructorului ca si constante. De
exemplu, dindu-se declaratia de clasa complex in <complex.h>, expresia
zz1*3+zz2*complex(1,2) va apela doua functii si nu cinci. Cele doua operatii * vor
apela functii, dar operatia + si constructorul apelat pentru a crea complex(3) si
complex(1,2) vor fi expandate inline.

6.5 Obiecte mari

Pentru orice utilizare a unui operator binar complex declarat in prealabil, se transfera
o copie a fiecarui operand la funlctia care implementeaza operatorul. Pentru copierea
a doua double acest lucru este acceptabil. Din nefericire, nu toate clasele au o
reprezentare convenabil de mica. Pentru a elimina copierea excesiva, se pot declara
functii care sa aiba ca argumente referinte. De exemplu:
class matrix{
double m[4][4]; public:
matrix();
friend matrix operator+(matrix&, matrix&);
friend matrix operator*(matrix&, matrix&);
};

Referintele permit utilizarea expresiilor care implica operatori aritmetici uzuali


pentru obiecte mari fara a face copieri excesive. Pointerii nu pot fi utilizati deoarece
nu este posibil sa se redefineasca sensul unui operator cind el se aplica la un pointer.
Operatorul + ar putea fi definit astfel:
matrix operator+(matrix& arg1, matrix& arg2)
{
matrix sum;
for(int i=0; i<4; i++)
for(int j=0; j<4; j++)
sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j]; return sum;
}

Acest operator+() are acces la operanzii lui + prin referinte, dar returneaza o valoare
obiect. Returnarea unei referinte pare sa fie mai eficienta:
class matrix{
//...
friend matrix& operator+(matrix&, matrix&);
friend matrix& operator*(matrix&, matrix&);
};

Aceasta este legal, dar provoaca probleme de alocare a memoriei. Intrucit o referinta
la rezultat va iesi in afara functiei ca referinta la valoarea returnata, ea nu poate fi
variabila automatica. Intrucit un operator este adesea utilizat mai mult decit o data
intr-o expresie rezultatul el nu poate fi o variabila locala statica. Ea va fi alocata de
obicei in memoria libera. Copierea valorii returnate este adesea mai ieftina (in
executie de timp, spatiu de cod si spatiu de data ) si mai simplu de programat.

6.6 Asignare si Initializare

Sa consideram o clasa sir foarte simpla:


struct string{
char* p;
int size; //vectorul spre care pointeaza p
string(int sz){ p = new char[size=sz]; }
~string(){ delete p; }
};

Un sir este o data structurata care consta dintr-un pointer spre un vector de caractere
si din dimensiunea acelui vector. Vectorul este creat printr-un constructor si sters
printr-un destructor. Cu toate acestea, asa cum se arata in &5.10 aceasta poate sa
creeze probleme. De exemplu:
void f()
{
string s1(10);
string s2(20);
s1=s2;
}
va aloca doi vectori de caractere, dar asignarea s1=s2 va distruge pointerul spre unul
din ei si va duplica pe celalalt. Destructorul va fi apelat pentru s1 si s2 la iesirea din
f() si atunci va sterge acelasi vector de doua ori cu rezultate dezastruoase. Solutia la
aceasta problema este de a defini asignarea de obiecte in mod corespunzator.
struct string{
char* p; int size; //vectorul spre care pointeaza p string(int sz){ p = new
char[size=sz]; } ~string(){ delete p; } void operator=(string&);
};
void string::operator=(string& a)
{
if(this == &a)
return; //a se avea grija de s=s; delete p; p = new char[size=a.size]; strcpy(p,
a.p);
}

Aceasta definitie a lui string va asigura ca exemplul precedent sa functioneze asa


cum s-a intentionat. Cu toate acestea, o mica modificare a lui f() va face ca problema
sa reapara intr-o forma diferita.
void f()
{
string s1(10);
string s2 = s1;
}

Acum numai un sir este construit, iar doua sint distruse. Un operator de asignare
definit de utilizator nu poate fi aplicat la un obiect neinitializat.
O privire rapida la string::operator=() arata de ce acesta este nerezonabil:
pointerul p ar contine o valoare aleatoare nedefinita. Un operator de atribuire adesea
se bazeaza pe faptul ca argumentele lui sint initializate. Pentru o initializare ca cea
precedenta, aceasta prin definitie nu este asa. In consecinta, trebuie sa se defineasca o
functie care sa se ocupe cu initializarea:
struct string{
char* p;
int size;
string(int sz){ p = new char[size=sz]; }
~string(){ delete p; }
void operator=(string&);
string(string&);
};
void string::string(string& a)
{
p = new char[size=a.size];
strcpy(p, a.p);
}

Pentru un tip X, constructorul X(X&) are grija de initializare pentru un obiect de


acelasi tip cu X. Nu se poate suprautiliza caci asignarea si initializarea sint operatii
diferite. Aceast lucru este important mai ales atunci cind se declara un destructor.
Daca o clasa X are un destructor care realizeaza o sarcina netriviala, cum ar fi
dealocare in memoria libera, este foarte probabil ca el necesita complementul
complet al functiilor pentru eliminarea completa a copierii pe biti a obiectelor:
class X{
// ...
X(something); //constructor: creaza obiecte
X(X&); //constructor: copiere in initializare
operator=(X&);//atribuire: stergere si copiere
~X(); //destructor: stergere
};

Exista inca doua cazuri cind se copiaza un obiect: ca un argument de functie si ca o


valoare returnata de functie. Cind un argument este pasat, o variabila neinitializata
numita argument formal se initializeaza. Semantica este identica cu cea a altor
initializari. Acelasi lucru este valabil pentru functii cu return, desi acestea sint mai
putin evidente. In ambele cazuri, X(X&) va fi aplicat daca este definit:
string g(string arg){ return arg; } main()
{
string s = "asdf";
s = g(s);
}

Evident, valoarea lui s se cade sa fie "asdf" dupa apelul lui g(). Luarea unei copii a
valorii lui s in argumentul arg nu este dificil; se face un apel a lui string(string&).
Luarea unei copii a acestei valori ca iesire a lui g() face un alt apel la string(string&);
de data aceasta, variabila initializata este una temporara, care apoi este atribuita lui s.
Aceasta variabila temporara este desigur distrusa folosind cit de repede posibil
string::~string().

6.7 Indexare

O functie operator[] poate fi utilizata pentru a da indicilor un inteles pentru obiectele


unei clase. Argumentul al doilea (indicele) al unei functii operator[] poate fi de orice
tip. Aceasta face posibil sa se defineasca tablouri asociative, etc.. Ca un exemplu, sa
recodificam exemplul din &2.3.10 in care un tablou asociativ se foloseste pentru a
scrie un program mic pentru calculul numarului de aparitii al cuvintelor dintr-un
fisier. Aici se defineste un tip de tablou asociativ:
struct pair{ char* name; int val; };
class assoc{
pair* vec; int max; int free; public: assoc(int);
int& operator[](char*); void print_all();
};
Un assoc pastreaza un vector de perechi de dimensiune maxima. Indexul primului
element al vectorului neutilizat este pastrat in free. Constructorul arata astfel:
assoc::assoc(int s)
{
max = (s<16) ? s : 16;
free = 0;
vec = new pair[max];
}

Implementarea utilizeaza aceeasi metoda ineficienta ca si cea utilizata in &2.3.10. Cu


toate acestea, un assoc poate creste usor cind se produce depasire:
#include <string.h>
int& assoc::operator[](char* p)
/* mentine un set de perechi "pair"; cautarea sirului spre care pointeaza p,
returneaza o referinta spre partea intreaga a lui "pair" si se construieste o pereche
"pair" noua daca "p" nu a fost gasit */
{register pair* pp;
for(pp=&vec[free-1]; vec<=pp; pp--) if(strcmp(p, pp->name) == 0)
return pp->val;
if(free == max)
{ //depasire: se creste vectorul
pair* nvec = new pair[max*2]; for(int i=0; i<max; i++)
nvec[i] = vec[i];
delete vec; vec = nvec; max = 2*max;
}
pp = &vec[free++]; pp->name = new char[strlen(p)+1]; strcpy(pp->name, p); pp-
>val = 0; //valoare initiala: 0 return pp->val;
}

Intrucit reprezentarea unui assoc este ascunsa, noi avem nevoie de o cale de al afisa.
Sectiunea urmatoare va arata cum poate fi definit un iterator propriu. Aici noi vom
utiliza o functie simpla de imprimare:
void assoc::print_all()
{for(int i=0; i<free; i++)
cout << vec[i].name << ":" << vec[i].val << "\n";
}
In final putem scrie programul principal:
main() //numara aparitiile fiecarui cuvint de la intrare
{const MAX = 256; //mai mare decit cel mai mare cuvint
char buff[MAX]; assoc vec(512); while(cin>>buf)
vec[buff]++; vec.print_all();
}

6.8 Apelul unei functii

Apelul unei functii, adica notatia expresie(lista_de_expr.), poate fi interpretat ca o


operatie binara iar operatorul de apel () poate fi incarcat intr-un anumit fel ca si
ceilalti operatori. Lista argument pentru o functie operator() se evalueaza si se
verifica potrivit regulilor de pasare al argumentelor obisnuite. Supraincarcarea
apelului de functie pare sa fie utila in primul rind pentru a defini tipurile cu numai o
singura operatie. Noi nu am definit un iterator pentru tabloul asociativ de tip assoc.
Aceasta s-ar putea face definind o clasa assoc_iterator cu sarcina de a prezenta
elementele dintr-un assoc intr-o anumita ordine. Iteratorul necesita acces la datele
memorate intr-un assoc si de aceea este facut un friend:
class assoc{
friend class assoc_iterator; pair* vec; int max; int free;
public: assoc(int); int& operator[](char*);
};

Iteratorul poate fi definit astfel:


class assoc_iterator{
assoc* cs; //tabloul assoc curent
int i; //index curent
public: assoc_iterator(assoc& s){ cs=&s; i=0; } pair* operator()()
{return (i<cs->free)? &cs->vec[i++]:0;}
};

Un assoc_iterator trebuie sa fie initializat pentru un tablou assoc si va returna un


pointer spre o pereche noua pair a acelui tablou de fiecare data cind este activat
utilizind operatorul ().
Cind se ajunge la sfirsitul tabloului se returneaza 0:
main() //numara aparitiile fiecarui cuvint de la intrare
{
const MAX = 256; //mai mare decit cel mai mare cuvint
char buff[MAX]; assoc vec(512); while(cin >> buff)
vec[buff]++; assoc_iterator next(vec); pair* p; while(p = next())
cout << p->name << ":" << p->val << "\n";
}

Un tip iterator de acest fel are avantajul fata de un set de functii care fac acelasi
lucru: el are datele private propri pentru a tine seama de iteratie. Este de asemenea
important ca multe iteratii de un astfel de tip sa poata fi activate simultan.
Evident, aceasta utilizare a obiectelor pentru a reprezenta iteratii nu are nimic de a
face cu supraincarcarea operatorului. La multi le plac iteratorii cu operatii de forma
first(), next() si last().

6.9 O clasa sir

Iata o versiune mai realista a clasei sir. Ea calculeaza referintele la un sir pentru a
minimiza copierea si utilizeaza sirurile de caractere standard din C++ ca si constante.
#include <iostream.h>
#include <string.h>
#include <process.h>
class string{
struct srep{
char* s; //pointer spre data
int n; //numarul de referinte
};
srep* p;
public:
string(char*); //string x = "abc";
string(); //string x;
string(string&); //string x = string...
string& operator=(char*);
string& operator=(string&);
~string();
char& operator[](int i); friend ostream& operator<<(ostream&, string&); friend
istream& operator<<(istream&, string&); friend int operator==(string& x, char* s)
{return strcmp(x.p->s, s) == 0;} friend int operator==(string& x, string& y)
{return strcmp(x.p->s, y.p->s) == 0;} friend int operator!=(string& x, char* s)
{return strcmp(x.p->s, s) != 0;} friend int operator!=(string& x, string& y)
{return strcmp(x.p->s, y.p->s) != 0;}
};

Constructorii si destructorii sint ca de obicei triviali:


string::string()
{p = new srep;
p->s = 0;
p->n = 1;
}

string::string(char* s)
{
p = new srep;
p->s = new char[strlen(s)+1];
strcpy(p->s, s);
p->n = 1;
}

string::string(string& x)
{
x.p->n++;
p = x.p;
}

string::~string()
{
if(--p->n == 0)
{
delete p->s;
delete p;
}
}

De obicei, operatorii de asignare sint similari cu constructorii. Ei trebuie sa stearga


primul operand sting al lor:
string& string::operator=(char* s)
{
if(p->n > 1)
{ //deconectare p->n--; p = new srep;
}
else if(p->n == 1)
delete p->s;
p->s = new char[strlen(s)+1]; strcpy(p->s, s); p->n = 1; return *this;
}

Este recomandabil sa ne asiguram ca asignarea unui obiect la el insusi


lucreaza corect:
string& string::operator=(string& x)
{
x.p->n++;
if(--p->n == 0)
{
delete p->s;
delete p;
}
p = x.p;
return *this;
}

Operatorul de iesire este pus cu intentia de a demonstra utilizarea numaratorului de


referinte. El face ecou pe fiecare sir de intrare (utilizind operatorul <<, definit mai
jos):
ostream& operator<<(ostream& s, string& x)
{
return s << x.p->s << "[" << x.p->n << "]\n";
}

Operatorul de intrare utilizeaza functia de intrare standard a sirurilor de caractere


(&8.4.1):
istream& operator>>(istream& s, string& x)
{
char buf[256];
s >> buf;
x = buf;
cout << "echo: " << x << "\n";
return s;
}

Operatorul de indexare este furnizat pentru acces la caractere individuale. Indexul


este verificat:
void error(char* p)
{
cerr << p << "\n";
exit(1);
}
char& string::operator[](int i)
{
if(i<0 || strlen(p->s)<i)
error("index out of range"); return p->s[i];
}

Programul principal pur si simplu exerseaza operatorii string. El continua sa faca


acest lucru pina cind este recunoscut sirul, executa string pentru a salva cuvinte in el
si se opreste cind gaseste sfirsitul de fisier. Apoi imprima toate sirurile in ordine
inversa.
main()
{
string x[100];
int n;
cout << "here we go\n";
for(n=0; cin>>x[n]; n++)
{
string y;
if(n==100)
error("too many strings"); cout << (y = x[n]); if(y == "done")
break;
}
cout << "here we go back again\n"; for(int i=n-1; 0<=i; i--) cout << x[i];
}

6.10 Prieteni si Membri

In final, este posibil sa discutam cind sa utilizam membri si cind sa utilizam prieteni
pentru a avea acces la partea privata a unui tip definit de utilizator. Anumite operatii
trebuie sa fie membri: constructori, destructori si functii virtuale (vezi capitolul
urmator).
class X{
//...
X(int);
int m();
friend int f(X&);
};

La prima vedere nu exista nici un motiv de a alege un friend f(X&) in locul unui
membru X::m() (sau invers) pentru a implementa o operatie asupra unui obiect al
clasei X. Cu toate acestea, membrul X::m() poate fi invocat numai pentru un "obiect
real", in timp ce friend f(X&) ar putea fi apelat pentru un obiect creat printr-o
conversie implicita de tip. De exemplu:
void g()
{
1.m(); //eroare
f(1); //f(X(1));
}

O operatie care modifica starea unui obiect clasa ar trebui de aceea sa fie un membru
si nu un prieten. Operatorii care cer operanzi lvalue pentru tipurile fundamentale (=,
*=, ++, etc) sint definiti mai natural ca membri pentru tipuri definite de utilizator.
Dimpotriva, daca se cere conversie implicita de tip pentru toti operanzii unei operatii,
functia care o implementeaza trebuie sa fie un prieten si nu un membru. Acesta este
adesea cazul pentru functii care implementeaza operatori ce nu necesita ope- ranzi
lvalue cind se aplica la tipurile fundamentale (+, -, ||, etc.)
Daca nu sint definite tipuri de conversii, pare ca nu sint motive de a alege un
membru in schimbul unui prieten care sa aiba un argument referinta sau invers.In
anumite cazuri programatorul poate avea o preferinta pentru sintaxa unui apel. De
exemplu, multa lume se pare ca prefera notatia inv(m) pentru a inversa o matrice m,
in locul alternativei m.inv(). Evident, daca inv() inverseaza matricea m si pur si
simplu nu returneaza o matrice noua care sa fie inversa lui m, atunci ea trebuie sa fie
un membru.
Toate celelalte lucruri se considera indreptatite sa aleaga un membru: nu este posibil
sa se stie daca cineva intr-o zi va defini un operator de conversie. Nu este totdeauna
posibil sa se prezica daca o modificare viitoare poate cere modificari in starea
obiectului implicat. Sintaxa de apel a functiei membru face mai clar utilizatorului
faptul ca obiectul poate fi modificat; un argument referinta este pe departe mai putin
evident. Mai mult decit atit, expresiile dintr-un membru pot fi mai scurte decit
expresiile lor echivalente dintr-o functie prieten. Functia prieten trebuie sa utilizeze
un argument explicit in timp ce membrul il poate utiliza pe acesta implicit. Daca nu
se foloseste supraincarcarea, numele membrilor tind sa fie mai scurte decit numele
prietenilor.
6.11 Goluri

Ca majoritatea caracteristicilor limbajelor de programare, supraincarcarea


operatorului poate fi utilizata atit bine cit si eronat. In particular, abilitatea de a
defini sensuri noi pentru operatorii vechi poate fi utilizata pentru a scrie programe
care sint incomprehensibile. Sa ne imaginam de exemplu fata unui citiltor al unui
program in care operatorul + a fost facut sa noteze operatia de scadere.
Mecanismul prezentat aici ar trebui sa protejeze programatorul/cititorul de excesele
rele de supraincarcare prevenind pro- gramatorul de schimbarea sensului operatorilor
pentru tipurile de date de baza cum este int prin conservarea sintaxei expresiilor si al
operatorilor de precedenta. Probabil ca este util sa utilizam intii supraincarcarea
operatorilor pentru a mima utilizarea conventionala a operatorilor. Se poate utiliza
notatia de apel de functie cind o astfel de utilizare conventionala a opera- torilor nu
este stabilita sau cind setul de operatori disponibil pentru supraincarcare in C++ nu
este adecvat pentru a mima utili- zarea conventionala.

6.12 Exercitii

1. (*2). Sa se defineasca un iterator pentru clasa string. Sa se defineasca un


operator de concatenare + si un operator += de "adaugare la sfirsit". Ce alte operatii
a-ti dori sa aveti asupra sirurilor?
2. (*1.5). Sa se furnizeze un operator subsir pentru clasa string prin
supraincarcarea lui ().
3. (*3). Sa se proiecteze clasa string asa ca operatorul subsir sa fie folosit in
partea stinga a unei asignari. Intii sa se scrie o versiune in care un sir poate fi atribuit
la un subsir de aceeasi lungime, apoi o versiune in care lungimile pot fi diferite.
4. (*2). Sa se proiecteze o clasa string asa ca ea sa aiba o valoare semantica
pentru atribuire, transferul parametrilor, etc.; adica, cind se copiaza reprezentarea
sirului si nu structura de control a datei din clasa string.
5. (*3). Sa se modifice clasa string din exemplul precedent pentru a copia siruri
numai cind este necesar. Astfel, sa se pastreze o reprezentare comuna a doua siruri
pina cind unul din siruri se modifica. Nu incercati sa aveti un operator de subsir care
poate fi utilizat in partea stinga in acelasi timp.
6. (*4). Sa se proiecteze o clasa string cu valoarea semantica delayed copy si un
operator subsir care poate fi utilizat in partea stinga.
7. (*2). In programul urmator ce conversii se utilizeaza in fiecare expresie?
struct X{
int i;
X(int); operator+(int);
};
struct Y{
int i;
Y(X);
operator+(X);
operator int();
};
X operator* (X, Y);
int f(X);
X x=1;
Y y=x ;
int i=2;

main()
{
i+10;
y+10;
y+10*y;
x+y+i;
x*x+i;
f(7);
f(y);
y+y;
106+y;
}

Sa se defineasca atit X cit si Y de tip intreg. Sa se modifice programul asa ca el sa se


execute si sa imprime valorile fiecarei expresii legale.
8. (*2). Sa se defineasca o clasa INT care se comporta ca un int. Indicatie: sa se
defineasca INT::operator int().
9. (*1). Sa se defineasca o clasa RINT care se comporta ca un int exceptind
faptul ca singurele operatii admise sint + (unar sau binar), - (unar sau binar), *, /, %.
Indicatie: sa nu se defineasca RINT::operator int().
10. (*3). Sa se defineasca o clasa LINT care se comporta ca un RINT exceptind
faptul ca ea are cel putin o precizie de 64 biti.
11. (*4). Sa se defineasca o clasa care implementeaza o aritmetica de precizie
arbitrara. Indicatie: va fi necesar sa se gestioneze memoria intr-un mod similar cu cel
facut pentru clasa string.
12. (*2). Sa se scrie un program care sa nu fie citibil prin utilizarea operatorului de
supraincarcare si a macrourilor. O idee: sa se defineasca + ca sa insemne - si
viceversa pentru INT; apoi sa se utilizeze un macro pentru a defini int care sa
insemne INT. Sa se redefineasca functii populare, utilizind argumente de tip referinta
si citeva comentarii eronate pentru a crea o confuzie mare.
13. (*3). Sa se permute rezultatul exercitiului precedent cu un friend. Sa se indice
fara a rula ce face programul cu friend. Cind veti termina acest exercitiu ve-ti sti ce
trebuie sa eliminati.
14. (*2). Sa se rescrie exemplul complex (&6.3.1), exemplul tiny (&6.3.2) si
exemplul string (&6.9) fara a utiliza functiile friend. Sa se utilizeze numai functiile
membru. Sa se testeze fiecare din versiunile noi. Sa se compare cu versiunile care
utilizeaza functiile friend. Sa se rescrie exercitiul 5.3.
15. (*2). Sa se defineasca un tip vec4 ca un vector de 4 flotante. Sa se defineasca
operatorul [] pentru vec4. Sa se defineasca operatorii +, -, *, /, =, +=, -=, *=, /=
pentru combinatii de vectori de numere flotante.
16. (*3). Sa se defineasca o clasa mat4 ca un vector de 4 vec4. Sa se defineasca
operatorul [] care returneaza un vec4 pentru mat4. Sa se defineasca operatiile uzuale
cu matrici pentru acest tip. Sa se defineasca o functie care face o eliminare Gauss
pentru mat4.
17. (*2). Sa se defineasca o clasa vector similara cu vec4, dar cu dimensiunea data
ca un argument pentru constructorul vector::vector(int).
18. (*3). Sa se defineasca o clasa matrix similara cu mat4, dar cu dimensiunile
date ca argumente la constructorul matrix::matrix(int, int).
CAPITOLUL 7

CLASE DERIVATE

Acest capitol descrie conceptul de clasa derivata din C++. Clasele derivate
furnizeaza un mecanism simplu, flexibil si eficient, pentru a specifica o interfata
alternativa pentru o clasa si pentru a defini o clasa adaugind facilitati la o clasa
existenta fara a reprograma sau recompila. Utilizind clasele derivate, se poate furniza
de asemenea, o interfata comuna pentru diferite clase asa ca obiectele acelor clase sa
poata fi manipulate identic in alte parti ale unui program. Aceasta de obicei implica
plasarea informatiilor de tip in fiecare obiect asa ca astfel de obiecte sa poata fi
utilizate corespunzator in contextele in care tipul nu poate fi cunoscut la compilare;
se da con- ceptul de functie virtuala pentru a trata astfel de dependente de tip precaut
si elegant. In principiu, clasele derivate exista pentru a face mai usor unui
programator sa exprime partile comune.

7.1 Introducere

Consideram scrierea unor facilitati generale (de exemplu o lista inlantuita, o tabela de
simboluri, un sistem de simulare) in intentia de a fi utilizate de multa lume in
contexte diferite. Evident nu sint putini candidati pentru astfel de beneficii de a le
avea standardizate. Fiecare programator experimentat se pare ca a scris (si a testat)
o duzina de variante pentru tipurile multime, tabela de hashing, functii de sortare,
etc., dar fiecare programator si fiecare program pare ca are o versiune separata a
acestor concepte, facind programul greu de citit, greu de verificat si greu de
schimbat. Mai mult decit atit, intr-un program mare ar putea foarte bine sa fie copii
de coduri identice (sau aproape identice) pentru a trata astfel de concepte de baza.
Motivatia pentru acest haos este in parte faptul ca conceptual este dificil sa se
prezinte facilitati atit de generale intr-un limbaj de programare si partial din cauza ca
facilitatile de generalitate mare de obicei impun depasiri de spatiu si/sau timp, ceea
ce le face nepotrivite pentru cele mai simple facilitati utilizate (liste inlantuite,
vectori, etc.) unde ele ar trebui sa fie cele mai utile. Conceptul C++ de clasa derivata,
prezentat in &7.2 nu furnizeaza o solutie generala pentru toate aceste probleme, dar
furnizeaza un mod de a invinge unele cazuri speciale importante. De exemplu, se va
arata cum se defineste o clasa de liste inlantuite generica si eficienta, asa ca toate
versiunile ei sa aiba cod comun. Scrierea facilitatilor de uz general nu este triviala,
iar aspectele proiectarii este adesea ceva diferit de aspectele proiectarii unui program
cu scop special. Evident, nu exista o linie bine definita care sa faca distinctie intre
facilitatile cu scop general si cele cu scop special, iar tehnicile si facilitatile
limbajului prezentat in acest capitol pot fi vazute ca fiind din ce in ce mai utile pe
masura ce dimensiunea si complexitatea programului creste.

7.2 Clase derivate

Pentru a separa problemele de intelegere a mecanismelor limbajului si tehnicile


pentru a le utiliza, conceptul de clasa derivata se introduce in trei stadii. Intii,
facilitatile limbajului (notatia si semantica se vor descrie folosind exemple mici care
nu intentioneaza sa fie reale). Dupa aceasta, se demonstreaza niste clase derivate
netriviale si in final se prezinta un program complet.

7.2.1 Derivare

Consideram construirea unui program care se ocupa cu angajatii unei firme. Un astfel
de program ar putea avea o structura de felul:
struct employee{
char* name; short age; short departament; int salary; employee* next;
//.......
};
Cimpul next este o legatura intr-o lista pentru date employee similare. Acum vrem sa
definim structura manager:
struct manager{
employee emp; //angajatii manager employee* group;
//...
};
Un manager este de asemenea un angajat (employee); datele angajatului se
memoreaza in emp care este un membru al obiectului manager. Aceasta poate fi
evident pentru un cititor uman, dar nu exista nimic care sa distinga membri emp. Un
pointer spre un ma- nager (manager*) nu este un pointer spre un employee
(employee*), asa ca nu se pot utiliza unul in locul celuilalt. In particular, nu se poate
pune un manager intr-o lista de angajati fara a scrie cod special. Se poate sau utiliza
tipul de conversie explicit spre manager* sau sa se puna adresa membrului emp intr-
o lista de angajati, dar ambele sint neelegante si pot fi obscure. Conceptia corecta
este de a afirma ca un manager este un employee cu citeva informatii adaugate:
struct manager : employee{employee* group;
//.......
};
Manager este derivat din employee si invers, employee este o clasa de baza pentru
manager. Clasa manager are membri clasei employee (name, age, etc.) in plus fata
de membrul group.
Cu aceasta definitie a lui employee si manager, noi putem crea acum o lista de
employee, din care unii sint manageri. De exemplu:
void f()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; //se pune m1, e1, m2, e2 in lista
m1.next = &e1; e1.next = &m2; m2.next = &e2; e2.next = 0;
}

Intrucit un manager este un employee, un manager* poate fi utilizat ca un


employee*. Dar un employee nu este in mod necesar un manager, asa ca un
employee* nu poate fi utilizat ca un mana- ger*. Aceasta se explica in detaliu in
&7.2.4.

7.2.2. Functii membru

Structurile de date simple, cum ar fi employee si manager, sint in realitate


neinteresante si adesea nu sint utile in mod special, asa ca, sa consideram adaugarea
de functii la ele. De exemplu:
class employee{
char* name;
//......
public:
employee* next; void print();
//......
};
class manager : public employee{
//......
public:
void print();
//......
};

Trebuie sa se raspunda la niste intrebari. Cum poate o functie membru al clasei


derivate manager sa utilizeze membri clasei de baza employee ? Ce membri ai clasei
de baza employee poate utiliza o functie nemembru dintr-un obiect de tip manager ?
In ce mod poate afecta programatorul raspunsul la aceste probleme ?
Consideram:
void manager::print(){ cout << "name is:" << name << "\n"; }
Un membru al unei clase derivate poate utiliza un nume public al clasei de baza
propri in acelasi mod ca si alti membri, adica fara a specifica un obiect. Se presupune
obiectul spre care pointeaza this, asa ca numele (corect) se refera la this->name. Cu
toate acestea, functia manager::print() nu se va compila; un membru al clasei derivate
nu are permisiunea speciala de a face acces la un membru privat din clasa lui de
baza, asa ca functia nu are acces la name.
Aceasta este o surpriza pentru multi, dar sa consideram varianta ca o functie membru
ar putea face acces la membri privati ai clasei sale de baza. Conceptul de membru
privat ar deveni lipsit de sens prin facilitatea care ar permite unui programator sa
cistige acces la partea privata a unei clase pur si simplu prin derivarea unei clase noi
din ea. Mai mult decit atit, s-ar putea sa nu se mai gaseasca toti utilizatorii unui nume
privat uitindu-ne la functiile declarate ca membri si prieteni ai acelei clase. Ar trebui
sa se examineze fiecare fisier sursa al programului complet pentru clase derivate,
apoi sa se examineze fiecare functie din acele clase, apoi sa se gaseasca fiecare clasa
derivata din aceste clase, etc.. Aceasta este impractic.
Pe de alta parte, este posibil sa se utilizeze mecanismul friend pentru a admite astfel
de accese pentru functii specifice sau pentru orice funcie a unei clase specifice (asa
cum s-a des- cris in &5.3). De exemplu:
class employee{
friend void manager::print();
//.......
};

ar rezolva problema pentru manager::print(), iar clasa:


class employee{
friend class manager;
//.......
};

ar face ca orice membru al clasei employee sa fie accesibil pentru orice functie din
clasa manager. In particular, se face ca name sa fie accesibil pentru
manager::print().
O alta alternativa, uneori mai clara, este ca clasa derivata sa utilizeze numai membri
publici ai clasei de baza propri. De exemplu:
void manager::print()
{
employee::print(); //imprima informatie employee
//........ //imprima informatie manager
}

Sa observam ca operatorul :: trebuie utilizat deoarece fun- ctia print() a fost redefinita
in manager. O astfel de reutilizare a unui nume este tipica. Un neprecaut ar putea
scrie:
void manager::print()
{
print(); //imprima informatie employee
//........ //imprima informatie manager
}

si ar gasi ca programul este o secventa nedorita de apeluri recursive cind se apeleaza


manager::print().

7.2.3 Vizibilitate

Clasa employee a fost facuta o clasa de baza publica prin declaratia:


class manager : public employee{ /* ... */ };
Aceasta inseamna ca un membru public al clasei employee este de asemenea un
membru public al clasei manager. De exemplu:
void clear(manager* p){ p->next = 0; }
se va compila deoarece next este un membru public atit al lui employee cit si al lui
manager. Lasind la o parte din declaratie cuvintul public se poate defini o clasa
derivata privata:
class manager : employee{ /* ... */ }
Aceasta inseamna ca un membru public al clasei employee este un membru privat al
clasei manager. Adica, membri functiilor manager pot utiliza membri publici ai lui
employee ca inainte, dar acesti membri nu sint accesibili utilizatorilor clasei
manager. In par- ticular, dindu-se aceasta declaratie de manager, functia clear() nu se
va compila. Prietenii unei clase derivate au acelasi acces la membri clasei de baza ca
si functiile membru. Declaratia public a claselor de baza este mai frecventa decit
declaratia private, ceea ce este pacat pentru ca declaratia unei clase de baza publice
este mai lunga decit una privata. De asemenea, este o sursa de erori pentru incepatori.
Cind este declarata o structura, clasa ei de baza este im- plicit o clasa de baza
publica. Adica:
struct D : B{ /* ... */ }
inseamna
class D : public B{ public: /* ... */ }
Aceasta implica faptul ca daca noi nu gasim data ascunsa furnizata de utilizarea lui
class, public si friends, ca fiind utile, atunci noi putem pur si simplu elimina aceste
cuvinte si sa ne referim la struct. Facilitatile limbajului, cum ar fi functiile membru,
constructorii si operatorii de supraincarcare sint independente de mecanismul de
pastrare a datelor. Este posibil de asemenea sa se declare unii din membri publici
(dar nu toti) ai unei clase de baza public ca membri ai unei clase derivate. De
exemplu:
class manager : employee{
//.......
public:
//.......
employee::name; employee::departament;
};

Notatia:
class_name::member_name;
nu introduce un membru nou ci pur si simplu face un membru public al unei clase de
baza private pentru o clasa derivata. Acum name si departament pot fi utilizate pentru
un manager, dar salary si age nu pot fi utilizate. Natural, nu este posibil de a face ca
un membru privat al unei clase de baza sa devina un membru public al unei clase
derivate. Nu este posibil sa se faca publice numele supraincarcate utilizind aceste
notatii. Pentru a rezuma, o clasa derivata alaturi de furnizarea caracteristicilor
suplimentare aflate in clasa ei de baza, ea poate fi utilizata pentru a face ca nume ale
unei clase sa nu fie accesibile utilizatorului. Cu alte cuvinte, o clasa derivata poate fi
utilizata pentru a furniza acces transparent, semitransparent si netransparent la clasa
ei de baza.

7.2.4 Pointeri

Daca o clasa derivata are o clasa de baza (base) publica, atunci un pointer spre clasa
derivata poate fi asignat la o variabila de tip pointer spre clasa base fara a utiliza
explicit tipul de conversie. O conversie inversa de la un pointer spre base la un
pointer spre derived trebuie facuta explicit. De exemplu:
class base{ /* ... */ }; class derived : public base{ /* ... */ }; derived m;
base* pb = &m; //conversie implicite
derived* pd = pb; //eroare: un base* nu este un derived*
pd =(derived*)pb; //conversie explicita

Cu alte cuvinte, un obiect al unei clase derivate poate fi tratat ca un obiect al clasei de
baza propri cind se manipuleaza prin pointeri. Inversul nu este adevarat. Daca base ar
fi fost o clasa privata de baza, conversia implicita a lui derived* spre base* nu se
face. O conversie implicita nu se poate face in acest caz deoarece un membru public
a lui base poate fi accesat printr-un pointer la base, dar nu printr-un pointer la
derived:
class base{
int m1; public:
int m2; //m2 este un membru public a lui base
};
class derived : base{
//m2 nu este un membru public al lui derived
};
derived d;
d.m2 = 2; //eroare: m2 este din clasa privata base
base* pb = &d; //eroare (base este privata)
pb->m2 = 2; //ok
pb = (base*)&d; //ok: conversie explicita
pb->m2 = 2; //ok

Printre altele, acest exemplu arata ca utilizind conversia explicita noi putem incalca
regulile de protectie. Aceasta evident nu este recomandabil si facind aceasta de
obicei programatorul cistiga o "recompensa". Din nefericire, utilizarea nedisciplinata
a conversiei explicite poate de asemenea crea un iad pentru victime inocente
mentinind un program care sa le contina. Din fericire, nu exista nici un mod de
utilizare a conversiei explicite care sa permita utilizarea numelui privat m1. Un
membru privat al unei clase poate fi utilizat numai de membri si prieteni ai acelei
clase.

7.2.5 Ierarhizarea claselor

O clasa derivata poate fi ea insasi a clasa de baza. De exemplu:


class employee{ /* ... */ }; class secretary : employee{ /* ... */ }; class manager :
employee{ /* ... */ }; class temporary : employee{ /* ... */ }; class consultant :
temporary{ /* ... */ }; class director : manager{ /* ... */ }; class vice_president :
manager{ /* ... */ }; class president : vice_president{ /* ... */ };
O multime de clase inrudite se numeste traditional o ierar- hie de clase. Intrucit se
poate deriva o clasa dintr-o singura clasa de baza, o astfel de ierarhie este un arbore
si nu poate fi o structura mai generala de graf. De exemplu:
class temporary{ /* ... */ };
class employee{ /* ... */ };
class secretary : employee{ /* ... */ };
//nu in C++
class temporary_secretary : temporary : secretary{ /* ... */ };
class consultant : temporary : employee{ /* ... */ };
Aceasta este pacat, intrucit un graf aciclic orientat al unei clase derivate poate fi
foarte util. Astfel de structuri nu pot fi declarate, dar pot fi simulate utilizind membri
de tipuri corespunzatoare. De exemplu:
class temporary{ /* ... */ };
class employee{ /* ... */ };
class secretary : employee{ /* ... */ };
//Alternative
class temporary_secretary : secretary{
temporary temp;
//......
};
class consultant : employee{
temporary temp;
//......
};

Aceasta nu este elegant si sufera exact de problemele pentru care clasele derivate au
fost inventate. De exemplu, intrucit consultant nu este derivat din temporary, un
consultant nu poate fi pus intr-o lista de temporary employee fara a scrie un cod
special. Cu toate acestea, aceasta tehnica a fost aplicata cu succes in multe programe
utile.

7.2.6 Constructori si Destructori

Anumite clase derivate necesita constructori. Daca clasa de baza are un constructor,
atunci constructorul poate fi apelat, iar daca constructorul necesita argumente, atunci
astfel de argumente trebuie furnizate. De exemplu:
class base{
//.......
public:
base(char* n, short t);
~base();
};
class derived : public base{
base m; public:
derived(char *n);
~derived();
};

Argumentele pentru constructorul clasei de baza se specifica in definitia unui


constructor al clasei derivate. In acest caz, clasa de baza actioneaza exact ca un
membru nedenumit al clasei derivate (&5.5.4). De exemplu:
derived::derived(char* n) : (n, 10), m("member", 123)
{
//.......
}

Obiectele clasei sint constituite de jos in sus: intii baza, apoi membri si apoi insasi
clasa derivata. Ele sint distruse in ordine inversa: intii clasa derivata, apoi membri si
apoi baza.

7.2.7 Cimpuri de tip

Pentru a utiliza clase derivate mai mult decit o prescurtare convenabila in declaratii,
trebuie sa se rezolve problema urma- toare: dindu-se un pointer de tip base*, la care
tip derivat apartine in realitate obiectul pointat? Exista trei solutii fundamentale la
aceasta problema:
[1] Asigurarea ca sint pointate numai obiecte de un singur tip (&7.3.3);
[2] Plasarea unui cimp de tip in clasa de baza pentru a fi consultat de functii;
[3] Sa se utilizeze functii virtuale (&7.2.8).
Pointerii la clasa de baza se utilizeaza frecvent in proiectarea de clase container, cum
ar fi multimea, vectorul si lista. In acest caz, solutia 1 produce liste omogene; adica
liste de obiecte de acelasi tip. Solutiile 2 si 3 pot fi utilizate pentru a construi liste
eterogene; adica liste de pointeri spre obiecte de tipuri diferite. Solutia 3 este o
varianta speciala de tip sigur al solutiei 2. Sa examinam intii solutia simpla de
cimpuri_tip, adica solutia 2. Exemplul manager/employee va fi redefinit astfel:
enum empl_type {M, E};
struct employee{
empl_type type;
employee* next;
char* name;
short departament;
//.......
};
struct manager : employee{
employee* group; short level;
//........
};

Dindu-se aceasta noi putem scrie acum o functie care imprima informatie despre
fiecare employee:
void print_employee(employee* e)
{
switch(e->type)
{
case E: cout<<e->name<<"\t"<<e->departament<<"\n";
//........
break;
case M: cout<<e->name<<"\t"<<e->departament<<"\n";
//........
manager* p = (manager*)e; cout<<"level"<<p->level<<"\n";
//........
break;
}
}

si sa o utilizam pentru a imprima o lista de angajati, astfel:


void f(employee* ll)
{
for( ; ll; ll=ll->next)
print_employee(ll);
}

Aceasta functioneaza frumos, mai ales intr-un program scris de o singura persoana,
dar are o slabiciune fundamentala care depinde de programatorul care manipuleaza
tipurile intr-un mod care nu poate fi verificat de compilator. Aceasta de obicei
conduce la doua tipuri de erori in programele mai mari. Primul este lipsa de a testa
cimpul de tip si cel de al doilea este imposibilitatea de a plasa toate cazurile posibile
intr-un switch cum ar fi cel de sus. Ambele sint usor de eliminat cind programul se
scrie si foarte greu de eliminat cind se modifica un program netrivial; in special un
program mare scris de altcineva.
Aceste probleme sint adesea mai greu de eliminat din cauza ca functiile de felul lui
print() sint adesea organizate pentru a avea avantaje asupra partilor comune ale
claselor implicate. De exemplu:
void print_employee(employee* e)
{
cout << e->name << "\t" << e->departament << "\n";
//........
if(e->type == M)
{
manager* p = (manager*)e; cout << " level " << p->level << "\n";
//.......
}
}
A gasi toate instructiunile if aflate intr-o functie mare care trateaza multe clase
derivate poate fi dificil si chiar cind sint localizate poate fi greu de inteles ce fac.

7.2.8 Functii virtuale

Functiile virtuale rezolva problemele solutiei cu cimpuri de tip, permitind


programatorului sa declare functii intr-o clasa de baza care pot fi redefinite in fiecare
clasa derivata. Compilatorul si incarcatorul vor garanta corespondenta corecta intre
obiecte si functii aplicate la ele. De exemplu:
struct employee{
employee* next; char* name; short departament;
//........
virtual void print();
};

Cuvintul cheie virtual indica faptul ca functia print() poate avea versiuni diferite
pentru clase derivate diferite si ca este sarcina compilatorului sa gaseasca pe cel
potrivit pentru fiecare apel al functiei print(). Tipul functiei se declara in clasa de
baza si nu poate fi redirectat intr-o clasa derivata. O functie virtuala trebuie sa fie
definita pentru clasa in care este declarata intii. De exemplu:
void employee::print()
{
cout << name << "\t" << departament << "\n";
//........
}

Functia virtuala poate fi utilizata chiar daca nu este derivata nici o clasa din clasa ei
iar o clasa derivata care nu are nevoie de o versiune speciala a functiei virtuale nu
este necesar sa furnizeze vreo versiune. Cind se scrie o clasa derivata, pur si simplu
se furnizeaza o functie potrivita daca este necesar. De exemplu:
struct manager : employee{employee* group;
short level;
//.......
void print();
};
void manager::print()
{employee::print();
cout << "\tlevel" << level << "\n";
}

Functia print_employee() nu este acum necesara deoarece functiile membru print()


si-au luat locul lor, iar o lista de angajati poate fi minuita astfel:
void f(employee* ll)
{
for( ; ll; ll=ll->next)
ll->print();
}

Fiecare angajat va fi scris potrivit tipului lui. De exemplu:


main()
{
employee e;
e.name = "J. Brown"; e.departament = 1234; e.next = 0;
manager m;
m.name = "J. Smith"; m.departament = 1234; m.level = 2; m.next = &e;
f(&m);
}

va produce:
J. Smith 1234
level 2
J. Browh 1234
Sa observam ca aceasta va functiona chiar daca f() a fost scrisa si compilata inainte
ca clasa derivata manager sa fi fost vreodata gindita! Evident implementind-o pe
aceasta va fi nevoie sa se memoreze un anumit tip de informatie in fiecare obiect al
clasei employee. Spatiul luat (in implementarea curenta) este suficient ca sa se
pastreze un pointer. Acest spatiu este rezervat numai in obiectele clasei cu functii
virtuale si nu in orice obiect de clasa sau chiar in orice obiect al unei clase derivate.
Aceasta incarcare se plateste numai pentru clasele pentru care se declara functii
virtuale. Apelind o functie care utilizeaza domeniul de rezolutie al operatorului :: asa
cum se face in manager::print() se asigura ca nu se utilizeaza mecanismul virtual.
Altfel manager::print() ar suferi o recursivitate infinita. Utilizarea unui nume calificat
are un alt efect deziderabil: daca o functie virtuala este inline (deoarece nu este
comuna), atunci substitutia inline poate fi utilizata unde :: se utilizeaza in apel.
Aceasta furnizeaza programatorului un mod eficient de a trata unele cazuri speciale
importante in care o functie virtuala apeleaza o alta pentru acelasi obiect. Intrucit
tipul obiectului se determina in apelul primei functii virtuale, adesea nu este nevoie
sa fie determinat din nou pentru un alt apel pentru acelasi obiect.

7.3 Interfete alternative

Dupa prezentarea facilitatilor limbajului relativ la clasele derivate, discutia poate


acum sa revina la problemele pe care trebuie sa le rezolve. Ideea fundamentala pentru
clasele descrise in aceasta sectiune este ca ele sint scrise o data si utilizate mai tirziu
de programatori care nu pot modifica definitiile lor. Clasele, fizic vor consta din unul
sau mai multe fisiere antet care definesc o interfata si unul sau mai multe fisiere care
definesc o implementare. Fisierele antet vor fi plasate undeva de unde utilizatorul
poate lua o copie folosind directiva #include. Fisierele care specifica definitia sint de
obicei compilate si puse intr-o biblioteca.

7.3.1 O interfata

Consideram scrierea unei clase slist pentru liste simplu inlantuite in asa fel ca clasa
sa poata fi utilizata ca o baza pentru a crea atit liste eterogene cit si omogene de
obiecte de tipuri inca de definit. Intii noi vom defini un tip ent:
typedef void* ent;
Natura exacta a tipului ent nu este importanta, dar trebuie sa fie capabil sa pastreze
un pointer. Apoi noi definim un tip slink:
class slink{
friend class slist; friend class slist_iterator; slink* next; ent e; slink(ent a, slink* p)
{
e=a;
next=p;
}
};

Un link poate pastra un singur ent si se utilizeaza pentru a implementa clasa slist:
class slist{
friend class slist_iterator;
slink* last;//last->next este capul listei
public:
int insert(ent a);//adauga la capul listei
int append(ent a);//adauga la coada listei
ent get(); //returneaza si elimina capul listei
void clear(); //elimina toate linkurile
slist(){ last=0; }
slist(ent a)
{last = new slink(a, 0);
last->next = last;
}
~slist(){ clear(); }
};
Desi lista este evident implementata ca o lista inlantuita, implementarea ar putea fi
schimbata astfel incit sa utilizeze un vector de ent fara a afecta utilizatorii. Adica
utilizarea lui slink nu este aratata in declaratiile functiilor publice ale lui slist, ci
numai in partea privata si in definitiile de functie.

7.3.2 O implementare

Implementarea functiilor din slist este directa. Singura problema este aceea ca, ce
este de facut in cazul unei erori sau ce este de facut in caz ca utilizatorul incearca un
get() dintr-o lista vida. Aceasta se va discuta in &7.3.4. Iata definitiile pentru
membri lui slist. Sa observam cum memorind un pointer spre ultimul element al unei
liste circulare se permite implementarea simpla atit a operatiei append() cit si a
operatiei insert():
int slist::insert(ent a)
{
if(last)
last->next = new slink(a, last->next);
else
{
last = new slink(a, 0);
last->next = last;
}
return 0;
}

int slist::append(ent a)
{
if(last)
last = last->next = new slink(a, last->next);
else
{last = new slink(a, 0);
last->next = last;
}
return 0;
}
ent slist::get()
{
if(last==0)
slist_handler("get from empty slist"); slink* f = last->next; ent r = f->e; last =
(f==last) ? 0 : f->next; delete f; return r;
}

Sa observam modul in care se apeleaza slist_handler (declaratia lui poate fi gasita in


&7.3.4). Acest pointer la numele functiei se utilizeaza exact ca si cum ar fi numele
functiei. Aceasta este o prescurtare pentru o notatie de apel mai explicita:
(*slist_handler)("get from empty list");
In final, slist::clear() elimina toate elementele dintr-o lista:
void slist::clear()
{slist* l = last;
if(l==0)
return;
do{
slink* ll = l; l = l->next; delete ll;
}while(l!=last);
}

Clasa slist nu furnizeaza nici o facilitate pentru cautarea intr-o lista ci numai mijlocul
de a insera si de a sterge membri. Cu toate acestea, atit clasa slist, cit si clasa slink,
declara ca clasa slist_iterator este un prieten, asa ca noi putem declara un iterator
potrivit. Iata unul in stilul prezentat in &6.8:
class slist_iterator{slink* ce;
slist* cs; public:
slist_iterator(slist& s){cs=&s; ce=0;}
ent operator()()
{
slink* ll;
if(ce == 0) ll = ce = cs->last;
else{ ce = ce->next;
ll = (ce==cs->last) ? 0 : ce;
}
return ll ? ll->e : 0;
}
};

7.3.3 Cum sa o folosim


Asa cum este, clasa slist virtual nu este utila. Inainte de toate, la ce foloseste o lista
de pointeri void* ? Smecheria este de a deriva o clasa din slist pentru a obtine o lista
de obiecte al unui tip care este de interes intr-un program particular. Sa consideram
un compilator pentru un limbaj de felul lui C++. Aici listele de nume vor fi utilizate
extensiv; un nume este ceva de forma:
struct name{char* string;
//.......
};
Pointerii spre name vor fi pusi in lista in locul obiectelor name. Aceasta permite
utilizarea cimpului de informatie unica, e, a lui slist si admite ca un nume sa fie in
mai multe liste in acelasi timp. Iata o definitie a unei clase nlist care deriva trivial din
clasa slist:
#include "slist.h"
#include "name.h"
struct nlist : slist{
void insert(name* a){ slist::insert(a); }
void append(name* a){ slist::append(a); }
name* get(){ return (name*)slist::get(); }
nlist(){}
nlist(name* a) : (a){}
};
Functiile clasei noi sint sau mostenite direct din slist, sau fac numai conversie de tip.
Clasa nlist nu este nimic altceva decit o alternativa de interfata pentru clasa slist. Din
cauza ca tipul ent in realitate este void*, nu este necesar sa se converteasca explicit
pointerii name* utilizati ca parametri actuali (&2.3.4).
Listele de nume ar putea fi utilizate in acest fel intr-o clasa care reprezinta o definitie
de clasa:
struct classdef{nlist friends;
nlist constructors; nlist destructors; nlist members; nlist operators; nlist virtuals;
//........
void add_name(name*); classdef();
~classdef();
};
si numele s-ar putea adauga la acele liste in aceasta maniera:
void classdef::add_name(name* n)
{if(n->is_friend())
{
if(find(&friends, n)) error("friend redeclared");
else if(find(&members, n))
error("friend redeclared as member");
else
friends.append(n);
}
if(n->is_operator()) operators.append(n);
//........
}
unde is_operator() si is_friend() sint functii membru ale clasei name. Functia find()
ar putea fi scrisa astfel:
int find(nlist* ll, name* n)
{
slist_iterator ff(*(slist*)ll);
ent p;
while(p = ff())
if(p == n)
return 1; return 0;
}

Aici se utilizeaza conversia de tip explicita pentru a folosi un slist_iterator pentru un


nlist. O solutie mai buna pentru a face un iterator pentru nlist, se arata in &7.3.5. Un
nlist s-ar putea imprima printr-o functie astfel:
void print_list(nlist* ll, char* list_name)
{
slist_iterator count(*(slist*)ll); name* p; int n = 0; while(count())
n++; cout << list_name << "\n" << n << "members\n"; slist_iterator print(*(slist*)ll);
while(p = (name*)print())
cout << p->string << "\n";
}

7.3.4 Tratarea erorilor

Exista 4 conceptii la problema in legatura cu ce sa facem cind o facilitate cu scop


general, cum ar fi slist intilneste o eroare la executie (in C++, unde nu sint
prevazute facilitati specifice ale limbajului pentru tratarea erorilor la executie):
[1] Se returneaza o valoare ilegala si se lasa ca utilizatorul sa o verifice;
[2] Se returneaza o valoare de stare suplimentara si se lasa ca utilizatorul sa o
verifice;
[3] Se apeleaza o functie furnizata ca parte a clasei slist;
[4] Se apeleaza o functie eroare care se presupune ca o va furniza utilizatorul.
Pentru un program mic scris de un singur utilizator, nu exista un motiv pentru a alege
o solutie sau alta. Pentru o faci- litate generala solutia este cit se poate de diferita.
Prima conceptie, care returneaza o valoare ilegala, nu este fezabila. In general nu
exista un mod de a sti ca o valoare particulara este ilegala pentru toti utilizatorii unui
slist.
Conceptia a doua, care returneaza o valoare stare, poate fi utilizata in unele cazuri (o
variatie a acestei scheme se foloseste pentru sirurile standard I/O istream si ostream;
asa cum se explica in &8.4.2). Cu toate acestea, ea sufera de probleme serioase, caci
daca o facilitate esueaza des, utilizatorii nu se vor mai obosi sa verifice valoarea
starii. Mai mult decit atit, o facilitate poate fi utilizata in sute sau mii de locuri intr-un
program. Verificarea starii in fiecare loc ar face programul mult mai greu de citit.
Cea de a treia conceptie, care furnizeaza o functie de eroare, nu este flexibila. Nu
exista o cale pentru implementatorul unei facilitati de scop general sa stie cum
utilizatorii ar dori sa fie tratate erorile. De exemplu, un utilizator ar putea prefera
erori scrise in daneza sau romana.
Cea de a patra conceptie, lasind ca utilizatorul sa furnizeze o functie eroare, are o
anumita atractie cu conditia ca implementatorul sa prezinte clasa ca o biblioteca
(&4.5) ce con- tine versiuni implicite pentru functiile de tratare a erorilor.
Solutiile 3 si 4 pot fi facute mai flexibile (si esential echivalente) specificind un
pointer spre o functie, decit functia insasi. Aceasta permite proiectantului unei
facilitati de forma lui slist sa furnizeze o functie eroare implicita, ceea ce face ca
programatorilor sa le fie mai simplu decit sa furnizeze fun- ctia lor proprie cind este
necesar. De exemplu:
typedef void (*PFC)(char*); //pointer spre un tip functie
extern PFC slist_handler; extern PFC set_slist_handler(PFC);
Functia set_slist_handler() permite utilizatorului sa inlocuiasca prelucrarea implicita.
O implementare conventionala furnizeaza o functie implicita de tratare a erorilor care
intii scrie un mesaj in cerr, apoi termina programul utilizind exit():
#include "slist.h"
#include <stream.h>
void default_error(char* s)
{
cerr << s << "\n";
exit(1);
}
De asemenea, se declara un pointer la o functie eroare si din motive de notatie o
functie pentru setarea lui:
PFC slist_handler = default_error;
PFC set_slist_handler(PFC handler)
{
PFC rr = slist_handler;
slist_handler = handler;
return rr;
}

Sa observam modul in care set_slist_handler() returneaza slist_handler. Aceasta este


convenabil pentru utilizator ca sa seteze si sa reseteze prelucrarile sub forma unei
stive. Aceasta poate fi mai util in programe mari in care o slist ar putea fi utilizata in
diferite contexte; fiecare din ele poate apoi furniza rutinele propri de tratare a
erorilor. De exemplu:
PFC old = set_slist_handler(my_handler);
//cod unde my_handler va fi utilizat in caz de eroare in slist
set_slist_handler(old); //resetare
Pentru a cistiga chiar un control mai bun, slist_handler ar putea fi un membru al
clasei slist, permitind astfel ca diferite liste sa aiba diferite tratari de erori simultan.

7.3.5 Clase generice

Evident s-ar putea defini liste de alte tipuri (classdef*, int, char*, etc.) in acelasi mod
cum a fost definita clasa nlist: prin derivare triviala din clasa slist. Procesul de
definire de astfel de tipuri noi este plicticos (si de aceea este inclinat spre erori), dar
nu poate fi "mecanizat" prin utilizare de ma- crouri. Din pacate, aceasta poate fi cit se
poate de dureros cind se utilizeaza preprocesorul standard C (&4.7 si &r11.1).
Macrou- urile rezultate sint, totusi, cit se poate de usor de utilizat.
Iata un exemplu in care un slist generic, numit gslist, poate fi furnizat ca un macro.
Intii niste instrumente pentru a scrie astfel de macrouri se includ din <generic.h>:
#include "slist.h"
#ifndef GENERICH
#include <generic.h>
#endif

Sa observam cum #ifndef se utilizeaza pentru a asigura ca <generic.h> nu se include


de doua ori in aceeasi compilare.
GENERICH se defineste in <generic.h>
Numele pentru clasa generica noua se defineste utilizind name2() care este un
macro_name de concatenare din <generic.h>:
#define gslist(type) name2(type, gslist)
#define gslist_iterator(type) name2(type, gslist_iterator)
In final, clasa gslist(type) si gslist_iterator(type) pot fi scrise:
#define gslistdeclare(type) \
struct gslist(type) : slist{ \
int insert(type a){return slist::insert(ent(a));} \
int append(type a){return slist::append(ent(a));} \
type get(){return type(slist::get());} \
gslist(type)(){} \
gslist(type)(type a):(ent(a)){} \
~gslist(type)(){clear();} \
}; \
\
struct gslist_iterator(type) : slist_iterator{ \
gslist_iterator(type)(gslist(type)& s):((slist&)s){}\
type operator()() \
{return type(slist_iterator::operator()());} \
};

Un backslash ("\") indica faptul ca linia urmatoare este parte a macroului care se
defineste.
Utilizind acest macro, o lista de pointeri spre name, asa cum a fost utilizata in
prealabil clasa nlist, poate fi definita astfel:
#include "name.h"
typedef name* Pname;
declare(gslist, Pname); //declara clasa gslist(Pname)
gslist(Pname) nl; //declara un gslist(Pname)

Macroul declare este definit in <generic.h>. El concateneaza argumentele lui si


apeleaza macroul cu acel nume, in acest caz gslistdeclare definit mai sus. Un nume
argument al lui declare trebuie sa fie un nume simplu. Tehnica de macro_expandare
utilizata aici nu poate trata un nume de felul name*; astfel se utilizeaza typedef.
Utilizind derivarea se asigura ca toate exemplarele unei clase generice au cod comun.
Tehnica poate fi utilizata numai pentru a crea clase de obiecte de aceeasi dimensiune
sau mai mica decit clasa de baza utilizata in macro. Aceasta este totusi idea- la pentru
liste de pointeri. O gslist este utilizata in &7.6.2.

7.3.6 Interfete restrictive

Clasa slist este o clasa cit se poate de generala. Uneori o astfel de generalitate nu este
necesara sau nu este de dorit. Forme restrictive cum ar fi stive si cozi sint chiar mai
frecvente decit insasi listele generale. Nedeclarind clasa de baza publica, se pot
furniza astfel de structuri de date. De exemplu o coada de intregi poate fi definita
astfel:
#include "slist.h"
class iqueue : slist{//presupune sizeof(int)<=sizeof(void*)
public:
void put(int a){ slist::append((void*)a); }
int get(){ return int(slist::get()); }
iqueue(){}
};

Doua operatii logice se fac prin aceasta derivare: conceptul de lista este restrins la
conceptul de coada, iar tipul int se specifica pentru a restringe conceptul unei cozi la
tipul de coada de date intregi (iqueue). Aceste doua operatii ar putea fi date separat.
Aici prima este o lista care este restrinsa asa ca ea ar putea fi utilizata numai ca o
stiva:
#include "slist.h"
class stack : slist{
public:slist::insert;
slist::get;
stack(){}
stack(ent a) : (a){}
};
care poate fi apoi utilizata sa creeze tipul "stiva de pointeri spre caractere":
#include "stack.h"
class cpstack : stack{
public:
void push(char* a){ slist::insert(a); }
char* pop(){ return (char*)slist::get(); }
};

7.4 Adaugarea la o clasa

In exemplele precedente, nu se adauga nimic la clasa de baza prin clasa derivata.


Functiile se definesc pentru clasele derivate numai pentru a furniza conversie de tip.
Fiecare clasa deri- vata furnizeaza pur si simplu o interfata in loc de o multime de
rutine comune. Aceasta este o clasa speciala importanta, dar motivul cel mai frecvent
pentru care se defineste o clasa noua ca o clasa derivata este faptul ca se vrea ceea ce
furnizeaza clasa de baza, plus inca ceva.
Pot fi definite date si functii membre noi pentru o clasa derivata, in plus fata de cele
mostenite din clasa ei de baza. Sa observam ca atunci cind un element este pus intr-o
slist in pre- alabil definita, se creaza un slink care contine doi pointeri. Aceasta creare
ia timp. De un pointer ne putem dispensa, cu con- ditia ca este necesar ca un obiect la
un moment dat sa fie numai intr-o lista, asa ca pointerul next poate fi plasat in
obiectul insusi (nu intr-un obiect slink separat). Ideea este de a furniza o clasa olink
cu numai un cimp next si o clasa olist care poate manipula pointeri la astfel de
inlantuiri. Obiectele oricarei clase derivate din olink pot fi manipulate prin olist.
Litera "o" din nume este pentru a ne reaminti ca un obiect poate fi numai intr-o olist
la un moment dat:
struct olink{ olink* next; };
Clasa olist este similara cu clasa slist. Diferenta este ca un utilizator al clasei olist
manipuleaza obiectele clasei olink direct:
class olist{
olink* last; public:
void insert(olink* p); void append(olink* p); olink* get();
//.......
};

Noi putem deriva clasa name din clasa olink:


class name : olink{ /* ... */ };
Acum este trivial sa se faca o lista de name care poate fi utilizata fara a aloca spatiu
sau timp suplimentar.
Obiectele puse in olist isi pierd tipul, adica compilatorul stie ca ele sint olink. Tipul
propriu poate fi restabilit folosind conversia explicita de tip a obiectelor luate din
olist. De exemplu:
void f()
{
olist ll;
name nn;
ll.insert(&nn); //tipul lui &nn este pierdut
name* pn = (name*)ll.get(); // si se restaureaza
}

Alternativ, tipul poate fi restabilit derivind o alta clasa din olist care sa trateze
conversia de tip:
class onlist : olist{
//.......
name* get(){return (name*)olist::get();}
};

Un nume poate sa fie la un moment dat numai intr-o olist. Aceasta poate fi nepotrivit
pentru name, dar nu exista prescurtari ale claselor pentru care sa fie in intregime
potrivita. De exemplu, clasa shape din exemplul urmator utilizeaza exact aceasta
tehnica pentru ca o lista sa pastreze toate formele. Sa observam ca slist ar putea fi
definita ca o clasa derivata din olist, astfel unificind cele doua concepte. Cu toate
acestea, utilizarea claselor de baza si derivate la acest nivel microscopic al
programarii poate conduce la un cod foarte controlat.
7.5 Liste eterogene

Listele precedente sint omogene. Adica, numai obiectele unui singur tip au fost puse
in lista. Mecanismul de clasa derivata este utilizat pentru a asigura aceasta. Listele, in
general, este necesar sa nu fie omogene. O lista specificata in termenii de pointeri
spre o clasa poate pastra obiecte de orice clasa derivata din acea clasa; adica, ea poate
fi eterogena. Aceasta este probabil singurul aspect mai important si mai util al
claselor derivate si este esential in stilul programarii prezentate in exemplul urmator.
Acest stil de programare este adesea numit bazat pe obiect sau orientat spre obiect; se
bazeaza pe operatii aplicate intr-o maniera uniforma la obiectele unei liste eterogene.
Sensul unor astfel de operatii depinde de tipul real al obiectelor din lista (cunoscut
numai la executie), nu chiar de tipul elementelor listei (cunoscut la compilare).

7.6 Un program complet

Sa consideram un program care deseneaza figuri geometrice pe ecran. El consta din


trei parti:
[1] Un control de ecran: rutine de nivel inferior si structuri de date care definesc
ecranul; acestea stiu desena numai puncte si linii drepte;
[2] O biblioteca de figuri: un set de definitii si figuri generale cum ar fi dreptunghi,
cerc, etc. si rutine standard pentru a le manipula;
[3] Un program aplicativ: un set de definitii specifice a-l plicatiei si cod care sa le
utilizeze.
De obicei, cele trei parti vor fi scrise de persoane diferite. Partile sint scrise in
ordinea prezentarii lor cu adaugarea complicatiilor pe care proiectul de nivel mai
inferior nu are idee despre modul in care codul lui va fi eventual utilizat. Exemplul
urmator releva acest lucru. Pentru ca exemplul sa fie simplu pentru prezentare,
biblioteca de figuri furnizeaza numai citeva servicii simple, iar programul de aplicatii
este trivial. O conceptie extrem de simpla a ecranului se utilizeaza asa ca cititorul sa
poata incerca programul chiar daca nu sint disponibile facilitatile de grafica. Este
simplu sa se schimbe partea cu ecranul a programului cu ceva potrivit fara a schimba
codul bibliotecii de figuri sau programul de aplicatie.

7.6.1 Controlul ecranului


Intentia a fost sa se scrie controlul ecranului in C (nu in C++) pentru a accentua
distinctia intre nivelele implementarii. Aceasta s-a constatat a fi plicticos, asa ca s-a
facut un compromis: stilul de utilizare este din C (nu exista functii membru, functii
virtuale, operatori definiti de utilizator, etc.), dar se folosesc constructori, se declara
si se verifica argumentele functie, etc.. Ca rezultat, controlul ecranului arata foarte
mult ca un program in C care a fost modificat ca sa posede avantajele lui C++ fara a
fi total rescris.
Ecranul este reprezentat ca un tablou de caractere bidimensional, manipulat prin
functiile put_point() si put_line() ce utilizeaza structura point cind ne referim la
ecran:
//fisierul screen.h
const XMAX=40, YMAX=24;
struct point{
int x, y;
point(){}
point(int a, int b){ x=a; y=b; }
};
overload put_point; extern void put_point(int a, int b);
inline void put_point(point p){ put_point(p.x, p.y); }
overload put_line; extern void put_line(int, int, int, int); inline void put_line(point a,
point b)
{put_line(a.x, a.y, b.x, b.y);} extern void screen_init(); extern void screen_refresh();
extern void screen_clear();
#include <stream.h>
Inainte de a utiliza o functie put(), ecranul trebuie sa fie initializat prin screen_init(),
iar schimbarile ecranului spre structuri de date sint reflectate pe ecran numai dupa
apelul lui screen_refresh(). Cititorul va afla ca refresh se face pur si simplu scriind o
copie noua a tabloului ecran sub versiunea precedenta. Iata functiile si definitiile de
date pentru ecran:
#include "screen.h"
#include <stream.h>
enum color{black='*', white=' '}; char screen[XMAX][YMAX]; void screen_init()
{for(int y=0; y<YMAX; y++)
for(int x=0; x<XMAX; x++)
screen[x][y] = white;
}
Punctele se scriu numai daca sint in domeniul ecranului:
inline int on_screen(int a, int b)
{ return 0<=a && a<XMAX && 0<=b && b<YMAX; } void put_point(int a, int b)
{
if(on_screen(a, b))
screen[a][b]=black;
}

Functia put_line() se foloseste pentru a desena linii:


void put_line(int x0, int y0, int x1, int y1)
{ /* Traseaza linia de la (x0, y0) la (x1, y1).
Linia de trasat este b(x-x0) + a(y-y0) = 0.
Se minimizeaza abs(eps),unde eps=2*(b(x-x0) + a(y-y0)).
Newman and Sproul:
"Principles of Interactive Computer Graphics" McGrow-Hill, New York, 1979, pp
33-44.
*/
register dx = 1;
int a = x1-x0;
if(a<0)
dx = -1, a = -a; register dy = 1; int b = y1-y0; if(b<0)
dy = -1, b = -b; int two_a = 2*a; int two_b = 2*b; int xcrit = -b+two_a; register eps =
0; for(;;)
{
put_point(x0, y0);
if(x0==x1 && y0==y1)
break; if(eps<=xcrit)
x0 += dx, eps += two_b; if(eps>=a || a<=b)
y0 += dy, eps -= two_a;
}
}

Pentru stergere si resetare se folosesc functiile:


void screen_clear()
{ screen_init(); }

void screen_refresh()
{for(int y=YMAX-1; 0<=y; y--) //de sus in jos
{
for(int x=0; x<XMAX; x++) //de la stinga la dreapta
cout.put(screen[x][y]); cout.put('\n');
}
}
Se utilizeaza functia ostream::put() pentru a imprima caracterele ca si caractere;
ostream::operator<<() imprima caracterele ca si intregi mici. Acum putem sa ne
imaginam ca aceste definitii sint disponibile numai ca iesiri intr-o biblioteca pe care
nu o putem modifica.

7.6.2 Biblioteca de figuri

Noi trebuie sa definim conceptul general de figura. Acest lucru trebuie facut intr-un
astfel de mod incit figura sa poata fi comuna pentru toate figurile particulare (de
exemplu cercuri si patrate) si intr-un astfel de mod ca orice figura poate fi manipulata
exclusiv prin interfata furnizata de clasa shape:
struct shape{
shape(){ shape_list.append(this);}
virtual point north(){ return point(0, 0); }
virtual point south(){ return point(0, 0); }
virtual point east(){ return point(0, 0); }
virtual point neast(){ return point(0, 0); }
virtual point seast(){ return point(0, 0); }
virtual point draw(){}; virtual void move(int, int){};
};

Ideea este ca figurile sint pozitionate prin move() si se plaseaza pe ecran prin draw().
Figurile pot fi pozitionate relativ una fata de alta folosind conceptul de contact
points, denu- mit dupa punctele de pe compas. Fiecare figura particulara defineste
sensul acelor puncte pentru ea insasi si fiecare defineste cum se deseneaza. Pentru a
salva hirtie, in acest exemplu sint definite numai punctele de compas necesare.
Constructorul shape::shape() adauga figura la o lista de figuri shape_list. Aceasta
lista este un gslist, adica o versiune a unei liste ge- nerice simplu inlantuite asa cum a
fost definita in &7.3.5. Ea si un iterator de corespondenta s-au facut astfel:
typedef shape* sp; declare(gslist, sp); typedef gslist(sp) shape_list; typedef
gslist_iterator(sp) sl_iterator;
asa ca shape_list poate fi declarata astfel:
shape_lst shape_list;
O linie poate fi construita sau din doua puncte sau dintr-un punct si un intreg.
Ultimul caz construieste o linie orizontala de lungime specificata printr-un intreg.
Semnul intregului indica daca punctul este capatul sting sau drept. Iata definitia:
class line : public shape{
/* linie de la "w" la "e"; north() se defineste ca "deasupra
centrului atit de departe cit este north de punctul cel
mai din nord"
*/
point w, e; public:
point north()
{return point((w.x+e.x)/2, e.y<w.y?w.y:e.y);}
point south()
{return point((w.x+e.x)/2, e.y<w.y?e.y:w.y);}
void move(int a, int b)
{w.x += a; w.y += b; e.x += a; e.y += b;}
void draw(){ put_line(w,e); }
line(point a, point b){ w = a; e = b; }
line(point a, int l){w=point(a.x+l-1,a.y);e=a;}
};
Un dreptunghi este definit similar:
class rectangle : public shape{
/*
nw------n-------ne
| |
w c e
| |
sw------s-------se
*/
point sw,ne; public:
point north(){return point((sw.x+ne.x)/2, ne.y);}
point south(){return point((sw.x+ne.x)/2, sw.y);}
point neast(){ return ne; } point swest(){ return sw; } void move(int a, int b)
{ sw.x+=a; sw.y+=b; ne.x+=a; ne.y+=b; }
void draw(); rectangle(point, point);
};

Un dreptunghi este construit din doua puncte. Codul este complicat din necesitatea
de a figura pozitia relativa a celor doua puncte:
rectangle::rectangle(point a, point b)
{if(a.x<=b.x)
{
if(a.y<=b.y){ sw=a; ne=b; }
else{ sw=point(a.x, b.y); ne=point(b.x, a.y); }
}
else
{
if(a.y<=b.y){ sw=point(b.x, a.y); ne=point(a.x, b.y); }
else{ sw=b; ne = a; }
}
}

Pentru a desena un dreptunghi trebuie desenate cele patru laturi ale sale:
void rectangle::draw()
{point nw(sw.x, ne.y);
point se(ne.x, sw.y); put_line(nw, ne); put_line(ne, se); put_line(se, sw); put_line(sw,
nw);
}
In plus fata de definitiile lui shape, o bibliotece de figuri mai contine si functiile de
manipulare a figurilor. De exemplu:
void shape_refresh(); //deseneaza toate figurile
void stack(shape* p, shape* q); //pune p in virful lui q

Functie refresh() este necesara pentru a invinge greutatile legate de gestiunea


ecranului. Ea pur si simplu redeseneaza toate figurile. Sa observam ca nu exista nici
o idee despre ce fel de figuri deseneaza:
void shape_refresh()
{
screen_clear();
sl_iterator next(shape_list);
shape* p;
while(p = next())
p->draw(); screen_refresh();
}

In final, iata o functie de mare utilitate; ea pune o figura pe o alta specificind ca o


figura south() trebuie sa fie deasupra unei figuri north():
void stack(shape* p, shape* q) //pune p peste q
{
point n = p->north();
point s = q->south();
q->move(n.x-s.x, n.y-s.y+1);
}

Acum sa ne imaginam ca aceasta biblioteca se considera proprietatea unei anumite


companii care vinde software si ca ea vinde numai fisierul header care contine
definitiile shape si versiunile compilate ale definitiilor functiilor. Inca este posibil
pentru noi sa definim figuri noi si sa avem avantajul de a utiliza functii pentru
figurile noastre.

7.6.3 Programul de aplicatie

Programul de aplicatie este extrem de simplu. Se defineste figura myshape, care arata
un pic ca o fata, apoi se scrie un program main care deseneaza o astfel de fata purtind
o palarie. Declaratia lui myshape:
#include "shape.h"
class myshape : public rectangle{
line* l_eye; line* r_eye; line* mouth;
public:
myshape(point, point);
void draw();
void move(int, int);
};

Ochii si gura sint separate si sint obiecte independente create prin constructorul
myshape:
myshape::myshape(point a, point b) : (a, b)
{int ll = neast().x-swest().x+1;
int hh = neast().y-swest().y+1;
l_eye = new line(point(swest().x+2,swest().y+hh*3/4),2);
r_eye = new line(point(swest().x+ll-4,swest().y+hh*3/4),2);
mouth = new line(point(swest().x+2,swest().y+hh/4),ll-4);
}
Obiectele eye si mouth sint resetate separat prin functia shape_refresh() si ar putea fi
in principiu manipulate indepen- dent de obiectul myshape la care ele apartin. Acesta
este un mod de a defini facilitati pentru o ierarhie de obiecte construite cum ar fi
myshape. Un alt mod este ilustrat de nas. Nu este definit nasul; el pur si simplu se
adauga la figura prin functia draw():
void myshape::draw()
{
rectangle::draw();
put_point(point((swest().x+neast().x)/2,
(swest().y+neast().y)/2));
}
myshape se muta transferind dreptunghiul de baza si obiectele secundare l_eye, r_eye
si mouth:
void myshape::move(int a, int b)
{
rectangle::move(a, b);
l_eye->move(a, b);
r_eye->move(a, b);
mouth->move(a, b);
}
In final noi putem construi citeva figuri si sa le mutam un pic:
main()
{shape* p1 = new rectangle(point(0, 0), point(10, 10));
shape* p2 = new line(point(0, 15), 17);
shape* p3 = new myshape(point(15, 10), point(27, 18));
shape_refresh(); p3->move(-10, -10); stack(p2, p3); stack(p1, p2); shape_refresh();
return 0;
}
Sa observam din nou cum functiile de forma shape_refresh() si stack() manipuleaza
obiecte de tipuri care au fost definite mult dupa ce au fost scrise aceste functii (si
posibil compilate).
*************
* *
* *
* *
* ** ** *
* * *
* *
* ***** *
* *

7.7 Memoria libera

Daca noi utilizam clasa slist, am putea gasi ca programul nostru utilizeaza timp
considerabil pentru alocare si dealocare de obiecte ale clasei slink. Clasa slink este un
prim exemplu de clasa care ar putea beneficia de faptul ca programatorul sa aiba
control asupra memoriei libere. Tehnica optimizata descrisa in &5.5.6 este ideala
pentru acest tip de obiect. Intrucit orice slink se creaza folosind new si se distruge
folosind delete de catre membri clasei slist, nu exista probleme cu alte metode de
alocare de memorie.
Daca o clasa derivata asigneaza la this constructorul pentru clasa ei de baza va fi
apelat numai dupa ce s-a facut asignarea, iar valoarea lui this in constructorul clasei
de baza va fi cea atribuita prin constructorul clasei derivate. Daca clasa de baza
asigneaza la this, valoarea asignata va fi cea utilizata de constructor pentru clasa
derivata. De exemplu:
#include <stream.h>
struct base{ base(); };
struct derived : base{ derived(); }; base::base()
{
cout << "\tbase 1: this=" << int(this) << "\n"; if(this == 0)
this = (base*)27; cout << "\tbase 2: this=" << int(this) << "\n";
}
derived::derived()
{
cout << "\tderived 1: this=" << int(this) << "\n"; if(this == 0)
this = (derived*)43; cout << "\tderived 2: this=" << int(this) << "\n";
}

main()
{
cout << "base b;\n";
base b;
cout << "new base;\n";
new base;
cout << "derived d;\n";
derived d;
cout << "new derived;\n";
new derived;
cout << "at the end\n";
}
produce iesirea:
base b;
base 1: this=2147478307
base 2: this=2147478307
new base;
base 1: this=0
base 2: this=27
derived d;
derived 1: this=2147478306
derived 2: this=2147478306
new derived;
derived 1: this=0
base 1: this=43
base 2: this=43
derived 2: this=43
at the end
Daca un destructor pentru o clasa derivata asigneaza la this, atunci valoarea asignata
este cea vazuta de destructor pentru clasa lui de baza. Cind cineva asigneaza la this
un constructor este important ca o atribuire la this sa se faca pe ori- ce cale a
constructorului. Din nefericire, este usor sa se uite o astfel de atribuire. De exemplu,
la prima editare a acestei carti cea de a doua linie a constructorului derived::derived()
era:
if(this==0)
this=(derived*)43;
In consecinta, constructorul clasei de baza base::base() nu a fost apelat pentru d.
Programul a fost legal si s-a executat corect, dar evident nu a facut ce a intentionat
autorul.

7.8 Exercitii

1. (*1). Se defineste:

class base{
public:
virtual void ian(){ cout << "base\n"; }
};

Sa se deriveze doua clase din base si pentru fiecare definitie a lui ian() sa se scrie
numele clasei. Sa se creeze obiecte ale acestei clase si sa se apeleze ian() pentru ele.
Sa se asigneze adresa obiectelor claselor derivate la pointeri de tip base* si sa se
apeleze ian() prin acesti pointeri.
2. (*2). Sa se implementeze primitivele screen (&7.6.1) intr-un mod rezonabil
pentru sistemul d-voastra.
3. (*2). Sa se defineasca o clasa triunghi si o clasa cerc.
4. (*2). Sa se defineasca o functie care deseneaza o linie ce leaga doua figuri
gasind "punctele de contact" cele mai apropiate si le conecteaza.
5. (*2). Sa se modifice exemplul shape asa ca line sa fie derivata din rectangle
si invers.
6. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi utilizata fara iterator.
7. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi folosita numai printr-un iterator. Iteratorular trebui sa aiba operatii pentru
parcurgeri inainte sau inapoi, operatiipentru a insera si sterge elemente in lista si un
mod de a face acces laelementul curent.
8. (*2). Sa se implementeze o versiune generica a unei liste dublu inlantuite.
9. (*4). Sa se implementeze o lista in care obiectele (si nu numai pointerii spre
obiecte) se insereaza si se extrag. Sa se faca sa functioneze pentru o clasa X unde
X::X(X&), X::~X() si X::operator=(X&) sint definite.
10. (*5). Sa se proiecteze si sa se implementeze o bibliote8ca pentru a scrie
simulari de drivere de evenimente. Indicatie: <task.h>.Acesta este un program mai
vechi si puteti scrie unul mai bun. Ar trebui safie o clasa task. Un obiect al clasei task
ar putea sa fie capabil sa salvezestarea lui si sa aiba de restabilit acea stare (noi ar
trebui sa definimtask::save() si task::restore()) asa ca ar trebui sa opereze ca o
corutina.
Taskuri specifice pot fi definite ca obiecte de clase derivate din clasa task.Programul
de executat printr-un task ar putea fi specificat ca o functievirtuala. Ar putea fi
posibil sa se paseze argumentele la un task nou caargumente pentru constructorul lui.
Ar trebui sa fie un distribuitorimplementat ca un concept de timp virtual. Sa se
furnizeze o functie task::delay(long) care "consuma" timp virtual. Daca distribuitorul
este o parte a clasei task sau este separat, va fi o decizie majora a proiectarii.
Taskurile vor trebui sa comunice. Sa se proiecteze o clasa queue pentru aceasta. Sa se
trateze erorile de la executie intr-un mod uniform. Cum se depaneaza programele
scrise utilizind o astfel de biblioteca?
MANUAL DE REFERINTA

1 Introducere

Limbajul de programare C++ este limbajul C extins cu clase, functii inline, operator
de supraincarcare, nume de functie supraincarcat, tipurile constant, referinta,
operatorii de gesti- une a memoriei libere, verificarea argumentelor functiei si o
sintaxa noua de definire a functiilor. Limbajul C este descris in "The C Programming
Language" de Brian W. Kernighan si Dennis M. Richie, Prentice Hall, 1978. Acest
manual a fost derivat din sistemul UNIX V "The C Programming Language -
Reference Manual" cu permisiunea lui AT&T Ball Laboratories. Diferentele dintre
C++ si C sint rezumate in &15. Manualul descrie limbajul C++ din iunie 1985.

2 Conventii lexicale

Exista sase clase de lexicuri: identificatori, cuvinte cheie, constante, siruri, operatori
si alti separatori. Blancuri- le, tabulatori, rindul nou si comentariile (cu un singur
cuvint "spatii albe") asa cum se descrie mai jos se ignora exceptind faptul ca ele
servesc la a separa lexicuri. Anumite spatii albe se cer pentru a separa printre altele
identificatori, cuvinte cheie si constante adiacente.
Daca sistemul de intrare a fost descompus in lexicuri pina la un caracter dat, lexicul
urmator va include cel mai lung sir de caractere care este posibil sa constituie un
lexic.

2.1 Comentarii

Caracterele /* incep un comentariu care se termina cu caracterele */. Comentariile nu


se vor imbrica. Caracterele // incep un comentariu care se termina in linia in care a
fost inceput.

2.2 Identificatori (Nume)


Un identificator este un sir de litere si cifre de lungime arbitrara; primul caracter
trebuie sa fie o litera; caracterul subliniere _ conteaza ca litera. Literele mari sint
diferite de cele mici.

2.3 Cuvinte cheie

Identificatorii urmatori sint rezervati pentru a fi utilizati ca si cuvinte cheie si nu pot


fi utilizati in alte scopuri:
asm auto break case char
class const continue default delete
do double else enum extern
float for friend goto if
inline int long new operator
overload public register return short
sizeof static struct switch this
typedef union unsigned virtual void
while

2.4 Constante

Exista diferite tipuri de constante, asa cum se indica mai jos. Caracteristicile
hardware care afecteaza dimensiunile sint rezumate in &2.6.

2.4.1 Constante intregi

O constanta intreaga care consta dintr-un sir de cifre se considera in octal daca ea
incepe cu 0 (cifra zero) si zecimal in caz contrar. Cifrele 8 si 9 nu fac parte din
sistemul de numeratie octal. Un sir de cifre precedate de 0x sau 0X se considera ca
fiind un intreg hexazecimal. Cifrele hexazecimale contin si literele a..f respectiv A..F
care reprezinta valorile 10..15. O constanta zecimala a carei valoare depaseste
intregul cu semn cel mai mare se considera de tip long; o constanta octala sau
hexazecimala care depaseste intregul fara semn cel mai mare se conside- ra de tip
long; altfel constantele intregi se considera de tip int.
2.4.2 Constante long explicite

O constanta intreaga zecimala, octala sau hexazecimala urmata imediat de litera l sau
L este o constanta de tip long.

2.4.3 Constante caracter

O constanta caracter este un caracter inclus intre caractere apostrof ('x'). Valoarea
unei constante caracter este valoarea numerica a caracterului din setul de caractere al
calculatorului. Constantele caracter se considera de tip int. Anumite caractere
negrafice, apostroful si backslashul pot fi reprezentate potrivit tabelei urmatoare de
secvente escape:
new_line NL (LF) \n
horizontal tab HT \t
vertical tab VT \v
backspace BS \b
carriage return CR \r
form feed FF \f
backslash \ \\
simple quote ' \'
bit pattern 0ddd \ddd
bit pattern 0xddd \xddd

Secventa escape \ddd consta dintr-un backslash urmat de una, doua sau trei cifre
octale care specifica valoarea caracterului dorit. Un caz special al acestei constructii
este \0 (care nu este urmat de o cifra), care indica caracterul NULL. Secventa
escape \xddd consta din backslash urmat de una, doua sau trei cifre hexazecimale
care specifica valoarea caracterului dorit. Daca caracterul care urmeaza dupa
backslash nu este unul din cei specificati mai sus, atunci caracterul backslash se
ignora.

2.4.4 Constante flotante

O constanta flotanta consta dintr-o parte intreaga, un punct zecimal, o parte


fractionara, un e sau un E si un exponent intreg cu un semn optional. Partile intregi si
fractionare constau fiecare dintr-o secventa de cifre. Partea intreaga sau partea
fractionara (dar nu simultan) pot lipsi; punctul zecimal sau e (E) si exponentul (dar
nu simultan) pot lipsi. O constanta flotanta are tipul double.
2.4.5 Constante enumerative

Numele declarate ca enumeratori (vezi &8.10) sint constante de tip int.

2.4.6 Constante declarate

Un obiect (&5) de orice tip poate fi specificat sa aiba o valoare constanta in domeniul
numelui lui (&4.1). Pentru pointeri declaratorul &const (&8.3) se utilizeaza pentru
a atinge acest fapt; pentru obiecte nepointer se utilizeaza specificatorul const (&8.2).

2.5 Siruri

Un sir este o succesiune de caractere delimitate prin ghilimele (" ... "). Un sir are
tipul "tablou de caractere" si clasa de memorie static (vezi &4 mai jos) si se
initializeaza cu caracterele date. Toate sirurile, chiar daca sint scrise identic sint
distincte. Compilatorul plaseaza un octet null (\0) la sfirsitul fiecarui sir asa ca
programele care parcurg sirul pot gasi sfirsitul lui. Intr-un sir, ghilimelele " trebuie
precedate de \; in plus secventele escape asa cum s-au descris pentru constantele
caracter, se pot folosi intr-un sir. In sfirsit, new_line poate apare numai imediat
dupa \.

2.6 Caracteristici hardware

Tabela de mai jos rezuma anumite proprietati care variaza de la masina la masina.

| DEC | Naturale | IBM 370 | AT&T3B |


| VAX | 6800 | EBCDIC | ASCII |
| ASCII | ASCII | | |
|---------------------------------------------------------------|
| char | 8 biti | 8 biti | 8 biti | 8 biti |
| int | 32 | 16 | 32 | 32 |
| short | 16 | 16 | 16 | 16 |
| long | 32 | 32 | 32 | 32 |
| float | 32 | 32 | 32 | 32 |
| double | 64 | 64 | 64 | 64 |
| pointer | 32 | 32 | 24 | 32 |
| | +-38 | +-38 | +-76 | +-38 |
| float range | +-10 | +-10 | +-10 | +-10 |
| | +-38 | +-38 | +-76 | +-308|
| double range | +-10 | +-10 | +-10 | +-10 |
| field type | signed | unsigned | unsigned | unsigned |
| field order | right_to_ | left_to_ | left_to_ | left_to_ |
| | left | right | right | right |
| char | signed | unsigned | unsigned | unsigned |

3 Notatia sintactica

In notatia sintactica utilizata in acest manual categoriile sintactice se indica prin


italice, iar cuvintele literale si caracterele din constante in forma reala. Variantele se
listeaza pe linii separate. Un simbol terminal optional sau neoptional se indica prin
indicele "opt" asa ca:
{expresie opt}
indica o expresie optionala inclusa intre paranteze. Sintaxa este rezumata in &14.

4 Nume si Tipuri

Un nume denota un obiect, o functie, un tip, o valoare sau o eticheta. Un nume se


introduce intr-un program printr-o declaratie (&8). Un nume poate fi utilizat numai
intr-o regiune a textului programului numit domeniul lui. Un obiect este o regiune de
memorie care determina existenta lui. Intelesul valorilor gasite intr-un obiect se
determina prin tipul numelui utilizat pentru a-l accesa.

4.1 Domenii

Exista trei feluri de domenii: local, fisier si clasa. Local: In general, un nume declarat
intr-un bloc(&9.2) este local la acel bloc si poate fi folosit in el numai dupa punctul
de declaratie si in blocurile incluse in el. Cu toate acestea, etichetele (&9.12) pot
fi utilizate oriunde in functia in care ele sint declarate. Numele argumentelor
formale pentru o functie se trateaza ca si cind ar fi fost declarate in blocul cel mai
extern al acelei functii. Fisier: Un nume declarat in afara oricarui bloc (&9.2) sau
clasa (&8.5) poate fi utilizat in fisierul in care a fost declarat dupa punctul in
care a fost declarat. Clasa: Numele unui membru al clasei este local la clasa lui si
poate fi utilizat numai intr-o functie membru al acelei clase (&8.5.2), dupa un
operator aplicat la un obiect din clasa lui (&7.1) sau dupa operatorul -> aplicat la
un pointer spre un obiect din clasa lui (&7.1). Membri clasei statice (&8.5.1) si
functiile membru pot fi referite de asemenea acolo unde numele clasei lor este in
domeniu utilizind operatorul :: (&7.1). O clasa declarata intr-o clasa (&8.5.15)
nu se considera un membru si numele ei l apartine la domeniul care o include.
Un nume poate fi ascuns printr-o declaratie explicita a aceluiasi nume intr-un bloc
sau clasa. Un nume intr-un bloc sau clasa poate fi ascuns numai printr-un nume
declarat intr-un bloc sau clasa inclusa. Un nume nelocal ascuns poate insa sa fie
utilizat cind domeniul lui se specifica folosind operatorul :: (&7.1). Un nume de clasa
ascuns printr-un nume non-tip poate fi insa utilizat daca este prefixat prin class, struct
sau union (&8.2). Un nume enum ascuns printr-un nume non-tip poate insa sa fie
utilizat daca este prefixat de enum (&8.2).

4.2 Definitii

O declaratie (&8) este o definitie daca nu este o declaratie de functie (&10) si ea


contine specificatorul extern si nu are initializator sau corpul functiei sau este
declaratia unui nume de clasa (&8.8).

4.3 Linkare

Un nume din domeniul fisierului care nu este declarat expli- cit ca static este comun
fiecarui fisier intr-un program multifi- sier; asa este numele unei functii. Astfel de
nume se spune ca sint externe. Fiecare declaratie a unui nume extern din program se
refera la acelasi obiect (&5), functie (&10), tip (&8.7), clasa (&8.5), enumerare
(&8.10) sau valoare de enumerare (&8.10).
Tipurile specificate in toate declaratiile unui nume extern trebuie sa fie identice. Pot
exista mai multe definitii pentru un tip, o enumerare, o functie inline (&8.1) sau o
constanta care nu este agregat cu conditia ca definitiile care apar in diferite fisiere sa
fie identice, iar toti initializatorii sa fie expresii constante (&12). In toate celelalte
cazuri, trebuie sa fie exact o definitie pentru un nume extern din program.
O implementare poate cere ca o constanta neagregat utiliza- ta unde nu este definita,
sa trebuiasca sa fie declarata extern explicit si sa aiba exact o definitie in program.
Aceeasi restrictie poate fi impusa si asupra functiilor inline.

4.4 Clase de memorie

Exista doua clase de memorie: clase automatice si clase statice. Obiectele automatice
sint locale. Ele apar la fiecare apel al unui bloc si se elimina la iesirea din el.
Obiectele statice exista si isi mentin valorile pe timpul executiei intregului program.
Anumite obiecte nu sint asociate cu nume si viata lor se controleaza explicit utilizind
operatorii new si delete; vezi &7.2 si &9.14.

4.5 Tipuri fundamentale

Obiectele declarate ca si caractere (char) sint destul de mari pentru a memora orice
membru al setului de caractere implementat si daca un caracter real din acel set se
memoreaza intr-o variabila caracter, valoarea ei este echivalenta cu codul intreg al
acelui caracter.
Sint disponibile trei dimensiuni de tip intreg, declarate short int, int si long int.
Intregii long furnizeaza memorie nu mai putina decit cei short, dar implementarea
poate face ca sau intregii short sau cei long sau ambii sa fie intregi fara alte atribute
(int). Intregii int au dimensiunea naturala pe masina gazda sugerata de arhitectura ei.
Fiecare enumerare (&8.10) este un set de constante denumite. Proprietatile lui enum
sint identice cu cele ale lui int. Intregii fara semn asculta de legile aritmeticii modulo
2^n unde n este numarul de biti din reprezentare.
Numerele flotante in simpla precizie (float) si in dubla precizie (double) pot fi
sinonime in anumite implementari.
Deoarece obiectele de tipurile anterioare pot fi interpretate in mod util ca numere, ele
vor fi referite ca tipuri aritmetice. Tipurile char, int de toate dimensiunile si enum vor
fi numite tip integral. Tipurile float si double vor fi numite tip floating.
Tipul void specifica o multime vida de valori. Valoarea (inexistenta) a unui obiect
void nu poate fi utilizata si nu se pot aplica conversii explicite sau implicite.
Deoarece o expresie void noteaza o valoare inexistenta, o astfel de expresie poate fi
utilizata numai ca o expresie instructiune (&9.1) sau ca operandul sting al unei
expresii cu virgula (&7.15). O expresie poate fi convertita explicit spre tipul void
(&7.2).

4.6 Tipuri derivate

Exista un numar conceptual infinit de tipuri derivate construite din tipurile


fundamentale:
tablouri de obiecte de un tip dat;
functii care au argumente de tipuri date si returneaza obiecte de un tip dat;
pointeri spre obiecte de un tip dat;
referinte la obiecte de un tip dat;
constante care sint valori de un tip dat;
clase ce contin o secventa de obiecte de tipuri diferite, un set de functii pentru
manipularea acestor obiecte si un set de restrictii asupra accesului la aceste obiecte si
functii;
structuri care sint clase fara restrictii la acces;
reuniuni care sint structuri capabile sa contina obiecte de tipuri diferite in momente
diferite.
In general aceste metode de a construi obiecte pot fi aplicate recursiv.
Un obiect de tip void* (pointer spre void) poate fi utilizat pentru a pointa spre obiecte
de tip necunoscut.

5 Obiecte si Lvalori

Un obiect este o regiune de memorie; o lvaloare este o expresie care se refera la un


obiect. Un exemplu evident de expresie lvaloare este numele unui obiect. Exista
operatori care produc lvalori: de exemplu, daca E este o expresie de tip poin- ter,
atunci *E este o expresie lvaloare care se refera la obiectul spre care pointeaza E.
Numele lvaloare vine de la expresia de atribuire E1 = E2 in care operatorul sting
trebuie sa fie o lvaloare. Discutia de mai jos despre operatori indica despre fiecare
daca el asteapta un operand lvaloare si daca el produce o lvaloare.

6 Conversii

Un numar de operatori pot, depinzind de operanzii lor, sa provoace conversia valorii


unui operand de un tip la altul. Aceasta sectiune explica rezultatul asteptat de la o
astfel de conversie. Paragraful &6.6 rezuma conversiile cerute de cei mai obisnuiti
operatori; ea va fi suplimentata la nevoie printr-o discutie despre fiecare operator.
Paragraful &8.5.6 descrie conversiile definite de utilizator.

6.1 Caractere si Intregi

Un caracter sau un intreg scurt poate fi utilizat oriunde se poate utiliza un intreg.
Conversia unui intreg scurt spre unul mai lung implica totdeauna extensie de semn;
intregii sint cantitati cu semn. Daca extensia de semn apare sau nu pentru caractere
este dependent de masina; vezi &2.6. Tipul explicit unsigned char forteaza ca
valorile sa fie de la zero la cea mai mare valoare permisa de masina.
Pe masinile care trateaza caracterele ca fiind cu semn, caracterele ASCII sint toate
pozitive. Cu toate acestea, o constanta caracter specificata cu backslash sufera o
extensie de semn si poate apare negativa; de exemplu, '\377' are valoarea -1.
Cind un intreg lung se converteste spre un intreg mai scurt sau spre char, se
trunchiaza la stinga; bitii in plus sint pierduti.

6.2 Flotante in simpla si dubla precizie


Aritmetica in flotanta simpla precizie poate fi utilizata pentru expresii de tip float.
Conversiile intre numere flotante in simpla precizie si dubla precizie sint din punct
de vedere matematic corecte in masura in care permite hardware-ul.

6.3 Flotante si Intregi

Conversiile valorilor flotante spre tipul intreg tind sa fie dependente de masina; in
particular directia trunchierii numere lor negative variaza de la masina la masina.
Rezultatul este imprevizibil daca valoarea nu incape in spatiul prevazut.
Conversiile valorilor intregi spre flotante se rezolva bine. Se pot pierde cifre daca
destinatia nu are suficienti biti.

6.4. Pointeri si Intregi

O expresie de tip intreg poate fi adunata sau scazuta dintr-un pointer;


intr-un astfel de caz primul se converteste asa cum se specifica in discutia despre
operatorul de adunare. Doi pointeri spre obiecte de acelasi tip pot fi scazuti; in acest
caz rezultatul se converteste spre int sau long in functie de masina; vezi &7.4.

6.5 Intregi fara semn

Ori de cite ori se combina un intreg fara semn si un intreg de tip int, ultimul se
converteste spre unsigned si rezultatul este unsigned. Valoarea este cel mai mic
intreg fara semn congruent cu intregul cu semn (modulo 2^dimensiunea cuvintului).
In reprezentarea prin complement fata de 2, aceasta conversie este conceptuala si in
realitate nu exista o schimbare in structura bitilor.
Cind un intreg fara semn se converteste spre long, valoarea rezultatului este numeric
aceeasi cu cea a intregului fara semn. Astfel conversia are ca rezultat completarea cu
zerouri nesemnificative la stinga.

6.6 Conversii aritmetice

Un numar mare de operatori conduc la conversii si produc rezultate de un tip similar


cu tipurile descrise mai sus. Aceste conversii vor fi numite "conversii aritmetice
uzuale".
orice operanzi de tip char, unsigned char sau short se convertesc spre int.
daca unul din operanzi este double, atunci si celalalt se converteste spre double si
acesta este tipul rezultatului. altfel daca unul din operanzi este unsigned long atunci
si celalalt se converteste spre unsigned long si acesta este tipul rezultatului.
altfel, daca unul dintre operanzi este long, celalalt este convertit spre long si acesta
este tipul rezultatului.
altfel, daca unul din operanzi este unsigned, celalalt se converteste spre unsigned si
acesta este tipul rezultatu lui.
altfel, ambii operanzi trebuie sa fie int si acesta este tipul rezultatului.

6.7 Conversii de pointeri

Conversiile urmatoare pot fi facute ori de cite ori se atribuie, se initializeaza sau se
compara pointeri. constanta 0 poate fi convertita spre un pointer si se garanteaza ca
aceasta valoare va produce un pointer dis tinct de orice pointer spre orice obiect.
un pointer spre orice tip poate fi convertit spre un void*.
un pointer spre o clasa poate fi convertit spre un pointer spre o clasa de baza publica
a acelei clase; vezi &8.5.3. un nume al unui vector poate fi convertit spre un pointer
spre primul lui element. un identificator care se declara ca "functie ce returneaza ..."
cind se utilizeaza altundeva decit ca nume intr-un apel de functiei, se converteste
in pointer spre "functia ce returneaza ...".

6.8 Conversii de referinte

Conversia urmatoare poate fi facuta ori de cite ori se initializeaza referintele.


o referinta la o clasa poate fi convertita spre o referin ta spre o clasa de baza publica a
acelei clase; vezi &8.6.3.

7 Expresii

Precedenta operatorilor unei expresii este aceeasi cu ordinea subsectiunilor majore


ale acestei subsectiuni, intii fiind precedenta cea mai inalta. Astfel, de exemplu,
expresiile care se refera la operanzii lui + (&7.4) sint acele expresii care sint definite
in &7.1-&7.4. Operatorii din fiecare subsectiune au aceeasi precedenta.
Asociativitatea stinga sau dreapta este specificata in fiecare subsectiune pentru
operatorii discutati in ea. Precedenta si asociativitatea tuturor operatorilor unei
expresii este rezumata in gramatica din &14.
Altfel ordinea evaluarii expresiilor este nedefinita. In particular compilatorul
considera ca el este liber sa calculeze subexpresiile in ordinea in care el considera ca
acest lucru este cel mai eficient, chiar daca subexpresiile implica efecte secundare.
Ordinea in care au loc efectele secundare nu este specificata. Expresiile care implica
un operator comutativ si unul asociativ (*, +, &, |, ^) pot fi rearanjate arbitrar, chiar si
in prezenta parantezelor; pentru a forta o ordine particulara a evaluarii trebuie sa se
utilizeze un temporar explicit.
Tratarea depasirilor si verificarea impartirii intr-o evaluare de expresie este
dependenta de masina. Majoritatea imple mentarilor existente ale lui C++ ignora
depasirile de intregi, tratarea impartirii la zero si toate exceptiile flotante difera de la
masina la masina si de obicei se ajusteaza printr-o functie de biblioteca.
In plus fata de intelesul standard descris in &7.2-&7.15 operatorii pot fi
supraincarcati, adica li se dau sensuri cind se aplica la tipuri definite de utilizator;
vezi &7.16.

7.1 Expresii primare

Expresiile primare implica . -> :: indexare si apel de functie grupindu-se de la stinga


la dreapta.
expression_list:
expression
expression_list, expression
id:
identifier
operator_function_name
typedef_name :: identifier
typedef_name :: operator_function_name
primary_expression:
id
:: identifier
constant
string
this
(expression)
primary_expression[expression] primary_expression(expression_list opt)
primary_expression.id
primary_expression->id

Un identificator este o expresie primara, cu conditia ca el sa aiba o declaratie


potrivita (&8). Un operator_function_name este un identificator cu un inteles special;
vezi &7.16 si &8.5.11.
Operatorul :: urmat de un identificator din domeniul fisierului este acelasi lucru cu
identificatorul. El permite unui obiect sa fie referentiat chiar daca identificatorul lui a
fost ascuns (&4.1).
Un typedef_name (&8.8) urmat de :: si de un identificator este o expresie primara.
Typedef_name trebuie sa noteze o clasa (&8.5) iar identificatorul trebuie sa noteze
un membru al acelei clase. Tipul lui se specifica printr-o declaratie de identificator.
Type_name poate fi ascuns printr-un nume non_type; in acest caz typedef_name
poate fi inca gasit si utilizat.
O constanta este o expresie primara. Tipul ei poate fi int, long, float sau double in
functie de forma ei.
Un sir este o expresie primara. Tipul lui este "tablou de caractere". El este de obicei
convertit imediat spre un pointer spre primul lui caracter (&6.7).
Cuvintul cheie this este o variabila locala din corpul unei functii membru (vezi
&8.5); este un pointer spre obiectul pentru care functia a fost apelata.
O expresie in paranteze este o expresie primara a carui tip si valoare sint identice cu a
expresiei fara paranteze. Prezenta parantezelor nu afecteaza faptul ca expresia este o
lvaloare. O expresie primara urmata de o expresie in paranteze patrate este o expresie
primara. Sensul intuitiv este acela al unui indice. De obicei, expresia primara are
tipul "pointer spre ...", expresia indice este int, iar tipul rezultatului este "...".
Expresia E1[E2] este identica (prin definitie) cu *((E1) + (E2)). Toate cheile
necesare intelegerii acestei notatii sint continute in aceasta sectiune impreuna cu
discutiile din &7.1, &7.2 si &7.4 despre identificatori, * si respectiv +; &8.4.2
rezuma mai jos implicatiile. Apelul unei functii este o expresie primara urmata de
paranteze care contin o lista de expresii separate prin virgula, care este posibil sa fie
si vida, iar expresiile formeaza argumentele reale ale functiei. Expresia primara
trebuie sa fie de tipul "functie care returneaza ... " sau "pointer spre o functie care
returneaza ... " iar rezultatul apelului functiei este de tipul "...".
Fiecare argument formal se initializeaza cu argumentul actual al lui (&8.6). Se fac
conversii standard (&6.6-&6.8) si conversii definite de utilizator (&8.5.6). O functie
poate schimba valorile argumentelor ei formale, dar aceste schimbari nu pot afecta
valorile argumentelor actuale exceptind cazul in care argumentele formale sint de tip
referinta (&8.4). O functie poate fi declarata ca sa accepte mai putine sau mai multe
argumente decit sint specificate in declaratia de functie (&8.4). Orice argument real
de tip float pentru care nu exista un argument formal se converteste inaintea apelului
spre double; cel de tip char sau short se converteste spre int si ca de obicei, numele
de tablouri se convertesc spre pointeri. Ordinea de evaluare a argumentelor este
nedefinita prin limbaj; sa luam nota de faptul ca compilatoarele difera intre ele.
Apelurile recursive sint permise la orice functie.
O expresie primara urmata de un punct si de un identificator (sau un identificator
calificat printr-un typedef_name utilizind operatorul ::) este o expresie. Prima
expresie trebuie sa fie un obiect al unei clase, iar identificatorul trebuie sa numeasca
un membru al acelei clase. Valoarea este membrul numit al obiectului si el este o
lvaloare daca prima expresie este o lvaloare. Sa observam ca "obiectele unei clase"
pot fi structuri (&8.5.12) sau reuniuni (&8.5.13).
O expresie primara urmata de o sageata (->) si de un identificator (sau un
identificator calificat printr-un typedef_name utilizind operatorul ::) este o expresie.
Prima expresie trebuie sa fie un pointer la un obiect al unei clase iar identificatorul
trebuie sa numeasca un membru al acelei clase. Rezultatul este o lvaloare care
referentiaza membrul numit al clasei spre care pointeaza expresia pointer. Astfel
expresia E1->MOS este acelasi lucru cu (*E1).MOS. Clasele se discuta in &8.5.
Daca o expresie are tipul "referinta la ..." (vezi &8.4 si &8.6.3) valoarea expresiei
este obiectul notat prin referinta. O referinta poate fi gindita ca un nume al unui
obiect (&8.6.3).

7.2 Operatori unari

Expresiile cu operatori unari se grupeaza de la dreapta la stinga.


unary_expression:
unary_operator expression
expression++
expression--
sizeof expression
sizeof(type_name)
(type_name) expression
simple_type_name (expression_list)
new type_name initializer_opt
new (type_name)
delete expression
delete [expression] expression

unary_operator: unul dintre

* & + - ! ~ ++ --

Operatorul unar * inseamna indirectare: expresia trebuie sa fie un pointer, iar


rezultatul este o lvaloare care se refera la un obiect spre care pointeaza expresia.
Daca tipul expresiei este "pointer spre ..." tipul rezultatului este "... ".
Rezultatul operatorului unar & este un pointer spre obiectul referit de operand.
Operandul trebuie sa fie o lvaloare. Daca ti- pul expresiei este "...", atunci tipul
rezultatului este "pointer spre ...".
Rezultatul operatorului unar + este valoarea operandului sau valoarea rezultata in
urma conversiilor aritmetice obisnuite. Operandul trebuie sa fie de tip aritmetic.
Rezultatul operatorului unar - este negativarea operandului sau.
Operandul trebuie sa fie de tip aritmetic. Se fac conversii aritmetice obisnuite.
Negativarea unei cantitati fara semn se calculeaza scazind valoarea ei din 2^n unde n
este numarul de biti dintr-un int.
Rezultatul operatorului de negatie logica ! este 1 daca valoarea operandului sau este
0, si este 0 daca valoarea operandului sau este diferita de zero. Tipul operandului este
int. Se aplica la orice tip aritmetic sau la pointeri.
Operatorul ~ produce complementul fata de 1 al operandului sau. Se fac conversii
aritmetice uzuale. Tipul operandului trebuie sa fie intreg.

7.2.1 Incrementare si Decrementare


Operandul prefixului ++ este incrementat. Operandul trebuie sa fie o lvaloare.
Expresia ++x este echivalenta cu x += 1. Vezi discutiile despre adunare (&7.4) si
operatorii de asignare (&7.14) pentru informatii despre conversii.
Operandul prefixat prin -- se decrementeaza in mod analog ca si in cazul operatorului
prefix ++.
Valoarea obtinuta prin aplicarea unui operator ++ postfix este valoarea operandului.
Operandul trebuie sa fie o lvaloare. Dupa ce este luat in evidenta rezultatul; obiectul
este incrementat in aceeasi maniera ca si operatorul prefix ++. Tipul rezultatului este
acelasi ca si tipul operandului. Valoarea obtinuta prin aplicarea unui operator --
postfix este valoarea operandului. Operandul trebuie sa fie o lvaloare. Dupa ce
rezultatul este luat in evidenta, obiectul este decremen- tat in aceeasi maniera ca si
pentru operatorul prefix --. Tipul rezultatului este acelasi cu al operandului.

7.2.2 Sizeof

Operatorul sizeof produce dimensiunea in octeti a operandului sau. (Un octet este
nedefinit prin limbaj cu exceptia terme- nilor valorii lui sizeof. Cu toate acestea, in
toate implementarile existente un octet este spatiul cerut pentru a pastra un caracter.)
Cind se aplica la un tablou, rezultatul este numarul total de octeti din tablou.
Dimensiunea se determina din declaratiile obiectelor dintr-o expresie. Expresia este
semantic o constanta fara semn si poate fi utilizata oriunde se cere o constanta.
Operatorul sizeof se poate aplica de asemenea la un nume de tip in paranteze. In
acest caz el produce dimensiunea in octeti a unui obiect de tip indicat.
7.2.3 Conversia explicita de tip

Un simple_type_name inclus optional in paranteze (&8.2) urmat de o expresie in


paranteze (sau o lista de expresii daca ti- pul este o clasa cu un constructor declarat in
mod corespunzator &8.5.5) face ca valoarea expresiei sa fie convertita spre tipul
denumit. Pentru a exprima conversia spre un tip care nu are un nume simplu, trebuie
inclus in paranteze numele tipului (&8.7). Daca numele tipului este in paranteze,
expresia nu este necesar sa fie in paranteze. Aceasta constructie se numeste cast.
Un pointer poate fi convertit explicit spre orice tip de intregi care sint destul de mari
pentru a le pastra. Ori de cite ori se cere un int sau long, acest lucru este dependent de
masina. Functia de transformare este de asemenea dependenta de masina, dar se
intentioneaza ca aceasta sa nu fie o surpriza pentru cei care cunosc structura de
adresare a masinii. Detalii despre anumite masini particulare se dau in &2.6.
Un obiect de tip intreg se poate converti in mod explicit spre un pointer.
Transformarea totdeauna face ca un intreg convertit dintr-un pointer sa revina inapoi
la acelasi pointer, dar printre altele ea este dependenta de masina.
Un pointer spre un tip poate fi convertit explicit spre un pointer spre un alt tip.
Pointerul rezultat poate provoca ex ceptii de adresare la utilizare daca pointerul care
se converteste nu se refera la un obiect aliniat in memorie in mod cores- punzator. Se
garanteaza ca un pointer spre un obiect de o dimensiune data poate fi convertit spre
un pointer spre un obiect cu o dimensiune mai mica si din nou inapoi fara a face
schimbari. Diferite masini pot diferi in numarul de biti in pointeri si in cerintele de
aliniere pentru obiecte. Agregatele se aliniaza la limita cea mai stricta ceruta de
oricare din componentele lor.
Un obiect poate fi convertit spre o clasa de obiecte numai daca a fost declarat un
constructor potrivit sau un operator de conversie (&8.5.6). Un obiect poate fi
convertit explicit spre o referinta de tip X& daca un pointer spre acel obiect poate fi
convertit explicit spre un X*.

7.2.4 Memoria libera

Operatorul new creaza un obiect de tipul type_name (vezi &8.7) la care se aplica el.
Durata de viata a unui obiect creat prin new nu este restrinsa la domeniul in care se
creaza. Operatorul new returneaza un pointer la obiectul pe care il creaza. Cind acel
obiect este un tablou se returneaza un pointer la primul sau element. De exemplu, atit
new int, cit si new int[10] returneaza un int*. Se poate furniza un initializator pentru
anumite obiecte de clasa (&8.6.2). Pentru a obtine memorie operatorul new (&7.2) va
apela functia:
void* operator new(long);
Argumentul specifica numarul de octeti cerut. Memoria va fi neinitializata. Daca
operatorul new() nu poate gasi cantitatea de memorie ceruta, atunci el va returna
valoarea zero.
Operatorul delete va distruge un obiect creat prin operatorul new. Rezultatul este
void. Operandul lui delete trebuie sa fie un pointer returnat de new. Efectul aplicarii
lui delete la un pointer care nu este obtinut prin operatorul new este nedefinit. Cu
toate acestea, stergind un pointer cu valoarea zero este inofensiv. Pentru a elibera
memorie operatorul delete va apela functia:
void operator delete(void*);
In forma:
delete [expression] expression
cea de a doua expresie pointeaza spre un vector iar prima expresie da numarul de
elemente al acelui vector. Specificarea numarului de elemente este redondant
exceptind cazul cind se sterg vectori de anumite clase (vezi &8.5.8).

7.3 Operatori multiplicatori

Operatorii multiplicatori *, / si % se grupeaza de la stinga la dreapta. Se fac conversii


aritmetice obisnuite.
multiplicative_expression
expression * expression
expression / expression
expression % expression

Operatorul binar * indica inmultire. Operatorul * este asociativ si expresia cu diferite


inmultiri la acelasi nivel poate fi rearanjata de compilator.
Operatorul / indica impartire. Cind intregii pozitivi se impart trunchierea este zero;
dar forma trunchierii este depen- denta de masina daca operandul este negativ. Pe
masinile indicate in acest manual, restul are acelasi semn cu deimpartitul. Este
totdeauna adevarat ca (a / b) * b + a % b este egal cu a (daca b nu este zero).
Operatorul binar % produce restul impartirii primei expresii prin cea de a doua. Se
fac conversii aritmetice uzuale. Operanzii trebuie sa nu fie flotanti.

7.4 Operatori aditivi

Operatorii aditivi + si - se grupeaza de la stinga la dreapta. Se fac conversii aritmetice


obisnuite. Exista unele posibilitati de tip suplimentare pentru fiecare operator.
aditive_expression:
expression + expression
expression - expression

Rezultatul operatorului + este suma operanzilor. Un pointer spre un obiect dintr-un


tablou poate fi adunat cu o valoare de tip intreg. Ultimul este in toate cazurile
convertit spre un deplasament inmultindu-l prin lungimea obiectului spre care
pointeaza acel pointer. Rezultatul este un pointer de acelasi tip ca si pointerul original
si care pointeaza spre un alt obiect din acelasi tablou, potrivit deplasamentului fata de
obiectul original. Astfel daca P este un pointer spre un obiect dintr-un ta- blou,
expresia P + 1 este un pointer spre obiectul urmator din tablou.
Nu sint admise alte combinatii de tip pentru pointeri.
Operatorul + este asociativ si expresiile cu diferite adunari la acelasi nivel pot fi
rearanjate de compilator. Rezultatul operatorului – este diferenta dintre operanzi.
Se fac conversii aritmetice obisnuite. In plus, o valoare de tip intreg poate fi scazuta
dintr-un pointer si apoi se aplica aceleasi conversii ca si pentru adunare.
Daca doi pointeri spre obiecte de acelasi tip se scad, rezultatul sete convertit
(impartind la lungimea obiectului) la un intreg ce reprezinta numarul de obiecte
care separa obiectele pointate. Depinzind de masina, intregul rezultat poate fi de tip
int sau long (&2.6). Aceasta conversie va da in general un rezultat neasteptat daca
pointerii nu pointeaza spre obiecte din acelasi tablou, intrucit pointerii, chiar spre
obiecte de acelasi tip, nu difera in mod necesar printr-un multiplu de lungimea
obiectului.

7.5 Operatori de deplasare

Operatorii de deplasare << si >> se grupeaza de la stinga la dreapta. Ambii realizeaza


conversii aritmetice obisnuite asupra operanzilor lor, fiecare din ei trebuind sa fie de
tip intreg. Apoi operandul drept se converteste spre int. Tipul rezultatului este cel al
operandului sting.
Rezultatul este nedefinit daca operandul drept este negativ sau este mai mare sau egal
decit lungimea obiectelor in biti.
shift_expression:
expression << expression
expression >> expression

Valoarea lui E1 << E2 este E1 (interpretat ca o configuratie de biti) deplasat la stinga


cu E2 biti; bitii liberi se completeaza cu zero. Deplasarea la dreapta este garantata a fi
o deplasare logica (se completeaza cu 0) daca E1 este fara semn; altfel ea este
aritmetica (se copiaza bitul de semn).
7.6 Operatori relationali

Operatorii de relatie se grupeaza de la stinga la dreapta, dar acest fapt nu este foarte
util; a < b < c nu inseamna ceea ce s-ar parea.
relational_expression:
expression < expression
expression > expression
expression <= expression
expression >= expression

Operatorii < (mai mic decit), > (mai mare decit), <= (mai mic sau egal cu) si >= (mai
mare sau egal cu) produc zero daca relatia specificata este falsa si unu daca este
adevarata. Tipul rezultatului este int. Se fac conversii aritmetice obisnuite. Doi
pointeri pot fi comparati; rezultatul depinde de locatiile relative din spatiul de adrese
al obiectelor pointate. Comparatia de pointeri este portabila numai cind pointerii
pointeaza spre obiecte din acelasi tablou.

7.7 Operatori de egalitate

equality_expression:
expression == expression
expression != expression

Operatorii == (egal) si != (diferit) sint analogi cu operatorii de relatie exceptind


precedenta mai mica a lor. (Astfel a<b == c<d este 1 daca a<b si c<d au aceeasi
valoare de adevar). Un pointer poate fi comparat cu zero.

7.8 Operatorul SI pe biti


and_expression:
expression & expression
Operatorul & este asociativ si expresiile care implica & pot fi rearanjate. Se executa
conversii aritmetice obisnuite; rezultatul este functia si pe biti a operanzilor.
Operatorii se aplica numai la operanzi intregi.

7.9 Operatorul SAU EXCLUSIV pe biti

exclusive_or_expression:
expression ^ expression
Operatorul ^ este asociativ si expresiile care implica ^ pot fi rearanjate. Se fac
conversii aritmetice obisnuite; rezultatul este functia sau exclusiv pe biti al
operanzilor. Operatorii se aplica numai la operanzi intregi.

7.10 Operatorul SAU INCLUSIV pe biti

inclusive_or_expression:
expression | expression
Operatorul | este asociativ si expresiile care implica | pot fi rearanjate. Se fac
conversii aritmetice obisnuite; rezultatul este functia sau inclusiv pe biti a
operanzilor. Operatorii se aplica numai la operanzi intregi.

7.11 Operatorul logic SI

logical_and_expression:
expression && expression
Operatorul && se grupeaza de la stinga la dreapta. El returneaza 1 daca ambii
operanzi ai lui sint diferiti de zero, si zero in celelalte cazuri. Spre deosebire de &,
&& garanteaza evaluarea de la stinga la dreapta; mai mult decit atit, cel de al doilea
operand nu se evalueaza daca primul operand este zero. Operanzii nu este necesar
sa aiba acelasi tip, dar fiecare trebuie sa aiba unul din tipurile fundamentale sau sa fie
un poin- ter. Rezultatul este totdeauna int.

7.12 Operatorul logic SAU

logical_or_expression:
expression || expression
Operatorul || se grupeaza de la stinga la dreapta. El retur- neaza 1 daca oricare din
operanzi este diferit de zero, si zero altfel. Spre deosebire de |, || garanteaza o
evaluare de la stinga la dreapta; mai mult decit atit, operandul al doilea nu este
evaluat daca valoarea primului operand este diferita de 0.
Nu este necesar ca operanzii sa aiba acelasi tip, dar fiecare trebuie sa aiba unul din
tipurile fundamentale sau sa fie un pointer. Rezultatul este intotdeauna int.

7.13 Operator conditional

conditional_expression:
expression ? expression : expression
Expresiile conditionale se grupeaza de la dreapta la stinga. Prima expresie se
evalueaza si daca nu este zero, rezultatul este valoarea celei de a doua expresii, altfel
este a celei de a treia expresii. Daca este posibil, se fac conversii aritmetice pentru a
aduce expresiile a doua si a treia la un tip comun. Daca este posibil se fac conversii
de pointeri pentru a aduce expresiile a doua si a treia la un tip comun. Rezultatul are
tipul comun:
numai una din expresiile doi sau trei se evalueaza.

7.14 Operatori de asignare

Exista mai multi operatori de asignare si toti se grupeaza de la dreapta la stinga. Toti
cer o lvaloare ca operand sting si tipul unei expresii de asignare este acela al
operandului sting; aceasta lvaloare trebuie sa nu se refere la o constanta (nume de
tablou, nume de functie sau const). Valoarea este valoarea memorata in operandul
sting dupa ce asignarea a avut loc.
assigment_expresion;
expression assigment_operator expression
assigment_operator: unul dintre

= += -= *= /= %= >>= <<= &= ^= |=

In atribuire simpla cu =, valoarea expresiei o inlocuieste pe cea a obiectului referit


prin operandul din partea stinga. Daca ambii operanzi au tipul aritmetic, operandul
drept se converteste spre tipul operandului sting preparat pentru asignare. Daca
argumentul sting are tipul pointer operandul drept trebuie sa fie de acelasi tip sau de
un tip care poate fi convertit spre el (vezi &6.7). Ambii operanzi pot fi obiecte de
aceeasi clasa. Obiectele de anumite clase nu pot fi atribuite (vezi &8.5.3).
Asignarea la un obiect de tip "referinta la ..." atribuie la obiectul notat prin referinta.
Comportamentul unei expresii de forma E1 op= E2 poate fi dedus luind echivalentul
ei de forma E1 = E1 op (E2); totusi E1 se evalueaza numai o data. In += si -=,
operatorul sting poate fi un pointer, caz in care operandul drept (de tip intreg) se
converteste asa cum s-a explicat in &7.4; toti operanzii drepti si toti operanzii stingi
non_pointer trebuie sa aiba tipul aritmetic.

7.15 Operatorul virgula


comma_expression:
expression, expression
O pereche de expresii separate printr-o virgula se evalueaza de la stinga la dreapta si
valoarea expresiei din stinga este eliminata. Tipul si valoarea rezultatului este tipul si
valoarea operandului din dreapta. Acest operator se grupeaza de la stinga la dreapta.
In contextele in care virgulei i se da un inteles special, de exemplu in listele
parametrilor reali ale functiilor (&7.1) si in listele de initializatori (&8.6), operatorul
virgula asa cum a fost descris in aceasta sectiune poate sa apara numai in paranteze;
de exemplu:
f(a, (t=3, t+2), c)
are trei argumente, cel de al doilea are valoarea 5.

7.16 Operatori de supraincarcare

Majoritatea operatorilor pot fi supraincarcati, adica declarati sa accepte obiectele


unei clase ca operanzi (vezi &8.5.11). Nu este posibil sa se schimbe precedenta
operatorilor si nici sa se schimbe sensul operatorilor aplicati la obiecte non_clasa. In
telesul predefinit al operatorilor = si & (unar) aplicati la obiectele unei clase se poate
schimba.
Identitatile dintre operatori aplicate la tipurile de baza (de exemplu a++ <=> a += 1)
nu este necesar sa se indeplineasca pentru operatorii aplicati la tipurile clasa. Unii
operatori, de exemplu cel de asignare, cere ca un operand sa fie o lvaloare cind se
aplica la tipuri de baza; aceasta nu se cere cind operatorii sint declarati pentru tipuri
clasa.

7.16.1Operatori unari

Un operator unar, daca este prefix sau postfix, poate fi definit printr-o functie
membru (vezi &8.5.4) fara apartenente sau o functie prieten (vezi &8.5.10) care are
un argument dar nu ambele. Astfel, pentru operatorul unar @, atit x@, cit si @x pot
fi interpretati fie ca x.operator@(), fie ca operator@(x). Cind operatorii ++ si -- sint
supraincarcati, nu este posibil sa se faca distinctie intre aplicatia prefix si cea postfix.

7.16.2Operatori binari

Un operator binar poate fi definit sau printr-o functie membru care are un argument
sau printr-o functie prieten care are doi parametri, dar nu ambele. Astfel, pentru un
operator binar @, x@y poate fi interpretat sau x.operator@(y) sau operator@(x, y).

7.16.3Operatori speciali

Apelul de functie
primary_expression (expression_list_opt)
si indexarea
primary_expression[expression]
se considera operatori binari. Numele functiilor care se definesc sint operator() si
operator[]. Astfel, un apel x(arg) este interpretat ca x.operator()(arg) pentru un obiect
de clasa x. O expresie de forma x[y] se interpreteaza ca x.operator[](y).

8 Declaratii

Declaratiile se utilizeaza pentru a specifica interpretarea data fiecarui identificator;


ele nu neaparat rezerva memorie asociata cu identificatorul. Declaratiile au forma:
declaration:
decl_specifiers_opt declarator_list_opt;
name_declaration
asm_declaration
Declaratorii din lista declaratiilor contin identificatorii de declarat. Numai in
definitiile functiilor externe (&10) sau declaratiile de functii externe se pot omite
decl_specifier. Numai cind, declarind o clasa (&8.5) sau o enumerare (&8.10), adica,
atunci cind decl_specifier este un specificator de clasa (class_specifier), sau de
enumerare (enum_specifier) se poate ca declarator_list sa fie vida.
Name_declarations se descriu in &8.8;
declaratiile asm se descriu in &8.11.
decl_specifier:
sc_specifier
type_specifier
fct_specifier
friend
typedef
decl_specifiers:
decl_specifier decl_specifiers_opt
Lista trebuie sa fie autoconsistenta in modul descris mai jos.

8.1 Specificatori de clasa de memorie

Specificatorii de clasa de memorie sint:


sc_specifier:
auto
static
extern
register

Declaratiile care utilizeaza specificatorii auto, static si register servesc de asemenea


ca definitii prin faptul ca ele implica rezervarea unei cantitati de memorie de o
marime potrivita. Daca o declaratie extern nu este o definitie (&4.2) trebuie sa existe
undeva o definitie pentru identificatorii dati. O declaratie register este cel mai bine sa
fie gindita ca o declaratie auto impreuna cu sugestia ca, compilatorul stie faptul ca
variabilele respective vor fi utilizate din greu. Aceasta informatie poate fi ignorata.
Operatorul de adresa & nu poate fi aplicat la ele. Specificatorii auto sau register pot fi
utilizati numai pentru nume de obiecte declarate intr-un bloc si pentru argumente
formale. Se poate sa nu existe functii statice intr-un bloc si nici argumente statice
formale. Cel putin un sc_specifier poate fi dat intr-o declaratie. Daca sc_specifier
este absent dintr-o declaratie, clasa de memorie se considera automatic intr-o functie
si static in afara.
Exceptie: functiile nu sint niciodata automatice. Specificatorii static si extern pot fi
folositi numai pentru nume de obiecte sau functii.
Anumiti specificatori pot fi folositi numai in declaratii de functii:
fct_specifiers:
overload
inline
virtual

Specificatorul overload permite ca un nume sa fie utilizat pentru a nota diferite


functii: vezi &8.9. Specificatorul inline este numai o informatie pentru compi- lator;
el nu afecteaza intelesul programului si poate fi ignorat. Se indica faptul ca
substitutia inline a corpului functiei este de preferat implementarii obisnuite al
apelului de functie. O functie (vezi &8.5.2 si &8.5.10) definita intr-o declaratie a
unei clase este implicit o functie inline. Specificatorul virtual poate fi utilizat numai
in decla- ratiile membrilor unei clase; vezi &8.5.4.
Specificatorul friend poate fi folosit sa se suprapuna peste numele care ascund
regulile pentru membri unei clase si poate fi utilizat numai intr-o declaratie de clasa;
vezi &8.5.10.
Specificatorul typedef se foloseste pentru a introduce un nume pentru un tip; vezi
&8.8.

8.2 Specificatori de tip

Specificatorii de tip sint:


type_specifier:
simple_type_name
class_specifier
enum_specifier
elaborated_type_specifier
const
Cuvintul const poate fi adaugat la orice specificator de tip legal. Altfel, intr-o
declaratie se poate da cel mult un specifi- cator de tip. Un obiect de tip const nu este
o lvaloare. Daca specificatorul de tip lipseste dintr-o declaratie, el se ia int.
simple_type_name:
typedef_name
char
short
int
long
unsigned
float
double
void
Cuvintele long, short si unsigned pot fi gindite ca adjective. Ele pot fi aplicate la int;
unsigned de asemenea se poate aplica la char, short si long.
Specificatorii de clasa si enumerare se discuta in &8.5 si respectiv &8.10.
elaborate_type_specifier:
key type_def_name
key identifier
key:
class
struct
union
enum

Un specificator de tip elaborat poate fi utilizat pentru a face referire la numele unei
clase sau la numele unei enumerari unde numele poate fi ascuns printr-un nume
local. De exemplu:
class x { /*...*/ };
void f(int)
{
class x a;
// ...
}

Daca numele de clasa sau de enumerare nu a fost in prealabil declarat, specificatorul


de tip elaborat actioneaza ca o decla- ratie de nume; vezi &8.8.
8.3 Declaratori

declarator_list:
init_declarator
init_declarator, declarator_list
init_declarator:
declarator initializer_opt
Initializatorii se discuta in &8.6. Specificatorii din declaratie indica tipul si clasa de
memorie al obiectelor la care se refera declaratorii. Declaratorii au sintaxa:
declarator:
dname
(declarator)
*const_opt declarator
&const_opt declarator
declarator(argument_declaration_list) declarator[constant_expression_opt]
dname:
simple_dname
typedef_name :: simple_dname
simple_dname:
identifier
typedef_name
~typedef_name
operator_function_name
conversion_function_name
Gruparea este aceeasi ca in expresii.

8.4 Intelesul (sensul) declaratorilor

Fiecare declarator se considera a fi o asertiune care, cind apare intr-o expresie o


constructie asemanatoare cu declaratorul, produce un obiect de tipul si clasa de
memorie indicata. Fiecare declarator contine exact un dname; el specifica
identificatorul care este declarat. Exceptind declaratiile unor functii speciale (vezi
&8.5.2), un dname va fi un identificator simplu. Daca apare ca un declarator un
identificator "neimpodobit", atunci el are tipul indicat de specificatorul care se afla in
capul declaratiei. Un declarator in paranteze este identic cu un declarator
"neimpodobit", dar legaturile declaratorilor complecsi pot fi alterate prin paranteze;
vezi exemplele de mai jos.
Sa ne imaginam o declaratie:
T D1
unde T este un specificator de tip (int, etc.) si D1 este un declarator. Sa presupunem
ca aceasta declaratie face ca identifi- catorul sa aiba tipul " ... T", unde "..." este vid
daca D1 este chiar un identificator (cum ar fi tipul lui x in "int x" exact int).
Daca D1 are forma:
*D
tipul identificatorului pe care il contine este "... pointer la T". Daca D1 are forma:
*const D
tipul identificatorului pe care il contine este "... pointer constant la T" adica, acelasi
tip ca si *D, dar identificatorul continut nu este o lvaloare. Daca D1 are forma:
&D
sau
&const D
tipul identificatorului pe care il contine este "... referinta la T". Intrucit o referinta nu
poate fi prin definitie o lvaloare, utilizarea lui lvalue este redondanta. Nu este posibil
sa avem o referinta spre void (void&). Daca D1 are forma:
D(argument_declaration_list)
atunci identificatorul pe care il contine are tipul "... functie care are argumente de tip
argument_declaration_list si returneaza T".
argument_declaration_list:
arg_declaration_list_opt ... _opt argument_declaration_list:
arg_declaration_list, argument_declaration
argument_declaration
argument_declaration:
decl_specifiers declarator
decl_specifiers declarator = expression
decl_specifiers abstract_declarator
decl_specifiers abstract_declarator = expression
Daca argument_declaration_list se termina cu trei puncte, atunci numarul
argumentelor se stie numai ca este egal sau mai mare decit numarul de argumente
specificat; daca este vid, fun- ctia nu are argumente.Toate declaratiile pentru o
functie trebuie sa corespunda exact atit in privinta tipului valorii returnate, cit si in
numarul si tipul argumentelor. Argumentul argument_declaration_list se utilizeaza
pentru a verifica si converti argumentele actuale in apeluri si pentru a verifica
asignarile pointerilor spre functii. Daca este specificata o expresie intr-o declaratie de
argument, atunci aceasta expresie se utilizeaza ca argument implicit. Argumentele
implicite vor fi utilizate in apeluri acolo unde ultimele argumente sint omise. Un
argument implicit nu poate fi redefinit printr-o declaratie ulterioara. Cu toate acestea,
o declaratie poate adauga argumente implicite care nu s-au dat in declaratiile
precedente.
Un identificator poate fi furnizat optional ca un argument nume; daca este prezent
intr-o declaratie de functie, el nu poate fi utilizat intrucit el iese imediat afara din
domeniul sau; daca este prezent intr-o definitie de functie (&10) el numeste un
argument formal. Daca D1 are forma:
D[constant_expression]
sau
D[]
atunci identificatorul pe care il contine are tipul "... tablou de T". In primul caz
expresia constanta este o expresie a carei valoare se determina la compilare si a carei
tip este int (expresiile constante se definesc in &12). Cind diferite specificari de
tablouri sint adiacente, se creaza un tablou multidimensional; expresiile constante
care specifica limitele tablourilor se pot omite numai pentru primul membru al
secventei. Aceasta omisiune este utila cind tabloul este extern si definitia reala, care
aloca memorie, se da in alta parte. Prima expresie constanta poate fi omisa de
asemenea cind declaratorul este urmat de initializare. In acest caz dimensiunea se
calculeaza din numarul elementelor initiale furnizate.
Un tablou poate fi construit din unul din tipurile de baza, dintr-un pointer, dintr-o
structura sau reuniune sau dintr-un alt tablou (pentru a genera un tablou
multi_dimensional).
Nu toate posibilitatile admise prin sintaxa de mai sus sint permise. Restrictiile sint:
functiile nu pot returna tablouri sau functii, desi ele pot returna pointeri spre astfel
de lucruri; nu exista tablouri de functii, desi pot fi tablouri de pointeri spre functii.

8.4.1 Exemple

Declaratia:
int i, *pi, f(), *fpi(), (*pif)();
declara un intreg i, un pointer pi spre un intreg, o functie f care returneaza un intreg,
o functie fpi care returneaza un pointer spre un intreg si un pointer pif spre o functie
care returneaza un intreg. Este util mai ales sa se compare ultimii doi. Sensul lui
*fpi() este *(fpi()) si aceeasi constructie intr-o expresie cere apelul functiei fpi si apoi
utilizind indirectarea prin pointer rezulta producerea unui intreg. La declaratorul
(*pif)(), parantezele sint necesare pentru a indica faptul ca indirectarea printr-un
pointer la o functie produce o functie, care apoi este apelata. Functiile f si fpi se
declara fara argumente, iar pif pointeaza spre o functie care nu are argumente.
Declaratiile:
const a=10, *pc=&a, *const cpc=pc; int b, *const cp=&b;
declara: a - o constanta intreaga; pc - un pointer spre o constanta intreaga;
cpc - un pointer constant spre o constanta intreaga; b - un intreg; cp - pointer
constant spre un intreg. Valoarea lui pc poate fi schimbata si la fel si obiectul spre
care pointeaza cp. Exemple de operatii ilegale sint:
a=1;
a++;
*pc=2;
cp=&a;
cpc++;

Exemple de operatii legale:


b=a;
*cp=a;
pc++;
pc=cpc;

Declaratia:
fseek(FILE*, long, int);
declara o functie care poate fi apelata cu zero, unu sau doi parametri de tip int. Ea
poate fi apelata in oricare din modurile:
point(1, 2);
point(1);
point();

Declaratia:
printf(char* ...);
declara o functie care poate fi apelata cu un numar variabil de argumente si tipuri. De
exemplu:
printf("hello world");
printf("a=%d b=%d", a, b);

Cu toate acestea, trebuie ca totdeauna char* sa fie primul sau parametru. Declaratia:
float fa[17], *afp[17];
declara un tablou de numere flotante si un tablou de pointeri spre numere flotante.
In final:
static int x3d[3][5][7];
declara un tablou de intregi tridimensional de ordinul 3x5x7. x3d este un tablou de 3
elemente; fiecare element este un tablou de 5 elemente; fiecare din acestea fiind la
rindul lui un tablou de sapte intregi. Oricare din expresiile x3d, x3d[i], x3d[i][j],
x3d[i][j][k] pot apare intr-o expresie.
8.4.2 Tablouri, Pointeri si Indici

Ori de cite ori intr-o expresie apare un identificator de tip tablou, el este convertit
intr-un pointer spre primul element al tabloului. Din cauza acestei conversii,
tablourile nu sint lvalori. Exceptind cazul in care operatorul de indexare [] a fost
declarat pentru o clasa (&7.16.3), el se interpreteaza in asa fel incit E1[E2] este
identic cu *((E1)+(E2)). Din cauza regulilor de conversie care se aplica la + daca E1
este un tablou si E2 un intreg, E1[E2] se refera la al E2-lea membru al lui E1. De
aceea, in ciuda aparentei asimetrice, indexarea este o operatie comutativa. O regula
consistenta se aplica in cazul tablourilor multidi- mensionale. Daca E este un tablou
ndimensional de ordinul ixjx...xk, atunci E care apare intr-o expresie este convertit
spre un pointer spre un tablou (n-1)dimensional de ordinul jx...xk. Daca operatorul *
se aplica explicit sau implicit ca rezultat al indexarii, rezultatul este tabloul (n-
1)dimensional, care este convertit imediat intr-un pointer.
De exemplu, consideram:
int x[3][5];
Aici x este un tablou de 3x5 intregi. Cind x apare intr-o ex- presie, el se converteste
spre un pointer spre (primul din cele trei) elementul care este un tablou de ordinul 5.
In expresia x[i], care este echivalenta cu *(x+i), x este convertit intii spre un pointer
asa cum s-a descris mai sus; apoi x+i este convertit spre tipul lui x, care implica
multiplicarea lui i prin lungimea obiectului spre care pointeaza pointerul, si anume
obiecte de 5 intregi. Rezultatele se aduna si se aplica indirectarea pentru a produce un
tablou de cinci intregi care la rindul lui este convertit spre un pointer spre primul
dintre intregi. Daca exista un alt indice se aplica din nou aceeasi regula; de data
aceasta rezulta un intreg. Din toate acestea rezulta ca tablourile din C++ sint pastrate
pe linie (ultimul indice variaza mai repede) si ca primul indice din declaratie ajuta sa
se determine cantitatea de memorie consumata de un tablou dar el nu joaca alt rol in
calculele de indici.

8.5 Declaratii de clasa

O clasa este un tip. Numele ei devine un typedef_name (vezi &8.8) care poate fi
utilizat chiar in specificarea clasei. Obiectele unei clase constau dintr-o secventa de
membri.
class_specifier:
class_head{member_list_opt}
class_head{member_list_opt public: member_list_opt}
class_head:
aqqr identifier_opt
aqqr identifier: public_opt typedef_name
aqqr:
class
struct
union

Obiectele de clasa pot fi asignate, pasate ca argumente la functii si returnate de


functii (exceptind obiectele unor clase derivate; vezi &8.5.3).
Alti operatori plauzibili, cum ar fi egalitatea, pot fi definiti de utilizator; vezi
&8.5.11.
O structura este o clasa cu toti membri publici; vezi &8.5.9. O reuniune este o
structura care contine numai un membru la un moment dat; vezi &8.5.13. O lista de
membri (member_list) poate declara ca membri date, functii, clase, enumerari,
cimpuri (&8.5.14) si prieteni (&8.5.10). O lista de membri poate de asemenea
contine declaratii pentru a specifica vizibilitatea numelor membrilor; vezi &8.5.9.
member_list:
member_declaration member_list_opt member_declaration:
decl_specifiers_opt member_declarator;;
function_definition; _opt
member_declarator:
declarator
identifier_opt: constant_expresion
Membri care sint obiecte de clasa trebuie sa fie obiecte ale claselor declarate in
prealabil. In particular, o clasa cl poate sa nu contina un obiect de clasa cl, dar ea
poate contine un pointer spre un obiect de clasa cl.
Un exemplu simplu de declaratie a unei structuri este:
struct tnod{
char tword[20]; int count; tnode* left; tnode* right;
};
care contine un tablou de 20 de caractere, un intreg si doi pointeri spre structuri
similare. Odata ce aceasta declaratie a fost data, declaratia:
tnode s, *sp;
declara pe s ca fiind un tnode si sp un pointer spre un tnode. Cu aceste declaratii:
sp->count
se refera la cimpul count al structurii spre care pointeaza sp;
s.left
se refera la pointerul spre subarborele sting al lui s; iar
s.right->tword[0]
se refera la primul caracter al membrului tnod al subarborelui drept al lui s.
8.5.1 Membri statici

Un membru care este data a unei clase poate fi static; functiile care sint membri nu
pot fi. Membri pot sa nu fie auto, register sau extern. Exista numai o singura copie a
unui membru static comuna pentru toate obiectele unei clase dintr-un program. Un
membru static mem al unei clase cl poate fi referit prin cl::mem, adica fara a se face
referire la un obiect. El exista chiar daca nu s-a creat nici un obiect al clasei cl. Nu se
poate specifica nici un initializator pentru un membru static si nu poate fi o clasa cu
un constructor.

8.5.2 Functii membru

O functie declarata ca membru (fara specificatorul friend (&8.5.10)) se numeste


functie membru si se apeleaza utilizind sintaxa membrului unei clase (&7.1). De
exemplu:
struct tnod{
char tword[20]; int count; tnode* left; tnode* right; void set(char*, tnode* l, tnode*
r);
};
tnode n1, n2;
n1.set("asdf", &n2, 0);
n2.set("ghjk",0, 0);

Definitia unei functii membru se considera ca este in dome- niul clasei sale. Aceasta
inseamna ca poate utiliza direct numele clasei sale. Daca definitia unei functii
membru este lexic in afara declaratiei de clasa, numele functiei membru trebuie sa fie
calificat prin numele clasei folosind operatorul ::. Definitiile functiilor se discuta in
&10. De exemplu:
void tnode::set(char* w, tnode* l, tnode* r)
{
count = strlen(w);
if(sizeof(tword) <= count)
error("tnode string too long"); strcpy(tword, w); left = l; right = r;
}

Notatia tnode::set() specifica faptul ca set() este un mem- bru al clasei tnode si este in
domeniul de vizibilitate al clasei tnode. Numele membrilor tword, count, left si right
se refera la obiectul pentru care a fost apelata functia. Astfel, in apelul n1.set("abc",
0, 0) tword se refera la n1.tword, iar in apelul n2.set("def", 0, 0) el se refera la
n2.tword. Functiile strlen, error si strcpy se presupun ca sint declarate in alta parte;
vezi &10. Intr-o functie membru, cuvintul cheie this este un pointer spre obiectul
pentru care a fost apelata functia.
O functie membru poate fi definita (&10) in declaratia de clasa, caz in care ea este
inline (&8.1). Astfel:
struct x{
int f(){ return b; }
int b;
};

este echivalenta cu:


struct x{
int f(); int b;
};
inline x::f(){ return b; }
Este legal sa se aplice adresa operatorului la o functie membru. Cu toate acestea,
tipul pointerului rezultat spre functie este nedefinit, asa ca orice utilizare a ei este
dependenta de implementare.

8.5.3 Clase derivate

In constructia:
aqqr identifier : public_opt typedef_name
typedef_name trebuie sa noteze o clasa in prealabil declarata, care se numeste clasa
de baza pentru clasa ce se declara. Pentru sensul de public vezi &8.5.9. Membri
clasei de baza pot fi refe- riti ca si cum ei ar fi membri clasei derivate, exceptind
cazul in care numele membrilor bazei au fost redefiniti in clasa derivata; in acest caz
operatorul :: (&7.1) poate fi utilizat pentru a ne referi la membri ascunsi. O clasa
derivata poate fi ea insasi folosita ca o clasa de baza. Nu este posibila derivarea dintr-
o reuniune (&8.5.13). Un pointer spre o clasa derivata poate fi convertit implicit intr-
un pointer spre o clasa de baza publica (&6.7).
Asignarea nu este definita implicit (vezi &7.14 si &8.5) pentru obiectele unei clase
derivate dintr-o clasa pentru care operatorul = a fost definit (&8.5.11). De exemplu:
class base{
public:
int a, b;
};
class derived : public base{
public:
int b, c;
};
derived d;
d.a=1;
d.base::b=2;
d.b=3;
d.c=4;
base* bp=&d;

asigneaza cei patru membri ai lui d iar bp devine un pointer spre d.

8.5.4 Functii virtuale

Daca clasa de baza base contine o functie virtuala (&8.1) vf si o clasa derivata
contine de asemenea o functie vf, atunci ambele functii trebuie sa aiba acelasi tip, iar
un apel al lui vf pentru un obiect al clasei derivate implica derived::vf. De exemplu:
struct base{
virtual void vf(); void f();
};
class derived : public base{
public:
void vf(); void f();
};
derived d;
base* bp=&d;
bp->vf();
bp->f();

Apelurile invoca derived::vf si base::f,respectiv pentru obiectul clasei derivate numit


d. Adica, interpretarea apelului unei functii virtuale depinde de tipul obiectului
pentru care ea este apelata, in timp ce interpretarea apelului unei functii membru
nevirtuale depinde numai de tipul pointerului care desemneaza acel obiect.
O functie virtuala nu poate fi un prieten (&8.5.10). O functie f dintr-o clasa derivata
dintr-o clasa care are o functie virtuala f este ea insasi considerata virtuala. O functie
virtu- ala care a fost definita intr-o clasa de baza nu este necesar sa fie definita intr-o
clasa derivata. In acest caz, functia defi- nita pentru clasa de baza este utilizata in
toate apelurile.
8.5.5 Constructori

O functie membru cu acelasi nume ca si clasa ei se numeste constructor. El se


utilizeaza pentru a construi valori de tipul clasei lui. Daca o clasa are un constructor,
fiecare obiect al acelei clase trebuie sa fie initializat inainte de a face orice utilizare a
obiectului (vezi &8.6). Un constructor nu poate fi declaratt virtual sau prieten.
Daca o clasa are o clasa de baza sau obiecte membru cu constructori, constructorii lor
se apeleaza inaintea constructo- rului clasei derivate. Deci, mai intii se apeleaza
constructorul pentru clasa de baza. Vezi &10 pentru un exemplu de felul in care pot
fi specificate argumentele pentru constructori si &8.5.8 pentru a vedea cum se pot
utiliza constructorii pentru gestionarea memoriei libere.
Un obiect al unei clase cu un constructor nu poate fi membru al unei reuniuni.
Nu se poate specifica un tip de valoare returnata de un constructor si nici nu se poate
folosi o instructiune return in corpul unui constructor.Un constructor poate fi utilizat
explicit ca sa creeze obiecte noi de tipul lui, utilizind sintaxa:
typedef_name(argument_list_opt);
De exemplu:
complex zz = complex(1, 2.3);
cprint(complex(7.8, 1.2));

Obiectele create in acest fel sint fara nume (exceptind cazul in care constructorul a
fost utilizat ca initializator; ca in cazul lui zz de mai sus), cu viata limitata in
domeniul in care au fost ele create.

8.5.6 Conversii

Un constructor avind un argument specifica o conversie de la tipul argumentului lui,


la tipul clasei. Astfel de conversii se utilizeaza implicit in plus fata de conversiile
standard (&6.6- &7). O asignare la un obiect apartinind clasei X este legala daca
tipul T al valorii asignate este X sau daca a fost declarata o conversie de tip de la T la
X. Constructorii se utilizeaza similar pentru conversia initializatorilor (&8.6), al
argumentelor functiei (&7.1) si al valorilor returnate de functie (&9.10). De exemplu:
class X{
//...
X(int);
};
f(X arg)
{
X a=1; //a = X(1)
a=2; //a = X(2)
f(3); //f(X(3))
}

Cind un constructor pentru clasa X nu accepta tipul asignat, nu se incearca sa se


gaseasca alti constructori care sa converteasca valoarea asignata intr-un tip acceptabil
de un constructor pentru clasa respectiva. De exemplu:
class X{ /* ... */ X(int); };
class Y{ /* ... */ Y(X); };
Y a = 1; //este ilegal; nu se incearca Y(X(1))

O functie membru a clasei X cu un nume de forma:


conversion_function_name : operator type
specifica o conversie de la tipul X la tipul type. Tipul type poate sa nu contina
declaratorii [] "vector de" sau () "functie ce returneaza". Se va utiliza implicit ca si
constructorii de mai sus (numai daca este unic &8.9) sau poate fi apelat explicit
utilizind notatia cast. De exemplu:
class X{
//......
operator int();
};
X a;
int i=int(a);
i=(int)a;
i=a;

In toate cele trei cazuri valoarea asignata va fi convertita spre X::operator int().
Conversiile definite de utilizator pot fi utilizate numai in asignari si initializari. De
exemplu:
X a, b;
// ...
int i = (a) ? 1+a : 0;
int j = (a && b) ? a+b : i;

8.5.7 Destructori
O functie membru a clasei cl numita ~cl se numeste destructor; el nu are argumente
si nici nu se poate specifica o valoare de revenire pentru el; se utilizeaza pentru a
distruge valorile de tip cl imediat inainte de a distruge obiectul care le contine. Un
destructor nu poate fi apelat explicit. Destructorul pentru o clasa de baza se executa
dupa destructorul pentru clasa lui derivata. Destructorii pentru obiectele membru se
executa dupa destructorul pentru obiectul pentru care ele sint membre. Vezi &8.5.8
pentru o explicatie despre felul in care destructorii pot fi utilizati pentru a gestiona
memoria libera. Un obiect al unei clase cu un destructor nu poate fi un membru al
unei reuniuni.

8.5.8 Memorie libera

Cind se creaza un obiect de clasa folosind operatorul new constructorul va utiliza


(implicit) operatorul new pentru a obtine memoria ceruta (&7.1). Asignind memorie
la pointerul this inainte de orice folosire a unei functii membru, constructorul poate
sa implementeze obiectul. Prin atribuirea lui 0 la this, un destructor poate elimina
operatia de alocare standard pentru obiectele clasei sale. De exemplu:

class cl{
int v[10];
cl(){this = my_allocator(sizeof(cl));}
~cl(){my_deallocator(this); this=0;}
};

La intrarea intr-un constructor, this este diferit de zero daca alocarea a avut deja loc
(asa este cazul pentru auto, static si obiectele membre) si zero altfel.
Apeluri la constructori pentru o clasa de baza si pentru obiectele membru se vor face
dupa o asignare la this. Daca constructorul unei clase de baza asigneaza la this, noua
valoare va fi folosita de asemenea de catre constructorul claselor derivate (daca
exista vreuna).
Numarul elementelor trebuie sa fie specificat cind se sterge un vector de obiecte al
unei clase cu un destructor. De exemplu:
class x{
//......
~X();
};
X.p = new X[size];
delete[size].p;
8.5.9 Vizibilitatea numelor membri

Membri unei clase declarate cu cuvintul cheie class sint privati, adica, numele lor
pot fi utilizate numai de functiile membru (&8.5.2) si functiile prietene (&8.5.10)
exceptind cazul cind ele apar dupa eticheta "public"; in acest caz ele sint publice. Un
membru public poate fi utilizat in orice functie. O structura struct este o clasa cu toti
membri publici (&8.5.12). Daca o clasa derivata se declara struct sau daca cuvintul
cheie public precede numele clasei de baza in declaratia clasei derivate, atunci
membri publici ai clasei de baza sint publici pentru clasa derivata; altfel ei sint
privati. Un membru public mem pentru o clasa de baza privata base poate fi declarat
ca sa fie public pentru o clasa derivata printr-o declaratie de forma:
typedef_name::identifier;
unde typedef_name noteaza clasa de baza si identifier este numele membrului clasei
de baza. O astfel de declaratie trebuie sa apara in partea publica a clasei derivate.
Consideram:
class base{
int a; public:
int b, c; int bf();
};

class derived : base{


int d; public:
base::c; int e; int df();
};
int ef(derived&);
Functia externa ef poate folosi numai numele c, e si df. Functia df fiind un membru al
lui derived poate folosi b, c, bf, d, e si df, dar nu si pe a. Fiind un membru al lui base,
functia bf poate utiliza membri a, b, c si bf.

8.5.10Prieteni

Un prieten al unei clase este o functie nemembru care poate utiliza numele
membrilor privati dintr-o clasa. Un prieten nu este in domeniul unei clase si nu se
apeleaza utilizind sintaxa de selectie de membru (exceptind cazul in care el este un
membru al unei alte clase). Exemplul urmator ilustreaza diferenta dintre membri si
prieteni:
class private{
int a; friend void friend_set(private*, int);
public:
void member_set(int);
};
void friend_set(private* p, int i){ p->a=i; } void private::member_set(int i){ a=i; };
private obj; friend_set(&obj, 10); obj.member_set(10);
Cind o declaratie friend se refera la un nume sau la un operator supraincarcat numai
functia specificata prin tipurile argument devine un prieten. Un membru al unei clase
cl1 poate fi prietenul clasei cl2. De exemplu:
class cl2{
friend char* cl1::foo(int);
// ...
};

Toate functiile unei clase cl1 pot fi facute prietene ale clasei cl2 printr-o singura
declaratie:
class cl2{
friend class cl1;
//......
};

O functie friend definita (&10) intr-o declaratie de clasa este inline.

8.5.11Functii operator
----------------

Cei mai multi operatori pot fi supraincarcati astfel incit sa aiba ca operanzi obiecte de
clasa.
operator_function_name:
operator operator
operator: unul din
new delete
+ - * / % ^ & | ~
! = < > += -= *= /= %=
^= &= |= << >> >>= <<= == !=
<= >= && || ++ -- () []
Ultimii doi operatori sint pentru apelul functiilor si pentru indexare. O functie
operator (exceptind operatorii new si delete; vezi &7.2) trebuie sau sa fie o functie
membru sau sa aiba cel putin un argument de clasa. Vezi de asemenea &7.16.

8.5.12Structuri

O structura este o clasa cu toti membri publici. Adica:


struct s{ /*...*/ };
este echivalent cu:
class s{public: /*...*/ };
O structura poate avea functii membru (inclusiv constructori si destructori). Baza
unei structuri derivate este publica.Adica:
struct s : b { /*...*/ };
este echivalent cu:
class s : public b{public: /*...*/ };

8.5.13Reuniuni

O reuniune poate fi gindita ca o structura a carei obiecte membru incep la


deplasamentul 0 si a carei dimensiune este suficienta pentru a contine oricare din
obiectele membru ale ei. Cel mult unul din obiectele membru pot fi pastrate intr-o
reuniune in orice moment. O reuniune poate avea functii membru (inclusiv
constructori si destructori). Nu este posibil sa se deriveze o clasa dintr-o reuniune.
Un obiect al unei clase cu un constructor sau un destructor poate fi un membru al
unei reuniuni.
O reuniune de forma:
union{member_list};
este numita reuniune anonima; ea defineste un obiect nedenumit. Numele mebrilor
unei reuniuni anonime trebuie sa fie distincte de alte nume din domeniul unde este
declarata reuniunea; ei pot fi utilizati direct in acel domeniu fara a utiliza sintaxa de
acces uzuala la un membru (&8.5). De exemplu:
union{ int a; char* p; };
a=1;
//......
p="asdf";
Aici a si p se utilizeaza ca variabile obisnuite (nemembru), dar intrucit ele sint
membri ai unei reuniuni ele trebuie sa aiba aceeasi adresa.

8.5.14Cimpuri de biti
Un membru_declarator de forma:
identifier_opt : constant_expression
specifica un cimp; lungimea lui este separata de numele cimpului prin doua puncte.
Cimpurile se impacheteaza in intregi masina; ele nu se pot pastra pe mai multe
cuvinte. Un cimp care nu poate fi pastrat in spatiul ramas al unui intreg se pune in
cuvintul urmator. Nici un cimp nu poate fi mai mare decit un cuvint. Cimpurile sint
asignate de la dreapta spre stinga pe unele masini si de la stinga spre dreapta pe
altele; vezi &2.6.
Un cimp nedenumit este util pentru cadraje, pentru a se conforma cu conditiile
impuse din afara. Ca un caz special, un cimp nedenumit cu dimensiunea 0 specifica
alinierea cimpului urmator la o limita de cuvint. Implementarile nu sint supuse unor
restrictii, altele decit sa accepte cimpuri intregi. Totusi, chiar cimpurile int pot fi
considerate ca unsigned. Din aceste motive, cimpurile trebuie sa fie declarate ca
unsigned. Operatorul adresa & nu poate fi aplicat la ele, asa ca nu exista pointeri spre
cimpuri. Cimpurile nu pot fi membri ai unei reuniuni.

8.5.15Clase imbricate

O clasa poate fi declarata intr-o alta clasa. Aceasta, totusi este numai o notatie
convenabila intrucit clasa interioara apartine domeniului care o include. De exemplu:
int x;
class enclose{
int x;
class inner{
int y; void f(int);
};
int g(inner*);
};
inner a;
void inner::f(int i){ x=i; }; //asignare la ::x
int enclose::g(inner* p){ return p->y; } // eroare

8.6 Initializare

Un declarator poate specifica o valoare initiala pentru identificatorul care se declara:


initializer:
= expression
= {initializer_list, opt}
(expression_list)
initializer_list:
expression
initializer_list, initializer_list
{initializer_list}
Toate expresiile dintr-un initializator pentru o variabila statica trebuie sa fie expresii
constante (care este descrisa in &12) sau expresii care se reduc la adresa unei
variabile in prealabil declarate, posibil modificate cu o expresie constanta.
Variabilele automatice sau registru pot fi initializate prin expresii arbitrare care
implica constante, variabile si functii in prealabil declarate.
Variabilele statice si externe care nu sint initializate se garanteaza ca au valoarea
initiala diferita de zero; variabilele automatice si registru care nu sint initializate au o
valoare initiala imprevizibila.
Cind un initializator se aplica la un scalar (un pointer sau un obiect al tipului
aritmetic), el consta dintr-o singura expresie, poate in acolade. Valoarea initiala a
obiectului este egala cu a expresiei; se fac aceleasi conversii ca si la asignare.
Sa observam ca intrucit () nu sint un initializator, X a(); nu este declaratia unui obiect
al clasei X, ci declaratia unei functii care nu are argumente si returneaza o valoare de
tip X.

8.6.1 Liste initializatoare

Cind variabila declarata este un agregat (o clasa sau un tablou) initializatorul poate
consta dintr-o lista de initializa- tori separati prin virgula si inclusa in acolade pentru
membri agregatului, scrisi in ordinea crescatoare a indicilor sau a ordinii
membrilor. Daca tabloul contine subagregate, aceasta regula se aplica recursiv la
membri agregatului. Daca sint mai putini initializatori in lista decit membri ai
agregatului atunci agregatul se completeaza cu zerouri.
Acoladele pot fi utilizate dupa cum urmeaza. Daca initializatorul incepe cu o acolada
stinga, atunci lista de initializa- tori care urmeaza (separati prin virgula) initializeaza
membri agregatului; este eroare daca sint mai multi initializatori decit membri. Daca,
totusi, initializatorul nu incepe cu o acolada stinga, atunci se iau atitea elemente din
lista cite sint necesare pentru a initializa membri agregatului; membri ramasi sint
lasati sa initializeze membrul urmator al agregatului din care face parte agregatul
curent.
De exemplu:
int x[] = {1, 3, 5};
declara si initializeaza pe x ca si tablou de o dimensiune, care are trei membri,
intrucit nu s-a specificat nici o dimensiune si sint trei initializatori.
float y[4][3] = {{1, 3, 5}, {2, 4, 6}, {3, 5, 7},};
este o initializare complet inclusa in acolade: 1, 3 si 5 initializeaza prima linie a
tabloului y[0] si anume y[0][0], y[0][1] si y[0][2]. La fel urmatoarele doua linii
initializeaza y[1] si y[2]. Initializatorul se termina mai devreme si de aceea y[3] se
initializeaza cu zero. Exact acelasi efect s-ar fi obtinut prin:
float y[4][3] = { 1, 3, 5, 2, 4, 6, 3, 5, 7};
Initializatorul pentru y incepe cu o acolada stinga, dar cel pentru y[0] nu mai incepe
si de aceea se initializeaza trei ele- mente din lista. La fel urmatoarele trei elemente
se iau succesiv pentru y[1] si y[2]. De asemenea:
float y[4][3] = { {1}, {2}, {3}, {4} };
initializeaza prima coloana a lui y (privit ca un tablou bidimensional) si lasa restul 0.

8.6.2 Obiecte de clasa

Un obiect cu membri privati nu poate fi initializat folosind o lista initializatoare; nici


un obiect al unei reuniuni. Un obiect al unei clase cu un constructor trebuie sa fie
initializat. Daca o clasa are un constructor care nu are argumente, acel constructor se
foloseste pentru obiecte care nu sint explicit initializate. O lista de argumente pentru
constructor se poate adauga la numele dintr-o declaratie sau la tipul dintr-o expresie
din new. Initializarile urmatoare produc toate aceeasi valoare (&8.4):
struct complex{float re, im;
complex(float r, float i=0){ re=r; im=i; }
};
complex zz1(1, 0);
complex zz2(1);
complex* zp1 = new complex(1, 0); complex* zp2 = new complex(1);
Obiectele de clasa pot fi de asemenea initializate prin utilizarea
explicita a operatorului =. De exemplu:
complex zz3 = complex(1, 0);
complex zz4 = complex(1);
complex zz5 = 1;
complex zz6 = zz3;

Daca exista un constructor care are ca referinta un obiect al clasei lui proprii, atunci
el va fi apelat cind se initiali- zeaza un obiect al unei clase cu un alt obiect al acelei
clase, dar nu cind un obiect este initializat cu un constructor. Un obiect poate fi un
membru al unui agregat numai: (1) daca obiectele clasei nu au un constructor sau (2)
unul din constructorii ei nu are argumente sau (3) daca agregatul este o clasa cu un
constructor care specifica o lista de initializare membru (vezi &10). In cazul (2)
constructorul respectiv se apeleaza cind se creaza agregatul. Daca agregatul este o
clasa (dar nu daca este un vector) argumentele implicite se pot folosii pentru apelul
constructorului. Daca un membru al unui agregat are un destructor atunci acel
destructor se apeleaza cind agregatul este distrus. Constructorii pentru obiecte statice
nelocale se apeleaza in ordinea in care ei apar in fisier; destructorii se apeleaza in
ordine inversa. Nu este definit apelul unui constructor si al unui destructor pentru un
obiect static local daca nu este ape- lata functia in care este definit obiectul. Daca se
apeleaza con- structorul pentru un obiect static local, atunci el este apelat dupa
constructorii pentru obiectele globale care il preced lexical. Daca este apelat
destructorul pentru un obiect static local, atunci el este apelat inaintea destructorilor
pentru obiecte globale care il preced lexical.

8.6.3 Referinte

Cind o variabila se declara ca este T&, adica "referinta la tipul T", ea trebuie sa fie
initializata printr-un obiect de tip T sau printr-un obiect care poate fi convertit spre
tipul T. Referinta devine un alt nume pentru obiect. De exemplu:
int i;
int &r=i;
r=1; // valoarea lui i devine 1
int* p=&r; // p pointeaza spre i
Valoarea unei referinte nu poate fi schimbata dupa initializare. Sa observam ca
initializarea este tratata diferit fata de asignare. Daca initializatorul pentru o referinta
la tipul T nu este o lvaloare se creaza un obiect de tip T si acesta este initializat cu
initializatorul. Referinta devine un nume pentru acel obiect. Domeniul de existenta al
unui obiect creat in acest fel este domeniul in care el este creat. De exemplu:
double& rr=1;
este legal si rr va pointa spre double care contine valoarea 1.0. Sa observam ca o
referinta la o clasa B poate fi initializata printr-un obiect al clasei D cu conditia ca B
sa fie o clasa de baza publica a lui D (in acest caz D este un B). Referintele sint utile
mai ales ca parametri formali. De exemplu:
struct B{ /*...*/ }; struct D : B{ /*...*/ }; int f(B&);
D a; f(a);

8.6.4 Tablouri de caractere

Un tablou de tip char poate fi initializat printr-un sir: caractere succesive din sir
initializeaza membri tabloului. De exemplu:
char msg[] = "Syntax error on line %s\n";
arata un tablou de caractere ai carui membri se initializeaza cu un sir. Sa observam ca
sizeof[msg] == 25.
8.7 Nume de tip

Uneori (pentru a specifica explicit conversiile de tip si ca un argument al lui sizeof


sau new) se cere sa se furnizeze numele unui tip de data. Aceasta se realizeaza
utilizind un type_name, care in esenta este o declaratie pentru un obiect de acel tip
care omite numele obiectului.
type_name:
type_specifier abstract_declarator abstract_declarator:
empty
*abstract_declarator
abstract_declarator(argument_declaration_list)
abstract_declarator[constant_expression_opt]
(abstract_declarator)
Este posibil sa se identifice unic locatia din abstract_declarator unde ar apare
identificatorul daca constructor ar fi un declarator intr-o declaratie. Tipul denumit
este apoi acelasi cu tipul identificatorului ipotetic. De exemplu:
int
int*
int* [3]
int (*)[3]
int* ()
int (*)()

numesc respectiv tipurile "intreg", "pointer spre intreg", "tablou de trei pointeri spre
intreg", "pointer spre un tablou de trei intregi", "functie care returneaza un pointer
spre intreg" si "pointer spre o functie care returneaza un intreg".

8.8 Typedef

Declaratiile care contin specificatorul (decl_specifier) typedef definesc identificatori


care pot fi utilizati mai tirziu ca si cum ei ar fi fost cuvinte cheie de tipuri care
numesc tipuri fundamentale sau derivate.
typedef_name: identifier
In domeniul unei declaratii care implica typedef, fiecare identificator care apare ca
parte a oricarui declarator devine sintactic echivalent cu cuvintul cheie care numeste
tipul asociat cu identificatorul in modul descris in &8.4. Specificatorul typedef poate
sa nu fie utilizat pentru un membru al clasei. Numele unei clase sau al unei enumerari
este de asemenea un nume typedef. De exemplu, dupa:
typedef int MILES, *KLICKSP;
struct complex{ double re, im };

constructiile:
MILES distance;
extern KLICKSP metricp;
complex z, *zp;

sint toate declaratii legale; tipul lui distance este int, cel al lui metricp este "pointer
spre int". Typedef nu introduce tipuri noi, ci numai sinonime pentru tipurile care ar
putea fi specificate in alt mod. Astfel in exemplul de mai sus distance este
considerata sa aiba exact ace- lasi tip ca multe alte obiecte int. O declaratie de clasa
introduce un tip nou. De exemplu:
struct X{ int a; };
struct Y{ int a; };
X a1;
Y a2;
int a3;

declara trei variabile de trei tipuri diferite.


O declaratie de forma:
name_declaration:
aqqr identifier; enum identifier;
specifica faptul ca un identificator este numele unei anumite clase sau o enumerare
(posibil nedefinite inca). Astfel de declaratii admit declaratia claselor care se refera
una la alta.
class vector; class matrix{//......
friend vector operator*(matrix&, vector&);
};
class vector{
//......
friend vector operator*(matrix&, vector&);
};

8.9 Nume de functii supraincarcate

Cind se specifica declaratii de functii diferite pentru un singur nume, se spune ca


numele acela este supraincarcat. Cind se utilizeaza acel nume, selectia se face prin
compararea tipurilor argumentelor actuale (efective) cu tipurile argumentelor
formale. Gasirea functiei care este apelata se realizeaza in trei pasi separati:
1. Cauta o corespondenta exacta si o utilizeaza daca ea a fost gasita;
2. Cauta o corespondenta utilizind conversiile standard (&6.6-8) si utilizeaza
una din ele.
3. Cauta o corespondenta folosind conversii definite de utilizator (&8.5.6).
Daca se gaseste un set unic de conversii, se foloseste acesta. Zero, un caracter (char)
sau short se considera o cores- pondenta exacta pentru un argument de tip double.
Numai conversiile urmatoare vor fi aplicate pentru un argument la o functie
supraincarcata: int spre long, int spre double, conversiile de pointer si referinta
(&6.7-&8). Pentru a supraincarca numele unei alte functii decit a unui membru sau
functie operator trebuie ca declaratia overload sa preceada orice declaratie a functiei
(vezi &8.1). De exemplu:
overload abs;
double abs(double);
int abs(int);
abs(1); //apeleaza abs(int);
abs(1.0); //apeleaza abs(double);

De exemplu :
class X{ ... X(int); };
class Y{ ... Y(int); };
class Z{ ... Z(char*); };
overload int f(X), f(Y);
overload int g(X), g(Z);
f(1); //ilegal: f(X(1)) sau f(Y(1))
g(1); //g(X(1))
g("asdf"); //g(Z("asdf"))

Operatorul adresa & poate fi aplicat numai la un nume supra- incarcat intr-o asignare
sau o initializare, unde tipul asteptat determina care functie ia adresa. De exemplu :
int operator=(matrix&, matrix&);
int operator=(vector&, vector&);
int(*pfm)(matrix&, matrix&) = &operator=;
int(*pfv)(vector&, vector&) = &operator=;
int(*pfx)( /*...*/ ) = &operator=; //eroare

8.10 Declaratii de enumerare


Enumerarile sint tipuri int cu constante denumite.
enum_specifier:
enum identifier_opt {enum_list} enum_list:
enumerator
enum_list, enumerator
enumerator:
identifier
identifier = constant_expression
Identificatorii dintr-o lista de enumerare (enum_list) sint declarati ca si constante si
pot apare oriunde se cere o constanta. Daca nu apar enumeratori cu =, atunci valorile
constantelor corespunzatoare incep la 0 si se maresc cu 1 in timp ce declaratia se
citeste de la stinga spre dreapta. Un enumerator cu = da identificatorului asociat
valoarea indicata; identificatorii urmatori continua marirea cu 1 de la valoarea
asignata.
Numele enumeratorilor trebuie sa fie distincte pentru variabilele ordinare. Numele
enumeratorilor cu constante diferite trebuie de asemenea sa fie distincte.
Valorile enumeratorilor nu este necesar sa fie distincte.
Rolul identificatorului enum_specifier este analog cu cel al numelui de clasa; el
numeste o enumerare particulara. De exemplu:
enum color{read, yellow, green=20, blue}; color col=read; color* cp=&col; if(*cp
== blue)
//......

face ca, color sa fie un tip ce descrie diferite culori si apoi declara col ca un obiect de
acel tip si cp ca un pointer spre un obiect de acel tip. Valorile posibile sint din setul
{0,1,20,21}.

8.11 Declaratia asm

O declaratie asm are forma:


asm{string};
Sensul unei declaratii asm nu este definit. De obicei se foloseste pentru a transfera
informatie prin compilator la un asamblor.

9 Instructiuni

Instructiunile se executa in secventa, daca nu se indica altfel.


9.1 Instructiunea expresie

Majoritatea instructiunilor sint instructiuni expresie, care au forma:


expression;
De obicei instructiunile expresie sint atribuiri sau apeluri de functie.

9.2 Instructiunea compusa (blocul)

Diferite instructiuni pot fi utilizate unde se asteapta una singura furnizind o


instructiune compusa (de asemenea, aceasta se numeste "bloc"):
compound_statement:
{statement_list_opt} statement_list:
statement
statement statement_list

Sa observam ca o declaratie este un exemplu de o instructiune (&9.14).

9.3 Instructiune conditionala

Cele doua forme ale instructiunii conditionale sint:


if(expression) statement
if(expression) statement else statement Expresia trebuie sa fie de tip aritmetic sau
pointer sau un tip de clasa pentru care este definita o conversie spre tipul pointer sau
aritmetic (vezi &8.5.6). Expresia este evaluata si daca nu este zero, se executa prima
subinstructiune. Daca se utilizeaza else se executa cea de a doua subinstructiune daca
expresia este zero. De obicei ambiguitatea "else" se rezolva conectind un else cu
ultimul if intilnit.

9.4 Instructiunea WHILE

Instructiunea while are forma:


while(expression) statement
Subinstructiunea se executa repetat atita timp cit valoarea expresiei ramine diferita de
zero. Testul are loc inaintea fiecarei executii a instructiunii. Expresia se trateaza ca si
in instructiunea conditionala (&9.3).

9.5 Instructiunea DO
Instructiunea do are forma:
do statement while(expression);
Subinstructiunea se executa repetat pina cind valoarea devine zero. Testul are loc
dupa fiecare executie a instructiunii. Expresia este tratata ca intr-o instructiune
conditionala (9.3).

9.6 Instructiunea FOR

Instructiunea for are formatul:


for(statement_1; expression_1 opt; expression_2 opt)
statement_2;
Aceasta instructiune este echivalenta cu:
statement_1
while(expression_1)
{
statement_2
expression_2;
}

cu exceptia faptului cind in statement_2 se executa o instructiune continue, caz in


care se executa expression_2 inainte de a se executa expression_1. Prima instructiune
specifica initiali- zarea pentru ciclu; prima expresie exprima un test care se face
inaintea oricarei iteratii si se iese din ciclu cind expresia devine zero; cea de a doua
expresie adesea exprima o incrementare care se face dupa fiecare iteratie.
Oricare din expresii sau chiar si ambele pot fi vide. Lipsa expresiei expression_1 face
while echivalent cu while(1). Sa observam ca daca statement_1 este o declaratie,
domeniul numelui declarat se extinde pina la sfirsitul blocului care include
instructiunea for.

9.7 Instructiunea SWITCH

Instructiunea switch transfera controlul unei instructiuni dintr-un set de instructiuni,


in functie de valoarea unei expresii. Are forma:
switch(expression) statement
Tipul expresiei expression trebuie sa fie aritmetic sau pointer. Orice instructiune din
statement poate avea o etichetata case sau mai multe, dupa cum urmeaza:
case constant_expression :
unde expresia constanta trebuie sa fie de acelasi tip cu expresia din switch; de obicei
se fac conversii aritmetice. Nu se poate sa existe doua constante identice de tip case
in acelasi switch. Expresiile constante se definesc in &12.
Este posibil sa existe o eticheta de forma:
default:
Cind se executa instructiunea switch, se evalueaza expresia ei si se compara cu
fiecare constanta case. Daca una din constantele case este egala cu valoarea expresiei
atunci controlul este transferat instructiunii etichetate cu constanta respectiva case.
Daca nu exista nici o constanta case care sa aiba aceeasi valoare cu cea a expresiei si
daca exista eticheta default, atunci controlul este transferat instructiunii etichetate cu
default. Daca nu exista nici o constanta case care sa aiba aceeasi valoare cu expresia
si nu exista eticheta default, atunci nici una din instructiunile din switch nu se
executa; case si default nu alte- reaza controlul, care continua nestinjenit de la o
eticheta la alta. Pentru a iesi din switch vezi break (&9.8). De obicei instructiunea
care este subiectul unui switch este compusa. Pot apare declaratii in capul acestei
instructiuni, dar initializarile variabilelor registru si automatice sint inefective.

9.8 Instructiunea BREAK

Instructiunea:
break;
are ca actiune terminarea celei mai interioare instructiuni while, do, for sau switch;
controlul este transferat la instructiunea urmatoare.

9.9 Instructiunea CONTINUE

Instructiunea:
continue;
transfera controlul la partea de continuare a ciclului celui mai interior care o contine;
adica la sfirsitul ciclului. Mai exact, in fiecare din instructiunile:
while(......)
{......
contin: ;
}
for(......)
{
......
contin: ;
}
do{
......
contin: ;
}while(......);
O instructiune continue este echivalenta cu goto contin (dupa contin este o
instructiune vida, &9.13).

9.10 Instructiunea RETURN

O functie revine la functia care a apelat-o prin intermediul instructiunii return, care
are una din formele:
return;
return expresie;
Prima forma poate fi utilizata numai in functii care nu returneaza o valoare, adica o
functie care returneaza o valoare de tip void. Cea de a doua forma poate fi utilizata
numai in functii care returneaza o valoare; valoarea expresiei este returnata la functia
care a facut apelul. Daca este necesar, expresia se converteste ca si intr-o initializare
spre tipul functiei in care ea apare. Atingerea sfirsitului unei functii este echivalenta
cu o instructiune return fara valoare returnata.

9.11 Instructiunea GOTO

Controlul poate fi transferat neconditionat prin instructiunea:


goto identifier;
unde identifier trebuie sa fie o eticheta (&9.12) localizata in functia curenta. Nu este
posibil sa se transfere controlul peste o declaratie cu initializator, exceptind
transferarea controlului peste un bloc interior fara a intra in el.

9.12 Instructiunea etichetata

Orice instructiune poate fi precedata de o eticheta de forma:


identifier:
care serveste sa declare pe identifier ca eticheta. Singura utilizare a unei etichete este
de a fi utilizata intr-o instructiune goto. Domeniul unei etichete este functia curenta,
excluzind orice subbloc in care acelasi identificator este redeclarat (vezi &4.1).

9.13 Instructiunea NULL

Instructiunea null are forma:

;
O instructiune null este utila pentru a introduce o eticheta inainte de } a unei
instructiuni compuse sau sa furnizeze un corp nul pentru o instructiune ciclica cum ar
fi while.

9.14 Instructiunea declarativa

O instructiune declarativa se utilizeaza pentru a introduce un identificator nou intr-un


bloc; ea are forma:
declaration_statement: declaration
Daca un identificator introdus printr-o declaratie a fost in prealabil declarat intr-un
bloc exterior, declaratia externa este ascunsa pe domeniul blocului, dupa care el isi
reia existenta. Orice initializari ale variabilelor auto si register se fac ori de cite ori se
executa instructiunile declarative ale lor. Este posibil sa se intre intr-un bloc dar nu
asa incit sa nu se execute initializarile; vezi &9.11.
Initializarile variabilelor cu clasa de memorie static (&4.4) se fac numai o data si
anume cind incepe executia programului.

10 Definitii de functii

Un program consta dintr-un sir de declaratii. Codul pentru orice functie poate fi
dat numai in afara oricarui bloc sau intr-o declaratie de clasa. Definitiile de functii
au forma:
function_definition:
decl_specifiers_opt fct_declarator base_initializer_opt fct_body
decl_specifiers register, auto, typedef pot sa nu fie utilizati, iar friend si virtual pot fi
utilizate numai intr-o definitie de clasa (&8.5). Un declarator de functie este un
declarator pentru o "functie care returneaza ..." (&8.4). Argumentele formale sint
domeniul celui mai extern bloc al corpului functiei. Declaratorii de functie au forma:
fct_declarator:
declarator(argument_declaration_list) Daca un argument este specificat register,
argumentul actual corespunzator va fi copiat, daca este posibil, intr-un registru din
afara setului functiei. Daca o expresie constanta este specificata ca un initializator
pentru un argument aceasta valoare se utilizeaza ca o valoare implicita a
argumentului.
Corpul functiei are forma:
fct_body:
compound_statement
Exemplu complet de definitie de functie.
int max(int a, int b, int c)
{
int m = (a>b) ? a : b;
return (m>c) ? m : c;
}
Aici int este specificatorul de tip; max(int a,int b, intc) este fct_declarator; { ... } este
corpul functiei.
Intrucit in contextul unei expresii un nume de tablou (in particular ca argument
efectiv) se ia drept pointer la primul element al tabloului, declaratia de argument
formal "array of..." se ajusteaza pentru a fi citit ca "pointer la ...".
Initializatorii pentru o clasa de baza si pentru membri pot fi specificati in definitia
constructorului. Aceasta este cel mai util pentru obiectele de clasa, constante si
referinte unde semanticile de initializare si asignare difera. Un initializator al bazei
are forma:
base_initializer:
:member_initializer_list member_initializer_list:
member_initializer
member_initializer, member_initializer_list
member_initializer:
identifier_opt(argument_list_opt)
Daca identifier este prezent intr-un member_initializer argumentul lista se utilizeaza
pentru clasa de baza. De exemplu:
struct base{
base(int); // ...
};
struct derived : base{
derived(int); base b; const c;
};
derived::derived(int a) : (a+1), b(a+2), c(a+3){ /* ... */ }
derived d(10);
Intii, se apeleaza constructorul clasei de baza base::base() pentru obiectul d cu
argumentul 11; apoi constructorul pentru membrul b cu argumentul 12 si
constructorul pentru membrul c cu argumentul 13; apoi se executa corpul
derived::derived() (vezi &8.5.5). Ordinea in care se apeleaza constructorii pentru
membri este nespecificata. Daca clasa de baza are un constructor care poate fi apelat
fara argumente, nu se furnizeaza nici o lista de argumente. Daca membrul unei clase
are un constructor care poate fi apelat fara argumente, atunci nu este necesar sa se
furnizeze nici un argument pentru acel membru.

11 Linii de control ale compilatorului


Compilatorul contine un preprocesor capabil de macrosubstitutie, compilare
conditionala si incluziune de fisiere denumite. Liniile care incep cu # comunica cu
acest preprocesor. Aceste linii au sintaxa independenta de restul limbajului; ele pot
apare oriunde si au efecte independente de domeniu si sint valabile pina la sfirsitul
fisierului cu programul sursa. Sa observam ca definitiile const si inline sint o alta
alternativa fata de multe utilizari ale lui #define.

11.1 Substitutia de siruri

O linie de control a compilatorului de forma:


#define identifier token_string
face ca preprocesorul sa inlocuiasca intrarile ulterioare ale lui identifier cu sirul
token_string dat. Punctul si virgula din token_string sau de la sfirsitul lui sint parte a
sirului de substitutie. O linie de forma:
#define identifier(identifier, ..., identifier) token_string
unde nu exista spatiu intre identificator si '(', este macrodefinitie cu argumente.
Intrarile ulterioare ale primului identificator urmat de '(' si de siruri delimitate prin
virgula si terminate prin ')' se inlocuiesc prin token_string din definitie. Fiecare
aparitie a unui identificator mentionat in lista argumentelor formale a definitiei se
inlocuieste prin sirul corespunzator de la apel. Argumentele efective din apel sint
sirurile separate prin virgule; virgulele din sirurile incluse intre ghilimele nu separa
argumente. Numarul argumentelor formale si reale trebuie sa fie acelasi. Sirurile si
constantele caracter din token_string sint analizate pentru descoperirea argumentelor
formale. O definitie lunga poate fi continuata pe o alta linie utilizind \ la sfirsitul
liniei de continuat. O linie de forma:
#undef identifier
face ca definitia preprocesor a lui identifier sa fie anulata.

11.2 Incluziune de fisiere

O linie de control a compilatorului de forma:


#include "filename"
face ca linia respectiva sa fie inlocuita prin continutul fisierului filename. Fisierul
denumit este cautat intii in directorul fisierului sursa, apoi intr-o secventa de locuri
specificate standard. O linie de control de forma:
#include <filename>
cauta filename numai in locurile specificate standard si nu si in directorul fisierului
sursa (cum se specifica locurile standard nu este parte a limbajului).
Directivele #include pot fi imbricate.
11.3 Compilarea conditionata

O linie de control a compilatorului de forma:


#if expression
verifica daca expresia este diferita de zero. Expresia trebuie sa fie o expresie
constanta (&12). In plus fata de operatiile obisnuite din C++ se poate utiliza un
identificator unar. Acesta cind se aplica la un identificator, atunci valoarea lui este
diferita de zero daca identificatorul respectiv a fost definit utilizind #define si nu s-a
utilizat ulterior pentru el #undef; altfel valoarea lui este zero.
O linie de control de forma:
#ifdef identifier
verifica daca identifier este definit curent in preprocesor, adica daca el a fost obiectul
unei linii de control #define.
O linie de control de forma:
#ifndef identifier
verifica daca identifier este nedefinit curent in preprocesor. Toate cele trei forme sint
urmate de un numar arbitrar de linii, care pot contine si o linie de control:
#else
si apoi de linia de control:
#endif
Daca conditia verificata este adevarata, atunci liniile dintre #else si #endif sint
ignorate. Daca conditia verificata este falsa, atunci toate liniile dintre cea de test si
#else sau, in lipsa lui #else, #endif sint ignorate. Aceste constructii pot fi imbricate.

11.4 Linie de control

In beneficiul altor preprocesoare care genereaza programe C++, o linie de forma:


#linie constant "filename"
face ca, compilatorul sa considere ca numarul de linie al liniei sursa urmatoare se da
printr-o constanta, iar fisierul de intrare curent este denumit prin identificator. Daca
identificatorul lipseste, numele fisierului nu se schimba. Aceasta se face pentru a
elimina erorile.

12 Expresii constante

In diferite locuri C++ cere expresii care se evalueaza ca o constanta: cum ar fi


limitele unui tablou (&8.4), expresiile case (&9.7) argumentele implicite ale
functiilor (&8.4) si initializatorii (&8.6). In ultimul caz expresia poate implica numai
constante intregi, constante caracter, constante enumerative, valori const care nu sint
agregate initializate cu expresii constante si expresii sizeof care pot fi legate prin
operatorii binari:

+ - * / % & | ^ <<
>> == != < > <= >= && ||

sau cei unari:

+ - ~ !

sau prin operatorul ternar:

?:

Parantezele pot fi utilizate pentru grupari, dar nu pentru apeluri de functii.


In alte cazuri expresiile constante pot de asemenea sa contina operatorul unar &
aplicat la obiecte statice sau externe, sau la tablouri statice sau externe indexate cu o
expresie constanta. Unarul & poate fi aplicat implicit prin aparitia unui tablou fara
indici sau a unei functii. Regula de baza este ca initializatorii trebuie evaluati sau ca o
constanta sau ca adresa a unui obiect declarat in prealabil static sau extern + sau - o
constanta. O posibilitate mai mica este atinsa pentru o expresie constanta dupa #if;
nume declarate const, expresii sizeof si constante enumerative nu sint admise.

13 Consideratii de portabilitate

Anumite parti ale lui C++ sint inerent dependente de masina. Urmarind lista
necazurilor potentiale, bulinele nu inseamna ca vor apare toate "necazurile", dar se
sublinieaza unele dintre principalele necazuri. Caracteristicile curate hardware cum
este marimea unui cuvint, proprietatile aritmeticii flotante si impartirea intreaga in
practica au dovedit ca acestea nu constituie prea mult o problema. Alte aspecte ale
hardware-ului sint reflectate in diferite implementari. Unele dintre acestea, in
particular extensia de semn (care converteste un caracter negativ intr-un intreg
negativ) si ordinea in care octetii sint plasati in cuvint este o pacoste care trebuie
privita cu multa grija. Majoritatea necazurilor celorlalte reprezinta doar probleme
minore.
Numarul variabilelor registru care pot fi in realitate plasate in registrii variaza de la
masina la masina, asa cum este de fapt si cu setul tipurilor valide. Nu mai putin,
toate compilatoarele fac lucruri proprii masinii pentru care el a fost construit;
declaratiile de variabile registru incorecte sau in exces sint ignorate.
Ordinea evaluarii argumentelor functiilor nu este specificata de catre limbaj. Aceasta
ordine este de la dreapta la stinga pentru unele masini si de la stinga la dreapta pentru
altele.
De cind constantele caracter sint obiecte reale ale tipului int, sint permise si
constantele caracter multi-caracter. Implementarea specifica este foarte dependenta
de masina deoarece ca- racterele sint asignate de la stinga la dreapta pentru unele
masini si de la dreapta la stinga pentru altele.

14 Sumar de sintaxa

Acest sumar al sintaxei C++ se intentioneaza sa fie un ajutor pentru intelegerea


limbajului. Ceea ce se prezinta nu sint instructiuni exacte ale limbajului.

14.1 Expresii

expression:
term
expression binary_operator expression
expression ? expression : expression
expression_list
expression_list:
expression
expression_list, expression
term:
primary_expression
unary_operator term
term++
term--
sizeof expression
sizeof (type_name)
(type_name) expression
simple_type_name (expression_list)
new type_name initializer_opt
new (type_name)
delete expression
delete [expression] expression
special_operator:
() []
free_store_operator: one of
new delete abstract_declarator:
empty
*abstract_declarator
abstract_declarator (argument_declaration_list) abstract_declarator
[constant_expression_opt]
simple_type_name:
typedef_name
char
short
int
long
unsigned
float
double
void
typedef_name:
identifier
14.2 Declaratii
declaration:
decl_specifiers_opt declarator_list_opt;
name_declaration
asm declaration
name_declaration:
aggr identifier; enum identifier;
aggr:
class
struct
union
asm_declaration:
asm (string); decl_specifiers:
decl_specifier decl_specifiers_opt decl_specifier:
sc_specifier
type_specifier
fct_specifier
friend
typedef
type_specifier:
simple_type_name
class_specifier
enum_specifier
elaborated_type_specifier
const
sc_specifier:
auto
extern
register
static
fct_specifier:
inline
overload
virtual
elaborated_type_specifier:
key typedef_name
key identifier
key:
class
struct
union
enum
declarator_list:
init_declarator
init_declarator, declarator_list
init_declarator:
declarator initializer_opt declarator:
dname
(declarator)
const_opt declarator
& const_opt declarator declarator (argument_declaration_list) declarator
[constant_expression_opt]
dname:
simple_dname
typedef_name::simple_dname
simple_dname:
identifier
typedef_name
~typedef_name
operator_function_name
conversion_function_name
operator_function_name:
operator operator conversion_function_name:
operator type argument_declaration_list:
arg_declaration_list_opt ..._opt arg_declaration_list:
arg_declaration_list, argument_declaration
argument_declaration
argument_declaration:
decl_specifiers declarator
decl_specifiers declarator = expression
decl_specifiers abstract_declarator
decl_specifiers abstract_declarator = expression
class_specifiers:
class_head { member_list_opt }
class_head { member_list_opt public:member_list_opt }
class_head:
aggr identifier_opt
aggr identifier:public_opt typedef_name
member_list:
member_declaration member_list_opt member_declaration:
decl_specifiers_opt member_declarator initializer_opt
function_definition;_opt
member_declarator:
declarator
identifier_opt:constant_expression
initializer:
= expression
= { initializer_list }
= { initializer_list, }
( expression_list )
initializer_list:
expression
initializer_list, initializer_list
{ initializer_list }
enum_specifier:
enum identifier_opt { enum list } enum_list:
enumerator
enum_list, enumerator
enumerator:
identifier
identifier = constant_expression
14.3 Instructiuni

compound_statement:
{ statement_list_opt } statement_list:
statement
statement statement_list
statement:
declaration
compound_statement
expression_opt;
if(expression) statement
if(expression) statement else statement while(expression) statement do statement
while(expression);
for(statement expression_opt;expression_opt) statement
switch(expression) statement
case constant_expression : statement
default : statement
break;
continue;
return expression_opt;
goto identifier;
identifier : statement;

14.4 Definitii externe

program:
external_definition
external_definition program
external_definition
funtion_definition
declaration
function_definition
decl_specifiers_opt fct_declarator base_initializer_opt
fct_body fct_declarator:
declarator(argument_declaration_list) fct_body:
compound_statement base_initializer:
:member_initializer_list member_initializer_list:
member_initializer
member_initializer, member_initializer_list
member_initializer:
identifier_opt (argument_list_opt)

14.5 Preprocesor

#define identifier token_string


#define identifier(identifier,..., identifier) token_string
#else
#endif
#if expression
#ifdef identifier
#ifndef identifier
#include "filename"
#include <filename>
#line constant "filename"
#undef identifier

15 Diferente fata de C

15.1 Extensii

Tipurile argumentelor unei functii pot fi specificate (&7.1) si vor fi verificate (&7.1).
Vor avea loc si conversiile de tip (&7.1). Aritmetica flotanta in simpla precizie poate
fi folosita pentru expresii flotante (&6.2).
Numele functiilor pot fi supraincarcate (&8.9). Operatorii pot fi supraincarcati
(&7.16, &8.5.11). Functiile pot fi substituite inline (&8.1).
Obiectele data pot fi constante (&8.4). Pot fi declarate obiecte ale tipului referinta
(&8.4, &8.6.3). Alocarea si dealocarea sint furnizate de operatorii new si
delete (&7.2).
Clasele pot furniza incapsularea datelor (&8.5.9), garanteaza initializarea (&8.6.2),
conversiile definite de utilizator (&8.5.6) si tipizarea dinamica prin folosirea
functiilor virtuale (&8.5.4). Numele unei clase sau enumerari este un nume de tip
(&8.5). Orice pointer poate fi asignat spre void* fara folosirea unei matrite
(&7.14).
O declaratie in interiorul unui bloc este o instructiune (&9.14).
Pot fi declarate reuniuni fara nume (&8.5.13).

15.2 Sumar de incompatibilitati

Multe constructii in C sint legale in C++, intelesul lor raminind neschimbat.


Exceptiile sint urmatoarele:
Programele C care folosesc unul din noile cuvinte cheie:
class const delete friend inline
new operator overload public signed
this virtual volatile
ca identificatori, nu sint corecte.
In C++ declaratia functiei f() inseamna ca f nu primeste argumente, pe cind in C
aceasta inseamna ca f poate lua argumente de orice tip. In C un nume extern poate fi
definit de mai multe ori, pe cind in C++ trebuie sa fie definit exact o data.
Numele de clase din C++ se afla in acelasi domeniu al numelor valabil si pentru
celelalte nume, lucru ilustrat in urmatoarele constructii:
int s;
struct s { /*...*/ };
void f() { int s; struct s a; }
void g() { ::s = 1; }

15.3 Anacronisme

Extensiile prezentate aici pot fi furnizate pentru a face mai usoara utilizarea
programelor C ca programe C++. Notati ca fiecare dintre aceste particularitati
prezinta aspecte neastepta- te. O implementare care furnizeaza aceste extensii de
asemenea poate furniza utilizatorului o cale de a se asigura ca aceste lucruri nu vor
apare in fisierul sursa.
Numele inca nedefinite pot fi utilizate intr-un apel ca si numele de functii. In acest
caz numele trebuie implicit declarat ca o functie ce returneaza int cu tipul
argumentului (...).
Cuvintul cheie void poate fi folosit pentru a indica faptul ca functia nu primeste
argumente;deci void este echivalent cu ().
Programe utilizind sintaxa C pentru definirea functiilor.
old_function_definition:
decl_specifiers_opt old_function_declarator
declaration_list fct_body old_function_declarator:
declarator (parameter_list) parameter_list:
identifier
identifier, identifier

de exemplu, functia:
max(a, b){ return (a<b) ? b : a; }
poate fi utilizata.
Daca o functie definita ca cea de mai sus nu are o declaratie anterioara, tipul
argumentelor ei vor fi (...), care sint neverificate. Daca, in schimb, functia este
anterior declarata, tipurile argumentelor ei trebuie sa corespunda cu cele din
declaratie.
Un punct poate fi folosit in locul operatorului de rezolutie de domeniu :: pentru a
specifica numele din definitia functiei membru. De exemplu:
int cl.fct(){ /* ... */ }
Acelasi nume poate fi declarat pentru ambele clase sau enumerari si obiectul data sau
functie, in acelasi scop.

You might also like