Sunteți pe pagina 1din 17

Cap.

2
C++ şi programarea orientată pe obiecte
Deşi ideea programării orientate pe obiecte (POO) a apărut în anii 1960, fiind pusă în practică prin
intermediul limbajelor SIMULA (1967) şi SMALLTALK (1975), totuşi, aceasta a avut o dezvoltare
rapidă odată cu apariţia limbajului C++. De fapt, C++, proiectat de un colectiv condus de Bjarne
Stroustrup, a apărut prin „îmbinarea” caracteristicilor limbajului C cu mecanismele de „modelare“ ale
limbajului SIMULA 67. Primul compilator realizat de Bjarne Stroustrup s-a numit “C with Classes“ ;
prima versiune comercială a acestuia a apărut la AT&T în 1983, cu denumirea modificată în cea
actuală, C++ (sugerată de Rik Masciti, un colaborator apropiat a lui B. Stroustrup). Denumirea de C++
semnifică de fapt multiplele facilităţi adăugate limbajului C.
Scopul acestui capitol este de a face o scurtă trecere în revistă a celor mai importante caracteristici şi
concepte introduse de limbajul C++, fără a se prezenta detaliile sintactice ale acestuia.

2.1. Extensii ale limbajului C în limbajul C++


Limbajul C++ este un supraset al limbajului C. El oferă suport atât pentru programarea procedurală cât
şi pentru abstractizarea datelor, dar caracterisica sa principală este suportul oferit pentru programarea
orientată pe obiecte.
Extensiile limbajului C se referă la două aspecte:
• adăugarea unor facilităţi care nu sunt legate direct de programarea orientată pe obiecte;
• adăugarea elementelor de bază pentru a oferi suport programării orientate pe obiecte.
În primul caz este vorba despre elemente precum: tipul referinţă, substituţia in-line a funcţiilor etc., pe
când în cazul al doilea este vorba despre elemente precum: noţiunea de clasă, moştenire, polimorfism
etc.
În acest paragraf se vor discuta aspectele legate de extensiile limbajului C++ ce nu privesc
programarea orientată pe obiecte, urmând ca în paragraful următor, prin câteva exemple simple, să se
prezinte câteva aspecte legate de programarea orientată pe obiecte.

2.1.1. Noi tipuri de date


Limbajul C++ extinde tipurile de date fundamentale ale limbajului C cu încă două tipuri de date:
bool şi wchar_t. În plus, C++ permite utilizarea a încă unui tip de date numit string, pentru
care a fost creată o clasă string. Declaraţiile acestei clase se află în fişierul string.h. Despre
clasa string se va discuta într-un capitol ulterior.
A) Tipul bool reprezintă valorile logice (booleene), pentru care se utilizează constantele predefinite
true şi false. Există o asemănare din acest punct de vedere cu limbajul Pascal, care are tipul
predefinit Boolean, precum şi cu Java, care are tipul boolean.
Spre deosebire de Pascal însă, există o compatibilitate între tipul bool şi tipurile întregi de date.
Astfel, variabilele de tipul bool pot primi valori de tip întreg, deoarece compilatorul efectuează în
mod automat o conversie de la tipul întreg respectiv la tipul bool. De exemplu, pentru secvenţa:
bool boolVar;
int intVar;
// ...
boolVar = intVar;
se generează o instrucţiue echivalentă de forma:
boolVar = intVar ? true : false;
În mod asemănător, valorile de tip bool pot fi utilizate în locul valorilor de tip întreg, compilatorul
realizând o conversie automată de forma:

2 -1
intVal = boolVal ? 1 : 0 ;
În acest mod există o compatibilitate cu regulile C de evaluare a expresiilor condiţionale din anumite
instrucţiuni precum instrucţiunile repetitive şi instrucţiunea if.
Avantajul utilizării tipului bool constă în faptul că permite scrierea unui cod mai general şi mai
intuitiv decât utilizarea tipului int. De exemplu, pentru funcţia cu prototipul următor:
bool Apartine(double x, double a, double b);
se poate şti faptul că ea returnează o valoare logică, pe când un prototip de forma:
int Apartine(double x, double a, double b);
nu oferă o asemenea certitudine.
B) Tipul wchar_t (wide character) este o extensie a tipului char, care permite utilizarea
mulţimilor de caractere reprezentate intern pe doi octeţi (cum este Unicode). Pentru acest tip,
sizeof(wchar_t) are valoarea 2, ceea ce permite utilizarea a peste 64000 de caractere diferite.

2.1.2. Declararea variabilelor


Spre deosebire de limbajul C, în care declaraţiile locale trebuie făcute doar la începutul unui bloc,
înainte de începutul instrucţiunilor, în limbajul C++ declaraţiile locale pot fi făcute oriunde în cadrul
unui bloc, acolo unde sunt permise instrucţiuni. Domeniul de referire (vizibilitate) al unor astfel de
variabile declarate local reprezintă o parte din blocul în care au fost declarate, începând din linia
declaraţiei respective şi până la sfârşitul blocului curent.
Exemplu.
void main() {
int k = 5 ; // incepe domeniul variabilei k
// ...
k = k + 3 ;
// ...
float x = 7 ; // incepe domeniul variabilei x
// ...
// incepe domeniul variabilei i
for (int i=0 ; i<k ; i++)
printf(″%d″, i) ;
// ...
// se termina domeniul variabilelor k, x si i
}

2.1.3. Referinţe
Unul dintre marile dezavantaje ale limbajului C faţă de alte limbaje apropiate precum Pascal sau Ada
îl constituie modul de transmitere a parametrilor la apelul funcţiilor. Limbajul C permite doar
transferul prin valoare, ceea ce face necesară utilizarea pointerilor în cazul în care o funcţie modifică
valoarea unui anumit parametru.
Limbajul C++ introduce în plus noţiunea de referinţă. O referinţă este un nume alternativ (un alias) al
unei variabile. Un asemenea tip de date este un tip derivat, care se obţine dintr-un tip de bază cu
ajutorul operatorului &. Dacă T este un tip de date, notaţia T& reprezintă tipul referinţă derivat din T.
Valorilor elementelor de tip referinţă sunt asemănătoare pointerilor, în sensul că o referinţă are ca
valoare adresa de memorie a unei variabile ce aparţine unui tip de bază. Există însă câteva deosebiri
importante între pointeri şi referinţe:
a) O referinţă trebuie întotdeauna iniţializată la declarare. De exemplu, declaraţia:
int k ;
int &r = k ;
declară variabila referinţă r având o valoare egală cu adresa de memorie a variabilei k. Această
iniţializare se deosebeşte de atribuire, ea asigurând doar faptul că variabila de bază (k) are un nou
nume (r). În cadrul blocului unde au fost declarate cele două variabile, se poate folosi atât numele k
cât şi r pentru a referi acelaşi obiect.

2 -2
b) În momentul utilizării într-un program, referinţele sunt în mod automat dereferite, nemaifiind
nevoie de operatorul * pentru dereferire (ca în cazul pointerilor). Exemplu:
int k = 5 ;
int &r = k, *p ;
p = &k ; // valoarea lui p = adresa lui k
r = r + 1 ; // aceasta inseamna k = k + 1
*p = *p + 1 ; // aceasta inseamna tot k = k + 1
Cele două observaţii anterioare impun câteva precizări:
• valoarea unei referinţe nu poate fi modificată după iniţializare, ea referind întotdeauna
acelaşi obiect cu care a fost iniţializată. De exemplu, instrucţiunea r=r+1 nu înseamnă că
se adaugă 1 la valoarea lui r, ci la obiectul referit de r;
• operatorii nu acţionează asupra referinţei, ci asupra variabilei la care aceasta face referire.
Principala utilizare a referinţelor o constituie transferul parametrilor funcţiilor. În acest caz, un
parametru formal de tip referinţă reprezintă un alt nume pentru parametrul actual corespunzător în
cazul unui apel. Orice modificare a valorii parametrului formal înseamnă de fapt modificarea valorii
parametrului actual respectiv.
Exemplul 2.1. Interschimbarea a două valori. Funcţia Interschimbare1 utilizează ca parametri formali
pointeri, pe când Interschimbare2 utilizează referinţe.
void Interschimbare1(int *a, int *b) {
int c = *a ;
*a = *b ;
*b = c ;
}
void Interschimbare2(int &a, int &b) {
int c = a ;
a = b ;
b = c ;
}
void main() {
int x = 7, y = 5 ;
Interschimbare1(&x, &y) ; // apelul primei functii
printf(″%d %d\n″, x, y) ;
x = 7 ; y = 5 ;
Interschimbare2(x, y) ; // apelul celei de-a doua functii
printf(″%d %d\n″, x, y) ;
}
În momentul apelului, parametrii formali de tip referinţă se iniţializează cu adresa parametrilor actuali
corespunzători, ceea ce impune ca aceşti parametri actuali să fie nume de variabile cu un tip de date
identic cu tipul de bază al referinţei lor şi nu adrese de memorie.

2.1.4. Funcţii inline


În cazul funcţiilor cu un număr restrâns de instrucţiuni, timpul necesar mecanismului de apel (crearea
cadrelor pe stivă, transferul parametrilor etc.) poate deveni semnificativ în raport cu timpul de execuţie
al funcţiei, ceea ce duce la mărirea timpului de execuţie a programului şi scăderea eficienţei acestuia.
O altenativă o poate constitui folosirea macrodefiniţiilor, care sunt substituite de compilator în etapa
de preprocesare.
Exemplulu 2.2. În secvenţa următoare se utilizează o macrodefiniţie pentru determinarea minimului a
două valori.
#define minim(a, b) ((a < b) ? a : b)
void main()
{

2 -3
int x = 7, y = 5, z ;
z = minim(x, y) ;
// ...
}
Limbajul C++ oferă în plus posibilitatea expandării inline a funcţiilor. Expandarea inline a unei funcţii
înseamnă generarea de către compilator a codului corespunzător funcţiei, fără să se mai genereze o
secvenţă de apel a acesteia. Declararea unei funcţii inline se face prin specificarea cuvântului cheie
inline înaintea definiţiei acesteia, iar pentru funcţiile membre ale unei clase prin includerea
corpului funcţiei în cadrul declaraţiei clasei.
Exemplul 2.3. Funcţia inline minim este echivalentă cu macrodefiniţia anterioară:
inline int minim(int a, int b)
{
return ((a < b) ? a : b) ;
}
În cazul funcţiilor inline, compilatorul încearcă să plaseze o instanţă a funcţiei inline în acelaşi
segment de cod ca şi cel al funcţiei apelante, dar nu se garantează acest lucru. Pentru funcţii
complicate (care conţin instrucţiuni repetitive sau care sunt recursive) nu se realizează mecanismul
inline.
În general, utilizarea funcţiilor inline este mai eficientă decât a funcţiilor obişnuite, dar mai puţin
eficientă decât utilizarea macrodefiniţiilor.
Exemplul 2.4. Pentru exemplificare, se va utiliza o aceeaşi operaţie de ridicare la putere a unor
numere prin trei metode diferite: macrodefiniţie, funcţie inline şi funcţie uzuală. Se va prelua timpul
sistem pentru fiecare din cele trei funcţii (f1, f2, f3), astfel încât să se determine timpul de execuţie
al fiecărei funcţii.
# include <time.h>
# define SQR(x) (x)*(x) // macrodefinitie
inline float Sqr (float x) // functie inline
{ return ( x*x ); }
float sqr (float x) // functie uzuala
{ return ( x*x ); }
void f1() {
float x = 2.5, y;
for (long k=0; k<10000000; k++)
y = SQR(x); // se apeleaza macrodefinitia
}
void f2() {
float x = 2.5, y;
for (long k=0; k<10000000; k++)
y = Sqr(x); // se apeleaza functia inline
}
void f3() {
float x = 2.5, y;
for (long k=0; k<10000000; k++)
y = sqr(x); // se apeleaza functia uzuala
}
void main() {
time_t ltime;
time(&ltime);
printf("Timpul in secunde:\t%ld\n", ltime);
f1();
time(&ltime);

2 -4
printf("Timpul in secunde:\t%ld\n", ltime);
f2();
time(&ltime);
printf("Timpul in secunde:\t%ld\n", ltime);
f3();
time(&ltime);
printf("Timpul in secunde:\t%ld\n", ltime);
}

2.1.5. Argumente implicite ale funcţiilor


În mod uzual, o regulă de bază pentru multe dintre limbajele de programare impune ca la definiţia şi la
apelul unei funcţii să fie utilizat acelaşi număr de parametri. Limbajul C permite definirea (destul de
dificilă) a unor funcţii cu număr variabil de parametri, cu ajutorul operatorului ‘…’. Sarcina de tratare
a parametrilor revine total programatorilor, compilatorul neefectuând nici o verificare.
Limbajul C++ oferă în plus, faţă de limbajul C, o modalitate mai simplă şi mai eficientă de tratare a
funcţiilor cu un număr variabil de parametri: este vorba despre funcţii cu valori implicite pentru
parametri.
Un parametru cu valoare implicită este declarat, în mod obişnuit, în antetul unei funcţii prin nume şi
tip de date, dar în plus el este iniţializat direct în antet. Dacă un apel al funcţiei respective conţine un
parametru actual cu o altă valoare decât cea specificată la iniţializare, valoarea actuală este folosită ca
valoare de inţializare; dacă însă parametrul actual corepunzător lipseşte, ca valoare de iniţializare, se
va considera valoarea specificată în antet.
Exemplul 2.5. Funcţia Distanta poate determina atât distanţa dintre două puncte în plan, cât şi între un
punct şi origine.
double Distanta(double x, double y,
double x0 = 0, double y0 = 0);
{
return sqrt((x-x0)*(x-x0)+(y-y0)*(y-y0)) ;
}
void main() {
double x1 = 3, y1 = 5, x2 = 4, y2 = 6, d1, d2 ;
// distanta intre (x1,y1) si origine
d1 = Distanta(x1, y1);
// distanta intre (x1, y1) si (x2, y2)
d2 = Distanta(x1, y1, x2, y2);
// ...
}
Observaţii:
a) un parametru implicit poate fi iniţializat doar cu o expresie constantă, care se poate evalua la
compilare;
b) se pot specifica mai mulţi parametri impliciţi în cadrul unei funcţii; ei trebuie să ocupe însă
ultimele poziţii, pentru că altfel, în cazul unui apel, nu se pot determina corect parametri
actuali corespunzători.
2.1.6. Funcţii supraîncărcate
Supraîncărcarea numelui funcţiilor înseamnă de fapt posibilitatea existenţei mai multor funcţii cu
acelaşi nume care efectuează operaţii diferite. În exemplul următor se definşte funcţia add utilizată atât
pentru operaţia de adunare a numerelor întregi, reale şi complexe, cât şi pentru operaţia de concatenare
a şirurilor de caractere. Limbajul C++ permite, în plus, definirea unor funcţii utilizator supraîncărcate.
Exemplul 2.6. Se definesc mai multe funcţii cu numele add:
int add(int a, int b) { return a + b ; }
double add(double a, double b) { return a + b ; }
char* add(char *a, char *b) { strcat(a, b) ; return a ; }
struct complex { double re ; double im ; } ;

2 -5
complex add(complex a, complex b) {
complex c ;
c.re = a.re + b.re ;
c.im = a.im + b.im ;
return c ;
}
void main() {
int k = add(5, 1) ;
double s = add(1.5, 8.4) ;
char *s1 = ″abc″, *s2 = ″xyz″, *s3 = add(s1, s2) ;
// ...
}
Compilatorul alege funcţia efectivă ce va fi apelată în funcţie de tipul parametrilor şi de numărul
acestora.
Observaţii:
a) Pentru a defini două funcţii supraîncărcate diferite, dar cu acelaşi nume, acestea trebuie să
difere prin numărul parametrilor sau cel puţin prin tipul de date al unuia dintre parametri.
b) Deoarece nu se verifică şi tipul valorii returnate, două funcţii supraîncărcate nu pot diferi
doar prin tipul valoarii returnate.

2.1.7. Operatori de gestiune a memoriei


În cazul utilizării obiectelor dinamice în cadrul limbajului C, crearea şi distrugerea acestora înseamnă
de fapt alocarea şi dealocarea memoriei pentru acestea. În mod uzual se folosesc funcţiile standard
malloc şi free.
Limbajul C++ posedă în plus doi operatori reprezentaţi prin cuvintele cheie new şi delete. Sintaxa
uzuală de folosire este următoarea:
<nume pointer> = new [(] <tip> [)] ;
delete <nume pointer> ;
Exemple :
double *p = new double ;
double *p = new(double) ;
Se observă superioritatea operatorului new faţă de funcţia malloc, prin faptul că el poate determina
singur cantitatea de memorie ce trebuie alocată, precum şi tipul pointerului ce se returnează.
Operatorul new se poate utiliza şi pentru alocarea memoriei pentru elemente compuse. În cazul
tablourilor, trebuie specificat numărul de componente ce se doreşte alocat.
Exemple:
struct punct { double x, y ; } ;
//se aloca memorie pentru o structura cu 2 componente
struct punct *p = new struct punct(2, 4) ;
//se aloca memorie pentru un tablou cu 10 componente
double *q = new double[10] ;
//se aloca memorie pentru un tablou de 10 pointeri la int
int **pp = new int*[10] ;
Observaţii.
a) Ca şi funcţia malloc, operatorul new alocă memorie în zona heap a programului.
b) În cazul în care tipul de date este o clasă de obiecte, operatorul apelează în mod implicit
constructorul clasei.
Complementarul lui new este delete. Acţiunea efectuată de acesta este asemănătoare cu a funcţiei
free, eliberând zona de memorie spre care indică un anumit pointer. În plus, faţă de free, el oferă
protecţie în cazul încercării eliberării memoriei pentru un pointer NULL.

2 -6
Utilizarea lui delete are un dezavantaj în cazul în care se încearcă dealocarea unui tablou. De
exemplu:
int *p = new int[10];
delete p ;
În acest caz nu se eliberează efectiv decât zona ocupată de primul element al tabloului. Pentru a se
elibera întreaga zonă, trebuie specificat numărul de componente ale tabloului care trebuie eliberate.
Acest număr se specifică la sfârşitul cuvântului delete între paranteze drepte, ca în exemplul
următor:
delete[10] p ;
Pentru a evita eventualele erori ce pot apare în cazul în care se dealocă un alt număr de componente
decât cel alocat, se poate utiliza sintaxa simplificată:
delete[] p ;
caz în care, numărul de elemente dealocate este determinat în mod automat de către compilator.
Operatorul new se poate utiliza şi pentru crearea tablourilor multidimensionale, caz în care trebuie
specificate toate dimensiunile tabloului. De exemplu, expresia:
new int[2][3][4]
creează în memorie două tablouri consecutive de tipul:
int [3][4]
şi returnează un pointer la primul tablou, adică un pointer de tipul:
int (*)[3][4]
Exemple:
int a[2][4] = {1, 2, 3, 4}, (*p)[4];
p = new int[2][4];
for (int i=0; i<2; i++)
for (int j=0; j<2; j++)
p[i][j] = a[i][j];
// ...
delete[] p;
Observaţie. Indiferent de numărul de dimensiuni al unui tablou alocat cu operatorul new, sintaxa
pentru dealocarea lui cu operatorul delete este aceeaşi (o singură pereche de paranteze drepte).
Ca şi new, operatorul delete apelează implicit destructorul unei clase, dacă pointerul asupra căruia
se aplică indică spre o instanţă a unei anumite clase.

2.1.8. Funcţii template


Limbajul C++ oferă suport pentru abstractizarea şi parametrizarea datelor. Principalele noţiuni
adăugate sunt cele de funcţii template şi clase template. Despre aceste noţiuni se va discuta într-un
capitol separat, în acest paragraf se vor specifica doar câteva noţiuni privind funcţiile generice.
O funcţie template conţine cel puţin un tip de date generic (nespecificat), ceea ce măreşte gradul de
generalitate al acesteia. Sintaxa de definire a unei funcţii template impune prezenţa construcţiei:
template ‘<’ class < nume> ‘>’
înainte de antetul funcţiei. În această construcţie, <nume> reprezintă numele tipului de date (sau
clasei) ce este parametru pentru funcţie (a nu se confunda cu parametri formali) şi poate fi utilizat în
corpul funcţiei.
O funcţie template descrie o clasă de funcţii, iar fiecare apel al unei asemenea funcţii reprezintă o
instanţă a funcţiei respective. De regulă, sintaxa pentru instanţierea funcţiei este aceeaşi ca şi pentru
apelul unei funcţii obişnuite. Anumite compilatoare impun specificarea explicită a tipului de date cu
care se realizează instanţierea.
Exemplul 2.7. Funcţia Interschimb interschimbă valorile a doi parametri la care tipul de date este
generic. În funcţia main() se vor apela două instanţe ale acestei funcţii, pentru care tipul generic Tip
este instanţiat la int şi respectiv la double.

2 -7
#include <iostream.h>
template <class Tip>
void Interschimb(Tip & a, Tip & b) {
Tip temp;
temp = b;
b = a;
a = temp;
}
void main() {
int a=3, b=5;
double x=33.3, y=55.5;
Interschimb(a,b);
//Este corect si: Interschimb <int>(a, b);
Cout << a << " " << b << endl;
Interschimb(x,y);
//Este corect si: Interschimb <double>(x, y);
Cout << x << " " << y << endl;
}
2.1.9. Operatori de intrare-ieşire
Ca şi limbajul C, limbajul C++ posedă instrucţiuni specifice pentru operaţiile de intrare şi ieşire. În
afara funcţiilor specifice limbajului C, limbajul C++ pune în plus la dispoziţie două ierarhii de clase
pentru realizarea acestor operaţii. Conceptul de bază în aceste ierarhii este cel de stream, care poate
fi asimilat cu un flux de date între un program şi un dispozitiv periferic.
Operaţiile de intrare-ieşire specifice limbajului C++ se vor descrie într-un capitol separat, în acest
paragraf se vor prezenta doar câteva elemente de bază pentru a putea fi folosite pentru citirea şi
scrierea datelor.
Există două clase importante, numite istream şi ostream, folosite pentru gestionarea operaţiilor
de intrare şi de ieşire; de asemenea există clasa iostream (derivată din istream şi ostream)
folosită pentru ambele tipuri de operaţii. Declaraţiile principalelor clase, o serie de constante şi obiecte
folosite pentru aceste operaţii se află în fişierul header iostream.h. Cele mai utilizate obiecte sunt
următoarele:
a) cin – folosit pentru citirea datelor de la dispozitivul standard de intrare;
b) cout – folosit pentru scrierea datelor la dispozitivul standard de ieşire;
c) cerr - folosit pentru afişarea mesajelor de eroare;
d) clog – utilizat ca şi cerr, dar pentru fiecare mesaj de eroare, zona tampon nu se goleşte.
Pentru realizarea efectivă a operaţiilor de intrare-ieşire s-au definit anumiţi operatori, care reprezintă
supraîncarcărea unor operatori standard ai limbajului C. De exemplu, pentru a citi valori de la
tastatură a fost supraîncărcat operatorul de deplasare dreapta (>>), iar pentru operaţia de scriere a fost
supraîncărcat operatorul <<.
Astfel, pentru a citi o valoare de la tastatură şi a o atribui unei variabile de tip întreg n, se poate scrie:
cin >> n ;
iar pentru a afişa pe display valoarea lui n se poate scrie:
cout << n ;
Aceşti operatori au fost supraîncărcaţi pentru toate tipurile de date predefinite, inclusiv pentru şiruri de
caractere. În acest fel operanzii celor doi operatori pot avea orice tip de date predefinit.
Deoarece operatorii returnează şiruri de date, ei pot fi concatenaţi, astfel încât se pot citi sau scrie mai
multe date cu ajutorul unui singur operator. De exemplu:
int n = 7 ;
double x = 4.5 ;
cout << n << x ;
Exemplul 2.8. Program care determină suma elementelor unui tablou cu valori citite de la tastatură.

2 -8
#include <iostream.h>
void main() {
int n = 10 ;
double s = 0, x[10] ;
cout << ”Introduceti elementele sirului: ” ;
for (int i = 0 ; i < n ; i++) {
cout << ”x[” << i << ”]= ” ;
cin >> x[i] ;
s += x[i] ;
}
cout << endl << ”s = ” << s << endl ;
}
Observaţie. Cuvântul rezervat endl reprezintă caracterul ‘\n’.
Limbajul C++ posedă de asemenea funcţii membru ale claselor de intrare-ieşire ce pot fi utilizate
pentru citirea şi scrierea datelor. De exemplu, funcţiile get (inserează un caracter în şirul de intrare)
şi put (inserează următorul caracter în şirul de ieşire) au următoarele declaraţii:
ostream& put(char c) ;
istream& get(signed char &c) ;
Exemplul 2.9. Program pentru copierea fişierului standard de intrare în fişierul standard de ieşire:
#include <iostream.h>
void main() {
int c ;
while ( (c = cin.get()) != EOF)
cout.put(c) ;
}

2.2. Elemente preliminare privind programarea orientată pe obiecte


În acest paragraf, prin intermediul unor exemple simple, se vor prezenta câteva aspecte legate de
programarea orientată pe obiecte.
2.2.1. Diferenţa dintre clasă şi structură
Listingul programului din Exemplul 2.10 evidenţiază o serie de concepte din C++ cum ar fi: clasă,
structură, constructor, obiect etc.
Exemplul 2.10. Diferenţa dintre clasă şi structură
# include <iostream.h>
# include <stdio.h>
class CLS {
int a, b; // a si b sunt de tip private
public:
CLS (int z = 0) { a = b = z; } // Constructorul clasei CLS
void Imp(char *mesaj = " ") {
printf ("%s a si b = %d %d\n", mesaj, a, b);
}
};
struct STRU {
int a, b; // a si b sunt de tip public aici
STRU (int z = 0) {a = b = z;} // Constructorul structurii STRU
};
// Programul principal
void main (void)
{ CLS ob_c(1); // Se declara obiectul ob_c
STRU ob_s(10); // Se declara obiectul ob_s

2 -9
ob_c.Imp("Dupa creare, datele obiectului ob_c devin: ");
// ob_c.a = 111; ar conduce la eroarea
// CLS :: a is not accessible
cout << "Datele obiectului ob_s inainte de modificare = "\
<< ob_s.a << " " << ob_s.b << endl;
ob_s.a = 100;
ob_s.b = 1;
cout << "si dupa = " << ob_s.a << " " << ob_s.b << endl;
}
Observaţie. În C++ spre deosebire de C, comentariile sunt precedate de “//“. Dar în C++ se acceptă şi
forma comentariului din C: /* text comentariu */.
Construcţia:
class CLS {
int a, b; // a şi b sunt de tip private
public:
CLS (int z = 0) { a = b = z; } // Constructorul clasei CLS
void Imp(char *mesaj = " ") {
printf ("%s a si b = %d %d\n", mesaj, a, b);
}
};
reprezintă declararea clasei CLS. Această construcţie pune în evidenţă un şablon care va servi la
crearea ulterioară a obiectelor.
Obiectul ob_c ce aparţine clasei CLS este declarat în programul principal prin:
CLS ob_c(1);
Expresia ob_c(1) din această instrucţiune instruieşte compilatorul ca să iniţializeze ambele variabile
de tip întreg a şi b cu valoarea 1. Funcţia care este automat apelată la declararea obiectului ca să
realizeze această iniţializare este constructorul clasei CLS, denumit la fel ca şi clasa, CLS. În acest
exemplu nu apare şi funcţia destructor al clasei. În program întâlnim şi linia de comentariu
// ob_c.a = 111; ar conduce la eroarea CLS::a is not accessible
Eroarea menţionată ar apărea pentru că accesul direct la elementele (variabilele) a şi b situate
deasupra cuvântului cheie public este îngrădit, ele fiind de tipul private.
Dacă în funcţia main() am fi întâlnit linia sursă:
CLS ob_c;
ea nu ar fi fost refuzată din punct de vedere sintactic. În acest caz, a şi b ar fi fost iniţializate cu
valoarea asumată 0 (se vede că în interiorul constructorului CLS, z = 0).
Corpul funcţiei CLS este delimitat, ca oricare funcţie din C, de acolade, iar aici, ca unică
instrucţiune se întâlneşte dubla atribuire a = b = z; echivalentă cu instrucţiunile a = z; b =
z;. Deci, în C++, orice linie sursă se termină prin separatorul “;“ ca şi în C.
Programul mai conţine construcţia:
struct STRU {
int a, b; // a şi b sunt de tip public aici
STRU (int z = 0) {a = b = z;} // Constructorul structurii STRU
};
care este şablonul unei structuri cu numele STRU ce conţine două variabile a şi b tot de tip întreg (ca
şi variabilele a şi b din structura CLS) şi o funcţie (constructorul structurii STRU, denumit tot STRU).
Obiectul ob_s creat în linia
STRU ob_s(10);
va iniţializa variabilele a şi b cu valoarea 10. Şi aici, în lipsa unei valori explicite, a şi b vor fi
iniţializate cu valoarea asumată în constructor, 0. De data aceasta variabilele a şi b din structura STRU

2 -10
fiind de tip public sunt accesibile din orice instrucţiune din funcţia main(), fără a apela la
serviciile constructorului. Deci, putem scrie:
ob_s.a = 100;
şi variabila a din obiectul ob_s devine egală cu 100, în loc de 10.
Se observă de asemenea că, atât constructorul clasei CLS cât şi constructorul structurii STRU
sunt definiţi în interiorul acestora, sau altfel spus sunt definiţi în modul inline.
În sfârşit, acest program ne arată şi modalitatea afişării datelor; este vorba de liniile în care
apare cuvântul cheie cout, echivalentul funcţiei printf() din limbajul C, dar mai comod decât
aceasta. Funcţia printf() este acceptată şi în C++ şi păstrează încorsetările din C. De exemplu, în
C, pentru afişarea valorii variabilei întregi j, se recurge la secvenţa:
# include <stdio.h>
// Prototipul functiei printf() este definit în
// fisierul antet stdio.h
...
int j = 10;
printf (“j = %d\n“, j);
Funcţia printf() are două argumente: “j = %d\n“ şi j. Primul argument conţine un şir de
caractere şi un caracter de conversie. Acesta este precedat de caracterul “%“. Când compilatorul
întâlneşte %d acesta este atenţionat că va fi afişat un întreg în format zecimal, în cazul nostru al doilea
argument al funcţiei printf(), adică întregul j. Ieşirea se efectuează la fişierul logic stdout (de
regulă atribuit ecranului videoterminalului).
Dacă j ar fi fost de tipul double (virgulă mobilă, dublă precizie), formatul trebuia modificat
din %d în %f (sau %g). Deci, am fi avut:
...
double j = 10;
printf (“j = %f\n“, j);
În contextul limbajului C++, cout înlocuieşte stdout. Acesta este alcătuit din literalul "Datele
obiectului ob_s inainte de modificare:", urmat de variabila ob_s.a, apoi de un al
doilea literal " " (adică un şir de spaţii), şi în sfârşit de variabila ob_s.b. Dacă a şi b ar fi fost de tip
double, nu mai trebuia schimbat nimic în linia sursă în care apare obiectul cout, membru al clasei
ostream; aceasta, deoarece pentru fiecare tip de informaţie, C++ pune la dispoziţie în contextul
obiectului cout o “metodă“ (funcţie), care analizează şi înţelege mesajul (inclusiv tipurile variabilelor
care trebuie afişate) şi realizează conversiile adecvate. Elementele clasei de ieşire ostream se află în
fişierul antet iostream.h, inclus în exemplul de faţă prin linia:
# include <iostream.h>.
Funcţia membră a clasei. Pentru a afişa variabilele a şi b ale clasei CLS, aceasta conţine funcţia Imp
definită prin:
void Imp(char *mesaj = " ")
{ printf ("%s a si b = %d %d\n", mesaj, a, b); }
şi numită funcţie membru (member) a clasei. Se observă că aceasta este definită în corpul clasei CLS,
deci inline, în porţiunea publică a acestei clase, accesibilă din orice funcţie, inclusiv din funcţia
main(). Cuvântul cheie void din faţa funcţiei Imp arată că aceasta nu întoarce nici un rezultat. De
altfel, din corpul funcţiei Imp lipseşte instrucţiunea return. Prezenţa sa ar fi ilegală, dacă s-a stabilit
că funcţia Imp nu întoarce nici un rezultat. Variabila mesaj este un pointer (notaţie *mesaj) la un
şir de caractere. Formatul listării unui şir de caractere este %s. Şirul asumat, în lipsa unui text atribuit
lui mesaj, este un şir de lungime nulă (şir vid). Variabilele a şi b fiind de tipul întreg, fiecare vor fi
asociate unui format %d. Notaţia \n este caracterul de tip spaţiu alb (white space) numit avans la linie
nouă (line feed, cod 0x0a în hexazecimal sau 10 în zecimal).

2 -11
Funcţiile de tip membru ale unei clase au privilegiul de a avea acces direct la variabilele clasei,
inclusiv la cele de tipul private. Nu a mai fost nevoie de construcţii de genul ob_c.a sau
ob_c.b, ci referirea s-a făcut direct la a sau b. Pentru aflarea valorilor variabilelor a şi b ale
obiectului ob_c, în funcţia main() a fost adăugată linia următoare:
ob_c.Imp("Dupa creare, datele obiectului ob_c devin:");
plasată undeva după linia:
CLS ob_c(1);
O altă variantă a funcţiei Imp() este cea în care nu se mai recurge la serviciile funcţiei printf(),
ci la cout:
void Imp(char *mesaj = " ")
{ cout << mesaj << a << “ “ << b << endl; }
unde cuvântul endl este echivalentul lui \n din C.
Funcţiile “inline“ au avantajul că la apelarea lor nu se mai parcurg secvenţele de salvare/refacere
în/din stivă a parametrilor de intrare, execuţia crescând în rapiditate.

2.2.2. Redefinirea funcţiilor şi operatorilor


Pentru a prezenta alte caracteristici ale limbajului C++, considerăm listingul programului din
Exemplul 2.11.
Exemplul 2.11. O clasă cu constructor, destructor, o funcţie membru redefinită şi o altă funcţie care
redefineşte operatorul || pentru concatenarea unor şiruri
# include <string.h>
# include <stdio.h>
class SIR {
char *text;
public:
SIR(char *sir) ; // Constructorul clasei
~SIR() { delete text; } // Destructorul clasei
int compar(SIR &s1, SIR &s2);
int compar(SIR &s1, SIR &s2, unsigned int ncar);
void operator || (SIR &s) {strcat (text, s.text);}
void Imp(char *mesaj = " ")
{ printf("%s %s\n", mesaj, text); }
};
SIR :: SIR(char *sir) // Definirea constructorului
{
text = new char[strlen(sir)];
strcpy(text, sir);
}
int SIR :: compar(SIR &s1, SIR &s2)
{
return strcmp(s1.text, s2.text);
}
int SIR :: compar(SIR &s1, SIR &s2, unsigned ncar)
{
return strncmp(s1.text, s2.text, ncar);
}
// Programul principal
void main (void)
// Se creeaza 2 obiecte de tip sir
{ SIR sir1("abcd"), sir2("abcdef");

2 -12
sir1.Imp("Primul sir este: ");
sir2.Imp("Al doilea sir este: ");
int rez1, rez2;
rez1 = sir1.compar(sir1, sir2);
printf("Rezultatul primei comparatii este %d\n", rez1);
rez2 = sir1.compar (sir1, sir2, 4);
printf("Rez. celei de-a doua comparatii este %d\n", rez2);
sir1 || sir2;
sir1.Imp("Sir1 dupa concatenare este: ");
}
Programul conţine clasa SIR în care singura dată de tip private este pointerul la un şir de caractere
*text. Constructorul SIR şi funcţia compar() sunt numai declarate în cadrul clasei, pe când
destructorul ~SIR şi funcţia denumită operator||() sunt definite în mod inline. Faptul că
denumirea compar este întâlnită de două ori nu este o eroare. Funcţia compar() este redefinită. Ea
are două şabloane (prototipuri). Primul şablon este utilizat când se compară două şiruri de lungimi
diferite sau nu, iar al doilea când se compară numai primele ncar caractere ale acestora. Notaţiile
SIR &s1, SIR &s2 reprezintă referinţele celor două şiruri ce trebuie comparate.
Funcţia SIR este definită în afara clasei prin:
SIR::SIR(char *sir) // Definirea constructorului
{
text = new char[strlen(sir)];
strcpy(text, sir);
}
în care apare notaţia “::“ prin care se precizează apartenenţa unei funcţii la o anumită clasă.
Instrucţiunea
text = new char[strlen(sir)];
arată că, începând de la adresa text, operatorul new va aloca un număr de caractere egal cu lungimea
efectivă a şirului sir. Lungimea şirului este returnată de funcţia strlen() (string length).
În limbajul C pentru alocarea dinamică a unui spaţiu de memorie se foloseşte funcţia malloc() care
cere specificarea numărului de octeţi. Deci, în C scriem:
text = malloc(strlen(sir));
fapt acceptat şi în C++.
Instrucţiunea strcpy(text, sir) realizează copierea şirului sir în spaţiul alocat la adresa
text. Prototipurile funcţiilor strlen() şi strcpy() (string copy) sunt declarate în fişierul antet
string.h.
Operatorul delete din definiţia destructorului ~SIR este opusul lui new şi are ca efect eliberarea
zonei de memorie ocupată de obiect. În C, opusul funcţiei malloc() este funcţia free(). Deci,
echivalenţa din C a liniei delete sir este linia free(sir).
Funcţia compar() întoarce un rezultat de tip întreg. Celor două prototipuri declarate în clasa SIR le
corespund două definiţii. Primul prototip tratează cazul comparării a două şiruri de lungime oarecare:
int SIR :: compar(SIR &s1, SIR &s2)
{ // Se compară d.p.d.v. lexicografic două şiruri de caractere
return strcmp(s1.text, s2.text);
}
Funcţia strcmp() cu prototip în fişierul antet string.h, întoarce rezultatul comparaţiei a două
şiruri de caractere. Pentru compararea primelor ncar caractere din două şiruri, definiţia este
următoarea:
int SIR :: compar(SIR &s1, SIR &s2, unsigned ncar)
{ // Se compară primele ncar caractere din doua siruri

2 -13
return strncmp(s1.text, s2.text, ncar);
}
Valoarea rezultatului comparaţiei cu funcţiile strcmp() şi strncmp() se obţine prin scăderea
nedistructivă a caracterelor celor două şiruri aflate pe aceeaşi poziţie relativă. Dacă:
s1 < s2, rezultatul < 0
s1 == s2, rezultatul = 0
s1 > s2, rezultatul > 0
Al treilea membru al clasei SIR redefineşte operatorul || (operatorul SAU logic), adică asociază
operatorului || un sens nou. Prin construcţia
void operator || (SIR &s)
se anunţă compilatorul C++ că operatorului || i-a fost temporar asociat sensul descris în corpul funcţiei:
{strcat(text, s.text);}.
Instrucţiunea dintre acolade are ca efect concatenarea a două şiruri. Lista de parametri text, s.text
ascunde două referinţe la şirurile obiectului curent (text), respectiv al obiectului s (s.text). Se
alipeşte deci şirul punctat de către s.text la şirul punctat de pointerul text. Funcţia strcat() al
cărui prototip se află în fişierul string.h nu întoarce nici un rezultat.
În programul principal se creează două obiecte sir1 şi sir2 cu structura sir1 = a b c d \0,
respectiv sir2 = a b c d e f \0. În linia rez1 = sir1.compar(sir1, sir2) se
realizează compararea celor două şiruri de lungime diferită. Se va utiliza prima formă a funcţiei
compar(). Şirurile nefiind identice, funcţia va întoarce în variabila rez1 o valoare negativă. Se
remarcă construcţia:
nume_obiect.nume_metoda(lista_de_argumente).
Linia rez2 = sir1.compar(sir1, sir2, 4) conduce la o comparaţie conform celei de a
doua metode, care se bazează pe funcţia strncmp. Cum, primele 4 poziţii ale celor două şiruri
coincid, în variabila rez2 se va găsi valoarea 0.
În urma concatenării, sir1 se va prelungi, în el regăsind acum valoarea a b c d a b c d e f
’\0’, fapt atestat prin executarea liniei sir1.Imp("Sir1 dupa concatenare este: ").
Şi aici se observă prezenţa construcţiei nume_obiect.nume_metoda (lista_de_argumente) în care
operatorul “punct“ (.) uneşte obiectul cu funcţia apartenentă la clasa care la creat.
2.2.3. Despre moştenire, clase derivate şi reutilizarea codului
Considerăm un program care conţine clasa RAND_UNIF, prin care se generează numere aleatoare cu
o distribuţie uniformă.
Exemplul 2.12. Generarea unor numere aleatoare cu distribuţie uniformă prin metoda liniar
congruenţială
# include <limits.h>
class RAND_UNIF {
long x;
void gen_nr() { x = x*1103515245 + 12345; }
public:
RAND_UNIF(long n = 0) {x = n;} // Constructorul clasei
void seed(long n);
unsigned int ui_unif () {
gen_nr();
return x & LONG_MAX;
}
double d_unif() {
gen_nr();
return (x & LONG_MAX) / (double) LONG_MAX;
}
};

2 -14
// Generarea a 10 numere aleatoare intregi si in virgula mobila,
// care sunt uniform distribuite in gamele [0...LONG_MAX] si [0...1]
# include <stdio.h>
void main (void){
RAND_UNIF aleator; // Se creeaza obiectul aleator
unsigned int nai;
double nad;
for (int i = 0; i < 10; i++) {
nai = aleator.ui_unif ();
nad = aleator.d_unif ();
printf (" i = %d", i);
printf(" Nr. aleat. unif. distrib. = %u %f \n", nai, nad);
}
}
În clasa RAND_UNIF, în secţiunea private, pe lângă variabila x de tip long este încapsulată şi
funcţia gen_nr() de tip inline.
Funcţia membru ui_unif() este definită în modul inline. Ea apelează la serviciile funcţiei
gen_nr(), cu care generează numere întregi fără semn cu relaţia: x = x*1103515245 +
12345. Deoarece prima dată seed() este 0, x va fi egal cu 12345.
Valorii x i se aplică o mască binară LONG_MAX (o constantă definită în fişierul antet limits.h, a
cărei valoare depinde de tipul calculatorului; de exemplu, în cazul unui calculator care permite un
întreg lung pe 32 biţi, LONG_MAX = 231-1). Datorită acestei “măşti“ se asigură tăiere bitului de
semn.
În cadrul funcţiei main() se creează mai întâi obiectul cu numele aleator, după care, utilizând
bucla for se generează primele 10 numere aleatoare.
Să presupunem acum că dorim să generăm numere aleatoare uniform distribuite, însă între două limite
stabilite de noi. Mai mult, nu vrem să modificăm clasa RAND_UNIF, ci să utilizăm atât structura de
date, cât şi codul anterior. De asemenea, se doreşte realizarea unui generator de numere cu o distribuţie
exponenţială în jurul unei valori medii.
Pentru aceasta, vom alcătui două clase “derivate“ din clasa de bază RAND_UNIF. Listingul în care
apar aceste clase denumite R_UNIF_LIM şi R_EXP_LIM este prezentat în Exemplul 2.13.
Exemplul 2.13. Declararea si definirea clasei de bază pentru generarea unor numere aleatoare cu
distributie uniforma prin metoda liniar congruentiala
# include <limits.h>
# include <math.h>
class RAND_UNIF {
long x;
void gen_nr() { x = x*1103515245 + 12345; }
public:
RAND_UNIF(long n = 0) {x = n;} // Constructorul clasei
void seed(long n);
unsigned int ui_unif() {
gen_nr();
return x & LONG_MAX;
}
double d_unif() {
gen_nr();
return (x & LONG_MAX) / (double) LONG_MAX;
}
};

2 -15
// Declararea si definirea claselor derivate
struct R_UNIF_LIM : public RAND_UNIF {
int inf, sup;
R_UNIF_LIM(int i, int s) {inf = i; sup = s;}
unsigned int ui_unif();
};
struct R_EXP_LIM : public RAND_UNIF {
int media;
R_EXP_LIM(int m) { media = m; }
unsigned int ui_unif();
};
unsigned int R_UNIF_LIM :: ui_unif() // Se redefineste ui_unif()
{
return (unsigned int) ((RAND_UNIF::ui_unif() % (sup-inf)) + inf);
}
unsigned int R_EXP_LIM :: ui_unif() // Se redefineste ui_unif()
{
return (unsigned int) (-media*log(1.0-RAND_UNIF::d_unif()) + 0.5);
}
// Generarea a 100 numere aleatoare uniform distribuite in gama
// [INF...SUP] si exponential distribuite in jurul valorii MEDIA
# define INF 10
# define SUP 100
# define MEDIA 500
# include <stdio.h>
void main (void)
{
R_UNIF_LIM n(INF,SUP); // Se creeaza obiectul n
R_EXP_LIM m( MEDIA ); // Se creeaza obiectul m
unsigned int nai;
unsigned int naexp;
for (int i = 0; i < 100; i++) {
nai = n.ui_unif();
naexp = m.ui_unif();
printf (" i =%3d", i);
printf(" Nr. aleat. unif. distrib. si exp. distrib.\
= %3u %4u \n", nai, naexp );
}
}
În acest exemplu se evidenţiază modul declarării unei clase derivate, astfel:
struct R_UNIF_LIM : public RAND_UNIF{ // …… };
Tipul clasei derivate este struct, adică structură. Variabilele inf şi sup ale structurii sunt publice,
deci direct accesibile din program.
Se observă că funcţia ui_unif() va fi reutilizată de către cele două clase derivate, conform
definiţiilor precizate mai sus.
Cele două redefiniri ale funcţiei ui_unif() permit determinarea distribuţiilor menţionate mai sus.
De fapt, pentru determinarea unor numere uniform distribuite în gama [INF...SUP] şi exponenţial
distribuite în jurul valorii MEDIA, am moştenit metoda de determinare a numerelor uniform distribuite
între limitele 0, ..., LONG_MAX şi cu ea am creat două metode noi pentru determinarea distribuţiilor
precizate.

2 -16
2.2.4. Caracteristici ale limbajului C++ - limbaj orientat pe obiecte
1. Clasa este o structură de date abstractă, un şablon, care, pe lângă date, conţine şi funcţii, fapt
neobişnuit în limbajul C;
2. Obiectul este creat de către producătorul (constructorul) clasei conform şablonului acesteia;
3. Mecanismul de ascundere a datelor constă în tehnica de plasare a valorilor în variabilele de tip
private ale obiectului;
4. Moştenirea este proprietatea unui obiect de a prelua anumite proprietăţi ale obiectelor apartenente
unei clase de bază (ierarhic superioare).
5. Obiectele comunică între ele prin mesaje. Funcţiile membre ale clasei (metodele) interpretează
mesajele (valorile argumentelor) şi asigură “comportarea“ corespunzătoare a obiectelor.
Polimorfismul, o noţiune care este corelată cu comportamentul diferit al unei metode în raport cu
tipul obiectului va fi explicat în cursurile următoare.
Constructorul şi destructorul sunt funcţii speciale ale unei clase. Constructorul este automat apelat la
crearea obiectelor, iar destructorul eliberează resursele de memorie ocupate de acel obiect la momentul
încheierii existenţei acelui obiect.

2 -17

S-ar putea să vă placă și