Oop
Oop
ANUL I
2012-2013
Adrian DEACONU
2009 - 2010
Cuvânt înainte
Cartea de faţă se doreşte a fi, în principal, un ghid pentru studenţii din domeniul
Informatică, dar, evident, ea poate fi utilă tuturor celor care vor să înveţe să programeze
orientat pe obiecte, în general şi în C++, în particular. Este bine ca cel care citeşte această
lucrare să nu fie începător în programare şi, mai mult, este trebuie să aibă cunoştiinte avansate
despre limbajul C. Anumite concepte generale cum ar fi constante, variabile, funcţii, tipuri
numerice, caractere, string-uri, pointeri, tablouri etc. se consideră cunoscute.
Lucrarea este structurată pe două părţi.
În prima parte se prezintă elementele introduse odată cu apariţia limbajului C++, care
nu existau în C şi care nu au neaparat legatură cu programarea orientată p e obiecte.
În partea a doua este facută o prezentare teoretică a programarii orientate pe obiecte,
introducand şi conceptele POO. După această scurtă prezentare pur teoretică se prezintă
programarea orientată pe obiecte din C++. Tot aici sunt prezentate şi o parte din clasele care
se instalează odată cu mediul de programare (clase pentru lucrul cu fluxuri, clasa complex etc.).
La fiecare capitol, sunt date exemple sugestive, care ilustrează din punct de vedere
practic elemente de noutate. Este bine ca aceste exemple să fie înţelese şi, acolo unde este
nevoie, să fie scrise şi rulate de către cititor. Programele din această carte nu conţin erori,
deoarece ele au fost întâi testate şi abia apoi introduse în lucrare.
În general, tot ceea ce este prezentat în această carte (teorie şi aplicatii) este recunoscut
atât de compilatorul C++ al firmei Borland, cât şi de compilatorul Visual C++ al companiei
Microsoft.
Autorul.
Introducere
Limbajul C++ este o extensie a limbajului C. Aproape tot ce ţine de limbajul C este
recunoscut şi de către compilatorul C++. Limbajul C++ a apărut ca o necesitate, în sensul că el a
adus completări limbajului C care elimină câteva neajunsuri mari ale acestuia. Cel mai important
neajuns al limbajului C este lipsa posibilităţii de a scrie cod orientat pe obiecte în adevăratul sens
al cuvantului. În C se poate scrie într-o manieră rudimentară cod orientat pe obiecte folosind
tipul struct, în interiorul căruia putem avea atât câmpuri, cât şi metode. Orientarea pe obiecte cu
tipul struct are câteva mari lipsuri: membrii săi se comportă toţi ca nişte membri publici (accesul
la ei nu poate fi restrictionat), nu avem constructori, destructori, moştenire etc.
Limbajul C a fost lansat în anul 1978 şi s-a bucurat încă de la început de un real succes.
Acest lucru s-a datorat uşurinţei cu care un programator avansat putea scrie programe în
comparaţie cu restul limbajelor ce existau atunci pe piaţă, datorită în special modului abstract şi
laconic în care se scrie cod. De asemenea, modul de lucru cu memoria, cu fişiere este mult mai
transparent. Acest lucru are ca mare avantaj viteza crescută de execuţie a aplicaţiilor, dar poate
foarte uşor conduce (mai ales pentru începatori) la erori greu de detectat, datorate “călcării” în
afara zonei de memorie alocate.
La sfârşitul anilor ’80 a apărut limbajul C++ ca o extensie a limbajului C. C++ preia
facilităţile oferite de limbajul C şi aduce elemente noi, dintre care cel mai important este
noţiunea de clasă, cu ajutorul căreia se poate scrie cod orientat pe obiecte în toată puterea
cuvantului. Limbajul C++ oferă posibilitatea scrierii de funcţii şi clase şablon, permite
redefinirea (supraîncarcarea) operatorilor şi pentru alte tipuri de date decât pentru cele care există
deja definiţi, ceea ce oferă programatorului posibilitatea scrierii codului într-o manieră mult mai
elegantă, mai rapidă şi mai eficientă.
În anul 1990 este finalizat standardul ANSI-C, care a constituit baza elaborării de către
firma Borland a diferitelor versiuni de medii de progra mare.
În prezent sunt utilizate într-o mare măsură limbajele Java (al cărui compilator este
realizat firma Sun) şi Visual C++ (care face parte din pachetul Visual Studio al firmei
Microsoft), care au la baza tot standardul ANSI-C. Există însă şi competitori pe masură. Este
vorba în prezent în special de limbajele ce au la bază platforma .NET - alternativa Microsoft
pentru maşina virtuală Java. Poate cel mai puternic limbaj de programare din prezent este C#
(creat special pentru platforma .NET). Limbajul C# seamănă cu C/C++, dar totuşi el nu este
considerat ca facând parte din standardul ANSI-C.
În C++ se poate programa orientat pe obiecte ţinându-se cont de toate conceptele:
abstractizare, moştenire, polimorfism etc.
Odată cu mediul de programare al limbajului C++ (fie el produs de firma Borland sau de
firma Microsoft) se instalează şi puternice ierarhii de clase, pe care programatorul le poate folosi,
particulariza sau îmbogăţi.
Obiectivele cursului
Cursul intitulat Programare orientată pe obiecte I are ca obiectiv principal
familiarizarea studenţilor cu modul de gândire orientat pe obiecte în general şi cu
programare orientată pe obiecte din C++ în particular.
2
Resurse
Parcurgerea unităţilor de învăţare aferente ambelor module necesită instalarea unui
mediu de programare C++, este de preferat Visual C++ 2008.
Structura cursului
Cursul este structurat în două module, astfel: primul modul cuprinde patru unităţi
de învăţare, iar al doilea modul cuprinde zece unităţi de învăţare. La rândul ei,
fiecare unitate de învăţare cuprinde: obiective, aspecte teoretice privind tematica
unităţii de învăţare respective, exemple, teste de autoevaluare precum şi probleme
propuse spre discuţie şi rezolvare.
De asemenea, la sfarşitul cursului este ataşată o anexă legată de urmărirea
execuţiei unei aplicaţii pas cu pas şi o bibliografie.
La sfârşitul unităţilor de învăţare sunt indicate teme de control. Rezolvarea acestor
teme de control este obligatorie. Temele vor fi trimise de către studenţi prin e -mail.
Cuprins
Introducere
3
valori implicite pentru parametrii funcţiilor
funcţiile inline
funcţiile şi clasele şablon (suport adevărat pentru programare generică)
tratarea excepţiilor stil modern (folosind instrucţiunea try … catch)
supraîncarcarea operatorilor
clasa complex
etc.
Competenţe
La sfârşitul acestui modul studenţii vor:
cunoaste completarile sunt aduse de C++ limbajului C;
şti cum se declară variabilele în C++
înţelege cum se lucrează cu fluxuri în C++ în general şi cum se fac citirile de la
tastatură, respectiv afişările pe ecran în particular
cunoaste mdul de alocarea dinamică a memoriei în C++
face cunostiinta cu completarile aduse la modul de scriere al funcţiilor în C ++
şti să supraîncarce operatori
şti să trateze excepţiile in C++
Cuprins
4
Durata medie de parcurgere a unităţii de învăţare este de 2 ore.
5
U1.1. Declaraţia variabilelor în C++
În C variabilele locale trebuie să fie declarate pe primele linii ale corpului funcţiei
(inclusiv în funcţia principală). În C++ declaraţiile variabilelor pot fi făcute aproape oriunde în
program. Ele vor fi cunoscute în corpul funcţiei din locul în care au fost declarate în jos.
Declaraţiile de variabile pot apărea chiar şi în interiorul instrucţiunii for. Iată un exemplu în acest
sens:
int n=10,a[10];
for (int s=0,i=0;i<n;i++)
s+=a[i];
float ma=s/n;
Spre deosebire de C++, în Java nici variabila i din exemplul de mai sus nu ar fi fost
cunoscută după ce se iese din instrucţiunea for.
int n;
float x;
cin>>n>>x;
6
Instrucţiunea de mai sus are următoarea semnificaţie: din fluxul standard de intrare se
extrag două valori (una întreagă şi apoi una reală). Cele două valori se depun în variabilele n şi
respectiv x.
Pentru afişarea pe ecranul monitorului se foloseşte obiectul cout care corespunde fluxului
standard de ieşire stdout. Iată un exemplu.
În fluxul standard de ieşire se trimit: o constantă de tip string, o valoare întreagă (cea
reţinută în variabila n), un alt string constant şi o valoare reală (reţinută în variabila y). După
afişare, manipulatorul endl introdus de asemenea în flux face salt la începutul următoarei linii de
pe ecran.
În C++ avem două obiecte pentru fluxuri de erori. Este vorba de cerr şi clog. Primul
obiect corespunde fluxului standard de erori stderr din C. Introducerea de date în fluxul cerr are
ca efect afişarea lor imediată pe ecranul monitorului, dar pe altă cale decât cea a fluxului cout.
Al doilea flux de erori (clog) nu are corespondent în C. Acest flux este unul buffer-izat, în sensul
că mesajele de erori se colectează într-o zonă de memorie RAM, de unde pot ajung pe ecran
numai când se doreşte acest lucru, sau la terminarea execuţiei programului. Golirea buffer-ului
pe ecran se poate face apelând metoda flush(), sub forma:
clog.flush();
U1.3. Manipulatori
Manipulatorii pot fi consideraţi nişte funcţii speciale care se introduc în lanţurile de
operatori << sau >> în general pentru formatare. În exemplul din capitolul anterior am folosit
manipulatorul endl, care face salt la linie nouă.
Manipulatorii fără parametri sunt descrişi în fişierul antet “iostream.h”, iar cei cu
parametri apar în fişierul antet “iomanip.h”. Dăm în continuare lista manipulatorilor:
Manipulator Descriere
Dec Pregăteşte citirea/scrierea întregilor în baza 10
Hex Pregăteşte citirea/scrierea întregilor în baza 16
Oct Pregăteşte citirea/scrierea întregilor în baza 8
Ws Scoate toate spaţiile libere din fluxul de intrare
Endl Trimite caracterul pentru linie nouă în fluxul de ieşire
Ends Inserează un caracter NULL în flux
Flush Goleşte fluxul de ieşire
Resetiosflags(long) Iniţializează biţii de formatare la valoarile date de argumentul long
setiosflags(long) Modifică numai biţii de pe poziţiile 1 date de parametrul long
Stabileşte precizia de conversie pentru numerele în virgulă mobilă
Setprecision(int)
(numărul de cifre exacte)
Stabileşte lungimea scrierii formatate la numărul specificat de
setw(int)
caractere
Stabileşte baza în care se face citirea/scrierea întregilor (0, 8, 10
setbase(int)
sau 16), 0 pentru bază implicită
Stabileşte caracterul folosit pentru umplerea spaţiilor goale în
setfill(int)
momentul scrierii pe un anumit format
7
Biţii valorii întregi transmise ca parametru manipulatorilor setiosflags şi resetiosflags
indică modul în care se va face extragerea, respectiv introducerea datelor din/în flux. Pentru
fiecare dintre aceşti biţi în C++ există definită câte o constantă.
După cum le spune şi numele, indicatorii de formatare arată modul în care se va face
formatarea la scriere, respectiv la citire în/din flux. Indicatorii de formatare sunt constante întregi
definite în fişierul antet “iostream.h”. Fiecare dintre aceste constante reprezintă o putere a lui 2,
din cauză că fiecare indicator se referă numai la un bit al valorii întregi în care se memorează
formatarea la scriere, respectiv la citire. Indicatorii de formatare se specifică în parametrul
manipulatorului setiosflags sau resetiosflags. Dacă vrem să modificăm simultan mai mulţi biţi de
formatare, atunci vom folosi operatorul | (sau pe biţi).
Dăm în continuare lista indicatorilor de formatare:
Indicator Descriere
ios::skipws Elimină spaţiile goale din buffer-ul fluxului de intrare
ios::left Aliniază la stânga într-o scriere formatată
ios::right Aliniază la dreapta într-o scriere formatată
ios::internal Formatează după semn (+/-) sau indicatorul bazei de numeraţie
ios::scientific Pregăteşte afişarea exponenţială a numerelor reale
ios::fixed Pregăteşte afişarea zecimală a numerelor reale (fără exponent)
ios::dec Pregăteşte afişarea în baza 10 a numerelor întregi
ios::hex Pregăteşte afişarea în baza 16 a numerelor întregi
ios::oct Pregăteşte afişarea în baza 8 a numerelor întregi
Foloseşte litere mari la afişarea numerelor (lietra ‘e’ de la exponent şi
ios::uppercase
cifrele în baza 16)
ios::showbase Indică baza de numeraţie la afişarea numerelor întregi
ios::showpoint Include un punct zecimal pentru afişarea numerelor reale
ios::showpos Afişează semnul + în faţa numerelor pozitive
ios::unitbuf Goleşte toate buffer-ele fluxurilor
ios::stdio Goleşte buffer-ele lui stdout şi stderr după inserare
De exemplu, pentru a afişa o valoare reală fără exponent, cu virgulă, aliniat la dreapta, pe
8 caractere şi cu două zecimale exacte procedăm astfel:
float x=11;
cout<<setiosflags(ios::fixed|ios::showpoint|ios::right
);
cout<<setw(8)<<setprecision(2)<<x;
void main()
{
int i=100;
cout<<setfill('.');
cout<<setiosflags(ios::left);
cout<<setw(10)<<"Baza 8";
cout<<setiosflags(ios::right);
cout<<setw(10)<<oct<<i<<endl;
cout<<setiosflags(ios::left);
cout<<setw(10)<<"Baza 10";
cout<<setiosflags(ios::dec | ios::right);
cout<<setw(10)<<i<<endl;
cout<<setiosflags(ios::left);
cout<<setw(10)<<"Baza 16";
cout<<setiosflags(ios::right);
cout<<setw(10)<<hex<<i<<endl;
}
Baza 8...........144
Baza 10..........100
Baza 16...........64
Rezumat
Teme de control
-------------------------------------------------------
|Nr. | NUMELE SI PRENUMELE |Varsta|
|crt.| | |
|----|-----------------------------------------|------|
| 1|Ion Monica | 19|
| 2|Ionescu Adrian Ionel | 25|
| 3|Popescu Gigel | 17|
| 4|Popescu Maria | 28|
|----------------------------------------------|------|
| Media varstelor: | 22.25|
-------------------------------------------------------
Cuprins
Eliberarea memoriei alocate dinamic anterior se poate face cu ajutorul operatorului delete
astfel:
delete pointer_catre_tip;
struct TArbBin
{
char info[20];
struct TArbBin *ls,*ld;
}*rad;
// ....
//....
delete rad;
int n,*a;
//....
delete [] a;
11
În C++ alocarea şi eliberarea dinamică a memoriei pentru o matrice de numere reale se
poate face ceva mai uşor decât în C folosind operatorii new şi delete.
Prezentăm în continuare două posibile metode de alocare dinamică a memorie pentru o
matrice de ordin 2 (cu m linii şi n coloane).
#include<iostream.h>
#include<malloc.h>
int main(void)
{
int m,n;
float **a;
//....
for (i=0;i<m;i++)
delete [] a[i];
delete [] a;
return 0;
}
#include<iostream.h>
#include<malloc.h>
int main(void)
{
int m,n;
float **a;
//....
for (i=0;i<m;i++)
delete [] a[i];
delete [] a;
return 0;
}
Pentru a intelege mai bine ideea de mai sus de alocare a memoriei pentru o matrice, să
vedem ce se întamplă în memorie.
Prima data se alocă memorie pentru un vector (cu m elemente) de pointeri către tipul float
(tipul elementelor matricii). Adresa către această zonă de memorie se reţine în pointerul a. După
prima alocare urmează m alocări de memorie necesare stocării efective a elementelor matricei. În
vectorul de la adresa a (obţinut în urma primei alocări) se reţin adresele către începuturile celor
m zone de memorie carespunzătoare liniilor matricei. Tot acest mecanism de alocare a memoriei
este ilustrat în figura 1.
a
a[0][0] a[0][1] …. a[0][n-1]
a[0]
a[1][0] a[1][1] …. a[1][n-1]
a[1] ….
a[m-1]
Eliberarea memoriei necesare stocării matricei se face evident tot în m+1 paşi.
Avantajul alocării dinamice pentru o matrice în stilul de mai sus este dat de faptul că nu
este necesară o zonă de memorie continuă pentru memorarea elementelor matricei. Dezavantajul
constă însă în viteza scăzută de execuţie a programului în momentul alocării şi eliberării
memoriei (se fac m+1 alocări şi tot atâtea eliberări de memorie).
13
Propunem în continuare o altă metodă de alocare a memoriei pentru o matrice cu numai
două alocări de memorie (şi două eliberări).
#include<iostream.h>
#include<malloc.h>
int main(void)
{
int m,n;
float **a;
//....
delete [] a[0];
delete [] a;
return 0;
}
În cazul celei de a doua metode, întâi alocăm de asemenea memorie pentru a reţine cele
m adrese de început ale liniilor matricei, după care alocăm o zonă de memorie continuă necesară
stocării tuturor celor m*n elemente ale matricei (întâi vom reţine elementele primei linii, apoi
elementele celei de a doua linii etc.). Adresa de început a zonei de memorie alocate pentru
elementele matricei este reţinută în pointerul a[0]. În a[1] se reţine adresa celei de a (n+1)-a
căsute de memorie (a[1]=a[0]+n), adică începutul celei de-a doua linii a matricei. În general, în
a[i] se reţine adresa de inceput a liniei i+1, adică a[i]=a[i-1]+n=a[0]+i*n. Schema de
alocare a memoriei este prezentată în figura 2.
Este evident că al doilea mod de alocare a memoriei este mai rapid decât primul (cu
numai două alocări şi două eliberări) şi, cum calculatoarele din prezent sunt înzestrate cu
memorii RAM de capacitate foarte mare, alocarea unei zone mari şi continue de memorie nu mai
reprezintă un dezavantaj. Aşa că în practică preferăm a doua modalitate de alocare dinamică a
memorie pentru o matrice.
14
a[0] a[1] …. a[m-1]
a
a[0][0] … a[0][n-1] a[1][0] … a[m-1][0] … a[m-1][n-1]
Rezumat
C++ oferă o alternativă mai elegantă şi modernă pentru alocarea dinamică a memoriei,
folosind operatorii new şi delete. Cu ajutorul acestor operatori alocăm memorie mai uşor, fără a
mai fi nevoie de conversii şi fără apeluri de funcţii.
Teme de control
1. Să se aloce dinamic memorie pentru un vector de vectori de elemente de tip double cu
următoarea proprietate: primul vector are un element, al doilea are două elemente, în
general al k-lea vector are k elemente, k{1, 2, …, n}. Să se citească de la tastatură n
(numărul de vectori) precum şi elementele vectorilor. Să se construiască un nou
vector format cu mediile aritmetice ale celor n vectori. În final să se elibereze toate
zonele de memorie alocate dinamic.
15
4. Scrieţi funcţii pentru introducerea unui element într-o stivă de caractere, scoaterea
unui element din stivă, afişarea conţinutului stivei şi eliberarea meoriei ocupate de
stivă. Stiva se va memora dinamic folosind pointeri către tipul struct.
7. Scrieţi o funcţie pentru introducerea unei valori reale într -un arbore binar de căutare şi
o funcţie pentru parcurgerea în inordine a arborelui binar. Folosiţi aceste funcţii
pentru a sorta un vector de numere reale citit de la tastatură. Pentru memorarea
arborelui se vor folosi pointeri către tipul struct.
Cuprins
În C++ se pot scrie mai multe funcţii cu acelaşi nume, dar cu parametri diferiţi (ca număr
sau/şi ca tip), în engleza overloading. La apelul unei funcţii se caută varianta cea mai apropiată
de modul de apelare (ca număr de parametrii şi ca tip de date al parametrilor).
16
De exemplu, putem scrie trei funcţii cu acelaşi nume care calculează maximul dintre
două, respectiv trei valori:
# include <iostream.h>
void main(void)
{
int a=1,b=2,c=0,max1,max2;
float A=5.52f,B=7.1f,max3;
double A2=2,B2=1.1,max4;
În C transmiterea parametrilor în funcţie se face prin valoare (pentru cei de intrare) sau
prin adresă (pentru cei de ieşire). Transmiterea parametrilor prin adresă este pretenţioasă (la apel
suntem obligaţi în general să utilizăm operatorul adresa &, iar în corpul funcţiei se foloseşte
operatorul *).
În C++ transmiterea parametrilor de ieşire (care se returnează din funcţii), se poate face
într-o manieră mult mai elegantă, şi anume prin referinţă. În definiţia funcţiei, fiecare parametru
transmis prin referinţă este precedat de semnul &.
17
Dăm ca exemplu interschimbarea valorilor a două variabile în ambele forme (transmitere
prin adresă şi prin referinţă).
# include <iostream.h>
void main()
{
int x=1,y=2;
# include <iostream.h>
18
else (*p)++;
*b=new int[*n];
*c=new int[*p];
int k=0,h=0;
for (i=0;i<m;i++)
if ((*a)[i]>=0) (*b)[k++]=(*a)[i];
else (*c)[h++]=(*a)[i];
delete [] *a;
}
void main()
{
int i,n,n1,n2,*a,*a1,*a2;
# include <iostream.h>
19
void separare(int m,int *&a,int &n,int *&b,int &p,int *&c)
{
n=0;
p=0;
for (int i=0;i<m;i++)
if (a[i]>=0) n++;
else p++;
b=new int[n];
c=new int[p];
int k=0,h=0;
for (i=0;i<m;i++)
if (a[i]>=0) b[k++]=a[i];
else c[h++]=a[i];
delete [] a;
}
void main()
{
int i,n,n1,n2,*a,*a1,*a2;
În C++ există posibilitatea ca la definirea unei funcţii o parte dintre parametri (transmişi
prin valoare) să primească valori implicite. În situaţia în care lipsesc argumente la apelul
funcţiei, se iau valorile implicite dacă există pentru acestea. Numai o parte din ultimele
argumente din momentul apelului unei functii pot lipsi şi numai dacă există valorile implicite
pentru acestea în definiţia funcţiei. Nu poate lipsi de exemplu penultimul argument, iar ultimul
să existe în momentul apelului funcţiei.
Dăm un exemplu simplu în care scriem o funcţie inc pentru incrementarea unei variabile
întregi (similară procedurii cu acelaşi nume din limbajele Pascal şi Delphi):
20
void inc(int &x,int i=1)
{
x+=i;
}
Funcţia inc poate fi apelată cu unul sau doi parametri. Astfel, apelul inc(a,5) este
echivalent cu x+=5 (în corpul funcţiei variabila i ia valoarea 5, valoare transmisă din apelul
funcţiei). Dacă apelăm însă funcţia sub forma inc(a), atunci pentru i se ia valoarea implicită
1, situaţie în care x se măreşte cu o unitate.
Scrieţi o functie care primeşte 5 parametri de tip int care returnează maximul celor 5
valori. Daţi valori implicite parametrilor aşa încât funcţia să poată fi folosită pentru a calcula
maximul a două numere întregi (când se apelează cu 2 parametri), a trei, patru şi respectiv cinci
valori întregi.
În C++ există posibilitatea declarării unei funcţii ca fiind inline. Fiecare apel al unei
funcţii inline este înlocuit la compilare cu corpul funcţiei. Din această cauză funcţiile inline se
aseamănă cu macrocomenzile. Spre deosebire însă de macrocomenzi, funcţiile inline au tip
pentru parametrii şi pentru valoarea returnată. De fapt, ele se declară şi se descriu ca şi funcţiile
obişnuite, numai că în faţa definiţiei se pune cuvântul rezervat inline. Modul de apel al
macrocomenzilor diferă de cel al funcţiilor inline. În acest sens dăm un exemplu comparativ în
care scriem o macrocomandă pentru suma a două valori şi respectiv o funcţie inline pentru suma
a două valori întregi.
# include <iostream.h>
void main()
{
int x;
x=2*suma(5,3);
cout<<x<<endl;
x=2*suma2(5,3);
cout<<x<<endl;
}
Pe ecran se vor afişa valorile 13 şi respectiv 16. Primul rezultat poate fi pentru unii
neaşteptat. Dacă suntem familiarizaţi cu modul de utilizare al macrocomenzilor rezultatul nu mai
este însă deloc surprinzător. La compilare, apelul suma(5,3) este înlocuit efectiv în cod cu 5+3,
ceea ce înseamnă că variabilei x i se va atribui valoarea 2*5+3, adică 13.
Din cauză că apelurile funcţiei inline se înlocuiesc cu corpul ei, codul funcţiei inline
trebuie să fie în general de dimensiuni mici. În caz contrar şi/sau dacă apelăm des în program
funcţiile inline, dimensiunea executabilului va fi mai mare.
21
De reţinut este faptul că în cazul compilatorului Borland C++ nu se acceptă intrucţiuni
repetitive şi nici instrucţiuni throw (vezi capitolul dedicat tratării excepţiilor) în corpul funcţiei
inline. Dacă incercăm totuşi utilizarea lor în corpul unei funcţii declarate inline, atunci la
compilare funcţia va fi considerată obişnuită (ignorându-se practic declaratia inline) şi se va
genera un warning.
Funcţiile inline sunt foarte des utilizate în descrierea claselor. Astfel, funcţiile ”mici”,
fără cicluri repetitive, pot fi descrise inline, adică direct în corpul clasei.
Unul dintre cele mai frumoase suporturi pentru programare generică este oferit de
limbajul C++ prin intermediul funcţiilor şi claselor şablon (template). Astfel, în C++ putem scrie
clase sau funcţii care pot funcţiona pentru unul sau mai multe tipuri de date nespecificate. Să
luăm spre exemplu sortarea unui vector. Ideea algoritmului este aceeaşi pentru diverse tipuri de
date: întregi, reale, string-uri etc. Fără a folosi şabloane ar trebui să scriem câte o funcţie de
sortare pentru fiecare tip de date.
Înaintea fiecărei funcţii şablon se pune cuvântul rezervat template urmat de o enumerare
de tipuri de date generice (precedate fiecare de cuvântul rezervat class). Enumerarea se face între
semnele < (mai mic) şi > (mai mare):
T1, T2, … sunt tipurile generice de date pentru care scriem funcţia şablon. Este esenţial
de reţinut faptul că toate tipurile de date generice trebuie folosite în declararea parametrilor
funcţiei.
În cele mai multe cazuri se foloseşte un singur tip generic de date.
Iată câteva exemple simple de funcţii şablon:
Dacă în interiorul aceluiaşi program avem apeluri de funcţii şablon pentru mai multe
tipuri de date, atunci pentru fiecare dintre aceste tipuri de date compilatorul generează câte o
funcţie în care tipurile generice de date se înlocuiesc cu tipurile de date identificate la întâlnirea
apelului. De asemenea, la compilare se verifică dacă sunt posibile instanţele funcţiilor şablon
pentru tipurile de date identificate. De exemplu, compilarea codului funcţiei şablon inc nu
generează erori, dar tentativa de apelare a ei cu doi parametrii de tip string se soldează cu eroare
la compilare pentru că cele două string-uri se transmit prin doi pointeri (către char), iar
operatorul += nu este definit pentru doi operanzi de tip pointer.
Dăm în continuare câteva posibile apeluri ale funcţiilor şablon de mai sus:
int i=2,j=0,k;
double m,a[4]={1.5,-5E2,8,0},x=2,y=5.2;
char *s1=”Un string”,*s2=”Alt string”,*s;
Funcţiile şablon şi clasele şablon alcătuiesc fiecare câte o clasă de funcţii, respectiv de
clase, în sensul că ele au aceeaşi funcţionalitate, dar sunt definite pentru diverse tipuri de date. O
funcţie template de sortare putem spune că este de fapt o clasă de funcţii de sortare pentru toate
tipurile de date care suportă comparare. În cazul sortării, tipului nespecificat este cel al
elementelor vectorului.
Clasele şablon le vom prezenta într-un capitol următor.
Rezumat
23
Teme
1. Funcţie care primeşte ca primi parametrii lungimea unui vector de numere întregi
precum şi pointerul către acest vector. Să se construiască un alt vector (alocat
dinamic) format cu elementele care sunt numere prime ale vectorului iniţial. Să se
returneze prin referinţă lungimea vectorului construit precum şi pointerul către noul
vector.
Cuprins
24
Obiectivele unităţii de învăţare
După cum putem obseva din lista de mai sus, operatorii care pot fi supraîncărcaţi sunt
unari sau binari, adică au aritatea 1 sau 2. De altfel, singurul operator C/C++ ternar ?: nu poate fi
redefinit.
Un operator se defineşte asemănător cu o funcţie. La definirea operatorului trebuie să se
ţină însă cont de aritatea lui, care trebuie să coincidă cu numărul parametrilor, dacă funcţia este
externă unei clase, iar în loc de numele funcţiei apare cuvântul rezervat operator urmat de
simbolul sau simbolurile care caracterizează acel operator:
tipret operator<simbol(uri)>(parametri)
{
// corpul operatorului
}
struct Multime
{
int n,e[1000];
};
În campul n vom reţine numărul de elemente al mulţimii, iar în e vom reţine elementele
mulţimii.
În C++ la tipul struct Multime ne putem referi direct sub forma Multime.
Pentru tipul Multime vom arăta vom supraîncărca următorii operatori: + pentru reuniunea
a două mulţimi şi pentru adăugarea unui element la o mulţime, << pentru introducerea
elementelor mulţimii într-un flux de ieşire şi >> pentru extragerea elementelor unei mulţimi
dintr-un flux de intrare.
Cea mai simplă metodă (de implementat) pentru reuniunea a două mulţimi este:
Complexitatea algoritmului folosit mai sus pentru a reuni mulţimile a şi b este evident
O(a.n b.n). Se poate însă şi mai bine. În loc de căutarea secvenţială a elementului b.e[i] în
vectorul a.e putem aplica o căutare rapidă, bineînţeles dacă sortăm rapid în prealabil vectorul a.e.
Complexitatea algoritmului devine: O(a.n log(a.n) + b.n log(a.n)) = O((a.n + b.n)
log(a.n)). Dacă reunim însă mulţimea b cu a (în ordine inversă), atunci complexitatea devine
O((a.n + b.n) log(b.n)). Este evident că, pentru a îmbunătăţi complexitatea algoritmului, vom
reuni a cu b dacă b.n > a.n şi, respectiv b cu a în situaţia în care a.n > b.n. Complexitatea devine
atunci O((a.n + b.n) log(min{b.n, a.n})) = O(max{a.n, b.n} log(min{b.n, a.n})).
Pentru sortarea mulţimii cu mai puţine elemente vom aplica algoritmul de sortare prin
interclasare pentru că el are în orice situaţie complexitatea O(m log(m)), unde m este numărul
de elemente.
Reuniunea rapidă a două mulţimi este:
a1=new int[m-s+1];
a2=new int[d-m];
sortIntercls(s,m,a,a1);
sortIntercls(m+1,d,a,a2);
intercls(m-s+1,a1,d-m,a2,b);
delete [] a1;
delete [] a2;
}
if (a.n<b.n)
{
if(a.n)sortIntercls(0,a.n-1,a.e,c.e);
c.n=a.n;
for (i=0;i<b.n;i++)
if (!cautBin(b.e[i],0,a.n-1,c.e))
c.e[c.n++]=b.e[i];
}
else
{
if(b.n)sortIntercls(0,b.n-1,b.e,c.e);
c.n=b.n;
for (i=0;i<a.n;i++)
if (!cautBin(a.e[i],0,b.n-1,c.e))
c.e[c.n++]=a.e[i];
}
return c;
}
Pentru testarea operatorilor care i-am scris pentru tipul Multime propunem următoarea
funcţie principală:
void main()
{
int x;
Multime a,b,c;
Pentru tipul Multime nu putem supraîncărca operatorul = pentru că el este definit deja
pentru operanzi de tipul struct !
În final prezentăm câteva reguli de care trebuie să ţinem seama atunci când
supraîncărcăm un operator:
29
9. Un operator, ca în cazul unei funcţii, poate fi definit în 2 feluri: ca fiind membru unei
clase sau ca fiind extern unei clase, dar nu simultan sub ambele forme pentru aceleaşi
tipuri de date.
10. Operatorii <<, >> se definesc de obicei ca funcţii externe unei clase pentru că primul
operand este de obicei un flux.
Facem menţiunea că în cele mai multe cazuri operatorii sunt supraîncărcaţi pentru obiecte
în momentul în care scriem o clasă nouă. Vom ilustra acest lucru la momentul potrivit.
Rezumat
În C++ operatorii pot fi redefiniţi şi pentru alte tipuri de date decât cele pentru care există
deja. Supraîncărcarea unui operator se face foarte asemănător cu descrierea unei funcţii. Practic
diferenţa esenţială constă în faptul că numele funcţiei ce descrie operatorul este obligatoriu
format din cuvântul rezervat operator urmat de simbolurile ce definesc acel operator.
Teme de control
Propunem cititorului să implementeze pentru tipul Multime definit mai sus următorii
operatori:
1. operatorul – pentru diferenţa a două multimi şi pentru scoaterea unui element dintr-o
mulţime
2. operatorul * pentru intersecţia a două mulţimi
3. operatorii +=, -=, *=
4. operatorul ! care returnează numărul de elemente al mulţimii
5. operatorii <= şi < pentru a verifica dacă o mulţime este inclusă, respectiv strict
inclusă în altă mulţime
6. operatorii >= si > pentru a verifica dacă o mulţime conţine, respectiv conţine strict
altă mulţime
7. operatorul < pentru a verifica dacă un element aparţine unei mulţimi
8. operatorii == şi != pentru a verifica dacă două mulţimi coincid ca şi conţinut,
respectiv sunt diferite.
Scrieţi tipul Multime, funcţiile (fără main) şi operatorii de mai sus într-un fişier cu numele
Multime.cpp. În viitor, de fiecare dată când veţi avea nevoie într-o aplicaţie să lucraţi cu mulţimi
veţi putea include acest fişier.
30
Excepţiile se împart în excepţii sincrone şi asincrone. Excepţiile ce pot fi detectate cu
uşurinţă sunt cele de tip sincron. În schimb, excepţiile asincrone sunt cauzate de evenimente care
nu pot fi controlate din program.
Este mai puţin cunoscut faptul că în limbajul C există posibilitatea tratării excepţiilor
folosind funcţiile setjmp şi longjmp:
# include <conio.h>
# include <stdio.h>
# include <stdlib.h>
# include <process.h>
# include <setjmp.h>
void mesaj_eroare()
{
perror("Au fost erori in timpul executiei!");
getch();
}
void mesaj_ok()
{
perror("Program incheiat cu succes!");
getch();
}
void main()
{
char numef[100];
FILE *fis;
jmp_buf stare;
31
longjmp(stare,1); // salt la setjmp si
} // se trece pe ramura else
else
{
perror("Eroare! Nu am putut deschide fis.");
atexit(mesaj_eroare); // mesaj er. parasire progr.
exit(1);
}
fclose(fis);
atexit(mesaj_ok); // mesaj parasire program cu succes
}
În exemplul de mai sus am folosit funcţia atexit care specifică funcţia ce se va apela
imediat înainte de părăsirea programului.
Dacă dorim să semnalăm faptul că un program s-a încheiat cu insucces, putem apela
funcţia abort în locul funcţiei exit. Funcţia abort părăseşte programul şi returnează valoarea 3, nu
înainte însă de a afişa mesajul Abnormal program termination.
În C++ tratarea excepţiilor este mult mai modernă. Tratarea excepţiilor se face cu ajutorul
instrucţiunii try. După cuvântul rezervat try urmează un bloc de instrucţiuni neapărat delimitat de
acolade. Se încearcă execuţia (de unde şi denumirea) instrucţiunilor din blocul try şi dacă se
generează o excepţie, atunci execuţia instrucţiunilor blocului try este întreruptă şi excepţia este
captată (prinsă) şi tratată eventual pe una din ramurile catch ce urmează după blocul try. Facem
menţiunea că instrucţiunile ce se execută pe o ramură catch sunt de asemenea delimitate
obligatoriu de acolade.
Dăm un exemplu de tratare a excepţiei ce apare în urma împărţirii prin zero:
#include<iostream.h>
void main()
{
int a=1,b=0;
try
{
cout<<(a/b)<<endl;
}
catch (...)
{
cerr<<"Impartire prin zero."<<endl;
}
}
La prima apariţie a unei excepţii în interiorul blocului try se generează o valoare a cărei
tip va face ca excepţia să fie tratată pe o anumită ramura catch. Între parantezele rotunde ce
urmează după cuvântul rezervat catch apare o variabilă sau apar trei puncte (...). Ramura catch
cu trei puncte prinde orice excepţie generată în blocul try care nu a fost prinsă de altă ramură
catch de deasupra.
În momentul în care detectăm o excepţie putem arunca o valoare cu ajutorul instrucţiunii
throw.
32
Ne propunem să scriem o funcţie pentru scoaterea rădăcinii pătrate dintr -un număr real cu
ajutorul metodei numerice de înjumătăţire a intervalului. Vom trata eventualele excepţii ce pot
apărea:
#include<iostream.h>
double s,d,m,fs,fd,fm;
if (v>1)
{
s=1;
d=v;
}
else
{
s=v;
d=1;
}
fs=fct(s,v);
fd=fct(d,v);
while (d-s>precizie)
{
m=(s+d)/2;
fm=fct(m,v);
if (!fm) return m;
if (fs*fm<0) {d=m; fd=fm;}
else {s=m; fs=fm;}
}
return m;
}
void main()
{
double x,p;
În funcţia radical parametrul real precizie indică distanţa maximă între soluţia numerică
(valoarea aproximativă a rădăcinii pătrate din v) găsită şi soluţia exactă (în situaţia noastră
valoarea exactă a rădăcinii pătrate din v). Funcţia radical generează excepţii într-una din
situaţiile: dacă v este negativ sau dacă precizia dată nu este un număr pozitiv.
În situaţia în care în funcţia radical trimitem o valoare negativă sau zero pentru precizie,
execuţia instrucţiunilor din corpul funcţiei este intreruptă şi se generează o valoare reală care
reţine precizia găsită ca fiind invalidă.
Dacă în funcţia radical trimitem o valoare v care este negativă, atunci execuţia
instructiunilor din corpul funcţiei este întreruptă şi se generează mesajul “Radical din numar
negativ.”.
Evident că la apelul funcţiei radical, în situaţia în care precizia nu este pozitivă, se va
intra pe a doua ramură catch a instrucţiunii try, iar dacă valoarea din care vrem să extragem
rădăcina pătrată este negativă se va ajunge pe prima ramură catch. În ambele situaţii se trimit
mesaje în fluxul standard de erori (cerr), mesaje ce vor apărea pe ecran.
Dacă folosim ramura catch cu trei puncte, ea trebuie pusă ultima. Astfel, numai
eventualele excepţii care scapă de ramurile catch de deasupra ajung pe ramura catch(…).
Instrucţiunea throw poate să nu returneze nici o valoare, situaţie în care se ajunge cu execuţia
programului pe ramura catch(…), dacă există.
După cum am văzut, instrucţiunea throw folosită în funcţia radical are un efect
asemănător cu un apel al instrucţiunii return în sensul că execuţia instructiunilor din corpul
funcţiei este întreruptă. Deosebirea este că înstructiunea throw dacă returnează o valoare, atunci
ea este interceptată numai în interiorul unui bloc try, iar instrucţiunea return returnează valoarea
funcţiei.
Pot exista situaţii în care o excepţie este identificată direct în interiorul blocului throw. Ea
poate fi aruncată spre a fi captată de una dintre ramurile catch. În exemplul următor testăm dacă
deîmpărţitul este zero înainte de a efectua împărţirea. Dacă deîmpărţitul este zero, aruncăm o
excepţie prin intermediul unui mesaj:
#include<iostream.h>
void main()
{
int a=1,b=0;
try
{
if (!b) throw "Impartire prin zero.";
34
cout<<(a/b)<<endl;
}
catch (char *mesajEroare)
{
cerr<<mesajEroare<<endl;
}
}
Rezumat
Temă
Modificaţi funcţiile legate de tipul Multime din capitolul anterior aşa încât să fie tratate şi
eventualele erori ce pot apărea în utilizarea lor.
35
Modulul 2. Programarea orientată pe obiecte în C++
Cuprins
Introducere
Competenţe
La sfârşitul acestui modul studenţii:
se vor familiariza cu conceptele programării orientate pe obiecte;
vor şti să scrie clase, ierarhii de clase în C ++
vor fi capabili să scrie cod generic orientat pe obiecte
vor putea să lucreze cu diverse fluxuri orientat p e obiecte: fişiere, string-uri etc.
Cuprins
La baza programării orientate pe obiecte (POO) stă conceptul de clasă. Clasa este o
colecţie de câmpuri (date) şi metode (funcţii) care în general utilizează şi prelucrează datele
clasei. Metodele se mai numesc şi funcţii membre clasei. În C++, o clasă se redactează cu
ajutorul tipului class, care poate fi considerată o îmbunătăţire a tipului struct din C.
O clasă este un tip abstract de date, la facilităţile căreia avem acces prin intermediul unui
obiect definit ca o instanţă a acelei clase. Obiectul este de fapt o variabilă de tip clasă. Un obiect
poate fi comparat cu o “cutie neagră” în care introducem şi extrage informaţii despre care ne
asigurăm când proiectăm şi redactăm clasa că se prelucrează corect. Obiectele sunt nişte “piese”
ce le asamblăm pe o “placă de bază” (programul principal) pentru a obţin e aplicaţia dorită.
Programarea orientată pe obiecte este mai mult decât o tehnică de programare, ea este un
mod de gândire. După cum am mai spus, într-o clasă găsim câmpuri şi metode. A programa
orientat pe obiecte nu înseamnă însă a scrie o mulţime de date şi funcţii grupate într-o clasă. Este
mult mai mult decât atât. Când se scrie un program trebuie simţită o împărţire firească în module
de sine stătătoare a codului aşa încât ele să conducă apoi la o îmbinare naturală şi facilă.
Programarea orientată pe obiecte (în engleza object oriented programming - OOP), pe
scurt POO, creşte modularitatea programului, iar depanarea şi modificările programelor ale caror
cod este scris orientat pe obiecte se realizează mult mai uşor. De asemenea, codul redactat
orientat pe obiecte poate fi oricând refolosit atunci când se scriu programe noi în care apar idei
asemănătoare.
POO devine indispensabilă atunci când se scriu programe de dimensiuni cel puţin medii.
În cazul acestor programe este necesar aportul mai multor programatori, care pot fi specializaţi
pe diferite domenii. Problema se împarte în probleme mai mici, care sunt repartizate la
programatori diferiţi. Redactarea obiect orientată permite îmbinarea mult mai uşoară a
secvenţelor de program, fără a fi nevoie de conversii sau adaptări. Scriind codul orientat pe
obiecte creăm o “trusă” de unelte care creşte în timp, unelte pe care le putem refolosi ulterior.
În continuare vom prezenta restul conceptelor care stau la baza programării orientate pe
obiecte: abstractizarea datelor, moştenirea, polimorfismul, încapsularea datelor.
Abstractizarea datelor, în engleză - data abstraction, reprezintă procesul de definire a
unui tip de date denumit tip abstract de date (în engleză – abstract data type, sau pe scurt
ADT), recurgând şi la ascunderea datelor.
Definirea unui tip abstract de date implică specificarea reprezentării interne a datelor
pentru acel tip, precum şi un set suficient de funcţii cu ajutorul cărora putem utiliza acel tip de
date fără a fi nescesară cunoaşterea structurii sale interne. Ascunderea datelor asigură
37
modificarea valorilor acestor date fără a altera buna funcţionare a programelor care apelează
funcţiile scrise pentru tipul abstract de date.
Abstractizarea datelor nu este un concept legat neapărat de POO. Limbajul C oferă câteva
exemple de tipuri abstracte de date. De exemplu tipul FILE este o structură complexă de date
scrisă pentru lucrul cu fişiere în C, pentru care nu avem nevoie să cunoaştem câmpurile sale,
atâta timp cât avem definite suficiente funcţii care lucrează cu acest tip de date: fopen, fclose,
fwrite, fread, fprintf, fscanf, fgets, fputs, fgetc, fputc, feof, fseek, ftell etc. Toate aceste funcţii
realizează o interfaţă de lucru cu fişiere, la baza căreia stă însă tipul FILE.
Clasele sunt de departe însă cele mai bune exemple de tipuri abstracte de date.
Datorită faptului că o clasă este un tip de date deosebit de complex, crearea unui obiect
(alocarea memoriei, iniţializarea datelor etc.) se face prin intermediul unei funcţii membre
speciale numite constructor.
În majoritatea limbajelor eliberarea memoriei alocate pentru un obiect se face prin
intermediul unei alte funcţii membre speciale denumite destructor.
Când vorbim despre programarea orientată pe obiecte, prin încapsularea datelor
înţelegem faptul că accesul la date se poate face doar prin intermediul unui obiect. Mai mult,
datele declarate ca fiind private (cele care ţin de bucătăria internă a unei clase) nu pot fi accesate
decât în momentul descrierii clasei.
Moştenirea (în engleză - inheritance) este un alt concept care stă la baza reutilizării
codului orientat pe obiecte. O clasă poate moşteni caracteristicile uneia sau mai multor clase.
Noua clasă poate extinde sau/şi particulariza facilitaţile moştenite. Clasa moştenită se numeşte
clasa de baz㸠în engleză - base class, iar cea care moşteneşte se numeşte clasa derivată, în
engleză - derived class. O colecţie de clase realizate pe principiul moştenirii se numeşte ierarhie
de clase. Într-o ierarhie clasele sunt organizate arborescent, rădăcina arborelui este în general o
clasă abstractă (ce nu poate fi instanţiată) care trasează specificul ierarhiei.
După cum am spus, moştenirea este folosită în două scopuri: pentru a particulariza o clasă
(de exemplu pătratul moşteneşte dreptunghiul) sau/şi pentru a îmbogăţi o clasă prin adăugarea de
facilităţi noi clasei derivate (de exemplu unei clase ce lucrează cu un polinom îi putem adăuga o
nouă metodă cum ar fi cea pentru calculul valorii într-un punct). În cele mai multe situaţii însă
clasa derivată particularizează o clasă deja existentă şi o îmbogăţeşte în acelaşi timp (de
exemplu, la clasa dedicată pătratului putem adăuga o metodă pentru calcularea razei cercului
înscris, metodă ce nu putea exista în clasa scrisă pentru dreptunghi).
Limbajul C++ este unul dintre puţinele limbaje care acceptă moştenirea multiplă, adică
o clasă poate fi derivată din mai multe clase.
În sens general, prin polimorfism inţelegem proprietatea de a avea mai multe forme.
Când vorbim însă despre POO, polimorfismul constă în faptul că o metodă cu acelaşi nume şi
aceeaşi parametri poate fi implementată diferit pe nivele diferite în cadrul aceleaşi ierarhii de
clase.
În continuare prezentăm o posibilă reţetă de scriere a unei clase noi:
1. Căutăm dacă există ceva facilităţi într-o clasă sau în mai multe clase deja existente pe
care să le putem adapta clasei noastre. Dacă există, atunci tot ceea ce avem de făcut
este să moştenim aceste clase.
2. Trebuie să alegem bine încă de la început datele membre clasei. De obicei majoritatea
datelor membre clasei (dacă nu chiar toate) se declară private sau protected, în cazul
în care clasa este posibil să fie reutilizată în alt scop.
3. Scriem suficienţi constructori pentru clasă. Aceştia vor crea obiecte sub diverse
forme, iniţializând diferit câmpurile clasei.
4. Trebuie scrise suficiente funcţii (metode) pentru prelucrarea câmpurilor. Nu trebuie
să fim zgârciţi în a scrie metode, chiar dacă par a nu fi de folos într-o primă fază. Nu
se ştie niciodată când va fi nevoie de ele în proiectul nostru sau dacă vom refolosi
38
clasa altă dată. Scriem metode de tipul set… şi get…. Ele modifică valorile
câmpurilor şi, respectiv, le interoghează. Acest lucru este necesar deoarece, după cum
am văzut, în general câmpurile sunt ascunse (private). Prin intermediul lor limităm
accesul şi ne asigurăm de atribuirea corectă de valori câmpurilor.
5. O parte dintre metode, cele care ţin de bucătăria internă a clasei (sunt folosite numai
în interiorul clasei), le vom ascunde, adică le vom declara private sau protected.
6. Scriem metode aşa încât obiectul clasei să poată interacţiona cu alte obiecte. Pregătim
astfel posibilitatea de a lega modulul nostru la un proiect (un program).
Urmărind reţeta de mai sus să vedem cum proiectăm o clasă care lucrează cu un număr
raţional, în care numărătorul şi numitorul sunt memoraţi separat, în două câmpuri întregi:
După ce vom avea suficiente cunoştinţe vom implementa această clasă ca un prim
exemplu.
Cuprins
39
Obiectivele unităţii de învăţare
Ne propunem să vedem cum se scrie cod orientat pe obiecte în C++. Mai întâi o să
vedem care sunt neajunsurile POO din C folosind tipul struct şi vom vedea efectiv
cum se declară şi descrie o clasă în C++, o metodă, un constructor, un destructor
etc.
Cu ajutorul tipului struct din C putem scrie cod orientat pe obiecte, însă, după cum vom
vedea, cod adevărat orientat pe obiecte putem scriem numai în C++ cu ajutorul tipului class. Să
prezentăm însă pe scurt neajunsurile programării orientate pe obiecte din C, folosind tipul struct.
În interiorul tipului struct, pe lângă câmpuri putem avea şi funcţii.
Membrii unei structuri (fie ei date sau funcţii) pot fi accesaţi direct, adică toţi se
comportă ca fiind publici, declararea unor date sau funcţii interne private sau protected fiind
imposibilă.
Trebuie avut grijă asupra corectitudinii proiectării unui obiect în C (reţinut într-o
variabilă de tip struct), astfel încât acesta să suporte moştenirea. Este necesară scrierea unor
funcţii suplimentare pentru a permite unui obiect să-şi acceseze corect datele.
Moştenirea comportării obiectului ca răspuns la diferite mesaje necesită funcţii suport
pentru a se realiza corect.
Toate aceste probleme nu există dacă utilizăm tipul class din C++, care răspunde în
totalitate cerinţelor ridicate de POO.
Menţionăm că tot ceea ce apare între paranteze pătrate este interpreta t a fi opţional.
Ca şi în cazul tipurilor struct, union şi enum din C, descrierea unei clase se încheie cu ;
(punct şi virgulă). Între acolada închisă şi caracterul ; (punct şi virgulă) pot fi definite obiecte
sau/şi pointeri către tipul class.
40
După numele clasei urmează opţional : (două puncte) şi numele claselor moştenite
despărţite prin virgulă. Vom reveni cu detalii când vom discuta despre moştenire.
Declaraţiile datelor şi metodele pot fi precedate de unul dintre specificatorii de acces
(care sunt cuvinte cheie, rezervate) public, protected sau private. În lipsa unui specificator de
acces, datele şi metodele sunt considerate a fi implicit private !
Membrii de tip public pot fi accesaţi în interiorul clasei, în interiorul eventualelor clase
derivate şi prin intermediul unui obiect al clasei respective sau al unei clase derivate sub forma
obiect.membru_public.
Membrii declaraţi protected ai unei clase pot fi accesaţi în interiorul clasei sau în
interiorul unei eventuale clase derivate, dar nu pot fi accesaţi sub forma
obiect.membru_protected, unde obiect este o instanţă a clasei respective sau a unei clase derivate
din aceasta.
Datele şi metodele de tip private pot fi accesate numai din interiorul clasei la care sunt
membre.
În general, datele unei clase se declară private sau protected, iar majoritatea metodelor se
declară publice. Accesul la datele clasei se va face prin intermediul unor funcţii membre special
definite în acest scop. Aceste funcţii sunt de tipul set… (pentru setarea valorilor câmpurilor),
respectiv get… (care returnează valorile câmpurilor obiectului).
Ordinea definirii membrilor în funcţie de modul lor de acces este opţională. Pot aparea
mai multe declaraţii sau nici una de tip public, protected sau private pe parcursul descrierii
clasei.
class nume_clasa
{
//....
tip_returnat functie(parametri) // fct. membra inline
{
// descriere fct. (fara instructiuni repetitive)
}
//....
};
class nume_clasa
{
//....
tip_returnat functie(parametri);
//....
};
class nume_clasa
{
//....
tip_returnat functie(parametri);
//....
};
class Test
{
void fct(Test,int);
void fct2(Test t, int k);
};
Dacă o funcţie care conţine instrucţiuni repetitive este descrisă în momentul definirii
clasei, atunci la compilare suntem atenţionaţi că această funcţie nu va fi considerată a fi inline.
U2.4. Constructori
class nume_clasa
{
//....
nume_clasa(parametri); // declaratie constructor
//....
};
42
Pot exista mai mulţi constructori definiţi pentru o clasă, dar cu parametri diferiţi. În lipsa
scrierii de constructori de către programator, la compilare se generează constructori impliciţi.
Un obiect se crează static folosind unul dintre constructorii clasei astfel:
Dacă folosim constructorul fără parametri al clasei, atunci un obiect se crează static
astfel:
ob.membru
Pentru a construi un obiect dinamic definim un pointer către clasă şi folosim operatorul
new:
Crearea unui obiect dinamic folosind constructorul fără parametri se realizează astfel:
ob->membru
U2.5. Destructori
Destructorul în C++ poartă numele clasei precedat de semnul ~ (tilda), nu are parametri
şi nici tip returnat:
class nume_clasa
{
//....
~nume_clasa(); // definire destructor
//....
};
43
Este evident că pentru a putea distruge un obiect când nu mai avem nevoie de el pe
parcursul execuţiei programului, el trebuie creat dinamic. Distrugerea unui obiect creat anterior
dinamic se face astfel:
class dreptunghi
{
private:
float x1,y1,x2,y2; // datele interne clasei ce definesc
dreptunghiul.
public: // (coordonatele a doua varfuri diagonal
opuse)
dreptunghi(float X1,float Y1,float X2,float Y2) //
constructor
{
x1=X1; // se initializeaza datele interne clasei: x1, y1,
x2 si y2
x2=X2; // cu valorile primite ca parametri: X1, Y1, X2 si
Y2
y1=Y1;
y2=Y2;
}
float get_x1()
{
return x1;
}
float arie() // metoda descrisa inline
{
return fabs((x2-x1)*(y2-y1));
}
float perimetru(); // functie membra care va fi descrisa
ulterior
};
Funcţiile membre arie şi perimetru se aplică obiectului curent, cel din care se apelează
aceste funcţii. De aceea nu este nevoie să transmitem ca parametru obiectul dreptunghi pentru
care vrem să calculăm aria, respectiv perimetrul, aşa cum se întamplă în cazul funcţiilor externe
clasei.
44
Pentru testarea clasei dreptunghi propunem următoarea funcţie principală:
void main(void)
{
dreptunghi d(20,10,70,50); // se creaza obiectul d folosind
constructorul
//primeste ca param. X1=20, Y1=10, X2=70,
Y2=50
cout<<"Aria dreptunghiului: "<<d.arie()<<endl;
cout<<"Perimetrul dreptunghiului: "<<d.perimetru()<<endl;
cout<<"Dreptungiul are ordonata: "<<d.get_x1()<<endl;
// cout<<" Dreptungiul are ordonata: "<<d.x1;
}
Dacă se încearcă accesarea uneia dintre datele membre clasei: x1, y1, x2 sau y2, atunci se
obţine eroare la compilare, deoarece ele sunt declarate private. De aceea afişarea valorii reţinute
în x1 din obiectul d este greşită (linia comentată). Pentru a putea fi accesată valoarea x1 direct
sub forma d.x1, ea ar fi trebuit să fie declarată public în interiorul clasei. Datele membre unei
clase se declară în general cu unul dintre specificatorii private sau protected. Dacă dorim să se
poată obţine valoarea unui câmp privat sau protejat scriem o funcţie publică de tip get…, aşa cum
este cazul funcţiei get_x1 din clasa dreptunghi. Pentru modificarea valorii reţinute într-un câmp
putem scrie o funcţie publică a cărui nume de regulă începe cu set…. Pentru câmpul x1, funcţia
membră set… (definită în interioul clasei dreptunghi) este:
public:
void set_x1(float X1)
{
x1=X1; // campul privat
}
Evident, functia set_x1 poate fi apelată în funcţia main de mai sus astfel:
d.set_x1(10);
Cuprins
45
Obiectivele unităţii de învăţare
Vom vedea ce sunt funcţiile prietene unei clase, utilitatea lor, precum şi modul în
care putem defini operatori având ca operanzi obiecte.
class nume_clasa
{
//....
friend tip_returnat functie_prietena(parametri)
{ // functie prietena inline
// descriere functie
}
//....
};
class nume_clasa
{
//....
friend tip_returnat functie_prietena(parametri);
//....
};
Ca şi în cazul funcţiilor membre, funcţiile friend care conţin instrucţiuni repetitive trebuie
descrise în exteriorul descrierii clasei (adică nu inline):
class nume_clasa
{
//....
46
friend tip_returnat functie_prietena(parametri);
//....
};
tip_returnat functie_prietena(parametri)
{
// descriere functie
}
Rescriem în continuare clasa dreptunghi de mai sus cu funcţii prietene în loc de funcţii
membre:
# include <math.h>
# include <iostream.h>
class dreptunghi
{
private:
float x1,y1,x2,y2;
public:
dreptunghi(float X1,float Y1,float X2,float Y2)
{
x1=X1; x2=X2; y1=Y1; y2=Y2;
}
float get_x1(dreptunghi d)
{
return d.x1;
}
float set_x1(dreptunghi,float);
friend float arie(dreptunghi d) // descrierea functiei prietena clasei
// dreptunghi. Se primeste ca argument
{ // un obiect de tipul clasei dreptunghi
return fabs((d.x2-d.x1)*(d.y2-d.y1));
}
friend float perimetru(dreptunghi); // functie prietena ce va fi
// descrisa ulterior
};
float perimetru(dreptunghi d)
{
return 2*(fabs(d.x2-d.x1)+fabs(d.y2-d.y1));
}
void main(void)
{
dreptunghi d(20,10,70,50);
cout<<"Aria dreptunghiului: "<<arie(d)<<endl;
47
cout<<"Perimetrul dreptunghiului: "<<perimetru(d)<<endl;
cout<<"Dreptungiul are ordonata: "<<get_x1(d)<<endl;
}
Într-o funcţie prietenă, din cauză că este externă clasei, de obicei transmitem ca
parametru şi obiectul asupra căruia se referă apelul funcţiei. Astfel, de exemplu în cazul functiei
set_x1 trimitem ca parametru obiectul d pentru care dorim să setăm valoarea câmpului x1.
Aşadar, în general o funcţie prietenă unei clase are un parametru suplimentar faţă de situaţia în
care aceeaşi funcţie ar fi fost scrisă ca fiind membră clasei. Aceaşi regulă se aplică şi supraîncării
unui operator ca fiind prieten clasei.
Din cauză că sunt funcţii prietene clasei, datele interne private ale obiectului d de tip
dreptunghi au putut fi accesate sub forma: d.x1, d.y1, d.x2 şi d.y2. Evident, dacă funcţiile nu erau
prietene clasei, ci erau funcţii externe obişnuite (definiţiile lor nu apăreau declarate friend în
interiorul definţiei clasei), accesul la datele membre obiectului d nu era posibil.
Operatorii pot fi supraincărcaţi ca membrii unei clase sau ca fiind prieteni unei clase.
Regulile legate de modul de definire şi descriere ale unui operator sunt aceleaşi ca şi pentru orice
funcţie membră, respectiv prietenă unei clase. Un operator nu poate fi însă definit sub ambele
forme (interior şi exterior clasei) pentru aceleaşi tipuri de operanzi. Operatorii pot fi definiţi de
mai multe ori în interiorul aceleaşi clase, dar cu tipuri pentru parametri diferite.
Dacă un operator de aritate n este definit ca fiind membru unei clase, atunci el va avea n-
1 parametri. Aşadar, un operator unar nu va avea nici un parametru şi se va aplica obiectului care
îl apelează. Pentru un operator binar avem un singur parametru. Operatorul binar se aplică între
primul obiect, considerat a fi obiectul curent, din care se apelează operatorul, şi valoarea primită
ca argument (vezi operatorul + din clasa complex prezentată mai jos).
Dacă un operator de aritate n este definit ca fiind prieten unei clase, atunci el va avea n
parametri.
În general operatorii <<, >> vor fi definiţi ca fiind prieteni clasei pentru că primul
argument este de obicei un flux în care se trimit, respectiv din care se extrag valorile câmpurilor
obiectului.
În continuare prezentăm o clasă care lucrează cu un număr complex:
# include <iostream.h>
# include <conio.h>
class complex
{
private:
float re,im;
public:
complex(float x=0,float y=0) // constructor cu parametri cu valori implicite
{
re=x;
im=y;
}
complex operator+(complex z2) // operator membru clasei pentru adunare
{
complex z;
z.re=re+z2.re;
48
z.im=im+z2.im;
return z;
}
complex operator+(float r) // adunare cu un numar real
{
complex z;
z.re=re+r;
z.im=im;
return z;
}
friend complex operator+(float r,complex z2) // real+complex
{
complex z;
z.re=r+z2.re;
z.im=z2.im;
return z;
}
friend complex operator-(complex z1,complex z2) // operator prieten
{
complex z;
z.re=z1.re-z2.re;
z.im=z1.im-z2.im;
return z;
}
friend istream& operator>>(istream &fl,complex &z);
friend istream& operator<<(istream &fl,complex &z);
};
void main(void)
{
complex z,z1(1),z2(2,5);
În exemplul de mai sus, constructorul are valori implicite pentru cei doi parametri. Astfel,
pentru obiectul z din programul principal datele interne re şi im se vor iniţializa cu valorile
implicite, adică ambele vor fi 0. Obiectul z1 din programul principal va avea re iniţializat cu 1 şi
im cu valoarea implicită 0. În fine, obiectul z2 va avea re iniţializat cu 2, iar im cu 5 (valorile
transmise ca parametri în constructor).
Pentru a supraîncărca operatorii de atribuire corect, şi nu numai pentru aceştia, folosim
pointerul this.
Cuprins
În C (şi în C++), o variabilă locală într-o funcţie poate fi declarată ca fiind statică. Pentru
această variabilă se păstreză valoarea şi, când se reapelează funcţia, variabila va fi iniţializată cu
valoarea pe care a avut-o la apelul anterior. De fapt pentru variabila locală statică este alocată o
zonă de memorie de la începutul până la sfârşitul execuţiei programului, dar accesul la variabilă
nu este posibil decât din interioul funcţiei în care este declarată. Iată un exemplu simplu :
#include<stdio.h>
void fct(void)
{
static int x=1;
50
x++;
printf("%d ",x);
}
void main(void)
{
int i;
for (i=0;i<10;i++) fct();
}
2 3 4 5 6 7 8 9 10 11
În C++ un membru al unei clase poate fi declarat ca fiind static. Un membru static este
folosit în comun de toate obiectele clasei. Pentru un câmp static se alocă o zonă (unică) de
memorie încă de la începutul execuţiei programului. Câmpul static va putea fi interogat şi
modificat de toate obiectele clasei, modificări ce vor fi vizibile în toate obiectele clasei. Mai
mult, un membru static poate fi accesat direct (dacă este vizibil în acel loc) sub forma :
NumeClasa::MembruStatic;
În continuare vom da un exemplu în care într-un câmp static vom memora numărul de
instanţe create pentru o clasă:
#include<iostream.h>
class test
{
private:
static int n;
public:
test()
{
n++;
}
static int NrInstante()
{
return n;
}
};
void main()
{
test t1,t2;
cout<<test::NrInstante(); // apel metoda statica
}
51
Evident, în urma execuţiei programului, pe ecran se va afişa numărul 2.
Orice câmp static trebuie definit în exteriorul descrierii clasei. În momentul definirii se
foloseşte operatorul de rezoluţie pentru indicarea clasei la care este membru câmpul static (vezi
câmpul x din exemplul de mai sus).
Pointer-ul this (“acesta” în engleză) a fost introdus în C++ pentru a indica adresa la care
se află memorat obiectul curent. El este folosit numai în corpul funcţiilor membre, inclusiv în
constructori şi destructor, pentru a ne putea referi la obiectul din care s-a apelat funcţia, respectiv
la obiectul care se construieşte sau se distruge.
Compilatorul C++ modifică fiecare funcţie membră dintr-o clasă astfel:
1) Transmite un argument în plus cu numele this, care este de fapt un pointer către
obiectul din care s-a apelat funcţia membră (this indică adresa obiectului care a apelat
metoda).
2) Este adăugat prefixul this-> tuturor variabilelor şi funcţiilor membre apelate din
obiectul curent (dacă nu au deja dat de programator).
Apelul unui membru (dată sau funcţie) al unei clase din interiorul unei funcţii membre se
poate face sub forma this->membru, scriere care este echivalentă cu membru (fără
explicitarea this->) dacă nu există cumva un parametru al funcţiei cu numele membru.
O instrucţiune de forma return *this returnează o copie a obiectul curent (aflat la
adresa this).
Să vedem în continuare cum se defineşte corect operatorul = (de atribuire) pentru două
numere complexe. După cum se ştie, expresia a=b are valoarea ce s-a atribuit variabilei a.
Aşadar, o atribuire de numere complexe de forma z1=z2 va trebui sa aibă valoarea atribuită lui
z1, adică operatorul = definit în clasa complex va trebui să returneze valoarea obiectului curent:
# include <iostream.h>
class rational
{
private:
long a,b;
52
void simplificare();
public:
rational(long A=0,long B=1)
{
a=A;
b=B;
simplificare();
}
long numarator()
{
return a;
}
long numitor()
{
return b;
}
rational operator+(rational x)
{
rational y;
y.a=a*x.b+b*x.a;
y.b=b*x.b;
y.simplificare();
return y;
}
rational operator-(rational x)
{
rational y;
y.a=a*x.b-b*x.a;
y.b=b*x.b;
y.simplificare();
return y;
}
rational operator*(rational x)
{
rational y;
y.a=a*x.a;
y.b=b*x.b;
y.simplificare();
return y;
}
rational operator/(rational x)
{
rational y;
y.a=a*x.b;
y.b=b*x.a;
y.simplificare();
return y;
}
rational operator=(rational&x)
{
a=x.a;
b=x.b;
53
return *this;
}
rational operator+=(rational x)
{
return *this=*this+x;
}
rational operator-=(rational x)
{
return *this=*this-x;
}
rational operator*=(rational x)
{
return *this=*this*x;
}
rational operator/=(rational x)
{
return *this=*this/x;
}
int operator==(rational x)
{
return a==x.a && b==x.b;
}
int operator!=(rational x)
{
return !(*this==x);
}
rational operator++() // preincrementare ++r
{
a+=b;
return *this;
}
rational operator--() // predecrementare --r
{
a-=b;
return *this;
}
rational operator++(int) // postincrementare r++
{
rational c=*this;
a+=b;
return c;
}
rational operator--(int) // postdecrementare r--
{
rational c=*this;
a-=b;
return c;
}
long operator!()
{
return !a;
}
54
friend ostream& operator<<(ostream& fl,rational x)
{
fl<<"("<<x.a<<","<<x.b<<")";
return fl;
}
friend istream& operator>>(istream& fl,rational &x)
{
fl>>x.a>>x.b;
return fl;
}
};
void rational::simplificare()
{
long A=a,B=b,r;
void main(void)
{
rational x(7,15),y(1,5),z;
z=x+y;
cout<<x<<"+"<<y<<"="<<z<<endl;
cout<<x<<"-"<<y<<"="<<(x-y)<<endl;
cout<<x<<"*"<<y<<"="<<(x*y)<<endl;
cout<<x<<"/"<<y<<"="<<(x/y)<<endl;
}
Scrierea unui constructor de copiere este necesară numai într-o unei clasă în care există
alocare dinamică a memoriei. Constructorul de copiere trebuie să asigure copierea corectă a
instanţelor unei clase.
55
Constructorul de copiere poate fi folosit direct de programator pentru crearea unui obiect
nou (ca orice constructor), dar el este apelat în general automat în timpul execuţiei programului
atunci când se transmite un obiect ca parametru într-o funcţie prin valoare şi la returnarea unui
obiect prin valoare dintr-o funcţie.
În lipsa unui constructor de copiere definit de programator, compilatorul crează un
constructor de copiere implicit, dar care nu va şti însă să facă alocare dinamică de memorie.
Dacă în clasă avem un câmp de tip pointer, atunci, după copierea unui obiect, pentru ambele
obiecte (cel vechi şi cel nou construit) câmpurile de tip pointer vor indica aceeaşi zonă de
memorie. Astfel, dacă modificăm ceva la această adresă prin intermediul câmpului pointer al
unui obiect, modificarea va fi vizibilă şi din celelălat obiect. Acest lucru nu este în general dorit.
De aceea programatorul trebuie să scrie constructorul de copiere care va aloca o zonă nouă de
memorie pe care o va reţine în câmpul de tip pointer al obiectului creat şi în această zonă de
memorie va copia ce se află la adresa câmpului obiectului care este copiat.
Un constructor de copiere are următoarea structură:
#include<stdio.h>
#include<string.h>
#include<iostream.h>
class string
{
private:
char *s; // sirul de caractere retinut in string
public:
string(char *st="") // constructor
{
s=new char[strlen(st)+1];
strcpy(s,st);
}
string(const string &str) // contructor de copiere
{
delete [] s;
s=new char[strlen(str.s)+1];
strcpy(s, str.s);
}
~string() // destructor
{
delete [] s;
}
string operator+(string str) // apelare constructor copiere
{
char *st;
56
st=new char[strlen(s)+strlen(str.s)+1];
string str2(st);
sprintf(str2.s,"%s%s",s,str.s);
return str2; // apelare constructor de copiere
}
string operator=(const string &str) // atribuire
{
delete [] s;
s=new char[strlen(str.s)+1];
strcpy(s, str.s);
return *this; // se apeleaza constructorul de copiere
}
string operator+=(const string &str)
{
*this=*this+str;
return *this; // apelare constructor de copiere
}
int operator==(const string &str) // identice ?
{
if (!strcmp(s,str.s)) return 1;
return 0;
}
int operator<(string str) // apelare constructor de copiere
{
if (strcmp(s,str.s)<0) return 1;
return 0;
}
int operator<=(const string &str)
{
if (strcmp(s,str.s)<=0) return 1;
return 0;
}
int operator>(const string &str)
{
if (strcmp(s,str.s)>0) return 1;
return 0;
}
int operator>=(const string &str)
{
if (strcmp(s,str.s)>=0) return 1;
return 0;
}
void set(char *st) // setararea unui string
{
delete [] s;
s=new char[strlen(st)+1];
strcpy(s,st);
}
void get(char *st) // extragere sir caractere din obiectul string
{
strcpy(st,s);
}
57
int operator!() // se returneaza lungimea string-ului
{
return strlen(s);
}
char operator[](int i) // returneaza caracterul de pe pozitia
i
{
return s[i];
}
friend ostream& operator<<(ostream &fl,const string &str)
{
fl<<str.s;
return fl;
}
friend istream& operator>>(istream &fl,const string &str)
{
fl>>str.s;
return fl;
}
};
s2.set("string-ul 2");
s=s1+s2;
cout<<"Concatenarea celor doua string-uri: "<<s<<endl;
s+=s1;
cout<<"Concatenarea celor doua string-uri: "<<s<<endl;
cout<<"Lungimea string-ului: "<<!s<<endl;
cout<<"Pe pozitia 5 se afla caracterul: "<<s[4]<<endl;
if (s1==s2) cout<<"String-urile sunt identice"<<endl;
else cout<<"String-urile difera"<<endl;
if (s1<s2) cout<<"s1 < s2"<<endl;
s.get(st);
cout<<"String-ul extras: "<<st<<endl;
}
Constructorul de copiere se apelează când se transmite un obiect de tip string prin valoare
(vezi operatorii + şi <). De asemenea, de fiecare dată când se returnează un obiect de tip string
(vezi operatorii +, = şi +=), este apelat constructorul de copiere definit în interiorul clasei, care
spune modul în care se face efectiv copierea (cu alocările şi eliberările de memorie aferente).
Pentru a vedea efectiv traseul de execuţie pentru programul de mai sus, propunem
cititorului rularea acestuia pas cu pas. Rularea pas cu pas în mediul de programare Borland se
face cu ajutorul butonului F7 sau F8. Lăsarea liberă a execuţiei programului până se ajunge la
linia curentă (pe care se află cursorul) se face apăsând butonul F4. Pentru ca să fie posibilă
urmărirea execuţiei programului, în meniul Options, la Debugger, trebuie bifat On în Source
Debugging. În Visual C++ urmărirea execuţiei pas cu pas a programului se face apasand butonul
F11, iar lăsarea liberă a execuţiei programului până la linia curentă se face apăsâ nd Ctrl+F10.
58
Lăsăm plăcerea cititorului de a completa alte şi funcţii în clasa string cum ar fi pentru
căutarea unui string în alt string, înlocuirea unui şir de caractere într-un string cu un alt şir de
caractere, extragerea unui subşir de caractere dintr-un string etc.
Cuprins
În C++ o clasă poate sa nu fie derivată din nici o altă clasă, sau poate deriva una sau mai
multe clase de bază:
59
Modul în care sunt văzute în
Tip date şi metode Specificator mod de
clasa derivată datele şi
din clasa de bază derivare
metodele clasei de bază
public public public
public protected protected
public private protected
protected public protected
protected protected protected
protected private protected
private public private
private protected private
private private private
După cum se poate vedea din tabelul de mai sus pentru a avea acces cât mai mare la
membrii clasei de bază este indicată folosirea specificatorului public în momentul derivării.
Constructorul unei clase derivate poate apela constructorul clasei de bază, creându-se în
memorie un obiect al clasei de bază (denumit sub-obiect al clasei derivate), care este văzut ca o
particularizare a obiectului clasei de bază la un obiect de tipul clasei derivate. Apelul
constructorului clasei de bază se face astfel:
Parametrii constructorului baza (la apelul din clasa derivată) se dau în funcţie de
parametrii constructorului deriv. De exemplu, pentru o clasă patrat derivată dintr-o clasă
dreptunghi (ambele cu laturile paralele cu axele de coordonate), apelul constructorului
dreptunghi la definirea constructorului patrat se poate face astfel:
Dreptunghiul din exemplul de mai sus este definit prin coordonatele a două vârfuri
diagonal opuse, iar pătratul prin coordonatele vârfului stânga-sus şi prin lungimea laturii sale. De
60
aceea, pătratul este văzut ca un dreptunghi particular, având cele două vârfuri diagonal opuse de
coordonate (X,Y), respectiv (X+L,Y+L).
Asupra moştenirii multiple o să revenim după ce introducem noţiunea de virtual.
În clase diferite în cadrul unei ierarhii pot apărea funcţii cu aceeaşi semnătură (acelaşi
nume şi aceiaşi parametri), în engleză overriding. Astfel, putem avea situaţia în care într-o clasă
derivată există mai multe funcţii cu aceeaşi semnatură (unele moştenite din clasele de pe nivele
superioare ale ierarhiei şi eventual una din clasa derivată). În această situaţie se pune problema
cărei dintre aceste funcţii se va răsfrânge apelul dintr-un obiect alocat static al clasei derivate.
Regula este că se apelează funcţia din clasa derivată (dacă există), iar dacă nu există o functie în
clasa derivată, atunci se caută funcţia de jos în sus în ierarhie. Dacă dorim să apelăm o anumită
funcţie de pe un anumit nivel, atunci folosim operatorul de rezoluţie pentru a specifica din ce
clasă face parte funcţia dorită:
clasa::functie(parametri_de_apel);
După cum putem vedea, problemele sunt rezolvate într-o manieră elegantă atunci când se
lucrează cu obiecte alocate static.
Dacă lucrăm însă cu pointeri către clasă, problemele se complică. Putem defini un pointer
p către clasa de baza B care reţine adresa unui obiect dintr-o clasă derivată D. Când se apelează o
funcţie sub forma p.functie(…), funcţia este căutată mai întâi în clasa B către care este definit
pointerul p, ci nu în clasa D aşa cum ne-am putea aştepta. Mai mult, dacă funcţia există în clasa
D şi nu există în B, vom obţine eroare la compilare. De fapt, pointerul p reţine adresa către
subobiectul din clasa B, construit odată cu obiectul clasei derivate D, din cauza că p este un
pointer către clasa B.
Iată în continuare situaţia descrisă mai sus:
#include<iostream.h>
class B
{
public:
B()
{
cout<<"Constructor clasa de baza"<<endl;
}
void functie()
{
cout<<"Functie clasa de baza"<<endl;
}
};
class D:public B
{
public:
int x;
D()
{
cout<<"Constructor clasa derivata"<<endl;
61
}
void functie()
{
cout<<"Functie clasa derivata"<<endl;
}
};
void main()
{
B *p=new D;
p->functie();
}
După cum am văzut, este evident că pe ecran în urma execuţiei programului de mai sus
vor apărea mesajele:
Dacă functie nu era implementată în clasa de baza B, obţineam eroare la compilare pentru
că pointerul p reţine adresa subobiectului şi este evident că în această situaţie la adresa indicată
de p nu există nimic referitor la clasa D. Din aceleaşi considerente, dacă încercăm referirea la
campul x sub forma p->x, vom obţine de asemenea eroare la compilare.
Există posibilitatea ca o funcţie membră unei clase să fie declarată ca fiind virtuală.
Să precizăm şi faptul că numai funcţiile membre unei clase pot fi declarate ca fiind
virtuale.
Declaraţia unei funcţii din clasa de bază ca fiind virtuale se adresează situaţiilor de tipul
celei de mai sus (clasa D este derivată din B şi B *p=new D;). Astfel, dacă în faţa declaraţiei
metodei functie din clasa B punem cuvântul rezervat virtual, atunci în urma execuţie
programului de mai sus pe ecran se vor afişa mesajele:
Deci, declaraţia virtual a funcţiei din clasa de bază a ajutat la identificarea corectă a
apelului funcţiei din clasa derivată.
Să vedem ce se întâmplă de fapt atunci când declarăm o funcţie ca fiind virtuală.
Cuvântul cheie virtual precede o metodă a unei clase şi semnalează că, dacă o funcţie
este definită într-o clasă derivată, aceasta trebuie apelată prin intermediul unui pointer.
Compilatorul C++ construieşte un tabel în memorie denumit tablou virtual (în engleză Virtual
Memory Table – VMT) cu pointerii la funcţiile virtuale pentru fiecare clasă. Fiecare instanţă a
unei clase are un pointer către tabelul virtual propriu. Cu ajutorul acestei reguli compilatorul C++
poate realiza legarea dinamică între apelul funcţiei virtuale şi un apel indirect prin intermediul
unui pointer din tabelul virtual al clasei. Putem suprima mecanismul apelării unei funcţii virtuale
explicitând clasa din care face parte funcţia care este apelată folosind operatorul de rezoluţie.
62
În C++, în cadrul unei ierarhii nu pot apărea funcţii virtuale cu acelaşi nume şi aceeaşi
parametri, dar cu tip returnat diferit. Dacă încercăm să scriem astfel de funcţii este semnalată
eroare la compilare. Astfel, dacă în exemplul de mai sus metoda functie din clasa de bază ar fi
avut tipul returnat int şi era virtuală, obţineam eroare la compilare. Putem avea însă într-o
ierarhie funcţii care nu sunt virtuale, cu acelaşi nume şi aceaşi parametri, dar cu tip returnat
diferit.
Datorită faptului că apelul unei funcţii virtuale este localizat cu ajutorul tabelei VMT,
apelarea funcţiilor virtuale este lentă. De aceea preferăm să declarăm funcţiile ca fiind virtuale
numai acolo unde este necesar.
Concluzionând, în final putem spune că declararea funcţiilor membre ca fiind virtuale
ajută la implementarea corectă a polimorfismului în C++ şi în situaţiile în care lucrăm cu pointeri
către tipul clasă.
#include<iostream.h>
class B
{
public:
B()
{
cout<<"Constructor clasa de baza"<<endl;
}
~B()
{
cout<<"Destructor clasa de baza"<<endl;
}
};
class D:public B
{
public:
D()
{
cout<<"Constructor clasa derivata"<<endl;
}
~D()
{
cout<<"Destructor clasa derivata"<<endl;
}
};
void main()
{
B *p=new D;
delete p;
63
}
După cum am văzut, pointerul p reţine adresa obiectului clasei B construit odată cu
obiectul clasei D. Neexistând nici o legatură cu obiectul clasei derivate, distrugerea se va face
numai la adresa p, adică se va apela numai destructorul clasei B. Pentru a se distruge şi obiectul
clasei derivate D, trebuie să declarăm destructorul clasei de bază ca fiind virtual. În această
situaţie la execuţia programului, pe ecran vor apărea urmatoarele patru mesaje:
Aşadar, este indicat ca într-o ierarhie de clase în care se alocă dinamic memorie
destructorii să fie declaraţi virtuali !
Într-o ierarhie întâlnim situaţii în care la un anumit nivel, o funcţie nu este implementată.
Cu această situaţie ne întâlnim mai ales în clasele abstracte care pregătesc ierarhia, trasează
specificul ei. Funcţiile care nu se implementează pot fi declarate ca fiind pur virtuale. Astfel,
tentativa de apelare a unei pur virtuale se soldează cu eroare la compilare. În lipsa posibilităţii
declarării funcţiilor pur virtuale, în alte limbaje de programare, pentru metodele neimplementate
se dau mesaje.
O funcţie pur virtuală se declară ca una virtuală numai că la sfârşit punem =0.
Compilatorul C++ nu permite instanţierea unei clase care conţine metode pur virtuale. Dacă o
clasă D derivată dintr-o clasă B ce conţine funcţii pur virtuale nu are implementată o funcţie care
este pur virtuală în clasa de bază, atunci problema este transferată clasei imediat derivate din D,
iar clasa D la rândul ei devine abstractă, ea nu va putea fi instanţiată.
Dăm în continuare un exemplu în care clasa patrat este derivată din clasa dreptunghi,
care la randul ei este derivată din clasa abstractă figura. Pentru fiecare figură dorim să reţinem
denumirea ei şi vrem să putem calcula aria şi perimetrul. Dreptunghiul şi pătratul se consideră a
fi cu laturile paralele cu axele de coordonate. Dreptunghiul este definit prin coordonatele a două
vârfuri diagonal opuse, iar pătratul prin coordonatele unui varf şi prin lungimea laturii sale.
void main()
{
dreptunghi d(100,50,200,200);
patrat p(200,100,80);
65
cout<<"Arie "<<d.getnume()<<": "<<d.arie()<<endl;
cout<<"Perimetru "<<d.getnume()<<": "<<d.perimetru()<<endl;
cout<<"Arie "<<p.getnume()<<": "<<p.arie()<<endl;
cout<<"Perimetru "<<p.getnume()<<": "<<p.perimetru()<<endl;
}
În exemplul de mai sus, în interiorul clasei patrat, dacă dorim să apelăm funcţia
perimetru din clasa dreptunghi folosim operatorul de rezoluţie:
dreptunghi :: perimetru();
Funcţia perimetru din clasa patrat se poate rescrie folosind funcţia perimetru din clasa
dreptunghi astfel:
1. Câmpul nume reţine denumirea figurii şi este moştenit în clasele dreptughi şi patrat.
Funcţia getnume returnează numele figurii şi este de asemenea moştenită în clasele
derivate.
2. Funcţia arie nu este definită şi în clasa patrat. În consecinţă, apelul d.arie() se va
răsfrânge asupra metodei din clasa de baza dreptunghi.
3. Clasa figura conţine metode pur virtuale, deci este abstractă. Ea trasează specificul
ierarhiei (clasele pentru figuri derivate din ea vor trebui să implementeze metodele
arie şi perim). Clasa figura nu poate fi instanţiată.
Este evident că în exemplul de mai sus puteam să nu scriem clasa figura, câmpul nume şi
funcţia getnume putând fi redactate în interiorul clasei dreptunghi.
Definirea clasei abstracte figura permite tratarea unitară a conceptului de figură în cadrul
ierarhiei. Astfel, tot ceea ce este descendent direct sau indirect al clasei figura este caracterizat
printr-un nume (care poate fi completat direct şi interogat indirect prin intermediul funcţiei
getnume) şi două metode (pentru arie şi perimetru).
O să dăm în continuare o posibilă aplicaţie la tratarea unitară a conceptului de figură. Mai
întâi însă o să scriem încă o clasă derivată din figura – clasa cerc caracterizată prin coordonatele
centrului şi raza sa:
#define PI 3.14159
Scriem un program în care construim aleator obiecte de tip figură (dreptunghi, pătrat sau
cerc). Ne propunem să sortăm crescator acest vector de figuri după arie şi să afişăm denumirile
figurilor în această ordine:
do
{
while (fig[i]->arie()<fig[m]->arie()) i++;
while (fig[j]->arie()>fig[m]->arie()) j--;
if (i<=j)
{
figaux=fig[i]; fig[i]=fig[j]; fig[j]=figaux;
if (i<m) i++;
if (j>m) j--;
}
}
while (i<j);
if (s<m) qsort(s,m-1,fig);
if (m<d) qsort(m+1,d,fig);
}
void main(void)
{
figura **fig;
int i,n;
randomize();
cout<<"Dati numarul de figuri:";
cin>>n;
67
fig = new figura*[n];
for (i=0;i<n;i++)
switch (random(3))
{
case 0: fig[i]=new patrat(random(100),random(100)
,random(100)+100);
break;
case 1: fig[i]=new dreptunghi(random(100),random(100)
,random(100)+100,random(100)+100);
break;
case 2: fig[i]=new cerc(random(100),random(100)
,random(100)+100);
break;
}
qsort(0,n-1,fig);
cout<<"Figurile sortate dupa arie:"<<endl;
for (i=0;i<n;i++)
cout<<fig[i]->getnume()<<" cu aria: "<<fig[i]->arie()<<endl;
for (i=0;i<n;i++) delete [] fig[i];
delete [] fig;
}
Cuprins
68
U6.1. Moştenire multiplă
Limbajul C++ suportă moştenirea multiplă, adică o clasă D poate fi derivată din mai
multe clase de bază B1, B2, … , Bn (n > 0):
O clasă nu poate avea o clasă de bază de mai multe ori. Deci, nu putem avea spre
exemplu o derivare de forma:
Ce se întâmplă dacă două clase B1 şi B2 sunt baze pentru o clasă D, iar B1 şi B2 sunt
derivate din aceeaşi clasă C ? În aceasta situaţie, aşa cum vom vedea în subcapitolul urmator,
clasa C se declară în general ca fiind virtuală.
O clasă C se moşteneste virtual dacă poate exista situaţia ca la un moment dat două clase
B1 şi B2 să fie baze pentru o aceeaşi clasă derivată D, iar B1 şi B2 să fie descendente (nu
neaparat direct) din aceeaşi clasă C.
Dăm un exemplu:
class C
{
protected:
void fct() {}
};
În exemplul de mai sus, clasa D este derivată din B1 şi B2, iar B1 şi B2 sunt ambele
derivate din clasa C. Astfel, facilitatile oferite de clasa C (functia fct) ajung în D de două ori: prin
intermediul clasei B1 şi prin intermediul clasei B2. De fapt, orice obiect al clasei D va avea în
această situaţie două sub-obiecte ale clasei C. Din cauză că funcţia fct este apelată în interiorul
clasei D nu se poate decide asupra cărei dintre cele două funcţii din cele două subobiecte se va
răsfrânge apelul. Astfel, obţinem eroare la compilare. Pentru a elimina această problema, clasa C
trebuie să fie declarată virtuală în momentul în care ambele clase, B1 şi B2, derivează pe C:
class C
{
protected:
void fct() {}
};
În exemplul de mai sus, întâi se va apela constructorul clasei virtuale B2, apoi al clasei
B1 şi în final se apelează constructorul clasei derivate D.
Pentru moştenirea multiplă considerăm situaţia în care clasa patrat este derivată din
clasele dreptunghi şi romb, iar clasele dreptunghi şi romb sunt derivate ambele din clasa
patrulater, care este derivată la randul ei din clasa abstractă figura. Dreptunghiul şi pătratul au
laturile paralele cu axele de coordonate, iar rombul are două laturi paralele cu axa Ox.
# include <math.h>
# include <string.h>
# include <stdlib.h>
# include <iostream.h>
# define PI 3.14159
class figura
{
protected:
char nume[20]; // denumirea figurii
public: // functii pur virtuale
virtual float arie() = 0;
virtual float perimetru() = 0;
char* getnume()
{
return nume;
}
};
patrulater(X,Y,X+L,Y,X+L*cos(U)+L,Y+L*sin(U),X+L*cos(U),Y+L*sin(U))
{
strcpy(nume,"Romb");
x=X;
y=Y;
l=L;
u=U;
72
}
virtual float arie()
{
return l*l*sin(u);
}
virtual float perimetru()
{
return 4*l;
}
};
void main(void)
{
patrulater P(10,10,100,40,110,100,20,30);
dreptunghi d(10,20,200,80);
romb r(20,50,100,PI/3);
patrat p(20,10,100);
73
Observaţii:
1) Patrulaterul este dat prin coordonatele celor 4 vârfuri ale sale (în sens trigonometric
sau invers trigonometric): (x1,y1), (x2,y2), (x3,y3) şi (x4,y4).
2) Dreptunghiul (paralel cu axele de coordonate) este considerat ca fiind definit prin
două vârfuri diagonal opuse având coordonatele: (x1,y1) şi (x2,y2).
3) Rombul (cu două laturi paralele cu abscisa) este caracterizat prin coordonatele
vârfului stânga-sus (x,y), lungimea laturii sale l şi unghiul din vârful de coordonate
(x,y) având măsura u în radiani.
4) Pătratul (paralel cu axele de coordonate) are vârful stânga-sus de coordonate (x,y) şi
latura de lungime l.
5) Perimetrul patrulaterului e calculat ca suma lungimilor celor 4 laturi:
( x 2 x1 ) 2 ( y 2 y1 ) 2 ( x3 x 2 ) 2 ( y 3 y 2 ) 2
( x 4 x3 ) 2 ( y 4 y 3 ) 2 ( x1 x 4 ) 2 ( y1 y 4 ) 2 .
n
xi yi 1 xi 1 yi
i 1
S A1A2 ... An , unde Ai ( xi , y i ) (i 1... n) si An1 A1 .
2
l
D C
l
l
u
A
B
(x,y) l
Vârful A are coordonatele (x,y), iar B are (x+l,y) (translaţia lui A pe axa Ox cu l).
Pentru a găsi coordonatele punctului D am aplicat o rotaţie a punctului B în jurul lui
A cu un unghi u în sens trigonometric:
Rezumat
74
Am văzut până acum cum se scrie cod orientat pe obiecte: cum se scrie o clasă, o metodă,
un constructor, destructorul clasei, o funcţie prietenă. De asemenea, am făcut cunoştinţă cu
moştenirea din C++ şi problemele pe care le ridică moştenirea multiplă. Am văzut cum se
rezolvă elegant aceste probleme cu ajutorul claselor virtuale.
Temă
figura
poligon elipsa
Cuprins
75
Durata medie de parcurgere a unităţii de învăţare este de 2 or e.
C++ permite declararea unei clase în interiorul (imbricată) altei clase. Pot exista clase
total diferite cu acelaşi nume imbricate în interiorul unor clase diferite.
Dăm un exemplu:
class X
{
//....
class Y // clasa Y imbricata in X
{
//....
};
//....
};
class Z
{
//....
class Y // clasa Y imbricata in Z
{
//....
};
//....
};
La instanţierea claselor X sau Z (din exemplul de mai sus) nu se crează instanţe ale
claselor Y imbricate în acestea. Instanţierea clasei imbricate trebuie făcută explicit:
Clasa imbricată nu are acces asupra datelor private sau protected ale clasei în care este
imbricată. De asemenea, o clasă nu are acces asupra datelor private sau protected ale
eventualelor clase imbricate.
Ca şi în cazul unei funcţii şablon, unul sau mai multe tipuri de date pot să nu fie
explicitate în momentul definirii unei clase şablon. În momentul compilării, în locul unde se
instanţiază clasa, se identifică aceste tipuri de date şi se înlocuiesc cu tipurile identificate. O clasă
şablon se declară astfel:
#include<conio.h>
#include<stdlib.h>
#include<iostream.h>
77
void golire(); // golire stiva
~Stiva() // destructor
{
golire();
}
};
void main()
{
char x;
Stiva<char> st;
do
{
cout<<endl<<endl;
cout<<" 1. Introducere element in stiva"<<endl;
78
cout<<" 2. Scoatere element din stiva"<<endl;
cout<<" 3. Afisare continut stiva"<<endl;
cout<<" 4. Golire stiva"<<endl<<endl;
cout<<"Esc - Parasire program"<<endl<<endl;
switch (getch())
{
case '1':
cout<<"Dati un caracter: "<<flush;
st<<getche();
break;
case '2':
try
{
st>>x;
cout<<"Am scos din stiva: "<<x;
}
catch (char *mesajeroare)
{
cerr<<"Eroare: "<<mesajeroare;
}
break;
case '3':
if (!st) cout<<"Stiva este goala!";
else cout<<"Stiva contine: "<<st;
break;
case '4':
st.golire();
cout<<"Stiva a fost golita!";
break;
case 27: return;
default:
cerr<<"Trebuie sa apasati 1,2,3,4 sau Esc";
}
cout.flush();
getch();
}
while (1);
}
După ce avem scrisă clasa şablon, nu trebuie decât să specificăm tipul de date pentru care
vrem să o folosim. Astfel, clasa Stiva de mai sus o putem folosi pentru orice tip de date.
Rezumat
Am vazut cum se scrie o clasă şablon şi cum se foloseşte pentru diverse tipuri de date.
Am dat un exemplu ilustrativ: clasă şablon de lucru cu o stivă. Astfel, această clasă poate fi
folosită pentru a lucra cu o stivă de caractere, o stivă de numere întregi, de numere reale etc.
79
Teme de control
În finalul prezentării din punct de vedere teoretic al programarii orientate pe obiecte din
C++, propunem să se scrie:
1) Un program care utilizează clasa şablon Stiva şi pentru alt tip de date decât char.
2) O clasă şablon de lucru cu o coadă (similară clasei Stiva). Să se utilizeze acestă clasă
pentru a rezolva următoarea problemă:
Se deschide un fişier text. Se citesc caracterele fişierului unul câte unul. Când se
întâlneşte o consoană, se introduce în coadă. Când se întâlneşte o vocală, dacă nu
este goală coada, se scoate o consoană din listă. Restul caracterelor (cele care nu
sunt litere) se vor ignora. La sfârşit să se afişeze conţinutul cozii.
3) Clasa şablon de lucru cu o mulţime ce reţine elementele într-un vector alocat dinamic.
Clasa va avea implementaţi operatorii: +, - şi * pentru reuniune, diferenţă şi
respectiv intersecţie, operatorii << şi >> pentru introducerea unui element în mulţime
şi respectiv scoaterea unui element din mulţime, operatorul << pentru introducerea
conţinutului mulţimii într-un flux de ieşire, operatorul ! care returnează numărulde
elemente al mulţimii, operatorii =, +=, -=, *=, ==, !=, constructor fără
parametri, constructor de copiere, destructor etc. Se vor folosi metode rapide pentru
reuniune, diferenţă şi intersecţie.
4) Clasă şablon pentru prelucrarea unui vector. Pentru citire se va folosi >>, pentru
scriere <<, atribuirea se va face cu =, compararea de doi vectori cu == şi !=,
calcularea normei cu !, suma vectorială cu +, diferenţa vectorială cu -, amplificarea
cu scalar cu *, produsul scalar cu *, se vor defini operatorii +=, -= şi *=, functii
pentru calcularea mediei aritmetice, pentru sortare etc. Să se deriveze clasa vector
pentru lucrul cu un vector tridimensional. Să se definească în această clasă în plus
operatorul / pentru produs vectorial.
5) Clasă şablon de lucru cu matrici. Clasa va conţine: operatorul >> pentru citire,
scrierea se va face cu <<, atribuirea cu =, compararea cu == şi !=. Determinantul se
va calcula cu !, suma cu +, diferenţa cu -, produsul cu *. De asemenea, se vor defini
operatorii +=, -= şi *= şi funcţii pentru transpusă, ridicarea la putere şi inversarea
matricii. Să se folosească această clasă pentru calcularea sumei: At + (At) 2 + … +
(At)n, unde n este un număr întreg pozitiv.
6) Clasă de lucru cu polinoame. Clasa va conţine operatorii << şi >> pentru introducere
şi scoatere în / din flux, operatorii =, ==, !=, operatorii +, - şi * pentru operaţiile
intre doua matrici, * pentru amplificare cu o valoare, / pentru câtul împărţirii şi %
pentru restul împărţirii. De asemenea, se vor descrie operatorii +=, -=, *=, /=,
%= şi o funcţie pentru calcularea valorii polinomului într-un punct. Să se folosească
aceasta clasă pentru calcularea celui mai mare divizor comun şi a celui mai mic
multiplu comun pentru două polinoame.
7) Clasă de lucru cu numere întregi mari într-o baza b. Cifrele numărului (valori între 0
şi b-1) se vor memora într-un şir de numere întregi. Se vor descrie operatorii: <<
(afişare), >> (citire), = (atribuire); <, >, <=, >=, ==, != pentru comparare; +, -, *, / şi
80
% pentru operaţii aritmetice, operatorii: +=, -=, *=, /= şi %=. Să se testeze
clasa pentru calcularea lui n! pentru un număr întreg pozitiv relativ mare n (de
exemplu pentru n = 20).
8) Clasă pentru transformări elementare aplicate unui punct din plan, de coordonate (x,
y). Cu ajutorul clasei să se poată aplica translaţii, rotaţii în jurul originii, simetrii faţă
de axele de coordonate unui punct din plan. Folosind această clasă să se calculeze
pentru un punct bidimensional simetricul faţă de o dreaptă oarecare dată sub forma
generală ax+by+c=0.
Cuprins
Ne propunem să studiem două ierarhii de clase: streambuf şi ios pentru o mai bună
înţelegere a modului de lucru cu fluxuri în C++.
81
Din clasa streambuf sunt derivate două clase: filebuf (care gestionează buffer-ele pentru
fluxurile de fişiere) şi strstreambuf care gestionează buffer-ele pentru fluxurile de lucru cu
string-uri:
streambuf
filebuf strstreambuf
streambuf()
streambuf(char *s,int n)
filebuf()
filebuf(int fd)
filebuf(int fd,char *s,int n)
~filebuf()
Clasa strstreambuf este scrisă pentru operaţii de intrare / ieşire în / din zone de memorie
RAM. Clasa are 5 constructori şi nici un destructor.
Ierarhia de clase pornită de ios este scrisă pentru a lucra cu fluxuri de date. Transmiterea
şi primirea datelor se face prin intermediul unei zone tampon prelucrate prin intermediul unui
obiect instanţă al unei clase din ierarhia streambuf.
Clasa ios are 2 constructori:
ios(streambuf *buf)
ios()
82
Primul constructor asociază un buffer buf, obiect al clasei streambuf unui flux,
constructorul primeşte ca argument adresa spre obiectul de tip buffer cu care va lucra fluxul. Al
doilea constructor crează un obiect al clasei ios, fără a-l lega la un buffer. Legarea se poate face
ulterior cu ajutorul funcţiei init, care este membră clasei ios.
Clasa ios conţine următoarele câmpuri:
1) int bad() returnează o valoare întreagă nenulă dacă a apărut cel puţin o eroare în
prelucrarea fluxului. Verificarea se face interogând biţii ios::badbit şi ios::hardfail ai valorii din
câmpul state al clasei ios.
2) void clear(int st=0); setează câmpul state la valoarea întreagă primită ca argument.
Dacă st este 0 (valoare de altfel implicită), atunci starea fluxului se iniţializează din nou ca fiind
bună. Un apel de forma flux.clear() readuce fluxul în stare bună (fără erori).
3) int eof() returnează o valoare nenulă, dacă nu mai sunt date în flux (sfârşit de fişier).
De fapt funcţia interoghează bitul ios::eofbit al câmpului state.
4) int fail() returnează o valoarea nenulă, dacă o operaţie aplicată fluxului a eşuat. Se
verifică biţii ios::failbit, ios::badbit şi ios::hardfail ai câmpului state (în plus faţă de funcţia bad
verifică şi bitul ios::failbit).
5) char fill() returnează caracterul de umplere al spaţiile libere de la scrierea formatată.
6) char fill(char c) setează caracterul de umplere al spaţiile goale de la scrierea formatată
la valoarea c şi returnează caracterul folosit anterior în acest scop.
7) long ios_flags() returnează valoarea reţinută în x_flags.
8) long ios_flags(long flags) setează câmpul x_flags la valoarea primită ca argument şi
returnează vechea valoare.
9) int good() returnează o valoare nenulă dacă nu au apărut erori în prelucrarea fluxului.
Acest lucru se realizează consultând biţii câmpului state.
10) void init(streambuf *buf); transmite adresa buffer-ului de tip streambuf cu care va
lucra fluxul.
83
11) int precision(int p); setează precizia de tipărire a numerelor reale la valoarea primită
ca argument şi returnează vechea valoare. De fapt câmpul ios::precision se seteaza la valoarea p
şi se returnează vechea sa valoare.
12) int precision() returnează valoarea preciziei la tipărire a valorilor reale (reţinută în
câmpul ios::precision).
13) streambuf* rdbuf() returnează adresa către obiectul responsabil cu buffer-ul fluxului.
14) int rdstate() returnează starea fluxului (valoarea câmpului state).
15) long setf(long setbits, long field); resetează biţii (îi face 0) din x_flags pe poziţiile
indicate de biţii cu valoare 1 ai parametrul field şi apoi setează biţii din x_flags la valoarea 1 pe
poziţiile în care biţii sunt 1 în parametrul setbits. Funcţia returnează vechea valoare a lui x_flags.
16) long setf(long flags) modifică biţii câmpului x_flags la valoare 1 pe poziţiile în care
biţii parametrului flags sunt 1 şi returnează vechea valoare a lui x_flags.
17) void setstate(int st); setează biţii câmpului state la valoarea 1 pe poziţiile în care biţii
parametrului st sunt 1.
18) void sync_with_stdio(); sincronizează fişierele stdio cu fluxurile iostream. În urma
sincronizării viteza de execuţie a programului scade mult.
19) ostream* tie() returnează adresa către fluxul cu care se afla legat fluxul curent. De
exemplu, fluxurile cin şi cout sunt legate. Legătura dintre cele două fluxuri constă în faptul că
atunci când unul dintre cele două fluxuri este folosit, atunci mai întâi celălalt este golit. Dacă
fluxul curent (din care este apelată funcţia tie) nu este legat de nici un flux, atunci se returnează
valoarea NULL.
20) ostream* tie(ostream* fl) fluxul fl este legat de fluxul curent, cel din care a fost
apelată această funcţie. Este returnat fluxul anterior legat de fluxul curent. Ca efect al legării unui
flux de un alt flux este faptul că atunci când un flux de intrare mai are caractere în buffer sau un
flux de ieşire mai are nevoie de caractere, atunci fluxul cu care este legat este întâi golit. Impl icit,
fluxurile cin, cerr şi clog sunt legate de fluxul cout.
21) long unsetf(long l) setează biţii din x_flags la valoarea 0 pe pozitiile în care
parametrul l are biţii 1.
22) int width() returnează lungimea pe care se face afişarea formatată.
23) int width(int l); setează lungimea pe care se face afişarea formatată la valoarea
primită ca argument şi returnează vechea valoare.
Din clasa ios sunt derivate 4 clase: istream, ostream, fstreambase şi strstreambase:
ios
Clasa istream realizează extrageri formatate sau neformatate dintr-un buffer definit ca
obiect al unei clase derivate din streambuf.
Constructorul clasei istream este:
istream(strstream *buf);
Constructorul asociază un obiect de tip buffer buf, primit ca argument, unui flux de
intrare.
Funcţiile membre clasei istream:
În clasa istream este supraîncărcat şi operatorul >> pentru extragere de date în modul
text din fluxul de intrare.
Clasa ostream realizează introduceri formatate sau neformatate într-un buffer definit ca
obiect al unei clase derivate din streambuf. Clasa are un singur constructor cu următoarea
structură:
ostream(streambuf* buf);
85
Constructorul asociază un buffer, obiect al unei clase derivate din streambuf, fluxului de
ieşire.
Metodele clasei ostream sunt:
În clasa ostream este supraîncărcat operatorul << pentru introducere de valori în modul
text în flux.
Clasa iostream este derivată din clasele istream şi ostream şi are ca obiecte fluxuri care
suportă operaţii de intrare şi ieşire. Clasa iostream are un singur constructor care asociază unui
buffer buf, obiect al clasei streambuf, un flux, obiect al clasei iostream:
iostream(streambuf* buf);
Rezumat
Cuprins
86
Obiectivele unităţii de învăţare
fstreambase();
Parametrul mod specifică modul de deschidere al fişierului (text sau pe octeţi, pentru
scriere sau pentru citire etc.). Pentru modurile de deschidere sunt definite constante întregi în
interiorul clasei ios. Acestea sunt:
Constantă mod
Semnificaţie
deschidere
ios::in Deschidere fişier pentru citire
ios::out Deschidere fişier pentru scriere
ios::ate Se face poziţionare la sfârşitul fişierului care e deja deschis
ios::app Deschidere fişier pentru adăugare la sfârşit
ios::trunc Trunchiază fişierul
ios::nocreate Deschiderea fişierului se face numai dacă acesta există
ios::noreplace Deschiderea fişierului se face numai dacă acesta nu există
ios::binary Deschiderea fişierului se face în modul binar (pe octeti)
87
fstreambase(int d);
1) void attach(int d); face legatura între fluxul curent (din care se apelează funcţia attach)
cu un fişier deschis, al cărui descriptor este d.
2) void close(); închide buffer-ul filebuf şi fişierul.
3) void open(const char* s, int mod, int prot=filebuf::openprot); deschide un fişier în
mod similar ca şi în cazul celui de-al doilea constructor care are aceeaşi parametri cu funcţia
open. Ataşează fluxului curent fişierul deschis.
4) filebuf* rdbuf() returnează adresa către buffer-ul fişierului.
5) void setbuf(char* s, int n); setează adresa şi lungimea zonei de memorie cu care va
lucra obiectul buffer al clasei filebuf ataşat fişierului.
Din clasele istream şi fstreambuf este derivată clasa ifstream, care este o clasă
specializată pentru lucrul cu un fişier de intrare (pentru extrageri de date).
istream fstreambuf
ifstream
ifstream();
Al doilea constructor crează un obiect al clasei ifstream, deschide un fişier pentru operatii
de citire şi îl ataşează obiectului creat. Pentru ca deschiderea fişierului să se încheie cu succes,
trebuie ca fişierul să existe. Semnificaţia parametrilor este aceeaşi ca la constructorul al doilea al
clasei fstreambase:
ifstream(int d);
1) void open(const char* s, int mod, int prot=filebuf::openprot); deschide un fişier pentru
citire în mod similar cu al doilea constructor al clasei.
2) filebuf* rdbuf() returnează adresa către buffer-ul fluxului curent, obiect al clasei
filebuf.
Din clasele ostream şi fstreambuf este derivată clasa ofstream specializată pentru
operaţii de iesire pe fişiere.
ostream fstreambuf
ofstream
Clasa ofstream are, de asemenea, 4 constructori asemănători cu cei din clasa fstreambuf.
Primul constructor crează un flux pe care nu -l ataşează unui fişier:
ofstream();
Al doilea constructor crează un obiect al clasei ofstream, deschide un fişier pentru scriere
şi îl ataşează obiectului creat.
ofstream(int d);
Există şi constructorul care crează obiectul şi îl leagă de fişierul al cărui descriptor este d.
Se specifică în plus şi adresa zonei de memorie tampon ce se va utiliza precum şi lungimea
acesteia:
1) void open(const char* s, int mod, int prot=filebuf::openprot); deschide un fişier pentru
scriere în mod similar cu al doilea constructor al clasei.
2) filebuf* rdbuf() returnează adresa către buffer-ul buf cu care lucrează fluxul de ieşire.
Din clasele iostream, ifstream şi ofstream este derivată clasa fstream, care este
specializată pentru a lucra cu un fişier în care sunt posibile atât operaţii de intrare, cât şi ieşire:
89
iostream ifstream ofstream
fstream
Clasa fstream are, de asemeanea, 4 constructori (asemănători cu cei din clasa fstreambuf).
Un prim constructor crează un flux pe care nu -l ataşează nici unui fişier:
fstream();
Al doilea constructor crează un obiect al clasei fstream, deschide un fişier pentru operaţii
de citire şi de scriere şi îl ataşează obiectului creat. Semnificaţia parametrilor este aceeaşi ca la
constructorul al doilea al clasei fstreambase:
Al treilea constructor crează obiectul şi îl leagă de un fişier I/O deschis deja, al cărui
descriptor d este primit ca argument:
fstream(int d);
1) void open(const char* s, int mod, int prot=filebuf::openprot); deschide un fişier pentru
citire şi scriere în mod similar cu al doilea constructor al clasei, care are aceeaşi parametri cu
funcţia open.
2) filebuf* rdbuf() returnează adresa către buffer-ul fişierului.
Pentru a lucra cu un fişier, se instanţiază una dintre clasele: ifstream, ofstream sau
fstream. Prelucrarea fişierului se face cu ajutorul metodelor moştenite din clasele: ios, istream,
ostream, fstreambase.
Pentru a exemplifica modul de lucru cu fişiere dăm listingul câtorva programe:
# include <iostream.h>
# include <fstream.h>
# include <conio.h>
# define lmaxlinie 79 // lungimea maxima a liniei fisierului text
int main()
{
char numef[100],linie[lmaxlinie+1];
long n=0;
90
cout<<"AFISARE CONTINUT FISIER TEXT"<<endl<<endl;
cout<<"Dati numele fisierului text: ";
cin>>numef;
ifstream fis(numef); // creare obiect si deschidere fisier pentru
citire
if (fis.bad()) // verificare daca fisierul nu a fost deschis cu
succes
{
cerr<<"Eroare! Nu am putut deschide '"<<numef<<"' pt citire";
getch();
return 1;
}
while (!fis.eof())
{
n++; // numarul de linii afisate
fis.getline(linie,lmaxlinie); // citire linie din fisier text
cout<<linie<<endl; // afisare pe ecran
if (n%24==0) getch(); // dupa umplerea unui ecran se
} // asteapta apasarea unei taste
fis.close();
cout<<endl<<"AM AFISAT "<<n<<" LINII"<<endl<<flush;
getch();
return 0;
}
Observaţii:
1) Cu ajutorul funcţiei bad() am verificat dacă fişierul nu a fost deschis cu succes (dacă
fişierul nu există pe disc).
2) Funcţia getline (din clasa istream) citeşte o linie de lungime maximă lmaxlinie=79
caractere dintr-un fişier text. Dacă linia are mai mult de lmaxlinie caractere, atunci
sunt citite exact lmaxlinie caractere, restul caracterelor de pe linia curentă a fişierului
fiind citite data următoare.
# include <iostream.h>
# include <fstream.h>
# include <conio.h>
# define lbuf 1000 // lungimea buffer-ului de citire din fisier
int main()
{
ifstream fiss; // fisier pentru operatii de citire
ofstream fisd; // fisier pentru operatii de scriere
char numefs1[100],numefs2[100],numefd[100],s[lbuf];
cout<<"CONCATENARE DE DOUA FISIERE"<<endl<<endl;
cout<<"Dati numele primului fisier sursa: ";
cin>>numefs1;
cout<<"Dati numele celui de-al doilea fisier sursa: ";
cin>>numefs2;
91
cout<<"Dati numele fisierului destinatie: ";
cin>>numefd;
fisd.open(numefd,ios::binary); // deschidere fisier pe octeti (mod
binar)
if (fisd.bad()) // verificare daca fisierul nu s-a deschis cu succes
{
cerr<<"Eroare! Nu am putut deschide fisierul '"<<numefd<<"'
pt scriere.";
getch(); return 1;
}
fiss.open(numefs1,ios::binary); // deschidere fisier pe octeti (mod
binar)
if (fiss.bad()) // verificare daca fisierul nu s-a deschis cu succes
{
fisd.close();
cerr<<"Eroare! Nu am putut deschide fisierul '"<<numefs1<<"'
pt citire.";
getch(); return 1;
}
while (!fiss.eof())
{
fiss.read(s,lbuf); // se citesc maxim lbuf baiti din fisier
fisd.write(s,fiss.gcount());// se scriu baitii cititi mai sus
in fisier
}
fiss.close(); // inchidere fisier
fiss.open(numefs2,ios::binary); // deschidere fisier pe octeti (mod
binar)
if (fiss.bad()) // verificare daca fisierul nu s-a deschis cu succes
{
fisd.close();
cerr<<"Eroare! Nu am putut deschide fisierul '"<<numefs2<<"'
pt citire.";
getch(); return 1;
}
while (!fiss.eof())
{
fiss.read(s,lbuf); // se citesc maxim lbuf baiti din fisier
fisd.write(s,fiss.gcount());// se scriu baitii cititi mai sus
in fisier
}
fiss.close(); // inchidere fisier
if (fisd.good()) cout<<"Concatenarea s-a incheiat cu succes!"<<endl;
fisd.close(); // inchidere fisier
getch();
return 0;
}
Observaţii:
92
1) Pentru a deschide un fisier în modul binar trebuie specificat explicit acest lucru
folosind ios::binary în parametrul al doilea al funcţiei open. Dacă nu se face
specificarea ios::binary, atunci fişierul este deschis în modul text.
2) Funcţia gcount() (din clasa istream) returnează numărul de caractere efectiv citite
ultima dată cu ajutorul funcţiei read. În programul de mai sus valoarea returnată de
gcount() este lbuf, adică 1000, excepţie face ultima citire din fişier.
3) Cu ajutorul funcţiei good() am verificat dacă fişierul a fost prelucrat fără să apară
erori.
# include <iostream.h>
# include <fstream.h>
# include <conio.h>
# include <stdlib.h>
int main()
{
char numef[100];
int i,n;
float *a;
fstream fis;
cout<<"QUICKSORT"<<endl<<endl;
cout<<"Dati numele fisierului text cu elementele sirului: ";
cin>>numef;
fis.open(numef,ios::in); // deschidere fisier pentru citire
if (fis.bad())
{
cerr<<"Eroare! Nu am putut deschide fisierul '"<<numef<<"' pt citire.";
getch();
return 1;
}
fis>>n; // citirea numarului intreg n din fisier
a=new float[n];
if (a==NULL)
{
cerr<<"Eroare! Memorie insuficienta.";
getch();
return 1;
}
93
for (i=0;i<n;i++)
{
if (fis.eof())
{
cerr<<"Eroare! Elemente insuficiente in fisier.";
getch();
return 1;
}
fis>>a[i]; // citirea unui numar real (float)
}
fis.close();
qsort((void *)a, n, sizeof(float), sort_function); // sortare QuickSort
cout<<"Dati numele fisierului text in care se va depune sirul sortat: ";
cin>>numef;
fis.open(numef,ios::out); // deschidere fisier pentru scriere
if (fis.bad())
{
cerr<<"Eroare! Nu am putut crea fisierul '"<<numef<<"'.";
getch();
return 1;
}
fis<<n<<endl;
for (i=0;i<n;i++) fis<<a[i]<<" ";
delete [] a;
if (fis.good()) cout<<"Am sortat sirul !"<<endl;
fis.close();
getch();
return 0;
}
Observaţii:
1) Variabila fis este folosită întâi pentru a prelucra un fişier de intrare şi apoi pentru unul
de ieşire.
2) Citirea unor valori dintr-un fişier în modul text a fost realizată cu ajutorul
operatorului >>, iar scrierea cu operatorul <<.
3) Sortarea şirului am făcut-o cu ajutorul funcţiei qsort, a cărei definiţie o găsim în
fişierul antet stdlib.h sau în search.h. Cu ajutorul acestei funcţii se pot sorta şiruri de
elemente de orice tip. Noi am folosit-o pentru sortarea unui şir de valori de tip float.
Funcţia sort_function compară două valori de tipul elementelor din şir. Dacă se
doreşte o sortare crescătoare, funcţia trebuie să returneze o valoare negativă. Se
returnează 0, dacă sunt egale valorile şi, respectiv, se returnează un număr pozitiv
dacă prima valoare e mai mare decât a doua.
# include <iostream.h>
# include <fstream.h>
# include <stdlib.h>
# include <process.h>
# include <conio.h>
# include <stdio.h>
94
# include <string.h>
struct tstoc
{
char cod_prod[10],den_prod[50];
double cant,pret;
};
void main()
{
char c;
do
{
cout<<"GESTIONAREA STOCULUI UNEI FIRME"<<endl<<endl;
cout<<" 1) Vanzare / cumparare marfa"<<endl;
cout<<" 2) Adaugare produs in stoc"<<endl;
cout<<" 3) Afisare produs dupa cod"<<endl;
cout<<" 4) Modificare pret"<<endl;
cout<<" 5) Stergere produs din stoc"<<endl;
cout<<" 6) Cautare produs dupa nume"<<endl;
cout<<" 7) Afisare stoc"<<endl;
102
cout<<" 8) Valoare totala stoc"<<endl;
cout<<endl<<"Esc - Exit"<<endl<<endl;
c=getch();
switch (c)
{
case '1': VanzCump(); break;
case '2': Adaugare(); break;
case '3': AfisProd(); break;
case '4': ModifPret(); break;
case '5': Stergere(); break;
case '6': Cautare(); break;
case '7': AfisStoc(); break;
case '8': ValStoc(); break;
}
}
while (c!=27);
}
Observaţie:
Fluxurile C++ pentru fişiere lucrează numai cu caractere şi şiruri de caractere. De aceea,
când am folosit funcţiile read, respectiv write pentru citirea, respectiv scrierea unei variabile de
tipul struct tstoc, am fost nevoiţi să facem conversii de la şir de caractere la tipul adresă către un
tip struct tstoc. De fapt, în variabilele s (de tip adresă către un şir de caractere) şi st (pointer către
tipul struct tstoc) s-a reţinut aceeaşi adresă de memorie. Am citit şi scris şirul de caractere de
lungime sizeof(struct tstoc), şir de caractere aflat la aceeaşi adresă către care pointează şi st. Cu
alte cuvinte, scrierea şi citirea datelor la adresa st s-au făcut prin intermediul variabilei s.
Rezumat
Cuprins
103
Obiectivele unităţii de învăţare
strstreambase();
strstreambase(const char* buf, int n, char* start);
Primul constructor crează un obiect al clasei strstreambase, fără a specifica însă şirul de
caractere cu care se va lucra. Legătura se va face dinamic cu un şir de caractere prima dată când
obiectul va fi utilizat.
Al doilea constructor crează un obiect de strstreambase, obiect ce va folosi şirul de
caractere aflat la adresa buf, care are lungimea n, al cărui poziţie de pornire este start.
Funcţia membră rdbuf clasei strstreambase va returna adresa buffer-ului (obiect al clasei
strstreambuf), cu care lucrează fluxul de tip string:
strstreambuf* rdbuf();
Din clasele istream şi strstreambase este derivată clasa istrstream, care este
specializată (după cum îi spune numele şi clasele din care este derivată) pentru a lucra cu fluxuri
de intrare de tip string:
104
Primul constructor crează un obiect al clasei istrstream şi specifică faptul că acesta va
lucra cu string-ul s.
Al doilea constructor, în plus faţă de primul, limitează la n numărul de caractere din şirul
s cu care se va lucra. Cu alte cuvinte, string-ul s nu va putea fi mai lung de n caractere.
Din clasele ostream şi strstreambase este derivată clasa ostrstream, care este
specializată pentru a lucra cu fluxuri de ieşire de tip string (string-ului nu i se vor putea aplica
decât operaţii de scriere).
Clasa ostrstream are doi constructori:
ostrstream();
ostrstream(char* s, int n, int poz=ios::out);
Primul constructor crează un obiect de tipul ostrstream, obiect care va lucra cu un şir de
caractere alocat dinamic.
Al doilea constructor crează un obiect al clasei ostrstream şi specifică faptul că acesta va
lucra cu şirul de caractere s de lungime maximă n. Poziţionarea în string-ul s se face implicit la
începutul acestuia. Dacă ultimul parametru este specificat ca având valoarea ios::app sau
ios::ate, atunci poziţionarea în string-ul s se va face pe ultima poziţie a acestuia, adică pe poziţia
pe care se afla caracterul ‘\0’ (delimitatorul de sfârşit de string).
Pe lângă cei doi constructori, clasa ostrstream mai are două funcţii membre:
Din clasele iostream, istrstream şi ostrstream este derivată clasa strstream, care este o
clasă de fluxuri ce lucrează cu string-uri ce suportă operaţii atat de intrare, cât şi de ieşire:
strstream
strstream();
strstream(char* s, int n, int poz=ios::out);
Primul constructor crează un obiect de tipul strstream, obiect care va lucra cu un şir de
caractere alocat dinamic.
Al doilea constructor crează un obiect al clasei strstream şi specifică faptul că acesta va
lucra cu şirul de caractere s de lungime maxima n. Poziţionarea în string-ul s se face implicit la
începutul acestuia. Dacă valoarea ultimului parametru este ios::app sau ios::ate, atunci
poziţionarea în string-ul s se va face pe ultima poziţie a acestuia, adică pe poziţia pe care se afla
caracterul ‘\0’ (delimitatorul de sfârşit de string).
Pe lângă cei doi constructori, în clasa strstream există şi funcţia membră str(), care
returnează adresa către şirul de caractere cu care lucrează fl uxul.
Când se doreşte a se lucra cu fluxuri de tip string se instanţiază una dintre clasele:
istream, ostream şi strstream. Prelucrarea string-ului se face utilizând metodele din clasele
superioare: ios, istream, ostream, strstreambase.
105
Spre exemplificare, prezentăm următorul program care efectuează operaţii aritmetice cu
numere reale (citirea expresiei matematice se face dintr-un string):
# include <stdio.h>
# include <conio.h>
# include <math.h>
# include <iostream.h>
# include <strstrea.h>
void main(void)
{
char s[100],c=0,op;
float nr1,nr2;
istrstream *si;
do
{
cout<<" Calcule matematice (operatii: +,-,*,/,^)"<<endl;
cout<<"---------------------------------------------)"<<endl;
cout<<endl<<"<numar_real> <operator> <numar_real>: ";
gets(s);
cout<<endl;
si=new istrstream(s); // construire obiect
*si>>nr1>>op>>nr2;
if (!si->bad()) switch (op)
{
case '+':
cout<<nr1<<op<<nr2<<"="<<(nr1+nr2);
break;
case '-':
cout<<nr1<<op<<nr2<<"="<<(nr1-nr2);
break;
case '*':
cout<<nr1<<op<<nr2<<"="<<(nr1*nr2);
break;
case '/':
if (nr2)
cout<<nr1<<op<<nr2<<"="<<(nr1/nr2);
else cout<<"Eroare! Impartire prin 0.";
break;
case '^': // ridicare la putere
if (nr1>0)
cout<<nr1<<op<<nr2<<"="<<pow(nr1,nr2);
else cerr<<"Eroare! Numar negativ.";
break;
default:
cerr<<"Eroare! Operator necunoscut";
}
else cerr<<"Eroare! Utilizare incorecta.";
cout<<endl<<endl<<"Apasati Esc pentru a parasi programul,";
cout<<" orice alta tasta pentru a continua.";
c=getch();
106
delete si; // distrugere obiect
}
while (c!=27);
}
Rezumat
Pentru prelucrarea string-urilor în C++ instanţiem una din clasele: istrstream, ostrstream
sau strstream. Practic, un şir de caractere NULL terminat se “îmbracă” într-un obiect pentru a fi
prelucrat, urmând ca în final el să poată fi extras din obiect cu metoda str() care returnează
adresa şirului.
C++ oferă o clasă de lucru cu un număr complex. În această clasă sunt supraîncărcaţi o
serie de operatori. De asemenea există o mulţime de metode prietene clasei pentru numere
complexe.
În continuare vom prezenta clasa complex aşa cum există ea în Borland C++. Pentru a
lucra cu ea trebuie inclus fişierul antet “complex.h”.
Clasa complex conţine două date de tip double. Este vorba de re, care reţine partea reală a
numarului complex şi de im, partea imaginară a numarului complex. Aceste date sunt private.
Clasa complex are 2 constructori:
complex();
complex(double Re, double Im=0);
Primul constructor crează un obiect fără a iniţializa partea reală şi cea imaginară a
numarului complex.
Al doilea constructor iniţializează partea reală şi pe cea imaginară cu cele două valori
primite ca argumente. Pentru al doilea argument există valoarea implicită 0. Evident, în cazul în
care valoarea pentru partea imaginară este omisă, la construcţia obiectului complex ea se va
iniţializa cu 0 (vezi capitolul dedicat valorilor implicite pentru parametrii funcţiilor în C++).
Există o mulţime de funcţii (matematice) definite ca fiind prietene clasei complex:
re 2 im 2
107
6) complex polar(double r, double u=0); crează un obiect de tip complex pornind de la
norma şi argumentul acestuia. Numărul complex care se crează este rcos(u)+rsin(u)i. Se
returnează obiectul creat.
7) double abs(complex &z) returnează modulul numărului complex z, adică re2+im2
(pătratul normei).
8) complex acos(complex &z) returnează arccosinus din numărul complex z.
9) complex asin(complex &z) returnează arcsinus din numărul complex z.
10) complex atan(complex &z) returnează arctangentă din numărul complex z.
11) complex cos(complex &z) returnează cosinus din numărul complex z.
12) complex cosh(complex &z) returnează cosinus hiperbolic din z.
13) complex exp(complex &z) returnează ez.
14) complex log(complex &z) returnează logaritm natural din numărul z.
15) complex log10(complex &z) returnează logaritm zecimal (în baza 10) din z.
16) complex pow(double r, complex &z) ridică numărul real r la puterea z.
17) complex pow(complex &z, double r) ridică numărul complex z la puterea r.
18) complex pow(complex &z1, complex &z2) ridică z1 la puterea z2.
19) complex sin(complex &z) returnează sinus din numărul complex z.
20) complex sinh(complex &z) returnează sinus hiperbolic din z.
21) complex sqrt(complex &z) returnează radical din numărul complex z.
22) complex tan(complex &z) returnează tangentă din numărul complex z.
23) complex tanh(complex &z) returnează tangentă hiperbolică din z.
# include <stdio.h>
# include <conio.h>
# include <complex.h>
# include <iostream.h>
# include <strstrea.h>
void main(void)
{
char s[100],c=0,op;
complex nr1,nr2; // doua numere complexe (obiecte complex)
istrstream *si;
108
do
{
cout<<" Calcule cu numere complexe (+, -, *, / si ^)"<<endl;
cout<<"---------------------------------------------)"<<endl;
cout<<endl<<"<numar_complex> <operator> <numar_complex>: ";
gets(s);
cout<<endl;
si=new istrstream(s);
*si>>nr1>>op>>nr2;
if (si->good())
switch (op)
{
case '+':
cout<<nr1<<op<<nr2<<"="<<(nr1+nr2);
break;
case '-':
cout<<nr1<<op<<nr2<<"="<<(nr1-nr2);
break;
case '*':
cout<<nr1<<op<<nr2<<"="<<(nr1*nr2);
break;
case '/':
if (abs(nr2))
cout<<nr1<<op<<nr2<<"="<<(nr1/nr2);
else cout<<"Eroare! Impartire prin 0.";
break;
case '^': // ridicare la putere
cout<<nr1<<op<<nr2<<"="<<pow(nr1,nr2);
break;
default:
cerr<<"Eroare! Operator necunoscut.";
}
else cerr<<"Eroare! Utilizare incorecta.";
cout<<endl<<endl<<"Apasati Esc pentru a parasi programul,";
cout<<" orice alta tasta pentru a continua.";
c=getch();
delete si;
}
while (c!=27);
}
Rezumat
În finalul fiscuţiei noastre despre programarea orientată pe obiecte din C++ am prezentat
clasa complex scrisă pentru lucrul cu numere complexe. Nu prea avem membri în clasa complex
109
ci mai mult funcţii prietene pentru a apropia mai mult modul de lucru cu numere complexe de cel
cu numere reale din C.
110
ANEXA - Urmărirea execuţiei unui program. Rularea pas cu pas.
Pentru a vedea efectiv traseul de execuţie şi modul în care se îşi modifică variabilele
valorile într-un program, putem rula pas cu pas. Acest lucru se face în Borland C/C++ cu ajutorul
butoanelor F7 sau F8, iar în Visual C++ cu F11, combinaţii de taste care au ca efect rularea liniei
curente şi trecerea la linia următoare de execuţie.
Execuţia programului până se ajunge la o anumită linie se face apăsând pe linia
respectivă butonul F4 în Borland C/C++ şi respectiv Ctrl+F10 în Visual C++.
111
BIBLIOGRAFIE
6. T. Faison, Borland C++ 3.1 Object-Oriented Programming, ediţia a doua, Sams Publishing,
Carmel, IN, 1992.
12. S. C. Dewhurst, K. T. Stark, Programming in C++, Prentice Hall, Englewood Cliffs, NJ,
1989.
14. B. S. Lippman, C++ Primer, ediţia a doua, Addison-Wesley, Reading, MA, 1991.
17. I. Pohl, C++ for C Programmers, The Benjamin/Cummings Publishing Company. Redwood
City, CA, 1989.
19. K. Weiskamp, B. Flaming, The Complete C++ Primer, Academic Press, Inc., San Diego,
CA, 1990.
20. J. D. Smith, Reusability & Software Construction: C & C++, John Wiley & Sons, Inc., New
York, 1990.
112
21. G. Booch, Object-Oriented Design with Applications, The Benjamin/Cummings Publishing
Company. Redwood City, CA, 1991.
22. B. Meyer, Object-Oriented Software Construction, Pretice Hall International (U.K.) Ltd.
Hertfordshire, Marea Britanie, 1988.
113