Clase si obiecte
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.
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 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.
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
n Exemplul 2.1
#include <iostream.h>
class Complex
void set(double x, double y)
void display()
};
void main()
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
n
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)
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 redec 717c27h lara) 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.
n Exemplul 2.2
#include <iostream.h>
void K::set(int x)
class K; // declaratie nume clasa K
K* pob1; // corect, pointer la K
K pb2; // error: 'pb2' uses undefined class 'K'
class K
K(K& r); // corect, contine o referinta la K
int get();// declaratie (prototip) functie membra
void set(int x);
// corect: definitii functii membre in
// domeniul de definitie al clasei
int K::get()
void K::set(int x)
K pob3; // corect, clasa K este definita
void fk()
n
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() 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()
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
#include <iostream.h>
class U
void set(int x)
void fu1(const U* pu, int i)
void fu2(const U& r, int i)
void main()
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
Dar, intentia cu care se utilizeaza argumente de tip
pointer (sau referinta) la
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
#include <iostream.h>
class U
void set(int x)
void fu1(const U* ps, U* pd)
// poate accesa ob. const U* ps
ps->set(i); // eroare, set() nu poate
// accesa obiectul const U* ps
pd->set(i); // corect, set() poate
// accesa obiectul U* pd
i = pd->get(); // corect get()const
// poate accesa obiectul U* pd
void main()
Se poate observa ca functia membra get()const 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() nu poate accesa decat obiecte date prin pointer normal (in sensul ca nu este pointer la constanta).
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)
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.
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.
n Exemplul 2.3
Se defineste o clasa W se acceseaza prin pointeri datele si functiile membre ale clasei W astfel
#include <iostream.h>
class W
void setb(int x)
int geta()
int getb()
void main ()
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);
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 Complex;
void fc1()
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 Complex
void set(double x, double y)
void setre(double x)
void setim(double y)
double getre()
double getim()
void display();
};
inline void Complex::display()
void main()
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.
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.
n 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 S
int gets()
int getv()
void incs()
void incv()
int S::s; // definitia var. statice s a clasei S
void main ()
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
n
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 Task;
Task *Task::chain=0; // definirea datei statice
void Task::schedule(int p)
void fs2()
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.
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 Complex
void set(double x, double y)
void Setre(double x)
void Setim(double y)
double getre()
double getim()
void display();
};
inline void Complex::display()
void main()
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.
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).
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.
n Exemplul 2.5
#include <iostream.h>
#define MAX_SIZE 1000
class IntStack
void push (int v);
int pop();
void IntStack::push(int v)
int IntStack::pop()
void fs1()
n
In clasa IntStack este definit un vector de numere intregi de o dimensiune maxima definita in program, vect[MAX_SIZE], 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()
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.
n Exemplul 2.6
#include <iostream.h>
class Complex
Complex(double v)
Complex(double x, double y)
//
void fc2 ()
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.
n
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(), 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.
n 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 DStack
DStack(int s)
void push(int x);
int pop();
void DStack::push(int x)
int DStack::pop()
void fd1()
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.
n
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 ~X() 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 DStack
//
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.
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)
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()
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 ;
Complex::Complex(Complex &r)
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
void fd2()
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.
n Exemplul 2.9
Se defineste constructorul de copiere al clasei DStack astfel
DStack::DStack(DStack &r)
Bineinteles, declaratia acestuia trebuie sa apara in interiorul clasei DStack
Fie functia
void fd3()
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 ), 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.
n
n 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)
n 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 DStack
DStack(DStack &r);
~DStack();
void push(int x);
int pop();
DStack::DStack(DStack &r)
DStack::~DStack()
DStack h()
void fd5()
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.
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 X;
X::X(T t)
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 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
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 X;
inline X::X()
inline X::X(int sx)
inline X::~X()
class Y;
inline Y::Y(int sy)
inline Y::Y(int sx, int sy):x(sx)
inline Y::~Y()
void fx()
La executia functiei fx(), data membra x de clasa X a obiectului y2 se initializeaza folosind argumentul 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.
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.
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.
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 X;
tip_returnat f(lista_argumente)
Pentru evidentierea utilitatii functiilor de tip friend se considera urmatorul exemplu.
n 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
double get(int i, int j)const
Matrix::Matrix()
Matrix::Matrix(double pm[][4])
class Vector
double get(int i) const
Vector::Vector()
Vector::Vector(double *pv)
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)
return vr;
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 ;
class Vector ;
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)
return vr;
Apelul acestei functii de inmultire multiply() intr-o functie oarecare fm() este urmatorul:
void fm();
double m[][4] = ;
Matrix m1(m);
Vector v1(v);
Vector v2 = multiply(m1,v1);
Este posibil ca o functie membra a unei clase sa fie declarata friend in alta clasa. De asemenea este posibil ca o intreaga clasa sa fie declarata friend a unei alte clase si, in acesta situatie, toate functiile membre sunt functii friend. De exemplu
class Y;
class X;
Aceasta declaratie face ca toate functiile membre ale clasei Y sa fie functii friend ale clasei X
Obiectele (instante ale claselor) se pot aloca in memoria heap folosind operatorul new. La alocarea memoriei pentru un singur obiect se pot transmite argumente care sunt folosite pentru initializarea obiectului, prin apelul acelei functii constructor a clasei care prezinta cea mai buna potrivire cu argumentele de apel. Eliberarea memoriei ocupata de un obiect se realizeaza prin operatorul delete, care apeleaza implicit destructorul clasei.
De exemplu, alocari si eliberari pentru obiecte de tipurile Complex si DStack
Complex *pc0 = new Complex; // apel constr. implicit
Complex *pc1 = new Complex(2);// apel constr. cu 1 arg
Complex *pc2 = new Complex(3.5,2.9); //apel constr. 2 arg
delete pc0; // apel implicit destructor
delete pc1; // apel implicit destructor
delete pc2; // apel implicit destructor
DStack *pstack = new DStack(100); // creare stiva dim 100
pstack->push(5);
int x = pstack->pop();
delete pstack; // apel implicit destructor
Perntru alocarea dinamica a unui vector de obiecte de tipul X, trebuie sa existe un constructor implicit al clasei X, care este apelat pentru fiecare din elementele vectorului creat. Pentru stergerea unui vector de obiecte se foloseste operatorul delete care apeleaza implicit destructorul clasei pentru fiecare din elementele vectorului alocat. Daca, pentru un vector alocat dinamic, se apeleaza operatorul delete (in loc de delete ), se apeleaza destructorul clasei o singura data, dupa care rezultatul executiei este imprevizibil, cel mai adesea se produce eroare de executie si abandonarea programului. Cateva exemple de alocari si eliberari de vectori de obiecte
Complex *pv1 = new Complex[4];
delete []pv1;
DStack *stack1 = new DStack[3];
delete []stack1;
Pentru fiecare element al fiecaruia dintre vectori, este apelat constructorul implicit la creare si destructorul la stergere.
Operatorii new si delete prezinta mai multe avantaje fata de functiile similare malloc() si free(). In primul rand, new aloca automat spatiul necesar pentru memorarea unui obiect de tipul specificat, fara sa necesite folosirea operatorului sizeof. Apoi, pointerul returnat de new este de tipul specificat, ne mai fiind nevoie de conversie de la tipul void , asa cum se procedeaza la apelul functiei malloc(). In sfarsit, operatorii new si delete pot fi supraincarcati, permitand modalitati diferentiate de alocare dinamica pentru tipurile definite de utilizator.
Exercitii
E2.1 Sa se defineasca o clasa Date pentru memorarea datei sub forma (zi, luna, an). Clasa va contine atatia constructori, cat sunt necesari pentru urmatoarele definitii de obiecte:
Date data1(15, 3, 99);// 15 martie 99
Date data2(18); // 18 luna curenta, anul curent
Date data3(20, 4); // 20 aprilie, anul curent
E2.2 Sa se defineasca o clasa CharStack pentru o stiva de caractere, a carei dimensiune se stabileste printr-un parametru transmis la constructie. In programul principal se vor crea doua stive de dimensiune 100, respectiv 200 caractere. Inserati un caracter in prima stiva, dupa aceea il extrageti din aceasta si-l introduceti in cea de-a doua stiva. Afisati rezultatul unei operatii de extragere din a doua stiva.
E2.3 Sa se construiasca o clasa de obiecte Contor pentru urmarirea functionarii unui aparat de inregistrare (contor) cu cifre de unitati, zeci, sute. Pentru aceasta clasa se vor defini constructori, destructorul, functia avans() care semnifica avansul cu o tura si functia display() de afisare a starii curente a contorului. Sa se defineasca un obiect de tip Contor pozitionat pe valoarea 972. Sa se transnita contorului 5 semnale de avans si sa se afiseza pozitia in care se afla.
E2.4 Se considera urmatorul program in care o variabila globala numbers memoreaza numarul obiectelor "in viata" la un moment dat:
#include <iostream.h>
int numbers = 0;
class Item
~Item()
void main()
Item *pob6 = new Item[3];
delete [] pob6;
Care sunt mesajele care apar la consola la executia acestui program? Sa se explice evolutia numarului de obiecte in viata in cursul executiei.
E2.5 Se se modifice programul de mai sus astfel incat sa se foloseasca un membru static al clasei Item pentru contorizarea obiectelor in viata. Sa se verifice echivalenta mesajelor afisate la consola.
E2.6 Se defineste o clasa de obiecte IntArray care permite reprezentarea unor vectori de numere intregi astfel
class IntArray
~IntArray()
int GetCount()
int Add(int x);
int GetAt(int i, int *v)
int IntArray::Add(int x)
return count-1;
Aceasta clasa va reproduce in mare masura clasa CUIntArray din MFC, daca i se vor mai adauga si alte functii.
Vectorul contine count elemente (intregi) si ocupa un spatiu in memorie necesar pentru un numar de intregi egal cu size. Atunci cand este necesar, cresterea sizeiunii vectorului se face cu grows elemente.
(a) Se cere sa se redea mesajele care se afiseaza la consola la executia urmatoarei functii
void fci()
(b) Ce se intampla daca pentru copierea in noul vector p se inlocuieste instructiunea
p[i] = p1[i]; cu *p++ = *p1++;
Sa se explice diferenta si cauza erorii, daca aceasta apare la compilare sau la executie.
E2.7 Sa se defineasca pentru clasa IntArray o functie int InsertAt(int x, int i) care sa permita inserarea in vector a unui numar intreg x in pozitia data i, si o alta functie int RemoveAt(int i) care sa elimine un element din vector de la pozitia data i. Folosind aceste functii, sa se insereze in vectorul creat mai sus un element cu valoarea 0 intre numerele 22 si 33 si sa se elimine numarul 44 din vector. Sa se afiseze la consola vectorul rezultat.
E2.8 Pentru operatii asupra unui vector de intregi de tipul IntArray se defineste o functie fint() care are ca argument de apel un obiect din clasa IntArray, astfel
void fint(IntArray a)
Ce se intampla la apelul acestei functii din functia main()? Ce fel de eroare poate sa apara si de ce? Care sunt remediile posibile?
E2.9 Sa se defineasca o functie membra int Append(IntArray &array) a clasei IntArray care sa adauge un vector la sfarsitul unui alt vector. In functia main() se va crea un vector cu continutul si un alt vector cu continutul . Sa se ataseze primul vector la sfarsitul celui de-al doilea vector. si sa se afiseze vectorul rezultat obtinut.
E2.10 Sa se definesca o functie membra int InsertAt(int i, IntArray &array) a clasei IntArray care sa permita inserarea unui vector in interiorul altui vector, incepand cu o pozitie data. In functia main() se va crea un vector cu continutul si un vector cu continutul 11 12 13 14 15. Sa se insereze primul vector intre elementele si ale celui de-al doilea vector si sa se afiseze vectorul rezultat obtinut.
E2.11 Se modifica functia:
int InsertAt(int i, IntArray &array); astfel:
int InsertAt(int i, const IntArray &array).
Ce modificari trebuie sa fie aduse celorlate functii membre ale clasei IntArray pentru a se compila si executa programul de mai sus?
|