Tablouri de Obiecte
In C++ tablourile sunt tipuri
derivate, create utilizand operatorul de declarare [
] in combinatie cu un tip fundamental (exceptand tiupl void)
sau un tip definit de utilizator. Tablourile pot fi unidimensionale
sau
multidimensionale. Modul de declarare a tablourilor, cunoscut
din C, se regaseste integral si in C++. In C++ tipul elementelor
tabloului poate
fi o clasa, caz in care spunem ca am declarat un tablou de obiecte.
Initializarea
obiectelor
dintr-un tablou
Asa cum am vazut deja in
lucrarea
anterioara la instantierea unui obiect, pe langa alocarea spatiului
necesar de memorie, are loc si initializarea
obiectului prin
lansarea in executie a unuia dintre constructorii clasei, corespunzator
formei de declaratie. Intrucat crearea unui tablou de n
obiecte
reprezinta de fapt instantierea "in bloc" a n obiecte, este
normal ca pentru fiecare element al tabloului sa existe cate un apel al
unui constructor. Deci, pentru un tablou de n obiecte
se vor executa n apeluri de constructor. Intrebarea care se
ridica este cum putem specifica modul de initializare a elementelor tabloului,
adica cum putem indica in declaratia tabloului care constructor sa fie
apelat pentru a realiza initializarea? Se disting trei moduri de initializare
sintetizate in exemplul de mai jos:
| class
Complex {
public: Complex() {re=0; im = 0;} Complex(int i) {re=i;} Complex(int i, int j) {re=i; im=j;} private: int re; int im; }; ... main()
{
... } |
Accesarea elementelor
tabloului.
Operatia fundamentala asupra
variabilelor de tip tablou este indexarea,
adica accesarea unui element de indice dat. Operatorul de indexare
este
'[ ]'. Astfel, referirea
elementului cu indice i dintr-un tablou
T se realizeaza cu expresia
T[i].
Tipul tablou reprezinta
de fapt o concretizare a conceptului de
secventa.
O secventa de elemente apartinand unei multimi M este o functie
definita pe multimea numerelor
naturale, cu valori in M (M trebuie sa fie
o
multime discreta). Practic, secventa realizeaza numerotarea elementelor
lui M, inducand astfel automat
o ordonare a multimii respective.
Generalizarea Tablourilor
Lipsa de
generalitate
a tipului tablou, asa cum este el implementat in limbajele
de programare este data de urmatoarele
restrictii:
Pointeri
In C++ pointerii sunt tipuri
derivate, create utilizand operatorul
de declarare * in combinatie cu un
tip fundamental sau definit
de utilizator. Modul
de declarare a pointerilor cunoscut din C se regaseste integral si in C++.
Un pointer la un anumit tip T nu este altceva decat o
adresa la
care se gaseste (sau s-ar putea gasi) un obiect de tip
T.
Atentie: Declaratia unei variabile de tip pointer nu face decat sa rezerve spatiul necesar memorarii unei adrese; O astfel de declaratie nu initializeaza variabila declarata cu valoarea unei adrese particulare si nici nu rezerva spatiu pentru vreun obiect valid spre care sa indice variabila respectiva. Daca tipul pointerului este o clasa, observatia de mai sus ramane valabila, consecinta fiind ca o asemenea declaratie nu va declansa executia nici unui constructor!!!
Pointeri la Tablouri si
Functii.
O notatie mai complicata
se foloseste in cazul pointerilor la tablouri
si functii:
| int
(*vp)[10]; //vp este pointer la tablou de 10
//int-uri int (*fp)(char,float); // fp este pointer la o //functie cu parametrii (char,float) si tip //returnat int |
Pointerul zero
In C++ valoarea 0 este in
mod normal considerata ca fiind de tip int, dar, ca urmare a
existentei unui set de reguli de conversie automata, ea poate fi folosita
ca o constanta a oricarui tip intreg, virgula flotanta, pointer sau
pointer la membru. Tipul actual al constantei se deduce din context.
Deoarece nici unui obiect nu ii este alocata in memorie adresa 0,
aceasta constanta poate fi folosita in lucrul cu tipurile pointeri ca o
valoare speciala, pentru a indica faptul ca un pointer nu refera nici un
obiect, deci ca echivalent al lui 'nici-o-adresa'.
In C a fost incetatenita practica de a defini un macro numit
NULL pentru a reprezenta pointerul zero. Si in C++ se poate
folosi acest macro, dar deoarece in acest limbaj mecanismele de verificare
a tipurilor sunt mai puternice, se recomanda fie utilizarea literalului 0
ca atare, fie definirea unei constante NULL sub forma:
Initializarea unei variabile pointer
O variabila pointer
poate indica la un moment dat fie un obiect alocat
static, fie unul alocat dinamic. In
cazul obiectelor alocate static avem urmatoarele exemple de initializare:
| int x;
int *pi=&x; //pi se initializeaza cu adresa lui x int func2(char c, float f)
{. .. }
|
Operatorii new
si delete
In C++ au fost introdusi
2 operatori pentru alocarea dinamica a memoriei: new-pentru
alocare si delete-pentru eliberare. Un exemplu
de utilizare a lor a fost dat deja in lucrarea precedenta.
In afara de modificarile de sintaxa, cei 2 operatori aduc in plus fata de functiile malloc/free un lucru important din perspectiva lucrului cu clase, si anume:
Operatia fundamentala efectuata
asupra unei variabile pointer este
dereferentierea sau indirectarea, adica referirea obiectului
indicat de pointer. Operatorul de indirectare
este '*'.
Exemple:
Una din cele mai frecvent
intalnite categorii de erori in programele
C++ este legata de lucrul cu pointeri. In aceasta categorie
intra:
char *sir, a;
int *nr = NULL, b;
a = *sir; // oops!
b = *nr; // idem
char *sir;
int a, *nr = &a;
delete sir; // you, small brain!
delete nr; // --"--"-
int *tnr = new int [6];
delete tnr; // in loc de delete[ ] tnr;
//. . .
delete ptr;
if (!ptr) {...}; //maybe yes, maybe no...
char *sir=new char[6];
strcpy(sir,"macrostabilizare"); //sir nu are suficient spatiu
ceea ce se intampla aici este ca se aloca spatiu pentru a memora 1
(!!!) obiect de tip tip, obiectul respectiv fiind
initializat (daca e
posibila conversia) cu valoarea n;
ceea ce s-ar fi dorit de fapt, ar fi fost rezervarea de spatiu pentru
n
obiecte.
tip *pt=new tip(n); //in loc de tip[n]
T *p = new T[10], *q;
for(int i=0, q=p; i<10; i++, q++)
q = &p[5]; delete[ ] q; //biiig mistake!!!
delete q; //don't do that!!!!
delete[] p; //corect; este singura posibilitate de stergere!!!
Majoritatea programelor care contin asemenea erori se soldeaza, pe langa anomaliile de executie, cu afisarea mesajului "Segmentation fault" (sub Unix) sau "Null pointer assignment" (sub DOS).
Pointerul this
Acest simbol poate fi referit
doar in interiorul functiilor membru
non-statice ale unei clase. El este numele generic al pointerului spre
obiectul curent, adica obiectul
pentru
care se executa la un moment dat functiile respective
(poate fi interpretat ca adresa obiectului vazuta dinspre interiorul
acestuia).
Am vazut in lucrarea precedenta ca, daca
intr-un program se creaza mai multe obiecte ale unei clase, din
perspectiva utilizatorului lucrurile apar ca si cum fiecare obiect isi are
propriile lui exemplare ale membrilor non-statici ai clasei. Pentru datele
membru acest lucru este chiar adevarat. In cazul functiilor membru insa,
fizic codul lor este stocat in memorie intr-un singur exemplar, iar
prototipul real al acestor functii contine ca prim parametru un pointer la
clasa respectiva:
Asa se explica faptul ca orice referire simpla a unui membru non-static
d
in interiorul unei functii membru non-statica este echivalenta cu:
this->d.
class AClass {
ReturnType MemberFunc(lista_param_form);
}
//. . .
//functia AClass::MemberFunc are prototipul real
ReturnType MemberFunc(AClass *this,lista_param_form);
Un apel de forma:
se traduce de catre compilator prin:
AnObject.MemberFunc(lista_param_act);
Acest lucru este insa transparent pentru utilizator.
MemberFunc(&AnObject, lista_param_act);
Folosind
constructia *this se realizeaza referirea intregului
continut al unui
obiect, din interiorul lui insusi.
Tablouri si
pointeri
Intre un nume de tablou static si un pointer exista totusi cateva
deosebiri importante (in afara de accea ca elementele unui tablou static
se afla in stiva, iar cele ale unui tablou dinamic se afla in heap). Vom
vedea in continuare care sunt acestea:
In limbajul C notiunea
de pointer a fost extinsa astfel incat un
nume de tablou poate fi asimilat cu un pointer spre inceputul tabloului.
Cu alte
cuvinte,
fiind dat un tablou de forma:
numele tab poate
fi folosit
apropape in toate locurile in care se asteapta un pointer la tipul
tip_elem.
tip_elem tab[n];
Exemple:
In concluzie, asimilarea numelor de tablouri cu pointerii si invers, se
refera la modul de accesare a elementelor tablourilor respective; dupa cum
s-a vazut in exemplele anterioare, putem aplica operatorul de indexare
asupra unui pointer si, respectiv, operatorul de indirectare asupra unui
nume de tablou.
aici e locul sa facem precizarea ca in C/C++ transmisia tablourilor ca
parametri nu
se face niciodata prin valoare, ci numai prin adresa; acest lucru este
valabil chiar daca functia func de mai sus ar avea
prototipul:
tipf func(tip_elem *p, alti_param_form) {
//. . .
}
tip_elem tab[10];
//. . .
func(tab, alti_param_act);
sau
tipf func(tip_elem p[ ], alti_param_form);
tipf func(tip_elem p[10], alti_param_form);
Este valabila si situatia inversa, adica folosirea unui pointer ca nume de
tablou intr-o operatie de indexare:
*tab = val_elem; //se initializeaza primul elem. al tabloului
tip_elem *pe = new tip_elem[k]; //se aloca spatiu contiguu
for(int i=0;i < k;i++)
//pt k obiecte de
tip tip_elem
pe[i] = expr; //se initializeaza al (i+1)-lea obiect
Pe scurt, numele de tablouri alocate static pot fi utilizate doar ca
referinte constante.
tip_elem tab[n], altu[n], *ptab = new tip_elem[k];
tab = altu; //eroare
tab = ptab; //eroare
tab = new tip_elem[q]; //eroare
delete[ ] tab; //eroare
delete[ ] ptab; //corect
ptab = altu; //corect
delete [ ] ptab; //acum eroare (vezi eroarea 2 cu
pointeri)
In schimb, in interiorul unei functii careia i-a fost transmis
tab ca
parametru, acesta nu mai este perceput ca tablou static, ci ca pointer,
chiar daca in prototipul functiei el apare ca tablou (vezi exemplul).
Drept
urmare, in interiorul unei asemenea functii nu mai putem aplica operatorul
sizeof pentru a afla dimensiunea reala a tabloului cu care
se lucreaza la
un moment dat. Singura solutie in acest sens este de a transmite si
dimensiunea ca parametru.
sizeof(tab) = n * sizeof(tip_elem) si
sizeof(ptab) = sizeof(tip_elem*)
In programele C++ nu se poate
realiza nici la nivelul compilarii, nici
al executivului, verificari privind depasirea limitelor indicilor de
tablouri, indiferent daca
acestea au fost alocate static sau dinamic. Tablourile
sunt tipuri de baza, iar la crearea unui tablou nu se memoreaza nicaieri
dimensiunea
lui. Situatiile in care au loc depasiri ale
limitelor tablourilor nu sunt sesizate ca exceptii, iar
efectele sunt nedefinite.
Tablouri de
pointeri
Un tablou de pointeri la
un tip T poate fi asimilat cu o matrice
de elemente de tip T, diferenta fiind ca, in primul caz
liniile matricei pot avea lungimi
diferite:
Aritmetica
pointerilor
In limbajul C++ s-a definit
un set de operatii aritmetice in care unul
din operanzi este un pointer, iar celalalt o expresie de tip intreg.
Astfel: orice constructie
de forma: p[i], unde p este un pointer la un
tip T sau numele unui tablou cu elemente de
tip T, este echivalenta cu
*(p+i), cu
conditia ca T sa nu fie
void. Cu
alte cuvinte, p+i reprezinta adresa unui obiect aflat la
deplasament i*sizeof(T) fata de adresa
indicata de p.
Este posibila si aplicarea
operatiei de scadere, sub forma: p-i, expresia reprezentand
adresa
unui obiect aflat la deplasament (-i)*sizeof(T) fata de
adresa
indicata de p. Este evident ca daca avem
secventa:
a doua instructiune reprezinta
o tentativa de accesare a unui spatiu de memorie
la care nu avem dreptul, rezultatul fiind o comportare nedefinita
a programului (vezi eroarea 5 cu pointeri). In schimb,
secventa:
int *p=new int[5];
*(p-2)=6;
este corecta.
int *p=new int[5], *q;
for(i=5,q=p+5;i>0;i--) *(q-i)=expr;
Este posibila si scaderea
intre 2 pointeri, cu conditia ca ei sa fie
de acelasi tip si, in plus, sa indice elemente ale aceluiasi tablou.
Aceasta ultima conditie
nu poate fi verificata automat, asigurarea ei fiind
o sarcina a programatorului. Daca avem urmatoarea secventa:
rezultatul expresiei q-r
este un numar intreg, reprezentand numarul elementelor
tabloului p aflate intre pointerii q si
r.
T *p=new T[n];
T *q,*r;
q=p+i; // i<n
r=p+j; // j<n
Referinte
Prin referinta la un obiect
se intelege o alternativa la numele obiectului.
Notatia X& inseamna referinta la un obiect de tip X.
Exemple:
In continuare, simbolurile
i si alt_i vor putea fi utilizate in domeniul lor
de vizibilitate unul in locul altuia. Astfel:
int i = 4;
int& alt_i = &i; //i si alt_i sunt nume ale aceluiasi obiect
alt_i = 5; //efectul: i=5
i = 7;
int j = alt_i+2; //efectul: j=9
alt_i++; //efect: i=8
Notiunea de referinta a fost
introdusa in limbajul C++ in primul rand
cu scopul de a rezolva problema parametrilor de intrare/iesire ai unei functii intr-o maniera
mai comoda pentru programator decat o fac pointerii.
Exemplu: fie functia:
apelul la o asemenea functie
ar fi: func(&a), unde a este o variabila de tip
int.
void func(int *p) {
//. . .
}
*p = ...;
//. . .
In varianta cu referinte,
aceeasi functie, respectiv apel ar arata
astfel:
void func(int &p) {
//. . .
}
p = ...;
//. . .
. . .
func(a);
In ambele variante modificarile aduse parametrului formal in timpul executiei lui func se pastreaza la revenirea din functie. Se observa ca in varianta a doua, referirea parametrilor, atat a celui formal, cat si a celui actual, se face mai natural, intr-o maniera similara manipularii parametrilor declarati cu var in programele Pascal.
Nu intotdeauna insa scopul
transmiterii de parametri prin referinta
este acela ca ei sa serveasca drept parametri de iesire, ci acela de economisire de
spatiu. Corespunzator parametrilor declarati ca referinte,
de fapt se transmite adresa parametrilor de apel. Ca urmare, pentru un tip T care satisface
conditia
In cazul in care tipul T reprezinta o clasa, transmiterea prin referinta a obiectelor de tip T ca parametri in apelul unei functii evita executia copy-constructorului clasei respective.
Referintele pot fi utilizate
si in cazul rezultatelor returnate de
unele functii. In acest caz apelurile la functiile respective vor
reprezenta expresii lvalue, deci vor putea sa
apara ca membri
stangi in atribuiri, de exemplu.
Problema
Sa se implementeze in C++,
in varianta cu alocare
dinamica
stiva de la problema din lucrarea
precedenta, respectand aceeasi
interfata. Clasa va
trebui sa aiba implementati urmatorii constructori:
Bibliografie
[Cop94]
J. Coplien - "Advanced C++ Programming Styles and Idioms",
Addison-Wesley, 1991
[Str97]
B. Stroustrup - "The C++ Programming Language", 3rd ed., AT&T,
1997