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 clasei
  • private: 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:

  1. Definiți o clasă Point2D care reprezintă coordonatele unui punct în plan.
  2. Definiți o clasă Square care conține următoarele metode și orice variabile membre utile:
    • float area(): calculează și întoarce aria pătratului
    • float perimeter(): calculează și întoarce perimetrul pătratului
  3. Adăugați la clasa Square un constructor fără parametri și un destructor care scriu la consolă câte un mesaj diferit.
  4. 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.