C++ POO Lab Lucrarea 2

De la WikiLabs

După parcurgerea acestei platforme, veți deveni familiari cu diferite concepte de programare orientată pe obiecte.

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());
    }
};

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;

protected:
    bool mHasFeathers; // accesibil din eventualele clase derivate

public:
    virtual 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;
        }
    }
};

Moștenire

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

Clasa de bază = clasa de la care se preia structura si comportamentul

Clasa derivata = clasa care preia structura si comportamentul

Î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ă)

Avantaje:

- reutilizare cod

- extensie a unei clase deja scrise, fără a fi necesara recompilarea ei

- utilizarea polimorfismului în timpul execuției, prin folosirea funcțiilor virtuale

Exemplu (pentru o clasa mostenita din clasa Animal de mai sus)

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

Relativ la specificatorul de acces declarat intre clasa derivata si clasa de baza (class Pisica : public Animal) avem si posibilitatea de a folosi private:

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

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.

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)

Avantaje:

- 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)

- 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)

#include <iostream>

class Animal {
private:
    std::string mName;
    std::string mColor;
    int mAge;

protected:
    bool mHasFeathers; // accesibil din eventualele clase derivate

public:
    virtual void makeSound() {
        std::cout << "Animal " << mName << " makes a sound!" << std::endl;
    }

    void setName(std::string str) {
        mName = 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;
        }
    }
};

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;
}

Secventa de mai sus afiseaza:

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!

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)

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