Acest capitol descrie conceptul de clasa derivata din C++. Clasele derivate
furnizeaza un mecanism simplu, flexibil si eficient, pentru a specifica o interfata
alternativa pentru o clasa si pentru a defini o clasa adaugind facilitati la
o clasa existenta fara a reprograma sau recompila. Utilizind clasele derivate,
se poate furniza de asemenea, o interfata comuna pentru diferite clase asa ca
obiectele acelor clase sa poata fi manipulate identic in alte parti ale unui
program. Aceasta de obicei implica plasarea informatiilor de tip in fiecare
obiect asa ca astfel de obiecte sa poata fi utilizate corespunzator in contextele
in care tipul nu poate fi cunoscut la compilare; se da con- ceptul de functie
virtuala pentru a trata astfel de dependente de tip precaut si elegant. In principiu,
clasele derivate exista pentru a face mai usor unui programator sa exprime partile
comune.
7.1 Introducere
Consideram scrierea unor facilitati generale (de exemplu o lista inlantuita,
o tabela de simboluri, un sistem de simulare) in intentia de a fi utilizate
de multa lume in contexte diferite. Evident nu sint putini candidati pentru
astfel de beneficii de a le avea standardizate. Fiecare programator experimentat
se pare ca a scris (si a testat) o duzina de variante pentru tipurile multime,
tabela de hashing, functii de sortare, etc., dar fiecare programator si fiecare
program pare ca are o versiune separata a acestor concepte, facind programul
greu de citit, greu de verificat si greu de schimbat. Mai mult decit atit, intr-un
program mare ar putea foarte bine sa fie copii de coduri identice (sau aproape
identice) pentru a trata astfel de concepte de baza. m6y24yo
Motivatia pentru acest haos este in parte faptul ca conceptual este dificil
sa se prezinte facilitati atit de generale intr-un limbaj de programare si partial
din cauza ca facilitatile de generalitate mare de obicei impun depasiri de spatiu
si/sau timp, ceea ce le face nepotrivite pentru cele mai simple facilitati utilizate
(liste inlantuite, vectori, etc.) unde ele ar trebui sa fie cele mai utile.
Conceptul C++ de clasa derivata, prezentat in &7.2 nu furnizeaza o solutie
generala pentru toate aceste probleme, dar furnizeaza un mod de a invinge unele cazuri speciale importante. De exemplu,
se va arata cum se defineste o clasa de liste inlantuite generica si eficienta,
asa ca toate versiunile ei sa aiba cod comun. Scrierea facilitatilor de uz general
nu este triviala, iar aspectele proiectarii este adesea ceva diferit de aspectele
proiectarii unui program cu scop special. Evident, nu exista o linie bine definita
care sa faca distinctie intre facilitatile cu scop general si cele cu scop special,
iar tehnicile si facilitatile limbajului prezentat in acest capitol pot fi vazute
ca fiind din ce in ce mai utile pe masura ce dimensiunea si complexitatea programului
creste.
7.2 Clase derivate
Pentru a separa problemele de intelegere a mecanismelor limbajului si tehnicile
pentru a le utiliza, conceptul de clasa derivata se introduce in trei stadii.
Intii, facilitatile limbajului (notatia si semantica se vor descrie folosind
exemple mici care nu intentioneaza sa fie reale). Dupa aceasta, se demonstreaza
niste clase derivate netriviale si in final se prezinta un program complet.
7.2.1 Derivare
Consideram construirea unui program care se ocupa cu angajatii unei firme.
Un astfel de program ar putea avea o structura de felul: struct employeeA char* name; short age; short departament; int salary; employee* next;
//.......
S;
Cimpul next este o legatura intr-o lista pentru date employee similare. Acum
vrem sa definim structura manager: struct managerA employee emp; //angajatii manager employee* group;
//...
S;
Un manager este de asemenea un angajat (employee); datele angajatului se memoreaza
in emp care este un membru al obiectului manager. Aceasta poate fi evident pentru
un cititor uman, dar nu exista nimic care sa distinga membri emp. Un pointer
spre un ma- nager (manager*) nu este un pointer spre un employee (employee*),
asa ca nu se pot utiliza unul in locul celuilalt. In particular, nu se poate
pune un manager intr-o lista de angajati fara a scrie cod special. Se poate
sau utiliza tipul de conversie explicit spre manager* sau sa se puna adresa
membrului emp intr-o lista de angajati, dar ambele sint neelegante si pot fi
obscure. Conceptia corecta este de a afirma ca un manager este un employee cu
citeva informatii adaugate: struct manager : employeeAemployee* group;
//.......
S;
Manager este derivat din employee si invers, employee este o clasa de baza pentru
manager. Clasa manager are membri clasei employee (name, age, etc.) in plus
fata de membrul group.
Cu aceasta definitie a lui employee si manager, noi putem crea acum o lista
de employee, din care unii sint manageri. De exemplu: void f()
A manager m1, m2; employee e1, e2; employee* elist; elist = &m1; //se pune m1, e1, m2, e2 in lista m1.next = &e1; e1.next = &m2; m2.next = &e2; e2.next = 0;
S
Intrucit un manager este un employee, un manager* poate fi utilizat ca un employee*.
Dar un employee nu este in mod necesar un manager, asa ca un employee* nu poate
fi utilizat ca un mana- ger*. Aceasta se explica in detaliu in &7.2.4.
7.2.2. Functii membru
Structurile de date simple, cum ar fi employee si manager, sint in realitate
neinteresante si adesea nu sint utile in mod special, asa ca, sa consideram
adaugarea de functii la ele. De exemplu: class employeeA char* name;
//...... public: employee* next; void print();
//......
S; class manager : public employeeA
//...... public: void print();
//......
S;
Trebuie sa se raspunda la niste intrebari. Cum poate o functie membru al clasei
derivate manager sa utilizeze membri clasei de baza employee ? Ce membri ai
clasei de baza employee poate utiliza o functie nemembru dintr-un obiect de
tip manager ? In ce mod poate afecta programatorul raspunsul la aceste probleme
?
Consideram: void manager::print()A cout << "name is:" << name <<
"\n"; S
Un membru al unei clase derivate poate utiliza un nume public al clasei de baza
propri in acelasi mod ca si alti membri, adica fara a specifica un obiect. Se
presupune obiectul spre care pointeaza this, asa ca numele (corect) se refera
la this->name. Cu toate acestea, functia manager::print() nu se va compila;
un membru al clasei derivate nu are permisiunea speciala de a face acces la
un membru privat din clasa lui de baza, asa ca functia nu are acces la name.
Aceasta este o surpriza pentru multi, dar sa consideram varianta ca o functie
membru ar putea face acces la membri privati ai clasei sale de baza. Conceptul
de membru privat ar deveni lipsit de sens prin facilitatea care ar permite unui
programator sa cistige acces la partea privata a unei clase pur si simplu prin
derivarea unei clase noi din ea. Mai mult decit atit, s-ar putea sa nu se mai
gaseasca toti utilizatorii unui nume privat uitindu-ne la functiile declarate
ca membri si prieteni ai acelei clase. Ar trebui sa se examineze fiecare fisier
sursa al programului complet pentru clase derivate, apoi sa se examineze fiecare
functie din acele clase, apoi sa se gaseasca fiecare clasa derivata din aceste
clase, etc.. Aceasta este impractic.
Pe de alta parte, este posibil sa se utilizeze mecanismul friend pentru a admite
astfel de accese pentru functii specifice sau pentru orice funcie a unei clase
specifice (asa cum s-a des- cris in &5.3). De exemplu: class employeeA friend void manager::print();
//.......
S;
ar rezolva problema pentru manager::print(), iar clasa: class employeeA friend class manager;
//.......
S;
ar face ca orice membru al clasei employee sa fie accesibil pentru orice functie
din clasa manager. In particular, se face ca name sa fie accesibil pentru manager::print().
O alta alternativa, uneori mai clara, este ca clasa derivata sa utilizeze numai
membri publici ai clasei de baza propri. De exemplu: void manager::print()
A employee::print(); //imprima informatie employee
//........ //imprima informatie manager
S
Sa observam ca operatorul :: trebuie utilizat deoarece fun- ctia print() a
fost redefinita in manager. O astfel de reutilizare a unui nume este tipica.
Un neprecaut ar putea scrie: void manager::print()
A print(); //imprima informatie employee
//........ //imprima informatie manager
S
si ar gasi ca programul este o secventa nedorita de apeluri recursive cind
se apeleaza manager::print().
7.2.3 Vizibilitate
Clasa employee a fost facuta o clasa de baza publica prin declaratia: class manager : public employeeA /* ... */ S;
Aceasta inseamna ca un membru public al clasei employee este de asemenea un
membru public al clasei manager. De exemplu: void clear(manager* p)A p->next = 0; S se va compila deoarece next este un membru public atit al lui employee cit si
al lui manager. Lasind la o parte din declaratie cuvintul public se poate defini
o clasa derivata privata: class manager : employeeA /* ... */ S
Aceasta inseamna ca un membru public al clasei employee este un membru privat
al clasei manager. Adica, membri functiilor manager pot utiliza membri publici
ai lui employee ca inainte, dar acesti membri nu sint accesibili utilizatorilor
clasei manager. In par- ticular, dindu-se aceasta declaratie de manager, functia
clear() nu se va compila. Prietenii unei clase derivate au acelasi acces la
membri clasei de baza ca si functiile membru. Declaratia public a claselor de
baza este mai frecventa decit declaratia private, ceea ce este pacat pentru
ca declaratia unei clase de baza publice este mai lunga decit una privata. De
asemenea, este o sursa de erori pentru incepatori.
Cind este declarata o structura, clasa ei de baza este im- plicit o clasa de
baza publica. Adica: struct D : BA /* ... */ S inseamna class D : public BA public: /* ... */ S
Aceasta implica faptul ca daca noi nu gasim data ascunsa furnizata de utilizarea
lui class, public si friends, ca fiind utile, atunci noi putem pur si simplu
elimina aceste cuvinte si sa ne referim la struct. Facilitatile limbajului,
cum ar fi functiile membru, constructorii si operatorii de supraincarcare sint
independente de mecanismul de pastrare a datelor. Este posibil de asemenea sa
se declare unii din membri publici (dar nu toti) ai unei clase de baza public
ca membri ai unei clase derivate. De exemplu: class manager : employeeA
//....... public:
//....... employee::name; employee::departament;
S;
Notatia: class_name::member_name; nu introduce un membru nou ci pur si simplu face un membru public al unei clase
de baza private pentru o clasa derivata. Acum name si departament pot fi utilizate
pentru un manager, dar salary si age nu pot fi utilizate. Natural, nu este posibil
de a face ca un membru privat al unei clase de baza sa devina un membru public
al unei clase derivate. Nu este posibil sa se faca publice numele supraincarcate
utilizind aceste notatii. Pentru a rezuma, o clasa derivata alaturi de furnizarea
caracteristicilor suplimentare aflate in clasa ei de baza, ea poate fi utilizata
pentru a face ca nume ale unei clase sa nu fie accesibile utilizatorului. Cu
alte cuvinte, o clasa derivata poate fi utilizata pentru a furniza acces transparent,
semitransparent si netransparent la clasa ei de baza.
7.2.4 Pointeri
Daca o clasa derivata are o clasa de baza (base) publica, atunci un pointer
spre clasa derivata poate fi asignat la o variabila de tip pointer spre clasa
base fara a utiliza explicit tipul de conversie. O conversie inversa de la un
pointer spre base la un pointer spre derived trebuie facuta explicit. De exemplu:
class baseA /* ... */ S; class derived : public baseA /* ... */ S; derived m; base* pb = &m; //conversie implicite derived* pd = pb; //eroare: un base* nu este un derived* pd =(derived*)pb; //conversie explicita
Cu alte cuvinte, un obiect al unei clase derivate poate fi tratat ca un obiect
al clasei de baza propri cind se manipuleaza prin pointeri. Inversul nu este
adevarat. Daca base ar fi fost o clasa privata de baza, conversia implicita
a lui derived* spre base* nu se face. O conversie implicita nu se poate face
in acest caz deoarece un membru public a lui base poate fi accesat printr-un
pointer la base, dar nu printr-un pointer la derived: class baseA int m1; public: int m2; //m2 este un membru public a lui base
S; class derived : baseA
//m2 nu este un membru public al lui derived
S; derived d; d.m2 = 2; //eroare: m2 este din clasa privata base base* pb = &d; //eroare (base este privata) pb->m2 = 2; //ok pb = (base*)&d; //ok: conversie explicita pb->m2 = 2; //ok
Printre altele, acest exemplu arata ca utilizind conversia explicita noi putem
incalca regulile de protectie. Aceasta evident nu este recomandabil si facind
aceasta de obicei programatorul cistiga o "recompensa". Din nefericire,
utilizarea nedisciplinata a conversiei explicite poate de asemenea crea un iad
pentru victime inocente mentinind un program care sa le contina. Din fericire,
nu exista nici un mod de utilizare a conversiei explicite care sa permita utilizarea
numelui privat m1. Un membru privat al unei clase poate fi utilizat numai de
membri si prieteni ai acelei clase.
7.2.5 Ierarhizarea claselor
O clasa derivata poate fi ea insasi a clasa de baza. De exemplu: class employeeA /* ... */ S; class secretary : employeeA /* ... */ S; class
manager : employeeA /* ... */ S; class temporary : employeeA /* ... */ S; class
consultant : temporaryA /* ... */ S; class director : managerA /* ... */ S;
class vice_president : managerA /* ... */ S; class president : vice_presidentA
/* ... */ S;
O multime de clase inrudite se numeste traditional o ierar- hie de clase. Intrucit
se poate deriva o clasa dintr-o singura clasa de baza, o astfel de ierarhie
este un arbore si nu poate fi o structura mai generala de graf. De exemplu: class temporaryA /* ... */ S; class employeeA /* ... */ S; class secretary : employeeA /* ... */ S;
//nu in C++ class temporary_secretary : temporary : secretaryA /* ... */ S; class consultant : temporary : employeeA /* ... */ S;
Aceasta este pacat, intrucit un graf aciclic orientat al unei clase derivate
poate fi foarte util. Astfel de structuri nu pot fi declarate, dar pot fi simulate
utilizind membri de tipuri corespunzatoare. De exemplu: class temporaryA /* ... */ S; class employeeA /* ... */ S; class secretary : employeeA /* ... */ S;
//Alternative class temporary_secretary : secretaryA temporary temp;
//......
S; class consultant : employeeA temporary temp;
//......
S;
Aceasta nu este elegant si sufera exact de problemele pentru care clasele derivate
au fost inventate. De exemplu, intrucit consultant nu este derivat din temporary,
un consultant nu poate fi pus intr-o lista de temporary employee fara a scrie
un cod special. Cu toate acestea, aceasta tehnica a fost aplicata cu succes
in multe programe utile.
7.2.6 Constructori si Destructori
Anumite clase derivate necesita constructori. Daca clasa de baza are un constructor,
atunci constructorul poate fi apelat, iar daca constructorul necesita argumente,
atunci astfel de argumente trebuie furnizate. De exemplu: class baseA
//....... public: base(char* n, short t);
Ibase();
S; class derived : public baseA base m; public: derived(char *n);
Iderived();
S;
Argumentele pentru constructorul clasei de baza se specifica in definitia unui
constructor al clasei derivate. In acest caz, clasa de baza actioneaza exact
ca un membru nedenumit al clasei derivate (&5.5.4). De exemplu: derived::derived(char* n) : (n, 10), m("member", 123)
A
//.......
S
Obiectele clasei sint constituite de jos in sus: intii baza, apoi membri si
apoi insasi clasa derivata. Ele sint distruse in ordine inversa: intii clasa
derivata, apoi membri si apoi baza.
7.2.7 Cimpuri de tip
Pentru a utiliza clase derivate mai mult decit o prescurtare convenabila in
declaratii, trebuie sa se rezolve problema urma- toare: dindu-se un pointer
de tip base*, la care tip derivat apartine in realitate obiectul pointat? Exista
trei solutii fundamentale la aceasta problema:
a1i Asigurarea ca sint pointate numai obiecte de un singur tip (&7.3.3);
a2i Plasarea unui cimp de tip in clasa de baza pentru a fi consultat de functii;
a3i Sa se utilizeze functii virtuale (&7.2.8).
Pointerii la clasa de baza se utilizeaza frecvent in proiectarea de clase container,
cum ar fi multimea, vectorul si lista. In acest caz, solutia 1 produce liste
omogene; adica liste de obiecte de acelasi tip. Solutiile 2 si 3 pot fi utilizate
pentru a construi liste eterogene; adica liste de pointeri spre obiecte de tipuri
diferite. Solutia 3 este o varianta speciala de tip sigur al solutiei 2. Sa
examinam intii solutia simpla de cimpuri_tip, adica solutia 2. Exemplul manager/employee
va fi redefinit astfel: enum empl_type AM, ES; struct employeeA empl_type type; employee* next; char* name; short departament;
//.......
S; struct manager : employeeA employee* group; short level;
//........
S;
Dindu-se aceasta noi putem scrie acum o functie care imprima informatie despre
fiecare employee: void print_employee(employee* e)
A switch(e->type)
A case E: cout<<e->name<<"\t"<<e->departament<<"\n";
//........ break; case M: cout<<e->name<<"\t"<<e->departament<<"\n";
//........ manager* p = (manager*)e; cout<<"level"<<p->level<<"\n";
//........ break;
S
S
si sa o utilizam pentru a imprima o lista de angajati, astfel: void f(employee* ll)
A for( ; ll; ll=ll->next) print_employee(ll);
S
Aceasta functioneaza frumos, mai ales intr-un program scris de o singura persoana,
dar are o slabiciune fundamentala care depinde de programatorul care manipuleaza
tipurile intr-un mod care nu poate fi verificat de compilator. Aceasta de obicei
conduce la doua tipuri de erori in programele mai mari. Primul este lipsa de
a testa cimpul de tip si cel de al doilea este imposibilitatea de a plasa toate
cazurile posibile intr-un switch cum ar fi cel de sus. Ambele sint usor de eliminat
cind programul se scrie si foarte greu de eliminat cind se modifica un program
netrivial; in special un program mare scris de altcineva.
Aceste probleme sint adesea mai greu de eliminat din cauza ca functiile de felul
lui print() sint adesea organizate pentru a avea avantaje asupra partilor comune
ale claselor implicate. De exemplu: void print_employee(employee* e)
A cout << e->name << "\t" << e->departament
<< "\n";
//........ if(e->type == M)
A manager* p = (manager*)e; cout << " level " << p->level
<< "\n";
//.......
S
S
A gasi toate instructiunile if aflate intr-o functie mare care trateaza multe
clase derivate poate fi dificil si chiar cind sint localizate poate fi greu
de inteles ce fac.
7.2.8 Functii virtuale
Functiile virtuale rezolva problemele solutiei cu cimpuri de tip, permitind
programatorului sa declare functii intr-o clasa de baza care pot fi redefinite
in fiecare clasa derivata. Compilatorul si incarcatorul vor garanta corespondenta
corecta intre obiecte si functii aplicate la ele. De exemplu: struct employeeA employee* next; char* name; short departament;
//........ virtual void print();
S;
Cuvintul cheie virtual indica faptul ca functia print() poate avea versiuni
diferite pentru clase derivate diferite si ca este sarcina compilatorului sa
gaseasca pe cel potrivit pentru fiecare apel al functiei print(). Tipul functiei
se declara in clasa de baza si nu poate fi redirectat intr-o clasa derivata.
O functie virtuala trebuie sa fie definita pentru clasa in care este declarata
intii. De exemplu: void employee::print()
A cout << name << "\t" << departament << "\n";
//........
S
Functia virtuala poate fi utilizata chiar daca nu este derivata nici o clasa
din clasa ei iar o clasa derivata care nu are nevoie de o versiune speciala
a functiei virtuale nu este necesar sa furnizeze vreo versiune. Cind se scrie
o clasa derivata, pur si simplu se furnizeaza o functie potrivita daca este
necesar. De exemplu: struct manager : employeeAemployee* group; short level;
//....... void print();
S; void manager::print()
Aemployee::print(); cout << "\tlevel" << level << "\n";
S
Functia print_employee() nu este acum necesara deoarece functiile membru print()
si-au luat locul lor, iar o lista de angajati poate fi minuita astfel: void f(employee* ll)
A for( ; ll; ll=ll->next) ll->print();
S
Fiecare angajat va fi scris potrivit tipului lui. De exemplu: main()
A employee e; e.name = "J. Brown"; e.departament = 1234; e.next = 0; manager m; m.name = "J. Smith"; m.departament = 1234; m.level = 2; m.next = &e; f(&m);
S
va produce:
J. Smith 1234 level 2
J. Browh 1234
Sa observam ca aceasta va functiona chiar daca f() a fost scrisa si compilata
inainte ca clasa derivata manager sa fi fost vreodata gindita! Evident implementind-o
pe aceasta va fi nevoie sa se memoreze un anumit tip de informatie in fiecare
obiect al clasei employee. Spatiul luat (in implementarea curenta) este suficient
ca sa se pastreze un pointer. Acest spatiu este rezervat numai in obiectele
clasei cu functii virtuale si nu in orice obiect de clasa sau chiar in orice
obiect al unei clase derivate.
Aceasta incarcare se plateste numai pentru clasele pentru care se declara functii
virtuale. Apelind o functie care utilizeaza domeniul de rezolutie al operatorului
:: asa cum se face in manager::print() se asigura ca nu se utilizeaza mecanismul
virtual. Altfel manager::print() ar suferi o recursivitate infinita. Utilizarea
unui nume calificat are un alt efect deziderabil: daca o functie virtuala este
inline (deoarece nu este comuna), atunci substitutia inline poate fi utilizata
unde :: se utilizeaza in apel. Aceasta furnizeaza programatorului un mod eficient
de a trata unele cazuri speciale importante in care o functie virtuala apeleaza
o alta pentru acelasi obiect. Intrucit tipul obiectului se determina in apelul
primei functii virtuale, adesea nu este nevoie sa fie determinat din nou pentru
un alt apel pentru acelasi obiect.
7.3 Interfete alternative
Dupa prezentarea facilitatilor limbajului relativ la clasele derivate, discutia
poate acum sa revina la problemele pe care trebuie sa le rezolve. Ideea fundamentala
pentru clasele descrise in aceasta sectiune este ca ele sint scrise o data si
utilizate mai tirziu de programatori care nu pot modifica definitiile lor. Clasele,
fizic vor consta din unul sau mai multe fisiere antet care definesc o interfata
si unul sau mai multe fisiere care definesc o implementare. Fisierele antet
vor fi plasate undeva de unde utilizatorul poate lua o copie folosind directiva
#include. Fisierele care specifica definitia sint de obicei compilate si puse
intr-o biblioteca.
7.3.1 O interfata
Consideram scrierea unei clase slist pentru liste simplu inlantuite in asa
fel ca clasa sa poata fi utilizata ca o baza pentru a crea atit liste eterogene
cit si omogene de obiecte de tipuri inca de definit. Intii noi vom defini un
tip ent: typedef void* ent;
Natura exacta a tipului ent nu este importanta, dar trebuie sa fie capabil sa
pastreze un pointer. Apoi noi definim un tip slink: class slinkA friend class slist; friend class slist_iterator; slink* next; ent e; slink(ent
a, slink* p)
A e=a; next=p;
S
S;
Un link poate pastra un singur ent si se utilizeaza pentru a implementa clasa
slist: class slistA friend class slist_iterator; slink* last;//last->next este capul listei public: int insert(ent a);//adauga la capul listei int append(ent a);//adauga la coada listei ent get(); //returneaza si elimina capul listei void clear(); //elimina toate linkurile slist()A last=0; S slist(ent a)
Alast = new slink(a, 0); last->next = last;
S
Islist()A clear(); S
S;
Desi lista este evident implementata ca o lista inlantuita, implementarea ar
putea fi schimbata astfel incit sa utilizeze un vector de ent fara a afecta
utilizatorii. Adica utilizarea lui slink nu este aratata in declaratiile functiilor
publice ale lui slist, ci numai in partea privata si in definitiile de functie.
7.3.2 O implementare
Implementarea functiilor din slist este directa. Singura problema este aceea
ca, ce este de facut in cazul unei erori sau ce este de facut in caz ca utilizatorul
incearca un get() dintr-o lista vida. Aceasta se va discuta in &7.3.4. Iata
definitiile pentru membri lui slist. Sa observam cum memorind un pointer spre
ultimul element al unei liste circulare se permite implementarea simpla atit
a operatiei append() cit si a operatiei insert(): int slist::insert(ent a)
A if(last) last->next = new slink(a, last->next); else
A last = new slink(a, 0); last->next = last;
S return 0;
S
int slist::append(ent a)
A if(last) last = last->next = new slink(a, last->next); else
Alast = new slink(a, 0); last->next = last;
S return 0;
S ent slist::get()
A if(last==0) slist_handler("get from empty slist"); slink* f = last->next; ent
r = f->e; last = (f==last) ? 0 : f->next; delete f; return r;
S
Sa observam modul in care se apeleaza slist_handler (declaratia lui poate fi
gasita in &7.3.4). Acest pointer la numele functiei se utilizeaza exact
ca si cum ar fi numele functiei. Aceasta este o prescurtare pentru o notatie
de apel mai explicita:
(*slist_handler)("get from empty list");
In final, slist::clear() elimina toate elementele dintr-o lista: void slist::clear()
Aslist* l = last; if(l==0) return; doA slink* ll = l; l = l->next; delete ll;
Swhile(l!=last);
S
Clasa slist nu furnizeaza nici o facilitate pentru cautarea intr-o lista ci
numai mijlocul de a insera si de a sterge membri. Cu toate acestea, atit clasa
slist, cit si clasa slink, declara ca clasa slist_iterator este un prieten,
asa ca noi putem declara un iterator potrivit. Iata unul in stilul prezentat
in &6.8: class slist_iteratorAslink* ce; slist* cs; public: slist_iterator(slist& s)Acs=&s; ce=0;S ent operator()()
A slink* ll; if(ce == 0) ll = ce = cs->last; elseA ce = ce->next; ll = (ce==cs->last) ? 0 : ce;
S return ll ? ll->e : 0;
S
S;
7.3.3 Cum sa o folosim
Asa cum este, clasa slist virtual nu este utila. Inainte de toate, la ce foloseste
o lista de pointeri void* ? Smecheria este de a deriva o clasa din slist pentru
a obtine o lista de obiecte al unui tip care este de interes intr-un program
particular. Sa consideram un compilator pentru un limbaj de felul lui C++. Aici
listele de nume vor fi utilizate extensiv; un nume este ceva de forma: struct nameAchar* string;
//.......
S;
Pointerii spre name vor fi pusi in lista in locul obiectelor name. Aceasta permite
utilizarea cimpului de informatie unica, e, a lui slist si admite ca un nume
sa fie in mai multe liste in acelasi timp. Iata o definitie a unei clase nlist
care deriva trivial din clasa slist:
#include "slist.h"
#include "name.h" struct nlist : slistA void insert(name* a)A slist::insert(a); S void append(name* a)A slist::append(a); S name* get()A return (name*)slist::get(); S nlist()AS nlist(name* a) : (a)AS
S;
Functiile clasei noi sint sau mostenite direct din slist, sau fac numai conversie
de tip. Clasa nlist nu este nimic altceva decit o alternativa de interfata pentru
clasa slist. Din cauza ca tipul ent in realitate este void*, nu este necesar
sa se converteasca explicit pointerii name* utilizati ca parametri actuali (&2.3.4).
Listele de nume ar putea fi utilizate in acest fel intr-o clasa care reprezinta
o definitie de clasa: struct classdefAnlist friends; nlist constructors; nlist destructors; nlist members; nlist operators; nlist
virtuals;
//........ void add_name(name*); classdef();
Iclassdef();
S; si numele s-ar putea adauga la acele liste in aceasta maniera: void classdef::add_name(name* n)
Aif(n->is_friend())
A if(find(&friends, n)) error("friend redeclared"); else if(find(&members, n)) error("friend redeclared as member"); else friends.append(n);
S if(n->is_operator()) operators.append(n);
//........
S unde is_operator() si is_friend() sint functii membru ale clasei name. Functia
find() ar putea fi scrisa astfel: int find(nlist* ll, name* n)
A slist_iterator ff(*(slist*)ll); ent p;
while(p = ff()) if(p == n) return 1; return 0;
S
Aici se utilizeaza conversia de tip explicita pentru a folosi un slist_iterator
pentru un nlist. O solutie mai buna pentru a face un iterator pentru nlist,
se arata in &7.3.5. Un nlist s-ar putea imprima printr-o functie astfel: void print_list(nlist* ll, char* list_name)
A slist_iterator count(*(slist*)ll); name* p; int n = 0; while(count()) n++; cout << list_name << "\n" << n << "members\n";
slist_iterator print(*(slist*)ll); while(p = (name*)print()) cout << p->string << "\n";
S
7.3.4 Tratarea erorilor
Exista 4 conceptii la problema in legatura cu ce sa facem cind o facilitate
cu scop general, cum ar fi slist intilneste o eroare la executie (in C++, unde
nu sint prevazute facilitati specifice ale limbajului pentru tratarea erorilor
la executie):
a1i Se returneaza o valoare ilegala si se lasa ca utilizatorul sa o verifice;
a2i Se returneaza o valoare de stare suplimentara si se lasa ca utilizatorul
sa o verifice;
a3i Se apeleaza o functie furnizata ca parte a clasei slist;
a4i Se apeleaza o functie eroare care se presupune ca o va furniza utilizatorul.
Pentru un program mic scris de un singur utilizator, nu exista un motiv pentru
a alege o solutie sau alta. Pentru o faci- litate generala solutia este cit
se poate de diferita.
Prima conceptie, care returneaza o valoare ilegala, nu este fezabila. In general
nu exista un mod de a sti ca o valoare particulara este ilegala pentru toti
utilizatorii unui slist.
Conceptia a doua, care returneaza o valoare stare, poate fi utilizata in unele
cazuri (o variatie a acestei scheme se foloseste pentru sirurile standard I/O
istream si ostream; asa cum se explica in &8.4.2). Cu toate acestea, ea
sufera de probleme serioase, caci daca o facilitate esueaza des, utilizatorii
nu se vor mai obosi sa verifice valoarea starii. Mai mult decit atit, o facilitate
poate fi utilizata in sute sau mii de locuri intr-un program. Verificarea starii in fiecare loc ar face programul mult mai greu de
citit.
Cea de a treia conceptie, care furnizeaza o functie de eroare, nu este flexibila.
Nu exista o cale pentru implementatorul unei facilitati de scop general sa stie
cum utilizatorii ar dori sa fie tratate erorile. De exemplu, un utilizator ar
putea prefera erori scrise in daneza sau romana.
Cea de a patra conceptie, lasind ca utilizatorul sa furnizeze o functie eroare,
are o anumita atractie cu conditia ca implementatorul sa prezinte clasa ca o
biblioteca (&4.5) ce con- tine versiuni implicite pentru functiile de tratare
a erorilor.
Solutiile 3 si 4 pot fi facute mai flexibile (si esential echivalente) specificind
un pointer spre o functie, decit functia insasi. Aceasta permite proiectantului
unei facilitati de forma lui slist sa furnizeze o functie eroare implicita,
ceea ce face ca programatorilor sa le fie mai simplu decit sa furnizeze fun-
ctia lor proprie cind este necesar. De exemplu: typedef void (*PFC)(char*); //pointer spre un tip functie extern PFC slist_handler; extern PFC set_slist_handler(PFC);
Functia set_slist_handler() permite utilizatorului sa inlocuiasca prelucrarea
implicita. O implementare conventionala furnizeaza o functie implicita de tratare
a erorilor care intii scrie un mesaj in cerr, apoi termina programul utilizind
exit():
#include "slist.h"
#include <stream.h> void default_error(char* s)
A cerr << s << "\n"; exit(1);
S
De asemenea, se declara un pointer la o functie eroare si din motive de notatie
o functie pentru setarea lui:
PFC slist_handler = default_error;
PFC set_slist_handler(PFC handler)
A
PFC rr = slist_handler; slist_handler = handler; return rr;
S
Sa observam modul in care set_slist_handler() returneaza slist_handler. Aceasta
este convenabil pentru utilizator ca sa seteze si sa reseteze prelucrarile sub
forma unei stive. Aceasta poate fi mai util in programe mari in care o slist
ar putea fi utilizata in diferite contexte; fiecare din ele poate apoi furniza
rutinele propri de tratare a erorilor. De exemplu:
PFC old = set_slist_handler(my_handler);
//cod unde my_handler va fi utilizat in caz de eroare in slist set_slist_handler(old); //resetare
Pentru a cistiga chiar un control mai bun, slist_handler ar putea fi un membru
al clasei slist, permitind astfel ca diferite liste sa aiba diferite tratari
de erori simultan.
7.3.5 Clase generice
Evident s-ar putea defini liste de alte tipuri (classdef*, int, char*, etc.)
in acelasi mod cum a fost definita clasa nlist: prin derivare triviala din clasa
slist. Procesul de definire de astfel de tipuri noi este plicticos (si de aceea
este inclinat spre erori), dar nu poate fi "mecanizat" prin utilizare
de ma- crouri. Din pacate, aceasta poate fi cit se poate de dureros cind se
utilizeaza preprocesorul standard C (&4.7 si &r11.1). Macrou- urile
rezultate sint, totusi, cit se poate de usor de utilizat.
Iata un exemplu in care un slist generic, numit gslist, poate fi furnizat ca
un macro. Intii niste instrumente pentru a scrie astfel de macrouri se includ
din <generic.h>:
#include "slist.h"
#ifndef GENERICH
#include <generic.h>
#endif
Sa observam cum #ifndef se utilizeaza pentru a asigura ca <generic.h>
nu se include de doua ori in aceeasi compilare.
GENERICH se defineste in <generic.h>
Numele pentru clasa generica noua se defineste utilizind name2() care este un
macro_name de concatenare din <generic.h>:
#define gslist(type) name2(type, gslist)
#define gslist_iterator(type) name2(type, gslist_iterator)
In final, clasa gslist(type) si gslist_iterator(type) pot fi scrise:
#define gslistdeclare(type) \ struct gslist(type) : slistA \ int insert(type a)Areturn slist::insert(ent(a));S \ int append(type a)Areturn slist::append(ent(a));S \ type get()Areturn type(slist::get());S \ gslist(type)()AS \ gslist(type)(type a):(ent(a))AS \
Igslist(type)()Aclear();S \
S; \
\ struct gslist_iterator(type) : slist_iteratorA \ gslist_iterator(type)(gslist(type)& s):((slist&)s)AS\ type operator()() \
Areturn type(slist_iterator::operator()());S \
S;
Un backslash ("\") indica faptul ca linia urmatoare este parte a
macroului care se defineste.
Utilizind acest macro, o lista de pointeri spre name, asa cum a fost utilizata
in prealabil clasa nlist, poate fi definita astfel:
#include "name.h" typedef name* Pname; declare(gslist, Pname); //declara clasa gslist(Pname) gslist(Pname) nl; //declara un gslist(Pname)
Macroul declare este definit in <generic.h>. El concateneaza argumentele
lui si apeleaza macroul cu acel nume, in acest caz gslistdeclare definit mai
sus. Un nume argument al lui declare trebuie sa fie un nume simplu. Tehnica
de macro_expandare utilizata aici nu poate trata un nume de felul name*; astfel
se utilizeaza typedef.
Utilizind derivarea se asigura ca toate exemplarele unei clase generice au cod
comun. Tehnica poate fi utilizata numai pentru a crea clase de obiecte de aceeasi
dimensiune sau mai mica decit clasa de baza utilizata in macro. Aceasta este
totusi idea- la pentru liste de pointeri. O gslist este utilizata in &7.6.2.
7.3.6 Interfete restrictive
Clasa slist este o clasa cit se poate de generala. Uneori o astfel de generalitate
nu este necesara sau nu este de dorit. Forme restrictive cum ar fi stive si
cozi sint chiar mai frecvente decit insasi listele generale. Nedeclarind clasa
de baza publica, se pot furniza astfel de structuri de date. De exemplu o coada
de intregi poate fi definita astfel:
#include "slist.h" class iqueue : slistA//presupune sizeof(int)<=sizeof(void*) public: void put(int a)A slist::append((void*)a); S int get()A return int(slist::get()); S iqueue()AS
S;
Doua operatii logice se fac prin aceasta derivare: conceptul de lista este
restrins la conceptul de coada, iar tipul int se specifica pentru a restringe
conceptul unei cozi la tipul de coada de date intregi (iqueue). Aceste doua
operatii ar putea fi date separat. Aici prima este o lista care este restrinsa
asa ca ea ar putea fi utilizata numai ca o stiva:
#include "slist.h" class stack : slistA public:slist::insert; slist::get; stack()AS stack(ent a) : (a)AS
S; care poate fi apoi utilizata sa creeze tipul "stiva de pointeri spre caractere":
#include "stack.h" class cpstack : stackA public: void push(char* a)A slist::insert(a); S char* pop()A return (char*)slist::get(); S
S;
7.4 Adaugarea la o clasa
In exemplele precedente, nu se adauga nimic la clasa de baza prin clasa derivata.
Functiile se definesc pentru clasele derivate numai pentru a furniza conversie
de tip. Fiecare clasa deri- vata furnizeaza pur si simplu o interfata in loc
de o multime de rutine comune. Aceasta este o clasa speciala importanta, dar
motivul cel mai frecvent pentru care se defineste o clasa noua ca o clasa derivata
este faptul ca se vrea ceea ce furnizeaza clasa de baza, plus inca ceva.
Pot fi definite date si functii membre noi pentru o clasa derivata, in plus
fata de cele mostenite din clasa ei de baza. Sa observam ca atunci cind un element
este pus intr-o slist in pre- alabil definita, se creaza un slink care contine
doi pointeri. Aceasta creare ia timp. De un pointer ne putem dispensa, cu con-
ditia ca este necesar ca un obiect la un moment dat sa fie numai intr-o lista,
asa ca pointerul next poate fi plasat in obiectul insusi (nu intr-un obiect
slink separat). Ideea este de a furniza o clasa olink cu numai un cimp next
si o clasa olist care poate manipula pointeri la astfel de inlantuiri. Obiectele
oricarei clase derivate din olink pot fi manipulate prin olist. Litera "o"
din nume este pentru a ne reaminti ca un obiect poate fi numai intr-o olist
la un moment dat: struct olinkA olink* next; S;
Clasa olist este similara cu clasa slist. Diferenta este ca un utilizator al
clasei olist manipuleaza obiectele clasei olink direct: class olistA olink* last; public: void insert(olink* p); void append(olink* p); olink* get();
//.......
S;
Noi putem deriva clasa name din clasa olink: class name : olinkA /* ... */ S;
Acum este trivial sa se faca o lista de name care poate fi utilizata fara a
aloca spatiu sau timp suplimentar.
Obiectele puse in olist isi pierd tipul, adica compilatorul stie ca ele sint
olink. Tipul propriu poate fi restabilit folosind conversia explicita de tip
a obiectelor luate din olist. De exemplu: void f()
A olist ll; name nn; ll.insert(&nn); //tipul lui &nn este pierdut name* pn = (name*)ll.get(); // si se restaureaza
S
Alternativ, tipul poate fi restabilit derivind o alta clasa din olist care
sa trateze conversia de tip: class onlist : olistA
//....... name* get()Areturn (name*)olist::get();S
S;
Un nume poate sa fie la un moment dat numai intr-o olist. Aceasta poate fi
nepotrivit pentru name, dar nu exista prescurtari ale claselor pentru care sa
fie in intregime potrivita. De exemplu, clasa shape din exemplul urmator utilizeaza
exact aceasta tehnica pentru ca o lista sa pastreze toate formele. Sa observam
ca slist ar putea fi definita ca o clasa derivata din olist, astfel unificind
cele doua concepte. Cu toate acestea, utilizarea claselor de baza si derivate
la acest nivel microscopic al programarii poate conduce la un cod foarte controlat.
7.5 Liste eterogene
Listele precedente sint omogene. Adica, numai obiectele unui singur tip au
fost puse in lista. Mecanismul de clasa derivata este utilizat pentru a asigura
aceasta. Listele, in general, este necesar sa nu fie omogene. O lista specificata
in termenii de pointeri spre o clasa poate pastra obiecte de orice clasa derivata
din acea clasa; adica, ea poate fi eterogena. Aceasta este probabil singurul
aspect mai important si mai util al claselor derivate si este esential in stilul
programarii prezentate in exemplul urmator. Acest stil de programare este adesea
numit bazat pe obiect sau orientat spre obiect; se bazeaza pe operatii aplicate intr-o maniera uniforma la obiectele unei liste
eterogene. Sensul unor astfel de operatii depinde de tipul real al obiectelor
din lista (cunoscut numai la executie), nu chiar de tipul elementelor listei
(cunoscut la compilare).
7.6 Un program complet
Sa consideram un program care deseneaza figuri geometrice pe ecran. El consta
din trei parti:
a1i Un control de ecran: rutine de nivel inferior si structuri de date care
definesc ecranul; acestea stiu desena numai puncte si linii drepte;
a2i O biblioteca de figuri: un set de definitii si figuri generale cum ar fi
dreptunghi, cerc, etc. si rutine standard pentru a le manipula;
a3i Un program aplicativ: un set de definitii specifice a-l plicatiei si cod
care sa le utilizeze.
De obicei, cele trei parti vor fi scrise de persoane diferite. Partile sint
scrise in ordinea prezentarii lor cu adaugarea complicatiilor pe care proiectul
de nivel mai inferior nu are idee despre modul in care codul lui va fi eventual
utilizat. Exemplul urmator releva acest lucru. Pentru ca exemplul sa fie simplu
pentru prezentare, biblioteca de figuri furnizeaza numai citeva servicii simple,
iar programul de aplicatii este trivial. O conceptie extrem de simpla a ecranului
se utilizeaza asa ca cititorul sa poata incerca programul chiar daca nu sint
disponibile facilitatile de grafica. Este simplu sa se schimbe partea cu ecranul
a programului cu ceva potrivit fara a schimba codul bibliotecii de figuri sau
programul de aplicatie.
7.6.1 Controlul ecranului
Intentia a fost sa se scrie controlul ecranului in C (nu in C++) pentru a accentua
distinctia intre nivelele implementarii. Aceasta s-a constatat a fi plicticos,
asa ca s-a facut un compromis: stilul de utilizare este din C (nu exista functii
membru, functii virtuale, operatori definiti de utilizator, etc.), dar se folosesc
constructori, se declara si se verifica argumentele functie, etc.. Ca rezultat,
controlul ecranului arata foarte mult ca un program in C care a fost modificat
ca sa posede avantajele lui C++ fara a fi total rescris.
Ecranul este reprezentat ca un tablou de caractere bidimensional, manipulat
prin functiile put_point() si put_line() ce utilizeaza structura point cind
ne referim la ecran:
//fisierul screen.h const XMAX=40, YMAX=24; struct pointA int x, y; point()AS point(int a, int b)A x=a; y=b; S
S; overload put_point; extern void put_point(int a, int b); inline void put_point(point p)A put_point(p.x, p.y); S overload put_line; extern void put_line(int, int, int, int); inline void put_line(point
a, point b)
Aput_line(a.x, a.y, b.x, b.y);S extern void screen_init(); extern void screen_refresh();
extern void screen_clear();
#include <stream.h>
Inainte de a utiliza o functie put(), ecranul trebuie sa fie initializat prin
screen_init(), iar schimbarile ecranului spre structuri de date sint reflectate
pe ecran numai dupa apelul lui screen_refresh(). Cititorul va afla ca refresh
se face pur si simplu scriind o copie noua a tabloului ecran sub versiunea precedenta.
Iata functiile si definitiile de date pentru ecran:
#include "screen.h"
#include <stream.h> enum colorAblack='*', white=' 'S; char screenaXMAXiaYMAXi; void screen_init()
Afor(int y=0; y<YMAX; y++) for(int x=0; x<XMAX; x++) screenaxiayi = white;
S
Punctele se scriu numai daca sint in domeniul ecranului: inline int on_screen(int a, int b)
A return 0<=a && a<XMAX && 0<=b && b<YMAX;
S void put_point(int a, int b)
A if(on_screen(a, b)) screenaaiabi=black;
S
Functia put_line() se foloseste pentru a desena linii: void put_line(int x0, int y0, int x1, int y1)
A /* Traseaza linia de la (x0, y0) la (x1, y1).
Linia de trasat este b(x-x0) + a(y-y0) = 0.
Se minimizeaza abs(eps),unde eps=2*(b(x-x0) + a(y-y0)).
Newman and Sproul:
"Principles of Interactive Computer Graphics" McGrow-Hill, New York,
1979, pp 33-44.
*/ register dx = 1; int a = x1-x0; if(a<0) dx = -1, a = -a; register dy = 1; int b = y1-y0; if(b<0) dy = -1, b = -b; int two_a = 2*a; int two_b = 2*b; int xcrit = -b+two_a; register
eps = 0; for(;;)
A put_point(x0, y0); if(x0==x1 && y0==y1) break; if(eps<=xcrit) x0 += dx, eps += two_b; if(eps>=a || a<=b) y0 += dy, eps -= two_a;
S
S
Pentru stergere si resetare se folosesc functiile: void screen_clear()
A screen_init(); S
void screen_refresh()
Afor(int y=YMAX-1; 0<=y; y--) //de sus in jos
A for(int x=0; x<XMAX; x++) //de la stinga la dreapta cout.put(screenaxiayi); cout.put('\n');
S
S
Se utilizeaza functia ostream::put() pentru a imprima caracterele ca si caractere;
ostream::operator<<() imprima caracterele ca si intregi mici. Acum putem
sa ne imaginam ca aceste definitii sint disponibile numai ca iesiri intr-o biblioteca
pe care nu o putem modifica.
7.6.2 Biblioteca de figuri
Noi trebuie sa definim conceptul general de figura. Acest lucru trebuie facut
intr-un astfel de mod incit figura sa poata fi comuna pentru toate figurile
particulare (de exemplu cercuri si patrate) si intr-un astfel de mod ca orice
figura poate fi manipulata exclusiv prin interfata furnizata de clasa shape: struct shapeA shape()A shape_list.append(this);S virtual point north()A return point(0, 0); S virtual point south()A return point(0, 0); S virtual point east()A return point(0, 0); S virtual point neast()A return point(0, 0); S virtual point seast()A return point(0, 0); S virtual point draw()AS; virtual void move(int, int)AS;
S;
Ideea este ca figurile sint pozitionate prin move() si se plaseaza pe ecran
prin draw(). Figurile pot fi pozitionate relativ una fata de alta folosind conceptul
de contact points, denu- mit dupa punctele de pe compas. Fiecare figura particulara
defineste sensul acelor puncte pentru ea insasi si fiecare defineste cum se
deseneaza. Pentru a salva hirtie, in acest exemplu sint definite numai punctele
de compas necesare. Constructorul shape::shape() adauga figura la o lista de
figuri shape_list. Aceasta lista este un gslist, adica o versiune a unei liste
ge- nerice simplu inlantuite asa cum a fost definita in &7.3.5. Ea si un
iterator de corespondenta s-au facut astfel: typedef shape* sp; declare(gslist, sp); typedef gslist(sp) shape_list; typedef
gslist_iterator(sp) sl_iterator; asa ca shape_list poate fi declarata astfel: shape_lst shape_list;
O linie poate fi construita sau din doua puncte sau dintr-un punct si un intreg.
Ultimul caz construieste o linie orizontala de lungime specificata printr-un
intreg. Semnul intregului indica daca punctul este capatul sting sau drept.
Iata definitia: class line : public shapeA
/* linie de la "w" la "e"; north() se defineste ca "deasupra
centrului atit de departe cit este north de punctul cel mai din nord"
*/ point w, e; public: point north()
Areturn point((w.x+e.x)/2, e.y<w.y?w.y:e.y);S point south()
Areturn point((w.x+e.x)/2, e.y<w.y?e.y:w.y);S void move(int a, int b)
Aw.x += a; w.y += b; e.x += a; e.y += b;S void draw()A put_line(w,e); S line(point a, point b)A w = a; e = b; S line(point a, int l)Aw=point(a.x+l-1,a.y);e=a;S
S;
Un dreptunghi este definit similar: class rectangle : public shapeA
/* nw------n-------ne
| |
w c e
| | sw------s-------se
*/ point sw,ne; public: point north()Areturn point((sw.x+ne.x)/2, ne.y);S point south()Areturn point((sw.x+ne.x)/2, sw.y);S point neast()A return ne; S point swest()A return sw; S void move(int a, int
b)
A sw.x+=a; sw.y+=b; ne.x+=a; ne.y+=b; S void draw(); rectangle(point, point);
S;
Un dreptunghi este construit din doua puncte. Codul este complicat din necesitatea
de a figura pozitia relativa a celor doua puncte: rectangle::rectangle(point a, point b)
Aif(a.x<=b.x)
A if(a.y<=b.y)A sw=a; ne=b; S elseA sw=point(a.x, b.y); ne=point(b.x, a.y); S
S else
A if(a.y<=b.y)A sw=point(b.x, a.y); ne=point(a.x, b.y); S elseA sw=b; ne = a; S
S
S
Pentru a desena un dreptunghi trebuie desenate cele patru laturi ale sale: void rectangle::draw()
Apoint nw(sw.x, ne.y); point se(ne.x, sw.y); put_line(nw, ne); put_line(ne, se); put_line(se, sw);
put_line(sw, nw);
S
In plus fata de definitiile lui shape, o bibliotece de figuri mai contine si
functiile de manipulare a figurilor. De exemplu: void shape_refresh(); //deseneaza toate figurile void stack(shape* p, shape* q); //pune p in virful lui q
Functie refresh() este necesara pentru a invinge greutatile legate de gestiunea
ecranului. Ea pur si simplu redeseneaza toate figurile. Sa observam ca nu exista
nici o idee despre ce fel de figuri deseneaza: void shape_refresh()
A screen_clear(); sl_iterator next(shape_list); shape* p;
while(p = next()) p->draw(); screen_refresh();
S
In final, iata o functie de mare utilitate; ea pune o figura pe o alta specificind
ca o figura south() trebuie sa fie deasupra unei figuri north(): void stack(shape* p, shape* q) //pune p peste q
A point n = p->north(); point s = q->south(); q->move(n.x-s.x, n.y-s.y+1);
S
Acum sa ne imaginam ca aceasta biblioteca se considera proprietatea unei anumite
companii care vinde software si ca ea vinde numai fisierul header care contine
definitiile shape si versiunile compilate ale definitiilor functiilor. Inca
este posibil pentru noi sa definim figuri noi si sa avem avantajul de a utiliza
functii pentru figurile noastre.
7.6.3 Programul de aplicatie
Programul de aplicatie este extrem de simplu. Se defineste figura myshape, care
arata un pic ca o fata, apoi se scrie un program main care deseneaza o astfel
de fata purtind o palarie. Declaratia lui myshape:
#include "shape.h" class myshape : public rectangleA line* l_eye; line* r_eye; line* mouth; public: myshape(point, point); void draw(); void move(int, int);
S;
Ochii si gura sint separate si sint obiecte independente create prin constructorul
myshape: myshape::myshape(point a, point b) : (a, b)
Aint ll = neast().x-swest().x+1; int hh = neast().y-swest().y+1; l_eye = new line(point(swest().x+2,swest().y+hh*3/4),2); r_eye = new line(point(swest().x+ll-4,swest().y+hh*3/4),2); mouth = new line(point(swest().x+2,swest().y+hh/4),ll-4);
S
Obiectele eye si mouth sint resetate separat prin functia shape_refresh() si
ar putea fi in principiu manipulate indepen- dent de obiectul myshape la care
ele apartin. Acesta este un mod de a defini facilitati pentru o ierarhie de
obiecte construite cum ar fi myshape. Un alt mod este ilustrat de nas. Nu este
definit nasul; el pur si simplu se adauga la figura prin functia draw(): void myshape::draw()
A rectangle::draw(); put_point(point((swest().x+neast().x)/2,
(swest().y+neast().y)/2));
S myshape se muta transferind dreptunghiul de baza si obiectele secundare l_eye,
r_eye si mouth: void myshape::move(int a, int b)
A rectangle::move(a, b); l_eye->move(a, b); r_eye->move(a, b); mouth->move(a, b);
S
In final noi putem construi citeva figuri si sa le mutam un pic: main()
Ashape* p1 = new rectangle(point(0, 0), point(10, 10)); shape* p2 = new line(point(0, 15), 17); shape* p3 = new myshape(point(15, 10), point(27, 18)); shape_refresh(); p3->move(-10, -10); stack(p2, p3); stack(p1, p2); shape_refresh();
return 0;
S
Sa observam din nou cum functiile de forma shape_refresh() si stack() manipuleaza
obiecte de tipuri care au fost definite mult dupa ce au fost scrise aceste functii
(si posibil compilate).
*************
* *
* *
* *
* ** ** *
* * *
* *
* ***** *
* *
7.7 Memoria libera
Daca noi utilizam clasa slist, am putea gasi ca programul nostru utilizeaza
timp considerabil pentru alocare si dealocare de obiecte ale clasei slink. Clasa
slink este un prim exemplu de clasa care ar putea beneficia de faptul ca programatorul
sa aiba control asupra memoriei libere. Tehnica optimizata descrisa in &5.5.6
este ideala pentru acest tip de obiect. Intrucit orice slink se creaza folosind
new si se distruge folosind delete de catre membri clasei slist, nu exista probleme
cu alte metode de alocare de memorie.
Daca o clasa derivata asigneaza la this constructorul pentru clasa ei de baza
va fi apelat numai dupa ce s-a facut asignarea, iar valoarea lui this in constructorul
clasei de baza va fi cea atribuita prin constructorul clasei derivate. Daca
clasa de baza asigneaza la this, valoarea asignata va fi cea utilizata de constructor
pentru clasa derivata. De exemplu:
#include <stream.h> struct baseA base(); S; struct derived : baseA derived(); S; base::base()
A cout << "\tbase 1: this=" << int(this) << "\n";
if(this == 0) this = (base*)27; cout << "\tbase 2: this=" << int(this)
<< "\n";
S derived::derived()
A cout << "\tderived 1: this=" << int(this) << "\n";
if(this == 0) this = (derived*)43; cout << "\tderived 2: this=" << int(this)
<< "\n";
S
main()
A cout << "base b;\n"; base b; cout << "new base;\n"; new base; cout << "derived d;\n"; derived d; cout << "new derived;\n"; new derived; cout << "at the end\n";
S produce iesirea: base b; base 1: this=2147478307 base 2: this=2147478307 new base; base 1: this=0 base 2: this=27 derived d; derived 1: this=2147478306 derived 2: this=2147478306 new derived; derived 1: this=0 base 1: this=43 base 2: this=43 derived 2: this=43 at the end
Daca un destructor pentru o clasa derivata asigneaza la this, atunci valoarea
asignata este cea vazuta de destructor pentru clasa lui de baza. Cind cineva
asigneaza la this un constructor este important ca o atribuire la this sa se
faca pe ori- ce cale a constructorului. Din nefericire, este usor sa se uite
o astfel de atribuire. De exemplu, la prima editare a acestei carti cea de a
doua linie a constructorului derived::derived() era: if(this==0) this=(derived*)43;
In consecinta, constructorul clasei de baza base::base() nu a fost apelat pentru
d. Programul a fost legal si s-a executat corect, dar evident nu a facut ce
a intentionat autorul.
7.8 Exercitii
1. (*1). Se defineste: class baseA public: virtual void ian()A cout << "base\n"; S
S;
Sa se deriveze doua clase din base si pentru fiecare definitie a lui ian()
sa se scrie numele clasei. Sa se creeze obiecte ale acestei clase si sa se apeleze
ian() pentru ele. Sa se asigneze adresa obiectelor claselor derivate la pointeri
de tip base* si sa se apeleze ian() prin acesti pointeri.
2. (*2). Sa se implementeze primitivele screen (&7.6.1) intr-un mod rezonabil
pentru sistemul d-voastra.
3. (*2). Sa se defineasca o clasa triunghi si o clasa cerc.
4. (*2). Sa se defineasca o functie care deseneaza o linie ce leaga doua figuri
gasind "punctele de contact" cele mai apropiate si le conecteaza.
5. (*2). Sa se modifice exemplul shape asa ca line sa fie derivata din rectangle
si invers.
6. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi utilizata fara iterator.
7. (*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care
poate fi folosita numai printr-un iterator. Iteratorular trebui sa aiba operatii
pentru parcurgeri inainte sau inapoi, operatiipentru a insera si sterge elemente
in lista si un mod de a face acces laelementul curent.
8. (*2). Sa se implementeze o versiune generica a unei liste dublu inlantuite.
9. (*4). Sa se implementeze o lista in care obiectele (si nu numai pointerii
spre obiecte) se insereaza si se extrag. Sa se faca sa functioneze pentru o
clasa X unde X::X(X&), X::IX() si X::operator=(X&) sint definite.
10. (*5). Sa se proiecteze si sa se implementeze o bibliote8ca pentru a scrie
simulari de drivere de evenimente. Indicatie: <task.h>.Acesta este un
program mai vechi si puteti scrie unul mai bun. Ar trebui safie o clasa task.
Un obiect al clasei task ar putea sa fie capabil sa salvezestarea lui si sa
aiba de restabilit acea stare (noi ar trebui sa definimtask::save() si task::restore())
asa ca ar trebui sa opereze ca o corutina.
Taskuri specifice pot fi definite ca obiecte de clase derivate din clasa task.Programul
de executat printr-un task ar putea fi specificat ca o functievirtuala. Ar putea
fi posibil sa se paseze argumentele la un task nou caargumente pentru constructorul
lui. Ar trebui sa fie un distribuitorimplementat ca un concept de timp virtual.
Sa se furnizeze o functie task::delay(long) care "consuma" timp virtual.
Daca distribuitorul este o parte a clasei task sau este separat, va fi o decizie
majora a proiectarii.
Taskurile vor trebui sa comunice. Sa se proiecteze o clasa queue pentru aceasta.
Sa se trateze erorile de la executie intr-un mod uniform. Cum se depaneaza programele
scrise utilizind o astfel de biblioteca?