3.Clase: problema alocarii memoriei

Fata de setul de functii pentru gestionarea memoriei din C (malloc(), etc.), operatorii new si delete sīnt construiti astfel īncīt sa valorifice avantajele limbajului C++. Diferentele importante īntre malloc() si new sīnt:

. Functia malloc() nu cunoaste la ce va fi folosita memoria alocata. De exemplu, cīnd 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 cīmpurile name, address si phone, sa nu devina inutilizabila la disparitia obiectului. Īn exemplul urmator este creat un obiect Person iar datele sīnt 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, eliberīnd memoria ocupata de cele trei cīmpuri 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());

}

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 sīnt copiati īn tmp).

. Apare o situatie periculoasa: valorile din p sīnt 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 ramīn īn memorie, pointerii catre ei ramīn 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 cīmpuri 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 īntīlnita forma de reasignare. Totusi, faptul ca C++ permite reacoperirea nu īnseamna ca aceasta facilitate trebuie folosuita tot timpul. Citeva regului utile sīnt:

. 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 cīnd 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 cuvīntul 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

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 cuvīntul cheie this, pentru adresarea acestui substrat (this nu este accesibil īn contextul functiilor membru declarate static, nediscutate īnca). Cuvīntul 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 cīnd este necesara folosirea explicita a lui this.

Prevenirea distrugerii proprii cu this.

Asa cum am vazut, operatorul '=' poate fi redefinit īn clasa Person astfel īncīt sa se obtina, prin asignare, doua copii ale aceluiasi obiect. Atīt timp cīt cele doua variabile sīnt 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, cīnd 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 īntīmpla 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 stīnga, 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. Cīnd 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);

}

return (*this) // īntoarce obiectul curent

}

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", apelīndu-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 pīna 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 sīnt des īntīlniti īn C++ si sīnt 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: Oricīnd este creat un obiect, este apelat un constructor !.

Regulile constructorului sīnt:

. 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 īncīt 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 cīnd este creat un obiect si initializat cu alt obiect (chiar daca noul obiect este o variabila ascunsa sau doar temporara):

. Cīnd 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 [ ]

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, specificīnd dimensiunea tabloului. El serveste si ca un constructor implicit, compilatorul punīnd 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 stīnga, 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. Aceasta clasa este declarata īn fisierul header iostream.h si defineste operatorul numai pentru tipurile de baza, int, char*, etc. (cīte 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