|
CLASE DERIVATE IN C++
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.
1 Introducere
Consideram scrierea unor facilitati generale (de exemplu o lista inlantuita, o tabela de simboluri, un sistem de simulare) in intentia de a fi utilizate de multa lume in contexte diferite. Evident nu sint putini candidati pentru astfel de beneficii de a le avea standardizate. Fiecare programator experimentat se pare ca a scris (si a testat) o duzina de variante pentru tipurile multime, tabela de hashing, functii de sortare, etc., dar fiecare programator si fiecare program pare ca are o versiune separata a acestor concepte, facind programul greu de citit, greu de verificat si greu de schimbat. Mai mult decit atit, intr-un program mare ar putea foarte bine sa fie copii de coduri identice (sau aproape identice) pentru a trata astfel de concepte de baza.
Motivatia pentru acest haos este in parte faptul ca conceptual este dificil sa se prezinte facilitati atit de generale intr-un limbaj de programare si partial din cauza ca facilitatile de generalitate mare de obicei impun depasiri de spatiu si/sau timp, ceea ce le face nepotrivite pentru cele mai simple facilitati utilizate (liste inlantuite, vectori, etc.) unde ele ar trebui sa fie cele mai utile. Conceptul C++ de clasa derivata, prezentat in &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.
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.
2.1 Derivare
Consideram construirea unui program care se ocupa cu angajatii unei firme. Un astfel de program ar putea avea o structura de felul:
struct employee;
Cimpul next este o legatura intr-o lista pentru date employee similare. Acum vrem sa definim structura manager:
struct manager;
Un manager este de asemenea un angajat (employee); datele angajatului se memoreaza in emp care este un membru al obiectului manager. Aceasta poate fi evident pentru un cititor uman, dar nu exista nimic care sa distinga membri emp. Un pointer spre un ma-nager (manager*) nu este un pointer spre un employee (employee*), asa ca nu se pot utiliza unul in locul celuilalt. In particular, nu se poate pune un manager intr-o lista de angajati fara a scrie cod special. Se poate sau utiliza tipul de conversie explicit spre manager* sau sa se puna adresa membrului emp intr-o lista de angajati, dar ambele sint neelegante si pot fi obscure. Conceptia corecta este de a afirma ca un manager este un employee cu citeva informatii adaugate:
struct manager : employee;
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()
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 &2.4.
2.2. Functii membru
Structurile de date simple, cum ar fi employee si manager, sint in realitate neinteresante si adesea nu sint utile in mod special, asa ca, sa consideram adaugarea de functii la ele. De exemplu:
class employee;
class manager : public employee;
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()
Un membru al unei clase derivate poate utiliza un nume public al clasei de baza propri in acelasi mod ca si alti membri, adica fara a specifica un obiect. Se presupune obiectul spre care pointeaza this, asa ca numele (corect) se refera la this->name. Cu toate acestea, functia manager::print() nu se va compila; un membru al clasei derivate nu are permisiunea speciala de a face acces la un membru privat din clasa lui de baza, asa ca functia nu are acces la name.
Aceasta este o surpriza pentru multi, dar sa consideram varianta ca o functie membru ar putea face acces la membri privati ai clasei sale de baza. Conceptul de membru privat ar deveni lipsit de sens prin facilitatea care ar permite unui programator sa cistige acces la partea privata a unei clase pur si simplu prin derivarea unei clase noi din ea. Mai mult decit atit, s-ar putea sa nu se mai gaseasca toti utilizatorii unui nume privat uitindu-ne la functiile declarate ca membri si prieteni ai acelei clase. Ar trebui sa se examineze fiecare fisier sursa al programului complet pentru clase derivate, apoi sa se examineze fiecare functie din acele clase, apoi sa se gaseasca fiecare clasa derivata din aceste clase, etc.. Aceasta este impractic.
Pe de alta parte, este posibil sa se utilizeze mecanismul friend pentru a admite astfel de accese pentru functii specifice sau pentru orice funcie a unei clase specifice (asa cum s-a des- cris in &5.3). De exemplu:
class employee;
ar rezolva problema pentru manager::print(), iar clasa:
class employee;
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()
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()
si ar gasi ca programul este o secventa nedorita de apeluri recursive cind se apeleaza manager::print().
2.3 Vizibilitate
Clasa employee a fost facuta o clasa de baza publica prin declaratia:
class manager : public employee;
Aceasta inseamna ca un membru public al clasei employee este de asemenea un membru public al clasei manager. De exemplu:
void clear(manager* p)
se va compila deoarece next este un membru public atit al lui employee cit si al lui manager. Lasind la o parte din declaratie cuvintul public se poate defini o clasa derivata privata:
class manager : employee
Aceasta inseamna ca un membru public al clasei employee este un membru privat al clasei manager. Adica, membri functiilor manager pot utiliza membri publici ai lui employee ca inainte, dar acesti membri nu sint accesibili utilizatorilor clasei manager. In par- ticular, dindu-se aceasta declaratie de manager, functia clear() nu se va compila. Prietenii unei clase derivate au acelasi acces la membri clasei de baza ca si functiile membru. Declaratia public a claselor de baza este mai frecventa decit declaratia private, ceea ce este pacat pentru ca declaratia unei clase de baza publice este mai lunga decit una privata. De asemenea, este o sursa de erori pentru incepatori.
Cind este declarata o structura, clasa ei de baza este im- plicit o clasa de baza publica. Adica:
struct D : B
inseamna
class D : public B
Aceasta implica faptul ca daca noi nu gasim data ascunsa furnizata de utilizarea lui class, public si friends, ca fiind utile, atunci noi putem pur si simplu elimina aceste cuvinte si sa ne referim la struct. Facilitatile limbajului, cum ar fi functiile membru, constructorii si operatorii de supraincarcare sint independente de mecanismul de pastrare a datelor. Este posibil de asemenea sa se declare unii din membri publici (dar nu toti) ai unei clase de baza public ca membri ai unei clase derivate. De exemplu:
class manager : employee;
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.
2.4 Pointeri
Daca o clasa derivata are o clasa de baza (base) publica, atunci un pointer spre clasa derivata poate fi asignat la o variabila de tip pointer spre clasa base fara a utiliza explicit tipul de conversie. O conversie inversa de la un pointer spre base la un pointer spre derived trebuie facuta explicit. De exemplu:
class base; class derived : public base; derived m;
base* pb = &m;//conversie implicite
derived* pd = pb;//eroare: un base* nu este un derived*
pd =(derived*)pb;//conversie explicita
Cu alte cuvinte, un obiect al unei clase derivate poate fi tratat ca un obiect al clasei de baza propri cind se manipuleaza prin pointeri. Inversul nu este adevarat. Daca base ar fi fost o clasa privata de baza, conversia implicita a lui derived* spre base* nu se face. O conversie implicita nu se poate face in acest caz deoarece un membru public a lui base poate fi accesat printr-un pointer la base, dar nu printr-un pointer la derived:
class base;
class derived : base;
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.
2.5 Ierarhizarea claselor
O clasa derivata poate fi ea insasi a clasa de baza. De exemplu:
class employee; class secretary : employee; class manager : employee; class temporary : employee; class consultant : temporary; class director : manager; class vice_president : manager; class president : vice_president;
O multime de clase inrudite se numeste traditional o ierar- hie de clase. Intrucit se poate deriva o clasa dintr-o singura clasa de baza, o astfel de ierarhie este un arbore si nu poate fi o structura mai generala de graf. De exemplu:
class temporary;
class employee;
class secretary : employee;
//nu in C++
class temporary_secretary : temporary : secretary;
class consultant : temporary : employee;
Aceasta este pacat, intrucit un graf aciclic orientat al unei clase derivate poate fi foarte util. Astfel de structuri nu pot fi declarate, dar pot fi simulate utilizind membri de tipuri corespunzatoare. De exemplu:
class temporary;
class employee;
class secretary : employee;
//Alternative
class temporary_secretary : secretary;
class consultant : employee;
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.
2.6 Constructori si Destructori
Anumite clase derivate necesita constructori. Daca clasa de baza are un constructor, atunci constructorul poate fi apelat, iar daca constructorul necesita argumente, atunci astfel de argumente trebuie furnizate. De exemplu:
class base;
class derived : public base;
Argumentele pentru constructorul clasei de baza se specifica in definitia unui constructor al clasei derivate. In acest caz, clasa de baza actioneaza exact ca un membru nedenumit al clasei derivate (&5.5.4). De exemplu:
derived::derived(char* n) : (n, 10), m('member', 123)
Obiectele clasei sint constituite de jos in sus: intii baza, apoi membri si apoi insasi clasa derivata. Ele sint distruse in ordine inversa: intii clasa derivata, apoi membri si apoi baza.
2.7 Cimpuri de tip
Pentru a utiliza clase derivate mai mult decit o prescurtare convenabila in declaratii, trebuie sa se rezolve problema urma- toare: dindu-se un pointer de tip base*, la care tip derivat apartine in realitate obiectul pointat? Exista trei solutii fundamentale la aceasta problema:
[1] Asigurarea ca sint pointate numai obiecte de un singur tip (&3.3);
[2] Plasarea unui cimp de tip in clasa de baza pentru a fi consultat de functii;
[3] Sa se utilizeze functii virtuale (&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 ;
struct employee;
struct manager : employee;
Dindu-se aceasta noi putem scrie acum o functie care imprima informatie despre fiecare employee:
void print_employee(employee* e)
}
si sa o utilizam pentru a imprima o lista de angajati, astfel:
void f(employee* ll)
Aceasta functioneaza frumos, mai ales intr-un program scris de o singura persoana, dar are o slabiciune fundamentala care depinde de programatorul care manipuleaza tipurile intr-un mod care nu poate fi verificat de compilator. Aceasta de obicei conduce la doua tipuri de erori in programele mai mari. Primul este lipsa de a testa cimpul de tip si cel de al doilea este imposibilitatea de a plasa toate cazurile posibile intr-un switch cum ar fi cel de sus. Ambele sint usor de eliminat cind programul se scrie si foarte greu de eliminat cind se modifica un program netrivial; in special un program mare scris de altcineva.
Aceste probleme sint adesea mai greu de eliminat din cauza ca functiile de felul lui print() sint adesea organizate pentru a avea avantaje asupra partilor comune ale claselor implicate. De exemplu:
void print_employee(employee* e)
}
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.
2.8 Functii virtuale
Functiile virtuale rezolva problemele solutiei cu cimpuri de tip, permitind programatorului sa declare functii intr-o clasa de baza care pot fi redefinite in fiecare clasa derivata. Compilatorul si incarcatorul vor garanta corespondenta corecta intre obiecte si functii aplicate la ele. De exemplu:
struct employee;
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()
Functia virtuala poate fi utilizata chiar daca nu este derivata nici o clasa din clasa ei iar o clasa derivata care nu are nevoie de o versiune speciala a functiei virtuale nu este necesar sa furnizeze vreo versiune. Cind se scrie o clasa derivata, pur si simplu se furnizeaza o functie potrivita daca este necesar. De exemplu:
struct manager : employee;
void manager::print()
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)
Fiecare angajat va fi scris potrivit tipului lui. De exemplu:
main()
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 furnizeazaprogramatorului 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.
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.
3.1 O interfata
Consideram scrierea unei clase slist pentru liste simplu inlantuite in asa fel ca clasa sa poata fi utilizata ca o baza pentru a crea atit liste eterogene cit si omogene de obiecte de tipuri inca de definit. Intii noi vom defini un tip ent:
typedef void* ent;
Natura exacta a tipului ent nu este importanta, dar trebuie sa fie capabil sa pastreze un pointer. Apoi noi definim un tip slink:
class slink
};
Un link poate pastra un singur ent si se utilizeaza pentru a implementa clasa slist:
class slist
slist(ent a)
~slist()
};
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.
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 &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)
return 0;
}
int slist::append(ent a)
{
if(last)
last = last->next = new slink(a, last->next);
else
return 0;
}
ent slist::get()
Sa observam modul in care se apeleaza slist_handler (declaratia lui poate fi gasita in &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()
while(l!=last);
}
Clasa slist nu furnizeaza nici o facilitate pentru cautarea intr-o lista ci numai mijlocul de a insera si de a sterge membri. Cu toate acestea, atit clasa slist, cit si clasa slink, declara ca clasa slist_iterator este un prieten, asa ca noi putem declara un iterator potrivit. Iata unul in stilul prezentat in &6.8:
class slist_iterator
ent operator()()
return ll ? ll->e : 0;
}
};
3.3 Cum sa o folosim
Asa cum este, clasa slist virtual nu este utila. Inainte de toate, la ce foloseste o lista de pointeri void* ? Smecheria este de a deriva o clasa din slist pentru a obtine o lista de obiecte al unui tip care este de interes intr-un program particular. Sa consideram un compilator pentru un limbaj de felul lui C++. Aici listele de nume vor fi utilizate extensiv; un nume este ceva de forma:
struct name;
Pointerii spre name vor fi pusi in lista in locul obiectelor name. Aceasta permite utilizarea cimpului de informatie unica, e, a lui slist si admite ca un nume sa fie in mai multe liste in acelasi timp. Iata o definitie a unei clase nlist care deriva trivial din clasa slist:
#include 'slist.h'
#include 'name.h'
struct nlist : slist
void append(name* a)
name* get()
nlist()
nlist(name* a) : (a)
Functiile clasei noi sint sau mostenite direct din slist, sau fac numai conversie de tip. Clasa nlist nu este nimic altceva decit o alternativa de interfata pentru clasa slist. Din cauza ca tipul ent in realitate este void*, nu este necesar sa se converteasca explicit pointerii name* utilizati ca parametri actuali (&2.3.4).
Listele de nume ar putea fi utilizate in acest fel intr-o clasa care reprezinta o definitie de clasa:
struct classdef;
si numele s-ar putea adauga la acele liste in aceasta maniera:
void classdef::add_name(name* n)
if(n->is_operator()) operators.append(n);
}
unde is_operator() si is_friend() sint functii membru ale clasei name. Functia find() ar putea fi scrisa astfel:
int find(nlist* ll, name* n)
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 &3.5. Un nlist s-ar putea imprima printr-o functie astfel:
void print_list(nlist* ll, char* list_name)
3.4 Tratarea erorilor
Exista 4 conceptii la problema in legatura cu ce sa facem cind o facilitate cu scop general, cum ar fi slist intilneste o eroare la executie (in C++, unde nu sint prevazute facilitati specifice ale limbajului pentru tratarea erorilor la executie):
[1] Se returneaza o valoare ilegala si se lasa ca utilizatorul sa o verifice;
[2] Se returneaza o valoare de stare suplimentara si se lasa ca utilizatorul sa o verifice;
[3] Se apeleaza o functie furnizata ca parte a clasei slist;
[4] Se apeleaza o functie eroare care se presupune ca o va furniza utilizatorul.
Pentru un program mic scris de un singur utilizator, nu exista un motiv pentru a alege o solutie sau alta. Pentru o faci- litate generala solutia este cit se poate de diferita.
Prima conceptie, care returneaza o valoare ilegala, nu este fezabila. In general nu exista un mod de a sti ca o valoare particulara este ilegala pentru toti utilizatorii unui slist.
Conceptia a doua, care returneaza o valoare stare, poate fi utilizata in unele cazuri (o variatie a acestei scheme se foloseste pentru sirurile standard I/O istream si ostream; asa cum se explica in &8.4.2). Cu toate acestea, ea sufera de probleme serioase, caci daca o facilitate esueaza des, utilizatorii nu se vor mai obosi sa verifice valoarea starii. Mai mult decit atit, o facilitate poate fi utilizata in sute sau mii de locuri intr-un
program. Verificarea starii in fiecare loc ar face programul mult mai greu de citit.
Cea de a treia conceptie, care furnizeaza o functie de eroare, nu este flexibila. Nu exista o cale pentru implementatorul unei facilitati de scop general sa stie cum utilizatorii ar dori sa fie tratate erorile. De exemplu, un utilizator ar putea prefera erori scrise in daneza sau romana.
Cea de a patra conceptie, lasind ca utilizatorul sa furnizeze o functie eroare, are o anumita atractie cu conditia ca implementatorul sa prezinte clasa ca o biblioteca (&4.5) ce con- tine versiuni implicite pentru functiile de tratare a erorilor.
Solutiile 3 si 4 pot fi facute mai flexibile (si esential echivalente) specificind un pointer spre o functie, decit functia insasi. Aceasta permite proiectantului unei facilitati de forma lui slist sa furnizeze o functie eroare implicita, ceea ce face ca programatorilor sa le fie mai simplu decit sa furnizeze fun- ctia lor proprie cind este necesar. De exemplu:
typedef void (*PFC)(char*);//pointer spre un tip functie
extern PFC slist_handler; extern PFC set_slist_handler(PFC);
Functia set_slist_handler() permite utilizatorului sa inlocuiasca prelucrarea implicita. O implementare conventionala furnizeaza o functie implicita de tratare a erorilor care intii scrie un mesaj in cerr, apoi termina programul utilizind exit():
#include 'slist.h'
#include <stream.h>
void default_error(char* s)
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)
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.
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) : slist
int append(type a)
type get()
gslist(type)()
gslist(type)(type a):(ent(a))
~gslist(type)()
};
struct gslist_iterator(type) : slist_iterator{
gslist_iterator(type)(gslist(type)& s):((slist&)s)
type operator()()
};
Un backslash ('') indica faptul ca linia urmatoare este parte a macroului care se defineste.
Utilizind acest macro, o lista de pointeri spre name, asa cum a fost utilizata in prealabil clasa nlist, poate fi definita astfel:
#include 'name.h'
typedef name* Pname;
declare(gslist, Pname); //declara clasa gslist(Pname)
gslist(Pname) nl; //declara un gslist(Pname)
Macroul declare este definit in <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 &6.2.
3.6 Interfete restrictive
Clasa slist este o clasa cit se poate de generala. Uneori o astfel de generalitate nu este necesara sau nu este de dorit. Forme restrictive cum ar fi stive si cozi sint chiar mai frecvente decit insasi listele generale. Nedeclarind clasa de baza publica, se pot furniza astfel de structuri de date. De exemplu o coada de intregi poate fi definita astfel:
#include 'slist.h'
class iqueue : slist
int get()
iqueue()
};
Doua operatii logice se fac prin aceasta derivare: conceptul de lista este restrins la conceptul de coada, iar tipul int se specifica pentru a restringe conceptul unei cozi la tipul de coada de date intregi (iqueue). Aceste doua operatii ar putea fi date separat. Aici prima este o lista care este restrinsa asa ca ea ar putea fi utilizata numai ca o stiva:
#include 'slist.h'
class stack : slist{
public:slist::insert;
slist::get;
stack()
stack(ent a) : (a)
care poate fi apoi utilizata sa creeze tipul 'stiva de pointeri spre caractere':
#include 'stack.h'
class cpstack : stack
char* pop()
4 Adaugarea la o clasa
In exemplele precedente, nu se adauga nimic la clasa de baza prin clasa derivata. Functiile se definesc pentru clasele derivate numai pentru a furniza conversie de tip. Fiecare clasa deri- vata furnizeaza pur si simplu o interfata in loc de o multime de rutine comune. Aceasta este o clasa speciala importanta, dar motivul cel mai frecvent pentru care se defineste o clasa noua ca o clasa derivata este faptul ca se vrea ceea ce furnizeaza clasa de baza, plus inca ceva.
Pot fi definite date si functii membre noi pentru o clasa derivata, in plus fata de cele mostenite din clasa ei de baza. Sa observam ca atunci cind un element este pus intr-o slist in pre- alabil definita, se creaza un slink care contine doi pointeri. Aceasta creare ia timp. De un pointer ne putem dispensa, cu con- ditia ca este necesar ca un obiect la un moment dat sa fie numai intr-o lista, asa ca pointerul next poate fi plasat in obiectul insusi (nu intr-un obiect slink separat). Ideea este de a furniza o clasa olink cu numai un cimp next si o clasa olist care poate manipula pointeri la astfel de inlantuiri. Obiectele oricarei clase derivate din olink pot fi manipulate prin olist. Litera 'o' din nume este pentru a ne reaminti ca un obiect poate fi numai intr-o olist la un moment dat:
struct olink;
Clasa olist este similara cu clasa slist. Diferenta este ca un utilizator al clasei olist manipuleaza obiectele clasei olink direct:
class olist;
Noi putem deriva clasa name din clasa olink:
class name : olink;
Acum este trivial sa se faca o lista de name care poate fi utilizata fara a aloca spatiu sau timp suplimentar.
Obiectele puse in olist isi pierd tipul, adica compilatorul stie ca ele sint olink. Tipul propriu poate fi restabilit folosind conversia explicita de tip a obiectelor luate din olist. De exemplu:
void f()
Alternativ, tipul poate fi restabilit derivind o alta clasa din olist care sa trateze conversia de tip:
class onlist : olist
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.
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).
6 Un program complet
Sa consideram un program care deseneaza figuri geometrice pe ecran. El consta din trei parti:
[1] Un control de ecran: rutine de nivel inferior si structuri de date care definesc ecranul; acestea stiu desena numai puncte si linii drepte;
[2] O biblioteca de figuri: un set de definitii si figuri generale cum ar fi dreptunghi, cerc, etc. si rutine standard pentru a le manipula;
[3] Un program aplicativ: un set de definitii specifice a-l plicatiei si cod care sa le utilizeze.
De obicei, cele trei parti vor fi scrise de persoane diferite. Partile sint scrise in ordinea prezentarii lor cu adaugarea complicatiilor pe care proiectul de nivel mai inferior nu are idee despre modul in care codul lui va fi eventual utilizat. Exemplul urmator releva acest lucru. Pentru ca exemplul sa fie simplu pentru prezentare, biblioteca de figuri furnizeaza numai citeva servicii simple, iar programul de aplicatii este trivial. O conceptie extrem de simpla a ecranului se utilizeaza asa ca cititorul sa poata incerca programul chiar daca nu sint disponibile facilitatile de grafica. Este simplu sa se schimbe partea cu ecranul a programului cu ceva potrivit fara a schimba codul bibliotecii de figuri sau programul de aplicatie.
6.1 Controlul ecranului
Intentia a fost sa se scrie controlul ecranului in C (nu in C++) pentru a accentua distinctia intre nivelele implementarii. Aceasta s-a constatat a fi plicticos, asa ca s-a facut un compromis: stilul de utilizare este din C (nu exista functii membru, functii virtuale, operatori definiti de utilizator, etc.), dar se folosesc constructori, se declara si se verifica argumentele functie, etc.. Ca rezultat, controlul ecranului arata foarte mult ca un program in C care a fost modificat ca sa posede avantajele lui C++ fara a fi total rescris.
Ecranul este reprezentat ca un tablou de caractere bidimensional, manipulat prin functiile put_point() si put_line() ce utilizeaza structura point cind ne referim la ecran:
//fisierul screen.h
const XMAX=40, YMAX=24;
struct point{
int x, y;
point()
point(int a, int b)
};
overload put_point; extern void put_point(int a, int b);
inline void put_point(point p)
overload put_line; extern void put_line(int, int, int, int); inline void put_line(point a, point b)
extern void screen_init(); extern void screen_refresh(); extern void screen_clear();
#include <stream.h>
Inainte de a utiliza o functie put(), ecranul trebuie sa fie initializat prin screen_init(), iar schimbarile ecranului spre structuri de date sint reflectate pe ecran numai dupa apelul lui screen_refresh(). Cititorul va afla ca refresh se face pur si simplu scriind o copie noua a tabloului ecran sub versiunea precedenta. Iata functiile si definitiile de date pentru ecran:
#include 'screen.h'
#include <stream.h>
enum color; char screen[XMAX][YMAX]; void screen_init()
Punctele se scriu numai daca sint in domeniul ecranului:
inline int on_screen(int a, int b)
void put_point(int a, int b)
Functia put_line() se foloseste pentru a desena linii:
void put_line(int x0, int y0, int x1, int y1)
}
Pentru stergere si resetare se folosesc functiile:
void screen_clear()
void screen_refresh()
}
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.
6.2 Biblioteca de figuri
Noi trebuie sa definim conceptul general de figura. Acest lucru trebuie facut intr-un astfel de mod incit figura sa poata fi comuna pentru toate figurile particulare (de exemplu cercuri si patrate) si intr-un astfel de mod ca orice figura poate fi manipulata exclusiv prin interfata furnizata de clasa shape:
struct shape
virtual point north()
virtual point south()
virtual point east()
virtual point neast()
virtual point seast()
virtual point draw(); virtual void move(int, int);
};
Ideea este ca figurile sint pozitionate prin move() si se plaseaza pe ecran prin draw(). Figurile pot fi pozitionate relativ una fata de alta folosind conceptul de contact points, denu- mit dupa punctele de pe compas. Fiecare figura particulara defineste sensul acelor puncte pentru ea insasi si fiecare defineste cum se deseneaza. Pentru a salva hirtie, in acest exemplu sint definite numai punctele de compas necesare. Constructorul shape::shape() adauga figura la o lista de figuri shape_list. Aceasta lista este un gslist, adica o versiune a unei liste ge- nerice simplu inlantuite asa cum a fost definita in &3.5. Ea si un iterator de corespondenta s-au facut astfel:
typedef shape* sp; declare(gslist, sp); typedef gslist(sp) shape_list; typedef gslist_iterator(sp) sl_iterator;
asa ca shape_list poate fi declarata astfel:
shape_lst shape_list;
O linie poate fi construita sau din doua puncte sau dintr-un punct si un intreg. Ultimul caz construieste o linie orizontala de lungime specificata printr-un intreg. Semnul intregului indica daca punctul este capatul sting sau drept. Iata definitia:
class line : public shape
point south()
void move(int a, int b)
void draw()
line(point a, point b)
line(point a, int l)
};
Un dreptunghi este definit similar:
class rectangle : public shape
point south()
point neast() point swest() void move(int a, int b)
void draw(); rectangle(point, point);
Un dreptunghi este construit din doua puncte. Codul este complicat din necesitatea de a figura pozitia relativa a celor doua puncte:
rectangle::rectangle(point a, point b)
else
}
else
else
}
}
Pentru a desena un dreptunghi trebuie desenate cele patru laturi ale sale:
void rectangle::draw()
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()
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
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.
6.3 Programul de aplicatie
Programul de aplicatie este extrem de simplu. Se defineste figura myshape, care arata un pic ca o fata, apoi se scrie un program main care deseneaza o astfel de fata purtind o palarie. Declaratia lui myshape:
#include 'shape.h'
class myshape : public rectangle;
Ochii si gura sint separate si sint obiecte independente create prin constructorul myshape:
myshape::myshape(point a, point b) : (a, b)
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()
myshape se muta transferind dreptunghiul de baza si obiectele secundare l_eye, r_eye si mouth:
void myshape::move(int a, int b)
In final noi putem construi citeva figuri si sa le mutam un pic:
main()
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 Memoria libera
Daca noi utilizam clasa slist, am putea gasi ca programul nostru utilizeaza timp considerabil pentru alocare si dealocare de obiecte ale clasei slink. Clasa slink este un prim exemplu de clasa care ar putea beneficia de faptul ca programatorul sa aiba control asupra memoriei libere. Tehnica optimizata descrisa in &5.5.6 este ideala pentru acest tip de obiect. Intrucit orice slink se creaza folosind new si se distruge folosind delete de catre membri clasei slist, nu exista probleme cu alte metode de alocare de memorie.
Daca o clasa derivata asigneaza la this constructorul pentru clasa ei de baza va fi apelat numai dupa ce s-a facut asignarea, iar valoarea lui this in constructorul clasei de baza va fi cea atribuita prin constructorul clasei derivate. Daca clasa de baza asigneaza la this, valoarea asignata va fi cea utilizata de constructor pentru clasa derivata. De exemplu:
#include <stream.h>
struct base;
struct derived : base; base::base()
derived::derived()
main()
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.
8 Exercitii
1. (*1). Se defineste:
class base
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 (&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.
(*2). Sa se proiecteze si sa se implementeze o lista dublu inlantuita care poate fi folosita numai printr-un iterator. Iteratorular trebui sa aiba operatii pentru parcurgeri inainte sau inapoi, operatiipentru a insera si sterge elemente in lista si un mod de a face acces laelementul curent.
8. (*2). Sa se implementeze o versiune generica a unei liste dublu inlantuite.
9. (*4). Sa se implementeze o lista in care obiectele (si nu numai pointerii spre obiecte) se insereaza si se extrag. Sa se faca sa functioneze pentru o clasa X unde X::X(X&), X::~X() si X::operator=(X&) sint definite.
10. (*5). Sa se proiecteze si sa se implementeze o bibliote8ca pentru a scrie simulari de drivere de evenimente. Indicatie: <task.h>.Acesta este un program mai vechi si puteti scrie unul mai bun. Ar trebui safie o clasa task. Un obiect al clasei task ar putea sa fie capabil sa salvezestarea lui si sa aiba de restabilit acea stare (noi ar trebui sa definimtask::save() si task::restore()) asa ca ar trebui sa opereze ca o corutina.
Taskuri specifice pot fi definite ca obiecte de clase derivate din clasa task.Programul de executat printr-un task ar putea fi specificat ca o functievirtuala. Ar putea fi posibil sa se paseze argumentele la un task nou caargumente pentru constructorul lui. Ar trebui sa fie un distribuitorimplementat ca un concept de timp virtual. Sa se furnizeze o functie task::delay(long) care 'consuma' timp virtual. Daca distribuitorul este o parte a clasei task sau este separat, va fi o decizie majora a proiectarii.
Taskurile vor trebui sa comunice. Sa se proiecteze o clasa queue pentru aceasta. Sa se trateze erorile de la executie intr-un mod uniform. Cum se depaneaza programele scrise utilizind o astfel de biblioteca?