![]()
![]()
In
loc de "Hello world!" ... o Stiva
Vom incepe discutia despre clase si obiecte prin prezentarea unui exemplu sugestiv, si anume realizarea si utilizarea unei stive in C++:
| stack.h | stack.cc | main.cc |
| #ifndef
__STACK_H
#define __STACK_H const int STACK_SIZE=10; class
Stack {
inline
int Stack::pop()
{
#endif
|
#include
"stack.h"
Stack::Stack()
{
Stack::Stack(int
size) {
Stack::~Stack()
{
int
Stack::top()
{
void
Stack::push(int
i) {
|
#include
<iostream.h>
#include "stack.h" void
main(void) {
q.push(1);
|
In
fisierul stack.h
vedem felul in care se defineste o clasa
in C++. Observam ca declaratia clasei este impartita in doua sectiuni
delimitate de cuvintele-cheie public
resp. private,
denumite
specificatori
de acces. Existenta acestor sectiuni face posibila delimitarea
clara a interfetei clasei de implementarea sa, realizandu-se astfel materializarea
conceptului de incapsulare .
Astfel,
tot ceea ce este declarat intr-o sectiune public
a unei clase apartine interfetei acelei clase, fiind prin urmare accesibil
din toate celelalte functii din program. Din acest motiv sunt declarate
aici metodele (functiile membru) prin care se vor efectua operatiile uzuale
pentru o stiva (push, pop, top), ce au fost determinate in urma procesului
de abstractizare.
Sectiunile
privateale
unei clase vor contine declaratiile acelor membri ai clasei (date si metode)
care contribuie la implementarea clasei si care conform principiului incapsularii
trebuie ascunse de "ochii lumii", adica nu au voie sa fie accesate decat
din metodele ce apartin respectivei clase. In cazul exemplului nostru au
fost declarate aici datele prin care este reprezentata stiva, adica tabloul
de numere intregi in care sunt stocate elementele sitivei (int
*items) resp. indicatorul varfului
de stiva (int sp).
In sectiunea public, pe langa cele trei metode corepunzatoare operatiilor cu stiva, mai sunt declarate trei metode avand o forma speciala:
In
fisierul stack.ccavem
implementarea metodelor clasei Stack.
Primul lucru pe care il observam in semnatura fiecarei functii membru este
constructia:
Simbolul ":: " este numit operator de specificare a domeniului (scope resolution specifier). Important este ca el spune compilatorului ca aceasta implementare a functiei nume_metoda apartine clasei nume_clasa. Rezulta asadar ca mai multe clase pot folosi acelasi nume de functie. Prin intermediul constructiei de mai sus se obtine o identificare univoca a unei functii in intreg programul.
In implementarea celor doi constructori ai clasei Stack, vedem cele doua tipuri de operatii care se executa in mod normal intr-un constructor:
Atentie:
Constructorii
(si nu alte metode) sunt locul in care trebuie plasata initializarea unui
obiect. Daca se ignora implementarea constructorilor pentru o clasa, instantele
acelor clase vor fi neinitializate! Tot astfel, pentru orice clasa
care contine atribute ce au fost alocate dinamic in constructor, trebuie
implementat explicit destructorul in care sa aiba loc eliberarea memoriei
alocate.
Programare Structurata vs. Programare Obiectuala. Discutie comparativa.
In cele ce urmeaza vom compara implementarea de mai sus a stivei cu implementarea intr-o maniera procedurala (in C) a aceleeasi stive. Cele doua implementari pe baza carora vom purta discutia sunt prezentate aici.
In programarea structurata (procedurala), multe programe au bibliotecile si modulele organizate in jurul unei structuri de date, cum este si stiva din exemplul nostru: datele sunt stocate intr-o structura si exista un numar de functii (proceduri) strans cuplate cu respectiva structura, care opereaza pe variabile "instante" ale structurii. Caracteristica fundamentala a versiunii procedurale a programului o reprezinta centrarea lui in jurul functiilor. Cu alte cuvinte obiectele de date trebuie sa fie "la indemana" functiilor, in loc ca functiile sa fie "la indemana" datelor. Aceasta perspectiva se reflecta la nivelul implementarii prin aceea ca functiilor care opereaza asupra unei structuri de date trebuie sa li se transmita explicit instante ale structurii, ca parametri.
In varianta obiectuala lucrurile stau exact invers, intrucat din cauza cuplarii lor stranse cu structura de date, privim functiile ca apartinand structurii. Astfel, In loc sa mai transmitem structura Stack ca parametru fiecarei functii asociate, plasam aceste functii in interiorul structurii. Deoarece vor apartine structurii, aceste functii se vor numi functii membru.
Observati ca functiile membru nu fac referire la vreun pointer spre structura de date, si nici macar nu acceseaza explicit vreo instanta a structurii de date, asa cum se intampla in cazul implementarii procedurale. Acest lucru se datoreaza faptului ca interiorul unei clase este considerat un domeniu aparte, iar functiile pot fi privite ca existand inauntrul instantelor structurii.
In
programul C++ fiecare declaratie a unei "variabile" de tip
Stack aloca
spatiu undeva in memorie, la fel cum se face si pentru structurile din
C. De exemplu, instantei q
din main
i se aloca spatiu la executie. Aceasta instanta se numeste obiect
al clasei Stack.
Functiile membru ale unei clase sunt aplicate obiectelor sale. Expresiile
din aceste functii care se refera la membri ai clasei utilizeaza datele
alocate fizic pentru obiectul care le apeleaza.
Clasele sunt create utilizandu-se asa cum am vazut deja cuvantul-cheie class. Clasele reprezinta mecanismul fundamental de abstractizare utilizat in C++. Declararea a unei clase defineste un nou tip care uneste cod si date, si care apoi este folosit pentru a declara (instantia) obiecte din clasa respectiva. Putem spune ca o clasa este o abstractizare logica, in timp ce un obiect are o existenta fizica.
Din punct de vedere sintactic constructia classeste similara cu constructia struct. Iata forma generala a unei declaratii de clasa care nu mosteneste nici o alta clasa:
Din declaratiile de mai sus se desprind urmatoarele observatii:nume-clasa lista de obiecte;

Notatia UML pentru obiecte este data de un dreptunghi simplu, in interiorul caruia apare inscris numele obiectului si/sau a clasei in una din urmatoarele trei forme figurate mai jos:

Specificatorii
de Acces
Asa
cum am vazut in exemplul de mai sus, pentru a implementa
mecanismul de ascundere a informatiei (incapsulare a datelor) au fost introdusi
in limbajul C++ un numar de specificatori de acces prin care se stabilesc
in mod diferentiat drepturile de acces asupra membrilor unei clase. Acesti
specificatori de acces sunt in numar de trei si anume: private,
public
si protected.
Aparitia unuia dintre aceste cuvinte-cheie urmat de simbolul doua-puncte
(":") determina gradul de protectie pentru toti membrii care sunt declarati
in continuare, pana la sfarsitul clasei sau pana la intalnirea urmatorului
specificator de acces. Aceste atribute de protectie au urmatoarea semantica:
Ce
sunt de fapt functiile inline? Ele sunt functii al caror cod va
fi expandat la fiecare apel al lor: compilatorul, in loc sa genereze
o secventa clasica de apel, pur si simplu va insera in locul respectiv
tot corpul de instructiuni al functiei. Cu alte cuvinte functiile inline
vin sa inlocuiasca intr-o maniera eleganta macro-urile din C.
Avantajele
folosirii functiilor inline fata de macro-uri sunt:
Cand
declaram o functie inline? Dezavantajul utilizarii functiilor inline
consta in cresterea dimensiunii codului din cauza duplicarii instructiunilor.
Asadar vom declara inline doar functii de dimensiuni
reduse si care au un impact major asupra performantelor programului.
Are sens sa declaram o functie inline atunci cand executia instructiunilor
de apel, respcetiv de revenire din functie ar egala sau depasi ca timp
executia instructiunilor propriu-zise ale functiei.
Declararea
inline
a unei functii membru dintr-o clasa se poate face explicit, ca in exemplul
nostru, sau implicit prin includerea definitiei functiei in direct
in declaratia clasei, ca mai jos:
| class
Stack {
public: .... int pop() { return items[sp--]; } .... }; |
Constructorii
Doua
idei importante care au stat in spatele proiectarii limbajului C++ au fost:
Asa
cum am vazut deja in primul exemplu o functie constructor
este o functie-membru speciala care are intotdeauna acelasi nume cu clasa
in care este definita si care are ca principal scop initializarea starii
obiectelor la crearea lor.
Fiecare
clasa are cel putin 2 (doi) constructori.
Caracteristici
Se
pot enumera urmatoarele caracteristici ale constructorilor:
Complementul
functiei-constructor este destructorul. In multe cazuri, un obiect va trebui
sa efectueze o anumita suita de actiuni atunci cand urmeaza sa fie distrus.
Aceasta suita de actiuni constituie activitatea destructorului.Un obiect
se distruge in una din urmatoarele trei situatii:
- cand este parasit blocul in care a fost creat (obiecte locale)
- la terminarea programului (obiecte globale)
- cand se aplica operatorul delete
(obiecte create dinamic cu new).
In
cazul in care nu este definit explicit un destructor, compilatorul va genera
automat unul cu corpul de instructiuni vid .
Cand
trebuie sa definim un destructor? Definim
explicit un destructor atunci cand la crearea obiectului au fost alocate
prin
program resurse. Cele mai intalnite alocari sunt alocarile dinamice
de memorie si deschiderea de fisiere. Astfel, in destructor se dezaloca
memoria si se inchid fisierele deschise.
Atentie: Functiilemalloc, respectiv free utilizate in C pentru alocarea si dezalocarea de memorie nu determina lansarea in executie a constructorilor, respectiv a destructorului! In locul lor, in C++ folosim operatorii new si delete, despre care vom vorbi in lucrarea urmatoare.
Caracteristici
Se
pot enumera urmatoarele caracteristici ale destructorilor:
Clase
si Structuri
Spuneam
mai
sus ca exista o relatie de similitudine intre constructiille class
si struct,
ultima fiindu-ne cunoscuta din C. Limbajul C++ accepta declaratiile de
tip struct
pastrand atat sintaxa cat si semantica din C. Constructiile de tip struct
din C++ au fost insa extinse pentru a se comporta la fel ca si cele de
tip class.
Singura diferenta intre struct si class este
legata de drepturile de acces implicite: daca pentru classmembrii
sunt implicit privati, pentru structmembrii
sunt considerati implicit publici.
In
practica exista o regula clara privind folosirea uneia sau a celeilalte
constructii: cuvantul-cheie classe
folosit pentru a sublinia definirea unui TDA (tip de date abstract), si
nu doar a unei agregari (grupari) de date. Asadar, atunci cand sunt grupate
impreuna atat date cat si functii se utilizeaza class;
cand se grupeaza doar date se foloseste struct.
Este
posibil sa permitem unei functii care NU este membru al unei clase
sa aiba
acces nerestrictiv la toti membrii acelei clase (inclusiv la cei privati),
declarand respectiva functie ca prieten (friend) al clasei. Pentru a face
acest lucru trebuie ca prototipul functiei, precedat de cuvantul-cheie
friend
sa fie inclus in declaratia acelei clase.
Observatie: Cand spunem
ca o functie f declarata ca friend intr-o clasa
AClass are acces nerestrictiv la membrii acestei
clase, inseamna ca functia f poate referi orice
membru al unui obiect de tip AClass, obiect care
poate fi: parametru/variabila locala in f (aceasta este
situatia cea mai frecventa), sau variabila globala. Afirmatia este valabila indiferent
daca obiectul respectiv este referit direct, prin numele lui, sau
indirect, printr-un pointer ori o
referinta. Totodata, f
poate
accesa orice membru static din
AClass (folosind notatia
cunoscuta
AClass::nume_membru_static).
Vom
extinde exemplul cu stiva, de mai sus, incluzand
o declaratie ca friend a unei functii care concateneaza doua stive date
ca parametru (asezand-o pe cea de-a doua peste prima):
| class
Stack {
public: .... friend void StackMerge(Stack&, Stack&); }; .... void
StackMerge(Stack &s1, Stack &s2) {
.... void
main(void) {
q.push(1); q.push(2);
|
Trebuie facuta observatia ca in acest exemplu nu s-a castigat nimic facand ca StackMerge sa fie functie friend, ea putand la fel de bine sa fie o functie membru. Exista insa conditii in care existenta functiilor friend se justifica: este vorba in special de supraincarcarea anumitor operatori (vezi Lucrarea 4 - Supraincarcarea Functiilor si a Operatorilor) sau de crearea anumitor tipuri de functii I/O (vezi Lucrarea 7 - Sistemul de I/O in C++ . Streamuri).
Sa vedem acum CINE poate sa primeasca statutul de friend al unei clase:
| class
AClass;
class
AnotherClass {
|
In acest caz poate sa apara si situatia particulara in care dorim ca TOATE functiile din clasa AnotherClass sa aiba statut de friend pentru clasa AClass. Spunem atunci ca AnotherClass este o clasa friend pentru AClass. Limbajul C++ ne pune la dispozitie o notatie care permite declararea unei intregi clase ca friend, in loc de a insira toate functiile ei explicit:
| class
AClass{
friend class AnotherClass; //. . . }; |
Elemente caracteristice ale statutului de friend:
-daca clasa B este friend in A si clasa
C este friend in B, nu inseamna
ca C este
friend in A;
-daca functia f este friend in A si
A este friend in B, nu inseamna
ca f este
friend pentru B.
Atentie:Trebuie
mereu avut in vedere ca oricat de utile sau comode ar fi aceste functii
sau clase "friend" utilizarea lor reprezinta o incalcare a principiului
incapsularii si prin urmare ele trebuie utilizate doar atunci cand acest
lucru se impune cu adevarat! O discutie mai
detaliata pe aceasta tema se gaseste in Anexa
2.
Membrii
Statici
Un
tip aparte de membri ai unei clase sunt membrii statici. Atat
functiile membru cat si datele membru (atributele) unei clase pot fi declarate
static.
Variabile
Membru Statice
Daca
declaratia unei variabile membru este precedata de cuvantul-cheie static,
atunci va exista o copie unica a acelei variabile care va fi folosita in
comun de catre toate obiectele instantiate din respectiva clasa. Spre deosebire
de variabilele membru obisnuite, pentru variabilele statice nu sunt create
copii individuale ale acestora pentru fiecare obiect in parte. Accesarea
unei variabile statice se face folosind numele clasei si operatorul de
specificare a domeniului ("::").
Atentie: Cand declarati o data membru ca fiind static intr-o clasa, ea nu este inca definita! Cu alte cuvinte, prin declarare nu se aloca memorie, acest lucru facandu-se doar prin definire. Pentru a defini o variabila membru statica, aceasta trebuie prevazuta cu o definire globala undeva in afara clasei. Variabilele statice (ca si functiile membru statice de altfel) pot fi utilizate indiferent daca exista sau nu instante ale clasei respective. De aceea, initializarea unei variabile statice NU poate cadea in sarcina constructorilor clasei (constructorii, se stie, se executa doar la momentul generarii unei instante). Constructorii pot, insa, sa modifice valorile variabilelor statice (de exemplu, pentru a contoriza numarul instantelor unei clase, create la executia unui program).
Variabilele
membru statice sunt folosite cel mai adesea pentru a asigura controlul
accesului la o resursa comuna (ex. scrierea intr-un fisier).
Acest
tip de variabile se mai folosesc si pentru a stoca informatii comune unei
intregi clase de obiecte.
Functii
Membru Statice
Si
functiile membru pot fi declarate ca statice. In C++ o functie membru statica
se comporta asemanator cu o functie globala al carei domeniu este delimitat
de clasa in care este definita.
Functiile membru statice pot avea urmatoarele intrebuintari:
| class
X {
public: void foo() {fooCounter++; . . . } static void printCounts() { printf("foo called %d times \n", fooCounter); } private: static int fooCounter; //. . . } int X::fooCounter = 0; //initializarea unei variab. statice int
main() {
|
Intrebari si Probleme
1. Modificati clasa Stack,
renuntand la unul dintre constructorii definiti initial, in asa fel incat
aceasta sa nu afecteze programul principal.
2. Scrieti un program de test in C++ in care sa vizualizati diferitele
situatii in care sunt apelate fiecare tip de constructori , precum si mecanismul
de apel al destructorului. Vizualizarea se va face prin afisarea de mesaje
din corpul constructorilor resp. al destructorului.
3. Considerand definitia propusa initial pentru clasa Stack,spuneti
care este greseala care va face ca secventa de mai jos sa nu aiba efectul
scontat, si anume acela de a obtine doua stive (r
si q)
cu continut asemanator.
| main()
{
Stack q(5); //cream o stiva de cinci elemente
q.push(1); q.push(2); // ... si punem in ea
Stack r = q; // cream o noua stiva initializata prin
|
Bibliografie
[Cop94]
J. Coplien - "Advanced C++ Programming Styles and Idioms",
Addison-Wesley, 1991
[Sch97]
H. Shildt - "C++. Manual complet", Editura TEORA,
1997