Sunteți pe pagina 1din 10

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

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