Clase (I)

Clasele sunt o extindere a conceptului de structuri de date: la fel ca structurile de date, clasele conțin membri, dar ele mai conțin și funcții ca membri.

Un obiect este o instanță a unei clase. Făcând analogia cu variabilele, o clasă este ca un tip de dată, iar un obiect este asemenea unei variabile.

Clasele se definesc cu ajutorul cuvântului cheie class sau folosind cuvântul cheie struct, cu următoarea sintaxă:

class nume_clasă {
  specificator_acces_1:
    membru1;
  specificator_acces_2:
    membru2;
  ...
} denumiri_obiecte;


unde nume_clasă este un identificator valid pentru clasa respectivă, denumiri_obiecte este o listă opțională cu identificatorii obiectelor aparținând acelei clase. Corpul definiției poate conține membri (care pot fi date sau funcții) și, opțional, specificatori de acces.

Clasele au același format ca structurile de date, cu excepția faptului că includ și funcții, precum și noutatea pe care o reprezintă acești specificatori de acces. Un specificator de acces este oricare dintre următoarele trei cuvinte cheie: private, public sau protected. Acești specificatori modifică drepturile de accesare a membrilor, după cum urmează:

  • membrii private ai clasei pot fi accesați numai de către alți membri ai aceleiași clase (sau de către "friends" = prieteni).
  • membrii protected ai clasei pot fi accesați de către alți membri ai aceleiași clase (sau de către "friends" = prieteni), dar și de către membri ai claselor derivate din acea clasă.
  • În sfârșit, membrii public sunt accesibili de oriunde obiectul este vizibil.

In mod implicit, toți membrii unei clase declarate cu cuvântul cheie class au accces de tip private. De aceea, orice membru declarat înainte de orice specificator de acces este privat automat. De exemplu:

1
2
3
4
5
6
class Dreptunghi {
    int latime, inaltime;
  public:
    void seteaza_valori (int,int);
    int aria (void);
} drept;

definește o clasă (adică, un tip de dată) numită Dreptunghi si un obiect (adică, o variabilă) al acestei clase, numit drept. Această clasă conține patru membri: două date de tip int (membrii latime si inaltime) cu acces privat (deoarece private este nivelul de acces implicit) și două funcții membru cu acces public: funcțiile seteaza_valori și aria, pentru care, deocamdată, am inclus numai declarațiile, nu și definițiile lor.

Să observăm diferența dintre numele clasei și numele obiectului: în exemplul anterior, Dreptunghi reprezintă numele clasei (adică, tipul de dată), în timp ce drept este un obiect de tipul Dreptunghi. Avem exact aceeași relație ca între int și a din următoarea declarație:

1
int a;

unde int este tipul de dată (clasa) și a este numele variabilei (obiectul).

După definițiile lui Dreptunghi si drept, oricare dintre membrii publici ai obiectului drept poate fi accesat ca o funcție, respectiv ca o variabilă obișnuită, doar inserând un punct (.) între numele obiectului și numele membrului. Respectă exact sintaxa de accesare a membrilor unei structuri de date obișnuite. De exemplu:

1
2
drept.seteaza_valori (3,4);
aria_mea = drept.aria();

Membrii lui drept care nu pot fi accesați din afara clasei sunt latime și inaltime, deoarece ei au acces privat și pot fi referiți numai de către alți membri ai aceleiași clase.

Iată exemplul complet al clasei Dreptunghi:
// exemplu clase
#include <iostream>
using namespace std;

class Dreptunghi {
    int latime, inaltime;
  public:
    void seteaza_valori (int,int);
    int aria() {return latime*inaltime;}
};

void Dreptunghi::seteaza_valori (int x, int y) {
  latime = x;
  inaltime = y;
}

int main () {
  Dreptunghi drept;
  drept.seteaza_valori (3,4);
  cout << "aria: " << drept.aria();
  return 0;
}
aria: 12
Acest exemplu reintroduce operatorul scope (::, de două ori două puncte), pe care l-am întâlnit în capitolele anterioare referitoare la spații de nume. Aici este folosit în definiția funcției seteaza_valori pentru a defini un membru al clasei din afara clasei.

Să observăm că definiția funcției membru aria a fost inclusă direct în definiția clasei Dreptunghi dată fiind simplitatea ei. În schimb, pentru seteaza_valori s-a declarat doar prototipul în interiorul clasei, iar definiția a fost dată în exterior. În această definiție externă, operatorul scope (::) este folosit pentru a preciza că funcția ce se definește este membră a clasei Dreptunghi și nu o funcție obișnuită nemembră.

Operatorul scope (::) precizează clasa căreia îi aparține membrul ce urmează a fi definit, acordându-i exact același domeniu ca în cazul în care funcția ar fi fost definită direct în interiorul definiției clasei. De exemplu, funcția seteaza_valori din exemplul anterior are acces la variabilele latime și inaltime, care sunt membri privati ai clasei Dreptunghi, fiind accesibili doar de către alți membri ai clasei precum acesta.

Singura diferență dintre definirea unei funcții membru complet în interiorul definiției clasei sau doar includerea declarației si definirea ulterioară în afara clasei constă în faptul că în primul caz funcția este considerată automat de către compilator ca funcție membru inline , în timp ce în a doua situație este o funcție membru obișnuită (not-inline). Aceasta nu are consecințe în comportament, ci poate numai posibile optimizări de compilare.

Membrii latime și inaltime au acces privat (să ne amintim că dacă nu se precizează altceva, toți membrii unei clase definiți cu cuvântul cheie class au nivel de acces privat). Dacă sunt declarați privat, accesul din exteriorul clasei nu este permis. Și chiar are sens, căci am definit deja o funcție membru care să seteze valorile pentru acei membri din interiorul obiectului: funcția seteaza_valori. De aceea, restul programului nu are nevoie de acces direct la ei. Probabil că într-un program așa simplu cum este acesta, este mai greu de înțeles cât de utilă poate fi restricționarea accesului la aceste variabile, dar în proiecte mai mari poate fi foarte important ca valorile să nu poată fi modificate în mod neașteptat (neașteptat din punctul de vedere al obiectului).

Cea mai importantă proprietate a unei clase constă în faptul că reprezintă un tip de dată, adică se pot declara mai multe obiecte de acel tip. De exemplu, urmând exemplul anterior al clasei Dreptunghi, am fi putut declara și obiectul dreptb pe lângă drept:

// exemplu: o clasă, două obiecte
#include <iostream>
using namespace std;

class Dreptunghi {
    int latime, inaltime;
  public:
    void seteaza_valoris (int,int);
    int aria () {return latime*inaltime;}
};

void Dreptunghi::seteaza_valoris (int x, int y) {
  latime = x;
  inaltime = y;
}

int main () {
  Dreptunghi drept, dreptb;
  drept.seteaza_valori (3,4);
  dreptb.seteaza_valori (5,6);
  cout << "aria lui drept: " << drept.aria() << endl;
  cout << "aria lui dreptb: " << dreptb.aria() << endl;
  return 0;
}
aria lui drept: 12
aria lui dreptb: 30  

În acest caz particular, clasa (tipul de obiecte) este Dreptunghi și avem două instanțe (adică obiecte): drept și dreptb. Fiecare dintre ele are propriile variabile membru și funcții membru.

Să remarcăm că apelul drept.aria() nu are același rezultat ca apelul dreptb.aria(). Acest lucru se întâmplă deoarece fiecare obiect al clasei Dreptunghi are propriile sale variabile latime și inaltime, așa cum, într-un fel, au și propriile funcții membru seteaza_valori si aria care operează chiar cu variabilele membru ale obiectului.

Clasele permit programarea bazată pe paradigma orientării pe obiecte: informațiile și funcțiile sunt membre ale obiectului, reducând necesitatea transmiterii și transpotării de handlere sau alte variabile de stare ca parametri ai funcțiilor, deoarece sunt componente ale obiectului al cărui membru a fost apelat. Să observăm că nu a fost transmis niciun argument în apelurile drept.aria și dreptb.aria. Acele funcții membru au folosit direct datele membru ale obiectelor respective, drept și dreptb.

Constructori

Ce s-ar fi întâmplat în exemplul anterior dacă am fi apelat funcția membru aria înainte să fi apelat seteaza_valori? Am fi avut rezultat nedeterminat, deoarece membrilor latime și inaltime nu li s-ar fi atribuit nicio valoare.

Pentru a evita aceasta, o clasă poate include o funcție specială numită constructor, care este apelată automat ori de câte ori este creat un nou obiect din clasa respectivă, funcție care permite clasei să inițializeze membrii sau să aloce spațiu de memorie.

Această functie constructor este definită exact ca o funcție membru obișnuită, dar poartă numele clasei și nu returnează niciun tip, nici chiar void.

Clasa Dreptunghi de mai sus poate fi îmbunătățită ușor prin implementarea unui constructor:

// exemplu: clasă constructor
#include <iostream>
using namespace std;

class Dreptunghi {
    int latime, inaltime;
  public:
    Dreptunghi (int,int);
    int aria () {return (latime*inaltime);}
};

Dreptunghi::Dreptunghi (int a, int b) {
  latime = a;
  inaltime = b;
}

int main () {
  Dreptunghi drept (3,4);
  Dreptunghi dreptb (5,6);
  cout << "aria lui drept: " << drept.aria() << endl;
  cout << "aria lui dreptb: " << dreptb.aria() << endl;
  return 0;
}
aria lui drept: 12
aria lui dreptb: 30  

Rezultatele acestui exemplu sunt identice cu cele ale exemplului anterior. Dar acum, clasa Dreptunghi nu conține funcția membru seteaza_valori, având în schimb un constructor care are rol asemănător: inițializează valorile variabilelor latime și inaltime prin transmiterea lor ca parametri.

Să observăm că parametrii sunt transmiși constructorului în momentul creării obiectelor clasei:

1
2
Dreptunghi drept (3,4);
Dreptunghi dreptb (5,6);

Constructorii nu pot fi apelați explicit, ca orice duncție membru. Ei se execută o singură dată, în momentul în care este creat un nou obiect al clasei respective.

Trebuia să remarcăm că nici prototipul constructorului din definiția clasei, nici definiția lui ulterioară nu returnează valori; nici chiar void: Constructorii niciodată nu returnează valori, ei doar inițializează obiecte.

Supraîncărcarea constructorilor

Ca orice altă funcție, un constructor poate fi supraîncărcat cu diferite versiuni de parametri: număr diferit de parametri și/sau tipuri de date diferite pentru parametri. Compilatorul îl va apela automat pe cel al căror parametri se potrivesc cu argumentele transmise:

// supraîncărcarea constructorilor de clasă
#include <iostream>
using namespace std;

class Dreptunghi {
    int latime, inaltime;
  public:
    Dreptunghi ();
    Dreptunghi (int,int);
    int aria (void) {return (latime*inaltime);}
};

Dreptunghi::Dreptunghi () {
  latime = 5;
  inaltime = 5;
}

Dreptunghi::Dreptunghi (int a, int b) {
  latime = a;
  inaltime = b;
}

int main () {
  Dreptunghi drept (3,4);
  Dreptunghi drepb;
  cout << "aria lui drept: " << drept.aria() << endl;
  cout << "aria lui dreptb: " << dreptb.aria() << endl;
  return 0;
}
aria lui drept: 12
aria lui dreptb: 25  

În exemplul de mai sus, se costruiesc două obiecte drept și dreptb ale clasei Dreptunghi. drept este construit transmițând două argumente, ca în exemplul anterior.

Acest exemplu introduce și un constructor special: constructorul implicit. Constructorul implicit este cedl constructor care nu are niciun parametru și este special pentru că se apelează atunci când se declară un obiect fără să fie inițializat prin vreun argument. În exemplul de mai sus, the constructorul implicit se apelează pentru dreptb. Să observăm că pentru construcția lui dreptb nu s-au pus nici parantezele - de fapt, chiar nu este permisă folosirea parantezelor fârâ parametri la apelul constructorului implicit:

1
2
Dreptunghi dreptb;   // ok, s-a apelat constructorul implicit
Dreptunghi dreptc(); // oops, NU a fost apelat constructorul implicit 

O pereche de paranteze vide ar însemna declararea unei funcții dreptc în loc de declarația unui obiect: ar fi interpretată ca o funcție care nu are argumente și returnează o valoare de tip Dreptunghi.

Inițializare uniformă

Modalitatea de apelare a constructorilor prin includerea argumentelor între paranteze, așa cum am arătat mai sus, se numește forma funcțională. Dar constructorii pot fi apelați folosind și cu alte sintaxe:

În primul rând, constructorii cu un singur parametru pot fi apelați folosind sintaxa de inițializare a unei variabile (un semn egal urmat de argument):

nume_clasă id_obiect = valoare_de_inițializare;

Mai recent, C++ a introdus posibilitatea apelării constructorilor prin inițializarea uniformă, care este, în mare măsură, la fel ca forma funcțională, dar folosește acolade ({}) în loc de paranteze (()):

nume_clasă id_obiect { valoare, valoare, valoare, ... }

Opțional, această ultima sintaxă poate include un semn egal înainte de acolade.

Iată un exemplu cu cele patru modalități de a construi obiecte dintr-o clasă al cărei constructor are un singur parametru:

// clase și inițializarea uniformă
#include <iostream>
using namespace std;

class Cerc {
    double raza;
  public:
    Cerc(double r) { raza = r; }
    double circum() {return 2*raza*3.14159265;}
};

int main () {
  Cerc foo (10.0);   // forma funcțională
  Cerc bar = 20.0;   // inițializare prin atribuire
  Cerc baz {30.0};   // inițializare uniformă
  Cerc qux = {40.0}; // la fel ca POD

  cout << "circumferinta lui foo: " << foo.circum() << '\n';
  return 0;
}
circumferinta lui foo: 62.8319

Un avantaj al initializării uniforme față de forma funcțională este că, spre deosebire de paranteze, acoladele nu pot fi confundate cu declarații de funcții, deci pot fi folosite pentru apelul explicit al constructorilor impliciți:

1
2
3
Dreptunghi drepttb;   // constructor implicit apelat
Dreptunghi dreptc(); // declarație de funcție (NU este apelat constructorul implicit)
Dreptunghi dreptd{}; // constructor implicit apelat 

Alegerea metodei de apelare a constructorilor este mai mult o problemă de stil. Cele mai multe coduri existente folosesc forma funcțională, iar unele ghiduri mai noi sugerează alegerea inițializării uniforme, deși are și ea anumite minusuri privind tipurile de date din lista_de_inițializare.

Inițializarea membrilor în constructori

Când se folosește un constructor pentru a inițializa alți membri, aceștia pot fi inițializați direct, fără a mai recurge la instrucțiuni în corpul lor. Acest lucru se poate face inserând, înaintea corpului cosntructorului, simbolul două puncte (:) și lista de inițializări pentru membrii clasei. De exemplu, să considerăm o clasă cu următoarea declarație:

1
2
3
4
5
6
class Dreptunghi {
    int latime,inaltime;
  public:
    Dreptunghi(int,int);
    int aria() {return latime*inaltime;}
};

Constructorul pentru aceasta clasă ar putea fi definit, ca de obicei, astfel:

1
Dreptunghi::Dreptunghi (int x, int y) { latime=x; inaltime=y; }

Dar se poate defini, de asemenea, folosind inițializarea membrilor astfel:

1
Dreptunghi::Dreptunghi (int x, int y) : latime(x) { inaltime=y; }

Sau chiar:

1
Dreptunghi::Dreptunghi (int x, int y) : latime(x), inaltime(y) { }

Să observăm că în acest ultim caz constructorul nu face altceva decât să inițializeze membrii, deci corpul funcției este vid.

Pentru membrii având tipuri fundamentale, nu are importanță care tip de constructor este definit, deoarece nu se fac inițializări implicite, dar pentru membrii obiecte (acia pentru care tipul de dată este o clasă), dacă nu sunt inițializați după două puncte, atunci vor fi construiți implicit.

Construcția implicită a tuturor membrilor unei clase poate sau nu poate fi convenabilă întotdeauna: în unele cazuri, este o risipă (atunci când membrul este reinițializat ulterior în constructor), dar în alte cazuri, construcția implicită chiar nu este posibilă (când clasa nu are un constructor implicit). În aceste cazuri, membrii ar trebui inițializați în lista de inițializare a membrilor. De exemplu:

// inițializare membru
#include <iostream>
using namespace std;

class Cerc {
    double raza;
  public:
    Cerc(double r) : raza(r) { }
    double aria() {return raza*raza*3.14159265;}
};

class Cilindru {
    Cerc baza;
    double inaltime;
  public:
    Cilindru(double r, double h) : baza (r), inaltime(h) {}
    double volum() {return baza.aria() * inaltime;}
};

int main () {
  Cilindru foo (10,20);

  cout << "volumul lui foo: " << foo.volum() << '\n';
  return 0;
}
volumul lui foo: 6283.19

În acest exemplu, clasa Cilindru are un membru obiect al cărui tip este altă clasă (baza este de tipul Cerc). Deoarece obiectele clasei Cerc pot fi construite doar cu un parametru, constructorul lui Cilindru trebuie să apeleze constructorul lui baza, iar singura cale de a face acest lucrueste prin lista de inițializare a membrilor.

Aceste inițializări pot folosi sintaxa de inițializare uniformă, cu acolade {} în loc de paranteze ():

1
Cilindru::Cilindru (double r, double h) : baza{r}, inaltime{h} { }

Pointeri la clase

Obiectele pot fi și ele referite prin pointeri: odată declarată, o clasă devine un tip de dată valid, deci poate fi folosită ca tip de dată spre care să pointeze un pointer. De exemplu:

1
Dreptunghi * pdrept;

este un pointer către un obiect al clasei Dreptunghi.

Analog structurilor de date obișnuite, membrii unui obiect pot fi accesați direct printr-un pointer folosind operatorul săgeată (->). Iată un exemplu cu câteva posibile combinații:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// exemplu de pointer la clase
#include <iostream>
using namespace std;

class Dreptunghi {
  int latime, inaltime;
public:
  Dreptunghi(int x, int y) : latime(x), inaltime(y) {}
  int aria(void) { return latime * inaltime; }
};


int main() {
  Dreptunghi obj (3, 4);
  Dreptunghi * foo, * bar, * baz;
  foo = &obj;
  bar = new Dreptunghi (5, 6);
  baz = new Dreptunghi[2] { {2,5}, {3,6} };
  cout << "aria lui obj: " << obj.aria() << '\n';
  cout << "aria lui *foo: " << foo->aria() << '\n';
  cout << "aria lui *bar: " << bar->aria() << '\n';
  cout << "aria lui baz[0]:" << baz[0].aria() << '\n';
  cout << "aria lui baz[1]:" << baz[1].aria() << '\n';       
  delete bar;
  delete[] baz;
  return 0;
}	

Acest exemplu folosește câțiva operatori pentru a opera cu obiecte și pointeri (operatorii *, &, ., ->, []). Ei pot fi interpretați astfel:

ExpresiaSe citește
*xpointat de x
&xadresa lui x
x.ymember y of object x
x->ymembrul y al obiectului pointat de x
(*x).ymembrul y al obiectului pointat de x (echivalentă cu anterioara)
x[0]primul obiect pointat de x
x[1]al doilea obiect pointat de x
x[n]al (n+1)-lea obiect pointat de x

Cele mai multe dintre aceste expresii au fost prezentate în capitolele anterioare. Cele mai importante, capitolul referitor la tablouri a prezentat operatorul offset ([]) și capitolul despre structurile de date normale a prezentat operatorul săgeată (->).

Clase definite cu struct și union

Clasle pot fi definite nu numai cu cuvântul cheie class, ci și cu cuvintele cheie struct și union.

Cuvântul cheie struct, folosit în general pentru a declara structuri de date obișnuite, poate fi folosit și pentru declararea de clase care au ca mambri funcții, cu aceeași sintaxă ca în cazul lui class. Singura diferență între cele două constă în faptul că membrii clasei declarate cu struct au acces implcit de tip public, în timp ce membrii claselor declarate cu class au accesul implicit private. În rest, cele două cuvinte cheie sunt echivalent în acest context.

În schimb, conceptul de uniune este diferit de cel de clasă declarată cu struct sau class, căci uniunea stochează o singură dată membru la un moment dat, dar altfel poate conține, ca și clasa, funcții membru. Nivelul implicit de acces pentru o uniune este public.
Index
Index