Elemente de programare orientată pe obiecte (OOP)
Introducerea paradigmei orientate pe obiecte și a conceptelor de bază ale acesteia.
Bacalaureat
De la proceduri la obiecte: clase și instanțe
Programarea procedurală ține datele într-o parte (variabile, structuri) și prelucrările în alta (funcții). Când programul crește, nimic nu garantează că datele sunt folosite corect: orice funcție poate modifica orice câmp. Programarea orientată pe obiecte rezolvă exact această problemă — leagă datele de operațiile permise asupra lor.
O clasă este un tip de date definit de utilizator: un șablon care descrie ce atribute (date membru) și ce metode (funcții membru) au entitățile de acel fel. Un obiect este o instanță a clasei — o variabilă concretă, cu propriul set de valori pentru atribute.
Confuzia numărul unu la examen: clasa nu este obiect. Clasa e planul de arhitectură, obiectul e casa construită după plan. Definirea clasei nu alocă memorie pentru atribute; alocarea are loc la crearea fiecărui obiect. Din aceeași clasă pot exista mii de obiecte, fiecare cu valori diferite, dar toate cu același cod al metodelor.
În C++ clasa se declară cu class, iar structura cu struct. Diferența dintre ele este una singură și e întrebare sigură: accesul implicit al membrilor este private la class și public la struct. În rest sunt identice — struct poate avea metode, constructori, moștenire.
Fiecare metodă nestatică primește automat pointerul ascuns this, care indică obiectul asupra căruia a fost apelată metoda; de aceea aceeași metodă lucrează pe datele obiectului curent, fără să primească explicit obiectul ca parametru.
Cele patru principii ale paradigmei, în forma cerută la BAC: abstractizarea (reținem doar ce e esențial pentru problemă), încapsularea, moștenirea și polimorfismul.
Încapsularea și modificatorii de acces
Încapsularea înseamnă ascunderea reprezentării interne a unui obiect și expunerea unei interfețe controlate. Practic: atributele se declară private, iar accesul la ele se face doar prin metode publice.
Modificatorii de acces, exact cu efectul lor:
- private — membrul e vizibil doar în interiorul clasei (și pentru funcțiile/clasele declarate friend). Este accesul implicit la class.
- protected — vizibil în clasa proprie și în clasele derivate, dar nu din afară. Există tocmai pentru moștenire.
- public — vizibil de oriunde; formează interfața clasei.
Metodele care doar citesc o valoare privată se numesc accesori (getteri), cele care o modifică mutatori (setteri). Rostul lor nu e birocratic: setterul poate valida datele (o notă între 1 și 10, o vârstă pozitivă), lucru imposibil dacă atributul ar fi public și oricine i-ar putea atribui orice.
O metodă care nu modifică starea obiectului se marchează cu const după lista de parametri — un obiect declarat const poate apela doar astfel de metode.
Membrii static aparțin clasei, nu obiectului: există într-un singur exemplar, comun tuturor instanțelor (tipic: un contor de obiecte create). O metodă statică nu are pointerul this și de aceea nu poate accesa atribute nestatice — capcană frecventă la itemii de teorie.
Greșeala clasică: a crede că private înseamnă „privat pentru fiecare obiect”. Nu — accesul este la nivel de clasă. O metodă a clasei poate citi membrii privați ai oricărui obiect din aceeași clasă primit ca parametru, nu doar ai lui this.
Constructori și destructori
Constructorul este metoda apelată automat la crearea unui obiect. Semne distinctive cerute la examen: are exact numele clasei, nu are tip returnat (nici măcar void) și nu se apelează explicit ca o metodă obișnuită.
Tipuri de constructori:
- implicit (fără parametri) — dacă programatorul nu scrie niciun constructor, compilatorul îl generează. Important: din momentul în care ai scris măcar un constructor cu parametri, cel implicit nu mai este generat, iar o declarație de forma unui obiect fără argumente dă eroare de compilare.
- parametrizat — inițializează atributele cu valori primite; se poate supraîncărca (mai mulți constructori cu semnături diferite).
- de copiere — primește ca parametru o referință constantă la un obiect din aceeași clasă și construiește o copie. Se apelează la inițializarea unui obiect din altul și la transmiterea/returnarea prin valoare. Dacă obiectul conține pointeri și memorie alocată dinamic, copierea implicită (superficială) duplică doar adresa, iar la eliberare aceeași zonă se șterge de două ori — de aceea se scrie un constructor de copiere profundă.
Inițializarea se poate face și prin lista de inițializare, scrisă după două puncte, înaintea corpului constructorului — obligatorie pentru membrii constanți și pentru referințe.
Destructorul are numele clasei precedat de tilda, nu are parametri și nu returnează nimic. Rezultă direct: destructorul nu poate fi supraîncărcat — există unul singur. Se apelează automat la ieșirea obiectului din domeniul de vizibilitate sau la delete, iar rolul lui este eliberarea resurselor (memorie dinamică, fișiere).
Ordinea în ierarhii: la construire se execută întâi constructorul clasei de bază, apoi al celei derivate; la distrugere ordinea este exact inversă.
Moștenirea și polimorfismul
Moștenirea permite definirea unei clase derivate pornind de la o clasă de bază, preluându-i membrii și adăugând sau redefinind ce e specific. Relația se citește „este un”: un Elev este o Persoană.
Ce nu se moștenește, listă cerută la teorie: constructorii, destructorul, operatorul de atribuire și relațiile de friend. Membrii private ai bazei sunt moșteniți, dar nu sunt accesibili direct în derivată — se ajunge la ei doar prin metodele publice sau protected ale bazei. Aici se nasc cele mai multe confuzii: „moștenit” nu e sinonim cu „accesibil”.
Tipul moștenirii schimbă accesul în derivată: la moștenire public, membrii publici rămân publici și cei protected rămân protected; la protected ambele categorii devin protected; la private totul devine private. În C++ moștenirea implicită la class este private, la struct este public.
Polimorfismul înseamnă că același apel se comportă diferit în funcție de tipul real al obiectului. Se distinge:
- polimorfism la compilare (static) — supraîncărcarea funcțiilor și a operatorilor; alegerea se face de compilator, după semnătură;
- polimorfism la execuție (dinamic) — obținut prin funcții virtuale. Dacă o metodă din bază e declarată virtual și e redefinită (suprascrisă) în derivată, atunci un apel prin pointer sau referință la clasa de bază execută versiunea din clasa reală a obiectului, decisă la rulare (legare tardivă).
O metodă pur virtuală (declarată egală cu zero) face clasa abstractă: o clasă abstractă nu poate fi instanțiată, poate fi doar bază pentru altele.
Regulă de siguranță întrebată des: dacă o clasă are funcții virtuale, destructorul trebuie declarat virtual; altfel, la ștergerea prin pointer de bază, destructorul derivatei nu se mai execută și resursele rămân nealiberate.
Supraîncărcarea metodelor și a operatorilor
Supraîncărcarea (overloading) înseamnă mai multe funcții cu același nume, dar cu semnături diferite — număr diferit de parametri sau tipuri diferite. Compilatorul alege varianta potrivită după argumentele apelului.
Capcana clasică: tipul returnat nu face parte din semnătură. Două funcții care diferă doar prin ce returnează nu sunt supraîncărcare validă, ci eroare de compilare. La fel, combinarea neatentă a supraîncărcării cu parametri impliciți poate crea apeluri ambigue.
Nu confunda supraîncărcarea cu suprascrierea (overriding): prima presupune același nume și semnături diferite, în aceeași clasă, rezolvată la compilare; a doua presupune aceeași semnătură, în clasa derivată față de bază, și devine polimorfică la execuție dacă metoda e virtuală.
Supraîncărcarea operatorilor dă operatorilor obișnuiți un înțeles pentru tipurile proprii: adunarea a doi vectori, compararea a două fracții, afișarea unui obiect. Se folosește cuvântul cheie operator urmat de simbolul dorit. Operatorul poate fi definit:
- ca metodă membru — operandul din stânga este obiectul curent, deci operatorul primește cu un parametru mai puțin decât aritatea sa;
- ca funcție externă, de regulă friend — necesară când operandul stâng nu este obiect al clasei, cazul tipic fiind operatorul de inserare în flux pentru afișare.
Restricții de reținut, pentru că apar la itemii de teorie: nu se pot crea operatori noi, nu se pot schimba aritatea, precedența și asociativitatea, iar operatorii de acces la membru prin punct, de rezoluție a domeniului, operatorul condițional ternar și sizeof nu pot fi supraîncărcați. Cel puțin un operand trebuie să fie de tip definit de utilizator — nu poți redefini adunarea a două numere întregi.
De reținut
- clasă
- tip de date definit de utilizator care reunește atribute (date membru) și metode (funcții membru); este un șablon și nu alocă memorie pentru date
- obiect
- instanță concretă a unei clase, cu propriul set de valori pentru atribute, creată automat prin apelul unui constructor
- încapsulare
- ascunderea datelor interne ale unui obiect prin declararea lor private și accesarea lor controlată, prin metode publice
- modificator protected
- nivel de acces care face membrul vizibil în clasa proprie și în clasele derivate, dar nu și din exteriorul ierarhiei
- constructor
- metodă cu numele clasei, fără tip returnat, apelată automat la crearea obiectului pentru a-i inițializa atributele
- constructor de copiere
- constructor care primește o referință constantă la un obiect din aceeași clasă și creează o copie a acestuia
- destructor
- metodă cu numele clasei precedat de tilda, fără parametri și fără tip returnat, apelată automat la distrugerea obiectului; nu poate fi supraîncărcată
- moștenire
- mecanism prin care o clasă derivată preia membrii unei clase de bază; nu se moștenesc constructorii, destructorul, operatorul de atribuire și relațiile friend
- funcție virtuală
- metodă a clasei de bază care, redefinită în derivată, se apelează prin legare tardivă în funcție de tipul real al obiectului indicat
- clasă abstractă
- clasă ce conține cel puțin o metodă pur virtuală și care, din acest motiv, nu poate fi instanțiată
Greșeli frecvente
Greșit: Confundarea clasei cu obiectul — se spune că definirea clasei rezervă memorie pentru atribute
Corect: Clasa este doar un tip (un șablon); memoria pentru atribute se alocă la crearea fiecărui obiect. Codul metodelor este comun tuturor instanțelor
Greșit: Se crede că două funcții care diferă doar prin tipul returnat reprezintă supraîncărcare
Corect: Tipul returnat nu face parte din semnătură; supraîncărcarea cere număr sau tipuri diferite de parametri, altfel rezultă eroare de compilare
Greșit: Supraîncărcarea (overloading) este confundată cu suprascrierea (overriding)
Corect: Overloading: același nume, semnături diferite, rezolvat la compilare. Overriding: aceeași semnătură, în clasa derivată, polimorfic la execuție dacă metoda este virtuală
Greșit: Se afirmă că membrii private ai bazei nu sunt moșteniți de clasa derivată
Corect: Sunt moșteniți, dar nu sunt accesibili direct în derivată; se ajunge la ei doar prin metodele publice sau protected ale clasei de bază
Greșit: Se declară mai mulți destructori sau se apelează destructorul explicit, ca pe o metodă obișnuită
Corect: Destructorul este unic (nu are parametri, deci nu poate fi supraîncărcat) și se apelează automat la ieșirea din domeniu sau la delete
Test — 6 întrebări ca la examen
1. Care este singura diferență dintre class și struct în C++?
- struct nu poate conține metode
- accesul implicit al membrilor: private la class, public la struct
- struct nu poate fi folosită în moștenire
- class nu poate avea constructori
Vezi răspunsul
accesul implicit al membrilor: private la class, public la struct. În C++ struct și class sunt identice funcțional, singura deosebire fiind accesul implicit (private la class, public la struct). Prima variantă e tentantă pentru că vine din limbajul C, unde structurile chiar nu au metode — dar în C++ struct poate avea metode, constructori și poate moșteni.
2. Un obiect este creat, iar apoi programul iese din blocul în care a fost declarat. Ce se apelează automat?
- constructorul de copiere
- un setter implicit
- destructorul
- nimic, memoria se eliberează doar la delete
Vezi răspunsul
destructorul. La ieșirea din domeniul de vizibilitate se apelează automat destructorul obiectului. Ultima variantă este capcana: delete este necesar doar pentru obiectele alocate dinamic, nu pentru cele locale, care se distrug singure.
3. Care dintre următoarele NU se moștenește de la clasa de bază?
- metodele publice
- atributele protected
- constructorii clasei de bază
- metodele const
Vezi răspunsul
constructorii clasei de bază. Constructorii, destructorul, operatorul de atribuire și relațiile friend nu se moștenesc. Atributele protected sunt moștenite și rămân accesibile în derivată, motiv pentru care a doua variantă pare plauzibilă dar este greșită.
4. O clasă conține cel puțin o metodă pur virtuală. Rezultă că:
- clasa este abstractă și nu poate fi instanțiată
- clasa nu poate avea atribute
- toate metodele ei devin virtuale
- clasa nu poate fi moștenită
Vezi răspunsul
clasa este abstractă și nu poate fi instanțiată. O metodă pur virtuală face clasa abstractă, deci nu se pot crea obiecte de acel tip, ci doar din clase derivate care implementează metoda. Ultima variantă e răspunsul opus adevărului: rostul clasei abstracte este exact acela de a fi moștenită.
5. O clasă cu funcții virtuale are un destructor NEvirtual, iar obiectul unei clase derivate este șters printr-un pointer la clasa de bază. Consecința este:
- programul nu compilează
- se apelează doar destructorul clasei de bază, iar resursele derivatei rămân neeliberate
- se apelează ambele destructoare, în ordinea derivată apoi bază
- obiectul nu este distrus deloc
Vezi răspunsul
se apelează doar destructorul clasei de bază, iar resursele derivatei rămân neeliberate. Fără destructor virtual, apelul se leagă static la tipul pointerului, deci se execută doar destructorul bazei și apare pierdere de memorie. A treia variantă descrie comportamentul corect, dar el se obține doar dacă destructorul este declarat virtual.
6. Un obiect conține un pointer către o zonă alocată dinamic. Este copiat cu constructorul de copiere generat implicit. Ce se întâmplă la distrugerea celor două obiecte?
- fiecare obiect eliberează propria zonă, corect
- zona nu se eliberează niciodată
- se copiază doar adresa, iar aceeași zonă se eliberează de două ori
- compilatorul refuză copierea obiectelor cu pointeri
Vezi răspunsul
se copiază doar adresa, iar aceeași zonă se eliberează de două ori. Copierea implicită este superficială: duplică adresa, nu conținutul, astfel încât ambele obiecte indică aceeași zonă, eliberată de două ori la distrugere. Prima variantă ar fi adevărată doar cu un constructor de copiere profundă, scris explicit de programator.
Deschide varianta interactivă — cu AI care îți explică