Sunteți pe pagina 1din 36

Scurt tutorial de C++

2.Introducere in C++
2.1. Noi extensii pentru C++ cout, cin si cerr n analogie cu C, C++ defineste driverele standard de intrare si iesire care snt deschise cnd un program este executat. Driverele snt: . cout, analog cu stdout . cin, analog cu stdin . cerr, analog cu stderr. Sintactic, aceste drivere nu snt folosite cu functii: datele snt citite sau scrise prin intermediul operatorilor << si >> : #include <iostream.h> int main() { int ival; char sval[30]; cout << "Introduceti un numar: " << '\n'; cin >> ival; cout << " Si un sir :" << '\n'; cin >> sval; cout << " Numarul este :" << ival << "\n iar sirul este :" << sval << '\n\'; return (0); } Programul citeste un numar si un sir de pe driverul cin (de obicei tastatura) si afiseaza aceste date pe cout (monitor). Putem face urmatoarele observatii: . Driverele snt declarate n headerul iostream.h . Driverele cout, cin si cerr snt de fapt 'obiecte' dintr-o anumita clasa care proceseaza intrarea si iesirea unui program. . Driverul cin citeste datele si copie informatiile n variabile folosind operatorul >> (deplasare dreapta). Vom descrie mai trziu cum pot fi (re)definite actiunile operatorilor n C++, altfel dect actiunea lor implicita. . Operatorii ce manipuleaza cin, cout si cerr (adica >> si << ) manipuleaza n acelasi timp si variabile de diferite tipuri. Actiunea lor depinde n final de tipul datelor. Driverele cin, cout si cerr nu fac parte din gramatica lui C++, asa cum este ea definita de compilatorul ce parseaza fisierul sursa. Definitiile lor apar doar n fisierul header iostream.h. n acelasi timp, se pot folosi si vechile functii printf() si scanf(); e o chestiune de gust. Exista nsa niste avantaje si dezavantaje: . Comparat cu functiile standard C, printf () si scanf (), folosirea operatorilor << si >> mpreuna cu driverele corespunzatoare este mult mai sigura n ceea ce priveste tipul datelor (formatul lor este descris de programator n primul caz si este recunoscut de compilator n cel de-al doilea) . Functiile printf () si scanf (), precum si alte functii ce folosesc formatul explicit, implementeaza n fond un mini-limbaj care este interpretat numai la executie. Din contra, compilatorul C++ cunoaste exact ce actiune de intrare/iesire trebuie sa efectueze. . Folosirea operatorilor << si >> ilustreaza posibilitatile lui C++, nsa nu mbunatatesc lizibilitatea programului. Cuvntul cheie const Cuvntul cheie const apare deseori n programele C++, desi face parte de asemeni si din limbajul C. El exprima faptul ca valoarea unei variabile sau a unui argument nu poate fi modificata. De exemplu: int main() { int const ival = 3; //o constanta int initializata cu 3 ival = 4 ; // asignarea va duce la un mesaj de eroare return (0); } Variabilele declarate const pot fi, n opozitie ca n C, folosite ca specificatori de lungime pentru tablouri, ca n exemplul: int const size = 20; char buf [size]; // tablou de 20 de caractere O alta utilizare a cuvntului cheie const apare n declaratiile pointerilor . n declaratia char const * buf; buf este o variabila pointer, ce pointeaza catre chars. Tot ce se gaseste la adresa pointata de buf nu poate fi schimbat, deoarece chars a fost declarat constant. n schimb, pointerul buf poate fi modificat. Deci atribuirea * buf = 'a' este gresita, n schimb buf++ nu. Chiar si declaratia char const *const buf; este permisa; aici nici pointerul, nici valoarea de la adresa buf nu pot fi schimbate. Regula de baza este deci: tot ce apare chiar nainte de cuvntul cheie const nu poate fi modificat. Referinte n afara de declaratiile normale de variabile, C++ permite ca referintele sa fie declarate ca sinonime ale variabilelor. O referinta catre o variabila este ca un pseudonim: numele variabilei si numele referintei pot fi

Scurt tutorial de C++


folosite n instructiuni ce afecteaza variabila: int int_value; int &ref = int_value; n acest exemplu variabila int_value este definita initial, apoi este definita si referinta ref care conduce la initializarea adresei sale cu adresa variabile int_value. n definitia lui ref, operatorul de referinta & indica faptul ca ref nu este el nsusi un ntreg, ci o referinta la un ntreg. Urmatoarele instructiuni int_value++; ref++; au acelasi efect. La aceeasi locatie de memorie o valoare int este incrementata cu unu; nu conteaza cum este numita locatia. Referintele joaca un rol important n C++ ca un mijloc de a pasa argumente ce pot fi modificate (ca argumentele var din Pascal). De exemplu, n C standard, o functie ce incrementeaza valoarea argumentului sau cu 5 dar nu returneaza nimic (void), necesita un argument pointer: void increase (int *valp) // asteapta un pointer la un intreg { *valp += 5; } int main() { int x; increase (&x) // adresa lui x este pasata ca argument return (0); } Aceasta constructie poate fi folosita si n C++, dar acelasi efect poate fi obtinut cu o referinta: void increase (int &valr) // asteapta o referinta la un ntreg { valr += 5 ; } int main() { int x; increase (x) ; // o referinta la x este pasata ca argument return (0); } Modul n care compilatorul C++ implementeaza referintele reprezinta de fapt o simpla utilizare de pointeri. Sugestiile ce pot fi date pentru folosirea referintelor ca argumente snt urmatoarele: . n acele situatii n care un apel de functie nu modifica argumentele sale, se poate pasa o copie a variabilelor: void some_func (int val) { printf (" %d \n", val); } int main() { int x; some_func (x); // este pasata o copie, deci x nu va fi schimbat return (0); } . n situatia n care functia schimba valoarea argumentelor, va fi pasata adresa: void increase (int * valp) { *valp += 5; } int main() { int x ; increase (&x); // este pasat un pointer, x poate fi schimbat return (0); } . Referintele pot fi rezervate pentru acele cazuri n care argumentul nu este schimbat de functie, dar unde este preferabil sa folosesti o referinta (pointer de fapt) catre variabila dect o copie a varibilei. Aceste situatii apar atunci cnd trebuie pasata o variabila de mari dimensiuni (struct) ; stiva creste foarte mult prin folosirea unei copii a variabilei: struct person // structura cu dimensiune mare { char name [80], address [90]; double salary; }; void printperson (person &p) // functia cere o referinta la o structura { printf ("Name : %s\n ","Address : %s\n", p.name, p.address); } int main() { person boss; printperson (boss); // variabila nu este alterata de functie } O mbunatatire se poate aduce folosind cuvntul const: void printperson (person const &p) . (Mai exista un motiv pentru folosirea referintei pentru pasarea unui obiect ca argument pentru o functie: n aceste caz activarea unei copii a constructorului obiectului este evitata. Amanunte mai trziu). Referintele pot duce de asemeni la aparitia unui cod 'urt'. O functie, de exemplu, poate ntoarce o referinta la o variabila, ca n exemplul urmator:

Scurt tutorial de C++


int &func (void) { static int value; return (value); } permitnd urmatoarele constructii: func () = 20; func () += func (); n final, evidentiem cteva diferente ntre pointeri si referinte: . O referinta nu poate exista prin ea nsasi, adica fara a face referire la ceva. O declaratie de forma int &ref; nu este permisa (la cine refera ref ?). Exceptii snt cele declarate external (se presupune ca ele snt initializate n alta parte), cele cuprinse n listele de argumente ale unei functii (apelul va face initializarea), referintele ca tip returnat de o functie (functia va determina la ce valoare va face referire) si referintele ce fac parte ca date dintr-o clasa (descrise mai trziu). n contrast, pointerii snt variabile n sine. Ei pointeaza fie la ceva concret, fie la nimic. . Cu exceptia cazului referintelor reprezentnd valoarea returnata de o functie, referintele snt pseudonime pentru alte variabile si nu pot fi 'redirectionate" catre alte variabile. Odata referinta definita, ea se refera strict la acea variabila. Din contra, pointerii pot fi oricnd reasignati. . Cnd un operator adresa & este folosit asupra unei referinte, expresia va contine adresa variabilei la care se refera referinta. Din contra, pointerii fiind ei nsisi variabile, adresa lor nu va avea nici o legatura cu adresa variabilei pointate. 2.2.Functii ca parte dintr-o structura Am vazut ca functiile pot face parte dintr-o structura. Asemenea functii se numesc functii 'membru'. n continuare prezentam modul de definire a unor asemenea functii. struct person { char name [80], address [90] ; void print (void); } Functia membru print () este definita folosind numele structurii (person) si operatorul scope (::): void person::print () { printf ("Name: %s\n" "Address: %s\n", name, address); } n aceasta definitie a functiei membru, numele functiei este precedat de numele structurii urmat de ::. Observam cum pot fi adresate cmpurile din structura fara a folosi si numele structurii (variabila name sau address). Deoarece functia print () este parte a structurii person, implicit si variabila name (address) se refera la acelasi tip. Un mod de apel este urmatorul: person p; strcpy (p.name, "Karel"); strcpy (p.address, "Copou, nr. 33"); p.print (); Avantajul unei functii membru consta n faptul ca apelul functiei poate adresa automat adresele cmpurilor din structura din care este invocata. De aceea n instructiunile functiei print () variabilele names si address snt cele ale aceleiasi structuri p. 2.3.Ascunderea datelor: public, private C++ contine posibilitati sintactice speciale pentru implementarea ascunderii datelor (adica a abilitatii programului de a ascunde date de alte parti ale programului, evitnd adresari gresite sau coliziuni de nume). C++ poseda doua cuvinte cheie speciale legate de acest subiect: private si public. Ele pot fi inserate n definitia unei structuri. Cuvntul public defineste toate cmpurile unei structuri accesibile de ntreg codul program; cuvntul private defineste toate cmpurile accesibile doar de codul ce face parte din structura (adica accesibile numai functiilor membru). Mai exista cuvntul cheie protected. Folosirea lui va fi exemplificata mai trziu. ntr-o structura toate cmpurile snt public daca nu este specificat explicit altceva. struct person { public: void setname(char const *n), setaddress (char const *a), print (void); char const *getname (void), *getaddress (void); private: char name [80], address [80]; }; Cmpurile name si address snt singurele accesibile pentru functiile membru definite n struct, fiind precedate de private. Exemplu: person x; x.setname ("Frank"); //o.k., setname() este public strcpy(x.name, "Kurt"); //gresit, name este privat Conceptul de ascundere a datelor este realizat astfel n aceasta structura: datele au nume doar n interiorul structurii. Ele snt accesate din afara de functii speciale, ce fac parte din structura, si care controleaza traficul ntre cmpurile structurii si restul programului (mai snt numite functii 'interfata'). Mai notam ca functiile setname() si setaddress() snt declarate ca avnd un argument char const * (deci nu vor altera sirurile primite ca argument). n acelasi timp functiile getname() si getaddress() ntorc un char const *: apelul nu poate modifica sirurile la care vor fi pointate valorile returnate. Iata un exemplu de implementare: void person::setname (char const * n) { strcpy (name, n, 79); name [79] = '\0'; } char const * person::getname () { return ( (char const * ) name ); }

Scurt tutorial de C++


n general, puterea unei functii membru si conceptul de ascundere a datelor snt legate de faptul ca functiile interfata pot efectua actiuni specifice, cum ar fi validarea datelor. Un alt exemplu al conceptului de ascundere al datelor este urmatorul: ca o alternativa la functia membru ce pastreaza datele n memorie (ca mai sus), se poate dezvolta o biblioteca cu functii interfata ce sa stocheze datele pe disc. Conversia programului pentru utilizarea noii metode de stocare se va face doar prin relinkeditarea cu noua biblioteca. Desi ascunderea datelor poate fi realizata prin intermediul struct, cel mai adesea snt folosite clasele. O clasa este n principiu echivalenta cu o structura cu exceptia faptului ca toate cmpurile snt, daca nu e specificat altfel, private. Definitia clasei person se va face nlocuind cuvntul struct cu cuvntul class (si recomandam ca prima litera din numele claselor sa fie litera mare, deci class Person). 2.4.Structuri n C versus structuri n C++ n C nu este obisnuit sa definesti functii care sa proceseze o structura si care vor necesita ca un argument sa fie un pointer la struct . Iata un fragment de fisier header imaginar n C: /* definitia unei structuri PERSON_ */ typedef struct { char name [80], address [80]; } PERSON_ /* cteva functii pentru manipularea structurii PERSON_ */ /* initializeaza cmpurile cu un nume si o adresa */ extern void initialize (PERSON_ *p, char const *nm, char const * adr); /* listeaza informatiile */ extern void print (PERSON_ const *p); /* etc. */ n C++, declaratiile functiilor implicate vor fi plasate n interiorul definitiei structurii (sau clasei). Argumentul ce indica asupra carei structuri se va aplica functia nu mai este deci necesar. class Person { public: void initialize (char const * nm, char const * adr); void print (void); // etc... private: char name [80], address [80]; }; Argumentul struct este implicit n C++. Un apel de functie n C PERSON_ x; initialize (&x, "un nume", "o adresa"); devine n C++ Person x; x.initialize ("un nume", "o adresa");

3.Clase
n acest capitol vom introduce si vom exemplifica notiunea de clasa. n pasi succesivi vom construi clasa Person ce va putea fi folosita pentru crearea unei baze de date. Definitia acestei clase este: class Person { public: // functiile interfata void setname( char const *n); void setaddress (char const *a); void setphone (char const *p); char const *getname (void); char const *getaddress (void); char const *getphone (void); private: //cmpurile de date char *name; char *address; char *phone; }; Datele snt private, deci pot fi accesate doar de functiile din clasa Person. Aceste functii fie seteaza unui cmp o anumita valoare (set..()), fie inspecteaza datele (get...()). 3.1. Constructori si destructori O clasa n C++ poate contine doua functii speciale care snt implicate n munca interna a clase respective. Ele snt numite constructor si destructor. Constructorul Functia constructor are prin definitie acelasi nume ca si clasa corespunzatoare. Ea nu are o valoare returnata specifica, nici macar void. De exemplu, pentru clasa Person, constructorul este Person::Person (). Sistemul C++ asigura apelul unui constructor pentru o anumita clasa atunci cnd este creat un obiect al acelei clase . Este desigur posibil sa definesti o clasa fara nici un constructor explicit: n acest caz sistemul fie nu face nici un apel, fie apeleaza un constructor fals (ce nu face nimic) n momentul crearii unui obiect. Daca un obiect este o variabila locala ne-statica ntr-o functie, constructorul este apelat n momentul executiei functiei. Daca

Scurt tutorial de C++


obiectul este o variabila statica sau globala, constructorul este apelat naintea nceperii executiei programului (chiar nainte de main()). #include <stdio.h> // o clasa Test cu o functie constructor class Test { public: Test (); }; //functia constructor declarata public Test::Test () { puts ("constructorul clase Test este apelat");} // programul test Test g; // obiect global void func () { Test l; // obiect local n functia func() puts(" Aici e apelat func"); } int main() { Test x; // obiect local n functia main() puts ("functia main()"); func(); return (0); } Aici am definit o clasa ce contine doar o functie: constructorul. Constructorul nsusi nu are dect o singura actiune: listeaza un mesaj. Programul contine trei obiecte din clasa Test: unul global, unul local n main() si unul local n func(). Se poate observa: numele constructorului (acelasi ca al clasei), lipsa valorii returnate si lipsa argumentelor (asa numitul constructor implicit - e posibil nsa sa fie definiti si constructoru cu argumente). Apelul constructorului n executia programului are loc n ordinea: 1) la crearea obiectului global g; 2) la crearea obiectului local x din functia main(); 3) la crearea obiectului local l din functia func(). Pe ecran se va obtine deci n final: constructorul clasei Test este apelat constructorul clasei Test este apelat functia main() constructorul clasei Test este apelat Aici este apelat func Destructorul O a doua functie speciala este destructorul (opusul constructorului n sensul ca ea este apelata n momentul n care un obiect nceteaza sa mai existe). Pentru obiectele ne-statice locale, destructorul este chemat atunci cnd functia n care este definit obiectul ntoarce valoarea return; pentru cele statice (globale), destructorul este apelat naintea terminarii programului. Chiar daca programul este ntrerupt cu un exit(), vor fi apelati destructorii pentru obiectele existente. Cnd se defineste un desrtuctor, se respecta urmatoarele reguli: . Destructorul are acelasi nume ca si clasa, dar precedat de tilda . Destructorul nu are nici un argument sau o valoare de returnat. Exemplu: class Test { public: Test (); //constructorul ~Test (); // destructorul ... }; O prima aplicatie Una din aplicatiile constructorului si destructorului o reprezinta managementul alocarii memoriei. Exemplificam cu clasa Person. Ea contine trei pointer private, toti de tip char *. Acesti membrii snt manipulati prin functiile interfata. Cnd un nume, adresa sau telefon snt definite, va fi alocata memorie pentru memorarea acestor date. Evidentiem operatiile necesare: . Constructorul clasei se asigura ca toate datele snt initial pointeri NULL . Destructorul va elibera toata memoria alocata . Definirea unui nume (adresa, telefon), prin intermediul functiilor set..() va consta n doi pasi. n primul, memoria alocata anterior va fi eliberata; n al doilea sirul (argumentul functiei set..()) este copiat n memorie . Inspectarea unei date prin intermediul functiilor get..() va returna un pointer: fie pointerul NULL (data nu e definita), fie un pointer catre zona de memorie alocata datei. Functiile set..() snt prezentate n continuare. Copierea sirurilor se face printr-o functie imaginara xtrdup(), care copie sirul sau termina executia programului daca memoria nu este suficienta. // functiile interfata set..() void Person::setname (char const *n) { free (name); name = xstrdup (n); } void Person::setaddress (char const *n) { free (address); address = xstrdup (n); } void Person::setphone (char const *n)

Scurt tutorial de C++


{ free (phone); name = xstrdup (n); } Sa observam ca desi instructiunea free(..) este executata neconditionat, nu apar actiuni incorecte: daca datele au fost deja definite, la o noua definire memoria alocata anterior va fi eliberata; daca datele n-au fost nca definite, pointerii snt NULL, iar free (0) nu executa nimic. Mai trebuie precizat ca acest cod este mai familiar programatorilor n C (ce utilizeaza functia free()), pentru C++ existnd o instructiune mai potrivita, si anume delete. // functiile interfata get..() char const *Person :: getname () { return (name); } char const *Person :: getaddress () { return (address); } char const *Person :: getphone () { return (phone); } n final, constructorul, destructorul si definitia clasei Person: class Person { public: Person (); //constructorul ~Person (); //destructorul // functiile ce seteaza datele void setname (char const *n); void setaddress (char const *a); void phone (char const *p); //functiile ce inspecteaza datele char const *getname (void); char const *getaddress (void); char const *getphone (void); private: char * name; char *address; char *phone; }; //constructorul Person::Person () { name = address = phone = 0; } //destructorul Person :: ~Person () { free (name); free (address); free (phone); } Exemplificam n continuare cu un posibil program pentru folosirea acestei clase. Functia printperson () va fi utilizata pentru listarea datelor unui obiect de tip Person. Argumentul functiei va fi de tip referinta la un obiect Person. Faptul ca functia nu modifica argumentul (deci obiectul) este clar din prezenta cuvntului const. Mai remarcam ca destructorul nu este apelat n mod explicit. void printperson (Person const &p) { printf ("Numele: %s\n", "Adresa: %s\n", "Telefon: %s\n", p.getname(), p.getaddress(), p.getphone()); } int main () { Person p; p.setname("Liviu T."); p.setaddress(" Iasi, Copou"); p.setphone ("123213"); printperson (p) return (0); } Acest cod este doar un exemplu didactic; majoritatea compilatoarelor C++ n-ar putea genera cod executabil. Motivul este ca functia printperson () primeste un argument const, dar apelurile pentru acest argument (get.. ()) l-ar putea modifica, deci 'constanta' lui p n-ar putea fi garantata. Solutia va fi declararea acestor functii ca nemodificnd obiectul (explicatii, putin mai trziu). S-ar mai putea modifica codul lui printperson () pentru o verificare initiala a existentei unor date concrete (nume, adresa, telefon) si listarea numai acelora (cum ?). Constructori cu argumente n C++ este permisa descrierea de constructori cu argumente. Pentru clasa Person un asemenea constructor poate fi definit ca utiliznd trei argumente pentru nume, adresa, telefon: Person::Person (char const *n, char const *a, char const *p) { name = xstrdup (n); address = xstrdup (a); phone = xstrdup (p); } Constructorul trebuie inclus n definitia clasei. O declaratie ntr-un fisier header ar putea arata astfel: class Person { public: Person::Person (char const *n, char const *a, char const *p); .. ... }; Deoarece n C++ este permisa acoperirea functiilor, o asemenea declaratie poate coexista cu un constructor fara argumente. Utilizarea constructorului cu argumente este ilustrata n continuare: int main () { Person a ("Karel", "Brasov", "213342"), b; . . . }

Scurt tutorial de C++


Ordinea apelului constructorului Posibilitatea de a defini constructori cu argumente ne permite sa monitorizam exact n ce moment al executiei programului este creat sau distrus un obiect. class Test { public: Test () ; //constructor fara argumente Test (char const *name); // constructor cu argumente ~Test (); private: char *n //datele }; Test::Test () { n = strdup ("Fara nume"); printf ("Creat obiect Test fara nume\n"); } Test::Test (chat const *name) { n = strdup (name); printf ("Creat obiect Test cu numele %s\n",n); } Test::~Test () { printf ("Distrus obiect Test cu numele %s\n", n); free (n); } Test globaltest ("global"); void func () { Test functest ("func"); int main () { Test maintest ("main"); func (); return (0); } Listigul dupa rularea programului va fi: Creat obiect Test cu numele global Creat obiect Test cu numele main Creat obiect Test cu numele func Distrus obiect Test cu numele func Distrus obiect Test cu numele main Distrus obiect Test cu numele global

Functii membru constante si obiecte constante Cuvntul cheie const este deseori folosit n declaratiile functiilor membru, indicnd faptul ca aceste functii nu pot altera cmpurile de date, ci numai inspecta. Exemplu: class Person { public: .... char const *getname (void) const; char const *getaddress (void) const; char const *getphone (void) const; private: .. ... }; Dupa cum se poate observa, cuvntul const apare dupa lista argumentelor. Regula cuvntului const se aplica si aici: tot ce apare dupa cuvntul cheie nu poate fi alterat sau nu poate altera datele. Aceeasi specificatie trebuie sa apara si n definitia functiilor membru: char const *Person::getname () const { return (name); } O functie membru declarata const nu poate altera nici un cmp al clasei sale. Scopul functiilor const este acela de a permite crearea de obiecte const. Pentru asemenea obiecte pot fi apelate numai functiile ce nu le modifica, deci cele declarate const (cu singura exceptie: constructorul si destructorul, apelati automat). Sa cream un obiect const din clasa Person: Person const me ("Profesor", "Iasi", "146141"); //se face initializarea de catre constructor Instructiunea me.setname ("Student") este deci ilegala. 3.3 Operatorii new si delete Limbajul C++ defineste doi operatori ce snt specifici pentru alocarea si dealocarea de memorie. Ei snt new si delete. Fie, de exemplu, o variabila pointer la un int folosita pentru a pointa catre memoria alocata de new, memorie eliberata mai trziu de operatorul delete: int *ip; ip = new int; . . . delete ip; Fiind operatori, new si delete nu necesita paranteze (ca pentru functiile malloc() si free()). Alocarea si dealocarea tablourilor

Scurt tutorial de C++


Cnd operatorul new este folosit pentru alocarea memoriei pentru un tablou, dimensiunea variabilei trebuie plasata, ntre paranteze patrate, dupa tip: int *intarray; intarray = new int [20] ; //se aloca 20 de ntregi Regula sintactica pentru operatorul new este ca el trebuie urmat de un tip, urmat optional de o dimensiune; tiupul si numarul snt folosite de compilator pentru calcularea dimensiunii memoriei necesare alocarii. Un tablou este dealocat de operatorul delete: delete [] intarray; n aceasta instructiune operatorii de tablou [] indica faptul ca este dealocat un tablou. Regula este urmatoare: de cte ori new este urmat de [], delete trebuie urmat tot de [].

4.Clase: problema alocarii memoriei


Fata de setul de functii pentru gestionarea memoriei din C (malloc(), etc.), operatorii new si delete snt construiti astfel nct sa valorifice avantajele limbajului C++. Diferentele importante ntre malloc() si new snt: . Functia malloc() nu cunoaste la ce va fi folosita memoria alocata. De exemplu, cnd este alocata memorie pentru mai multi int, va trebui sa exprimam lungimea corecta cu ajutorul functiei sizeof (int). Din contra, new cere numai precizarea tipului; functia sizeof() este implicit apelata de compilator. . Singurul mod de a initializa memoria alocata de malloc() este folosirea lui calloc() (ce aloca memorie si o seteaza la o anumita valoare). Operatorul new cheama implicit constructorul obiectului pentru care este memorata memorie, constructor care poate fi nsotit de argumente. O comparatie analoaga poate fi facuta si ntre free() si delete. Apelul constructorului si a destructorului unui obiect au o serie de consecinte. Multe probleme apar din cauza incorectei alocari a memorie, a lipsei de memorie, a depasirilor, etc. C++ nu rezolva magic aceste probleme, dar pune la dispozitia programatorului o serie de mijloace pentru depasirea lor. 4.1. Clase cu date de tip pointer Sa revenim la clasa Person: class Person { public: //constructori si destructor Person(); Person( char const *n, char const *a, char const *p); ~Person(); void setname (char const *n); void setaddress (char const *a); void setphone (char const *p); char const *getname (void) const; char const *getaddress (void) const; char const *getphone (void) const; private: //datele char *name; char *address; char *phone; }; n aceasta clasa destructorul este necesar pentru a preveni ca memoria, odata alocata pentru cmpurile name, address si phone, sa nu devina inutilizabila la disparitia obiectului. n exemplul urmator este creat un obiect Person iar datele snt listate. Dupa terminarea functiei main(), memoria alocata va fi eliberata. Person:~Person () { delete name; delete address; delete phone; } void main() { Person kk ("Karel", "Berlin", "14999078"); *bill = new Person ("Bill", "Whashington", "0912021423045"); printf ("%s, %s, %s\n", kk.getname(), kk.getaddress(), kk.getphone()); printf ("%s, %s, %s\n", bill->getname(), bill->getaddress(), bill->gerphone()); delete bill; } Memoria alocata pentru obiectul kk este automat eliberata la terminarea lui main(). Variabila bill este un pointer, iar un pointer, chiar n C++, nu este obiectul nsusi. De aceea memoria alocata de obiectul pointat de bill trebuie eliberata explicit. Operatorul delete asigura si apelul destructorului, elibernd memoria ocupata de cele trei cmpuri ale obiectului pointat. 4.2. Operatorul de asignare Variabilele de tip struct sau class pot fi direct asignate n C++ la fel ca variabilele struct din C. Actiunea implicita implica copierea bit cu bit. void printperson (Person const &p) { Person tmp; tmp = p; printf ("Name: %s\n Address: %s\n Phone %s\n", tmp.getname(), tmp.getaddress(), tmp.getphone()); }

Scurt tutorial de C++


Executia acestei functii pas cu pas este urmatoarea: . Functia printperson () asteapta o referinta la un obiect Person (parametrul p). . Functia defineste un obiect local tmp. Este deci apelat constructorul implicit al clasei Person, ce seteaza pointerii name, address si phone pe zero (daca a fost definit astfel). . Obiectul referit de p este copiat n tmp (sizeof (Person) biti snt copiati n tmp). . Apare o situatie periculoasa: valorile din p snt pointeri, ce pointeaza o anumita zona de memorie; dupa operatiunea de asignare, aceasta memorie este pointata de doua obiecte: p si tmp. . Situatia periculoasa devine acuta dupa terminarea functiei printperson (). Obiectul tmp este distrus. Destructorul clasei Person elibereaza memoria pointata de pointerii name, address si phone din obiectul tmp; dar aceeasi memorie este folosita si de p!!. n acest fel se pierd datele (n fond stringurile ramn n memorie, pointerii catre ei ramn la aceeasi valoare, dar o noua alocare ulterioara va putea folosi acea zona de memorie ocupata de stringuri.) n concluzie: orice clasa ce contine un constructor si un destructor , precum si cmpuri pointeri pentru adresarea memoriei alocate, este un candidat potential pentru necazuri !. Redefinirea operatorului de asignare De fapt, modul corect de asignare a unui obiect Person cu un alt obiect este nu de a copia continutul bit cu bit, ci de a crea un obiect echivalent, care sa aiba memorie alocata proprie, dar care sa contina aceleasi srtinguri. Exista mai multe solutii pentru asta. Una din ele consta n definirea unei functii speciale pentru asignarea obiectelor Person. Iata un exemplu: void Person::assign (Person const &other) {// sterge vechea memorie utilizata delete name; delete address; delete phone; // copie datele name = strdup (other.name); address = strdup (other.address); phone = strdup (other.phone); } Astfel putem redefini functia printperson(): void printperson (Person const &p) { Person tmp; tmp.assign (p); printf ("Name: %s\n Address: %s\n Phone %s\n", tmp.getname(), tmp.getaddress(), tmp.getphone()); } Aceasta solutie cere ca programatorul sa foloseasca o functie membru specific n loc de operatorul '='. n general, problema reasignarii operatorilor se rezolva n C++ folosind operatorul de reacoperire. Reacoperirea operatorului de asignare este poate cea mai ntlnita forma de reasignare. Totusi, faptul ca C++ permite reacoperirea nu nseamna ca aceasta facilitate trebuie folosuita tot timpul. Citeva regului utile snt: . Reacoperirea operatorului ar trebui folosita n situatia n care un operator are o actiune definita, dar aceasta actiune nu este dorita sau are si efecte secundare negative (vezi cazul precedent). . Reacoperirea poate fi folosita cnd utilizarea operatorului este comuna si nu apar ambiguitati introduse prin redefinire. De exemplu, redefinirea operatorului '+' pentru a lucra si cu numere complexe. . n toate celelalte cazuri este preferabila definirea unei functii membru. Functia operator=() Pentru a implementa reacoperirea n cadrul unei clase, clasa respectiva va mai contine o functie public cu numele acelui operator. Se va crea astfel o functie corespunzatoare. De exemplu, pentru reacoperirea operatorului '+' va fi definita functia operator+(). Numele consta din cuvntul cheie operator nsotit de simbolul operatorului. n cazul nostru avem (redefinirea lui '='): class Person { public: . . . . void operator= (Person const &other); . . . . private: . . . }; void Person::operator= (Person const &other) {// sterge vechea memorie utilizata delete name; delete address; delete phone; // copie datele name = strdup (other.name); address = strdup (other.address); phone = strdup (other.phone); } Aceasta implementare este doar o prima versiune; o versiune mai buna va fi prezentata ulterior. Apelul acestei functii (similara cu functia assign () definita anterior) se face astfel: Person pers ("Frank", "Londra", "41526399"), copy; copy = pers // un prim tip de apel copy.operator= (pers); // al doilea tip de apel

Scurt tutorial de C++


Folositi primul tip de apel, ca recomandare. 4.3. Pointerul this Asa cum am vazut, o functie membru al unei clase este apelata n contextul explicit al unui obiect al acelei clase; exista deci un 'substrat' implicit al functiei. C++ defineste cuvntul cheie this, pentru adresarea acestui substrat (this nu este accesibil n contextul functiilor membru declarate static, nediscutate nca). Cuvntul cheie this este o variabila pointer, care va contine ntotdeauna adresa obiectului n chestiune. Pointerul this este implicit declarat n fiecare functie membru (fie ea private sau public), ca si cum n fiecare functie ar exista declaratia: extern <nume clasa> *this; . O functie membru, ca setname(), ar putea fi implementata n doua moduri, folosind sau nu pointerul this; //alternativa 1: folosirea implicita a lui this void Person::setname (char const *n) { delete name; name = strdup (n); } // alternativa 2: folosirea explicita a lui this void Person::setname(char const *n) { delete this->name; this->name = strdup (n); } Exista situatii cnd este necesara folosirea explicita a lui this. Prevenirea distrugerii proprii cu this. Asa cum am vazut, operatorul '=' poate fi redefinit n clasa Person astfel nct sa se obtina, prin asignare, doua copii ale aceluiasi obiect. Att timp ct cele doua variabile snt diferite, prima versiune a functiei operator=() va functiona corect: memoria pentru obiectul asignat este eliberata, dupa care este alocata din nou pentru memorarea noilor stringuri. Totusi, cnd un obiect este asignat lui nsusi, (autoasignare), apare urmatoarea problema: deoarece primul pas este eliberarea memorie, se pierde chiar continutul datelor obiectului respectiv. Iata un exemplu: void fubar (Person const &p) { p = p; } Aici se vede clar ca se poate ntmpla ceva gresit, dar pot exista si autoasignari mai putin evidente: Person one, two, *pp; pp = &one; . . . . . . *pp = two; . . . . . one = *pp; Problema autoasignarii poate fi rezolvata cu ajutorul pointerului this. n implementarea operatorului reacoperit '=' se va testa la nceput daca obiectul din dreapta nu este acelasi cu obiectul curent; daca e asa, nu se face nimic. Obtinem versiunea mbunatatita: void Person::operator= (Person const &other) { if (this != &other) {delete name; delete address; delete phone; name = strdup (other.name); address = strdup (other.address); phone = strdup (other.phone); } } (Exista nsa si o varianta si mai buna !) Asociativitatea operatorilor si this Sintaxa lui C++ spune ca asociativitatea operatorului de asignare este de la dreapta la stnga, adica n instructiunea a = b = c; expresia b = c este evaluata prima, iar rezultatul este asignat lui a. Implementarea operatorului de reacoperire nu permite totusi constructii de acest fel., deoarece functia membru este de tip void. In concluzie, implementarea precedenta rezolva problemele de alocare, dar nu si pe cele sintactice. Problema sintactica poate fi ilustrata astfel. Cnd rescriem expresia a = b = c sub forma explicita de apel de functie obtinem: a.operator= (b.operatot= (c)); sintactic este gresit deoarece expresia b.operator=(c) ntoarce void, iar clasa Person nu contine functia membru operator=(void). Problema poate fi depasita folosindu-l pe this. Functia de reacoperire esteapta ca argument o referinta la un obiect Person; n acelasi timp poate returna o referinta la un asemenea obiect. Aceasta referinta poate fi folosita ca argument pentru o asignare ulterioara. Este o obisnuita de a lasa ca functia de reacoperire a asignarii sa ntoarca o referinta la obiectul curent (adica *this), o referinta de tip const. n final, versiunea cea mai buna a operatorului reacoperit de asignare va fi: class Person { public: . . . . . Person const &operator= (Person const &other) . . . . . }; Person const &Person::operator= (Person const &other) { if (this != &other) {delete name; delete address; delete phone; name = strdup (other.name); address = strdup (other.address); phone = strdup (other.phone); }

10

Scurt tutorial de C++


return (*this) } 4.4. Constructorul copy: Initializare si asignare Sa definim pentru nceput clasa String: class String { public: String(); String (char const *s); ~String (); String const &operator= (String const &other); void set (char const *data); //interfata char const *get (void); private: char *str; } . Clasa contine un pointer char * str pentru adresarea unei zone de memorie. Din acest motiv clasa are un constructor, care va pune pointerul catre zero, si un destructor, care va elibera memoria. . Din acelasi motiv, clasa are si un operator reacoperit (cel de asignare). Codul acestei functii poate arata astfel: String const &String::operator= (String const &other) { if (this != &other) { delete str; str = strdup (other.str); } return (*this); } . Clasa mai are si un constructor cu un argument, care va fi un sir . Interfata va avea rolul de a seta pointerul clasei catre zona de memorie unde se va afla sirul dorit (argumentul functiei set ()). Un posibil apel: String a ("Hello World \n"); Fie urmatorul cod: String a ("Hello World \n"), // instructiunea 1 b, //instructiunea 2 c = a; //instructiunea 3 int main() { b = c ; //instructiunea 4 return (0); } . Instructiunea 1 este o initializare. Obiectul a este initializat cu sirul "Hello World", apelndu-se constructorul cu un argument. Aceasta forma este identica cu String a = "Hello World\n". Desi apare aici operatorul '=', nu este o asignare, ci o initializare, (deci apelul unui constructor). . n instructiunea 2 este creat tot un obiect String. Nefiind nici un argument, este chemat constructorul implicit. . n instructiunea 3 este creat obiectul c care este initializat cu obiectul a . Aceasta forma de initializare nu a mai fost prezentata pna acum. Deoarece putem rescrie instructiunea n forma String c (a); , aceasta initializare sugereaza ca este apelat un constructor, cu un argument referinta la un obiect de tip String. Asemenea constructori snt des ntlniti n C++ si snt numiti constructori copy. . n instructiunea 4 un obiect este asignat altuia. Nu este creat nici un obiect nou, deci este apelata functia de reacoperire. Regula de baza ce trebuie retinuta: Oricnd este creat un obiect, este apelat un constructor !. Regulile constructorului snt: . Nu ntoarce nici o valoare . Are acelasi nume ca si clasa . Lista de argumente poate fi dedusa din cod; argumentul este fie prezent ntre paranteze, fie urmeaza unui '=' n concluzie, pentru instructiunea 3 clasa String trebuie sa contina un constructor copy: class String { public: . . . . . String (String const &other); . . . . . }; // definitia constructorului copy String::String (String const &other) { str = strdup (other.str); } Actiunea constructorului copy este identica cu cea a operatorului de asignare reacoperit: un obiect este duplicat, astfel nct sa aiba propria zona de memorie. Totusi el este mai simplu din urmatoarele puncte de vedere: . Nu trebuie sa dealoce zona de memorie alocata anterior pentru ca obiectul n chestiune este creat chiar atunci (nu are asa ceva) . Nu trebuie sa verifice auto-duplicarea, deoarece nici o variabila nu se poate initializa cu ea nsasi. n afara acestor utilizari ale constructorului copy, mentionate mai sus, el mai poate avea si alte functii, legate de faptul ca este apelat ntotdeauna cnd este creat un obiect si initializat cu alt obiect (chiar daca noul // ntoarce obiectul curent

11

Scurt tutorial de C++


obiect este o variabila ascunsa sau doar temporara): . Cnd o functie are ca argument un obiect, n loc de un pointer sau o referinta la obiect, C++ apeleaza constructorul copy pentru a pasa o copie a acelui obiect ca argument. Acest argument, de obicei creat n stiva, este n fond un nou obiect, creat si initializat cu datele obiectului pasat ca argument. Iata un exemplu: void func (String s) { puts (s.get ()); } int main () { String hi ("Hello World"); func (hi); return (0); } n acest cod hi nu este tratat de functia func(), desi este argumentul ei. Se creeaza o variabila temporara n stiva folosindu-se constructorul copy. Aceasta variabila este cunoscuta de functie sub numele de s. . Constructorul copy este de asemeni apelat implicit n momentul n care o functie returneaza un obiect. Iata un exemplu: String getline () { char buf [100]; // defineste zona tampon gets (buf); //citeste zona tampon String ret = buf // converteste zona n String return (ret); / / o returneaza } Un obiect String ascuns este initializat cu valoarea ntoarsa ret (folosind constructorul copy) si este returnata de functie. Variabila locala ret dispare dupa terminarea actiunii functiei getline(). Pentru a demonstra ca constructorul copy nu este chemat n orice situatie, iata urmatorul exemplu. Rescriem functia getline astfel: String getline() { char buf [100]; gets (buf); return (buf); } Codul este corect, desi valoarea returnata nu se suprapune prototipului String. n aceasta situatie, C++ nceraca sa converteasca char * la un String: acest lucru este posibil daca este dat un constructor ce asteapta un char * ca argument. Deci aici va fi apelat constructorul cu un argument char *. Similaritati ntre constructorul copy si functia operator=() . Duplicarea datelor (private) apare si n constructorul copy si n functia de reacoperire . Dealocarea memoriei ocupate apare n functia de reacoperire si n destructor. Cele doua actiuni (duplicarea si dealocarea) pot fi codate n doua functii primitive, de exemplu copy () si destroy (), ce vor fi folosite n constructorul copy, n functia de reacoperire si n destructor. Rescriem, de exemplu, clasa Person: class Person { public: Person (Person const &other) ~Person (); Person const &operator= (Person const &other); . . . . private: char *name, *address, *phone; void copy (Person const &other); void destroy (void); }; //implementarea pentru copy() si destroy() void Person::copy (Person const &other) { name=strdup (other.name); address=strdup (other.address); phone=strdup (other.phone); } void Person::destroy () { delete name; delete address; delete phone; } n final rescriem si cele trei functii public n care se aloca (dealoca ) memorie: Person::Person (Person const &other) { copy (other); } //copiere neconditionata Person::~Person () { destroy (); } //dealocare neconditionata Person const &Person::operator= (Person const &other) { if (this != &other) { destroy (); copy (other); } return (*this); } 4.5. Alte exemple de reacoperire a operatorilor Acoperirea operatorului [ ]

12

Scurt tutorial de C++


Ca exemplu pentru reacoperire, prezentam o clasa ce va reprezenta un tablou de ntregi. Indexarea elementelor se face cu operatorul standard [ ], dar l vom reacoperi pentru a efectua si o verificare contra depasirilor: int main () { Intarray x (20); //20 de ntregi for (register int i = 0; i < 20; i ++) x [i] = i * 2; //asigneaza elementele for (i = 0; i <= 20; i++) printf ("Pentru index %d: valoarea %d\n", i, x [i]); return (0); } n acest exemplu se creeaza un tablou de 20 de ntregi. Elementele lui pot fi asignate sau identificate. Codul va produce eroare, datorita faptului ca ultimul for va produce o depasire (este adresat x[20], desi ultimul element este x[19]). Definitia clasei este: class Intarray { public: Intarray (int sz = 1); // constructor implicit Intarray (Intarray const &other); ~Intarray (); Intarray const &operator= (Intarray const &other); //interfata int &operator[] (int index); private: int *data, size; }; Facem urmatoarele observatii: . Clasa are un constructor cu un argument implicit, specificnd dimensiunea tabloului. El serveste si ca un constructor implicit, compilatorul punnd dimensiunea 1 daca nu apare nici un argument . Clasa utilizeaza un pointer intern pentru adresarea memoriei: deci este necesar un constructor copy, o functie de reacoperire a asignarii si un destructor . Interfata este definita ca o functie ce ntoarce o referinta la un ntreg. Aceasta permite ca expresii de tip x[10] sa fie folosite si n partea stnga, si n partea dreapta a unui operator de asignare. Putem deci utiliza aceeasi functie si pentru setarea unei valori si pentru adresarea ei Implementarea functiilor: Intarray::Intarray (int sz) { if (sz < 1) //verifica dimensiunea legala { printf ("Tablou: dimensiunea trebuie sa fie >= 1, nu %d!\n", sz); exit (1);`} size = sz; data = new int [sz]; } //constructorul copy Intarray::Intarray (Intarray const &other) { size = other.size; data = new int [size]; //creeaza noua zona for (register int i = 0; i < size; i++) data [i] = other.data [i] //copieaza valorile obiectului other } //reacoperirea asignarii Intarray const &Intarray::operator= (Intarray const &other) { if (this != &other) { size = other.size; delete [] data; // elibereaza vechea memorie data = new int [size]; for (register int i = 0; i < size; i++) data [i] = other.data [i]; } return (*this); } // functia interfata = reacoperirea operatorului [] int &Intarray::operator[] (int index) { // verifica limitele tabloului if (index < 0 || index >= size) { printf ("Tablou: depasire de margini, indexul=%d trebuie sa fie ntre 0 si %d\n", index, size -1); exit (1); } return (data [index]); } Adaptarea operatorilor cin, cout, cerr Vom prezenta modul n care o clasa poate fi adaptata penru utilizarea device-urilor cout , cerr si a operatorului << (pentru cin si operatorul >> se va face similar). Implementarea reacoperirii operatorului << n contextul celor doua device-uri se face n cadrul clasei de baza pentru cout sau cerr, care este ostream.

13

Scurt tutorial de C++


Aceasta clasa este declarata n fisierul header iostream.h si defineste operatorul numai pentru tipurile de baza, int, char*, etc. (cte o definitie de reacoperire pentru fiecare tip). Sa redefinim, de exemplu, operatorul pentru a putea procesa o noua clasa, Person, adica sa putem scrie: Person kr ("John", "Chicago", "9087798"); cout << "Numele, adresa si telefonul persoanei kr:\n" << kr << '\n'; Instructiunea cout << kr implica operatorul << si cei doi noi operanzi: unul de tip ostream& si unul de tip Person&. Actiunea dorita este definita cu functia operator<< () care cere o lista de doua argumente: //declaratie n fisierul person.h extern ostream &operator<< (ostream &, Person const &); // definitia din fisierul sursa ostream &operator<< (ostream &ostream, Person const &pers) { return (stream << "Numele: " << pers.getname () << "Adresa: " << pers.getaddress () << "Telefon: " << pers.getphone () ); } Observatii: . Functia trebuie sa ntorca o referinta la un obiect ostream pentru a putea folosi operatorul 'n lant' . Cei doi operanzi ai operatorului << reprezinta cele doua argumente ale functiei de reacoperire

5.Functii si date de tip static


Fiecare obiect al unei clase are propriul sau set de functii si date publice sau private, accesul facndu-se n functie de statutul ales (private sau public). n anumite situatii este de dorit ca unul sau mai multe cmpuri sa fie accesibile tuturor obiectelor dintr-o clasa. Un exemplu de asemenea situatie este numele directorului de start ntr-un program care scaneaza recursiv directoarele discului. Un alt exemplu este o variabila 'flag' care sa indice o anumita initializare: numai primul obiect creat al clasei face initializarea, punnd 'flagul' pe 'yes'. Aceste situatii se ntlnesc si n coduri C, unde anumite functii trebuie sa acceseze anumite variabile. Solutia n C este de a defini toate functiile n acelasi fisier sursa, iar variabilele de tip static: n acest fel variabilele snt cunoscute doar n acest fisier. O alta solutie este de a da variabilei n cauza un nume neuzual (_6DODU), spernd ca nici o alta parte a programului nu va utiliza numele din greseala. Ambele solutii snt neelegante. C++ permite utilizarea de functii si date statice, comune tuturor obiectelor unei clase. 5.1 Date statice O data membru al unei clase poate fi declarata static, indiferent de zona n care a fost inclusa (publica sau privata). O asemenea data este creata si initializata o singura data, n contrast cu datele ne-statice, care snt create pentru fiecare obiect al clasei. O data static este creata la nceputul programului, dar face totusi parte din clasa. Datele static declarate public snt ca variabilele normale:ele pot fi accesate de ntregul cod folosindu-se numele lor, precedat de numele clasei si de operatorul scope. Exemplu: class Test { public: static int public_int; private: static int private_int; } int main () { Test::public_int = 145; // ok Test::private_int = 12; // gresit, nu poate fi accesata return (0); } Fragmentul de cod nu este pentru un compilator C++: el ilustreaza numai declaratia, nu si definitia datelor membre statice. Date statice private Penru ilustrarea utilizarii unei date membru statice declarata private, iata urmatorul exemplu: class Directory { public: // constructori, destructori, etc. . . private: // data membru static char path []; }; Data membru path este o variabila private static. Ea exista ntr-un singur exemplar, chiar daca se creeaza mai multe obiecte din clasa Directory. Aceasta data poate fi inspectata sau alterata de constructor, destructor sau orice alta functie membru al clasei Directory. Deoarece constructorii snt apelati pentru fiecare nou obiect al clasei, datele static nu snt niciodata initializate de constructori, ci cel mult modificate. Motivul este ca datele static exista nainte de apelul constructorului primului obiect din acea clasa. Datele static pot fi initializate n timpul definitiei lor, n afara

14

Scurt tutorial de C++


tuturor celorlalte functii membru, ca si variabilele globale. De obicei, definitia si initializarea datelor static apar n fisierul sursa ce contine si definitia clasei. Data membru path poate fi definita si initializata n fisierul sursa al constructorului: // data membru static: definitie si initializare char Directory::path [200] = "/usr/local"; // constructorul implicit Directory::Directory () { . . } Trebuie retinut ca definitia unei date statice poate apare n orice fisier sursa (dar numai o singura data). n declaratia clasei membrii statici snt doar declarati: numai la definitia lor tipul si numele clasei apare explicit. De asemeni, dimensiunea datei poate fi omisa n declaratie, dar este necesara la definitie. Un al doilea exemplu de folosire al unei date private static este dat n continuare. Clasa Graphics defineste comunicarea programului cu un device grafic. Pregatirea initiala al device-ului, care n cazul nostru ar reprezenta o trecere din modul text n cel grafic, este o actiune a constructorului si depinde de o variabila statica nobjects (un 'flag'). Variabila respectiva numara obiectele de tip Graphics prezente la un moment dat. Cnd se creeaza primul obiect, este initializat si modul grafic (de catre constructor). n mod similar, destructorul face trecerea de la modul grafic la cel text daca nu mai exista nici un obiect grafic. class Graphics { public: // constructorul, destructorul Graphics (); ~Graphics (); //alte functii de interfata . . private: // numara obiectele static int nobjects; void setgraphicsmode (); // functie ipotetica de trecere n modul grafic void settextmode (); // trecere napoi n modul text } // data membru statica int Graphics::nobjects = 0; // constructorul Graphics::Graphics () { if (! nobjects) setgraphicsmode (); nobjects++; } // destructorul Graphics::~Graphics () { nobjects--; if (! nobjects) settextmode (); } Este evident ca daca snt definiti mai multi constructori, fiecare va trebui sa incrementeze nobjects si daca e posibil sa initializeze modul grafic. Date statice public Datele membru pot fi declarate si n sectiunea public, desi nu este recomandat (violarea principiului de ascundere a datelor). De exemplu, daca data membru statica path ar fi declarata n sectiunea public, ntregul cod al programului ar avea acces la aceasta variabila. Si n acest caz variabila path va trebui definita, iar n fisierul sursa va trebui sa contina definitia : char Directory::path [200]. 5.2 Functii membru static n afara de datele static, C++ permite si declararea de functii static. Conform conceptului, aceste functii vor putea fi aplicate tuturor obiectelor din acea clasa. Functiile static pot adresa doar date static din acea clasa; cele ne-statice snt inaccesibile (daca nu ar fi asa, carui obiect ar apartine ?). n acelasi timp, functiile statice nu pot apela functii ne-statice din clasa respectiva. Acest lucru este dat de faptul ca functiile static nu contin pointerul this. Functiile care snt statice si care snt declarate n sectiunea public pot fi apelate fara specificarea unui obiect al clasei: class Directory { public: // constructori, destructori . . // functiile static publice

15

Scurt tutorial de C++


static void setpath (char const *newpath); private: // stringul static static char path []; }; // definitia variabilei static char Directory::path [199] = "/usr/local"; // functia static void Directory::setpath (char const *newpath) { strncpy (path, newpath, 199); } int main () { // Alternativa (1): apelul setpath() fara nici un obiect din clasa Directory Directory::setpath ("/etc"); // Alternativa (2): cu un object Directory dir; dir.setpath ("/etc"); return (0); } n acest exemplu, functia setpath() este o functie public static. C++ permite si definirea functiilor private static: n acest caz functia poate fi apelata numai de un obiect al clasei (nu si din afara). De asemeni, functia va putea accesa numai datele statice sau alte functii statice. Mostenirea Cnd programam n C, n mod obisnuit vedem solutia problemei sub forma top-down: functiile snt definite n functie de alte sub-functii, si tot astfel pna ajungem la functiile sistemului. n top se gaseste main() care va apela restul functiilor. n C++ dependentele ntre cod si date pot fi definite n termeni de clase dependente de alte clase. Aceasta seamana cu notiunea de compunere, n care obiectele contineau ca date obiecte din alte clase. Dar relatia descrisa n continuare este de un tip diferit: o clasa poate fi definita prin intermediul alteia, definita anterior, aceasta ducnd la situatia n care noua clasa 'mosteneste' toate functiile clasei vechi, dar si adauga propriile functii. n locul unei compuneri, unde o clasa contine alta clasa, vom avea o 'derivare', n care o clasa data devine alta clasa. Un alt sinonim folosit pentru derivare este 'mostenirea': noua clasa mosteneste functionalitatea clasei existente, desi aceasta din urma nu apare ca data specifica n noua clasa. Vom numi clasa initiala - clasa de baza-, iar noua clasa, - clasa derivata. Derivarea claselor este deseori folosita atunci cnd se exploateaza la maxim posibilitatile limbajului C++. Asa cum am vazut si n primul laborator, clasele snt identificate n procesul de analiza al problemei, dupa care obiectele (ale claselor definite) vor reprezenta entitati ale problemei. Clasele snt plasate ntr-o ierarhie, n care cele mai putin functionale snt plasate n vrf, iar celelalte, prin derivare, capata noi functionalitati. Sa consideram o clasificare a vehicolelor pentru a construi o ierarhie de clase. Prima clasa va fi Vehicle, care va avea ca functionalitate posibilitatea de a seta sau afla greutatea unui vehicol. Urmatorul nivel n ierarhie va contine : land-, water- si air-vehicle. 5.3. Tipuri de relatii Relatia ntre clasele propuse reprezinta diferitele tipuri de vehicole. Clasa Vehicle este deci 'cel mai mare numitor comun' al ierarhiei. Iata un exemplu de implementare: class Vehicle { public: // constructori Vehicle (); Vehicle (int wt); // interfata int getweight () const; void setweight (int wt); private: // data int weight; } Pentru a reprezenta vehicole care calatoresc pe uscat, definim o noua clasa ce pastreaza functionalitatea clasei Vehicle, dar adauga si noi informatii. De exemplu sa presupunem ca sntem interesati de viteza s de greutate. Relatia ntre Vehicle si Land poate fi reprezentata si printr-o compozitie, dar ar fi gresit: compozitia ar sugera ca vehicolul Land contine un vehicol, pe cnd relatia este de fapt: vehicolul Land este un tip special de Vehicol. O relatie n termenii unei compozitii ar introduce si cod n plus. De exemplu, considernd urmatorul cod (n care este folosita compozitia): class Land { public: void setweight (int wt); private: Vehicle v; // compunere cu Vehicle }; void Land::setweight (int wt) {

16

Scurt tutorial de C++


v.setweight (wt); } Folosind compozitia, functia setweight() al clasei Land serveste doar la pasarea argumentului catre Vehicle::setweight(). Deci n ceea ce priveste greutatea, apare numai un cod redundant. Relatia este mai bine pusa n evidenta prin derivare: Land este derivata din Vehicle, Vehicle fiind clasa de baza. class Land: public Vehicle { public: // constructori Land (); Land (int wt, int sp); // interfata void setspeed (int sp); int getspeed () const; private: // data int speed; }; Prin postfixarea clasei Land n definitia sa cu public Vehicle se defineste derivarea: clasa Land va contine toate functionalitatile clasei Vehicle plus propriile informatii. Acestea constau ntr-un constructor cu doua argumente si o functie interfata pentru accesarea datei speed. Pentru ilustrarea folosirii acestei clase, iata un exemplu: Land veh (1200, 145); int main () { printf ("Vehicle weighs %d\n" "Speed is %d\n", veh.getweight (), veh.getspeed ()); return (0); } Acest exemplu demonstreaza si doua aspecte ale derivarii. n primul rind, getweight() nu este un membru direct al clasei Land, si totusi este apelat cu numele veh.getweight(). Acest membru face parte implicita din clasa, este 'mostenit' de la 'tatal' Vehicle. n al doilea rnd, desi clasa derivata Land contine acum functionalitatile lui Vehicle, partile private ale clasei Vehicle ramn private (n sensul ca pot fi accesate doar de functiile membru ale obiectelor Vehicle). Asta nseamna ca functiile membru din Land trebuie sa utilizeze functiile interfata (getweight(), setweight()) pentru a accesa cmpul weight. Aceasta restrictie este necesara pentru a pastra conceptul de ascundere a datelor. Clasa Vehicle poate fi, mai trziu, recodata; clasa Land ramne neschimbata. n exemplul urmator presupunem ca urmatoarea clasa, Auto, este capabila sa reprezinte greutatea, viteza si numele unei masini. Clasa este derivata din clasa Land: class Auto: public Land { public: // constructori Auto (); Auto (int wt, int sp, char const *nm); Auto (Auto const &other); // constructorul copy Auto const &operator= (Auto const &other); // asignarea ~Auto (); // destructorul // interfata char const *getname () const; void setname (char const *nm); private: // data char const *name; }; Deoarece clasa Auto este derivata din clasa Land care este derivata din clasa Vehicle, putem vorbi de o derivare n lant. 5.4. Constructorul unei clase derivate Asa cum am putut vedea din definitia clasei Land, exista un constructor pentru a seta si greutatea si viteza obiectului. O implementare 'slaba' este urmatoarea: Land::Land (int wt, int sp) { setweight (wt); setspeed (sp); } Implementarea are dezavantaje: compilatorul C++ va genera cod pentru apelul constructorului implicit al clasei de baza la fiecare apel al constructorului clasei derivate (daca nu se specifica expres altfel). Acest lucru este echivalent cu o compozitie de obiecte. Solutia corecta este de a apela direct constructorul clasei Vehicle ce asteapta un argument int. Pentru a obtine acest lucru, sintaxa cere ca apelul constructorului clasei de baza sa urmeze lista de argumente a constructorului clasei derivate. Land::Land (int wt, int sp) : Vehicle (wt) { setspeed (sp);

17

Scurt tutorial de C++


} 6.5. Redefinirea functiilor membru Actiunea tuturor functiilor definite n clasa de baza poate fi redefinita n clasa derivata. Sa presupunem, de exemplu, ca sistemul de clasificare a vehicolelor trebuie sa fie capabil sa reprezinte autocamioanele, care constau n din doua parti: camionul propriu-zis si remorca. Fiecare parte are propria sa greutate, nsa getweight() trebuie sa returneze greutatea totala. Deci definitia clasei Truck va fi derivata din clasa Auto, dar va avea un cmp de date suplimentar. Convenim sa reprezentam greutatea camionului n clasa Auto, cea a remorcii aparnd distinct n noua clasa: class Truck: public Auto { public: // constructori Truck (); Truck (int engine_wt, int sp, char const *nm, int trailer_wt); // interfata: setarea a doua greutati void setweight (int engine_wt, int trailer_wt); // returnarea greutatii int getweight () const; private: // data int trailer_weight; }; // examplu de constructor Truck::Truck (int engine_wt, int sp, char const *nm, int trailer_wt) : Auto (engine_wt, sp, nm) { trailer_weight = trailer_wt; } Sa observam ca clasa Truck contine doua functii deja prezente n clasa de baza: . Functia setweight(). Redefinirea n Truck nu pune probleme deosebite. Noua definitie o va acoperi cea veche, din Vehicle, deci va putea fi apelata numai cu doua argumente. . Functia getweight(). Are acelasi numar de argumente ca si functia din clasa Vehicle.n acest caz clasa Truck redefineste functia membru. Urmatorul cod reprezinta redefinirea functiei getweight(): int Truck::getweight () const { return ( Auto::getweight () + //suma greutatii camionului trailer_weight); //si a remorcii } Sa observam ca apelul Auto::getweight() selecteaza explicit functia din clasa Auto. O instructiune de forma return (getweight()+trailer_weight); va duce la o recursie infinita. 5.5. Mostenirea multipla C++ permite s derivarea unei clase din mai multe clase, crend asa numita mostenire multipla (clasa cu mai multi parinti). De exemplu, clasa Engine va contine functii pentru stocarea urmatoarelor informatii: numar de serie, puterea, tipul combustibilului, etc.. class Engine { public: // constructori Engine (); Engine (char const *serial_nr, int power, char const *fuel_type); // functii necesare datorita prezentei pointerilor n definitia clasei Engine (Engine const &other); Engine const &operator= (Engine const &other); ~Engine (); // interfata void setserial (char const *serial_nr); char const *getserial () const; void setpower (int power); int getpower () const; void setfueltype (char const *type); char const *getfueltype () const; private: // data char const *serial_number, fuel_type; int power; }; Pentru a reprezenta un obiect Auto, dar cu toate informatiile despre motor, se poate deriva o noua clasa, MotorCar, din clasa Auto si din clasa Engine. Functionitatile ambelor clase vor fi mostenite de clasa noua: class MotorCar: public Auto, public Engine { public: // constructori

18

Scurt tutorial de C++


MotorCar (); MotorCar (int wt, int sp, char const *nm, char const *ser, int pow, char const *fuel); }; MotorCar::MotorCar (int wt, int sp, char const *nm, char const *ser, int pow, char const *fuel) : Engine (ser, pow, fuel), Auto (wt, sp, nm) { } Cteva observatii: . Cuvntul cheie public este prezent n fata ambelor nume ale claselor de baza, aceasta deoarece implicit, n C++, derivarea este private. . Noua clasa nu introduce functii noi, ci numai le combina pe cele pre-existente (agregheaza tipuri mai simple ntr-un tip complex - abilitate a C++ des utilizata). . Constructorul, care asteapta sase argumente, nu contine cod propriu. sarcina lui este numai de a activa constructorii claselor de baza. Ca observatie asupra sintaxei: dupa lista de argumente snt apelati cei doi constructori, fiecare cu propria lista de argumente. Ordinea n care snt apelati este data de ordinea derivarii. Primul constructor apelat este cel al clasei Auto, deoarece MotorCar este mai nti derivat din Auto. n final, o regula de recunoastere sintactica: relatia 'este' nseamna derivare (Motorcar 'este' si un Auto, si un Engine), iar relatia 'are' nseamna compunere (de exemplu, MotorCar 'are' un Engine (motor), caz n care n clasa MotorCar va trebui sa apara un obiect de tip Engine). n acest ultim caz va trebui sa definim explicit functiile de interfata: (am presupus ca n clasa MotorCar, cu compunere, exista un obiect engine din clas Engine) void MotorCar::setpower (int pow) { engine.setpower (pow);} int MotorCar::getpower () const { return (engine.getpower ()); } // etcetera, la fel si pentru set/getserial(), si set/getfueltype() Asemenea probleme (redundanta codului) snt evitate folosind derivarea. 5.6. Conversia ntre clasele de baza si cele derivate Atunci cnd se utilizeaza mostenirea n definirea unei clase, se poate spune ca un obiect al clasei derivate este n acelasi timp si un obiect al clasei de baza. Acest lucru are consecinte importante. Conversii n asignarea obiectelor Sa definim doua obiecte, unul al clasei derivate si unul al clasei de baza: Vehicle v (900); // vehicol cu greutatea de 900 kg Auto a(1200, 130, "Ford");//automobil cu greutatea de 1200 kg, viteza de 30 km/h, //marca Ford Obiectul a este initializat cu o valoare specifica. Totusi, deoarece Auto este n acelasi timp si Vehicle, se poate face asignarea obiectului de baza cu obiectul derivat: v = a; Efectul asignarii este ca obiectul v primeste valoarea 1200 pentru cmpul weight. Un Vehicle nu are cmpurile speed si name, deci aceste date nu vor fi asignate. Asignarea inversa ( a unui obiect drivat cu unul de baza) nu este acceptata, deoarece pot exista cmpuri (speed sau name) care nu pot primi o valoare. Regula generala: daca obiectul din stnga are toate cmpurile celui din dreapta, se poate face asignarea, altfel nu. Aceasta regula se refera srtict la operatorul de asignare (=) cel implicit. Se poate, de exemplu, redefini operatorul = n clasa Auto astfel nct sa poata fi executata si instructiunea a = v; Auto const &Auto::operator= (Vehicle const &veh) { setweight (veh.getweight ()); . . //cod referitor la asignarea cmpurilor neexistente n Vehicle (speed si name) } Conversii n asignarea pointerilor Sa definim urmatoarele obiecte si pointeri: Land l (1200, 130); Auto a (500, 75, "Daf"); Truck t (2600, 120, "Mercedes", 6000); Vehicle *vp; Putem asigna vp cu adresele celor trei obiecte din clasele derivate: vp = &l; vp = &a; vp = &t;. fiecare asemenea asignare fiind legala. De fiecare data se face o conversie implicita a tipului clasei derivate n tipul clasei de baza, deoarece vp este un pointer la un obiect de tip Vehicle. n schimb, folosind vp pot fi apelate numai functiile membru ce manipuleaza cmpul weight, deoarece este singura functionalitate a clasei Vehicle. Aceasta restrictie are un efect deosebit de important mai ales pentru clasa Truck. Dupa asignarea vp = & t; pointerul vp va pointa catre un obiect Truck, nsa apelul vp - > getweight() va ntoarce 2600, si nu 8600 (adica suma celor doua greutati, obinuta la apelul t.getweight()). Deci cnd o functie este apelata prin intermediul unui pointer, tipul pointerului si nu cel al obiectului pointat determina ce functie este accesibila. Exista deigur si o cale de a ocoli conversia implicita, folosind operatorul cast: Truck truck; Vehicle *vp; vp = &truck; // vp pointeaza catre un obiect truck

19

Scurt tutorial de C++


. . Truck *trp; trp = (Truck *) vp; printf ("Make: %s\n", trp->getname ()); Codul va fi executat numai daca vp va pointa ntr-adevar catre un obiect Truck si deci va exista o functie getname(). Altfel se va obtine o eroare. 5.7. Stocarea pointerilor clasei de baza Faptul ca pointerii unei clase de baza pot fi utilizati pentru adresarea claselor derivate poate fi folosit pentru dezvoltarea unor clase generale care sa proceseze obiectele claselor derivate. Un exemplu tipic de asemenea proces este stocarea obiectelor, fie ntr-un vector, fie ntr-o lista, arbore, etc. Clasele definite astfel nct sa stocheze alte clase snt denumite n general clase de stocaj (sau clase container). Ca exemplu prezentam clasa VStorage, folosita pentru stocarea pointerilor catre Vehicle. Pointerul actual poate adresa fie catre un obiect Vehicle, dar si un obiect Auto sau Track. Definitia clasei este urmatoarea: class VSTorage { public: // constructori, destructor VStorage (); VSTorage (VStorage const &other); ~VStorage (); VStorage const &operator= (VStorage const &other); // reacoperirea asignarii // interfata: void add (Vehicle *vp); // adauga Vehicle* n zona de stocare Vehicle *getfirst (void) const; // gaseste primul Vehicle* Vehicle *getnext (void) const; // gaseste urmatorul Vehicle* private: // data Vehicle **storage; int nstored, current; }; Facem urmatoarele observatii: . Clasa contine trei functii de interfata: una pentru a adauga un Vehicle n lista, alta pentru a regasi primul Vehicle si ultima pentru a obtine urmatorul pointer. Un exemplu de folosire este urmatorul: Land l (200, 20); // greutate 200, viteza 20 Auto a (1200, 130, "Ford"); // greutate 1200 , viteza 130, marca Ford VStorage garage; // zona de stocare garage.add (&l); // adauga la zona de stocare garage.add (&a); Vehicle *anyp; int total_wt = 0; for (anyp = garage.getfirst (); anyp; anyp = garage.getnext()) total_wt += anyp->getweight (); printf ("Total weight: %d\n", total_wt); Acest exemplu demonstreaza modul n care tipurile derivate (Auto, Land) snt convertite implicit la tipul de baza (Vehicle), astefel nct pot fi stocate n VStorage. Functia getweight(), fiind definita n clasa de baza, poate fi deci folosita pentru calcularea greutatii totale a vehicolelor stocate. . Clasa VStorage contine si functiile necesare pentru asignarea unui obiect cu un alt obiect, adica reacoperirea asignarii si constructorul copy. . Zona de date a clasei este cuprinsa n sectiunea private: un tablou de pointeri catre obiecte Vehicle si doi ntregi, pentru numarul obiectelor stocate si pentru indexul curent (returnat de getnext()). Sa retimen deci faptul ca C++ permite procesarea tuturor claselor derivate folosind o clasa de baza. Pare paradoxal ca aceasta clasa data ca exemplu, VStorage, poate stoca toate clasele derivate din Vehicle, chiar si cele viitoare. Faptul este posibil deoarece VStorage utilizeaza un protocol, definit n Vehicle si obligatoriu deci penru toate clasele derivate. Dezavantajul clasei VStorage este urmatorul: cnd adaugam un obiect Truck, fragmentul de cod precedent nu va afisa suma totala a greutatilor (se aduna numai greutatea camionului, nu si a remorcii, adica acea greutate continuta de obiectul Vehicle si returnata de functia anyp->getweight(). Exista, desigur, si un remediu (functii virtuale).

6.Polimorfism, legare n momentul executiei si functii virtuale


Asa cum am vazut n sectiunea 5, C++ este dotat cu proceduri specifice pentru derivarea unor clase dintr-o clasa de baza, pentru utilizarea pointerilor clasei de baza n adresarea catre obiecte din clasele derivate si pentru manipularea obiectelor derivate ntr-o clasa generica. In ceea ce priveste operatiile permise obiectelor dintr-o asemenea clasa generica, am vazut ca n clasa de baza snt definite actiuni comune tuturor obiectelor derivate (de exemplu, functia getweight() din clasa Vehicle). Daca un pointer al clasei de baza este folosit pentru adresarea unui obiect derivat, tipul pointerului, si nu a obiectului, determina ce functie este apelata, ceea ce duce uneori la rezultate incorecte (vezi cazul clasei Truck). C++ permite si depasirea unor asemenea situatii, deci ca un pointer Vehicle * vp sa apeleze functia Truck::getweight() daca vp pointeaza la un moment dat la un obiect din clasa Truck. Termenul pentru aceasta situatie este polimorfism: pointerul vp ia forme diferite cnd adreseaza obiecte diferite (cu alte cuvinte, se comporta ca si obiectul

20

Scurt tutorial de C++


pointat). Un alt termen penrtu acest fapt este legarea la momentul executie, si exprima faptul ca decizia care functie este apelata (al clasei de baza sau al clasei derivate) nu se cunoaste la momentul compilarii, ci numai n timpul executiei. 6.1. Functii virtuale Regula implicita n activarea unei functii membru prin intermediul unui pointer este ca tipul pointerului determina functia (vezi lab. 5). Aceasta reprezinta o legare statica (initiala), deoarece tipul functiei este cunoscut n momentul compilarii. Legarea dinamica (sau ulterioara) este 'implementata' n C++ cu ajutorul functiilor virtuale. O functie devine virtuala daca declaratia ei ncepe cu virtual. Odata ce o functie a fost declarata virtual n clasa de baza, definitia ei ramne virtuala n toate clasele derivate, chiar daca cuvntul virtual nu apare explicit. n clasa Vehicle doua functii membru vor fi declarate virtual, si anume getweight() si setweight(). Astfel functiile vor deveni virtuale si n clasa Truck, derivata din Vehicle: class Vehicle { public: // constructori Vehicle (); Vehicle (int wt); virtual int getweight () const; // interfata.. acum virtuala! virtual void setweight (int wt); private: // data int weight; } // functia getweight() din clasa Vehicle int Vehicle::getweight () const { return (weight); } class Land: public Vehicle { . } class Auto: public Land { . } class Truck: public Auto { public: // constructori Truck (); Truck (int engine_wt, int sp, char const *nm, int trailer_wt); void setweight (int engine_wt, int trailer_wt); // interfata: pentru setarea greutatii int getweight () const; //ntoarce greutatea compusa private: // data int trailer_weight; }; // functia getweight() pentru clasa Truck int Truck::getweight () const { return (Auto::getweight () + trailer_wt); } Efectul legarii dinamice este prezentat n continuare: Vehicle v (1200); // vehicol cu greutatea de 1200 Truck t (6000, 115,// autocamion avnd cabina cu greutatea de 6000, viteza 115, "Scania", 15000); // marca Scania, greutatea remorcii 15000 Vehicle *vp; // pointer generic la vehicle int main () { // alternativa (1) vp = &v; printf ("%d\n", vp->getweight ()); // alternativa (2) vp = &t; printf ("%d\n", vp->getweight ()); // alternativa (3) printf ("%d\n", vp->getspeed ()); return (0); } Deoarece functia getweight() este definita ca virtual, legatura se face dinamic: n instructiunile din alternativa (1), este apelata functia getweight() din Vehicle. n alternativa (2) este apelata functia getweight() din clasa Truck. La alternativa (3) va apare o eroare de sintaxa, neexistnd functia getspeed() n clasa Vehicle. Regula generala este ca atunci cnd se utilizeaza un pointer catre o clasa , numai functiile

21

Scurt tutorial de C++


membru ale acelei clase pot fi apelate (fie ca snt sau nu virtuale). Polimorfism n elaborarea programelor Cnd functiile snt declarate virtual n clasa de baza (si deci si n clasele derivate), si cnd aceste functii snt apelate folosind un pointer catre clasa de baza, acest pointer devine polimorf. n continuare vom ilustra efectul polimorfismului n dezvoltarea si elaborarea programelor. Un sistem de clasificare pentru vehicole poate fi implementat n C prin intermediul unei uniuni de struct, avnd si un cmp enumerare pentru determinarea tipului actual de vehicol reprezentat. O functie getweight() va determina, n principiu mai nti ce tip de vehicol este reprezentat, apoi va inspecta cmpurile relevante: typedef enum /* tipul de vehicol */ { is_vehicle, is_land, is_auto, is_truck, } Vtype; typedef struct /* tipul generic de vehicol */ { int weight; } Vehicle; typedef struct /* tipul Land: adauga viteza */ { Vehicle v; int speed; } Land; typedef struct /* tipul Auto: vehicol Land + nume */ { Land l; char *name; } Auto; typedef struct /* tipul Truck: Auto + remorca */ { Auto a; int trailer_wt; } Truck; typedef union /* toate tipurile de vehicole ntr-o uniune */ { Vehicle v; Land l; Auto a; Truck t; } AnyVehicle; typedef struct /* datele pentru toate vehicolele */ { Vtype type; AnyVehicle thing; } Object; int getweight (Object *o) /* ntoarce greutatea unui vehicol */ { switch (o->type) { case is_vehicle: return (o->thing.v.weight); case is_land: return (o->thing.l.v.weight); case is_auto: return (o->thing.a.l.v.weight); case is_truck: return (o->thing.t.a.l.v.weight + o->thing.t.trailer_wt); } } Dezavantajul acestui mod de implementare este ca nu poate fi usor schimbat (de exemplu, daca dorim sa definim tipul Airplane si sa-l adaugam, mpreuna cu alte cmpuri (nr. pasageri, etc.), va trebui sa re-editam si sa recompilam codul). Dimpotriva, C++ ofera posibilitatea polimorfismului. Avantajul este ca vechiul cod ramne utilizabil. Implementarea unui nou tip (Airplane) nseamna o noua clasa cu (posibil) propriile functii (virtuale) getweight() si setweight(). O functie de forma: void printweight (Vehicle const *any) { printf ("Weight: %d\n", any->getweight ()); } ramne valabila oricnd, si nici nu trebuie recompilata (legatura este dinamica). Cum este implementat polimorfismul ntelegerea implementarii polimorfismului nu este o conditie necesara pentru utilizarea acestei facilitati, dar va explica care este costul, din punct de vedere al memorie, al folosirii lui. Ideea fundamentala a polimorfismului este ca C++ compilerul nu cunoaste, la momentul compilarii, care functie este apelata. Aceasta nseamna ca adresa functiei trebuie memorata undeva, pentru a fi gasita n eventualitatea unui apel. Acest 'undeva' trebuie sa fie accesibil obiectelor n chestiune. Cea mai comuna implementare este urmatoarea: un obiect continnd functii virtuale pastreaza un prim cmp ascuns, pointnd catre un tablou ce

22

Scurt tutorial de C++


pastreaza adresele functiilor virtuale. Trebuie retinut ca aceasta implementare este dependenta de compilator, si nu este dictata de definitiile C++ ANSI. Tabelul adreselor functiilor virtuale este mpartit cu toate obiectele din clasa. Se poate ntmpla ca si doua clase sa mparta acelasi tablou. Cheltuielile de memorie nseamna: . un pointer n plus pentru fiecare obiect, ce pointeaza la: . un tablou de pointeri pentru fiecare clasa pentru memorarea adreselor functiilor virtuale O instructiune de genul vp - > getweight() va inspecta mai nti cmpul ascuns al obiectului pointat de vp. n cazul sistemului de clasificare al vehicolelor, acest pointer pointeaza catre un tablou cu doua adrese: una pentru functia getweight() si una pentru functia setweight(). Functia actuala care va fi apelata este determinata din acest tablou. 6.2. Functii virtuale pure Clasa Vehicle contine, n acest moment, propriile implementari ale functiilor virtuale getweight() si setweight(). n C++ este posibil ca o functie sa fie declarata virtuala, ntr-o clasa, fara a mai fi definita concret (implementarea ei facndu-se ntr-o clasa derivata). Aceasta facilitate, (deci numai declararea, nu si definirea functiilor), este permisa tocmai n aceasta idee: clasa derivata va trebui sa aiba grija de implementare. Deoarece compilatorul nu permite definirea unui obiect dintr-o clasa care nu contine implementarea concreta, clasa de baza va activa un protocol care va declara functia prin nume, argumente si tipul valorii returnate. n acest fel clasa de baza devine un model pentru clasele derivate. Asemenea clase snt denumite clase abstracte. Functiile care snt declarate n clasa de baza dar nu si definite se numesc functii pur virtuale. O functie este declarata pur virtuala prin precedarea declaratiei ei cu cuvntul virtual si postfixarea cu '= 0'. Exemplu: class Sortable { public: virtual int compare (Sortable const &other) const = 0; }; Aici clasa Sortable cere ca toate clasele derivate sa aiba implementate o functie compare(), care sa ntoarca un int si sa aiba ca argument o referinta la un alt obiect Sortable (care pentru a nu fi modificat de functie este declarat const). Nefiindu-i permisa modificarea obiectului curent, functia nsasi a fost declarata const. Clasa de baza (Sortable) poate fi folosita ca model pentru clasele derivate. Iata un exemplu n care intervine clasa Person, obtinndu-se capacitatea de comparare a doua persoane (alfabetic) dupa nume si adresa: class Person: public Sortable { public: // constructori, destructori, redefiniri Person (); Person (char const *nm, char const *add, char const *ph); Person (Person const &other); Person const &operator= (Person const &other); // interfata char const *getname () const; char const *getaddress () const; char const *getphone () const; void setname (char const *nm); void setaddress (char const *add); void setphone (char const *ph); int compare (Sortable const &other) const; // functia impusa de clasa Sortable private: // data members char *name, *address, *phone; }; int Person::compare (Sortable const &o) { Person const &other = (Person const &)o; register int cmp; // daca numele snt diferite if ( (cmp = strcmp (name, other.name)) ) return (cmp); // daca nu, compara adresele return (strcmp (address, other.address)); } Sa observam ca implementarea functiei Person::compare() nu cere ca argument o referinta la Person, ci la un obiect Sortable. Aceasta deoarece, prin posibilitatea de reacoperire a functiilor n C++, functia compare( Person const &other) este diferita de compare(Sortable const &other), cea ceruta de protocolul activat de clasa de baza. n implementarea efectiva vom apela efectiv operatorul cast de conversie pentru a obtine din argumentul de tip Sortable un argument de tip Person. 6.3. Comparerea numai ntre obiecte Person Uneori este folositor sa cunosti n implementarea concreta a unei functii virtuale pure ce este argumentul (other). De exemplu, functia Person::compare() ar trebui sa faca comparatia numai daca argumentul este un

23

Scurt tutorial de C++


obiect Person (pentru a evita erorile de executie). De aceea prezentam o versiune mbunatatita a clasei Sortable, dezvoltata astfel nct sa ceara fiecarei clase derivate implementarea unei functii int getsignature(): class Sortable { . virtual int getsignature () const = 0; . }; Functia concreta Person::compare() va putea acum compara nume si adrese numai daca semnatura obiectului curent si a obiectului other coincid: int Person::compare (Sortable const &o) { register int cmp; // prima data verifica semnaturile if ( (cmp = getsignature () - o.getsignature ()) ) return (cmp); Person const &other = (Person const &)o; if ( (cmp = strcmp (name, other.name)) ) return (cmp); return (strcmp (address, other.address)); } Problema de baza este desigur implementarea functiei getsiganture(). Aceasta va trebui sa ntoarca o valoare unica int pentru fiecare tip de clasa. O implementare eleganta este urmatoarea: class Person: public Sortable { . int getsignature () const; } int Person::getsignature () const { static int // variabila specifica pentru clasa Person tag; return ( (int) &tag ); // adresa &tag este unica pentru Person } 6.4. Destructori virtuali Cnd operatorul delete elibereaza memoria ocupata de un obiect alocat dinamic, un destructor corespunzator este apelat pentru a asigura ca memoria utilizata pna atunci va putea fi reutilizata. Sa consideram acum urmatorul cod, n care snt folosite doua clase din sectiunile anterioare: Sortable *sp; Person *pp = new Person ("Frank", "frank@icce.rug.nl", "633688"); sp = pp; // sp pointeaza acum spre un Person . . delete sp; // obiectul este distrus n acest exemplu un obiect din clasa derivata (Person) este distrus folosind un pointer catre clasa de baza (Sortable *). Prin definitie, acest apel va fi efectuat de destructorul clasei de baza, si nu al celei derivate. C+ + permite si n acest caz folosirea destructorilor virtuali, obtinuti prin prefixarea cu cuvntului virtual. Definitia clasei Sortable devine: class Sortable { public: virtual ~Sortable (); virtual int compare (Sortable const &other) const = 0; . . }; n general, raspunsul la ntrebarea daca destructorul unei clase trebuie sa fie o functie pur virtuala este nu: nu trebuie fortat faptul ca fiecare clasa derivata sa-si implementeze propriul destructor. Prin definirea destructorului ca virtual, dar nu pur virtual, clasa de baza ofera posibilitatea redefinirii destructorului n fiecare clasa derivata (dar nu obliga). El va fi folosit doar n acele clase care nu si definesc propriul destructor. De obicei, implementarea destructorului virtual va fi doar o instructiune vida. 6.5. Functiile virtuale si mostenirea multipla Asa cum am precizat n laboratorul precedent, este posibil sa deeivam o clasa din mai multe clase de baza. O asemenea clasa va mosteni toate proprietatile claselor parinte. O dificultate majora n mostenirea multipla poate aparea n momentul n care exista mai multe 'drumuri' de la o clasa derivata la o clasa de baza. Iata un exemplu, n care clasa Derived este derivata dublu din clasa Base:

24

Scurt tutorial de C++


class Base { public: void setfield (int val) { field = val; } int getfield () const { return (field); } private: int field; }; class Derived: public Base, public Base { }; Datorita dublei derivari, functionalitatea clasei Base apare de doua ori n clasa Derived. Deci n momentul unui apel al functiei setfield() de un obiect din clasa Derived, apare ambiguitatea: care din cele doua functii (identice) se vor executa.? n aceasta situatie compilatorul va genera eroare. Cazul de mai sus este simplu de evitat. Dar deoarece mostenirea poate fi facuta n lant, duplicarea poate fi ascunsa. De exemplu, daca derivam o clasa din clasele Auto si Air, (fie ea AirAuto), ea va contine n fond doua Vehicle (deci doua cmpuri weight, doua functii setweight(), etc.). Ambiguitatea n mostenirea multipla Sa vedem de ce clasa AirAuto introduce ambiguitate cnd este derivata din Auto si Air. . Un AirAuto este un Auto, deci un Land, deci un Vehicle . Un AirAuto este un Air, deci un Vehicle Compilatorul C++ va detecta ambiguitatea n clasa AirAuto si va genera eroare n cazul unui cod de forma: AirAuto cool; printf ("%d\n", cool.getweight()); Programatorul are doua cai de a rezolva explicit ambiguitatea (care functie getweight() ?) . Fie va modifica apelul folosind operatorul scope: printf ("%d\n", cool.Auto::getweight ()); Observati locul operatorului scope: nainte de numele functiei membru . Fie se poate crea o functie dedicata n clasa AirAuto, getweight(): int AirAuto::getweight () const { return (Auto::getweight ()); } A doua posibilitate este de preferat, deoarece subliniaza obligatia programatorului de a-si lua masuri speciale de precautie n cazul ambiguitatilor. Totusi mai exista o solutie eleganta. Clase de baza virtuale Putem obtine situatia n care clasa AirAuto sa contina numai un Vehicle. Acest lucru se obtine asigurndu-ne ca acea clasa care apare de mai multe ori n clasa derivata sa fie definita clasa de baza virtuala. Manifestarea unei clase de baza virtuala este urmatoarea: daca clasa B este o clasa de baza virtuala n clasa derivata D, atunci B poate fi prezenta n D, dar nu necesar. Compilatorul va ignora includerea daca ea deja a fost facuta. Pentru clasa AirAuto se va modifica derivarea pentru Land si Auto: class Land: virtual public Vehicle { . . }; class Air: virtual public Vehicle { . . }; Derivarea virtuala asigura faptul ca via clasa Land, clasa Vehicle este adaugata numai daca nu a fost deja adaugata (la fel si via Auto). Observatii privitoare la derivarea virtuala: . Derivarea virtuala este, spre deosebire de functiile virtuale, o problema de pura compilare: daca derivarea este virtuala sau nu defineste modul n care compilatorul construieste clasa derivata . n exemplul precedent era suficient ca una din clasele Auto sau Land sa fie construita prin derivare virtuala (nsa definirea ambelor clase astfel nu conduce la eroare) . Faptul ca acum clasa Vehicle din AirAuto nu mai este 'inclusa' n Auto sau Air are consecinte si asupra lantului de constructori. Constructorul lui AirAuto va apela direct constructorul clasei Vehicle (si nu prin intermediul contructorilor claselor Auto sau Air) Cnd derivarea virtuala nu este recomandata Exista nsa si situatii n care este recomandata dubla prezenta a unui membru al clasei de baza. Fie urmatorul exemplu: class Truck: public Auto { public: // constructori Truck (); Truck (int engine_wt, int sp, char const *nm, int trailer_wt); // interfata: pentru setarea celor doua cmpuri weight

25

Scurt tutorial de C++


void setweight (int engine_wt, int trailer_wt); int getweight () const; // returneaza greutatea totala private: // data int trailer_weight; }; // examplu de constructor Truck::Truck (int engine_wt, int sp, char const *nm, int trailer_wt) : Auto (engine_wt, sp, nm) { trailer_weight = trailer_wt; } // examplu de functie de interfata int Truck::getweight () const { return ( // suma Auto::getweight () + // camionului plus trailer_wt // a remorcii ); } Definitia arata modul n care obiectul Truck este construit astfel nct sa aiba doua cmpuri weight: unul de la derivarea din Auto si unul propriu, int trailer_weight. O asemenea definitie este desigur valida, dar poate fi rescrisa. Am putea lasa Truck derivat din Auto si din Vehicle, impunnd explicit dubla prezenta a clasei Vehicle (unul pentru a fi folosit pentru memorarea greutatii camionului si unul pentru remorca). Dar o derivare de tipul class Truck: public Auto, public Vehicle nu este acceptata de compilator, deoarece Vehicle este deja parte din Auto. Depasirea situatiei se pate face cu ajutorul unei clase intermediare: derivam clasa TrailerVeh din Vehicle si Truck din Auto si TrailerVeh. Toate ambiguitatile referitoare la functiile membru vor fi rezolvate n clasa Truck: class TrailerVeh: public Vehicle { public: TrailerVeh (int wt); }; TrailerVeh::TrailerVeh (int wt) : Vehicle (wt) { } class Truck: public Auto, public TrailerVeh { public: // constructori Truck (); Truck (int engine_wt, int sp, char const *nm, int trailer_wt); // interfata pentru setarea cmpurilor weight void setweight (int engine_wt, int trailer_wt); int getweight () const; // ntoarce greutatea totala }; // examplu de constructor Truck::Truck (int engine_wt, int sp, char const *nm, int trailer_wt) : Auto (engine_wt, sp, nm), TrailerVeh (trailer_wt) { } // examplu de functie interfata int Truck::getweight () const { return ( // suma dintre Auto::getweight () + // greutatea camionului plus TrailerVeh::getweight () // a remorcii ); }

7.Template
Cele mai moderne compilatoare C++ suporta un 'super-macro-mecanism' care permite programatorului sa defineasca functii sau clase generice. Acestea devin concrete odata ce codul lor este utilizat de entitati reale. Termenul general pentru asemenea functii (clase) este templat-uri. 7.1. Functii template Definitia unei functii template este foarte asemanatoare cu cea a unei functii concrete, cu exceptia faptului ca argumentele functiei snt numite ntr-un mod simbolic. Exemplu:

26

Scurt tutorial de C++


template <class T> void swap(T &a, T &b) { T tmp = a; a = b; b = tmp; } n acest exemplu este definita o functie template swap(), ce actioneaza asupra oricarui tip de obiecte, att timp ct aceste obiecte pot fi asignate unul altuia si pot fi initializate. Tipul general al argumentelor este numit T, si este dat n prima linie a functiei. Codul efectueaza urmatoarele operatii: . Pentru nceput, se creeaza o variabila de tip T (tmp) si este initializat cu argumentul a. . Apoi se interschimba variabilele a si b, prin intermediul variabilei locale tmp. Sa notam ca definitia unei functii template este similara cu #define n sensul ca functia template nu este codata nca; devine cod numai n momentul utilizarii. Un exemplu de apel al functiei: int main() { int a = 3, b = 16; double d = 3.14, e = 2.17; Person k("Karel", "Rietveldlaan 37", "5426044"), f("Frank", "Oostumerweg 17", "4032223"); swap(a, b); printf("a = %d, b = %d\n", a, b); swap(d, e); printf("d = %lf, e = %lf\n", d, e); swap(k, f); printf("k's name = %s, f's name = %s\n", k.getname(), f.getname()); return (0); } Odata ce compilatorul C++ detecteaza utilizarea functiei swap(), se genereaza si codul concret. n cazul nostru se creeaza trei functii, una pentru argumentul int, alta pentru double si a treia pentru Person. Compilatorul va genera si nume diferite pentru aceste functii (nume interne), ca de exemplu swap_int_int(), swap_double_double(), swap_Person_Person(). De notat ca n cazul ultimei functii, pentru utilizarea ei este necesar ca n clasa Person sa fie definit un constructor copy si redefinit operatorul de asignare. Faptul ca compilatorul genereaza codul numai n momentul executiei are o consecinta importanta: definitia functiei template nu poate fi inclusa ntr-o biblioteca, ci numai ntr-un fisier header. 7.2. Clase template 'Super-macro-mecanismul' oferit de compilator poate fi utilizat si pentru o clasa generica, utilizabila pentru orice tip de entitate. n mod obisnuit, clasele template snt clase container (de stocaj) si reprezinta tablouri, liste, stive sau arbori. Clasa template Array n exemplul urmator vom prezenta clasa template Array, ce va putea fi folosita pentru stocarea de matrici continnd orice elemente: #include <stdio.h> #include <stdlib.h> template<class T> class Array { public: // constructori, destructori virtual ~Array(void) { delete [] data; } Array(int sz = 10) { init(sz); } Array(Array<T> const &other); Array<T> const &operator=(Array<T> const &other); // interfata int size() const; T &operator[](int index); private: // date int n; T *data; void init(int sz); // initializatorul }; template <class T> void Array<T>::init(int sz) { if (sz < 1)

27

Scurt tutorial de C++


{ fprintf(stderr, "Array: cannot create array of size < 1\n" " requested: %d\n", sz); exit(1); } n = sz; data = new T[n]; } template <class T> Array<T>::Array(Array<T> const &other) { n = other.n; data = new T[n]; for (register int i = 0; i < n; i++) data[i] = other.data[i]; } template <class T> Array<T> const &Array<T>::operator=(Array<T> const &other) { if (this != &other) { delete []data; n = other.n; data = new T[n]; for (register int i = 0; i < n; i++) data[i] = other.data[i]; } return (*this); } template <class T> int Array<T>::size() const { return (n); } template <class T> T &Array<T>::operator[](int index) { if (index < 0 || index >= n) { fprintf(stderr, "Array: index out of bounds, must be between" " 0 and %d\n" " requested was: %d\n", n - 1, index); exit(1); } return (data[index]); } Referitor la codul precedent, facem urmatoarele observatii: . Definitia clasei ncepe cu template <class T>, similara cu definitia functiei template: aceasta linie contine numele simbolic T, ce se refera la tipul ce va fi utilizat de clasa. . n definitia clasei, toate functiile care contin ca argument un Array vor face referinta la acest argument ca Array<T>. . n definitiile functiilor, numele clasei este referit tot ca Array<T>. Motivul este faptul ca compilatorul va modifica numele clasei Array n momentul n care ea va fi utilizata concret. Numele simbolic T va deveni atunci parte din noul nume al clasei. n ceea ce priveste clasa propriu-zisa, remarcam faptul ca: . Clasa utilizeaza doua date membru: un pointer pentru un tablou alocat (data) si o dimensiune a tabloului (n). . Clasa contine un constructor copy, un destructor (virtual) si o functie de asignare reacoperita, deoarece se adreseaza memoriei alocate. . Instructiunea delete [] data din destructor si din functia de asignare asigura faptul ca destructorul obiectelor din tabloul pointat de data este apelat naintea dealocarii zonei. . Instructiunea data [i] = other.data [i] din functia de asignare copie datele dintr-un alt tablou. Instructiunea fie va copia memoria bit cu bit, fie va activa operatorul de asignare din clasa obiectelor stocate (ca n clasa Person). n ceea ce priveste clasele template, n general, compilatorul va trebui sa le cunoasca n momentul compilarii, adica ntregul cod va trebui pus ntr-un fisier header (array.h) care va fi inclus n fisierul sursa n care se va utiliza clasa. Folosirea clasei tempate Array este prezentata n continuare: #include <stdio.h> #include "array.t" #define PI 3.1415 int main() {

28

Scurt tutorial de C++


Array<int> intarr; for (register int i = 0; i < intarr.size(); i++) intarr[i] = i << 2; Array<double> doublearr; for (i = 0; i < doublearr.size(); i++) doublearr[i] = PI * (i + 1); for (i = 0; i < intarr.size(); i++) printf("intarr[%d] : %d\n" "doublearr[%d]: %g\n", i, intarr[i], i, doublearr[i]); return (0); } Remarcam faptul ca tipul actual de tablou trebuie specificat explicit n momentul definirii unui obiect din clasa template. Clasa Array poate fi utilizata att timp ct se poate aloca memorie si ct entitatile din tablou pot fi asignate. Aceasta nseamna ca pentru clasa Person, de exemplu, este nevoie de un constructor implicit si de o functie de asignare: int main() { Array<Person> staff(2); // tablou de doua persoane Person one, two; . // cod pentru asignarea numelui, adresei si telefonului staff[0] = one; staff[1] = two; printf("%s\n%s\n", staff[0].getname(), staff[1].getname()); return (0); } Deoarece tabloul staff contine obiecte Person, functiile de interfata ca getname() vor fi folosite pentru apelarea elementelor din tablou. Un alt mod de implementare a unei clase de stocaj O sarcina foarte des ntlnita n multe programe este cea de stocare a datelor, urmata apoi de sortarea, selectarea lor, etc. Datele stocate pot fi fie simpli ntregi, fie date complexe (asa cum face sistemul de operare). Conform principiului programarii orientate obiect, vom dezvolta doua clase: o clasa Storage, ce va stoca obiectele, si o clasa Storable, ce va contine prototipul obiectelor ce vor fi stocate. n ceea ce priveste functionalitatea clasei Storage, ea va trebui cel putin sa poata sa adauge obiecte si sa extraga obiecte (din zona de stocare). De asemeni va trebui sa poata furniza si numarul obiectelor stocate. Partea de date va contine un tablou dinamic (pointeri la obiectele stocate). Organizarea interna poate fi descrisa astfel: Functiile interfata pentru clasa Storage Interfata clasei este continuta n trei functii: . Functia add(Storable const *newobj) adauga un obiect la zona de stocaj. Functia realoca pointerii din tablou pentru a permite inserarea adresei noului obiect. . Functia Storable const *get(int index) ntoarce un pointer la obiectul care este stocat de catre pointerul nr. index. . Functia int nstored() ntoarce numarul obiectelor din zona de stocaj A copia sau a nu copia ? Exista doua moduri distincte de a descrie functia add(), n functie de faptul daca obiectele stocate snt copii ale originalelor sau obiectele n-sine. Cu alte cuvinte, functia add() doar stocheaza adresa obiectului (n tabloul de pointeri) sau face mai nti o copie a lui si apoi retine adresa (copiei) ? Intrebarea nu este triviala. Sa consideram urmatorul exemplu: Storage store; Storable something; store.add(something); // adauga n stocaj // presupunem ca este definit Storable::modify() something.modify(); // modifica obiectul original Storable *retrieved = store.get(0); // extrage din stocaj // n acest moment "*retrieved" este egal cu "something" ?! Daca stocam adresele initiale, raspunsul la ultima ntrebare va fi da. Daca stocam adresele copiilor, raspunsul va fi nu. Aceasta ultima abordare este necesara cnd dorim sa 'salvam' obiectele, pentru a putea fi regasite daca dintr-un motiv sau altul obiectul initial este modificat. Aceasta ultima abordare va fi aleasa n continuare. Cine face copia ? Stocarea copiilor nu reprezinta o problema deosebita. Dar daca vrem ca aceasta clasa, Storage, sa fie ct mai generala, va trebui sa renuntam a include o functie ce copie obiectele, deoarece nu stim dinainte ce fel de obiecte vom folosi. O abordare naiva, de genul: void Storage::add(Storable const *obj) {

29

Scurt tutorial de C++


Storable *to_store = new *obj; // adauga to_store n locul argumentului obj . . } nu va functiona. Codul ncearca sa faca o copie a lui obj folosind operatorul new, care n fond va apela constructorul copy din clasa Storable. Totusi, daca Storable este doar o clasa de baza, iar obiectul propriu-zis este dintr-o clasa derivata (fie ea Person), cum va putea constructorul clasei Storable sa creeze o copie a unui obiect Person ? Deci clasa obiectelor de stocat va trebui sa contina o functie care duplica un obiect si ntoarce un pointer la dublura lui. Daca denumim functia duplicate(), codul functiei add() devine: void Storage::add(Storable const *obj) { Storable *to_store = obj->duplicate(); // acum adauga to_store n loc de obj . . } Functia duplicate() este apelata prin folosirea unui pointer la obiectul original (pointerul obj). Clasa Storable din exemplul precedent este doar o clasa de baza care defineste un protocol, si nu clasa propriu-zisa a obiectelor stocate. Deci functia duplicate() nu trebuie implementata n Storable, ci numai n clasele derivate. Cu alte cuvinte, functia duplicate() este o functie pur virtuala. Clasa Storable Folosind aceeasi abordare putem defini acum clasa Storable. Va trebui sa raspundem la urmatoarele ntrebari: . Clasa Storable are nevoie numai de un constructor implicit, sau si de alti constructori (copy, de exemplu) ?. Raspunsul este nu, deoarece clasa este doar o clasa de baza. . Are nevoie clasa de un destructor ? Acesta trebuie sa fie (pur) virtual ? Raspunsul este da. Destructorul va fi apelat n momentul ncetarii existentei unui obiect Storable. Este foarte posibil ca clasele derivate sa aiba proprii destructori, ceea ce impune un destructor virtual n Storable (cnd obiectul pointat de Storable * este distrus va fi apelat destructorul actual al clasei obictului concret). Totusi, nu ar trebui sa fie pur virtual, deoarece e posibil ca unele clase derivate din Storable sa nu aiba nevoie de destructori. Daca este pur virtual, n clasa derivata tebuie explicit definit un destructor fara instructiuni. Definitia clasei si a functiilor este data n continuare: class Storable { public: virtual ~Storable(); virtual Storable *duplicate() const = 0; }; Storable::~Storable() { } Conversia unei clase existente la clasa Storable Pentru a arata practic acest lucru, sa consideram clasa Person. Ea va fi rescrisa n conformitate cu protocolul clasei Storable (este prezentat numai codul relevant sau nou): class Person: public Storable { Person(Person const &other); // constructorul copy Person const &operator=(Person const &other); // asignarea Storable *duplicate() const; // funtia de duplicare . . } n implementarea functiei Person::duplicate() vom putea folosi fie constructorul copy, fie constructorul implicit mpreuna cu operatorul de asignare reacoperit: // prima versiune: Storable *Person::duplicate() const { // foloseste constructorul implicit n new Person Person *dup = new Person; *dup = *this; // foloseste operatorul de asignare n *dup = *this return (dup); } // a doua versiune: Storable *Person::duplicate() const { return (new Person(*this)); // foloseste constructorul copy n new Person(*this) }

30

Scurt tutorial de C++


O asemenea conversie a clasei Person pentru a corespunde nevoilor clasei Storable presupune ca sursa ce contine codul initial este disponibila si poate fi modificata. Totusi, chiar daca definitia clasei Person nu este disponibila, ci continuta ntr-o biblioteca, conversia la formatul clasei Storable nu este dificila: class StorablePerson: public Person, public Storable { public: // functia de duplicare Storable *duplicate() const; }; Storable *StorablePerson::duplicate() const { return (new *(Person*)this); } Clasa Storage Acum putem implementa clasa Storage: class Storage: public Storable { public: // destructori, constructori ~Storage(); Storage(); Storage(Storage const &other); // reacoperirea asignarii Storage const &operator=(Storage const &other); // functia de duplicare Storable *duplicate() const; // interfata void add(Storable *newobj); int nstored() const; Storable *get(int index); private: // primitivele copy/destroy void destroy(); void copy(Storage const &other); // datele private int n; Storable **storage; }; Facem urmatoarele observatii: . Clasa contine un constructor copy si o reacoperire a operatorului de asignare, necesare datorita faptului ca n clasa Storage exista pointeri. . Clasa Storage este la rndul ei derivata din Storable. Aceasta nseamna ca obiectele de tip Storage pot fi la rndul lor stocate ntr-o zona de stocaj, crendu-se un 'super-stocaj' (de exemplu, o lista de grupuri de persoane). . Cele doua functii private copy si destroy() vor fi discutate putin mai trziu. Implementarea destructorului, a constructoruliui si a functiei de asignare: // constructorul implicit Storage::Storage() { n = 0; storage = 0; } // constructorul copy Storage::Storage(Storage const &other) { copy(other); } // destructorul Storage::~Storage() { destroy(); } // reacoperirea asignarii Storage const &Storage::operator=(Storage const &other) { if (this != &other) { destroy(); copy(other); } return (*this); } Functiile primitive copy() si destroy() copie neconditionat un alt obiect Storage sau distrug, tot neconditionat, continutul obiectului curent. Sa observam faptul ca copy() apeleaza duplicate() pentru a duplica obiectele stocate:

31

Scurt tutorial de C++


void Storage::copy(Storage const &other) { n = other.n; storage = new Storable* [n]; for (int i = 0; i < n; i++) storage [i] = other.storage [i]->duplicate(); } void Storage::destroy() { for (register int i = 0; i < n; i++) delete storage [i]; delete storage; } Functia duplicate(), necesara deoarece Storage este la rndul sau un Storable, foloseste constructorul copy pentru duplicarea obiectului curent: Storable *Storage::duplicate() const { return (new *this); } n final, prezentam functiile interfata care adauga obiectele n stocaj, le regasesc sau determina numarul obiectelor stocate: void Storage::add(Storable const *newobj) { storage = (Storable **) realloc(storage, (n + 1) * sizeof(Storable *)); // realoca tabloul de stocaj // pune copia lui newobj n storage storage [n] = newobj->duplicate(); n++; // creste numarul de obiecte din stocaj } Storable *Storage::get(int index) { if (index < 0 || index >= n) // verifica daca indexul este n afara rangului return (0); return (storage [index]); // ntoarce adresa obiectului stocat } int Storage::nstored() const { return (n); } 7.2. Arbore binar Aceasta sectiune va prezenta implementarea unui arbore binar n C++. Analog cu clasele Storage si Storable, doua clase separate vor fi folosite: una va reprezenta arborele iar cealalta obiectele ce vor fi stocate n arbore. Ele vor fi numite, logic, Tree si Node. Clasa Node Clasa Node este o clasa abstracta (pur virtuala), care defineste protocolul pentru utilizarea claselor derivate mpreuna cu clasa Tree. . Cnd datele snt stocate ntr-un arbore binar, locul exact de plasare a unei date este determinat de o anumita ordine: deci va trebui determinat modul n care se va face sortarea lor, ceea ce implica o comparatie ntre obiecte. Functia de comparatie va trebui sa informeze functia apelanta (cea care plaseaza obiectul) daca obiectul n chestiune este mai 'mic' sau mai 'mare' ca altul. Comparatia tine de clasa Node: arborele nu poate 'sti' ce obiecte snt memorate n el. Deci partea de protocol ce se va gasi n Node este virtual int compare(Node const *other) const = 0;. Functia de comparare va fi implementata n fiecare clasa derivata. . Similar cu clasa Storage, clasa Tree va contine copii ale obiectelor. Responsabilitatea duplicarii unui obiect este legata strict de clasa Node, unde va fi definita o functie pur virtuala: virtual Node *duplicate() const = 0; . n timpul procesarii unui arbore binar, arborele este parcurs recursiv, efectundu-se o operatie specifica fiecarui obiect stocat. Operatia depinde de tipul actual al obiectelor. Prin declararea unei functii pur virtuale: virtual void process() = 0; n clasa Node, responsabilitatea procesarii efective cade n sarcina claselor derivate. . n momentul stocarii unui obiect n arbore, se poate ntmpla ca acel obiect sa fi fost deja stocat. Pentru prevenirea dublei stocari, vom defini o functie virtuala already_stored() (dar nu pur virtuala), care implicit nu va face nimic: virtual void already_stored(); Ea va putea fi redefinita n clasele derivate Definitia completa si declaratia clasei Node este data n continuare: class Node { public: virtual ~Node(); // destructorul virtual Node* duplicate() const = 0; // duplicatorul virtual int compare(Node const *other) const = 0; // compararea a 2 obiecte virtual void process() = 0; // functia necesara procesarii unui nod

32

Scurt tutorial de C++


virtual void already_stored(); // verifica daca a fost deja stocat }; Node::~Node() { } void Node::already_stored() { } Clasa Tree Clasa Tree este responsabila pentru stocarea obiectelor derivate din Node. Pentru implementarea structurii recursive a arborelui, clasa Tree contine doi pointeri, unul pointnd spre subarborele stng, Tree *left, si unul pointnd spre subarborele drept, Tree *right. Informatia care este stocata ntr-un nod este reprezentata sub forma Node *info. Pentru scanarea unui arbore binar, clasa Tree ofera trei metode: preordine, inordine si postordine. Scanarea n preordine nseamna vizitarea mai nti a nodului curent, apoi a subarborelui stng, apoi a subarborelui drept. Scanarea n inordine nseamna scanarea mai nti a subarborelui stng, apoi a nodului curent si n final al subarborelui drept. Iar scanarea n postordine nseama pentru nceput subarborele stng, apoi subarborele drept iar n final nodul curent. Definitia clasei Tree este data n continuare: class Tree { public: // destructori, constructori ~Tree(); Tree(); Tree(Tree const &other); Tree const &operator=(Tree const &other); // asignarea void add(Node *what); // adaugarea unui Node // procesarea arborelui cu cele trei metode void preorder_walk(); void inorder_walk(); void postorder_walk(); private: // primitivele copy si destroy void copy(Tree const &other); void destroy(); // datele Tree *left, *right; Node *info; }; Functiile 'standard' Asa dupa cum se poate observa, clasa Tree contine trei cmpuri pointeri. Apare deci necesitatea definirii unui destructor, a unui constructor copy si a reacoperii operatorului de asignare. Ele vor fi implementate cu ajutorul primitivelor copy() si destroy(), prezentate ulterior: // destructorul: distruge arborele Tree::~Tree() { destroy(); } // constructorul implicit: initializeaza cu 0 Tree::Tree() { left = right = 0; info = 0; } // constructorul copy : initializeaza continutul unui alt obiect Tree::Tree(Tree const &other) { copy(other); } // reacoperirea asignarii Tree const &Tree::operator=(Tree const &other) { if (this != &other) { destroy(); copy(other); } return (*this); } Memorarea unui obiect n arbore Adaugarea unui obiect n arbore este un proces recursiv. n momentul apelului functiei add(), apar de fapt cteva posibilitati: . Cmpul info din nodul curent este un pointer NULL. n acest caz o copie a obiectului este inserata n nodul

33

Scurt tutorial de C++


curent . Daca arborele este partial 'umplut', este necesar sa se stabileasca daca obiectul de adaugat este 'naintea' sau 'dupa' obiectul din nodul curent. Comparatia este facuta de functia compare(). n functie de raspuns, inserarea se va face fie n subarborele stng, fie n subarborele drept (acesti subarbori vor trebui mai nti alocati). . Daca comparatia precedenta da raspunsul 'egal', obiectul nu va trebui inserat n arbore. n acest caz se va apela already_stored(). Functia add(): void Tree::add(Node *what) { if (! info) info = what->duplicate(); else { register int cmp = info->compare(what); if (cmp < 0) { if (! left) { left = new Tree; left->info = what->duplicate(); } else left->add(what); } else if (cmp > 0) { if (! right) { right = new Tree; right->info = what->duplicate(); } else right->add(what); } else info->already_stored(); } } Scanarea unui arbore Clasa Tree ofera trei metode de scanare a arborelui: preordine, inordine si postordine. Functiile vor fi recursive. void Tree::preorder_walk() { if (info) info->process(); if (left) left->preorder_walk(); if (right) right->preorder_walk(); } void Tree::inorder_walk() { if (left) left->inorder_walk(); if (info) info->process(); if (right) right->inorder_walk(); } void Tree::postorder_walk() { if (left) left->postorder_walk(); if (right) right->postorder_walk(); if (info) info->process(); }

34

Scurt tutorial de C++


Primitivele copy() si destroy() Functiile copy() si destroy() snt doi membri private ce implementeaza operatiile primitive n clasa Tree: copierea continutului unui alt arbore si distrugerea unui arbore: void Tree::destroy() { delete info; if (left) delete left; if (right) delete right; } void Tree::copy(Tree const &other) { info = other.info ? other.info->duplicate() : 0; left = other.left ? new Tree(*other.left) : 0; right = other.right ? new Tree(*other.right) : 0; } Cteva observatii: . Functia destroy() este recursiva, desi nu pare la prima vedere: instructiunea delete left va activa destructorul obiectului Tree pointat de left, care la rndul lui va apela destroy(), etc. . n mod analog, functia copy() este la rndul ei recursiva. Instructiunea left = new Tree (*other.left) va activa constructorul copy, care va apela n fond copy() pentru ramura stnga a arborelui. . Ca si n cazul functiei add(), nodurile snt duplicate cu ajutorul functiei duplicate(). Functia va avea implementarea concreta n clasele derivate din Node. Folosirea claselor Node si Tree Vom exemplifica folosirea celor doua clase cu un program care numara cuvintele dintr-un fisier. Cuvintele snt definite ca secvente de caractere despartite de spatii. Programul va afisa care cuvinte snt prezente si de cte ori. n continuare va fi prezentata clasa Strnode, derivata din Node, care va contine implementarea concreta a functiilor virtuale. Implementarea calcularii numarului de aparitii ale unui cuvnt se va face cu ajutorul functiei already_stored(), care va incrementa variabila privata times. class Strnode: public Node { public: // destructori constructori ~Strnode(); Strnode(); Strnode(Strnode const &other); Strnode(char const *s); Strnode const &operator=(Strnode const &other); // asignarea // functii cerute de protocolul clasei Node Node* duplicate() const; int compare(Node const *other) const; void process(); void already_stored(); private: // datele char *str; int times; }; Strnode::~Strnode() { delete str; } Strnode::Strnode() { str = 0; times = 0; } Strnode::Strnode(Strnode const &other) { str = strdupnew(other.str); times = other.times; } Strnode::Strnode(char const *s) { str = strdupnew(s); times = 1; } Strnode const &Strnode::operator=(Strnode const &other) { if (this != &other) { delete str; str = strdupnew(other.str); times = other.times; } return (*this); } Node *Strnode::duplicate() const { return (new Strnode(*this)); }

35

Scurt tutorial de C++


int Strnode::compare(Node const *other) const { Strnode *otherp = (Strnode *) other; if (str && otherp->str) return (strcmp(str, otherp->str)); if (! str && ! otherp->str) return (0); return ((int) otherp->str - (int) str ); } void Strnode::process() { if (str) printf("%4 d t %s \n", times, str); } void Strnode::already_stored() { times++; } void countfile(FILE *inf, char const *name) { Tree tree; char buf [255]; while (1) { fscanf(inf, " %s", buf); if (feof(inf)) break; Strnode *word = new Strnode(buf); tree.add(word); delete word; } tree.inorder_walk(); } int main(int argc, char **argv) { register int exitstatus = 0; if (argc > 1) for (register int i = 1; i < argc; i++) { FILE *inf = fopen(argv [i], "r"); if (! inf) { fprintf(stderr, "wordc: can't open \"%s\"\n", argv [i]); exitstatus++; } else { countfile(inf, argv [i]); fclose(inf); } } else countfile(stdin, "--stdin--"); return (exitstatus); } Concluzii finale n acest laborator am introdus doua abordari pentru constructia unei clase de stocaj. . Abordarea Storable/Storage defineste un prototip de stocat cu ajutorul functiei pur virtuale duplicate(). n timpul stocarii, aceasta functie este apelata pentru duplicarea unui obiect. Acest lucru mai impune implementarea functiei n fiecare clasa derivata din Storable pentru a putea fi plasata ntr-un obiect Storage . Abordarea template, prin folosirea clasei template Array, nu pune asemenea restrictii. Adica, conform definitiei unui obiect Array, pentru stocarea unor obiecte de (sa zicem) tip Person ntr-un tablou Array <Person> staff; tabloul nu trebuie modificat si nici clasa Person adaptata n vreun fel. Aceasta comparatie sugereaza ca abordarea template ar fi mult mai apropiata de clasa de tip container. Exista totusi si un dezavantaj: ori de cte ori o clasa template este folosita pentru o clasa anume (Person si Vehicle, de ex.), compilatorul trebuie sa construiasca o noua clasa reala, fiecare cu numele sau propriu (ArrayPerson, ArrayVehicle). n felul acesta fiecare functie din clasa template va apare n program de un numar de ori egal cu numarul noilor clase. Din contra, abordarea Storable/Storage necesita doar doua noi functii: un duplicator pentru Person si unul pentru Vehicle. nsusi codul pentru clasa container apare doar o data n program. n concluzie: . Cnd un program foloseste doar o singura clasa container, abordarea template este de preferat: este mai usor de folosit si nu cere precautii speciale sau conversii pentru clasa continuta . Cnd un program utilizeaza mai multe instante ale unei clase container, este de preferat abordarea Storable/Storage: se previne duplicarea codului, desi se cer si adaptari speciale pentru clasa continuta.

36

S-ar putea să vă placă și