Tematică științifică · 2.8
Programarea orientată pe obiecte
Clasa și obiectul, datele și funcțiile membru, specificatorii de acces, principiile programării orientate pe obiecte, constructorii și destructorii, supraîncărcarea, derivarea claselor și metodele virtuale, cu planurile de idei cerute la examen.
POO vs programarea structurată
Programarea orientată pe obiecte se bazează pe clase și obiecte, care țin la un loc datele și operațiile asupra lor; programarea structurată se bazează pe subprograme, iar datele stau separat de codul care le prelucrează.
În programarea structurată, legătura dintre date și subprogramul care le prelucrează se stabilește la fiecare apel, prin parametri. Orice subprogram poate prelucra orice dată vizibilă în punctul apelului, fără vreo restricție legată de rolul pe care acea dată îl are în program.
#include <iostream>
using namespace std;
/* Programare structurata: datele sunt intr-o structura, iar prelucrarile
lor in subprograme separate. */
struct Fractie {
int numarator, numitor;
};
/* Subprogramele stau in afara structurii. Legatura dintre ele si date se
face la fiecare apel, prin parametrul f. */
void citire(Fractie &f) {
cin >> f.numarator >> f.numitor;
}
void afisare(Fractie f) {
cout << f.numarator << "/" << f.numitor << '\n';
}
int main() {
Fractie a;
citire(a);
afisare(a);
/* Nimic nu opreste programul sa scrie direct in campurile structurii,
ocolind subprogramele si orice verificare din ele. */
a.numitor = 0; // se compileaza fara eroare
afisare(a); // 3/0
return 0;
}
Atribuirea a.numitor = 0; se compilează fără eroare, deși o fracție cu numitorul zero nu are
sens. Structura descrie numai forma datelor. Condițiile pe care valorile trebuie să le respecte se
verifică în subprogramele care prelucrează structura, iar o atribuire scrisă direct, ca aceasta,
nu trece prin niciun subprogram.
În programarea orientată pe obiecte (POO), datele și operațiile asupra lor se declară împreună, în aceeași clasă. Programul nu mai este format din subprograme care acționează asupra unor date comune, ci din obiecte care își păstrează fiecare datele proprii și care se prelucrează numai prin operațiile declarate în clasa lor.
#include <iostream>
using namespace std;
/* Programare orientata pe obiecte: datele si prelucrarile lor sunt in
aceeasi clasa. */
class Fractie {
private:
int numarator, numitor; // date membru, inaccesibile din afara clasei
public:
void citire() {
cin >> numarator >> numitor;
if (numitor == 0) // conditia se verifica intr-un singur loc
numitor = 1;
}
void afisare() {
cout << numarator << "/" << numitor << '\n';
}
};
int main() {
Fractie a;
a.citire();
a.afisare();
// a.numitor = 0; eroare de compilare: numitor este membru privat
return 0;
}
Datele și prelucrările sunt aceleași ca în programul de dinainte, dar datele nu mai sunt
accesibile din afara clasei, iar condiția asupra numitorului se scrie o singură dată, în metoda
care citește fracția. Din restul programului, fracția se poate folosi numai prin metodele citire
și afisare.
Deosebirile dintre cele două moduri de a scrie un program:
| Programarea structurată | Programarea orientată pe obiecte | |
|---|---|---|
| Elementul de bază | subprogramul | obiectul, ca instanță a unei clase |
| Datele și prelucrările | se declară separat, iar legătura dintre ele se face prin parametri, la fiecare apel | se declară împreună, în aceeași clasă |
| Accesul la date | orice subprogram poate prelucra orice dată vizibilă în punctul apelului | datele private se folosesc numai prin metodele clasei |
| Reutilizarea codului | prin apelul subprogramelor deja scrise | prin apelul metodelor, la care se adaugă derivarea claselor și metodele virtuale |
Programarea orientată pe obiecte are la bază patru principii, fiecare cu un mecanism propriu în limbajul C++:
- abstractizarea datelor — clasa, ca tip abstract de date;
- încapsularea — specificatorii de acces;
- moștenirea — derivarea claselor;
- polimorfismul — metodele virtuale.
Clase și obiecte
Clasa este un tip de date definit de utilizator, care se folosește la fel ca un tip predefinit. În declararea unei clase se descriu datele care alcătuiesc tipul, împreună cu metodele care le prelucrează. Declararea clasei nu rezervă memorie; ea stabilește numele tipului și membrii lui, la fel cum declararea unui tip structură nu rezervă memorie pentru nicio variabilă.
Obiectul este o variabilă de tipul definit de o clasă și se numește instanță a clasei. Memoria se rezervă la declararea obiectului, iar datele membru există separat pentru fiecare obiect: două obiecte ale aceleiași clase au aceiași membri, cu valori proprii. Metodele există o singură dată și se execută asupra datelor obiectului pentru care sunt apelate.
Forma generală a declarării unei clase în C++:
class nume_clasa {
specificator_de_acces:
lista de membri;
specificator_de_acces:
lista de membri;
};
Semnul punct și virgulă de după acolada de închidere este obligatoriu, ca la declararea unui tip structură. O clasă se poate declara oriunde într-un bloc de program, iar domeniul ei de existență se întinde de la declarație până la sfârșitul blocului.
Exemplu de clasă și de instanțiere a ei:
#include <iostream>
using namespace std;
/* DECLARAREA CLASEI: se stabilesc numele tipului si membrii lui. Aici nu
se rezerva memorie si nu se scrie corpul niciunei metode. */
class Fractie {
private:
int numarator, numitor;
public:
void citire();
void afisare();
double valoare();
};
/* DEFINIREA METODELOR, in exteriorul clasei: numele metodei se prefixeaza
cu numele clasei si cu operatorul de rezolutie ::
In corpul metodei, numarator si numitor se folosesc direct, dupa nume:
sunt datele obiectului pentru care se apeleaza metoda. */
void Fractie::citire() {
cin >> numarator >> numitor;
}
void Fractie::afisare() {
cout << numarator << "/" << numitor;
}
double Fractie::valoare() {
return (double)numarator / numitor;
}
int main() {
/* INSTANTIEREA: abia acum se rezerva memorie, cate o pereche de
numere pentru fiecare obiect. Metodele exista o singura data si se
aplica obiectului scris inaintea punctului. */
Fractie a, b; // doua obiecte, adica doua instante ale clasei Fractie
a.citire(); // citeste in numarator si numitor din a
b.citire(); // citeste in numarator si numitor din b
a.afisare();
cout << " = " << a.valoare() << '\n';
b.afisare();
cout << " = " << b.valoare() << '\n';
return 0;
}
Metodele scurte se pot defini în interiorul clasei; compilatorul le tratează atunci ca funcții
inline și înlocuiește fiecare apel cu corpul funcției. Definirea în exteriorul clasei se face
prefixând numele metodei cu numele clasei și cu operatorul de rezoluție ::. Operatorul de
rezoluție leagă numele metodei de numele clasei căreia îi aparține, astfel încât două clase pot
avea metode cu același nume.
Accesul la un membru se face cu operatorul . pentru un obiect și cu operatorul -> când
obiectul este referit printr-un pointer.
Membrii unei clase
Membrii unei clase sunt datele membru și funcțiile membru:
- datele membru — variabilele declarate în clasă; li se spune și câmpuri sau atribute. Valorile lor la un moment dat formează starea obiectului.
- funcțiile membru — funcțiile declarate în clasă; li se spune și metode. Citirea și modificarea stării obiectului se fac prin metode.
O metodă are acces la toate datele membru ale clasei, oricare ar fi nivelul lor de acces, și le
folosește direct, după nume, fără să le primească prin parametri. La fiecare apel, metoda
primește automat un parametru ascuns, pointerul this, care conține adresa obiectului pentru
care a fost apelată; scrierea numarator în corpul unei metode este forma prescurtată a scrierii
this->numarator.
Datele membru nu se pot inițializa în corpul clasei, la declarare, fiindcă declararea clasei nu rezervă memorie. Inițializarea lor se face prin constructor.
Pe lângă membrii obișnuiți, o clasă poate avea membri statici, declarați cu static. Un
membru static nu aparține unui obiect anume, ci este comun tuturor instanțelor clasei: pentru o
dată membru statică se rezervă o singură zonă de memorie, oricâte obiecte s-ar declara. Un membru
static se folosește prefixat cu numele clasei și cu operatorul de rezoluție, sub forma
nume_clasa::nume_membru, chiar și înainte de declararea vreunui obiect. O dată membru statică se
declară în clasă, dar se inițializează în exteriorul ei:
class Fractie {
private:
/* DATE MEMBRU obisnuite: fiecare obiect are perechea lui, cu valori
proprii. */
int numarator, numitor;
/* DATA MEMBRU STATICA: exista una singura, comuna tuturor obiectelor.
Aici numai se declara. */
static int cate;
public:
/* FUNCTIE MEMBRU STATICA: se apeleaza fara obiect, deci nu primeste
pointerul this si nu poate folosi numarator sau numitor. */
static int numarul() {
return cate;
}
};
/* Initializarea datei membru statice, in exteriorul clasei. */
int Fractie::cate = 0;
Metodele statice nu primesc pointerul this, fiindcă nu se apelează pentru un obiect anume. De
aceea, o metodă statică poate folosi date membru nestatice numai prin intermediul unui obiect
primit ca parametru.
Niveluri de acces la membrii clasei
Nivelul de acces al unui membru se stabilește prin unul dintre specificatorii de acces
private, protected și public, scris urmat de două puncte. Domeniul de acțiune al unui
specificator se întinde din punctul în care este scris până la sfârșitul declarației clasei sau
până la următorul specificator.
| Specificatorul | Din metodele clasei | Din metodele clasei derivate | Din afara clasei |
|---|---|---|---|
private | da | nu | nu |
protected | da | da | nu |
public | da | da | da |
Într-o clasă declarată cu class, membrii scriși înaintea primului specificator sunt privați.
Membrii publici formează interfața clasei: numai prin ei se poate folosi obiectul din restul
programului. Membrii privați formează implementarea clasei, adică reprezentarea internă a
datelor, și se pot folosi numai din metodele clasei. Specificatorul protected se deosebește de
private numai într-o ierarhie de clase; într-o clasă din care nu se derivă alte clase, cele
două au același efect.
O clasă poate declara funcții prietene, cu friend. O funcție prietenă nu este membru al
clasei, dar are acces la membrii ei privați și protejați. Se declară prietenă o funcție obișnuită
care prelucrează obiectele clasei, dar care nu poate fi metodă a ei. Specificatorii de acces nu au
niciun efect asupra unei funcții prietene, deci declarația friend se poate scrie în orice punct
al declarației clasei.
#include <iostream>
using namespace std;
class Fractie {
private:
/* PRIVATE: la acesti membri ajung numai metodele clasei Fractie si
functiile declarate prietene. */
int numarator, numitor;
protected:
/* PROTECTED: ca si private, dar in plus ajung aici si metodele
claselor derivate din Fractie. */
void simplifica() {
int a = numarator, b = numitor;
while (b != 0) { // algoritmul lui Euclid pentru cmmdc
int r = a % b;
a = b;
b = r;
}
if (a != 0) {
numarator = numarator / a;
numitor = numitor / a;
}
}
public:
/* PUBLIC: interfata clasei, adica singurele nume care se pot folosi
din restul programului. */
void atribuie(int a, int b) {
numarator = a;
numitor = (b == 0 ? 1 : b);
simplifica(); // corect: apel din interiorul clasei
}
void afisare() {
cout << numarator << "/" << numitor << '\n';
}
/* Functia de mai jos nu este metoda a clasei, dar are acces la
numarator si numitor, fiindca e declarata prietena. */
friend bool egale(const Fractie &x, const Fractie &y);
};
bool egale(const Fractie &x, const Fractie &y) {
return x.numarator * y.numitor == y.numarator * x.numitor;
}
int main() {
Fractie a, b;
a.atribuie(1, 2); // corect: atribuie este public
b.atribuie(2, 4);
a.afisare(); // 1/2
b.afisare(); // 1/2, dupa simplificare
cout << (egale(a, b) ? "egale" : "diferite") << '\n';
// a.numarator = 5; eroare: numarator este private
// a.simplifica(); eroare: simplifica este protected
return 0;
}
Abstractizarea datelor
Abstractizarea datelor constă în a reține, dintre proprietățile lucrului reprezentat, numai pe cele necesare rezolvării problemei și în a le descrie printr-un tip nou, împreună cu operațiile care se pot executa asupra datelor de acel tip.
Într-un program care calculează media unui elev se rețin numele și notele; data nașterii sau adresa nu intră în program, fiindcă niciun rezultat nu depinde de ele. Proprietățile reținute se stabilesc de la o problemă la alta, nu o dată pentru toate programele.
Tipul obținut astfel se numește tip abstract de date: o mulțime de date care au aceeași reprezentare, împreună cu mulțimea operațiilor care se pot executa asupra acestor date. Clasa este mecanismul cu care limbajul C++ definește un tip abstract de date: datele membru dau reprezentarea, funcțiile membru dau operațiile.
#include <iostream>
#include <cstring>
using namespace std;
/* PROBLEMA: se calculeaza media unui elev.
Despre un elev se pot sti oricat de multe lucruri: data nasterii,
adresa, clasa, numele parintilor. Programul acesta retine numai doua
dintre ele, numele si notele, fiindca media nu depinde de celelalte.
Aceasta alegere este abstractizarea datelor. */
class Elev {
private:
/* REPREZENTAREA tipului: proprietatile pastrate din realitate. */
char nume[40];
float note[20];
int cate; // cate note s-au inregistrat pana acum
public:
/* OPERATIILE tipului: tot ce se poate face cu un elev, in problema
aceasta. Impreuna cu reprezentarea de mai sus, ele formeaza tipul
abstract de date "Elev". */
void initializeaza(const char *n) {
strcpy(nume, n);
cate = 0; // elevul porneste fara nicio nota
}
void adaugaNota(float nota) {
if (cate < 20) // in vector incap cel mult 20 de note
note[cate++] = nota;
}
float media() {
if (cate == 0) // fara note, media nu se poate calcula
return 0;
float suma = 0;
for (int i = 0; i < cate; i++)
suma = suma + note[i];
return suma / cate;
}
void afisare() {
cout << nume << ": media " << media() << '\n';
}
};
int main() {
Elev e; // o data de tipul abstract Elev
e.initializeaza("Ionescu Ana");
e.adaugaNota(9);
e.adaugaNota(10);
e.adaugaNota(8);
e.afisare(); // Ionescu Ana: media 9
return 0;
}
Clasa Elev de mai sus este un tip abstract de date: reprezentarea este dată de nume, note și
cate, iar operațiile, de cele patru metode publice. Într-un program de secretariat, care
tipărește adeverințe, despre același elev s-ar reține alte date, iar operațiile ar fi altele. Prin
abstractizare nu se descrie elevul în întregime, ci numai ceea ce trebuie știut despre el pentru
rezolvarea problemei date.
Încapsularea
Încapsularea este unul dintre principiile fundamentale ale programării orientate pe obiecte și
constă în gruparea datelor și a metodelor care le prelucrează în aceeași clasă și în ascunderea
datelor față de restul programului. Datele membru se declară private sau protected, metodele
care formează interfața se declară public, iar restul programului ajunge la date numai prin
interfață.
Verificările asupra valorilor unei date membru se scriu o singură dată, în metodele care atribuie valori acelei date. Dacă numitorul unei fracții nu poate fi zero, condiția se pune în metoda care îl modifică, iar restul programului nu o poate ocoli, fiindcă nu are acces direct la dată.
class Fractie {
private:
int numarator, numitor;
public:
/* Numitorul se poate modifica numai prin aceasta metoda, iar ea
respinge valoarea zero. */
bool schimbaNumitorul(int x) {
if (x == 0)
return false;
numitor = x;
return true;
}
int numitorul() {
return numitor;
}
};
Reprezentarea internă a datelor se poate schimba fără ca programele care folosesc clasa să fie modificate, atât timp cât interfața rămâne aceeași. O fracție se poate păstra prin numărător și numitor sau prin numărător și inversul numitorului; apelurile din restul programului sunt aceleași în ambele cazuri.
Metodele publice pun astfel la dispoziția restului programului o interfață la datele obiectului: prin ele se citesc și se modifică datele membru, care rămân ascunse.
Constructori
Constructorul este o metodă specială a clasei, care se apelează automat la instanțierea unui
obiect și asigură inițializarea datelor membru. Poartă numele clasei și nu întoarce nicio valoare;
la declararea și la definirea lui nu se scrie niciun tip de rezultat, nici măcar void. Adresa unui
constructor nu este accesibilă programatorului, iar constructorul nu se poate apela pe nume, ca o
metodă obișnuită.
Constructorii se pot supraîncărca, deci o clasă poate avea mai mulți constructori, care se deosebesc prin numărul și tipul parametrilor; compilatorul apelează constructorul potrivit după numărul și tipul argumentelor din instanțiere. Constructorii cu nume consacrate sunt:
- constructorul implicit — cel fără parametri;
- constructorul cu parametri — primește din instanțiere valorile datelor membru, deci obiectul se creează gata inițializat;
- constructorul de copiere — are ca parametru o referință constantă la un obiect de același tip și îi copiază datele membru.
#include <iostream>
using namespace std;
class Fractie {
private:
int numarator, numitor;
public:
Fractie(); // constructorul implicit
Fractie(int a, int b); // constructor cu parametri
Fractie(const Fractie &f); // constructorul de copiere
void afisare();
};
Fractie::Fractie() {
numarator = 0;
numitor = 1;
}
Fractie::Fractie(int a, int b) {
numarator = a;
numitor = (b == 0 ? 1 : b);
}
Fractie::Fractie(const Fractie &f) {
numarator = f.numarator;
numitor = f.numitor;
}
void Fractie::afisare() {
cout << numarator << "/" << numitor << '\n';
}
int main() {
Fractie a; // apeleaza constructorul implicit
Fractie b(3, 4); // apeleaza constructorul cu parametri
Fractie c(b); // apeleaza constructorul de copiere
a.afisare();
b.afisare();
c.afisare();
return 0;
}
Datele membru se pot inițializa și prin lista de inițializare, scrisă între antetul constructorului și corpul lui:
Fractie::Fractie(int a, int b) : numarator(a), numitor(b) { }
Materialul acesta se citește pe educamp.ro și nu se tipărește.
S-a încheiat minutul de citit liber.
Ce cuprinde subcapitolul
- POO vs programarea structurată
- Clase și obiecte
- Membrii unei clase
- Niveluri de acces la membrii clasei
- Abstractizarea datelor
- Încapsularea
- Constructori
- Destructori
- Supraîncărcarea
- Derivarea claselor
- Polimorfismul
- Apariții la examen
Continuă lectura ca și cursant
Cel puțin un subcapitol din fiecare capitol este disponibil gratuit și integral. Pentru a citi toate celelalte subcapitole ale disciplinei, te înscrii la cursul de pregătire.
Cursul cuprinde suportul de curs publicat până acum, care se completează capitol cu capitol, și două întâlniri de câte două ore în fiecare lună. Prima săptămână este gratuită, fără plată și fără card. Dacă vrei să vezi mai întâi cum este prezentată materia, poți reveni la primul subcapitol al capitolului.
Începe săptămâna gratuită Sunt cursant — login
Află când publicăm materiale noi
Materia este publicată treptat, capitol cu capitol. Înscrie-te pentru a primi un e-mail atunci când apare un capitol nou de informatică.
Nu am putut înregistra adresa. Verifică e-mailul și materia aleasă, apoi încearcă din nou.
Surse
- Berian, D., Cocoș, A., „Programare orientată pe obiecte. Îndrumător de laborator”, Facultatea de Automatică și Calculatoare, Universitatea „Politehnica” din Timișoara
- Mușan, M., „Programare orientată obiect”, suport de laborator