POO - Laborator 3 Initializarea si attribuirea obiectelor Vom extinde exemplul cu stive din Laboratorul 2, pentru a oferi cele doua operatii importante legate de copierea obiectelor: initializarea si atribuirea. 1) Mai intii, pentru a putea afisa usor un obiect de tip stiva, prevedem o functie operator care sa ofere aceeasi functionalitate ca si operatorul << pentru tipuri simple de date. Concret, am dori sa putem scrie: cout << s; unde s este insa un obiect de tip stiva. Aceasta inseamna ca redefinim operatorul standard al limbajului (<<) pentru a putea lucra cu stive. Evident, trebuie sa definim ce intelegem prin aceasta operatie. Dorim sa afisam toate elementele din stiva (daca exista), in ordinea in care vor fi extrase prin operatii de tip pop, separate prin sirul " <-- ". Pentru aceasta operatie, trebuie sa definim o functie operator. Functiile operator au un nume special, de forma: operatorX unde X este operatorul care se redefineste pentru clasa respectiva. O functie operator poate fi membra a clasei sau nu. Daca functia nu este membra a clasei dar trebuie sa aiba acces la elementele private ale clasei, ea trebuie declarata ca functie friend. In aceasta situatie, operatorul << este binar: primul operand este un dispozitiv de iesire (de tip ostream) iar al doilea este unul de tip stiva. In acest caz, o expresie de genul: cout << s este interpretata ca: operator<<(cout, s); adica un apel al functiei operator<<, cu parametrii cout si s. Pentru a putea scrie expresii uzuale de forma: cout << s1 << s2; care sunt interpretate, conform asociativitatii de la stinga la dreapta: (cout << s1) << s2; adica: operator<< ( operator<< (cout, s1), s2); e clar ca trebuie sa definim functia operator cu tipul ostream (deci trebuie sa intoarca dispozitivul de iesire). Pentru eficienta, asemenea obiecte complexe sunt transferate prin referinta, deci prototipul functiei operator va fi: ostream& operator<< (ostream&, stack&); In definitia clasei (fisierul Stack.h din Laborator 2) adaugam deci linia: friend ostream & operator<< (ostream &, stack &); iar in fisierul Stack.cpp scriem implementarea: ostream& operator<< (ostream& dev, stack& s) { dev << "Stack " << s.nume << ": "; if (s.is_empty()) { dev << "Null\n"; return dev; } for(int i = s.sp; i >= 0; i--) { dev << s.buf[i]; if (i > 0) dev << " <-- "; } dev << "\n"; return dev; } De observat ca nu mai utilizam operatorul de rezolutie de forma stack:: in fata numelui functiei (functia nu este membra a clasei stack). 2) Constructor de copiere La tipurile obisnuite de date, utilizam intializari la declararea variabilelor de forma: void f() { float x = 7; float y = x; } Nu se facea distinctie intre initializare si atribuire. in sensul ca secventa de mai sus era perfect echivalenta cu: float x, y; x = 7; y = x; In cazul obiectelor, aceste operatii sunt distincte si trebuie sa dispunem de mijloace adecvate de implementare. In cazul operatiei de intializare, care trebuie sa aibe loc la crearea obiectului, aceasta se face printr-un constructor special, numit constructor de copiere. In cazul concret al clasei stack, am dori ceva de genul: stack a; a.push(1); // Operatii oarecare, deci a poate fi in orice stare stack b(a); // Creem stiva b si o intializam cu stiva a Prevedem deci in definitia clasei (Stack.h) un constructor de forma: stack (stack &); deci o functie care are ca parametru un obiect de tip stack, transmis prin referinta. Se prefera transmiterea prin referinta din motive de eficienta (ceea ce se transmite de fapt este o adresa de memorie). In implementarea clasei (Stack.cpp) includem aceasta functie: stack::stack(stack &s) { // // Constructor de copiere // buf = new int [nrmax = s.nrmax]; sp = s.sp; strncpy(nume, s.nume, 9); for (int i = 0; i <= sp; i++) buf[i] = s.buf[i]; cout << "Constructor de copiere pentru obiectul " << nume << "\n"; } In esenta, se vede ca e vorba de aceleasi operatii ca la celelalte functii constructor, numai ca datele initiale sunt luate din obiectul stack care apare ca parametru. In plus, se copiaza si continutul stivei (continutul bufferului, de la indicii 0 pina la sp inclusiv). 3) Atribuirea In cazul atribuirii, am dori ceva de genul: stack a; stack b; // Operatii diverse asupra stivelor a si b a = b; // Atribuire: a trebuie sa fie identica cu b, dar cu spatiu distinct de memorie // pentru bufferul alocat dinamic In acest caz, e vorba de o supradefinire a operatorului = (de artibuire), care trebuie sa capete semnificatie speciala. Concret, vom elibera vechiul buffer, il vom realoca conform noii dimensiuni si vom face aceleasi operatii de copiere ca la constructorul de copiere. Asemenea operatori se implementeaza de obicei prin functii operator membre ale clasei. Vom scrie deci in definitia clasei (Stack.h) ceva de genul (deocamdata): void operator= (stack &); In cazul functiilor operator membre ale clasei (pentru operatori binari), o expresie de forma: a = b; este interpretata ca: a.operator=(b); adica apelul functiei operator= asociate obiectului a, careia i se transmite ca parametru obiectul b. Implementarea (care se adauga in Stack.cpp) este: void stack::operator= (stack &s) { delete buf; strncpy(nume, s.nume, 9); nrmax = s.nrmax; sp = s.sp; buf = new int [nrmax]; for(int i = 0; i <= sp; i++) buf[i] = s.buf[i]; } Noul program principal (TStack.cpp) prevede diverse operatii de testare ale acestor noi metode. Tema a) Functiile constructor date ca exemplu nu testeaza daca alocarea memorie a fost efectuata corect. Extindeti aceste functii a.i. sa testeza daca pointerul buf este != NULL dupa apelul operatorului new. In caz contrar, se va da un mesaj de eroare si se va opri aplicatia. Aceste operatii sunt realizate automat prin macroinstructiunea assert (descrierea ei se poate citi din mediul integrat -help). Trebuie inclus fisierul header #assert.h. Un apel macro de forma: buf = new int[200]; assert (buf != NULL); va conduce la oprirea aplicatiei in cazul incalcarii conditiei logice specificate, cu un mesaj la consola de forma: Assertion failed: buf != NULL, File: ------, Line: ------- In acelasi spirit, eliberarea memoriei cu delete ar trebui facuta numai daca buf este != NULL (adica s-a alocat ceva). b) Dupa modelul clasei stack, definiti o clasa MyString pentru siruri de caractere, care sa aiba un cimp de tip int numit length si un pointer catre un sir uzual de caractere: class MyString { int length; char *buf; }; Prevedeti un constructor cu parametru de tip char *, care sa permita initializari de forma: MyString a("Un exemplu"); un operator de atribuire = membru al clasei, deci o functie: void operator=(MyString&); si un operator de concatenare +, nemembru al clasei, dar de tip friend, de forma: friend MyString opearator+ (MyString &, MyString &); Astfel, putem scrie ceva de genul: a = b + c; // Concateneaza c la b si atribuie lui a care se va expanda in: a.operator=( operator+(b, c) );