Diferență între revizuiri ale paginii „C++ POO Lab Lucrarea 2”

De la WikiLabs
Jump to navigationJump to search
 
(Nu s-au afișat 16 versiuni intermediare efectuate de același utilizator)
Linia 118: Linia 118:
 
     }
 
     }
 
};
 
};
 +
</syntaxhighlight>
 +
 +
==== Supraîncărcarea ====
 +
 +
Dacă în limbajul C nu este permisă declararea mai multor funcții cu același nume, în C++ este posibil acest lucru, folosind conceptul de '''supraîncărcare'''. Prin urmare, se pot declara funcții sau metode cu același nume, dar doar dacă există totuși diferențe care permit compilatorului să decidă care din aceste funcții este apelată. Diferențele se fac prin '''semnătura''' funcției.
 +
 +
Semnătura este formată din următoarele elemente:
 +
# Numele funcției
 +
# Tipul argumentelor funcției
 +
# Ordinea argumentelor funcției
 +
# Numărul argumentelor funcției
 +
# Alți specificatori (de exemplu <code>const</code>)
 +
 +
Semnătura NU conține și tipul întors de funcție. Prin urmare, funcțiile de mai jos au aceeași semnătură:
 +
<syntaxhighlight lang="C++>
 +
int divide (int n, int m);
 +
double divide (int n, int m);
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Linia 136: Linia 153:
 
Problema este că în cazul în care se dorește instanțierea unui număr mare de obiecte, acest mod de atribuire de valori câmpurilor devine anevoios. În acest sens, limbajele orientate pe obiecte oferă posibilitatea definirii unor metode speciale care se apelează automat în momentul în care se creează obiecte noi, și care se numesc constructori:
 
Problema este că în cazul în care se dorește instanțierea unui număr mare de obiecte, acest mod de atribuire de valori câmpurilor devine anevoios. În acest sens, limbajele orientate pe obiecte oferă posibilitatea definirii unor metode speciale care se apelează automat în momentul în care se creează obiecte noi, și care se numesc constructori:
  
<div class="regula"><font color="#ff0000">Atenție: </font>Constructorul este o metodă specială, definit în interiorul unei clase, care se apelează automat la instanțierea obiectelor, și care are rolul de a inițializa câmpurile din obiect.</div>
+
<div class="regula"><font color="#ff0000">Atenție: </font>'''Constructorul''' este o metodă specială, definită în interiorul unei clase, care se apelează automat la instanțierea obiectelor, și care are rolul de a inițializa câmpurile din obiect.</div>
 +
 
 +
În C++, constructorul poate fi recunoscut după cele două particularități:
 +
# Are același nume cu numele clasei;
 +
# Nu are tip returnat (nici măcar ''void'').
  
=== Modificatori de acces și încapsulare ===
+
Astfel, putem crea un constructor nou pentru clasa '''Animal''':
 +
<syntaxhighlight lang="C++>
 +
class Animal {
 +
    std::string mName;
 +
    std::string mColor;
 +
    uint8_t mAge;
 +
    bool mHasFeathers;
 +
   
 +
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
 +
        mName = name;
 +
        mColor = color;
 +
        mAge = age;
 +
        mHasFeathers = hasFeathers;
 +
    }
  
Membrii unei clase se pot împărți în două mari categorii:
+
    void makeSound() {
# Membri care se expun pentru a fi utilizați de alte obiecte;
+
        printf("Animal %s makes a sound!\n", mName.c_str());
# Membri care există doar pentru a ajuta la implementarea internă a funcționalității obiectului.
+
    }
 +
};
 +
</syntaxhighlight>
  
Pentru a permite ascunderea anumitor membri ai unei clase, limbajele orientate pe obiecte oferă conceptul de modificatori de acces. În C++ există trei modificatori de acces, din care doi sunt prezentați mai jos:
+
Constructorii, fiind metode în clasă, beneficiază de supraîncărcare, deci pot exista mai mulți constructori într-o clasă, dar cu semnături diferite. Astfel, trebuie să aveți în vedere următoarele reguli:
# '''public''' - membrii declarați publici sunt vizibili și accesibili din orice alt obiect de orice tip;
 
# '''private''' - membrii declarați privați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv.
 
# '''protected''' - membrii declarați protejați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv sau obiecte de tip clase derivate din clasa respectivă (pentru '''derivat''', vedeți paragraful moștenire).
 
  
Se recomandă ca în general toate câmpurile fie declarate private. În acest fel, este se ''încapsulează'' în obiect, iar accesul la ele se face prin metode publice care permit validarea modificărilor sau a citirilor de date din obiect:
+
# <div class="regula"><font color="#ff0000">Atenție: </font>Dacă nu declarați explicit un constructor în clasă, compilatorul va introduce automat un constructor implicit, fără argumente, care nu are niciun efect de inițializare a câmpurilor.</div>
 +
# <div class="regula"><font color="#ff0000">Atenție: </font>Dacă declarați explicit cel puțin un constructor în clasă, compilatorul nu va mai introduce constructorul implicit. Dacă doriți puteți crea obiecte folosind constructorul fără argumente, trebuie să-l declarați explicit. Pentru a ușura sintaxa, puteți folosi cuvântul cheie <code>default</code> care reprezintă implementarea implicită a constructorului fără argumente</div>
  
<syntaxhighlight lang="C++">
+
<syntaxhighlight lang="C++" line highlight="7">
 
class Animal {
 
class Animal {
private:
 
 
     std::string mName;
 
     std::string mName;
 
     std::string mColor;
 
     std::string mColor;
     int mAge;
+
     uint8_t mAge;
 +
    bool mHasFeathers;
 +
   
 +
    Animal() = default;
  
protected:
+
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
    bool mHasFeathers; // accesibil din eventualele clase derivate
+
        mName = name;
 +
        mColor = color;
 +
        mAge = age;
 +
        mHasFeathers = hasFeathers;
 +
    }
  
public:
+
     void makeSound() {
     virtual void makeSound() {
 
 
         printf("Animal %s makes a sound!\n", mName.c_str());
 
         printf("Animal %s makes a sound!\n", mName.c_str());
 
     }
 
     }
 +
};
 +
</syntaxhighlight>
  
    // this is a getter method
+
==== Inițializarea și atribuirea câmpurilor de către constructor ====
    std::string getName() {
 
        return mName;
 
    }
 
  
    // this is a setter method
+
În momentul în care se apelează un constructor, acesta trebuie să modifice valorile câmpurilor obiectului. Acest lucru se poate face în două moduri:
    void setAge(int age) {
+
# Prin atribuire
        if(age > 0) {
+
# Prin inițializare
            mAge = age;
 
        }
 
    }
 
};
 
</syntaxhighlight>
 
  
=== Moștenire ===
+
===== Atribuirea câmpurilor =====
  
Moștenirea este mecanismul prin care o clasă preia structura (câmpuri) și comportamentul (metodele) unei alte (sau mai multor) clase, la care poate adaugă elemente specifice
+
Varianta cea mai simplă, dar nu și cea mai eficientă, este accea de a atribui valori câmpurilor. Acest lucru este evidențiat în exemplul de mai sus. Totuși, există și posibilitatea ca numele argumentelor constructorului să coincidă cu numele câmpurilor. În această situație, atribuirea devine ambiguă.
  
Clasa de bază = clasa de la care se preia structura si comportamentul
+
<div class="regula"><font color="#ff0000">Atenție: </font>Când utilizați o variabilă, compilatorul o caută incremental, din blocul de instrucțiuni (scope-ul) cel mai apropiat în exterior. Deci dacă există o variabilă locală și un câmp cu același nume, compilatorul va considera variabila locală ca fiind cea la care vă referiți.</div>
  
Clasa derivata = clasa care preia structura si comportamentul
+
Pentru a rezolva acest conflict de nume, se poate utiliza cuvântul cheie <code>this</code> care reprezintă un pointer la obiectul curent (cel care se construiește de către constructor):
  
În C++, moștenirea poate fi multiplă, în sensul ca o clasa derivata poate moșteni mai multe clase de baza. Nu în toate limbajele exista acest concept (în Java de exemplu, există doar moștenire simplă)
+
<syntaxhighlight lang="C++">
 +
class Animal {
 +
    std::string name;
 +
    std::string color;
 +
    uint8_t age;
 +
    bool hasFeathers;
 +
   
 +
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
 +
        this->name = name;
 +
        this->color = color;
 +
        this->age = age;
 +
        this->hasFeathers = hasFeathers;
 +
    }
  
'''Avantaje''':
+
    void makeSound() {
 +
        printf("Animal %s makes a sound!\n", name.c_str());
 +
    }
 +
};
 +
</syntaxhighlight>
  
- reutilizare cod
+
<div class="regula"><font color="#ff0000">Atenție: </font>Cuvântul cheie <code>this</code> poate fi folosit în orice metodă (ne-statică) dintr-o clasă (inclusiv în constructor), și reprezintă pointer la obiectul curent (pentru care s-a apelat metoda respecitvă). Deci poate fi folosit mai departe împreună cu operatorul <code>-></code> pentru a accesa membrii (câmpurile sau metodele) obiectului.</div>
  
- extensie a unei clase deja scrise, fără a fi necesara recompilarea ei
+
<div class="sfat"><font color="darkgreen">Sfat:</font> Pentru a evita problemele de conflict de nume între câmpuri și variabile locale, folosiți întotdeauna convenția de a numi câmpurile cu prefixul <code>m</code>: ''mName'', ''mHasFeathers'' sau ''mMember''.</div>
  
- utilizarea polimorfismului în timpul execuției, prin folosirea funcțiilor virtuale
+
===== Inițializarea câmpurilor =====
  
Exemplu (pentru o clasa mostenita din clasa Animal de mai sus)
+
Limbajul C++ pune la dispoziție o sintaxă specială care permite inițializarea câmpurilor în constructor. Acest lucru este esențial atunci când se declară câmpuri constante sau câmpuri de tip referință. Diferența dintre atribuire inițializare este că cea din urmă se face doar la declararea variabilei iar prima se poate face oricând după aceea, atât timp cât variabila nu este declarată constantă.
  
<syntaxhighlight lang="C++">
+
Sintaxa este evidențiată mai jos:
class Pisica : public Animal {
+
<syntaxhighlight lang="C++" line highlight="7">
public:
+
class Animal {
     Pisica() : Animal() // constructorul implicit Animal este chemat explicit
+
    std::string mMame;
     {
+
     std::string mColor;
      mHasFeathers = false;
+
     uint8_t mAge;
     } // campul mHasFeathers este protected in Animal, deci poate fi accesat de Pisica
+
    bool mHasFeathers;
}; // nu uitati de ; la sfarsitul unei clase
+
      
</syntaxhighlight>
+
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) : mName(name), mColor(color), mAge(age), mHasFeathers(hasFeathers) {
  
Relativ la specificatorul de acces declarat intre clasa derivata si clasa de baza  (class Pisica : '''public''' Animal) avem si posibilitatea de a folosi '''private''':
+
    }
{| class="wikitable"
 
|+ Accesul asupra membrilor moșteniți
 
|-
 
! Protecția in clasa de baza !! Modif de acces utilizat în lista claselor de bază !! Dreptul de acces în clasa derivată
 
|-
 
| public || public || public
 
|-
 
| private || public || inaccesibil
 
|-
 
| protected || public || protected
 
|-
 
| public || private || private
 
|-
 
| private || private || inaccesibil
 
|-
 
| protected || private || private
 
|}
 
  
=== Polimorfism ===
+
    void makeSound() {
 +
        printf("Animal %s makes a sound!\n", mName.c_str());
 +
    }
 +
};
 +
</syntaxhighlight>
  
Polimorfismul '''parametric''' – mecanismul prin care putem defini o metodă cu același nume în aceași clasă, funcții care trebuie să difere prin numărul și/sau tipul parametrilor și/sau ordinea lor. Selecția funcției se realizează la '''compilare''' (legarea timpurie (early binding)). Se numește foarte des '''supraîncarcare''' pentru a evita confuzia cu polimorfismul de moștenire.
+
Numele de dinainte de paranteză este cel al câmpului, iar în paranteză este expresia cu care se inițializează acesta.
  
Polimorfismul '''de moștenire''' – mecanismul prin care o metodă din clasa de bază este redefinită cu aceiași semnatură (return value, nume, parametri) în clasele derivate. Selecția funcției se va realiza '''la execuție''' (legare întârziată == late binding). Acesta este sensul în care se folosește cel mai des, și deci sensul in care va fi interpretat cuvantul '''polimorfism''' (daca nu este specificat de ce tip este)
+
=== Modificatori de acces și încapsulare ===
  
'''Avantaje''':
+
Membrii unei clase se pot împărți în două mari categorii:
 +
# Membri care se expun pentru a fi utilizați de alte obiecte;
 +
# Membri care există doar pentru a ajuta la implementarea internă a funcționalității obiectului.
  
- clasa de baza si pointerul de tip clasa de baza pot fi scrise in codul sursa al unei biblioteci compilate cu mult timp inaintea compilarii clasei derivate (eg. ani de zile)
+
Pentru a permite ascunderea anumitor membri ai unei clase, limbajele orientate pe obiecte oferă conceptul de modificatori de acces. În C++ există trei modificatori de acces, din care doi sunt prezentați mai jos:
 +
# '''public''' - membrii declarați publici sunt vizibili și accesibili din orice alt obiect de orice tip;
 +
# '''private''' - membrii declarați privați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv.
 +
# '''protected''' - membrii declarați protejați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv sau obiecte de tip clase derivate din clasa respectivă (pentru '''derivat''', vedeți paragraful moștenire).
  
- pointerul de tip clasa de baza poate referi orice obiect, de orice tip clasa derivata a clasei de baza (și isi poate schimba dinamic valoarea, la execuție)
+
Se recomandă ca în general toate câmpurile să fie declarate private. În acest fel, este se ''încapsulează'' în obiect, iar accesul la ele se face prin metode publice care permit validarea modificărilor sau a citirilor de date din obiect:
  
 
<syntaxhighlight lang="C++">
 
<syntaxhighlight lang="C++">
#include <iostream>
 
 
 
class Animal {
 
class Animal {
 
private:
 
private:
Linia 249: Linia 288:
 
     std::string mColor;
 
     std::string mColor;
 
     int mAge;
 
     int mAge;
 
+
     bool mHasFeathers;
protected:
 
     bool mHasFeathers; // accesibil din eventualele clase derivate
 
  
 
public:
 
public:
     virtual void makeSound() {
+
     void makeSound() {
         std::cout << "Animal " << mName << " makes a sound!" << std::endl;
+
         printf("Animal %s makes a sound!\n", mName.c_str());
    }
 
 
 
    void setName(std::string str) {
 
        mName = str;
 
 
     }
 
     }
  
Linia 274: Linia 307:
 
     }
 
     }
 
};
 
};
 
class Pisica : public Animal {
 
public:
 
    Pisica() : Animal() { mHasFeathers = false; }
 
 
    // cuvantul '''override''' este optional dar recomandat si produce o eroare de compilare daca metoda makeSound din Pisica nu suprascrie metoda
 
    // makeSound din Animal din cauza unei erori de sintaxa (eg. nume usor schimbat, alt return type, alti parametri / ordine sau tip al lor
 
    void makeSound() override {
 
        std::cout << "Pisica " << getName() << " makes a sound!" << std::endl;
 
    }
 
 
    // metoda din clasa Pisica ascunde implementarea din clasa Animal, dar nu o suprascrie polimorfic pentru ca setAge nu este virtuala in Animal
 
    void setAge(int age) {
 
        if (age >= 20) std::cout << "WARN: Longeviva pisica... sigur nu este o greseala?" << std::endl;
 
 
        Animal::setAge(age); // chemare setAge din clasa de baza Animal
 
    }
 
};
 
 
int main() {
 
 
    std::cout << std::endl << "Demo Animal:" << std::endl;
 
    Animal *pAnimal;
 
    Animal un_animal;
 
    un_animal.setName("TestoasaNinja");
 
 
    un_animal.makeSound(); // metoda makeSound din clasa Animal
 
    un_animal.setAge(99); // metoda setAge din clasa Animal
 
 
    pAnimal = &un_animal;
 
    pAnimal->makeSound(); // metoda makeSound din clasa Animal
 
    pAnimal->setAge(99); // metoda setAge din clasa Animal
 
 
    std::cout << std::endl << "Demo Pisica:" << std::endl;
 
    Pisica tom;
 
    tom.setName("Tom");
 
    tom.makeSound(); // metoda makeSound din clasa Pisica
 
    tom.setAge(20); // metoda setAge din clasa Pisica
 
 
    Pisica *pPisica;
 
    pPisica = &tom;
 
    pPisica->makeSound(); // metoda makeSound din clasa Pisica
 
    pPisica->setAge(20); // metoda setAge din clasa Pisica
 
 
    // demonstratie POLIMORFISM (de mostenire):
 
    std::cout << std::endl << "Demo Polimorfism (de mostenire):" << std::endl;
 
    pAnimal = &tom; // pointer de tip clasa de baza Animal, care contine o adresa de tip clasa derivata, Pisica
 
    pAnimal->makeSound(); // ATENTIE: medoda makeSound din clasa Pisica (makeSound ESTE virtuala in clasa de baza Animal)
 
    pAnimal->setAge(20); // ATENTIE: metoda setAge din clasa Animal (setAge NU este virtuala in clasa de baza Animal)
 
 
    // pPisica = &un_animal; // eroare de compilare: invalid conversion from ‘Animal*’ to ‘Pisica*’ [-fpermissive]
 
 
    return 0;
 
}
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Secventa de mai sus afiseaza:
+
<div class="regula"><font color="#ff0000">Atenție: </font>Una din cele două diferențe între <code>class</code> și <code>struct</code> este că pentru prima, modificatorul de acces implicit este '''private''' iar pentru a doua este '''public'''. Prin urmare, modificatorul '''private''' de la începutul clasei din exemplul de mai sus poate lipsi.</div>
 
 
<syntaxhighlight>
 
Demo Animal:
 
Animal TestoasaNinja makes a sound!
 
Animal TestoasaNinja makes a sound!
 
 
 
Demo Pisica:
 
Pisica Tom makes a sound!
 
WARN: Longeviva pisica... sigur nu este o greseala?
 
Pisica Tom makes a sound!
 
WARN: Longeviva pisica... sigur nu este o greseala?
 
 
 
Demo Polimorfism (de mostenire):
 
Pisica Tom makes a sound!
 
</syntaxhighlight>
 
 
 
=== Functii/Metode virtuale pure, clase abstracte ===
 
 
 
- o functie/metoda virtuala pura este o functie/metoda care nu are corp (body), nici macar gol
 
 
 
virtual std::string toString() {}; // nu este o functie/metoda virtuala pura, are corp
 
 
 
virtual std::string toString() = 0; // este o functie/metoda virtuala pura
 
 
 
O clasa abstracta este o clasa care contine cel putin o metoda virtuala pura, si deci nu se poate instantia (nu exista obiecte de tip clasa
 
 
 
Exemplu (pentru o clasa mostenita din clasa Animal de mai sus)
 
 
 
<syntaxhighlight lang="C++">
 
class Pisica : public Animal {
 
public:
 
    Pisica() : Animal() // constructorul implicit Animal este chemat explicit
 
    {
 
      mHasFeathers = false;
 
    } // campul mHasFeathers este protected in Animal, deci poate fi accesat de Pisica
 
}; // nu uitati de ; la sfarsitul unei clase
 
</syntaxhighlight>
 

Versiunea curentă din 12 aprilie 2022 20:11

După parcurgerea acestei platforme, veți deveni familiari cu diferite concepte de programare orientată pe obiecte: clasa, obiectul, câmpul, metoda, constructorul, încapsularea.

Noțiuni despre paradigme de programare

De la inventarea mașinilor de calcul, lupta pentru performață și avans tehnologic s-a dat pe două fronturi. Pe de o parte prin îmbunătățirea performațelor hardware-ului, prin creșterea frecveței, a numărului de core-uri, a memoriei disponibile și a complexității setului de instrucțiuni, iar pe de altă parte, prin găsirea de noi metode de programare eficientă a algoritmilor. Aceste metode noi nu însemnă exclusiv limbaje noi, cu toate că dacă urmărim evoluția acestora în ultimii ani, vom observa o creștere semnificativă, atât ca număr, cât și ca varietate. De ce apar în continuare limbaje noi, dacă există deja un număr atât de mare? Există mai multe motive, dar cel principal constă în apariția unor funcționalități noi, care trebuie exprimate în moduri care încă nu există (spre exemplu acceleratoarele SIMD - Single Instruction Multiple Data sunt greoi de programat folosind C, deoarece limbajul nu are suport pentru tipuri de date vectoriale). Dar apariția limbajelor noi mai este influențată de un lucru, și anume apariția unor noi paradigme.


Paradigma de programare se referă la modul în care este descris un algoritm. Sigur, prin descriere nu ne referim la modul în care se fac adunările sau înmulțirile sau la limbajul în sine, ci la un nivel mai înalt, și anume la cum sunt structurate datele, și la legătura dintre structurile de date și secvențele de program care acționează asupra lor. Există un număr relativ mic de paradigme de programare, și vom numi imediat niște exemple, dar e important de reținut că nu se poate spune că una este sau nu mai bună decât cealaltă, ci, ca și în cazul limbajelor de programare, fiecare este optimă pentru anumite clase de aplicații.

Programarea imperativă procedurală

Programarea imperativă descrie un algoritm la nivel de instrucțiune care modifică starea programului. Programarea procedurală structurează aceste instrucțiuni pe secvențe distincte, numite proceduri sau funcții (a nu se confunda totuși cu programarea funcțională), care acționează asupra unor structuri de date globale, vizibile tuturor procedurilor, sau locale, vizibile fiecărei proceduri în parte. Ca exemplu de limbaj de programare procedural este C-ul original (nu C++). Programele scrise în C sunt construite din funcții (care pot lua argumente, calcula și întoarce valori) și date globale.


Programarea procedurală se folosește pe scară largă în programare la nivel foarte jos (kernel, sistem de operare, embedded), deoarece C-ul oferă control absolut asupra resurselor hardware, și se potrivește foarte bine cu modul în care procesorul execută programul. Pe de altă parte, în cazul aplicațiilor la nivel înalt, cum ar fi editoare de text, aplicații client - server, simulatoare, programe de sinteză, etc., acolo unde se scrie o cantitate enormă de algoritmi, faptul că programarea procedurală nu oferă o structurare a datelor și o legătură între date și procedurile care le folosesc, o face foarte greu de folosit. În aceste cazuri apar probleme de securitate, probleme de memorie, probleme de mentenanță și dezvoltare ulterioară, etc.

Programarea declarativă funcțională

Programarea funcțională se referă la stilul și limbajele de programare în care computația se tratează evaluând expresii matematice. Spre deosebire de programarea imperative, unde apar stări și variabile, programarea funcțională pe bazează pe un sistem formal numit lambda-calcul, care descrie un program sub forma compunerii unor funcții matematice.

Unul din primele limbaje funcționale este Lisp dar există și limbaje funcționale moderne (Scala, Haskel, etc.). Ce e și mai interesant este faptul că limbajele imperative sau OOP consacrate (C++, Java) au început în ultimul timp să suporte și ele expresii lambda, fapt care duce treptat la apariția unor limbaje de programare hibride.

Programarea orientată obiect

Programarea orientată obiect are ca punct de plecare programarea procedurală, în sensul că se bazează tot pe noțiunile de stare și variabile, dar introduce o serie de concepte care ajută la structurarea programelor:

  • clase și obiecte;
  • încapsulare;
  • polimorfism;
  • moștenire.

Clase și obiecte

Structura în C

Plecând de la limbajul C, ne amintim că acesta pune la dispoziție o serie de tipuri de date primitive, incluse în standardul limbajului. Ca exemple, avem: int, long, char, double, etc. În plus, există posibilitatea de a crea structuri compuse folosind cuvântul cheie struct. O structură în C este compusă din una sau mai multe variabile care pot fi ori de tip primitiv, ori alte structuri.


În continuare avem un exemplu de definiție a unor structuri în C.

struct string{
    char* str;
    unsigned length;
};

struct person{
    struct string *first_name;
    struct string *last_name;
    unsigned age;
    float height;
    float weight;
};

Se vede că structura person conține pointeri la două structuri de tip string. Relația este descrisă de schema bloc următoare (cu exemple de valori pentru variabilele primitive):

Schema bloc pentru relația dintre structurile string și person

În continuare, vom da un exemplu de utilizare a acestor structuri:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(){
    //make a default string with no content and length = 30
    struct string * some_string = (struct string*)malloc(sizeof(struct string));
    unsigned default_length = 30;
    some_string->length = default_length;
    some_string->str = (char*)malloc(default_length * sizeof(char));
    strcpy(some_string->str, "Vasile");

    //make a person structure in which all strings refer to the default empty string
    struct person* new_person = (struct person*)malloc(sizeof(struct person));
    new_person->first_name = some_string;
    new_person->last_name = some_string;
    new_person->age = 29;
    new_person->height = (float)1.7;
    new_person->weight = 68.9F;

    printf("Persoana se numeste %s %s, are varsta de %d ani, inaltimea %f si greutatea %f\n", new_person->first_name->str, 
        new_person->last_name->str,
        new_person->age,
        new_person->height,
        new_person->weight);

    return 0;
}

În acest exemplu, schema bloc este diferită dintr-un punct de vedere esențial: ambii pointeri de tip string sunt referință la aceeași adresă, respectiv la același string. Astfel, dacă se modifică new_person->first_name, atunci implicit se modifică și new_person->last_name (de fapt este aceeași structură):

Schema bloc pentru relația dintre structurile string și person cu același pointer pentru nume


Pentru a face un rezumat, structura, in C, este un tip de dată compusă din tipuri primitive, sau alte structuri. Analog cu orice alt tip de dată, se pot defini variabile de tipul structurii, așa cum se pot defini variabile de tip primitiv. Limitarea fundamentală a structurilor este că acestea nu pot conține decât date, nu și funcții. Astfel se introduce noțiunea de clasă.

Clasa în C++

O clasă în C++ se declară similar cu un struct, folosind cuvântul cheie class. De fapt, o structură și o clasă sunt echivalente în C++, cu excepția unui singur detaliu care va fi discutat în secțiunea #Modificatori de acces și încapsulare. Prin urmare, clasa este un tip de dată și reprezintă un șablon după care se crează (instanțiază) obiecte.

Noțiunea de obiect este inspirat din sistemele reale, în care diverse obiecte de diferite tipuri interacționează pentru a produce un efect dorit, de exemplu piesele unei mașini care conlucrează pentru a face mașina să meargă. Diverse obiecte din mașină sunt similare, fap parte din aceeași clasă de obiecte, spre exemplu o mașina are patru roți și deși aceste obiecte au aceleași proprietăți (de exemplu dimensiune, presiune sau grad de uzură), fiecare din aceste obiecte au valori diferite pentru aceste proprietăți. În plus, toate roțile, indiferent de valorile proprietăților, au același rol, deci aceeași funcționalitate: se învârt pentru a face mașina să se deplaseze, se pot umfla sau desumfla, etc. În programare, proprietățile unei clase de obiecte se definesc prin variabile iar funcționalitatea prin funcții.

Putem spune astfel că o clasă definește proprietățile și funcționalitatea obiectelor, iar fiecare obiect în parte poate avea valori diferite pentru aceste proprietăți. Ca definiții:

Atenție: Clasa este o structură care poate conține variabile, numite câmpuri ale clasei, și funcții, numite metode are clasei. Câmpurile și metodele sunt membrii clasei.
Atenție: Clasa este un tip de dată. Datele de tipul clasei se numesc obiecte. Obiectele sunt instanțe ale clasei. Astfel, când se creează un obiect nou, se mai spune că se instanțiază un obiect de tipul clasei respective.
class Animal {
    std::string mName;
    std::string mColor;
    unsigned mAge;
    bool mHasFeathers;
    
    void makeSound() {
        printf("Animal %s makes a sound!\n", mName.c_str());
    }
};

Supraîncărcarea

Dacă în limbajul C nu este permisă declararea mai multor funcții cu același nume, în C++ este posibil acest lucru, folosind conceptul de supraîncărcare. Prin urmare, se pot declara funcții sau metode cu același nume, dar doar dacă există totuși diferențe care permit compilatorului să decidă care din aceste funcții este apelată. Diferențele se fac prin semnătura funcției.

Semnătura este formată din următoarele elemente:

  1. Numele funcției
  2. Tipul argumentelor funcției
  3. Ordinea argumentelor funcției
  4. Numărul argumentelor funcției
  5. Alți specificatori (de exemplu const)

Semnătura NU conține și tipul întors de funcție. Prin urmare, funcțiile de mai jos au aceeași semnătură:

int divide (int n, int m); 
double divide (int n, int m);

Constructorul

Când se instanțiază obiecte, câmpurile acestora sunt neinițializate. Pentru a atribui valori acestor câmpuri, putem folosi operatorul ”.” pentru a accesa aceste câmpuri, similar cu sintaxa limbajului C:

int main() {
    Animal firstAnimal;
    firstAnimal.mName = "Dog";
    firstAnimal.mColor = "black";
    firstAnimal.mAge = 2;
    firstAnimal.mHasFeathers = false;
}

Problema este că în cazul în care se dorește instanțierea unui număr mare de obiecte, acest mod de atribuire de valori câmpurilor devine anevoios. În acest sens, limbajele orientate pe obiecte oferă posibilitatea definirii unor metode speciale care se apelează automat în momentul în care se creează obiecte noi, și care se numesc constructori:

Atenție: Constructorul este o metodă specială, definită în interiorul unei clase, care se apelează automat la instanțierea obiectelor, și care are rolul de a inițializa câmpurile din obiect.

În C++, constructorul poate fi recunoscut după cele două particularități:

  1. Are același nume cu numele clasei;
  2. Nu are tip returnat (nici măcar void).

Astfel, putem crea un constructor nou pentru clasa Animal:

class Animal {
    std::string mName;
    std::string mColor;
    uint8_t mAge;
    bool mHasFeathers;
    
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
        mName = name;
        mColor = color;
        mAge = age;
        mHasFeathers = hasFeathers;
    }

    void makeSound() {
        printf("Animal %s makes a sound!\n", mName.c_str());
    }
};

Constructorii, fiind metode în clasă, beneficiază de supraîncărcare, deci pot exista mai mulți constructori într-o clasă, dar cu semnături diferite. Astfel, trebuie să aveți în vedere următoarele reguli:

  1. Atenție: Dacă nu declarați explicit un constructor în clasă, compilatorul va introduce automat un constructor implicit, fără argumente, care nu are niciun efect de inițializare a câmpurilor.
  2. Atenție: Dacă declarați explicit cel puțin un constructor în clasă, compilatorul nu va mai introduce constructorul implicit. Dacă doriți să puteți crea obiecte folosind constructorul fără argumente, trebuie să-l declarați explicit. Pentru a ușura sintaxa, puteți folosi cuvântul cheie default care reprezintă implementarea implicită a constructorului fără argumente
 1class Animal {
 2    std::string mName;
 3    std::string mColor;
 4    uint8_t mAge;
 5    bool mHasFeathers;
 6    
 7    Animal() = default;
 8
 9    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
10        mName = name;
11        mColor = color;
12        mAge = age;
13        mHasFeathers = hasFeathers;
14    }
15
16    void makeSound() {
17        printf("Animal %s makes a sound!\n", mName.c_str());
18    }
19};

Inițializarea și atribuirea câmpurilor de către constructor

În momentul în care se apelează un constructor, acesta trebuie să modifice valorile câmpurilor obiectului. Acest lucru se poate face în două moduri:

  1. Prin atribuire
  2. Prin inițializare
Atribuirea câmpurilor

Varianta cea mai simplă, dar nu și cea mai eficientă, este accea de a atribui valori câmpurilor. Acest lucru este evidențiat în exemplul de mai sus. Totuși, există și posibilitatea ca numele argumentelor constructorului să coincidă cu numele câmpurilor. În această situație, atribuirea devine ambiguă.

Atenție: Când utilizați o variabilă, compilatorul o caută incremental, din blocul de instrucțiuni (scope-ul) cel mai apropiat în exterior. Deci dacă există o variabilă locală și un câmp cu același nume, compilatorul va considera variabila locală ca fiind cea la care vă referiți.

Pentru a rezolva acest conflict de nume, se poate utiliza cuvântul cheie this care reprezintă un pointer la obiectul curent (cel care se construiește de către constructor):

class Animal {
    std::string name;
    std::string color;
    uint8_t age;
    bool hasFeathers;
    
    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) {
        this->name = name;
        this->color = color;
        this->age = age;
        this->hasFeathers = hasFeathers;
    }

    void makeSound() {
        printf("Animal %s makes a sound!\n", name.c_str());
    }
};
Atenție: Cuvântul cheie this poate fi folosit în orice metodă (ne-statică) dintr-o clasă (inclusiv în constructor), și reprezintă pointer la obiectul curent (pentru care s-a apelat metoda respecitvă). Deci poate fi folosit mai departe împreună cu operatorul -> pentru a accesa membrii (câmpurile sau metodele) obiectului.
Sfat: Pentru a evita problemele de conflict de nume între câmpuri și variabile locale, folosiți întotdeauna convenția de a numi câmpurile cu prefixul m: mName, mHasFeathers sau mMember.
Inițializarea câmpurilor

Limbajul C++ pune la dispoziție o sintaxă specială care permite inițializarea câmpurilor în constructor. Acest lucru este esențial atunci când se declară câmpuri constante sau câmpuri de tip referință. Diferența dintre atribuire inițializare este că cea din urmă se face doar la declararea variabilei iar prima se poate face oricând după aceea, atât timp cât variabila nu este declarată constantă.

Sintaxa este evidențiată mai jos:

 1class Animal {
 2    std::string mMame;
 3    std::string mColor;
 4    uint8_t mAge;
 5    bool mHasFeathers;
 6    
 7    Animal(std::string name, std::string color, uint8_t age, bool hasFeather) : mName(name), mColor(color), mAge(age), mHasFeathers(hasFeathers) {
 8
 9    }
10
11    void makeSound() {
12        printf("Animal %s makes a sound!\n", mName.c_str());
13    }
14};

Numele de dinainte de paranteză este cel al câmpului, iar în paranteză este expresia cu care se inițializează acesta.

Modificatori de acces și încapsulare

Membrii unei clase se pot împărți în două mari categorii:

  1. Membri care se expun pentru a fi utilizați de alte obiecte;
  2. Membri care există doar pentru a ajuta la implementarea internă a funcționalității obiectului.

Pentru a permite ascunderea anumitor membri ai unei clase, limbajele orientate pe obiecte oferă conceptul de modificatori de acces. În C++ există trei modificatori de acces, din care doi sunt prezentați mai jos:

  1. public - membrii declarați publici sunt vizibili și accesibili din orice alt obiect de orice tip;
  2. private - membrii declarați privați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv.
  3. protected - membrii declarați protejați nu sunt accesibili decât din obiecte de același tip cu obiectul respectiv sau obiecte de tip clase derivate din clasa respectivă (pentru derivat, vedeți paragraful moștenire).

Se recomandă ca în general toate câmpurile să fie declarate private. În acest fel, este se încapsulează în obiect, iar accesul la ele se face prin metode publice care permit validarea modificărilor sau a citirilor de date din obiect:

class Animal {
private:
    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;
        }
    }
};
Atenție: Una din cele două diferențe între class și struct este că pentru prima, modificatorul de acces implicit este private iar pentru a doua este public. Prin urmare, modificatorul private de la începutul clasei din exemplul de mai sus poate lipsi.