Sunteți pe pagina 1din 28

1) Enunţaţi teorema de structură Bohm-Jacopini şi prezentaţi structurile algoritmice decizionale şi

iterative în C++.

Algoritmul proiectat pentru rezolvarea unei anumite probleme trebuie implementat într-un limbaj de
programare; prelucrarea datelor se realizează cu ajutorul instrucţiunilor. Instrucţiunea descrie un proces
de prelucrare pe care un calculator îl poate executa. O instrucţiune este o construcţie validă (care respectă
sintaxa limbajului) urmată de ; . Ordinea în care se execută instrucţiunile unui program defineşte aşa-
numita structură de control a programului.

Limbajele moderne sunt alcătuite pe principiile programării structurate. Conform lui C. Bohm şi G.
Jacobini, orice algoritm poate fi realizat prin combinarea a trei structuri fundamentale:

 structura secvenţială;
 structura alternativă (de decizie, de selecţie);
 structura repetitivă (ciclică).

Reprezentarea prin schemă logică şi prin pseudocod a structurilor de decizie şi repetitive sunt descrise în
capitolul 1. Se vor prezenta în continure doar instrucţiunile care le implementează.

 Instrucţiunea if:
Sintaxa:

if (expresie)

instrucţiune1;

[ else

instrucţiune2; ]

Ramura else este opţională. La întâlnirea instrucţiunii if, se evaluează expresie (care reprezintă o condiţie)
din paranteze. Dacă valoarea expresiei este 1 (condiţia este îndeplinită) se execută instrucţiune1; dacă
valoarea expresiei este 0 (condiţia nu este îndeplinită), se execută instrucţiune2. Deci, la un moment dat,
se execută doar una dintre cele două instrucţiuni: fie instrucţiune1, fie instrucţiune2. După execuţia
instrucţiunii if se trece la execuţia instrucţiunii care urmează acesteia.

Observaţii:

1. Instrucţiune1 şi instrucţiune2 pot fi instrucţiuni compuse (blocuri), sau chiar alte instrucţiuni if (if-uri
imbricate).
2. Deoarece instrucţiunea if testează valoarea numerică a expresiei (condiţiei), este posibilă
prescurtarea: if (expresie), în loc de if (expresie != 0).
3. Deoarece ramura else a instrucţiunii if este opţională, în cazul în care aceasta este omisă din
secvenţele if-else imbricate, se produce o ambiguitate. De obicei, ramura else se asociază ultimei
instrucţiuni if.

1
4. Instrucţiunea switch
În unele cazuri este necesară o decizie multiplă specială. Instrucţiunea switch permite acest lucru.

Reprezentare prin schema logică (figura 3.2): Reprezentare prin pseudocod:

test_expresie

instrucţiune1
break
Dacă expresie=expr_const_1
instrucţiune2
instrucţiune1;
break
[ieşire;]

Altfel dacă expresie=expr_const_2


instrucţiune_n
instrucţiune2;

Figura 3.2. Decizia multiplă [ieşire;]


Se testează dacă valoarea pentru expresie este una dintre constantele specificate (expr_const_1,
expr_const_2, etc.) şi se execută instrucţiunea de pe ramura corespunzătoare. În schema logică
test_expresie este una din condiţiile: expresie=expr_const_1, expresie=expr_const_2, etc.

Sintaxa: Altfel dacă


expresie=expr_const_n-1
switch (expresie)
instrucţiune_n-1;
{
[ieşire;]
case expresie_const_1: instructiune_1;
Altfel instrucţiune_n;
[break;]

case expresie_const_2: instructiune_2;

[break;]

..................................

case expresie_const_n-1: instructiune_n-1;

[break;]

[ default: instructiune_n; ]

2
Este evaluată expresie (expresie aritmetică), iar valoarea ei este comparată cu valoarea expresiilor
constante 1, 2, etc. (expresii constante=expresii care nu conţin variabile). În situaţia în care valoarea
expresie este egală cu valoarea expr_const_k, se execută instrucţiunea corespunzătoare acelei ramuri
(instrucţiune_k). Dacă se întâlneşte instrucţiunea break, parcurgerea este întreruptă, deci se va trece la
execuţia primei instrucţiuni de după switch. Dacă nu este întâlnită instrucţiunea break, parcurgerea
continuă. Break-ul cauzează deci, ieşirea imediată din switch.

În cazul în care valoarea expresiei nu este găsită printre valorile expresiilor constante, se execută cazul
marcat cu eticheta default (când acesta există). Expresiile expresie, expresie_const_1,
expresie_const_2,etc., trebuie să fie întregi. În exemplul următor, ele sunt de tip char, dar o dată de tip
char este convertită automat în tipul int.

2) Descrieţi procesarea eficientă a tablourilor de date în C++ cu ajutorul pointerilor. Exprimaţi


relaţia dintre tablouri de date şi pointeri (corespondenţa între indexare şi aritmetica pointerilor).

Pointerii sunt variabile care au ca valori adresele altor variabile (obiecte). Variabila este un nume simbolic
utilizat pentru un grup de locaţii de memorie. Valoarea memorată într-o variabilă pointer este o adresă.

Din punctul de vedere al conţinutului zonei de memorie adresate, se disting următoarele categorii de
pointeri:

 pointeri de date (obiecte) - conţin adresa unei variabile din memorie;


 pointeri generici (numiţi şi pointeri void) - conţin adresa unui obiect oarecare, de tip neprecizat;
 pointeri de funcţii (prezentaţi în capitolul 6.11.)- conţin adresa codului executabil al unei funcţii.

În limbajele C/C++ există o strânsă legătură între pointeri şi tablouri, deoarece numele unui tablou este un
pointer (constant!) care are ca valoare adresa primului element din tablou. Diferenţa dintre numele unui
tablou şi o variabilă pointer este aceea că unei variabile de tip pointer i se pot atribui valori la execuţie,
lucru imposibil pentru numele unui tablou. Acesta are tot timpul, ca valoare, adresa primului său element.
De aceea, se spune că numele unui tablou este un pointer constant (valoarea lui nu poate fi schimbată).
Numele unui tablou este considerat ca fiind un rvalue (right value-valoare dreapta), deci nu poate apare
decât în partea dreaptă a unei expresii de atribuire. Numele unui pointer (în exemplul următor,  ptr) este
considerat ca fiind un lvalue (left value-valoare stânga), deci poate fi folosit atât pentru a obţine valoarea
obiectului, cât şi pentru a o modifica printr-o operaţie de atribuire.

Exemplu:

int a[0],  ptr; // a este definit ca &a[0]; a este pointer constant

a = a + 1; // ilegal

ptr = a ; // legal: ptr are aceeaşi valoare ca şi a, respectiv adresa elementului a[0]

// ptr este variabilă pointer, a este constantă pointer.

3
int x = a[0]; // echivalent cu x =  ptr; se atribuie lui x valoarea lui a[0]

Deoarece numele tabloului a este sinonim pentru adresa elementului de indice zero din tablou, asignarea
ptr=&a[0] poate fi înlocuită, ca în exemplul anterior, cu ptr=a.

5.4. TABLOURI DE POINTERI

Să ne amintim de la pointeri către şiruri de caractere:

char  p=”heLLO”;

 ( p+1) = ’e’  p[1] = ’e’;

În mod analog:

str_ptr[1] = ”este”;

 ( str_ptr[1] + 1) = ’s’;  str_ptr[1][1]=’s’;

Un tablou de pointeri este un tablou ale cărui elemente sunt pointeri. Modul general de declarare a unui
tablou de pointeri:

tip  nume_tablou[dim];

Să considerăm exemplul în care se declară şi se iniţializează tabloul de pointeri str_ptr (figura 5.4.):

char  str_ptr[3] = { ”Programarea”, ”este”, ”frumoasă!” };

str_ptr
Deoarece operatorul de indexare [ ] are
str_ptr[0] prioritate mai mare decât operatorul de
”Programarea”
deferenţiere  , declaraţia char  str_ptr[3]
este echivalentă cu char  (str_ptr[3]), care
str_ptr[1] precizează că str_ptr este un vector de trei
”este”
elemente, fiecare element este pointer către
Figura 5.4. Tabloul de pointeri str_ptr caracter.
str_ptr[2]
”frumoasă! ”
În ceea ce priveşte declaraţia: char  (str_ptr[3]), se poate observa:

1. str_ptr[3] este de tipul char  (fiecare dintre cele trei elemente ale vectorului str_ptr[3] este de tipul
pointer către char);

4
2.  (str_ptr[3]) este de tip char (conţinutul locaţiei adresate de un element din str_ptr[3] este de tip
char).
Fiecare element (pointer) din str_ptr este iniţializat să pointeze către un şir de caractere constant. Fiecare
dintre aceste şiruri de caractere se găsesc în memorie la adresele memorate în elementele vectorului
str_ptr: str_ptr[0], str_ptr[1], etc.

Putem conculziona:

 str_ptr este un pointer către un pointer de caractere.


  str_ptr este pointer către char. Este evident, deoarece str_ptr[0] este pointer către char, iar
 str_ptr =  (str_ptr [0] + 0 )  str_ptr[0].

   str_ptr este un de tip char. Este evident, deoarece str_ptr[0][0] este de tip char, iar
  str_ptr=  (  str_ptr)   (str_ptr[0])=  (str_ptr[0]+0) 

str_ptr[0][0].

3) Definiţi şi exemplificaţi tipurile de date definite de utilizator „enumerare” şi „structură” în


limbajul în C++.
DATE ÎN LIMBAJUL C/C++

Aşa cum s-a văzut în capitolul 1, un program realizează o prelucrare de informaţie. Termenul de
prelucrare trebuie să fie considerat într-un sens foarte general (de exemplu, în programul prezentat în
paragraful 2.2., prelucrarea se referea la un text şi consta în afişarea lui). În program datele apar fie sub
forma unor constante (valori cunoscute anticipat, care nu se modifică), fie sub forma de variabile.
Constantele şi variabilele sunt obiectele informaţionale de bază manipulate într-un program.Fiecare
categorie de date este caracterizată de atributele:

 Nume;
 Valoare;
 Tip;
 Clasa de memorare.
De primele trei tipuri de atribute ne vom ocupa în continuare, urmând ca de atributul clasă de memorare
să ne ocupăm în paragraful 6.8.

Numele unei date Numele unei date este un identificator şi, ca urmare, trebuie să respecte regulile
specifice identificatorilor. Deasemenea, numărul de caractere care intră în compunerea unui identificator
este nelimitat, însă, implicit, numai primele 32 de caractere sunt luate în considerare. Aceasta înseamnă că
doi identificatori care au primele 32 de caractere identice, diferenţiindu-se prin caracterul 33, vor fi
consideraţi identici.

2.5.1. TIPURI DE DATE Tipul unei date constă într-o mulţime de valori pentru care s-a adoptat un
anumit mod de reprezentare în memoria calculatorului şi o mulţime de operatori care pot fi aplicaţi
acestor valori. Tipul unei date determină lungimea zonei de memorie ocupată de acea dată. În general,
lungimea zonei de memorare este dependentă de calculatorul pe care s-a implementat compilatorul.

5
Tabelul 2.1. prezintă lungimea zonei de memorie ocupată de fiecare tip de dată pentru compilatoarele sub
MS-DOS şi UNIX/LINUX.

Tipurile de bază sunt:

 char un singur octet (1 byte=16 biţi), capabil să conţină codul unui caracter din setul
local de caractere;

 int număr întreg, reflectă în mod tipic mărimea naturală din calculatorul utilizat;
 float număr real, în virgulă mobilă, simplă precizie;
 double număr real, în virgulă mobilă, dublă precizie.

În completare există un număr de calificatori, care se pot aplica tipurilor de bază char, int, float sau
double: short, long, signed şi unsigned. Astfel, se obţin tipurile derivate de date. Short şi long se referă la
mărimea diferită a întregilor, iar datele de tip unsigned int sunt întotdeauna pozitive. S-a intenţionat ca
short şi long să furnizeze diferite lungimi de întregi, int reflectând mărimea cea mai "naturală" pentru un
anumit calculator. Fiecare compilator este liber să interpreteze short şi long în mod adecvat propriului
hardware; în nici un caz, însă, short nu este mai lung decât long. Toţi aceşti calificatori pot aplicaţi tipului
int. Calificatorii signed (cel implicit) şi unsigned se aplică tipului char. Calificatorul long se aplică tipului
double. Dacă într-o declaraţie se omite tipul de bază, implicit, acesta va fi int.

Să considerăm, de exmplu, tipul int, folosit pentru date întregi (pozitive sau negative). Evident că
mulţimea valorilor pentru acest tip va fi, de fapt, o submulţime finită de numere întregi. Dacă pentru
memorarea unei date de tip int se folosesc 2 octeţi de memorie, atunci valoarea maximă pentru aceasta va
1 1
fi  2 16 - 1, deci 2 15 - 1 (32767), iar valoarea minimă va fi -  2 16 , deci -2 15 (-32768). Încercarea de a
2 2
calcula o expresie de tip int a cărei valoare se situează în afara acestui domeniu va conduce la o eroare de
execuţie.

Mulţimea valorilor pentru o dată de tip unsigned int (întreg fără semn) va fi formată din numerele întregi
situate în intervalul [0, 2 16 - 1].

În header-ul <values.h> sunt definite constantele simbolice (cum ar fi: MAXINT, MAXSHORT,
MAXLONG, MINDOUBLE, MINFLOAT, etc.) care au ca valoare limitele inferioară şi superioară ale
intervalului de valori pentru tipurile de date enumerate. (de exemplu MAXINT reprezintă valoarea
întregului maxim care se poate memora, etc. )

Fără a detalia foarte mult modul de reprezentare a datelor reale (de tip float sau double), vom sublinia
faptul că, pentru acestea, este importantă şi precizia de reprezentare. Deoarece calculatorul poate
reprezenta doar o submulţime finită de valori reale, în anumite cazuri, pot apare erori importante.

exponent
Numerele reale pot fi scrise sub forma: N = mantisa  baza

unde:baza reprezintă baza sistemului de numeraţie; mantisa (coeficientul) este un număr fracţionar
normalizat ( în faţa virgulei se află 0, iar prima cifră de după virgulă este diferită de zero); exponentul este
un număr întreg. Deoarece forma internă de reprezentare este binară, baza=2. În memorie vor fi

6
reprezentate doar mantisa şi exponentul. Numărul de cifre de după virgulă determină precizia de
exprimare a numărului. Ce alte cuvinte, pe un calculator cu o precizie de 6 cifre semnificative, două valori
reale care diferă la a 7-a cifră zecimală, vor avea aceeaşi reprezentare. Pentru datele de tip float, precizia
de reprezentare este 6; pentru cele de tip double, precizia este 14, iar pentru cele de tip long double,
precizia este 20.

Lungimea zonei de memorie ocupate de o dată de un anumit tip (pe câţi octeţi este memorată data) poate
fi aflată cu ajutorul operatorului sizeof.

Exemplu:

cout<<"Un int este memorat pe "<<sizeof(int)<<"octeti.\n";

Instrucţiunea are ca efect afişarea pe monitor a mesajului: Un int este memorat pe 2 octeţi.

4) Prezentaţi principiul căutării binare şi exemplificaţi în C++ aplicarea la o structură de tip tablou
unidimensional.
4.2. TABLOURI UNIDIMENSIONALE

Tablourile unidimensionale sunt tablouri cu un singur indice (vectori). Dacă tabloul conţine dim_1
elemente, indicii elementelor au valori întregi din intervalul [0, dim_1-1].

La întâlnirea declaraţiei unei variabile tablou, compilatorul alocă o zonă de memorie continuă (dată de
produsul dintre dimensiunea maximă şi numărul de octeţi corespunzător tipului tabloului) pentru păstrarea
valorilor elementelor sale. Numele tabloului poate fi utilizat în diferite expresii şi valoarea lui este chiar
adresa de început a zonei de memorie care i-a fost alocată. Un element al unui tablou poate fi utilizat ca
orice altă variabilă (în exemplul următor, atribuirea de valori elementelor tabloului vector). Se pot efectua
operaţii asupra fiecărui element al tabloului, nu asupra întregului tablou.

Exemplu:

// Declararea tabloului vector


vector
int vector[6];
100 vector[0]

101
// Iniţializarea elementelor tabloului
102
vector[1]
vector[0]=100; 103

vector[1]=101; 104 vector[2]


105
vector[2]=102;

Figura 4.1.vector[3]
7
vector[4]

vector[5]
vector[3]=103;

vector[4]=104;

vector[5]=105;

Variabilele tablou pot fi iniţializate în momentul declarării:

declaraţie_tablou=listă_valori;

Valorile din lista de valori sunt separate prin virgulă, iar întreaga listă este inclusă între acolade:

Exemple:

//1

int vector[6]={100,101,102,103,104,105};

vector 100 101 102 103 104 105

[0] [5]

//2

double x=9.8;

double a[5]={1.2, 3.5, x, x-1, 7.5};

La declararea unui vector cu iniţializarea elementelor sale, numărul maxim de elemente ale tabloului
poate fi omis, caz în care compilatorul determină automat mărimea tabloului, în funcţie de numărul
elementelor iniţializate.

Exemplu:

char tab[]={ ’A’, ’C’, ’D’, ’C’};

tab ’A’ ’B’ ’C’ ’D’

1
[0] [3]

float data[5]={ 1.2, 2.3, 3.4 };

8
data 1.2 2.3 3.4 ? ?

[0] [4]

Adresa elementului de indice i dintr-un tablou unidimensional poate fi calculată astfel:

adresa_elementului_i = adresa_de_bază + i  lungime_element

5) Implementaţi în C++ doi algoritmi de sortare a unui tablou de numere unidimensional, folosind
ca instrument de lucru pointeri.

6) Prezentaţi conceptul de recursivitate (directă şi indirectă) în programare şi explicaţi mecanismul


de realizare a recursivităţii. Exemplificări: tipuri de algoritmi recursivi.

7) Prezentaţi cadrul general de utilizare şi principiile metodei de programare „Backtracking”.


Prezentaţi algoritmul general „Backtracking” şi modul de particularizare pentru diverse probleme
concrete.

8) Prezentaţi şi exemplificaţi mecanismele de transfer ale parametrilor unei funcţii în C++.

9) Descrieţi structurile de date de tip stivă şi coadă şi implementaţi în C++ operaţiile caracteristice.

10) Descrieţi modalităţile de definire a metodelor unei clase de obiecte şi prezentaţi avantajele şi
dezavantajele fiecărei metode. Exemplificaţi modul de definire a funcţiilor „inline” în
implementarea claselor.
În general, pentru modelul de date orientat pe obiect, se consideră definitorii următoarele

concepte:

abstractizare, obiect, atribut, metodă, clasă, încapsulare, moştenire, polimorfism.

9
Abstractizarea

- procesul de simplificare a realităţii prin reţinerea caracteristicilor şi comportamentelor esenţiale şi


constituirea lor într-un model adecvat rezolvării problemelor.

- prin abstractizare se izolează aspectele esenţiale de cele neesenţiale, în funcţie de perspectiva de


abordare a problemei de rezolvat

- pentru o aceeaşi problemă, pot exista mai multe abstractizări, deci mai multe modele.

- Selectarea obiectelor abstracte şi a claselor este un punct important în realizarea unui program.

Obiectul

- o entitate individualizată prin nume

- conţine o mulţime de date descriu proprietăţile şi nivelul acestora

funcţii definesc comportamentul.

- obiectele pot fi clasificate în mulţimi. O mulţime de obiecte de acelaşi fel constituie o clasă de obiecte,
descrisă prin modelul comun al obiectelor sale.

Clasa

- grup de obiecte care fac parte din ea

- o clasă este o descriere a proprietăţilor şi comportamentului unui tip obiect; o clasă poate fi considerată
ca un tip special de dată, iar obiectele sale ca variabile de acest tip.

- pentru identificarea claselor unui sistem trebuie luate în consideraţie următoarele aspecte:

¤ Există informaţii care trebuiesc stocate sau analizate? Dacă da, ele sunt posibile candidate
pentru o clasă.

¤ Există sisteme externe cu care va comunica sistemul construit? Dacă da, este normal să fie luate
în considerare la modelarea sistemului.

¤Există biblioteci sau componente din proiecte anterioare care pot fi folosite? În caz afirmativ
acestea conţin clase candidate.

10
¤ Sunt device-uri în sistem care vor trebui gestionate? Orice device conectat la sistem înseamnă o
clasă candidat care sa-l gestioneze.

¤ Există părţi organizaţionale în sistem? Va fi nevoie de clase în care să fie implementate.

Definirea unei clase se realizează prin:

• precizarea operanzilor sau a atributelor;

• funcţiile membre;

• proprietăţile asociate componentelor pentru a stabili modalităţi de referire.

Obs:

- Atributele sunt de un anumit tip (de exemplu întregi, reale, caractere etc.).
- Setul de valori ale atributelor unui obiect la un moment dat formează starea curentă a obiectului
respectiv.
- Funcţiile care definesc comportamentul obiectelor sunt cunoscute ca metode ale clasei.
- Împreună, atributele şi metodele sunt membrii clasei, identificabili prin nume.

O clasă este un tip de dată definit de utilizator printr-o construcţie de forma:

Date sau atribute

Membrii

Funcţii sau metode

Aşa cum variabilele în programe sunt de tip global şi local sau după modul de alocare sunt statice şi
dinamice, analog, pentru a defini o anumită disciplină în manipularea claselor se asociază grupurilor de
elemente din clasă un specificator de control al cărui câmp de acţiune este anulat de un altul.

Funcţii inline alternativă la utilizarea excesivă a macro-definiţiilor din limbajul C.

Compilatorul înlocuieşte orice apel al unei funcţii inline cu codul acesteia. În felul acesta se elimină
operaţiile suplimentare de apel şi revenire din funcţie.

11
Utilizarea funcţiilor inline măreşte viteza de execuţie a programului prin evitarea operaţiilor pe stivă, ce
au loc în cazul apelurilor de funcţii obişnuite, dar plăteşte drept preţ pentru aceasta creşterea dimensiunii
codului executabil.

Declaraţia unei funcţii inline este formată din declaraţia unei funcţii obişnuite precedată de cuvântul cheie
inline:

Sintaxa pentru definiţia claselor permite declararea implicită a unei funcţii inline. O funcţie membră a
unei clase definită (nu doar declarată) în interiorul clasei este implicit funcţie inline:

Alt exemplu:

12
Metoda “tipareste” este declarată explicit, ea fiind doar declarată în cadrul clasei, iar în locul unde este
definită este prefixată de cuvântul cheie “inline”.

O funcţie membră definită în afara clasei este implicit o funcţie normală (nu este inline). Dar şi o astfel de
funcţie poate fi declarată explicit inline:
Fie exemplul următor:

În acest exemplu, funcţia set( ) din clasa Complex poate fi declaratã explicit inline :

inline void Complex::set(double x, double y){ … }

Obs:

1) Metodele care nu sunt membru al unei clase nu pot fi declarate inline decât explicit.

2)Funcţiile inline nu pot fi declarate funcţii externe, deci nu pot fi utilizate decât în modulul de program
în care au fost definite şi nu pot conţine instrucţiuni ciclice (while, for, do-while).

11) Prezentaţi specificatorii de control ai accesului la membrii claselor şi proprietăţile


acestora.

Specificatorii de control sunt:

• public – ceea ce înseamnă că elementul respectiv este accesibil oricărei clase externe;

• private – membrul este accesibil numai de funcţii din interiorul clasei sau de către funcţii prietene
(friend) ale clasei;

13
• protected – similar cu private dar accesul se extinde pentru funcţiile membre şi prietene derivate din
clasa respectivă.

Atunci, forma generală de declaraţie a unei clase devine:

Obs:

- Efectul unui specificator de acces dureaza pâna la urmatorul specificator de acces.

- Implicit, daca nu se declara nici un specificator de acces, datele sau functiile membre sunt de tip private.

- O funcţie membră a unei clase are acces la toate datele membre din clasa respectivă, indiferent de
specificatorul de acces.

- La proiectarea unei clase se au în vedere următoarele aspecte:

• funcţiile membru să acopere întreaga gamă de prelucrări;

• între variabilele şi funcţiile membre să fie o concordanţă perfectă pentru a nu apare erori în

execuţia programelor ce folosesc clase.

- Întrucât o clasă este un tip de dată, declararea unui obiect se face asemănător oricărei declaraţii de dată:

nume_clasă nume_obiect;

- Declararea unui obiect mai este numită şi instanţierea clasei, în sensul că se creează o instanţă a acelei
clase, o entitate concretă din mulţimea descrisă de clasa respectivă.

14
- Pentru a pune în evidenţă faptul că un membru aparţine unui obiect se utilizează formularea:
nume_obiect. nume_membru.

- O clasă conţine deci:

- o parte protejata/privata care asigura implementarea clasei;

- o parte publica care reprezintă interfaţa clasei respective cu celelalte secţiuni din program

Exemplul 1: pentru implementarea calculului cu numere raţionale, se defineşte clasa Rational:

#include<iostream.h>
main ( )
class rational
{
{

rational a;
int p,q;

public: a.nr (4 , 5);

void nr (int x=0 , int y=1) cout<<a.numarator( )<<a.numitor( );

{ }

p=x;

q=y;

int numarator ( )

return p;

int numitor ( )

15
return q;

};

Exemplul 2: pentru implementarea calculului cu numere complexe, se defineşte clasa Complex care
trebuie să aibă o structură de tip articol pentru a grupa partea reală şi partea imaginară a numărului
complex şi să conţină funcţii membre pentru operaţiile de adunare, scădere, înmulţire, împărţire, ridicare
la putere şi extragere de radical.

Obiect al clasei Complex

(ca şi variabilele, obiectele pot fi parametrii


pentru funcţii)
(componenta c.img a obiectului cpl)

16
Obs: Funcţiile vor fi descrise în cadrul clasei numai dacă au codul extrem de scurt şi nu conţin
instrucţiuni de ciclare. Altfel, clasa va conţine numai prototipul funcţiei.

Atunci când se descrie ulterior corpul unei metode (funcţii), pentru a specifica apartenenţa sa la clasa
respectivă, numele metodei este prefixat cu numele clasei din care face parte, folosind operatorul de
rezoluţie (::), astfel:

tip_rezultat nume_clasă :: nume_metodă(lista parametrilor)

corp metodă

12) Explicaţi care este scopul şi în ce constă supraîncărcarea operatorilor. Prezentaţi modul de
implementare în C++ a acestui mecanism.

Supraincarcarea operatorilor

Prin supraincarcarea operatorilor se extinde tipul obiectelor asupra carora se pot aplica acesti operatori.
Se pot supraincarca aproape toti operatorii in C++.
Operatorii care pot fi supradefiniti sunt:

Alte restrictii la supraincarcarea operatorilor:


- Se pot supraincarca doar operatorii existenti; nu se pot crea noi operatori.
- Nu se poate modifica numarul de operanzi ai operatorilor limbajului (operatorii unari nu pot fi
supraincarcati ca operatori binari, si invers).

17
- Nu se poate modifica precedenta si asociativitatea operatorilor.

Limbajul C++ permite supradefinirea operatorului op prin definirea unei functii numite operator op

Unde op este un operator ca : +, -, [] , & etc.

Supradefinirea se realizeaza astfel:

tip_val_intoarsa operator op (lista_declar_parametri)


{
// . . . . corpul functiei
}

Exemplul 1: Pentru clasa complex , putem atribui operatorului + semnificatia: expresia a+b (a, b sunt
obiecte din clasa complex) reprezinta "suma" a doua numere complexe si este un numar complex. Astfel,
supradefinirea operatorului + consta in definrea unei functii cu numele: operator +
Supraincarcarea operatorului +

#include<iostream.h>

#include<conio.h>

class complex

{public:

float x,y;

void display()

{cout<<x<<"+"<<y<<"*i";

cout<<endl;

complex operator+(complex z)

{complex s;

s.x=x+z.x;

s.y=y+z.y;

18
return s;

};

void main()

{clrscr();

complex a,b,aux;

cout<<"primul numar "<<endl;

cout<<"partea reala ";

cin>>a.x;

cout<<"partea imaginara ";

cin>>a.y;

cout<<"al doilea numar "<<endl;

cout<<"partea reala ";

cin>>b.x;

cout<<"partea imaginara ";

cin>>b.y;

aux=a+b;

cout<<endl<<"Suma celor doua numere "<<endl;

aux.display();

cout<<endl;

cout<<"primul numar"<<endl;

a.display();

cout<<endl<<"al doilea numar "<<endl;

b.display();

a=a+b;

cout<<endl<<"primul numar trece in suma celor doua:"<<endl;

19
a.display();

cout<<"cel de al doilea numar ramane neschimbat "<<endl;

b.display();

getch();

13)Definiţi conceptul de polimorfism şi prezentaţi procedeele de implementare a polimorfismului în


C++.

Polimorfism

Polimorfismul se referă la posibilitatea de a folosi acelaşi nume cu înţelesuri diferite. În C++,


polimorfismul poate fi realizat în mai multe moduri.

Polimorfismul funcţiilor reprezintă posibilitatea de a asocia acelaşi nume la diferite funcţii. Evitarea
ambiguităţii în procesul de referire se obţine prin:
• atribuirea de liste de parametrii de lungimi diferite;
• atribuirea de liste de parametrii cu aceeaşi lungime, dar parametrii corespunzători ca poziţie au tipuri
diferite;
În cazul funcţiilor, polimorfismul poartă şi numele de supraîncărcare.

Exemplul 1:
int unu (int i)
{return i;}
In acest caz functia unu ( )
int unu (int i, int j) este redefinita folosind un
{return i+j;} numar diferit de parametrii

Exemplul 2:
int doi (int i)
In acest caz functia doi ( )
{return i;}
int doi (float i) este supraincarcata folosind
{return floor(i);} tipuri diferite de parametrii.

20
Obs:
1) Nu pot fi supraincarcate doua functii care difera doar prin tipul rezultatului returnat
Deci urmatoarea "supraincarcare" a functiilor este gresita:
int trei (int i); float trei (int i);

2) Deoarece constructorul este si el o functie, poate fi supraincarcat:


complex ( ); //constructor
{real=0; imag=0;}
complex (float real, float imag) // constructor supraincarcat
{real=a; imag=b;}
Obs:
1) Supraincarcarea constructorilor ii face mai usor de utilizat in clase diferite. De aici rezulta flexibilitatea
limbajului C++.
2) De cate ori este definita o functie supraincarcata, ea trebuie declarata complet.
3) Importanta functiilor supraincarcate consta in aceea ca ele permit ca seturi de functii inrudite sa fie
accesibile printr-un singur nume. Datorita polimorfismului se pot declara mai multe "versiuni" ale unei
functii diferite prin numarul si/sau tipul parametrilor, eventual prin tipul rezultatului, iar compilatorul va
alege versiunea corecta ce urmeaza a fi utilizata.
4) La redefinirea functiilor trebuie sa se tina seama de utilizarea unor nume sugestive de functii, care au
sens in cadrul problemei, pentru a usura intelegerea codului de alti programatori.
5) Supraincarcarea functiilor se mai numeste si polimorfism functional si are rolul de a reduce numarul
denumirilor functiilor folosite.

O altă situaţie în care se pune problema polimorfismului o reprezintă suprascrierea metodelor.


Prin moştenire, clasa derivată preia metodele expuse de clasa părinte, conform cu regulile de derivare.
Însă, ea are posibilitatea să furnizeze o altă implementare pentru o metoda moştenită.
Practic, se defineşte o metodă în clasa derivată cu aceeaşi semnătură cu cea din clasa părinte, dar
care are o altă implementare. O operaţie a unei clase copil cu acelaşi prototip (signatură) cu cea a unei
operaţii a clasei părinte suprascrie operaţia clasei părinte.
În momentul execuţiei, metoda din clasa derivată este identificată şi executată înaintea metodei
din clasa părinte.

21
14) Definiţi conceptele de moştenire/derivare. Prezentaţi modul de construcţie a ierarhiilor de clase
în C++ şi restricţiile existente la moştenire.

Clase derivate. Moşteniri

Moştenirea reprezintă o relaţie între clase

elementul definitoriu al OOP.

Relaţia permite constituirea unei noi clase, numită derivată (sau fiu) pornind de la clase
existente, denumite de bază (sau părinte).

Moştenirea simplă - în procesul de construire participă o singură clasă


de bază
multiplă - în procesul de construire participă mai multe
clase de bază

Derivarea permite definirea într-un mod simplu, eficient şi flexibil a


unor clase noi prin adăugarea unor funcţionalităţi claselor deja existente,
fără să fie necesară reprogramarea sau recompilarea acestora.

Derivarea claselor este legată de implementarea conceptului de moştenire.

O clasă care asigură proprietăţi comune mai multor clase se defineşte ca o clasă de bază.
O clasă derivată moşteneşte de la una sau mai multe clase de bază toate caracteristicile acestora,
cărora le adaugă alte caracteristici noi, specifice ei.
Se spune că o clasă D moşteneşte o clasă A, dacă obiectele din clasa D conţin toate atributele
clasei A şi au acces la toate metodele acestei clase.
Dacă D moşteneşte A, atunci obiectele din D vor avea toate atributele şi acces la toate metodele
lui A, dar în plus: - D poate defini noi atribute şi metode;
- D poate redefini metode ale clasei de bază;

22
- metodele noi şi cele redefinite au acces la toate
atributele dobândite sau nou definite.
Deci prin derivare sunt construite clasele din aproape în aproape, organizându-se pe niveluri de
agregare.
Clasele agregate de pe nivelul k preiau operanzii şi funcţiile membre ale claselor de pe nivelul k-
1, care intră prin derivare în componenţa lor, precum şi proprietăţile acestora.
În limbajul C++ este permisă moştenirea multiplă. Pentru a defini o clasă fiu ca fiind derivată
dintr-o clasă părinte (sau mai multe clase părinte), se procedează astfel:

class nume_clasa_fiu : lista_clase_părinte


{ descriere membri noi ai clasei fiu};

În lista claselor părinte se specifică numele claselor părinte, separate prin virgulă şi, eventual,
precedate de modificatori de acces – se pot folosi modificatorii public sau private.
Obs:
1) Pentru fiecare clasă părinte se poate specifica un modificator de acces.
Dacă nu se specifică niciun modificator de acces atunci, implicit, se consideră modificatorul public.

2) O clasă derivată are acces la membrii clasei părinte care au fost definiţi ca fiind publici sau protejaţi şi
nu are acces la membrii privaţi. Prin derivare se construiesc ierarhii de clase, deci din clasa fiu se pot
deriva alte clase noi.

3) Întrucât ierarhiile de clase nu sunt definitive, ci oferă posibilitatea extinderii prin adăugarea de noi
clase derivate, se preferă ca la derivare să se folosească modificatorul de acces public. De aceea acesta
este modificatorul explicit.

15) Prezentaţi modificatorii de acces utilizaţi în cadrul unei relaţii de moştenire şi efectul acestora.
Moştenirea multiplă: Este posibilă moştenirea din mai multe clase de bază. De exemplu:

class X : public Y, public Z, ….public W {


// corpul clasei
};

23
Evident, specificatorii de acces pot să difere (pot fi oricare din public, private, protected).
O clasă de bază se numeşte bază directă dacă ea este menţionată în lista de clase de bază.
O clasă de bază se numeşte bază indirectă dacă nu este bază directă dar este clasă de bază pentru
una din clasele menţionate în lista claselor de bază.
Într-o clasă derivată se moştenesc atât membrii bazelor directe cât şi membrii bazelor indirecte.
De exemplu, se completează programul de descriere a organizării personalului unei instituţii şi
pentru alte categorii de personal (personal temporar, consultant, secretară, director), prin adăugarea câte
unei clase pentru fiecare categorie de personal.
Modul în care aceste clase se derivează din anumite clase de bază evidenţiază relaţiile de ierarhie
între categoriile pe care le reprezintă:
class Personal { /* … */ };
class Angajat : public Personal{ /* …*/ };
class Administrator : public Angajat { /* … */ };
class Director : public Administrator { /* … */ };
class Secretara : public Angajat { /* … */ };
class Temporar : public Personal { /* … */};
class Consultant : public Temporar, public Administrator
{ /* … */ };

Această organizare a claselor se poate descrie printr-un graf aciclic direcţionat astfel:

Clasa (nodul) din vârful ierarhiei reprezintă categoria cea mai generală a ierarhiei, iar toate
celelalte moştenesc, direct sau indirect, din această clasă de bază, fiind clase mai specializate.

Controlul accesului la membrii clasei de bază

24
Accesul la membrii clasei de bază moşteniţi în clasa derivată este controlat de specificatorul de
acces (public, protected, private) din declaraţia clasei derivate.

O regulă generală este că, indiferent de specificatorul de acces declarat la derivare, datele de tip
private în clasa de bază nu pot fi accesate dintr-o clasă derivată. O altă regulă generală este că, prin
derivare, nu se modifică tipul datelor în clasa de bază.

Moştenirea de tip public a clasei de bază

Dacă specificatorul de acces din declaraţia unei clase derivate este public, atunci:
• Datele de tip public ale clasei de bază sunt moştenite ca date de tip public în clasa derivată şi
deci pot fi accesate din orice punct al domeniului de definiţie al clasei derivate.
• Datele de tip protected în clasa de bază sunt moştenite protected în clasa derivată, deci pot fi
accesate numai de funcţiile membre şi friend ale clasei derivate.

Exemplu:
Diverse situaţii de acces la membrii clasei de bază din clasa derivată atunci când specificatorul de
acces este public:
class Base {
int a;
protected:
int b;
public:
int c;
void seta(int x){a = x; cout << "seta din baza\n";}
void setb(int y){b = y; cout << "setb din baza\n";}
void setc(int z){c = z; cout << "setc din baza\n";}
};
class Derived : public Base {
int d;
public:
void seta(int x) {
a = x; // eroare, a este private
}
void setb(int y) {
b = y;
cout << "setb din derivata\n";
}
void setc(int z) {
c = z;
}
};
}

25
Moştenirea de tip protected a clasei de bază

Dacă specificatorul de acces din declaraţia clasei derivate este protected, atunci toţi membrii de
tip public şi protected din clasa de bază devin membri protected în clasa derivată.
Bineînţeles, membrii de tip private în clasa de bază nu pot fi accesaţi din clasa derivată.

Moştenirea de tip private a clasei de bază

Dacă specificatorul de acces din declaraţia clasei derivate este private, atunci toţi membrii de tip
public şi protected din clasa de bază devin membri de tip private în clasa derivată şi pot fi accesaţi numai
din funcţiile membre şi friend ale clasei derivate.
Bineînţeles, membrii de tip private în clasa de bază nu pot fi accesaţi din clasa derivată.

Moştenirea multiplă:

O clasă poate avea mai multe clase de bază directe, dacă acestea sunt specificate în declaraţia
clasei.

În exemplul următor este prezentată clasa Derived care moşteneşte două clase de bază, Base1 şi
Base2.

class Base1 {
protected:
int x;
public:
Base1(int i) {
cout << "Constructor baza 1\n"; x = i;
}
~Base1() { cout <<"Destructor baza 1\n"; }
int getx(){return x;}
};
class Base2{
protected:
int y;
public:
Base2(int j){
cout << "Constructor baza 2\n"; y = j;
}
int gety() { return y;}
~Base2() { cout <<"Destructor baza 2\n"; }
};
class Derived : public Base1, public Base2 {

26
int d;
public:
Derived (int i, int j);
~Derived(){ cout << "Destructor derivata\n"; }
};
Derived::Derived(int i, int j): Base1(i), Base2(j){
cout << "Constructor derivata\n";
}
void fbm(){ În funcţia fbm() este creat
Derived obd(3,4);
cout << obd.getx() << " "<< obd.gety() << endl; obiectul obd de clasă
}
Derived.

Constructorul clasei Derived transferă câte un argument constructorilor fiecărei clase de bază,
care iniţializează datele membre x şi y.
La execuţia funcţiei fbm() se afişează următoarele mesaje, care indică ordinea în care sunt apelaţi
constructorii şi destructorii clasei derivate şi ai claselor de bază:
Constructor baza 1
Constructor baza 2
Constructor derivata
3 4
Destructor derivata
Destructor baza 2
Destructor baza 1
Avantajele folosirii moştenirii:

• reutilizabilitatea – când o funcţionalitate este moştenită din altă clasă, codul respectiv nu trebuie
rescris, el trebuie doar apelat în noul context; o altă implicaţie este legată de fiabilitatea codului, deoarece
prin moştenire, o anumită funcţionalitate este scrisă doar la nivelul unei clase şi apoi moştenită şi utilizată
în toate clasele derivate;
• consistenţa interfeţei – când două sau mai multe clase sunt derivate din aceeaşi clasă părinte, se asigură
faptul că comportamentul moştenit este acelaşi pentru toate clasele;
• componentele software – moştenirea dă posibilitatea programatorilor să construiască componente
software reutilizabile şi gruparea lor în biblioteci; în acest fel, efortul de dezvoltare al unui produs nou
este diminuat prin utilizarea de librării cu funcţionalitate deja implementată;
• dezvoltarea rapidă de prototipuri – atunci când sistemul software este construit folosindu-se
componente reutilizabile, timpul de dezvoltare este concentrat pe înţelegerea elementelor specifice ale
sistemului; astfel se construiesc versiuni de sistem, numite prototipuri, care pun accent pe aspectele critice
ale sistemului.
Un prototip este dezvoltat, utilizatorii îl folosesc, iar a doua versiune a sistemului este realizată pe baza
experienţei acumulată cu prima şi a feedbackului de la utilizatori.

27
Deşi moştenirea prezintă foarte multe avantaje, există şi o serie de costuri de care trebuie să se
ţină seama în momentul proiectării ierarhiilor de clase:
• viteza de execuţie – este influenţată prin prisma faptului că metodele moştenite, care au un caracter mai
general, sunt de regulă mai lente decât codul specializat; însă afectarea vitezei de execuţie este
compensată cu creşterea vitezei de dezvoltare;
• dimensiunea programelor – este influenţată în sensul că devine mai mare în cazul programelor care
folosesc librării de componente, decât programele care folosesc cod specializat, deoarece nu toate
componentele dintr-o librărie sunt folosite în proiect, dar librăria trebuie adăugată în totalitatea sa;
• complexitatea programelor – o ierarhie de clase introduce un anumit grad de complexitate în sistem;
cu cât ierarhia este mai mare, nivelurile de abstractizare mai multe, cu atât sistemul care foloseşte această
ierarhie este mai complex.

28

S-ar putea să vă placă și