r9y1yp
Acest capitol descrie facilitatile pentru a defini tipuri noi pentru care accesul
la date este restrins la un set specific de functii de acces. Sint explicitate
modurile in care o data structurata poate fi protejata, initializata, accesata
si in final eliminata. Exemplele includ clase simple utilizate pentru gestiunea
tabelei de simboluri, manipularea stivei, manipularea multimilor si implementarea
unei reuniuni "incapsulate".
5.1 Introducere si privire generala
Scopul conceptului de clasa C++ este de a furniza programatorului un instrument
pentru a crea tipuri noi care pot fi folosite tot atit de convenabil ca si tipurile
predefinite. Ideal, un tip definit de utilizator ar trebui sa nu difere de tipurile
predefinite in modul in care sint utilizate, ci numai in modul in care sint
create.
Un tip este reprezentarea concreta a unei idei (concept). De exemplu, tipul
float din C++ cu operatiile +, -, *, etc., furnizeaza o versiune restrinsa dar
concreta a conceptului matematic de numar real. Motivul de a desemna un tip
nou este de a furniza o definitie concreta si specifica a conceptului care nu
are un corespondent direct si evident intre tipurile predefinite. De exemplu,
cineva poate furniza tipul "trunk_module" intr-un program ce se ocupa
cu telefoanele sau tipul "list_of_paragraphs" pentru un program de
procesare de text.
Un program care furnizeaza tipuri care sint strins legate de conceptele aplicatiei
este de obicei mai usor de inteles si mai usor de modificat decit un program
care nu face asa ceva. Un set de tipuri definite de utilizator bine ales face
un program mai concis; el de asemenea permite compilatorului sa detecteze utilizari
ilegale ale obiectelor care altfel nu ar fi detectate pina in momentul in care
nu se testeaza efectiv programul.
Ideea fundamentala in definirea unui tip nou este de a separa detaliile incidentale
ale implementarii (de exemplu, aranjamentul datelor utilizate pentru a memora
un obiect al tipului) de proprietatile esentiale ale utilizarii lui corecte
(de exemplu, lista completa de functii care pot avea acces la date). O astfel
de separare poate fi exprimata prin canalizarea tuturor utilizarilor datelor
structurii si a rutinelor de memorare interna printr-o interfata specifica.
Acest capitol consta din 4 parti separate:
&5.2 Clase si Membri. Aceasta sectiune introduce notiunea de baza: tip definit
de utilizator numita clasa.
Accesul la obiectele unei clase se poate restringe la un set de functii declarate
ca o parte a clasei; astfel de functii se numesc functii membru.
Obiectele unei clase pot fi create si initializate prin functii membru declarate
in mod specific pentru acest scop: astfel de functii se numesc constructori.
O functie membru poate fi declarata pentru a "sterge" un astfel de
obiect al unei clase cind el este distrus; o astfel de functie se numeste destructor.
&5.3 Interfete si Implementari. Aceasta sectiune prezinta doua exemple de
modul in care pot fi proiectate, implementate si utilizate clasele.
&5.4 Prieteni si Reuniuni. Aceasta sectiune prezinta multe detalii suplimentare
despre clase. Arata cum se face accesul la partile private ale unei clase si
cum se poate admite accesul pentru o functie care nu este membru al acelei clase.
O astfel de functie se numeste prieten. Aceasta sectiune de asemenea arata cum
se defineste o reuniune distinctiva.
&5.5 Constructori si Destructori. Un obiect poate fi creat ca un obiect
automatic, static sau in memoria libera. Un obiect, de asemenea, poate fi un
membru al unui anumit agregat (o clasa sau un vector), care la rindul lui poate
fi alocat in una din cele 3 moduri indicate mai sus. Utilizarea constructorilor
si destructorilor se explica in detaliu.
5.2 Clase si Membri
Clasa este un tip definit de utilizator. Aceasta sectiune introduce facilitatile
de baza pentru a defini o clasa, crearea obiectelor unei clase, manipularea
acestor obiecte si in final stergerea acestor obiecte dupa utilizare.
5.2.1 Functii membru
Sa consideram implementarea conceptului de data utilizind o structura pentru
a defini reprezentarea unei date si un set de functii pentru manipularea variabilelor
de acest tip: struct dateA int month, day, year; S; date today; void set_date(date*, int,
int, int); void next_date(date*); void print_date(date*);
Nu exista conexiuni explicite intre functii si tipul datei. O astfel de conexiune
se poate stabilii declarind functiile ca membri: struct dateAint month, day, year; void set(int, int, int); void get(int*, int*, int*); void next(); void print();
S;
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()
Amy_birthday.set(30, 12, 1950); today.set(18, 1, 1985); my_birthday.print(); today.next();
S
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()
A if(++day > 28)
A
//do the hard part
S
S
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 dateA int month, day, year; public: void set(int, int, int); void get(int*, int*, int*); void next(); void print();
S;
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
A cout << month << "/" << day << "/"
<< year;
S
Cu toate acestea, functiile care nu sint membru nu pot folosi membri privati
ai clasei date. De exemplu: void backdate()
A today.day--; //eroare
S
Exista citeva beneficii in urma restringerii accesului, la o data structurata,
la o lista de functii declarata explicit. Orice eroare care face ca date sa
aiba o valoare ilegala (de exemplu December 36, 1985) trebuie sa fie cauzata
de codul unei functii membru, asa ca primul stadiu al depanarii, localizarea,
este rezolvat inainte ca programul sa se execute.
Acesta este un caz special al observatiei generale ca orice schimbare in comportarea
tipului date poate, si trebuie sa fie efectuata prin schimbarea membrilor lui.
Un alt avantaj este ca un utilizator de un astfel de tip este necesar numai
sa examineze definitia functiilor membru pentru a invata utilizarea lui.
Protectia datelor private se bazeaza pe restrictia utilizarii numelor membru
ale clasei. Se poate trece peste aceasta prin manipularea de adrese si conversie
explicita de tip, dar aceasta evident este un fel de inselatorie.
5.2.3 Autoreferinta
Intr-o functie membru, ne putem referi direct la membri unui obiect pentru care
functia membru este apelata. De exemplu: class xA int m; public: int readm()A return m; S x aa; x bb; void f()
A int a = aa.readm(); int b = bb.readm();
//.......
S
S
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 xA int m; public: int readm()A return this->m; S
S;
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 dlinkA dlink* pre; //legatura precedenta dlink* suc; //legatura urmator public: void append(dlink*);
//........
S;
void dlink::append(dlink* p)
A 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
S
dlink* list_head; void f(dlink* a, dlink* b)
A
//....... list_head->append(a); list_head->append(b);
S
Legaturile de aceasta natura generala sint baza pentru clasele lista descrise
in capitolul 7. Pentru a adauga o legatura la o lista, trebuie puse la zi obiectele
spre care pointeaza this, pre si suc. Ele toate sint de tip dlink, asa ca functia
membru dlink::append() poate sa faca acces la ele.
Unitatea de protectie in C++ este clasa, nu un obiect individual al unei clase.
5.2.4 Initializare
Utilizarea functiilor de felul set_data() pentru a furniza initializarea pentru
obiectele clasei nu este eleganta si este inclinata spre erori.
Intrucit nicaieri nu se afirma ca un obiect trebuie initializat, un programator
poate uita sa faca acest lucru sau (adesea cu rezultate dezastruoase) sa faca
acest lucru de doua ori. O conceptie mai buna este de a permite programatorului
sa declare o functie cu scopul explicit de a initializa obiecte. Deoarece o
astfel de functie construieste valori de un tip dat, ea se numeste constructor.
Un constructor se recunoaste deoarece are acelasi nume ca si clasa insasi. De
exemplu: class dateA
//...... date(int, int, int);
S;
Cind o clasa are un constructor, toate obiectele acelei clase vor fi initializate.
Daca constructorul cere argumente, ele pot fi furnizate: date today = date(23, 6, 1983); date xmas(25, 12, 0); //forma prescurtata date my_birthday: //ilegal, lipseste initializarea
Este adesea util sa se furnizeze diferite moduri de initializare a obiectelor
unei clase. Aceasta se poate face furnizind diferiti constructori.
De exemplu: class dateA 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
S;
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 dateA int month, day, year; public:
//.......... date(int d=0, int m=0, int y=0); date(char*); //date reprezentat ca sir
S; date::date(int d, int m, int y)
Aday = d ? d : today.day; month = m ? m : today.month; year = y ? y : today.year;
//verifica faptul ca date este valida
//..........
S
Cind se utilizeaza o valoare pentru un argument pentru a indica "ia valoarea
implicita", valoarea aleasa trebuie sa fie in afara setului posibil de
valori pentru argument. Pentru zi si luna este clar acest lucru, dar valoarea
zero pentru an poate sa nu fie o alegere evidenta. Din fericire nu exista anul
zero in calendarul european. 1AD(year == 1) vine imediat dupa 1BC(year == -1),
dar aceasta probabil ar fi prea subtil pentru un program real.
Un obiect al unei clase fara constructori poate fi initializat atribuindu-i
un alt obiect al acelei clase. Aceasta se poate face, de asemenea, cind constructorii
au fost declarati. De exemplu: date d = today; //initializare prin asignare
In esenta, exista un constructor implicit ca o copie de biti a obiectelor din
aceeasi clasa. Daca nu este dorit acest constructor implicit pentru clasa X,
el poate fi redefinit prin constructorul denumit X(X&) (aceasta se va discuta
mai departe in &6.6).
5.2.5 Curatire (stergere)
Mai frecvent este cazul in care un tip definit de utilizator are un constructor
pentru a asigura initializarea proprie. Multe tipuri necesita, de asemenea,
un destructor, care sa asigure stergerea obiectelor de un tip. Numele destructorului
pentru clasa X este IX() ("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_stackA int size; char* top; char* s; public: char_stack(int sz)Atop = s = new charasize=szi;S
Ichar_stack()A delete s; S void push(char c)A *top++ = c; S char pop()A return *--top; S
S;
Cind char_stack iese in afara domeniului, se apeleaza destructorul: void f()
A char_stack s1(100); char_stack s2(200); s1.push('a'); s2.push(s1.pop()); char ch = s2.pop(); cout << chr(ch) << "\n";
S
Cind f() este apelata, constructorul char_stack va fi apelat pentru s1 ca sa
aloce un vector de 100 de caractere si pentru s2 pentru a aloca un vector de
200 de caractere; la revenirea din f(), acesti doi vectori vor fi eliminati.
5.2.6 "Inline"
Cind programam folosind clasele, este foarte frecvent sa utilizam multe functii
mici. In esenta, o functie este realizata unde un program structurat, in mod
traditional, ar avea un anumit mod tipic de utilizare a unei date structurate;
ceea ce a fost o conventie devine un standard recunoscut prin compilator. Aceasta
poate conduce la ineficiente teribile deoarece costul apelului unei functii
este inca mai inalt decit citeva referinte la memorie necesare pentru corpul
unei functii triviale.
Facilitatile functiilor "in linie" au fost proiectate pentru a trata
aceasta problema. O functie membru definita (nu numai declarata) in declaratia
de clasa se considera ca fiind in linie. Aceasta inseamna de exemplu, ca, codul
generat pentru functiile care utilizeaza char_stack-ul prezentat mai sus nu
contine nici un apel de functie exceptind cele utilizate pentru a implementa
operatiile de iesire. Cu alte cuvinte, nu exista un cost de timp mai mic decit
cel luat in seama cind proiectam o clasa; chiar si cele Mai costisitoare operatii
pot fi realizate eficient. Aceasta observatie invalideaza motivele cele mai
frecvent utilizate in favoarea utilizarii membrilor publici ai datelor. O functie
mem- bru poate, de asemenea, sa fie declarata inline in afara declaratiei de
clasa. De exemplu: class char_stackA int size; char* top; char* s; public: char pop();
//......
S
inline char char_stack::pop()
A return *--top;
S
5.3 Interfete si Implementari
Ce face o clasa buna? Ceva ce are un set mic si bine definit de operatori. Ceva
ce poate fi vazut ca o "cutie neagra" manipulata exclusiv prin acel
set de operatii. Ceva a carei reprezentare reala ar putea fi conceputa sa fie
modificata fara a afecta modul de utilizare a acelui set de operatii. Containerele
de toate felurile furnizeaza exemple evidente: tabele, multimi, liste, vectori,
dictionare, etc.. O astfel de clasa va avea o operatie de inserare, care de
obicei va avea de asemenea operatii pentru a verifica daca un membru specific
a fost inserat, poate va avea operatii pentru sortarea membrilor, poate va avea
operatii pentru examinarea tuturor membrilor intr-o anumita ordine si in final
ar putea, de asemenea, sa aiba o operatie pentru eliminarea unui membru. Clasele
container de obicei au constructori si destructori.
Ascunderea datelor si o interfata bine definita pot fi de asemenea obtinute
prin conceptul de modul (vezi de exemplu, &4.4: fisiere ca module). Cu toate
acestea, o clasa este un tip; pentru a o utiliza, trebuie sa se creeze obiecte
ale clasei respective si se pot crea atit de multe astfel de obiecte cite sint
necesare. Un modul este el insusi un obiect; pentru a-l utiliza, cineva este
necesar sa-l initializeze si exista exact un astfel de obiect.
5.3.1 Implementari alternative
Atita timp cit declaratia partii publice a unei clase si declaratia functiilor
membru ramin neschimbate, implementarea unei clase poate fi schimbata fara a
afecta utilizatorii ei. Sa consideram o tabela de simboluri de felul celei utilizate
pentru calculatorul de birou din capitolul 3. Este o tabela de nume: struct nameA char* string; name* next; double value;
S;
Iata o versiune a clasei tabela:
//file table.h: class tableA name* tbl; public: table()Atbl = 0;S name* look(char*, int=0); name* insert(char* s)Areturn look(s, 1);S
S;
Aceasta tabela difera de cea definita in capitolul 3 prin aceea ca este un tip
propriu. Se pot declara mai multe tabele, putem avea un pointer spre o tabela,
etc.. De exemplu:
#include "table.h" table globals; table keywords; table* locals; main()
Alocals = new table;
//.........
S
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) Afor(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
charastrlen(p) + 1i; strcpy(nn->string, p); nn->value = 1; nn->next
= tbl; tbl = nn; return nn;
S
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 tableAname** tbl; int size; public: table(int sz=15);
Itable(); name* look(char*, int=0); name* insert(char* s)Areturn look(s, 1);S
S;
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)
A if(sz < 0) error("negative table size"); tbl = new name*asize=szi; for(int i=0;
i < sz; i++) tblaii = 0;
S
table::Itable()
Afor(int i=0; i < size; i++)
Aname* nx; for(name* n=tblaii; n; n=nx)
A nx = n->next; delete n->string; delete n;
S
S delete tbl;
S
O versiune mai simpla si mai clara a lui table::Itable() 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)
A int ii = 0; char* pp = p;
while(*pp) ii == ii << 1 ^ *pp++; if(ii < 0) ii = -ii; ii %= size; for(name* n = tblaiii; 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
charastrlen(p) + 1i; strcpy(nn->string, p); nn->value = 1; nn->next
= tblaiii; tblaiii = nn; return nn;
S
Evident, functiile membru ale unei clase trebuie sa fie recompilate ori de cite
ori se face o schimbare in declaratia de clasa. Ideal, o astfel de schimbare
nu ar trebui sa afecteze de loc utilizatorii unei clase. Din nefericire, nu
este asa. Pentru a aloca o variabila de clasa, compilatorul are nevoie sa cunoasca
dimensiunea unui obiect al clasei. Daca dimensiunea unui astfel de obiect este
schimbata, fisierele care contin utilizari ale clasei trebuie sa fie recompilate.
Softwarul care determina setul minim de fisiere ce necesita sa fie recompilate
dupa o schimbare a declaratiei de clasa poate fi (si a fost) scris, dar nu este
inca utilizat pe scara larga. Noi ne putem intreba, de ce nu a fost proiectat
C++ in asa fel ca recompilarea utilizatorilor unei clase sa fie necesara dupa
o schimbare in partea privata? Si de ce trebuie sa fie prezenta partea privata
in declaratia de clasa? Cu alte cuvinte, intrucit utilizatorii unei clase nu
sint admisi sa aiba acces la membri privati, de ce declaratiile lor trebuie
sa fie prezente in fisierele antet ale utilizatorului? Raspunsul este eficienta.
Pe multe sisteme, atit procesul de compilare cit si secventa de operatii care
implementeaza apelul unei functii sint mai simple cind dimensiunea obiectelor
automatice (obiecte pe stiva) se cunoaste la compilare. Aceasta problema ar
putea fi eliminata reprezentind fiecare obiect al clasei ca un pointer spre
obiectul "real". Intrucit toti acesti pointeri ar avea aceeasi dimensiune,
iar alocarea obiectelor "reale" ar putea fi definita intr-un fisier
unde este disponibila partea privata, acest fapt ar putea rezolva problema.
Cu toate acestea, aceasta solutie impune referirea la o memorie suplimentara
cind se face acces la membri unei clase si mai rau ar implica cel putin un apel
al alocatorului si dealocatorului de memorie pentru fiecare apel de functie
cu un obiect automatic al clasei. De asemenea s-ar face implementarea unei functii
membru inline care sa faca acces la date private fezabile. Mai mult decit atit,
o astfel de schimbare ar face imposibila linkarea impreuna a fragmentelor de
programe C++ si C (deoarece un compilator C ar trata diferit o structura fata
de un compilator C++). Aceasta este nepotrivit in C++.
5.3.2 O clasa completa
Programarea fara ascunderea datelor (folosind structuri) necesita Mai putina
bataie de cap decit programarea cu ascunderea de date (utilizind clase). Se
poate defini o structura fara prea mare bataie de cap, dar cind definim o clasa
noi trebuie sa ne concentram sa furnizam un set complet de operatii pentru tipul
nou; aceasta este o deplasare importanta in domeniul utilizarii. Timpul cheltuit
in proiectarea unui nou tip este de obicei recuperat de multe ori in dezvoltarea
si testarea unui program. Iata un exemplu de tip complet, intset, care furnizeaza
conceptul de "multime de intregi". class intsetAint cursize, maxsize; int* x; public: intset(int m, int n); //cel putin m intregi
//in 1..n Iintset(); int member(int t); //este "t" un membru? void insert(int t); //adauga "t" la multime void iterate(int& i)Ai = 0;S int ok(int& i)Areturn i < cursize;S int next(int& i)Areturn xai++i;S
S;
Pentru a testa aceasta clasa noi putem crea si apoi imprima un set de intregi
aleatori. Un astfel de set ar putea constitui niste numere de loterie. Acest
set simplu ar putea fi utilizat pentru a verifica un sir de intregi punind in
evidenta duplicatele, dar pentru majoritatea aplicatiilor tipul set ar trebui
sa fie putin mai migalos elaborat. Ca totdeauna sint posibile erori:
#include <stream.h> void error(char* s)
Acerr << "set: " << s << "\n"; exit(1);
S
Clasa intset se utilizeaza in functia main() care asteapta doua argumente intregi.
Primul argument specifica numarul de numere aleatoare de generat. Cel de al
doilea argument specifica domeniul intregilor aleatori care se asteapta: main(int argc, char* argvai)
A if(argc != 3) error("No arguments expected"); int count = 0; int m = atoi(argva1i); //numarul elementelor multimii int n = atoi(argva2i); //in domeniul 1..n intset s(m, n); while(cout < m)
A int t = randint(n); if(s.member(t) == 0)
A s.insert(t); count++;
S
S print_in_order(&s);
S
Motivul ca argumentul numarator argc sa fie 3 pentru un program care cere 2
argumente este faptul ca numele programului este totdeauna pasat ca argva0i.
Functia: extern int atoi(char*); este o functie standard de biblioteca pentru covertirea reprezentarii sub forma
de sir a unui intreg in forma lui interna binara.
Numerele aleatoare se genereaza utilizind functia standard rand(): extern int rand(); //nu este prea aleatoare int randint(int n) //in domeniul 1..n
A int r = rand(); if(r < 0) r = -r; return 1 + r % n;
S
Detaliile de implementare ale unei clase ar trebui sa fie de un interes mai
mic pentru un utilizator, dar aici sint in orice caz si functiile membru. Constructorul
aloca un vector intreg de dimensiune maxima a multimii specificate, iar destructorul
o dealoca: intset::intset(int m, int n) //cel mult m intregi in 1..n
A if(m < 1 || n < m) error("illegal intset size"); cursize = 0; maxsize = m; x = new intamaxsizei;
S
intset::Iintset()Adelete x;S
Intregii se insereaza asa ca ei sa fie tinuti in ordine crescatoare in multime: void intset::insert(int t)
A if(++cursize > maxsize) error("too many elements"); int i = cursize-1; xaii = t; while(i >
0 && xai-1i > xaii)
A int t = xaii; //permuta xaii si xai-1ixaii = xai-1i; xai-1i = t; i--;
S
S
Se foloseste o cautare binara pentru a gasi un membru: int intset::member(int t) //cautare binara
A int l = 0; int n = cursize-1;
while(l <= n)
A int m = (l+n)/2; if(t < xami) n = m-1; else if(t > xami) l = m+1; else return 1; //gasit
S return 0; //negasit
S
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 intsetA
//......... void iterate(int& i)Ai = 0;S int ok(int& i)Areturn i < cursize;S int next(int& i)Areturn xai++i;S
S;
Pentru a permite ca aceste trei operatii sa coopereze si sa reaminteasca cit
de departe a progresat iteratia, utilizatorul trebuie sa furnizeze un argument
intreg. Intrucit argumentele sint pastrate intr-o lista sortata, implementarea
lor este triviala. Acum poate fi definita functia print_in_order: void print_in_order(intset* set)
A int var; set->iterate(var);
while(set->ok(var)) cout << set->next(var) << "\n";
S
O alta varianta de a furniza un iterator se prezinta in &6.8.
5.4 Prieteni si Reuniuni
Aceasta sectiune descrie inca citeva facilitati relativ la clase. Se prezinta
un mod de a acorda acces functiilor membre la membri privati. Se descrie cum
se pot rezolva conflictele numelor membre, cum se pot imbrica declaratiile de
clase si cum pot fi eliminate imbricarile nedorite. De asemenea se discuta cum
pot fi obiectele unei clase divizate intre membri ei si cum se pot utiliza pointerii
spre membri. In final exista un exemplu care arata cum se poate proiecta o reuniune
discriminatorie.
5.4.1 Prieteni
Presupunem ca noi trebuie sa definim doua clase, vector si matrix. Fiecare din
ele ascunde reprezentarea ei si furnizeaza un set complet de operatii pentru
manipularea obiectelor ei. Acum sa definim o functie care inmulteste o matrice
cu un vector. Pentru simplificare, presupunem ca un vector are patru elemente,
cu indicii 0..3 si ca o matrice are 4 vectori indexati cu 0..3. Presupunem de
asemenea, ca elementele unui vector sint accesate printr-o functie elem() care
verifica indexul si ca matrix are o functie similara. O conceptie este de a
defini o functie globala multiply() de forma: vector multiply(matrix& m, vector& v)
Avector r; for(int i=0; i<3; i++)
A
//raii = maii * v; r.elem(i) = 0; for(int j=0; j<3; j++) r.elem(i) += m.elem(i, j) * v.elem(j);
S return r;
S
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 vectorAfloat va4i;
//........ friend vector multiply(matrix&, vector&);
S; class matrixAvector va4i;
//........ friend vector multiply(matrix&, vector&);
S;
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)
A vector r; for(int i=0; i<3; i++)
A
//raii = maii*v; r.vaii = 0; for(int j=0; j<3; j++) r.vaii += m.vaiiaji * v.vaji;
S return r;
S
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 xA
//........ void f();
S;
class yA
//........ friend void x::f();
S;
Nu este ceva iesit din comun ca toate functiile unei clase sa fie pritene ale
alteia. Exista chiar o prescurtare pentru acest fapt: class xA friend class y;
//........
S;
Aceasta declaratie, friend, face ca toate functiile membre ale clasei y sa
fie prietene ale clasei x.
5.4.2 Calificarea numelor de membri
Ocazional, este util sa se faca distinctie explicita intre numele membre ale
unei clase si alte nume. Se poate folosi operatorul de rezolutie a domeniului
"::": class xA int m; public: int readm()A return x::m; S void setm(int m)A x::m = m; S
S;
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_fileA //.......... public: int open(char*, char*);
S;
int my_file::open(char* name, char* spec)
A //........... if(::open(name, flag))
A
//utilizeaza open() din UNIX(2)
//..........
S
//...........
S
5.4.3 Clase imbricate
Declaratiile de clasa pot fi imbricate. De exemplu: class setA struct setmemA int mem; setmem* next; setmem(int m,setmem* n)A mem=m; next=n; S
S; setmem* first; public: set()Afirst = 0;S insert(int m)Afirst = new setmem(m, first);S
//.......
S;
Daca clasa imbricata nu este foarte simpla, astfel de declaratii sint foarte
incurcate. Mai mult decit atit, clasele imbricate sint mai mult o facilitate
in notatie, intrucit o clasa imbricata nu este ascunsa in domeniul clasei care
o include din punct de vedere lexical: class setA struct setmemA int mem; setmem* next; setmem(int m, setmem* n);
S;
//.......
S;
setmem::setmem(int m, setmem* n)
Amem = m; next = n;
S 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 setmemA friend class set; //acces numai prin membri
//lui set int mem; setmem* next; setmem(int m, setmem* n)A mem=m; next=n; S
S;
class setA setmem* first; public: set()A first = 0; S insert(int m)A first = new setmem(m, first); S
S;
5.4.4 Membri statici
O clasa este un tip, nu un obiect data si fiecare obiect al clasei are copia
lui proprie a membrilor date ai clasei. Cu toate acestea, unele tipuri sint
implementate mai elegant daca toate obiectele acelui tip au in comun unele date.
Este preferabil ca o astfel de data comuna sa fie declarata ca parte a clasei.
De exemplu, pentru a gestiona taskuri intr-un sistem de operare, este adesea
utila o lista a tuturor taskurilor: class taskA//........ task* next; static task* task_chain; void schedule(int); void wait(event);
//........
S;
Declarind membrul task_chain ca static se asigura ca va fi numai o copie a lui,
nu o copie pentru fiecare obiect task. Este inca in domeniul clasei task si
poate fi accesat "din afara" numai daca a fost declarat public. In
acest caz, numele lui tre- buie sa fie calificat prin numele clasei sale: task::task_chain
Intr-o functie membru, se poate face referire prin task_chain. Utilizarea membrilor
statici ai clasei poate reduce considerabil necesarul de memorie pentru variabilele
globale.
5.4.5 Pointeri spre membri
Este posibil sa se ia adresa unui membru al unei clase. A lua adresa unei functii
membru este adesea util intrucit tehnicile si motivele pentru a utiliza pointeri
la functii prezentate in &4.6.9 se aplica in mod egal si la functii membru.
Totusi exista un defect curent in limbaj: nu este posibil sa se exprime tipul
pointerului obtinut dintr-o astfel de operatie. In consecinta trebuie sa folosim
trucuri folosind avantajele din implementarea curenta. Exemplul de mai jos nu
este garantat ca fun- ctioneaza si utilizarea lui trebuie localizata in asa
fel incit sa poata fi usor convertit spre a utiliza constructiile propri ale
limbajului. Trucul folosit este acela de a avea avantajul faptului ca this este
implementat curent ca primul argument (ascuns) al unei functii membru.
#include <stream.h> struct clA char* val; void print(int x)A cout << val << x << "/n"; S cl(char* v)Aval = v;S
S;
//"se ia" tipul functiilor membru: typedef void (*PROC)(void*, int); main()
Acl z1("z1 "); cl z2("z2 ");
PROC pf1 = PROC(&z1.print);
PROC pf2 = PROC(&z2.print); z1.print(1);
(*pf1)(&z1, 2); z2.print(3);
(*pf2)(&z2, 4);
S
In multe cazuri, functiile virtuale (vezi capitolul 7) pot fi utilizate cind
altfel s-ar utiliza pointeri spre functii.
Versiunile ulterioare de C++ vor suporta un concept de pointer spre un membru:
cl::* inseamna "pointer spre un membru a lui cl". De exemplu: typedef void(cl::*PROC)(int);
PROC pf1=&cl::print;//nu este nevoie de conversie explicita
PROC pf2 = &cl::print;
Operatorii . si -> se utilizeaza pentru un pointer spre o functie membru.
De exemplu:
(z1.*pf1)(2);
((&z2)->*pf2)(4);
5.4.6 Structuri si Reuniuni
Prin definitie o structura este pur si simplu o clasa cu toti membri publici,
adica: struct sA ... este pur si simplu o prescurtare pentru: classA public: ...
Structurile se folosesc cind ascunderea datelor este nepotrivita. O reuniune
numita se defineste ca o structura in care fiecare membru are aceeasi adresa
(vezi &r8.5.13). Daca se stie ca numai un membru al unei structuri va avea
o valoare utila la un moment dat, o reuniune poate salva spatiu. De exemplu,
se poate defini o reuniune pentru a pastra unitatile lexicale dintr-un compilator
C: union tok_valA char* p; //sir char va8i; //identificator (maxim 8 caractere) long i; //valori intregi double d; //valori flotante
S;
Problema este ca, in general compilatorul nu poate sa stie care membru este
utilizat in fiecare moment, asa ca nu poate fi testat tipul. De exemplu: void strange(int i)
A tok_val x; if(i) x.p = "2"; else x.d = 2; sqrt(x.d); //eroare daca i != 0
S
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_valueA char* p; //sir char va8i; //identificator long i; //valori intregi double d; //valori flotante tok_value(char*) //trebuie sa decida intre
//p si v tok_value(int ii)Ai = ii;S tok_value(double dd)Ad == dd;S
S;
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()
A tok_val a = 10; //a.i = 10 tok_val b = 10.0; //b.d = 10.0
S
Cind acest lucru nu este posibil (pentru tipurile char* si chara8i, 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)
A if(strlen(pp) <= 8) strncpy(v, pp, 8); //sir scurt else p = pp; //sir lung
S
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_valA char tag; unionA char* p; char va8i; long i; double d;
S; int check(char t, char* s)
A if(tag != t)
A error(s); return 0;
S return 1;
S public: tok_val(char* pp); tok_val(long ii)A i=ii; tag='I'; S tok_val(double dd)A d=dd; tag='D'; S long& ival()A check('I', "ival"); return i; S double& fval()Acheck('D', "fval"); return d; S char*& sval()A check('S', "sval"); return p; S char* id()A check('N', "id"); return v; S
S;
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)
A if(strlen(pp) <= 8)
A //sir scurt tag = 'N'; strncpy(v, pp, 8); //copiaza 8 caractere
S else
A tag = 'S'; p = pp; //se pastreaza numai pointerul
S
S
Tipul tok_val poate fi folosit astfel: void f()
A tok_val t1("short"); //asignare la v tok_val t2("long string"); //asignare la p char sa8i; strncpy(s, t1.id(), 8); //ok strncpy(s, t2.id(), 8); //testul va esua
S
5.5 Constructori si Destructori
Cind o clasa are un constructor, el este apelat ori de cite ori se creaza un
obiect al acelei clase. Cind o clasa are un destructor, el este apelat ori de
cite ori este distrus un obiect al acelei clase. Obiectele pot fi create ca:
a1i 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;
a2i Un obiect static: se creaza o data la pornirea programului si se distruge
o data cu terminarea programului;
a3i Un obiect in memoria libera: este creat folosind operatorul new si distrus
folosind operatorul delete;
a4i 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_stackA int size; char* top; char* s; public: char_stack(int sz)Atop=s=new charasize=szi;S
Ichar_stack()Adelete s;S //destructor void push(char c)A*top++=c;S char pop()Areturn *--top;S
S; void h()
A char_stack s1(100); char_stack s2 = s1; //apar probleme char_stack s3(99); s3 = s2; //apar probleme
S
Aici constructorul char_stack::char_stack() se apeleaza de doua ori: pentru
s1 si s3. Nu se apeleaza pentru s2 deoarece variabila s2 a fost initializata
prin atribuire. Totusi, destructorul char_stack::Ichar_stack() se apeleaza de
trei ori: pentru s1, s2 si s3. Mai mult decit atit, interpretarea implicita
a atribuirii ca si copiere de biti face ca s1, s2 si s3 sa contina fiecare la
sfirsitul lui h() un pointer spre vectorul de caractere alocat in memoria libera
cind a fost creat s1. Nu va ramine nici un pointer spre vectorul de caractere
alocate cind a fost creat s3. Astfel de anomalii pot fi eliminate asa cum se
va vedea in capitolul 6.
5.5.2 Memoria statica
Consideram: table tbl1(100); void f()A static table tbl2(200); S main()
A f();
S
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::Itable() 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)
A static table t(a); //eroare
S
Traditional, executia lui main() a fost vazuta ca executia programului. Aceasta
nu a fost niciodata asa, nici chiar in C, dar numai alocind un obiect static
al unei clase cu un constructor si/sau un destructor programatorul poate sa
aiba un mod evident si simplu de a specifica cod de executat inainte si/sau
dupa apelul lui main.
Apelind constructori si destructori pentru obiecte statice se realizeaza functii
extrem de importante in C++. Este modul de a asigura initializari propri si
de a curata structuri de date din biblioteci. Consideram <stream.h>.
De unde vin cin, cout si cerr? Unde au fost ele initializate? Si ce este Mai
important, intrucit sirurile de iesire pastreaza zone tampon interne de caractere,
cum se videaza aceste zone tampon? Raspunsul simplu si clar este acela ca activitatea
se face prin constructori si des- tructori corespunzatori inainte si dupa executia
lui main(). Exista alternative de a utiliza constructori si destructori pentru
initializarea si stergerea facilitatilor de biblioteca.
Daca un program se termina utilizind functia exit(), se vor apela destructorii
pentru obiectele statice, dar daca, programul se termina folosind abort(), ei
nu vor fi apelati. Sa observam ca aceasta implica faptul ca exit() nu termina
programul imediat. Apelind exit() intr-un destructor se poate ajunge la o recursivitate
infinita.
Uneori, cind noi proiectam o biblioteca, este necesar sau pur si simplu convenabil
sa inventam un tip cu un constructor si un destructor cu singurul scop al initializarii
si stergerii. Un astfel de tip va fi folosit numai o data: sa aloce un obiect
static prin apelul constructorului.
5.5.3 Memoria libera
Fie: main()
A table* p = new table(100); table* q = new table(200); delete p; delete p; //probabil o eroare
S
Constructorul table::table() va fi apelat de doua ori si la fel si destructorul
table::Itable(). Este bine de amintit ca C++ nu ofera garantie ca un destructor
este apelat vreodata pentru un obiect creat folosind new. Programul precedent
nu il sterge pe q, dar pe p il sterge de doua ori. In functie de tipul lui p
si q, programatorul poate sau nu sa considere aceasta ca o eroare. Ne- stergind
un obiect de obicei nu este o eroare, ci numai o pierdere de spatiu. Stergind
p de doua ori este de obicei o eroare serioasa. Un rezultat frecvent al aplicarii
lui delete de doua ori la acelasi pointer este un ciclu infinit in rutina de
gestionare a memoriei libere, dar comportamentul in acest caz nu este specificat
prin definitia limbajului si depinde de implementare.
Utilizatorul poate defini o implementare noua pentru operatorii new si delete
(vezi &3.2.6). Este de asemenea posibil sa se specifice modul in care interactioneaza
constructorul si destructorul cu operatorii new si delete (vezi &5.5.6).
5.5.4 Obiectele clasei ca membri
(clase de obiecte ca membri)
Consideram: class classdefA table members; int no_of_members;
//........... classdef(int size);
Iclassdef();
S;
Intentia este clara; aceea ca classdef sa contina o tabela de members de dimensiune
size si problema este de a obtine constructorul table::table() apelat cu argumentul
size. Se poate face astfel: classdef::classdef(int size)
:members(size)
A no_of_members = size;
//...........
S
Argumentele pentru un constructor membru (table::table()) se plaseaza in definitia
(nu in declaratia) constructorului clasei care il contine (aici classdef::classdef()).
Constructorul membru este apoi apelat inaintea corpului constructorului care
specifica lista argumentelor lui.
Daca sint mai multi membri ce necesita liste de argumente pentru constructori,
ei pot fi specificati in mod analog. De exemplu: class classdefA table members; table friends; int no_of_members;
//.......... classdef(int size);
Iclassdef();
S;
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)
A no_of_members = size;
//...........
S
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
A no_of_members = size;
//...........
S
Daca un constructor pentru un membru nu necesita argumente, atunci nu este
necesar sa se specifice nici o lista de argumente. De exemplu, intrucit table::table()
a fost definit cu argumentul implicit 15, ceea ce urmeaza este corect: classdef::classdef(int size)
:members(size)
A no_of_members = size;
//...........
S
si dimensiunea lui friends table va fi 15.
Cind o clasa care contine clase (de exemplu classdef) se distruge, intii se
executa corpul destructorului propriu acelei clase si apoi se executa destructorii
membrilor.
Consideram varianta traditionala de a avea clase ca membri si anume aceea de
a avea membri pointeri si ai initializa pe acestia intr-un constructor: class classdefA table* members; table* friends; int no_of_members;
//............ classdef(int size);
Iclassdef();
S; classdef::classdef(int size)
A members = new table(size); friends = new table; //dimensiune implicita no_of_members
= size;
//...........
S
Intrucit tabelele au fost create folosind new, ele trebuie sa fie distruse
utilizind delete: classdef::Iclassdef()
A//........... delete members; delete friends;
S
Obiectele create separat ca acestea pot fi utile, dar sa observam ca members
si friends pointeaza spre obiecte separate care cer o alocare si o dealocare
fiecare. Mai mult decit atit, un pointer plus un obiect in memoria libera ia
mai mult spatiu decit un obiect membru.
5.5.5 Vectori si Obiecte clasa
Pentru a declara un vector de obiecte ale unei clase cu un constructor acea
clasa trebuie sa aiba un constructor care sa poata fi apelat fara o lista de
argumente. Nici argumentele implicite nu pot fi utilizate. De exemplu: table tblveca10i; 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 tableA
//......... void init(int sz); //ca si constructorul vechi public: table(int sz)Ainit(sz);S //ca inainte dar nu
//exista valoare implicita table()Ainit(15);S //implicit
//.........
S;
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()
A table* t1 = new table; table* t2 = new tablea10i; delete t1; //o tabela delete t2; //apar probleme: 10 tabele
S
In acest caz programatorul trebuie sa furnizeze dimensiunea vectorului: void g(int sz)
Atable* t1 = new table; table* t2 = new tableaszi; delete t1; deleteaszi t2; S
Dar de ce nu poate compilatorul sa deduca numarul de elemente din cantitatea
de memorie alocata? Deoarece alocatorul de memorie libera nu este o parte a
limbajului si ar putea fi furnizata de programator.
5.5.6 Obiecte mici
Cind se utilizeaza multe obiecte mici alocate in memoria libera, noi putem
sa aflam ca programul consuma timp considerabil pentru alocare si dealocare
de astfel de obiecte. O solutie este de a furniza un alocator cu scopuri generale
mai bun si o a doua este ca proiectarea unei clase sa nu se faca pentru a fi
gestionata in memoria libera, definind constructori si destructori.
Sa consideram clasa name folosita in exemplul table. Ea ar putea fi definita
astfel: struct nameAchar* string; name* next; double value; name(char*, double, name*);
Iname();
S;
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)
Aregister name* p = nfree //prima alocare if(p) nfree = p->next; else
Aname* q = (name*)new charaNALL * sizeof(name)i; for(p = nfree = &qaNALL-1i; q<p; p--) p->next = p-1;
(p+1)->next = 0;
S this = p; string = s; //initializare value = v; next = n;
S
Atribuirea la this informeaza compilatorul ca programatorul a luat controlul
si ca mecanismul implicit de alocare de memorie nu trebuie sa fie utilizat.
Constructorul name::name() trateaza cazul in care numele este alocat numai prin
new, dar pentru multe tipuri acesta este de obicei cazul; &5.5.8 explica
cum se scrie un constructor pentru a trata atit memoria libera, cit si alte
tipuri de alocari.
Sa observam ca spatiul nu ar putea fi alocat pur si simplu astfel: name* q = new nameaNALLi; intrucit aceasta ar cauza o recursivitate infinita cind new apeleaza name::name().
Dealocarea este de obicei triviala: name::Iname()
A next = nfree; nfree = this; this = 0;
S
Atribuind 0 la this intr-un destructor se asigura ca nu se va utiliza destructorul
standard.
5.5.7 Goluri
Cind se face o atribuire la this intr-un constructor, valoarea lui this este
nedefinita pina la acea atribuire. O referinta la un membru inaintea acelei
atribuiri este de aceea nedefinita si probabil cauzeaza un destructor.
Compilatorul curent nu incearca sa asigure ca o atribuire la this sa apara pe
orice cale a executiei: mytype::mytype(int i)
Aif(i) this = mytype_alloc(); //asignare la membri
S; 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)
A if(this == 0) this = mytype_alloc(); //asignare la membri
S;
Nu exista o facilitate echivalenta care sa permita unui destructor sa decida
daca obiectele lui au fost create folosind new si nici o facilitate care sa
permita sa se decida daca el a fost apelat prin delete sau printr-un obiect
din afara domeniului. Daca cunoasterea acestui lucru este importanta, utilizatorul
poate memora undeva informatii corespunzatoare pe care sa le citeasca destructorul.
O alta varianta este ca utilizatorul sa se asigure ca obiectele acelei clase
sint numai alocate in mod corespunzator. Daca prima problema este tratata, ultima
este neinteresanta.
Daca implementatorul unei clase este de asemenea numai utilizatorul ei, este
rezonabil sa se simplifice clasa bazindu-ne pe presupunerile despre utilizarea
ei. Cind o clasa este proiectata pentru o utilizare larga, astfel de presupuneri
este adesea mai bine sa fie eliminate.
5.5.8 Obiecte de dimensiune variabila
Luind controlul asupra alocarii si dealocarii, utilizatorul poate de asemenea,
construi obiecte a caror dimensiune nu este determinata la momentul compilarii.
Exemplele precedente de implementare a claselor container vector, stack, insert
si table ca dimensionate fix, acceseaza direct structuri care contin pointeri
spre dimensiunea reala. Aceasta implica faptul ca sint necesare doua operatii
de creare de astfel de obiecte in memoria libera si ca orice acces la informatiile
memorate va implica o indirectare suplimentara. De exemplu: class char_stackA int size; char* top; char* s; public: char_stack(int sz)A top=s=new charasize=szi; S
Ichar_stack()A delete s; S //destructor void push(char c)A *top++=c; S char pop()A return *--top; S
S;
Daca fiecare obiect al unei clase este alocat in memoria libera, aceasta nu
este necesar. Iata o alternativa: class char_stackA int size; char* top; char sa1i; public: char_stack(int sz); void push(char c)A *top++=c; S char pop()A return *--top; S
S;
char_stack::char_stack(int sz)
A if(this) error("stack not on free store"); if(sz<1) error("stack size < 1"); this = (char_stack*)new charasizeof(char_stack)+sz-1i; size = sz; top = s;
S
Observam ca un destructor nu mai este necesar, intrucit delete poate elibera
spatiul utilizat de char_stack fara vreun ajutor din partea programatorului.
5.6 Exercitii
1. (*1). Sa se modifice calculatorul de birou din capitolul 3 pentru a utiliza
clasa table.
2. (*1). Sa se proiecteze tnode (&r8.5) ca o clasa cu consructori, destructori,
etc.. Sa se defineasca un arbore de tnodes ca o clasa cu constructori, destructori,
etc..
3. (*1). Sa se modifice clasa intset (&5.3.2) intr-o multime de siruri.
4. (*1). Sa se modifice clasa intset intr-o multime de noduri unde node este
o structura pe care sa o definiti.
5. (*3). Se defineste o clasa pentru analizarea, memorarea, evaluarea si imprimarea
expresiilor aritmetice simple care constau din constante intregi si operatiile
'+', '-', '*' si '/'. Interfata publica ar trebui sa arate astfel: class exprA
//......... public: expr(char*); int eval(); void print();
S;
Argumentul sir pentru constructorul expr::expr() este expresia. Functia expr::eval()
returneaza valoarea expresiei, iar expr::print() imprima reprezentarea expresiei
la cout. Un program ar putea arata astfel: expr x("123/4+123*4-3"); cout << "x = " << x.eval()
<< "\n"; x.print();
Sa se defineasca expr class de doua ori: o data utilizind o lista inlantuita
de noduri si o data utilizind un sir de caractere. Sa se experimenteze diferite
moduri de imprimare a expre- siei: cu paranteze complete, notatie postfix, cod
de asamblare, etc..
6. (*1). Sa se defineasca o clasa char_queue asa ca interfata publica sa nu
depinda de reprezentare. Sa se implementeze char_queue: (1) ca o lista inlantuita
si (2) ca un vector.
7. (*2). Sa se defineasca o clasa histograma care tine seama de numerele dintr-un
anumit interval specificat ca argumente la constructorul histogramei. Sa se
furnizeze functii pentru a imprima histograme. Sa se trateze domeniul valorilor.
Recomandare: <task.h>.
8. (*2). Sa se defineasca niste clase pentru a furniza numere aleatoare de o
anumita distributie. Fiecare clasa are un constructor care specifica parametri
pentru distributie si o functie draw care returneaza valoarea "urmatoare".
Recomandare: <task.h>. Vezi de asemenea clasa intset.
9. (*2). Sa se rescrie exemplul date (&5.2.2) exemplul char_stack (&5.2.5)
si exemplul intset (&5.3.2) fara a utiliza functii membru (nici chiar constructori
si destructori). Sa se utilizeze numai class si friend. Sa se testeze versiunile
noi. Sa se compare cu versiunile care utilizeaza functiile membru.
10. (*3). Sa se proiecteze o clasa pentru o tabela de simboluri si o clasa de
intrare in tabela de simboluri pentru un anumit limbaj. Sa aruncam o privire
la compilatorul limbajului respectiv pentru a vedea cum arata tabela de simboluri
reala.
11. (*2). Sa se modifice clasa expresie din exercitiul 5 pentru a trata variabile
si operatorul de asignare =. Sa se foloseasca clasa tabela de simboluri din
exercitiul 10.
12. (*1). Fiind dat programul:
#include <stream.h> main()
A cout << "Hello, word\n";
S sa se modifice pentru a avea la iesire:
Initialize
Hello, world
Clean up
Sa nu se modifice functia main().