Diferență între revizuiri ale paginii „C++ POO Lab Lucrarea 3”
(Nu s-au afișat 5 versiuni intermediare efectuate de același utilizator) | |||
Linia 4: | Linia 4: | ||
* Destructorul | * Destructorul | ||
* Referința | * Referința | ||
+ | * Metode ''const-qualified'' | ||
* Supraîncărcarea operatorilor | * Supraîncărcarea operatorilor | ||
* Constructorul de copiere | * Constructorul de copiere | ||
Linia 275: | Linia 276: | ||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | = Metode ''const-qualified'' = | ||
+ | |||
+ | În cazul în care un obiect este declarat <code>const</code>, sau se accesează printr-o referință constantă, acest obiect nu poate fi modificat. Modificarea unui obiect înseamnă modificarea valorilor câmpurilor sale. Astfel, pe lângă faptul că nu putem atribui valori noi câmpurilor în cazul în care avem acces la ele, nu putem nici apela metode care au ca efect modificarea obiectului. Iată un exemplu mai jos: | ||
+ | |||
+ | <syntaxhighlight lang="C++"> | ||
+ | class Animal { | ||
+ | public: | ||
+ | std::string mName; | ||
+ | std::string mColor; | ||
+ | int mAge; | ||
+ | bool mHasFeathers; | ||
+ | |||
+ | public: | ||
+ | void makeSound() { | ||
+ | printf("Animal %s makes a sound!\n", mName.c_str()); | ||
+ | } | ||
+ | |||
+ | // this is a getter method | ||
+ | std::string getName() { | ||
+ | return mName; | ||
+ | } | ||
+ | |||
+ | // this is a setter method | ||
+ | void setAge(int age) { | ||
+ | if(age > 0) { | ||
+ | mAge = age; | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | int main() { | ||
+ | const Animal animal; | ||
+ | animal.mName = "Cat"; // eroare de compilare - nu se poate modifica acest camp | ||
+ | animal.makeSound(); // eroare de compilare - nu se poate apela aceasta metoda | ||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | În exemplul de mai sus, este evident că nu putem modifica membrul <code>mName</code> al obiectului <code>animal</code> pentru că acesta este declarat <code>const</code>. Cu toate acestea, metoda <code>makeSound</code> nu modifică obiectul (nu modifică niciunul din câmpurile sale la apelare), dar nici aceasta nu poate fi apelată. Motivul este dat de faptul că programatorul trebuie să specifice explicit către compilator care sunt metodele care nu modifică obiectul, compilatorul nu face aceste verificări automat. O metodă care nu modifică obiectul pentru care se apelează se declară <code>const</code> și se numește ''const-qualified'': | ||
+ | |||
+ | <syntaxhighlight lang="C++"> | ||
+ | class Animal { | ||
+ | public: | ||
+ | std::string mName; | ||
+ | std::string mColor; | ||
+ | int mAge; | ||
+ | bool mHasFeathers; | ||
+ | |||
+ | public: | ||
+ | void makeSound() const { | ||
+ | printf("Animal %s makes a sound!\n", mName.c_str()); | ||
+ | } | ||
+ | |||
+ | // this is a getter method | ||
+ | std::string getName() const { | ||
+ | return mName; | ||
+ | } | ||
+ | |||
+ | // this is a setter method | ||
+ | void setAge(int age) { | ||
+ | if(age > 0) { | ||
+ | mAge = age; | ||
+ | } | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | int main() { | ||
+ | const Animal animal; | ||
+ | animal.mName = "Cat"; // eroare de compilare - nu se poate modifica acest camp | ||
+ | animal.makeSound(); // aici nu se mai produce nicio eroare de compilare, metoda makeSound este const-qualified | ||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Deci, pentru obiecte constante sau folosind referințe constante, se pot apela doar metode const-qualified. | ||
= Supraîncărcarea operatorilor = | = Supraîncărcarea operatorilor = | ||
Linia 520: | Linia 597: | ||
= Operatorul de copiere prin atribuire = | = Operatorul de copiere prin atribuire = | ||
+ | == Introducere == | ||
+ | În situația numărul 3 de mai sus (atunci când se atribuie un obiect unui alt obiect, în altă parte decât la inițializare), această copiere se realizează de un operator special, numit operatorul de copiere prin atribuie (copy-assignment operator). Acesta, similar cu constructorul de copiere, există în mod implicit, și copiază conținutul obiectului la nivel de byte. Și exact în același fel, câteodată este necesar să putem modifica acest comportament. | ||
+ | |||
+ | Forma unui operator de copiere prin atribuire pentru o clasă oarecare <code>T</code> este: | ||
+ | <syntaxhighlight lang="C++"> | ||
+ | T& operator=(const T & source) { | ||
+ | //aici se realizeaza copierea | ||
+ | return *this; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Prin urmare, operatorul de copiere prin atribuire va lua ca argument o referință constantă la un alt obiect de același fel, care reprezintă sursa copierii de date. Acesta poate fi de tip <code>void</code>, dar ca să se respecte specificația care spune că rezultatul operatorului de atribuire este valoarea atribuită, este recomandat ca întotdeauna această să întoarcă o referință la obiectul curent. Astfel, se pot realiza lanțuri de atribuiri. | ||
+ | |||
+ | == Exemplu == | ||
+ | |||
+ | Continuând exemplul de mai sus, vom adăuga acum operatorul de copiere prin atribuire: | ||
+ | <syntaxhighlight lang="C++"> | ||
+ | #include <cstdint> | ||
+ | #include <cstring> | ||
+ | #include <iostream> | ||
+ | |||
+ | // clasa Array modeleaza un array alocat dinamic | ||
+ | class Array{ | ||
+ | public: | ||
+ | int *arr; | ||
+ | size_t dim; | ||
+ | |||
+ | // constructorul aloca memorie si stocheaza adresa primului element in *arr | ||
+ | Array(int dimensiune){ | ||
+ | arr = (int*) malloc(dimensiune * sizeof(int)); | ||
+ | dim = dimensiune; | ||
+ | std::cout << "Acest mesaj provine din constructor" << std::endl; | ||
+ | } | ||
+ | |||
+ | Array(const Array & source) { | ||
+ | arr = (int*) malloc(source.dim * sizeof(int)); | ||
+ | dim = source.dim; | ||
+ | memcpy(arr, source.arr, dim * sizeof(int)); | ||
+ | std::cout << "Acest mesaj provine din constructorul de copiere" << std::endl; | ||
+ | } | ||
+ | |||
+ | Array & operator=(const Array & source) { | ||
+ | free(arr); // obiectul curent aici exista deja, deci trebuie sa stergem memoria alocata in constructor inainte de a aloca altceva | ||
+ | arr = (int*) malloc(source.dim * sizeof(int)); | ||
+ | dim = source.dim; | ||
+ | memcpy(arr, source.arr, dim * sizeof(int)); | ||
+ | std::cout << "Acest mesaj provine din operatorul de copiere prin atribuire" << std::endl; | ||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | // destructorul se asigura ca memoria alocata este eliberata atunci cand obiectul este distrus | ||
+ | ~Array(){ | ||
+ | free(arr); | ||
+ | std::cout << "Acest mesaj provine din destructor" << std::endl; | ||
+ | } | ||
+ | }; | ||
+ | |||
+ | int main() { | ||
+ | Array array(4); | ||
+ | Array other(5); | ||
+ | other = array; | ||
+ | return 0; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Ieșire: | ||
+ | <syntaxhighlight lang="text"> | ||
+ | Acest mesaj provine din constructor | ||
+ | Acest mesaj provine din constructor | ||
+ | Acest mesaj provine din operatorul de copiere prin atribuire | ||
+ | Acest mesaj provine din destructor | ||
+ | Acest mesaj provine din destructor | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | <div class="regula"><font color="#ff0000">The Rule of Three:</font> O regulă cunoscută în limbajul C++ este următoarea: dacă aveți de nevoie să implementați explicit constructorul de copiere SAU operatorul de copiere prin atribuire SAU destructorul, atunci trebuie să le implementați pe toate trei!.</div> |
Versiunea curentă din 12 aprilie 2022 20:08
Introducere
Această lucrare are ca scop familiarizarea cu următoarele noțiuni:
- Destructorul
- Referința
- Metode const-qualified
- Supraîncărcarea operatorilor
- Constructorul de copiere
- Operatorul de copiere prin atribuire
Destructorul
Introducere
Așa cum s-a prezentat în laboratorul anterior, crearea unui obiect se face folosind o metodă specială, numită constructor. Printre altele, cu ajutorul constructorului putem:
- Inițializa câmpurile obiectului pe care îl construim
- Condiționa valorile cu care inițializăm câmpurile (de exemplu vârsta unei persoane nu poate avea valoare negativa)
- Aloca memorie în mod dinamic
Pe de altă parte, odată ce nu mai avem nevoie de un obiect, acesta trebuie eliminat într-un mod controlat, astfel încât memoria ocupată să poată fi refolosită. Acest lucru este realizat cu ajutorul unei alte metode speciale, numită destructor, ce este apelată automat atunci când contextul în care a fost declarat obiectul se încheie (out of scope). Pentru mai multe informații legate de context (scope) în C++, accesați acest link.
Destructorul (pentru că nu poate exista decât unul singur în fiecare clasă), poate fi recunoscut după următoarele proprietăți:
- are numele format din caracterul
~
urmat de numele clasei; - nu are argumente;
- nu are tip returnat (nici măcar void).
Exemplu
În exemplul de mai jos sunt evidențiate mecanismele de ’construire’ si ’distrugere’ a obiectelor:
#include <iostream>
// clasa Array modeleaza un array alocat dinamic
class Array{
public:
int *arr;
// constructorul aloca memorie si stocheaza adresa primului element in *arr
Array(int dimensiune){
arr = (int*) malloc(dimensiune * sizeof(int));
std::cout << "Acest mesaj provine din constructor" << std::endl;
}
// destructorul se asigura ca memoria alocata este eliberata atunci cand obiectul este distrus
~Array(){
free(arr);
std::cout << "Acest mesaj provine din destructor" << std::endl;
}
};
int main() {
Array array(4);
std::cout << "Obiectul a fost construit!" << std::endl;
return 0;
}
Output:
Acest mesaj provine din constructor
Obiectul a fost construit!
Acest mesaj provine din destructor
Observăm că, odată ce contextul în care a fost declarat obiectul (contextul este, aici, funcția main), destructorul acestui obiect a fost apelat automat si a fost generat mesajul corespunzător.
Referința
Introducere
O referință reprezintă un nume alternativ cu care poate fi accesat un obiect/o variabilă. Așa cum vom prezenta în continuare, mecanismul de referințe simplifică accesul la date intre diferite entități (de exemplu între funcții).
Sintaxa pentru declararea unei referințe este următoarea: <tip_de_date>& nume_referinta = variabila
Odată declarată, cu ajutorul referinței vom accesa aceeași informație pe care o accesăm cu variabila cu care aceasta a fost legată (cele două făcând referire la aceeași zonă de memorie).
Exemplu
#include <iostream>
int main() {
int numar = 25;
// rNumar va fi o referinta (alt nume) a variabilei numar
int& rNumar = numar;
// observam ca au aceeasi valoare
std::cout << "Numar initial: " << numar << std::endl;
std::cout << "rNumar initial: " << rNumar << std::endl << std::endl;
rNumar++;
numar++;
std::cout << "Numar final: " << numar << std::endl;
std::cout << "rNumar final: " << rNumar << std::endl;
return 0;
}
Output:
Numar initial: 25
rNumar initial: 25
Numar final: 27
rNumar final: 27
În exemplul anterior se observă că deși folosim două variabile diferite, valorile celor doua sunt întotdeauna identice, iar o modificare asupra uneia se reflectă și asupra celeilalte.
Asocierea referințelor
Asocierea între o referință și o variabilă este permanentă și nu putem asocia referința cu altă variabilă! Putem, în schimb, să asociem mai multe referințe cu aceeași variabilă. Observați datele de ieșire ale următorului exemplu:
#include <iostream>
int main() {
#include <iostream>
int main() {
int numar_1 = 25;
int numar_2 = 30;
int& r1Numar = numar_1;
int& r2Numar = numar_1;
r1Numar = numar_2;
r1Numar++;
std::cout << "Valoarea lui r1Numar: " << r1Numar << std::endl;
std::cout << "Valoarea lui r2Numar: " << r2Numar << std::endl;
std::cout << "Valoarea lui numar_1: " << numar_1 << std::endl;
std::cout << "Valoarea lui numar_2: " << numar_2 << std::endl;
return 0;
}
Ieșire:
Valoarea lui r1Numar: 31
Valoarea lui r2Numar: 31
Valoarea lui numar_1: 31
Valoarea lui numar_2: 30
Observăm că expresia r1Numar = numar_2;
nu a asociat referința r1Numar
cu variabila numar_2
, ci a determinat copierea valorii numar_2
în variabila cu care rNumar
este asociată.
De asemenea, observăm că variabila numar_1
este asociată cu două referințe.
O referință nu poate fi asociată cu o valoare. Următorul program va genera erori de compilare:
int main() {
int& rNumar = 20; // aceasta expresie nu este permisa
return 0;
}
Transmiterea argumentelor prin referință
Utilitatea referințelor este evidențiată atunci când avem nevoie sa accesăm o variabilă din contexte diferite (precum două metode diferite). Folosind referințe putem economisi memorie, folosind același spațiu de memorie accesat cu două nume de variabile diferite.
Următorul exemplu nu folosește referințe pentru pasarea argumentelor (pass-by-value).
În acest caz, de fiecare dată când funcția concat
este apelată, în memorie este alocat spațiu pentru două variabile noi ce iau valorile argumentelor. Acest lucru nu este eficient atunci când avem nevoie să transmitem informații de dimensiuni mari.
#include <iostream>
#include <string>
/*
*
* Exemplu pass-by-value
*
**/
std::string concat(std::string a, std::string b){
return a + b;
}
int main() {
std::string a = "Ana are ";
std::string b = "mere.";
std::string result = concat(a, b);
std::cout << "Rezultat: " << result << std::endl;
return 0;
}
Următorul exemplu folosește referințe pentru transmiterea argumentelor (pass-by-reference).
În acest caz, de fiecare dată când funcția sum este apelată, argumentele funcției nu sunt copii ale variabilelor, ci referințe către variabilele transmise funcției.
#include <iostream>
#include <string>
/*
*
* Exemplu pass-by-reference
*
**/
std::string concat(std::string & a, std::string & b){
return a + b;
}
int main() {
std::string a = "Ana are ";
std::string b = "mere.";
std::string result = concat(a, b);
std::cout << "Rezultat: " << result << std::endl;
return 0;
}
Referințe constante (const)
Pentru a evita problema menționată mai sus (modificarea accidentală a unei variabile transmise unei funcții ca referință), se introduce conceptul de referință constantă. Acest tip de referință poate fi folosită doar pentru citirea informațiilor dintr-un obiect sau accesarea valorii unei variabile, nu și pentru modificarea acestora. Declararea unei referințe constante se face folosind cuvântul cheie const
.
Iată un exemplu:
#include <iostream>
#include <string>
/*
*
* Exemplu pass-by-reference
*
**/
std::string concat(const std::string & s1, const std::string & s2){
return s1 + s2;
}
int main() {
std::string a = "Ana are ";
std::string b = "mere.";
std::string result = concat(a, b);
std::cout << "Rezultat: " << result << std::endl;
return 0;
}
Folosind referințele s1 și s2 se pot citi datele din variabilele a și b din main
, dar fără a risca modificarea lor. Dacă se încearcă modificarea uneia din ele (de exemplu s2 = "pere";
), se va genera o eroare de compilare.
#include <iostream>
#include <string>
/*
*
* Exemplu pass-by-const-reference
*
**/
std::string concat(const std::string & s1, const std::string & s2){
return s1 + s2;
}
int main() {
std::string result = concat("Ana are ", "mere.");
std::cout << "Rezultat: " << result << std::endl;
return 0;
}
Metode const-qualified
În cazul în care un obiect este declarat const
, sau se accesează printr-o referință constantă, acest obiect nu poate fi modificat. Modificarea unui obiect înseamnă modificarea valorilor câmpurilor sale. Astfel, pe lângă faptul că nu putem atribui valori noi câmpurilor în cazul în care avem acces la ele, nu putem nici apela metode care au ca efect modificarea obiectului. Iată un exemplu mai jos:
class Animal {
public:
std::string mName;
std::string mColor;
int mAge;
bool mHasFeathers;
public:
void makeSound() {
printf("Animal %s makes a sound!\n", mName.c_str());
}
// this is a getter method
std::string getName() {
return mName;
}
// this is a setter method
void setAge(int age) {
if(age > 0) {
mAge = age;
}
}
};
int main() {
const Animal animal;
animal.mName = "Cat"; // eroare de compilare - nu se poate modifica acest camp
animal.makeSound(); // eroare de compilare - nu se poate apela aceasta metoda
return 0;
}
În exemplul de mai sus, este evident că nu putem modifica membrul mName
al obiectului animal
pentru că acesta este declarat const
. Cu toate acestea, metoda makeSound
nu modifică obiectul (nu modifică niciunul din câmpurile sale la apelare), dar nici aceasta nu poate fi apelată. Motivul este dat de faptul că programatorul trebuie să specifice explicit către compilator care sunt metodele care nu modifică obiectul, compilatorul nu face aceste verificări automat. O metodă care nu modifică obiectul pentru care se apelează se declară const
și se numește const-qualified:
class Animal {
public:
std::string mName;
std::string mColor;
int mAge;
bool mHasFeathers;
public:
void makeSound() const {
printf("Animal %s makes a sound!\n", mName.c_str());
}
// this is a getter method
std::string getName() const {
return mName;
}
// this is a setter method
void setAge(int age) {
if(age > 0) {
mAge = age;
}
}
};
int main() {
const Animal animal;
animal.mName = "Cat"; // eroare de compilare - nu se poate modifica acest camp
animal.makeSound(); // aici nu se mai produce nicio eroare de compilare, metoda makeSound este const-qualified
return 0;
}
Deci, pentru obiecte constante sau folosind referințe constante, se pot apela doar metode const-qualified.
Supraîncărcarea operatorilor
Dacă dorim să tratăm instanțele claselor (i.e. obiectele) așa cum tratăm tipurile de date fundamentale, trebuie să putem folosi aceiași operatori pe care îi folosim si cu tipurile fundamentale.
Cunoaștem, de exemplu, faptul că putem aduna două valori de tip întreg folosind operatorul '+'. În mod similar, am dori să putem calcula și suma a două numere complexe definite de clasa Complex
din următorul exemplu:
class Complex{
double mReal; // partea reala
double mImaginar; // partea imaginara
public:
/*
* Constructorul ce initializeaza campurile obiectului
*
* */
Complex(double real, double imaginar){
mReal = real;
mImaginar = imaginar;
}
/*
* Constructorul fara argumente
*
* */
Complex() = default;
/*
* Metoda: get_suma
*
* Intoarce suma (sub forma de obiect de tip Complex) dintre obiectul curent si obiectul primit ca argument
*
* */
Complex get_sum(Complex alt_numar){
double real_rezultat = mReal + alt_numar.mReal;
double imaginar_rezultat = mImaginar + alt_numar.mImaginar;
Complex rezultat(real_rezultat, imaginar_rezultat);
return rezultat;
}
/*
* Metoda: afisare
*
* Afiseaza numarul complex in formatul a+bi
*
* */
void afisare(){
std::cout << mReal << '+' << mImaginar << 'i' << std::endl;
}
};
Folosind cunoștințele acumulate până acum, putem realiza suma a doua obiecte de tip Complex
implementând metoda get_suma
din exemplul de mai sus. Totuși, aceasta nu ne permite să adunăm două obiecte de tip Complex
folosind operatorul '+', așa cum dorim.
Fie clasa Complex
definită anterior. Acesta este un exemplu de utilizare a funcției get_sum
:
int main() {
Complex complex_1 (5, 10);
Complex complex_2 (-1, 3);
Complex suma = complex_1.get_sum(complex_2);
suma.afisare();
return 0;
}
Ieșire:
4+13i
Supraîncărcarea operatorilor presupune implementarea uneia sau mai multor funcții speciale (numite funcții operator) ce descriu comportamentul operatorilor doriți.
Numele unei astfel de funcții începe cu cuvântul cheie operator
și este urmat de simbolul operatorului supraîncărcat. De asemenea, la fel ca funcțiile obișnuite, operatorii vor avea tip de date returnat și (opțional) argumente.
Sintaxa pentru declararea unei funcții operator:
tip_returnat operator simbol (<lista argumentelor>)
Reguli generale
Supraîncărcarea operatorilor trebuie să respecte următoarele reguli:
- Prioritatea operatorilor nu poate fi schimbată (ex. '*' are prioritate față de '+')
- Asociativitatea și comutativitatea operatorilor nu pot fi schimbate
- Numărul de operanzi ai unui operator nu poate fi schimbat (ex. operatorul '+' este binar, în timp ce '!' este unar)
- Nu pot fi supraîncărcați operatori noi, doar cei definiți în standardul C++. Consultați acest link pentru lista operatorilor.
Exemplu
Metoda get_sum
din exemplul anterior este înlocuită în exemplul următor de operatorul '+'.
#include <iostream>
class Complex{
double mReal; // partea reala
double mImaginar; // partea imaginara
public:
/*
* Constructorul ce initializeaza campurile obiectului
*
* */
Complex(double real, double imaginar){
mReal = real;
mImaginar = imaginar;
}
/*
* Constructorul fara argumente
*
* */
Complex() = default;
/*
* Metoda: operator +
*
* Intoarce suma (sub forma de obiect de tip Complex) dintre obiectul curent si obiectul primit ca argument
*
* */
Complex operator +(Complex alt_numar){
double real_rezultat = mReal + alt_numar.mReal;
double imaginar_rezultat = mImaginar + alt_numar.mImaginar;
Complex rezultat(real_rezultat, imaginar_rezultat);
return rezultat;
}
/*
* Metoda: afisare
*
* Afiseaza numarul complex in formatul a+bi
*
* */
void afisare(){
std::cout << mReal << '+' << mImaginar << 'i' << std::endl;
}
};
int main() {
Complex complex_1 (5, 10);
Complex complex_2 (-1, 3);
Complex suma = complex_1 + complex_2;
suma.afisare();
return 0;
}
Constructorul de copiere
Introducere
Obiectele se copiază în două situații:
- Atunci când se inițializează un obiect (i se atribuie o valoare la declarație);
- Atunci când se transmite un obiect către o funcție cu argumente de tip pass-by-value, deci nu ca referință.
- Atunci când se atribuie un obiect unui alt obiect (evident de același fel), în altă parte decât la inițializare.
În primele două situații, această copiere se realizează de un constructor special, numit constructor de copiere. Acest constructor există implicit în toate clasele fiind adăugat de compilator. Copierea implicită pe care o face acesta este prin a duplica tot conținutul obiectului sursă, așa cum este acesta salvat în memorie. În multe situații, acest comportament este cel corect. Totuși, există și situații când acest comportament implicit trebuie modificat. Prin urmare trebuie definit explicit acest constructor de copiere în cadrul clasei.
Forma unui constructor de copiere pentru o clasă oarecare T
este:
T(const T & source);
Prin urmare, constructorul de copiere ca lua ca argument o referință constantă la un alt obiect de același fel, care reprezintă sursa copierii de date.
Exemplu
Reluând exemplul de la capitolul #Destructorul, vom adăuga acum constructorul de copiere, pentru a evita crearea mai multor obiecte care să aibă pointer intern la aceeași zonă de memorie:
#include <cstdint>
#include <cstring>
#include <iostream>
// clasa Array modeleaza un array alocat dinamic
class Array{
public:
int *arr;
size_t dim;
// constructorul aloca memorie si stocheaza adresa primului element in *arr
Array(int dimensiune){
arr = (int*) malloc(dimensiune * sizeof(int));
dim = dimensiune;
std::cout << "Acest mesaj provine din constructor" << std::endl;
}
Array(const Array & source) {
arr = (int*) malloc(source.dim * sizeof(int));
dim = source.dim;
memcpy(arr, source.arr, dim * sizeof(int));
std::cout << "Acest mesaj provine din constructorul de copiere" << std::endl;
}
// destructorul se asigura ca memoria alocata este eliberata atunci cand obiectul este distrus
~Array(){
free(arr);
std::cout << "Acest mesaj provine din destructor" << std::endl;
}
};
void test(Array array) {
std::cout << "Mesaj din functie!" << std::endl;
}
int main() {
Array array(4);
std::cout << "Obiectul a fost construit!" << std::endl;
Array copy = array;
std::cout << "Obiectul a fost copiat!" << std::endl;
test(array);
std::cout << "Functia a fost apelata." << std::endl;
return 0;
}
Ieșire:
Acest mesaj provine din constructor
Obiectul a fost construit!
Acest mesaj provine din constructorul de copiere
Obiectul a fost copiat!
Acest mesaj provine din constructorul de copiere
Mesaj din functie!
Acest mesaj provine din destructor
Functia a fost apelata.
Acest mesaj provine din destructor
Acest mesaj provine din destructor
Operatorul de copiere prin atribuire
Introducere
În situația numărul 3 de mai sus (atunci când se atribuie un obiect unui alt obiect, în altă parte decât la inițializare), această copiere se realizează de un operator special, numit operatorul de copiere prin atribuie (copy-assignment operator). Acesta, similar cu constructorul de copiere, există în mod implicit, și copiază conținutul obiectului la nivel de byte. Și exact în același fel, câteodată este necesar să putem modifica acest comportament.
Forma unui operator de copiere prin atribuire pentru o clasă oarecare T
este:
T& operator=(const T & source) {
//aici se realizeaza copierea
return *this;
}
Prin urmare, operatorul de copiere prin atribuire va lua ca argument o referință constantă la un alt obiect de același fel, care reprezintă sursa copierii de date. Acesta poate fi de tip void
, dar ca să se respecte specificația care spune că rezultatul operatorului de atribuire este valoarea atribuită, este recomandat ca întotdeauna această să întoarcă o referință la obiectul curent. Astfel, se pot realiza lanțuri de atribuiri.
Exemplu
Continuând exemplul de mai sus, vom adăuga acum operatorul de copiere prin atribuire:
#include <cstdint>
#include <cstring>
#include <iostream>
// clasa Array modeleaza un array alocat dinamic
class Array{
public:
int *arr;
size_t dim;
// constructorul aloca memorie si stocheaza adresa primului element in *arr
Array(int dimensiune){
arr = (int*) malloc(dimensiune * sizeof(int));
dim = dimensiune;
std::cout << "Acest mesaj provine din constructor" << std::endl;
}
Array(const Array & source) {
arr = (int*) malloc(source.dim * sizeof(int));
dim = source.dim;
memcpy(arr, source.arr, dim * sizeof(int));
std::cout << "Acest mesaj provine din constructorul de copiere" << std::endl;
}
Array & operator=(const Array & source) {
free(arr); // obiectul curent aici exista deja, deci trebuie sa stergem memoria alocata in constructor inainte de a aloca altceva
arr = (int*) malloc(source.dim * sizeof(int));
dim = source.dim;
memcpy(arr, source.arr, dim * sizeof(int));
std::cout << "Acest mesaj provine din operatorul de copiere prin atribuire" << std::endl;
return *this;
}
// destructorul se asigura ca memoria alocata este eliberata atunci cand obiectul este distrus
~Array(){
free(arr);
std::cout << "Acest mesaj provine din destructor" << std::endl;
}
};
int main() {
Array array(4);
Array other(5);
other = array;
return 0;
}
Ieșire:
Acest mesaj provine din constructor
Acest mesaj provine din constructor
Acest mesaj provine din operatorul de copiere prin atribuire
Acest mesaj provine din destructor
Acest mesaj provine din destructor