Un tip de date intr-un limbaj de programare este o reprezentare a unui
concept. De exemplu, tipul float din C++, impreuna cu operatiile definite
asupra acestuia (+, -, *, etc.) reprezinta o versiune a conceptului matematic
de numere reale. Pentru alte concepte, care nu au o reprezentare directa prin
tipurile predefinite ale limbajului, se pot defini noi tipuri de date care sa
specifice aceste concepte. Un program care defineste tipuri de date strans
corelate cu conceptele continute in aplicatie este mai concis, mai usor
de inteles si de modificat. j8s5sm
O clasa este un tip de date definit de utilizator. O declarare a unei clase
defineste un tip nou care reuneste date si functii. Acest tip nou poate fi folosit
pentru a declara obiecte de acest tip, deci un obiect este un exemplar (o instanta)
a unei clase.
Forma generala de declaratie a unei clase care nu mosteneste nici o alta clasa
este urmatoarea:
class nume_clasa A date si functii membre private specificator_de_acces date si functii membre specificator_de_acces date si functii membre
……………………………………………. specificator_de_acces date si functii membre
S lista_obiecte;
Cuvantul-cheie class introduce declaratia clasei (tipului de date) cu
numele nume_clasa. Daca este urmata de corpul clasei (cuprins intre acolade),
aceasta declaratie este totodata o definitie. Daca declaratia contine numai
cuvantul-cheie class si numele clasei, atunci aceasta este doar o declaratie
de nume de clasa.
Corpul clasei contine definitii de date membre ale clasei si definitii sau declaratii
(prototipuri) de functii membre ale clasei, despartite printr-unul sau mai multi
specificatori de acces. Un specificator_acces poate fi unul din cuvintele-cheie
din C++: public private protected
Specificatorii private si protected asigura o protectie de acces la datele sau
functiile membre ale clasei respective, iar specificatorul public permite accesul
la acestea si din afara clasei. Efectul unui specificator de acces dureaza pana
la urmatorul specificator de acces. Implicit, daca nu se declara nici un specificator
de acces, datele sau functiile membre sunt de tip private. De aceea, toate datele
sau functiile declarate de la inceputul blocului clasei pana la
primul specificator de acces sunt de tip private. Intr-o declaratie de
clasa se poate schimba specificatorul de acces ori de cate ori se doreste:
unele declaratii sunt trecute public, dupa care se poate reveni la declaratii
private, etc. Diferenta intre tipurile de acces private si protected consta
in modul in care sunt mostenite drepturile de acces in clase
derivate si va fi detaliat in sectiunea care se ocupa cu clasele derivate
(sectiunea 5).
Declararea unor obiecte de tipul definit de clasa prin lista_obiecte este optionala.
2.1 Date si functii membre ale clasei
Datele declarate intr-o clasa se numesc date membre si, de obicei, ele
sunt protejate (private sau protected), dar exista si situatii in care
sunt declarate public. Nu se pot declara auto, extern sau register datele membre
ale unei clase.
Functiile definite intr-o clasa se numesc functii membre (sau metode ale
clasei) si de obicei ele sunt de tip public, dar pot fi si protejate.
In exemplul urmator se considera definitia unui tip de date pentru reprezentarea
numerelor complexe, clasa Complex.
? Exemplul 2.1
#include <iostream.h> class ComplexA double re; double im; public: void init()A re = 0; im = 0;
S void set(double x, double y)A re = x; im = y;
S void display()A cout << re <<” “ << im << endl;
S
S; void main()A
Complex c1; c1.init(); c1.display(); // afiseaza 0 0 c1.set(7.2, 9.3); c1.display(); // afiseaza 7.2 9.3
S
Clasa Complex contine doua date membre private, re si im de tip double si trei
functii membre public, init(), set() si display().
In functia main() a programului de mai sus s-a declarat un obiect cu numele
c1 de tipul (clasa) Complex. Pentru un obiect dintr-o clasa data se poate apela
orice functie membra a clasei, folosind operatorul punct de acces la un membru
(al unei structuri, uniuni sau clase); deci se poate scrie: c1.init(), acest
lucru insemnand apelul functiei init() pentru obiectul c1 din clasa
Complex.
?
2.1.1 Domeniul clasei. Operatorul de rezolutie
O clasa defineste un tip de date al carui nume este numele clasei, precum
si un domeniu al clasei. In acelasi timp, o clasa care nu este o clasa
locala sau o clasa interna altei clase (acestea sunt descrise in subsectiunea
2.8), are un domeniu de definitie (este cunoscuta in acest domeniu) care
incepe de la prima pozitie dupa incheierea corpului clasei si se
intinde pana la sfarsitul fisierului in care este introdusa
definitia ei si al fisierelor care il includ pe acesta.
Datele si functiile membre ale clasei care nu sunt declarate public au, in
mod implicit, ca domeniu de definitie, domeniul clasei respective, adica sunt
cunoscute si pot fi folosite numai din functiile membre ale clasei.
Datele si functiile membre publice ale clasei au ca domeniu de definitie intreg
domeniul de definitie al clasei, deci pot fi folosite in acest domeniu.
Functiile unei clase pot fi definite in interiorul clasei, asa cum sunt
functiile init()set()si display() ale clasei Complex, sau pot fi declarate in
interiorul clasei (prin declararea prototipului) si definite in exteriorul
ei.
Pentru definirea unei functii in afara clasei (dar, bineinteles
in domeniul ei de definitie) numele functiei trebuie sa fie calificat
(insotit) de numele clasei respective prin operatorul de rezolutie (::).
Sintaxa de definire a unei functii in exteriorul clasei este urmatoarea:
tip_returnat nume_clasa::nume_functie(lista_argumente)A
// corpul functiei
S
In domeniul de definitie al unei clase se pot crea obiecte (instante)
ale clasei. Fiecare obiect contine cate o copie individuala a fiecarei
variabile a clasei respective (daca nu este de tip static; acest caz va fi descris
intr-o subsectiune urmatoare) si pentru fiecare obiect se poate apela
orice functie membra publica a clasei.
Accesul la datele membre publice sau apelul functiilor membre publice ale unui
obiect se poate face folosind un operator de selectie membru: operatorul punct
(.) daca se cunoaste obiectul, sau operatorul -> daca se cunoaste pointerul
la obiect.
Datele si functiile membre protejate ale clasei (private sau protected) au ca
domeniu de definitie domeniul clasei respective si de aceea nu pot fi accesate
decat prin functiile membre ale clasei.
Mai trebuie remarcat inca un aspect referitor la domeniul de definitie
al claselor.
Domeniul in care pot fi definite obiecte de tipul unei clasei este domeniul
in care este definita clasa. Inainte de definitia clasei nu se pot
defini sau declara obiecte din acea clasa. Acest lucru inseamna, implicit,
ca nici in corpul unei clase nu se pot declara obiecte de tipul aceleiasi
clase (deoarece nu s-a completat definitia clasei).
Numele unei clase se poate declara (sau redeclara) folosind constructia: class nume_clasa;
Intr-un domeniu in care a fost declarat numele unei clase se pot
declara pointeri la clasa respectiva. De asemenea, in corpul unei clasei
se pot declara pointeri sau referinte la aceeasi clasa.
In exemplul urmator sunt prezentate comentat mai multe situatii referitoare
la declaratii si definitii de clase, obiecte, date si functii membre ale acestora.
Erorile sunt specificate chiar cu mesajul produs de compilator.
? Exemplul 2.2
#include <iostream.h>
…………………………………. void K::set(int x)A // error:'K':is not a class
// or namespace name a = x;
S class K; // declaratie nume clasa K
K* pob1; // corect, pointer la K
K pb2; // error: 'pb2' uses undefined class 'K' class KA int a; // a este membru privat al clasei
K k; //error:'k':uses'K'which is being defined
K* pobk; // corect, pointer la K public: int b; // b este membru public al clasei
K()AS
K(K& r); // corect, contine o referinta la K int get();// declaratie (prototip) functie membra void set(int x);
S;
// corect: definitii functii membre in
// domeniul de definitie al clasei int K::get()A return a;
S void K::set(int x)A a = x;
S
K pob3; // corect, clasa K este definita void fk()A
K ob4;
K ob5;
K* pob5 = &ob5; ob4.b = 5; // corect, b este membru public pob5->b = 6;
ob4.a = 2; // error:'a' : cannot access
// private member in class 'K' ob4.set(2); // corect, functie publica pob5->get();
S
?
2.1.2 Pointerul this. Functii membre de tip const
Orice functie membra apelata pentru un obiect al unei clase primeste un argument
ascuns, pointerul la obiectul pentru care a fost invocata functia, numit pointerul
this. Intr-o clasa X pointerul constant this este declarat implicit astfel:
X* const this;
Deoarece this este cuvant cheie, el nu poate fi declarat explicit. De
asemenea, fiind declarat implicit pointer constant, el nu poate fi modificat,
dar poate fi folosit explicit.
In exemplul de mai sus, functiei init() i se transmite implicit pointerul
la obiectul c1, cu numele this. Prin intermediul acestui pointer functia acceseaza
datele membre ale obiectului c1 (instanta a clasei Complex). Asignarile din
functia init() Are = 0; im = 0;S sunt asignari ale datelor membre ale obiectului
c1 accesate prin intermediul pointerului la acesta (cu numele this), primit
ca argument implicit la apelul functiei. Acest lucru s-ar putea scrie mai explicit
astfel:
void init()A this->re = x; this->im = y;
S
Dar, odata acest mecanism stabilit si cunoscut, nu mai este nevoie sa fie
scris de fiecare data, deci nu se va intalni niciodata o astfel
de utilizare a pointerului this. In schimb, pointerul this este folosit
in functii membre care manevreaza pointeri.
In general, la apelul unei functii in care un argument este de tip
pointer la constanta sau referinta la constanta, se interzice modificarea obiectului
indicat sau referit astfel. Dar, pentru functiile membre ale claselor, folosirea
unui pointer sau referinta la constanta necesita conditii suplimentare fata
de folosirea acestora in functii nemembre. De exemplu:
#include <iostream.h> class UA int u; public: int get() Areturn u;S void set(int x) Au = x;S
S; void fu1(const U* pu, int i)A pu->set(i); pu->get();
S void fu2(const U& r, int i)A r.set(i); r.get();
S void main()A
U ob; fu1(&ob,2); fu2(ob,3);
S
La compilarea acestui program se obtine de patru ori urmatorul mesaj de eroare
de compilare: 'get' : cannot convert 'this' pointer from 'const class U *' to
'class U *const'. Specificatorul const pentru argumentul formal de tip pointer
(respectiv referinta) la clasa U ale celor doua functii fu1() si fu2(), transforma
tipul pointerului this transmis implicit acestor functii membre nestatice in
“pointer constant la constanta”, adica el are forma: const U* const
this si se interzice accesul functiilor membre (in acest caz, functiile
get() si set()) la obiectul indicat.
Dar, intentia cu care se utilizeaza argumente de tip pointer (sau referinta)
la constanta in apelul functiilor este de a interzice modificarea obiectului,
nu de a interzice accesul complet la acesta. Suportul oferit de limbajul C++
pentru rezolvarea acestei probleme este de a declara de tip const acele functii
care au dreptul de acces la un obiect indicat prin pointer sau referinta la
constanta.
Pentru clasa U, se poate declara de tip const functia get() si atunci obiectul
poate fi accesat numai pentru citire atunci cand este transmis ca argument
pointer (sau referinta) la constanta. Programul modificat arata astfel:
#include <iostream.h> class UA int u; public: int get() const Areturn u;S void set(int x) Au = x;S
S; void fu1(const U* ps, U* pd)A int i = ps->get(); // corect, get()constA….S
// poate accesa ob. const U* ps ps->set(i); // eroare, set() A….S nu poate
// accesa obiectul const U* ps pd->set(i); // corect, set() A…S poate
// accesa obiectul U* pd i = pd->get(); // corect get()constA….S
// poate accesa obiectul U* pd
S void main()A
U ob1, ob2; fu1(&ob1,&ob2);
S
Se poate observa ca functia membra get()const A...S poate accesa atat
un obiect dat prin pointer la constanta (ps), cat si un obiect dat prin
pointer normal (pd), in timp ce functia membra set() A…S nu poate
accesa decat obiecte date prin pointer normal (in sensul ca nu este
pointer la constanta).
2.1.3 Functii membre inline
Im programarea folosind clase, se obisnuieste sa fie definite si apelate
multe functii mici (cu numar redus de instructiuni), si acest lucru poate produce
un cost ridicat de executie, datorita operatiilor necesare pentru rezervarea
spatiului in stiva necesar functiei, apoi pentru transferul argumentelor
si returnarea unei valori. De multe ori este posibil ca aceste operatii implicate
in apelul unei functii sa depaseasca timpul de executie util al functiei.
Acesta problema se rezolva prin intermediul functiilor inline.
In general, o functie declarata inline se schimba la compilare cu corpul
ei, si se spune ca apelul functiei se realizeaza prin expandare. In felul
acesta se elimina operatiile suplimentare de apel si revenire din functie. Dezavantajul
functiilor inline este acela ca produc cresterea dimensiunlor programului compilat,
de aceea se recomanda a fi utilizate pentru functii de dimensiuni mici (maximum
3-4 instructiuni). In plus, mai exista si unele restructii privind functiile
inline: ele nu pot fi declarate functii externe, deci nu pot fi utilizate decat
in modulul de program in care au fost definite si nu sunt admise
instructini ciclice (while, for, do-while). Atributul inline poate fi neglijat
de compilator daca functia nu poate fi tratata astfel.
O functie membra a unei clase definita (nu doar declarata) in interiorul
clasei este implicit functie inline. Acest lucru inseamna ca, de exemplu,
functia init() din clasa Complex este implicit inline. O functie membra definita
in afara clasei este implicit o functie normala (nu este inline). Dar
si o astfel de functie poate fi declarata explicit inline . De exemplu, functia
set() din clasa K:
inline void K::set(int x)A a = x;
S
Aceasta posibilitate de definire a functiilor inline in implementarea
claselor face ca numeroase apeluri de functii sa nu produca un cost suplimentar,
asa cum, in mod eronat, se considera uneori. In toate instructiunile
programului din Exemplul 2.1 descris mai sus, apelurile de functii se executa
prin expandare si deci nu produc cost suplimentar.
2.1.4 Pointeri la date si functii membre
Este posibil accesul la un membru al unei clase printr-un pointer care memoreaza
adresa acelui membru. Un pointer la un membru poate fi obtinut prin aplicarea
operatorului adresa & numelui acestui membru calificat cu numele clasei;
de exemplu &X::m, pentru membrul m al clasei X. Se vor preciza modurile
de definire si utilizare a pointerilor la membrii claselor in exemplul
urmator.
? Exemplul 2.3
Se defineste o clasa W se acceseaza prin pointeri datele si functiile membre
ale clasei W astfel:
#include <iostream.h> class WA int a; public: int b; void seta(int x)Aa = x;S void setb(int x)Ab = x;S int geta()Areturn a;S int getb()Areturn b;S
S; void main ()A
W ob1, ob2; ob1.setb(5); int W::*pdm; // pdm este un pointer la o data
// membra de tip int a clasei W
W* pw = &ob2; pw->setb(6); pdm = &W::a; // eroare, a este privat pdm = &W::b; // pdm indica data b a clasei W cout << ob1.*pdm << “ ” ; cout << pw->*pdm << endl; // afiseaza 5 6
void (W::*pfm)(int);// pfm este pointer la o
// functie membra a clasei W cu
// argument int si return void pfm = &W::seta; // pfm indica functia seta
(ob1.*pfm)(7); // ob1.a = 7
(pw->*pfm)(10); // ob2.a = 10 pfm = &W::setb; // pfm indica functia setb
(ob1.*pfm)(8); // ob1.b = 8
(pw->*pfm)(11); // ob2.b = 11 cout << ob1.geta() << " "; cout << ob2.getb() << endl; // afiseaza 7 8 cout << ob1.geta() << " "; cout << ob2.getb() << endl; // afiseaza 10 11
S
Prin declaratia int W::*pdm; pointerul pdm este definit ca un tip de “pointer
la o data de tip intreg a clasei W”. Un pointer definit ca tip printr-o
astfel de declaratie, poate indica (poate primi adresa) oricare data membra
de tip intreg a clasei W care nu este protejata. Din acest moment, pointerul
poate fi folosit prin operatorul de selectie membru .* pentru un obiect din
clasa respectiva (ob1.*pdm), sau prin operatorul de selectie ->* pentru un
pointer la un obiect din clasa respectiva (pw->*pdm).
Prin declaratia void (W::*pfm)(int); se defineste pointerul pdf ca un tip de
“pointer la o functie membra a clasei W care are un argument de apel de
tip int si returneaza un void”. Dupa aceasta definitie, pointerul pfm
poate primi adresa oricarei functii care indeplineste conditia data (este
o functie membra a clasei W, are un argument de apel de tip int si returneaza
un void). De exemplu, poate primi adresa functiei seta() prin asignarea: pfm
= &W::seta; Din acest moment, functia seta() poate fi apelata prin pointerul
sau pfm folosind operatorul de selectie .* pentru un obiect din clasa W ((ob1.*pfm)(7);
) sau operatorul de selectie ->* pentru un pointer la un obiect din clasa
W ((pw->*pfm)(10);).
2.1.5 Incapsularea datelor
Aparent, accesul la datele sau functiile membre ale unei clase s-ar putea
rezolva simplu prin declararea de tip public a datelor care se doresc a fi accesate
din orice punct al domeniului de definitie al clasei. Intr-adevar, urmatoarea
implementare este posibila:
class ComplexA public: double re; double im;
//…………
S; void fc1()A
Complex c1; c1.re = 5.6; // nu apare eroare de compilare c1.im = 7.9;
S
Dar o astfel de implementare nu respecta principiul incapsularii datelor
si este recomandat sa fie evitata.
Problema ce inseamna o clasa bine definita are mai multe aspecte. Din
punct de vedere al dreptului de acces la membrii clasei, o clasa bine definita
permite incapsularea (sau ascunderea informatiilor), prin care un obiect
poate ascunde celor care-l folosesc “secretele” sale, adica modul
de implementare, prin interzicerea accesului la datele si functiile private
sau protected.
In general, un obiect (instanta a unei clase) are o stare, data de totalitatea
variabilelor sale, si o comportare, reprezentata de functiile pe care le poate
executa. Starea unui obiect variaza in cursul existentei acestuia si depinde
de desfasurarea in timp a tuturor functiilor pe care le-a executat.
Incapsularea prevede o bariera explicita in calea accesului la starea
unui obiect. Conform acestui principiu al incapsularii, asupra unui obiect
se poate actiona numai prin functiile pe care acesta le pune la dispozitie in
interfata si care sunt de tip public.
Incapsularea este procesul de separare a elementelor unui tip de date
abstract (clasa) in doua parti: structura, data de implementarea acestuia,
si comportarea sa, accesata prin interfata. Implementarea consta din definirea
datelor si a functiilor membru, iar interfata consta din declaratiile functiilor
membru de tip public.
Ascunderea informatiilor este conceputa in C++ pentru prevenirea accidentelor
nu a fraudelor. Nici un limbaj de programare nu poate interzice unei persoane
sa “vada” implementarea unei clase, dar poate interzice unei functii
din program sa citeasca date la care nu are dreptul de acces. (Un sisteme de
operare poate, totusi, sa interzica accesul de citire la unele fisiere, deci
asccunderea sa fie reala, chiar pentru persoane, nu numai pentru functii din
program.)
Revenind la modul in care se pot accesa datele membre ale unei clase,
se poate remarca ca, in general, respectand principiul incapsularii,
datele membre sunt declarate private sau protected si nu pot fi accesate direct
(pentru citire sau scriere) din functii nemembru ale clasei care nu sunt de
tip friend (sau nu apartin unei clase friend a clasei respective). Pentru citirea
sau modificarea unora dintre datele membre protejate in clasa respectiva
se pot prevedea functii membre de tip public, care pot fi apelate din orice
punct al domeniului de definitie al clasei si fac parte din interfata clasei.
De exemplu, pentru clasa Complex, o implementare care respecta principiul incapsularii,
dar, in acelasi timp permite accesul la datele private ale clasei poate
arata astfel:
#include <iostream.h> class ComplexA double re; double im; public: void init()A re = 0; im = 0;
S void set(double x, double y)A re = x; im = y;
S void setre(double x)Are = x;S void setim(double y) Aim = y;S double getre()Areturn re;S double getim()Areturn im;S void display();
S; inline void Complex::display()A cout << re << “ ” << im << endl;
S void main()A
Complex c1; c1.set(7.2, 9.3); c1.display(); // afiseaza 7.2 9.3 c1.setre(1.3); c1.setim(2.8); c1.display(); // afiseaza 1.3 2.8
S
Datele membre ale clasei (re si im) sunt date de tip private, iar accesul la
acestea este posibil prin intermediul functiilor membre publice set(), setre(),
setim(), etc.
Intr-o astfel de implementare, in care majoritatea functiilor sunt
inline (este posibil in acest caz simplu, cu functii de dimensiuni mici)
nu apar decat apeluri de functii prin expandate, deci implementarea este
atat eleganta cat si eficienta.
2.1.6 Date si functii membre de tip static
O data membra a unei clase poate fi declarata static in declaratia clasei.
Va exista o singura copie a unei date de tip static, care nu apartine nici unuia
dintre obiectele (instantele) clasei, dar este partajata de toate acestea. Declaratia
unei date de tip static intr-o clasa este doar o declaratie, nu o definitie
si este necesara definitia acesteia in alta parte in program (in
afara clasei). Aceasta se face redeclarand variabila de tip static folosind
operatorul de rezolutie pentru clasa careia ii apartine, si fara specificatorul
static.
O variabila membru de tip static a unei clase exista inainte de a fi creat
un obiect din clasa respectiva si este initializata cu 0. Cea mai frecventa
utilizare a datelor membre statice este de a asigura accesul la o resursa comuna
mai multor obiecte, deci pot inlocui variabilele globale.
? Exemplul 2.4
Se considera o clasa S care contine o variabila normala v si o variabila statica
s. Data de tip static este declarata in clasa S si definita in afara
acesteia, folosind operatorul de rezolutie. Se poate urmari evolutia diferita
a celor doua variabile v si s prin crearea a doua obiecte x si y de tip S, si
prin aplelul functiilor incs() si incv() pentru obiectul x astfel:
class SA int v; static int s; // declaratia var. statice s public:
S() A v = 0;S int gets() Areturn s;S int getv() Areturn v;S void incs() As++;S void incv() Av++;S
S; int S::s; // definitia var. statice s a clasei S
void main ()A cout << “Inainte de incrementare\n”; cout <<“x.s: “<<x.gets()<<“y.s: “<<y.gets()<<
endl; cout <<“x.v: “<<x.getv()<<“y.v: “<<y.getv()<<
endl; x.incs(); x.incv(); cout << “Dupa incrementare\n”; cout <<“x.s: “<<x.gets()<<“y.s: “<<y.gets()<<
endl; cout <<“x.v: “<<x.getv()<<“y.v: “<<y.getv()<<
endl;
S
La executia acestui program se afiseaza continutul variabilelor s si v pentru
obiectele x si y. Mesajele afisate sunt urmatoarele:
Inainte de incrementare x.s: 0 y.s: 0 x.v: 0 y.v: 0
Dupa incrementare x.s: 1 y.s: 1 x.v: 1 y.v: 0
Diferenta intre comportarea unei date membre de tip static fata de o
data normala este evidenta: dupa incrementarea variabilei s pentru obiectul
x, ambele obiecte x si y vad aceeasi valoare a variabilei statice s.
?
Functiile membre ale unei clase pot fi, de asemenea, declarate de tip static.
La fel ca si datele membre statice, o functie membra de tip static se declara
in interiorul clasei si se defineste in afara acesteia, folosind
operatorul de rezolutie pentru clasa respectiva. O functie membra statica are
vizibilitatea limitata la fisierul in care a fost definita si este independenta
de instantele (obiectele) clasei. Fiind independenta de obiectele clasei, o
functie statica nu primeste pointerul this, chiar daca apelul se face pentru
un obiect al clasei respective.
Fie, de exemplu clasa Task care contine o data membra statica si o functie membra
statica:
class TaskA static Task *chain; // declaratie data statica public: static void schedule(int);// decl. functie statica
S;
Task *Task::chain=0; // definirea datei statice void Task::schedule(int p)A // definire func. statica
Task::chain = 0;
S void fs2()A
Task T1;
T1.schedule(4);
Task::schedule(2);
S
Apelul unei functii statice se poate face fie ca functie membra a unui obiect
din clasa respectiva, asa cum apare in primul apel din functia fs2().
In aceasta situatie se foloseste doar tipul obiectului T1 pentru apel,
nu obiectul in sine, si nici pointerul acestuia nu este transmis implicit
(ca un pointer this) functiei schedule(). Compilatorul chiar da un mesaj de
atentionare (warning): 'T1' : unreferenced local variable.
Alta restrictie referitoare la functiile membre statice este aceea ca ele nu
pot avea acces decat la datele statice ale clasei si la datele si functiile
globale ale programului.
Se poate remarca faptul ca specificatorul static are in C++, ca si in
C, doua semnificatii: aceea de vizibilitate restrictionata la nivelul fisierului
in care sunt definite si aceea de alocare statica, adica obiectele exista
si-si mentin valorile lor de-a lungul executiei intregului program.
2.2 Clase, structuri si uniuni
In C++ structurile au o functionaliate foarte apropiata de cea a claselor:
ele definesc tipuri de date noi, permit gruparea de date si functii, permit
mostenirea. De fapt, singura diferenta intre clase si structuri in
C++ este aceea ca, implicit, toti membrii unei structuri sunt de tip public.
Se poate verifica usor acest lucru, chiar pe exemplul din aceasta sectiune.
Daca se inlocuieste cuvantul cheie class cu cuvantul cheie
struct si se introduce specificatorul de acces private care sa modifice tipul
implicit de acces la date, se obtine acelasi program, cu aceeasi functionare:
#include <iostream.h> struct ComplexA private: double re; double im; public: void init()A re = 0; im = 0;
S void set(double x, double y)A re = x; im = y;
S void Setre(double x)Are = x;S void Setim(double y) Aim = y;S double getre()Areturn re;S double getim()Areturn im;S void display();
S; inline void Complex::display()A cout << re << “ ” << im << endl;
S void main()A
Complex c1; c1.set(7.2, 9.3); c1.display(); // afiseaza 7.2 9.3
S
Se poate observa ca, spre deosebire de C, in C++ obiectele de tip structura
pot fi declarate folosind doar numele structurii, fara sa mai fie nevoie sa
fie precedat de cuvantul-cheie struct.
Aceasta dubla posibilitate de definire a unor tipuri noi de date (prin clase
si prin structuri) provine din modul in care a evoluat limbajul C++, pornind
de la C. Structurile au fost pastrate in C++ in primul rand
pentru translatarea directa a programelor existente, din C in C++. Daca
structurile tot trebuie sa existe in C++, atunci adaugarea trasaturilor
suplimentare care sunt proprii claselor (functii membru, derivare, mostenire,
etc) este o problema simplu de rezolvat la nivelul proiectarii compilatoarelor,
iar structurile C++ au devenit astfel mai puternice.
In sfarsit, existenta in momentul de fata a doua cuvinte-cheie
pentru definirea tipurilor noi de date, permite evoluarea libera a conceptului
class, in timp ce conceptul struct poate fi pastrat in continuare
pentru asigurarea compatibilitatii cu programele C deja existente.
Chiar daca se poate folosi o structura acolo unde se doreste definirea unui
tip de date abstract (clasa), o practica corecta de scriere a programelor este
considerata aceea in care clasele sunt utilizate pentru definirea tipurilor
de date noi, iar structurile sunt utilizate atunci cand se doreste o structura
de tip C.
Ca si structurile, uniunile (union) in C++ definesc tipuri noi si pot
contine atat date cat si functii membru, care sunt implicit publice.
In acelasi timp, o uniune C++ pastreaza toate capacitatile din C, printre
care cea mai importanta este aceea ca toate datele impart aceleasi locatii
de memorie.
Exista mai multe restrictii in utilizarea uniunilor in C++. In
primul rand uniunile nu pot fi folosite in mecanismul de derivare
a tipurilor de date, nici ca tipuri de baza, nici ca tipuri derivate si deci,
nu pot avea functii membru de tip virtual (acestea sunt legate de derivare si
vor fi studiate in sectiunea 5). Desi o uniune poate avea constructori,
nu sunt admise date membre care au un constructor. De asemenea, nu pot fi membri
variabile de tip static.
2.3 Constructori si destructori
Utilizarea unor functii membre ale unei clase, asa cum este functia set()
din clasa Complex, pentru initializarea obiectelor este ineleganta si permite
strecurarea unor erori de programare. Deoarece nu exista nici o constrangere
din partea limbajului ca un obiect sa fie initializat (de exemplu, nu apare
nici o eroare de compilare daca nu este apelata functia set() pentru un obiect
din clasa Complex), programatorul poate sa uite sa apeleze functia de initializare
sau sa o apeleze de mai multe ori. In cazul simplu al clasei prezentate
ca exemplu pana acum, acest lucru poate produce doar erori care se evidentiaza
usor. In schimb, pentru alte clase, erorile de initializare pot fi dezastruoase
sau mai greu de identificat.
Din aceasta cauza, limbajul C++ prevede o modalitate eleganta si unitara pentru
initializarea obiectelor de tipuri definite de utilizator, prin intermediul
unor functii speciale numite functii constructor (sau, mai scurt, constructori).
2.3.1 Constructori
In exemplul urmator se defineste o clasa care descrie o stiva de numere
intregi, clasa IntStack. Detalii asupra acestui tip de date se pot citi
mai jos, in subsectiunea 2.7.
? Exemplul 2.5
#include <iostream.h>
#define MAX_SIZE 1000 class IntStack A int vectaMAX_SIZEi; int tos; public:
IntStack()Atos = 0;S void push (int v); int pop();
S; void IntStack::push(int v)A if (tos < MAX_SIZE) vectatos++i = v;
S int IntStack::pop()A if (tos > 0) return vecta--tosi; else return -;1;
S void fs1()A
IntStack stack; stack.push(4); stack.push(9); int x = stack.pop(); int y = stack.pop(); int z = stack.pop(); cout << x << “ ” << y << “ ”
<< z << endl;
//afiseaza 9 4 -1 stack.push(1); stack.push(2); x = stack.pop(); y = stack.pop(); z = stack.pop(); cout << x << “ ” << y << “ ”
<< z << endl;
// afiseaza 2 1 -1
S
?
In clasa IntStack este definit un vector de numere intregi de o
dimensiune maxima definita in program, vectaMAX_SIZEi, in care se
intoduc si se extrag numere in ordinea ultimul intrat-primul extras (last
in-first out). Doua functii membre ale clasei, push() si pop() realizeaza introducerea,
respectiv extragerea, unui numar intreg din obiectul de tip IntStack pentru
care sunt apelate. In functia push() se previne introducerea unui nou
numar daca stiva este plina. In functia pop() se returneaza o valoare
corecta numai daca stiva nu este goala; daca nu exista nici un numar in
stiva, nu se citeste nimic din memorie si se returneaza -; 1. Aceasta tratare
a situatiei de eroare nu este suficienta, deoarece valoarea -; 1 poate
fi returnata si ca data corecta. O tratare completa a situatiilor de eroare
la executia functiilor membre ale unei clase este prezentata in sectiunea
8.
Variabila tos indica prima pozitie libera din stiva si ea trebuie neaparat sa
fie initializata la 0 (inceputul stivei) inainte ca stiva sa poata
fi folosita, altfel pot apare erori de executie impredictibile (scriere la adrese
de memorie necontrolate). Initializarea s-ar putea face printr-o functia membra
care sa fie apelata explicit, dar o modalitate mai buna este aceea de a folosi
o functie membra speciala pentru initializare, denumita functie constructor.
Un constructor este o functie cu acelasi nume cu numele clasei, care nu returneaza
nici o valoare si care initializaza datele membre ale clasei.
De exemplu, in clasa de mai sus constructorul:
IntStack()Atos=0;S initializeaza la 0 variabila tos.
Pentru aceeasi clasa pot fi definite mai multe functii constructor, ca functii
supraincarcate, care pot fi selectate de catre compilator in functie
de numarul si tipul argumentelor de apel, la fel ca in orice supraincarcare
de functii.
Un constructor implicit pentru o clasa X este un constructor care poate fi apelat
fara nici un argument. Deci un constructor implicit este fie un constructor
care are lista de argumente vida, fie un constructor cu unul sau mai multe argumente,
toate fiind prevazute cu valori implicite. De exemplu, X::X(int=0) este un constructor
implicit, deoarece el poate fi apelat fara nici un argument, avand definita
o valoare implicita a argumentului.
In general, constructorii se declara de tip public, pentru a putea fi
apelati din orice punct al domeniului de definitie al clasei respective. La
crearea unui obiect dintr-o clasa oarecare este apelat implicit acel constructor
al clasei care prezinta cea mai buna potrivire a argumentelor. Daca nu este
prevazuta nici o functie constructor, compilatorul genereaza un constructor
implicit de tip public, ori de cate ori este necesar.
In Exemplul 2.6 se definesc pentru clasa Complex mai multe functii constructor:
constructor implicit, cu un argument si cu doua argumente.
? Exemplul 2.6
#include <iostream.h> class ComplexA double re; double im; public:
Complex()A cout << "Constructor implicit\n";
S
Complex(double v)A
Cout << "Constructor cu 1 arg\n"); re = v; im = v;
S
Complex(double x, double y)A
Cout << "Constructor cu 2 arg\n"; re = x; im = y;
S
//…………..
S; void fc2 ()A
Complex c1;
Complex c2(5);
Complex c3(4,6);
S
La executia functiei fc2(), sunt afisate urmatoarele mesaje:
Constructor implicit
Constructor cu 1 arg
Constructor cu 2 arg
In fiecare dintre aceste situatii a fost creat un obiect de tip Complex,
ca obiect local functiei fc2() (c1, c2, c3) si de fiecare data a fost apelat
constructorul care are acelasi numar si tip de argumente cu cele de apel.
?
Un constructor este apelat ori de cate ori este creat un obiect dintr-o
clasa care are un constructor (definit sau generat de compilator). Un obiect
poate fi creat intr-unul din urmatoarele moduri:
• ca variabila globala,
• ca variabila locala,
• prin utilizarea explicita a operatorului new,
• ca obiect temporar,
• prin apelul explicit al unui constructor.
In fiecare situatie, constructorul are rolul de a crea structura de baza
a obiectului (sa construiasca obiectele care reprezinta date membre nestatice,
sa construiasca tabelele care se refera la derivare si mostenire, daca este
cazul) si, in final, sa execute codul specificat in corpul constructorului.
Un constructor generat de compilator pentru o clasa X are forma generala X::X()AS,
adica nu prevede executia unui cod, ci doar asigura constructia obiectelor membre
si ale tabelelor de derivare, daca este cazul.
In toate situatiile afara de ultima, constructorul este apelat in
mod implicit, atunci cand se creaza obiectul. Apelul explicit al unui
constructor, desi admis de limbaj, este putin utilizat.
Pe langa initializarea datelor membre, in constructori se executa,
atunci cand este necesar, operatiile de alocare dinamica a unor date.
De exemplu, implementarea clasei IntStack prezentata in Exemplul 2.5 poate
produce un mare consum de memorie, in mod nejustificat: orice obiect de
tipul IntStack este creat cu un vector de date de dimensiunea maxima MAX_SIZE
definita ca o constanta in program, chiar daca un numar mare de obiecte
ar necesita dimensiuni mult mai reduse. Alocarea dinamica a spatiului strict
necesar pentru vectorul de numere intregi se poate efectua la crearea
obiectului, prin functiile constructor. Exemplul urmator prezinta aceasta posibilitate.
? Exemplul 2.7
Se reia implementarea tipului de date stiva de numere intregi cu alocarea
dinamica a vectorului in memoria libera, folosind clasa DStack:
class DStackA int *pvect; int size; int tos; public:
DStack()A pvect = NULL; size = 0; tos = 0;
S
DStack(int s)A pvect = new intasi; size = s; tos = 0;
S void push(int x); int pop();
S; void DStack::push(int x)A if (tos < size) pvectatos++i = x;
S int DStack::pop()A if (tos>0) return pvecta--tosi; else return -1;
S void fd1()A
DStack stack1(100); stack1.push(4); stack1.push(9); int x = stack1.pop(); int y = stack1.pop(); int z = stack1.pop(); cout << x << “ ” << y << “ ”
<< z << endl;
//afiseaza 9 4 -1
DStack stack2(200); stack2.push(1); stack2.push(2); x = stack2.pop(); y = stack2.pop(); z = stack2.pop(); cout << x << “ ” << y << “ ”
<< z << endl;
// afiseaza 2 1 -1
S
La declararea unui obiect de clasa DStack, se transmite ca argument dimensiunea
dorita a stivei, iar constructorul aloca spatiul necesar in memoria heap.
In rest, implementarea clasei DStack este asemanatoare clasei IntStack,
prezentata mai sus.
?
2.3.2 Destructori
Multe din clasele definite intr-un program necesita o operatie inversa
celei efectuate de constructor, pentru stergerea completa a obiectelor atunci
cand sunt distruse (eliminate din memorie). O astfel de operatie este
efectuata de o functie membra a clasei, numita functie destructor. Numele destructorului
unei clasei X este IX() si este o functie care nu primeste nici un argument
si nu returneaza nici o valoare.
In implementarea din Exemplul 2.7 a stivei de numere intregi, la
iesirea din functia fd1(), obiectele stack1 si stack2 sunt eliminate din memorie,
dar vectorii corespunzatori lor, alocati dinamic in memoria heap, trebuie
sa fie si ei stersi, pentru a nu ocupa in mod inutil memoria libera. Aceasta
operatie se poate executa in functia destructor astfel: class DStackA
//……. public:
IDStack()A if (pvect)A delete pvect; pvect = NULL;
S
//………..
S;
Destructorii sunt apelati implicit in mai multe situatii:
• atunci cand un obiect local sau temporar iese din domeniul de
definitie;
• la sfarsitul programului, pentru obiectele globale;
• la apelul operatorului delete, pentru obiectele alocate dinamic.
Apelul explicit al unui destructor este rar utilizat. Daca o clasa nu are un
destructor, compilatorul genereaza un destructor implicit.
2.3.3 Constructori de copiere
Functia principala a unui constructor este aceea de a initializa datele membre
ale obiectului creat, folosind pentru aceasta operatie valorile primite ca argumente.
Exemple de astfel de initializari se gasesc in toti constructorii definiti
pana in prezent.
O alta forma de initializare care se poate face la crearea unui obiect este
prin copierea datelor unui alt obiect de acelasi tip. Aceasta operatie este
posibila prin intermediul unui constructor mai special al clasei, numit constructor
de copiere. Forma generala a constructorului de copiere al unei clase X este:
X::X(X& r)A
// initializare obiect folosind referinta r
S
Constructorul primeste ca argument o referinta r la un obiect din clasa X si
initializaza obiectul nou creat folosind datele continute in obiectul
referinta r. Pentru crearea unui obiect printr-un constructor de copiere, argumentul
transmis trebuie sa fie o referinta la un obiect din aceeasi clasa.
De exemplu, pentru obiecte de tip Complex:
void fc3()A
Complex c1(4,5); // Constructor initializare
Complex c2(c1); // Constructor copiere bitwise
Complex c3 = c2; // Constructor copiere bitwise c3.display(); // afiseaza 4 5
S
La crearea primului obiect (c1) este este apelat constructorul cu doua argumente
al clasei Complex. Cel de-al doilea obiect (c2) este creat prin apelul constructorului
de copiere al clasei Complex, avind ca argument referinta la obiectul
c1. Este posibila si declaratia (definitia) de forma Complex c3 = c2; a unui
obiect prin care se apeleaza, de asemenea, constructorul de copiere.
Constructorul de copiere poate fi definit de programator; daca nu este definit
un constructor de copiere al clasei, compilatorul genereaza un constructor de
copiere care copiaza datele membru cu membru din obiectul referinta in
obiectul nou creat. Aceasta modalitate de copiere mai este denumita copie la
nivel de biti sau copie bit cu bit (bitwise copy).
Clasa Complex poate fi completata cu un constructor de copiere astfel:
class Complex A
//………….. public:
Complex(Complex &r);
S;
Complex::Complex(Complex &r)A re = r.re; im = r.im;
S
Se pune intrebarea urmatoare: de ce mai este nevoie sa fie definit un
constructor de copiere daca el este oricum generat de compilator atunci cand
este necesar?
Pentru obiecte din clasa Complex, functionarea este aceeasi, atat in
situatia in care in clasa nu este definit un constructor de copiere,
si deci el este generat de catre compilator, cat si daca acesta a fost
definit in clasa. Mesajele care se afiseaza la consola daca functia fc3()
este executata dupa introducerea constructorului de copiere sunt:
Constructor cu 2 arg
Constructor copiere
Constructor copiere
4 5
Cu totul alta este situatia in cazul in care un obiect contine
date alocate dinamic in memoria heap. Constructorul de copiere generat
implicit de compilator copiaza doar datele membre declarate in clasa (membru
cu membru) si nu stie sa aloce date dinamice pentru obiectul nou creat. Folosind
un astfel de constructor, se ajunge la situatia ca doua obiecte, cel nou creat
si obiectul referinta, sa contina pointeri cu aceeasi valoare, deci care indica
spre aceeasi zona din memoria heap. O astfel se situatie este o sursa puternica
de erori de executie subtile si greu de depistat.
Exemplul urmator evidentiaza aceasta problema.
? Exemplul 2.8
Se considera clasa DStack prezentata in mai sus si o functie fd2() definita
astfel:
void fd2()A
DStack stack1(100);
DStack stack2(stack1); stack1.push(11); stack1.push(12); stack2.push(21); stack2.push(22); int x = stack1.pop(); int y = stack1.pop(); cout << x << “ ” << y << endl; // afiseaza
22 21
S
Problema care apare este evidenta: deoarece prin copierea membru cu membru
pointerul pvect al obiectului stack2 primeste valoarea pointerului pvect al
obiectului stack1, instructiunile stack2.push(21) si stack2.push(22) scriu peste
valorile introduse mai inainte in stiva stack1, astfel incat
extragerile din stiva stack1 gasesc valorile introduse in stiva stack2.
Diferitele combinatii de operatii pot da cele mai variate rezultate.
O alta problema apare la iesirea din functia fd2(). Pentru clasa DStack a fost
definit un destructor care sterge vectorul pvect din memorie folosind operatorul
delete. La distrugerea obiectului stack2 este sters din memorie vectorul indicat
de pointerul pvect al acestui obiect, iar la distrugerea obiectului stack1 se
incearca din nou stergerea aceleiasi zone de memorie, dat fiind ca cei
doi pointeri aveau valoare egala. O astfel de operatie produce eroare de executie
si abandonarea programului, ceea ce se si poate observa la executia functiei
fd2().
Solutia o reprezinta definirea unui constructor de copiere care sa previna astfel
de situatii. Un constructor de copiere definit de programator trebuie sa aloce
spatiu pentru datele dinamice create in memoria heap si dupa aceea sa
copieze valorile din obiectul de referinta. Un exemplu de constructor de copiere
definit pentru clasa DStack va explicita mai usor acest aspect.
? Exemplul 2.9
Se defineste constructorul de copiere al clasei DStack astfel:
DStack::DStack(DStack &r)A size = r.size; tos = r.tos; pvect = new intasizei; for (int i=0; i< size; i++) pvectaii = r.pvectaii;
S
Bineinteles, declaratia acestuia trebuie sa apara in interiorul
clasei DStack.
Fie functia:
void fd3()A
DStack stack1(100); stack1.push(11);
DStack stack2(stack1); stack1.push(12); stack2.push(21); int x = stack1.pop(); int y = stack1.pop(); cout << x << “ ” << y << endl; // afisaeza
12 11 x = stack2.pop(); y = stack2.pop(); cout << x << “ ” << y << endl; // afiseaza
21 11
S
Executia acesteia se termina normal; dupa constructia prin copiere a obiectului
stack2, acesta are propriul vector de numere intregi, in care a
preluat o valoare introdusa in stiva stack1 inainte de copiere (valoarea
11), si el continua operatiile de introducere si extragere din acest punct.
Cele doua obiecte sunt complet independente si asupra lor se pot executa operatii
in mod separat, inclusiv operatia de distrugere care se executa in
mod implicit la sfarsitul functiei. Acest lucru este evidentiat de mesajele
afisate.
?
Se poate stabili cu certitudine ca programatorul trebuie sa prevada un constructor
de copiere “inteligent”, in orice clasa in care exista
date alocate dinamic. Daca un astfel de constructor este definit, compilatorul
nu mai genereaza constructorul implicit de copiere membru cu membru si sunt
evitate erorile de tipul celor descrise mai sus.
Erori care au originea in constructia obiectelor prin copiere membru
cu membru (deci folosind constructorul de copiere implicit generat de compilator)
pot sa apara in orice situatie in care se construieste un obiect
prin copiere. Astfel de situatii vor fi prezentate in exemplele care urmeaza.
? Exemplul 2.10
La trasmiterea unui obiect ca argument prin valoare unei functii, se construieste
un obiect local functiei folosindu-se constructorul de copiere. De aici pot
proveni toate problemele de tipul celor descrise mai sus. Se pot observa mai
intuitiv, daca se executa functia fd4() in situatia in care nu s-a
definit constructorul de copiere al clasei DStack, si in situatia in
care s-a definit ca mai sus un astfel de constructor.
void g(DStack ds)A int x = ds.pop(); int y = ds.pop(); cout << x << “ ” << y << endl; // afiseaza
66 55
S void fd4()A
DStack stack(100); stack.push(55); stack.push(66); g(stack);
S
Executia corecta a functiei fd4() care apeleaza functia g() avand ca
argument un obiect de tip DStack are loc numai daca a fost definit (ca mai sus)
constructorul de copiere al clasei DStack. Altfel apare eroare de executie.
? Exemplul 2.11
La returnarea unui obiect dintr-o functie se creaza un obiect temporar folosind
constructorul de copiere. Pentru a observa comportarea obiectelor create se
adauga mesaje de identificare in constructorii si destructorul clasei
DStack astfel:
class DStackA int *pvect; int size; int tos; public:
DStack(int s)A
Cout << "Constructor initializare\n"; pvect = new intasi; size = s; tos = 0;
S
DStack(DStack &r);
IDStack(); void push(int x); int pop();
S;
DStack::DStack(DStack &r)A
Cout << "Constructor copiere\n"; size = r.size; tos = r.tos; pvect = new intasizei; for (int i=0; i< size; i++) pvectaii = r.pvectaii;
S
DStack::IDStack()A cout << "Destructor\n"; if (pvect)A delete pvect; pvect = NULL;
S
S
DStack h()A
DStack s(200); return s;
S void fd5()A h(); cout << "Revenire din h()\n";
S
La revenirea din functia h(), se construieste un obiect temporar folosind ca
referinta obiectul de tip DStack returnat de functia h(). Mesajele care se afiseaza
la executia functiei fd5() sunt urmatoarele:
Constructor initializare
Constructor copiere
Destructor
Destructor
Revenire din h()
Primul mesaj indica construirea obiectului s in functia h() folosind
constructorul de initialzare al clasei; al doilea mesaj se afiseaza la constructia
unui obiect temporar avand ca referinta obiectul returnat de functia h().
Aceste obiecte sunt distruse inainte de iesirea din functia fds5().
Daca a fost definit constructorul de copiere al clasei (ca mai sus), executia
este corecta. Daca se elimina acest constructor, apare eroare de executie, datorita
tentativei de a sterge a doua oara vectorul de numere, care este acelasi pentru
cele doua obiecte.
2.3.4 Conversia datelor prin constructori
Conversia unei variabile intre doua tipuri dintre care cel putin unul
este un tip definit de utilizator (clasa) se poate face prin constructori sau
prin supraincarcarea operatorului de conversie. In aceasta sectiune
se prezinta cazul de conversie prin constructori care este o conversie de la
un tip de date predefinit la un tip definit de utilizator (clasa). Conversia
prin supraincarcarea operatoruilor este descrisa in sectiunea 4.
Un constructor cu un argument T al unei clase X foloseste valoarea acestuia
pentru initializarea obiectului de clasa X construit. Acest mod de initializare
poate fi privit ca o conversie de la tipul de date T, la tipul de date X. Daca
T este un tip predefinit, un astfel de constructor este definit simplu, ca membru
nestatic al clasei :
class XA public:
X(T t);
// …………
S;
X::X(T t)A
// initializare obiect X
S
Cazul in care si T este un tip definit de utilizator este prezentat in
sectiunea 4.
Daca ne referim la clasa Complex definita in aceasta sectiune, instructiunea:
Complex c1 = 7.8; este o conversie de la tipul double la tipul Complex: se creeaza obiectul c1
de tip Complex cu valori ale datelor membre initializate folosind data de tip
double din care se face conversia. Daca implementarea clasei Complex este cea
din Exemplul 2.6, care contine cate un mesaj de identificare pentru fiecare
constructor, atunci, la executia acestei instructiuni se afiseaza mesajul:
Constructor cu 1 arg
Operatia de conversie printr-un constructor cu un argument are loc direct, fara
alte operatii intermediare, daca intervine in declararea obiectului, asa
cum este in exemplul dat mai sus. In alte modalitati de declarare
apar operatii suplimentare. De exemplu, instructiunile:
Complex c2; c2 = 9.3;
creaza mai intai obiectul c2 de tip Complex, folosind constructorul
implicit al clasei; dupa aceasta este creat un obiect temporar folosind constructorul
cu un argument, pentru conversia de la valoarea 9.3 de tip double si acest obiect
este utilizat pentru operatia de asignare al carui membru stang este obiectul
c2.
Se poate remarca ineficienta unei astfel de sectiuni de program. Mai mult, asignarea
intre obiecte de tip definit de utilizator ridica aceleasi probleme ca
si copierea prin constructorii de copiere: operatia de asignare predefinita
pe care o executa compilatorul este o asignare prin copiere membru cu membru
a datelor. Se poate intui ca problemele care apar sunt aceleasi ca si in
cazul constructorilor de copiere: pentru obiectele care contin date alocate
dinamic in memoria heap, copierea membru cu membru conduce la situatia
ca doua obiecte, cel asignat (membru stanga) si cel din care se executa
asignarea (membru dreapta) sa contina pointeri cu aceeasi valoare, deci care
indica spre aceeasi zona din memoria heap. De aici, evident, apar numeroase
probleme.
Se poate observa acest comportament folosind conversii si asignari a obiectelor
din clasa DStack:
DStack stack1 = 8;//corect, conversie prin constructor
DStack stack2; // construire cu constructor implicit stack2 = 7; // conversie, apoi asignare
// la asignare apare eroare de executie
Solutia de a supraincarca operatorul de asignare este prezentata in
sectiunea 4.
2.4 Obiecte de tipuri definite de utilizator membre ale claselor. Clase locale.
Clase imbricate
Datele membre ale unei clase pot fi atat variabile de tipuri predefinite
cat si obiecte de tipuri definite de utilizator. Membrii care sunt de
tip definit de utilizator (clasa) trebuie sa fie obiecte de tipuri (clase) definite
mai inainte, nu doar declarate ca nume.
Daca o clasa contine obiecte membre de tipuri definite de utilizator, argumentele
necesare pentru construirea acestora sunt plasate in definitia (nu in
declaratia) constructorului clasei care le contine. Fie urmatoarele definitii
de clase si functii:
class XA int *px; public:
X();
X(int sx);
IX();
S; inline X::X()A cout << "Constructor X implicit\n"; px = NULL;
S inline X::X(int sx)A cout << "Constructor X cu 1 arg\n"; px = new intasxi;
S inline X::IX()A cout << "Destructor X\n"; if (px)A delete px; px = NULL;
S
S class YA int *py;
X x; // data membra de clasa X public:
Y(int sy);
Y(int sx, int sy);
IY();
S; inline Y::Y(int sy)A cout << "Constructor Y cu 1 arg\n"; py = new intasyi;
S inline Y::Y(int sx, int sy):x(sx)A cout << "Constructor Y cu 2 arg\n"; py = new intasyi;
S inline Y::IY()A cout << "Destructor Y\n"); if (py)A delete py; py = NULL;
S
S void fx()A
Y y1(7);
Y y2(4,5);
S
La executia functiei fx(), data membra x de clasa X a obiectului y2 se initializeaza
folosind argumentul 4 transmis prin intermediul constructorului clasei Y. Mesajele
care se afiseaza la executia functiei f6() sunt urmatoarele:
Constructor X implicit
Constructor Y cu 1 arg
Constructor X cu 1 arg
Constructor Y cu 2 arg
Destructor Y
Destructor X
Destructor Y
Destructor X
Se observa ca se construieste mai intai data membra x si apoi
obiectul de clasa Y care o contine. Daca sunt mai multe date membre care se
initializeaza, acestea se pot trece in orice ordine, separate prin virgula
in definitia constructorului obiectului care le contine. Constructorii
obiectelor membre sunt apelati in ordinea in care acestea sunt specificate
in declaratia clasei. La distrugerea unui obiect, se executa mai intai
destructorul obiectului si apoi, in ordinea inversa declaratiei, destructorii
datelor membre ale acestuia.
2.4.1 Ordinea de executie a constructorilor si destructorilor
Un constructor este apelat la declararea obiectului, iar destructorul este apelat
atunci cand obiectul este distrus. Daca exista mai multe declaratii de
obiecte, atunci ele sunt construite in ordinea declaratiei si sunt distruse
in ordinea inversa a declaratiei.
Obiectele membre ale unei clase se construiesc inaintea obiectului respectiv.
Destructorii sunt apelati in ordine inversa: destructorul obiectului si
apoi destructorii membrilor (un exemplu este dat in subsectiunea urmatoare)
Functiile constructor a obiectelor globale sunt executate inaintea executiei
functiei main(). Constructorii obiectelor globale din acelasi fisier sunt executati
in ordinea declaratiilor, de la stanga la dreapta si de sus in
jos. Este greu de precizat ordinea de apel a constructorilor globali distribuiti
in mai multe fisiere. Dectructorii obiectelor globale sunt apelati in
ordine inversa, dupa incheiere functiei main().
Alte precizari cu privire la ordinea de executie a constructorilor si destructorilor
se vor face in sectiunea 5, dedicata claselor derivate.
3.3.1 Clase locale. Clase imbricate
O clasa locala este o clasa definita in interiorul unei functii. O astfel
de clasa este cunoscuta numai in interiorul acelei functii si este supusa
mai multor restrictii: toate functiile clasei locale trebuie sa fie definite
in interiorul clasei; clasele locale nu admit variabile de tip static;
clasa locala nu are acces la variabilele locale ale functiei in care a
fost declarata. Din cauza acestor restrictii, clasele locale sunt rar utilizate
in programarea C++.
O clasa imbricata este o clasa definita in interiorul altei clase. O
astfel de clasa este cunoscuta numai in domeniul clasei in care
a fost definita, de aceea numele acesteia trebuie sa fie calificat cu numele
clasei care o contine folosind operatorul de rezolutie (::). Utilizarea specifica
a claselor imbricate este in mecanismele de tratare a exceptiilor. Deoarece
exceptiile sunt definite pentru o anumita clasa, este mai normal ca tipul exceptiei
(definit ca o clasa) sa apartina clasei care o defineste. Un exemplu de clasa
imbricata folosita in tratarea exceptiiloe este prezentat in sectiunea
8.
2.5 Functii si clase friend
Este posibil sa fie admis unei functii nemembre sa acceseze datele private sau
protected ale unei clasei prin declararea acesteia de tip friend a clasei. Pentru
declararea unei functii f() de tip friend a clasei X se include prototipul functiei
f(), precedat de specificatorul friend in definitia clasei X, iar functia
insasi se defineste in alta parte in program astfel:
class XA
//…….. friend tip_returnat f(lista_argumente);
S;
………………………..…… tip_returnat f(lista_argumente)A
// corpul functiei
S
Pentru evidentierea utilitatii functiilor de tip friend se considera urmatorul
exemplu.
? Exemplul 2.12
Se considera problema de inmultire a unei matrice cu un vector. Astfel
de operatii sunt deosebit de frecvente, in multe domenii: fizica, proiectare
automata, grafica, etc. Cele doua clase care descriu o matrice 4x4 (folosita
in transformarile grafice tridimensionale) si un vector de dimensiune
4 sunt definite astfel: class Matrix A double ma4ia4i; public:
Matrix();
Matrix(double pmaia4i); void set(int i, int j, double e) A maiiaji=e;S double get(int i, int j)constAreturn maiiaji;S
S;
Matrix::Matrix() A for (int i=0;i<4;i++) for (int j=0;j<4;j++) if (i==j)maiiaji = 1.0; else maiiaji = 0.0;
S
Matrix::Matrix(double pmaia4i)A for (int i=0;i<4;i++) for (int j=0;j<4;j++) maiiaji = pmaiiaji;
S class Vector A double va4i; public:
Vector();
Vector(double *pv); void set(int i, double e) Avaii=e;S double get(int i) const Areturn vaii;S
S;
Vector::Vector()A for (int i=0;i<3;i++) vaii=0.0; va3i = 1.0;
S
Vector::Vector(double *pv)A for (int i=0;i<4;i++) vaii= pvaii;
S
Fiecare din cele doua clase are prevazut un constructor implicit si un constructor
de initializare. Modificatorul const care este prezent in unele declaratii
va fi precizat in subsectiunea urmatoare.
Dat fiind ca o functie nu poate fi membra a doua clase, cel mai natural mod
de inmultire a unei matrici cu un vector ar parea sa fie prin definirea
unei functii nemembre multiply()care acceseaza elementele celor doua clase (clasa
Matrix si clasa Vector) prin functiile de interfata get() si set().
Vector Matrix::multiply(const matrix &mat, const Vector &vect)A
Vector vr; for (int i=0;i<4;i++)A double x =0.0; for (int j=0;j<4;j++) x += vect.get(j)*mat.get(i,j); vr.set(i,x);
S return vr;
S
Dar acest mod de operare poate fi foarte ineficient, daca functiile de interfata
get() si set()ar verifica incadrarea argumentului (indicele) in
valorile admisibile. Daca o astfel de verificare nu se face, alte programe care
le foloseste (in afara de functia multiply() ) ar putea provoca erori
in program.
Solutia pentru aceasta problema o constitue declararea functiei multiply() ca
functie friend in cele doua clase. Modificarile care se intoduc in
cele doua clase si in functia multipy() arata astfel:
class Vector; class Matrix A
//…..……… friend Vector multiply(const Matrix &mat, const Vector &vect);
S; class Vector A
//…..……… friend Vector multiply(const Matrix &mat, const Vector &vect);
S;
Declaratia friend poate fi plasata in orice parte, public, private sau
protected a clasei. Functia multiply() se rescrie pentru accesul direct la elementele
vectorului si matricei astfel:
Vector multiply(const Matrix &mat, const Vector &vect)A
Vector vr; for (int i=0;i<4;i++) A double x = 0.0; for (int j=0;j<4;j++) x += vect.vaji * mat.majiaii; vr.vaii = x;
S return vr;
S
Apelul acestei functii de inmultire multiply() intr-o functie oarecare
fm() este urmatorul: