This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA
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:
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
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 la 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
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.p); }
//a se avea grija de s=s; delete p; p = new char[size=a.size]; strcpy(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
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
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
} 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)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 manager (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 intro 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"<
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
};
\ \ 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
}; 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
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:
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
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 (n1)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 dintro reuniune (&8.5.13). Un pointer spre o clasa derivata poate fi convertit implicit intrun 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
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) [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:
abstract_declarator
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 [constant_expression_opt] dname: simple_dname typedef_name::simple_dname simple_dname: identifier typedef_name ~typedef_name operator_function_name
(argument_declaration_list)
declarator
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
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 Pot fi declarate reuniuni fara nume (&8.5.13).
(&9.14).
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