Professional Documents
Culture Documents
BJARNE STROUSTRUP
ADDISON-WESLEY
PUBLISHING COMPANY 1986
PREFATA
===========
Structura cartii
----------------
Observatii de proiectare
------------------------
Note istorice
-------------
Observatii filozofice
---------------------
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
------
[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].
CUPRINS
=======
NUME
PAG.
=========================================================
========
NUME
PAG.
=========================================================
========
2.3.10. Referinte
36
2.3.11. Registrii
39
2.4. Constante
40
2.4.1. Constante intregi
40
2.4.2. Constante in flotanta
41
2.4.3. Constante caracter
41
2.4.4. Siruri
42
2.4.5. Zero
43
2.4.6. Const
43
2.4.7. Enumerari
45
2.5. Salvarea spatiului
46
2.5.1. Cimpuri
46
2.5.2. Reuniuni
47
2.6. Exercitii
49
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
NUME
PAG.
=========================================================
========
6.11. Goluri
167
6.12. Exercitii
168
NUME
PAG.
=========================================================
========
8.5. Manipularea sirurilor
220
8.6. Blocare in bufer
221
8.7. Eficienta
223
8.8. Exercitii
224
MANUAL DE REFERINTA
227
1. Introducere
227
2. Conventii lexicale
227
2.1. Comentarii
227
2.2. Identificatori (Nume)
227
2.3. Cuvinte cheie
228
2.4. Constante
228
2.4.1. Constante intregi
228
2.4.2. Constante long explicite
228
2.4.3. Constante caracter
229
2.4.4. Constante flotante
229
2.4.5. Constante enumerative
229
2.4.6. Constante declarate
229
2.5. Siruri
230
2.6. Caracteristici hardware
230
3. Notatia sintactica
230
4. Nume si Tipuri
231
4.1. Domenii
231
4.2. Definitii
232
4.3. Linkare
232
4.4. Clase de memorie
232
4.5. Tipuri fundamentale
232
4.6. Tipuri derivate
233
5. Obiecte si Lvalori
233
6. Conversii
234
6.1. Caractere si Intregi
234
6.2. Flotante in simpla si dubla pre-
cizie
234
6.3. Flotante si Intregi
234
6.4. Pointeri si Intregi
235
6.5. Intregi fara semn
235
6.6. Conversii aritmetice
235
6.7. Conversii de pointeri
236
6.8. Conversie de referinta
236
7. Expresii
236
7.1. Expresii primare
237
7.2. Operatori unari
239
7.2.1. Incrementare si Decrementare
239
7.2.2. Sizeof
240
7.2.3. Conversie explicita de tip
240
7.2.4. Memoria libera
241
7.3. Operatori multiplicatori
242
7.4. Operatori aditivi
242
7.5. Operatori de deplasare
243
7.6. Operatori relationali
243
7.7. Operatori de egalitate
244
7.8. Operatorul SI pe biti
244
7.9. Operatorul SAU-EXCLUSIV pe biti
244
NUME
PAG.
=========================================================
========
7.10. Operatorul SAU-INCLUSIV pe biti
244
7.11. Operatorul logic SI
244
7.12. Operatorul logic SAU
245
7.13. Operator conditional
245
7.14. Operatori de asignare
245
7.15. Operatorul virgula
246
7.16. Operatori de supraincarcare
246
7.16.1. Operatori unari
247
7.16.2. Operatori binari
247
7.16.3. Operatori speciali
247
8. Declaratii
247
8.1. Specificatori de clasa de memorie
248
8.2. Specificatori de tip
249
8.3. Declaratori
250
8.4. Intelesul ( sensul ) declaratorilor
251
8.4.1. Exemple
253
8.4.2. Tablouri, Pointeri si Indici
254
8.5. Declaratii de clasa
255
8.5.1. Membri statici
256
8.5.2. Functii membru
257
8.5.3. Clase derivate
258
8.5.4. Functii virtuale
259
8.5.5. Constructori
259
8.5.6. Conversii
260
8.5.7. Destructori
261
8.5.8. Memoria libera
261
8.5.9. Vizibilitatea numelor membri
262
8.5.10. Prieteni
263
8.5.11. Functii operator
264
8.5.12. Structuri
264
8.5.13. Reuniuni
264
8.5.14. Cimpuri de biti
265
8.5.15. Clase imbricate
265
8.6. Initializare
266
8.6.1. Liste initializatoare
266
8.6.2. Obiecte de clasa
267
8.6.3. Referinte
268
8.6.4. Tablouri de caractere
269
8.7. Nume de tip
269
8.8. Typedef
270
8.9. Nume de functii supraincarcate
271
8.10. Declaratii de enumerare
272
8.11. Declaratia ASM
273
9. Instructiuni
273
9.1. Instructiunea expresie
273
9.2. Instructiunea compusa (blocul)
273
9.3. Instructiunea conditionala
274
9.4. Instructiunea WHILE
274
9.5. Instructiunea DO
274
9.6. Instructiunea FOR
274
9.7. Instructiunea SWITCH
275
9.8. Instructiunea BREAK
276
9.9. Instructiunea CONTINUE
276
NUME
PAG.
=========================================================
========
9.10. Instructiunea RETURN
276
9.11. Instructiunea GOTO
277
9.12. Instructiunea etichetata
277
9.13. Instructiunea NULL
277
9.14. Instructiunea declarativa
277
10. Definitii de functii
278
11. Linii de control ale compilatorului
279
11.1. Substitutia de siruri
280
11.2. Incluziune de fisiere
280
11.3. Compilarea conditionata
281
11.4. Linie de control
281
12. Expresii constante
282
13. Consideratii de portabilitate
282
14. Sumar de sintaxa
283
14.1. Expresii
283
14.2. Declaratii
284
14.3. Instructiuni
286
14.4. Definitii externe
286
14.5. Preprocesor
287
15. Diferente fata de C
287
15.1. Extensii
287
15.2. Sumar de incompatibilitati
288
15.3. Anacronisme
288
CAPITOLUL 1
===========
1.1 Introducere
-----------
1.1.1 Iesire
------
#include <stream.h>
main()
{
cout << "Hello, world\n";
}
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
$
char* p;
char *const q;
char v[10];
char c;
//......
p = &c; // p pointeaza spre c
1.4 Expresii si Instructiuni
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;
}
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"
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++);
}
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;
}
1.9 Referinte
Ultima versiune a lui ostream din nefericire contine o eroare serioasa. Problema este
ca ostream este copiat de doua ori pentru fiecare utilizare a lui <<: odata ca un
argument si odata ca valoare returnata. Aceasta lasa starea nemodificata dupa fiecare
apel. Este nevoie de o facilitate pentru a pasa un pointer la ostream mai degraba decit
sa se paseze insasi ostream.
Aceasta se poate realiza utilizind referintele. O referinta actioneaza ca un nume
pentru un obiect; T& inseamna referinta la T. O referinta trebuie initializata si devine
un nume alternativa pentru obiectul cu care este initializat. De exemplu:
ostream& s1 = my_out; ostream& s2 = cout;
Referintele s1 si my_out pot fi utilizate acum in acelasi mod si cu acelasi inteles. De
exemplu, atribuirea:
s1 = s2;
copiaza obiectul referit prin s2 (adica cout) in obiectul referit prin s1 (adica my_out).
Membri se selecteaza utilizind operatorul punct:
s1.put("don't use ->");
si daca utilizam operatorul adresa, primim adresa obiectului referit:
&s1 == &my_out
Prima utilizare evidenta a referintei este ca sa ne asiguram ca adresa unui obiect, mai
degraba decit obiectul insusi, este pasata la o functie de iesire (aceasta se numeste in
anumite limbaje apel prin referinta):
ostream& operator<<(ostream& s, complex z)
{
return s << "(" << z.real << "," << z.imag << ")";
}
Corpul functiei este neschimbat dar asignarea facuta lui s va afecta acum obiectul dat
ca argument. In acest caz, returnindu-se o referinta de asemenea se imbunatateste
eficienta, intru- cit modul evident de implementare a unei referinte este un pointer si
un pointer este mai ieftin sa fie transferat decit o structura mare.
Referintele sint de asemenea esentiale pentru definirea sirurilor de intrare deoarece
operatorului input i se da variabila in care se citeste ca operand. Daca referintele nu
sint utilizate, utilizatorul ar trebui sa paseze pointeri expliciti functiilor de intrare:
class istream{ //........
int state; public:
istream& operator>>(char&); istream& operator>>(char*); istream&
operator>>(int&); istream& operator>>(long&);
//........
};
Sa observam ca se folosesc doua operatii separate pentru a citi intr-o zona long si
intr-o zona int si numai una pentru scriere. Motivul este ca un int poate fi convertit
spre long prin regulile implicite de conversie.
1.10 Constructori
Definirea lui ostream ca si clasa, face ca datele membru sa fie private. Numai o
functie membru poate accesa membri privati, asa ca noi trebuie sa furnizam una
pentru initializare. O astfel de functie se numeste constructor si se distinge avind
acelasi nume ca si al clasei lui:
class ostream{ //.......
ostream(streambuf*); ostream(int size, char* s); };
Aici se furnizeaza doi constructori. Unul ia un streambuf pentru o iesire reala iar
celalalt ia o dimensiune si un pointer spre caractere pentru formatarea sirului. Intr-o
declaratie, argumentul lista necesar pentru un constructor se adauga la nume. Noi
putem declara acum streamuri astfel:
ostream my_out(&some_stream_buffer); char xx[256]; ostream xx_stream(256,xx);
Declaratia lui my_out seteaza nu numai cantitatea corespunzatoare de memorie ci de
asemenea apeleaza si constructorul ostream::ostream(streambuf*) pentru a-l
initializa cu argumentul &some_stream_buffer, care este un pointer spre un obiect
potrivit al clasei streambuf. Declaratia functiei xx_stream() se trateaza similar, dar
utilizeaza celalalt constructor. Declarind constructori pentru o clasa nu furnizam
numai un mod de a initializa obiecte, ci de asemenea se asigura ca toate obiectele
clasei vor fi initializate. Cind s-a declarat un constructor pentru o clasa, nu este
posibil sa se declare o variabila a acelei clase fara a apela un constructor. Daca o
clasa are un constructor care nu ia argumente, acel constructor va fi apelat daca nu se
da nici un argument in declaratie.
1.11 Vectori
#include <stream.h>
void error(char* p)
{
cerr << p << "\n"; // cerr is the error output stream
exit(1);
}
void vector::set_size(int)
{
/* dummy */
}
int& vec::operator[](int i)
{
if(i<low || high<i)
error("vec index out of range"); return elem(i);
}
main()
{
vector a(10);
for(int i=0; i<a.size(); i++)
{
a[i] = i;
cout << a[i] << " ";
}
cout << "\n";
vec b(10, 19);
for(i=0; i<b.size(); i++)
b[i+10] = a[i]; for(i=0; i<b.size(); i++)
cout << b[i+10] << " "; cout << "\n";
}
Acesta produce:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9
Noi am dori, de exemplu, unul din acei vectori pentru tipul matrice pe care l-am
definit. Din nefericire, C++ nu furnizeaza o facilitate pentru a defini o clasa vector cu
tipul elementelor ca argument. Un mod de a proceda ar fi sa se copieze atit definitia
clasei cit si functiile membru. Acest lucru nu este ideal, dar adesea este acceptabil.
Noi putem utiliza macroprocesor pentru a mecaniza acel task. De exemplu, clasa
vector este o versiune simplificata a unei clase care poate fi gasita intr-un fisier
header standard. Noi am putea scrie:
#include <vector.h>
declare(vector, int);
main()
{
vector (int)vv(10);
vv[2] = 3;
vv[10] = 4; //eroare de rang
}
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.
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
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.
main()
{
while(a < 4)
f();
}
produce iesirea:
a=1 b=1 c=1
a=2 b=1 c=2
a=3 b=1 c=3
O variabila statica care nu este explicit initializata este initializata cu zero (&2.4.5).
Utilizind operatorii new si delete, programatorul poate crea obiecte a caror durata de
viata poate fi controlata direct (&3.2.4).
2.2 Nume
Un nume (identificator) consta dintr-un sir de litere si cifre. Primul caracter trebuie sa
fie litera. Caracterul subliniere _ se considera a fi o litera. C++ nu impune limite
asupra numarului de caractere dintr-un nume, dar anumite implementari nu sint sub
controlul scriitorului de compilatoare (in particular, incarcatorul). Anumite medii de
executie sint de asemenea necesare pentru a extinde sau restringe setul de caractere
acceptat intr-un identificator; extensiile (de exemplu, cele care admit caracterul $
intr-un nume) produc programe neportabile. Un cuvint cheie C++ (vezi &r.2.3) nu
poate fi utilizat ca un nume. Exemple de nume:
hello this_is_a_most_unusually_long_name
DEFINED fo0 bAr u_name HorseSence
var0 var1 CLASS _class ___
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";
}
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;
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.
Din tipurile fundamentale (si din tipurile definite de utilizator) se pot deriva alte
tipuri folosind operatorii de declaratie:
pointer
& adresa
[] vector
() functie
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);
}
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
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
defineste un tip nou numit address care consta din elementele de care avem nevoie
pentru a trimite o scrisoare la cineva (address nu este in general destul pentru a
gestiona toate scrisorile, dar este suficient pentru un exemplu). Sa observam punct-
virgula de la sfirsit este unul din foarte putinele locuri din C++ unde este necesar sa
o avem dupa o acolada inchisa, asa ca lumea este inclinata sa o uite.
Variabilele de tip adresa pot fi declarate exact ca si alte variabile, iar elementele
individuale pot fi accesate utilizind operatorul '.'(punct). De exemplu:
address jd;
jd.name = "Jim Dandy";
jd.number = 61;
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;
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
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'
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};
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;
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;
};
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
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.1 Analizorul
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();
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:
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.
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.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).
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)
//.........
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
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).
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 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;
}
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;
}
3.3.2 Goto
Are putine utilizari in limbajele de nivel inalt, dar poate fi foarte util cind un program
C++ este generat printr-un program in loc ca programul sa fie scris direct de catre o
persoana; de exemplu, goto-urile pot fi utilizate intr-un analizor generat dintr-o
gramatica printr-un generator de analizoare.
Goto poate fi, de asemenea, important in acele cazuri cind eficienta optimala este
esentiala, de exemplu, in ciclul interior al unei aplicatii de timp real.
Una din putinele utilizari bune ale lui goto este iesirea dintr-un ciclu imbricat sau
switch (instructiunea break intrerupe numai ciclul sau switch-ul cel mai interior care
o contine). De exemplu:
for(int i=0; i<n; i++)
for(int j=0; j<m; j++)
if(nm[i][j] == a)
goto found;
// not found
//...........
found:
// nm[i][j] == a;
Exista de asemenea instructiunea continue, care transfera controlul la sfirsitul
instructiunii ciclice, asa cum s-a explicat in &3.1.5.
3.4 Comentarii si Decalari
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
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>.
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?
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).
Cea mai simpla solutie la problema partitionarii unui program in diferite fisiere este
de a pune definitiile de functii si date intr-un numar potrivit de fisiere sursa si de a
declara tipurile necesare pentru a comunica, intr-un singur fisier antet care este inclus
de toate celelalte fisiere. Pentru programul calculator putem folosi fisiere.c : lex.c,
sgn.c, table.c, main.c si un fisier antet dc.h, care contine declaratiile fiecarui nume
utilizat in Mai mult decit un fisier.c:
//dc.h declaratii comune pentru programul calculator
#include <stream.h>
enum token_value
{
NAME, NUMBER, END, PLUS = '+', MINUS = '-', MUL = '*',
DIV = '/', PRINT = ';', ASSIGN = '=', LP = '(', RP = ')'
};
extern int no_of_errors;
extern double error(char* s);
extern token_value get_token();
extern token_value curr_tok;
extern double number_value;
extern char name_string[256];
extern double expr();
extern double term();
extern double prim();
struct name{
char* string;
name* next;
double value;
};
extern name* look(char* p, int ins = 0);
inline name* insert(char* s){return look(s, 1);}
Codul real al lui lex.c va arata astfel:
//lex.c : analiza de intrare si analiza lexicala
#include "dc.h"
#include <ctype.h>
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }
Sa observam ca, utilizind fisierele antet in acest fel se asigura ca fiecare declaratie a
unui obiect definit de utilizator intr-un fisier antet va fi intr-un anumit punct inclus
fisierul in care el este definit. De exemplu, cind compilam lex.c, compilatorul va
intilni:
extern token_value get_token();
// ...
token_value get_token() { /* ... */ }
Aceasta asigura ca, compilatorul va detecta orice inconsistenta in tipurile specificate
pentru un nume. De exemplu, daca get_token() a fost declarat sa returneze o valoare
de tip token_value, dar este definit sa returneze un int, atunci compilarea lui lex.c va
esua, cu eroare de neconcordanta de tip.
Fisierul sgn.c va arata astfel:
//sgn.c : analiza sintactica si evolutiva
#include "dc.h"
double prim() { /* ... */ }
double term() { /* ... */ }
double expr() { /* ... */ }
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) { /* ... */ }
Aceasta interfata cu analizorul lexical este cit se poate de incurcata. Lipsa unui tip
propriu de lexic arata necesitatea de a prezenta utilizatorului pe get_token() cu
bufferele de lexicuri reale number_value si name_string.
//lex.c : definitiile pentru intrare si analiza lexicala
#include <stream.h>
#include <ctype.h>
#include "error.h"
#include "lex.h"
token_value curr_tok;
double number_value;
char name_string[256];
token_value get_token() { /* ... */ }
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.
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;
}
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
}
Cu alte cuvinte, un argument de tip T[] va fi convertit spre T* cind este transferat.
Rezulta ca o asignare la un element al argumentului vector schimba valoarea
elementului argumentului respectiv. Cu alte cuvinte, vectorii difera de alte tipuri prin
aceea ca vectorul nu este pasat prin valoare (si nici nu poate fi pasat prin valoare).
Dimensiunea unui vector nu este disponibila in functia apelata. Aceasta poate fi o
pacoste, dar exista dife- rite moduri de tratare a acestei probleme. Sirurile se termina
prin zero, asa ca dimensiunea lor se poate calcula usor. Pentru alte tipuri de vectori se
poate transfera un al doilea argument care contine dimensiunea sau un tip care
contine un pointer si un indicator de lungime in locul vectorului (&11.11). De
exemplu:
void compute1(int* vec_ptr, int vec_size); //un mod
struct vec{ //un alt mod
int* ptr; int size;
};
void compute2(vec v);
Tablourile multidimensionale sint mai ciudate, dar adesea pot fi utilizati vectori de
pointeri in locul lor si nu au nevoie de o tratare speciala. De exemplu:
char* day[] = {"mon","tue","wed","thu","fri","sat","sun"};
Cu toate acestea consideram definirea unei functii care manipuleaza o matrice
bidimensionala. Daca dimensiunile sint cunoscute la compilare, nu exista nici o
problema:
void print_m34(int m[3][4])
{
for(int i=0; i<3; i++)
{
for(int j=0; j<4; j++)
cout << " " << m[i][j]; cout << "\n";
}
}
Cazul dificil apare cind trebuie pasate ambele dimensiuni. "Solutia evidenta" pur si
simplu nu functioneaza:
void print_mij(int m[][], int dim1, int dim2) //eroare
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << m[i][j]; //surpriza
cout << "\n";
}
}
In primul rind, argumentul m[][] este ilegal deoarece trebuie sa fie cunoscuta
dimensiunea a doua a tabloului pentru a gasi locatia unui element.
In al doilea rind, expresia m[i][j] este corect interpretata ca *(*(m+i)+j), dar aceasta
este improbabil ca este ce a dorit programatorul. O solutie corecta este:
void print_mij(int** m, int dim1, int dim2)
{
for(int i=0; i<dim1; i++)
{
for(int j=0; j<dim2; j++)
cout << " " << ((int*)m)[i*dim2+j]; //obscur
cout << "\n";
}
}
Expresia utilizata pentru a face acces la elementele tabloului este echivalenta cu cea
generata de compilator cind cunoaste ultima dimensiune. Se poate introduce o
variabila auxiliara pentru a face codul mai putin obscur:
int* v = (int*)m; v[i*dim2+j];
4.6.6 Argumente implicite
O functie necesita adesea mai multe argumente in general, decit este nevoie in cazul
cel mai simplu sau in cazul cel mai frecvent. De exemplu, biblioteca stream are o
functie hex() care produce un sir ce contine reprezentarea hexazecimala a unui intreg.
Un al doilea intreg se foloseste pentru a specifica numarul de caractere disponibile
pentru reprezentarea primului argument. Daca numarul de caractere este prea mic
pentru a reprezenta intregul, apare trunchierea; daca este prea mare, sirul este
completat cu spatii. Adesea, programatorul nu se intereseaza despre numarul de
caractere necesare pentru a reprezenta intregul atita timp cit exista spatiu suficient,
asa ca argumentul al doilea este 0 pentru a indica faptul ca la conversie sa se utilizeze
"exact atitea caractere cite sint necesare". Pentru a elimina apelurile de forma hex(i,
0), functia se declara astfel:
extern char* hex(long, int = 0);
Initializarea pentru cel de al doilea parametru inseamna ca acesta este un parametru
implicit. Adica, daca numai un argument este prezent intr-un apel, cel de al doilea
este utilizat impli- cit. De exemplu:
cout << "**" << hex(31) << hex(32, 3) << "**";
se interpreteaza astfel:
cout << "**" << hex(31, 0) << hex(32, 3) << "**";
si va imprima:
**1f 20**
Un argument implicit se verifica din punct de vedere al tipului in momentul
declararii functiei si este evaluat in momentul apelului. Este posibil sa se furnizeze
argumente implicite numai pentru argumente din ultimele pozitii, asa ca:
int f(int, int = 0, char* = 0); //ok
int g(int = 0, int = 0, char*); //error
int h(int = 0, int, char* = 0); //error
Sa observam ca in acest caz spatiul dintre * si = este semnificativ (*= este operatorul
de asignare):
int nasty(char *= 0); //syntax error
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:
Exista numai doua lucruri care pot fi facute cu o functie: apelul ei si sa se ia adresa
ei. Pointerul obtinut functiei poate fi apoi utilizat pentru a apela functia. De exemplu:
void error(char* p){/*...*/} void (*efct)(char*); //pointer spre functie void f()
{efct = &error; //efct pointeaza spre error
(*efct)("error"); //apelul lui error prin efct
}
Pentru a apela o functie printr-un pointer (de exemplu efct) intii trebuie sa i se
atribuie pointerului adresa functiei res- pective. Intrucit operatorul () de apel de
functie are prioritate mai mare decit operatorul *, nu se poate scrie apelul prin
*efct("error") caci aceasta inseamna *(efct("error")), ceea ce este o eroare de tip.
Acelasi lucru se aplica la sintaxa declaratiei (vezi de asemenea &7.3.4).
Sa observam ca pointerii spre functii au tipurile argumentelor declarate ca si functiile
insasi. In asignarea de pointeri, tipul functiei trebuie sa corespunda exact. De
exemplu:
void (*pf)(char*); //pointer spre void(char*);
void f1(char*); //void(char*);
int f2(char*); //int(char*);
void f3(int*); //void(int*);
void f()
{
pf = &f1; //ok
pf = &f2; //eroare: tipul valorii returnate
// este eronat
pf = &f3; //eroare: argument de tip eronat
(*pf)("asdf"); //ok
(*pf)(1); //eroare: tip de argument eronat
int i = (*pf)("qwer"); //eroare: void se asigneaza la int
}
Regulile pentru pasarea argumentelor sint aceleasi atit pentru apelurile directe la o
functie cit si pentru apelurile la o functie printr-un parametru. Adesea este convenabil
sa se defineasca un nume pentru tipul unui pointer spre o functie pentru a elimina
utilizarea tot timpul a unei sintaxe neevidente. De exemplu:
typedef int (*SIG_TYP)(); //din <signal.h> typedef void (*SIG_ARG_TYP)();
SIG_TYP signal(int, SIG_ARG_TYP);
Adesea este util un vector de pointeri spre functii. De exemplu, sistemul de meniuri
pentru editorul bazat pe "mouse" se implementeaza utilizind vectori de pointeri spre
functii ce reprezinta operatii. Sistemul nu poate fi descris aici in detaliu dar ideea
generala este aceasta:
typedef void (*PF)();
PF edit_ops[]={cut, paste, snarf, search}; //op. de editare
PF file_ops[]={open, reshape, close, write};//tratarea fis.
Definirea si initializarea pointerilor care definesc actiunile selectate dintr-un meniu
asociat cu butoanele mouse-ului:
PF* button2 = edit_ops;
PF* button3 = file_ops;
Intr-o implementare completa, este necesara mai multa informatie pentru a defini
fiecare element. De exemplu, un sir care specifica textul de afisat trebuie sa fie
pastrat undeva. Pe masura ce se utilizeaza sistemul, sensul butoanelor mouse se
schimba frecvent cu contextul. Astfel de schimbari se realizeaza (partial) schimbind
valoarea pointerilor de butoane. Cind un utilizator selecteaza un meniu, cum ar fi
elementul 3 pentru butonul 2, se executa operatia asociata: (*button2[3])();
Un mod de a cistiga o apreciere a puterii expresive a pointerilor spree functii este
incercarea de a scrie cod fara ele. Un meniu poate fi modificat la executie inserind
functii noi intr-o tabela operator. Este de asemenea usor sa se construiasca meniuri
noi la executie.
Pointerii spre functii pot fi utilizati sa furnizeze rutine care pot fi aplicate la obiecte
de tipuri diferite:
typedef int (*CFT)(char*, char*); int sort(char* base, unsigned n, int sz, CFT cmp)
/* Sorteaza cele n elemente ale vectorului "base" in ordine crescatoare utilizind
functia de comparare spre care pointeaza "cmp". Elementele sint de dimensiune
"sz".
Algoritm foarte ineficient: bubble sort.
*/
{
for(int i = 0; i < n-1; i++)
for(int j = n-1; i < j; j--)
{
char* pj = base+j*sz; //b[j]
char* pj1 = pj-sz; //b[j-1]
if((*cmp)(pj, pj1) < 0) //swap b[j] and b[j-1]
for(int k = 0; k < sz; k++)
{
char temp = pj[k];
pj[k] = pj1[k];
pj1[k] = temp;
}
}
}
Rutina de sortare nu cunoaste tipul obiectelor pe care le sorteaza, ci numai numarul
de elemente (dimensiunea vectorului), dimensiunea fiecarui element si functia de
apelat pentru a face compararea. Tipul lui sort() ales este acelasi cu tipul rutinei
qsort() din biblioteca C standard. Programele reale utilizeaza qsort(). Intrucit sort()
nu returneaza o valoare, ar trebui declarata cu void, dar tipul void nu a fost introdus
in C cind a fost definit qsort(). Analog, ar fi mai onest sa se foloseasca void* in loc
de char* ca tip de argument. O astfel de functie sort() ar putea fi utilizata pentru a
sorta o tabela de forma:
struct user{char* name;
char* id; int dept;
};
typedef user* Puser;
user heads[]={"McIlroy M.D.", "doug", 11271,
"Aho A.V.", "ava", 11272,
"Weinberger P.J.", "pjw", 11273,
"Schryer N.L.", "nls", 11274,
"Schryer N.L.", "nls", 11275,
"Kernighan B.W.", "bwk", 11276
};
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:
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
#define PI = 3.141593;
#define MAX(a, b) a > B ? a : b
#define fac(a) (a) * fac((a) - 1)
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".
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.
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
}
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
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).
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; }
};
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();
//......
}
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 implementare a lui table::look() utilizind o cautare liniara prin lista inlantuita
de nume din tabela:
#include <string.h> name* table::look(char* p, int ins) {for(name* n = tbl; n; n = n-
>next)
if(strcmp(p, n->string) == 0)
return n; if(ins == 0)
error("name not found"); name* nn = new name; nn->string = new char[strlen(p) +
1]; strcpy(nn->string, p); nn->value = 1; nn->next = tbl; tbl = nn; return nn;
}
Acum consideram o inlantuire a clasei utilizind cautarea prin hashing asa cum s-a
facut in exemplul cu calculatorul de birou. Este insa mai dificil sa facem acest lucru
din cauza restrictiei ca, codul scris folosind versiunea de clasa table de mai jos, sa nu
se schimbe.
class table{name** tbl;
int size; public:
table(int sz=15);
~table(); name* look(char*, int=0);
name* insert(char* s){return look(s, 1);}
};
Structura datelor si constructorul s-au schimbat pentru a reflecta nevoia pentru o
dimensiune specifica a tabelei cind se utilizeaza hashingul. Prevazind constructorul
cu un argument implicit ne asiguram ca, codul vechi care nu a specificat dimen-
siunea unei tabele este inca corect. Argumentele implicite sint foarte utile in situatii
cind vrem sa schimbam o clasa fara a afecta codul vechi. Constructorul si
destructorul acum gestioneaza crearea si stergerea tabelelor de hashing:
table::table(int sz)
{
if(sz < 0)
error("negative table size"); tbl = new name*[size=sz]; for(int i=0; i < sz; i++)
tbl[i] = 0;
}
table::~table()
{for(int i=0; i < size; i++)
{name* nx;
for(name* n=tbl[i]; n; n=nx)
{
nx = n->next;
delete n->string;
delete n;
}
}
delete tbl;
}
O versiune mai simpla si mai clara a lui table::~table() se poate obtine declarind un
destructor pentru class name. Functia lookup este aproape identica cu cea utilizata in
exemplul cu calculatorul de birou (&3.1.3):
name* table::look(char* p, int ins)
{
int ii = 0;
char* pp = p;
while(*pp)
ii == ii << 1 ^ *pp++; if(ii < 0)
ii = -ii; ii %= size; for(name* n = tbl[ii]; n; n = n->next)
if(strcmp(p, n->string) == 0)
return n; if(ins == 0)
error("name not found"); name* nn = new name; nn->string = new char[strlen(p) +
1]; strcpy(nn->string, p); nn->value = 1; nn->next = tbl[ii]; tbl[ii] = nn; return nn;
}
Evident, functiile membru ale unei clase trebuie sa fie recompilate ori de cite ori se
face o schimbare in declaratia de clasa. Ideal, o astfel de schimbare nu ar trebui sa
afecteze de loc utilizatorii unei clase. Din nefericire, nu este asa. Pentru a aloca o
variabila de clasa, compilatorul are nevoie sa cunoasca dimensiunea unui obiect al
clasei. Daca dimensiunea unui astfel de obiect este schimbata, fisierele care contin
utilizari ale clasei trebuie sa fie recompilate. Softwarul care determina setul minim de
fisiere ce necesita sa fie recompilate dupa o schimbare a declaratiei de clasa poate fi
(si a fost) scris, dar nu este inca utilizat pe scara larga. Noi ne putem intreba, de ce nu
a fost proiectat C++ in asa fel ca recompilarea utilizatorilor unei clase sa fie necesara
dupa o schimbare in partea privata? Si de ce trebuie sa fie prezenta partea privata in
declaratia de clasa? Cu alte cuvinte, intrucit utilizatorii unei clase nu sint admisi sa
aiba acces la membri privati, de ce declaratiile lor trebuie sa fie prezente in fisierele
antet ale utilizatorului? Raspunsul este eficienta. Pe multe sisteme, atit procesul de
compilare cit si secventa de operatii care implementeaza apelul unei functii sint mai
simple cind dimensiunea obiectelor automatice (obiecte pe stiva) se cunoaste la
compilare. Aceasta problema ar putea fi eliminata reprezentind fiecare obiect al
clasei ca un pointer spre obiectul "real". Intrucit toti acesti pointeri ar avea aceeasi
dimensiune, iar alocarea obiectelor "reale" ar putea fi definita intr-un fisier unde este
disponibila partea privata, acest fapt ar putea rezolva problema. Cu toate acestea,
aceasta solutie impune referirea la o memorie suplimentara cind se face acces la
membri unei clase si mai rau ar implica cel putin un apel al alocatorului si
dealocatorului de memorie pentru fiecare apel de functie cu un obiect automatic al
clasei. De asemenea s-ar face implementarea unei functii membru inline care sa faca
acces la date private fezabile. Mai mult decit atit, o astfel de schimbare ar face
imposibila linkarea impreuna a fragmentelor de programe C++ si C (deoarece un
compilator C ar trata diferit o structura fata de un compilator C++). Aceasta este
nepotrivit in C++.
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++];}
};
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.
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*);
};
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); }
};
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;}
};
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: ...
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
}
}
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
}
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
}
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
}
Fie:
main()
{
table* p = new table(100);
table* q = new table(200);
delete p;
delete p; //probabil o eroare
}
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;
//...........
}
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.
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
}
Cind se utilizeaza multe obiecte mici alocate in memoria libera, noi putem sa aflam
ca programul consuma timp considerabil pentru alocare si dealocare de astfel de
obiecte. O solutie este de a furniza un alocator cu scopuri generale mai bun si o a
doua este ca proiectarea unei clase sa nu se faca pentru a fi gestionata in memoria
libera, definind constructori si destructori.
Sa consideram clasa name folosita in exemplul table. Ea ar putea fi definita astfel:
struct name{char* string;
name* next; double value; name(char*, double, name*);
~name();
};
Programatorul poate avea avantaje din faptul ca alocarea si dealocarea obiectelor
unui tip poate fi facuta pe departe mai eficient (in timp si spatiu) decit cu o
implementare generala prin new si delete. Ideea generala este de a prealoca "felii" de
obiecte de tip name si de a le lega intre ele, reducind alocarea si dealocarea la
operatii simple asupra listelor inlantuite. Variabila nfree este antetul unei liste de
nume neutilizate. const NALL = 128; name* nfree;
Alocatorul utilizat prin operatorul new pastreaza dimensiunea unui obiect impreuna
cu obiectul pentru ca operatorul delete sa functioneze corect. Aceste spatii
suplimentare se elimina simplu la un alocator specific unui tip. De exemplu,
alocatorul urmator utilizeaza 16 octeti pentru a memora un name la masina mea, in
timp ce alocatorul general foloseste 20. Iata cum se poate face aceasta:
name::name(char* s, double v, name* n)
{register name* p = nfree //prima alocare
if(p)
nfree = p->next;
else
{name* q = (name*)new char[NALL * sizeof(name)];
for(p = nfree = &q[NALL-1]; q<p; p--)
p->next = p-1;
(p+1)->next = 0;
}
this = p; string = s; //initializare value = v; next = n;
}
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
};
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
CAPITOLUL 6
OPERATOR SUPRAINCARCAT
6.1 Introducere
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=
/= << >> >>= <<= == != <= >= &&
|| ++ -- [] () 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
}
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.
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).
Totusi, scrierea unei functii pentru fiecare combinatie dintre complex si double ca si
pentru operator*() de mai sus, este o tendinta de nesuportat. Mai mult decit atit, o
facilitate realista pentru aritmetica complexa trebuie sa furnizeze cel putin o duzina
de astfel de functii; vezi de exemplu, tipul complex asa cum este el declarat in
<complex.h>.
6.3.1 Constructori
iar operatiile care implica variabilele complexe si constantele intregi vor fi legale. O
constanta intreaga va fi interpretata ca un complex cu partea imaginara zero. De
exemplu, a=b*2 inseamna:
a = operator*(b, complex(double(2), double(0)))
O conversie definita de utilizator se aplica implicit numai daca ea este unica
(&6.3.3).
Un obiect construit prin utilizarea implicita sau explicita a unui constructor este
automatic si va fi distrus la prima ocazie; de obicei imediat dupa instructiunea care l-
a creat.
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);
};
6.4 Constante
Nu este posibil sa se defineasca constante de tip clasa in sensul ca 1.2 si 12e3 sint
constante de tip double. Totusi constantele de tip predefinit pot fi utilizate daca in
schimb, functiile membru ale unei clase se utilizeaza ca sa furnizeze o interpretare
pentru ele. Constructorii care au un singur argument furnizeaza un mecanism general
pentru acest lucru. Cind constructorii sint simpli si se substituie inline, este cit se
poate de rezonabil sa interpretam apelurile constructorului ca si constante. De
exemplu, dindu-se declaratia de clasa complex in <complex.h>, expresia
zz1*3+zz2*complex(1,2) va apela doua functii si nu cinci. Cele doua operatii * vor
apela functii, dar operatia + si constructorul apelat pentru a crea complex(3) si
complex(1,2) vor fi expandate inline.
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&);
};
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.
Un sir este o data structurata care consta dintr-un pointer spre un vector de caractere
si din dimensiunea acelui vector. Vectorul este creat printr-un constructor si sters
printr-un destructor. Cu toate acestea, asa cum se arata in &5.10 aceasta poate sa
creeze probleme. De exemplu:
void f()
{
string s1(10);
string s2(20);
s1=s2;
}
va aloca doi vectori de caractere, dar asignarea s1=s2 va distruge pointerul spre unul
din ei si va duplica pe celalalt. Destructorul va fi apelat pentru s1 si s2 la iesirea din
f() si atunci va sterge acelasi vector de doua ori cu rezultate dezastruoase. Solutia la
aceasta problema este de a defini asignarea de obiecte in mod corespunzator.
struct string{
char* p; int size; //vectorul spre care pointeaza p string(int sz){ p = new
char[size=sz]; } ~string(){ delete p; } void operator=(string&);
};
void string::operator=(string& a)
{
if(this == &a)
return; //a se avea grija de s=s; delete p; p = new char[size=a.size]; strcpy(p,
a.p);
}
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);
}
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
Intrucit reprezentarea unui assoc este ascunsa, noi avem nevoie de o cale de al afisa.
Sectiunea urmatoare va arata cum poate fi definit un iterator propriu. Aici noi vom
utiliza o functie simpla de imprimare:
void assoc::print_all()
{for(int i=0; i<free; i++)
cout << vec[i].name << ":" << vec[i].val << "\n";
}
In final putem scrie programul principal:
main() //numara aparitiile fiecarui cuvint de la intrare
{const MAX = 256; //mai mare decit cel mai mare cuvint
char buff[MAX]; assoc vec(512); while(cin>>buf)
vec[buff]++; vec.print_all();
}
Iata o versiune mai realista a clasei sir. Ea calculeaza referintele la un sir pentru a
minimiza copierea si utilizeaza sirurile de caractere standard din C++ ca si constante.
#include <iostream.h>
#include <string.h>
#include <process.h>
class string{
struct srep{
char* s; //pointer spre data
int n; //numarul de referinte
};
srep* p;
public:
string(char*); //string x = "abc";
string(); //string x;
string(string&); //string x = string...
string& operator=(char*);
string& operator=(string&);
~string();
char& operator[](int i); friend ostream& operator<<(ostream&, string&); friend
istream& operator<<(istream&, string&); friend int operator==(string& x, char* s)
{return strcmp(x.p->s, s) == 0;} friend int operator==(string& x, string& y)
{return strcmp(x.p->s, y.p->s) == 0;} friend int operator!=(string& x, char* s)
{return strcmp(x.p->s, s) != 0;} friend int operator!=(string& x, string& y)
{return strcmp(x.p->s, y.p->s) != 0;}
};
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;
}
}
char& string::operator[](int i)
{
if(i<0 || strlen(p->s)<i)
error("index out of range"); return p->s[i];
}
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
main()
{
i+10;
y+10;
y+10*y;
x+y+i;
x*x+i;
f(7);
f(y);
y+y;
106+y;
}
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.1 Derivare
Consideram construirea unui program care se ocupa cu angajatii unei firme. Un astfel
de program ar putea avea o structura de felul:
struct employee{
char* name; short age; short departament; int salary; employee* next;
//.......
};
Cimpul next este o legatura intr-o lista pentru date employee similare. Acum vrem sa
definim structura manager:
struct manager{
employee emp; //angajatii manager employee* group;
//...
};
Un manager este de asemenea un angajat (employee); datele angajatului se
memoreaza in emp care este un membru al obiectului manager. Aceasta poate fi
evident pentru un cititor uman, dar nu exista nimic care sa distinga membri emp. Un
pointer spre un ma- nager (manager*) nu este un pointer spre un employee
(employee*), asa ca nu se pot utiliza unul in locul celuilalt. In particular, nu se poate
pune un manager intr-o lista de angajati fara a scrie cod special. Se poate sau utiliza
tipul de conversie explicit spre manager* sau sa se puna adresa membrului emp intr-
o lista de angajati, dar ambele sint neelegante si pot fi obscure. Conceptia corecta
este de a afirma ca un manager este un employee cu citeva informatii adaugate:
struct manager : employee{employee* group;
//.......
};
Manager este derivat din employee si invers, employee este o clasa de baza pentru
manager. Clasa manager are membri clasei employee (name, age, etc.) in plus fata
de membrul group.
Cu aceasta definitie a lui employee si manager, noi putem crea acum o lista de
employee, din care unii sint manageri. De exemplu:
void f()
{
manager m1, m2;
employee e1, e2;
employee* elist;
elist = &m1; //se pune m1, e1, m2, e2 in lista
m1.next = &e1; e1.next = &m2; m2.next = &e2; e2.next = 0;
}
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
}
7.2.3 Vizibilitate
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.
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.
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();
};
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.
Pentru a utiliza clase derivate mai mult decit o prescurtare convenabila in declaratii,
trebuie sa se rezolve problema urma- toare: dindu-se un pointer de tip base*, la care
tip derivat apartine in realitate obiectul pointat? Exista trei solutii fundamentale la
aceasta problema:
[1] Asigurarea ca sint pointate numai obiecte de un singur tip (&7.3.3);
[2] Plasarea unui cimp de tip in clasa de baza pentru a fi consultat de functii;
[3] Sa se utilizeze functii virtuale (&7.2.8).
Pointerii la clasa de baza se utilizeaza frecvent in proiectarea de clase container, cum
ar fi multimea, vectorul si lista. In acest caz, solutia 1 produce liste omogene; adica
liste de obiecte de acelasi tip. Solutiile 2 si 3 pot fi utilizate pentru a construi liste
eterogene; adica liste de pointeri spre obiecte de tipuri diferite. Solutia 3 este o
varianta speciala de tip sigur al solutiei 2. Sa examinam intii solutia simpla de
cimpuri_tip, adica solutia 2. Exemplul manager/employee va fi redefinit astfel:
enum empl_type {M, E};
struct employee{
empl_type type;
employee* next;
char* name;
short departament;
//.......
};
struct manager : employee{
employee* group; short level;
//........
};
Dindu-se aceasta noi putem scrie acum o functie care imprima informatie despre
fiecare employee:
void print_employee(employee* e)
{
switch(e->type)
{
case E: cout<<e->name<<"\t"<<e->departament<<"\n";
//........
break;
case M: cout<<e->name<<"\t"<<e->departament<<"\n";
//........
manager* p = (manager*)e; cout<<"level"<<p->level<<"\n";
//........
break;
}
}
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.
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";
}
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.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;
}
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;
}
};
Evident s-ar putea defini liste de alte tipuri (classdef*, int, char*, etc.) in acelasi mod
cum a fost definita clasa nlist: prin derivare triviala din clasa slist. Procesul de
definire de astfel de tipuri noi este plicticos (si de aceea este inclinat spre erori), dar
nu poate fi "mecanizat" prin utilizare de ma- crouri. Din pacate, aceasta poate fi cit se
poate de dureros cind se utilizeaza preprocesorul standard C (&4.7 si &r11.1).
Macrou- urile rezultate sint, totusi, cit se poate de usor de utilizat.
Iata un exemplu in care un slist generic, numit gslist, poate fi furnizat ca un macro.
Intii niste instrumente pentru a scrie astfel de macrouri se includ din <generic.h>:
#include "slist.h"
#ifndef GENERICH
#include <generic.h>
#endif
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)
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(); }
};
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).
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.
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
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).
*************
* *
* *
* *
* ** ** *
* * *
* *
* ***** *
* *
Daca noi utilizam clasa slist, am putea gasi ca programul nostru utilizeaza timp
considerabil pentru alocare si dealocare de obiecte ale clasei slink. Clasa slink este un
prim exemplu de clasa care ar putea beneficia de faptul ca programatorul sa aiba
control asupra memoriei libere. Tehnica optimizata descrisa in &5.5.6 este ideala
pentru acest tip de obiect. Intrucit orice slink se creaza folosind new si se distruge
folosind delete de catre membri clasei slist, nu exista probleme cu alte metode de
alocare de memorie.
Daca o clasa derivata asigneaza la this constructorul pentru clasa ei de baza va fi
apelat numai dupa ce s-a facut asignarea, iar valoarea lui this in constructorul clasei
de baza va fi cea atribuita prin constructorul clasei derivate. Daca clasa de baza
asigneaza la this, valoarea asignata va fi cea utilizata de constructor pentru clasa
derivata. De exemplu:
#include <stream.h>
struct base{ base(); };
struct derived : base{ derived(); }; base::base()
{
cout << "\tbase 1: this=" << int(this) << "\n"; if(this == 0)
this = (base*)27; cout << "\tbase 2: this=" << int(this) << "\n";
}
derived::derived()
{
cout << "\tderived 1: this=" << int(this) << "\n"; if(this == 0)
this = (derived*)43; cout << "\tderived 2: this=" << int(this) << "\n";
}
main()
{
cout << "base b;\n";
base b;
cout << "new base;\n";
new base;
cout << "derived d;\n";
derived d;
cout << "new derived;\n";
new derived;
cout << "at the end\n";
}
produce iesirea:
base b;
base 1: this=2147478307
base 2: this=2147478307
new base;
base 1: this=0
base 2: this=27
derived d;
derived 1: this=2147478306
derived 2: this=2147478306
new derived;
derived 1: this=0
base 1: this=43
base 2: this=43
derived 2: this=43
at the end
Daca un destructor pentru o clasa derivata asigneaza la this, atunci valoarea asignata
este cea vazuta de destructor pentru clasa lui de baza. Cind cineva asigneaza la this
un constructor este important ca o atribuire la this sa se faca pe ori- ce cale a
constructorului. Din nefericire, este usor sa se uite o astfel de atribuire. De exemplu,
la prima editare a acestei carti cea de a doua linie a constructorului derived::derived()
era:
if(this==0)
this=(derived*)43;
In consecinta, constructorul clasei de baza base::base() nu a fost apelat pentru d.
Programul a fost legal si s-a executat corect, dar evident nu a facut ce a intentionat
autorul.
7.8 Exercitii
1. (*1). Se defineste:
class base{
public:
virtual void ian(){ cout << "base\n"; }
};
Sa se deriveze doua clase din base si pentru fiecare definitie a lui ian() sa se scrie
numele clasei. Sa se creeze obiecte ale acestei clase si sa se apeleze ian() pentru ele.
Sa se asigneze adresa obiectelor claselor derivate la pointeri de tip base* si sa se
apeleze ian() prin acesti pointeri.
2. (*2). Sa se implementeze primitivele screen (&7.6.1) intr-un mod rezonabil
pentru sistemul d-voastra.
3. (*2). Sa se defineasca o clasa triunghi si o clasa cerc.
4. (*2). Sa se defineasca o functie care deseneaza o linie ce leaga doua figuri
gasind "punctele de contact" cele mai apropiate si le conecteaza.
5. (*2). Sa se modifice exemplul shape asa ca line sa fie derivata din rectangle
si invers.
6. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi utilizata fara iterator.
7. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi folosita numai printr-un iterator. Iteratorular trebui sa aiba operatii pentru
parcurgeri inainte sau inapoi, operatiipentru a insera si sterge elemente in lista si un
mod de a face acces laelementul curent.
8. (*2). Sa se implementeze o versiune generica a unei liste dublu inlantuite.
9. (*4). Sa se implementeze o lista in care obiectele (si nu numai pointerii spre
obiecte) se insereaza si se extrag. Sa se faca sa functioneze pentru o clasa X unde
X::X(X&), X::~X() si X::operator=(X&) sint definite.
10. (*5). Sa se proiecteze si sa se implementeze o bibliote8ca pentru a scrie
simulari de drivere de evenimente. Indicatie: <task.h>.Acesta este un program mai
vechi si puteti scrie unul mai bun. Ar trebui safie o clasa task. Un obiect al clasei task
ar putea sa fie capabil sa salvezestarea lui si sa aiba de restabilit acea stare (noi ar
trebui sa definimtask::save() si task::restore()) asa ca ar trebui sa opereze ca o
corutina.
Taskuri specifice pot fi definite ca obiecte de clase derivate din clasa task.Programul
de executat printr-un task ar putea fi specificat ca o functievirtuala. Ar putea fi
posibil sa se paseze argumentele la un task nou caargumente pentru constructorul lui.
Ar trebui sa fie un distribuitorimplementat ca un concept de timp virtual. Sa se
furnizeze o functie task::delay(long) care "consuma" timp virtual. Daca distribuitorul
este o parte a clasei task sau este separat, va fi o decizie majora a proiectarii.
Taskurile vor trebui sa comunice. Sa se proiecteze o clasa queue pentru aceasta. Sa se
trateze erorile de la executie intr-un mod uniform. Cum se depaneaza programele
scrise utilizind o astfel de biblioteca?
MANUAL DE REFERINTA
1 Introducere
Limbajul de programare C++ este limbajul C extins cu clase, functii inline, operator
de supraincarcare, nume de functie supraincarcat, tipurile constant, referinta,
operatorii de gesti- une a memoriei libere, verificarea argumentelor functiei si o
sintaxa noua de definire a functiilor. Limbajul C este descris in "The C Programming
Language" de Brian W. Kernighan si Dennis M. Richie, Prentice Hall, 1978. Acest
manual a fost derivat din sistemul UNIX V "The C Programming Language -
Reference Manual" cu permisiunea lui AT&T Ball Laboratories. Diferentele dintre
C++ si C sint rezumate in &15. Manualul descrie limbajul C++ din iunie 1985.
2 Conventii lexicale
Exista sase clase de lexicuri: identificatori, cuvinte cheie, constante, siruri, operatori
si alti separatori. Blancuri- le, tabulatori, rindul nou si comentariile (cu un singur
cuvint "spatii albe") asa cum se descrie mai jos se ignora exceptind faptul ca ele
servesc la a separa lexicuri. Anumite spatii albe se cer pentru a separa printre altele
identificatori, cuvinte cheie si constante adiacente.
Daca sistemul de intrare a fost descompus in lexicuri pina la un caracter dat, lexicul
urmator va include cel mai lung sir de caractere care este posibil sa constituie un
lexic.
2.1 Comentarii
2.4 Constante
Exista diferite tipuri de constante, asa cum se indica mai jos. Caracteristicile
hardware care afecteaza dimensiunile sint rezumate in &2.6.
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.
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.
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 \.
Tabela de mai jos rezuma anumite proprietati care variaza de la masina la masina.
3 Notatia sintactica
4 Nume si Tipuri
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
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.
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.
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).
5 Obiecte si Lvalori
6 Conversii
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.
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.
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.
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 ...".
7 Expresii
* & + - ! ~ ++ --
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
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).
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.
equality_expression:
expression == expression
expression != expression
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.
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.
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.
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.
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.
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
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
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;
// ...
}
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.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++;
Declaratia:
fseek(FILE*, long, int);
declara o functie care poate fi apelata cu zero, unu sau doi parametri de tip int. Ea
poate fi apelata in oricare din modurile:
point(1, 2);
point(1);
point();
Declaratia:
printf(char* ...);
declara o functie care poate fi apelata cu un numar variabil de argumente si tipuri. De
exemplu:
printf("hello world");
printf("a=%d b=%d", a, b);
Cu toate acestea, trebuie ca totdeauna char* sa fie primul sau parametru. Declaratia:
float fa[17], *afp[17];
declara un tablou de numere flotante si un tablou de pointeri spre numere flotante.
In final:
static int x3d[3][5][7];
declara un tablou de intregi tridimensional de ordinul 3x5x7. x3d este un tablou de 3
elemente; fiecare element este un tablou de 5 elemente; fiecare din acestea fiind la
rindul lui un tablou de sapte intregi. Oricare din expresiile x3d, x3d[i], x3d[i][j],
x3d[i][j][k] pot apare intr-o expresie.
8.4.2 Tablouri, Pointeri si Indici
Ori de cite ori intr-o expresie apare un identificator de tip tablou, el este convertit
intr-un pointer spre primul element al tabloului. Din cauza acestei conversii,
tablourile nu sint lvalori. Exceptind cazul in care operatorul de indexare [] a fost
declarat pentru o clasa (&7.16.3), el se interpreteaza in asa fel incit E1[E2] este
identic cu *((E1)+(E2)). Din cauza regulilor de conversie care se aplica la + daca E1
este un tablou si E2 un intreg, E1[E2] se refera la al E2-lea membru al lui E1. De
aceea, in ciuda aparentei asimetrice, indexarea este o operatie comutativa. O regula
consistenta se aplica in cazul tablourilor multidi- mensionale. Daca E este un tablou
ndimensional de ordinul ixjx...xk, atunci E care apare intr-o expresie este convertit
spre un pointer spre un tablou (n-1)dimensional de ordinul jx...xk. Daca operatorul *
se aplica explicit sau implicit ca rezultat al indexarii, rezultatul este tabloul (n-
1)dimensional, care este convertit imediat intr-un pointer.
De exemplu, consideram:
int x[3][5];
Aici x este un tablou de 3x5 intregi. Cind x apare intr-o ex- presie, el se converteste
spre un pointer spre (primul din cele trei) elementul care este un tablou de ordinul 5.
In expresia x[i], care este echivalenta cu *(x+i), x este convertit intii spre un pointer
asa cum s-a descris mai sus; apoi x+i este convertit spre tipul lui x, care implica
multiplicarea lui i prin lungimea obiectului spre care pointeaza pointerul, si anume
obiecte de 5 intregi. Rezultatele se aduna si se aplica indirectarea pentru a produce un
tablou de cinci intregi care la rindul lui este convertit spre un pointer spre primul
dintre intregi. Daca exista un alt indice se aplica din nou aceeasi regula; de data
aceasta rezulta un intreg. Din toate acestea rezulta ca tablourile din C++ sint pastrate
pe linie (ultimul indice variaza mai repede) si ca primul indice din declaratie ajuta sa
se determine cantitatea de memorie consumata de un tablou dar el nu joaca alt rol in
calculele de indici.
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
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.
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;
};
In constructia:
aqqr identifier : public_opt typedef_name
typedef_name trebuie sa noteze o clasa in prealabil declarata, care se numeste clasa
de baza pentru clasa ce se declara. Pentru sensul de public vezi &8.5.9. Membri
clasei de baza pot fi refe- riti ca si cum ei ar fi membri clasei derivate, exceptind
cazul in care numele membrilor bazei au fost redefiniti in clasa derivata; in acest caz
operatorul :: (&7.1) poate fi utilizat pentru a ne referi la membri ascunsi. O clasa
derivata poate fi ea insasi folosita ca o clasa de baza. Nu este posibila derivarea dintr-
o reuniune (&8.5.13). Un pointer spre o clasa derivata poate fi convertit implicit intr-
un pointer spre o clasa de baza publica (&6.7).
Asignarea nu este definita implicit (vezi &7.14 si &8.5) pentru obiectele unei clase
derivate dintr-o clasa pentru care operatorul = a fost definit (&8.5.11). De exemplu:
class base{
public:
int a, b;
};
class derived : public base{
public:
int b, c;
};
derived d;
d.a=1;
d.base::b=2;
d.b=3;
d.c=4;
base* bp=&d;
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();
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
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.
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();
};
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;
//......
};
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
8.5.13Reuniuni
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
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.
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);
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
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
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;
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
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}.
9 Instructiuni
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).
Instructiunea:
break;
are ca actiune terminarea celei mai interioare instructiuni while, do, for sau switch;
controlul este transferat la instructiunea urmatoare.
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).
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.
;
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.
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.
12 Expresii constante
+ - * / % & | ^ <<
>> == != < > <= >= && ||
+ - ~ !
?:
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
14.1 Expresii
expression:
term
expression binary_operator expression
expression ? expression : expression
expression_list
expression_list:
expression
expression_list, expression
term:
primary_expression
unary_operator term
term++
term--
sizeof expression
sizeof (type_name)
(type_name) expression
simple_type_name (expression_list)
new type_name initializer_opt
new (type_name)
delete expression
delete [expression] expression
special_operator:
() []
free_store_operator: one of
new delete abstract_declarator:
empty
*abstract_declarator
abstract_declarator (argument_declaration_list) abstract_declarator
[constant_expression_opt]
simple_type_name:
typedef_name
char
short
int
long
unsigned
float
double
void
typedef_name:
identifier
14.2 Declaratii
declaration:
decl_specifiers_opt declarator_list_opt;
name_declaration
asm declaration
name_declaration:
aggr identifier; enum identifier;
aggr:
class
struct
union
asm_declaration:
asm (string); decl_specifiers:
decl_specifier decl_specifiers_opt decl_specifier:
sc_specifier
type_specifier
fct_specifier
friend
typedef
type_specifier:
simple_type_name
class_specifier
enum_specifier
elaborated_type_specifier
const
sc_specifier:
auto
extern
register
static
fct_specifier:
inline
overload
virtual
elaborated_type_specifier:
key typedef_name
key identifier
key:
class
struct
union
enum
declarator_list:
init_declarator
init_declarator, declarator_list
init_declarator:
declarator initializer_opt declarator:
dname
(declarator)
const_opt declarator
& const_opt declarator declarator (argument_declaration_list) declarator
[constant_expression_opt]
dname:
simple_dname
typedef_name::simple_dname
simple_dname:
identifier
typedef_name
~typedef_name
operator_function_name
conversion_function_name
operator_function_name:
operator operator conversion_function_name:
operator type argument_declaration_list:
arg_declaration_list_opt ..._opt arg_declaration_list:
arg_declaration_list, argument_declaration
argument_declaration
argument_declaration:
decl_specifiers declarator
decl_specifiers declarator = expression
decl_specifiers abstract_declarator
decl_specifiers abstract_declarator = expression
class_specifiers:
class_head { member_list_opt }
class_head { member_list_opt public:member_list_opt }
class_head:
aggr identifier_opt
aggr identifier:public_opt typedef_name
member_list:
member_declaration member_list_opt member_declaration:
decl_specifiers_opt member_declarator initializer_opt
function_definition;_opt
member_declarator:
declarator
identifier_opt:constant_expression
initializer:
= expression
= { initializer_list }
= { initializer_list, }
( expression_list )
initializer_list:
expression
initializer_list, initializer_list
{ initializer_list }
enum_specifier:
enum identifier_opt { enum list } enum_list:
enumerator
enum_list, enumerator
enumerator:
identifier
identifier = constant_expression
14.3 Instructiuni
compound_statement:
{ statement_list_opt } statement_list:
statement
statement statement_list
statement:
declaration
compound_statement
expression_opt;
if(expression) statement
if(expression) statement else statement while(expression) statement do statement
while(expression);
for(statement expression_opt;expression_opt) statement
switch(expression) statement
case constant_expression : statement
default : statement
break;
continue;
return expression_opt;
goto identifier;
identifier : statement;
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
15 Diferente fata de C
15.1 Extensii
Tipurile argumentelor unei functii pot fi specificate (&7.1) si vor fi verificate (&7.1).
Vor avea loc si conversiile de tip (&7.1). Aritmetica flotanta in simpla precizie poate
fi folosita pentru expresii flotante (&6.2).
Numele functiilor pot fi supraincarcate (&8.9). Operatorii pot fi supraincarcati
(&7.16, &8.5.11). Functiile pot fi substituite inline (&8.1).
Obiectele data pot fi constante (&8.4). Pot fi declarate obiecte ale tipului referinta
(&8.4, &8.6.3). Alocarea si dealocarea sint furnizate de operatorii new si
delete (&7.2).
Clasele pot furniza incapsularea datelor (&8.5.9), garanteaza initializarea (&8.6.2),
conversiile definite de utilizator (&8.5.6) si tipizarea dinamica prin folosirea
functiilor virtuale (&8.5.4). Numele unei clase sau enumerari este un nume de tip
(&8.5). Orice pointer poate fi asignat spre void* fara folosirea unei matrite
(&7.14).
O declaratie in interiorul unui bloc este o instructiune (&9.14).
Pot fi declarate reuniuni fara nume (&8.5.13).
15.3 Anacronisme
Extensiile prezentate aici pot fi furnizate pentru a face mai usoara utilizarea
programelor C ca programe C++. Notati ca fiecare dintre aceste particularitati
prezinta aspecte neastepta- te. O implementare care furnizeaza aceste extensii de
asemenea poate furniza utilizatorului o cale de a se asigura ca aceste lucruri nu vor
apare in fisierul sursa.
Numele inca nedefinite pot fi utilizate intr-un apel ca si numele de functii. In acest
caz numele trebuie implicit declarat ca o functie ce returneaza int cu tipul
argumentului (...).
Cuvintul cheie void poate fi folosit pentru a indica faptul ca functia nu primeste
argumente;deci void este echivalent cu ().
Programe utilizind sintaxa C pentru definirea functiilor.
old_function_definition:
decl_specifiers_opt old_function_declarator
declaration_list fct_body old_function_declarator:
declarator (parameter_list) parameter_list:
identifier
identifier, identifier
de exemplu, functia:
max(a, b){ return (a<b) ? b : a; }
poate fi utilizata.
Daca o functie definita ca cea de mai sus nu are o declaratie anterioara, tipul
argumentelor ei vor fi (...), care sint neverificate. Daca, in schimb, functia este
anterior declarata, tipurile argumentelor ei trebuie sa corespunda cu cele din
declaratie.
Un punct poate fi folosit in locul operatorului de rezolutie de domeniu :: pentru a
specifica numele din definitia functiei membru. De exemplu:
int cl.fct(){ /* ... */ }
Acelasi nume poate fi declarat pentru ambele clase sau enumerari si obiectul data sau
functie, in acelasi scop.