Class & Struct
Declararea unei clase implică definirea unui nou tip de date. Ea conține structura pe care o va urma un obiect (en: object) -- o variabilă declarată ca acest tip nou de date. Obiectul are identitate (poate fi diferențiat de alte obiecte similare), stare (valorile variabilelor care vin "la pachet cu ea") și comportament (funcții care vin "la pachet" cu ea).
În C++, class
și struct
sunt același lucru. Există o unică diferență, minoră, detaliată mai jos. O clasă numită Vector2
ce reține coordonatele unui vector bidimensional, împreună cu o variabilă vec
de tip Vector2
ar putea fi definite astfel:
class Vector2 {
float x, y;
};
...
int main() {
Vector2 vec;
return 0;
}
Identitatea unui obiect este dată de adresa de memorie la care se află, aceasta fiind, în mod evident, unică. Starea obiectului se referă la valorile variabilelor declarate în clasă - în această situație, starea ar fi determinată de vec.x
și vec.y
. Pentru a adăuga comportament la obiectul nostru, vec
, putem defini metoda (en: method, def: funcție definită ca parte dintr-o clasă) sum
:
class Vector2 {
float x, y;
Vector2 vectorSum(Vector2 &other) {
Vector2 result;
result.x = x + other.x;
result.y = y + other.y;
return result;
}
};
Access Specifier
Cu clasa definită mai sus, putem declara variabile, dar nu putem accesa nimic din conținutul lor. Secvența:
int main() {
Vector2 vec;
vec2.x = 1.f;
}
nu compilează, pentru că, implicit, fieldurile și metodele din clase sunt private
. Din acest motiv, nu avem acces la fieldul x
din Vector2
, decât atunci când scriem cod în interiorul clasei. Aceasta este și singura diferență între class
și struct
în C++.
Access specifierele din C++ sunt următoarele:
public
: accesibil atât în clasă, cât și în exteriorul claseiprivate
: accesibil doar în clasăprotected
: accesibil doar în clasă și în clase care moștenesc (en: inherit) de la clasă (mai multe despre asta la Inheritance)
Așadar, pentru a putea accesa x
și y
din variabila vec
, am putea declara astfel:
class Vector2 {
public:
float x, y;
Vector2 vectorSum(Vector2 &other) {
Vector2 result;
result.x = x + other.x;
result.y = y + other.y;
return result;
}
};
Directiva public:
(ca și celelalte două) are efect până când este specificat altul, deci ambele variabile și metoda sunt toate accesibile din afara clasei. Secvența următoare ar funcționa acum:
int main() {
Vector2 u;
u.x = 1.f;
u.y = 2.f;
Vector2 v;
v.x = 0.f;
v.y = 3.f;
Vector2 sum = u.sum(v);
return 0;
}
pentru a apela o metodă de pe un obiect, sintaxa este similară cu cea pentru a accesa o variabilă membru.
this
this
este un pointer către obiectul curent. Metoda sum
definită mai sus ar putea fi scrisă astfel:
class Vector2 {
public:
float x, y;
Vector2 vectorSum(Vector2 &other) {
Vector2 result;
result.x = this->x + other.x;
result.y = this->y + other.y;
return result;
}
};
și ar avea aceeași semnificație. this
este implicit.
Observație: Operatorul ->
este o combinație între *
și .
. this->x
este o formă mai comodă de a scrie (*this).x
și funcționează pentru orice pointer.
Constructor
Constructorul este un bloc de cod care se execută atunci când un obiect este creat. Clasa noastră Vector2
cu un constructor ar putea arăta așa:
class Vector2 {
public:
float x, y;
Vector2() {
x = 0.f;
y = 0.f;
}
...
};
Declararea unei variabile de tip Vector2
utilizând acest constructor poate fi oricare dintre variantele:
Vector2 vec
Vector2 vec()
Vector2 vec = Vector2()
Atunci când declarăm noi un constructor, cel implicit dispare. Asta înseamnă că în situația:
class Vector2 {
public:
float x, y;
Vector2(float x, float y) {
this->x = x;
this->y = y;
}
...
};
putem declara variabile de tip Vector2
doar în următoarele moduri:
Vector2 vec(1.f, 3.f)
Vector2 vec = Vector2(1.f, 3.f)
Vector2 vec
de exemplu, ar da eroare de compilare, pentru că nu mai există niciun constructor cu 0 argumenți. Nu ne oprește nimeni, în schimb, să ne declarăm noi unul:
class Vector2 {
public:
float x, y;
Vector2() {
x = 0.f;
y = 0.f;
}
Vector2(float x, float y) {
this->x = x;
this->y = y;
}
...
};
Destructor
Destructorul se apelează în momentul în care un obiect este distrus (explicit sau dacă iese din scope). Aveți deja o înțelegere intuitivă a noțiunii de scope. În principiu, se reduce la "acoladele" între care se află o variabilă. Atunci când programul trece de acea acoladă închisă, variabila declarată se pierde și este apelat destructorul.
class Vector {
private:
// Putem inițializa membri și așa, se execută înaintea constructorului
uint32_t *v = nullptr;
Vector(uint32_t length) {
v = (uint32_t *) malloc(length * sizeof(uint32_t));
}
~Vector() {
free(v);
}
};
int main() {
{
Vector v; // Se apelează constructorul și memoria este alocată
} // Se apelează destructorul și memoria este eliberată
return 0;
}
Exerciții
Rezolvați fiecare dintre următoarele cerințe și testați codul pe câteva exemple simple:
- Definiți o clasă
Point2D
care reprezintă coordonatele unui punct în plan. - Definiți o clasă
Square
care conține următoarele metode și orice variabile membre utile:float area()
: calculează și întoarce aria pătratuluifloat perimeter()
: calculează și întoarce perimetrul pătratului
- Adăugați la clasa
Square
un constructor fără parametri și un destructor care scriu la consolă câte un mesaj diferit. - Adăugați la clasa
Square
un constructor care ia ca parametru colțul din stânga sus și perimetrul pătratului și inițializează corect variabilele membre.