CLASE
Acest capitol descrie facilitatile pentru a defini tipuri noi pentru care accesul la date este restrins la un set specific de functii de acces. Sint explicitate modurile in care o data structurata poate fi protejata, initializata, accesata si in final eliminata. Exemplele includ clase simple utilizate pentru gestiunea tabelei de simboluri, manipularea stivei, manipularea multimilor si implementarea unei reuniuni "incapsulate".
5.1 Introducere si privire generala
Scopul conceptului de clasa C++ este de a furniza programatorului un instrument pentru a crea tipuri noi care pot fi folo- site 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 telefoa 858y2424i nele 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 tipu-lui) de proprietatile esentiale ale utilizarii lui corecte (de exemplu, lista completa de functii care pot avea acces la date). O astfel de separare poate fi exprimata prin canalizarea tuturor utilizarilor datelor structurii si a rutinelor de memorare interna printr-o interfata specifica.
Acest capitol consta din 4 parti separate:
&5.2 Clase si Membri. Aceasta sectiune introduce notiunea de baza: tip definit de utilizator numita clasa.
Accesul la obiectele unei clase se poate restringe la un set de functii declarate ca o parte a clasei; astfel de functii se numesc functii membru. Obiectele unei clase pot fi create si initializate prin functii membru declarate in mod specific pentru acest scop: astfel de functii se numesc constructori. O functie membru poate fi declarata pentru a "sterge" un astfel de obiect al unei clase cind el este distrus; o astfel de functie se numeste destructor.
&5.3 Interfete si Implementari. Aceasta sectiune prezinta doua exemple de modul in care pot fi proiectate, implementate si utilizate clasele.
&5.4 Prieteni si Reuniuni. Aceasta sectiune prezinta multe detalii suplimentare despre clase. Arata cum se face accesul la partile private ale unei clase si cum se poate admite accesul pentru o functie care nu este membru al acelei clase. O astfel de functie se numeste prieten. Aceasta sectiune de asemenea arata cum se defineste o reuniune distinctiva.
&5.5 Constructori si Destructori. Un obiect poate fi creat ca un obiect automatic, static sau in memoria libera. Un obiect, de asemenea, poate fi un membru al unui anumit agregat (o clasa sau un vector), care la rindul lui poate fi alocat in una din cele 3 moduri indicate mai sus. Utilizarea constructorilor si destructorilor se explica in detaliu.
5.2 Clase si Membri
Clasa este un tip definit de utilizator. Aceasta sectiune introduce facilitatile de baza pentru a defini o clasa, crearea obiectelor unei clase, manipularea acestor obiecte si in final stergerea acestor obiecte dupa utilizare.
5.2.1 Functii membru
Sa consideram implementarea conceptului de data utilizind o structura pentru a defini reprezentarea unei date si un set de functii pentru manipularea variabilelor de acest tip:
struct date;
date today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(date*);
Nu exista conexiuni explicite intre functii si tipul datei. O astfel de conexiune se poate stabilii declarind functiile ca membri:
struct date;
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()
Intrucit diferite structuri pot avea functii membru cu ace-
lasi nume, trebuie sa se specifice numele structurii cind se
defineste o functie membru:
void date::next()
}
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;
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
Cu toate acestea, functiile care nu sint membru nu pot folosi membri privati ai clasei date. De exemplu:
void backdate()
Exista citeva beneficii in urma restringerii accesului, la o data structurata, la o lista de functii declarata explicit. Orice eroare care face ca date sa aiba o valoare ilegala (de exemplu December 36, 1985) trebuie sa fie cauzata de codul unei functii membru, asa ca primul stadiu al depanarii, localizarea, este rezolvat inainte ca programul sa se execute. Acesta este un caz special al observatiei generale ca orice schimbare in comportarea tipului date poate, si trebuie sa fie efectuata prin schimbarea membrilor lui. Un alt avantaj este ca un utilizator de un astfel de tip este necesar numai sa examineze definitia functiilor membru pentru a invata utilizarea lui.
Protectia datelor private se bazeaza pe restrictia utilizarii numelor membru ale clasei. Se poate trece peste aceasta prin manipularea de adrese si conversie explicita de tip, dar aceasta evident este un fel de inselatorie.
5.2.3 Autoreferinta
Intr-o functie membru, ne putem referi direct la membri unui obiect pentru care functia membru este apelata. De exemplu:
class x
x aa;
x bb;
void f()
}
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
};
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;
void dlink::append(dlink* p)
dlink* list_head;
void f(dlink* a, dlink* b)
Legaturile de aceasta natura generala sint baza pentru cla- sele lista descrise in capitolul 7. Pentru a adauga o legatura la o lista, trebuie puse la zi obiectele spre care pointeaza this, pre si suc. Ele toate sint de tip dlink, asa ca functia membru dlink::append() poate sa faca acces la ele. Unitatea de protectie in C++ este clasa, nu un obiect individual al unei clase.
5.2.4 Initializare
Utilizarea functiilor de felul set_data() pentru a furniza initializarea pentru obiectele clasei nu este eleganta si este inclinata spre erori. Intrucit nicaieri nu se afirma ca un obiect trebuie initializat, un programator poate uita sa faca acest lucru sau (adesea cu rezultate dezastruoase) sa faca acest lucru de doua ori. O conceptie mai buna este de a permite programatorului sa declare o functie cu scopul explicit de a initializa obiecte. Deoarece o astfel de functie construieste valori de un tip dat, ea se numeste constructor. Un constructor se recunoaste deoarece are acelasi nume ca si clasa insasi. De exemplu:
class date;
Cind o clasa are un constructor, toate obiectele acelei clase vor fi initializate. Daca constructorul cere argumente, ele pot fi furnizate:
date today = date(23, 6, 1983);
date xmas(25, 12, 0); //forma prescurtata
date my_birthday: //ilegal, lipseste initializarea
Este adesea util sa se furnizeze diferite moduri de initializare a obiectelor unei clase. Aceasta se poate face furnizind diferiti constructori. De exemplu:
class date;
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;
date::date(int d, int m, int y)
Cind se utilizeaza o valoare pentru un argument pentru a indica "ia valoarea implicita", valoarea aleasa trebuie sa fie in afara setului posibil de valori pentru argument. Pentru zi si luna este clar acest lucru, dar valoarea zero pentru an poate sa nu fie o alegere evidenta. Din fericire nu exista anul zero in calendarul european. 1AD(year == 1) vine imediat dupa 1BC(year == -1), dar aceasta probabil ar fi prea subtil pentru un program real.
Un obiect al unei clase fara constructori poate fi initializat atribuindu-i un alt obiect al acelei clase. Aceasta se poate face, de asemenea, cind constructorii au fost declarati. De exemplu:
date d = today; //initializare prin asignare
In esenta, exista un constructor implicit ca o copie de biti a obiectelor din aceeasi clasa. Daca nu este dorit acest constructor implicit pentru clasa X, el poate fi redefinit prin constructorul denumit X(X&) (aceasta se va discuta mai departe in &6.6).
5.2.5 Curatire (stergere)
Mai frecvent este cazul in care un tip definit de utilizator are un constructor pentru a asigura initializarea proprie. Multe tipuri necesita, de asemenea, un destructor, care sa asigure stergerea obiectelor de un tip. Numele destructorului pentru clasa X este ~X() ("complementul constructorului"). In particular, multe clase utilizeaza memoria libera (vezi &3.2.6) ce se aloca printr-un constructor si se dealoca printr-un destructor. De exemplu, iata un tip de stiva conventionala care a fost complet eliberata de tratarea erorilor pentru a o prescurta:
class char_stack
~char_stack()
void push(char c)
char pop()
};
Cind char_stack iese in afara domeniului, se apeleaza destructorul:
void f()
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 me- morie 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
inline char char_stack::pop()
5.3 Interfete si Implementari
Ce face o clasa buna? Ceva ce are un set mic si bine definit de operatori. Ceva ce poate fi vazut ca o "cutie neagra" manipulata exclusiv prin acel set de operatii. Ceva a carei reprezentare reala ar putea fi conceputa sa fie modificata fara a afecta modul de utilizare a acelui set de operatii.
Containerele de toate felurile furnizeaza exemple evidente: tabele, multimi, liste, vectori, dictionare, etc.. O astfel de clasa va avea o operatie de inserare, care de obicei va avea de asemenea operatii pentru a verifica daca un membru specific a fost inserat, poate va avea operatii pentru sortarea membrilor, poate va avea operatii pentru examinarea tuturor membrilor intr-o anumita ordine si in final ar putea, de asemenea, sa aiba o operatie pentru eliminarea unui membru. Clasele container de obicei au constructori si destructori.
Ascunderea datelor si o interfata bine definita pot fi de asemenea obtinute prin conceptul de modul (vezi de exemplu, &4.4: fisiere ca module). Cu toate acestea, o clasa este un tip; pentru a o utiliza, trebuie sa se creeze obiecte ale clasei respective si se pot crea atit de multe astfel de obiecte cite sint necesare. Un modul este el insusi un obiect; pentru a-l utiliza, cineva este necesar sa-l initializeze si exista exact un astfel de obiect.
5.3.1 Implementari alternative
Atita timp cit declaratia partii publice a unei clase si declaratia functiilor membru ramin neschimbate, implementarea unei clase poate fi schimbata fara a afecta utilizatorii ei. Sa consideram o tabela de simboluri de felul celei utilizate pentru calculatorul de birou din capitolul 3. Este o tabela de nume:
struct name;
Iata o versiune a clasei tabela:
//file table.h:
class table
name* look(char*, int=0);
name* insert(char* 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()
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)
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)
};
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)
table::~table()
}
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)
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 recu- perat de multe ori in dezvoltarea si testarea unui program. Iata un exemplu de tip complet, intset, care furnizeaza conceptul de "multime de intregi".
class intset
int ok(int& i)
int next(int& i)
};
Pentru a testa aceasta clasa noi putem crea si apoi imprima un set de intregi aleatori. Un astfel de set ar putea constitui niste numere de loterie. Acest set simplu ar putea fi utilizat pentru a verifica un sir de intregi punind in evidenta duplicatele, dar pentru majoritatea aplicatiilor tipul set ar trebui sa fie putin mai migalos elaborat. Ca totdeauna sint posibile erori:
#include <stream.h>
void error(char* s)
Clasa intset se utilizeaza in functia main() care asteapta doua argumente intregi. Primul argument specifica numarul de numere aleatoare de generat. Cel de al doilea argument specifica domeniul intregilor aleatori care se asteapta:
main(int argc, char* argv[])
}
print_in_order(&s);
}
Motivul ca argumentul numarator argc sa fie 3 pentru un program care cere 2 argumente este faptul ca numele programului este totdeauna pasat ca argv[0]. Functia:
extern int atoi(char*);
este o functie standard de biblioteca pentru covertirea reprezentarii sub forma de sir a unui intreg in forma lui interna binara.
Numerele aleatoare se genereaza utilizind functia standard rand():
extern int rand(); //nu este prea aleatoare
int randint(int n) //in domeniul 1..n
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
intset::~intset()
Intregii se insereaza asa ca ei sa fie tinuti in ordine cresca- toare in multime:
void intset::insert(int t)
}
Se foloseste o cautare binara pentru a gasi un membru:
int intset::member(int t) //cautare binara
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
int ok(int& i)
int next(int& i)
};
Pentru a permite ca aceste trei operatii sa coopereze si sa reaminteasca cit de departe a progresat iteratia, utilizatorul trebuie sa furnizeze un argument intreg. Intrucit argumentele sint pastrate intr-o lista sortata, implementarea lor este triviala. Acum poate fi definita functia print_in_order:
void print_in_order(intset* set)
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)
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;
class matrix;
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)
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;
class y;
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;
Aceasta declaratie, friend, face ca toate functiile membre ale clasei y sa fie prietene ale clasei x.
5.4.2 Calificarea numelor de membri
Ocazional, este util sa se faca distinctie explicita intre numele membre ale unei clase si alte nume. Se poate folosi operatorul de rezolutie a domeniului "::":
class x
void setm(int 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;
int my_file::open(char* name, char* spec)
//...........
}
5.4.3 Clase imbricate
Declaratiile de clasa pot fi imbricate. De exemplu:
class set
};
setmem* first;
public:
set()
insert(int m)
//.......
};
Daca clasa imbricata nu este foarte simpla, astfel de declaratii sint foarte incurcate. Mai mult decit atit, clasele imbricate sint mai mult o facilitate in notatie, intrucit o clasa imbricata nu este ascunsa in domeniul clasei care o include din punct de vedere lexical:
class set;
//.......
};
setmem::setmem(int m, setmem* 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
};
class set
insert(int m)
};
5.4.4 Membri statici
O clasa este un tip, nu un obiect data si fiecare obiect al clasei are copia lui proprie a membrilor date ai clasei. Cu toate acestea, unele tipuri sint implementate mai elegant daca toate obiectele acelui tip au in comun unele date. Este preferabil ca o astfel de data comuna sa fie declarata ca parte a clasei. De exemplu, pentru a gestiona taskuri intr-un sistem de operare, este adesea utila o lista a tuturor taskurilor:
class task;
Declarind membrul task_chain ca static se asigura ca va fi numai o copie a lui, nu o copie pentru fiecare obiect task. Este inca in domeniul clasei task si poate fi accesat "din afara" numai daca a fost declarat public. In acest caz, numele lui tre- buie sa fie calificat prin numele clasei sale:
task::task_chain
Intr-o functie membru, se poate face referire prin task_chain. Utilizarea membrilor statici ai clasei poate reduce considerabil necesarul de memorie pentru variabilele globale.
5.4.5 Pointeri spre membri
Este posibil sa se ia adresa unui membru al unei clase. A lua adresa unei functii membru este adesea util intrucit tehnicile si motivele pentru a utiliza pointeri la functii prezentate in &4.6.9 se aplica in mod egal si la functii membru. Totusi exista un defect curent in limbaj: nu este posibil sa se exprime tipul pointerului obtinut dintr-o astfel de operatie. In consecinta trebuie sa folosim trucuri folosind avantajele din implementarea curenta. Exemplul de mai jos nu este garantat ca fun- ctioneaza si utilizarea lui trebuie localizata in asa fel incit sa poata fi usor convertit spre a utiliza constructiile propri ale limbajului. Trucul folosit este acela de a avea avantajul faptului ca this este implementat curent ca primul argument (ascuns) al unei functii membru.
#include <stream.h>
struct cl
cl(char* v)
};
//"se ia" tipul functiilor membru:
typedef void (*PROC)(void*, int);
main()
In multe cazuri, functiile virtuale (vezi capitolul 7) pot fi utilizate cind altfel s-ar utiliza pointeri spre functii.
Versiunile ulterioare de C++ vor suporta un concept de pointer spre un membru: cl::* inseamna "pointer spre un membru a lui cl". De exemplu:
typedef void(cl::*PROC)(int);
PROC pf1=&cl::print;//nu este nevoie de conversie explicita
PROC pf2 = &cl::print;
Operatorii . si -> se utilizeaza pentru un pointer spre o functie membru. De exemplu:
(z1.*pf1)(2);
((&z2)->*pf2)(4);
5.4.6 Structuri si Reuniuni
Prin definitie o structura este pur si simplu o clasa cu toti membri publici, adica:
struct s;
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)
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
tok_value(double 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()
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)
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;
int check(char t, char* s)
return 1;
}
public:
tok_val(char* pp);
tok_val(long ii)
tok_val(double dd)
long& ival()
double& fval()
char*& sval()
char* id()
};
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)
else
}
Tipul tok_val poate fi folosit astfel:
void f()
5.5 Constructori si Destructori
Cind o clasa are un constructor, el este apelat ori de cite ori se creaza un obiect al acelei clase. Cind o clasa are un destructor, el este apelat ori de cite ori este distrus un obiect al acelei clase. Obiectele pot fi create ca:
[1] Un obiect automatic: se creaza de fiecare data cind se intilneste declaratia lui la executia programului si este distrus de fiecare data cind se iese din blocul in care el a aparut;
[2] Un obiect static: se creaza o data la pornirea programului si se distruge o data cu terminarea programului;
[3] Un obiect in memoria libera: este creat folosind operatorul new si distrus folosind operatorul delete;
[4] Un obiect membru: ca membru al unei clase ori ca un element de vector.
Un obiect poate de asemenea, sa fie construit intr-o expresie prin folosirea explicita a unui constructor (&6.4), caz in care el este un obiect automatic. In subsectiunile care urmeaza se presupune ca obiectele sint ale unei clase cu un constructor si un destructor. Ca exemplu se utilizeaza clasa table din &5.3.
5.5.1 Goluri
Daca x si y sint obiecte ale clasei cl, x=y inseamna copie- rea 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
~char_stack() //destructor
void push(char c)
char pop()
};
void h()
Aici constructorul char_stack::char_stack() se apeleaza de doua ori: pentru s1 si s3. Nu se apeleaza pentru s2 deoarece variabila s2 a fost initializata prin atribuire. Totusi, destructorul char_stack::~char_stack() se apeleaza de trei ori: pentru s1, s2 si s3. Mai mult decit atit, interpretarea implicita a atribuirii ca si copiere de biti face ca s1, s2 si s3 sa contina fiecare la sfirsitul lui h() un pointer spre vectorul de caractere alocat in memoria libera cind a fost creat s1. Nu va ramine nici un pointer spre vectorul de caractere alocate cind a fost creat s3. Astfel de anomalii pot fi eliminate asa cum se va vedea in capitolul 6.
5.5.2 Memoria statica
Consideram:
table tbl1(100);
void f()
main()
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)
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()
Constructorul table::table() va fi apelat de doua ori si la fel si destructorul table::~table(). Este bine de amintit ca C++ nu ofera garantie ca un destructor este apelat vreodata pentru un obiect creat folosind new. Programul precedent nu il sterge pe q, dar pe p il sterge de doua ori. In functie de tipul lui p si q, programatorul poate sau nu sa considere aceasta ca o eroare. Ne- stergind un obiect de obicei nu este o eroare, ci numai o pierdere de spatiu. Stergind p de doua ori este de obicei o eroare serioasa. Un rezultat frecvent al aplicarii lui delete de doua ori la acelasi pointer este un ciclu infinit in rutina de gestionare a memoriei libere, dar comportamentul in acest caz nu este specificat prin definitia limbajului si depinde de implementare.
Utilizatorul poate defini o implementare noua pentru operatorii new si delete (vezi &3.2.6). Este de asemenea posibil sa se specifice modul in care interactioneaza constructorul si destructorul cu operatorii new si delete (vezi &5.5.6).
5.5.4 Obiectele clasei ca membri
(clase de obiecte ca membri)
Consideram:
class classdef;
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)
Argumentele pentru un constructor membru (table::table()) se plaseaza in definitia (nu in declaratia) constructorului clasei care il contine (aici classdef::classdef()). Constructorul membru este apoi apelat inaintea corpului constructorului care specifica lista argumentelor lui.
Daca sint mai multi membri ce necesita liste de argumente pentru constructori, ei pot fi specificati in mod analog. De exemplu:
class classdef;
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)
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
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)
si dimensiunea lui friends table va fi 15.
Cind o clasa care contine clase (de exemplu classdef) se distruge, intii se executa corpul destructorului propriu acelei clase si apoi se executa destructorii membrilor.
Consideram varianta traditionala de a avea clase ca membri si anume aceea de a avea membri pointeri si ai initializa pe acestia intr-un constructor:
class classdef;
classdef::classdef(int size)
Intrucit tabelele au fost create folosind new, ele trebuie sa fie distruse utilizind delete:
classdef::~classdef()
Obiectele create separat ca acestea pot fi utile, dar sa observam ca members si friends pointeaza spre obiecte separate care cer o alocare si o dealocare fiecare. Mai mult decit atit, un pointer plus un obiect in memoria libera ia mai mult spatiu decit un obiect membru.
5.5.5 Vectori si Obiecte clasa
Pentru a declara un vector de obiecte ale unei clase cu un constructor acea clasa trebuie sa aiba un constructor care sa poata fi apelat fara o lista de argumente. Nici argumentele implicite nu pot fi utilizate. De exemplu:
table tblvec[10];
este o eroare deoarece table::table() necesita un argument intreg. Nu exista nici un mod de a specifica argumente pentru un constructor intr-o declaratie de vector. Pentru a permite declararea vectorilor de tabele, ar putea fi modificata declaratia clasei table (&5.3.1) astfel:
class table //ca inainte dar nu
//exista valoare implicita
table() //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()
In acest caz programatorul trebuie sa furnizeze dimensiunea vectorului:
void g(int sz)
Dar de ce nu poate compilatorul sa deduca numarul de elemente din cantitatea de memorie alocata? Deoarece alocatorul de memorie libera nu este o parte a limbajului si ar putea fi furnizata de programator.
5.5.6 Obiecte mici
Cind se utilizeaza multe obiecte mici alocate in memoria libera, noi putem sa aflam ca programul consuma timp considerabil pentru alocare si dealocare de astfel de obiecte. O solutie este de a furniza un alocator cu scopuri generale mai bun si o a doua este ca proiectarea unei clase sa nu se faca pentru a fi gestionata in memoria libera, definind constructori si destructori.
Sa consideram clasa name folosita in exemplul table. Ea ar putea fi definita astfel:
struct name;
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)
this = p;
string = s; //initializare
value = v;
next = n;
}
Atribuirea la this informeaza compilatorul ca programatorul a luat controlul si ca mecanismul implicit de alocare de memorie nu trebuie sa fie utilizat. Constructorul name::name() trateaza cazul in care numele este alocat numai prin new, dar pentru multe tipuri acesta este de obicei cazul; &5.5.8 explica cum se scrie un constructor pentru a trata atit memoria libera, cit si alte tipuri de alocari.
Sa observam ca spatiul nu ar putea fi alocat pur si simplu astfel:
name* q = new name[NALL];
intrucit aceasta ar cauza o recursivitate infinita cind new apeleaza name::name().
Dealocarea este de obicei triviala:
name::~name()
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)
;
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)
;
Nu exista o facilitate echivalenta care sa permita unui destructor sa decida daca obiectele lui au fost create folosind new si nici o facilitate care sa permita sa se decida daca el a fost apelat prin delete sau printr-un obiect din afara domeniului. Daca cunoasterea acestui lucru este importanta, utilizatorul poate memora undeva informatii corespunzatoare pe care sa le citeasca destructorul. O alta varianta este ca utilizatorul sa se asigure ca obiectele acelei clase sint numai alocate in mod corespunzator. Daca prima problema este tratata, ultima este neinteresanta.
Daca implementatorul unei clase este de asemenea numai utilizatorul ei, este rezonabil sa se simplifice clasa bazindu-ne pe presupunerile despre utilizarea ei. Cind o clasa este proiectata pentru o utilizare larga, astfel de presupuneri este adesea mai bine sa fie eliminate.
5.5.8 Obiecte de dimensiune variabila
Luind controlul asupra alocarii si dealocarii, utilizatorul poate de asemenea, construi obiecte a caror dimensiune nu este determinata la momentul compilarii. Exemplele precedente de implementare a claselor container vector, stack, insert si table ca dimensionate fix, acceseaza direct structuri care contin pointeri spre dimensiunea reala. Aceasta implica faptul ca sint necesare doua operatii de creare de astfel de obiecte in memoria libera si ca orice acces la informatiile memorate va implica o indirectare suplimentara. De exemplu:
class char_stack
~char_stack() //destructor
void push(char c)
char pop()
};
Daca fiecare obiect al unei clase este alocat in memoria libera, aceasta nu este necesar. Iata o alternativa:
class char_stack
char pop()
};
char_stack::char_stack(int sz)
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 capitolul3 pentru a utiliza clasa table.
2. (*1). Sa se proiecteze tnode (&r8.5) ca o clasa cu consructori, destructori, etc.. Sa se defineasca un arbore de tnodes ca o clasa cu constructori, destructori, etc..
3. (*1). Sa se modifice clasa intset (&5.3.2) intr-o multime de siruri.
4. (*1). Sa se modifice clasa intset intr-o multime de noduri unde node este o structura pe care sa o definiti.
5. (*3). Se defineste o clasa pentru analizarea, memorarea, evaluarea si imprimarea expresiilor aritmetice simple care constau din constante intregi si operatiile '+', '-', '*' si '/'. Interfata publica ar trebui sa arate astfel:
class expr;
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()
sa se modifice pentru a avea la iesire:
Initialize
Hello, world
Clean up
Sa nu se modifice functia main().
|