Curs de C++ - Cuprins
Nota: toate exemplele de program din acest curs pot fi gasite la:
exemple.tar.gz
exemple.zip
Cap.2 Notiunile de baza ale programarii orientate
obiect
Motto: "Procesul programarii are mai multe faze: se incepe cu definirea
CONCEPTELOR, dupa care se trece la stabilirea RELATIILOR dintre ele. Abia dupa
aceea se poate trece la scrierea codului" (Bjarne Stroustrup)
2.1. Premisele limbajelor orientate
obiect
In ultimii ani, programarea orientata pe obiecte a devenit foarte populara,
mai ales datorita avantajelor sale care ajuta dezvoltarii proiectelor actuale,
ce devin din ce in ce mai complexe. Acest stil de programare duce la impartirea
aplicatiilor in mai multe module, astfel incat cel ce dezvolta un modul nu
trebuie sa cunoasca detaliile de implementare a altor module.
Nu in ultimul rand, trebuie sa amintim ca programarea orientata pe obiecte
este un concept foarte natural. In lumea inconjuratoare, zi de zi, in orice
moment, avem de-a face cu Obiecte. Imprejurul nostru sunt o multitudine
de obiecte, interconectate intre ele, comunicand unele cu altele
intr-un fel sau altul.
Domeniul in care acest stil de programare s-a dovedit cel mai util este
dezvoltarea interfetelor utilizator si a aplicatiilor bazate pe acestea.
Programarea structurata este bazata pe ecuatia enuntata de Niklaus
Wirth:
Structuri de date + Algoritmi = Program
Programarea structurata a fost o etapa ce a trebuit sa fie depasita,
deoarece este deficitara in ceea ce priveste posibilitatea reutilizarii
programelor, scalabilitatii si extinderii unor module de program, atribute de
neinlocuit in realizarea aplicatiilor complexe. Principala deficienta a
programarii structurate consta in tratarea separata a algoritmilor si a
structurilor de date ce se prelucreaza. De obicei, in natura, o entitate este
caracterizata atat printr-o structura, cat si printr-un anume comportament. In
mod normal, obiectele evolueaza in timp, adeseori modificandu-si structura si
functionalitatea.
2.2. Concepte fundamentale
Ideea de baza de la care pleaca programarea orientata obiect este de a grupa
structurile de date cu operatiile care prelucreaza respectivele date. Un
asemenea ansamblu poarta denumirea de obiect sau clasa.
Proiectarea de programe utilizand clase se numeste programare orientata pe
obiecte (OOP).
In mod frecvent, pentru structurile de date se utilizeaza denumirea de
date membre sau campuri, iar pentru procedurile ce prelucreaza
aceste date, termenul de functii membre sau metode.
In analogie cu ecuatia programarii structurate, se poate considera ca
valabila urmatoarea relatie:
Date + Metode = Obiect
Acest ansamblu este bazat pe principiul fundamental al incapsularii
datelor, conform caruia accesul la datele membre se poate face numai prin
intermediul setului de metode asociat. Acest principiu determina o abstractizare
a datelor in sensul ca un obiect este caracterizat complet de specificatiile
metodelor sale, detaliile de implementare fiind transparente pentru utilizator.
Acest aspect este hotarator in cazul proiectelor complexe, de dimensiuni mari,
care nu pot fi realizate decat cu ajutorul unor echipe de programatori.
Aplicatiile pot fi impartite cu usurinta in module, astfel ca cel ce dezvolta un
modul nu trebuie sa cunoasca detaliile de implementare a celorlalte module.
Consecintele imediate sunt scaderea timpului de dezvoltare a aplicatiilor,
simplificarea activitatii de intretinere a modulelor, si cresterea calitatii
programelor.
Privind limbajele orientate obiect ca o evolutie a limbajelor structurate,
constatam ca notiunea de clasa este o generalizare a notiunii de structura de
date. O clasa descrie un ansamblu de obiecte similare. Un obiect este asadar o
variabila de un anumit tip clasa. In mod uzual, se foloseste exprimarea ca un
obiect este instantierea unei clase.
Un alt concept important in cadrul programarii orientate obiect este cel de
polimorfism, care se refera la posibilitatea de a opera cu mai multe
variante ale unei functii, care efectueaza o anumita operatie in mod specific
pentru anume obiecte.
Evolutia si ierarhizarea claselor de obiecte se bazeaza pe conceptul de
mostenire. Astfel, procedeul numit derivare permite definirea unei
noi clase (clasa derivata) pornind de la o clasa existenta (clasa de
baza), prin adaugarea de noi date si metode, eventual redefinirea unor
metode. Clasa derivata mosteneste de la clasa de baza structura de date si
metodele aferente. Este posibila totodata si derivarea unei clase din mai multe
clase de baza, aceasta operatie fiind denumita mostenire multipla.
Asadar, dintr-o clasa de baza pot fi derivate mai multe clase si fiecare
clasa derivata poate deveni la randul ei o clasa de baza pentru alte clase
derivate. Se poate astfel realiza o ierarhie de clase, care sa modeleze
sisteme complexe. Construirea ierarhiei de clase constituie activitatea
fundamentala de realizare a unei aplicatii orientate obiect, reprezentand in
fapt faza de proiectare a respectivului sistem.
In capitolele urmatoare veti afla implementarea acestor concepte in limbajul
C++.
Cap. 3. Clase
3.1. Declararea claselor
O sintaxa simplificata a declararii unei clase este urmatoarea:
class NumeClasa { ... declaratii variabile
membre ... declaratii functii membre ... }
Dupa cum se poate observa din aceasta alcatuire a declaratiei, clasa este
asemanatoare cu o structura din limbajul C, dar care poate avea in componenta sa
membri atat de tip variabila cat si de tip functie. Asa cum spuneam si in
capitolul precedent, pentru datele din interiorul clasei se utilizeaza de obicei
termenul de date membre, iar pentru functii denumirea de functii
membre sau metode. O clasa permite incapsularea in interiorul
sau a datelor si a codului.
Intocmai ca in limbajul C, pentru a putea utiliza efectiv un tip de date (in
cazul de fata o clasa), trebuie sa definim o variabila de acel tip. Intr-un mod
similar declaratiei
int i;
putem scrie:
NumeClasa variabila
Vom considera de acum incolo ca variabila poarta numele de
obiect. Exprimarea uzuala este ca un obiect este instantierea unei
clase.
3.2. Membrii unei clase
Accesarea membrilor unei clase se face ca in cazul structurilor din limbajul
C:
obiect.VariabilaMembra = valoare
pentru
accesul la o variabila membra, si
obiect.FunctieMembra()
pentru apelarea
unei functii membre.
Pentru exemplificare sa consideram o implementare a notiunii de punct. Ca
variabile membre avem nevoie doar de coordonatele x si y care definesc pozitia
in spatiu a unui punct. Am mai declarat o functie care calculeaza aria
dreptunghiului avand colturile (0, 0) si (x, y).
class Point {
unsigned x, y;
unsigned long Arie()
{
return x * y;
};
unsigned GetX();
unsigned GetY();
void SetX(unsigned X);
void SetY(unsigned Y); };
unsigned Point::GetX() {
return x; }
unsigned Point::GetY() {
return y; }
void Point::SetX(unsigned X) {
x = X; }
void Point::SetY(unsigned Y) {
y = Y; }
Am folosit un operator nou, specific C++, ::, numit operator de
rezolutie, numit si operator de acces sau de domeniu. El
permite accesul la un identificator, dintr-un bloc in care acesta nu este
vizibil datorita unei alte declaratii locale. Un exemplu de folosire este
urmatorul:
char *sir = "variabila globala";
void
functie() {
char *sir = "variabila locala";
printf("%s\n", ::sir); // afiseaza variabila globala
printf("%s\n", sir); // afiseaza variabila locala }
Pentru definitiile functiilor membre aflate in afara declaratiei clasei este
necesara specificarea numelui clasei urmat de acest operator, indicand faptul ca
functia are acelasi domeniu cu declaratia clasei respective si este membra a ei,
desi este definita in afara declaratiei.
3.3. Crearea si distrugerea
obiectelor
Sa consideram urmatorul program C++:
void main() {
Point p; }
In momentul definirii variabilei p, va fi alocat automat spatiul de
memorie necesar, acesta fiind eliberat la terminarea programului. In exemplul de
mai sus, variabila p este de tip static. In continuare vom modifica acest
program pentru a folosi o variabila dinamica (pointer).
void
main() {
Point *p;
p = new Point;
p->x = 5;
p->y = 10;
printf("Aria = %d\n", p->Aria());
delete p; }
Ati observat utilizarea unor operatori pe care nu ii cunoasteti din limbajul
C: new si delete. C++ introduce o metoda noua pentru gestiunea
dinamica a memoriei, similara celei utilizate in C (malloc() si free()), dar
superioara si special construita pentru programarea orientata pe obiecte.
Operatorul new este folosit pentru alocarea memoriei, iar sintaxa
acestuia este:
variabila = new tip; variabila = new
tip(valoare_initiala); variabila = new tip[n];
Este usor de intuit ca prima varianta aloca spatiu pentru variabila
dar nu o initializeaza, a doua varianta ii aloca spatiu si o initializeaza cu
valoarea specificata, iar a treia aloca un tablou de dimensiune n. Acest
operator furnizeaza ca rezultat un pointer continand adresa zonei de memorie
alocate, in caz de succes, sau un pointer cu valoarea NULL (practic 0)
atunci cand alocarea nu a reusit.
Eliminarea unei variabile dinamice si eliberarea zonei de memorie aferente
se realizeaza cu ajutorul operatorului delete. Sintaxa acestuia
este:
delete variabila;
Desi acesti doi operatori ofera metode flexibile de gestionare a obiectelor,
exista situatii in care aceasta nu rezolva toate problemele (de exemplu
obiectele care necesita alocarea unor variabile dinamice in momentul crearii
lor). De aceea pentru crearea si distrugerea obiectelor in C++ se folosesc niste
functii membre speciale, numite constructori si destructori.
Constructorul este apelat automat la instantierea unei clase, fie ea
statica sau dinamica.
Destructorul este apelat automat la eliminarea unui obiect, la
incheierea timpului de viata in cazul static, sau la apelul unui delete
in cazul dinamic.
Din punct de vedere cronologic, constructorul este apelat dupa alocarea
memoriei necesare, deci in faza finala a crearii obiectului, iar destructorul
inaintea eliberarii memoriei aferente, deci in faza initiala a distrugerii
sale.
Constructorii si destructorii se declara si se definesc similar cu celelalte
functii membre, dar prezinta o serie de caracteristici specifice:
numele lor coincide cu numele clasei careia ii apartin; destructorii se
disting de constructori prin faptul ca numele lor este precedat de caracterul
~
nu pot returna nici un rezultat
nu se pot utiliza pointeri catre constructori sau destructori
constructorii pot avea parametri, destructorii insa nu. Un constructor fara
parametri poarta denumirea de constructor implicit.
De remarcat este faptul ca in cazul in care o clasa nu dispune de
constructori sau destructori, compilatorul de C++ genereaza automat un
constructor respectiv destructor implicit.
Sa completam in continuare clasa Point cu un constructor si un
destructor:
Point::Point() // constructor implicit {
x = 0;
y = 0; }
Point::Point(unsigned X, unsigned Y) {
x = X;
y = Y; }
Point::~Point() { }
Ati remarcat cu aceasta ocazie modul de marcare a comentariilor in C++: tot
ce se afla dupa caracterul // este considerat comentariu.
De notat este faptul ca definitii de forma
Point p;
sau
Point *p = new
Point();
duc la apelarea constructorului implicit.
O intrebare care poate apare este motivul pentru care am realizat functiile
GetX(), GetY(), SetX(), SetY(), cand puteam utiliza direct variabilele
membru x si y. Deoarece una din regulile programarii C++, adoptata
in general de catre specialisti, este de a proteja variabilele membru (veti afla
in capitolul urmator cum), acestea neputand fi accesate decat prin intermediul
unor functii, care au rolul de metode de prelucrare a datelor incapsulate in
interiorul clasei.
3.4. Conceptul de
mostenire
Daca intrebam un zoolog ce este un caine, ne va raspunde ca este un
reprezentant al speciei canine domesticus. Un caine este un tip de
carnivor, un carnivor este un tip de mamifer, si asa mai departe. Zoologul
imparte animalele in regn, clasa, ordin, familie, gen si specie. Aceasta
ierarhie stabileste o relatie de genul "este un/este o". Putem remarca
acest tip de relatie oriunde in aceasta lume: Mercedes este un tip de
masina, care la randul sau este un tip de autovehicul, si exemplele pot
continua. Astfel, cand spunem ca ceva este un tip de altceva diferit, spunem ca
este o specializare a acelui lucru, asa cum o masina este un tip mai special de
autovehicul.
Conceptul de caine mosteneste, deci primeste in mod automat, toate
caracteristicile unui mamifer. Deoarece este un mamifer, cunoastem faptul ca se
misca si respira aer - toate mamiferele se misca si respira aer prin definitie.
Conceptul de caine aduce in plus ideea de a latra, de a misca coada, si asa mai
departe. Putem clasifica mai departe cainii in caini de paza si caine de
vanatoare, iar cainii de vanatoare in cocker spanioli si dobermani, etc.
Asa cum am vazut mai sus, conceptul de mostenire este o notiune foarte
naturala si pe care o intalnim in viata de zi cu zi. In C++ intalnim notiunea de
derivare, care este in fapt o abstractizare a notiunii de mostenire. O
clasa care adauga proprietati noi la o clasa deja existenta vom spune ca este
derivata din clasa originala. Clasa originala poarta denumirea de
clasa de baza.
Clasa derivata mosteneste toate datele si functiile membre ale clasei de
baza; ea poate adauga noi date la cele existente si poate suprascrie sau adauga
functii membre. Clasa de baza nu este afectata in nici un fel in urma acestui
proces de derivare si ca urmare nu trebuie recompilata. Declaratia si codul
obiect sunt suficiente pentru crearea clasei derivate, ceea ce permite
reutilizarea si adaptarea usoara a codului deja existent, chiar daca fisierul
sursa nu este disponibil. Astfel, nu este necesar ca programatorul unei clase
derivate sa cunoasca modul de implementare a functiilor membre din componenta
clasei de baza.
O notiune noua legata de derivare este cea de supraincarcare sau
suprascriere a functiilor membre. Aceasta se refera, in mod evident, la
redefinirea unor functii a clasei de baza in clasa derivata. De notat este
faptul ca functiile originale din clasa parinte sunt in continuare accesibile in
clasa derivata, deci caracteristicile clasei de baza nu sunt pierdute.
Dintr-o clasa de baza pot fi derivate mai multe clase si fiecare clasa
derivata poate servi mai departe ca baza pentru alte clase derivate. Se poate
astfel realiza o ierarhie de clase, care sa modeleze adecvat sisteme
complexe. Pornind de la clase simple si generale, fiecare nivel al ierarhiei
acumuleaza caracteristicile claselor "parinte" si le adauga un anumit grad de
specializare. Mai mult decat atat, in C++ este posibil ca o clasa sa mosteneasca
simultan proprietatile mai multor clase, procedura numita mostenire
multipla. Construirea ierarhiei de clase reprezinta activitatea fundamentala
de realizare a unei aplicatii orientate obiect, reprezentand in fapt faza de
proiectare a respectivului sistem.
Sintaxa simplificata a derivarii este:
class NumeClasaDerivata : NumeClasaDeBaza
In continuare vom deriva din clasa Point o clasa specializata, GraphicPoint,
care va "sti" sa deseneze efectiv punctul pe ecran:
class GraphicPoint
: Point {
unsigned color;
GraphicPoint(unsigned X, unsigned Y, unsigned Color);
~GraphicPoint();
void Draw();
void SetX(unsigned X);
void SetY(unsigned Y); };
GraphicPoint::GraphicPoint(unsigned X,
unsigned Y, unsigned Color) :
Point(X, Y) {
color =
Color; }
GraphicPoint::~GraphicPoint() { }
GraphicPoint::Draw() {
// ...
// apelare primitive grafice pentru desenarea efectiva a
punctului }
GraphicPoint::SetX(unsigned X) {
Point::SetX(); // apelul functiei SetX() apartinand clasei de baza
Draw(); }
GraphicPoint::SetY(unsigned Y) {
Point::SetY();
Draw(); }
Se observa din exemplul de mai sus ca am adaugat o variabila noua fata de
clasa Point, color, pentru a putea memora culoarea cu care se face
desenarea punctului. De asemenea, am suprascris constructorul si destructorul
clasei parinte. In constructorul derivat, am apelat constructorul original
folosind constructia:
ClasaDerivata::ClasaDerivata() : ClasaDeBaza()
In clasa GraphicPoint am adaugat o functie membra noua,
Draw(), care deseneaza efectiv punctul pe ecran. Am suprascris functiile
SetX() si SetY(), apeland in ambele functiile originale, utilizand
sintaxa:
ClasaDeBaza::FunctieMembra()
apeland apoi functia
de desenare, Draw().
Regulile de functionare ale constructorilor si destructorilor, descrise in
paragraful precedent, raman valabile si in cazul claselor derivate, cu doua
observatii privind ordinea de apelare a acestora:
la instantierea clasei derivate, se apeleaza mai intai constructorul clasei
de baza, apoi se apeleaza propriul constructor.
la distrugerea unui obiect al unei clase derivate, este apelat mai intai
propriul constructor, si apoi destructorul clasei de baza (deci in ordine
inversa crearii obiectului).
In capitolul urmator vor fi prezentate notiuni de programare avansata in
C++, cum ar fi controlul accesului la clase, redefinirea operatorilor,
mostenirea multipla si altele.
Cap. 4. Programare avansata utilizand
clase
4.1. Controlul accesului la
clase
Spre deosebire de limbajele orientate obiect pure, C++ permite controlul
accesului la membrii claselor. In acest scop, s-au creat trei specificatori de
cotrol al accesului:
public, membrul poate fi accesat de orice functie din domeniul
declaratiei clasei;
private, membrul este accesibil numai functiilor membre si
prietene ale clasei;
protected, similar cu private, insa accesul se extinde si la
functiile membre si prietene ale claselor derivate.
De remarcat este faptul ca o functie membra a unei clase are acces la toti
membrii clasei, indiferent de specificatorul de acces.
Asadar, sintaxa declaratiei unei clase derivate incluzand controlul
accesului este:
class NumeClasaDerivata : SpecificatorAcces
NumeClasaDeBaza
unde SpecificatorAcces poate fi
public sau private.
| Atributul din clasa de baza |
Modificator de acces |
Accesul mostenit de clasa derivata |
Accesul din exterior |
private protected public |
private private private |
inaccesibil private private |
inaccesibil inaccesibil inaccesibil |
private protected public |
public public public |
inaccesibil protected public |
inaccesibil inaccesibil accesibil |
Se observa ca pentru a oferi clasei derivate acces la un membru al clasei de
baza, acesta trebuie declarat protected sau public. Pentru respectarea
principiului incapsularii datelor, datele membre pentru care se ofera acces
claselor derivate se declara in clasa de baza cu atributul protected. De
asemenea, pentru a conserva dreptul de acces in urma derivarii, se utilizeaza
derivarea public. Accesul poate fi stopat pe orice nivel ar ierarhiei de clase
printr-o derivare private.
Stabilirea atributelor de acces ale membrilor unei clase, precum si ale
derivarilor, trebuie sa se faca astfel incat dezvoltarea ierarhiei de clase fara
a afecta incapsularea datelor.
Sa reluam in continuare exemplul din capitolul precedent, completat cu
specificatori de acces:
class Point { protected:
unsigned x, y; public:
Point();
Point(unsigned X, unsigned Y);
~Point();
unsigned long Arie();
unsigned GetX();
unsigned GetY();
void SetX(unsigned X);
void SetY(unsigned Y); };
class GraphicPoint : public
Point {
unsigned color;
public:
GraphicPoint(unsigned X, unsigned Y, unsigned Color);
~GraphicPoint();
void Draw();
void SetX(unsigned X);
void SetY(unsigned Y); };
Se observa ca variabilele membru x si y sunt declarate protected, asa incat
vor fi vizibile si vor avea acelasi atribut in clasa GraphicPoint (desi nu sunt
utilizate). In mod normal, x si y ar trebui sa fie declarati private, intrucat
nu sunt utilizati decat in interiorul clasei Point. Functiile din GraphicPoint
nu acceseaza acesti doi membri direct, ci prin intermediul metodelor publice de
accesare a lor oferite de clasa Point.
De notat este faptul ca implicit, daca nu este utilizat nici un specificator
de acces, membrii sunt considerati private.
void main() {
Point *p;
p = new Point;
p->x = 5; // operatie imposibila: x este membru privat
p->y = 8; // operatie imposibila: y este membru privat
p->SetX(5); // corect: acces la variabila x prin intermediul functiei
SetX()
p->SetY(8);
printf("Aria = %d\n", p->Aria());
delete p; }
Am demonstrat prin acest exemplu de program ca din exteriorul unei clase nu
pot fi accesate datele membre private sau protected.
4.2. Functii si clase
prietene
In paragraful precedent, am afirmat ca principiul incapsularii datelor este
bine sa fie respectat in cadrul elaborarii ierarhiei de clase. Cu toate acestea,
exista situatii in care este greu sa se respecte acest principiu. De aceea,
Bjarne Stroustrup a introdus un concept menit sa rezolve si aceste situatii
particulare, pentru a oferi solutii elegante in vederea rezolvarii tuturor
situatiilor posibile. Acest concept este cel de friend, care permite
practic abateri controlate de la ideea protectiei datelor prin incapsulare.
Mecanismul de friend este bine sa fie folosit numai in cazul in
care nu exista alta solutie!
Mecanismul de friend (sau prietenie) a aparut datorita
imposibilitatii ca o metoda sa fie membru a mai multor clase.
Functiile prietene sunt functii care nu sunt metode ale unei
clase, dar care au totusi acces la membrii privati ai acesteia. Orice functie
poate fi prietena a unei clase, indiferent de natura acesteia.
Sintaxa declararii unei functii prietene in cadrul declaratiei unei clase
este urmatoarea:
friend NumeFunctie
Iata si un exemplu:
class Point {
friend unsigned long Calcul(unsigned X, unsigned Y); public:
friend unsigned long AltaClasa::Calcul(unsigned X, unsigned
Y); ... };
unsigned long Calcul(unsigned X, unsigned Y) {
return X * Y / 2; }
unsigned long AltaClasa::Calcul(unsigned X,
unsigned Y) {
... }
Dupa cum se vede din exemplul de mai sus, nu are nici o importanta in cadrul
carei sectiuni este declarata functia prietena.
Clasele prietene sunt clase care au acces la membrii privati
ai unei clase. Sintaxa declararii unei clase prietene este:
friend class NumeClasaPrietena
Iata si un exemplu:
class PrimaClasa {
... };
class ADouaClasa {
... friend class PrimaClasa; };
In exemplul de mai sus, clasa PrimaClasa are acces la membrii privati ai
clasei ADouaClasa.
Important este sa remarcam ca relatia de prietenie nu este
tranzitiva. Daca o clasa A este prietena a clasei B, si clasa B este
prietena a unei clase C, aceasta nu inseamna ca A este prietena a clasei C. De
asemenea, proprietatea de prietenie nu se mosteneste in clasele
derivate.
4.3. Cuvantul cheie
this
Toate functiile membre ale unei clase primesc un parametru ascuns,
pointer-ul this, care reprezinta adresa obiectului in cauza. Acesta poate
fi utilizat in cadrul functiilor membre.
Iata si un exemplu:
unsigned long Point::Arie() {
return this->x * this->y; }
4.4. Redefinirea
operatorilor
Dupa cum stiti, C/C++ are definite mai multe tipuri de date: int,
char, etc. Pentru utilizarea acestor tipuri de date exista definiti mai
multi operatori: adunare(+), inmultire (*), etc. C++ permite
programatorilor sa isi defineasca acesti operatori pentru a lucru cu propriile
clase.
Sintaxa supraincarcarii unui operator este:
operator Simbol
unde Simbol este simbolul oricarui operator C++, exceptand: .,
.*, ::, ?:. Aceasta definire se face in cadrul clasei,
intocmai ca o functie membra.
| Tipul operatorului |
Simbolul operatorului |
Asociativitate |
Observatii |
|
Binar |
() [] -> |
-> |
Se definesc ca functii membre |
|
Unar |
+ - ~ * & (tip) |
<- |
|
|
Unar |
++ -- |
<- |
Nu se poate distinge intre pre si post |
|
Unar |
new delete |
<- |
Poate fi supradefinit si pentru o clasa |
|
Binar |
-> * / % + - & | && || |
-> |
|
|
Binar |
<< >> < <= > >= == != |
-> |
|
|
Binar |
= += -= *= /= %= &= ^= |= <<= >>= |
<- |
Se definesc ca functii membre |
|
Binar |
, |
-> |
|
Dupa cum vom vedea in continuare, exista doua variante de definire a
operatorilor:
ca functie membra a clasei;
ca functie prietena a clasei.
Pentru exemplificare, ne propunem sa extindem clasa Point cu
utilizarea unor operatori.
class Point {
// ...
Point& operator += (Point __p);
Point& operator -= (Point __p);
Point operator + (Point __p);
Point operator - (Point __p);
Point& operator = (Point __p);
int operator == (Point __p);
int operator != (Point __p);
int operator < (Point __p);
int operator > (Point __p);
int operator <= (Point __p);
int operator >= (Point __p); };
Point& Point::operator +=
(Point __p) {
x += __p.x;
y += __p.y;
return *this; }
Point& Point::operator -= (Point __p) {
x -= __p.x;
y -= __p.y;
return *this; }
Point Point::operator + (Point __p) {
return Point(x + __p.x, y + __p.y); }
Point Point::operator -
(Point __p) {
return Point(x - __p.x, y - __p.y); }
int Point::operator ==
(Point __p) {
return x == __p.x && y == __p.y; }
int Point::operator !=
(Point __p) {
return !(*this == __p); }
int Point::operator < (Point
__p) {
return x < __p.x && y < __p.y; }
int Point::operator
> (Point __p) {
return x > __p.x && y > __p.y; }
int Point::operator
<= (Point __p) {
return x <= __p.x && y <= __p.y; }
int
Point::operator >= (Point __p) {
return x >= __p.x && y >= __p.y; }
Am utilizat mai sus varianta cu functii membre. In continuare vom descrie
implementarea operatorului + folosind cea de-a doua
varianta.
class Point {
// ...
friend Point operator + (Point __p1, Point __p2); }
Point operator
+ (Point __p1, Point __p2) {
return Point(__p1.x + __p2.x, __p1.y + __p2.y); }
Definirea operatorilor ca functii membre a unei clase prezinta o restrictie
majora: primul operand este obligatoriu de tipul clasa respectiv.
Supradefinirea operatorilor este supusa in C++ unui set de restrictii:
nu este permisa introducerea de noi simboluri de operatori;
patru operatori nu pot fi redefiniti (vezi mai sus);
caracteristicile operatorilor nu pot fi schimbate: pluralitatea (nu se poate
supradefini un operator unar ca operator binar sau invers), precedenta si
asociativitatea;
functia operator trebuie sa aiba cel putin un parametru de tipul
clasa caruia ii este asociat operatorul supradefinit.
Programatorul are libertatea de a alege natura operatiei realizate de un
operator, insa este recomandat ca noua operatie sa fie apropiata de semnificatia
initiala.
4.4.1. Redefinirea operatorului
=
Operatorul = este deja predefinit in C++, pentru operanzi de tip
clasa. Daca nu este supradefinit, atribuirea se face membru cu membru, in mod
similar cu initializarea obiectului efectuata de catre compilator. Pot exista
situatii in care se doreste o atribuire specifica clasei, ca atare poate fi
supradefinit.
Point& Point::operator = (Point __p) {
x = __p.x;
y = __p.y;
return *this; }
4.4.2. Redefinirea operatorului
[]
Operatorul de indexare [] se defineste astfel:
int &operator[](int)
4.4.3. Redefinirea operatorilor new si
delete
Acesti doi operatori pot fi supradefiniti pentru a realiza operatii
specializate de alocare/eliberare dinamica a memoriei. Functia operator
new trebuie sa primeasca un argument de tipul size_t care sa
precizeze dimensiunea in octeti a obiectului alocat si sa returneze un pointer
de tip void continand adresa zonei alocate:
void *operator new(size_t)
cu mentiunea ca size_t este definit in stdlib.h. Chiar daca
parametrul de tip size_t este obligatoriu, calculul dimensiunii
obiectului in cauza si generarea sa se face de catre compilator.
Functia operator delete trebuie sa primeasca ca prim parametru un
pointer de tipul clasei in cauza sau void, continand adresa obiectului de
distrus, si un al doilea parametru, optional, de tip size_t. Functia nu
intoarce nici un rezultat.
void operator delete(void *, size_t)
Trebuie sa mentionam aici ca operatorii new si delete
supradefiniti pastreaza toate proprietatile operatorilor new si
delete standard.
4.4.4. Redefinirea operatorilor
unari
Operatorii unari pot fi supradefiniti utilizand o functie membra fara
parametri sau o functie prietena cu un parametru de tipul clasa respectiv.
Trebuie subliniat ca pentru operatorii ++ si -- dispare distinctia
intre utilizarea ca prefix si cea ca postfix, de exemplu intre x++ si
++x, respectiv x-- si --x, dupa cum mentionam si in tabela
cu operatori de mai sus.
4.5. Mostenirea multipla
Limbajul C++ permite crearea de clase care mostenesc proprietatile mai
multor clase de baza. Mostenirea multipla creste astfel flexibilitatea
dezvoltarii ierarhiei de clase. Daca derivarea normala duce la construirea unei
ierarhii de tip arbore, derivarea multipla va genera ierarhii de tip graf.
Sintaxa completa pentru operatia de derivare este urmatoarea:
class NumeClasaDerivata : ListaClaseDeBaza
unde ListaClaseDeBaza este:
SpecificatorAcces NumeClasaDeBaza, ...
4.5.1. Clase virtuale
Utilizarea mostenirii multiple se poate complica odata cu cresterea
dimensiunii ierarhiei de clase. O situatie care poate apare este derivarea din
doua clase de baza, Clasa1 si Clasa2, care la randul lor sunt
derivate dintr-o clasa comuna, ClasaDeBaza. In acest caz, noua clasa,
ClasaNoua, va contine datele membre ale clasei ClasaDeBaza
duplicate. Daca prezenta acestor date duplicate este utila, ele pot fi distinse
evident cu ajutorul operatorului de rezolutie, ::. Totusi, in cele mai
multe cazuri, aceasta duplicare nu este necesara si duce la consum inutil de
memorie. De aceea, in C++ a fost creat un mecanism care sa evite aceasta
situatie, prin intermediul conceptului de clasa virtuala. Sintaxa
este:
class NumeClasaDerivata : SpecificatorAcces virtual
NumeClasaDeBaza
Aceasta declaratie nu afecteaza clasa in cauza, ci numai clasele derivate
din aceasta. Astfel, clasele Clasa1 si Clasa2 considerate vor fi
declarate virtuale. Trebuie mentionat faptul ca declararea virtual a
acestor clase va afecta definirea constructorului clasei ClasaNoua,
deoarece compilatorul nu poate hotari care date vor fi transferate catre
constructorul ClasaDeBaza, specificate de constructorii Clasa1 si
Clasa2. Constructorul ClasaNoua va trebui modificat astfel incat
sa trimita datele pentru constructorul ClasaDeBaza. De asemenea, trebuie
precizat ca intr-o ierarhie de clase derivate, constructorul clasei virtuale
este intotdeauna apelat primul.
4.6. Conversii de tip definite de
programator
Dupa cum stiti, in C/C++ exista definit un set de reguli de conversie pentru
tipurile fundamentale de date. C++ permite definirea de reguli de conversie
pentru clasele create de programator. Regulile astfel definite sunt supuse unor
restrictii:
intr-un sir de conversii nu este admisa decat o singura conversie definita
de programator;
se recurge la aceste conversii numai dupa ce se verifica existenta altor
solutii (de exemplu, pentru o atribuire, se verifica mai intai supraincarcarea
operatorului de atribuire si in lipsa acestuia se face conversia).
Exista doua metode de a realiza conversii de tip, prezentate mai
jos.
4.6.1. Supraincarcarea operatorului unar
"cast"
Sintaxa este:
operator TipData()
respectiv:
operator (TipData)
Operatorul "cast" este unar, asadar are un singur parametru, adresa
obiectului in cauza, si intoarce un rezultat de tipul operatorului. Ca urmare,
prin aceasta metoda se pot defini numai conversii dintr-un tip clasa intr-un tip
de baza sau un alt tip clasa.
De remarcat este faptul ca, in cazul conversiei dintr-un tip clasa intr-un
alt tip clasa, functia operator trebuie sa aiba acces la datele membre ale
clasei de la care se face conversia, deci trebuie declarata prietena a clasei
respective.
4.6.2. Conversii de tip folosind
constructori
Aceasta metoda consta in definirea unui constructor ce primeste ca parametru
tipul de la care se face conversia. Constructorul intoarce intotdeauna ca
rezultat un obiect de tipul clasei de care apartine, ca urmare folosind aceasta
metoda se pot realiza numai conversii dintr-un tip de baza sau un tip clasa
intr-un tip clasa.
Trebuie mentionat faptul ca, in cazul conversiei dintr-un tip clasa intr-un
alt tip clasa, constructorul trebuie sa aiba acces la datele membre ale clasei
de la care se face conversia, deci trebuie declarata prietena a clasei
respective.
4.7. Constructorul de
copiere
O situatie care poate aparea deseori este initializarea unui obiect cu
datele membre ale unui obiect de acelasi tip. In 4.4.1 am descris
supraincarcarea oparatorului de atribuire. Exista totusi situatii in care acest
operator nu poate fi utilizat, cum ar fi la transferul unui obiect ca parametru
sau la crearea unui instante temporare a unei clase, atunci cand copierea membru
cu membru nu este adecvata. Pentru a rezolva aceste situatii, C++ a introdus un
constructor special, numit constructorul de copiere.
Sintaxa este:
NumeClasa::NumeClasa (NumeClasa
&NumeObiectSursa)
In continuare vom completa clasa Point cu un constructor de
copiere:
Point::Point(Point &p) {
p.x = x;
p.y = y; }
In cazul in care clasa nu dispune de constructor de copiere, compilatorul
genereaza automat un constructor de copiere care realizeaza copierea membru cu
membru.
4.8. Clase abstracte
In C++ exista posibilitatea de a defini clase generale, care sunt destinate
crearii de noi clase prin derivare, ele neputand fi instantiate si utilizate ca
atare. Acest gen de clase se numesc clase abstracte. Ele se constituie ca
baza in cadrul elaborarii de ierarhii de clase, putand fi folosite, spre
exemplu, pentru a impune anumite restrictii in realizarea claselor derivate.
In vederea construirii unor astfel de clase, s-a introdus conceptul de
functie virtuala pura. O astfel de functie este declarata in cadrul
clasei, dar nu este definita. O clasa care contine o functie virtuala pura este
considerata abstracta. Sintaxa definirii acestor functii este:
virtual TipData NumeFunctieMembra() = 0
Se impune aici observatia ca functiile virtuale pure trebuie definite in
clasele derivate, altfel si acestea vor fi considerate abstracte.
4.9. Membri statici ai unei
clase
In mod normal, datele membre ale unei clase sunt alocate in cadrul fiecarui
obiect. In C++, se pot defini date membre cu o comportare speciala, numite
date statice. Acestea sunt alocate o singura data, existand sub forma
unei singuri copii, comuna tuturor obiectelor de tipul clasa respectiv, iar
crearea, initializarea si accesul la aceste date sunt independente de obiectele
clasei. Sintaxa este:
static DeclarareMembru
Functiile membre statice efectueaza de asemenea operatii care nu sunt
asociate obiectelor individuale, ci intregii clase. Functiile exterioare clasei
pot accesa membrii statici ale acesteia astfel:
NumeClasa::NumeMembru
Obiect::NumeMembru
Functiile membre statice nu primesc ca parametru implicit adresa unui
obiect, asadar in cadrul lor cuvantul cheie this nu poate fi utilizat. De
asemenea, membrii normali ai clasei nu pot fi referiti decat specificand numele
unui obiect.
In urmatorul capitol va fi prezentata notiunea de stream.
Cap. 5. Stream-uri
Cap. 5.1. Introducere
Stream-urile au in principal rolul de a abstractiza operatiile de intrare-
iesire. Ele ofera metode de scriere si citire a datelor independente de
dispozitivul I/O si chiar independente de platforma. Stream-urile incapsuleaza
(ascund) problemele specifice dispozitivului cu care se lucreaza, sub libraria
standard iostream.
Alt avantaj al folosirii stream-urilor se datoreaza implementarii librariei
iostream, care utilizeaza un sistem de buffer-e. Se stie ca in general
operatiile de intrare/iesire cu dispozitivele periferice sunt relativ mari
consumatoare de timp, astfel incat aplicatiile sunt uneori nevoite sa astepte
terminarea acestor operatiuni. Informatiile trimise catre un stream nu sunt
scrise imediat in dispozitivul in cauza, ci sunt transferate intr-o zona de
memorie tampon, din care sunt descarcate catre dispozitiv abia in momentul
umplerii acestei zone de memorie.
In C++ stream-urile au fost implementate utilizand clase, dupa cum
urmeaza:
clasa streambuf gestioneaza buffer-ele.
clasa ios este clasa de baza pentru clasele de stream-uri de intrare
si de iesire. Clasa ios are ca variabila membru un obiect de tip
streambuf.
clasele istream si ostream sunt derivate din
ios.
clasa iostream este derivata din istream si ostream si
ofera metode pentru lucrul cu terminalul.
clasa fstream ofera metode pentru operatii cu fisiere.
Cap. 5.2. Obiecte
standard
Cand un program C++ care include iostream.h este lansat in executie,
sunt create si initializate automat patru obiecte:
cin gestioneaza intrarea de la intrarea standard
(tastatura).
cout gestioneaza iesirea catre iesirea standard (ecranul).
cerr gestioneaza iesirea catre dispozitivul standard de eroare
(ecranul), neutilizand buffer-e.
clog gestioneaza iesirea catre dispozitivul standard de eroare
(ecranul), utilizand buffer-e.
Cap. 5.3. Redirectari
Dispozitivele standard de intrare, iesire si eroare pot fi
redirectate catre alte dispozitive. Erorile sunt de obicei redirectate
catre fisiere, iar intrarea si iesirea pot fi conduse ("piped")
catre fisiere utilizand comenzi ale sistemului de operare (utilizarea iesirii
unui program ca intrare pentru altul).
Sintaxa pentru operatii de iesire, cout:
cout << InformatieDeTrimisLaIesire;
respectiv pentru intrare, cin:
cin >> NumeVariabila;
De fapt, cin si cout sunt niste obiecte definite global, care
au supraincarcat operatorul >> respectiv << de mai
multe ori, pentru fiecare tip de parametru in parte (int, char *,
etc.):
istream &operator >> (TipParametru
&)
De exemplu:
#include <iostream.h>
void
main() {
int IntegerNumber;
cout << "IntegerNumber = ";
cin >> IntegerNumber;
cout << "\nWhat you entered = " << IntegerNumber <<
endl; }
Acest scurt program citeste de la intrarea standard o valoare intreaga, pe
care o trimite apoi catre iesirea standard. Se observa posibilitatea de a
utiliza simbolurile '\n', '\t', s.a.m.d (ca la printf,
scanf, etc.). Utilizarea simbolului endl va forta golirea zonei
tampon, adica trimiterea datelor imediat catre iesire.
Atat operatorul >> cat si << returneaza o
referinta catre un obiect al clasei istream. Deoarece cin
respectiv cout este si el un obiect istream, valoarea returnata de
o operatie de citire/scriere din/in stream poate fi utilizata ca
intrare/iesire pentru urmatoarea operatie de acelasi fel.
Cap. 5.4. cin
Functia cin.get()
Functia membra get() poate fi utilizata pentru a obtine un singur
caracter din intrare, apeland-o fara nici un parametru, caz in care returneaza
valoarea utilizata, sau ca referinta la un caracter.
get() fara parametri
In aceasta forma, functia intoarce valoarea caracterului gasit. De remarcat
este faptul ca, spre deosebire de operatorul >>, nu poate fi
utilizata pentru a citi mai multe intrari, deoarece valoarea returnata este de
tip intreg, nu un obiect istream. Mai jos, un exemplu de
utilizare:
#include <iostream.h>
void main() {
char c;
while((c = cin.get()) != EOF)
{
cout << "c = " << c << endl;
} }
Citirea de siruri de caractere utilizand get()
Operatorul >> nu poate fi utilizat pentru a citi corect siruri
de caractere de la intrare deoarece spatiile sunt interpretate ca separator
intre diverse valori de intrare. In astfel de cazuri trebuie folosita functia
get(). Sintaxa de utilizare a functiei get in acest caz este
urmatoarea:
cin.get(char *PointerLaSirulDeCaractere, int LungimeMaxima, char
Sfarsit);
Primul parametru este un pointer la zona de memorie in care va fi depus
sirul de caractere. Al doilea parametru reprezinta numarul maxim de caractere ce
poate fi citit plus unu. Cel de-al treilea parametru este caracterul de
incheiere a citirii, care este optional (implicit considerat '\n').
In cazul in care caracterul de incheiere este intalnit inainte de a fi citit
numarul maxim de caractere, acest caracter nu va fi extras din stream. Exista o
functie similara functiei get(), cu aceeasi sintaxa, numita
getline(). Functionarea sa este identica cu get(), cu exceptia
faptului ca acel ultim caracter mentionat mai sus este si el extras din
stream.
Functia cin.ignore()
Aceasta functie se utilizeaza pentru a trece peste un numar de caractere
pana la intalnirea unui anume caracter. Sintaxa sa este:
cin.ignore(int NumarMaximDeCaractere, char
Sfarsit);
Primul parametru reprezinta numarul maxim de caractere ce vor fi ignorate
iar al doilea parametru caracterul care trebuie gasit.
Functia cin.peek()
Aceasta functie returneaza urmatorul caracter din stream, fara insa a-l
extrage.
Functia cin.putback()
Aceasta functie insereaza in stream un caracter.
Cap. 5.5. cout
Cap. 5.5.1. Functii membre ale
cout
Functia cout.flush()
Functia cout.flush() determina trimiterii catre iesire a tuturor
informatiilor aflate in zona de memorie tampon. Aceasta functie poate fi apelata
si in forma cout << flush.
Functia cout.put()
Functia cout.put() scrie un caracter catre iesire. Sintaxa sa este
urmatoarea:
cout.put(char Caracter);
Deoarece aceasta functie returneaza o referinta de tip ostream, pot
fi utilizate apeluri succesive ale acesteia, ca in exemplul de mai
jos:
#include <iostream.h>
void main() {
cout.put('H').put('i').put('!').put('\n'); }
Functia cout.write()
Aceasta functie are acelasi rol ca si operatorul <<, cu
exceptia faptului ca se poate specifica numarul maxim de caractere ce se doresc
scrise. Sintaxa functiei cout.write() este:
cout.write(char *SirDeCaractere, int
CaractereDeScris);
Cap. 5.5.2. Formatarea
iesirii
Functia cout.width()
Aceasta functie permite modificarea dimensiunii valorii trimise spre iesire,
care implicit este considerata exact marimea campului in cauza. Ea modifica
dimensiunea numai pentru urmatoarea operatie de iesire. Sintaxa este:
cout.width(int Dimensiune);
Functia cout.fill()
Functia cout.fill() permite modificarea caracterului utilizat pentru
umplerea eventualului spatiu liber creat prin utilizarea unui dimensiuni mai
mari decat cea necesara iesirii, cu functia cout.width(). Sintaxa
acesteia este:
cout.fill(char Caracter);
Cap. 5.5.3. Optiuni de formatare a
iesirii
Pentru formatarea iesirii sunt definite doua functii membre ale cout,
si anume:
Functia cout.setf()
Aceasta functie activeaza o optiune de formatare a iesirii, primita ca
parametru:
cout.setf(ios::Optiune);
Unde Optiune poate fi:
showpos determina adaugarea semnului plus (+) in fata valorilor
numerice pozitive;
left, right, internal schimba alinierea
iesirii;
dec, oct, hex schimba baza de numeratie pentru valori
numerice;
showbase determina adaugarea identificatorului bazei de numeratie in
fata valorilor numerice.
Functia cout.setw()
Aceasta functie modifica dimensiunea iesirii, fiind similara functiei
cout.width(). Sintaxa sa este:
cout.setw(int Dimensiune);
In continuare vom exemplifica utilizarea functiilor pentru formatarea
iesirii:
#include <iostream.h> #include
<iomanip.h>
void main() {
int number = 783;
cout << "number = " << number;
cout.setf(ios::showbase);
cout << "number in hexa = " << hex << number;
cout.setf(ios::left);
cout << "number in octal, aligned to the left = " << oct
<< number; }
Cap. 5.6. Operatii de intrare/iesire cu
fisiere
Lucrul cu fisiere se face prin intermediul clasei ifstream pentru
citire respectiv ofstream pentru scriere. Pentru a le utiliza,
aplicatiile trebuie sa includa fstream.h. Clasele ofstream si
ifstream sunt derivate din clasa iostream, ca urmare toti
operatorii si toate functiile descrise mai sus sunt mostenite si de aceasta
clasa.
Sintaxele pentru constructorii acestor doua clase sunt:
ofstream Variabila(char *NumeFisier, ios::Mod);
ifstream
Variabila(char *NumeFisier);
Acesti constructori au rolul de a deschide fisierul specificat ca parametru.
Cel de-al doilea parametru al constructorului ofstream este optional si
specifica modul de deschidere a fisierului:
ios::append - adauga la sfarsitul fisierului;
ios::atend - pozitioneaza pointer-ul la sfarsitul fisierului, insa
informatiile pot fi scrise oriunde in cadrul fisierului;
ios::truncate - este modul de deschide implicit: vechiul continut al
fisierului este pierdut;
ios::nocreate - daca fisierul nu exista, atunci operatia
esueaza;
ios::noreplace - daca fisierul deja exista, atunci operatia
esueaza.
Pot fi utilizate prescurtarile: app pentru append, ate
pentru atend, si trunc pentru truncate.
Pentru a inchide aceste fisiere trebuie apelata functia membra
close().
Rezultatul operatiilor de intrare/iesire poate fi testat prin intermediul a
patru functii membre:
eof() verifica daca s-a ajuns la sfarsitul fisierului;
bad() verifica daca s-a executat o operatie invalida;
fail() verifica daca ultima operatie a esuat;
good() verifica daca toate cele trei rezultate precedente sunt
false.
In urmatorul capitol veti afla despre mecanismul de tratare a exceptiilor
oferit de C++.
Cap. 6. Tratarea exceptiilor
Este un fenomen "natural" ca in programe sa se strecoare erori, de diverse
naturi. Activitatea de programare implica si actiuni mai putini placute, adica
testarea, depanarea si corectarea erorilor. Costurile de indepartare a erorilor
creste de obicei direct proportional cu intarzierea din cadrul procesului de
dezvoltare cand sunt descoperite.
Trebuie insa inteleasa diferenta dintre erori (bug-uri) si exceptii.
Exceptiile sunt situatiile neasteptate aparute in cadrul sistemului care ruleaza
un program. Programele trebuie sa fie pregatite pentru a trata aceste situatii
exceptionale.
In C++ s-a realizat un mecanism facil de tratare a exceptiilor. Astfel, o
exceptie este un obiect a carui adresa este trimisa dinspre zona de cod unde a
aparut problema catre o zona de cod care trebuie sa o rezolve.
Pasii care trebuiesc in general urmati in vederea tratarii exceptiilor in
cadrul programelor C++ sunt:
1. se identifica acele zone din program in care se efectueaza o operatie
despre care se cunoaste ca ar putea genera o exceptie, si se marcheaza in cadrul
unui bloc de tip try. In cadrul acestui bloc, se testeaza conditia de
aparitie a exceptiei, si in caz pozitiv se semnaleaza aparitia exceptiei prin
intermediul cuvantului cheie throw;
2. se realizeaza blocuri de tip catch pentru a capta exceptiile
atunci cand acestea sunt intalnite.
Blocurile catch urmeaza un bloc try, in cadrul carora sunt
tratate exceptiile.
Sintaxa pentru try:
try {
// cod
throw TipExceptie; }
Sintaxa pentru throw:
throw TipExceptie;
Sintaxa pentru catch:
catch(TipExceptie) {
// cod tratare exceptie }
Daca TipExceptie este "...", este captata orice exceptie
aparuta.
Dupa un bloc try, pot urma unul sau mai multe blocuri catch.
Daca exceptia corespunde cu una din declaratiile de tratare a exceptiilor,
aceasta este apelata. Daca nu exista definita nici o rutina de tratare a
exceptiei, este apelata rutina predefinita, care incheie executia programului in
curs. Dupa ce rutina este executata, programul continua cu instructiunea imediat
urmatoare blocului try.
TipExceptie nu este altceva decat instantierea unei clase vide (care
determina tipul exceptiei), putand fi declarat ca:
class TipExceptie
{};
In continuare prezentam un exemplu de program care utilizeaza tratarea
exceptiilor.
#include <iostream.h>
#define MAXX 80 #define MAXY 25
class Point { public:
class xZero {}; class xOutOfScreenBounds {};
Point(unsigned __x, unsigned __y) { x = __x; y = __y; }
unsigned GetX() { return x; }
unsigned GetY() { return y; }
void SetX(unsigned __x) { if(__x > 0) if(__x < = MAXX) x = __x; else throw xOutOfScreenBounds(); else throw xZero(); }
void SetY(unsigned __y) { if(__y > 0) if(__y < = MAXY) y = __y; else throw xOutOfScreenBounds(); else throw xZero(); }
protected: int x, y; };
main() { Point p(1, 1); try { p.SetX(5); cout << "p.x successfully set to " << p.GetX() << "." << endl; p.SetX(100); } catch(Point::xZero) { cout << "Zero value!\n"; } catch(Point::xOutOfScreenBounds) { cout << "Out of screen bounds!\n"; } catch(...) { cout << Unknown exception!\n"; } }
Datorita faptului ca exceptia este instantierea unei clase, prin derivare
pot fi realizate adevarate ierarhii de tratare a exceptiilor. Trebuie avuta insa
in vedere posibilitatea de aparitie a unor exceptii chiar in cadrul codului de
tratare a unei exceptii, situatii care trebuie evitate.
In urmatorul capitol va fi prezentata notiunea de template.
Cap. 7. Template-uri
Template-ul implementeaza asa-zisul concept de "tip
parametrizat" ("parametrized type"). Un template reprezinta o familie
de tipuri sau functii, cu alte cuvinte, un sablon sau model. Acest concept a
fost introdus in primul rand pentru a creste gradul de reutilizabilitate a
codului. De exemplu, pentru a implementa o lista de numere intregi este necesara
in mod normal realizarea unei clase speciale (sa spunem ListOfIntegers),
iar pentru o lista de siruri alta clasa (sa spunem ListOfStrings).
Conceptul de template permite realizarea unei clase generale (sa spunem
List), care sa accepte orice tip de element, inclusiv tipuri necunoscute
la momentul implementarii acesteia. Tipul template-ului este stabilit in
momentul instantierii sale. Template-urile sunt foarte utile pentru realizarea
de biblioteci care trebuie sa ofere metode generice de prelucrare a datelor.
Sintaxa generala de declarare a unui template este urmatoarea:
template < ListaDeParametri > Declaratie
unde Declaratie reprezinta declararea sau definirea unei clase sau
functii, definirea unui membru static al unei clase template, definirea unei
clase sau functii membre al unei clase template, sau definirea unui membru
template al unei clase.
Clasele parametrizate (sau clasele template) se declara
astfel:
template <class NumeParametru> class NumeClasa {
// ...
// definirea clasei }
Particularizarea (= stabilirea tipului) clasei template se face prin
intermediul unei constructii de genul:
NumeClasa
<NumeParametru>
unde NumeParametru reprezinta tipul obiectului.
Functiile template se declara astfel:
template <class
NumeParametru> // ... // declaratia functiei
Sa consideram in continuare ca exemplu implementarea unei stive generice
folosind template-uri.
#include <iostream.h>
template
<class T> class StackItem { public:
StackItem *Next; T *Data; StackItem(T __Data, StackItem <T>
*__Next) {
Data = new T(__Data); Next = __Next;
}
};
template
<class T> class Stack { public:
T pop() {
T result = *(Data->Data); StackItem <T> *temp = Data; Data =
Data->Next; delete temp; return result;
} T top() {
} void push(T __Data) {
Data = new StackItem <T>(__Data, Data);
} int
isEmpty() {
} Stack() {
}
private:
};
main() {
Stack <int>
anIntegerStack; anIntegerStack.push(5); anIntegerStack.push(7); if(anIntegerStack.isEmpty())
- cout << "Stiva goala" << endl;
else
- cout << anIntegerStack.pop() << endl;
cout <<
anIntegerStack.top() << endl;
}
In exemplul urmator a fost implementata o lista generica (List). Ca
elemente a listei s-au folosit obiecte de tip Point (clasa definita in Cap. 4). Pentru parcurgerea usoara a listei a fost
implementata o clasa de tip "iterator", care poate fi considerata ca
fiind un "cursor" care strabate lista. Functia List.begin() returneaza un
iterator pozitionat pe primul element al listei, List.end() pe ultimul
element al listei. Saltul la urmatorul element al listei se face cu ajutorul
operatorului ++ din clasa Iterator.
#include
<iostream.h>
class Point { friend ostream& operator
<< (ostream& output, Point p); protected:
public:
Point() { } Point(unsigned X, unsigned Y) { } ~Point() {} unsigned GetX() { } unsigned GetY() { } void SetX(unsigned X) { } void SetY(unsigned Y) { }
};
ostream& operator <<
(ostream& output, Point p) {
output << "(" << p.x << ", " << p.y <<
")"; return output;
}
template <class T> class Item
{ public:
Item *Next; T *Data; Item(T __Data, Item <T> *__Next) {
Data = new T(__Data); Next = __Next;
}
};
template
<class T> class List { public:
T pop_front() {
T result = *(Data->Data); Item <T> *temp = Data; Data =
Data->Next; delete temp; return result;
} T front() { } void push_front(T __Data) {
Data = new Item <T>(__Data, Data);
} int empty() { } List() { }
class Iterator { friend class List
<T>; protected:
Item <T> *Current; Iterator(Item <T> *x) { }
public:
Iterator() {} int operator == (Iterator& x) {
return Current == x.Current;
} int operator != (Iterator&
x) {
return Current != x.Current;
} T operator *() { } Iterator& operator
++(int) {
Current = Current->Next; return
*this;
}
}; Iterator begin() {
} Iterator end() {
Item <T> *temp; for(temp = Data; temp; temp =
temp->Next); return Iterator(temp);
} private:
};
main() {
List <Point> anPointList; List <Point>::Iterator index,
end; anPointList.push_front(Point(1, 1)); anPointList.push_front(Point(3,
14)); index = anPointList.begin(); end =
anPointList.end(); if(anPointList.empty())
cout << "Lista vida" << endl;
else
for(; index != end; index++)
cout <<
endl;
}
Clasele template pot avea trei tipuri de prieteni (friends):
o clasa sau functie care nu este de tip template;
o clasa sau functie template;
o clasa sau functie template avand tipul specificat.
Daca este necesara particularizarea unei functii template sau a unei functii
membre a unei clase template pentru un anumit tip, functia respectiva poate fi
supraincarcata pentru tipul dorit.
Trebuie remarcat de asemenea ca in cazul in care o clasa template contine
membri statici, fiecare instanta a template-ului in cauza va contine propriile
date statice.
In capitolul ce urmeaza vor fi prezentate diverse particularitati legate de
programarea in C++ pe sisteme UNIX.
Cap. 8. Programarea in C++ pe sisteme
UNIX
Cel mai raspandit compilator in lumea UNIX este GCC, implementare free
realizata de catre Free Software Foundation. Acesta este un compilator
extensibil, existand extensii pentru C++, Objective-C, Pascal, Fortran, etc.
Executabilele generate de catre acest compilator nu trebuie neaparat sa fie free
software, chiar daca includ librariile standard C sau C++.
Sintaxa generala de apelare a compilatorului este:
gcc [ optiuni nume_fisier ]
g++ [ optiuni nume_fisier ]
Tipul fisierelor de intrare este determinat dupa sufixul acestora, si sunt
prelucrate dupa cum urmeaza:
.c - sursa C: preprocesare, compilare, asamblare
.C - sursa C++: preprocesare, compilare, asamblare
.cc - sursa C++: preprocesare, compilare, asamblare
.cxx - sursa C++: preprocesare, compilare, asamblare
.m - sursa Objective-C: preprocesare, compilare, asamblare
.i - sursa C preprocesata: compilare, asamblare
.ii - sursa C++ preprocesata: compilare, asamblare
.s - sursa asamblare: asamblare
.S - sursa asamblare: preprocesare, asamblare
Fisierele avand alte sufixe, cum ar fi .o (fisier obiect) sau
.a (fisier arhiva), sunt trimise catre link-editor.
Pentru a se evita parte din etapele prelucrarii pot fi utilizate urmatoarele
optiuni:
-c compileaza si asambleaza fisierele sursa, dar nu le link-
editeaza
-S compileaza fisierele sursa, fara insa a le asambla
-E nu lanseaza compilarea, ci doar preprocesarea
Implicit, gcc genereaza un fisier executabil avand denumirea
"a.aout"; fisierele obiect corespunzatoare intrarii "sursa.sufix"
sunt denumite "sursa.o", iar fisierele asamblate "sursa.s".
Denumirea iesirii poate fi schimbata cu ajutorul optiunii "-o
file".
Alte optiuni des utilizate:
optiuni de compilare:
-Idirector - adauga director la lista directoarelor in care
sunt cautate fisierele .h
-Ldirector - adauga director la lista directoarelor in care
sunt cautate bibliotecile
-O, -O1, -O2, -O3 - activeaza diverse nivele de optimizare a codului.
-O3 este nivelul cel mai avansat de optimizare
-O0 - dezactiveaza optimizarea codului
optiuni de link-editare:
-lbiblioteca - utilizeaza biblioteca specificata in cadrul etapei de
link-editare
optiuni de preprocesare:
-Dmacro - defineste macro-ul specificat ca fiind "1"
-Dmacro=valoare - defineste macro cu valoarea specificata
-Umacro - anuleaza macro-ul specificat
Optiuni specifice C++:
-fall-virtual - trateaza toate functiile membre ca fiind virtuale
-fthis-is-variable - permite utilizarea pointer-ului "this" ca
o variabile obisnuita
-fexternal-templates - produce cod obiect mai mic pentru declaratiile
de template-uri, generand doar o singura copie a fiecarei functii template acolo
unde aceasta este definita. Pentru a putea utiliza aceasta optiune, trebuie
marcate toate fisierele sursa care utilizeaza template-uri cu directiva
#pragma implementation (acolo unde sunt definite efectiv template-urile)
respectiv #pragma interface (acolo unde sunt declarate template-urile).
Atunci cand este utilizata aceasta optiune, toate instantierile de template-uri
sunt considerate external. De aceea, toate aceste instantieri trebuie sa
fie realizate in cadrul fisierului marcat cu "implementation", de exemplu prin
utilizarea unor declaratii "typedef" care sa faca referire la fiecare
instantiere
-falt-external-templates - are un comportament similar cu cel al
optiunii precedente, cu exceptia faptului ca este generata o singura copie a
fiecarei functii template acolo unde aceasta este definita pentru prima oara
-fhandle-exceptions - activeaza mecanismul de tratare a exceptiilor.
Programele care utilizeaza acest mecanism trebuie compilate cu aceasta optiune,
in caz contrar fiind generata o eroare la compilare (Nota: in versiunile
recente, 2.8.x, ale gcc, aceasta optiune a fost schimbata in
"-fexceptions" si este activata in mod implicit)
Exemple:
Pentru compilarea primului exemplu din capitolul 7:
gcc -o cap7_1 cap7_1.cc
Pentru compilarea exemplului din capitolul 6:
gcc -fhandle-exceptions -O3 -o cap6 cap6.cc
In capitolul urmator va fi prezentat limbajul Objective-C.
Cap. 9. Introducere in Objective-C
Objective-C este tot o extensie a limbajului C pentru lucrul orientat pe
obiecte, considerata uneori superioara C++. Sistemul de operare NextStep a fost
scris in intregime in Objective-C.
Spre deosebire de C++, pentru compilarea codului scris in Objective-C nu
este necesar un compilator special. Objective-C consta intr-un preprocesor care
transleaza codul Objective-C in cod C, urmand ca acesta sa fie transformat in
cod obiect de catre un compilator standard C.
Toate clasele sunt derivate in mod automat din clasa predefinita
Object. Aceasta clasa poate fi referita prin intermediul
identificatorului super.
Fata de C++ sunt cateva deosebiri: nu se poate realiza mostenire multipla,
nu exista nici un mecanism de control al accesului, etc.
Metodele de acces la datele incapsulate in interiorul clasei pot fi apelate
trimitand mesaje catre obiecte. Gestionarea mesajelor este transparenta pentru
programator, implementarea fiind realizata cu ajutorul unor functii de
biblioteca. Sintaza apelarii metodelor este urmatoarea:
[Obiect Metoda]
sau cu parametri:
[Obiect Metoda:Parametri ...]
Analog se pot referi datele apartinand clasei:
[Obiect DataMembra]
Declararea obiectelor se face astfel:
id variabila
cu remarca ca declararea unui
identificator nu duce la crearea obiectului. Pentru a crea obiecte in
Objective-C este nevoie de o fabrica de obiecte (factory object).
Obiectul fabrica, unul pentru fiecare clasa, poarta acelasi nume ca si clasa, si
este generat automat de catre preprocesorul Objective-C. Sarcina programatorului
este de a declara metodele de constructie a obiectelor. Aceste metode sunt
aplicate numai obiectului fabrica.
Sintaxa declararii claselor este urmatoarea:
@interface NumeClasa
: Object {
...
/* declaratii date membre */ }
+ metode_de_fabricare
-
metode_normale
@end
Metodele de fabricare se declara astfel:
+ NumeParametruExterior: (TipData)NumeParametruInterior
...
sau fara parametri:
+ NumeMetoda
Metodele normale se declara ca mai jos:
- NumeParametruExterior: (TipData)NumeParametruInterior
...
sau fara parametri:
- NumeMetoda
Exista o metoda de fabricare implicita, numita new, si care apartine
clasei comune, Object, si care poate fi evident supradefinita.
Sintaxa generala a definirii unei clase este:
@implementation
NumeClasa : Object {
...
/* date specifice fiecarei instante */ }
+
metode_de_fabricare {
/* cod */ };
{ - metode_normale {
/* cod */ };
@end
In interiorul metodelor normale poate fi utilizat identificatorul
self, care desemneaza obiectul apelat. Functionalitatea acestuia este
similara celei a identificatorului this din C++.
In ceea ce priveste parametrii metodelor, trebuie mentionat ca daca nu se
declara nici un tip, acesta este considerat de tipul clasei in cauza. De
asemenea, valoarea returnata este implicit de tipul clasei. Daca metoda nu
intoarce nici o valoare, conventia este ca ea sa intoarca identificatorul
obiectului care a primit mesajul, self.
In continuare vom rescrie clasa Point realizata in cap.
3.
#include <objc/Object.h>
@interface Point :
Object {
unsigned x, y; }
+ X: (unsigned)__x Y: (unsigned)__y; +
new; - SetX: (unsigned)__x; - SetY: (unsigned)__y; -
(unsigned)GetX; - (unsigned)GetY; - (unsigned
long)Arie;
@end
@implementation Point : Object {
unsigned x, y; }
+ X: (unsigned)__x Y: (unsigned)__y {
self = [super new];
[self SetX:__x];
[self SetY:__y];
return self; }
+ new {
self = [super new];
[self SetX:0];
[self SetY:0]; }
- SetX: (unsigned)__x {
x = __x;
return self; }
- SetY: (unsigned)__y {
y = __y;
return self; }
- (unsigned)GetX { return x; }
-
(unsigned)GetY { return y; }
- (unsigned long)Arie {
return x * y; }
@end
main() {
id p;
p = [Point X:5 Y:10];
printf("Aria = %d\n", [p Arie]); }
Cap. 10. Proiectarea si dezvoltarea de aplicatii
orientate obiect
Dezvoltarea de sisteme orientate obiect pare a fi, la prima vedere, mai
complicata si de durata mai mare decat dezvoltarea aplicatiilor traditionale. In
realitate, durata si costurile dezvoltarii de aplicatii orientate obiect sunt
mult mai mici.
Etapa fundamentala in cadrul acestui proces o constituie cea de proiectare a
sistemului, chiar daca este evident ca structura interna a acestuia este
irelevanta pentru utilizatori. S-a constatat de asemenea ca succesul
aplicatiilor orientate obiect depinde in principal de doi factori:
1. Existenta unei viziuni arhitecturale coerente si bine definite
Arhitectura unui sistem orientat pe obiecte cuprinde atat structura claselor
si interactiunea dintre obiecte, cat si impartirea aplicatiei in module si
nivele de abstractizare. Cateva conditii in vederea realizarii unei arhitecturi
corecte:
- nivele de abstractizare bine definite
- clase avand interfete bine definite, a caror modificare provoaca schimbari
minime asupra celorlalte clase
- modificarea modului de implementare a unei clase nu are repercursiuni
asupra interfetei sau implementarii celorlalte clase
- arhitectura sistemului este simpla, realizata prin abstractii si mecanisme
obisnuite
2. Urmarea unui ciclu de dezvoltare atat iterativ cat si incremental bine
administrat
Exista in principal doua tipuri de cicluri de dezvoltare:
- ciclu de dezvoltare nedefinit. In acest caz, este imposibil de stiut
viteza dezvoltarii sistemului, momentul la care va fi finalizat, calitatea
sistemului ramanand permanent sub semnul intrebarii. Este posibila ca o parte
din eforturile depuse sa fie ineficiente, asadar costurile dezvoltarii sa fie
foarte mari.
- reguli clare care stabilesc fiecare aspect al ciclului. In acest de-al
doilea caz, este impiedicata creativitatea si experimentul, care ar putea
produce o aplicatie avand calitate sporita. Cerintele utilizatorilor ajung cu
dificultate la nivelul programatorilor ce realizeaza aplicatia, ingreunand
procesul de dezvoltare si marindu-i costurile.
In realitate, nu vom intalni nicaieri vreunul dintre aceste cazuri distinct.
In cadrul dezvoltarii de sisteme software, aceste doua tipuri de cicluri de
dezvoltare se intrepatrund, inclinand mai mult sau mai putin spre una dintre
extreme, functie de deciziile luate de conducatorii acestor proiecte. S-a tras
in multe locuri concluzia ca un ciclu de dezvoltare ideal este atat de tip
iterativ cat si de tip incremental.
Ce inseamna ca un ciclu este iterativ? Un proces iterativ presupune
inbunatatirea succesiva a arhitecturii orientata pe obiecte, utilizand
experienta si rezultatele obtinute in fiecare etapa sau versiune in etapa
urmatoare de analiza si dezvoltare. Ce reprezinta un ciclu incremental? In
cadrul unui proces incremental, fiecare trecere printr-un astfel de ciclu de
analiza/dezvoltare conduce la inbunatatirea deciziilor, rezultand astfel in
final o solutie care intruneste adevaratele cerinte ale utilizatorilor, si are o
arhitectura clara, este eficienta si usor de intretinut.
De obicei, sistemele comerciale sunt realizate urmand un ciclu mai clar
definit, deoarece sunt realizate in cadrul unor companii cu numar mare de
programatori si trebuie executate intr-un interval predefinit de timp. Spre
deosebire de acestea, sistemele open source (realizate in lumea free
software) sunt construite dupa un ciclu mai vag definit, dar care de cele
mai multe ori conduce la sisteme mai extensibile, mai flexibile, si de calitate
superioara celor comerciale.
Sintetizat, etapele dezvoltarii unui sistem orientat pe obiecte
sunt:
- I. Analiza
1. Identificarea obiectelor din cadrul
sistemului; 2. Identificarea actiunilor efectuate de fiecare
obiect; 3. Identificare interactiunilor dintre aceste obiecte
(mesajele prin care comunica obiectele).
- II. Abstractizarea
1. Stabilirea claselor ale caror
instantiere sunt aceste obiecte; 2. Elaborarea ierarhiei de
clase.
- III. Implementarea
1. Impartirea pe module (clase) a
ierarhiei de clase; 2. Elaborarea claselor de baza
(fundamentale); 3. Elaborarea celorlalte clase din ierarhie (in
aceasta etapa se determina si disfunctiile in proiectare/implementare a claselor
de baza si este posibila revenirea la pct. 2). 4. Asamblarea intr-un
tot unitar a modulelor (claselor)
- IV. Testarea (se realizeaza si pe parcursul etapelor III:
2-4)
- V. Scrierea de documentatie (eventual in timpul etapelor III si
eventual IV).
Perioada de viata a unui sistem nu se rezuma insa la proiectarea si
dezvoltarea sa; ea continua cu lansarea de noi versiuni, corectarea erorilor ce
nu au fost detectate in cadrul etapei de testare, adaptarea sa functie de
cerintele utilizatorilor, etc.
In cadrul etapei de proiectare a sistemului orientat pe obiecte, nu trebuie
sa uitam ca un sistem complex bine construit este alcatuit din mai multe
componente simple (atomice) care interactioneaza intre ele. Sistemele monolitice
au costurile de proiectare, implementare si mai ales de intretinere mult mai
mari decat sistemele modulare. Programarea orientata pe obiecte ofera toate
avantajele in vederea crearii de sisteme modulare.
In ceea ce priveste ierarhiile de clase, exista doua categorii: in prima,
toate sau aproape toate clasele sunt derivate dintr-o clasa de baza, radacina;
intr-a doua, pot exista mai multe ierarhii de clase distincte. Avantajul de a
avea o singura clasa de baza este acela ca se poate evita cu usurinta mostenirea
multipla; dezavantajul este ca de multe ori in cadrul procesului de implementare
a claselor derivate poate aparea necesitatea modificarii clasei de baza.
In cadrul etapei de implementare a aplicatiei, este important sa se
stabileasca o conventie unitara privind stilul de programare utilizat. De cele
mai multe ori nu are importanta stilul adoptat, insa un stil unitar poate usura
foarte mult interconectarea dintre modulele componente si micsora costurile de
intretinere a sistemului. In continuare voi face cateva sugestii:
Indentarea
- dimensiunea tabularii sa fie de 2-4 caractere
Acoladele
-acoladele corespunzatoare sa fie aliniate vertical
-pe liniile continand o acolada sa nu apara si cod
Liniile lungi
-dimensiunea unei linii de cod sa fie mai mica decat latimea ecranului
-daca o linie este impartita pe mai multe randuri, cel de-al doilea rand cat
si urmatoarele sa fie indentate
Codul sursa
-sa nu existe spatii inainte si dupa operatorii unari
-sa existe spatiu inainte si dupa operatorii binari
-sa existe un spatiu dupa virgule si punct si virgula, dar nu inainte
-sa nu existe spatii inainte si dupa paranteze
-sa existe spatiu inainte si dupa cuvintele cheie
-sa fie utilizate linii libere pentru a separa diverse module de cod sursa
si a mari lizibilitatea programului
Comentariile
-textul unui comentariu sa fie separat de "//" cu un spatiu
-pe cat posibil, sa fie utilizate comentariile stil C++, "//", in loc de
cele in stil C, "/* */"
-sa fie la obiect si de nivel cat mai inalt
-sa indice operatia realizata de catre functie, efectele secundare, tipul
parametrilor, valorile pe care le poate returna
Denumirile identificatorilor
-denumirile sa fie cat mai descriptive posibil
-sa fie evitate abrevierile criptice
-sa nu fie utilizata notatia "ungureasca" (care prevede includerea tipului
unei variabile in denumirea sa)
Drepturile de acces la membri
-sa se declare mai intai membrii public, apoi cei protected,
iar apoi cei private
-datele membre sa apara dupa declararea metodelor
-prima metoda declarata sa fie constructorul, apoi destructorul
Definirea claselor
-ordinea definirii metodelor sa fie aceeasi cu a declararii
acestora
Un ultim sfat: nu este de ajuns sa cititi cursuri, carti sau cod sursa! Cea
mai buna metoda pentru a invata un limbaj de programare este de a scrie efectiv
cod.
|