Limbajul De Programare C++ (tutorial)

  • June 2020
  • PDF

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


Overview

Download & View Limbajul De Programare C++ (tutorial) as PDF for free.

More details

  • Words: 75,052
  • Pages: 292
THE C++ PROGRAMMING LANGUAGE BJARNE STROUSTRUP

ADDISON-WESLEY PUBLISHING COMPANY 1986

PREFATA =========== C++ este un limbaj de programare cu scop universal.El contine facilitati flexibile si eficiente pentru a definii tipuri noi. Programatorul poate partitiona o aplicatie in bucati manevrabile prin definiri de tipuri noi, care corespund mai bine la conceptele aplicatiei. Aceasta tehnica de construire a programului se numeste adesea abstractizare de date. Obiectele unui anumit tip definit de utilizator contin informatie de tip. Astfel de obiecte pot fi folosite convenabil in contextul in care tipul lor poate fi determinat in momentul compilarii. Programele utilizind

obiecte de astfel de tipuri se numesc adesea bazate pe obiecte. Cind se utilizeaza bine, aceste tehnici conduc la programe mai scurte, mai usor de inteles si mai usor de a le menine. Conceptul cheie din C++ este clasa. O clasa este un tip utilizator. Clasele furnizeaza ascunderea datelor, garantarea initializarii datelor, conversii de tip implicite pentru tipuri de date utilizator, tipuri dinamice, gestionarea memoriei controlate de utilizator si mecanismul de supraincarcare a operatorilor. C++ furnizeaza facilitati mai bune pentru verificarea tipului si pentru exprimarea modularitatii, decit o face limbajul C. De asemenea contine imbunatatiri care sint direct inrudite cu clasele, incluzind constantele simbolice, substitutia in linie a functiilor, argumente implicite ale functiilor care se supraincarca, operatori de gestionare a memoriei libere si un tip referinta.

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

Structura cartii ---------------Capitolul 1 este o trecere in revista rapida a caracteristicilor majore ale lui C++. La inceput sint caracteristici comune cu C. Cea de-a doua jumatate descrie facilitatile lui C++ pentru a definii tipuri noi. Capitolele 2, 3 si 4 descriu caracteristicile lui C+ + care nu sint implicate in definirea de tipuri noi. Deci se descrie subsetul lui C++ care este in esenta C. Informatia completa se da in manualul de referinta. Capitolele 5, 6, 7 descriu facilitatile lui C++ pentru a definii tipuri noi, trasaturi care nu fac parte

din C. Capitolul 5 prezinta conceptul de clasa, aratind cum obiectele de tip utilizator, pot fi initializate, accesate si in final eliminate. Capitolul 6 explica cum se pot definii operatorii unari si binari pentru un tip definit de utilizator, cum se pot face conversatii intre tipurile definite de utilizator si cum se specifica modul de creare, stergere si copiere a unei valori a unui tip definit de utilizator. Capitolul 7 descrie conceptul de clasa derivata, care permite programatorului sa construiasca clase mai complexe din unele mai simple, pentru a furniza interfete alternative pentru o clasa si a minui obiecte intr-o maniera eficienta si in deplina protectie, in contextul in care tipurile lor nu pot fi cunoscute la compilare. Capitolul 8 prezinta clasele ostream si istream furnizate pentru intrari si iesiri din biblioteca standard. Acest capitol prezinta o facilitate care este un exemplu real de utilizare a lui C++. In final este inclus manualul de referinta C++. Trimiterile se descriu astfel: &2.3.4 -> capitolul 2, sectiunea 3.4; &r8.5.5 -> trimitere in partea de referinta. Exercitiile sint gradate astfel: (*1) - exercitiul necesita 5 minute; (*2) - exercitiul necesita o ora; (*3) - exercitiul necesita o zi.

Observatii de proiectare -----------------------C++ nu are tipuri de date de nivel inalt si nici operatii primitive de nivel inalt. De exemplu, nu exista tipul matrice cu un operator de inversare sau tipul sir cu operatorul de concatenare. Daca un utilizator doreste

un astfel de tip, el poate fi definit. Defapt, definirea unui tip nou cu scopuri generale sau specific aplicative este scopul cel mai important al limbajului C++. Un tip definit de utilizator difera de unul predefinit numai in modul de definire si nu si in modul in care este utilizat.

Note istorice ------------C++ a aparut in vara anului 1983 (C cu clase). Incercari au fost facute inca din 1980. C++ inseamna C incrementat. C++ are ca scop principal scrierea de programe bune mai usor si mai placut pentru programatorul individual. O sursa de inspiratie a fost Simula 67; conceptul de clasa a fost imprumutat de aici. C si ANSI C sint foarte apropiate pentru a fi un subset a lui C++. C++ a fost dezvoltat din C si cu foarte mici exceptii C ramine un subset a lui C++. Observatii filozofice --------------------Un limbaj de programare serveste la doua scopuri inrudite: el furnizeaza un mijloc pentru programator de a specifica actiuni de executat si un set de concepte pentru programator care sa fie utilizate cind se gindeste in legatura cu ceea ce poate fi facut. Primul aspect in mod ideal cere un limbaj ce este "strins legat de masina" asa incit toate aspectele unei masini sa fie manevrate simplu si eficient intr-un mod care sa fie rezonabil de clar pentru programator.

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

Ginduri despre programare in C++ Ideal sarcina de concepere a unui program este impartita in 3 stadii: primul consta in intelegerea clara a problemei, apoi identificare conceptelor cheie implicate intr-o solutie si in final exprimarea solutiei printr-un program. Totusi, detaliile problemei si conceptele unei solutii adesea devin clar intelese numai prin efortul de a le exprima intr-un program; acesta este motivul alegerii limbajului de programare. In cele mai multe aplicatii exista concepte care nu sint reprezentate usor intr-un program nici printr-un tip fundamental si nici printr-o functie fara date statice asociate. Dindu-se un astfel de concept, se declara o clasa pentru a-l reprezenta in program. O clasa este un tip; adica, ea specifica cum obiectele din clasa se dezvolta: cum se creaza, cum pot fi manipulate, cum se anihileaza. O clasa de asemenea specifica cum se reprezinta obiectele, dar la un stadiu mai initial al proiectarii programu-lui aceasta nu

trebuie sa fie o conceptie majora. Cheia scrierii unui program bun este de a proiecta clasele in asa fel incit fiecare, in mod clar, sa reprezinte un singur concept. Adesea aceasta inseamna ca programatorul trebuie sa se concetreze asupra problemelor: cum se creaza obiectele din aceasta clasa? se poate ca obiectele din aceasta clasa sa fie copiate si/sau distruse? ce operatii pot fi facute cu astfel de obiecte? Daca nu sint raspun-suri bune la astfel de intrebari, conceptul probabil ca nu a fost clar definit si va trebui sa ne mai gindim asupra lui. Conceptele cu care este mai usor sa ne ocupam sint cele care au un formalism matematic traditional: numere de toate felurile, multimi, forme geometrice, etc.. Se cuvine sa fie biblioteci standard de clase care sa reprezinte astfel de concepte. Unul dintre cele mai puternice instrumente intelectuale pentru tratarea complexitatilor este ordonarea ierarhica; adica organizarea conceptelor inrudite intr-o structura de arbore cu cel mai general concept in radacina. In C++ clasele derivate reprezinta o astfel de structura. Un program poate fi adesea organizat ca o multime de arbori.

Reguli -----Iata citeva reguli care trebuiesc considerate cind invatam C++. [1] Cind programam, noi cream o reprezentare concreta a ideilor ce constituie solutia noastra la o anumita problema. Structura programului reflecta acele idei atit de direct cit este posibil: [a] Daca noi putem sa ne gindim la "el" ca la o idee separata, sa-l facem o clasa.

[b] Daca noi putem sa ne gindim la "el" ca la o entitate separata, sa-l facem obiect al unei anumite clase. [c] Daca doua clase au ceva seminificativ in comun, aceasta se face o clasa de baza. Majoritatea claselor din pro-gramul nostru vor avea ceva in comun: au o clasa de baza universala si ea trebuie proiectata cu multa atentie. [2] Cind noi definim o clasa care nu implementeaza o entitate matematica ca o matrice sau un numar complex sau un tip de nivel inferior ca o lista inlantuita: [a] Sa nu se utilizeze date globale. [b] Sa nu se utilizeze functii globale (care nu sint membri).

[c] Sa nu se utilizeze membri ale datelor publice.

[d] Sa nu se utilizeze frati, exceptind cazul in care ei se folosesc pentru a elimina [a], [b] sau [c]. [e] Sa nu se faca acces direct la membri de date a altor obiecte. [f] Sa nu se puna un tip 'cimp' intr-o clasa; sa se utilizeze functii virtuale. [g] Sa nu se utilizeze functii inline; exceptind cazul unei optimizari semnificative.

CUPRINS ======= NUME PAG. ========================================================= ======== 1 1

CAP.1 === "TUTORUL" LUI C++ 1.1.

Introducere 1.1.1. Iesire

1

1.1.2. Compilare

1

1.1.3. Intrare

1 2 2

1.2.

Comentariu

1.3.

Tipuri si Declaratii 1.3.1. Tipuri fundamentale

2

1.3.2. Tipuri derivate

3 3

1.4.

Expresii si Instructiuni

5 6 7 8 9

1.5.

Functii

1.6.

Structura programului

1.7.

Clase

1.8.

Operatorul overloading

1.9.

Referinte

10

1.10. Constructori

11

1.11. Vectori

13

1.12. Expandare inline

13

1.13. Clase derivate

15

1.14. Mai mult despre operatori

17

1.15. Prieteni (Friends)

18

1.16. Vectori generici 1.17. Vectori polimorfici

18 20

1.18. Functii virtuale CAP.2 === DECLARATII SI CONSTANTE

21 21 22 24 24

2.1. Declaratii 2.1.1. Domeniu 2.1.2. Obiecte si Lvalori 2.1.3. Durata de viata

25 25 26 27 28 29 30 31 32 34 36

2.2. Nume 2.3. Tipuri 2.3.1. Tipuri fundamentale 2.3.2. Conversia implicita de tip 2.3.3. Tipuri derivate 2.3.4. Void 2.3.5. Pointeri 2.3.6. Vectori 2.3.7. Pointeri si Vectori 2.3.8. Structuri 2.3.9. Echivalenta tipurilor

NUME

PAG. ========================================================= ======== 2.3.10. Referinte 36 2.3.11. Registrii 39 2.4. Constante 40 2.4.1. Constante intregi 40

41

2.4.2. Constante in flotanta

41

2.4.3. Constante caracter

42

2.4.4. Siruri

43

2.4.5. Zero

43

2.4.6. Const

45

2.4.7. Enumerari

46

2.5. Salvarea spatiului

46

2.5.1. Cimpuri

47

2.5.2. Reuniuni

49

2.6. Exercitii CAP.3 === EXPRESII SI INSTRUCTIUNI

51 51 52 56 58 60 61 62 63

3.1. Un calculator de birou 3.1.1. Analizorul 3.1.2. Functia de intrare 3.1.3. Tabela de simboluri 3.1.4. Tratarea erorilor 3.1.5. Driverul 3.1.6. Argumentele liniei de comanda 3.2. Sumar de operatori

65

3.2.1. Paranteze rotunde

66

3.2.2. Ordinea de evaluare

67

3.2.3. Incrementare si Decrementare

68

3.2.4. Operatori logici pe biti

69

3.2.5. Conversia tipului

70

3.2.6. Memoria libera

73

3.3. Sumarul instructiunilor

74

3.3.1. Teste

76

3.3.2. Goto

77

3.4. Comentarii si Decalari

79

3.5. Exercitii CAP.4 === FUNCTII SI FISIERE

83 83 84 86 88 90 92 93

4.1. Introducere 4.2. Linkare 4.3. Fisiere antet 4.3.1. Fisier antet unic 4.3.2. Fisiere antet multiple 4.3.3. Ascunderea datelor 4.4. Fisiere si Module

93 95 95 95 96 97 98 99 100

4.5. Cum se construieste o biblioteca 4.6. Functii 4.6.1. Declaratii de functii 4.6.2. Definitii de functii 4.6.3. Transferul argumentelor 4.6.4. Valoarea returnata 4.6.5. Argumente vector 4.6.6. Argumente implicite 4.6.7. Nume de functii supraincarcate

NUME PAG. ========================================================= ======== 4.6.8. Numar nespecificat de argumente 102 4.6.9. Pointer spre functie 104 4.7. Macrouri 107 4.8. Exercitii 110 CAP.5 === CLASE 113 5.1. Introducere si privire generala 113 5.2. Clase si Membri 114

114 115 116 118 120 121 121 122 125 128 128 131 131 132 133 134 137 137 138 139

5.2.1. Functii membru 5.2.2. Clase 5.2.3. Autoreferinta 5.2.4. Initializare 5.2.5. Curatire (Stergere) 5.2.6. In linie 5.3. Interfete si Implementari 5.3.1. Implementari alternative 5.3.2. O clasa completa 5.4. Prieteni si Reuniuni 5.4.1. Prieteni 5.4.2. Calificarea numelor membre 5.4.3. Clase imbricate 5.4.4. Membri statici 5.4.5. Pointeri spre membri 5.4.6. Structuri si Reuniuni 5.5. Constructori si Destructori 5.5.1. Goluri 5.5.2. Memoria statica 5.5.3. Memoria libera

140 142 143 144 145 147

5.5.4. Obiectele clasei de membri 5.5.5. Vectori si Obiecte clasa 5.5.6. Obiecte mici 5.5.7. Goluri 5.5.8. Obiecte de dimensiune variabila 5.6. Exercitii

CAP.6 === OPERATOR SUPRAINCARCAT 149 6.1. Introducere 149 6.2. Functiile operator 150 6.2.1. Operatori binari si unari 151 6.2.2. Sensul predefinit al operatorilor 151 6.2.3. Operatori si Tipuri definite de utilizatori 152 6.3. Conversia de tip definita de utilizator 152 6.3.1. Constructori 153 6.3.2. Operatori de conversie 154 6.3.3. Ambiguitati 155 6.4. Constante 157 6.5. Obiecte mari 157 6.6. Asignare si Initializare 158

160 162 163 166

6.7. Indexare 6.8. Apelul unei functii 6.9. O clasa sir 6.10. Prieteni si Membri

NUME PAG. ========================================================= ======== 6.11. Goluri 167 6.12. Exercitii 168 CAP.7 === CLASE DERIVATE 171 7.1. Introducere 171 7.2. Clase derivate 172 7.2.1. Derivare 172 7.2.2. Functii membru 173 7.2.3. Vizibilitate 175 7.2.4. Pointeri 176 7.2.5. Ierarhizarea claselor 177 7.2.6. Constructori si Destructori 178 7.2.7. Cimpuri de tip 179

181 183 183 184 186 187 189 190 191 193 193 193 196 198 200 202

7.2.8. Functii virtuale 7.3. Interfete alternative 7.3.1. O interfata 7.3.2. O implementare 7.3.3. Cum sa o folosim 7.3.4. Tratarea erorilor 7.3.5. Clase generice 7.3.6. Interfete restrictive 7.4. Adaugarea la o clasa 7.5. Liste eterogene 7.6. Un program complet 7.6.1. Controlul ecranului 7.6.2. Biblioteca de figuri 7.6.3. Programul de aplicatie 7.7. Memoria libera 7.8. Exercitii

CAP.8 === STREAMS 205 8.1. Introducere 205 8.2. Iesire 206

206

8.2.1. Iesirea tipurilor predefinite 8.2.2. Iesirea tipurilor utilizator

207 208 209 212 213 213 214 214 215 215

217 218

8.2.4. Iesire formatata 8.2.5. O functie de iesire virtuala 8.3. Fisiere si Streamuri 8.3.1. Initializarea iesire

streamurilor

de

8.3.2. Inchiderea streamurilor de iesire 8.3.3. Deschiderea fisierelor 8.3.4. Copierea streamurilor 8.4. Intrari tipurilor

predefi-

8.4.2. Starile streamului 8.4.3. Introducerea tipurilor de utilizator 8.4.4. Initializarea intrare

219

PAG.

de

8.2.3. Citeva detalii de implementare

8.4.1. Introducerea nite 216

definite

NUME

definite

streamurilor

de

========================================================= ======== 8.5. Manipularea sirurilor 220 8.6. Blocare in bufer 221 8.7. Eficienta 223 8.8. Exercitii 224 MANUAL DE REFERINTA 227 1. Introducere 227 2. Conventii lexicale 227 2.1. Comentarii 227 2.2. Identificatori (Nume) 227 2.3. Cuvinte cheie 228 2.4. Constante 228 2.4.1. Constante intregi 228 2.4.2. Constante long explicite 228 2.4.3. Constante caracter 229 2.4.4. Constante flotante 229 2.4.5. Constante enumerative 229 2.4.6. Constante declarate 229 2.5. Siruri 230

230 230 231 231 232 232 232 232 233 233 234 234

2.6. Caracteristici hardware 3. Notatia sintactica 4. Nume si Tipuri 4.1. Domenii 4.2. Definitii 4.3. Linkare 4.4. Clase de memorie 4.5. Tipuri fundamentale 4.6. Tipuri derivate 5. Obiecte si Lvalori 6. Conversii 6.1. Caractere si Intregi 6.2. Flotante cizie

234 234 235 235 235 236 236

in simpla si dubla

6.3. Flotante si Intregi 6.4. Pointeri si Intregi 6.5. Intregi fara semn 6.6. Conversii aritmetice 6.7. Conversii de pointeri 6.8. Conversie de referinta

pre-

236 237 239

7. Expresii 7.1. Expresii primare 7.2. Operatori unari 7.2.1. Incrementare si Decrementare

239

7.2.2. Sizeof

240

7.2.3. Conversie explicita de tip

240

7.2.4. Memoria libera

241 242 242 243 243 244 244 244

7.3. Operatori multiplicatori 7.4. Operatori aditivi 7.5. Operatori de deplasare 7.6. Operatori relationali 7.7. Operatori de egalitate 7.8. Operatorul SI pe biti 7.9. Operatorul SAU-EXCLUSIV pe biti

NUME PAG. ========================================================= ======== 7.10. Operatorul SAU-INCLUSIV pe biti 244 7.11. Operatorul logic SI 244

245 245 245 246 246 247 247 247 247 248 249 250 251 253 254 255 256 257 258 259

7.12. Operatorul logic SAU 7.13. Operator conditional 7.14. Operatori de asignare 7.15. Operatorul virgula 7.16. Operatori de supraincarcare 7.16.1. Operatori unari 7.16.2. Operatori binari 7.16.3. Operatori speciali 8. Declaratii 8.1. Specificatori de clasa de memorie 8.2. Specificatori de tip 8.3. Declaratori 8.4. Intelesul ( sensul ) declaratorilor 8.4.1. Exemple 8.4.2. Tablouri, Pointeri si Indici 8.5. Declaratii de clasa 8.5.1. Membri statici 8.5.2. Functii membru 8.5.3. Clase derivate 8.5.4. Functii virtuale

259 260 261 261 262 263 264 264 264 265 265 266 266 267 268 269 269 270 271 272

8.5.5. Constructori 8.5.6. Conversii 8.5.7. Destructori 8.5.8. Memoria libera 8.5.9. Vizibilitatea numelor membri 8.5.10. Prieteni 8.5.11. Functii operator 8.5.12. Structuri 8.5.13. Reuniuni 8.5.14. Cimpuri de biti 8.5.15. Clase imbricate 8.6. Initializare 8.6.1. Liste initializatoare 8.6.2. Obiecte de clasa 8.6.3. Referinte 8.6.4. Tablouri de caractere 8.7. Nume de tip 8.8. Typedef 8.9. Nume de functii supraincarcate 8.10. Declaratii de enumerare

273 273 273 273 274 274 274 274 275 276 276

8.11. Declaratia ASM 9. Instructiuni 9.1. Instructiunea expresie 9.2. Instructiunea compusa (blocul) 9.3. Instructiunea conditionala 9.4. Instructiunea WHILE 9.5. Instructiunea DO 9.6. Instructiunea FOR 9.7. Instructiunea SWITCH 9.8. Instructiunea BREAK 9.9. Instructiunea CONTINUE

NUME PAG. ========================================================= ======== 9.10. Instructiunea RETURN 276 9.11. Instructiunea GOTO 277 9.12. Instructiunea etichetata 277 9.13. Instructiunea NULL 277 9.14. Instructiunea declarativa 277

278 279 280 280 281 281 282 282 283 283 284 286 286 287 287 287 288 288

10. Definitii de functii 11. Linii de control ale compilatorului 11.1. Substitutia de siruri 11.2. Incluziune de fisiere 11.3. Compilarea conditionata 11.4. Linie de control 12. Expresii constante 13. Consideratii de portabilitate 14. Sumar de sintaxa 14.1. Expresii 14.2. Declaratii 14.3. Instructiuni 14.4. Definitii externe 14.5. Preprocesor 15. Diferente fata de C 15.1. Extensii 15.2. Sumar de incompatibilitati 15.3. Anacronisme

CAPITOLUL 1 =========== "TUTORUL" LUI C++ ================= 1.1 Introducere -----------

1.1.1 Iesire -----#include <stream.h> main() { cout << "Hello, world\n"; } #include <stream.h> include declaratii pentru facilitatile standard de intrare/iesire aflate in stream.h. Operatorul << scrie cel de al doilea operand al sau peste primul. 1.1.2 Compilare Se apeleaza cu litere mari CC. Daca programul este in fisierul hello.c, atunci se compileaza si se executa ca mai jos: $CC hello.c $a.out Hello, world $ 1.1.3 Intrare #include <stream.h> main() //converteste inch in cm {int inch = 0; cout << "inches"; cin >> inch; cout << inch; cout << "in="; cout << inch*2.54; cout << "cm\n"; } Exemplu de executie $a.out inches = 12 12 in = 30.48 cm $ Ultimii 4 operatori pot fi scrisi: cout << inch << "in=" << inch*2.54 << "cm\n";

1.2 Comentariu Incepe prin /* si se termina prin */. Comentariu poate incepe prin // si se termina la sfirsitul liniei respective. 1.3 Tipuri si Declaratii Fiecare nume si fiecare expresie are un tip care determina operatiile care pot fi facute asupra lor. O declaratie este o instructiune care introduce un nume intr-un program. O declaratie specifica un tip pentru acel nume. Un tip defineste utilizarea numelui sau a unei expresii. Operatiile de forma +, -, * si / se definesc pentru intregi. Dupa ce s-a inclus stream.h, un int poate fi cel de al doilea operand pentru << cind primul argument este ostream. Tipul unui obiect determina nu numai care operatii pot fi aplicate asupra lui, ci de asemenea intelesul acelor operatii. De exemplu, instructiunea: cout << inch << "in=" << inch*2.54 << "cm\n"; trateaza corect cele 4 valori de iesire care sint diferite. C++ are citeva tipuri de baza si diferite moduri de a crea altele noi. 1.3.1 Tipuri fundamentale char short int long float double sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(float) <= sizeof(double) const float pi = 3.14; const char plus = '+'; Operatori aritmetici: + - (unari si binari ambii) * / % Operatori de comparare ca in C. double d = 1; int i = 1; d = d + i; i = d + i; 1.3.2 Tipuri derivate * -> pointer *const -> pointer constant & -> adresa [] -> vector () -> functie char* p; char *const q;

char v[10]; char c; //...... p = &c; // p pointeaza spre c 1.4 Expresii si Instructiuni ~ &(si) ^ | << >> se aplica la intregi = op= x = sqrt (a = 3*x) ++ -Cea mai frecventa forma a unei instructiuni este o instructiune expresie; ea consta dintr-o expresie urmata de un punct si virgula. a = b*3+c; cout << "go go go"; lseek(fd, 0, 2); Instructiunea VIDA: ; Blocuri: { a = b + 2; b++; } Instructiunea IF: #include <stream.h> main() //converteste din inch in cm si invers { const float fac = 2.54; float x, in, cm; char ch = 0; cout << "enter lenght:"; cin >> x >> ch; if(ch=='i')

{ //inch in = x; cm = x*fac; } else if(ch=='c') { //cm in = x/fac; cm = x; } else in = cm = 0; cout << in << "in=" << cm << "cm\n"; } Instructiunea SWITCH: switch(ch) { case 'i': in = x; cm = x*fac; break; case 'c': in = x/fac; cm = x; break; default: in = cm = 0; break; } Instructiunea WHILE: while(*p!=0) { *q = *p; q = q+1; p = p+1; } *q = 0; while(*p) *q++ = *p++; *q = 0; while(*q++ = *p++); Instructiunea FOR: for(int i=0; i<10; i++) q[i] = p[i];

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

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

Declaratia overload pow informeaza compilatorul ca se intentioneaza sa se foloseasca numele pow pentru mai mult decit o singura functie. Daca o functie nu returneaza o valoare trebuie sa se declare void: void swap(int* p, int* q) { int t = *p; *p = *q; *q = t; } 1.6

Structura programului

Un nume care se utilizeaza ca sa refere acelasi lucru in doua fisiere sursa trebuie sa fie declarat ca extern: extern double sqrt(double); extern istream cin; Este bine ca aceste declaratii sa se plaseze intr-un fisier si apoi acesta sa se includa. De exemplu, daca declaratia pentru sqrt() este in math.h atunci putem scrie: #include <math.h> //........ x = sqrt(4); Daca este intre paranteze unghiulare se include de obicei din /usr/include/CC. Altfel se folosesc ghilimele. #include "math1.h" #include "/usr/bs/math2.h" Mai jos un sir se defineste intr-un fisier si se scrie in altul. //header.h extern char* prog_name; extern void f(); Fisierul main.c este programul principal: #include "header.h" char* prog_name = "silly, but complete"; main() { f(); }

si fisierul f.c imprima sirul: #include <stream.h> #include "header.h" void f(){ cout << prog_name << "\n"; } La executie se obtine textul: $CC main.c f.c -o silly $silly silly, but complete $

1.7 Clase Sa vedem cum putem defini tipul ostream. Pentru a simplifica aceasta sarcina, presupunem ca s-a definit tipul streambuf pentru buferarea caracterelor. Un streambuf este in realitate definit in <stream.h> unde se gaseste de asemenea definitia reala a lui ostream. Definitia tipului utilizator (numit clasa in C++) contine o specificatie a datei necesare pentru a reprezenta un obiect de acest tip si o multime de operatii pentru a manevra astfel de obiecte. Definitia are doua parti: o parte privata ce pastreaza informatia care poate fi utilizata numai de implementatorul ei si o parte publica ce reprezinta o interfata cu utilizatorul: class ostream{ streambuf* buf; int state; public: void put(char*); void put(long); void put(double); }; Declaratiile dupa eticheta public specifica interfata; utilizatorul poate apela cele 3 functii put(). Declaratiile ce se gasesc inaintea etichetei public specifica reprezentarea unui obiect al clasei ostream. Numele buf si state pot fi utilizate numai prin functiile put() declarate in partea public. O clasa defineste un tip si nu un obiect data, asa ca pentru a utiliza un ostream noi trebuie sa declaram unul (in acelasi mod in care noi declaram variabilele de tip int): ostream my_out; Presupunind ca my_out a fost deja initializat in mod corespunzator, el poate fi utilizat acum astfel: my_out.put("Hello, world\n");

Operatorul se foloseste pentru a selecta un membru al clasei pentru un obiect dat al acelei clase. Aici functia membru put() se apeleaza pentru obiectul my_out. Functia poate fi declarata astfel: void ostream::put(char* p) { while(*p) buf.sputc(*p++); } unde sputc() este o functie care pune un caracter in streambuf. Prefixul ostream este necesar pentru a distinge put() a lui ostream de alte apeluri ale lui put(). Pentru a apela o functie membru, un obiect al clasei trebuie sa fie specificat. In functia membru, acest obiect poate fi implicit referentiat asa cum se face in ostream::put() de mai sus; in fiecare apel, buf se refera la membrul buf al obiectului pentru care se apeleaza functia. Este de asemenea posibil sa ne referim explicit la acel obiect printr-un pointer numit this. Intr-o functie membru al unei clase X, acesta este implicit declarat ca X* (pointer spre X) si initializat cu un pointer spre obiectul pentru care functia este apelata. Definitia lui ostream::put() ar putea fi scrisa astfel: void ostream::put(char* p) { while(*p) this->buf.sputc(*p++); } Operatorul -> se utilizeaza pentru a selecta un membru al unui obiect. 1.8

Operatorul overloading

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

while(*p) buf.sputc(*p++); return *this; } defineste operatorul <<, ca membru al clasei ostream, asa ca s<


Referinte

Ultima versiune a lui ostream din nefericire contine o eroare serioasa. Problema este ca ostream este copiat de doua ori pentru fiecare utilizare a lui <<: odata ca un argument si odata ca valoare returnata. Aceasta lasa starea nemodificata dupa fiecare apel. Este nevoie de o facilitate pentru a pasa un pointer la ostream mai degraba decit sa se paseze insasi ostream. Aceasta se poate realiza utilizind referintele. O referinta actioneaza ca un nume pentru un obiect; T& inseamna referinta la T. O referinta trebuie initializata si devine un nume alternativa pentru obiectul cu care este initializat. De exemplu: ostream& s1 = my_out; ostream& s2 = cout;

Referintele s1 si my_out pot fi utilizate acum in acelasi mod si cu acelasi inteles. De exemplu, atribuirea: s1 = s2; copiaza obiectul referit prin s2 (adica cout) in obiectul referit prin s1 (adica my_out). Membri se selecteaza utilizind operatorul punct: s1.put("don't use ->"); si daca utilizam operatorul adresa, primim adresa obiectului referit: &s1 == &my_out Prima utilizare evidenta a referintei este ca sa ne asiguram ca adresa unui obiect, mai degraba decit obiectul insusi, este pasata la o functie de iesire (aceasta se numeste in anumite limbaje apel prin referinta): ostream& operator<<(ostream& s, complex z) { return s << "(" << z.real << "," << z.imag << ")"; } Corpul functiei este neschimbat dar asignarea facuta lui s va afecta acum obiectul dat ca argument. In acest caz, returnindu-se o referinta de asemenea se imbunatateste eficienta, intru- cit modul evident de implementare a unei referinte este un pointer si un pointer este mai ieftin sa fie transferat decit o structura mare. Referintele sint de asemenea esentiale pentru definirea sirurilor de intrare deoarece operatorului input i se da variabila in care se citeste ca operand. Daca referintele nu sint utilizate, utilizatorul ar trebui sa paseze pointeri expliciti functiilor de intrare: class istream{ //........ int state; public: istream& operator>>(char&); istream& operator>>(char*); istream& operator>>(int&); istream& operator>>(long&); //........ }; Sa observam ca se folosesc doua operatii separate pentru a citi intr-o zona long si intr-o zona int si numai una pentru scriere. Motivul este ca un int poate fi convertit spre long prin regulile implicite de conversie. 1.10 Constructori Definirea lui ostream ca si clasa, face ca datele membru sa fie private. Numai o functie membru poate accesa membri privati, asa ca noi trebuie sa furnizam una pentru initializare. O astfel de functie se numeste constructor si se distinge avind acelasi nume ca si al clasei lui: class ostream{ //.......

ostream(streambuf*); ostream(int size, char* s); }; Aici se furnizeaza doi constructori. Unul ia un streambuf pentru o iesire reala iar celalalt ia o dimensiune si un pointer spre caractere pentru formatarea sirului. Intr-o declaratie, argumentul lista necesar pentru un constructor se adauga la nume. Noi putem declara acum streamuri astfel: ostream my_out(&some_stream_buffer); char xx[256]; ostream xx_stream(256,xx); Declaratia lui my_out seteaza nu numai cantitatea corespunzatoare de memorie ci de asemenea apeleaza si constructorul ostream::ostream(streambuf*) pentru a-l initializa cu argumentul &some_stream_buffer, care este un pointer spre un obiect potrivit al clasei streambuf. Declaratia functiei xx_stream() se trateaza similar, dar utilizeaza celalalt constructor. Declarind constructori pentru o clasa nu furnizam numai un mod de a initializa obiecte, ci de asemenea se asigura ca toate obiectele clasei vor fi initializate. Cind s-a declarat un constructor pentru o clasa, nu este posibil sa se declare o variabila a acelei clase fara a apela un constructor. Daca o clasa are un constructor care nu ia argumente, acel constructor va fi apelat daca nu se da nici un argument in declaratie. 1.11 Vectori Conceptul de vector construit in C++ a fost proiectat pentru a permite o eficienta maxima la executie si memorie minima. Este de asemenea, mai ales cind se utilizeaza impreuna cu pointerii, un instrument puternic pentru construirea unor facilitati de nivel inalt. Noi putem, totusi, sa ne plingem de faptul ca dimensiunea unui vector trebuie sa fie specificata ca o constanta, ca nu exista verificarea depasirii limitelor vectorilor, etc.. Un raspuns la aceste plingeri este: noi insine putem programa acest lucru. Sa vedem daca acesta este un raspuns rezonabil, cu alte cuvinte, sa testam facilitatile de abstractizare ale lui C++ incercind sa furnizam aceste caracteristici pentru tipurile de vectori proiectati de noi si sa observam dificultatile implicate, costurile implicate si comoditatea utilizarii tipurilor de vectori rezultate. class vector{ int* v; int sz; public: vector(int); //constructor ~vector(); //destructor int size(){return sz;} void set_size(int); int& operator[](int); int& elem(int i){return v[i];} };

Functia size() returneaza numarul de elemente al vectorului; adica, indicii trebuie sa fie in domeniul 0..size()-1. Functia set_size() este furnizata pentru a schimba acea dimensiune, elem() furnizeaza acces la membri fara a verifica indexul, iar operator[] furnizeaza acces cu verificarea limitelor. Ideea este de a avea clasa ca o structura de dimensiune fixa care controleaza accesul la memoria reala a vectorului, care este alocata prin constructorul vectorului utilizind operatorul new de alocare de memorie. vector::vector(int s) {if(s<=0) error("bad vector size"); sz = s; v = new int[s]; } Noi putem declara vectori foarte asemanator cu vectorii care sint construiti in limbajul insusi: vector v1(100); vector v2(nelem*2-4); Operatia de acces poate fi definita ca: int& vector::operator[](int i) { if(i<0 || sz<=i) error("vector index out of range"); return v[i]; } Returnind o referinta se asigura ca notatia [] poate fi utilizata de ambele parti a unei atribuiri: v1[x] = v2[y]; Functia ~vector() este un destructor; adica este o functie declarata pentru a fi apelata implicit cind obiectul unei clase iese in afara domeniului. Destructorul pentru o clasa C se numeste ~C. Daca noi o definim astfel: vector::~vector() { delete v; } ea va fi utilizata pentru a sterge operatorul si pentru a dezaloca spatiul alocat prin constructor, asa ca atunci cind un vector iese afara din domeniu, tot spatiul lui este eliberat si poate fi reutilizat.

1.12 Expandare inline O functie membru nu este mai costisitoare la apel decit o functie nemembru cu acelasi numar de argumente (sa ne amintim ca o functie membru totdeauna are cel putin un argument), iar apelul functiilor C++ este aproximativ tot atit de eficient ca si in alte limbaje. Totusi, pentru functiile extrem de mici, apelul poate sa iasa in evidenta. Daca este asa, noi am putea dori sa specificam o functie care sa expandeze in linie. In caz afirmativ, compilatorul va genera cod propriu pentru functie in locul apelului. Semanticile apelului ramin neschimbate. De exemplu, daca size() si elem() sint substituite in linie: vector s(100); //.......... i = s.size(); x = elem(i-1); este echivalent cu i = 100; x = s.v[i-1]; Compilatorul este destul de abil pentru a genera un cod care este tot atit de bun ca si cel care se obtine direct prin macro expandare. Evident, compilatorul are nevoie uneori sa foloseasca variabile temporare si alte citeva abilitati pentru a prezerva semanticile. Noi dorim o indicatie a faptului ca functia se expandeaza inline care sa preceada definitia ei. Aceasta este cuvintul cheie inline sau pentru o functie membru, pur si simplu prin includerea definitiei functiei in declaratiile clasei, asa cum s-a facut pentru size() si elem() in exemplul precedent. Cind se utilizeaza bine, functiile inline maresc simultan viteza de executie si descresc dimensiunea codului obiect. Totusi, functiile inline din declaratiile clasei pot incetini compilarea, asa ca ele trebuie sa fie eliminate cind ele nu sint necesare. Pentru ca substitutia inline sa fie un beneficiu semnificativ pentru o functie, functia trebuie sa fie foarte mica. 1.13 Clase derivate Acum sa definim un vector pentru care un utilizator poate defini limitele indexului. class vec:public vector{ int low, high; public: vec(int,int); int& elem(int); int& operator[](int); };

Definind vec ca public vector inseamna inainte de toate ca un vec este un vector. Adica tipul vec are toate proprietatile tipului vector si in plus cele specific declarate pentru el. Clasa vector se spune ca este clasa de baza pentru vec si invers vec se spune ca este derivat din vector. Clasa vec modifica clasa vector furnizind un constructor diferit care cere utilizatorului sa specifice cele doua limite ale indexului in schimbul dimensiunii si produce accesul propriu functiilor elem(int) si operator[](int).elem() a lui vec se exprima usor in termenii lui elem() al lui vector: int& vec::elem(int i){ return vector::elem(i-low); } Scopul operatorului :: este de a elimina o recursivitate infinita calculind vec::elem() din el insusi. Unarul :: se poate folosi pentru a ne referi la nume nelocale. Ar fi rezonabil sa declaram vec::elem() inline din motive de eficienta, dar nu este necesar, sau este posibil sa-l scriem asa ca el sa utilizeze direct membrul privat V al clasei vector. Functiile unei clase derivate nu au nici un acces special la membri privati ai clasei de baza propri. Constructorul poate fi scris astfel: vec::vec(int lb, int hb) : (hb-lb+1) { if(hb-lb < 0) hb = lb; low = lb; high = hb; } Constructia: (hb-lb+1) se utilizeaza pentru a specifica lista argument pentru constructorul clasei de baza vector::vector(). Acest constructor se apeleaza inaintea corpului lui vec::vec(). Iata un mic exemplu care poate fi executat daca se compileaza cu restul declaratiilor lui vector: #include <stream.h> void error(char* p) { cerr << p << "\n"; // cerr is the error output stream exit(1); } void vector::set_size(int) { /* dummy */ }

int& vec::operator[](int i) { if(i
1 1

2 2

3 3

4 4

5 5

6 6

7 7

8 8

9 9

1.14 Mai mult despre operatori O alta directie a dezvoltarii este de a furniza vectori cu operatii: class Vec::public vector{ public: Vec(int s) : (s){ } Vec(Vec&); ~Vec(){ } void operator=(Vec&); void operator*=(Vec&); void operator*=(int); //...... };

Observam modul in care constructorul pentru clasa derivata Vec::Vec() este definit pentru a transfera argumentele lui la constructorul pentru clasa de baza vector::vector(). Operatorul de atribuire este supraincarcat si poate fi definit astfel: void Vec::operator=(Vec& a) { int s = size(); if(s != a.size()) error("bad vector size for ="); for(int i=0; i<s; i++) elem(i) = a.elem(i); } Atribuirea de Vec-uri acum copiaza elemente, in timp ce atribuirea de vectori copiaza pur si simplu structura care controleaza accesul la elemente. Totusi, ultima se intimpla cind se copiaza un vector fara utilizarea explicita a operatorului de atribuire: (1) cind un vector este initializat prin atribuirea unui alt vector; (2) cind un vector se paseaza ca argument; (3) cind un vector se paseaza ca valoare returnata de la o functie. Pentru a cistiga control in aceste cazuri pentru vectorii Vec, noi definim constructorul Vec(Vec&): Vec::Vec(Vec& a) : (a.size()) { int sz = a.size(); for(int i=0; i<sz; i++) elem(i) = a.elem(i); } Acest constructor initializeaza un Vec ca o copie a altuia si va fi apelat in cazurile mentionate precedent. Pentru operatori de forma = si +=, expresia din stinga este evident speciala si se pare natural ca ei sa se implementeze ca operatii asupra obiectelor notate prin acea expresie. In particular, este posibil pentru ei sa se schimbe valoarea primului lor operand. Pentru operatori de forma + si -, operandul sting nu necesita o atentie speciala. Noi am putea, de exemplu, sa transferam ambele argumente prin valoare si totusi sa capatam o implementare corecta a adunarii vectorilor. Vectorii pot fi mari, asa ca, pentru a elimina copierea, operanzii lui + se transfera operatorului operator+() prin referinta: Vec operator+(Vec& a, Vec& b) { int s = a.size();

if(s != b.size()) error("bad vector size for +"); Vec sum(s); for(int i=0; i<s; i++) sum.elem(i) = a.elem(i) + b.elem(i); return sum; } Iata un mic exemplu care poate fi executat daca se compileaza cu declaratiile de vector prezentate anterior: #include <stream.h rel="nofollow"> void error(char* p) {cerr << p << "\n"; exit(1); } void vector::set_size(int){ /*...*/ } void vec::operator[](int i){ /*...*/ } main() {Vec a(10); Vec b(10); for(int i=0; i
1.15 Prieteni (Friends) Functia operator+() nu opereaza direct asupra reprezentarii unui vector; intr-adevar, nu ar putea, deoarece nu este un membru. Totusi, uneori este de dorit ca sa se admita ca functii nemembru sa aiba acces la partea privata a unui obiect de clasa. De exemplu, neexistind functia cu acces "neverificat", vector:: elem(), noi ar trebui sa fortam verificarea indexului i fata de limitele vectorului de trei ori de fiecare data cind se executa ciclul. Aceasta problema a fost eliminata aici, dar ea este tipica, asa ca exista un mecanism pentru o clasa care sa accepte accesul la partea sa privata pentru o functie nemembru. O declaratie a unei functii precedate prin cuvintul cheie friend este pur si simplu plasata in declaratia clasei. De exemplu, dindu-se: class Vec; // Vec este un nume de clasa

class vector{ friend Vec operator+(Vec, Vec); //........... }; noi putem scrie: Vec operator+(Vec a, Vec b) { int s = a.size(); if(s != b.size()) error("bad vector size for +"); Vec& sum = *new Vec(s); int* sp = sum.v; int* ap = a.v; int* bp = b.v; while(s--) *sp++ = *ap++ + *bp++; return sum; } Un aspect particular util al mecanismului de prieten (friend) este ca o functie poate fi prieten a doua sau mai multe clase. Pentru a vedea aceasta, sa consideram definirea unui vector si a unei matrici si apoi definirea functiei de inmultire. 1.16 Vectori generici Noi am dori, de exemplu, unul din acei vectori pentru tipul matrice pe care l-am definit. Din nefericire, C++ nu furnizeaza o facilitate pentru a defini o clasa vector cu tipul elementelor ca argument. Un mod de a proceda ar fi sa se copieze atit definitia clasei cit si functiile membru. Acest lucru nu este ideal, dar adesea este acceptabil. Noi putem utiliza macroprocesor pentru a mecaniza acel task. De exemplu, clasa vector este o versiune simplificata a unei clase care poate fi gasita intr-un fisier header standard. Noi am putea scrie: #include declare(vector, int); main() { vector (int)vv(10); vv[2] = 3; vv[10] = 4; //eroare de rang } Fisierul vector.h defineste macrouri asa ca declare(vector, int) se expandeaza spre declaratia unei clase vector foarte asemanatoare cu cea definita, iar

implement(vector, int) se expandeaza spre definitiile functiilor acelei clase.Intrucit implement(vec- tor, int) se expandeaza in definitii de functii, el poate fi utilizat numai odata intr-un program, in timp ce declare(vector, int) trebuie sa fie utilizat odata in fiecare fisier care manipuleaza acest fel de vectori intregi. declare(vector, int); //...... implement(vector, char); da un tip (separat) "vector de caractere". 1.17 Vectori polimorfici O alta varianta ar fi ca sa definim vectorul nostru si cu alte clase container in termenii unor pointeri la obiectele unei anumite clase: class common{ /*........*/}; class vector{ common** v; //...... public: cvector(int); common*& elem(int); common*& operator[](int); //...... }; Sa observam ca deoarece pointerii si nu obiectele insasi sint memorati intr-un astfel de vector, un obiect poate fi "in" diferiti astfel de vectori in acelasi timp. Aceasta este o caracteristica foarte utila pentru clasele container de felul vectorilor, listelor inlantuite, multimilor, etc.. Mai mult decit atit, un pointer la o clasa derivata poate fi atribuit la un pointer spre clasa ei de baza, asa ca cvector de mai sus poate fi utilizat pentru a pastra pointeri spre obiectele tuturor claselor derivate din common. De exemplu: class apple : public common{ /*...*/ }; class orange : public common{ /*...*/ }; class apple_vector : public cvector{ public: cvector fruitbowl(100); //...... apple aa; orange oo; fruitbowl[0] = &aa; fruitbowl[1] = &oo; }; Totusi, tipul exact al unui obiect intr-o astfel de clasa container nu mai este cunoscut de compilator. De exemplu, in exemplul precedent noi stim ca un element al

vectorului este un common, dar este un apple sau un orange ? In mod obisnuit, tipul exact trebuie sa fie descoperit mai tirziu pentru a putea utiliza corect obiectul. Pentru a face aceasta, noi trebuie sau sa memoram o anumita forma a tipului de informatie in obiectul insusi sau sa ne asiguram ca numai obiectele unui tip dat se pun in container. Ultima varianta este atinsa trivial utilizind o clasa derivata. De exemplu, noi am putea face un vector de pointeri apple: class apple_vector : public cvector{ public: apple*& elem(int i) { return (apple*&)cvector::elem(i); } //......... }; utilizind notatia de type_casting. common*& (o referinta la pointer spre common) returnat prin cvector::elem spre apple*&. Aceasta utilizare a claselor derivate furnizeaza o alternativa a claselor generice. Este putin mai greu sa scriem in acest fel (daca nu sint utilizate macrouri asa incit clasele derivate sa fie de fapt utilizate pentru a implementa clase generice), dar are avantajul ca toate clasele derivate au in comun o singura copie a functiilor clasei de baza. Pentru o clasa generica de felul lui vector(type), trebuie sa se faca o noua copie a acelor functii (prin implement()) pentru fiecare tip nou utilizat. Alternativa de a memora identificatorul tipului in fiecare obiect ne conduce spre un stil de programare adesea referit ca bazat sau orientat spre obiect. 1.18 Functii virtuale Sa consideram scrierea unui program pentru afisarea formelor pe un ecran. Atributele comune ale formelor se reprezinta prin clasa shape, atribute specificate prin clase derivate specifice: class shape{point center; color col; //....... public: void move(point to) { center = to; draw(); } point where(){ return center; } virtual void draw(); virtual void rotate(int); //........ };

Functiile care pot fi definite fara cunostinte despre forma specifica (de exemplu move si where), pot fi declarate in mod obisnuit. Restul se declara virtual, adica se vor defini intr-o clasa derivata. De exemplu: class circle : public shape{ int radius; public: void draw(); void rotate(int i){} //....... }; Acum daca shape_vec este un vector de forme, noi putem scrie: for(int i=0; i<no_of_shapes; i++) shape_vec[i].rotate(45); pentru a roti (si redesena) toate formele cu 45 de grade. Acest stil este deosebit de util in programe interactive cind obiectele de tipuri diferite sint tratate uniform de catre softwareul de baza. CAPITOLUL 2 DECLARATII SI CONSTANTE 2.1

Declaratii

Exemple de declaratii: char ch; int count = 1; char* name = "Bjarne"; struct complex{ float re,im; } complex cvar; extern complex sqrt(complex); extern int error_number; typedef complex point; float real(complex* p){ return p->re; }; const double pi = 3.1415926535897932385; struct user; Majoritatea acestor declaratii sint de asemenea si definitii; adica ele definesc o entitate pentru numele la care se refera. Pentru ch, count si cvar, aceasta entitate este o cantitate corespunzatoare de memorie care sa se utilizeze ca o variabila. Pentru real, entitatea este o functie specifica. Pentru constanta pi entitatea este o valoare 3.1415... . Pentru complex, entitatea este un tip nou. Pentru point, entitatea este tipul complex asa ca point devine un sinonim pentru complex. Numai declaratiile extern complex sqrt(complex); extern int error_number; struct user; nu sint si definitii. Adica, entitatile la care se refera ele trebuie sa fie definita altundeva. Codul pentru functia sqrt() trebuie sa fie specificat

printr-o anumita alta declaratie, memoria pentru variabila error_number de tip intreg trebuie sa fie alocata printr-o anumita alta declaratie a lui error_number, iar o anumita alta declaratie a tipului user trebuie sa defineasca cum arata acel tip. Trebuie totdeauna sa fie exact o definitie pentru fiecare nume dintr-un program C++, dar pot fi multe declaratii si toate declaratiile trebuie sa fie compatibile cu tipul entitatii referite, asa ca fragmentul de mai jos are doua erori: int count; int count; // error : redefinition extern int error_number; extern short error_number; // error : type mismatch Anumite definitii specifica o "valoare" pentru entitatile pe care le definesc ele: struct complex{ float re,im; }; typedef complex point; float real(complex* p){ return p->re }; const double pi=3.1415926535897932385; Pentru tipuri, functii si constante "valoarea" este permanenta. Pentru tipuri de date neconstante valoarea initiala poate fi schimbata ulterior: int count = 1; char* name = "Bjarne"; //................ count = 2; name = "Marian"; Numai definitia char ch; nu specifica o valoare. Orice declaratie ce specifica o valoare este o definitie.

2.1.1 Domeniu O declaratie introduce un nume intr-un domeniu; adica un nume poate fi utilizat numai intr-o parte specifica a textului programului. Pentru un nume declarat intr-o functie (adesea numit nume local), domeniul lui se extinde din punctul declaratiei pina la sfirsitul blocului in care apare declaratia lui. Pentru un nume care nu este intro functie sau intr-o clasa (adesea numit nume global), domeniul se extinde din punctul declaratiei pina la sfirsitul fisierului in care apare declaratia lui. Un nume poate fi redefinit intr-un bloc pentru a referi o entitate diferita in blocul respectiv. Dupa iesirea din bloc numele isi reia intelesul lui precedent. De exemplu: int x; //global x f() {

int x; //local x. Ascunde globalul x x = 1; //asignarea la x local { int x; //ascunde primul local x x = 2; //asignarea la cel de al doilea local } x = 3; //asignarea la primul local x } int* p = &x; //ia adresa globalului x Ascunderea numelor este inevitabila cind se scriu programe mari. Totusi, un cititor poate usor sa nu observe ca un nume a fost ascuns si erorile datorate acestui fapt sint foarte dificil de gasit. In consecinta ar trebui minimizat numarul numelor ascunse. Utilizarea numelor de felul lui i si x pentru variabile globale sau locale in functii mari poate sa ne conduca la erori. Este posibil sa se utilizeze un nume global ascuns utilizind operatorul de rezolutie a domeniului "::". De exemplu: int x; f() {int x = 1; // ascunde globalul x ::x = 2; // asigneaza lui x global } Nu exista un mod de a utiliza un nume local ascuns. Domeniul unui nume incepe in punctul declaratiei lui; aceasta inseamna ca un nume poate fi utilizat chiar pentru a specifica valoarea lui initiala. De exemplu: int x; f(){ int x = x; } Aceasta este legal dar este fara sens. Este posibil sa utilizam un singur nume pentru a ne referi la doua obiecte diferite intr-un bloc fara a utiliza operatorul "::". De exemplu: int x = 11; f() { int y = x; // global x int x = 22; y = x; // local x } Variabila y este initializata cu 11, valoarea globalului x, iar apoi i se atribuie valoarea 22 a variabilei locale x. Numele argumentelor unei functii se considera declarate in blocul cel mai exterior functiei, deci

f(int x) { int x; // eroare } eroare, deoarece x este declarat de doua ori in acelasi domeniu.

2.1.2 Obiecte si Lvalori Se pot aloca si utiliza "variabile" ce nu au nume si este posibil sa se faca atribuiri la expresii care arata straniu (de exemplu *p[a+10]=7). In consecinta, exista nevoia de nume pentru "ceva aflat in memorie". Iata ghilimelele corespunzatoare pentru a face referire la manualul C++: "Un obiect este o regiune de memorie; o lvaloare este o expresie care refera un obiect" (&r.5). Cuvintul lvalue original a fost desemnat pentru a insemna "ceva ce poate fi in partea stinga a unei atribuiri". Totusi, nu orice lvalue poate fi utilizata in partea stinga a unei atribuiri; se poate avea o lvaloare care face referire la o constanta (vezi &2.4). 2.1.3 Durata de viata Daca programatorul nu specifica altfel, un obiect este creat cind definitia lui este intilnita si distrus cind numele lui iese afara din domeniu. Obiectele cu nume globale se creeaza si se initializeaza numai odata si "traiesc" pina cind se termina programul. Obiectele definite printr-o declaratie cu cuvintul cheie static se comporta la fel. De exemplu, (directiva #include <stream.h> a fost lasata afara din exemplele din acest capitol pentru a face economie de spatiu. Este necesar sa fie prezenta pentru exemplele care produc iesiri): int a = 1; void f() { int b = 1; //se initializeaza la fiecare apel a lui f() static int c = 1; //se initializeaza numai odata cout << "a=" << a++ << "b=" << b++ <<"c=" <
} produce iesirea: a=1 b=1 a=2 b=1 a=3 b=1

c=1 c=2 c=3

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

Nume

Un nume (identificator) consta dintr-un sir de litere si cifre. Primul caracter trebuie sa fie litera. Caracterul subliniere _ se considera a fi o litera. C++ nu impune limite asupra numarului de caractere dintr-un nume, dar anumite implementari nu sint sub controlul scriitorului de compilatoare (in particular, incarcatorul). Anumite medii de executie sint de asemenea necesare pentru a extinde sau restringe setul de caractere acceptat intr-un identificator; extensiile (de exemplu, cele care admit caracterul $ intr-un nume) produc programe neportabile. Un cuvint cheie C++ (vezi &r.2.3) nu poate fi utilizat ca un nume. Exemple de nume: hello this_is_a_most_unusually_long_name DEFINED fo0 bAr u_name HorseSence var0 var1 CLASS _class ___ Exemple de siruri de caractere care nu pot fi utilizate ca identificatori: 012 a fool $sys class 3var pay.dul foo-bar .name if Literele mari si mici sint distincte, asa ca Count si count sint nume diferite, dar nu este indicat sa se aleaga nume care difera numai putin unul de altul. Numele care incep cu subliniere se utilizeaza de obicei pentru facilitati in mediul de executie si de aceea nu se recomanda sa se utilizeze astfel de nume in programele aplicative. Cind compilatorul citeste un program, el totdeauna cauta cel mai lung sir care poate forma un sir, asa ca var10 este un singur nume si nu numele var urmat de numarul 10, iar elseif un singur nume, nu cuvintul cheie else urmat de if. 2.3

Tipuri

Orice nume (identificator) dintr-un program C++ are un tip asociat cu el. Acest tip determina ce operatii pot fi aplicate asupra numelui (adica la entitatea referita prin nume) si cum se interpreteaza aceste operatii. De exemplu: int error_number; float real(complex* p); Intrucit error_number este declarat sa fie int, lui i se pot face atribuiri, poate fi folosit in expresii aritmetice, etc.. Functia real, pe de alta parte, poate fi aplicata cu adresa unui complex ca parametru al ei. Este posibil sa se ia adresa oricaruia din ei. Anumite nume, cum ar fi int si complex, sint nume de tipuri. Un nume de tip este utilizat pentru a specifica tipul unui alt nume intr-o declaratie. Singura alta operatie asupra unui nume de tip este sizeof (pentru a determina cantitatea de memorie necesara pentru a pastra un obiect de acel tip) si new (pentru alocare de memorie libera pentru obiectele de tipul respectiv). De exemplu: main() { int* p = new int; cout << "sizeof(int) =" << sizeof(int) << "\n"; } Un nume de tip poate fi utilizat ca sa specifice explicit conversia de la un tip la altul (&3.2.4). De exemplu: float f; char* p; long ll = long(p); // converteste p spre long int i = int(f); // converteste f spre int

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

unsigned char unsigned short int unsigned int unsigned long int ,pentru a reprezenta intregi fara semn, valori logice, vectori de biti, etc.. Pentru o notatie mai compacta, int poate fi eliminat dintr-o combinatie de multicuvinte (de exemplu short este de fapt short int) fara a schimba intelesul; astfel long inseamna long int iar unsigned inseamna unsigned int. In general, cind un tip este omis intr-o declaratie, se presupune ca s-a omis int. De exemplu: const a = 1; static x; fiecare defineste un obiect de tip int. Intregul de tip caracter este cel mai potrivit pentru a mentine si manipula caractere pe un calculator dat; acest tip este de obicei pe 8 biti. Dimensiunile obiectelor din C++ sint exprimate in termeni multipli ai dimensiunii lui char, asa ca, prin definitie sizeof(char) = 1. Depinzind de hardware, un char este un intreg cu sau fara semn. Tipul caracter fara semn este sigur totdeauna fara semn (unsigned char) si utilizindu-l produce programe mai portabile, dar poate sa fie mai putin eficient decit daca este folosit ca tip char obisnuit. Motivul pentru a funiza mai multe tipuri de intregi, mai multe tipuri de intregi fara semn si mai multe tipuri de flotante este pentru a permite programatorului sa utilizeze avantajos caracteristicile hardware. Pe multe masini exista diferente semnificative in cerintele de memorie, timpul de acces la memorie si viteza de calcul dintre diferite varietati a tipurilor fundamentale. Cunoscind o masina, de obicei este usor a alege, de exemplu, tipul de intreg potrivit pentru o variabila particulara. A scrie cod de nivel inferior portabil cu adevarat este foarte greu. Ceea ce este garantat in legatura cu dimensiunile tipurilor fundamentale este: sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) sizeof(float) <= sizeof(double) Cu toate acestea, de obicei este rezonabil sa presupunem ca tipul char poate pastra intregi in intervalul 0..127 (el poate totdeauna pastra un caracter din setul de caractere al masinii), ca un short si un int au cel putin 16 biti, ca un int este apropiat de o dimensiune potrivita pentru aritmetica intregilor si ca un long are cel putin 24 de biti. A presupune mai mult este hazardos si chiar aceste reguli implicite nu se aplica universal (o tabela de caracteristici hardware pentru citeva masini se poate vedea in &r.2.6). Tipurile de intregi fara semn sint ideale pentru a utiliza memoria ca un vector pe biti. Utilizarea unui intreg fara semn in locul unui int pentru a cistiga un bit in plus pentru a reprezenta intregi pozitivi aproape totdeauna nu este o idee buna. Incercarea de a ne

asigura ca anumite valori sint pozitive prin declararea variabilelor de tip unsigned va fi ignorata prin reguli implicite de conversie. De exemplu: unsigned surprise = -1; este legal (dar compilatorul va face un avertisment despre el). 2.3.2 Conversie implicita de tip Tipurile fundamentale pot fi amestecate liber in expresii. Oricind este posibil, valorile se convertesc asa ca sa nu se piarda informatie (regula exacta poata fi gasita in &r.6.6). Exista cazuri in care informatia se poate pierde sau chiar distruge. Atribuirea unei valori de un tip la o variabila de un alt tip cu biti mai putini in reprezentarea ei este in mod necesar o sursa potentiala de erori. De exemplu, sa presupunem ca secventa urmatoare se executa pe o masina in care intregii se reprezinta in complement fata de doi si caracterele pe 8 biti: int i1 = 256 +255; char ch = i1; //ch == 255 int i2 = ch; //i2 == ? Un bit (cel mai semnificativ) este pierdut in atribuirea ch = i1 si ch va pastra toti bitii 1 (adica 8 biti de 1); deci nu exista o cale ca acesta sa poata deveni 511 cind se atribuie lui i2! Dar care ar putea fi valoarea lui i2 ? Pe VAX, unde un caracter este cu semn, raspunsul este 255. C++ nu are un mecanism la executie care sa detecteze un astfel de tip de problema, iar detectarea la compilare este prea dificila in general, asa ca programatorul trebuie sa fie atent la acest fapt. 2.3.3 Tipuri derivate --------------Din tipurile fundamentale (si din tipurile definite de utilizator) se pot deriva alte tipuri folosind operatorii de declaratie: pointer & adresa [] vector () functie si mecanismul de definitie de structura. De exemplu: int* a; float v[10];

char* p[20]; //vector de 20 de pointeri spre caractere void f(int); struct str{ short length; char* p; }; Regulile de compunere a tipurilor utilizind acesti operatori se explica in detaliu in &r8.3.4. Ideea de baza este ca declararea unui tip derivat oglindeste utilizarea lui. De exemplu: int v[10]; //declara un vector i = v[3]; //utilizeaza un element al vectorului int* p; //declaratie de pointer i = *p; //utilizeaza obiectul spre care se pointeaza Toate problemele in intelegerea notatiei pentru tipuri derivate apar din cauza faptului ca * si & sint operatori prefix iar [] si () sint postfix, asa ca parantezele trebuie sa fie utilizate pentru a exprima tipuri in care precedenta operatorilor este incomoda. De exemplu deoarece [] are o prioritate mai mare decit *: int *v[10]; //vectori de pointeri int (*p)[10] //pointer spre vector Poate fi plicticos sa utilizam o declaratie pentru fiecare nume pe care vrem sa-l introducem intr-un program, mai ales daca tipurile lor sint identice. Este posibil sa declaram diferite nume intr-o singura declaratie; in locul unui singur nume, declaratia pur si simplu contine o lista de nume separate prin virgula. De exemplu, se pot declara doi intregi astfel: int x, y; //int x; int y; Cind declaram tipuri derivate, trebuie sa observam ca operatorii se aplica numai la nume individuale (si nu la orice alte nume din aceeasi declaratie). De exemplu: int* p, y; //int *p; int y; nu int *y; int x, *p; //int x; int *p; int v[10], *p; //int v[10]; int *p; Opinia autorului este ca astfel de constructii fac un program mai putin lizibil si ar trebui eliminate. 2.3.4 Void Tipul void se comporta sintactic ca un tip fundamental. El poate totusi, sa fie utilizat numai ca parte a unui tip derivat; nu exista obiecte de tip void. Este folosit pentru a

specifica ca o functie nu returneaza o valoare sau ca tip de baza pentru pointeri spre obiecte de tip necunoscut. void f(); //f nu returneaza o valoare void* pv; //pointer spre un obiect de tip necunoscut Un pointer spre orice tip poate fi atribuit la o variabila de tip void*. Pentru inceput acesta nu pare prea util, deoarece un pointer void* nu poate fi indirectat dar aceasta restrictie este exact ceea ce face ca tipul void* sa fie util. El se utilizeaza in primul rind pentru a transfera la functii pointeri despre care nu se poate face presupunere asupra tipului obiectului spre care pointeza si pentru a returna obiecte fara tip dintr-o functie. Pentru a utiliza un astfel de obiect, trebuie sa se utilizeze conversia explicita de tip. Astfel de functii de obicei exista la cel mai inferior nivel al sistemului unde se manipuleaza resurse hardware reale. De exemplu: void* allocate(int size); void deallocate(void*); f() {int* pi = (int*)allocate(10 * sizeof(int)); char* pc = (char*)allocate(10); //.... deallocate(pi); deallocate(pc); } 2.3.5 Pointeri Pentru cele mai multe tipuri T, T* este tipul pointer spre T. Adica o variabila de tipul T* poate pastra adresa unui obiect de tipul T. Pentru pointeri spre vectori si pointeri spre functii exista notatii mai complicate: int* pi; char** cpp; //pointer spre pointer spre caractere int (*vp)[10] //pointer spre vector de 10 elemente int (*fp)(char,char*) //pointer spre o functie care are ca parametru (char, char*) si //returneaza un int Operatia fundamentala asupra unui pointer este indirectarea, adica referirea la un obiect pointat printr-un pointer spre el. Operatorul de indirectare este unarul * (prefixat). De exemplu: char c1 = 'a'; char* p = &c1; //p pastreaza adresa lui c1 char c2 = *p; //c2 = 'a' Variabila spre care pointeaza p este c1 si valoarea pastrata in c1 este 'a', asa ca valoarea lui *p atribuita lui c2 este 'a'.

Este posibil sa se faca unele operatii aritmetice cu pointerii. Iata de exemplu o functie care calculeaza numarul de caractere dintr-un sir (nesocotind 0 care termina sirul): int strlen(char* p) { int i = 0; while(*p++) i++; return i; } Un alt mod de a gasi lungimea este ca la inceput sa gasim sfirsitul sirului si apoi sa scadem adresa inceputului sirului din adresa sfirsitului: int strlen(char* p) { char* q = p; while(*q++); return(q-p-1); } Pointerii spre functii pot fi extrem de utili; ei se discuta in (&4.6.7).

2.3.6 Vectori Pentru un tip T, T[size] este tipul "vector de size elemente de tip T". Elementele sint indexate de la 0 la size-1. De exemplu: float v[3]; // un vector de 3 flotante: v[0],v[1],v[2] int a[2][5]; // doi vectori de 5 intregi char* vpc[32]; // vectori de 32 de pointeri spre caractere Un ciclu pentru a scrie valori intregi pentru caracterele mici ar putea fi scris astfel: extern int strlen(char*); char alpha[] = "abcdefghijklmnopqrstuvwxyz"; main() {int sz = strlen(alpha); for(int i=0; i<sz; i++) { char ch = alpha[i]; cout << "'" << chr(ch) << "'" << "=" << ch << "=0"

<< oct(ch) << "=0x" << hex(ch) << "\n"; } } Functia chr() returneaza reprezentarea sub forma de caracter a unui intreg mic; de exemplu, chr(80) este "P" pe o masina care utilizeaza setul de caractere ASCII. Functia oct() produce o reprezentare octala a argumentului sau intreg, iar hex() produce o reprezentare hexazecimala a argumentului sau intreg; chr(), oct() si hex() sint declarate in <stream.h>. Functia strlen() a fost utilizata pentru a numara caracterele din alpha (vezi &2.4.4). Cind se utilizeaza setul de caractere ASCII, iesirea va arata astfel: 'a' = 97 = 0141 = 0x61 'b' = 98 = 0142 = 0x62 'c' = 99 = 0143 = 0x63 Sa observam ca nu este necesar sa se specifice dimensiunea vectorului alpha; compilatorul calculeaza numarul de caractere din sirul de caractere specificat ca initializator. Utilizind un sir ca un initializator pentru un vector de caractere este convenabil, dar din nefericire este unica utilizare a sirurilor. Nu exista o atribuire similara a unui sir la un vector. De exemplu: char v[9]; v = "a string"; // error este o eroare deoarece atribuirea nu este definita pentru vectori. Evident sirurile sint potrivite numai pentru a initializa vectori de caractere; pentru alte tipuri trebuie sa se utilizeze o notatie mai laborioasa. Aceasta notatie poate fi de asemenea utilizata pentru vectori de caractere. De exemplu: int v1[] = {1,2,3,4}; int v2[] = {'a','b','c','d'}; char v3[] = {1,2,3,4}; char v4[] = {'a','b','c','d'}; Observam ca v4 este un vector de 4 (nu 5) caractere; nu este terminat printr-un zero, asa cum cer prin conventie toate rutinele de biblioteca. Aceasta notatie este de asemenea restrin sa la obiecte statice. Tablourile multidimensionale sint reprezentate ca vectori de vectori si notind cu virgula pentru a separa limitele ca in alte limbaje de programare se obtine la compilare o eroare deoarece virgula (,) este un operator de succesiune (vezi &3.2.2). De exemplu, sa incercam: int bad[5,2]; // error int v[5][2]; //correct

int bad = v[5,2]; // error int good = v[4][1]; // correct O declaratie char v[2][5]; declara un vector cu doua elemente; fiecare din ele este un vector de tip char [5]. In exemplul urmator, primul din acei vectori este initializat cu primele 5 litere iar cel de al doilea cu primele 5 cifre: char v[2][5] = {'a','b','c','d','e','0','1','2','3','4'}; main() { for(int i=0; i<2; i++) { for(int j=0; j<2; j++) cout << "v[" << i << "][" << j << "]=" chr(v[i][j]) << " "; cout << "\n"; } } va produce: v[0][0]=a v[0][1]=b v[0][2]=c v[0][3]=d v[0][4]=e v[1][0]=0 v[1][1]=1 v[1][2]=2 v[1][3]=3 v[1][4]=4

2.3.7 Pointeri si Vectori In C++, pointerii si vectorii sint foarte strinsi legati. Numele unui vector poate de asemenea, sa fie utilizat ca un pointer spre primul sau element, asa ca exemplul cu alfabetul ar putea fi scris astfel: char alpha[] = "abcdefghijklmnopqrstuvwxyz"; char* p = alpha; char ch; while(ch = *p++); cout << chr(ch) << "=" << ch << "=0" << oct(ch) << "\n"; Declaratia lui p ar putea de asemenea sa fie scrisa: char* p = &alpha[0]; Aceasta echivalenta este utilizata extensiv in apelurile de functii, in care un argument vector este totdeauna pasat ca un pointer la primul element al vectorului; astfel in acest exemplu: extern int strlen(char*); char v[] = "Annemarie"; char* p = v; strlen(p); strlen(v); este transferata aceeasi valoare la strlen in ambele apeluri.

Rezultatul aplicarii operatorilor +, -, ++, -- la pointeri depinde de tipul obiectului spre care pointeaza pointerul. Cind un operator aritmetic se aplica la un pointer spre un tip T, p este presupus ca pointeaza spre un element al vectorului de obiecte de tip T; p+1 inseamna elementul urmator al acelui vector iar p-1 elementul precedent. Aceasta implica faptul ca valoarea lui p+1 va fi cu sizeof(T) mai mare decit valoarea lui p. De exemplu: main() { char cv[10]; int iv[10]; char* pc = cv; int* pi = iv; cout << "char*" << long(pc+1) - long(pc) << "\n"; cout << "int*" << long(pi+1) long(pi) << "\n"; } va produce: char* 1 int* 4 deoarece caracterele ocupa un octet fiecare si intregii ocupa fiecare 4 octeti pe masina mea. Valorile pointer au fost convertite spre long inainte de a face scaderea utilizind conversia explicita de tip (&3.2.5). Ele au fost convertite spre long si nu spre tipul int deoarece exista masini pe care un pointer nu incape intr-un int (adica sizeof(int) < sizeof(char*)). Scaderea de pointeri este definita numai cind ambii pointeri pointeaza spre elemente ale aceluiasi vector (desi limbajul nu are un mod de a se asigura ca acest lucru este adevarat). Cind se scade un pointer dintr-un altul, rezultatul este numarul de elemente al vectorului dintre cei doi pointeri (un intreg). Se poate adauga un intreg la un pointer sau scadea un intreg dintr-un pointer; in ambele cazuri rezultatul este o valoare pointer. Daca acea valoare nu pointeaza spre un element al aceluiasi vector, ca si vectorul initial, rezultatul utilizarii valorii respective este nedefinit. De exemplu: int v1[10]; int v2[10]; int i = &v1[5] - &v1[3]; // 2 i = &v1[5] - &v2[3]; // rezultat nedefinit int* p = v2 + 2; // p==&v2[2] p = v2 - 2; // *p nedefinit

2.3.8 Structuri Un vector este un agregat de elemente de un acelasi tip; o structura este un agregat de elemente de orice tip. De exemplu: struct address{char* name; // "Jim Dandy" long number; // 61 char* street; // "South St" char* town; // "New Providence" char state[2]; // 'N' 'J' int zip; // 7974 }; defineste un tip nou numit address care consta din elementele de care avem nevoie pentru a trimite o scrisoare la cineva (address nu este in general destul pentru a gestiona toate scrisorile, dar este suficient pentru un exemplu). Sa observam punctvirgula de la sfirsit este unul din foarte putinele locuri din C++ unde este necesar sa o avem dupa o acolada inchisa, asa ca lumea este inclinata sa o uite. Variabilele de tip adresa pot fi declarate exact ca si alte variabile, iar elementele individuale pot fi accesate utilizind operatorul '.'(punct). De exemplu: address jd; jd.name = "Jim Dandy"; jd.number = 61; Notatia utilizata pentru initializarea vectorilor poate de asemenea sa fie utilizata pentru variabile de tip structura. De exemplu: address jd = {"Jim Dandy",61,"South St","New Providence", {'N','J'},7974}; Utilizind un constructor (&5.2.4) este de obicei mai bine. Sa observam ca jd.state nu poate fi initializat prin sirul "NJ". Sirurile sint terminate prin caracterul '\0' asa ca "NJ" are trei caractere, adica unul in plus decit ar incapea in jd.state. Obiectele structura sint adesea accesate prin pointeri folosind operatorul ->. De exemplu: void print_addr(address* p) { cout << p->name << "\n" << p->number << " " << p->street

<< "\n" << p->town << "\n" << chr(p->state[0]) << chr(p->state[1]) << " " << p->zip << "\n"; } Obiectele de tip structura pot fi atribuite, pasate ca si argumente la functie si returnate ca rezultat al unei functii. De exemplu: address current; address set_current(address next) { address prev = current; current = next; return prev; } Alte operatii plauzibile cum ar fi compararea (== si !=) nu sint definite. Cu toate acestea, utilizatorul poate defini astfel de operatori (vezi cap. 6). Nu este posibil sa se calculeze dimensiunea unui obiect de tip structura pur si simplu insumind membri ei. Motivul pentru aceasta este ca multe masini necesita ca obiecte de un anumit tip sa fie alocate numai la anumite adrese (un exemplu tipic este faptul ca un intreg trebuie sa fie alocat la o adresa de cuvint) sau pur si simplu pentru a trata astfel de obiecte mult mai eficient. Aceasta conduce spre "goluri" in structuri. De exemplu (pe masina mea): sizeof(address) este 24 si nu 22 cit ne-am astepta. Sa observam ca numele unui tip devine disponibil pentru utilizare imediat dupa ce el a fost intilnit si nu numai dupa declararea completa. De exemplu: struct link{ link* previsious; link* successor; }; Nu este posibil sa se declare obiecte noi de tip structura pina cind nu s-a terminat complet declaratia, deci struct no_good{ no_goog member; }; este o eroare (compilatorul nu este in stare sa determine dimensiunea lui no_good). Pentru a permite ca doua (sau mai multe) tipuri structura sa se refere unul la altul, pur si simplu se admite ca sa se declare ca un nume este numele unui tip structura. De exemplu: struct list; // to be defined later struct link{ link* pre; link* suc; list* member_of; }; struct list{ link* head; }; Fara prima declaratie a lui list, declaratia lui link ar produce o eroare sintactica.

2.3.9 Echivalenta tipurilor Doua tipuri structura sint diferite chiar daca ele au aceeasi membri. De exemplu: struct s1{ int a; }; struct s2{ int a; }; sint doua tipuri diferite, asa ca s1 x; s2 y = x; // error: type mismatch Tipurile structura sint de asemenea diferite de tipurile fundamentale, asa ca: s1 x; int i = x; // error: type mismatch Exista un mecanism pentru a declara un nume nou pentru un tip, fara a introduce un obiect nou. O declaratie prefixata prin cuvintul cheie typedef declara nu o noua variabila de un tip dat, ci un nume nou pentru tip. De exemplu: typedef char* pchar; pchar p1,p2; char* p3 = p1; Aceasta poate fi o prescurtare convenabila.

2.3.10Referinte O referinta este un nume pentru un obiect. Prima utilizare a referintelor este aceea de a specifica operatiile pentru tipuri definite de utilizator (ele se discuta in cap. 6). Ele pot fi de asemenea utile ca argumente de functii. Notatia X& inseamna referinta la X. De exemplu: int i = 1; int& r = i; // r si i acum se refera la acelasi obiect int x = r; // x = 1 r = 2; // i = 2 O referinta trebuie sa fie utilizata (trebuie sa fie ceva pentru ce este el nume). Sa observam ca initializarea unei referinte este ceva cit se poate de diferit de atribuirea la ea. In ciuda aparentelor, nici un operator nu opereaza asupra unei referinte. De exemplu: int ii = 0; int& rr = ii; rr++; // ii se incrementeaza cu 1 este legal, dar r++ nu incrementeaza referinta rr; ++ se aplica la un int, care se intimpla sa fie ii. In consecinta, valoarea referintei nu poate fi schimbata dupa initializare; ea totdeauna se refera la obiectul cu care a fost initializata pentru a-l

denumi. Pentru a primi un pointer spre obiectul notat prin referinta rr, se poate scrie &rr. Implementarea unei referinte este un pointer (constant) care este indirectat de fiecare data cind el este utilizat. Aceasta face initializarea unei referinte trivial cind initializatorul este o lvaloare (un obiect la care se poate lua adresa vezi &r5). Cu toate acestea, initializatorul pentru T& este necesar sa nu fie o lvaloare sau chiar de tip T. In astfel de cazuri: [1] Intii, se aplica conversia de tip daca este necesar (vezi &r6.6.8 si &r8.5.6); [2] Apoi valoarea rezultat este plasata intr-o variabila temporara; [3] In final, adresa acestuia se utilizeaza ca valoare a initializatorului. Consideram declaratia: double& dr = 1; Interpretarea acesteia este: double* drp; // referinta reprezentata printr-un pointer double temp; temp = double(1); drp = &temp; O referinta poate fi utilizata pentru a implementa o functie care se presupune ca schimba valoarea argumentelor sale. int x = 1; void incr(int& aa){ aa++; } incr(x); // x = 2; Semantica transferului de argumente se defineste ca si pentru initializare, asa ca atunci cind este apelata functia de mai sus argumentul aa a lui incr() devine un alt nume pentru x. Cu toate acestea, pentru a avea un program mai lizibil este cel mai bine sa eliminam functiile care isi modifica argumentele. Este adesea preferabil sa se returneze o valoare dintr-o functie in mod explicit sau sa se returneze un pointer spre argument. int x = 1; int next(int p){ return p+1; } x = next(x); // x = 2 void inc(int* p){ (*p)++; } inc(&x); // x = 3 Referintele pot fi de asemenea utilizate pentru a defini functii care pot fi utilizate atit in parttea stinga cit si in partea dreapta a unei atribuiri. Din nou, multe din cele mai interesante utilizari ale referintei se afla in proiectarea tipurilor netriviale definite de utilizator. Ca de exemplu, sa definim un tablou asociativ simplu. Intii noi definim struct pair prin: struct pair{ char* name; int val; }; Ideea de baza este ca un sir are o valoare intreaga asociata cu el. Este usor sa se defineasca o functie, find() care mentine o data structurata ce consta dintr-o pereche

pentru fiecare sir diferit ce a fost prezentat. O implementare foarte simpla (dar ineficienta) ar fi urmatoarea: const large = 1024; static pair vec[large+1]; pair* find(char* p) /* mentinerea unui set de "pair": se cauta p, se returneaza "pair"-ul respectiv daca se gaseste, altfel se returneaza un "pair" neutilizat */ { for(int i=0; vec[i].name; i++) if(strcmp(p,vec[i].name)==0) return &vec[i]; if(i==large) return &vec[large-1]; return &vec[i]; } Aceasta functie poate fi utilizata prin functia value() care implementeaza un tablou de intregi indexat prin siruri de caractere: int& value(char* p) { pair* res = find(p); if(res->name=='\0') // aici spre negasit:initializare { res->name=new char[strlen(p)+1]; strcpy(res->name,p); res->val = 0; // valoarea initiala: 0 } return res_val; } Pentru un parametru sir dat, value() gaseste obiectul intreg respectiv (nu valoarea intregului corespunzator); ea returneaza o referinta la el. Aceasta s-ar putea utiliza astfel: const MAX = 256; //mai mare decit cel mai mare cuvint main() //numara aparitiilor fiecarui cuvint de la intrare { char buf[MAX]; while(cin >> buf) value(buf++); for(int i=0; vec[i].name; i++) cout << vec[i].name << ":" << vec[i].val << "\n"; } Fiecare pas al ciclului citeste un cuvint de la intrarea standard cin in buf (vezi cap.8), iar apoi se pune la zi contorul asociat cu el prin find(). In final tabela rezultata de

cuvinte diferite de la intrare, fiecare cu numarul sau de aparitii, este imprimat. De exemplu, dindu-se intrarea aa bb bb aa aa bb aa aa programul va produce: aa : 5 bb : 3 Este usor sa se perfectioneze aceasta intr-un tip de tablou asociativ propriu folosind o clasa cu operatorul de selectie [] (vezi &6.7). 2.3.11Registrii Pe orice arhitectura de masina obiectele (mici) pot fi accesate mai rapid cind se plaseaza intr-un registru. Ideal, compilatorul va determina strategia optima pentru a utiliza orice registru disponibil pe masina pe care se compileaza programul. Totusi, acest task nu este trivial, asa ca uneori este util ca programatorul sa dea compilatorului aceasta informatie. Aceasta se face declarind un obiect registru. De exemplu: register int i; register point cursor; register char* p; Declaratiile de registrii ar trebui utilizate numai cind eficienta este intr-adevar importanta. Declarind fiecare variabila ca variabila registru se va ingreuna textul programului si se poate chiar marii dimensiunea codului si timpul de executie (de obicei sint necesare instructiuni de a incarca un obiect si de a memora un obiect dintr-un registru). Nu este posibil sa se ia adresa unui nume declarat ca registru si nici nu poate fi global un astfel de nume. 2.4 Constante C++ furnizeaza o notatie pentru valorile de tipuri fundamentale: constante caracter, constatante intregi si constante in virgula flotanta. In plus, zero (0) poate fi utilizat ca si o constanta pentru orice tip de pointer, iar sirurile de caractere sint constante de tip char[]. Este posibil, de asemenea, sa se specifice constante simbolice. O constanta simbolica este un nume a carui valoare nu poate fi schimbata in domeniul ei de existenta. In C++ exista trei feluri de constante simbolice: (1) oricarei valori de orice tip i se poate da un nume si sa fie folosita ca o consatnta adaugind cuvintul cheie const la definitia ei; (2) un set de constante intregi poate fi definit ca o enumerare; (3) orice nume de vector sau functie este o constanta.

2.4.1 Constante intregi Constantele intregi pot fi de patru feluri: zecimale, octale, hexazecimale si constante caracter. Constantele zecimale sint cele mai frecvent utilizate si arata asa cum ne asteptam noi: 0 1234 976 12345678901234567890 Tipul unei constante zecimale este int cu conditia ca ea sa incapa intr-un int, altfel ea este long. Compilatorul se cuvine sa avertizeze asupra constantelor care sint prea lungi ca sa fie reprezentate in calculator. O constanta care incepe cu zero urmat de x (0x) este hexazecimal, iar o constanta care incepe cu zero urmat de o cifra este in octal. Exemple de constante octale: 0 02 077 0123 Exemple de constante in hexazecimal: 0x0 0x2 0x38 0x53 Literele a, b, c, d, e si f sau echivalentele lor in litere mari se utilizeaza pentru a reprezenta 10, 11, 12, 13, 14 si respectiv 15. Notatiile octale si hexazecimale sint mai folosilositoare pentru a exprima structuri pe biti; utilizind aceste notatii pentru a exprima numere adevarate putem ajunge la surprize. De exemplu, pe o masina pe care un int se reprezinta ca un intreg prin complement fata de 2 pe 16 biti, intregul 0xffff este numarul negativ -1; daca s-ar folosi mai multi biti pentru a reprezenta un int, atunci acesta ar fi 65535.

2.4.2 Constante in flotanta O constanta in flotanta este de tip double. Din nou compilatorul da un avertisment despre constante flotante care sint prea mari pentru a putea fi reprezentate. Iata citeva constante in virgula flotanta: 1.23 .23 0.23 1. 1.0 1.2e10 1.23e-15 Sa observam ca nu poate apare un spatiu in mijlocul unei constante flotante. De exemplu: 65.43 e-21 nu este o constanta flotanta ci 4 lexicuri: 65.43 e - 21 si va cauza eroare sintactica. Daca noi dorim o constanta de tip float, noi putem defini una de forma (&2.4.6): const float pi8 = 3.14159265;

2.4.3 Constante caracter

Desi C++ nu are un tip caracter separat pentru date, ci mai degraba un tip intreg care poate pastra un caracter, trebuie sa avem o notatie speciala si convenabila pentru caractere. O constanta caracter este un caracter inclus intre caracterele apostrof: de exemplu 'a' si '0'. Astfel de constante caracter sint constante simbolice adevarate pentru valorile intregi ale caracterelor din setul de caractere al masinii pe care se executa programul C++ (care nu este in mod necesar acelasi set de caractere ca si cel utilizat de calculatorul pe care se compileaza programul). Astfel, daca noi executam programul pe o masina care utilizeaza setul de caractere ASCII, valoarea '0' este 48; daca masina utilizeaza setul EBCDIC, el este 240. Utilizind constantele caracter in locul notatiei zecimale programele devin mai portabile. Citeva caractere au de asemenea notatii standard in care se utilizeaza caracterul backslash (\): '\b' backspace '\f' formfeed '\n' newline '\r' cariage return '\t' horizontal tab '\v' vertical tab '\\' backslash '\'' simple quote '\"' double quote '\0' null, the integer value 0 Acestea sint caractere singulare in ciuda aparentei. Este posibil, de asemenea sa reprezentam un caracter printr-un numar octal de o cifra, doua sau trei (\ urmat de cifre octale) sau de un numar hexazecimal de una, doua sau trei cifre(\x urmat de cifre hexazecimale). De exemplu: '\6' '\x6' 6 ASCII ack '\60' '\x30' 48 ASCII '0' '\137' '\x05f' 95 ASCII '-' Aceasta face posibil ca sa se reprezinte fiecare caracter din setul caracterelor masina si in particular pentru a include astfel de caractere in siruri de caractere (vezi sectiunea urmatoare). Utilizind o notatie numerica pentru un caracter, programul respectiv nu mai este portabil pentru masini cu seturi diferite de caractere. 2.4.4 Siruri Un sir constant este o secventa de caractere inclusa intre ghilimele: "this is a string" Orice sir constant contine cu un caracter mai mult decit cele care apar in sir; ele toate se termina prin caracterul nul '\0', cu valoarea 0. De exemplu:

sizeof("asdf")==5; Tipul unui sir este "vector de un numar corespunzator de caractere", asa ca "asdf" este de tipul char[5]. Sirul vid se scrie "" (si are tipul char[1]). Sa observam ca pentru orice sir s, strlen(s) == sizeof(s) - 1 deoarece strlen() nu numara zeroul terminal. Conventia backslash pentru reprezentarea caracterelor negrafice pot de asemenea sa fie utilizate intr-un sir: aceasta face posibil sa se reprezinte ghilimelele si insusi caracterul backslash intr-un sir. Cel mai frecvent astfel de caracter este pe de parte caracterul '\n'. De exemplu: cout << "beep at end of message\007\n"; unde 7 este valoarea ASCII a caracterului bel. Nu este posibil sa avem un caracter newline "real" intr-un sir: "this is not a string but a syntax error" cu toate acestea, un backslash urmat imediat de un newline poate apare intr-un sir: ambele vor fi ignorate. De exemplu: cout << "this is\ ok" va scrie this is ok Este posibil ca sa avem caracterul nul intr-un sir, dar majoritatea programelor nu vor suspecta ca dupa el mai sint caractere. De exemplu, sirul "asdf\000hjkl" va fi tratat ca "asdf" prin functii standard cum ar fi strcpy() si strlen(). Cind se include o constanta numerica intr-un sir folosind notatia octala sau hexazecimala, este totdeauna bine sa se utilizeze trei cifre pentru numar. Notatia este destul de greu de utilizat fara sa apara probleme cind caracterul dupa o constanta de acest fel este o cifra. Consideram exemplele: char v1[]="a\x0fah\0129"; // 'a' '\xfa' 'h' '\12' '9' char v2[]="a\xfah\129"; // 'a' '\xfa' 'h' '\12' '9' char v3[]="a\xfad\127"; // 'a' '\xfad' '\127' Sa observam ca o notatie cu doua cifre hexazecimale nu este suficienta pe masini cu 9 biti pe byte.

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

2.4.6 Const Cuvintul cheie const poate fi adaugat la declaratia unui obiect pentru a face acel obiect o constanta in loc de variabila. De exemplu: const int model = 145; const int v[] = {1, 2, 3, 4}; Deoarece la un astfel de obiect nu i se poate atribui o valoare, el trebuie sa fie initializat. Declarind ceva ca este constant ne asiguram ca valoarea lui nu va fi schimbata in domeniul lui: model = 165; // error model++; // error Sa observam ca const modifica un tip; adica el restringe modul in care un obiect poate fi utilizat, in loc sa specifice cum se aloca constanta. Este, de exemplu, perfect rezonabil si uneori util sa declaram o functie care returneaza o constanta: const char* peek(int i){ return private[i]; } O functie de aceasta forma ar putea fi utilizata pentru a permite cuiva sa citeasca un sir care nu poate fi alterat. Cu toate acestea, un compilator poate avea avantaje de pe urma unui obiect care este o constanta in diferite moduri. Cel mai evident este faptul ca de obicei nu este nevoie ca sa fie alocata memorie pentru constanta deoarece compilatorul cunoaste valoarea lui. Mai mult decit atit, initializatorul pentru o constanta este adesea (dar nu totdeauna) o expresie constanta; daca este asa, ea poate fi evaluata la compilare. Cu toate acestea, de obicei este necesar sa se aloce memorie pentru un vector de constante deoarece compilatorul nu poate in general sa defineasca care elemente ale vectorului sint referite in expresii. Pe multe masini, totusi, o implementare eficienta poate fi atinsa chiar in acest caz plasind vectori de constante in memorii read_only. Cind utilizam un pointer, sint implicate doua obiecte; pointerul insusi si obiectul spre care se face pointarea. "Prefixind" o declaratie a unui pointer cu const se construieste obiectul ca o constanta, nu si pointerul. De exemplu: const char* pc = "asdf"; // pointer spre o constanta pc[3] = 'a'; // eroare pc = "ghjk"; // ok Pentru a declara ca pointerul insusi este o constanta si nu obiectul spre care pointeaza, se foloseste operatorul *const:

char *const cp = "asdf"; // pointer constant cp[3] = 'a'; // ok cp = "ghjk"; // eroare Pentru a face ca sa fie constante atit obiectele, cit si pointerul spre ele, trebuie ca ambele sa fie declarate ca si constante. De exemplu: const char *const cpe = "asdf"; // pointer constant spre // constanta cpc[3] = 'a'; // eroare cpc = "ghjk"; // eroare Un obiect care este o constanta cind este accesat printr-un pointer poate fi variabila cind este accesat in alt mod. Aceasta este util mai ales pentru argumentele functiilor. Declarind un pointer_argument ca si const, functiei I se interzice sa modifice obiectul spre care pointeaza pointerul respectiv. De exemplu: char* strcpy(char* p,const char* q);//nu poate modifica pe *q Se poate atribui adresa unei variabile la un pointer spre o constanta deoarece nu se intimpla nimic rau prin aceasta. Cu toate acestea, adresa unei constante nu se poate atribui la un pointer fara restrictii deoarece aceasta ar permite sa schimbe valoarea obiectului. De exemplu: int a = 1; const c = 2; const* p1 = &c; // ok const* p2 = &a; // ok int* p3 = &c; // eroare *p3 = 7; // schimba valoarea lui De obicei, daca tipul este omis intr-o declaratie, se alege int ca implicit. 2.4.7 Enumerari O alta posibilitate pentru a defini constante intregi, care este adesea mai convenabil decit utilizind const, este enumerarea. De exemplu: enum {ASM, AUTO, BREAK}; defineste trei constante intregi, numite enumeratori si atribuie valori la acestia. Deoarece valorile enumerator sint atribuite crescator de la zero, aceasta este echivalent cu scrierea: const ASM = 0; const AUTO = 1;

const BREAK = 2; O enumerare poate fi definita. De exemplu: enum keyword {ASM,AUTO,BREAK}; Numele enumerarii devine un sinonim pentru int, nu un nou tip. De exemplu: keyword key; switch(key) { case ASM: // se face ceva break; case BREAK:// face ceva break; } va conduce la un avertisment deoarece numai doua valori au fost tratate din cele trei. Valorile pot fi de asemenea date explicit enumeratorilor. De exemplu: enum int16 { sign = 0100000, most_significant = 0400000, last_significant = 1 }; Aceste valori nu este necesar sa fie distincte, crescatoare sau pozitive. 2.5

Salvarea spatiului

Cind programam aplicatii netriviale, invariabil vine vremea cind dorim mai mult spatiu de memorie decit este disponibil sau ne putem permite. Exista doua moduri de a obtine mai mult spatiu in afara de cel care este disponibil: [1] Sa se puna mai mult de un obiect mic intr-un octet; [2] Sa se utilizeze acelasi spatiu pentru a pastra diferite obiecte in momente diferite. Prima metoda poate fi realizata folosind cimpurile, iar cea de a doua folosind reuniunile. Aceste constructii se descriu in sectiunile urmatoare. Deoarece utilizarea lor tipica este pentru a optimiza pur si simplu un program si deoarece ele sint adesea cele mai neportabile parti ale programului, programatorul trebuie sa gindeasca de doua ori inainte de a le utiliza. Adesea o conceptie mai buna este sa schimbe modul in care se gestioneaza datele; de exemplu, sa se insiste mai mult asupra memoriei alocate dinamic (&3.2.6) si mai putin asupra memoriei prealocate static. 2.5.1 Cimpuri

Se pare extravagant ca sa se utilizeze un caracter pentru a reprezenta o variabila binara, de exemplu un comutator on/off, dar tipul char este cel mai mic obiect care poate fi alocat independent in C++. Este posibil, totusi, sa se inmanuncheze impreuna diferite astfel de variabile foarte mici ca si cimpuri intr-o structura. Un membru se defineste a fi un cimp specificind numarul de biti pe care ii ocupa, dupa numele lui. Se admit si cimpuri nedenumite; ele nu afecteaza sensul cimpurilor denumite, dar pot fi utilizate pentru a face o aranjare mai buna insa dependenta de masina: struct sreg{ unsigned enable : 1; unsigned page : 3; unsigned : 1; //neutilizat unsigned mode : 2; unsigned : 4; //neutilizat unsigned access : 1; unsigned length : 1; unsigned non_resident : 1; }; Aceasta se intimpla sa fie aranjarea bitilor la registru de stare 0 la DEC PDP11/45. Un cimp trebuie sa fie de tip intreg si se utilizeaza ca alti intregi exceptind faptul ca nu este posibil sa se ia adresa unui cimp. In modulul kernel al unui sistem de operare sau in debugger, tipul sreg ar putea fi utilizat astfel: sreg* sr0 = (sreg*)0777572; //........ if(sr0->access) //access violation {//clean up the mess sr0->access = 0; } Cu toate acestea, utilizind cimpuri pentru a putea impacheta diferite variabile intr-un singur octet nu neaparat se salveaza spatiu. Se salveaza spatiu la date, dar dimensiunea codului rezultat din manipularea acestor variabile se mareste pe majoritatea masinilor. Programele se stie ca se scurteaza semnificativ cind variabilele binare se convertesc de la cimpuri binare la caractere! Mai mult decit atit, de obicei este mai rapid sa se faca acces la char sau int decit pentru a face acces la un cimp. 2.5.2 Reuniuni Sa consideram o tabela de simboluri in care o intrare pastreaza un nume si o valoare, iar valoarea este sau un sir sau un intreg: struct entry{ char* name; char type; char* string_value; //se utilizeaza daca type == 's'

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

}; }; fudge a; a.i = 4096; int* p = a.p;

//bad usage

Cu toate acestea, aceasta nu este o conversie reala; pe anumite masini un int si un int* nu ocupa acelasi spatiu, iar pe altele nici un intreg nu poate avea o adresa impara. O astfel de utilizare a unei reuniuni nu este portabila si exista un mod explicit si portabil de a specifica aceasta conversie (&3.2.5). Reuniunile sint ocazional utilizate in mod deliberat pentru a elimina conversia de tip. Am putea, de exemplu, utiliza un fudge pentru a gasi reprezentarea pointerului 0: fudge.p = 0; int i = fudge.i; // i nu este necesar sa fie 0 Este de asemenea posibil sa se dea un nume unei reuniuni; adica ea formeaza un tip in adevaratul sens al lui. De exemplu, fudge ar putea fi declarata astfel: union fudge{ int i; int* p; }; si folosita exact ca inainte. Reuniunile numite au de asemenea, utilizari proprii (vezi &5.4.6). 2.6 Exercitii 1. (*1). Sa se execute programul "Hello, world" (&1.1.1). 2. (*1). Pentru fiecare din declaratiile din (&2.1) sa se faca urmatoarele: daca o declaratie nu este o definitie, sa se scrie o definitie pentru ea. Daca o declaratie este o definitie, sa se scrie o declaratie pentru ea, care nu este de asemenea o definitie. 3. (*1). Sa se scrie declaratii pentru urmatoarele: un pointer spre un caracter; un vector de 10 intregi; o referinta spre un vector de 10 intregi; un pointer spre un vector de siruri de caractere; un pointer spre un pointer la un caracter; o constanta intreaga; un pointer spre o constanta intreaga; un pointer constant spre un intreg. Sa se initializeze fiecare din ei. 4. (*1.5). Sa se scrie un program care imprima dimensiunea tipurilor fundamentale si a pointerului. Sa se utilizeze operatorul sizeof. 5. (*1.5). Sa se scrie un program care imprima literele 'a'..'z' si cifrele '0'..'9' si valorile lor intregi. Sa se faca acelasi lucru pentru alte caractere imprimabile. Sa se faca acelasi lucru, dar utilizind notatia hexazecimala. 6. (*1). Sa se imprime bitii care se folosesc pentru a reprezenta pointerul 0 pe sistemul d-voastra (&2.5.2).

7. (*1.5). Sa se scrie o functie care imprima exponentul si mantisa unui parametru in dubla precizie. 8. (*2). Care sint valorile cele mai mari si cele mai mici pe sistemul d-voastra pentru tipurile urmatoare: char, short, int, long, float, double, unsigned, char*, int* si void* ? Exista mai multe restrictii asupra valorilor ? De exemplu, poate int* sa aiba o valoare impara ? Care este cadrajul obiectelor de acele tipuri ? De exemplu poate un int sa aiba o adresa impara ? 9. (*1). Care este cel mai lung nume local pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ? Care este cel mai lung nume extern pe care il puteti utiliza intr-un program C++ pe sistemul d-voastra ? Exista vreo restrictie asupra caracterelor pe care le puteti utiliza intr-un nume ? 10. (*2). Definiti pe unu astfel: const one = 1; Incercati sa schimbati valoarea lui one la doi. Definiti pe num prin: const num[] = {1,2}; Incercati sa schimbati valoarea lui num[1] la 2. 11. (*1). Scrieti o functie care permuta doi intregi. Sa se utilizeze int* ca tip al argumentului. Scrieti o alta functie de permutare care utilizeaza int& ca tip de argument. 12. (*1). Care este dimensiunea vectorului str in exemplul urmator: char str[] = "a short string"; Care este lungimea sirului "a short string"? 13. (*1.5). Sa se defineasca o tabela de nume continind numele fiecarei luni din an si numarul de zile din fiecare luna. Sa se scrie tabela. Sa se faca aceasta de doua ori: odata utilizind un vector pentru nume si un vector pentru numarul de zile si odata utilizind un vector de structuri, fiecare structura pastrind numele lunii si numarul de zile din ea. 14. (*1). Sa se utilizeze typedef pentru a defini tipurile: unsigned char, constant unsigned char, pointer spre intreg, pointer spre pointer spre char, pointer spre vector de caractere, vector de 7 pointeri intregi, pointer spre un vector de 7 pointeri intregi, vector de 8 vectori de 7 pointeri intregi.

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

Un calculator de birou

Instructiunile si expresiile se introduc prin prezentarea programului calculatorului de birou care furnizeaza cele patru operatii aritmetice standard ca operatori infix asupra numerelor flotante. Utilizatorul poate, de asemenea, defini variabile. De exemplu, dindu-se intrarea: r = 2.5 area = pi * r * r (pi este predefinit), programul calculator va scrie: 2.5 19.635

unde 2.5 este rezultatul primei linii de intrare, iar 19.635 este rezultatul celei de a doua. Calculatorul consta din patru parti principale: un analizor, o functie de intrare, o tabela de simboluri si un driver. In realitate este un compilator miniatura cu un analizor care face analiza sintactica, functia de intrare realizind intrarea si analiza lexicala, tabela de simboluri pastrind informatia permanenta, iar driverul facind initializarea, iesirea si tratind erorile. Exista multe facilitati care pot fi adaugate la acest calculator pentru a-l face mai util, dar codul este destul de lung intrucit are 200 de linii si cele mai multe facilitati noi ar adauga cod fara a furniza aspecte noi in utilizarea lui C++. 3.1.1 Analizorul Iata o gramatica pentru limbajul acceptat de calculator: program: END //END este sfirsitul intrarii expr_list END expr_list: expression PRINT //PRINT este '\n' sau ; expresion PRINT expr_list expression: expression + term expression - term term term: term / primary term * primary primary primary: NUMBER //numar in flotanta din C++ NAME //nume din C++ fara subliniat NAME = expression _primary (expression) Cu alte cuvinte, un program este un sir de linii. Fiecare linie consta din una sau mai multe expresii separate prin punct- virgula. Unitatile de baza ale unei expresii sint numere, nume si operatorii *, /, +, - (atit unar cit si binar) si =. Numele nu trebuie sa fie declarate inainte sa fie utilizate. Stilul analizei sintactice utilizate este de obicei

numit analiza descendenta recursiva. Este o tehnica top-down directa. Intr-un limbaj cum este C++ in care apelurile de functii sint relativ ieftine, aceasta este o tehnica eficienta. Pentru fiecare productie din gramatica exista o functie care apeleaza alte functii. Simbolurile terminale (de exemplu END, NUMBER, + si -) se recunosc prin analizorul lexical, get_token(), iar simbolurile neterminale sint recunoscute prin functiile analizorului sintactic expr(), term() si prim(). De indata ce ambii operanzi ai unei (sub)expresii sint cunoscuti, ei se evalueaza. Intr-un compilator real se genereaza codul in acest punct. Analizorul utilizeaza o functie get_token() pentru a obtine o intrare. Valoarea ultimului apel a lui get_token() poate fi gasita in variabila curr_tok. Aceasta este o valoare de enumerare de tip token_value: enum token_value{ NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*', DIV = '/', PRINT =';', ASSIGN = '=', LP = '(', RP = ')' }; token_value curr_tok; Fiecare functie a analizorului presupune ca get_token() a fost apelat astfel incit curr_tok sa pastreaze tokenul (lexicul) urmator de analizat. Aceasta permite analizorului sa vada un lexic inainte si obliga fiecare functie a analizorului sa citeasca totdeauna un lexic in plus fata de cele pe care le utilizeaza productia pe care o trateaza ea. Fiecare functie a analizorului evalueaza expresia ei si returneaza o valoare. Functia expr() trateaza adunarea si scaderea. Ea consta dintr-un singur ciclu care cauta termeni de adunat sau scazut: double expr() { double left = term(); for(;;) //ciclu infinit switch(curr_tok) { case PLUS : get_token(); //salt peste '+' left += term(); break; case MINUS: get_token(); //salt peste '-' left -= term(); break; default: return left; } } Aceasta functie in realitate nu face ea insasi foarte mult. Intr-o maniera tipica pentru functii de nivel mai inalt dintr-un program mare, ea apeleaza alte functii pentru a face

"greul". Sa observam ca o expresie de forma 2 - 3 + 4 se evalueaza ca (2 - 3) + 4, asa cum se specifica in gramatica. Notatia curioasa for(;;) este modul standard de a specifica un ciclu infinit. O alternativa este while(1). Instructiunea switch se executa repetat pina cind nu se mai gaseste + sau - si in acest caz se executa instructiunea return din default. Operatorii += si -= se utilizeaza pentru a trata adunarea si scaderea. left = left + term(); left = left - term(); ar putea fi utilizate fara a schimba intelesul programului. Cu toate acestea left += term(); left -= term(); sint nu numai mai scurte, dar exprima direct operatia intentionata. Pentru un operator binar @, o expresie x @= y inseamna x = x @ y si se aplica la operatorii binari: + - * / % & | ^ << >> asa ca sint posibili urmatorii operatori de atribuire: = += -= *= /= %= &= |= ^= <<= >>= Fiecare este un lexic separat, asa ca a + = 1; este o eroare din cauza spatiului dintre + si =. (% este modulo sau restul impartirii, &, |, ^ sint operatorii logici pe biti and, or si xor, << si >> sint deplasari stinga si dreapta). Functiile term() si get_token() trebuie sa fie declarate inainte de expr(). Capitolul patru discuta cum sa se organizeze un program ca un set de fisiere. Cu o singura exceptie, declaratiile pentru acest exemplu de calculator de birou pot fi ordonate in asa fel incit fiecare este declarata exact o data inainte de a fi utilizata. Exceptie face expr(), care apeleaza term(), care apeleaza prim(), care la rindul ei apeleaza expr(). Acest ciclu trebuie sa fie intrerupt cumva. O declaratie: double expr(); inaintea definitiei lui prim() va fi nimerita. Functia term() trateaza inmultirea si impartirea: double term() //inmultire si impartire { double left = prim(); for(;;) switch(curr_tok) { case MUL: get_token(); //sare peste '*'

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

Cind se4 gaseste un NUMBER (adica o constanta flotanta), se returneaza valoarea ei. Rutina de intrare get_token() plaseaza valoarea in variabila globala number_value. Utilizarea unei variabile globale intr-un program indica adesea ca structura nu este cit se poate de "curata", ca un anumit fel de optimizare a fost aplicat. Asa este aici; un lexic in mod tipic consta din doua parti: o valoare care specifica tipul lexicului (token_value in acest program) si (cind este nevoie) valoarea lexicului. Aici exista numai o singura variabila simpla curr_tok, asa ca este nevoie de variabila globala number_value pentru a pastra valoarea ultimului NUMBER citit. Aceasta functioneaza deoarece calculatorul totdeauna utilizeaza un numar in calcul inainte de a citi un alt numar de intrare. In acelasi mod in care valoarea ultimului NUMBER intilnit este tinut in number_value, reprezentarea sirului de caractere a ultimului NAME intilnit este tinut in name_string. Inainte de a face ceva unui nume, inainte calculatorul trebuie sa vada daca el este asignat sau numai utilizat. In ambele cazuri se consulta tabela de simboluri. Tabela este prezentata in &3.1.3. Aici trebuie sa observam ca ea contine intrari de forma: struct name{ char* string; name* next; double value; }; unde next se utilizeaza numai de functiile care mentin tabela: name* look(char*); name* insert(char*); Ambele returneaza un pointer la un nume care corespunde la parametrul sir de caractere. Functia look() semnaleaza daca numele nu a fost definit. Aceasta inseamna ca in calculator un nume poate fi utilizat fara o declaratie prealabila, dar prima lui utilizare trebuie sa fie partea stinga a unei atribuiri. 3.1.2 Functia de intrare -----------------Citirea intrarii este adesea cea mai incurcata parte a unui program. Motivul este faptul ca daca un program trebuie sa comunice cu o persoana, el trebuie sa invinga capriciile, conventiile si erorile unei persoane sau a mai multora. Incercarea de a forta persoana sa se comporte intr-o maniera mai convenabila pentru masina este adesea, pe drept cuvint, considerata ofensiva. Sarcina unei rutine de intrare de nivel inferior este de a citi caractere unul dupa altul si sa compuna unitati de nivel mai inalt. Aici intrarea de nivel inferior se face cu get_token(). Regulile pentru intrarile in calculator au fost deliberat alese asa ca sa fie ceva incomod pentru sirul de functii care le manevreaza. Modificari neimportante in definitiile unitatilor ar face pe get_token() foarte simpla.

Prima problema este aceea ca, caracterul newline '\n' este semnificativ pentru calculator, dar sirul de functii de intrare il considera un caracter whitespace. Adica, pentru acele functii, '\n' este un terminator de unitate lexicala. Pentru a invinge aceasta trebuie examinate spatiile albe (spaces, tab, etc): char ch; do{ //sare peste spatiile albe, exceptind '\n' if(!cin.get(ch)) return curr_tok = END; }while(ch!='\n' && isspace(ch)); Apelul cin.get(ch) citeste un singur caracter din sirul de la intrarea standard in ch. Testul if(!cin.get(ch)) esueaza daca nici un caracter nu poate fi citit de la intrare (din cin). In acest caz se returneaza END pentru a termina sesiunea de calcul. Operatorul ! (not) se utilizeaza intrucit get() returneaza o valoare nenula in caz de succes. Functia isspace() din furnizeaza testul standard pentru spatiu alb (&8.4.1). Functia isspace(c) returneaza o valoare nenula daca c este un caracter alb, zero altfel. Testul este implementat ca o tabela de cautare, astfel, utilizind isspace este mai rapid decit daca s-ar testa individual caracterele spatiu alb. Acelasi lucru se aplica la functiile isalpha(), isdigit() si isalnum() utilizate in get_token(). Dupa ce s-a facut avans peste caracterele albe, se utilizeaza caracterul urmator pentru a determina ce fel de unitate lexicala incepe in sirul de intrare. Sa ne oprim la niste cazuri separate inainte de a prezenta functia completa. Expresiile terminatoare '\n' si ';' sint tratate astfel: switch(ch) { case ';' : case '\n' : cinn >> WS; //sare peste caractere albe return curr_tok = PRINT; } Saltul peste caractere albe (din nou) nu este necesar, dar daca ar trebui s-ar repeta apeluri ale lui get_token(). WS este un obiect de spatiu alb declarat in <stream.h>. El este utilizat numai pentru a indeparta spatiile albe. O eroare la intrare sau la sfirsitul intrarii nu va fi detectata pina la apelul urmator a lui get_token(). Sa observam modul in care diferite etichete ale lui case pot fi utilizate pentru un singur sir de instructiuni care trateaza acele cazuri. Se returneaza unitatea PRINT si se pune in curr_tok in ambele cazuri. Numerele se trateaza astfel: case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin >> number_value; return curr_tok = NUMBER;

Scrierea etichetelor orizontal in loc de vertical, in general, nu este o idee buna deoarece este mai greu de citit, dar nu este nimerit in cazul de fata sa avem o linie pentru fiecare cifra. Deoarece operatorul >> este deja definit pentru a citi constante in virgula flotanta dubla precizie, codul este trivial. Intii caracterul initial (o cifra sau un punct) se pune inapoi in cin si apoi constanta poate fi citita in number_value. Un nume, care este un lexic de tip NAME, este definit ca o litera care este posibil sa fie urmata de litere sau cifre: if(isalpha(ch)) {char* p = name_string; *p++ = ch; while(cin.get(ch) && isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok = NAME; } Aceasta construieste un sir terminat cu zero in name_string. Functiile isalpha() si isalnum() sint furnizate in , isalnum(c) este diferit de zero daca c este o litera sau o cifra si zero altfel. Iata in final functia de intrare completa: token_value get_token() {char ch; do{ //sare peste spatiile albe exceptind '\n' if(!cin.get(ch)) return curr_tok = END; }while(ch != '\n' && isspace(ch)); switch(ch) { case ';' : case '\n': cin >> WS; //salt peste spatii albe return curr_tok = PRINT; case '*': case '/': case '+': case '(': case ')': case '=': return curr_tok = ch; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': cin.putback(ch); cin >> number_value; return curr_tok = NUMBER; default : //NAME, NAME= sau eroare if(isalpha(ch)) { char* p = name_string; *p++ = ch; while(cin.get(ch) && isalnum(ch)) *p++ = ch; cin.putback(ch); *p = 0; return curr_tok = NAME;

} error("bad token"); return curr_tok = PRINT; } } Intrucit token_value al unui operator a fost definit prin valoarea intreaga a operatorului, toate alternativele (case) pentru operator se trateaza trivial. 3.1.3 Tabela de simboluri O singura functie are acces la tabela de simboluri: name* look(char* p, int ins = 0); Cel de al doilea argument indica daca sirul de caractere presupus trebuie sa fie in prealabil inserat sau nu. Folosirea argumentului implicit da pentru look posibilitatea convenabila de a scrie look("sqrt2") in loc de look("sqrt2", 0), adica se doreste cautare, nu inserare. Pentru a obtine notatii convenabile pentru inserare se defineste o a doua functie: inline name* insert(char* s){ return look(s, 1); } Asa cum s-a mentionat inainte, intrarile in tabela sint de forma: struct name{ char* string; name* next; double value; }; Elementul next se utilizeaza pentru a face inlantuirea numerelor in tabela. Tabela insasi este pur si simplu un vector de pointeri spre obiecte de tip nume: const TBLSZ = 23; name* table[TBLSZ]; Deoarece toate obiectele statice sint implicit initializate cu zero, aceasta declaratie triviala a lui table asigura de asemenea si initializarea. Pentru a gasi o intrare pentru un nume din tabela, look() utilizeaza un cod hash simplu (numele cu acelasi cod hash se inlantuie): int ii = 0; char* pp = p; while(*pp) ii = ii << 1 ^ *p++; if(ii < 0) ii = -ii; ii %= TBLSZ; Fiecare caracter din sirul de intrare p este "adaugat" la ii ("suma" caracterelor precedente) printr-un sau exclusiv. Un bit din x^y este setat daca si numai daca bitii corespunzatori din operanzii x si y sint diferiti. Inainte de a face un sau exclusiv, ii se

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

gasi cit de multa memorie este necesara, operatorul new pentru a aloca memorie, iar functia strcpy() pentru a copia sirul in zona respectiva. 3.1.4 Tratarea erorilor Intrucit programul este atit de simplu, tratarea erorilor nu este o preocupare majora. Functia eroare pur si simplu numara erorile, scrie un mesaj de eroare si returneaza: int no_of_errors; double error(char* s) {cerr << "error: " << s << "\n"; no_of_errors++; return 1; } Motivul pentru care se returneaza o valoare este faptul ca erorile de obicei apar in mijlocul evaluarii unei expresii, asa ca ar trebui sau sa se faca un abandon al acelei evaluari sau sa se returneze o valoare care in continuare sa fie putin probabil sa cauzeze erori. Ultima varianta este adecvata pentru acest calculator simplu. Daca get_token() ar tine numerele de linie, error() ar putea informa utilizatorul aproximativ asupra locului unde a aparut eroarea. Aceasta ar fi util la o folosire interactiva a calculatorului. Adesea un program trebuie sa fie terminat dupa o eroare deoarece nu exista o cale adecvata care sa permita continuarea executiei. Acest lucru se poate face apelind functia exit(), care la inceput videaza lucrurile de tipul fisierelor de iesire (&8.3.2) dupa care se termina programul iar valoarea returnata de el este argumentul lui exit(). Un mod mai drastic de terminare a programului este apelul lui abort() care termina imediat sau imediat dupa pastrarea undeva a informatiei pentru debugger (vidaj de memorie). 3.1.5 Driverul Cu toate bucatile programului construite noi avem nevoie numai de un driver care sa initializeze si sa porneasca tot procesul. In acest exemplu simplu functia main() poate fi construita astfel: int main() //insereaza nume predefinite { insert("pi")->value = 3.1415926535897932385; insert("e")->value = 2.7182818284590452354; while(cin) { get_token();

if(curr_tok == END) break; if(curr_tok == PRINT) continue; cout << expr() << "\n"; } return no_of_errors; } Prin conventie, main() returneaza zero daca programul se termina normal si altfel, o valoare diferita de zero, asa ca returnarea numarului de erori se potriveste bine cu aceasta conventie. Aici singurele initializari sint numerele predefinite pentru "pi" si "e" care se insereaza in tabela de simboluri. Sarcina primordiala a ciclului principal este sa citeasca expresii si sa scrie raspunsul. Aceasta se obtine prin linia: cout << expr() << "\n"; Testind pe cin la fiecare pas al ciclului se asigura ca programul sa se termine daca ceva merge rau in sirul de intrare iar testul pentru END asigura ca ciclul sa se termine corect cind get_token() intilneste sfirsitul de fisier. O instructiune break provoaca iesirea din instructiunea switch sau din ciclul care o contine (adica o instructiune for, while sau do). Testul pentru PRINT (adica pentru '\n' si ';') elibereaza pe expr() de necesitatea de a prelucra expresii vide. O instructiune continue este echivalenta cu trecerea la sfirsitul ciclului, asa ca in acest caz: while(cin) { //............ if(curr_tok == PRINT) continue; cout << expr() << "\n"; } este echivalent cu : while(cin) { //............ if(curr_tok == PRINT) goto end_of_loop; cout << expr() << "\n"; end_of_loop : ; } (ciclurile se descriu in detaliu in &r9). 3.1.6 Argumentele liniei de comanda Dupa ce programul a fost scris si testat, am observat ca tastarea expresiilor la intrarea standard a fost adesea mai mult decit necesar, deoarece in mod frecvent a trebuit sa

se evalueze o singura expresie. Daca este posibil ca aceasta expresie sa fie prezentata ca un argument al liniei de comanda, atunci multe accese cheie ar fi fost eliminate. Asa cum s-a mentionat in prealabil, un program incepe prin apelul lui main(). Cind aceasta s-a facut, main() primeste doua argumente, care specifica numarul de argumente si care de obicei se numeste argc si un vector de argumente, care de obicei se numeste argv. Argumentele sint siruri de caractere, asa ca tipul lui argv este char *[argc]. Numele unui program (intrucit el apare pe linia de comanda) se paseaza ca argv[0], asa ca argc este intotdeauna cel putin 1. De exemplu, pentru comanda: dc 150/1.1934 argumentele au aceste valori: argc 2 argv[0] "dc" argv[1] "150/1.1934" Nu este dificil sa fie pastrata linia de comanda ca argument. Problema este cum sa se foloseasca fara a face reprogramare. In acest caz, este trivial intrucit un sir de intrare poate fi limitat la un sir de caractere in loc de un fisier (&8.5). De exemplu, cin poate fi facut sa citeasca caractere dintr-un sir in loc de intrarea standard: int main(int argc, char* argv[]) {switch(argc) {case 1: break; //citeste din intrarea standard case 2: cin = *new istream(strlen(argv[1]),argv[1]); break; default: error("too many arguments"); return 1; } // ca inainte } Programul este neschimbat exceptind adaugarea argumentelor la main() si utilizarea lor in instructiunea switch. S-ar putea usor modifica main() pentru a accepta diferite argumente in linia de comanda, dar acest lucru nu este necesar, deoarece diferite expresii pot fi pasate ca un singur argument: dc "rate=1.1934;150/rate;19.75/rate217/rate" Ghilimelele sint necesare aici din cauza ca ';' este separator de comenzi in sistemul UNIX. 3.2 Sumar de operatori Operatorii C++ sint descrisi sistematic si complet in &r7. Aici, este un sumar al lor si niste exemple. Fiecare operator este urmat de unul sau mai multe nume utilizate in comun pentru el si de un exemplu de utilizare a lui. In aceste exemple class_name este numele unei clase, member este un nume al unui membru, un object este o expresie care produce un obiect, un pointer este o expresie care produce un pointer, o

expr este o expresie, iar o lvalue este o expresie ce noteaza un obiect neconstant. Un type poate fi un nume de tip general complet (cu *, (), etc.) numai cind el apare in paranteze. Altfel exista restrictii. Operatorii unari si operatorii de atribuire se asociaza de la dreapta; toti ceilalti se asociaza de la stinga. Adica a = b = c inseamna a = (b = c), a + b + c inseamna (a + b) + c, iar *p++ inseamna *(p++), nu (*p)++. | SUMAR DE OPERATORI | ----------------------------------------------------------------| :: domeniu de existenta class_name::member | | :: global ::name | ----------------------------------------------------------------| -> selectare de membru pointer->member | | [] indexare pointer[expr] | | () apel de functie expr(expr_list) | | () constructie de valoare type(expr_list) | | sizeof dimensiunea unui obiect sizeof expr | | sizeof dimensiunea unui tip sizeof(type) | ----------------------------------------------------------------| ++ increment postfixat lvalue++ | | ++ increment prefixat ++lvalue | | -decrement postfixat lvalue-| | -decrement prefixat --lvalue | |~ complement ~expr | |! negare !expr | |minus unar -expr | |+ plus unar +expr | |& adresa &lvalue | |* indirectare *expr | | new creaza(aloca) new type | | delete distruge(dealoca) delete pointer | | delete[] distruge un vector delete[expr]pointer| | (type) conversie de tip (type)expr | ----------------------------------------------------------------|* inmultire expr * expr | |/ impartire expr / expr | |% modulo(rest) expr % expr |

----------------------------------------------------------------|+ adunare(plus) expr + expr | |scadere(minus) expr - expr | ----------------------------------------------------------------| << deplasare stinga expr << expr | | >> deplasare dreapta expr >> expr | ----------------------------------------------------------------|< mai mic expr < expr | | <= mai mic sau egal expr <= expr | |> mai mare expr > expr | | >= mai mare sau egal expr >= expr | ----------------------------------------------------------------| == egal expr == expr | | != diferit expr != expr | ----------------------------------------------------------------|& si pe biti expr & expr | --------------------------------------------------------------------------------------------------------------------------------|^ sau exclusiv pe biti expr ^ expr | ----------------------------------------------------------------|| sau pe biti expr | expr | ----------------------------------------------------------------| && si logic expr && expr | ----------------------------------------------------------------| || sau logic expr || expr | ----------------------------------------------------------------|?: if aritmetic expr ? expr : expr | |= asignare simpla lvalue = expr | | *= inmultire si asignare lvalue *= expr | | /= impartire si asignare lvalue /= expr | | %= modulo si asignare lvalue %= expr | | += adunare si asignare lvalue += expr | | -= scadere si asignare lvalue -= expr | | <<= deplasare stinga si asignare lvalue <<= expr | | >>= deplasare dreapta si asignare lvalue >>= expr | | &= si pe biti si asignare lvalue &= expr | | |= sau pe biti si asignare lvalue |= expr | | ^= sau exclusiv pe biti si asignare lvalue ^= expr | |, virgula(succesiune) expr, expr | -----------------------------------------------------------------

Fiecare dreptunghi contine operatori cu aceeasi prioritate. Un operator are o prioritate mai mare decit operatorii aflati in dreptunghiuri inferioare. De exemplu: a+b*c inseamna a + (b * c) deoarece * are prioritate mai mare decit +, iar a + b - c inseamna (a + b) – c deoarece + si - au aceeasi prioritate, dar operatorii + si - sint asociati de la stinga spre dreapta. 3.2.1 Paranteze rotunde Parantezele rotunde sint suprasolicitate in sintaxa lui C++. Ele au un numar mare de utilizari: includ argumentele in apelurile de functii, include tipul intr-o conversie de tip, includ nume de tipuri pentru a nota functii si, de asemenea, pentru a rezolva conflictul prioritatilor intr-o expresie. Din fericire, ultimul caz nu este necesar foarte frecvent deoarece regulile cu nivelele de prioritate si de asociativitate sint astfel definite ca expresiile sa "functioneze" asa cum ne asteptam (adica sa re flecte utilizarile cele mai frecvente). De exemplu: if(i <= 0 || max < i) //.......... are intelesul obisnuit. Cu toate acestea, parantezele ar trebui utilizate ori de cite ori un programator este in dubiu despre acele reguli: if((i <= 0)||(max < i)) //.......... Utilizarea parantezelor este mai frecventa cind subexpresiile sint mai complicate; dar subexpresiile complicate sint o sursa de erori, asa ca daca simtim nevoia de a folosii paranteze am putea sa descompunem expresiile utilizind variabile auxiliare. Exista, de asemenea, cazuri cind prioritatea operatorilor nu conduce la o interpretare "evidenta". De exemplu: if(i & mask == 0) //.......... nu aplica o masca la i si apoi testeaza daca rezultatul este zero. Intrucit == are o prioritate mai mare decit &, expresia este interpretata ca: i & (mask == 0). In acest caz parantezele sint importante: if((i & mask) == 0) //..........

De asemenea, poate fi util sa observam ca secventa de mai jos nu functioneaza in modul in care s-ar astepta un utilizator naiv: if(0 <= a <= 99) //......... este legal, dar se interpreteaza ca: (0 <= a) <= 99 si rezultatul primei comparatii este 0 sau 1 si nu a (daca a diferit de 1). Pentru a testa daca a este in domeniul 0..99 se poate folosi: if(0 <= a && a <= 99) //..........

3.2.2 Ordinea de evaluare Ordinea de evaluare a subexpresiilor intr-o expresie este nedefinita. De exemplu: int i = 1; v[i] = i++; poate fi evaluata sau ca v[1] = 1, sau ca v[2] = 1. Un cod mai bun se poate genera in absenta restrictiilor asupra ordinii de evaluare a expresiilor. Ar fi mai bine daca compilatorul ne-ar avertiza despre astfel de ambiguitati;majoritatea compilatoarelor nu fac acest lucru. Operatorii && si || garanteaza faptul ca operandul lor sting se evalueaza inaintea celui drept. De exemplu, b = (a = 2, a + 1) atribuie lui b valoarea 3. Exemple de utilizare a lui && si || se dau in paragraful &3.3.1. Sa observam ca operatorul virgula este logic diferit de virgula folosita pentru a separa argumente intrun apel de functie. Sa consideram: f1(v[i], i++); //doua argumente f2((v[i], i++)); //un argument Apelul lui f1 are doua argumente, v[i] si i++, iar ordinea de evaluare a expresiilor argument este nedefinita. Ordinea de evaluare a expresiilor argument este neportabila si nu este precizata. Apelul lui f2 are un singur argument si anume expresia (v[i], i+ +). Parantezele nu pot fi utilizate pentru a forta ordinea de evaluare; a*(b/c) poate fi evaluata ca (a*b)/c deoarece * si / au aceeasi precedenta. Cind ordinea de evaluare este importanta, se pot introduce variabile temporare. De exemplu: (t = b / c, a * t)

3.2.3 Incrementare si Decrementare Operatorul ++ se utilizeaza pentru a exprima o incrementare directa in schimbul exprimarii ei folosind o combinatie intre adunare si atribuire. Prin definitie, ++lvalue inseamna: lvalue += 1 care din nou inseamna lvalue = lvalue + 1 cu conditia ca lvalue sa nu aiba efecte secundare. Expresia care noteaza obiectul de incrementat se evalueaza o singura data. Decrementarea este exprimata similar prin operatorul --. Operatorii ++ si -- pot fi utilizati ambii atit prefix cit si postfix. Valoarea lui ++x este noua valoare a lui x (adica cea incrementata). De exemplu y = ++x este echivalent cu y = (x += 1). Valoarea lui x++ este valoarea veche a lui x. De exemplu y=x++ este echivalent cu y = (t=x, x+=1, t), unde t este o variabila de acelasi tip cu x. Operatorii de incrementare sint utili mai ales pentru a incrementa si decrementa variabile in cicluri. De exemplu se poate copia un sir terminat cu zero astfel: inline void cpy(char* p, const char* q){while(*p++ = *q++);} Sa ne amintim ca incrementind si decrementind pointeri, ca si adunarea sau scaderea dintr-un pointer, opereaza in termenii elementelor vectorului spre care pointeaza pointerul in cauza; p++ face ca p sa pointeze spre elementul urmator. Pentru un pointer de tip T*, are loc prin definitie: long(p + 1) == long(p) + sizeof(T);

3.2.4 Operatori logici pe biti Operatorii logici pe biti &, |, ^, ~, >> si << se aplica la intregi; adica obiecte de tip char, short, int, long si corespunzatoarele lor fara semn (unsigned), iar rezultatele lor sint de asemenea intregi. O utilizare tipica a operatorilor logici pe biti este de a implementa seturi mici (vectori de biti). In acest caz fiecare bit al unui intreg fara semn reprezinta numai un membru al setului, iar numarul de biti limiteaza numarul de membri. Operatorul binar & este interpretat ca intersectie, | ca reuniune si ^ ca diferenta. O enumerare poate fi utilizata pentru a numi membri unui astfel de set. Iata un mic exemplu imprumutat din implementarea (nu interfata utilizator) lui <stream.h>: enum state_value{_good = 0, _eof = 1, _fail = 2, _bad = 4 }; Definirea lui _good nu este necesara. Eu numai am dorit sa existe un nume adecvat pentru starea in care nu sint probleme. Starea unui sir poate fi resetata astfel: cout.state = _good; Se poate testa daca un sir a fost deformat sau o operatie a esuat, ca mai jos: if(cout.state & (_bad | _fail)) //nu este bine

Parantezele sint necesare deoarece & are o precedenta mai mare decit |. O functie care intilneste sfirsitul intrarii poate sa indice acest lucru astfel: cin.state |= _eof. Se utilizeaza operatorul |= deoarece sirul ar putea fi deformat deja (adica state == _bad) asa ca: cin.state = _eof ar fi sters conditia respectiva. Se poate gasi modul in care difera doua stari astfel: state_value diff = cin.state ^ cout.state; Pentru tipul stream_state o astfel de diferenta nu este foarte folositoare, dar pentru alte tipuri similare ea este mai utila. De exemplu, sa consideram compararea unui vector de biti care reprezinta setul de intreruperi de prelucrat cu un altul care reprezinta setul de intreruperi ce asteapta sa fie prelucrat. Sa observam ca utilizind cimpurile (&2.5.1) se obtine o prescurtare convenabila pentru a deplasa masca si a extrage cimpuri de biti dintr-un cuvint. Aceasta se poate face, evident, utilizind operatorii logici pe biti. De exemplu, se pot extrage 16 biti din mijlocul unui int de 32 de biti astfel: unsigned short middle(int a){ return (a >> 8) & 0xffff; } Sa nu se faca confuzie intre operatorii logici pe biti cu cei logici &&, || si !. Acestia din urma returneaza sau 0 sau 1 si ei sint in primul rind utili pentru a scrie teste in if, while sau for (&3.3.1). De exemplu !0 (negatia lui 0) are valoarea 1, in timp ce ~0 (complementul lui zero) reprezinta valoarea -1 (toti biti sint unu). 3.2.5 Conversia tipului Uneori este necesar sa se converteasca o valoare de un tip spre o valoare de un alt tip. O conversie de tip explicit produce o valoare de un tip dat pentru o valoare a unui alt tip. De exemplu: float r = float(1); converteste valoarea 1 spre valoarea flotanta 1.0 inainte de a face atribuirea. Rezultatul unei conversii de un tip nu este o lvalue deci nu i se poate face o asignare (numai daca tipul este un tip referinta). Exista doua notatii pentru conversia explicita a tipului: notatia traditionala din C (de exemplu (double)) si notatia functionala (double(a)). Notatia functionala nu poate fi folosita pentru tipuri care nu au un nume simplu. De exemplu, pentru a converti o valoare spre un pointer se poate folosi notatia din C: char* p = (char*)0777; sau sa se defineasca un nume de tip nou: typedef char* pchar; char* p = pchar(0777); Dupa parerea mea, notatia functionala este preferabila pentru exemple netriviale. Sa consideram aceste doua exemple echivalente: pname n2 = pbase(n1->tp)->b_name; pname n3 = ((pbase)n2->tp)->b_name; Intrucit operatorul -> are prioritate mai mare decit (tip), ultima expresie se interpreteaza astfel:

((pbase)(n2->tp))->b_name Utilizind explicit conversia de tip asupra tipurilor pointer este posibil sa avem pretentia ca un obiect sa aiba orice tip. De exemplu: any_type* p = (any_type*)&some_object; va permite ca some_object sa fie tratat ca any_type prin p. Cind o conversie de tip nu este necesara ea trebuie eliminata. Programele care utilizeaza multe conversii explicite sint mai greu de inteles decit programele care nu le utilizeaza. Totusi, astfel de programe sint mai usor de inteles decit programele care pur si simplu nu utilizeaza tipuri pentru a reprezenta concepte de nivel mai inalt (de exemplu, un program care opereaza cu un registru de periferic folosind deplasari si mascari de intregi in loc de a defini o structura corespunzatoare si o operatie cu ea; vezi &2.5.2). Mai mult decit atit, corectitudinea unei conversii explicite de tip depinde adesea in mod esential de intelegerea de catre programator a modului in care diferite tipuri de obiecte sint tratate in limbaj si foarte adesea de detaliile de implementare. De exemplu: int i = 1; char* pc = "asdf"; int* pi = &i; i = (int)pc; pc = (char*)i; // nu se recomanda: pc s-ar putea sa-si // schimbe valoarea. Pe anumite masini // sizeof(int) < sizeof(char*) pi = (int*)pc; pc = (char*)pi; // nu se recomanda: pc s-ar putea sa-si // schimbe valoarea. Pe anumite masini // char* se reprezinta diferit de int* Pe multe masini nu se va intimpla nimic rau, dar pe altele rezultatul va fi dezastruos. In cel mai bun caz, un astfel de cod nu este portabil. De obicei este gresit sa presupunem ca pointerii la diferite structuri au aceeasi reprezentare. Mai mult decit atit, orice pointer poate fi asignat la un void* (fara un tip explicit de conversie) si un void* poate fi convertit explicit la un pointer de orice tip. In C++, conversia explicita de tip nu este necesara in multe cazuri in care in C este necesara. In multe programe conversia explicita de tip poate fi complet eliminata, iar in multe alte programe utilizarea ei poate fi localizata in citeva rutine. 3.2.6 Memoria libera Un obiect denumit este sau static sau automatic (vezi &2.1.3). Un obiect static se aloca cind incepe programul si exista pe durata executiei programului! Un obiect automatic se aloca de fiecare data cind se intra in blocul lui si este eliminat numai cind se iese din bloc. Adesea este util sa se creeze un obiect nou care exista numai cit timp este nevoie de el. In particular, adesea este util sa se creeze un obiect care poate

fi utilizat dupa ce se revine dintr-o functie in care el a fost creat. Operatorul new creaza astfel de obiecte, iar operatorul delete poate fi folosit pentru a le distruge mai tirziu. Obiectele alocate prin new se spune ca sint in memoria libera. Astfel de obiecte sint de exemplu nodurile unui arbore sau a unei liste inlantuite care sint parte a unei sructuri de date mai mari a carei dimensiune nu poate fi cunoscuta la compilare.Sa consideram modul in care s-ar putea scrie un compilator in stilul folosit la calculatorul de birou. Functiile de analiza sintactica ar putea construi o reprezentare sub forma de arbore a expresiilor, care sa fie utilizata de generatorul de cod. De exemplu: struct enode{ token_value oper; enode* left; enode* right; }; enode* expr() { enode* left = term(); for(;;) switch(curr_tok) { case PLUS : case MINUS: get_token(); enode* n = new enode; n->oper = curr_tok; n->left = left; n->right = term(); left = n; break; default : return left; } } Un generator de cod ar putea utiliza arborele rezultat astfel: void generate(enode* n) { switch(n->oper) { case PLUS: //face ceva potrivit starii curente delete n; } } Un obiect creat prin new exista pina cind este distrus explicit prin delete dupa care spatiul ocupat de el poate fi reutilizat prin new. Nu exista "colectarea rezidurilor". Operatorul delete se poate aplica numai la un pointer returnat de new sau la zero. Aplicarea lui delete la zero nu are nici un efect. Se pot, de asemenea, crea vectori de obiecte prin intermediul lui new. De exemplu:

char* save_string(char* p) { char* s = new char[strlen(p)+1]; strcpy(s, p); return s; } Sa observam ca pentru a dealoca spatiul alocat prin new, delete trebuie sa fie capabil sa determine dimensiunea obiectului alocat. De exemplu: int main(int argc, char* argv[]) { if(argc < 2) exit(1); char* p = save_string(argv[1]); delete p; } Aceasta implica faptul ca un obiect alocat utilizind implementarea standard prin new va ocupa putin mai mult spatiu decit un obiect static (de obicei un cuvint in plus). Este de asemenea, posibil sa se specifice dimensiunea unui vector explicit intr-o operatie de stergere. De exemplu: int main(int argc, char* argv[]) { if(argc < 2) exit(1); int size = strlen(argv[1])+1; char* p = save_string(argv[1]); delete[size] p; } Dimensiunea vectorului furnizata de utilizator se ignora exceptind unele tipuri definite de utilizator (&5.5.5). Operatorii de memorie libera se implementeaza prin functiile (&r7.2.3): void* operator new(long); void operator delete(void*); Implementarea standard a lui new nu initializeaza obiectul returnat. Ce se intimpla daca new nu gaseste memorie de alocat. Intrucit chiar memoria virtuala este finita, uneori se poate intimpla acest lucru; o cerere de forma: char* p = new char[100000000]; de obicei va cauza probleme. Cind new esueaza, ea apeleaza functia spre care pointeaza pointerul _new_handler (pointerii spre functii vor fi discutati in &4.6.9). Noi putem seta acel pointer direct sau sa utilizam functia set_new_handler(). De exemplu: #include <stream.h> void out_of_store() { cerr << "operator new failed: out of store\n"; exit(1);

} typedef void (*PF)(); //pointer spre tipul functiei extern PF set_new_handler(PF); main() { set_new_handler(&out_of_store); char* p = new char[100000000]; cout << "done, p= " << long(p) << "\n"; } de obicei niciodata nu va scrie done dar in schimb va produce: operator new failed: out of store Un _new_handler ar putea face ceva mai destept decit pur si simplu sa termine programul. Daca noi stim cum lucreaza new si delete, de exemplu, deoarece noi furnizam operatorii nostri proprii new() si delete(), handlerul ar putea astepta sa gaseasca memorie pentru new. Cu alte cuvinte, un utilizator ar putea furniza un colector de reziduri, redind in utilizare zonele sterse. Aceasta evident nu este o sarcina pentru un incepator. Din motive istorice, new pur si simplu returneaza pointerul 0 daca el nu gaseste destula memorie si nu a fost specificat un _new_handler. De exemplu: #include <stream.h> main() { char* p = new char[100000000]; cout << "done, p= " << long(p) << "\n"; } va produce done, p= 0 Noi am avertizat! Sa observam ca furnizind _new_handler se verifica depasirea memoriei pentru orice utilizare a lui new in program (exceptind cazul cind utilizatorul furnizeaza rutine separate pentru tratarea alocarii obiectelor de tipuri specifice definite de utilizator; vezi &5.5.6). 3.3 Sumarul instructiunilor Instructiunile C++ sint descrise sistematic si complet in &r.9. Cu toate acestea, dam mai jos un rezumat si citeva exemple. Sintaxa instructiunilor: statement: declaration

{ statement_list_opt } expression_opt; if(expression) statement if(expression) statement else statement switch(expression) statement while(expression) statement do statement while(expression); for(statement expression_opt; expression_opt) statement case constant_expression: statement default: statement break; continue; return expression_opt; goto identifier; identifier: statement statement_list: statement statement statement_list Sa observam ca o declaratie este o instructiune si ca nu exista nici o instructiune de atribuire sau de apel; atribuirea si apelul functiei se trateaza ca expresii. 3.3.1 Teste O valoare poate fi testata sau printr-o instructiune if sau printr-o instructiune switch: if(expression) statement if(expression) statement else statement switch(expression) statement Nu exista in C++ tipul boolean separat. Operatorii de comparare == != < <= > >= returneaza valoarea 1 daca compararea este adevarata si 0 altfel. Nu este ceva iesit din comun ca sa consideram ca true se defineste ca 1 si false ca 0. Intr-o instructiune if se executa prima (sau singura) instructiune daca expresia este diferita de zero si altfel se executa cea de a doua instructiune (daca este prezenta). Aceasta implica faptul ca orice expresie intreaga poate fi utilizata ca o conditie. In particular, daca a este un intreg: if(a) //........ este echivalent cu if(a != 0)

//........ Operatorii logici &&, || si ! sint cei mai utilizati in conditii. Operatorii && si || nu vor evalua cel de al doilea argument al lor numai daca este necesar. De exemplu: if(p && 1 < p->count) //........ intii testeaza ca p nu este nul si numai daca este asa se testeaza 1 < p->count. Anumite instructiuni if simple pot fi inlocuite convenabil inlocuindu-le prin expresii if aritmetice. De exemplu: if(a <= b) max = b; else max = a; este mai bine sa fie exprimat prin max = (a<=b) ? b:a; Parantezele in jurul conditiei nu sint necesare, dar codul este mai usor de citit cind sint utilizate. Anumite instructiuni switch simple pot fi scrise prin mai multe instructiuni if. De exemplu: switch(val) { case 1: f(); break; case 2: g(); break; default: h(); break; } se poate scrie if(val==1) f(); else if(val==2) g(); else h(); Intelesul este acelasi, dar prima versiune (cu switch) este de preferat din cauza ca natura operatiei (testul unei valori fata de un set de constante) este explicita in acest caz. Aceasta face ca instructiunea switch sa fie mai usor de citit. Sa avem grija ca un case al unui switch trebuie terminat cumva daca nu dorim ca executia sa continue cu case-ul urmator. De exemplu: switch(val) { case 1: cout << "case 1\n"; case 2: cout << "case 2\n"; default: cout << "default: case not found\n";

} cu val == 1 va imprima case 1 case 2 default: case not found spre marea surprindere a neinitiatilor. Cel mai frecvent mod de intrerupere al unui case este terminarea prin break, dar se poate adesea folosi o instructiune return sau goto. De exemplu: switch(val) {case 0: cout << "case 0\n"; case 1: cout << "case 1\n"; return; case 2: cout << "case 2\n"; goto case 1; default: cout << "default: case not found\n"; return; } Apelat cu val == 2, produce case 2 case 1 Sa observam ca o scriere de forma goto case 1; este o eroare sintactica. 3.3.2 Goto C++ are faimoasa instructiune goto. goto identifier; identifier: statement Are putine utilizari in limbajele de nivel inalt, dar poate fi foarte util cind un program C++ este generat printr-un program in loc ca programul sa fie scris direct de catre o persoana; de exemplu, goto-urile pot fi utilizate intr-un analizor generat dintr-o gramatica printr-un generator de analizoare. Goto poate fi, de asemenea, important in acele cazuri cind eficienta optimala este esentiala, de exemplu, in ciclul interior al unei aplicatii de timp real. Una din putinele utilizari bune ale lui goto este iesirea dintr-un ciclu imbricat sau switch (instructiunea break intrerupe numai ciclul sau switch-ul cel mai interior care o contine). De exemplu: for(int i=0; i
if(nm[i][j] == a) goto found; // not found //........... found: // nm[i][j] == a; Exista de asemenea instructiunea continue, care transfera controlul la sfirsitul instructiunii ciclice, asa cum s-a explicat in &3.1.5. 3.4 Comentarii si Decalari Utilizarea judicioasa a comentariilor si utilizarea consistenta a decalarilor poate face sarcina citirii si intelegerii unui program mai placuta. Exista diferite stiluri ale decalarilor. Autorul nu vede motive fundamentale pentru a prefera un stil fata de altul (deci, ca multi altii, eu am preferintele mele). Acelasi lucru se aplica si la stilurile de comentare. Comentariile pot fi omise, dar atunci citirea programului va fi serios afectata. Compilatorul nu intelege continutul unui comentariu, asa ca nu exista nici o cale de a ne asigura ca un comentariu: [1] este de neinteles; [2] descrie programul; [3] este pus la zi. Multe programe contin comentarii care sint incomprehensibile, ambigue si chiar eronate. Comentariile rele pot fi mai rele decit daca nu ar exista. Daca ceva poate fi exprimat in limbajul insusi, ar trebui sa fie mentionat in el, nu numai intr-un comentariu. Aceasta remarca este intarita de comentariile de mai jos: //variabila "v" trebuie sa fie initializata //variabila "v" trebuie sa fie folosita numai de functia "f()" //apeleaza functia "init()" inainte de a apela //orice alta functie din acest fisier //apeleaza functia "cleanup()" la sfirsitul programului //sa nu se utilizeze functia "wierd()" //functia "f()" are doua argumente Astfel de comentarii pot adesea sa fie interpretate ca necesare printr-o utilizare corespunzatoare a lui C++. De exemplu, s-ar putea utiliza regulile de linkere (&4.2) si vizibilitate, initializare si curatire pentru clase (vezi &5.5.2) pentru a face exemplele precedente redondante. Odata ce a fost afirmat ceva clar in limbaj, nu ar trebui mentionat a doua oara intr-un comentariu. De exemplu: a = b+c // a devine b+c count++ // se incrementeaza count

Astfel de comentarii sint mai rele decit redondanta: ele maresc cantitatea de text pe care trebuie sa o citeasca programatorul si ele adesea fac mai obscura structura programatorului. Preferintele autorului sint pentru: [1] Un comentariu pentru fiecare fisier sursa care sa afirme ce declaratii din el se utilizeaza in comun, scopuri generale pentru mentinere, etc. [2] Un comentariu pentru fiecare functie netriviala care sa indice scopul ei, algoritmul utilizat (daca nu este evident) si poate ceva despre mediul de executie al ei. [3] Citeva comentarii in locurile unde codul nu este evident si/sau neportabil. [4] Foarte mici alternative else. De exemplu: // tbl.c: Implementarea tabelei de simboluri /* Eliminare Gauss prin pivotare partiala. Vezi Ralston:...pg... */ //swap() presupune utilizarea stivei la un AT&T 3B20. /********************************* Copyright (c) 1984 AT&T. Inc. All rights reserved *********************************/ Un set de comentarii bine ales si bine scris este o parte esentiala a unui program bun. Scrierea de comentarii bune poate fi tot atit de dificil ca si scrierea programului insusi. Sa observam, de asemenea, ca daca se folosesc comentariile cu // intr-o functie, atunci orice parte a acelei functii poate fi comentata utilizind stilul de comentarii /*...*/ si viceversa.

3.5

Exercitii

1. (*1). Sa se scrie instructiunea urmatoare ca o instructiune while echivalenta: for(i = 0; i < max_length; i++) if(input_line[i] == '?') quest_count++;

Sa se rescrie utilizind un pointer ca si variabila de control; adica asa ca testul sa fie unul de forma *p == '?'. 2. (*1). Sa se includa complet in paranteze expresiile urmatoare: a = b + c * d << 2 & 8 a & 077 != 3 a == b || a == c && c < 5 c = x != 0 0 <= i < 7 f(1, 2) + 3 a = -1+ +b-- -5 a = b == c++ a=b=c=0 a[4][2] *= *b ? c : *d * 2 a - b, c = d 3. (*2). Sa se gaseasca 5 constructii C++ diferite pentru care sensul este nedefinit. 4. (*2). Sa se gaseasca 10 exemple de cod C++ neportabile. 5. (*1). Ce se intimpla daca se face o impartire cu zero pe sistemul d-voastra? Ce se intimpla in cazul unei depasiri superioare sau inferioare. 6. (*1). Sa se includa complet in paranteze expresiile urmatoare: *p++ *--p +++a-(int*)->m *p.m *a[i] 7. (*2). Sa se scrie functiile strlen() care returneaza lungimea unui sir, strcpy() care copiaza un sir in altul si strcmp() care compara doua siruri. Sa se considere ce tipuri de argumente si ce tipuri se cuvine sa se returneze, apoi sa se compare cu versiunile standard asa cum sint declarate in <string.h>. 8.

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

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

9. (*2). Sa se scrie o functie cat() care are doua argumente de tip sir si returneaza un sir care este concatenarea argumentelor. Sa se utilizeze new pentru a gasi memorie pentru rezultat. Sa se scrie o functie rev() care are un argument de tip sir si reutilizeaza caracterele din el. Adica, dupa rev(p), ultimul caracter a lui p va fi primul, etc. 10. (*2). Ce face exemplul urmator? void send(register* to, register* from, register count) {register n = (count+7)/8; switch(count%8) { case 0: do{ *to++ = *from++; case 7: *to++ = *from++; case 6: *to++ = *from++; case 5: *to++ = *from++; case 4: *to++ = *from++; case 3: *to++ = *from++; case 2: *to++ = *from++; case 1: *to++ = *from++; }while(--n > 0); } } De ce ar vrea cineva sa scrie un astfel de program? 11. (*2). Sa se scrie o functie atoi() care are ca argument un sir ce contine cifre si returneaza int-ul corespunzator. De exemplu, atoi("123") este 123. Sa se modifice atoi() pentru a trata sirurile octale din C++ si in plus si cele hexazecimale. Sa se modifice atoi() pentru a trata caracterele C++ utilizate intr-o notatie de constanta. Sa se scrie o functie itoa() care creaza un sir pornind de la un argument intreg. 12. (*2). Sa se rescrie get_token() (&3.1.2) asa ca sa citeasca o linie la un moment dat intr-un buffer si apoi sa compuna unitatile citind caracterele din buffer. 13. (*2). Sa se adauge functii de forma sqrt(), log() si sin() la calculatorul de birou din &3.1. Sa se predefineasca numele si apelul functiilor printr-un vector de pointeri spre functii. Sa nu se uite sa se verifice argumentele dintr-o functie call. 14. (*3). Sa se permita unui utilizator sa defineasca functii in calculatorul de birou: Scop: Sa se defineasca o functie ca un sir de operatii exact asa cum un utilizator ar trebui sa o scrie. Un astfel de sir poate fi memorat sau ca un sir de caractere sau ca o lista de unitati. Apoi se citeste si se executa acele operatii cind functia este apelata. Daca noi dorim ca o functie utilizator sa aiba argumente, noi trebuie sa inventam o notatie pentru aceasta. 15. (*1.5). Sa se converteasca calculatorul de birou pentru a utiliza un simbol structura in loc sa se utilizeze variabilele statice name_string si number_value: struct symbol{

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

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

Introducere

A avea un program complet intr-un fisier este de obicei imposibil deoarece codul pentru bibliotecile standard si de sistem sint in alta parte. Mai mult decit atit, avind fiecare utilizator codul sau intr-un singur fisier este ceva care este atit impractic cit si inconvenient. Modul in care este organizat un program in fisiere poate ajuta cititorul sa inteleaga structura unui program si sa permita compilatorului sa impuna acea structura. Intrucit unitatea de compilare este un fisier, tot fisierul trebuie sa fie recompilat ori de cite ori s-a facut in el o schimbare.

Pentru un program dimensionat chiar moderat, timpul petrecut pentru recompilare poate fi redus semnificativ partitionind programul in fisiere dimensionate potrivit. Sa consideram exemplul cu calculatorul de birou. A fost prezentat ca un singur fisier sursa. Daca il tastam, noi fara indoiala avem niste probleme minore in obtinerea declaratiilor in ordine corecta si cel putin o declaratie trebuie utilizata pentru a permite compilatorului sa trateze functiile mutual recursive expr(), term() si prim(). Textul amintit are patru parti (analizor lexical, analizor sintactic, tabela de simboluri si un driver), dar aceasta nu se reflecta in nici un fel in cod. In realitate calculatorul nu a fost scris in acest fel. Acesta nu este modul de a o face; chiar daca toate consideratiile metodologiei de programare, mentinere si eficienta compilarii au fost deconsiderate pentru acest program, autorul totusi va partitiona acest program de 200 de linii in mai multe fisiere pur si simplu pentru a face sarcina programarii mai placuta. Un program care consta din mai multe parti compilate separat trebuie sa fie consistent (necontradictoriu) in utilizarea numelor si tipurilor in exact acelasi mod ca si un program care consta dintr-un singur fisier sursa. In principiu, aceasta se poate asigura prin linker. Linkerul este programul care leaga partile compilate separat. Un linker uneori este numit (gresit) incarcator; linkerul UNIX-ului se numeste ld. Cu toate acestea linkerul disponibil pe majoritatea sistemelor este prevazut cu putine facilitati care sa verifice consistenta modulelor compilate separat. Programatorul poate compensa lipsa acestor facilitati ale linkerului furnizind informatii de tip suplimentare (declaratii). Un program poate fi realizat consistent asigurind ca declaratiile prezentate in compilari separate sa fie consistente. C++ a fost definit ca un instrument care sa incurajeze astfel de compilari cu declaratii explicite si este prevazut un linker care sa verifice consistenta modulelor respective. Un astfel de linker se spune ca face o linkare explicita. In cazul limbajului C nu se realizeaza o linkare explicita ci numai una implicita si ea este adesea saraca in testarea consistentei modulelor linkate. 4.2.

Link-editare

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

int f(); void g(){a = f();} 'a' si f() utilizati in file2.c sint cele definite in file1.c. Cuvintul cheie extern indica faptul ca declaratia lui a in file2.c este (chiar) o declaratie si nu o definitie. Daca 'a' ar fi fost initializata, extern ar fi fost pur si simplu ignorata deoarece o declaratie cu initializator este totdeauna o definitie. Un obiect trebuie sa fie definit exact odata intr-un program. Poate fi declarat de mai multe ori, dar tipul trebuie sa coincida exact. De exemplu: // file1.c: int a = 1; int b = 1; extern int c; // file2.c: int a; extern double b; extern int c; Exista trei erori: 'a' este definit de doua ori (int a: este o definitie insemnind int a = 0); 'b' este declarat de doua ori cu diferite tipuri; 'c' este declarat de doua ori dar nu este definit. Aceste tipuri de erori (erori de linkare) nu pot fi detectate cu un compilator care analizeaza odata numai un fisier. Ele sint, totusi, detectate la linkare. Programul urmator nu este in C++ (chiar daca el este in C): // file1.c: int a; int f(){return a;} // file2.c: int a; int g(){return f();} Intii, file2.c nu este C++ deoarece f() nu a fost declarat, asa ca, compilarea va esua. In al doilea rind programul nu se va putea linka deoarece 'a' este definit de doua ori. Un nume poate fi local la un fisier declarindu-l static. De exemplu: // file1.c static int a = 6; static int f(){/*.......*/} // file2.c static int a = 7; static int f(){/*.......*/} Intrucit fiecare 'a' si f() este declarat static, programul rezultat este corect. Fiecare fisier are pe 'a' si f() propriu. Cind variabilele si functiile sint declarate static explicit, un fragment de program este mai usor de inteles (nu trebuie sa ne uitam in alta parte). Utilizind static pentru functii putem avea, de asemenea, un efect benefic asupra cantitatii de functii utilizate si dind compilatorului informatii care pot fi utilizate in ideea realizarii unor optimizari. Consideram aceste doua fisiere:

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

Fisiere antet

Tipurile in toate declaratiile aceluiasi obiect trebuie sa fie consistente. Un mod de a atinge acest lucru ar fi de a furniza facilitatile de verificare de tip linkerului, dar deoarece multe linkere au fost proiectate in 1950 ele nu pot fi schimbate din motive practice. Este usor a schimba un linker, dar facind aceasta si scriind un program care depinde de imbunatatirile facute, cum mai poate fi acest program transferat portabil pe alte calculatoare ? O alta conceptie este de a asigura ca,codul supus compilarii sa fie sau consistent sau sa contina chei care sa permita compilatorului sa detecteze inconvenientele. O metoda imperfecta dar simpla de a atinge consistenta pentru declaratii in diferite fisiere este de a include fisiere antet, care sa contina informatii de interfata din fisierele sursa care contin cod executabil si/sau definitii de date. Mecanismul #include este o facilitate extrem de simpla de manipulare a textului sursa pentru a culege fragmente de programe sursa impreuna intr-o singura unitate (fisier) pentru compilare. Directiva: #include "to_be_included" inlocuieste linia in care apare #include cu continutul fisierului "to_be_included". Continutul ar trebui sa fie text sursa C++ intrucit compilatorul va proceda la citirea lui. Adesea, incluziunea este gestionata printr-un program separat numit preprocesor C, apelat de CC pentru a transforma fisierul sursa prezentat de programator intr-un

fisier fara a include directivele inainte de a incepe compilarea propriuzisa. O alta varianta este ca, compilatorul sa gestioneze aceste directive pe masura ce ele apar in textul sursa. Daca programatorul vrea sa vada efectul directivelor include, se poate folosi comanda: CC -E file.c pentru a prelucra fisierul file.c in acelasi mod ca si cind CC ar fi inainte de a incepe compilarea propriu-zisa. Pentru a include fisiere standard, se utilizeaza parantezele unghiulare in locul ghilimelelor. De exemplu: #include <stream.h> // din directorul include standard #include "myheader.h" // din directorul curent Avantajul lui "<", ">" este faptul ca numele real al directorului standard pentru include nu este construit in program. Un spatiu este semnificativ intr-o directiva include: #include < stream.h > // nu va gasi stream.h Ar fi extravagant sa se recompileze un fisier de fiecare data cind este inclus undeva, dar timpul necesar pentru a compila un astfel de fisier de obicei nu difera mult de timpul necesar pentru a citi o anumita forma precompilata a lui. Motivul este ca textul programului este o reprezentare cit se poate de compacta a programului si ca fisierele incluse, de obicei, contin numai declaratii si nu un cod care trebuie sa fie analizat extensiv de cart compilator. Regula urmatoare despre ce poate si ce nu poate fi plasat intr-un fisier antet nu este o cerinta a limbajului, ci pur si simplu o sugestie despre un mod rezonabil de a utiliza mecanismul #include. Un fisier antet poate contine: |Definitii de tip struct point{int x, y;}; | |Declaratii de functii extern int strlen{const char*}; | |Definitii de functii inline inline char get(){return *p++;}; | |Declaratii de date extern int a; | |Definitii de constante const float pi = 3.141593; | |Enumerari enum bool {false, true}; | |Directive #include #include <signal.h> | |Macro definitii #define Case break; case | |Comentarii /* check for end of file */ | Dar niciodata nu contine: |Definitii de functii ordinare char get(){return *p++;} | |Definitii de date int a; | |Definitii de agregate constante const tbl[] = {/*...*/}; |

In sistemul UNIX, fisierele antet sint cu extensia convenabila .h. Fisierele care contin definitii de functii si date vor avea extensia .c. De aceea ele sint frecvent referite ca "fisiere.h" si respectiv "fisiere.c". Macrourile se descriu in &4.7. Sa observam ca macrourile sint pe departe Mai putin utile in C++ decit in C, deoarece C++ are constructia const in limbaj pentru a defini constante inline. Motivul de a admite definirea de constante simple si nu si a agregatelor constante in fisierele.h este pragmatic. In principiu exista o singura problema in admiterea copiilor definitiilor de variabile (chiar si definitiile functiilor pot fi copiate). Cu toate acestea, este foarte dificil pentru un linker vechi sa verifice identitatea constantelor netriviale si sa elimine duplicatele nenecesare. Mai mult decit atit, cazurile simple sint pe departe mai frecvente si de aceea mai importante pentru generarea de cod. 4.3.1. Fisier antet unic Cea mai simpla solutie la problema partitionarii unui program in diferite fisiere este de a pune definitiile de functii si date intr-un numar potrivit de fisiere sursa si de a declara tipurile necesare pentru a comunica, intr-un singur fisier antet care este inclus de toate celelalte fisiere. Pentru programul calculator putem folosi fisiere.c : lex.c, sgn.c, table.c, main.c si un fisier antet dc.h, care contine declaratiile fiecarui nume utilizat in Mai mult decit un fisier.c: //dc.h declaratii comune pentru programul calculator #include <stream.h> enum token_value { NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*', DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')' }; extern int no_of_errors; extern double error(char* s); extern token_value get_token(); extern token_value curr_tok; extern double number_value; extern char name_string[256]; extern double expr(); extern double term(); extern double prim(); struct name{ char* string;

name* next; double value; }; extern name* look(char* p, int ins = 0); inline name* insert(char* s){return look(s, 1);} Codul real al lui lex.c va arata astfel: //lex.c : analiza de intrare si analiza lexicala #include "dc.h" #include token_value curr_tok; double number_value; char name_string[256]; token_value get_token() { /* ... */ } Sa observam ca, utilizind fisierele antet in acest fel se asigura ca fiecare declaratie a unui obiect definit de utilizator intr-un fisier antet va fi intr-un anumit punct inclus fisierul in care el este definit. De exemplu, cind compilam lex.c, compilatorul va intilni: extern token_value get_token(); // ... token_value get_token() { /* ... */ } Aceasta asigura ca, compilatorul va detecta orice inconsistenta in tipurile specificate pentru un nume. De exemplu, daca get_token() a fost declarat sa returneze o valoare de tip token_value, dar este definit sa returneze un int, atunci compilarea lui lex.c va esua, cu eroare de neconcordanta de tip. Fisierul sgn.c va arata astfel: //sgn.c : analiza sintactica si evolutiva #include "dc.h" double prim() { /* ... */ } double term() { /* ... */ } double expr() { /* ... */ } Fisierul table.c va arata astfel : //table.c : tabela de simboluri si lookup #include "dc.h" extern char* strcmp(const char*, const char*); extern char* strcpy(char*, const char*); extern int strlen(const char*); const TBLSZ = 23; name table[TBLSZ];

name* look(char* p, int ins) { /* ... */ } Sa observam ca table.c declara el insusi functiile standard de manipulare a sirurilor, asa ca nu exista modificari de consistenta asupra acestor declaratii. Este aproape totdeauna mai bine sa se includa un fisier antet decit sa se declare un nume extern intr-un fisier.c. Aceasta ar putea implica sa se includa "prea mult", dar aceasta nu afecteaza serios timpul necesar pentru compilare si de obicei va economisi timp pentru programator. Ca un exemplu al acestui fapt sa observam cum se redeclara strlen() din nou in main.c (de mai jos). Aceasta este o sursa potentiala de erori intrucit compilatorul nu poate verifica consistenta celor doua declaratii. Evident, aceasta problema s-ar putea elimina daca fiecare declaratie externa s-ar plasa in dc.h. Aceasta neglijenta a fost lasata in program din cauza ca este foarte frecventa in programele C si conduce la erori care insa nu sint greu de depistat. In final, fisierul main.c va arata astfel: //main.c: initializare ciclu principal si tratarea erorilor #include "dc.h" int no_of_errors; double error(char* s) { /* ... */ } extern int strlen(const char*); main(int argc, char* argv[]){//...} Exista un caz important in care dimensiunea fisierelor antet devine o pacoste serioasa. Un set de fisiere antet si o biblioteca pot fi utilizate pentru a extinde limbajul cu un set de tipuri generale si specifice aplicatiei (vezi capitolele 5-8). In astfel de cazuri, nu este iesit din comun sa gasim mii de linii ale fisierelor antet la inceputul fiecarui fisier care se compileaza. Continutul acelor fisiere este de obicei "inghetat" si se schimba foarte rar. O tehnica pentru a incepe compilarea cu continutul acestor fisiere antet poate fi de mare utilitate. Intr-un sens, se poate crea un anumit limbaj cu un anumit sens special cu ajutorul compilatorului existent. Nu exista proceduri standard pentru a crea un astfel de sistem de compilare. 4.3.2 Fisiere antet multiple Stilul unui singur fisier antet pentru un program partitionat este mult mai util cind programul este mic si partile lui nu se intentioneaza sa se utilizeze separat. Apoi, nu este o situatie serioasa faptul ca nu este posibil sa se determine care declaratii se plaseaza in fisierul antet si pentru ce motiv. Comentariile pot fi de ajutor. O alternativa este sa lasam ca fiecare parte a unui program sa aiba fisierul antet propriu care defineste facilitatile pe care le furnizeaza el. Fiecare fisier.c are atunci un fisier.h

corespunzator si fiecare fisier.c include fisierul.h propriu (care specifica ce furnizeaza el) si de asemenea pot fi si alte fisiere.h (care specifica de ce are el nevoie). Considerind aceasta organizare pentru calculator, noi observam ca error() este utilizata exact ca fiecare functie din program si ea insasi utilizeaza numai <stream.h>. Aceasta este tipic pentru functiile error() si implica faptul ca error() ar trebui sa fie separata de main(): //error.h: trateaza erorile extern int no_errors; extern double error(char* s); //error.c #include <stream.h> #include "error.h" int no_of_errors; double error(char* s) { /* ... */ } In acest stil de utilizare a fisierelor antet, un fisier.h si un fisierul.c pot fi vazute ca un modul in care fisierul.h specifica o interfata si fisierul.c specifica implementarea. Tabela de simboluri este independenta de restul, exceptind utilizarea functiei error(). Aceasta se poate face acum explicit: //table.h : declaratiile tabelei de simboluri struct name{ char* string; name* next; double value; }; extern name* look(char* p, int ins = 0); inline name* insert(char* s){return look(s, 1);} //table.c : definitiile tabelei de simboluri #include "error.h" #include <string.h> #include "table.h" const TBLSZ = 23; name* table[TBLSZ]; name* look(char* p, int ins) { /* ... */ } Sa observam ca declaratiile functiilor de manipulare a sirurilor sint incluse in <string.h>. Aceasta elimina o alta sursa potentiala de erori. //lex.h: declaratii pentru intrare si analiza lexicala enum token_value{

NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*', DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')' }; extern token_value curr_tok; extern double number_value; extern char name_string[256]; extern token_value get_token(); Aceasta interfata cu analizorul lexical este cit se poate de incurcata. Lipsa unui tip propriu de lexic arata necesitatea de a prezenta utilizatorului pe get_token() cu bufferele de lexicuri reale number_value si name_string. //lex.c : definitiile pentru intrare si analiza lexicala #include <stream.h> #include #include "error.h" #include "lex.h" token_value curr_tok; double number_value; char name_string[256]; token_value get_token() { /* ... */ } Interfata cu analizorul sintactic este curata: //syn.h : declaratii pentru analiza sintactica si evoluare #include "error.h" #include "lex.h" #include "syn.h" double prim() { /* ... */ } double term() { /* ... */ } double expr() { /* ... */ } Programul principal este pe cit de uzual pe atit de trivial: #include <stream.h> #include #include <syn.h> #include #include <string.h> main(int argc, char* argv[]) { /* ... */ } Cit de multe fisiere antet sa se utilizeze intr-un program depinde de multi factori. Multi dintre acestia au de a face mai mult cu modul de tratare al fisierelor pe sistemul

dumneavoastra, decit cu C++. De exemplu, daca editorul nu are facilitati de a cauta in acelasi timp in mai multe fisiere, utilizarea multor fisiere antet devine mai putin atractiva. Analog, daca deschiderea si citirea a 10 fisiere de 50 de linii fiecare este substantial mai costisitor decit citirea unui singur fisier de 500 de linii. Noi trebuie sa gidim de doua ori inainte de a folosi stilul fisierelor antet multiple pentru un program mic. Un sfat: un set de 10 fisiere antet plus fisierele standard antet este de obicei ceva normal de gestionat. Totusi, daca partitionati declaratiile unui program mare in fisiere antet de dimensiuni logic minime (punind fiecare declaratie de structura intrun fisier propriu, etc.), atunci ve-ti ajunge usor la sute de fisiere greu de gestionat. 4.3.3 Ascunderea datelor Utilizind fisierele antet, un utilizator poate defini explicit interfetele pentru a asigura utilizarea consistenta a tipurilor dintr-un program. Cu toate acestea, un utilizator poate ocoli interfata furnizata printr-un fisier antet inserind declaratiile externe in fisierele.c. Sa observam ca stilul urmator de legatura nu este recomandat: //file1.c : "extern" nu se utilizeaza int a = 7; const c = 8; void f(long) { /* ... */ } //file2.c : "extern" in fisierul.c extern int a; extern const c; extern f(int); int g(){ return f(a+c); } Intrucit declaratiile extern din file2.c nu sint incluse cu definitiile din file1.c compilatorul nu poate verifica consistenta acestui program. In consecinta, daca incarcatorul nu este mai destept decit de obicei, cele doua erori din acest program va trebui sa le gaseasca programatorul. Un utilizator poate proteja un fisier impotriva unei astfel de legaturi indisciplinate declarind ca static acele nume care nu se intentioneaza sa se utilizeze global. Astfel, ele au ca dome- niu fisierul respectiv si sint interzise pentru alte parti din program. De exemplu: //table.c : definitia tabelei de simboluri #include "error.h" #include <string.h> #include "table.h" const TBLSZ = 23; static name* table[TBLSZ]; name* look(char* p, int ins) { /* ... */ }

Aceasta va asigura ca toate accesele la table sa se faca prin look(). Nu este necesar sa se "ascunda" constanta TBLSZ. 4.4

Fisiere si Module

In sectiunea precedenta fisierele.c si .h definesc impreuna o parte a programului. Fisierul.h este interfata utilizata de alte parti ale programului; fisierul.c specifica implementarea. O astfel de entitate este numita, adesea, modul. Numai numele de care are nevoie sa le cunoasca utilizatorul se fac disponibile iar restul sint ascunse. Aceasta proprietate se numeste adesea ascunderea datelor, chiar daca data este numai unul din lucrurile ce se pot ascunde. Acest tip de modul furnizeaza o flexibilitate mare. De exemplu, o implementare poate consta din unul sau Mai multe fisiere.c si diferite interfete ce pot fi furnizate sub forma de fisiere.h. Informatia pe care un utilizator nu este necesar sa o cunoasca este ascunsa in fisierul.c. Daca se considera ca utilizatorul nu trebuie sa stie exact ce contine fisierul.c, atunci el nu trebuie sa fie disponibil in sursa. Fisierele de tip .obj sint suficiente. Este uneori o problema ca aceasta flexibilitate sa fie atinsa fara o structura formala. Limbajul insusi nu recunoaste un astfel de modul ca o entitate si nu exista nici o cale ca, compilatorul sa faca distinctie intre fisierele.h care definesc nume ce sa fie utilizate de alte module (exportate) de fisierele.h folosite pentru a declara nume din alte module (importate). Alta data, poate fi o problema ca un modul sa defineasca un set de obiecte si nu un nou tip. De exemplu, modulul table defineste o tabela; daca noi dorim doua tabele, nu exista un mod trivial de a furniza celalalt tabel utilizind aceasta idee de module. Capitolul 5 prezinta o solutie a acestei probleme. Fiecare obiect alocat static este implicit initializat cu zero, iar alte valori (constante) pot fi specificate de programator. Aceasta este doar o forma primitiva de initializare. Din fericire, utilizind clasele, se poate specifica un cod care sa fie executat pentru initializare inainte de a face orice utilizare a modulului si de asemenea se poate executa cod pentru anulare (curatire) dupa ultima utilizare a modulului. (vezi &5.5.2). 4.5

Cum se construieste o biblioteca

Fraze de genul "pune in biblioteca" si "gaseste intr-o anumita biblioteca" se utilizeaza des (in aceasta carte si in alta parte), dar ce inseamna acest lucru pentru un program C++ ?

Din nefericire, raspunsul depinde de sistemul de operare utilizat. Aceasta sectiune explica cum se face si se utilizeaza o biblioteca in versiunea 8 a sistemului UNIX. Alte sisteme furni zeaza facilitati similare. O biblioteca, in principiu, este o multime de fisiere.o obtinute prin compilarea unui set de fisiere.c. De obicei exista unul sau mai multe fisiere.h care contin declaratii necesare pentru a utiliza acele fisiere.o. Ca un exemplu, sa consideram ca avem de furnizat (in mod convenabil) un set de functii matematice pentru o multime nespecificata de utilizatori. Fisierul antet ar putea arata astfel: extern double sqrt(double); //subset al lui <math.h> extern double cos(double); extern double exp(double); extern double log(double); iar definitiile acestor functii vor fi memorate in fisierele sqrt.c, sin.c, cos.c, exp.c si respectiv log.c. O biblioteca numita math.a poate fi facuta astfel: $cc -c math.c sin.c cos.c exp.c log.c $ar cr math.a sqrt.o sin.o cos.o exp.o log.o $ranlib math.a Fisierele sursa se compileaza intii obtinindu-se fisiere obiect echivalente. Se utilizeaza apoi comanda ar pentru a face o arhiva numita math.a. In final arhiva respectiva este indexata pentru un acces mai rapid. Daca sistemul dumneavoastra nu are comanda ranlib, atunci probabil ca nu aveti nevoie de ea; sa va uitati in manualul de operare pentru detalii. Biblioteca poate fi utilizata astfel: $cc myprog.c math.a Acum, care este avantajul utilizarii lui math.a in loc de a utiliza direct fisierele.o? De exemplu: $ myprog.c sqrt.o sin.o cos.o exp.o log.o Pentru majoritatea programelor, gasirea setului corect de fisiere.o nu este un lucru trivial. In exemplul de mai sus, ele au fost toate incluse, dar daca functiile din myprog.c apeleaza numai functiile sqrt() si cos() atunci pare ca ar fi suficient: $cc myprog.c sqrt.o cos.o Acest lucru nu este tocmai asa deoarece cos.c utilizeaza sin.c. Linkerul apelat de comanda cc ca sa foloseasca un fisier.a (in acest caz math.a) stie sa extraga numai fisierele.o necesare, din multimea care a fost utilizata pentru a crea fisierul.a. Cu alte cuvinte, folosind o biblioteca, se pot include multe definitii folosind un singur nume (inclusiv definitii de functii si variabile utilizate de functii interne pe care utilizatorul nu le-a vazut niciodata) si in acelasi timp se asigura numai un numar minim de definitii include. 4.6

Functii

Modul tipic de a face ceva intr-un program C++ este de a apela o functie care sa faca lucrul respectiv. Definirea unei functii este o cale de a specifica cum sa se faca o operatie. O functie nu poate fi apelata daca ea nu este declarata. 4.6.1 Declaratii de functii O declaratie de functie da un nume functiei, tipul valorii returnate (daca returneaza vreuna) de functie, numarul si tipurile argumentelor care trebuie furnizate in apelul unei functii. De exemplu: extern double sqrt(double); extern elem* next_elem(); extern char* strcpy(char* to, const char* from); extern void exit(int); Semantica transferului de argumente este identica cu semantica initializarii. Tipurile argumentelor se verifica si se fac conversii implicite ale tipurilor argumentelor cind este necesar. De exemplu, dindu-se declaratiile precedente: doublesr2 = sqrt(2); va apela corect functia sqrt() cu valoarea 2.0. O declaratie de functie poate contine nume de argumente. Acest lucru poate fi un ajutor pentru cititor, dar compilatorul ignora pur si simplu astfel de nume. 4.6.2 Definitii de functii Fiecare functie care este apelata intr-un program trebuie sa fie definita undeva (o singura data). O definitie de functie este o declaratie de functie in care este prezent corpul functiei. De exemplu: extern void swap(int*, int*); //o declaratie void swap(int* p, int* q) //o definitie { int t = *p; *p = *q; *q = t; } O functie poate fi declarata inline pentru a elimina apelul functiei suprapunind-o peste el (&1.12), iar argumentele pot fi declarate register pentru a furniza un acces mai rapid la ele (&2.3.11). Ambele caracteristici pot fi eliminate si ele ar trebui sa fie eliminate ori de cite ori exista dubii in legatura cu utilitatea folosirii lor.

4.6.3 Transferul argumentelor Cind se apeleaza o functie se rezerva memorie pentru argumentele formale si fiecare argument formal se initializeaza prin argumentele efective corespunzatoare. Semantica transferului de parametri este identica cu semantica initializarii. In particular se verifica tipul unui argument efectiv cu tipul argumentului formal corespunzator si se fac toate conversiile de tip standard si definite de utilizator. Exista reguli speciale pentru transferul vectorilor (&4.6.5), o facilitate pentru transferul neverificat al argumentelor (&4.6.8) si o facilitate pentru specificarea argumentelor implicite (&4.6.6). Consideram: void f(int val, int& ref) { val++; ref++; } Cind se apeleaza f(), val++ mareste o copie locala a primului sau argument, in timp ce ref++ incrementeaza cel de al doilea argument efectiv. De exemplu: int i = 1; int j = 1; f(i, j); va incrementa pe j dar nu si pe i. Primul argument i este pasat prin valoare, iar cel de al doilea prin referinta. Asa cum s-a mentionat in &2.3.10, folosind functii care modifica argumentele apelate prin referinta se pot face programe greu de citit si in general ar trebui eliminate (dar vezi &6.5 si &8.4). Totusi, este mult mai eficient ca un obiect mare sa fie transferat prin referinta in loc sa fie transferat prin valoare. In acest caz, argumentul ar putea fi declarat const pentru a indica faptul ca referinta se utilizeaza numai din motive de eficienta iar functia apelata nu poate schimba valoarea obiectului: void f(const large& arg) { //valoarea lui arg nu poate fi schimbata } Analog, declarind un argument pointer const, cititorul este avertizat ca valoarea obiectului spre care pointeaza acel argument nu se schimba prin functia respectiva. De exemplu : extern int strlen(const char*); //din <string.h> extern char* strcpy(char* to, const char* from); extern int strcmp(const char*, const char*); Importanta acestei practici creste cu dimensiunea programului. Sa observam ca semantica transferului de argumente este diferita de semantica asignarii. Acest lucru este important pentru argumentele const, pentru argumentele referinta si pentru argumentele unor tipuri definite de utilizator (&6.6). 4.6.4 Valoarea returnata

O valoare poate fi (si trebuie) returnata dintr-o functie care nu este declarata void. Valoarea returnata se specifica printr-o instructiune return. De exemplu: int fact(int n) { return (n>1) ? n*fact(n-1) : 1; } Pot fi mai multe instructiuni return intr-o functie: int fact(int n) { if(n > 1) return n*fact(n-1); else return 1; } Ca si semantica transferului de argumente, semantica valorii returnate de o functie este identica cu semantica initializarii. O instructiune return se considera ca initializeaza o variabila de tipul returnat. Tipul expresiei returnate se verifica cu tipul valorii returnate de functie si la nevoie se fac toate conversiile de tip standard sau definite de utilizator. De exemplu: double f() { // ... return 1; //se converteste spre double(1) } De fiecare data cind se apeleaza o functie se creaza o copie noua pentru argumentele si variabilele automatice ale ei. Memoria este eliberata la revenirea din functie, asa ca nu este indicat sa se returneze un pointer spree o variabila locala. Continutul locatiei spre care se face pointarea se va schimba imprevizibil: int* f() { int local = 1; // ... return &local; //nu se face asa ceva } Din fericire, compilatorul avertizeaza asupra unor astfel de valori returnate. Iata un alt exemplu:

int& f() { return 1; }

//nu se face asa ceva

4.6.5 Argumente vector Daca se utilizeaza un vector ca un argument de functie, se transfera un pointer spre primul sau element. De exemplu: int strlen(const char*); void f() { char v[] = "a vector"; strlen(v); strlen("Nicholas"); } Cu alte cuvinte, un argument de tip T[] va fi convertit spre T* cind este transferat. Rezulta ca o asignare la un element al argumentului vector schimba valoarea elementului argumentului respectiv. Cu alte cuvinte, vectorii difera de alte tipuri prin aceea ca vectorul nu este pasat prin valoare (si nici nu poate fi pasat prin valoare). Dimensiunea unui vector nu este disponibila in functia apelata. Aceasta poate fi o pacoste, dar exista dife- rite moduri de tratare a acestei probleme. Sirurile se termina prin zero, asa ca dimensiunea lor se poate calcula usor. Pentru alte tipuri de vectori se poate transfera un al doilea argument care contine dimensiunea sau un tip care contine un pointer si un indicator de lungime in locul vectorului (&11.11). De exemplu: void compute1(int* vec_ptr, int vec_size); //un mod struct vec{ //un alt mod int* ptr; int size; }; void compute2(vec v); Tablourile multidimensionale sint mai ciudate, dar adesea pot fi utilizati vectori de pointeri in locul lor si nu au nevoie de o tratare speciala. De exemplu: char* day[] = {"mon","tue","wed","thu","fri","sat","sun"}; Cu toate acestea consideram definirea unei functii care manipuleaza o matrice bidimensionala. Daca dimensiunile sint cunoscute la compilare, nu exista nici o problema: void print_m34(int m[3][4])

{ for(int i=0; i<3; i++) { for(int j=0; j<4; j++) cout << " " << m[i][j]; cout << "\n"; } } Cazul dificil apare cind trebuie pasate ambele dimensiuni. "Solutia evidenta" pur si simplu nu functioneaza: void print_mij(int m[][], int dim1, int dim2) //eroare { for(int i=0; i
4.6.6 Argumente implicite O functie necesita adesea mai multe argumente in general, decit este nevoie in cazul cel mai simplu sau in cazul cel mai frecvent. De exemplu, biblioteca stream are o functie hex() care produce un sir ce contine reprezentarea hexazecimala a unui intreg. Un al doilea intreg se foloseste pentru a specifica numarul de caractere disponibile pentru reprezentarea primului argument. Daca numarul de caractere este prea mic pentru a reprezenta intregul, apare trunchierea; daca este prea mare, sirul este completat cu spatii. Adesea, programatorul nu se intereseaza despre numarul de caractere necesare pentru a reprezenta intregul atita timp cit exista spatiu suficient, asa ca argumentul al doilea este 0 pentru a indica faptul ca la conversie sa se utilizeze "exact atitea caractere cite sint necesare". Pentru a elimina apelurile de forma hex(i, 0), functia se declara astfel: extern char* hex(long, int = 0); Initializarea pentru cel de al doilea parametru inseamna ca acesta este un parametru implicit. Adica, daca numai un argument este prezent intr-un apel, cel de al doilea este utilizat impli- cit. De exemplu: cout << "**" << hex(31) << hex(32, 3) << "**"; se interpreteaza astfel: cout << "**" << hex(31, 0) << hex(32, 3) << "**"; si va imprima: **1f 20** Un argument implicit se verifica din punct de vedere al tipului in momentul declararii functiei si este evaluat in momentul apelului. Este posibil sa se furnizeze argumente implicite numai pentru argumente din ultimele pozitii, asa ca: int f(int, int = 0, char* = 0); //ok int g(int = 0, int = 0, char*); //error int h(int = 0, int, char* = 0); //error Sa observam ca in acest caz spatiul dintre * si = este semnificativ (*= este operatorul de asignare): int nasty(char *= 0); //syntax error

4.6.7 Nume de functii supraincarcate Adesea este o idee buna de a da la diferite functii nume diferite, dar cind niste functii fac acelasi lucru asupra obiectelor de tipuri diferite, poate fi mai convenabil sa le

dam acelasi nume. Utilizarea aceluiasi nume pentru operatii diferite pentru tipuri diferite se numeste supraincarcare. Tehnica este deja utilizata pentru operatii de baza in C++; exista un singur nume pentru adunare (+), dar el poate fi utilizat pentru a aduna valori de tipuri intregi, in flotant si pointeri. Aceasta idee se extinde simplu pentru a trata operatii definite de programator, adica functii. Pentru a proteja programatorul de reutilizarea accidentala a unui nume, un nume poate fi utilizat pentru mai multe functii numai daca este declarat la inceput ca fiind supraincarcat. De exemplu: overload print; void print(int); void print(char*); La compilare singurul lucru pe care functiile il au in comun este numele. Probabil ca intr-un anumit sens functiile sint similare, dar limbajul nu are restrictii asupra lor. Astfel numele supraincarcat al functiilor sint in primul rind o conventie de notatie. Aceasta conventie este semnificativa pentru functii cu nume conventionale, cum ar fi sqrt, print si open. Cind un nume este semantic semnificativ, cum ar fi operatorii +, * si << (&6.2) si in cazul constructorilor (&5.2.4 si &6.3.1), aceasta facilitate devine esentiala. Cind este apelata o functie f() supraincarcata, compilatorul trebuie sa stie care functie este apelata dintre cele cu numele f. Aceasta se face prin compararea tipurilor argumentelor efective cu tipurile argumentelor formale a tuturor functiilor numite f. Gasirea functiei care sa fie apelata se face in trei pasi separati: [1] Cauta o corespondenta exacta si daca exista se utilizeaza functia respectiva; [2] Cauta o corespondenta utilizind conversii predefinite si utilizeaza o functie gasita in acest fel; [3] Cauta o corespondenta folosind conversiile definite de utilizator (&6.3) si daca exista un set de conversii unic, se utilizeaza functia gasita. De exemplu: overload print(double), print(int); void f() { print(1); print(1.0); } Regula de corespondenta exacta va face ca f() sa scrie pe 1 ca un intreg, iar pe 1.0 ca un numar flotant. Zero, char sau short sint fiecare o corespondenta exacta pentru un argument int. Analog, un float este o corespondenta exacta pentru double. Pentru argumentele functiilor cu nume supraincarcate, regulile de conversie standard (&r.6.6) nu se aplica complet. Conversiile care pot distruge informatie nu se aplica, raminind int spre long, int spre double, zero spre long, zero spre double si conversia de pointeri; zero spre pointer, pointer spre void* si pointer spre clasa derivata pentru a pointa spre baza clasei (&7.2.4). Iata un exemplu in care este necesara conversia: overload print(double), print(long);

void f(int a){print(a);} Aici a poate fi imprimat sau ca double sau ca long. Ambiguitatea poate fi rezolvata utilizind tipul de conversie explicita (sau print(long(a)) sau print(double(a))). Dindu-se aceste reguli, se poate asigura ca cel mai simplu algoritm (functie) va fi utilizat, cind eficienta sau precizia calcului difera semnificativ pentru tipurile implicite. De exemplu: overload pow; int pow(int, int); double pow(double, double); //din <math.h> complex pow(double, complex); //din complex pow(complex, int); complex pow(complex, double); complex pow(complex, complex); Procesul de gasire a corespondentei ignora unsigned si const. 4.6.8 Numar nespecificat de argumente Pentru anumite functii nu este posibil sa se specifice numarul si tipul tuturor argumentelor asteptate intr-un apel. O astfel de functie se declara terminind lista argumentelor din declaratie prin trei puncte (...) care inseamna ca " pot fi mai multe argumente". De exemplu: int printf(char* ...); Aceasta specifica faptul ca un apel a lui printf trebuie sa aiba cel putin un argument de tip char* si poate sa aiba sau nu si altele. De exemplu: printf("Hello, word\n"); printf("My name is %s %s\n", first_name, second_name); printf("%d + %d = %d\n", 2, 3, 5); O astfel de functie trebuie sa se refere la o informatie care nu este disponibila compilatorului cind se interpreteaza lista de argumente. In cazul functiei printf(), primul argument este un sir de format care contine o succesiune de caractere speciale care permite ca printf() sa trateze corect celelalte argumente: %s inseamna "se asteapta un argument de tip char*" iar %d inseamna "asteapta un argument int". Cu toate acestea, compilatorul nu stie aceasta, asa ca el nu se poate asigura ca argumentele asteptate sa existe in realitate sau ca un argument este un tip propriu. De exemplu: printf("My name is %s %s\n", 2); se va compila si in cel mai bun caz se va scrie la executie ceva straniu. Evident daca un argument nu a fost declarat, compilatorul nu are informatia necesara pentru a face verificarea standard de tip si de a face eventual o conversie de tip. In acest caz, char sau short se transfera ca int, iar float ca double. Aceasta nu este in mod necesar ceea ce a vrut utilizatorul. Utilizarea la extrema a celor trei puncte conduce la imposibilitatea de a verifica argumentele, lasind programatorului deschisa problema aceasta. Un program bine proiectat necesita cel putin citeva functii pentru care tipurile argumentelor nu sint

specificate complet. Functiile supraincarcate si functiile care utilizeaza argumente implicite pot fi utilizate avind grija ca verificarea tipului sa se faca ori de cite ori se utilizeaza argumente de tip nespecificat. Numai cind atit numarul de argu mente cit si tipul argumentelor variaza este necesar sa se foloseasca trei puncte. Cea mai frecventa utilizare a celor trei puncte este de a specifica o interfata cu functiile de biblioteca ale lui C care sint definite fara a fi disponibile alternativele posibile: extern int fprintf(FILE*, char* ...); din <stdin.h> extern int execl(char* ...); din <system.h> extern int abort(...); din Un set de macrouri standard disponibile pentru a avea acces la argumente nespecificate in astfel de functii pot fi gasite in <stdargs.h>. Sa consideram scrierea unei functii eroare care are un argument intreg ce indica nivelul de eroare, urmat de un numar arbitrar de siruri. Ideea este de a compune mesajul de eroare pasind fiecare cuvint ca un argument de tip sir separat: void error(int ...); main(int argc, char* argv[]) {switch(argc) {case 1: error(0, argv[0], 0); break; case 2: error(0, argv[0], argv[1], 0); break; default: error(1, argv[0], "with", dec(argc-1), "arguments", 0); } } Functia eroare ar putea fi definita astfel: #include <stdargs.h> void error(int n ...) // "n" urmat de o lista de char* s terminata prin zero { va_list ap; va_start(ap, n); //arg startup for(;;) { char* p = va_arg(ap, char*); if(p == 0) break; cerr << p << " "; } va_end(ap); //curatirea argumentelor cerr << "\n"; if(n) exit(n); }

Intii se defineste va_list care este initializata prin apelul lui va_start(). Macroul va_start ia numele lui va_list si numele ultimului argument formal ca argumente. Macroul va_arg() se utilizeaza pentru a alege argumentul nedenumit in ordine. La fiecare apel programatorul trebuie sa furnizeze un tip; va_arg() presupune ca argumentul efectiv de acel tip a fost pasat, dar de obicei nu exista o cale de a asigura aceasta. Inainte de a reveni dintr-o functie in care s-a utilizat va_start(), trebuie apelata va_end(). Motivul este ca va_start() poate modifica stiva in asa fel ca revenirea nu se va Mai realiza cu succes: va_end() reface stiva la forma necesara revenirii corecte. 4.6.9 Pointer spre functie Exista numai doua lucruri care pot fi facute cu o functie: apelul ei si sa se ia adresa ei. Pointerul obtinut functiei poate fi apoi utilizat pentru a apela functia. De exemplu: void error(char* p){/*...*/} void (*efct)(char*); //pointer spre functie void f() {efct = &error; //efct pointeaza spre error (*efct)("error"); //apelul lui error prin efct } Pentru a apela o functie printr-un pointer (de exemplu efct) intii trebuie sa i se atribuie pointerului adresa functiei res- pective. Intrucit operatorul () de apel de functie are prioritate mai mare decit operatorul *, nu se poate scrie apelul prin *efct("error") caci aceasta inseamna *(efct("error")), ceea ce este o eroare de tip. Acelasi lucru se aplica la sintaxa declaratiei (vezi de asemenea &7.3.4). Sa observam ca pointerii spre functii au tipurile argumentelor declarate ca si functiile insasi. In asignarea de pointeri, tipul functiei trebuie sa corespunda exact. De exemplu: void (*pf)(char*); //pointer spre void(char*); void f1(char*); //void(char*); int f2(char*); //int(char*); void f3(int*); //void(int*); void f() { pf = &f1; pf = &f2;

//ok //eroare: tipul valorii returnate // este eronat pf = &f3; //eroare: argument de tip eronat (*pf)("asdf"); //ok (*pf)(1); //eroare: tip de argument eronat

int i = (*pf)("qwer"); //eroare: void se asigneaza la int } Regulile pentru pasarea argumentelor sint aceleasi atit pentru apelurile directe la o functie cit si pentru apelurile la o functie printr-un parametru. Adesea este convenabil sa se defineasca un nume pentru tipul unui pointer spre o functie pentru a elimina utilizarea tot timpul a unei sintaxe neevidente. De exemplu: typedef int (*SIG_TYP)(); //din <signal.h> typedef void (*SIG_ARG_TYP)(); SIG_TYP signal(int, SIG_ARG_TYP); Adesea este util un vector de pointeri spre functii. De exemplu, sistemul de meniuri pentru editorul bazat pe "mouse" se implementeaza utilizind vectori de pointeri spre functii ce reprezinta operatii. Sistemul nu poate fi descris aici in detaliu dar ideea generala este aceasta: typedef void (*PF)(); PF edit_ops[]={cut, paste, snarf, search}; //op. de editare PF file_ops[]={open, reshape, close, write};//tratarea fis. Definirea si initializarea pointerilor care definesc actiunile selectate dintr-un meniu asociat cu butoanele mouse-ului: PF* button2 = edit_ops; PF* button3 = file_ops; Intr-o implementare completa, este necesara mai multa informatie pentru a defini fiecare element. De exemplu, un sir care specifica textul de afisat trebuie sa fie pastrat undeva. Pe masura ce se utilizeaza sistemul, sensul butoanelor mouse se schimba frecvent cu contextul. Astfel de schimbari se realizeaza (partial) schimbind valoarea pointerilor de butoane. Cind un utilizator selecteaza un meniu, cum ar fi elementul 3 pentru butonul 2, se executa operatia asociata: (*button2[3])(); Un mod de a cistiga o apreciere a puterii expresive a pointerilor spree functii este incercarea de a scrie cod fara ele. Un meniu poate fi modificat la executie inserind functii noi intr-o tabela operator. Este de asemenea usor sa se construiasca meniuri noi la executie. Pointerii spre functii pot fi utilizati sa furnizeze rutine care pot fi aplicate la obiecte de tipuri diferite: typedef int (*CFT)(char*, char*); int sort(char* base, unsigned n, int sz, CFT cmp) /* Sorteaza cele n elemente ale vectorului "base" in ordine crescatoare utilizind functia de comparare spre care pointeaza "cmp". Elementele sint de dimensiune "sz". Algoritm foarte ineficient: bubble sort. */ { for(int i = 0; i < n-1; i++)

for(int j = n-1; i < j; j--) { char* pj = base+j*sz; //b[j] char* pj1 = pj-sz; //b[j-1] if((*cmp)(pj, pj1) < 0) //swap b[j] and b[j-1] for(int k = 0; k < sz; k++) { char temp = pj[k]; pj[k] = pj1[k]; pj1[k] = temp; } } } Rutina de sortare nu cunoaste tipul obiectelor pe care le sorteaza, ci numai numarul de elemente (dimensiunea vectorului), dimensiunea fiecarui element si functia de apelat pentru a face compararea. Tipul lui sort() ales este acelasi cu tipul rutinei qsort() din biblioteca C standard. Programele reale utilizeaza qsort(). Intrucit sort() nu returneaza o valoare, ar trebui declarata cu void, dar tipul void nu a fost introdus in C cind a fost definit qsort(). Analog, ar fi mai onest sa se foloseasca void* in loc de char* ca tip de argument. O astfel de functie sort() ar putea fi utilizata pentru a sorta o tabela de forma: struct user{char* name; char* id; int dept; }; typedef user* Puser; user heads[]={"McIlroy M.D.", "doug", 11271, "Aho A.V.", "ava", 11272, "Weinberger P.J.", "pjw", 11273, "Schryer N.L.", "nls", 11274, "Schryer N.L.", "nls", 11275, "Kernighan B.W.", "bwk", 11276 }; void print_id(Puser v, int n) {for(int i = 0; i < n; i++) cout << v[i].name << "\t" << v[i].id << "\t" << v[i].dept << "\n"; }

Pentru a putea face sortarea, intii trebuie sa definim functia de comparare potrivita. O functie de comparare trebuie sa returneze o valoare negativa daca primul ei argument este mai mic decit al doilea, zero daca sint egale si un numar pozitiv altfel: int cmp1(char* p, char* q) //se compara sirurile nume { return strcmp(Puser(p)->name, Puser(q)->name); } int cmp2(char* p, char* q) //se compara numerele dept { return Puser(p)->dept - Puser(q)->dept; } Programul acesta sorteaza si imprima: main() { sort((char*)heads, 6, sizeof(user), cmp1); print_id(heads, 6) cout << "\n"; sort((char*)heads, 6, sizeof(user), cmp2); print_id(heads, 6); //in ordinea numerelor de departament }

//in ordine alfabetica

Este posibil sa se ia adresa unei functii inline si de asemenea sa se ia adresa unei functii supraincarcate (&r8.9).

4.7

Macrouri

Macrourile se definesc in &r11. Ele sint foarte importante in C, dar sint pe departe mai putin utilizate in C++. Prima regula despre ele este: sa nu fie utilizate daca nu trebuie. S-a observat ca aproape fiecare macro demonstreaza o fisura fie in limbajul de programare, fie in program. Daca doriti sa folositi macrouri va rog sa cititi foarte atent manualul de referinta pentru implementarea preprocesorului C pe care il folositi. Un macro simplu se defineste astfel: #define name restul liniei Cind name se intilneste ca o unitate lexicala, el este inlocuit prin restul liniei. De exemplu: named = name va fi expandat prin: named = restul liniei Un macro poate fi definit, de asemenea, prin argumente. De exemplu:

#define mac(a, b) argunent1: a argument2: b Cind se utilizeaza mac, cele doua siruri de argumente trebuie sa fie prezente. Ele vor inlocui pe a si b cind se expandeaza mac(). De exemplu: expanded = mac(foo bar, yuc yuk) va fi expandat in: expanded = argument1: foo bar argument2: yuk yuk Macrourile manipuleaza siruri si stiu putin despre sintaxa lui C++ si nimic despre tipurile si regulile de existenta ale lui C++. Compilatorul vede numai formele expandate ale unui macro, asa ca o eroare intr-un macro va fi propagata cind macroul se expandeaza. Aceasta conduce la mesaje de eroare obscure, ele nefiind descoperite in definitia macroului. Iata citeva macrouri plauzibile: #define case break;case #define nl <<"\n" #define forever for(;;) #define MIN(a, b) (((a) < (b)) ? (a) : (b)) Iata citeva macrouri complete necesare: #define PI 3.141593 #define BEGIN { #define END } Iata citeva macrouri periculoase: #define SQUARE(a) a*a #define INCR_xx (xx)++ #define DISP = 4 Pentru a vedea de ce sint periculoase, sa incercam sa expandam: int xx = 0; //numarator global void f() { int xx = 0; //variabila locala xx = SQUARE(xx+2); //xx=xx+2*xx+2 INCR_xx; //incrementeaza localul xx if(a-DISP == b) //a-= 4==b { //.... } }

Daca noi dorim sa utilizam un macro trebuie sa utilizam operatorul de rezolutie a domeniului "::" cind dorim sa facem referinte la nume globale (&2.1.1) si sa includem in paranteze aparitiile numelor argumente ale macrourilor (vezi MIN de mai sus). Sa se observe diferenta efectelor de expandare a acestor doua macrouri: #define m1(a) something(a) // comentariu serios #define m2(a) something(a) /* comentariu serios */ De exemplu: int a = m1(1) + 2; int b = m2(1) + 2; se vor expanda in int a = something(1) // comentariu serios + 2 ; int b = something(1) /* comentariu serios */ + 2; Utilizind macrouri, noi putem proiecta limbajul nostru propriu; el va fi probabil mult mai incomprehensibil decit altele. Mai mult decit atit, preprocesorul C este un macroprocesor foarte simplu. Cind noi incercam sa facem ceva netrivial, noi probabil gasim sau ca este imposibil sau ceva nejustificat de greu de realizat (dar vezi &7.3.5).

4.8

Exercitii

1. (*1). Sa se scrie declaratii pentru: o functie care are ca argumente un pointer spre caractere si referinta la un intreg si nu returneaza nici o valoare; un pointer spre o astfel de functie; o functie care are un astfel de pointer ca argument; o functie care returneaza un astfel de pointer. Sa se scrie definitia unei functii care are un astfel de pointer ca argument si returneaza argumentul ei ca valoare. Sa se utilizeze typedef. 2. (*1). Ce semnifica linia de mai jos? La ce ar fi buna ea? typedef int(rifii&)(int, int); 3. (*1.5). Sa se scrie un program ca "Hello, world" care ia un nume din linia de comanda si care scrie "Hello, numele respectiv". Sa se modifice acest program pentru a lua oricite nume ca argumente si sa se scrie Hello la fiecare.

4. (*1.5). Sa se scrie un program care citeste un numar arbitrar de fisiere a caror nume se dau ca argumente in linia de comanda si le scrie unul dupa altul in cout. Acest program se poate numi cat deoarece concateneaza fisierele respective. 5. (*2). Sa se converteasca un program mic C intr-un program C++. Sa se modifice fisierele antet pentru a declara toate fun- ctiile apelate si sa declare tipul fiecarui argument. Sa se inlo- cuiasca #define prin enum, const sau inline unde este posibil. Sa se elimine declaratiile extern din fisierele C si sa se converteasca in sintaxa definitiilor de functii din C++. Sa se inlocuiasca apelurile lui malloc() si free() cu new si delete. Sa se elimine conversiile de tip explicit necesare. 6. (*2). Sa se implementeze sort() (&4.6.9) utilizind un algoritm de sortare mai eficient. 7. (*2). Sa consideram definitia lui struct tnode din &r8.5. Sa se scrie functia pentru introducerea unui cuvint nou intr-un arbore de tnode noduri. Sa se scrie o functie care listeaza arborele de tnode noduri. Sa se scrie o functie care listeaza arborele respectiv in ordine alfabetica a cuvintelor pe care le contine. Sa se modifice tnode astfel incit sa contina numai un pointer spre un cuvint de lungime arbitrara memorat in memoria libera folosind new. Sa se modifice functiile pentru a putea utiliza noua definitie a lui tnode. 8. (*2). Sa se scrie un modul care implementeaza o stiva. Fisierul.h trebuie sa declare functiile push(), pop() si orice alte functii potrivite. Un fisier.c defineste functiile si datele necesare de a fi pastrate pe stiva. 9. (*2). Sa cunoasteti fisierele antet standard. Sa se listeze fisierele din /usr/include si /usr/include/cc (sau orice alte fisiere antet standard pastrate de sistemul d-voastra). Cititi tot ce pare a fi interesant. 10. (*2). Sa se scrie o functie ce inverseaza un tablou bidimensional. 11. (*2). Sa se scrie un program care citeste din cin si scrie caracterele in cout codificat. Codul lui c poate fi c^key[i], unde key este un sir pasat ca argument al liniei de comanda. Programul utilizeaza caracterele din key intr-o maniera ciclica pina cind au fost citite toate caracterele de la intrare. Recodificarea textului cu aceeasi cheie produce textul original. Daca nu exista nici o cheie (s-a pasat sirul vid), atunci nu se face nici o codificare. 12. (*3). Sa se scrie un program care ajuta la descifrarea mesajelor codificate cu metoda descrisa mai sus fara a cunoaste cheia. A se consulta David Kahn: The code-breakers, Macmillan, 1967, New York, pp 207-213. 13. (*3). Sa se scrie o functie error care are un format asemanator cu printf, continind %s, %c si %d si un numar arbitrar de argumente. Sa nu se foloseasca printf(). A se consulta &8.2.4 daca nu se cunoaste sensul lui %s etc. Sa se utilizeze <stdargs.h>. 14. (*1). Cum am alege nume pentru tipuri de pointeri spre functii definite prin typedef?

15. (*2). Analizati niste programe pentru a avea o idee despre diversitatea stilurilor numelor utilizate in realitate. Cum se utilizeaza literele mari? Cum se utilizeaza sublinierea? Cind se utilizeaza nume scurte ca x si i? 16. (*1). Ce este gresit in macrodefinitiile de mai jos? #define PI = 3.141593; #define MAX(a, b) a > B ? a : b #define fac(a) (a) * fac((a) - 1) 17. (*3). Sa se scrie un macroprocesor care defineste si expandeaza macrouri simple (asa cum face macroprocesorul C). Sa citeasca din cin si sa scrie in cout. La inceput sa nu se incerce sa se trateze macrouri cu argumente. Calculatorul de birou (&3.1) contine o tabela de simboluri si un analizor lexical pe care noi l-am putea modifica.

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

Introducere si privire generala

Scopul conceptului de clasa C++ este de a furniza programatorului un instrument pentru a crea tipuri noi care pot fi folosite tot atit de convenabil ca si tipurile predefinite. Ideal, un tip definit de utilizator ar trebui sa nu difere de tipurile predefinite in modul in care sint utilizate, ci numai in modul in care sint create. Un tip este reprezentarea concreta a unei idei (concept). De exemplu, tipul float din C++ cu operatiile +, -, *, etc., furnizeaza o versiune restrinsa dar concreta a conceptului matematic de numar real. Motivul de a desemna un tip nou este de a

furniza o definitie concreta si specifica a conceptului care nu are un corespondent direct si evident intre tipurile predefinite. De exemplu, cineva poate furniza tipul "trunk_module" intr-un program ce se ocupa cu telefoanele sau tipul "list_of_paragraphs" pentru un program de procesare de text. Un program care furnizeaza tipuri care sint strins legate de conceptele aplicatiei este de obicei mai usor de inteles si mai usor de modificat decit un program care nu face asa ceva. Un set de tipuri definite de utilizator bine ales face un program mai concis; el de asemenea permite compilatorului sa detecteze utilizari ilegale ale obiectelor care altfel nu ar fi detectate pina in momentul in care nu se testeaza efectiv programul. Ideea fundamentala in definirea unui tip nou este de a separa detaliile incidentale ale implementarii (de exemplu, aranjamentul datelor utilizate pentru a memora un obiect al tipului) de proprietatile esentiale ale utilizarii lui corecte (de exemplu, lista completa de functii care pot avea acces la date). O astfel de separare poate fi exprimata prin canalizarea tuturor utilizarilor datelor structurii si a rutinelor de memorare interna printr-o interfata specifica. Acest capitol consta din 4 parti separate: &5.2 Clase si Membri. Aceasta sectiune introduce notiunea de baza: tip definit de utilizator numita clasa. Accesul la obiectele unei clase se poate restringe la un set de functii declarate ca o parte a clasei; astfel de functii se numesc functii membru. Obiectele unei clase pot fi create si initializate prin functii membru declarate in mod specific pentru acest scop: astfel de functii se numesc constructori. O functie membru poate fi declarata pentru a "sterge" un astfel de obiect al unei clase cind el este distrus; o astfel de functie se numeste destructor. &5.3 Interfete si Implementari. Aceasta sectiune prezinta doua exemple de modul in care pot fi proiectate, implementate si utilizate clasele. &5.4 Prieteni si Reuniuni. Aceasta sectiune prezinta multe detalii suplimentare despre clase. Arata cum se face accesul la partile private ale unei clase si cum se poate admite accesul pentru o functie care nu este membru al acelei clase. O astfel de functie se numeste prieten. Aceasta sectiune de asemenea arata cum se defineste o reuniune distinctiva. &5.5 Constructori si Destructori. Un obiect poate fi creat ca un obiect automatic, static sau in memoria libera. Un obiect, de asemenea, poate fi un membru al unui anumit agregat (o clasa sau un vector), care la rindul lui poate fi alocat in una din cele 3 moduri indicate mai sus. Utilizarea constructorilor si destructorilor se explica in detaliu.

5.2

Clase si Membri

Clasa este un tip definit de utilizator. Aceasta sectiune introduce facilitatile de baza pentru a defini o clasa, crearea obiectelor unei clase, manipularea acestor obiecte si in final stergerea acestor obiecte dupa utilizare. 5.2.1 Functii membru Sa consideram implementarea conceptului de data utilizind o structura pentru a defini reprezentarea unei date si un set de functii pentru manipularea variabilelor de acest tip: struct date{ int month, day, year; }; date today; void set_date(date*, int, int, int); void next_date(date*); void print_date(date*); Nu exista conexiuni explicite intre functii si tipul datei. O astfel de conexiune se poate stabilii declarind functiile ca membri: struct date{int month, day, year; void set(int, int, int); void get(int*, int*, int*); void next(); void print(); }; Functiile declarate in acest fel se numesc functii membru si pot fi invocate numai pentru o variabila specifica de tipul corespunzator utilizind sintaxa standard pentru accesul la membri unei structuri. De exemplu: date today; date my_birthday; void f() {my_birthday.set(30, 12, 1950); today.set(18, 1, 1985); my_birthday.print(); today.next(); } Intrucit diferite structuri pot avea functii membru cu acelasi nume, trebuie sa se specifice numele structurii cind se defineste o functie membru: void date::next() { if(++day > 28) { //do the hard part } }

Intr-o functie membru, numele membrilor pot fi folosite fara o referire explicita la un obiect. In acest caz, numele se refera la acel membru al obiectului pentru care a fost apelata functia.

5.2.2 Clase Declaratia lui date din subsectiunea precedenta furnizeaza un set de functii pentru manipularea unei variabile de tip date, dar nu specifica faptul ca acele functii ar trebui sa fie singurele care sa aiba acces la obiectele de tip date. Aceasta restrictie poate fi exprimata utilizind o clasa in locul unei structuri: class date{ int month, day, year; public: void set(int, int, int); void get(int*, int*, int*); void next(); void print(); }; Eticheta public separa corpul clasei in doua parti. Numele din prima parte, private, pot fi utilizate numai de functiile membre. Partea a doua, public, constituie interfata cu obiectele clasei. O structura (struct) este pur si simplu o clasa cu toti membri publici, asa ca functiile membru se definesc si se utilizeaza exact ca inainte. De exemplu: void date::print() //print folosind notatia US { cout << month << "/" << day << "/" << year; } Cu toate acestea, functiile care nu sint membru nu pot folosi membri privati ai clasei date. De exemplu: void backdate() { today.day--; //eroare } Exista citeva beneficii in urma restringerii accesului, la o data structurata, la o lista de functii declarata explicit. Orice eroare care face ca date sa aiba o valoare ilegala (de exemplu December 36, 1985) trebuie sa fie cauzata de codul unei functii

membru, asa ca primul stadiu al depanarii, localizarea, este rezolvat inainte ca programul sa se execute. Acesta este un caz special al observatiei generale ca orice schimbare in comportarea tipului date poate, si trebuie sa fie efectuata prin schimbarea membrilor lui. Un alt avantaj este ca un utilizator de un astfel de tip este necesar numai sa examineze definitia functiilor membru pentru a invata utilizarea lui. Protectia datelor private se bazeaza pe restrictia utilizarii numelor membru ale clasei. Se poate trece peste aceasta prin manipularea de adrese si conversie explicita de tip, dar aceasta evident este un fel de inselatorie. 5.2.3 Autoreferinta Intr-o functie membru, ne putem referi direct la membri unui obiect pentru care functia membru este apelata. De exemplu: class x{ int m; public: int readm(){ return m; } x aa; x bb; void f() { int a = aa.readm(); int b = bb.readm(); //....... } } In primul apel al membrului readm(), m se refera la aa.m iar in cel de al doilea la bb.m. Un pointer la obiectul pentru care o functie membru este apelata constituie un membru ascuns pentru functie. Argumentul implicit poate fi referit explicit prin this. In orice functie a unei clase x, pointerul this este declarat implicit ca: x* this; si este initializat ca sa pointeze spre obiectul pentru care functia membru este apelata. Intrucit this este un cuvint cheie el nu poate fi declarat explicit. Clasa x ar putea fi declarata explicit astfel: class x{ int m; public: int readm(){ return this->m; } };

Utilizarea lui this cind ne referim la membri nu este necesara; utilizarea majora a lui this este pentru a scrie functii membru care manipuleaza direct pointerii. Un exemplu tipic pentru this este o functie care insereaza o legatura intr-o lista dublu inlantuita: class dlink{ dlink* pre; //legatura precedenta dlink* suc; //legatura urmator public: void append(dlink*); //........ }; void dlink::append(dlink* p) { p->suc = suc; //adica p->suc = this->suc p->pre = this; //utilizarea explicita a lui this suc->pre = p; //adica, this->suc->pre = p; suc = p; //adica, this->suc = p } dlink* list_head; void f(dlink* a, dlink* b) { //....... list_head->append(a); list_head->append(b); } Legaturile de aceasta natura generala sint baza pentru clasele lista descrise in capitolul 7. Pentru a adauga o legatura la o lista, trebuie puse la zi obiectele spre care pointeaza this, pre si suc. Ele toate sint de tip dlink, asa ca functia membru dlink::append() poate sa faca acces la ele. Unitatea de protectie in C++ este clasa, nu un obiect individual al unei clase. 5.2.4 Initializare Utilizarea functiilor de felul set_data() pentru a furniza initializarea pentru obiectele clasei nu este eleganta si este inclinata spre erori.

Intrucit nicaieri nu se afirma ca un obiect trebuie initializat, un programator poate uita sa faca acest lucru sau (adesea cu rezultate dezastruoase) sa faca acest lucru de doua ori. O conceptie mai buna este de a permite programatorului sa declare o functie cu scopul explicit de a initializa obiecte. Deoarece o astfel de functie construieste valori de un tip dat, ea se numeste constructor. Un constructor se recunoaste deoarece are acelasi nume ca si clasa insasi. De exemplu: class date{ //...... date(int, int, int); }; Cind o clasa are un constructor, toate obiectele acelei clase vor fi initializate. Daca constructorul cere argumente, ele pot fi furnizate: date today = date(23, 6, 1983); date xmas(25, 12, 0); //forma prescurtata date my_birthday: //ilegal, lipseste initializarea Este adesea util sa se furnizeze diferite moduri de initializare a obiectelor unei clase. Aceasta se poate face furnizind diferiti constructori. De exemplu: class date{ int month, day, year; public: //........ date(int, int, int); //zi luna an date(char*); //date reprezentate ca sir date(int); //zi, luna si anul curent date(); //data curenta }; Constructorii respecta aceleasi reguli pentru tipurile de argumente ca si celelalte functii supraincarcate (&4.6.7). Atita timp cit constructorii difera suficient in tipurile argumentelor lor compilatorul le poate selecta corect, unul pentru fiecare utilizare: date today(4); date july4("july 4, 1983"); date guy("5 Nov"); date now; //initializare implicita Sa observam ca functiile membru pot fi supraincarcate fara a utiliza explicit cuvintul cheie overload. Intrucit lista completa a functiilor membru apare in declaratia de clasa si adesea este scurta, nu exista un motiv de a obliga utilizarea cuvintului overload care sa ne protejeze impotriva unei reutilizari accidentale a unui nume. Proliferarea constructorilor in exemplul date este tipica. Cind se proiecteaza o clasa exista totdeauna tentatia de a furniza "totul" deoarece se crede ca este mai usor sa se furnizeze o trasatura chiar in cazul in care cineva o vrea sau din cauza ca ea arata frumos si apoi sa se decida ce este in realitate necesar. Ultima varianta necesita un

timp mai mare de gindire, dar de obicei conduce la programe mai mici si mai comprehensibile. Un mod de a reduce numarul de functii inrudite este de a utiliza argumentele implicite. In date, fiecarui argument i se poate da o valoare implicita care se interpreteaza: "implicit ia data curenta". class date{ int month, day, year; public: //.......... date(int d=0, int m=0, int y=0); date(char*); //date reprezentat ca sir }; date::date(int d, int m, int y) {day = d ? d : today.day; month = m ? m : today.month; year = y ? y : today.year; //verifica faptul ca date este valida //.......... } Cind se utilizeaza o valoare pentru un argument pentru a indica "ia valoarea implicita", valoarea aleasa trebuie sa fie in afara setului posibil de valori pentru argument. Pentru zi si luna este clar acest lucru, dar valoarea zero pentru an poate sa nu fie o alegere evidenta. Din fericire nu exista anul zero in calendarul european. 1AD(year == 1) vine imediat dupa 1BC(year == -1), dar aceasta probabil ar fi prea subtil pentru un program real. Un obiect al unei clase fara constructori poate fi initializat atribuindu-i un alt obiect al acelei clase. Aceasta se poate face, de asemenea, cind constructorii au fost declarati. De exemplu: date d = today; //initializare prin asignare In esenta, exista un constructor implicit ca o copie de biti a obiectelor din aceeasi clasa. Daca nu este dorit acest constructor implicit pentru clasa X, el poate fi redefinit prin constructorul denumit X(X&) (aceasta se va discuta mai departe in &6.6). 5.2.5 Curatire (stergere) Mai frecvent este cazul in care un tip definit de utilizator are un constructor pentru a asigura initializarea proprie. Multe tipuri necesita, de asemenea, un destructor, care sa asigure stergerea obiectelor de un tip. Numele destructorului pentru clasa X este ~X() ("complementul constructorului"). In particular, multe clase utilizeaza memoria libera (vezi &3.2.6) ce se aloca printr-un constructor si se dealoca printr-un destructor.

De exemplu, iata un tip de stiva conventionala care a fost complet eliberata de tratarea erorilor pentru a o prescurta: class char_stack{ int size; char* top; char* s; public: char_stack(int sz){top = s = new char[size=sz];} ~char_stack(){ delete s; } void push(char c){ *top++ = c; } char pop(){ return *--top; } }; Cind char_stack iese in afara domeniului, se apeleaza destructorul: void f() { char_stack s1(100); char_stack s2(200); s1.push('a'); s2.push(s1.pop()); char ch = s2.pop(); cout << chr(ch) << "\n"; } Cind f() este apelata, constructorul char_stack va fi apelat pentru s1 ca sa aloce un vector de 100 de caractere si pentru s2 pentru a aloca un vector de 200 de caractere; la revenirea din f(), acesti doi vectori vor fi eliminati.

5.2.6 "Inline" Cind programam folosind clasele, este foarte frecvent sa utilizam multe functii mici. In esenta, o functie este realizata unde un program structurat, in mod traditional, ar avea un anumit mod tipic de utilizare a unei date structurate; ceea ce a fost o conventie devine un standard recunoscut prin compilator. Aceasta poate conduce la ineficiente teribile deoarece costul apelului unei functii este inca mai inalt decit citeva referinte la memorie necesare pentru corpul unei functii triviale. Facilitatile functiilor "in linie" au fost proiectate pentru a trata aceasta problema. O functie membru definita (nu numai declarata) in declaratia de clasa se considera ca fiind in linie. Aceasta inseamna de exemplu, ca, codul generat pentru functiile care utilizeaza char_stack-ul prezentat mai sus nu contine nici un apel de functie

exceptind cele utilizate pentru a implementa operatiile de iesire. Cu alte cuvinte, nu exista un cost de timp mai mic decit cel luat in seama cind proiectam o clasa; chiar si cele Mai costisitoare operatii pot fi realizate eficient. Aceasta observatie invalideaza motivele cele mai frecvent utilizate in favoarea utilizarii membrilor publici ai datelor. O functie mem- bru poate, de asemenea, sa fie declarata inline in afara declaratiei de clasa. De exemplu: class char_stack{ int size; char* top; char* s; public: char pop(); //...... } inline char char_stack::pop() { return *--top; }

5.3

Interfete si Implementari

Ce face o clasa buna? Ceva ce are un set mic si bine definit de operatori. Ceva ce poate fi vazut ca o "cutie neagra" manipulata exclusiv prin acel set de operatii. Ceva a carei reprezentare reala ar putea fi conceputa sa fie modificata fara a afecta modul de utilizare a acelui set de operatii. Containerele de toate felurile furnizeaza exemple evidente: tabele, multimi, liste, vectori, dictionare, etc.. O astfel de clasa va avea o operatie de inserare, care de obicei va avea de asemenea operatii pentru a verifica daca un membru specific a fost inserat, poate va avea operatii pentru sortarea membrilor, poate va avea operatii pentru examinarea tuturor membrilor intr-o anumita ordine si in final ar putea, de asemenea, sa aiba o operatie pentru eliminarea unui membru. Clasele container de obicei au constructori si destructori. Ascunderea datelor si o interfata bine definita pot fi de asemenea obtinute prin conceptul de modul (vezi de exemplu, &4.4: fisiere ca module). Cu toate acestea, o clasa este un tip; pentru a o utiliza, trebuie sa se creeze obiecte ale clasei respective si se pot crea atit de multe astfel de obiecte cite sint necesare. Un modul este el insusi un obiect; pentru a-l utiliza, cineva este necesar sa-l initializeze si exista exact un astfel de obiect.

5.3.1 Implementari alternative Atita timp cit declaratia partii publice a unei clase si declaratia functiilor membru ramin neschimbate, implementarea unei clase poate fi schimbata fara a afecta utilizatorii ei. Sa consideram o tabela de simboluri de felul celei utilizate pentru calculatorul de birou din capitolul 3. Este o tabela de nume: struct name{ char* string; name* next; double value; }; Iata o versiune a clasei tabela: //file table.h: class table{ name* tbl; public: table(){tbl = 0;} name* look(char*, int=0); name* insert(char* s){return look(s, 1);} }; Aceasta tabela difera de cea definita in capitolul 3 prin aceea ca este un tip propriu. Se pot declara mai multe tabele, putem avea un pointer spre o tabela, etc.. De exemplu: #include "table.h" table globals; table keywords; table* locals; main() {locals = new table; //......... } Iata o implementare a lui table::look() utilizind o cautare liniara prin lista inlantuita de nume din tabela: #include <string.h> name* table::look(char* p, int ins) {for(name* n = tbl; n; n = n>next) if(strcmp(p, n->string) == 0) return n; if(ins == 0) error("name not found"); name* nn = new name; nn->string = new char[strlen(p) + 1]; strcpy(nn->string, p); nn->value = 1; nn->next = tbl; tbl = nn; return nn; }

Acum consideram o inlantuire a clasei utilizind cautarea prin hashing asa cum s-a facut in exemplul cu calculatorul de birou. Este insa mai dificil sa facem acest lucru din cauza restrictiei ca, codul scris folosind versiunea de clasa table de mai jos, sa nu se schimbe. class table{name** tbl; int size; public: table(int sz=15); ~table(); name* look(char*, int=0); name* insert(char* s){return look(s, 1);} }; Structura datelor si constructorul s-au schimbat pentru a reflecta nevoia pentru o dimensiune specifica a tabelei cind se utilizeaza hashingul. Prevazind constructorul cu un argument implicit ne asiguram ca, codul vechi care nu a specificat dimensiunea unei tabele este inca corect. Argumentele implicite sint foarte utile in situatii cind vrem sa schimbam o clasa fara a afecta codul vechi. Constructorul si destructorul acum gestioneaza crearea si stergerea tabelelor de hashing: table::table(int sz) { if(sz < 0) error("negative table size"); tbl = new name*[size=sz]; for(int i=0; i < sz; i++) tbl[i] = 0; } table::~table() {for(int i=0; i < size; i++) {name* nx; for(name* n=tbl[i]; n; n=nx) { nx = n->next; delete n->string; delete n; } } delete tbl; } O versiune mai simpla si mai clara a lui table::~table() se poate obtine declarind un destructor pentru class name. Functia lookup este aproape identica cu cea utilizata in exemplul cu calculatorul de birou (&3.1.3): name* table::look(char* p, int ins)

{ int ii = 0; char* pp = p; while(*pp) ii == ii << 1 ^ *pp++; if(ii < 0) ii = -ii; ii %= size; for(name* n = tbl[ii]; n; n = n->next) if(strcmp(p, n->string) == 0) return n; if(ins == 0) error("name not found"); name* nn = new name; nn->string = new char[strlen(p) + 1]; strcpy(nn->string, p); nn->value = 1; nn->next = tbl[ii]; tbl[ii] = nn; return nn; } Evident, functiile membru ale unei clase trebuie sa fie recompilate ori de cite ori se face o schimbare in declaratia de clasa. Ideal, o astfel de schimbare nu ar trebui sa afecteze de loc utilizatorii unei clase. Din nefericire, nu este asa. Pentru a aloca o variabila de clasa, compilatorul are nevoie sa cunoasca dimensiunea unui obiect al clasei. Daca dimensiunea unui astfel de obiect este schimbata, fisierele care contin utilizari ale clasei trebuie sa fie recompilate. Softwarul care determina setul minim de fisiere ce necesita sa fie recompilate dupa o schimbare a declaratiei de clasa poate fi (si a fost) scris, dar nu este inca utilizat pe scara larga. Noi ne putem intreba, de ce nu a fost proiectat C++ in asa fel ca recompilarea utilizatorilor unei clase sa fie necesara dupa o schimbare in partea privata? Si de ce trebuie sa fie prezenta partea privata in declaratia de clasa? Cu alte cuvinte, intrucit utilizatorii unei clase nu sint admisi sa aiba acces la membri privati, de ce declaratiile lor trebuie sa fie prezente in fisierele antet ale utilizatorului? Raspunsul este eficienta. Pe multe sisteme, atit procesul de compilare cit si secventa de operatii care implementeaza apelul unei functii sint mai simple cind dimensiunea obiectelor automatice (obiecte pe stiva) se cunoaste la compilare. Aceasta problema ar putea fi eliminata reprezentind fiecare obiect al clasei ca un pointer spre obiectul "real". Intrucit toti acesti pointeri ar avea aceeasi dimensiune, iar alocarea obiectelor "reale" ar putea fi definita intr-un fisier unde este disponibila partea privata, acest fapt ar putea rezolva problema. Cu toate acestea, aceasta solutie impune referirea la o memorie suplimentara cind se face acces la membri unei clase si mai rau ar implica cel putin un apel al alocatorului si dealocatorului de memorie pentru fiecare apel de functie cu un obiect automatic al clasei. De asemenea s-ar face implementarea unei functii membru inline care sa faca acces la date private fezabile. Mai mult decit atit, o astfel de schimbare ar face imposibila linkarea impreuna a fragmentelor de programe C++ si C (deoarece un compilator C ar trata diferit o structura fata de un compilator C++). Aceasta este nepotrivit in C++. 5.3.2 O clasa completa

Programarea fara ascunderea datelor (folosind structuri) necesita Mai putina bataie de cap decit programarea cu ascunderea de date (utilizind clase). Se poate defini o structura fara prea mare bataie de cap, dar cind definim o clasa noi trebuie sa ne concentram sa furnizam un set complet de operatii pentru tipul nou; aceasta este o deplasare importanta in domeniul utilizarii. Timpul cheltuit in proiectarea unui nou tip este de obicei recuperat de multe ori in dezvoltarea si testarea unui program. Iata un exemplu de tip complet, intset, care furnizeaza conceptul de "multime de intregi". class intset{int cursize, maxsize; int* x; public: intset(int m, int n); //cel putin m intregi //in 1..n ~intset(); int member(int t); //este "t" un membru? void insert(int t); //adauga "t" la multime void iterate(int& i){i = 0;} int ok(int& i){return i < cursize;} int next(int& i){return x[i++];} }; Pentru a testa aceasta clasa noi putem crea si apoi imprima un set de intregi aleatori. Un astfel de set ar putea constitui niste numere de loterie. Acest set simplu ar putea fi utilizat pentru a verifica un sir de intregi punind in evidenta duplicatele, dar pentru majoritatea aplicatiilor tipul set ar trebui sa fie putin mai migalos elaborat. Ca totdeauna sint posibile erori: #include <stream.h> void error(char* s) {cerr << "set: " << s << "\n"; exit(1); } Clasa intset se utilizeaza in functia main() care asteapta doua argumente intregi. Primul argument specifica numarul de numere aleatoare de generat. Cel de al doilea argument specifica domeniul intregilor aleatori care se asteapta: main(int argc, char* argv[]) { if(argc != 3) error("No arguments expected"); int count = 0; int m = atoi(argv[1]); //numarul elementelor multimii int n = atoi(argv[2]); //in domeniul 1..n intset s(m, n); while(cout < m) { int t = randint(n); if(s.member(t) == 0) {

s.insert(t); count++; } } print_in_order(&s); } Motivul ca argumentul numarator argc sa fie 3 pentru un program care cere 2 argumente este faptul ca numele programului este totdeauna pasat ca argv[0]. Functia: extern int atoi(char*); este o functie standard de biblioteca pentru covertirea reprezentarii sub forma de sir a unui intreg in forma lui interna binara. Numerele aleatoare se genereaza utilizind functia standard rand(): extern int rand(); //nu este prea aleatoare int randint(int n) //in domeniul 1..n { int r = rand(); if(r < 0) r = -r; return 1 + r % n; } Detaliile de implementare ale unei clase ar trebui sa fie de un interes mai mic pentru un utilizator, dar aici sint in orice caz si functiile membru. Constructorul aloca un vector intreg de dimensiune maxima a multimii specificate, iar destructorul o dealoca: intset::intset(int m, int n) //cel mult m intregi in 1..n { if(m < 1 || n < m) error("illegal intset size"); cursize = 0; maxsize = m; x = new int[maxsize]; } intset::~intset(){delete x;} Intregii se insereaza asa ca ei sa fie tinuti in ordine crescatoare in multime: void intset::insert(int t) { if(++cursize > maxsize) error("too many elements"); int i = cursize-1; x[i] = t; while(i > 0 && x[i-1] > x[i]) { int t = x[i]; //permuta x[i] si x[i-1]x[i] = x[i-1]; x[i-1] = t; i--; } }

Se foloseste o cautare binara pentru a gasi un membru: int intset::member(int t) //cautare binara { int l = 0; int n = cursize-1; while(l <= n) { int m = (l+n)/2; if(t < x[m]) n = m-1; else if(t > x[m]) l = m+1; else return 1; //gasit } return 0; //negasit } In final, intrucit reprezentarea unei clase intset este ascunsa utilizatorului, noi trebuie sa furnizam un set de ope- ratii care permit utilizatorului sa itereze prin multime intr-o anumita ordine. O multime nu este ordonata intrinsec, asa ca noi nu putem furniza pur si simplu un mod de accesare la vector (miine, eu ma pot gindi sa reimplementez intset ca o lista inlantuita). Se furnizeaza trei functii: iterate() pentru a initializa o iteratie, ok() pentru a verifica daca exista un membru urmator si next() pentru a obtine membrul urmator: class intset{ //......... void iterate(int& i){i = 0;} int ok(int& i){return i < cursize;} int next(int& i){return x[i++];} }; Pentru a permite ca aceste trei operatii sa coopereze si sa reaminteasca cit de departe a progresat iteratia, utilizatorul trebuie sa furnizeze un argument intreg. Intrucit

argumentele sint pastrate intr-o lista sortata, implementarea lor este triviala. Acum poate fi definita functia print_in_order: void print_in_order(intset* set) { int var; set->iterate(var); while(set->ok(var)) cout << set->next(var) << "\n"; } O alta varianta de a furniza un iterator se prezinta in &6.8. 5.4 Prieteni si Reuniuni Aceasta sectiune descrie inca citeva facilitati relativ la clase. Se prezinta un mod de a acorda acces functiilor membre la membri privati. Se descrie cum se pot rezolva conflictele numelor membre, cum se pot imbrica declaratiile de clase si cum pot fi eliminate imbricarile nedorite. De asemenea se discuta cum pot fi obiectele unei clase divizate intre membri ei si cum se pot utiliza pointerii spre membri. In final exista un exemplu care arata cum se poate proiecta o reuniune discriminatorie. 5.4.1 Prieteni Presupunem ca noi trebuie sa definim doua clase, vector si matrix. Fiecare din ele ascunde reprezentarea ei si furnizeaza un set complet de operatii pentru manipularea obiectelor ei. Acum sa definim o functie care inmulteste o matrice cu un vector. Pentru simplificare, presupunem ca un vector are patru elemente, cu indicii 0..3 si ca o matrice are 4 vectori indexati cu 0..3. Presupunem de asemenea, ca elementele unui vector sint accesate printr-o functie elem() care verifica indexul si ca matrix are o functie similara. O conceptie este de a defini o functie globala multiply() de forma: vector multiply(matrix& m, vector& v) {vector r; for(int i=0; i<3; i++) { //r[i] = m[i] * v; r.elem(i) = 0; for(int j=0; j<3; j++) r.elem(i) += m.elem(i, j) * v.elem(j); } return r; }

Aceasta este intr-un anumit mod "natural" sa se faca asa, dar este ineficient. De fiecare data cind se apeleaza multiply(), elem() se apeleaza de 4*(1+4*3) ori. Acum, daca noi facem ca multiply() sa fie membru al clasei vector, noi am putea sa ne dispensam de verificarea indicilor cind se face acces la un element al vectorului si daca noi facem ca multiply() sa fie membru al clasei matrix, noi am putea sa ne dispensam de verificarea indicilor cind se face acces la elementul unei matrici. Cu toate acestea, o functie nu poate fi membru pentru doua clase. Ceea ce este necesar este o constructie a limbajului care sa asigure unei functii accesul la partea privata a unei clase. O functie nemembru la care i se permite accesul la partea privata a unei clase se numeste prieten al clasei. O fun- ctie devine prieten al unei clase printr-o declaratie de prieten in clasa respectiva. De exemplu: class matrix; class vector{float v[4]; //........ friend vector multiply(matrix&, vector&); }; class matrix{vector v[4]; //........ friend vector multiply(matrix&, vector&); }; Nu este nimic special in legatura cu o functie prieten exceptind dreptul de acces la partea privata a unei clase. In particular, o functie prieten nu are un pointer this (numai daca este o functie membru). O declaratie friend este o declaratie reala. Ea introduce numele functiei in domeniul cel Mai extern al unui program si il verifica fata de alte declaratii ale lui. O declaratie friend poate fi plasata sau in partea privata sau in partea publica a unei declaratii de clasa; nu are importanta unde se introduce. Functia multiply poate acum sa fie scrisa utilizind direct elementele vectorilor si matricilor: vector multiply(matrix& m, vector& v) { vector r; for(int i=0; i<3; i++) { //r[i] = m[i]*v; r.v[i] = 0; for(int j=0; j<3; j++) r.v[i] += m.v[i][j] * v.v[j]; } return r; }

Exista moduri de a trata aceasta problema particulara de eficienta fara a utiliza mecanismul friend (se poate defini operatia de inmultire pentru vectori si sa se defineasca multiply() folosind-o pe aceasta). Cu toate acestea, exista multe probleme care sint mult mai usor de rezolvat dind posibilitatea unei functii care nu este membru al unei clase sa faca acces la partea privata a acelei clase. Capitolul 6 contine multe exemple de utilizare a prietenilor. Meritele relative ale functiilor prietene si membre va fi discutata mai tirziu. O functie membru a unei clase poate fi prieten al alteia. De exemplu: class x{ //........ void f(); }; class y{ //........ friend void x::f(); }; Nu este ceva iesit din comun ca toate functiile unei clase sa fie pritene ale alteia. Exista chiar o prescurtare pentru acest fapt: class x{ friend class y; //........ }; Aceasta declaratie, friend, face ca toate functiile membre ale clasei y sa fie prietene ale clasei x. 5.4.2 Calificarea numelor de membri Ocazional, este util sa se faca distinctie explicita intre numele membre ale unei clase si alte nume. Se poate folosi operatorul de rezolutie a domeniului "::": class x{ int m; public: int readm(){ return x::m; } void setm(int m){ x::m = m; } };

In x::setm() numele argument m ascunde membrul m, asa ca membrul ar putea sa fie referit numai utilizind numele calificator al lui, x::m. Operandul sting a lui :: trebuie sa fie numele unei clase. Un nume prefixat prin :: trebuie sa fie un nume global. Aceasta este in particular util pentru a permite nume populare cum ar fi read, put si open sa fie folosite pentru nume de fun- ctii membru fara a pierde abilitatea de a se face referire la versiunea nemembru. De exemplu: class my_file{ //.......... public: int open(char*, char*); }; int my_file::open(char* name, char* spec) { //........... if(::open(name, flag)) { //utilizeaza open() din UNIX(2) //.......... } //........... }

5.4.3 Clase imbricate Declaratiile de clasa pot fi imbricate. De exemplu: class set{ struct setmem{ int mem; setmem* next; setmem(int m,setmem* n){ mem=m; next=n; } }; setmem* first; public: set(){first = 0;} insert(int m){first = new setmem(m, first);} //....... };

Daca clasa imbricata nu este foarte simpla, astfel de declaratii sint foarte incurcate. Mai mult decit atit, clasele imbricate sint mai mult o facilitate in notatie, intrucit o clasa imbricata nu este ascunsa in domeniul clasei care o include din punct de vedere lexical: class set{ struct setmem{ int mem; setmem* next; setmem(int m, setmem* n); }; //....... }; setmem::setmem(int m, setmem* n) {mem = m; next = n; } setmem m1(1, 0); Constructorii de forma set::setmem::setmem() nu sint necesari si nici legali. Singurul mod de ascundere a numelui unei clase este prin utilizarea tehnicii de fisiere_module (&4.4). Clasele netriviale este bine sa fie declarate separat: class setmem{ friend class set; //acces numai prin membri //lui set int mem; setmem* next; setmem(int m, setmem* n){ mem=m; next=n; } }; class set{ setmem* first; public: set(){ first = 0; } insert(int m){ first = new setmem(m, first); } };

5.4.4 Membri statici O clasa este un tip, nu un obiect data si fiecare obiect al clasei are copia lui proprie a membrilor date ai clasei. Cu toate acestea, unele tipuri sint implementate mai elegant

daca toate obiectele acelui tip au in comun unele date. Este preferabil ca o astfel de data comuna sa fie declarata ca parte a clasei. De exemplu, pentru a gestiona taskuri intr-un sistem de operare, este adesea utila o lista a tuturor taskurilor: class task{//........ task* next; static task* task_chain; void schedule(int); void wait(event); //........ }; Declarind membrul task_chain ca static se asigura ca va fi numai o copie a lui, nu o copie pentru fiecare obiect task. Este inca in domeniul clasei task si poate fi accesat "din afara" numai daca a fost declarat public. In acest caz, numele lui tre- buie sa fie calificat prin numele clasei sale: task::task_chain Intr-o functie membru, se poate face referire prin task_chain. Utilizarea membrilor statici ai clasei poate reduce considerabil necesarul de memorie pentru variabilele globale.

5.4.5 Pointeri spre membri Este posibil sa se ia adresa unui membru al unei clase. A lua adresa unei functii membru este adesea util intrucit tehnicile si motivele pentru a utiliza pointeri la functii prezentate in &4.6.9 se aplica in mod egal si la functii membru. Totusi exista un defect curent in limbaj: nu este posibil sa se exprime tipul pointerului obtinut dintr-o astfel de operatie. In consecinta trebuie sa folosim trucuri folosind avantajele din implementarea curenta. Exemplul de mai jos nu este garantat ca fun- ctioneaza si utilizarea lui trebuie localizata in asa fel incit sa poata fi usor convertit spre a utiliza constructiile propri ale limbajului. Trucul folosit este acela de a avea avantajul faptului ca this este implementat curent ca primul argument (ascuns) al unei functii membru. #include <stream.h> struct cl{ char* val; void print(int x){ cout << val << x << "/n"; } cl(char* v){val = v;} }; //"se ia" tipul functiilor membru: typedef void (*PROC)(void*, int); main()

{cl z1("z1 "); cl z2("z2 "); PROC pf1 = PROC(&z1.print); PROC pf2 = PROC(&z2.print); z1.print(1); (*pf1)(&z1, 2); z2.print(3); (*pf2)(&z2, 4); } In multe cazuri, functiile virtuale (vezi capitolul 7) pot fi utilizate cind altfel s-ar utiliza pointeri spre functii. Versiunile ulterioare de C++ vor suporta un concept de pointer spre un membru: cl::* inseamna "pointer spre un membru a lui cl". De exemplu: typedef void(cl::*PROC)(int); PROC pf1=&cl::print;//nu este nevoie de conversie explicita PROC pf2 = &cl::print; Operatorii . si -> se utilizeaza pentru un pointer spre o functie membru. De exemplu: (z1.*pf1)(2); ((&z2)->*pf2)(4);

5.4.6 Structuri si Reuniuni Prin definitie o structura este pur si simplu o clasa cu toti membri publici, adica: struct s{ ... este pur si simplu o prescurtare pentru: class{ public: ... Structurile se folosesc cind ascunderea datelor este nepotrivita. O reuniune numita se defineste ca o structura in care fiecare membru are aceeasi adresa (vezi &r8.5.13). Daca se stie ca numai un membru al unei structuri va avea o valoare utila la un moment dat, o reuniune poate salva spatiu. De exemplu, se poate defini o reuniune pentru a pastra unitatile lexicale dintr-un compilator C: union tok_val{ char* p; //sir char v[8]; //identificator (maxim 8 caractere)

long i; //valori intregi double d; //valori flotante }; Problema este ca, in general compilatorul nu poate sa stie care membru este utilizat in fiecare moment, asa ca nu poate fi testat tipul. De exemplu: void strange(int i) { tok_val x; if(i) x.p = "2"; else x.d = 2; sqrt(x.d); //eroare daca i != 0 } Mai mult decit atit, o reuniune definita in acest fel poate fi initializata. De exemplu: tok_val curr_val = 12; //eroare: se atribuie int la tok_val este ilegal. Se pot utiliza constructori care sa trateze corect aceasta problema: union tok_value{ char* p; //sir char v[8]; //identificator long i; //valori intregi double d; //valori flotante tok_value(char*) //trebuie sa decida intre //p si v tok_value(int ii){i = ii;} tok_value(double dd){d == dd;} }; Aceasta trateaza cazurile in care tipurile membru pot fi rezolvate prin reguli pentru nume de functii supraincarcate (vezi &4.6.7 si &6.3.3). De exemplu: void f() { tok_val a = 10; //a.i = 10 tok_val b = 10.0; //b.d = 10.0 }

Cind acest lucru nu este posibil (pentru tipurile char* si char[8], int si char, etc.), membrul propriu poate fi gasit numai examinind initializatorul la momentul executiei sau furnizind un extra argument. De exemplu: tok_val::tok_val(char* pp) { if(strlen(pp) <= 8) strncpy(v, pp, 8); //sir scurt else p = pp; //sir lung } Astfel de cazuri este mai bine sa fie eliminate. Utilizind constructorii nu putem preveni utilizarea eronata a unui tok_val prin atribuirea unei valori la un tip si apoi utilizarea ei ca fiind de alt tip. Aceasta problema poate fi rezolvata incluzind reuniunea intr-o clasa care tine seama de tipul valorii memorate. class tok_val{ char tag; union{ char* p; char v[8]; long i; double d; }; int check(char t, char* s) { if(tag != t) { error(s); return 0; } return 1; } public: tok_val(char* pp); tok_val(long ii){ i=ii; tag='I'; } tok_val(double dd){ d=dd; tag='D'; } long& ival(){ check('I', "ival"); return i; } double& fval(){check('D', "fval"); return d; } char*& sval(){ check('S', "sval"); return p; } char* id(){ check('N', "id"); return v; } };

Constructorul utilizeaza functia strncpy pentru a copia un sir scurt; strncpy() aminteste de strcpy(), ea avind un al treilea argument care defineste numarul de caractere ce se copiaza. tok_val::tok_val(char* pp) { if(strlen(pp) <= 8) { //sir scurt tag = 'N'; strncpy(v, pp, 8); //copiaza 8 caractere } else { tag = 'S'; p = pp; //se pastreaza numai pointerul } } Tipul tok_val poate fi folosit astfel: void f() { tok_val t1("short"); //asignare la v tok_val t2("long string"); //asignare la p char s[8]; strncpy(s, t1.id(), 8); //ok strncpy(s, t2.id(), 8); //testul va esua }

5.5

Constructori si Destructori

Cind o clasa are un constructor, el este apelat ori de cite ori se creaza un obiect al acelei clase. Cind o clasa are un destructor, el este apelat ori de cite ori este distrus un obiect al acelei clase. Obiectele pot fi create ca: [1] Un obiect automatic: se creaza de fiecare data cind se intilneste declaratia lui la executia programului si este distrus de fiecare data cind se iese din blocul in

care el a aparut; [2] Un obiect static: se creaza o data la pornirea programului si se distruge o data cu terminarea programului; [3] Un obiect in memoria libera: este creat folosind operatorul new si distrus folosind operatorul delete; [4] Un obiect membru: ca membru al unei clase ori ca un element de vector. Un obiect poate de asemenea, sa fie construit intr-o expresie prin folosirea explicita a unui constructor (&6.4), caz in care el este un obiect automatic. In subsectiunile care urmeaza se presupune ca obiectele sint ale unei clase cu un constructor si un destructor. Ca exemplu se utilizeaza clasa table din &5.3. 5.5.1 Goluri Daca x si y sint obiecte ale clasei cl, x=y inseamna copierea bitilor lui y in x (&2.3.8). Avind asignarea interpretata in acest fel noi putem sa ajungem la surprize (uneori nedorite) cind folosim obiecte ale unei clase pentru care a fost definit un constructor si un destructor. De exemplu: class char_stack{ int size; char* top; char* s; public: char_stack(int sz){top=s=new char[size=sz];} ~char_stack(){delete s;} //destructor void push(char c){*top++=c;} char pop(){return *--top;} }; void h() { char_stack s1(100); char_stack s2 = s1; //apar probleme char_stack s3(99); s3 = s2; //apar probleme } Aici constructorul char_stack::char_stack() se apeleaza de doua ori: pentru s1 si s3. Nu se apeleaza pentru s2 deoarece variabila s2 a fost initializata prin atribuire. Totusi, destructorul char_stack::~char_stack() se apeleaza de trei ori: pentru s1, s2 si s3. Mai mult decit atit, interpretarea implicita a atribuirii ca si copiere de biti face ca s1, s2 si s3 sa contina fiecare la sfirsitul lui h() un pointer spre vectorul de caractere alocat in memoria libera cind a fost creat s1. Nu va ramine nici un pointer spre

vectorul de caractere alocate cind a fost creat s3. Astfel de anomalii pot fi eliminate asa cum se va vedea in capitolul 6.

5.5.2 Memoria statica Consideram: table tbl1(100); void f(){ static table tbl2(200); } main() { f(); } Aici, constructorul table::table() asa cum a fost definit in &5.3.1 va fi apelat de doua ori: o data pentru tbl1 si o data pentru tbl2. Destructorul table::~table() va fi apelat de asemenea de doua ori: pentru a elimina tbl1 si tbl2 dupa iesirea din main(). Constructorii pentru obiecte globale statice intr-un fisier se executa in ordinea in care apar declaratiile; destructorul se apeleaza in ordine inversa. Daca un constructor pentru un obiect local static este apelat, el se apeleaza dupa ce au fost apelati constructorii pentru obiectele statice globale care il preced. Argumentele pentru constructorii de obiecte statice trebuie sa fie expresii constante: void g(int a) { static table t(a); //eroare } Traditional, executia lui main() a fost vazuta ca executia programului. Aceasta nu a fost niciodata asa, nici chiar in C, dar numai alocind un obiect static al unei clase cu un constructor si/sau un destructor programatorul poate sa aiba un mod evident si simplu de a specifica cod de executat inainte si/sau dupa apelul lui main. Apelind constructori si destructori pentru obiecte statice se realizeaza functii extrem de importante in C++. Este modul de a asigura initializari propri si de a curata structuri de date din biblioteci. Consideram <stream.h>. De unde vin cin, cout si cerr? Unde au fost ele initializate? Si ce este Mai important, intrucit sirurile de iesire pastreaza zone tampon interne de caractere, cum se videaza aceste zone tampon? Raspunsul simplu si clar este acela ca activitatea se face prin constructori si des- tructori corespunzatori inainte si dupa executia lui main(). Exista

alternative de a utiliza constructori si destructori pentru initializarea si stergerea facilitatilor de biblioteca. Daca un program se termina utilizind functia exit(), se vor apela destructorii pentru obiectele statice, dar daca, programul se termina folosind abort(), ei nu vor fi apelati. Sa observam ca aceasta implica faptul ca exit() nu termina programul imediat. Apelind exit() intr-un destructor se poate ajunge la o recursivitate infinita. Uneori, cind noi proiectam o biblioteca, este necesar sau pur si simplu convenabil sa inventam un tip cu un constructor si un destructor cu singurul scop al initializarii si stergerii. Un astfel de tip va fi folosit numai o data: sa aloce un obiect static prin apelul constructorului. 5.5.3 Memoria libera Fie: main() { table* p = new table(100); table* q = new table(200); delete p; delete p; //probabil o eroare } Constructorul table::table() va fi apelat de doua ori si la fel si destructorul table::~table(). Este bine de amintit ca C++ nu ofera garantie ca un destructor este apelat vreodata pentru un obiect creat folosind new. Programul precedent nu il sterge pe q, dar pe p il sterge de doua ori. In functie de tipul lui p si q, programatorul poate sau nu sa considere aceasta ca o eroare. Ne- stergind un obiect de obicei nu este o eroare, ci numai o pierdere de spatiu. Stergind p de doua ori este de obicei o eroare serioasa. Un rezultat frecvent al aplicarii lui delete de doua ori la acelasi pointer este un ciclu infinit in rutina de gestionare a memoriei libere, dar comportamentul in acest caz nu este specificat prin definitia limbajului si depinde de implementare. Utilizatorul poate defini o implementare noua pentru operatorii new si delete (vezi &3.2.6). Este de asemenea posibil sa se specifice modul in care interactioneaza constructorul si destructorul cu operatorii new si delete (vezi &5.5.6). 5.5.4 Obiectele clasei ca membri (clase de obiecte ca membri) Consideram: class classdef{

table members; int no_of_members; //........... classdef(int size); ~classdef(); }; Intentia este clara; aceea ca classdef sa contina o tabela de members de dimensiune size si problema este de a obtine constructorul table::table() apelat cu argumentul size. Se poate face astfel: classdef::classdef(int size) :members(size) { no_of_members = size; //........... } Argumentele pentru un constructor membru (table::table()) se plaseaza in definitia (nu in declaratia) constructorului clasei care il contine (aici classdef::classdef()). Constructorul membru este apoi apelat inaintea corpului constructorului care specifica lista argumentelor lui. Daca sint mai multi membri ce necesita liste de argumente pentru constructori, ei pot fi specificati in mod analog. De exemplu: class classdef{ table members; table friends; int no_of_members; //.......... classdef(int size); ~classdef(); }; Lista de argumente pentru membri se separa prin virgula (nu prin doua puncte), iar listele initializatorilor pentru membri pot fi prezentate in orice ordine: classdef::classdef(int size) :friends(size), members(size) { no_of_members = size; //........... }

Ordinea in care se apeleaza constructorii nu este specificata, asa ca nu se recomanda ca lista argumentelor sa fie cu efecte secundare: classdef::classdef(int size) :friends(size = size/2), members(size) //stil rau { no_of_members = size; //........... } Daca un constructor pentru un membru nu necesita argumente, atunci nu este necesar sa se specifice nici o lista de argumente. De exemplu, intrucit table::table() a fost definit cu argumentul implicit 15, ceea ce urmeaza este corect: classdef::classdef(int size) :members(size) { no_of_members = size; //........... } si dimensiunea lui friends table va fi 15. Cind o clasa care contine clase (de exemplu classdef) se distruge, intii se executa corpul destructorului propriu acelei clase si apoi se executa destructorii membrilor. Consideram varianta traditionala de a avea clase ca membri si anume aceea de a avea membri pointeri si ai initializa pe acestia intr-un constructor: class classdef{ table* members; table* friends; int no_of_members; //............ classdef(int size); ~classdef(); }; classdef::classdef(int size) { members = new table(size); friends = new table; //dimensiune implicita no_of_members = size; //........... } Intrucit tabelele au fost create folosind new, ele trebuie sa fie distruse utilizind delete: classdef::~classdef() {//...........

delete members; delete friends; } Obiectele create separat ca acestea pot fi utile, dar sa observam ca members si friends pointeaza spre obiecte separate care cer o alocare si o dealocare fiecare. Mai mult decit atit, un pointer plus un obiect in memoria libera ia mai mult spatiu decit un obiect membru.

5.5.5 Vectori si Obiecte clasa Pentru a declara un vector de obiecte ale unei clase cu un constructor acea clasa trebuie sa aiba un constructor care sa poata fi apelat fara o lista de argumente. Nici argumentele implicite nu pot fi utilizate. De exemplu: table tblvec[10]; este o eroare deoarece table::table() necesita un argument intreg. Nu exista nici un mod de a specifica argumente pentru un constructor intr-o declaratie de vector. Pentru a permite declararea vectorilor de tabele, ar putea fi modificata declaratia clasei table (&5.3.1) astfel: class table{ //......... void init(int sz); //ca si constructorul vechi public: table(int sz){init(sz);} //ca inainte dar nu //exista valoare implicita table(){init(15);} //implicit //......... }; Destructorul trebuie apelat pentru fiecare element al unui vector cind se distruge acel vector. Aceasta se face implicit pentru vectori care nu sint alocati utilizind new. Cu toate acestea, aceasta nu se poate face implicit pentru vectori din memoria libera deoarece compilatorul nu poate face distinctie dintre pointerul spre un singur obiect de un pointer spre primul element al unui vector de obiecte. De exemplu: void f() { table* t1 = new table; table* t2 = new table[10]; delete t1; //o tabela

delete t2; }

//apar probleme: 10 tabele

In acest caz programatorul trebuie sa furnizeze dimensiunea vectorului: void g(int sz) {table* t1 = new table; table* t2 = new table[sz]; delete t1; delete[sz] t2; } Dar de ce nu poate compilatorul sa deduca numarul de elemente din cantitatea de memorie alocata? Deoarece alocatorul de memorie libera nu este o parte a limbajului si ar putea fi furnizata de programator.

5.5.6 Obiecte mici Cind se utilizeaza multe obiecte mici alocate in memoria libera, noi putem sa aflam ca programul consuma timp considerabil pentru alocare si dealocare de astfel de obiecte. O solutie este de a furniza un alocator cu scopuri generale mai bun si o a doua este ca proiectarea unei clase sa nu se faca pentru a fi gestionata in memoria libera, definind constructori si destructori. Sa consideram clasa name folosita in exemplul table. Ea ar putea fi definita astfel: struct name{char* string; name* next; double value; name(char*, double, name*); ~name(); }; Programatorul poate avea avantaje din faptul ca alocarea si dealocarea obiectelor unui tip poate fi facuta pe departe mai eficient (in timp si spatiu) decit cu o implementare generala prin new si delete. Ideea generala este de a prealoca "felii" de obiecte de tip name si de a le lega intre ele, reducind alocarea si dealocarea la operatii simple asupra listelor inlantuite. Variabila nfree este antetul unei liste de nume neutilizate. const NALL = 128; name* nfree; Alocatorul utilizat prin operatorul new pastreaza dimensiunea unui obiect impreuna cu obiectul pentru ca operatorul delete sa functioneze corect. Aceste spatii suplimentare se elimina simplu la un alocator specific unui tip. De exemplu, alocatorul urmator utilizeaza 16 octeti pentru a memora un name la masina mea, in timp ce alocatorul general foloseste 20. Iata cum se poate face aceasta: name::name(char* s, double v, name* n)

{register name* p = nfree //prima alocare if(p) nfree = p->next; else {name* q = (name*)new char[NALL * sizeof(name)]; for(p = nfree = &q[NALL-1]; qnext = 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: . 8. (*2). Sa se defineasca niste clase pentru a furniza numere aleatoare de o anumita distributie. Fiecare clasa are un constructor care specifica parametri pentru distributie si o functie draw care returneaza valoarea "urmatoare". Recomandare: . Vezi de asemenea clasa intset. 9. (*2). Sa se rescrie exemplul date (&5.2.2) exemplul char_stack (&5.2.5) si exemplul intset (&5.3.2) fara a utiliza functii membru (nici chiar constructori si destructori). Sa se utilizeze numai class si friend. Sa se testeze versiunile noi. Sa se compare cu versiunile care utilizeaza functiile membru. 10. (*3). Sa se proiecteze o clasa pentru o tabela de simboluri si o clasa de intrare in tabela de simboluri pentru un anumit limbaj. Sa aruncam o privire la compilatorul limbajului respectiv pentru a vedea cum arata tabela de simboluri reala. 11. (*2). Sa se modifice clasa expresie din exercitiul 5 pentru a trata variabile si operatorul de asignare =. Sa se foloseasca clasa tabela de simboluri din exercitiul 10. 12. (*1). Fiind dat programul: #include <stream.h> main() { cout << "Hello, word\n"; }

sa se modifice pentru a avea la iesire: Initialize Hello, world Clean up Sa nu se modifice functia main().

CAPITOLUL 6 OPERATOR SUPRAINCARCAT Acest capitol descrie mecanismul pentru operatorul de supraincarcare furnizat de C+ +. Un programator poate defini un sens pentru operatori cind se aplica la obiectele unei clase specifice; in plus se pot defini fata de operatiile aritmetice, logice si relationale, apelul () si indexarea [] si atit initializarea cit si asignarea pot fi redefinite. Se pot defini conversii de tip implicite si explicite intre cele definite de utilizator si tipurile de baza. Se arata cum se defineste o clasa pentru care un obiect nu poate fi copiat sau distrus exceptind functiile specifice definite de utilizator. 6.1

Introducere

Programele adesea manipuleaza obiecte care sint reprezentari concrete ale conceptelor abstracte. De exemplu, datele de tip int din C++, impreuna cu operatorii +, -, *, /, etc., furnizeaza o implementare (restrictiva) a conceptului matematic de intregi. Astfel de concepte de obicei includ un set de operatori care reprezinta operatiile de baza asupra obiectelor intr-un mod concis, convenabil si conventional. Din nefericire, numai foarte putine astfel de concepte pot fi suportate direct prin limbajul de programare. De exemplu, ideile de aritmetica complexa, algebra matricilor, semnale logice si sirurile receptionate nu au un suport direct in C++. Clasele furnizeaza o facilitate pentru a specifica o reprezentare a obiectelor neprimitive in C++ impreuna cu un set de operatii care pot fi efectuate cu astfel de obiecte. Definind operatori care sa opereze asupra obiectelor unei clase, uneori se permite unui programator sa furnizeze o notatie mai conventionala si mai

convenabila pentru a manipula obiectele unei clase, decit s-ar putea realiza utilizind numai notatia functionala de baza. De exemplu: class complex{ double re, im; public: complex(double r, double i){re=r; im=i;} friend complex operator+(complex, complex); friend complex operator*(complex, complex); }; defineste o implementare simpla a conceptului de numere comlexe, unde un numar este reprezentat printr-o pereche de numere flotante in dubla precizie manipulate (exclusiv) prin operatorii + si *. Programatorul furnizeaza un inteles pentru + si * definind functiile denumite operator+ si operator*. De exemplu, dind b si c de tip complex, b+c inseamna (prin definitie) operator+(b, c). Este posibil acum sa se aproximeze interpretarea conventionala a expresiilor complexe. De exemplu: void f() { complex a = complex(1, 3.1); complex b = complex(1.2, 2); complex c = b; a = b+c; b = b+c*a; c = a*b+complex(1, 2); }

6.2

Functiile operator

Functiile care definesc intelesul pentru operatorii urmatori pot fi declarate: + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= /= << >> >>= <<= == != <= >= && || ++ -- [] () new delete Ultimii patru sint pentru indexare (&6.7), apel de functie (&6.8), alocare de memorie libera si dealocare de memorie libera (&3.2.6). Nu este posibil sa se schimbe precedenta acestor operatori si nici sintaxa expresiei nu poate fi schimbata. De exemplu, nu este posibil sa se defineasca un operator unar % sau unul binar !. Nu

este posibil sa se defineasca operatori noi, dar noi putem utiliza notatia de apel de functie cind acest set de operatori nu este adecvat. De exemplu, vom utiliza pow() si nu **. Aceste restrictii s-ar parea sa fie suparatoare, dar reguli mai flexibile pot foarte usor sa conduca la ambiguitati. De exemplu, definind un operator ** care sa insemne exponentiala, expresia a**p se poate interpreta atit ca a*(*p) cit si (a)**(p). Numele unei functii operator este cuvintul cheie operator urmat de operatorul insusi (de exemplu operator<<). O functie operator se declara si poate fi apelata ca orice alta functie; utilizarea unui operator este numai o prescurtare pentru un apel explicit a functiei operator. De exemplu: void f(complex a, complex b) { complex c = a+b; //prescurtare complex d = operator+(a, b); //apel explicit }

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

Cind sint supraincarcati operatorii ++ si --, nu este posibil sa se faca distinctie intre aplicatia postfix si cea prefix. 6.2.2 Sensul predefinit al operatorilor Nu se face nici o presupunere despre sensul unui operator definit de utilizator. In particular, intrucit supraincarcarea lui = nu se presupune ca implementeaza atribuirea la primul operand al lui; nu se face nici un test pentru a asigura ca acel operand este o lvalue (&r6). Sensurile unor operatori predefiniti se definesc astfel incit sa fie echivalente cu anumite combinatii de alti operatori asupra acelorasi argumente. De exemplu, daca a este un intreg, ++a inseamna a+=1, care la rindul ei inseamna a=a+1. Astfel de relatii nu au loc pentru operatorii definiti de utilizator, numai daca se intimpla ca utilizatorul sa le defineasca in acel fel. De exemplu, definitia lui operator++() pentru un tip complex nu poate fi dedusa din definitiile complex::operator+() si complex::operator=(). Din cauza unui accident istoric, operatorii = si & au sensuri predefinite cind se aplica la obiectele unei clase. Nu exista un mod elegant de a "nedefini" acesti doi operatori. Ei pot totusi sa fie dezactivati pentru o clasa X. Se poate, de exemplu, declara X::operator&() fara a furniza o definitie pentru el. Daca undeva se ia adresa unui obiect al clasei X, linkerul va detecta o lipsa de definitie. Pe anumite sisteme, linkerul este atit de "destept" incit el se descurca cind o fun- ctie neutilizata nu este definita. Pe astfel de sisteme aceasta tehnica nu poate fi utilizata. O alta alternativa este de a defini X::operator&() asa ca sa dea la executie o eroare. 6.2.3 Operatori si Tipuri definite de utilizatori O functie operator trebuie sau sa fie un membru sau sa aiba cel putin un argument obiect al unei clase (functiile care redefinesc operatorii new si delete nu sint necesare). Aceasta regula asigura ca un utilizator sa nu poata schimba sensul oricarei expresii care nu implica un tip de data definit de utilizator. In particular, nu este posibil sa se defineasca o functie operator care sa opereze exclusiv asupra pointerilor. O functie operator care intentioneaza sa accepte un tip de baza ca primul sau operand nu poate fi o functie membru. De exemplu, sa consideram adaugarea unei variabile complexe aa la intregul 2: aa+2 poate cu o functie membru corespunzatoare sa fie interpretata ca aa.operator+(2), dar 2+aa nu poate fi, intrucit nu exista nici o clasa int pentru care sa se defineasca + ca sa insemne 2.operator+(aa). Chiar daca ar fi, ar fi necesare doua functii membru diferite care sa trateze 2+aa si aa+2. Deoarece compilatorul nu cunoaste intelesul lui + definit de utilizator, el nu poate presupune ca el este comutativ si sa interpreteze 2+aa ca aa+2. Acest exemplu se trateaza trivial cind se utilizeaza functii friends.

Toate functiile operator sint prin definitie supraincarcate. O functie operator furnizeaza un inteles nou pentru un operator in plus fata de definitia predefinita si pot fi functii operator diferite cu acelasi nume atita timp cit ele difera suficient prin tipul argumentelor lor (&4.6.7). 6.3

Conversia de tip definita de utilizator

Implementarea numerelor complexe prezentata in introducere este prea restrictiva ca sa placa cuiva, asa ca ea trebuie extinsa. Aceasta este mai mult o repetitie triviala a tehnicilor prezentate anterior. class complex { double re, im; public: complex(double r, double i){re=r; im=i;} friend complex operator+(complex, complex); friend complex operator+(complex, double); friend complex operator+(double, complex); friend complex operator-(complex, complex); friend complex operator-(complex, double); friend complex operator-(double, complex); complex operator-(); //unar friend complex operator*(complex, complex); friend complex operator*(complex, double); friend complex operator*(double, complex); // ... }; Acum, cu aceasta declaratie a lui complex noi putem scrie: void f() { complex a(1, 1), b(2, 2), c(3, 3), d(4, 4), e(5, 5); a = -b-c; b = c*2.0*c; c = (d+e)*a; } Totusi, scrierea unei functii pentru fiecare combinatie dintre complex si double ca si pentru operator*() de mai sus, este o tendinta de nesuportat. Mai mult decit atit, o facilitate realista pentru aritmetica complexa trebuie sa furnizeze cel putin o duzina de astfel de functii; vezi de exemplu, tipul complex asa cum este el declarat in .

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 , expresia zz1*3+zz2*complex(1,2) va apela doua functii si nu cinci. Cele doua operatii * vor apela functii, dar operatia + si constructorul apelat pentru a crea complex(3) si complex(1,2) vor fi expandate inline. 6.5

Obiecte mari

Pentru orice utilizare a unui operator binar complex declarat in prealabil, se transfera o copie a fiecarui operand la funlctia care implementeaza operatorul. Pentru copierea a doua double acest lucru este acceptabil. Din nefericire, nu toate clasele au o reprezentare convenabil de mica. Pentru a elimina copierea excesiva, se pot declara functii care sa aiba ca argumente referinte. De exemplu: class matrix{ double m[4][4]; public: matrix(); friend matrix operator+(matrix&, matrix&); friend matrix operator*(matrix&, matrix&); }; Referintele permit utilizarea expresiilor care implica operatori aritmetici uzuali pentru obiecte mari fara a face copieri excesive. Pointerii nu pot fi utilizati deoarece nu este posibil sa se redefineasca sensul unui operator cind el se aplica la un pointer. Operatorul + ar putea fi definit astfel: matrix operator+(matrix& arg1, matrix& arg2) { matrix sum; for(int i=0; i<4; i++) for(int j=0; j<4; j++) sum.m[i][j] = arg1.m[i][j] + arg2.m[i][j]; return sum; } Acest operator+() are acces la operanzii lui + prin referinte, dar returneaza o valoare obiect. Returnarea unei referinte pare sa fie mai eficienta: class matrix{ //... friend matrix& operator+(matrix&, matrix&); friend matrix& operator*(matrix&, matrix&);

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

Asignare si Initializare

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

return; a.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>buf)

vec[buff]++; vec.print_all(); } 6.8

Apelul unei functii

Apelul unei functii, adica notatia expresie(lista_de_expr.), poate fi interpretat ca o operatie binara iar operatorul de apel () poate fi incarcat intr-un anumit fel ca si ceilalti operatori. Lista argument pentru o functie operator() se evalueaza si se verifica potrivit regulilor de pasare al argumentelor obisnuite. Supraincarcarea apelului de functie pare sa fie utila in primul rind pentru a defini tipurile cu numai o singura operatie. Noi nu am definit un iterator pentru tabloul asociativ de tip assoc. Aceasta s-ar putea face definind o clasa assoc_iterator cu sarcina de a prezenta elementele dintr-un assoc intr-o anumita ordine. Iteratorul necesita acces la datele memorate intr-un assoc si de aceea este facut un friend: class assoc{ friend class assoc_iterator; pair* vec; int max; int free; public: assoc(int); int& operator[](char*); }; Iteratorul poate fi definit astfel: class assoc_iterator{ assoc* cs; //tabloul assoc curent int i; //index curent public: assoc_iterator(assoc& s){ cs=&s; i=0; } pair* operator()() {return (ifree)? &cs->vec[i++]:0;} }; Un assoc_iterator trebuie sa fie initializat pentru un tablou assoc si va returna un pointer spre o pereche noua pair a acelui tablou de fiecare data cind este activat utilizind operatorul (). Cind se ajunge la sfirsitul tabloului se returneaza 0: main() //numara aparitiile fiecarui cuvint de la intrare { const MAX = 256; //mai mare decit cel mai mare cuvint char buff[MAX]; assoc vec(512); while(cin >> buff) vec[buff]++; assoc_iterator next(vec); pair* p; while(p = next()) cout << p->name << ":" << p->val << "\n"; }

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

O clasa sir

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

} string::string(char* s) { p = new srep; p->s = new char[strlen(s)+1]; strcpy(p->s, s); p->n = 1; } string::string(string& x) { x.p->n++; p = x.p; } string::~string() { if(--p->n == 0) { delete p->s; delete p; } } De obicei, operatorii de asignare sint similari cu constructorii. Ei trebuie sa stearga primul operand sting al lor: string& string::operator=(char* s) { if(p->n > 1) { //deconectare p->n--; p = new srep; } else if(p->n == 1) delete p->s; p->s = new char[strlen(s)+1]; strcpy(p->s, s); p->n = 1; return *this; } Este recomandabil sa ne asiguram ca asignarea unui obiect la el insusi lucreaza corect: string& string::operator=(string& x)

{ x.p->n++; if(--p->n == 0) { delete p->s; delete p; } p = x.p; return *this; } Operatorul de iesire este pus cu intentia de a demonstra utilizarea numaratorului de referinte. El face ecou pe fiecare sir de intrare (utilizind operatorul <<, definit mai jos): ostream& operator<<(ostream& s, string& x) { return s << x.p->s << "[" << x.p->n << "]\n"; } Operatorul de intrare utilizeaza functia de intrare standard a sirurilor de caractere (&8.4.1): istream& operator>>(istream& s, string& x) { char buf[256]; s >> buf; x = buf; cout << "echo: " << x << "\n"; return s; } Operatorul de indexare este furnizat pentru acces la caractere individuale. Indexul este verificat: void error(char* p) { cerr << p << "\n"; exit(1); } char& string::operator[](int i) {

if(i<0 || strlen(p->s)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"<level<<"\n"; //........ break; } } si sa o utilizam pentru a imprima o lista de angajati, astfel: void f(employee* ll) { for( ; ll; ll=ll->next) print_employee(ll); } Aceasta functioneaza frumos, mai ales intr-un program scris de o singura persoana, dar are o slabiciune fundamentala care depinde de programatorul care manipuleaza tipurile intr-un mod care nu poate fi verificat de compilator. Aceasta de obicei conduce la doua tipuri de erori in programele mai mari. Primul este lipsa de a testa cimpul de tip si cel de al doilea este imposibilitatea de a plasa toate cazurile posibile

intr-un switch cum ar fi cel de sus. Ambele sint usor de eliminat cind programul se scrie si foarte greu de eliminat cind se modifica un program netrivial; in special un program mare scris de altcineva. Aceste probleme sint adesea mai greu de eliminat din cauza ca functiile de felul lui print() sint adesea organizate pentru a avea avantaje asupra partilor comune ale claselor implicate. De exemplu: void print_employee(employee* e) { cout << e->name << "\t" << e->departament << "\n"; //........ if(e->type == M) { manager* p = (manager*)e; cout << " level " << p->level << "\n"; //....... } } A gasi toate instructiunile if aflate intr-o functie mare care trateaza multe clase derivate poate fi dificil si chiar cind sint localizate poate fi greu de inteles ce fac. 7.2.8 Functii virtuale Functiile virtuale rezolva problemele solutiei cu cimpuri de tip, permitind programatorului sa declare functii intr-o clasa de baza care pot fi redefinite in fiecare clasa derivata. Compilatorul si incarcatorul vor garanta corespondenta corecta intre obiecte si functii aplicate la ele. De exemplu: struct employee{ employee* next; char* name; short departament; //........ virtual void print(); }; Cuvintul cheie virtual indica faptul ca functia print() poate avea versiuni diferite pentru clase derivate diferite si ca este sarcina compilatorului sa gaseasca pe cel potrivit pentru fiecare apel al functiei print(). Tipul functiei se declara in clasa de baza si nu poate fi redirectat intr-o clasa derivata. O functie virtuala trebuie sa fie definita pentru clasa in care este declarata intii. De exemplu: void employee::print() { cout << name << "\t" << departament << "\n";

//........ } Functia virtuala poate fi utilizata chiar daca nu este derivata nici o clasa din clasa ei iar o clasa derivata care nu are nevoie de o versiune speciala a functiei virtuale nu este necesar sa furnizeze vreo versiune. Cind se scrie o clasa derivata, pur si simplu se furnizeaza o functie potrivita daca este necesar. De exemplu: struct manager : employee{employee* group; short level; //....... void print(); }; void manager::print() {employee::print(); cout << "\tlevel" << level << "\n"; } Functia print_employee() nu este acum necesara deoarece functiile membru print() si-au luat locul lor, iar o lista de angajati poate fi minuita astfel: void f(employee* ll) { for( ; ll; ll=ll->next) ll->print(); } Fiecare angajat va fi scris potrivit tipului lui. De exemplu: main() { employee e; e.name = "J. Brown"; e.departament = 1234; e.next = 0; manager m; m.name = "J. Smith"; m.departament = 1234; m.level = 2; m.next = &e; f(&m); } va produce: J. Smith 1234 level 2 J. Browh 1234

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

7.3

Interfete alternative

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

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

class slink{ friend class slist; friend class slist_iterator; slink* next; ent e; slink(ent a, slink* p) { e=a; next=p; } };

Un link poate pastra un singur ent si se utilizeaza pentru a implementa clasa slist: class slist{ friend class slist_iterator; slink* last;//last->next este capul listei public: int insert(ent a);//adauga la capul listei int append(ent a);//adauga la coada listei ent get(); //returneaza si elimina capul listei void clear(); //elimina toate linkurile slist(){ last=0; } slist(ent a) {last = new slink(a, 0); last->next = last; } ~slist(){ clear(); } }; Desi lista este evident implementata ca o lista inlantuita, implementarea ar putea fi schimbata astfel incit sa utilizeze un vector de ent fara a afecta utilizatorii. Adica utilizarea lui slink nu este aratata in declaratiile functiilor publice ale lui slist, ci numai in partea privata si in definitiile de functie. 7.3.2 O implementare Implementarea functiilor din slist este directa. Singura problema este aceea ca, ce este de facut in cazul unei erori sau ce este de facut in caz ca utilizatorul incearca un get() dintr-o lista vida. Aceasta se va discuta in &7.3.4. Iata definitiile pentru membri lui slist. Sa observam cum memorind un pointer spre ultimul element al unei

liste circulare se permite implementarea simpla atit a operatiei append() cit si a operatiei insert(): int slist::insert(ent a) { if(last) last->next = new slink(a, last->next); else { last = new slink(a, 0); last->next = last; } return 0; } int slist::append(ent a) { if(last) last = last->next = new slink(a, last->next); else {last = new slink(a, 0); last->next = last; } return 0; } ent slist::get() { if(last==0) slist_handler("get from empty slist"); slink* f = last->next; ent r = f->e; last = (f==last) ? 0 : f->next; delete f; return r; } Sa observam modul in care se apeleaza slist_handler (declaratia lui poate fi gasita in &7.3.4). Acest pointer la numele functiei se utilizeaza exact ca si cum ar fi numele functiei. Aceasta este o prescurtare pentru o notatie de apel mai explicita: (*slist_handler)("get from empty list"); In final, slist::clear() elimina toate elementele dintr-o lista: void slist::clear() {slist* l = last; if(l==0) return;

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

7.3.3 Cum sa o folosim Asa cum este, clasa slist virtual nu este utila. Inainte de toate, la ce foloseste o lista de pointeri void* ? Smecheria este de a deriva o clasa din slist pentru a obtine o lista de obiecte al unui tip care este de interes intr-un program particular. Sa consideram un compilator pentru un limbaj de felul lui C++. Aici listele de nume vor fi utilizate extensiv; un nume este ceva de forma: struct name{char* string; //....... }; Pointerii spre name vor fi pusi in lista in locul obiectelor name. Aceasta permite utilizarea cimpului de informatie unica, e, a lui slist si admite ca un nume sa fie in

mai multe liste in acelasi timp. Iata o definitie a unei clase nlist care deriva trivial din clasa slist: #include "slist.h" #include "name.h" struct nlist : slist{ void insert(name* a){ slist::insert(a); } void append(name* a){ slist::append(a); } name* get(){ return (name*)slist::get(); } nlist(){} nlist(name* a) : (a){} }; Functiile clasei noi sint sau mostenite direct din slist, sau fac numai conversie de tip. Clasa nlist nu este nimic altceva decit o alternativa de interfata pentru clasa slist. Din cauza ca tipul ent in realitate este void*, nu este necesar sa se converteasca explicit pointerii name* utilizati ca parametri actuali (&2.3.4). Listele de nume ar putea fi utilizate in acest fel intr-o clasa care reprezinta o definitie de clasa: struct classdef{nlist friends; nlist constructors; nlist destructors; nlist members; nlist operators; nlist virtuals; //........ void add_name(name*); classdef(); ~classdef(); }; si numele s-ar putea adauga la acele liste in aceasta maniera: void classdef::add_name(name* n) {if(n->is_friend()) { if(find(&friends, n)) error("friend redeclared"); else if(find(&members, n)) error("friend redeclared as member"); else friends.append(n); } if(n->is_operator()) operators.append(n); //........ } unde is_operator() si is_friend() sint functii membru ale clasei name. Functia find() ar putea fi scrisa astfel: int find(nlist* ll, name* n) {

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

7.3.4 Tratarea erorilor Exista 4 conceptii la problema in legatura cu ce sa facem cind o facilitate cu scop general, cum ar fi slist intilneste o eroare la executie (in C++, unde nu sint prevazute facilitati specifice ale limbajului pentru tratarea erorilor la executie): [1] Se returneaza o valoare ilegala si se lasa ca utilizatorul sa o verifice; [2] Se returneaza o valoare de stare suplimentara si se lasa ca utilizatorul sa o verifice; [3] Se apeleaza o functie furnizata ca parte a clasei slist; [4] Se apeleaza o functie eroare care se presupune ca o va furniza utilizatorul. Pentru un program mic scris de un singur utilizator, nu exista un motiv pentru a alege o solutie sau alta. Pentru o faci- litate generala solutia este cit se poate de diferita. Prima conceptie, care returneaza o valoare ilegala, nu este fezabila. In general nu exista un mod de a sti ca o valoare particulara este ilegala pentru toti utilizatorii unui slist. Conceptia a doua, care returneaza o valoare stare, poate fi utilizata in unele cazuri (o variatie a acestei scheme se foloseste pentru sirurile standard I/O istream si ostream; asa cum se explica in &8.4.2). Cu toate acestea, ea sufera de probleme serioase, caci

daca o facilitate esueaza des, utilizatorii nu se vor mai obosi sa verifice valoarea starii. Mai mult decit atit, o facilitate poate fi utilizata in sute sau mii de locuri intr-un program. Verificarea starii in fiecare loc ar face programul mult mai greu de citit. Cea de a treia conceptie, care furnizeaza o functie de eroare, nu este flexibila. Nu exista o cale pentru implementatorul unei facilitati de scop general sa stie cum utilizatorii ar dori sa fie tratate erorile. De exemplu, un utilizator ar putea prefera erori scrise in daneza sau romana. Cea de a patra conceptie, lasind ca utilizatorul sa furnizeze o functie eroare, are o anumita atractie cu conditia ca implementatorul sa prezinte clasa ca o biblioteca (&4.5) ce con- tine versiuni implicite pentru functiile de tratare a erorilor. Solutiile 3 si 4 pot fi facute mai flexibile (si esential echivalente) specificind un pointer spre o functie, decit functia insasi. Aceasta permite proiectantului unei facilitati de forma lui slist sa furnizeze o functie eroare implicita, ceea ce face ca programatorilor sa le fie mai simplu decit sa furnizeze fun- ctia lor proprie cind este necesar. De exemplu: typedef void (*PFC)(char*); //pointer spre un tip functie extern PFC slist_handler; extern PFC set_slist_handler(PFC); Functia set_slist_handler() permite utilizatorului sa inlocuiasca prelucrarea implicita. O implementare conventionala furnizeaza o functie implicita de tratare a erorilor care intii scrie un mesaj in cerr, apoi termina programul utilizind exit(): #include "slist.h" #include <stream.h> void default_error(char* s) { cerr << s << "\n"; exit(1); } De asemenea, se declara un pointer la o functie eroare si din motive de notatie o functie pentru setarea lui: PFC slist_handler = default_error; PFC set_slist_handler(PFC handler) { PFC rr = slist_handler; slist_handler = handler; return rr; } Sa observam modul in care set_slist_handler() returneaza slist_handler. Aceasta este convenabil pentru utilizator ca sa seteze si sa reseteze prelucrarile sub forma unei stive. Aceasta poate fi mai util in programe mari in care o slist ar putea fi utilizata in

diferite contexte; fiecare din ele poate apoi furniza rutinele propri de tratare a erorilor. De exemplu: PFC old = set_slist_handler(my_handler); //cod unde my_handler va fi utilizat in caz de eroare in slist set_slist_handler(old); //resetare Pentru a cistiga chiar un control mai bun, slist_handler ar putea fi un membru al clasei slist, permitind astfel ca diferite liste sa aiba diferite tratari de erori simultan. 7.3.5 Clase generice Evident s-ar putea defini liste de alte tipuri (classdef*, int, char*, etc.) in acelasi mod cum a fost definita clasa nlist: prin derivare triviala din clasa slist. Procesul de definire de astfel de tipuri noi este plicticos (si de aceea este inclinat spre erori), dar nu poate fi "mecanizat" prin utilizare de ma- crouri. Din pacate, aceasta poate fi cit se poate de dureros cind se utilizeaza preprocesorul standard C (&4.7 si &r11.1). Macrou- urile rezultate sint, totusi, cit se poate de usor de utilizat. Iata un exemplu in care un slist generic, numit gslist, poate fi furnizat ca un macro. Intii niste instrumente pentru a scrie astfel de macrouri se includ din : #include "slist.h" #ifndef GENERICH #include #endif Sa observam cum #ifndef se utilizeaza pentru a asigura ca nu se include de doua ori in aceeasi compilare. GENERICH se defineste in Numele pentru clasa generica noua se defineste utilizind name2() care este un macro_name de concatenare din : #define gslist(type) name2(type, gslist) #define gslist_iterator(type) name2(type, gslist_iterator) In final, clasa gslist(type) si gslist_iterator(type) pot fi scrise: #define gslistdeclare(type) \ struct gslist(type) : slist{ \ int insert(type a){return slist::insert(ent(a));} \ int append(type a){return slist::append(ent(a));} \ type get(){return type(slist::get());} \ gslist(type)(){} \ gslist(type)(type a):(ent(a)){} \ ~gslist(type)(){clear();} \

};

\ \ struct gslist_iterator(type) : slist_iterator{ \ gslist_iterator(type)(gslist(type)& s):((slist&)s){}\ type operator()() \ {return type(slist_iterator::operator()());} \ }; Un backslash ("\") indica faptul ca linia urmatoare este parte a macroului care se defineste. Utilizind acest macro, o lista de pointeri spre name, asa cum a fost utilizata in prealabil clasa nlist, poate fi definita astfel: #include "name.h" typedef name* Pname; declare(gslist, Pname); //declara clasa gslist(Pname) gslist(Pname) nl; //declara un gslist(Pname) Macroul declare este definit in . El concateneaza argumentele lui si apeleaza macroul cu acel nume, in acest caz gslistdeclare definit mai sus. Un nume argument al lui declare trebuie sa fie un nume simplu. Tehnica de macro_expandare utilizata aici nu poate trata un nume de felul name*; astfel se utilizeaza typedef. Utilizind derivarea se asigura ca toate exemplarele unei clase generice au cod comun. Tehnica poate fi utilizata numai pentru a crea clase de obiecte de aceeasi dimensiune sau mai mica decit clasa de baza utilizata in macro. Aceasta este totusi idea- la pentru liste de pointeri. O gslist este utilizata in &7.6.2. 7.3.6 Interfete restrictive Clasa slist este o clasa cit se poate de generala. Uneori o astfel de generalitate nu este necesara sau nu este de dorit. Forme restrictive cum ar fi stive si cozi sint chiar mai frecvente decit insasi listele generale. Nedeclarind clasa de baza publica, se pot furniza astfel de structuri de date. De exemplu o coada de intregi poate fi definita astfel: #include "slist.h" class iqueue : slist{//presupune sizeof(int)<=sizeof(void*) public: void put(int a){ slist::append((void*)a); } int get(){ return int(slist::get()); } iqueue(){}

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

Adaugarea la o clasa

In exemplele precedente, nu se adauga nimic la clasa de baza prin clasa derivata. Functiile se definesc pentru clasele derivate numai pentru a furniza conversie de tip. Fiecare clasa deri- vata furnizeaza pur si simplu o interfata in loc de o multime de rutine comune. Aceasta este o clasa speciala importanta, dar motivul cel mai frecvent pentru care se defineste o clasa noua ca o clasa derivata este faptul ca se vrea ceea ce furnizeaza clasa de baza, plus inca ceva. Pot fi definite date si functii membre noi pentru o clasa derivata, in plus fata de cele mostenite din clasa ei de baza. Sa observam ca atunci cind un element este pus intr-o slist in pre- alabil definita, se creaza un slink care contine doi pointeri. Aceasta creare ia timp. De un pointer ne putem dispensa, cu con- ditia ca este necesar ca un obiect la un moment dat sa fie numai intr-o lista, asa ca pointerul next poate fi plasat in obiectul insusi (nu intr-un obiect slink separat). Ideea este de a furniza o clasa olink cu numai un cimp next si o clasa olist care poate manipula pointeri la astfel de inlantuiri. Obiectele oricarei clase derivate din olink pot fi manipulate prin olist.

Litera "o" din nume este pentru a ne reaminti ca un obiect poate fi numai intr-o olist la un moment dat: struct olink{ olink* next; }; Clasa olist este similara cu clasa slist. Diferenta este ca un utilizator al clasei olist manipuleaza obiectele clasei olink direct: class olist{ olink* last; public: void insert(olink* p); void append(olink* p); olink* get(); //....... }; Noi putem deriva clasa name din clasa olink: class name : olink{ /* ... */ }; Acum este trivial sa se faca o lista de name care poate fi utilizata fara a aloca spatiu sau timp suplimentar. Obiectele puse in olist isi pierd tipul, adica compilatorul stie ca ele sint olink. Tipul propriu poate fi restabilit folosind conversia explicita de tip a obiectelor luate din olist. De exemplu: void f() { olist ll; name nn; ll.insert(&nn); //tipul lui &nn este pierdut name* pn = (name*)ll.get(); // si se restaureaza } Alternativ, tipul poate fi restabilit derivind o alta clasa din olist care sa trateze conversia de tip: class onlist : olist{ //....... name* get(){return (name*)olist::get();} }; Un nume poate sa fie la un moment dat numai intr-o olist. Aceasta poate fi nepotrivit pentru name, dar nu exista prescurtari ale claselor pentru care sa fie in intregime potrivita. De exemplu, clasa shape din exemplul urmator utilizeaza exact aceasta tehnica pentru ca o lista sa pastreze toate formele. Sa observam ca slist ar putea fi definita ca o clasa derivata din olist, astfel unificind cele doua concepte. Cu toate acestea, utilizarea claselor de baza si derivate la acest nivel microscopic al programarii poate conduce la un cod foarte controlat.

7.5

Liste eterogene

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

Un program complet

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

Intentia a fost sa se scrie controlul ecranului in C (nu in C++) pentru a accentua distinctia intre nivelele implementarii. Aceasta s-a constatat a fi plicticos, asa ca s-a facut un compromis: stilul de utilizare este din C (nu exista functii membru, functii virtuale, operatori definiti de utilizator, etc.), dar se folosesc constructori, se declara si se verifica argumentele functie, etc.. Ca rezultat, controlul ecranului arata foarte mult ca un program in C care a fost modificat ca sa posede avantajele lui C++ fara a fi total rescris. Ecranul este reprezentat ca un tablou de caractere bidimensional, manipulat prin functiile put_point() si put_line() ce utilizeaza structura point cind ne referim la ecran: //fisierul screen.h const XMAX=40, YMAX=24; struct point{ int x, y; point(){} point(int a, int b){ x=a; y=b; } }; overload put_point; extern void put_point(int a, int b); inline void put_point(point p){ put_point(p.x, p.y); } overload put_line; extern void put_line(int, int, int, int); inline void put_line(point a, point b) {put_line(a.x, a.y, b.x, b.y);} extern void screen_init(); extern void screen_refresh(); extern void screen_clear(); #include <stream.h> Inainte de a utiliza o functie put(), ecranul trebuie sa fie initializat prin screen_init(), iar schimbarile ecranului spre structuri de date sint reflectate pe ecran numai dupa apelul lui screen_refresh(). Cititorul va afla ca refresh se face pur si simplu scriind o copie noua a tabloului ecran sub versiunea precedenta. Iata functiile si definitiile de date pentru ecran: #include "screen.h" #include <stream.h> enum color{black='*', white=' '}; char screen[XMAX][YMAX]; void screen_init() {for(int y=0; y
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: .Acesta este un program mai vechi si puteti scrie unul mai bun. Ar trebui safie o clasa task. Un obiect al clasei task ar putea sa fie capabil sa salvezestarea lui si sa aiba de restabilit acea stare (noi ar trebui sa definimtask::save() si task::restore()) asa ca ar trebui sa opereze ca o corutina. Taskuri specifice pot fi definite ca obiecte de clase derivate din clasa task.Programul de executat printr-un task ar putea fi specificat ca o functievirtuala. Ar putea fi posibil sa se paseze argumentele la un task nou caargumente pentru constructorul lui. Ar trebui sa fie un distribuitorimplementat ca un concept de timp virtual. Sa se furnizeze o functie task::delay(long) care "consuma" timp virtual. Daca distribuitorul este o parte a clasei task sau este separat, va fi o decizie majora a proiectarii. Taskurile vor trebui sa comunice. Sa se proiecteze o clasa queue pentru aceasta. Sa se trateze erorile de la executie intr-un mod uniform. Cum se depaneaza programele scrise utilizind o astfel de biblioteca?

MANUAL DE REFERINTA 1 Introducere Limbajul de programare C++ este limbajul C extins cu clase, functii inline, operator de supraincarcare, nume de functie supraincarcat, tipurile constant, referinta, operatorii de gesti- une a memoriei libere, verificarea argumentelor functiei si o sintaxa noua de definire a functiilor. Limbajul C este descris in "The C Programming Language" de Brian W. Kernighan si Dennis M. Richie, Prentice Hall, 1978. Acest manual a fost derivat din sistemul UNIX V "The C Programming Language Reference Manual" cu permisiunea lui AT&T Ball Laboratories. Diferentele dintre C++ si C sint rezumate in &15. Manualul descrie limbajul C++ din iunie 1985. 2 Conventii lexicale Exista sase clase de lexicuri: identificatori, cuvinte cheie, constante, siruri, operatori si alti separatori. Blancuri- le, tabulatori, rindul nou si comentariile (cu un singur cuvint "spatii albe") asa cum se descrie mai jos se ignora exceptind faptul ca ele servesc la a separa lexicuri. Anumite spatii albe se cer pentru a separa printre altele identificatori, cuvinte cheie si constante adiacente. Daca sistemul de intrare a fost descompus in lexicuri pina la un caracter dat, lexicul urmator va include cel mai lung sir de caractere care este posibil sa constituie un lexic. 2.1

Comentarii

Caracterele /* incep un comentariu care se termina cu caracterele */. Comentariile nu se vor imbrica. Caracterele // incep un comentariu care se termina in linia in care a fost inceput. 2.2

Identificatori (Nume)

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

Cuvinte cheie

Identificatorii urmatori sint rezervati pentru a fi utilizati ca si cuvinte cheie si nu pot fi utilizati in alte scopuri: asm auto break case char class const continue default delete do double else enum extern float for friend goto if inline int long new operator overload public register return short sizeof static struct switch this typedef union unsigned virtual void while

2.4

Constante

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

2.4.2 Constante long explicite O constanta intreaga zecimala, octala sau hexazecimala urmata imediat de litera l sau L este o constanta de tip long. 2.4.3 Constante caracter O constanta caracter este un caracter inclus intre caractere apostrof ('x'). Valoarea unei constante caracter este valoarea numerica a caracterului din setul de caractere al calculatorului. Constantele caracter se considera de tip int. Anumite caractere negrafice, apostroful si backslashul pot fi reprezentate potrivit tabelei urmatoare de secvente escape: new_line NL (LF) \n horizontal tab HT \t vertical tab VT \v backspace BS \b carriage return CR \r form feed FF \f backslash \ \\ simple quote ' \' bit pattern 0ddd \ddd bit pattern 0xddd \xddd Secventa escape \ddd consta dintr-un backslash urmat de una, doua sau trei cifre octale care specifica valoarea caracterului dorit. Un caz special al acestei constructii este \0 (care nu este urmat de o cifra), care indica caracterul NULL. Secventa escape \xddd consta din backslash urmat de una, doua sau trei cifre hexazecimale care specifica valoarea caracterului dorit. Daca caracterul care urmeaza dupa backslash nu este unul din cei specificati mai sus, atunci caracterul backslash se ignora. 2.4.4 Constante flotante O constanta flotanta consta dintr-o parte intreaga, un punct zecimal, o parte fractionara, un e sau un E si un exponent intreg cu un semn optional. Partile intregi si fractionare constau fiecare dintr-o secventa de cifre. Partea intreaga sau partea fractionara (dar nu simultan) pot lipsi; punctul zecimal sau e (E) si exponentul (dar nu simultan) pot lipsi. O constanta flotanta are tipul double.

2.4.5 Constante enumerative Numele declarate ca enumeratori (vezi &8.10) sint constante de tip int. 2.4.6 Constante declarate Un obiect (&5) de orice tip poate fi specificat sa aiba o valoare constanta in domeniul numelui lui (&4.1). Pentru pointeri declaratorul &const (&8.3) se utilizeaza pentru a atinge acest fapt; pentru obiecte nepointer se utilizeaza specificatorul const (&8.2). 2.5

Siruri

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

Caracteristici hardware

Tabela de mai jos rezuma anumite proprietati care variaza de la masina la masina. | DEC | Naturale | IBM 370 | AT&T3B | | VAX | 6800 | EBCDIC | ASCII | | ASCII | ASCII | | | |---------------------------------------------------------------| | char | 8 biti | 8 biti | 8 biti | 8 biti | | int | 32 | 16 | 32 | 32 | | short | 16 | 16 | 16 | 16 | | long | 32 | 32 | 32 | 32 | | float | 32 | 32 | 32 | 32 | | double | 64 | 64 | 64 | 64 | | pointer | 32 | 32 | 24 | 32 |

| | +-38 | +-38 | +-76 | +-38 | | float range | +-10 | +-10 | +-10 | +-10 | | | +-38 | +-38 | +-76 | +-308| | double range | +-10 | +-10 | +-10 | +-10 | | field type | signed | unsigned | unsigned | unsigned | | field order | right_to_ | left_to_ | left_to_ | left_to_ | | | left | right | right | right | | char | signed | unsigned | unsigned | unsigned | 3 Notatia sintactica In notatia sintactica utilizata in acest manual categoriile sintactice se indica prin italice, iar cuvintele literale si caracterele din constante in forma reala. Variantele se listeaza pe linii separate. Un simbol terminal optional sau neoptional se indica prin indicele "opt" asa ca: {expresie opt} indica o expresie optionala inclusa intre paranteze. Sintaxa este rezumata in &14. 4 Nume si Tipuri Un nume denota un obiect, o functie, un tip, o valoare sau o eticheta. Un nume se introduce intr-un program printr-o declaratie (&8). Un nume poate fi utilizat numai intr-o regiune a textului programului numit domeniul lui. Un obiect este o regiune de memorie care determina existenta lui. Intelesul valorilor gasite intr-un obiect se determina prin tipul numelui utilizat pentru a-l accesa. 4.1

Domenii

Exista trei feluri de domenii: local, fisier si clasa. Local: In general, un nume declarat intr-un bloc(&9.2) este local la acel bloc si poate fi folosit in el numai dupa punctul de declaratie si in blocurile incluse in el. Cu toate acestea, etichetele (&9.12) pot fi utilizate oriunde in functia in care ele sint declarate. Numele argumentelor formale pentru o functie se trateaza ca si cind ar fi fost declarate in blocul cel mai extern al acelei functii. Fisier: Un nume declarat in afara oricarui bloc (&9.2) sau clasa (&8.5) poate fi utilizat in fisierul in care a fost declarat dupa punctul in care a fost declarat. Clasa: Numele unui membru al clasei este local la clasa lui si poate fi utilizat numai intr-o functie membru al acelei clase (&8.5.2), dupa un operator aplicat la un obiect din clasa lui (&7.1) sau dupa operatorul -> aplicat la un pointer spre un obiect din clasa lui (&7.1). Membri clasei statice (&8.5.1) si

functiile membru pot fi referite de asemenea acolo unde numele clasei lor este in domeniu utilizind operatorul :: (&7.1). O clasa declarata intr-o clasa (&8.5.15) nu se considera un membru si numele ei l apartine la domeniul care o include. Un nume poate fi ascuns printr-o declaratie explicita a aceluiasi nume intr-un bloc sau clasa. Un nume intr-un bloc sau clasa poate fi ascuns numai printr-un nume declarat intr-un bloc sau clasa inclusa. Un nume nelocal ascuns poate insa sa fie utilizat cind domeniul lui se specifica folosind operatorul :: (&7.1). Un nume de clasa ascuns printr-un nume non-tip poate fi insa utilizat daca este prefixat prin class, struct sau union (&8.2). Un nume enum ascuns printr-un nume non-tip poate insa sa fie utilizat daca este prefixat de enum (&8.2). 4.2

Definitii

O declaratie (&8) este o definitie daca nu este o declaratie de functie (&10) si ea contine specificatorul extern si nu are initializator sau corpul functiei sau este declaratia unui nume de clasa (&8.8). 4.3

Linkare

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

Clase de memorie

Exista doua clase de memorie: clase automatice si clase statice. Obiectele automatice sint locale. Ele apar la fiecare apel al unui bloc si se elimina la iesirea din el. Obiectele statice exista si isi mentin valorile pe timpul executiei intregului program.

Anumite obiecte nu sint asociate cu nume si viata lor se controleaza explicit utilizind operatorii new si delete; vezi &7.2 si &9.14. 4.5

Tipuri fundamentale

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

Tipuri derivate

Exista un numar conceptual infinit de tipuri derivate construite din tipurile fundamentale: tablouri de obiecte de un tip dat; functii care au argumente de tipuri date si returneaza obiecte de un tip dat; pointeri spre obiecte de un tip dat; referinte la obiecte de un tip dat; constante care sint valori de un tip dat; clase ce contin o secventa de obiecte de tipuri diferite, un set de functii pentru manipularea acestor obiecte si un set de restrictii asupra accesului la aceste obiecte si functii;

structuri care sint clase fara restrictii la acces; reuniuni care sint structuri capabile sa contina obiecte de tipuri diferite in momente diferite. In general aceste metode de a construi obiecte pot fi aplicate recursiv. Un obiect de tip void* (pointer spre void) poate fi utilizat pentru a pointa spre obiecte de tip necunoscut. 5 Obiecte si Lvalori Un obiect este o regiune de memorie; o lvaloare este o expresie care se refera la un obiect. Un exemplu evident de expresie lvaloare este numele unui obiect. Exista operatori care produc lvalori: de exemplu, daca E este o expresie de tip poin- ter, atunci *E este o expresie lvaloare care se refera la obiectul spre care pointeaza E. Numele lvaloare vine de la expresia de atribuire E1 = E2 in care operatorul sting trebuie sa fie o lvaloare. Discutia de mai jos despre operatori indica despre fiecare daca el asteapta un operand lvaloare si daca el produce o lvaloare. 6 Conversii Un numar de operatori pot, depinzind de operanzii lor, sa provoace conversia valorii unui operand de un tip la altul. Aceasta sectiune explica rezultatul asteptat de la o astfel de conversie. Paragraful &6.6 rezuma conversiile cerute de cei mai obisnuiti operatori; ea va fi suplimentata la nevoie printr-o discutie despre fiecare operator. Paragraful &8.5.6 descrie conversiile definite de utilizator. 6.1

Caractere si Intregi

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

Flotante in simpla si dubla precizie

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

Flotante si Intregi

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

Pointeri si Intregi

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

Intregi fara semn

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

Conversii aritmetice

Un numar mare de operatori conduc la conversii si produc rezultate de un tip similar cu tipurile descrise mai sus. Aceste conversii vor fi numite "conversii aritmetice uzuale". orice operanzi de tip char, unsigned char sau short se convertesc spre int.

daca unul din operanzi este double, atunci si celalalt se converteste spre double si acesta este tipul rezultatului. altfel daca unul din operanzi este unsigned long atunci si celalalt se converteste spre unsigned long si acesta este tipul rezultatului. altfel, daca unul dintre operanzi este long, celalalt este convertit spre long si acesta este tipul rezultatului. altfel, daca unul din operanzi este unsigned, celalalt se converteste spre unsigned si acesta este tipul rezultatu lui. altfel, ambii operanzi trebuie sa fie int si acesta este tipul rezultatului. 6.7

Conversii de pointeri

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

Conversii de referinte

Conversia urmatoare poate fi facuta ori de cite ori se initializeaza referintele. o referinta la o clasa poate fi convertita spre o referin ta spre o clasa de baza publica a acelei clase; vezi &8.6.3. 7 Expresii Precedenta operatorilor unei expresii este aceeasi cu ordinea subsectiunilor majore ale acestei subsectiuni, intii fiind precedenta cea mai inalta. Astfel, de exemplu, expresiile care se refera la operanzii lui + (&7.4) sint acele expresii care sint definite in &7.1-&7.4. Operatorii din fiecare subsectiune au aceeasi precedenta. Asociativitatea stinga sau dreapta este specificata in fiecare subsectiune pentru operatorii discutati in ea. Precedenta si asociativitatea tuturor operatorilor unei expresii este rezumata in gramatica din &14. Altfel ordinea evaluarii expresiilor este nedefinita. In particular compilatorul considera ca el este liber sa calculeze subexpresiile in ordinea in care el considera ca acest lucru este cel mai eficient, chiar daca subexpresiile implica efecte secundare.

Ordinea in care au loc efectele secundare nu este specificata. Expresiile care implica un operator comutativ si unul asociativ (*, +, &, |, ^) pot fi rearanjate arbitrar, chiar si in prezenta parantezelor; pentru a forta o ordine particulara a evaluarii trebuie sa se utilizeze un temporar explicit. Tratarea depasirilor si verificarea impartirii intr-o evaluare de expresie este dependenta de masina. Majoritatea imple mentarilor existente ale lui C++ ignora depasirile de intregi, tratarea impartirii la zero si toate exceptiile flotante difera de la masina la masina si de obicei se ajusteaza printr-o functie de biblioteca. In plus fata de intelesul standard descris in &7.2-&7.15 operatorii pot fi supraincarcati, adica li se dau sensuri cind se aplica la tipuri definite de utilizator; vezi &7.16. 7.1

Expresii primare

Expresiile primare implica . -> :: indexare si apel de functie grupindu-se de la stinga la dreapta. expression_list: expression expression_list, expression id: identifier operator_function_name typedef_name :: identifier typedef_name :: operator_function_name primary_expression: id :: identifier constant string this (expression) primary_expression[expression] primary_expression(expression_list opt) primary_expression.id primary_expression->id Un identificator este o expresie primara, cu conditia ca el sa aiba o declaratie potrivita (&8). Un operator_function_name este un identificator cu un inteles special; vezi &7.16 si &8.5.11.

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

expresie trebuie sa fie un obiect al unei clase, iar identificatorul trebuie sa numeasca un membru al acelei clase. Valoarea este membrul numit al obiectului si el este o lvaloare daca prima expresie este o lvaloare. Sa observam ca "obiectele unei clase" pot fi structuri (&8.5.12) sau reuniuni (&8.5.13). O expresie primara urmata de o sageata (->) si de un identificator (sau un identificator calificat printr-un typedef_name utilizind operatorul ::) este o expresie. Prima expresie trebuie sa fie un pointer la un obiect al unei clase iar identificatorul trebuie sa numeasca un membru al acelei clase. Rezultatul este o lvaloare care referentiaza membrul numit al clasei spre care pointeaza expresia pointer. Astfel expresia E1->MOS este acelasi lucru cu (*E1).MOS. Clasele se discuta in &8.5. Daca o expresie are tipul "referinta la ..." (vezi &8.4 si &8.6.3) valoarea expresiei este obiectul notat prin referinta. O referinta poate fi gindita ca un nume al unui obiect (&8.6.3). 7.2

Operatori unari

Expresiile cu operatori unari se grupeaza de la dreapta la stinga. unary_expression: unary_operator expression expression++ expression-sizeof expression sizeof(type_name) (type_name) expression simple_type_name (expression_list) new type_name initializer_opt new (type_name) delete expression delete [expression] expression unary_operator: unul dintre * & + - ! ~ ++ -Operatorul unar * inseamna indirectare: expresia trebuie sa fie un pointer, iar rezultatul este o lvaloare care se refera la un obiect spre care pointeaza expresia. Daca tipul expresiei este "pointer spre ..." tipul rezultatului este "... ".

Rezultatul operatorului unar & este un pointer spre obiectul referit de operand. Operandul trebuie sa fie o lvaloare. Daca ti- pul expresiei este "...", atunci tipul rezultatului este "pointer spre ...". Rezultatul operatorului unar + este valoarea operandului sau valoarea rezultata in urma conversiilor aritmetice obisnuite. Operandul trebuie sa fie de tip aritmetic. Rezultatul operatorului unar - este negativarea operandului sau. Operandul trebuie sa fie de tip aritmetic. Se fac conversii aritmetice obisnuite. Negativarea unei cantitati fara semn se calculeaza scazind valoarea ei din 2^n unde n este numarul de biti dintr-un int. Rezultatul operatorului de negatie logica ! este 1 daca valoarea operandului sau este 0, si este 0 daca valoarea operandului sau este diferita de zero. Tipul operandului este int. Se aplica la orice tip aritmetic sau la pointeri. Operatorul ~ produce complementul fata de 1 al operandului sau. Se fac conversii aritmetice uzuale. Tipul operandului trebuie sa fie intreg. 7.2.1 Incrementare si Decrementare Operandul prefixului ++ este incrementat. Operandul trebuie sa fie o lvaloare. Expresia ++x este echivalenta cu x += 1. Vezi discutiile despre adunare (&7.4) si operatorii de asignare (&7.14) pentru informatii despre conversii. Operandul prefixat prin -- se decrementeaza in mod analog ca si in cazul operatorului prefix ++. Valoarea obtinuta prin aplicarea unui operator ++ postfix este valoarea operandului. Operandul trebuie sa fie o lvaloare. Dupa ce este luat in evidenta rezultatul; obiectul este incrementat in aceeasi maniera ca si operatorul prefix ++. Tipul rezultatului este acelasi ca si tipul operandului. Valoarea obtinuta prin aplicarea unui operator -postfix este valoarea operandului. Operandul trebuie sa fie o lvaloare. Dupa ce rezultatul este luat in evidenta, obiectul este decremen- tat in aceeasi maniera ca si pentru operatorul prefix --. Tipul rezultatului este acelasi cu al operandului. 7.2.2 Sizeof Operatorul sizeof produce dimensiunea in octeti a operandului sau. (Un octet este nedefinit prin limbaj cu exceptia terme- nilor valorii lui sizeof. Cu toate acestea, in toate implementarile existente un octet este spatiul cerut pentru a pastra un caracter.) Cind se aplica la un tablou, rezultatul este numarul total de octeti din tablou. Dimensiunea se determina din declaratiile obiectelor dintr-o expresie. Expresia este semantic o constanta fara semn si poate fi utilizata oriunde se cere o constanta. Operatorul sizeof se poate aplica de asemenea la un nume de tip in paranteze. In acest caz el produce dimensiunea in octeti a unui obiect de tip indicat.

7.2.3 Conversia explicita de tip Un simple_type_name inclus optional in paranteze (&8.2) urmat de o expresie in paranteze (sau o lista de expresii daca ti- pul este o clasa cu un constructor declarat in mod corespunzator &8.5.5) face ca valoarea expresiei sa fie convertita spre tipul denumit. Pentru a exprima conversia spre un tip care nu are un nume simplu, trebuie inclus in paranteze numele tipului (&8.7). Daca numele tipului este in paranteze, expresia nu este necesar sa fie in paranteze. Aceasta constructie se numeste cast. Un pointer poate fi convertit explicit spre orice tip de intregi care sint destul de mari pentru a le pastra. Ori de cite ori se cere un int sau long, acest lucru este dependent de masina. Functia de transformare este de asemenea dependenta de masina, dar se intentioneaza ca aceasta sa nu fie o surpriza pentru cei care cunosc structura de adresare a masinii. Detalii despre anumite masini particulare se dau in &2.6. Un obiect de tip intreg se poate converti in mod explicit spre un pointer. Transformarea totdeauna face ca un intreg convertit dintr-un pointer sa revina inapoi la acelasi pointer, dar printre altele ea este dependenta de masina. Un pointer spre un tip poate fi convertit explicit spre un pointer spre un alt tip. Pointerul rezultat poate provoca ex ceptii de adresare la utilizare daca pointerul care se converteste nu se refera la un obiect aliniat in memorie in mod cores- punzator. Se garanteaza ca un pointer spre un obiect de o dimensiune data poate fi convertit spre un pointer spre un obiect cu o dimensiune mai mica si din nou inapoi fara a face schimbari. Diferite masini pot diferi in numarul de biti in pointeri si in cerintele de aliniere pentru obiecte. Agregatele se aliniaza la limita cea mai stricta ceruta de oricare din componentele lor. Un obiect poate fi convertit spre o clasa de obiecte numai daca a fost declarat un constructor potrivit sau un operator de conversie (&8.5.6). Un obiect poate fi convertit explicit spre o referinta de tip X& daca un pointer spre acel obiect poate fi convertit explicit spre un X*. 7.2.4 Memoria libera Operatorul new creaza un obiect de tipul type_name (vezi &8.7) la care se aplica el. Durata de viata a unui obiect creat prin new nu este restrinsa la domeniul in care se creaza. Operatorul new returneaza un pointer la obiectul pe care il creaza. Cind acel obiect este un tablou se returneaza un pointer la primul sau element. De exemplu, atit new int, cit si new int[10] returneaza un int*. Se poate furniza un initializator pentru anumite obiecte de clasa (&8.6.2). Pentru a obtine memorie operatorul new (&7.2) va apela functia: void* operator new(long);

Argumentul specifica numarul de octeti cerut. Memoria va fi neinitializata. Daca operatorul new() nu poate gasi cantitatea de memorie ceruta, atunci el va returna valoarea zero. Operatorul delete va distruge un obiect creat prin operatorul new. Rezultatul este void. Operandul lui delete trebuie sa fie un pointer returnat de new. Efectul aplicarii lui delete la un pointer care nu este obtinut prin operatorul new este nedefinit. Cu toate acestea, stergind un pointer cu valoarea zero este inofensiv. Pentru a elibera memorie operatorul delete va apela functia: void operator delete(void*); In forma: delete [expression] expression cea de a doua expresie pointeaza spre un vector iar prima expresie da numarul de elemente al acelui vector. Specificarea numarului de elemente este redondant exceptind cazul cind se sterg vectori de anumite clase (vezi &8.5.8).

7.3

Operatori multiplicatori

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

Operatori aditivi

Operatorii aditivi + si - se grupeaza de la stinga la dreapta. Se fac conversii aritmetice obisnuite. Exista unele posibilitati de tip suplimentare pentru fiecare operator. aditive_expression:

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

Operatori de deplasare

Operatorii de deplasare << si >> se grupeaza de la stinga la dreapta. Ambii realizeaza conversii aritmetice obisnuite asupra operanzilor lor, fiecare din ei trebuind sa fie de tip intreg. Apoi operandul drept se converteste spre int. Tipul rezultatului este cel al operandului sting. Rezultatul este nedefinit daca operandul drept este negativ sau este mai mare sau egal decit lungimea obiectelor in biti. shift_expression: expression << expression expression >> expression Valoarea lui E1 << E2 este E1 (interpretat ca o configuratie de biti) deplasat la stinga cu E2 biti; bitii liberi se completeaza cu zero. Deplasarea la dreapta este garantata a fi o deplasare logica (se completeaza cu 0) daca E1 este fara semn; altfel ea este aritmetica (se copiaza bitul de semn).

7.6

Operatori relationali

Operatorii de relatie se grupeaza de la stinga la dreapta, dar acest fapt nu este foarte util; a < b < c nu inseamna ceea ce s-ar parea. relational_expression: expression < expression expression > expression expression <= expression expression >= expression Operatorii < (mai mic decit), > (mai mare decit), <= (mai mic sau egal cu) si >= (mai mare sau egal cu) produc zero daca relatia specificata este falsa si unu daca este adevarata. Tipul rezultatului este int. Se fac conversii aritmetice obisnuite. Doi pointeri pot fi comparati; rezultatul depinde de locatiile relative din spatiul de adrese al obiectelor pointate. Comparatia de pointeri este portabila numai cind pointerii pointeaza spre obiecte din acelasi tablou. 7.7

Operatori de egalitate

equality_expression: expression == expression expression != expression Operatorii == (egal) si != (diferit) sint analogi cu operatorii de relatie exceptind precedenta mai mica a lor. (Astfel a
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 cauta filename numai in locurile specificate standard si nu si in directorul fisierului sursa (cum se specifica locurile standard nu este parte a limbajului). Directivele #include pot fi imbricate.

11.3 Compilarea conditionata O linie de control a compilatorului de forma: #if expression verifica daca expresia este diferita de zero. Expresia trebuie sa fie o expresie constanta (&12). In plus fata de operatiile obisnuite din C++ se poate utiliza un identificator unar. Acesta cind se aplica la un identificator, atunci valoarea lui este diferita de zero daca identificatorul respectiv a fost definit utilizind #define si nu s-a utilizat ulterior pentru el #undef; altfel valoarea lui este zero. O linie de control de forma: #ifdef identifier verifica daca identifier este definit curent in preprocesor, adica daca el a fost obiectul unei linii de control #define. O linie de control de forma: #ifndef identifier verifica daca identifier este nedefinit curent in preprocesor. Toate cele trei forme sint urmate de un numar arbitrar de linii, care pot contine si o linie de control: #else si apoi de linia de control: #endif Daca conditia verificata este adevarata, atunci liniile dintre #else si #endif sint ignorate. Daca conditia verificata este falsa, atunci toate liniile dintre cea de test si #else sau, in lipsa lui #else, #endif sint ignorate. Aceste constructii pot fi imbricate. 11.4 Linie de control In beneficiul altor preprocesoare care genereaza programe C++, o linie de forma: #linie constant "filename" face ca, compilatorul sa considere ca numarul de linie al liniei sursa urmatoare se da printr-o constanta, iar fisierul de intrare curent este denumit prin identificator. Daca identificatorul lipseste, numele fisierului nu se schimba. Aceasta se face pentru a elimina erorile. 12 Expresii constante In diferite locuri C++ cere expresii care se evalueaza ca o constanta: cum ar fi limitele unui tablou (&8.4), expresiile case (&9.7) argumentele implicite ale functiilor (&8.4) si initializatorii (&8.6). In ultimul caz expresia poate implica numai

constante intregi, constante caracter, constante enumerative, valori const care nu sint agregate initializate cu expresii constante si expresii sizeof care pot fi legate prin operatorii binari: + - * / % & | ^ << >> == != < > <= >= && || sau cei unari: +

- ~

!

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

13 Consideratii de portabilitate Anumite parti ale lui C++ sint inerent dependente de masina. Urmarind lista necazurilor potentiale, bulinele nu inseamna ca vor apare toate "necazurile", dar se sublinieaza unele dintre principalele necazuri. Caracteristicile curate hardware cum este marimea unui cuvint, proprietatile aritmeticii flotante si impartirea intreaga in practica au dovedit ca acestea nu constituie prea mult o problema. Alte aspecte ale hardware-ului sint reflectate in diferite implementari. Unele dintre acestea, in particular extensia de semn (care converteste un caracter negativ intr-un intreg negativ) si ordinea in care octetii sint plasati in cuvint este o pacoste care trebuie privita cu multa grija. Majoritatea necazurilor celorlalte reprezinta doar probleme minore. Numarul variabilelor registru care pot fi in realitate plasate in registrii variaza de la masina la masina, asa cum este de fapt si cu setul tipurilor valide. Nu mai putin,

toate compilatoarele fac lucruri proprii masinii pentru care el a fost construit; declaratiile de variabile registru incorecte sau in exces sint ignorate. Ordinea evaluarii argumentelor functiilor nu este specificata de catre limbaj. Aceasta ordine este de la dreapta la stinga pentru unele masini si de la stinga la dreapta pentru altele. De cind constantele caracter sint obiecte reale ale tipului int, sint permise si constantele caracter multi-caracter. Implementarea specifica este foarte dependenta de masina deoarece ca- racterele sint asignate de la stinga la dreapta pentru unele masini si de la dreapta la stinga pentru altele. 14 Sumar de sintaxa Acest sumar al sintaxei C++ se intentioneaza sa fie un ajutor pentru intelegerea limbajului. Ceea ce se prezinta nu sint instructiuni exacte ale limbajului. 14.1 Expresii expression: term expression binary_operator expression expression ? expression : expression expression_list expression_list: expression expression_list, expression term: primary_expression unary_operator term term++ term-sizeof expression sizeof (type_name) (type_name) expression simple_type_name (expression_list) new type_name initializer_opt new (type_name) delete expression delete [expression] expression

special_operator: () [] free_store_operator: one of new delete abstract_declarator: empty *abstract_declarator abstract_declarator (argument_declaration_list) [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 #line constant "filename" #undef identifier

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

O declaratie in interiorul unui bloc este o instructiune 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


Related Documents

Limbajul C
November 2019 13
Limbajul C
June 2020 16
Programare Visual C++
June 2020 10
Programare
May 2020 9