Tehnici de Programare in C++ 5. CLASE CONTAINER IN C++ 5.1 Colectii de obiecte O colectie (un container) este o clasa ce reuneste mai multe obiecte de un tip sau altul. Problema este de a avea cite o singura clasa pentru o fiecare structura (vector, lista, arbore, etc), indiferent de tipul datelor memorate in colectie, si nu cite o clasa separata pentru fiecare tip de date memorate. Numarul tipurilor de date in C++ nu este limitat, deoarece orice noua clasa definita reprezinta un nou tip de date. Prima abordare pentru clasele container in C++ a fost aceea de a crea o ierarhie de clase, astfel ca toate tipurile clasa sa fie derivate dintr-un tip de baza ("Object") si deci sa fie compatibile si substituibile intre ele. Ideea este de a memora in container obiecte sau pointeri la obiecte de tipul Object, care apoi vor fi inlocuite cu obiecte (pointeri la obiecte) de un tip derivat din Object. Trebuie observat ca, in general, sunt necesare conversii in ambele sensuri intre cele doua tipuri: - De la un tip T la tipul Object, cind se introduc date in container; - De la tipul Object la tipul T, cind se extrag date din container, pentru a se putea folosi metodele clasei T asupra obiectelor scoase din container (de exemplu, pentru afisarea lor). Sa consideram cea mai simpla structura de date - structura de vector. In C++ avem teoretic doua posibilitati de a defini componentele unui vector: - Componente de tip Object : Object x[10]; - Componente de tip Object* (pointeri la tipul Object): Object * x[10]; Nu este permisa declararea de vectori cu componente referinte, dupa cum nu se pot declara pointeri la referinte sau referinte la referinte. Memorarea de variabile de tip Object intr-un container nu permite apelarea de functii polimorfice (virtuale), deci ramine numai solutia unei colectii de pointeri la tipul de baza Object. Exemplu: O lista de pointeri la Object, un tip "String" derivat din Object si un program care foloseste o lista de siruri (de obiecte String). // clasa de la baza ierarhiei class Object { public: virtual void print() { cout << this << ' ';} virtual int equals (Object& obj) { return this==&obj;} } ; // operator utilizabil ptr. orice clasa derivata din Object inline int operator == (Object& ob1, Object& ob2) { return ob1.equals(ob2); } // un vector de pointeri la Object class Vector { Object* * vec; // adresa vector cu componente Object* int n, nmax; // dimensiune efectiva si maxima vector public: Vector (int size) { vec=new Object* [nmax=size]; n=0; } ~Vector() { delete [] vec; } void print (); void add (Object& obj) { vec[n++]= &obj; } int size() { return n;} Object & operator[] (int i) { assert (iprint(); cout <<" "; } cout << "\n"; } // siruri de caractere class String: public Object { char * str; public: String (char * s) { str =new char[strlen(s)+1]; strcpy (str,s);} ~String() { delete str;} void print () { cout << str << ' ';} int equals (Object& obj) { String & strobj= *(String*) &obj; return strcmp (str, strobj.str)==0 ? 1:0; } }; // program de test void main () { Vector a(10); String st[]={ "1","2","3","4","5"}; for (int i=0;i<5;i++) a.add (st[i]); // umplere vector cout << endl; a.print(); // afisare vector for (i=0;isize();i++) if (obj== lst->get(i)) return 1; return 0; } Parametrul ce specifica lista nu poate fi de tip "List" deoarece "List" este o clasa abstracta si nu poate fi instantiata. Am preferat utilizarea unei functii "get" in locul operatorului [] pentru selectia elementului din pozitia "i" deoarece utilizarea operatorului [] cu un pointer este susceptibila de o utilizare gresita. Comparatia trebuia scrisa astfel: if (obj == (*lst)[i]) si nu in forma urmatoare (corecta sintactic dar cu efect diferit): if (obj == lst[i]) Se poate defini si un operator de selectie, alaturi de functia "get" pentru situatiile cind se foloseste un obiect de tip "List" si nu un pointer la tipul "List". Sa presupunem ca din clasa abstracta "List" sunt derivate clasele concrete "Vector" si "LinkLst". Programul urmator creaza cite o lista din fiecare tip, le afiseaza si cauta in una din liste un obiect de tip "String". void main () { List* list1= new Vector(20); List* list2 = new LinkLst; String a[5]={"1","2","3","4","5"}; cout << endl; for (int i=0;i<5;i++) { list1->add(a[i]); list2->add(a[i]); } list1->print(); list2->print(); if (find (list1,String("3"))) cout << "este \n"; else cout << "nu este \n"; } Functiile "add", "print" si "get" sunt comune oricarui tip de lista, chiar daca implementarea lor este diferita. // functia Vector::get Object& Vector:: get (int i) { assert (i leg; while ( i-- && p !=NULL) p=p->leg; return * p->ptr; } // dimensiune lista int LinkLst::size() { int sz=0; Nod * p= first->leg; while (p !=NULL) { ++sz; p=p->leg; } return sz; } Pentru alegerea unui tip sau altul de lista este suficient sa modificam linia in care se initializeaza variabila de tip "List*", restul programului ramine la fel. Schimbarea implementarii poate fi necesara din motive de eficienta sau pentru afisarea in ordine a elementelor multimii (daca s-ar folosi o lista inlantuita ordonata). 5.4 Clase iterator In exemplele anterioare, parcurgerea unei liste (pentru afisare sau pentru comparare) folosea functia "get" si un indice, dar exista alte structuri de date pentru care nu este aplicabil accesul pozitional la elementele colectiei (de ex. un arbore sau un tabel de dispersie). De aceea s-a cautat o forma mai generala (si mai abstracta) pentru enumerarea elementelor unei colectii, care sa nu depinda de modul concret de adresare a elementelor colectiei (prin indici sau pointeri). Un iterator este un mecanism de parcurgere (vizitare) a componentelor unei colectii si are rolul de a abstractiza acest proces de enumerare a elementelor colectiei, independent de modul concret cum este implementata colectia (ca vector, ca lista cu pointeri, ca tabel de dispersie, ca arbore). Un iterator are 3 sau 4 componente : - O functie de initializare, care realizeaza pozitionarea pe primul element din colectie (pe inceputul colectiei); - O functie de verificare a sfirsitului colectiei ; - O functie de avans la elementul urmator din colectie (de incrementare a pozitiei curente). - O functie de extragere a elementului din pozitia curenta. De multe ori operatia de extragere a elementului curent este combinata cu operatia de avans la elementul urmator, intr-o singura functie. Un iterator poate fi privit ca un "super-pointer" sau ca un cursor ce se poate deplasa pas cu pas in cadrul unei colectii. In C++ avem posibilitatea ca o parte din functiile unui iterator sa fie exprimate prin operatori: operator de incrementare (++) si operatori de indirectare (* sau -> pentru un "smart-pointer"). In principiu, avem doua posibilitati de realizare a unui iterator pentru o colectie: - ca iterator intern, implementat prin citeva metode ale colectiei - ca iterator extern, implementat printr-un obiect dintr-o clasa de tip iterator. Iteratorul realizat ca o clasa separata de clasa colectie permite ca pentru o aceeasi colectie sa avem mai multi iteratori, in pozitii diferite. Clasa iterator trebuie sa aiba acces la date din clasa colectie, deci trebuie declarata ca o clasa prietena (pentru a nu face publice toate datele clasei container). In continuare se exemplifica clase iterator pentru vectori si liste. // iterator ptr. vectori class VecIter { Vector& avec; int crt; // pozitie curenta in vector public: VecIter (Vector* v): avec (*v) { crt=0;} operator int () { return crtfirst->leg;} operator int () { return crt != NULL ? 1:0 ; } Object* operator ++() { assert ( crt != NULL); Object* p= crt->ptr; crt=crt->leg; return p ; } }; // exemplu de utilizare iterator ptr afisare Vector a (10); ... // adaugare la vector Iterator * it= new VecIter (&a); while ( *it !=0) (++ *it)->print(); cout << "\n lista cu iterator: \n"; Observind ca ambele clase iterator contin aceleasi metode (la care se mai pot adauga si alte metode comune), este normal sa ne punem problema definirii unei clase abstracte "Iterator", din care sa fie derivate clasele concrete operator. // clasa absracta (interfata) ptr iteratori class Iterator { public: virtual operator int()=0; virtual Object* operator++()=0; }; // clase iterator concrete class VecIter: public Iterator { .. } class LinkIter: public Iterator {...} Existenta unei clase iterator generale, pentru parcurgerea oricarei colectii de obiecte, permite scrierea unor algoritmi "generici" sub forma unor functii aplicabile oricarei colectii : sortare colectie, cautare intr-o colectie, compararea a doua colectii, determinarea valorilor extreme dintr-o colectie, copierea unei colectii, etc. Exemplu: // algoritm generic: cautare obiect in lista int search (List* list, Object key) { Iterator * it = list->iterator(); while (*it !=0 ) if ( (*it).equals (key) ) return 1; else ++ (*it); return 0; }