Sari la conținut
EduCamp

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ăsubprogramulobiectul, ca instanță a unei clase
Datele și prelucrărilese declară separat, iar legătura dintre ele se face prin parametri, la fiecare apelse declară împreună, în aceeași clasă
Accesul la dateorice subprogram poate prelucra orice dată vizibilă în punctul apeluluidatele private se folosesc numai prin metodele clasei
Reutilizarea coduluiprin apelul subprogramelor deja scriseprin 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.

SpecificatorulDin metodele claseiDin metodele clasei derivateDin afara clasei
privatedanunu
protecteddadanu
publicdadada

Î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.

Ce cuprinde subcapitolul

  1. POO vs programarea structurată
  2. Clase și obiecte
  3. Membrii unei clase
  4. Niveluri de acces la membrii clasei
  5. Abstractizarea datelor
  6. Încapsularea
  7. Constructori
  8. Destructori
  9. Supraîncărcarea
  10. Derivarea claselor
  11. Polimorfismul
  12. 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ă.

Vei primi mesaje numai despre materia selectată și despre cursul de pregătire. Te poți dezabona oricând, dintr-o singură apăsare.

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
Actualizat: 3 septembrie 2026