C++ POO Lab Lucrarea 2
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):
Î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ă):
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:
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:
- 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.
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).
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:
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
Mecanism prin care un pointer la clasa de baza poate referi un obiect de tip clasa derivata, si poate accesa metodele acelui obiect de tip clasa derivata.
Avantaje:
- clasa de baza si pointerul de tip clasa de baza pot fi scrise in codul sursa al unei biblioteci compilate mult timp inaintea clasei derivate
- pointerul de tip clasa de baza poate referi orice obiect de tip clasa derivata (si se poate schimba dinamic, la executie)
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