C++ POO Lab Lucrarea 2

De la WikiLabs
Versiunea din 12 aprilie 2022 20:11, autor: Rhobincu (Discuție | contribuții) (Modificatori de acces și încapsulare)
(dif) ← Versiunea anterioară | Versiunea curentă (dif) | Versiunea următoare → (dif)

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
 1 class 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:

 1 class 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.