Sunteți pe pagina 1din 226

Metode evoluate

de programare

Limbajele C şi C++

3
Cuprins
Cuprins...........................................................................................................................4
1. Structura generală a unui program C..................................................................9
1.1. Istoric, concepţie , evoluţie...........................................................................................9
1.2. Conceptul de funcţie....................................................................................................10
1.2.1. Definiţia unei funcţii.............................................................................................11
1.2.2. Antet şi prototip.....................................................................................................11
1.2.3. Corpul unei funcţii................................................................................................11
1.3. Construcţiile de bază ale limbajului..........................................................................12
1.3.1. Caractere.................................................................................................................12
1.3.2. Nume.......................................................................................................................12
1.3.3. Cuvinte cheie..........................................................................................................13
1.3.4. Tipuri de bază.......................................................................................................13
1.3.5. Constante.................................................................................................................13
1.3.6. Variabile simple.....................................................................................................15
1.3.7. Tablouri....................................................................................................................15
1.3.8. Comentariu...............................................................................................................16
1.4. Preprocesare..................................................................................................................17
1.4.1. Includerea unui fişier sursă..................................................................................17
1.4.2. Constante simbolice...............................................................................................18
2. Clase de variabile (de memorie)........................................................................19
2.1. Variabile globale..........................................................................................................19
2.2. Variabile locale.............................................................................................................20
2.3. Variabile registru.........................................................................................................21
2.4. Iniţializare......................................................................................................................22
3. Expresii, operanzi, operatori..................................................................................24
3.1. Expresii..........................................................................................................................24
3.2. Operanzi.........................................................................................................................24
3.3. Operatori........................................................................................................................24
3.3.1. Operatori aritmetici................................................................................................25
3.3.2. Operatori relaţionali...............................................................................................25
3.3.3. Operatori de egalitate............................................................................................26
3.3.4. Operatori logici......................................................................................................26
3.3.5. Operatori logici pe biţi.........................................................................................27
3.3.6. Operatori de atribuire............................................................................................28
3.3.7. Operatori de incrementare şi decrementare........................................................29
3.3.8. Operatorul de conversie explicită (expresie cast)................................................30
3.3.9. Operatorul dimensiune (sizeof).............................................................................30
3.3.10. Regula conversiilor implicite...............................................................................31
3.3.11. Operatori condiţionali..........................................................................................32
3.3.12. Operatorul virgulă.................................................................................................32
4. Intrări / ieşiri standard.............................................................................................33
4.1. Funcţia standard printf................................................................................................33
4.2. Funcţia standard scanf................................................................................................35
4.3. Funcţia standard putchar.............................................................................................38
4.4. Funcţia standard getchar..............................................................................................38
4.5. Funcţiile standard getche şi getch.............................................................................39
5. Instrucţiuni...............................................................................................................40
5.1. Scurt istoric al metodelor de programare.................................................................40

4
5.1.1. Programare artizanală............................................................................................40
5.1.2. Programare procedurală.........................................................................................40
5.1.3. Programare modulară............................................................................................41
5.1.4. Programare structurată..........................................................................................41
5.1.5. Programare prin abstractizarea datelor................................................................42
5.1.6. Programarea orientată spre obiecte......................................................................43
5.2. Instrucţiunea vidă..........................................................................................................44
5.3. Instrucţiunea expresie...................................................................................................44
5.4. Instrucţiunea compusă..................................................................................................44
5.5. Instrucţiunea if..............................................................................................................45
5.6. Instrucţiunea while.......................................................................................................46
5.7. Instrucţiunea for...........................................................................................................47
5.8. Instrucţiunea do while................................................................................................48
5.9. Instructiunea switch.....................................................................................................49
5.10. Instrucţiunea break....................................................................................................51
5.11. Instrucţiunea continue................................................................................................51
5.12. Instrucţiunea goto.......................................................................................................51
5.13. Apelul şi revenirea dintr-o funcţie..........................................................................52
5.13.1. Apelul unei funcţii..............................................................................................52
5.13.2. Prototipul unei funcţii.........................................................................................52
5.13.3. Apel prin valoare şi apel prin referinţă...........................................................53
5.13.4. Revenirea dintr-o funcţie....................................................................................53
6. Pointeri.....................................................................................................................55
6.1. Declaraţia de pointer...................................................................................................55
6.2. Legătura dintre pointeri şi tablouri............................................................................56
6.3. Operaţii cu pointeri......................................................................................................57
6.3.1. Incrementare şi decrementare...............................................................................57
6.3.2. Adunarea şi scăderea unui întreg dintr-un pointer............................................57
6.3.3. Compararea a doi pointeri....................................................................................58
6.3.4. Diferenţa a doi pointeri........................................................................................58
6.3.5. Exemple..................................................................................................................59
6.4. Alocarea dinamică a memoriei...................................................................................59
6.5. Pointeri spre funcţii.....................................................................................................61
6.6. Tratarea parametrilor din linia de comandă.............................................................63
6.7. Modificatorul const......................................................................................................64
6.8. Stiva...............................................................................................................................65
7. Recursivitate.............................................................................................................67
8. Structuri, tipuri utilizator.......................................................................................72
8.1. Declaraţia de structură.................................................................................................72
8.2. Accesul la elementele unei structuri..........................................................................74
8.3. Atribuiri de nume pentru tipuri de date....................................................................75
8.4. Uniune...........................................................................................................................77
8.5. Câmp...............................................................................................................................81
8.6. Tipul enumerat.............................................................................................................83
9. Liste..........................................................................................................................85
9.1. Date structurate definite recursiv............................................................................85
9.2. Liste înlănţuite..............................................................................................................85
9.3. Lista liniară simplu înlănţuită.....................................................................................86
9.4. Crearea şi afişarea unei liste.......................................................................................87
10. Prelucrarea fişierelor.............................................................................................89
10.1. Fişiere..........................................................................................................................89
5
10.2. Nivelul inferior de prelucrare al fişierelor.............................................................89
10.2.1. Deschiderea unui fişier.......................................................................................90
10.2.2. Citirea dintr-un fişier (consultare).....................................................................91
10.2.3. Scrierea într-un fişier (creare, actualizare, adăugare)......................................91
10.2.4. Poziţionarea într-un fişier...................................................................................91
10.2.5. Închiderea unui fişier..........................................................................................92
10.3. Nivelul superior de prelucrare a fişierelor.............................................................93
10.3.1. Deschiderea unui fişier.........................................................................................93
10.3.2. Prelucrarea pe caractere a unui fişier...............................................................94
10.3.3. Închiderea unui fişier..........................................................................................94
10.3.4. Operaţiile de intrare-ieşire cu format.................................................................95
10.3.5. Intrări-ieşiri de şiruri de caractere.....................................................................96
10.3.6. Poziţionarea într-un fişier...................................................................................97
10.3.7. Prelucrarea fişierelor binare................................................................................97
10.4. Ştergerea unui fişier..................................................................................................99
11. Funcţii standard...................................................................................................100
11.1. Macrouri de clasificare...........................................................................................100
11.2. Macrouri de transformare a simbolurilor..............................................................101
11.3. Conversii....................................................................................................................102
11.4. Funcţii de prelucrare a şirurilor de caractere......................................................103
11.5. Funcţii de calcul......................................................................................................104
11.6. Funcţii pentru controlul proceselor.........................................................................105
11.7. Funcţii pentru gestiunea datei şi orei..................................................................105
11.8. Alte funcţii diverse de uz general.........................................................................106
12. Gestiunea ecranului în mod text......................................................................107
12.1. Setarea ecranului în mod text................................................................................108
12.2. Definirea unei ferestre............................................................................................108
12.3. Ştergerea unei ferestre.............................................................................................109
12.4. Gestiunea cursorului.................................................................................................109
12.5. Determinarea parametrilor ecranului......................................................................110
12.6. Modurile video alb/negru........................................................................................110
12.7. Setarea culorilor.......................................................................................................111
12.8. Gestiunea textelor.....................................................................................................111
13. Gestiunea ecranului în mod grafic...................................................................114
13.1. Setarea modului grafic............................................................................................114
13.2. Gestiunea culorilor....................................................................................................116
13.3. Starea ecranului..........................................................................................................118
14. Probleme diverse.................................................................................................120
14.1. Generarea combinărilor.............................................................................................120
14.2. Metoda greedy.........................................................................................................121
14.3. Metoda backtracking (căutare cu revenire).............................................................123
14.4. Metoda divide et impera (divide şi stăpâneşte)...................................................128
14.5. Metoda programării dinamice..................................................................................130
15. Facilităţi noi în limbajul C++..............................................................................135
15.1. Extinderea limbajului C.........................................................................................135
15.2. Operatori.................................................................................................................138
15.2.1. Operatorul de rezoluţie.........................................................................................138
15.2.2. Folosirea operatorului adresă pentru definirea tipului referinţă..........................138
15.2.3. Apelul prin referinţă.............................................................................................140
15.2.4. Operatori pentru alocare şi dezalocare dinamică a memoriei..............................141
15.3. Structuri, reuniuni şi tipuri enumerare...................................................................142
6
15.4. Funcţii.....................................................................................................................143
15.4.1. Iniţializarea parametrilor formali ai funcţiilor.....................................................143
15.4.2. Funcţii care returnează tipuri referinţă.................................................................146
15.4.3. Supraîncărcarea funcţiilor....................................................................................147
15.4.4. Funcţii inline........................................................................................................149
16. Noţiunea de clasă.................................................................................................153
16.1. Realizarea protecţiei datelor prin metoda programării modulare..........................153
16.2. Tipuri abstracte de date..........................................................................................154
16.3. Declararea claselor.................................................................................................155
16.4. Referirea la elementele claselor. Pointerul this......................................................156
16.5. Constructorul..........................................................................................................158
16.5.1. Iniţializarea obiectelor prin constructor...............................................................158
16.5.2 Apelarea constructorilor în cazul în care datele membru sunt obiecte...............159
16.6. Destructorul............................................................................................................161
16.7. Supraîncărcarea operatorilor..................................................................................162
16.7.1. Metoda generală de supraîncărcare......................................................................162
16.7.2. Supraîncărcarea operatorilor de atribuire.............................................................163
16.7.3. Supraîncărcarea operatorilor de incrementare şi decrementare...........................170
16.8. Conversii definite de programator.........................................................................172
16.8.1. Efectuarea conversiilor........................................................................................172
16.8.2. Conversii implicite definite de programator........................................................172
16.8.3. Supraîncărcarea operatorului de conversie explicită...........................................177
17. Metoda programării orientate obiect...................................................................181
17.1. Bazele teoretice ale metodei programării orientate obiect....................................181
17.2. Declararea claselor derivate...................................................................................182
17.3. Funcţii membru virtuale.........................................................................................183
17.4. Clase virtuale..........................................................................................................186
17.5. Clase abstracte. Funcţia membru virtuală pură......................................................191
18. Ierarhii de clase pentru operaţii de intrare/ieşire.................................................194
18.1. Streamuri................................................................................................................194
18.2. Ieşiri formatate.......................................................................................................195
18.2.1. Operatorul de inserare..........................................................................................195
18.2.2. Funcţia membru setf.............................................................................................197
18.2.3. Funcţiile membru width, fill şi precision.............................................................200
18.2.4. Manipulatori.........................................................................................................204
18.2.5. Supraîncărcarea operatorului de inserare.............................................................205
18.3. Intrări formatate.....................................................................................................207
18.3.1. Operatorul de extragere........................................................................................207
18.3.2. Starea de eroare....................................................................................................211
18.3.3. Supraîncărarea operatorului de extragere............................................................217
19. Anexă...................................................................................................................218
19.1. Un memento al sintaxei limbajului C....................................................................218
19.2. Lista programelor C++...........................................................................................223
20. Bibliografie:.........................................................................................................224

7
PARTEA I

Limbajul C

8
1. Structura generală a unui program C
1.1. Istoric, concepţie , evoluţie
Limbajul C a fost finalizat în 1972 de Dennis M. Ritchie şi Brian W. Kernighan de
la firma americană Bell Laboratories. Prima versiune a limbajului se numeşte BCPL apoi
următoarele poartă numele de A, B şi C. Cei doi autori au dezvoltat aceste prime versiuni în
jurul sistemului de operare UNIX. La vremea respectivă din aproximativ 13000 linii sursă ale
UNIX-ului doar 200 de linii sursă nu erau scrise în limbajul C. De acest fapt se leagă
detractorii limbajului care spun că limbajul C nu este un limbaj deosebit ci doar un fel de
limbaj “oficial” al sistemului de operare UNIX.
În anul 1978 apare manualul The C Programming Language care este de fapt şi
prima standardizare a limbajului. Cei doi autori intră astfel în istorie...
După anul 1980 odată cu dezvoltarea hardware apar şi primele PC-uri iar acestea
implică şi produse software adecvate. Principalele firme producătoare de sofware -
MICROSOFT şi BORLAND - au dezvoltat unelte adecvate pentru programarea şi
utilizarea limbajului C. Deocamdată firma BORLAND deţine supremaţia prin versiunile
mediului BORLAND C. Cele mai folosite sunt versiunile 2.0, 3.1, 4.0. În ultimii doi ani au
apărut aşa numitele medii “visuale”: VISUAL C versiunile 4.5 şi 5.0, C++ BUILDER care
sunt de fapt extensii ale mediului BORLAND C adaptate programării orientate obiect şi
interfeţei grafice WINDOWS 95. Mediile de programare BORLAND C pot compila 3
tipuri de programe sursă C:
- fişiere cu extensia .C (fişiere cu programe standard C);
- fişiere cu extensia .CP (fişiere cu programe C+, un C extins);
- fişiere cu extensia .CPP (fişiere cu programe C++).
Menţionăm că limbajul C++ a fost elaborat de Bjarne Stroustrup de la AT&T. El
este un superset al limbajului C şi permite principalele concepte ale programării prin
abstractizarea datelor şi programării orientate spre obiecte.
Limbajul C este un limbaj hibrid având facilităţi caracteristice limbajelor de
asamblare cât şi facilităţi ale limbajelor de înalt nivel.

Câteva dintre principalele caracteristici ale limbajului C sunt:

a) portabilitate: chiar dacă acest concept nu-i definit foarte riguros spunem că
că un program este portabil dacă el poate fi transferat uşor de la
un tip de calculator la altul; limbajul C este un astfel de limbaj;
b) flexibilitate: compilatorul face un număr mai redus de controale (face multe
conversii implicite);
c) programare structurată: limbajul are principalele structuri ale programării structurate:
structura secvenţială, structura iterativă şi structura de selecţie;
d) compactizare: unele instrucţiuni sunt scrise foarte compact; de exemplu i:=i+1
se poate scrie mai scurt ca i++;
e) lucrul pe biţi şi calcule cu adrese.

9
1.2. Conceptul de funcţie
Un program C se compune din una sau mai multe funcţii. Funcţia este o unitate lexicală
de program compilabilă independent. Una dintre funcţii este funcţie principală, numele ei este
predefinit şi anume main. Execuţia programului începe cu prima instrucţiune din funcţia
principală. Dăm în continuare 2 exemple de structuri de program (fişiere sursă):

1) directive de preprocesare 2) directive de preprocesare

declaraţii de date globale declaraţii de date globale

declaraţie prototip funcţia f1 implementare funcţia f1


... ...
declaraţie prototip funcţia fn implementare funcţia fn

void main(void) void main(void)


{ declaraţii { declaraţii
instrucţiuni instrucţiuni
} }

implementare funcţia f1
...
implementare funcţia fn

Funcţia principală main este obligatorie pentru orice program celelalte elemente fiind
optionale. Pentru ca o anumită funcţie să poată fi apelată e necesar ca ea să aibă declarat
prototipul dacă implementarea (definiţia) ei se află după funcţia main (exemplul 1). Dacă
funcţia principală se află la sfârşitul fişierului atunci nu mai e necesar prototipul funcţiei
apelate ci doar implementarea ei (exemplul 2). Comparând structura unui program C cu
structura unui program PASCAL se observă nivelul de imbricare diferit. În PASCAL apare o
imbricare a procedurilor şi funcţiilor pe când în C nu există o astfel de imbricare.

PASCAL C

...

1.2.1. Definiţia unei funcţii

10
Definiţia unei funcţii în limbajul C se compune din antet şi corp. O funcţie poate fi apelată
dacă este precedată de definiţia sau de prototipul ei. Aceste lucruri care sunt valabile în
limbajul C se regăsesc şi în limbajul C++.

1.2.2. Antet şi prototip

Antetul simplificat al unei funcţii în C are formatul:

tip nume_funcţie (lista_parametrilor_formali)

unde: tip - reprezintă tipul valorii returnate de funcţie sau dacă funcţia nu returnează nici
o valoare se pune cuvântul cheie void;
nume_funcţie - reprezintă un identificator clasic format dintr-un mixaj de litere
şi cifre, primul caracter fiind obligatoriu literă;
printre litere se numără şi liniuţa de subliniere(‘_’);
lista_parametrilor_formali – nume de variabile sau expresii separate prin virgule.

Exemple:

1) double radical (double x) // calculeaza radacina patrata din x si returneaza valoarea gasita
2) double radical_n (double x, int n) // calculeaza radacina de ordinul n din x

Prototipul unei funcţii este asemănător antetului dar la sfârşit se pune caracterul “;”

1.2.3. Corpul unei funcţii

Corpul unei funcţii C se compune din declaraţii de variabile locale şi instrucţiuni


scrise între acolade conform figurii următoare:

{ declaraţii
instrucţiuni
}
Şi pentru că autorii limbajului C consideră că un limbaj de programare se învaţă mai
repede scriind şi executând programe cât mai timpuriu vom da un mic exemplu de funcţie.

int modul (int i) // determina valoarea absoluta a intregului i si returneaza aceasta valoare
{ if (i < 0) return – i;
if (i = = 0) return 0;
else return i;
}

sau scris şi altfel


int modul (int I) // determina valoarea absoluta a intregului i si returneaza aceasta valoare
{ if(i)
if (i > 0) return i;
else return – i;
else return 0;
}
1.3. Construcţiile de bază ale limbajului
1.3.1. Caractere

11
Limbajul C foloseşte setul de caractere al codului ASCII care se codifică prin numere
întregi din intervalul [0,127], adică 128 de coduri. Un astfel de întreg se păstrează pe un
BYTE (OCTET) adică pe 8 biţi.
Mulţimea caracterelor se împarte în trei grupe:
- caractere negrafice (coduri cuprinse între 00=NUL şi 31 precum şi 127=DEL);
- spaţiu (codul 32);
- caractere grafice (coduri cuprinse între 33 şi 126).

Caracterele grafice se împart la rândul lor în:


- litere mari (coduri între 65 şi 90);
- litere mici (coduri între 97 şi 122);
- cifre (coduri între 48 şi 59);
- caractere speciale (celelalte coduri).

Caracterele negrafice au diferite funcţii. Astfel codul 10 semnifică LF (Line Feed),


adică deplasează cursorul pe linia următoare în coloana 1, iar codul 13 semnifică CR
(Carriage Return) adică deplasează cursorul în coloana 1 aceeaşi linie. În limbajul C
combinaţia celor două caractere se notează prin:
\n
şi are semnificaţia trecerii cursorului la linie nouă şi coloana 1 (newline).
Tabulatorul orizontal (deplasarea cursorului peste un anumit număr de poziţii) se precizează
prin notaţia:
\t
Să mai precizăm că următoarele 3 caractere: spaţiu, newline şi tabulator orizontal se mai
numesc spaţii albe (white spaces).

1.3.2. Nume

În limbajul C, un nume este o succesiune de litere şi eventual cifre, care începe cu o


literă, deci un nume este un identificator. Ca lungime un nume poate fi oricât de lung dar
numai primele 32 de caractere se iau în considerare. Folosind notaţia BNF (vezi anexa) un
nume se poate defini recursiv, astfel:

<nume> :: = <litera> | <nume><litera> | <nume><cifra>


<litera> :: = A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Z|
a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|z|_
<cifra> :: = 0|1|2|3|4|5|6|7|8|9|

Numele se folosesc pentru denumirea variabilelor, tablourilor, funcţiilor, etc.

Exemple:
A, _start, a_, matrice, matrice_patratica.
Dăm şi câteva contraxemple:
&B - conţine caracterul &;
x+y - conţine caracterul +;
1.3.3. Cuvinte cheie

Un cuvânt cheie este un cuvânt împrumutat din limba engleză, care are un înţeles
predefinit. Aceste cuvinte se scriu cu litere mici. Un cuvânt cheie nu poate avea altă utilizare
într-un program C decât cea care i-a fost predefinită. Fiind o succesiune de litere, un cuvânt

12
cheie este un nume. Lista cuvintelor cheie se dă în anexa de la sfârşitul cărţii. Sensul fiecărui
cuvânt cheie va rezulta la definirea construcţiei în care se utilizează.
Exemple:
for, do, if, while, else, break, return, int, long, double, static, extern.

1.3.4. Tipuri de bază

În limbajul C distingem câteva tipuri predefinte de date care se mai numesc tipuri
de bază. În general un tip de dată are trei atribute bine conturate:
- mulţimea valorilor;
- reprezentarea (internă şi externă);
- comportamentul (adică operaţiile ce se pot face cu acel tip).
În tabelul de mai jos dăm cuvântul cheie, mulţimea valorilor, reprezentarea internă.

Reprezentarea internă
Cuvânt cheie Mulţimea valorilor Lungime (biţi) Formatul intern

int întregii din [-215; 215-1] 16 reprezentare binară


short întregii din [-215; 215-1] 16 reprezentare binară
long întregii din [-231; 231 - 1] 32 reprezentare binară
unsigned întregii din [0; 216-1] 16 reprezentare binară
char 8 cod ASCII
float [-3.4*10-38; 3.4*1038] 32 virgulă flotantă simplă precizie
double [-1.7*10-308; 1.7*10308] 64 virgulă flotantă dublă precizie

Facem precizarea că se poate folosi şi combinaţia unsigned long pentru întregii fără
semn în dublă precizie.
Cu tipurile de bază se pot defini şi tipuri utilizator folosind instrucţiunea struct.

1.3.5. Constante

O constantă are un tip şi o valoare. Atât tipul cât şi valoarea unei constante se
definesc prin caracterele care compun constanta respectivă.

Constantă întreagă zecimală


O constantă întreagă este un şir de cifre care eventual este prefixat de un semn (+ sau -) şi
postfixat de litera L sau l. O constantă întreagă se reprezintă în binar pe 16 sau 32 de biţi.
Sufixul L sau l forţează reprezentarea pe 32 de biţi.

Constantă întreagă octală


O constantă întreagă octală este un şir de cifre octale (cifre cuprinse între 0 şi 7) precedat de
un zero nesemnificativ.

Constantă întreagă hexazecimală


O constantă întreagă hexazecimală este un şir de cifre hexazecimale prefixat cu 0x sau 0X.

Exemple:

Reprezentare Interpretare
13
12345 întreg zecimal reprezentat în binar pe 16 biţi
-12345 întreg zecimal reprezentat în binar pe 16 biţi
12345L întreg zecimal reprezentat în binar pe 32 biţi
012345 întreg octal reprezentat în binar pe 16 biţi (o cifră octală pe 3 biţi)
0xabcd întreg hexa reprezentat în binar pe 16 biţi (o cifră pe 4 biţi)
12345678 întreg zecimal reprezentat pe 32 de biţi

Constantă flotantă
O constantă flotantă este un număr raţional care se compune din următoarele elemente:
- un semn (+ sau -) care poate lipsi pentru numerele pozitive;
- o parte întreagă care poate fi şi vidă;
- o parte fracţionară care poate fi şi vidă;
- un exponent care poate fi şi vid.
Prezenţa părţii fracţionare este suficientă pentru ca numărul respectiv să reprezinte o
constantă flotantă. Absenţa părţii fracţionare implică prezenţa părţii întregi şi a exponentului.
Partea întreagă reprezintă o succesiune de cifre zecimale. Partea fracţionară se
compune din caracterul punct urmat de o succesiune de cifre zecimale, care poate fi şi vidă.
Exponentul se compune din litera e sau E urmată de un + sau -, opţional, şi un şir de cifre
zecimale.
Constantele flotante se păstrează în format flotant dublă precizie.
Exemple:
3.14; 0.314e1; 3.1415926; 2.71828; 2.; 271828E-5.

Constantă caracter
O constantă caracter reprezintă un caracter şi are ca valoare codul ASCII al
caracterului respectiv. O constantă caracter grafic se poate scrie incluzând caracterul
respectiv între caractere apostrof.

Exemple:

Constantă caracter valoare cod ASCII

‘A’ 65
‘B’ 66
‘a’ 97
‘0’ 48
‘9’ 58
‘*’ 77

Anumite caractere negrafice au notaţii speciale la scrierea cărora se utilizează


caracterul backslash (\). Dăm în continuare un tabel cu cele mai folosite caractere negrafice:

Caracter Reprezentare ca şi constantă caracter Valoare cod ASCII

Backspace ‘\b’ 8
Retur de car ‘\r’ 13
Newline ‘\n’ 10
14
Apostrof ‘\’’ 39
Backslash ‘\\’ 92
Tabulator vertical ‘\v’ 11
Salt pagină imprimantă ‘\f’ 12
Carcterul NUL ‘\0’ 0

Constantă şir de caractere


O constantă şir de caractere este o succesiune de zero sau mai multe caractere delimitate
prin ghilimele. Ghilimelele nu fac parte din şirul de caractere.
Exemple:
“123”; “Limbajul C”; “sir de caractere”; “sir\””; “”(şirul vid).
Constanta şir de caractere se reprezintă în memoria calculatorului printr-o
succesiune de octeţi în care se păstrează codurile ASCII ale caracterelor şirului, iar ultimul
octet conţine caracterul NUL care marchează sfârşitul şirului de caractere.
După cum se observă din exemplele date putem să folosim şi convenţia cu backslash.
De reţinut că ‘X’ şi “X” reprezintă construcţii diferite.

1.3.6. Variabile simple

Prin variabilă înţelegem o dată a cărei valoare se poate schimba în timpul execuţiei
programului care o conţine. Unei variabile i se ataşează un nume prin intermediul căruia o
putem referi sau modifica. Totodată valorile pe care le poate lua o variabilă trebuie să
aparţină aceluiaşi tip. Corespondenţa dintre numele şi tipul unei variabile se realizează
printr-o construcţie specială numită declaraţie.
După modul de grupare datele sunt:
- date izolate denumite şi variabile simple;
- date grupate:
- mulţimi ordonate de date de acelaşi tip la care ne referim cu indici,
- structuri în care date de tipuri diferite se grupează.

Declaraţia de variabilă simplă are formatul general:


tip listă_de_nume;
unde listă_de_nume este formată fie dintr-un singur nume fie din mai multe, separate prin
virgule.
În limbajul C trebuie declarate toate variabilele înainte de a fi utilizate.
Exemple:
int a,b,c; // a, b, c sunt variabile de tip int reprezentate in binar pe 2 octeţi;
float x,y,z; // x, y, z sunt variabile de tip float in format flotant simpla precizie pe 32 de biţi;
char car; // car este o variabila de tip char pe 1 octet, poate conţine coduri ASCII;
long i,j,k; // i, j, k sunt variabile de tip long in format binar pe 32 de biţi.

1.3.7. Tablouri

Un tablou ca orice variabilă simplă trebuie declarat înainte de a fi utilizat. Dacă


referirea la elementele tabloului se face cu un singur indice se spune că tabloul este
unidimensional (se mai numeşte vector); dacă referirea se face cu doi indici tabloul se
numeşte bidimensional (matrice); iar cu n indici tabloul este n-dimensional.
Declaraţia unui tablou în forma cea mai simplă este:

15
tip nume[l1][l2]...[ln];
unde:
l1, l2, ... ln sunt expresii constante care au valori întregi ce pot fi evaluate de compilator
la întâlnirea lor.
Evident că se pot declara mai multe tablouri deodată şi atunci numele de tablouri se
pot înşirui într-o listă, fiecare separat prin virgulă.

Exemple:

int t[5]; // s-a declarat un tablou unidimensional de 5 componente;


float a[5][3]; // s-a declarat un tablou bidimensional de 5*3=15 componente;

La elementele unui tablou ne referim prin variabile cu indici. O variabilă cu indici se


compune din numele tabloului urmat de unul sau mai mulţi indici, fiecare indice fiind inclus
în paranteze drepte. Numărul indicilor defineşte dimensiunea tabloului. Indicii sunt expresii
care au valori întregi. Limita inferioară a indicilor este zero. Astfel dacă t este tabloul de tip
int în exemplul de mai sus, elementele lui sunt:
t[0], t[1], t[2], t[3], t[4].

În cazul matricii a declarate mai sus elementele ei vor fi:


prima linie: a[0][0], a[0][1], a[0][2];
a doua linie: a[1][0], a[1][1], a[1][2];
...
a cincea linie: a[4][0], a[4][1], a[4][2];
Deci pentru dimensiunea k indicele variază între 0 şi lk-1.
Repartizarea memoriei pentru un tablou se face astfel: pentru fiecare element al
tabloului se repartizează o zonă de memorie necesară în conformitate cu tipul tabloului
respectiv (pentru tipul int câte 2 octeţi, pentru tipul float câte 4 octeţi, etc). Numele unui
tablou este un pointer, el poate fi utilizat în diferite construcţii şi el are ca valoare adresa
primului său element.
În desenul următor se indică repartizarea memoriei pentru tabloul t din exemplul de
mai înainte:

t
adresa lui
t[0]
t[0] t[1] t[2] t[3] t[4]

1.3.8. Comentariu

În limbajul C un comentariu se scrie între /* şi */ pe oricâte rânduri. între /* şi */ se


poate scrie o succesiune arbitrară de caractere, care însă nu poate să conţină secvenţa de
terminare (adică */). Comentariul poate fi inserat într-un program în orice poziţie în care este
legal să apară un caracter alb. El constituie o explicaţie pentru programator sau pentru
utilizator. Compilatorul nu interpretează comentariul în nici un mod. Comentariul se
recomandă a fi inserat în punctele în care programatorul poate lămuri prin explicaţii, anumite
aspecte ale procesului de calcul sau ale datelor utilizate. În mediile BORLAND C există şi o
16
altă convenţie de a marca un comentariu la nivelul unui rând cu ajutorul a două caractere
slash (//), convenţie pe care am folosit-o de altfel în exemplele anterioare.

1.4. Preprocesare
Un program sursă C poate fi prelucrat înainte de a fi compilat. O astfel de prelucrare
se numeşte preprocesare sau precompilare. Acest lucru se realizează printr-un program
special numit preprocesor. Acesta este apelat automat înainte de a începe compilarea.
Preprocesorul limbajului C realizează următoarele:
- includeri de alte fişiere (de obicei fişiere sursă);
- definiţii şi apeluri de macrouri simple;
- compilare condiţionată.

1.4.1. Includerea unui fişier sursă

Fişierele sursă pot fi incluse cu ajutorul directivei include.


Două formate se folosesc pentru a include un fişier sursă în locul în care apare directiva (de
obicei se pune la începutul programului):

#include “specificator_de_fisier”
sau
#include <specificator_de_fisier>

unde: specificator_de_fişier trebuie să fie un nume de fişier valid din punct de vedere al
sistemului de operare DOS şi de obicei are extensia “.h” sau “.c”.
Prima variantă este folosită de obicei când fişierele de inclus sunt furnizate de
utilizator; dacă nu este indicată calea atunci fişierele sunt căutate în directorul curent şi apoi
în directoarele setate pentru directiva include.
A doua variantă este folosită pentru încorporarea unui fişier standard care se caută în
directoarele setate pentru directiva include (de obicei astfel de fişiere standard sunt livrate în
biblioteci ataşate mediului de programare C).
Odată localizat fişierul dintr-o directivă include se înlocuieşte aceasta prin textul
fişierului sursă. Deci compilatorul nu va mai întâlni directiva include ci textul fişierului inclus
de preprocesor.
Includerile de fişiere se fac de obicei la început pentru ca textele fişierelor sursă (date
şi funcţii) să poată fi utilizate în tot fişierul sursă de lucru. De aceea, la începutul fişierelor
sursă vom întâlni mai multe includeri de fişiere standard: stdio.h, stdlib.h, etc. Textul unui
fişier inclus poate la rândul său să conţină directiva include. Fişierul stdio.h (prescurtarea de
la standard input output header) conţine printre altele şi funcţiile standard de intrare-ieşire
printf şi scanf. Fişierele cu extensia “.h” se mai numesc şi fişiere header (fişiere care se pun
la începutul fişierului sursă). Un alt exemplu de fişier header este iostream.h folosit în
mediul BORLAND C++ care conţine funcţiile cin (console input) şi cout (console output).

1.4.2. Constante simbolice

Cu directiva define se pot defini constante simbolice şi macrouri. Constantele


simbolice se definesc astfel:

17
#define nume succesiune_de_caractere

Preprocesorul substituie nume cu succesiune_de_caractere peste tot în fişierul sursă


care urmează poziţiei directivei define. Dacă succesiune_de_caractere nu încape pe un rând
atunci se poate continua pe rândul următor scriind caracterul “\” la sfârşitul primului rând.
Numele nume definit ca mai sus se spune că este o constantă simbolică. Se
recomandă ca nume să fie scris cu litere majuscule pentru a scoate în evidenţă că este o
constantă simbolică.
Construcţia succesiune_de_caractere folosită pentru a defini o constantă simbolică
poate la rândul ei să conţină alte constante simbolice.
O constantă simbolică poate fi redefinită (tot cu define) sau poate fi anihilată cu
undef (#undef nume).

Exemple:

1) #define PROCENT 10 // din acest punct al fisierului sursa se substituie


// PROCENT cu 10
...
#define PROCENT 15 // de aici PROCENT se substituie cu 15
...
#undef PROCENT // din acest punct constanta simbolica PROCENT
// isi inceteaza existenta
...

2) #define DIM 100 // s-au definit doua constante simbolice DIM


#define DOI_PI (2*3.1415926) // si DOI_PI
...
int vector[DIM]; // DIM va fi inlocuit cu 100
...
x=DOI_PI;
...

18
2. Clase de variabile (de memorie)
Compilatorul C alocă memorie variabilelor din program de dimensiune
corespunzătoare tipului fiecăreia.
Memoria se alocă în 2 moduri:
- static, repartizată într-o zonă specială asociată programului;
- dinamic, repartizată într-o zonă specială numită stivă (se comportă ca o listă LIFO).

În funcţie de modul cum se alocă memorie, vom distinge mai multe clase de variabile.
O primă clasă de variabile este aceea a variabilelor globale cărora li se alocă memorie pe
toată durata execuţiei programului şi ele pot fi utilizate în orice funcţie a programului. Altă
clasă de variabile este clasa variabilelor locale, aceste variabile au o utilizare locală la
nivelul unei funcţii.

2.1. Variabile globale


O variabilă globală are o definiţie şi atâtea declaraţii de variabilă externă câte sunt
necesare.
Definiţia unei variabile globale coincide sintactic cu o declaraţie obişnuită, dar care
este scrisă în afara oricărei funcţii a programului. Implicit, definiţia unei variabile globale
determină ca variabila respectivă să fie definită începând din punctul scrierii ei şi până la
sfârşitul fişierului sursă respectiv. De aceea se recomandă ca definiţiile variabilelor globale să
fie scrise la începutul fişierului sursă, pentru a fi accesate în orice funcţie a fişierului.
Pentru a utiliza variabilele globale şi în alte funcţii situate în alte fişiere sursă decât în
cel în care sunt definite, ele trebuie declarate ca externe în funcţiile respective.
O declaraţie de variabilă externă coincide cu o declaraţie obişnuită doar că începe cu cuvântul
cheie extern.

Exemplu:
fişierul în care sunt declarate ca variabile globale fişierul în care sunt folosite ca variabile externe

int i;
float f; void functie(. . .)
void main(void) { extern int i;
{ i = 10; extern double f;
... ...
f = 3.14; f = f*i;
... ...
} }

Variabilele i şi f sunt declarate în afara funcţiei main şi în afara oricărei funcţii, deci
sunt variabile globale. Ele pot fi folosite în toate funcţiile din fişierul sursă care conţine
definiţiile lor. Pentru a le utiliza în funcţii situate în alte fişiere sursă decât în cel în care sunt
definite ele sunt declarate ca externe. Deci variabilele i şi f sunt declarate ca externe în funcţia
functie din al doilea fişier sursă. Cele două fişiere sursă care pot fi scrise în momente şi de
persoane diferite se pot asambla într-un singur program cu ajutorul directivei de preprocesare
include.

2.2. Variabile locale

19
Variabilele locale nu sunt valabile în tot programul. Ele au o utilizare locală în două
feluri:
- ca şi variabile automatice (alocate pe stivă) la nivelul unei funcţii;
- ca şi variabile statice (alocate în zona programului) la nivel de fişier (eventual şi la
nivelul unei funcţii).

Variabilele declarate în interiorul unei funcţii şi care nu sunt declarate ca externe sunt
variabile locale. Lor li se alocă memorie pe stivă la intrarea în funcţia în care sunt declarate.
La ieşirea din funcţie, stiva se reface la conţinutul dinaintea apelului şi variabilele locale pierd
alocarea. Deci ori de câte ori se apelează o funcţie, variabilele locale acesteia (denumite şi
variabile automatice) se alocă (primesc memorie) pe stivă şi la ieşirea din funcţie variabilele
locale sunt şterse din stivă.
Variabilele locale pot să nu fie alocate pe stivă, ci într-o zonă de memorie destinată
acestui scop. O astfel de variabilă locală se numeşte variabilă statică. O variabilă locală
statică se declară printr-o declaraţie obişnuită, precedată de cuvântul cheie static. O variabilă
statică poate fi declarată atât în corpul unei funcţii cât şi în afara oricărei funcţii. În cazul unei
variabile statice declarată în interiorul unei funcţii alocarea nu se face pe stivă ci într-o zonă
de memorie destinată acestui scop, aceasta fiind deosebirea esenţială faţă de variabilele
automatice. În cazul în care o variabilă statică este declarată în afara funcţiilor, ea este
definită din punctul în care a fost declarată şi până la sfârşitul fişierului sursă care conţine
declaraţia ei. Spre deosebire de variabilele globale, o variabilă statică nu poate fi declarată ca
externă şi deci nu poate fi utilizată în funcţiile din alte fişiere sursă diferite de cel în care a
fost declarată.
Se recomandă ca tablourile mari să fie declarate statice, deoarece dacă sunt automatice pot
depăşi capacitatea stivei (care are implicit o valoare de câţiva Kocteţi).

Exemple:
1) Fişierul fisier1.c este un fişier sursă care conţine 2 variabile globale i şi d , o variabilă
statică x şi două funcţii f şi main. Funcţia main conţine variabila statică a iar funcţia f
conţine variabila statică b.

int i; // definiţia variabilei globale i


double d; // definiţia variabilei globale d
static int x; // definiţia variabilei statice x, locala fisierului fisier1.c

void main (void)


{ static char a; // definiţia variabilei statice a, locala funcţiei main
float c; // definiţia variabilei automatice c, locala funcţiei main
/* in acest moment se pot folosi variabilele i,d,x,a si c */
...
}

{ int p; // definiia variabilei automatice p, locala funciei f


static float b; // definiia variabilei statice b, locala funciei f
/* in acest moment se pot folosi variabilele i,d,x, p si b */
...
}

Variabilele a şi c fiind locale funcţiei main nu pot fi utilizate în funcţia f. Analog,


variabilele p şi b sunt locale în funcţia f, nu pot fi utilizate în funcţia main.

2) Fişierul fisier2.c conţine funcţiile f1 şi f2 care intră în componenţa aceluiaşi


program ca şi funcţiile main şi f din fişierul fisier1.c

20
static unsigned t; // definitia variabilei statice t, locala fisierului fisier2.c
void f1(...)
{ extern int i; // declaratie externa pentru i
extern double d; // declaratie externa pentru d
static int k; // definitia variabilei statice k, locala functiei f1
/* in acest moment se pot folosi varibilele i,d, k si t */
...
}

void f2(...)
{ extern int i; // declaratie externa pentru i
static double s; // definitia variabilei statice s, locala functiei f2
/* se pot folosi variabilele i, s si t */
...
}

Variabila statică x definită în fişierul fisier1.c nu poate fi utilizată în fişierul fisier2.c.


De asemenea, variabila statică t nu poate fi utilizată în fişierul fisier1.c. Variabila globală d
nu poate fi utilizată în funcţia f2, ea nefiind declarată ca şi externă.

Observaţii:
1o. Variabilele globale constituie un mijloc simplu de interfaţă între funcţiile unui
program. Se recomandă a fi folosite când dorim transferuri de valori între două sau mai multe
funcţii în aşa fel încât modificările realizate de o funcţie să fie accesibile pentru toate funcţiile
programului. Nu trebuie să se facă abuz în utilizarea variabilelor globale deoarece constituie
şi o sursă potenţială de erori, pentru că accesul unui mare număr de funcţii la anumite date
globale conduce deseori la modificări nedorite şi greu evidenţiabile.
2o. Funcţiile sunt considerate cu atributul implicit extern. Dacă într-un program există mai
multe fişiere sursă atunci o funcţie poate fi apelată oriunde, bine înţeles respectând convenţia
definirii ei sau a prototipului ei înainte de a fi folosită. Putem să limităm definind funcţiile cu
atributul static precedând antetul funcţiei prin cuvântul cheie static. Astfel funcţia respectivă
devine locală şi deci apelabilă doar în fişierul în care este definită.

2.3. Variabile registru


Limbajul C oferă posibilitatea de a aloca anumite variabile în regiştri
microprocesorului. Deoarece regiştri constituie un tip de memorie ultrarapidă în anumite
cazuri se poate economisi atât timp de execuţie cât şi memorie. Se pot aloca în regiştri numai
parametrii funcţiilor şi variabilele automatice de tip int, char şi de tip pointer. O variabilă
registru se declară în mod obişnuit, precedând declaraţia ei prin cuvântul rezervat register.
Alocarea într-un registru a unei variabile se face numai dacă există un registru
disponibil. În caz contrar, variabila registru se alocă pe stivă exact ca o variabilă automatică.
Alocarea se face în ordinea declarării variabilelor registru.
Exemplu:
void f (register int i)
{ register char c;
register int j;
...
}
Mai întâi se alocă parametrul i într-un registru, apoi se alocă c şi j în alţi doi regiştri.
Se recomandă alocarea în regiştri a variabilelor care au o utilizare mare, de exemplu a
indicilor de tablouri.

21
2.4. Iniţializare
Variabilelor li se pot atribui valori iniţiale. Pentru variabilele globale valorile iniţiale
se atribuie prin definiţiile lor, iar în cazul celorlalte variabile se pot atribui valori prin
declaraţiile lor. Pentru că de multe ori am folosit cuvintele definiţia variabilelor sau
declaraţiile varibilelor precizăm că ele au înţeles distinct în anumite contexte. O variabilă
globală se defineşte o singură dată şi se poate declara ori de câte ori e necesară utilizarea ei
în alte fişiere (evident declarată externă). În cazul celorlalte tipuri de variabile definiţiile şi
declaraţiile coincid. Totodată definiţia şi declaraţia (prototipul) unei funcţii nu coincid.
O variabilă simplă se poate iniţializa printr-o declaraţie de forma:

tip nume=expresie;

Variabilelor globale şi statice li se atribuie valori iniţiale la lansarea programului.


Expresia utilizată în cazul acestor variabile trebuie să fie o expresie constantă care să poată fi
evaluată de compilator la întâlnirea ei. Aceasta, deoarece variabilele globale şi statice se
iniţializează prin valori definite la compilare.
Variabilele automatice se iniţializează la execuţie, de fiecare dată când se activează
funcţia în care sunt declarate. Din această cauză, nu mai este necesar ca expresia să fie o
expresie constantă. Totuşi, la întâlnirea ei, trebuie să fie definiţi operanzii expresiei de
iniţializare.

Exemplu:
void f(int n)
{ int i=10;
int k=i+n;
...
}

La întâlnirea expresiei i+n sunt deja definiţi ambii operanzi:


- i a fost în prealabil iniţializat cu valoarea 10;
- n are o valoare care e transferată la apel.

Variabilele globale şi statice neiniţializate au implicit valoarea egală cu zero, iar


varibilele automatice neiniţializate au o valoare iniţială imprevizibilă.
Tablourile se pot iniţializa printr-o listă de expresii incluse între acolade. Astfel un
tablou unidimensional se poate iniţializa folosind următorul format:

tip nume[n] = { exp1, exp2, . . . expn }

La iniţializarea tablourilor se pot utiliza numai expresii constante. Numărul expresiilor


poate fi mai mic decât numărul elementelor tabloului. Elementele neiniţializate au valoarea
zero în cazul tablourilor globale şi statice. Dacă se iniţializează fiecare element al tabloului
atunci numărul n al elementelor acestuia nu mai este obligatoriu în declaraţia tabloului
respectiv. Deci se poate scrie astfel:

tip nume[ ] = { exp1, exp2, . . . expn}

Numărul elementelor tabloului se consideră că este egal cu cel al expresiilor care


realizează iniţializarea lor.
Pentru un tablou bidimensional vom folosi următoarea structură:

22
tip nume [n][m] = { {exp11, exp12, . . . exp1m}, // pentru linia întâi
{exp 21, exp22, . . . exp2m}, // pentru linia a doua
...
{exp n1, expn2, . . . expnm}, // pentru ultima linie
}

Numărul expresiilor poate fi mai mic decât m în oricare din acoladele corespunzătoare
celor n linii ale tabloului bidimensional. În acest caz se poate omite doar numărul n (al
liniilor tablourilor), m fiind obligatoriu. Formatul de iniţializare a tablourilor bidimensionale
se extinde imediat pentru tablouri cu mai multe dimensiuni.
Tablourile de tip caracter pot fi iniţializate folosind un format mai simplu decât cel
indicat anterior. Astfel, un tablou de tip caracter se poate iniţializa printr-o declaraţie de
forma:
char sir[n] = şir_de_caractere;

Evident, marginea superioară n poate fi omisă şi în acest caz. Totodată compilatorul


pune automat caracterul NUL după ultimul caracter al şirului utilizat în iniţializare.

Exemple:

1) int itab[] = {1,2,3,4,5} // tabloul de tip intreg are 5 elemente itab[0] = 1,. . . itab[4] = 5

2) int m[3][3] = {{-1,0,1},{-1},{0,1}}; // tabloul are 3 linii si 3 coloane.

Elementele primei linii sunt iniţializate astfel:


m[0][0] = –1;
m[0][1] = 0;
m[0][2] = 1;

În linia a doua se iniţializează numai m[1][0] = -1; Dacă tabloul ar fi declarat global sau static
atunci m[1][1] şi m[1][2] ar avea valoarea zero. Altfel ele au o valoare imprevizibilă.
Elementele ultimei linii se iniţializează astfel:
m[2][0] = 0;
m[2][1] = 1;

3) declaraţiile de mai jos sunt identice:

char sir [ ] = {‘L’,’I’,’M’,’B’,’A’,’J’,’U’,’L’, ‘ ‘,‘C’};


char sir [ ] = {“LIMBAJUL C”};

23
3. Expresii, operanzi, operatori
3.1. Expresii
O expresie în limbajul C este formată fie dintr-un operand fie din mai mulţi
operanzi legaţi între ei prin operatori. O expresie are o valoare şi un tip care se determină
aplicând operatorii conform priorităţilor şi asociativităţii acestora.
În limbajul C operatorii se asociază de la stânga la dreapta, exceptând operatorii unari
şi de atribuire, care se asociază de la dreapta la stânga.. Totodată pot fi folosite parantezele
rotunde pentru a impune o anumită ordine în executarea operaţiilor.

3.2. Operanzi
Un operand în limbajul C poate fi una din următoarele elemente:
- o constantă;
- o constantă simbolică;
- numele unei variabile simple;
- numele unui tablou;
- numele unei structuri;
- numele unei funcţii;
- referirea la elementul unui tablou (variabilă cu indici);
- referirea la elementul unei structuri;
- apelul unei funcţii;
- o expresie inclusă în paranteze rotunde.
Unele dintre elementele de mai sus nu au fost încă definite, ele se vor prezenta în lecţiile
viitoare.
Exemple: 9876 - constantă întreagă;
x - variabilă simplă;
t[i][3] - variabilă cu indici;
0xabcd - constantă hexazecimală;
t - nume de tablou;
(expresie) - expresie inclusă în paranteze rotunde;
f1 - numele unei funcţii.

3.3. Operatori
Operatorii limbajului C pot fi grupaţi în mai multe clase, dar oricum ei pot fi folosiţi
împreună într-o aceeaşi expresie. Operatorii au arităţi diferite: unari, binari, ternari şi totodată
o anumită prioritate implicită care e redată în tabelul de mai jos. Operatorii de aceeaşi
prioritate se află trecuţi în aceeaşi linie. Liniile tabelulul conţin operatorii limbajului C în
ordinea descrescătoare a priorităţilor. Astfel în prima linie se află operatorii de prioritate
maximă, iar în ultima linie operatorul virgulă cu prioritatea cea mai mică. Cu excepţia
operatorilor “.”, “->”,”&”,”*”, a parantezelor rotunde (folosite la definiţia şi apelul funcţiilor)
şi a parantezelor drepte (folosite la variabilele cu indici) ceilalţi operatori vor fi explicaţi în
această lecţie.

( ) [ ] . ->
24
- (unar) +(unar) *(unar) &(unar) ! ~ ++ -- (tip) sizeof
* / %
+ -
<< >>
< <= >= >
= = !=
&
^
|
&&
||
? : (ternar)
= op= op poate fi: *(binar) / % +(binar) –(binar) << >> & ^ |
,

3.3.1. Operatori aritmetici

Lista operatorilor aritmetici este redată mai jos:


- (minus unar);
+ (plus unar);
* / % operatori binari multiplicativi; (înmulţire, împărţire, restul împărţirii întregi);
+ - operatori binari aditivi (adunare şi scădere).

Operatorii de pe aceeaşi linie au aceeaşi prioritate. Cei unari au prioritate mai mare
decât cei binari. Operatorii multiplicativi au prioritate mai mare decât cei aditivi.

Exemple:
int i,j,k;
float x,y;
double t[10];
// se dau cateva exemple de expresii folosind operatorii aritmetici
i*x+t[5];
-y+k;
i%j; // daca i=9 si j=4 atunci i%j are valoarea 1
i/j; // daca i=9 si j=4 atunci i/j are valoarea 2
x*-y; // - este operatorul unar deci avem x*(-y)

3.3.2. Operatori relaţionali

Lista operatorilor relaţionali este redată astfel:


< (mai mic)
<= (mai mic sau egal; cele două caractere ce compun operatorul sunt concatenate)
> (mai mare)
>= (mai mare sau egal; cele două caractere ce compun operatorul sunt
concatenate)

Toţi operatorii relaţionali au aceeaşi prioritate. Ea este mai mică decât prioritatea
operatorilor aditivi. Rezultatul aplicării unui operator relaţional este 1 sau 0, după cum
operanzii se află în relaţia definită de operatorul respectiv sau nu.
Exemple:
25
a= 4 şi b= -5
atunci a>0 are valoarea 1;
a<=0 are valoarea 0;
a+b>0 are valoarea 0;
a>=b are valoarea 1;
a<0 are valoarea 0;
a+b>=b-a are valoarea 1;
a+b>=(b-a)*(b-a) are valoarea 0.

3.3.3. Operatori de egalitate

Lista operatorilor de egalitate este redată mai jos:


= = (egal; două semne “=” concatenate)
!= (diferit; semnele sunt concatenate).

Operatorii de egalitate au ambii aceeaşi prioritate şi este imediat mai mică decât a
operatorilor relaţionali. Operatorul “= =” testează egalitatea a doi operanzi. Dacă operanzii
sunt egali atunci rezultatul operaţiei “= =” este 1, în caz contrar este 0. Operatorul “!=”
furnizează rezultatul 1 când cei doi operanzi sunt diferiţi şi 0 când sunt egali.

Exemple:
a= 2 şi b=-1
atunci
a= =b are valoarea 0;
a!=b are valoarea 1;
a*b!=a+b are valoarea 1.

3.3.4. Operatori logici

Lista operatorilor logici este redată mai jos:


! (negaţia logică - operator unar);
&& (ŞI logic);
|| (SAU logic).

Operatorul “!” are aceeaşi prioritate cu operatorii unari “+” şi “-“. Operatorul “&&”
este mai prioritar decât operatorul “||”, dar are o prioritate mai mică decât operatorii de
egalitate.
În limbajul C nu există valori logice speciale. Valoarea fals se reprezintă prin zero.
Orice valoare diferită de zero reprezintă valoarea adevărat.
Dacă operatorul “!” se aplică la un operand a cărui valoare este zero, atunci rezultatul
este 1. Dacă acelaşi operator se aplică la un operand a cărui valoare este diferită de zero,
atunci rezultatul este 0.
Dăm în continuare tabelele operatorilor logici binari aplicate valorilor 0 şi 1.

&& 0 1 || 0 1 sau exclusiv 0 1


0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0

26
Chiar dacă pentru “sau exclusiv” nu există operator el se poate realiza prin expresia
următoare aplicată operanzilor a şi b: !a&&b||!b&&a sau folosind parantezele rotunde ((!a)
&&b)||((!b)&&a).
Operatorii logici se evaluează de la stânga la dreapta. Dacă la evaluarea unei expresii
se ajunge într-un punct în care se cunoaşte valoarea întregii expresii, atunci restul expresiei nu
se mai evaluează.
Dacă a=0 şi b=1 atunci expresia ! a||b are valoarea 1 pentru că !a are deja valoarea 1.

3.3.5. Operatori logici pe biţi

Lista operatorilor logici pe biţi este redată mai jos în ordinea descrecătoare a priorităţilor:
~ (operator unar; complement faţă de 1)
>> << (deplasări la dreapta, respectiv la stânga)
& (ŞI pe biţi)
^ (SAU-EXCLUSIV pe biţi)
| (SAU pe biţi)

Operatorul “~”, fiind unar, are aceeaşi prioritate ca şi ceilalţi operatori unari ai
limbajului C. El schimbă fiecare bit 1 al operandului în 0 şi invers.
Operatorul “>>” realizează deplasarea la dreapta care este echivalentă cu o împărţire
întreagă cu puteri a lui 2; a >> 3 este echivalentă cu [a/23].
Operatorul “<<” realizează deplasarea la stânga care este echivalentă cu o înmulţire
cu puteri a lui 2; a << 3 este echivalentă cu a*8.

Pentru operatorii &, |, ^ dăm în continuare tabelele operaţiilor:

& 0 1 | 0 1 ^ 0 1
0 0 0 0 0 1 0 0 1
1 0 1 1 1 1 1 1 0

Observaţii:
1o. Operanzii care nu ocupă un cuvânt (16 biţi) se extind la un cuvânt. De exemplu expresia
~0 are ca rezultat un cuvânt cu toţi biţi egali cu 1.
2o. Operatorii logici pe biţi se execută bit cu bit spre deosebire de operatorii logici care se
evaluează global. De exemplu dacă x=2 şi y=1 sunt variabile de tipul int atunci:
x&&y are valoarea 1 pentru că ambii operanzi sunt diferiţi de 0.
x&y are valoarea 0 conform schemei de mai jos
x= 0000 0000 0000 0010
y= 0000 0000 0000 0001
x&y= 0000 0000 0000 0000
3o. Operatorul & se foloseşte frecvent pentru a anula biţi din configuraţia unui cuvânt, iar
operatorul | pentru a seta (pune) biţi într-un anumit mod.
4o. Operanzii trebuie să fie întregi (de tipul int sau long).
5o. Atenţie la deplasări nu se modifică valoarea operandului; deci trebuie să facem o atribuire;
de exemplu a = a << 3 va modifica valoarea lui a pe când a << 3 nu modifică valoarea lui a.

Exemple:
1) Fie declaraţia:
27
int i;
atunci expresia i >> 8 & 255 are ca rezultat valoarea celui mai semnificativ octet a lui i; i
>> 8 deplasează octetul mai semnificativ al lui i în poziţia mai puţin semnificativă; se face
apoi un ŞI logic pe biţi cu masca 255 care păstrează octetul mai puţin semnificativ.

2) Fie expresia: (x >> 6) & ~(~ 0 << 3)


Să presupunem că x are valoarea în biţi: 1010 1011 1000 1101.
Atunci x>>6 are valoarea: 1111 1110 1010 1110
Al doilea operand pregăteşte o mască astfel:

~0 1111 1111 1111 1111


~0<<3 1111 1111 1111 1000
~(~0<<3) 0000 0000 0000 0110

Rezultatul final este dat de:


0000 0000 0000 0111
1111 1110 1010 1110
0000 0000 0000 0110

Practic s-a obţinut valoarea biţilor 8,7,6 a lui x (numerotaţi de la dreapta începând cu 0).

3.3.6. Operatori de atribuire

În forma cea mai simplă operatorul de atribuire se notează cu “=” şi se utilizează în


construcţii de forma:
v=expresie;

(v este fie o variabilă simplă, fie variabilă cu indici sau un element de structură).

Această construcţie se mai numeşte expresie de atribuire. Ea este considerată ca


fiind un caz particular de expresie. Tipul ei coincide cu tipul lui v, iar valoarea întregii
expresii este chiar valoarea atribuită lui v.

O expresie de forma:
v1=(v=expresie);

este şi ea legală şi se efectuează în felul următor :


- se evaluează expresia expresie şi valoarea ei se atribuie lui v;
- valoarea lui v se atribuie apoi şi lui v1.
Deoarece operatorii de atribuire se asociază de la dreapta la stânga, expresia de mai
sus se poate scrie şi fără paranteze:
v1=v=expresie;

În general, putem realiza atribuiri multiple printr-o expresie de forma:

vn =. . . =v1=v=expresie

Dacă expresia din dreapta semnului egal are un tip diferit de cel al variabilei v, atunci întâi
valoarea ei se converteşte spre tipul variabilei v şi pe urmă se realizează atribuirea.
Pentru operaţia de atribuire, în afara semnului egal se mai poate folosi şi succesiunea :
28
op=

unde prin op se înţelege unul din operatorii binari aritmetici sau logici pe biţi, adică unul din
următorii:
% / * - + & ^ | << >>

Acest mod de construcţie se foloseşte pentru a compacta un anumit tip de atribuire. Astfel
expresia:
v op = expresie;

este identică cu expresia de atribuire:

v = v op expresie;

Exemple:

int i, j;
double x, y;
int v[10];
i=5;
j=10;
x=y=10.01;
i +=1; // echivalenta cu i=i+1 si cu i++
x*=3; // echivalenta cu x=x*3
j<<=10; // echivalenta cu j=j<<10
v[i]*=i // echivalenta cu v[i]=v[i]*i
x /= x-y // echivalenta cu x = x/(x-y)

3.3.7. Operatori de incrementare şi decrementare

Aceşti operatori sunt unari şi au aceeaşi prioritate cu ceilalţi operatori unari ai


limbajului C. Operatorul de incrementare se notează prin “++” şi măreşte valoarea
operandului cu unu, iar operatorul de decrementare se notează prin “- -“ şi micşorează
valoarea operandului cu unu. Operatorii sunt folosiţi prefixat şi postfixat. Astfel operatorii
prefixaţi au notaţia:
++operand;
- - operand;

Ei se aplică mai întâi şi apoi se foloseşte valoarea operanzilor.


Astfel operatorii postfixaţi au notaţia:
operand++;
operand - -;

Se foloseşte valoarea operanzilor şi apoi se aplică incrementarea sau decrementarea.


Menţionăm că aceşti operatori se pot aplica numai la următorii operanzi:
- variabilă simplă;
- variabilă cu indici;
- referire la elementul unei structuri.

Exemple:
int i,j;
29
double x,y;
int vector [5];
j=i++; // este echivalent cu j=i si i=i+1;
y=--x; // este echivalent cu x=x-1 si y=x;
i=++vector[j] // este echivalent cu vector[j]=vector[j]+1 si i=vector[j]

3.3.8. Operatorul de conversie explicită (expresie cast)

Pentru forţarea tipului unui operand se foloseşte o construcţie de forma:

(tip) operand

Prin aceasta valoarea operandului se converteşte spre tipul indicat în paranteze.


Exemplu:
int i,j;
double y;
i=8; j=5;
y=i/j; // y are valoarea 1, pentru ca se face impartirea intreaga i/j

Dacă vom converti operanzii i şi j spre tipul double se va obţine rezultatul corect adică 1.6.
Deci:
int i,j;
double y;
i=8; j=5;
y=(double) i / (double) j; // y are valoarea 1.6,

Construcţia (tip) este un operator unar prin care se explicitează conversia dorită. Are aceeaşi
prioritate ca restul operatorilor unari.

3.3.9. Operatorul dimensiune (sizeof)

Pentru a determina lungimea în octeţi a unei date se poate folosi construcţia:

sizeof (data)

unde data poate fi:


- numele unei variabile simple;
- numele unui tablou;
- numele unei structuri;
- numele unui tip;
- referirea la elementul unui tablou sau structură.

Exemple:
int i;
long l;
float f;
double d;
char c;
int itablou[5];
double dtablou[5];
sizeof (i) // are valoarea 2;
sizeof (l) // are valoarea 4;
30
sizeof (f) // are valoarea 4;
sizeof (d) // are valoarea 8;
sizeof (c) // are valoarea 1;
sizeof (itablou[1]) // are valoarea 2;
sizeof (dtablou[1]) // are valoarea 8;
sizeof (itablou) // are valoarea 10;
sizeof (dtablou) // are valoarea 40.

3.3.10. Regula conversiilor implicite

În general o expresie C conţine operanzi de tipuri diferite. Pentru operatorii binari


există situaţii când operanzii nu sunt de acelaşi tip şi trebuie executate conversii astfel încât
operatorii să se aplice pentru operanzi de acelaşi tip. Aceste conversii le face automat
compilatorul. Există o regulă a conversiilor implicite care are următorii paşi:
- fiecare operand de tip char se converteşte spre tipul int şi fiecare operand de tipul float
se converteşte spre double;
- dacă unul dintre operanzi este de tip double atunci şi celălalt se converteşte spre tipul
double şi rezultatul va avea tipul double;
- dacă unul dintre operanzi este de tip long, atunci şi celălalt se converteşte spre tipul long
şi rezultatul va avea tipul long;
- dacă unul dintre operanzi este de tip unsigned, atunci şi celălalt se converteşte spre tipul
unsigned şi rezultatul va fi de tipul unsigned;
- la acest pas se ajunge numai dacă ambii operanzi sunt de tip int şi deci operaţia se execută
cu operanzii respectivi, iar rezultatul va fi de tip int.
Aplicând regula de mai sus pas cu pas (la fiecare operator în momentul efectuării lui), se
ajunge în final la evaluarea întregii expresii şi prin acesta se determină tipul expresiei. Regula
conversiilor implicite nu se aplică pentru operatorul de atribuire (valoarea expresiei din partea
dreaptă a semnului de atribuire se converteşte spre tipul variabilei din stânga semnului egal).

Exemple:
int i, j, k;
float a, b;
double x, y;
unsigned p;
long r;
char c;

expresii conversii tipul expresiei

i-j/k nu int
a/b a spre double; b spre double double
x+y nu double
i+a a spre double; i spre double double
i-3.14 i spre double double

expresii conversii tipul expresiei

i+3 nu int
i+x i spre double double
i-c c spre int int
x+10 10 spre double double
p-10 10 spre unsigned unsigned
r*5 5 spre long long
(double)(i/j) se realizează împărţirea întreagă între

31
i şi j şi rezultatul se converteşte spre double double

Dacă rezultatul unei operaţii depăşeşte domeniul de valori ce corespunde tipului


rezultatului, valoarea respectivă se trunchiază şi rezultatul este eronat.

3.3.11. Operatori condiţionali

Operatorii condiţionali sunt ? şi : şi se folosesc împreună în construcţii de forma:


exp1 ? exp2 : exp3
Evaluarea se face astfel:
- se evaluează expresia exp1;
- dacă exp1 este diferită de zero, atunci valoarea şi tipul expresiei condiţionale sunt egale cu
valoarea şi tipul expresiei exp2; altfel cu expresia exp3.
Exemplu: procesul de determinare a maximului a două numere a şi b este:
dacă a>b atunci max=a
altfel max=b
sfdacă

În limbajul C se poate realiza acest proces cu ajutorul operatorilor condiţionali astfel:


max= a>b ? a : b
Dacă a>b atunci expresia condiţională are valoarea şi tipul lui a altfel expresia condiţională
are valoarea şi tipul lui b.

3.3.12. Operatorul virgulă

Operatorul “,” este folosit pentru gruparea mai multor expresii într-una singură.
Cu ajutorul acestui operator (care are prioritatea cea mai mică) se construiesc expresii de
forma:
exp1, exp2,. . ., expn

Această expresie are valoarea şi tipul ultimei expresii (deci a lui expn).

Exemplu: k= (i=10, j=j+5; i+j)

Se execută pe rând cele două atribuiri de la stânga la dreapta din parantezele rotunde apoi se
face suma i+j şi în final se atribuie această sumă lui k.

32
4. Intrări / ieşiri standard
Limbajul C nu posedă instrucţiuni de intrare / ieşire. Aceste operaţii se realizează prin
apeluri de funcţii din bibliotecile standard ale mediului de programare. De obicei astfel de
funcţii asigură interfaţa programului cu terminalul de la care s-a lansat, cu imprimanta, etc.
Se numesc intrări standard şi ieşiri standard intrările respectiv ieşirile de la terminalul de
la care s-a lansat programul. Totodată se presupune că datele de intrare / ieşire sunt
organizate în fişiere.
Unui program C i se ataşează în mod implicit următoarele fişiere:

- stdin intrare standard;


- stdout ieşire standard;
- stderr ieşire standard pentru erori;
- stdprn ieşire pe imprimantă;
- stdaux intrare / ieşire serială.

4.1. Funcţia standard printf


Funcţia printf realizează ieşiri cu format la ieşirea standard stdout, deci afişare la
terminalul la care care s-a lansat programul. Funcţia printf se apelează printr-o instrucţiune
cu formatul:

int printf (control, lista_expresii);

unde control este un şir de caractere care conţine:


- texte de scris;
- specificatori de format pentru datele care se scriu din lista_expresii.
lista_expresii conţine expresii; valoarea fiecărei expresii se scrie conform unui
specificator de format corespondent din parametrul control.
Parametrul control este inclus între ghilimele, iar numărul specificatorilor de format
coincide cu numărul expresiilor din lista_expresii. Dacă dorim să scriem numai un text atunci
parametrul de control nu conţine nici un specificator de format iar lista_expresii este vidă
(practic este absentă).
Un specificator de format are formatul BNF următor:

%[-][d..d][.d..d][l1]l2

După cum se observă un specificator de format începe întotdeauna cu caracterul


procent (“%”). După acest caracter poate urma una din construcţiile următoare:
- un caracter “-“ opţional; prezenţa acestui caracter indică cadrarea la stânga a datei în
câmpul în care se scrie (implicit data se scrie cadrată la dreapta);
-un şir de cifre zecimale opţional, care defineşte lungimea minimă a câmpului în care se
scrie data corespunzătoare din lista_expresii; data se scrie astfel:
- în cazul în care necesită o lungime mai mică se scrie cadrată la dreapta sau
stânga (în funcţie de absenţa sau prezenţa semnului “-“)
- în cazul în care necesită o lungime mai mare se va scrie pe atâtea poziţii
câte îi sunt necesare;
-un punct urmat de un şir de cifre zecimale (după cum se observă opţional); acest element
indică precizia datei care se scrie:
33
- dacă data se scrie în virgulă flotantă, precizia defineşte numărul de cifre
aflate după marca zecimală (deci câte zecimale);
- dacă data este un şir de caractere atunci indică câte caractere se scriu.
-una sau două litere, care definesc tipul de conversie din formatul intern în formatul extern:
- prima litera poate fi “l”, ceea ce semnifică conversia din formatul intern
long în formatul extern definit de specificator;
- a doua literă este obligatorie întotdeauna şi are semnificaţiile de mai jos:

litera tipul de conversie realizat

d - din int intern în zecimal extern


o - din int intern în octal extern
x - din int intern în hexazecimal extern (litere mici pentru
cifrele mai mari ca 9 deci a,b,c,d,e,f,)
X - din int intern în hexazecimal extern (litere mici pentru
cifrele mai mari ca 9 deci A,B,C,D,E,F)
u - din unsigned intern în zecimal extern fără semn
c - din binar intern (cod ASCII) în caracterul corespunzător
s - din şir de coduri ASCII într-un şir de caractere (atenţie
ultimul cod este NUL (adică ‘\0’)
f - din float sau double intern în d...d.d...d (implicit 6 cifre
zecimale la partea fracţionară dacă nu e prezentă precizia)
e - din float sau double intern în d.d...de±ddd (implicit 6
cifre zecimale la partea fracţionară dacă nu e prezentă
precizia)
E - din float sau double intern în d.d...dE±ddd (implicit 6
cifre zecimale la partea fracţionară dacă nu e prezentă
precizia)
g - se aplică una din conversiile definite de literele f şi e
alegându-se aceea care are un număr minim de poziţii
G - se aplică una din conversiile definite de literele f şi E
alegându-se aceea care are un număr minim de poziţii

Literele d, o, x, u pot fi precedate de litera l conversia realizându-se din formatul


intern long în loc de int.

Observaţie.
1o. Dacă caracterul % nu este urmat de una din construcţiile de mai sus atunci nu reprezintă
un specificator de format.
Funcţia printf întoarce lungimea totală în octeţi a datelor scrise la terminal sau
valoarea simbolică EOF în caz de eroare. Precizăm că EOF este definită în fişierul header
stdio.h astfel:
#define EOF –1.

Totodată funcţia printf poate fi testată şi astfel:


EOF = = printf (control, lista_expresii)

Dacă are valoarea adevărat atunci la scrierea datelor s-a produs o eroare.
Exemple:
1)
#include<stdio.h>
34
#include<math.h>
void main(void)
{ int i=10; long j=11;
float a=1.2, b=1.3;
double A=1.4; B=1.5;
clrscr(); // inceputul instructiunilor executabile; se sterge ecranul
printf ("\ni*j = %d",i*j); // incep afisarile
printf ("\ni*j = %5d",i*j);
printf ("\ni*j = %-5d",i*j);
printf ("\ni*j = %5.5d",i*j);
printf ("\ni*j = %05d",i*j);
printf ("\na*b = %10.1f",a*b);
printf ("\nA*B = %10.5lf",A*B);
printf ("\nradical(a*b) = %lf",sqrt((double) a*b));
printf ("\nradical(A*B) = %15.10lf",sqrt(A*B));
printf ("\nradical(A*B) = %25.17lf",sqrt(A*B));
printf ("\nradical(A*B) = %25.19lf",sqrt(A*B));
getche(); // asteapta citirea unui caracter de la terminal
}

Rezultatele execuţiei programului sunt:


i*j = 110
i*j = 110
i*j = 110
i*j = 00110
i*j = 00110
a*b = 1.6
A*B = 2.10000
radical(a*b) = 1.249000
radical(A*B) = 1.4491376746
radical(A*B) = 1.44913767461894372
radical(A*B) = 1.4491376746189437200

2)
#define sir “PC WORLD”
void main (void)
{ printf(“*%10s*“,sir);
printf(“*%-10s*“,sir);
printf(“*%10.5s*“,sir);
printf(“*%-10.5s*“,sir);
}

Rezultatele execuţiei programului sunt:


* PC WORLD*
*PC WORLD *
* PC WO*
*PC WO *

4.2. Funcţia standard scanf


Funcţia de bibliotecă scanf realizează intrări cu format de la intrarea standard stdin
(intrare de la terminalul de la care s-a lansat programul) şi poate fi apelată printr-o
instrucţiune de apel de forma:
scanf (control, lista_adrese);
Ca şi în cazul funcţiei printf, parametrul control este delimitat de ghilimele şi poate
conţine texte şi specificatori de format. Caracterele albe din parametrul control sunt neglijate.
Celelalte caractere care nu fac parte dintr-un specificator de format, trebuie să existe la intrare
în poziţii corespunzătoare. Ele se folosesc în scopul realizării unor verificări asupra datelor
35
care se citesc.
Un specificator de format începe şi în acest caz prin caracterul procent, apoi:
- un caracter “*” opţional;
- un şir de cifre, opţional, care defineşte lungimea maximă a câmpului din
care se citeşte data de sub controlul specificatorului de format;
- una sau două litere care definesc tipul conversiei din formatul extern în
formatul intern.
Câmpul controlat de un specificator de format începe cu primul caracter care nu este
alb şi se termină:
a) fie la caracterul după care urmează un caracter alb;
b) fie la caracterul după care urmează un caracter care nu corespunde specificatorului
de format care controlează acel câmp;
c) fie la caracterul prin care se ajunge la lungimea maximă indicată în specificatorul
de format.
Condiţia c) este absentă în definirea câmpului dacă în specificatorul de format nu este
indicată lungimea maximă a câmpului.
Literele care termină un specificator de format sunt asemănătoare cu cele utilizate la
funcţia printf. În acest caz este realizată conversia inversă faţă de cea realizată de funcţia
printf.

Litera Tipul conversiei realizate

d - data din câmpul de intrare este un şir de cifre zecimale, precedat eventual de un
semn şi se converteşte din zecimal extern în binar de tip int
0 - ca şi în cazul literei d, dar din octal extern în binar de tip int.
x - ca şi în cazul literei d, dar din hexazecimal extern în binar de tip int; se utilizează
literele mici a-f sau mari A-F pentru cifrele mai mari ca 9.
X - ca şi în cazul literei x;
u - data este un şir de cifre zecimale care formează un număr întreg fără semn şi se
converteşte din zecimal extern în binar tipul unsigned.
c - data se consideră formată din caracterul curent de la intrare şi parametrului
corespunzător i se atribuie codul ASCII al acestui caracter; în acest caz nu se face
avans peste caracterele albe, ca şi în cazul celorlalţi specificatori.
s - data se consideră că este şir de caractere; şirul începe, conform regulii generale, cu
primul caracter care nu este alb şi se termină la caracterul după care urmează un
caracter alb sau când s-au citit atâtea caractere câte indică dimensiunea maximă din
specificatorul de format;
f - data de intrare reprezintă un număr flotant; deci conversie din flotant extern în
virgulă flotantă simplă precizie; data care se citeşte conţine un punct sau un exponent,
sau atât punct cât şi exponent.

Literele d, o, x şi u pot fi precedate de litera l şi în acest caz conversia se realizează


spre long, în loc de int.
De asemenea, litera f va fi precedată de litera l pentru a face conversii spre formatul
virgulă flotantă dublă precizie.
Lista_adrese conţine adresele zonelor receptoare ale datelor citite prin intermediul
funcţiei scanf. Fiecare dintre parametri reprezintă adresa unei zone receptoare sub forma:

&nume

Ea determină adresa zonei de memorie rezervată variabilei nume. Caracterul “&” din
36
construcţia de mai sus reprezintă un operator unar, numit operatorul adresă. El are aceeaşi
prioritate ca şi ceilalţi operatori unari din limbajul C.

Exemplu:
void main (void)
{ int i;
long n;
float x;
scanf (“ %d %ld %f ”,&i,&n,&x); // citeste datele din stdin si le atribuie lui i,n,x.
scanf(“ %d %*ld %f “, &i,&x); // caracterul * indica faptul ca valoarea pentru variabila n
// nu se citeste
}

Funcţia scanf returnează numărul de câmpuri citite corect din fişierul stdin. Dacă
apare o eroare la citire (din diverse motive de exemplu neconcordanţă dintre specificatorul de
format şi datele din fişierul stdin) atunci funcţia nu va returna numărul total de câmpuri;
citirea se va întrerupe în locul detectării erorii şi scanf va returna numărul de câmpuri citite
până în acel moment. Deci de multe ori pentru a valida formal intrarea datelor (atenţie nu e o
validare calitativă) se va folosi o construcţie de forma:

nr=scanf(...)

Prin construcţia de mai sus se poate pune în evidenţă sfârşitul de fişier, deoarece în
acest caz scanf returnează valoarea EOF. Sfârşitul de fişier se poate genera de la tastatură
acţionând în acelaşi timp tastele CTRL şi Z (deci CTRL / Z). Deorece funcţia scanf citeşte
datele de la intrarea standard prin intermediul unei zone de memorie intermediare (numită
zonă tampon sau buffer), ea are acces la caracterele din zona tampon numai după acţionarea
tastei ENTER (RETURN). De aceea după acţionarea combinaţiei CTRL / Z se va tasta şi
ENTER. Totodată această intermediere face posibilă eventuale corecturi înainte de a acţiona
tasta ENTER.

Exemplu:
Vom citi un întreg de cinci cifre de la intrarea standard şi vom afişa cifrele respective
precedate fiecare de două spaţii.

void main (void)


{ int c1,c2,c3,c4,c5; // c1,c2,c3,c4,c5 sunt cifrele intregului citit
scanf(“%1d %1d %1d %1d %1d”, &c1,&c2,&c3,&c4,&c5);
// se citesc cifrele intregului in cele 5 variabile cu %1d
printf(“%3d%3d%3d%3d%3d”,c1,c2,c3,c4,c5);
}

Pentru a citi şiruri de caractere se va folosi funcţia scanf fără a mai pune operatorul de
adresă în faţa numelui şirului de caractere, deoarece numele unui tablou reprezintă un pointer
(deci conţine o adresă şi anume adresa primului element de tablou).
Exemplu:
Vom citi numele şi prenumele unei persoane şi le afişăm pe ecran.

#define MAX 20
void main(void)
{ char nume[MAX+1], prenume[MAX+1]; //declararea tablourilor de caractere
scanf (“%20s %20s”,nume, prenume); //nu s-a mai pus operatorul de adresa
printf (“\nnumele: %s, prenumele: %s”,nume,prenume);

37
}

4.3. Funcţia standard putchar


Funcţia standard putchar se poate utiliza pentru a scrie un caracter în fişierul standard
de ieşire stdout, în poziţia curentă a cursorului. Ea se apelează folosind o instrucţiune de apel
de forma:
putchar (expresie);

unde expresie este codul ASCII al caracterului care se scrie la ieşirea standard.
Practic putchar nu este o funcţie în sensul definiţiei ce am dat-o în lecţia 1, ci este un
macrou definit în fişierul header stdio.h, care foloseşte o funcţie specială destinată prelucrării
fişierelor, funcţia putc, astfel:

#define putchar(c) putc(c,stdout)


Exemplu:
void main (void)
{ putchar (‘A’); // scrie caracterul A in fisierul de iesire in poziia curenta a cursorului
putchar (‘A’+2) // scrie caracterul de cod ASCII ‘A’+2=65+2=67, adica litera C
putchar (‘\n’); // scrie caracterul newline; deci inceput de linie nouă
}

4.4. Funcţia standard getchar


Această funcţie citeşte de la intrarea standard, stdin, caracterul curent şi returnează
codul ASCII al caracterului citit. Tipul valorii returnate este int. La întâlnirea sfârşitului de
fişier (CTRL/Z) se returnează valoarea EOF (adică -1). Ca şi putchar, getchar este un macrou
definit în fişierul header stdio.h, cu ajutorul unei funcţii speciale, getc destinate prelucrării
fişierelor, astfel:

#define getchar() getc(stdin)

De obicei getchar se foloseşte în expresii de atribuire de forma:


c=getchar();
După citirea caracterului curent de la intrarea standard se atribuie variabilei c codul ASCII al
caracterului citit sau EOF la întâlnirea sfârşitului de fişier.

Exemple:
1)
#include <stdio.h>
void main (void)
{ putchar(getchar() – ‘A’ + ‘a’); // citeste o litera mare si o rescrie ca litera mica }

2) exemplul următor testează dacă s-a citit o literă mare şi numai în acest caz aplică
transformarea ei în literă mică. În cazul în care la intrare nu se află o literă mare se rescrie
caracterul respectiv.

#include <stdio.h>
void main (void)
{ intc c;
putchar(((c = getchar() )>= ‘A’ && c<= ‘Z’) ? c-‘A’+’a’ :c);
}

38
4.5. Funcţiile standard getche şi getch
Funcţia getche citeşte de la intrarea standard caracterul curent şi returnează codul
ASCII al caracterului citit. Această funcţie are acces direct la caracter după ce a fost tastat şi
are forma:
getche();

Funcţia getch este similară cu getche cu singura condiţie că citirea se face fără ecou
(deci caracterul tastat nu se reafişează pe terminal). De fapt prezenţa/absenţa sufixului e
precizează că funcţia e sau nu cu ecou. Cele două funcţii fac citiri fără intermedierea zonei
tampon.

39
5. Instrucţiuni
5.1. Scurt istoric al metodelor de programare
Vom prezenta în continuare câteva metode de programare dar nu exhaustiv, nefiind
aici cadrul adecvat (eventual într-un curs de Software Engineering). O clasificare cronologică
a ceea ce vrem să prezentăm ar fi următoarea:
a) programarea artizanală;
b) programarea procedurală;
c) programarea modulară;
d) programarea structurată;
e) programarea prin abstractizarea datelor;
f) programarea orientată spre obiecte.

5.1.1. Programare artizanală

Această metodă de fapt nu este o metodă propriu-zisă ci este prima modalitate de programare
odată cu apariţia calculatoarelor. Intuiţia şi experienţa programatorului joacă un rol important.
Fiecare programator îşi are propriile reguli de programare. Programele sunt monolitice (un
singur corp de instrucţiuni), lungi şi greu de înţeles de alt programator. Însuşi cel ce a
elaborat un astfel de program întâmpină dificultăţi de înţelegere a propriului program după un
timp oarecare.

5.1.2. Programare procedurală

Odată cu apariţia primelor limbaje de înalt nivel se utilizează programarea


procedurală. Necesitatea ca anumite secvenţe de program să fie folosite de mai multe ori duce
la organizarea acestora în unităţi distincte de program numite în diverse limbaje
subprograme, subrutine, proceduri, etc. De multe ori procedurile trebuie să fie generale
deci procesarea să facă abstractizare de valorile datelor. De exemplu o procedură de calcul al
radicalului de ordinul 2 trebuie să calculeze acest lucru din orice număr real pozitiv iar pentru
cele negative să semnaleze eroare. Procedurile trebuie deci parametrizate cu anumite variabile
numite parametri formali. Valorile de la apel ale parametrilor formali se numesc parametri
efectivi. Programarea procedurală are la bază deci utilizarea procedurilor, iar acestea
realizează o abstractizare prin parametri. La apelare o procedură funcţionează după
principiul cutiei negre (black box): se cunosc intrările şi ieşirile rezultate din acestea dar nu
şi modul de transformare care nu e important în acest moment.
În majoritatea limbajelor procedurale de programare se consideră 2 categorii de proceduri:
- proceduri care definesc o valoare de revenire (denumite şi funcţii);
- proceduri care nu definesc o valoare de revenire.
În limbajele C şi C++ procedurile de ambele categorii se numesc funcţii.

40
5.1.3. Programare modulară

Pe măsură ce complexitatea aplicaţiilor a crescut, a apărut ideea de a descompune problemele


în subprobleme mai simple care la rândul lor pot fi descompuse în altele mai simple şi aşa
mai departe. În felul acesta se ajunge la o descompunere arborescentă a problemei date în
subprobleme mai simple. Programarea subproblemelor devine o problemă mai simplă şi
fiecare subproblemă are o anumită independenţă faţă de celelalte subprobleme. De asemenea,
interfaţa ei cu celelalte subprobleme este limitată şi bine precizată prin procesul de
descompunere a problemei iniţiale. De obicei, programarea unei subprobleme, componentă a
descompunerii arborescente a problemei iniţiale, conduce la realizarea unui număr relativ mic
de proceduri (funcţii). Aceste funcţii pot prelucra în comun anumite date. Unele dintre ele
sunt independente de funcţiile realizate pentru alte subprobleme componente ale
descompunerii arborescente. Altele realizează chiar interfaţa cu subproblemele învecinate.
Despre funcţiile obţinute în urma programării unei subprobleme se obişnuieşte să se spună că
sunt înrudite. De obicei, aceste funcţii, împreună cu datele pe care le prelucrează, se
păstrează într-un fişier şi se compilează independent.
O colecţie de funcţii înrudite, împreună cu datele pe care le prelucrează în comun formează
un modul. În felul acesta, problema iniţială se realizează printr-un program alcătuit din
module.
Programarea modulară are la bază elaborarea programelor pe module.
O parte din datele utilizate în comun de funcţiile modulului, sau chiar toate datele modulului,
nu sunt necesare şi în alte module. Aceste date pot fi protejate sau cum se mai spune,
ascunse în modul.
Limbajul C şi C++, permite ascunderea datelor în modul folosind date care au clasa de
memorie static. Mai mult, pot fi declarate şi funcţiile ca statice şi atunci ele vor fi ascunse în
modul (nu pot fi apelate din afara modului). Ascunderea funcţiilor în modul se face pentru
acele funcţii care nu se utilizează la realizarea interfeţei modulului cu celelalte module.
Ascunderea datelor şi funcţiilor în module permite protejarea datelor şi preîntâmpină
utilizarea eronată a funcţiilor.

5.1.4. Programare structurată

Descompunerea unei probleme în subprobleme mai simple se poate face succesiv în mai
multe etape, până când subproblemele sunt direct programabile sub forma unor proceduri sau
module. Această descompunere succesivă se mai numeşte rafinare pas cu pas (stepwise
refinement).. Evident că se obţine o descompunere arborescentă. Procedurile se pot organiza
sau nu în module. În cadrul procedurilor se folosesc anumite structuri de control a execuţiei.
Aceasta impune o anumită disciplină a programării. Structurile de control de sunt:
a) secvenţa;
b) iteraţia (pretestată, posttestată, cu număr prestabilit de ciclări);
c) alternativa (simplă, completă, generalizată).

Instrucţiunea de bază (primitivă) în cadrul acestor structuri de control este instrucţiunea


de atribuire.
Această abordare a programării s-a născut din necesitatea eliminării instrucţiunii de
control GO TO care face saltul necondiţionat la o instrucţiune precizată, alta decât
instrucţiunea următoare ei. Profesorul Dijsktra de la Universitatea din Eindhoven spunea, prin
anul 1965, “calitatea unui programator este invers proporţională cu numărul de instrucţiuni
GO TO folosite” şi a impus D-structurile de control:

41
a) secvenţa;
b) iteraţia pretestată;
c) alternativa simplă.

D-structurile se regăsesc în toate limbajele procedurale. Corespondenţa ar fi:


a) secvenţa este echivalentă cu execuţia instrucţiunilor în ordinea scrierii lor în
programul sursă;
b) iteraţia pretestată echivalentă cu WHILE ... DO;
c) alternativa simplă echivalentă cu IF ... THEN.

O ilustrare grafică a celor trei D-structuri se dă în continuare.

da
S1 C

nu da
S2 S C

S nu
.

Sn

S-a demonstrat ulterior (Bohm şi Jacopini) că orice algoritm se poate descrie doar cu
D-structurile dar pentru o mai bună lizibilitate şi înţelegere a programelor sursă s-au adăugat
şi iteraţia postestată (REPEAT ... UNTIL), iteraţia cu număr prestabilit de ciclări (FOR ...
DO), alternativa completă (IF ... THEN ... ELSE) şi alternativa generalizată (CASE ... OF).
În unele limbaje se folosesc şi alte structuri pe lângă cele de mai sus pentru o cât mai fidelă
reflectare a algoritmului.

5.1.5. Programare prin abstractizarea datelor

În toate aceste tehnologii anterioare se urmăreşte mai mult organizarea programului şi


mai puţin rezolvarea cât mai naturală a problemei. Programarea prin abstractizarea datelor şi
programarea orientată spre obiecte propun metodologii în care conceptele deduse din analiza
problemei să poată fi reflectate cât mai fidel în program şi să se poată manevra cu instanţieri
ale acestor concepte cât mai natural. Se realizează o mai mare fidelitate a programului faţă de
problemă. De exemplu dacă într-o problemă în care se procesează numere complexe e nevoie
să se lucreze într-o formă cât mai apropiată cu forma matematică se poate introduce tipul
COMPLEX (tip care nu există în limbajele de programare) şi apoi se pot declara variabile de
acest tip. Mai mult ar trebui să se poată face toate operaţiile matematice asupra datelor de tip
COMPLEX. În general un TAD (Tip Abstract de Date) are două componente fundamentale:
- datele membru (reflectă reprezentarea tipului);
- funcţiile membru (reflectă comportamentul tipului).

42
5.1.6. Programarea orientată spre obiecte

Un neajuns al programării prin abstractizarea datelor este faptul că nu permite exprimarea


legăturilor dintre diferite concepte (TAD-uri). Singura legătură dintre concepte care se poate
exprima, este aceea că datele membru ale unei clase pot fi obiecte ale unei alte clase. Acest
lucru nu este suficient în cazul în care conceptele sunt strâns dependente între ele formând
structuri ierarhice. Exprimarea ierarhiilor conduce la atribute suplimentare cum sunt cele de
moştenire. Aceste atribute conduc la un nou model de programare pe care îl numim
programare orientată obiect.
În vârful unei ierarhii se află fenomenul sau forma de existenţă care are trăsături comune
pentru toate celelalte componente ale ierarhiei respective. Pe nivelul următor al ierarhiei se
află componentele care pe lângă trăsăturile comune de pe nivelul superior, mai au şi trăsături
suplimentare, specifice. O ierarhie, de obicei, are mai multe nivele, iar situarea unui element
pe un nivel sau altul al ierarhiei este uneori o problemă deosebit de complexă. Dăm în
exemplul următor o ierarhie arborescentă pentru conceptele de paralelogram, dreptunghi,
romb şi pătrat.

Paralelogram

Dreptunghi Romb

Pătrat

Dacă paralelogramul se află în vârful ierarhiei atunci pe nivelul imediat inferior se aşează
dreptunghiul (paralelogramul cu un unghi drept) dar şi rombul (paralelgramul cu 2 laturi
alăturate congruente). Apoi pătratul se poate defini fie ca un dreptunghi cu laturile
congruente fie ca un romb cu un unghi drept. Conceptul de pe fiecare nivel se observă că
moşteneşte proprietăţile conceptului imediat superior din care este derivat.
La ora actuală, toate ramurile cunoaşterii ştiinţfice sunt pline de ierarhii rezultate în urma
clasificării cunoştinţelor acumulate în perioada lungă de observare a fenomenelor şi formelor
de existenţă a lumii materiale şi spirituale. Clasificările ierarhice ale cunoştinţelor pot fi
întâlnite atât în domeniile care pleacă de la cele mai concrete forme ale lumii materiale, cum
sunt botanica, zoologia, biologia, etc cât şi în domenii care studiază concepte dintre cele mai
abstracte, cum ar fi matematica sau filozofia.
Aceste ierarhii sunt rezultatul definirii conceptelor după regula includerii “genul
proxim şi diferenţa specifică”.
Limbajul C dispune de un set bogat de instrucţiuni care permit scrierea de:
- programe structurate,
- programe flexibile,
- programe compacte.
Totodată limbajul C permite aplicarea metodelor de programare procedurală, programare
modulară şi programare structurată. Pe lângă aceste metodologii limbajul C++ permite şi
programarea prin abstractizarea datelor şi programarea orientată spre obiecte.
43
Vom descrie în continuare instrucţiunile limbajului C. Ca o caracteristică sintactică
toate instrucţiunile limbajului se termină prin caracterul “;”, excepţie făcând instrucţiunile
care se termină cu acolada închisă.

5.2. Instrucţiunea vidă


Instrucţiunea vidă se reduce la caracterul “;”. Ea nu are nici un efect. Adesea este
nevoie de ea la construcţii în care se cere prezenţa unei instrucţiuni, dar nu este necesar să se
execute nimic în punctul respectiv.

5.3. Instrucţiunea expresie


Instrucţiunea expresie se obţine scriind punct şi virgulă după o expresie, deci:

expresie;

Există cazuri particulare ale instrucţiunii expresie:


1) expresia de atribuire, care de altfel este cel mai important caz particular al instrucţiunii
expresie:
v = expresie;
2) apelul unei funcţii:
nume_funcţie (par1, par2, . . . parn);
3) incrementările şi decrementările pre şi post fixate:
variabilă++; ++variabilă;
variabilă- -; - - variabilă;
Exemplu:

void main (void)


{ int i;
float f;
double d;
i=10; // instructiune de atribuire
i++; // i se mareste cu 1
f=i; // instructiune de atribuire (valoarea lui i se converteste in float)
d=++f; // incrementare lui f si atribuirea valorii lui d
putchar(‘a’); // instructiune de apel
}

5.4. Instrucţiunea compusă


Instrucţiunea compusă este o succesiune de declaraţii urmate de instrucţiuni,
succesiune inclusă între acolade:
{
declaraţii
instrucţiuni
}

Pot lipsi declaraţiile sau instrucţiunle dar nu în acelaşi timp. Dacă declaraţiile sunt
prezente, atunci ele definesc variabile care sunt valabile numai în instrucţiunea compusă
respectivă.
44
Exemplu:
...
{ int i=100; // variabila i este definita in aceasta instructiune compusa
i++; // i are valabilitate doar intre acolade; dupa acolada inchisa i isi
printf (“i=%d\n”,i); // pierde valabilitatea
}

Observaţii:
1o. După acolada inchisă a unei instrucţiuni compuse nu se pune ”;”.
2o. Corpul unei funcţii are aceeaşi structură ca şi instrucţiunea compusă, deci o funcţie are
formatul:

antetul funcţiei
instrucţiune compusă

5.5. Instrucţiunea if
Instrucţiunea if permite să realizăm o ramificare a execuţiei în funcţie de valoarea
unei expresii. Ea are două formate ce permit aplicarea structurii de alternativă simplă şi
compusă.
Formatul 1:
if (expresie) instructiune;

Efectul:
1) se evaluează expresia din paranteze;
2) dacă valoarea expresiei este diferită de zero (deci conform convenţiei are valoarea
adevărat), atunci se execută instructiune, altfel se trece la instrucţiunea următoare.

Formatul 2:
if (expresie) instructiune_1;
else instructiune_2;

Efectul:
1) se evaluează expresia din paranteze;
2) dacă valoarea expresiei este diferită de zero (deci conform convenţiei are valoarea
adevărat), atunci se execută instructiune_1, altfel se execută instructiune_2; apoi în
ambele cazuri se trece la instrucţiunea următoare.

Observaţii:
1o. Se pot folosi instrucţiuni if imbricate, nivelul de imbricare fiind oarecare (deci nu există o
limitare a numărului de imbricări).
2o. Pentru mai multe imbricări se foloseşte regula asocierii if-lui cu else astfel:
un else se pune în corespondenţă cu primul if care se află înaintea lui în textul sursă şi nu este
inclus în instrucţiunea care îl precede pe el şi nici nu îi corespunde deja un else.

Exemple

void main (void)


{ float x,y,a;
x=-5;
45
y=10;
if (x<0) // ultimul else se asociaza cu primul if iar
if (y<0) a=1; // penultimul else se asociaza cu cel de-al doilea if
else a=2;
else a=0;
}

void main (void)


{ float x,y,a;
x=-5;
y=10;
if (x<0) // ultimul else se asociaza cu primul if deoarece cel de-al
{ if (y<0) a=1; } // de-al doilea if este inclus in instructiunea compusa care
else a=0; // il precede pe else
}

void main (void) // citeste trei intregi si scrie minimul dintre ei


{int i,j,k,min;
scanf (“\ndati i=%d”, &i);
scanf (“\ndati j=%d”, &j);
scanf (“\ndati k=%d”, &k);
if (i>j) min=j;
else min=i;
if (k<min) min=k;
printf (“min(%d,%d,%d)= %d\n”,i,j,k,,min);
}

5.6. Instrucţiunea while


Instrucţiunea while are următorul format:

while (expresie) instructiune;

Cu ajutorul instrucţiunii while se realizează structura repetitivă pretestată


(condiţionată anterior).
Efectul:
1) se evaluează valoarea expresiei din paranteze;
2) dacă expresia are valoarea diferită de zero, atunci se execută instructiune şi se reia
punctul 1), altfel se trece la instrucţiunea următoare instrucţiunii while.

Deci instructiune se execută repetat atâta timp cât expresia din paranteză este diferită de
zero. Se observă că dacă expresia are valoarea zero de la început, atunci instructiune nu se
execută niciodată.
Antetul ciclului while este construcţia while (expresie) iar instructiune formează corpul
ciclului. În cazul în care este necesar să se execute repetat mai multe instrucţiuni, se utilizează
o instrucţiune compusă formată din instrucţiunile respective.

Exemplu:
Vom crea un program care citeşte un întreg n şi scrie n!. Algoritmul în pseudocod:

Citeste n
f=1
i=2
CâtTimp i<=n execută
f=f*i;
46
i=i+1
SfârşitCâtTimp
Scrie n,f

Programul în C este:

#include<stdio.h>
void main (void)
{ int n,i;
double f;
f=1.0;
i=2;
printf(“\n dati n= “);
scanf(“%d”,&n);
while (i<=n)
{ f=f*i;
i++;
}
printf(“\nn=%d, iar n!=%g\n”,n,f);
}

Corpul ciclului while se poate scrie mai compact astfel:

while (i<=n) f*=i++;

5.7. Instrucţiunea for


Instrucţiunea for, ca şi instrucţiunea while, se utilizează pentru a realiza o structură
repetitivă pretestată. Formatul instrucţiunii este:

for(exp1; exp2; exp3) instructiune;

Antetul ciclului este definit de for(exp1; exp2; exp3) iar instructiune formează corpul
ciclului. Prima expresie exp1 constituie partea de iniţializare a ciclului, iar exp3 este partea de
reiniţializare a ciclului. Condiţia de continuare a ciclului este exp2. De obicei exp1 şi exp3
reprezintă atribuiri.
Efectul:
1) se execută secvenţa de iniţializare definită de expresia exp1;
2) se evaluează exp2; dacă exp2 are valoarea zero, atunci se iese din ciclu, adică se trece la
instrucţiunea următoare instrucţiunii for, altfel se execută instrucţiunea din corpul
ciclului;
3) se execută apoi secvenţa de reiniţializare definită de exp3, apoi se reia secvenţa de la
punctul 2).
Observaţii:
1o. Ca şi în cazul instrucţiunii while, instrucţiunea din corpul ciclului for poate să nu se
execute niciodată dacă exp2 are valoarea zero chiar la prima evaluare.
2o. Expresiile din antetul instrucţiunii for pot fi şi vide; totuşi caracterele “;” vor fi
întotdeauna prezente.
3o. Comparând instrucţiunile for şi while observăm că instrucţiunea for cu formatul anterior
se poate realiza cu secvenţa următoare folosind while:

exp1;
47
while (exp2)
{ instructiune;
exp3;
}
Invers, o instrucţiune while de forma: while (exp) instructiune este echivalentă cu
următoarea instrucţiune for:
for(; exp; ) instructiune.

Autorii limbajului C propun ca instrucţiunea for să se folosească cu prioritate pentru


ciclurile care au pas.
Exemple:
Vom da o secvenţă de instrucţiuni care însumează elementele unui tablou:

s=0;
for(i=0; i<n; i++) s=s+tab[i];
sau scrisă mai compact:
for (s=0, i=0; i<n; i++) s+=tab[i];
În continuare vom da un mic program care afişează numărul caracterelor citite de la
intrarea standard stdin.
#include <stdio.h>
void main(void)
{ long n;
for (n=0; getchar()!=EOF; n++);
printf (“\nnumarul caracterelor citite =%ld\n”,n);
}
sau scrisă cu instrucţiunea while

#include <stdio.h>
void main(void)
{ long n=0;
while (getchar()!=EOF) n++;
printf (“\nnumarul caracterelor citite =%ld\n”,n);
}

5.8. Instrucţiunea do while


Această instrucţiune realizează structura repetitivă condiţionată posterior (posttestată)
dar modificată faţă de REPEAT . . . UNTIL. Această instrucţiune s-a introdus pentru o mai
mare flexibilitate în scrierea programelor. Formatul ei este:
do
instructiune;
while (exp);
Efectul:
1) se execută instrucţiunea instructiune;
2) se evaluează expresia exp din paranteze;
3) dacă valoarea expresiei este zero se trece la instrucţiunea următoare instrucţiunii do-
while; altfel se revine şi se execută din nou instructiune.

Observaţii:
1o. Structura realizată de instrucţiunea do-while poate fi realizată printr-o secvenţă în care
se foloseşte instrucţiunea while astfel:
instructiune;
48
while (exp) instructiune;
o
2 . Se observă că în cazul instrucţiunii do-while, corpul ciclului se execută cel puţin odată,
spre deosebire de ciclurile while şi for unde corpul ciclului poate să nu se execute niciodată.
Exemplu:
Vom da un program care calculează rădăcina pătrată dintr-un număr real a>=0.

#include<stdio.h>
#define EPS 1e-10
void main (void)
{ double x1,x2,y,a;
clrscr(); // sterge ecranul
printf(“\ndati un numar real pozitiv a=”);
if (scanf(“%lf”,&a) !=1 || a<0) printf (“numarul citit nu este pozitiv\n”);
else {
x2 = 1.0;
do { x1 = x2;
x2 = 0.5 *(x1+a/x1); // formula de iteratie
if ((y=x2-x1) < 0) y = -y;
}
while (y >= EPS);
printf (“radacina patrata din:%g este: %.2lf\n”,a,x2); // 2 zecimale
} //sfirsit else
}

5.9. Instructiunea switch


Instrucţiunea switch permite realizarea structurii alternativa generalizată. Ea este
echivalentă cu o imbricare de structuri de alternativă simple. Utilizarea instrucţiunii switch
face în schimb programul mult mai clar.
Formatul instrucţiunii switch este următorul:

switch (exp)
{ case c1: sir1
break;
case c2: sir2
break;
...
case cn: sirn
break;
default: sir
}

unde: c1,. . . cn sunt constante sau constante simbolice;


sir1, . . . ,sirn, sir sunt şiruri de instrucţiuni.

Efectul:
1) se evaluează expresia din paranteză;
2) se compară pe rând valoarea expresiei cu valorile constantelor c1, . . . , cn;
3) dacă valoarea expresiei coincide cu valoarea lui ck, se execută secvenţa de instrucţiuni
definită prin sirk; în cazul în care valoarea expresiei nu coincide cu nici una din
constantele c1, . . . , cn, se execută secvenţa de instrucţiuni definită prin sir;
4) după execuţia secvenţei sirk sau sir se trece la instrucţiunea următoare instrucţiunii
switch, adică la prima instrucţiune aflată după acolada închisă care termină instrucţiunea
switch respectivă; evident, acest lucru are loc dacă şirul care se execută nu impune, el
49
insuşi, un alt mod de continuare a execuţiei, de exemplu o revenire din funcţia respectivă,
un salt la o anumită instrucţiune, etc.
Observaţii:
1o. Ramura default nu este obligatorie. În lipsa ei, dacă valoarea expresiei nu coincide cu nici
una din constantele c1,. . . , cn, instrucţiunea switch respectivă nu are nici un efect.
2o.Construcţia break reprezintă o instrucţiune. Ea termină fiecare ramură de instrucţiuni
sir1, . . . , sirn, provocând saltul la instrucţiunea următoare instrucţiunii switch sau, cum se
mai spune, realizează ieşirea din instrucţiunea switch.
3o. Instrucţiunea break nu este obligatorie. În cazul în care este absentă, se execută secvenţial
următoarea ramură. De exemplu dacă avem secvenţa:
switch (exp)
{ case c1: sir1
case c2: sir2
}
ea se execută în felul următor:
- dacă valoarea expresiei este egală cu c1 se execută sir1 şi apoi sir2;
- dacă valoarea expresiei este egală cu c2 se execută sir2;
- dacă valoarea expresiei diferă de valorile c1 şi c2 instrucţiunea switch de mai sus
nu este efectivă, se trece la instrucţiunea următoare care urmează după switch.
- secvenţa de mai sus se putea realiza şi astfel:
if (exp = = c1)
{ sir1
sir2
}else if (exp = = c2) sir2

Exemplu:
Vom citi din fişierul de intrare construcţii de forma: op 1 operator op2, unde op1 şi op2 sunt
numere întregi (operanzi întregi) iar operator este un operator aritmetic {“+”, “-“, “*”, “/”}.
La ieşire se va scrie valoarea expresiei citite. De exemplu dacă se citeşte secvenţa 100/3 se
va afişa rezultatul 33. Programul permite citirea şi evaluarea mai multor astfel de expresii,
până la întâlnirea sfârşitului de fişier.

#include <stdio.h>
void main (void)
{ int op1,op2,operator,rezultat,i;
while (( i=scanf(“%d %c %d”, &op1,&operator, &op2)) != EOF)
if (i = = 3 ) // ramura adevarat inseamna ca s-au citit 3 campuri corecte
{ switch (operator)
{ case ‘+’: rezultat = op1 + op2 ; // avem adunare
break;
case ‘-‘ : rezultat = op1 – op2; // avem scadere
break;
case ‘*’ : rezultat = op1 * op2; // avem inmultire
break;
case ‘/’ : // avem impartire intreaga
if (op2 = = 0)
{ printf (“divizor nul\n”);
rezultat = 0;
} else rezultat = op1 / op2;
break;
default : printf (“operator eronat\n”);
rezultat = 0;
} // sfarsit switch
printf (“%d %c %d %d\n”, op1, operator, op2, rezultat);
} else
50
printf (“expresie eronat\n”); // sfarsit if
} // sfarsit while

5.10. Instrucţiunea break


Formatul instrucţiunii este următorul:

break;

De obicei instrucţiunea break se foloseşte pentru a ieşi dintr-un ciclu. Dacă există mai
multe cicluri imbricate instrucţiunea break va trece controlul la ciclul de nivel imediat
superior (deci imbricarea rămâne, nu se iese din toate ciclurile). O altă utilizare este în
instrucţiunea switch, după cum am observat în paragraful anterior.
Un alt exemplu de utilizare frecventă este ieşirea dintr-un ciclu infinit de forma:

for ( ; ; )
{. . .
if (exp) break;
...
}

5.11. Instrucţiunea continue


Formatul instrucţiunii este următorul:

continue;

Efectul:
1) în ciclurile while şi do-while ea realizează saltul la evaluarea expresiei care decide asupra
continuării ciclului;
2) în ciclul for ea realizează saltul la pasul de reiniţializare.
Observaţie:
1o. Instrucţiunea continue se utilizează numai în corpul unui ciclu, permiţând, după caz, să se
treacă la pasul următor al ciclului sau să se iasă din ciclu.

5.12. Instrucţiunea goto


Conform principiilor programării structurate instrucţiunea goto nu ar fi necesară. Dar
ea a fost introdusă în limbaj, deoarece, în anumite cazuri, se dovedeşte a fi utilă, asigurând o
flexibilitate mai mare în programare. De multe ori ieşirea dintr-un ciclu imbricat în alte
cicluri se realizează mai simplu cu ajutorul instrucţiunii goto. În lipsa ei ar trebui să folosim
mai mulţi indicatori şi teste asupra valorilor acestora pentru ieşirea din ciclu. Saltul făcut de
goto se face la o instrucţiune care este prefixată de o etichetă.

51
Prin etichetă vom înţelege un nume urmat de caracterul “:”. Etichetele sunt locale unei
funcţii.
Instrucţiunea goto are următorul format:

goto eticheta;

Efectul:
1) se realizează saltul la instrucţiunea prefixată de eticheta al cărei nume se află scris după
cuvântul cheie goto.

5.13. Apelul şi revenirea dintr-o funcţie


5.13.1. Apelul unei funcţii

În limbajul C funcţiile sunt de două tipuri:


- funcţii care returnează o valoare la revenirea din ele;
- funcţii care nu returnează nici o valoare la revenirea din ele.
O funcţie care nu returnează nici o valoare la revenirea din ea se apelează printr-o
instrucţiune de apel. Ea are următorul format:

nume (lista_parametrilor_efectivi); (*)


unde:
-nume este numele funcţiei;
-lista_parametrilor_efectivi este fie vidă, fie se compune din una sau mai multe
expresii separate prin virgulă.
Instrucţiunea de apel este un caz particular al instrucţiunii expresie. Parametrii efectivi
(de la apel) trebuie să corespundă cu cei formali (de la definirea funcţiei) prin ordine, tip şi
număr.
În cazul în care o funcţie returnează o valoare, ea poate fi apelată fie printr-o
instrucţiune de apel, fie sub forma unui operand al unei expresii.

Observaţii:
1o. Dacă nu dorim să utilizăm valoarea returnată de funcţia respectivă, apelul se face printr-o
instrucţiune de apel.
2o. Dacă dorim să utilizăm valoarea returnată de funcţie, vom folosi apelul funcţiei drept
operand într-o expresie, operandul având formatul (*).
Exemple de apeluri de funcţii folosite până acum sunt apelurile funcţiilor standard printf,
scanf, getchar şi putchar. Funcţiile printf şi putchar au fost apelate prin instrucţiuni de apel,
valorile returnate de ele nefiind utilizate. În schimb funcţiile scanf şi getchar au fost apelate în
ambele moduri, atât prin instrucţiuni de apel, cât şi ca operanzi în diferite expresii.
5.13.2. Prototipul unei funcţii

O funcţie poate fi apelată dacă ea este definită în fişierul sursă înainte de a fi apelată.
După cum am prezentat în lecţia 1 nu întotdeauna este posibil acest lucru şi în astfel de cazuri
apelul funcţiei trebuie să fie precedat de prototipul ei.
Prototipul unei funcţii are ca scop să informeze compilatorul despre:
- tipul valorii returnate de funcţie;
- tipurile parametrilor.

52
În felul acesta, la apelul unei funcţii, compilatorul poate face teste cu privire la tipul
expresiilor care reprezintă parametrii efectivi, precum şi unele conversii necesare asupra
valorii returnate de funcţie.
Observaţii:
1o. Tipurile parametrilor pot să lipsească. În acest caz, compilatorul nu controlează tipurile
parametrilor efectivi, singura informaţie conţinută de prototip fiind tipul valorii returnate de
funcţia respectivă.
2o. Absenţa atât a prototipului unei funcţii, cât şi a definiţiei funcţiei înainte de a fi apelată
este posibilă; în acest caz se presupune că funcţia returnează o valoare de tip int.
3o. În practică se recomandă utilizarea prototipurilor pentru toate funcţiile înainte de a fi
apelate. În acest scop, ele vor fi scrise la începutul fişierelor sursă.
Formatele posibile ale unui prototip sunt:

formatul 1: tip nume (lista_declaratiilor_de_parametri);


formatul 2: tip nume (lista_ tipurilor_parametrilor);
formatul 3: tip nume (void);
formatul 4: tip nume ();

Formatul 2 este cel mai utilizat. Formatul 3 se poate folosi pentru orice funcţie care nu
are parametri. Formatul 4 se poate folosi pentru orice funcţie la al cărei apel nu se doresc
teste referitoare la tipul parametrilor efectivi.
Funcţiile din biblioteca standard a limbajului C au prototipurile definite în fişierele de
tip .h.

5.13.3. Apel prin valoare şi apel prin referinţă

La apelul unei funcţii, fiecărui parametru formal i se atribuie valoarea parametrului


efectiv care-i corespunde. Deci, la apelul unei funcţii se transferă valorile parametrilor
efectivi. Din această cauză se spune că apelul este prin valoare (call by value). În anumite
limbaje de programare, la apel nu se transferă valorile parametrilor efectivi ci adresele
acestora. În acest caz se spune că apelul este prin referinţă (call by refference).
Între cele două tipuri de apeluri există o diferenţă esenţială şi anume: în cazul apelului
prin valoare funcţia apelată nu poate modifica parametrii efectivi din funcţia apelantă,
neavând acces la ei. În cazul apelului prin referinţă, funcţia apelată, dispunând de adresele
parametrilor efectivi, îi poate modifica.
În limbajul C apelul se realizează implicit prin valoare. În cazul că un parametru este
numele unui tablou atunci transferul se realizează prin referinţă deoarece numele unui tablou
este un pointer şi conţine adresa primului element al tabloului. Transferul prin referinţă se
realizează cu ajutorul variabilelor de tip pointer şi cu ajutorul operatorului de adresă (&).

5.13.4. Revenirea dintr-o funcţie

Revenirea dintr-o funcţie se poate face în două moduri:


- la întâlnirea instrucţiunii return;
- după execuţia ultimei sale instrucţiuni, adică a instrucţiunii care precede acolada
închisă ce termină corpul funcţiei respective.
Instrucţiunea return are două formate:

return; sau return expresie;

53
Primul format se utilizează când funcţia nu returnează o valoare, iar cel de-al doilea
când funcţia returnează o valoare. În acest ultim caz, funcţia returnează valoarea expresiei
specificate.
Observaţie:
1o. Când revenirea se face după execuţia ultimei instrucţiuni a funcţiei nu se returnează o
valoare; revenirea în acest caz, se face ca şi cum acolada închisă de la sfârşitul corpului
funcţiei ar fi precedată de instrucţiunea return.

Exemplu: vom da un exemplu de apel al funcţiei care determină rădacina pătratică dintr-un
număr nenegativ.
#include<stdio.h>
double radacina_2 (double); // prototipul functiei

void main (void) // functia principala care citeste d


// si afiseaza radacina patrata din d
{ double d;
clrscr(); // sterge ecranul
if (scanf (“%lf”,&d) != 1 || d<0)
printf (“numarul dat este eronat\n”);
else
printf (“d=%f, radacina patrata = %.10g\n”, d, radacina_2(d));

#define EPS 1e-10


double radacina_2 (double x)
{ double x1,x2,y;
x2 = 1.0;
do { x1 = x2;
x2 = 0.5 *(x1+x/x1); // formula de iteratie
if ((y=x2-x1) < 0) y = -y;
}
while (y >= EPS);
return x2;
}

Observaţie:
1o. Limbajul C dispune de o bibliotecă matematică în care sunt incluse o serie de funcţii
pentru calculul valorilor funcţiilor elementare. Există o funcţie numită sqrt (cu prototipul
double sqrt (double);). Fişierul care conţine biblioteca matematică se numeşte math.h şi
trebuie inclus în fişierul sursă de lucru dacă se doreşte utilizarea funcţiilor definite în el.

54
6. Pointeri
Un pointer este o variabilă care are ca valori adrese. Pointerii se folosesc pentru a
face referire la date cunoscute prin adresele lor. Astfel, dacă p este o variabilă de tip pointer
care are ca valoare adresa zonei de memorie alocată pentru variabila întreagă x atunci
construcţia *p reprezintă chiar valoarea variabilei x.
În construcţia de mai sus, *p, caracterul * se consideră ca fiind un operator unar care
furnizează valoarea din zona de memorie a cărei adresă este conţinută în p. Operatorul unar *
are aceeaşi prioritate ca şi ceilalţi operatori unari din limbajul C.
Dacă p conţine adresa zonei de memorie alocată variabilei x, vom spune că p
pointează spre x sau că p conţine adresa lui x.
Pentru a atribui unui pointer adresa unei variabile, putem folosi operatorul unar &.
Astfel, dacă dorim ca p să pointeze spre x, putem utiliza construcţia:

p = &x;

În limba română se utilizează şi alte denumiri pentru noţiunea de pointer: referinţă,


localizator; reper; indicator de adresă.

6.1. Declaraţia de pointer


Un pointer se declară ca orice variabilă cu deosebirea că numele pointerului este
precedat de caracterul *. Astfel, dacă, de exemplu, dorim să declarăm variabila p utilizată
anterior pentru a păstra adresa variabilei întregi x, vom folosi declaraţia următoare:

int *p;

Tipul int stabileşte în acest caz faptul că p conţine adrese de zone de memorie alocate
datelor de tip int. Declaraţia lui p se poate interpreta în felul următor: *p reprezintă conţinutul
zonei de memorie spre care pointează p, iar acest conţinut are tipul int.
În general, un pointer se declară prin:

tip *nume;

ceea ce înseamnă că nume este un pointer care pointează spre o zonă de memorie ce conţine
o dată de tipul tip.
Comparând declaraţia de pointer anterioară cu una obişnuită:

tip nume;

putem considera că:


tip *

dintr-o declaraţie de pointer reprezintă tip dintr-o declaraţie obişnuită. De aceea, construcţia

tip *

se spune că reprezintă un tip nou, tipul pointer.

55
Există cazuri în care dorim ca un pointer să fie utilizat cu mai multe tipuri de date. În
acest caz, la declararea lui nu dorim să specificăm un tip anume. Aceasta se realizează
folosind cuvântul cheie void:
void *nume;

Exemple:

1)
void main (void)
{ int x,y;
int *p;
y=x+10; // aceast atribuire este echivalenta cu secventa urmatoare
p=&x;
y=*p+100;

x=y; // este echivalenta cu secventa


p=&x;
*p=y;
}

2) funcţia permutare de mai jos realizează transferul parametrilor prin adresă:

void permutare (int *x, int *y) // x si y sunt pointeri


{ int temp;
temp = *x; // temp ia valoarea ce se afla la adresa continuta in x
*x=*y; // in zona a carei adresa se afla in x se transfera continutul
// zonei a carei adresa se afla in y
*y=temp; // in zona a carei adresa se afla in y se transfera valoarea
// lui temp
}

Apelul funcţiei permutare se face astfel:

permutare (&a, &b);

pentru a schimba valorile lui a cu b.

6.2. Legătura dintre pointeri şi tablouri


Numele unui tablou este un pointer deoarece el are ca valoare adresa primului său
element. Totuşi există o diferenţă între numele unui tablou şi o variabilă de tip pointer, şi
anume unui nume de tablou nu i se poate atribui altă adresă. Deci numele unui tablou trebuie
considerat ca fiind un pointer constant.
Dacă x este un parametru formal ce corespunde unui parametru efectiv care este un
nume de tablou, x poate fi declarat fie ca tablou fie ca pointer spre tipul tabloului.
Exemplu:

Fie funcţia cu antetul următor:


unsigned lungime (char x[ ]);

Să presupunem că această funcţie determină lungimea unui şir de caractere şi se poate apela
prin:
56
l=lungime(tablou);

unde tablou este de tip caracter.


Antetul funcţiei lungime poate fi schimbat în felul următor:

unsigned lungime (char *x);

Cele două declaraţii sunt identice deoarece declaraţia:

char x[ ];

defineşte pe x ca numele unui tablou de tip caracter; dar atunci el este un pointer spre
caractere deci se poate declara prin:
char *x;

6.3. Operaţii cu pointeri


Asupra pointerilor se pot face diferite operaţii. Deoarece ei conţin adrese atunci
operaţiile se realizează cu adrese.

6.3.1. Incrementare şi decrementare

Operatorii de incrementare şi decrementare se pot aplica variabilelor de tip pointer.


Efectul:
- operatorul de incrementare aplicat asupra unui operand de tip pointer spre tipul tip
măreşte adresa conţinută de operand cu numărul de octeţi necesari pentru a păstra
o dată de tipul tip.
- operatorul de decrementare se execută în mod analog, cu singura diferenţă că în
loc să se mărească adresa, ea se micşorează cu numărul corespunzător de octeţi.
De obicei decrementările şi incrementările adreselor sunt mai rapide ca execuţie când
se au în vedere prelucrări de tablouri.

Exemplu:
int tab[10];
int *p;
int i=0;
p=&tab[i];
p++; // p contine adresa lui tab[1]
// cu p se pot face referiri la orice element de tablou

6.3.2. Adunarea şi scăderea unui întreg dintr-un pointer

Dacă p este un pointer, sunt corecte expresiile de forma:


p+n şi p-n
unde n este de tip întreg.

Efectul:
- expresia p+n măreşte valoarea lui p cu n*nr_tip, unde nr_tip este numărul de
octeţi necesari pentru a memora o dată de tipul tip spre care pointează p;

57
- analog expresia p-n micşorează valoarea lui p cu n*nr_tip.

Dacă x este un tablou de tipul tip, atunci x este pointer, deci o expresie de forma:

x+n;

este corectă şi deoarece x este un pointer spre primul său element x[0], x+n va fi un pointer
spre elementul x[n]. Rezultă că valoarea elementului x[n] se poate reprezenta prin expresia:

*(x+n);

Astfel variabilele cu indici se pot înlocui prin expresii cu pointeri. Aceasta permite ca
la tratarea tablourilor să se folosească expresii cu pointeri în locul variabilelor cu indici.
Versiunile cu pointeri sunt de obicei optime în raport cu cele realizate prin intermediul
indicilor.

6.3.3. Compararea a doi pointeri

Doi pointeri care pointează spre elementele aceluiaşi tablou pot fi comparaţi folosind
operatorii de relaţie şi de egalitate. Astfel, dacă p şi q sunt doi pointeri care pointează spre
elementele tab[i], respectiv tab[j] ale tabloului tab, expresiile următoare au sens:

p<q p!=j p= =q.

Observaţii:
1o. Pointerii nu pot fi comparaţi decât în condiţiile amintite mai sus (deci dacă sunt pointeri
spre elementele aceluiaşi tablou).
2o. Operatorii = = şi != permit compararea unui pointer şi cu o constantă simbolică specială
având numele NULL. Aceste comparaţii permit să stabilim dacă un pointer conţine o adresă
sau nu. Astfel, dacă expresia:
p= = NULL

este adevărată, p nu conţine o adresă. Dacă expresia respectivă are valoarea fals atunci p
conţine o adresă. Constanta simbolică NULL este definită în fişierul stdio.h

6.3.4. Diferenţa a doi pointeri

Doi pointeri care pointează spre elementele aceluiaşi tablou pot fi scăzuţi. Rezultatul
diferenţei a doi pointeri este definit astfel: fie t un tablou de un tip oarecare şi p şi q doi
pointeri, p conţine adresa elementului t[i] iar q conţine adresa elementului t[i+n]. Atunci
diferenţa q-p are valoarea n.

6.3.5. Exemple

Vom da câteva funcţii asupra şirurilor de caractere:

58
funcţia lungime

unsigned lungime (char*x)


{ int i;
for (i=0; *x != ‘\0’; i++) x++; // sau for (i=0; *x++; i++);
return i;
}

funcţia copiaza
void copiaza(char *x, char *y) // copiaza din zona de adresa y
// in zona de adresa x
{ while(*x++ = *y++); }

funcţia concateneaza
void concateneaza (char *x, char *y)
// concateneaza sirul de adresa y la sfarsitul sirului
// de adresa x
{ while (*x) x++; // avans de adresa pana la sfarsitul sirului x
while (*x++= *y++);
}

funcţia compara
int compara (char *x, char *y)
{ while (*x= = *y)
{ if (*x= = ‘\0’) return 0;
x++;
y++;
return *x - *y; // daca diferenta caracterelor este
// negativa atunci x<y altfel x>y
}
}

6.4. Alocarea dinamică a memoriei


Biblioteca standard a limbajului C pune la dispoziţia utilizatorului funcţii care permit
alocarea de zone de memorie în timpul execuţiei programului. O astfel de zonă de memorie
poate fi utilizată pentru a păstra date temporare. Zona respectivă poate fi eliberată în
momentul în care nu mai sunt necesare datele care au fost păstrate în ea. Alocarea de zone de
memorie şi eliberarea lor în timpul execuţiei programelor permite gestionarea optimă a
memoriei de către programator. Un astfel de mijloc de gestionare a memoriei îl vom numi
alocare dinamică a memoriei.
Vom indica două funcţii din bibloteca limbajului C utilizate frecvent în alocarea
dinamică a memoriei. Prototipurile lor se află în fişierele standard alloc.h şi stdlib.h, deci
pentru a le utiliza vom include unul din aceste fişiere.
Funcţia malloc permite alocarea unui bloc de memorie a cărui dimensiune se specifică
în octeţi. Funcţia returnează un pointer spre începutul zonei alocate. Întrucât acest pointer
trebuie să permită memorarea oricărui tip de dată în zona alocată, el este de tip void *.
Prototipul funcţiei este:

void *malloc (unsigned n);

59
unde n este numărul de octeţi al zonei de memorie care se alocă. În cazul în care n este prea
mare, funcţia returnează pointerul NULL.

Funcţia free eliberează o zonă de memorie alocată prin malloc. Prototipul ei este:

void free (void *p);

unde p este pointerul returnat de malloc la alocare, deci este pointerul spre începutul zonei
care se eliberează.

Exemplu:

Funcţia memchar memorează un şir de caractere într-o zonă de memorie alocată prin funcţia
malloc. Ea returnează adresa de început a zonei în care s-a salvat şirul de caractere, deci
returnează un pointer spre tipul char.
#include <stdio.h>
#include <alloc.h>
#include <string.h>
char *memchar (char *s)
{ char *p;
if ((p=(char *)malloc(strlen(s)+1) ) != NULL)
{ strcpy (p,s);
return p;
} else
return NULL;
}

Observaţii:
1o. În fişierul stdio.h există definiţia constantei NULL.
2o. Fişierul alloc.h s-a inclus deoarece conţine prototipul funcţiei malloc.
3o. Fişierul string.h conţine prototipurile funcţiilor strlen şi strcpy.
4o. Funcţia malloc se apelează pentru a rezerva strlen(s)+1 octeţi; strlen returnează numărul
de octeţi ocupaţi de caracterele proprii ale lui s (fără caracterul NUL). Cum în zona de
memorie rezervată prin malloc se păstrează şi caracterul NUL, lungimea returnată de funcţia
strlen s-a mărit cu 1.
5o. Pointerul returnat de malloc a fost convertit spre char *, deoarece el este de tip void *.
Acest pointer se atribuie lui p, deci p pointează spre începutul zonei de memorie alocate prin
apelul funcţiei malloc. Se testează dacă acest pointer este diferit de NULL (deci dacă s-a
putut aloca memoria de dimensiunea cerută). În caz afirmativ, se transferă şirul prin apelul
funcţiei strcpy, returnându-se apoi valoarea pointerului p.

6.5. Pointeri spre funcţii


Numele unei funcţii este un pointer spre funcţia respectivă. El poate fi folosit ca
parametru efectiv la apeluri de funcţii. În felul acesta, o funcţie poate transfera funcţiei

60
apelate un pointer spre o funcţie. Aceasta, la rândul ei, poate apela funcţia care i-a fost
transferată în acest fel.

Exemplu:
Un exemplu matematic în care este nevoie de un astfel de transfer este cel cu privire la
calculul aproximativ al integralelor definite. Să presupunem că dorim să calculăm integrala
definită din funcţia f(x), între limitele a şi b, folosind formula trapezului:

I= h((f(a)+f(b))/2 +f(a+h)+f(a+2h)+. . . +f(a+(n-1)h)


unde
h=(b-a)/h.

În continuare construim o funcţie care calculează partea dreaptă a acestei relaţii.


Numim aria_trapez această funcţie.
Observaţii:
1o. Deoarece funcţia f(x) din relaţia de mai sus nu este definită în acest moment, ea trebuie să
figureze printre parametrii funcţiei aria_trapez, împreună cu limitele de integrare şi valoarea
lui n.
2o. Funcţia aria_trapez returnează valoarea aproximativă a integralei şi ea se va apela printr-o
expresie de atribuire, de exemplu:
aria=aria_trapez (a, b, n, f);

3o. Funcţia aria_trapez returnează o valoare flotantă în dublă precizie. De asemenea, şi funcţia
f(x) returnează o valoare flotantă în dublă precizie. De aici rezultă că prototipul funcţiei
aria_trapez este următorul:

double aria_trapez (double a, double b, int n, double (*f)());


sau
double aria_trapez (double, double, int , double (*)());

4o. Este necesar ca înaintea apelului funcţiei aria_trapez funcţia f(x) să fie definită sau să fie
prezent prototipul ei , de exemplu:
double f();

5o. Construcţia double (*f) () se interpretează în felul următor:


- *f înseamnă că f este un pointer;
- (*f)() înseamnă că f este un pointer spre o funcţie;
- double (*f) () înseamnă că f este un pointer spre o funcţie care returnează o valoare
flotantă în dublă precizie.
6o. Trebuie să se includă *f între paranteze, deoarece construcţia double *f(); este corectă,
dar înseamnă altceva, parantezele rotunde fiind prioritare operatorului unar *. În acest caz, se
declară f ca o funcţie ce returnează un pointer spre o valoare flotantă în dublă precizie.
7o. Ultimul parametru formal al funcţiei aria_trapez corespunde parametrului efectiv f şi deci
el trebuie declarat ca şi pointer spre o funcţie ce returnează o valoare flotantă în dublă
precizie. Conform observaţiei 5), dacă p este numele parametrului formal ce corespunde
parametrului efectiv f, atunci p se declară astfel:

double (*p)();

8o. În corpul funcţiei aria_trapez va trebui să apelăm funcţia f(x) pentru a calcula valorile:
f(a), f(b), f(a+h), . . . , f(a+(n-1)h).
61
În momentul programării funcţiei aria_trapez, nu se cunoaşte numele funcţiei concrete, ci
numai pointerul p spre ea. De aceea, vom înlocui numele funcţiei prin *p, deci vom folosi
apelurile:
(*p)(a), (*p)(b), (*p)(a+h), . . . ,(*p)(a+(n-1)h)
double aria_trapez(double x, double y, int m, double(*p)());
{ double h,s;
int i;
h=(y-x)/m;
for (i=1, s=0.0; i<m; i++) s+=(*p)(x+i*h);
s+=((*p)(x) + (*p)(y))/2;
s=h*s;
return s;
}

Vom utiliza funcţia aria_trapez pentru a calcula integrala definită din funcţia sin(x 2) pe
intervalul [0,1], cu o eroare mai mică decât 10-8. Vom nota cu In următoare sumă:

In= h((f(a)+f(b))/2 +f(a+h)+f(a+2h)+. . . +f(a+(n-1)h)

Paşii algoritmului sunt următorii:


Pasul 1. Se alege o valoare iniţială pentru n, de exemplu 10.
Pasul 2. Se calculează In.
Pasul 3. Se calculează I2n prin dublarea lui n.
Pasul 4. Dacă |In-I2n| < 10-8, algoritmul se întrerupe şi valoarea integralei, cu precizia admisă,
este I2n; altfel se dublează n, se pune In=I2n; n, şi se trece la pasul 3.

#define A 0.0
#define B 1.0
#define N 10
#define EPS 1e-8
#include <stdio.h>
#include <math.h>
double sinxp(double); // prototipul functiei sin(x*x)
double aria_trapez(double, double, int, double (*)());
void main (void) // functia principala
{ int n=N;
double in, i2n, vabs;
in=aria_trapez (A, B, n, sinxp);
do {
n=n*2;
i2n=aria_trapez(A, B, n, sinxp);
if ((vabs= in-i2n) < 0) vabs = -vabs;
in=i2n;
} while (vabs >= EPS);
printf (“valoarea integralei este : %g.10\n”,i2n);
}

double aria_trapez(double x, double y, int m, double(*p)());


{ double h,s;
int i;
h=(y-x)/m;
for (i=1, s=0.0; i<m; i++) s+=(*p)(x+i*h);
s+=((*p)(x) + (*p)(y))/2;
s=h*s;
return s;
}

62
double sinxp (double x)
{ return sin (x*x); }

6.6. Tratarea parametrilor din linia de comandă


În linia de comandă folosită la apelul execuţiei unui program se pot utiliza diferiţi
parametri. Aceşti parametri pot fi utilizaţi folosind parametrii argc şi argv ai funcţiei
principale.
Parametrul argc este de tip întreg şi indică numărul de parametri din linia de
comandă.
Parametrul argv este un tablou de pointeri spre zonele în care sunt păstraţi parametrii
liniei de comandă. Aceştia se consideră şiruri de caractere.
Astfel antetul funcţiei principale va fi :

main (int argc, char *argv[ ])

Exemplu:
Considerăm că la lansarea programului prog s-au furnizat parametrii:

31 MARTIE 1956

În acest caz argc=4, iar tabloul argv conţine pointerii:


- argv[0] - pointer spre numele programului (calea, numele şi extensia .EXE
- argv[1] - pointer spre şirul “31”;
- argv[2] - pointer spre şirul “MARTIE”;
- argv[3] - pointer spre şirul “1956”.

Observaţii:
1o. Lansarea unui program se face cu prima instrucţiune a funcţiei principale. Deci parametrii
argc şi argv au deja în acest moment valorile indicate mai sus, putând fi analizaţi chiar cu
prima instrucţiune executabilă.
2o. În mod frecvent, aceşti parametrii reprezintă diferite opţiuni ale programului, date
calendaristice, nume de fişiere, etc.
3o. argv[0] este întotdeauna pointerul spre numele fişierului cu imaginea executabilă a
programului.
void main ( int argc, char *argv[]) // va afisa parametrii din linia de comanda
{ int i;
for (i=0; i<argc; i++) printf (“%s\n”,argv[i]);
}

6.7. Modificatorul const


Am văzut anterior că o constantă se defineşte prin caracterele care intră în
compunerea ei. De asemenea, în acelaşi capitol s-a arătat că putem atribui un nume unei
constante printr-o construcţie #define. Un astfel de nume se spune că este o constantă
simbolică şi el se substituie prin şirul de caractere care îi corespunde, în faza de preprocesare.
63
Un alt mod de a defini o constantă este acela de a folosi modificatorul const într-o
declaraţie. Printr-o astfel de declaraţie, unui nume i se poate atribui o valoare constantă. În
acest caz, numele respectiv nu mai este tratat de preprocesor şi el poate fi folosit în program
în mod analog cu numele variabilelor. Unui astfel de nume declarat cu ajutorul
modificatorului const nu i se poate schimba valoarea printr-o expresie de atribuire, ca şi unei
variabile obişnuite.

Formatele declaraţiei cu modificatorul const sunt următoarele:

tip const nume = valoare;


const tip nume = valoare;
tip const nume;
const tip nume;
const nume = valoare;
const nume;

Exemplu:
void main (void)
{ const i=10; // i devine egal cu 10; nu este posibila o atribuire i=0
const pi = 3.1415926
char *const s=”martie”; // s este un pointer constant spre zona in care este
// pastrat sirul de caractere “martie”. Valoarea lui s
// nu poate fi schimbata dar continutul zonei spre
// care pointeaza s poate fi schimbat
*s= ‘1’; // schimba litera m cu 1
*(s+1)=’2’ // schimba litera a cu 2
char const *s=”aprilie”; // s este un pointer spre o zona constanta.
// valoare lui s poate schimbata dar sirul “aprilie”
// nu poate fi modificat
const char *s=”aprilie” // este identica cu declaratia de mai sus.
}

Modificatorul const se foloseşte frecvent la declararea parametrilor formali de tip


pointer. O astfel de declaraţie are formatul:

const tip *nume_parametru_formal;

Un parametru formal declarat prin construcţia :

tip *nume_parametru_formal;

corespunde unui parametru efectiv a cărui valoare este o adresă. La apel, valoarea
parametrului formal devine egală cu această adresă. Datorită acestui fapt, funcţia apelată
poate să modifice data aflată la adresa respectivă. Dacă se foloseşte modificatorul const
utilizat la declararea unui astfel de parametru formal atunci se interzice funcţiei apelate să
modifice data de la adresa recepţionată la apel de către parametrul formal corespunzător.
Acest mecanism este folosit frecvent în cazul funcţiilor de tratare a şirurilor de caractere.
De exemplu funcţia strlen din biblioteca standard a limbajului C are prototipul:

unsigned strlen (const char *s);

Ea se apelează prin expresii de atribuire de forma:

64
i=strlen(x);

unde x este un pointer spre o zonă de memorie în care se află un şir de caractere.
Funcţia strlen determină lungimea şirului aflat la adresa recepţionată de către
parametrul s. Ea nu are voie să modifice şirul respectiv şi din această cauză parametrul s se
declară folosind modificatorul const.

6.8. Stiva
Prin stivă (stack în engleză) înţelegem o mulţime ordonată de elemente la care accesul
se realizează conform principiului ultimul venit primul servit. În engleză stiva se mai
numeşte şi listă LIFO (Last In First Out).
O modalitate simplă de a implementa o stivă este păstrarea elementelor ei într-un tablou
unidimensional. În acest tablou se vor păstra elementele stivei unul după altul. De asemenea,
ele se pot scoate din tablou în ordinea inversă păstrării lor. La un moment dat se poate scoate
ultimul element pus pe stivă şi numai acesta.
Despre ultimul element pus în stivă se spune că este vârful stivei, iar despre primul
element că este baza stivei.
Accesul este permis doar la vârful stivei:
- un element se poate pune pe stivă numai după elementul aflat în vârful stivei şi
după această operaţie el ajunge vârful stivei;
- se poate scoate de pe stivă numai elementul aflat în vârful stivei şi după această
operaţie în vârful stivei rămâne elementul care a fost pus pe stivă înaintea lui.
Vom numi stack tablou de tip int afectat stivei şi next variabila care indică prima
poziţie liberă din stivă. Deci stack[0] este baza stivei iar stack[n] va fi vârful stivei. Vom
defini mai multe funcţii asociate tabloului stack:

- push funcţia care pune un element în stivă;


- pop funcţia care scoate un element din stivă;
- clear funcţia de iniţializare a stivei; după apelul ei stiva devine vidă;

#define MAX 1000


static int stack[1000];
static next = 0; // indicele pentru baza stivei

void push(int x) // pune pe stiva valoarea lui x


{ if (next < MAX)
stack [next++]=x;
else
printf (“stiva este depasita\n”);
}

int pop() // scoate elementul din varful stivei si returneaza valoarea lui
{ if (next >0)
return stack [--next];
else { printf (“stiva este vida\n”);
return 0;
}
}

void clear(void) // videaza stiva

65
{ next=0;
}

66
7. Recursivitate
Spunem că o funcţie C este recursivă dacă ea se autoapelează înainte de a se reveni
din ea. Funcţia se poate reapela fie direct, fie indirect (prin intermediul altor funcţii).
La fiecare apel al unei funcţii, parametrii şi variabilele locale se alocă pe stivă într-o
zonă independentă. De asemenea, orice apel recursiv al unei funcţii va conduce la o revenire
din funcţie la instrucţiunea următoare apelului respectiv. La revenirea dintr-o funcţie se
realizează curăţarea stivei, adică zona de pe stivă afectată la apel parametrilor şi variabilelor
automatice se eliberează.
Un exemplu simplu de funcţie apelata recursiv este funcţia de calcul al factorialului.
Putem defini recursiv funcţia factorial astfel:

factorial(n)= 1, dacă n=0


factorial(n)=n*factorial(n-1), dacă n>0

În limbajul C avem :
double factorial (int)
{ if (n= = 0) return 1.0;
else return n*factorial(n-1);
}

Observaţii:
1o. În general, o funcţie recursivă se poate realiza şi nerecursiv, adică fără să se autoapeleze.
2o. De obicei, recursivitatea nu conduce nici la economie de memorie şi nici la execuţia mai
rapidă a programelor. Ea permite însă o descriere mai compactă şi mai clară a funcţiilor.
Acest lucru rezultă şi din exemplul de mai sus de calcul al factorialului.
3o. În general, funcţiile recursive sunt de preferat pentru procese care se definesc recursiv.
Există şi excepţii. De exemplu algoritmul de generare a permutărilor de n obiecte poate fi
descris recursiv astfel: având în memorie toate cele (n-1)! permutări, atunci permutările de n
obiecte se generează înserând pe n în toate poziţiile posibile ale fiecărei permutări de n-1
obiecte. Dar ne aducem aminte că 10!=3628800 şi capacitatea stivei se depăşeşte repede.

Exemple:

1) Programul determină recursiv cmmdc (algoritmul lui Euclid) a două numere întregi (de
tip long):
cmmdc (a,b) = b, dacă a%b =0 (restul împărţirii lui a la b e zero)
cmmdc (a,b) = cmmdc (b,a%b), în caz contrar.

#include <iostream.h>
#include <conio.h>

long cmmdc(long a, long b)


{ if (!(a % b)) return b;
else return cmmdc(b, a % b);
}

void main(void)
{ long x,y;
clrscr();
cout << "dati un numar natural=";
cin >> x;
cout << "dati alt numar natural=";
67
cin >> y;
cout << "cmmdc(" << x << "," << y << ")=" << cmmdc (x,y);
}

Am folosit funcţiile de intrare / ieşire cin şi cout, imediat se observă modul lor de
utilizare.

2) Programul determină recursiv suma unor elemente de tablou unidimensional

a[1]+a[2]+ . . . + a[n]

#include <iostream.h>
#define MAX 100
int a[MAX];
// suma(n)= 0, daca n=0
// suma(n)=suma(n-1) + a[n] daca n>0
int suma(int n)
{ if (!n) return 0;
else return a[n]+suma(n-1);
}

void main(void)
{int n,i;
cout << "dati n= ";
cin >> n;
for (i=1; i<=n; i++)
{ cout << "a[" << i << "]= ";
cin >> a[i];
}
cout << "suma numerelor este " << suma(n);
}

3) Programul determină recursiv termenul al n-lea din şirul lui Fibonacci definit după cum
urmează:

fibonacci[0]=0
fibonacci[1]=1
fibonacci[n]=fibonacci[n-1]+fibonacci[n-2], dacă n>1
#include<iostream.h>

long fibonacci (long n)


{if (!n) return 0;
else if (n==1) return 1;
else return fibonacci(n-1) + fibonacci(n-2);
}

void main (void)


{ long n;
cout << "dati n = ";
cin >> n;
cout << "fibo(" << n << ") =" << fibonacci (n);
}

68
4) Programul determina maximul dintr-un vector de numere astfel:

M(n)= a[1], dacă n=1


M(n)= max { M(n-1), a[n] }, dacă n>1

#include<iostream.h>
#define MAX(x,y) (x > y ? x : y)
int a[100];

int M(int n)
{ if (n= =1) return a[1];
else return MAX (M(n-1), a[n]);
}

void main(void)
{int n,i;
cout << "dati n=";
cin >> n;
for (i=1; i<=n; i++)
{ cout << "a[" << i << "]= ";
cin >> a[i];
}
cout << "maximul este " << M(n);
}

5) Programul afisează un şir de caractere în mod recursiv, caracter cu caracter, considerând


că şirul de caractere este format din primul caracter(capul) + restul şirului de caractere
(coada).
#include <iostream.h>
#include <conio.h>
#define max 100
char sir [max];
int n;

void afis (int m)


{ if (m = = n+1) return;
else { cout << sir[m];
afis(m+1);
}
}

void main (void)


{int i;
do { cout << "\ndati lungimea sirului =";
cin >> n;
}
while ( (n< 0) || (n > max));
for(i=1; i<=n; i++)
{ cout << "sir[" << i << "]=";
cin >> sir[i];
}
afis(1);
getch();
}

6) Programul ce urmează e oarecum asemănător cu exemplul anterior doar că afişează şirul de


caractere de la sfârşit spre început.
#include <iostream.h>

69
#include <conio.h>
#define max 100
char sir [max];
void afis (int m)
{ if (m==0) return;
else { cout << sir[m];
afis(m-1);
}
}

void main (void)


{int n,i;
do {cout << "\ndati lungimea sirului ="), cin >> n;}
while ( (n< 0) || (n > max));
for(i=1; i<=n; i++)
{ cout << "sir[" << i << "]=";
cin >> sir[i];
}
afis(n);
getch();
}

7) Programul sortează prin metoda quicksort un vector de numere întregi:

#define dim 50
#include <stdio.h>
#include <conio.h>
int x[dim+1],i,n;

void tipsir ()
{ for (i=1; i<=n; i++)
{ printf("%3d",x[i]);
if (!(i % 20)) printf ("\n");
}
}

void quik(int st, int dr)


{int i,j,y;
i=st; j=dr; y=x[i];
do { while ((x[j] >= y) && (i<j)) j - -;
x[i]=x[j];
while ((x[i] <= y) && (i<j)) i++;
x[j]=x[i];
}
while (i != j);
x[i]=y;
if (st < i-1) quik(st,i-1);
if (i+1 < dr) quik(i+1,dr);
x[j]=x[i];
}
void citire (void)
{ int cod = 0;
n = dim+1;
while ( n <= 0 || n > dim || ! cod )
{ printf ("\ndati dim. sir:");
cod=scanf ("%d",&n);
}
i = 1;
while (i<=n)
{ printf ("x[%2d]=",i);
scanf ("%d", &x[i]);
70
i++;
}
}

void main(void)
{ clrscr();
citire();
clrscr();
printf ("\n\nsir initial\n");
tipsir();
quik(1,n);
printf ("\n\nsir sortat\n");
tipsir();
getche();
}

71
8. Structuri, tipuri utilizator
După cum am văzut datele de acelaşi tip se pot grupa în tablouri. Limbajul C permite
gruparea unor date de tipuri diferite sub alte forme de organizare numite structuri.
Tablourile au un tip şi anume tipul comun elementelor lor. Astfel, distingem tablouri
de tip întreg, de tip caracter, de tip flotant, etc. În cazul structurilor, nu mai avem un tip
comun. Fiecare structură reprezintă un nou tip de date, tip care se introduce prin declaraţia
structurii respective.
Un exemplu simplu de structură este data calendaristică, cu componentele următoare:
- ziua;
- luna;
- anul.
unde: ziua şi anul sunt date de tip întreg iar luna este un tablou de caractere.
Structura ca şi tabloul, este o mulţine ordonată de elemente. În exemplul de mai sus se
consideră că ziua este primul ei element, luna este al doilea iar anul este ultimul ei
element. Trebuie să precizăm că referirea la componentele unei structuri nu se mai face cu
ajutorul indicilor ci prin calificare.

8.1. Declaraţia de structură


O structură se poate declara în mai multe feluri, astfel:
Formatul 1:
struct nume_structura
{ lista_declaratii
};

Cu ajutorul acestui format se introduce un nou tip de dată cu numele nume_structură. Lista de
declaraţii este formată din declaraţii obişnuite. Tipul data_calendaristica îl putem introduce
astfel:

struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
};

O astfel de declaraţie se numeşte declaraţie de tip. Să reţinem că unui nou tip de date nu i se
alocă memorie, el este doar contabilizat ca un nou tip utilizator pe lângă tipurile predefinite
ale limbajului C.

Formatul 2:
struct nume_structura
{ lista_declaratii
}lista_variabile;

Un astfel de format introduce tipul utilizator nume_structura şi declară o listă de variabile în


care fiecare element din listă are tipul nume_structură. Prin exemplu următor se introduc
variabilele dc1 şi dc2 ca date elementare de tipul data_calendaristica şi tabloul dc de 13
componente.

72
struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
} dc1, dc2, dc[13];

Formatul 3:
struct { lista_declaraţii
} lista_variabile;

Acest format se foloseste dacă nu vrem sa dăm nume noului tip structurat şi totodată dacă nu
mai vrem să-l folosim. Deci nu vom mai pute declara alte date de tipul structurat nou introdus
pentru că tipul nu are nume.

Exemplu:
struct { int ziua;
char luna[11];
int anul;
} dc1, dc2, dc[13];

S-au declarat varibilele dc1, dc2 şi tabloul dc având noul tip structurat utilizator dar nu se mai
doreşte să declarăm alte date de acest tip.

Observaţii:
1o. Dacă se foloseşte formatul 1 atunci pentru a declara date de tipul utilizator nou introdus se
foloseşte o construcţie de forma:

struct nume_ structura lista_variabile;

Compilatorul alocă memorie varibilelor din lista de variabile, tratând această construcţie
ca şi declaraţiile obişnuite.
2o. Componentele unei structuri pot fi ele însele date structurate. O componentă care nu este
structurată se numeşte componentă elementară.
3o. Ca şi în cazul celorlalte tipuri de variabile se pot defini structuri globale, statice sau
automatice. Structurile statice se declară precedând declaraţiile lor prin cuvântul static, iar
cele externe prin cuvântul cheie extern.
4o. Elementele unei date de tip structură pot fi iniţializate după modelul iniţializării
variabilelor care au tipuri predefinite.
Exemple:
1) Introducem tipul utilizator data_calendaristica astfel:

struct data_calendaristica
{ int ziua;
char luna[11];
int anul;
};
pentru a iniţializa o dată de tipul data_calendaristică vom scrie:

struct data_calendaristica dc1={31, “martie”, 1956};

73
2) Dacă declarăm un nou tip date_personale şi în care vrem să folosim tipul
data_calendaristica, vom scrie:

struct date_personale
{ char nume[30];
char adresa[50];
struct data_calendaristica data_nasterii, data_angajarii;
};

8.2. Accesul la elementele unei structuri


Pentru a avea acces la componentele unei date structurate va trebui să folosim o
calificare de forma:
nume_data_structurata . nume_componenta

Astfel dacă avem tipul structurat data_calendaristica introdus in exemplele anterioare şi


declarăm data dc astfel:
struct data_calendaristica dc;

atunci pentru a ne referi la componentele datei dc vom folosi construcţiile:

dc.ziua
dc.anul
dc.luna (atenţie este pointer spre caractere)

Dacă avem declarat un tablou astfel:

struct data_calendaristica tdc[10];

atunci pentru fiecare componentă i ne vom referi astfel:

tdc[i].ziua
tdc[i].anul
tdc[i].luna (este pointer)
tdc[i].luna[0], tdc[i].luna[1], . . . , tdc[i].luna[11]

Ca şi tablourile structurile se pot transfera prin parametrii, transferând un pointer spre


data structurată respectivă, adică adresa de început a zonei alocate structurii. Deci, printr-un
apel de forma:
functie(&data_structurata);

se transferă funcţiei functie adresa de început a zonei alocate structurii data_structurata.


Dacă data_structurata este o structura de tipul tip, atunci antetul funcţiei functie este
următorul:
void functie(tip *p)

unde p este pointer spre tipul structurat tip.

Pentru data structurată dc de tipul data_calendaristica antetul funcţiei functie este:

74
void functie(struct data_calendaristica *p)

iar apelul pentru data dc se face


functie(&dc);

Printr-un astfel de apel, funcţia apelată nu are acces la numele datei structurate
transferate, ci numai la pointerul spre ea. De aceea se pune problema accesului la
componentele datei structurate prin pointerul la ea. În acest caz numele datei structurate se va
înlocui prin *p. Deci, în cazul datei structurate dc, transferate ca şi mai sus, în locul
construcţiei
dc.zi
vom scrie:
(*p).zi

înlocuind numele datei structurate dc prin *p, unde p este un pointer spre dc.

Observaţie:
1o. Parantezele rotunde din construcţia de mai sus sunt obligatorii, deoarece punctul este un
operator prioritar operatorului unar *.
2o. Construcţia de mai sus poate fi înlocuită prin p->zi care este identică cu ea. Simbolul ->
se compune din caracterele ‘-‘ şi ‘>’ scrise unul după celălalt fără spaţiu între ele. El se
numeşte săgeată şi este considerat a fi un operator cu aceeaşi prioritate ca şi punctul, deci de
prioritate maximă.

8.3. Atribuiri de nume pentru tipuri de date


După cum ştim tipurile de bază ale limbajului C, numite şi tipuri predefinite se
identifică printr-un cuvânt cheie (int, char, float, etc). Totodată prin instrucţiunea struct,
programatorul poate să introducă un tip nou. Programatorul poate să atribuie un nume unui
tip (predefinit sau utilizator) cu ajutorul construcţiei:

typedef tip nume_nou_tip;


unde:
- tip este numele unui tip predefinit sau al unui tip utilizator (introdus cu struct);
- nume_nou_tip este noul nume atribuit tipului respectiv.
După ce s-a atribuit un nou nume unui tip, numele respectiv poate fi utilizat pentru a
declara date de acel tip, la fel cum se utilizează în declaraţii cuvintele cheie int, char, float,
etc.

Observaţii:
1o. De obicei numele atribuit unui tip se scrie cu litere mari.
2o. Un exemplu de astfel de nume există în fişierul stdio.h pentru tipul fişier, căruia i s-a
atribuit numele FILE.
Exemple:
1) Fie declaraţiile:
typedef int INTREG;
typedef float REAL;
În continuare, denumirile INTREG şi REAL se pot folosi la fel ca şi cuvintele cheie
int şi float. Cu alte cuvinte, declaraţia:

75
INTREG i, j, tablou[10];

este identică cu declaraţia următoare:

int i, j, tablou[10];

Analog:
REAL x, y, z;

este identică cu declaraţia:

float x, y, z;

2) typedef struct data_calendaristica


{ int ziua;
char luna[11];
int anul;
} DC;

Prin această declaraţie se atribuie denumirea DC tipului structurat data_calendaristica.


În continuare putem declara date de tip DC:
DC data_nasterii, data_angajarii;
DC data_curenta ={31,”august”,1998};

3) typedef int *PI;


Prin această declaraţie se introduce un sinonim pentru tipul pointer spre întregi: int *.
Putem să declarăm în continuare pointeri spre întregi astfel:

PI p;
care este echivalentă cu:

int *p;

4) Declaraţia

typdef struct
{ double real;
double imaginar;
} COMPLEX;

introduce numele COMPLEX pentru datele de tip complex.


Funcţia următoare returnează modulul unui număr complex:

typedef struct
{ double real;
double imaginar;
} COMPLEX;
#include <math.h>
double modul (COMPLEX *x) // returneaza modulul numarului
// spre care pointeaza x
76
{ return sqrt (x->real * x->real + x->imaginar * x->imaginar);
}

8.4. Uniune
Limbajul C oferă utilizatorului posibilitatea de a folosi aceeaşi zonă de memorie
pentru a păstra date de tipuri diferite în momente diferite ale execuţiei programului. Astfel, de
exemplu, putem utiliza o zonă de memorie pentru a păstra la un moment dat o dată flotantă,
iar ulterior să reutilizăm aceeaşi zonă pentru o dată întreagă sau de tip pointer. Reutilizările
zonelor de memorie conduc la utilizarea mai eficientă a acesteia, uneori putându-se obţine o
economie substanţială a spaţiului de memorie alocat programului.
O uniune se declară printr-o construcţie asemănătoare declaraţiei de structură.
Deosebirea constă în înlocuirea cuvântului struct prin union:

union nume
{ tip_membru_1;
...
tip_membru_2;
};
Exemplu:
union u
{ int i;
float f;
double d;
};

Prin această declaraţie s-a definit tipul de date u. În continuare, putem declara date de
tipul u printr-o declaraţie de forma:
union u u1;

unde u1 este o dată de tip u căreia i se alocă o zonă de memorie care poate fi utilizată pentru
a păstra date de tipurile int, float sau double. Deoarece tipul double necesită memoria cea mai
mare se alocă 8 octeţi. Astfel zona u1 poate păstra pe oricare din celelalte componente ale
uniunii dar în momente diferite ale execuţiei programului.
Accesul la componentele unei uniuni se face la fel ca şi în cazul structurilor. Astfel,
pentru a ne referi la componenta i a uniunii u1 definită în exemplul anterior folosim
construcţia:
u1.i

sau dacă p este pointer spre tipul u declarat prin

union u *p;
atunci construcţia
p -> i

permite accesul la componenta i a uniunii spre care pointează p.

Pentru a evita erorile legate de evidenţa în fiecare moment a datei care se prelucrează
se ataşează unei uniuni o dată menită să indice componenta curentă. Această dată este
specificată pentru fiecare uniune. De exemplu pentru uniunea u1 definită anterior este

77
important să se ştie dacă zona de memorie conţine un întreg, un flotant în simplă precizie sau
un flotant în dublă precizie. Se definesc trei constante simbolice:

#define INTREG 1
#define F_SIMPLU 2
#define F_DUBLU 3

Modificăm tipul u ataşând data tip_curent de tip int astfel:


struct u
{int tip_curent;
union { int i;
float f;
double d;
} uu;
};

Declarăm structura us astfel:


struct u us;

În acest caz, în momentul în care se păstrează o dată în zona rezervată uniunii, se


atribuie componentei tip_curent una din constantele definite anterior:
- INTREG, dacă se păstrează un întreg;
- F_SIMPLU dacă se păstrează un flotant în simplă precizie;
- F_DUBLU dacă se păstrează un flotant în dublă precizie.
Astfel când se foloseşte componenta de tip int se va asocia atribuirea:

us.tip_curent=INTREG;

Analog se vor folosi ţi atribuirile următoare când se vor folosi componentele de tip
float sau de tip double:
us.tip_curent=F_SIMPLU;
respectiv:
us.tip_curent=F_DUBLU;

În felul acesta, se poate testa, în fiecare moment, tipul de dată prezent în zona
rezervată. Aceasta se poate face printr-o secvenţă de instrucţiuni if sau prin intermediul
instrucţiunii switch.

if (us.tip_curent = = INTREG) // se foloseste us.uu.i


else if (us.tip_curent = = FSIMPLU) // se foloseste us.uu.f
else if (us.tip_curent = = FDUBLU) // se foloseste us.uu.d
else eroare
sau folosind switch avem o construcţie mai clară de forma:
switch (us.tip_curent)
{ case INTREG:
// se foloseste us.uu.i
break;
case FSIMPLU:
// se foloseste us.uu.f
break;
case FDUBLU
// se foloseste us.uu.d

78
break;
default:
// eroare
}

Programul următor calculează ariile pentru următoarele figuri geometrice:


- cerc;
- dreptunghi;
- pătrat;
- triunghi.

Programul citeşte datele pentru o figură geometrică, calculează aria figurii respective
şi scrie rezultatul:
La intrare se folosesc următoarele formate:
- pentru cerc C raza;
- pentru dreptunghi D lungime laţime;
- pentru pătrat P latură;
- pentru triunghi T latură latură latură;
- sfârşit fişier EOF.

În cazul triunghiului, se utilizează formula lui HERON pentru calculul ariei:

aria = sqrt (p*(p-a)(p-b)(p-b))

unde p este semiperimetrul, iar a, b, c sunt cele 3 laturi.


#include <stdio.h>
#include <math.h>
#define PI 3.14159265
#define EROARE -1
#define CERC 1
#define PATRAT 2
#define DREPT 3
#define TRIUNGHI 4
typedef struct
{ int tip; // tipul figurii
union
{ double raza // cerc
double lp ; // patrat
double ld[2] ; // dreptunghi
double lt[3] ; // triunghi
} fig;
}FIG;

void main (void) // calculeaza arii


{
double aria,p;
int i;
char car[2];
FIG zfig;
for(; ;) // citeste primul caracter,el defineste tipul figurii geometrice
{ printf(“se cere o litera mare\n”);
if ((i = scanf("%1s",car)) == EOF) break;
if (i != 1)
{ printf (" se cere o litera mare\n");
continue;
}

79
zfig.tip = EROARE;
switch(car[0])
{case 'C': // cerc
printf("se cere raza cercului in flotanta\n");
i = scanf("%lf", &zfig.fig.raza);
if(i != 1)
{ printf("se cere raza cercului in flotanta\n");
break;
}
zfig.tip = CERC; // se pastreaza tipul figurii
break;

case 'P': // patrat


printf("se cere latura patratului in flotanta\n");
i = scanf("%lf",&zfig.fig.lp);
if( i !=1)
{ printf("se cere latura patratului in flotanta\n");
break;
}
zfig.tip = PATRAT;
break;
case 'D': // dreptunghi
printf("se cer laturile dreptunghiului in flotanta\n");
i = scanf("%lf %lf",&zfig.fig.ld[0],&zfig.fig.ld[1]);
if(i != 2)
{ printf("se cer laturile dreptunghiului in flotanta\n");
break;
}
zfig.tip = DREPT;
break;

case 'T': // triunghi


printf("se cer laturile triunghiului in flotanta\n");
i = scanf("%lf %lf %lf", &zfig.fig.lt[0], &zfig.fig.lt[1],&zfig.fig.lt[2]);
if(i != 3)
{ printf("se cer laturile triunghiului in flotanta\n");
break;
}
zfig.tip =TRI;
break;
printf("laturile nu formeaza un triunghi\n");
break;
default:
printf("se cere una din literele urmatoare\n");
printf("C pentru cerc\n");
printf("P pentru patrat\n");
printf("D pentru dreptunghi\n");
printf("T pentru triunghi\n");
} // sfarsit switch

switch (zfig.tip)
{case CERC: // aria cercului
printf("raza=%g aria=%g\n", zfig.fig.raza, PI*zfig.fig.raza*zfig.fig.raza);
break;
case PATRAT: // aria patratului
printf("latura =%g aria=%g\n",zfig.fig.lp, zfig.fig.lp*zfig.fig.lp);
break;
case DREPT: // aria dreptunghiului
printf("lungimea =%g latimea =%g\n", zfig.fig.ld[0], zfig.fig.ld[1]);
printf("aria=%g\n", zfig.fig.ld[0]*zfig.fig.ld[1]);
break;

80
case TRIUNGHI: // aria triunghiului
p=(zfig.fig.lt[0] + zfig.fig.lt[1] + zfig.fig.lt[2])/2;
if(p>zfig.fig.lt[0] && p>zfig.fig.lt[1] && p>zfig.fig.lt[2])
{p=p*(p-zfig.fig.lt[0])*(p-zfig.fig.lt[1])* (p-zfig.fig.lt[2]);
printf("a=%g b=%g c=%g\n", zfig.fig.lt[0], zfig.fig.lt[1], zfig.fig.lt[2]);
printf("aria = %g\n",sqrt(p));
}
else { printf (“ laturile nu formeaza un triunghi”);
break;
}
default : // avans pana la newline sau EOF
while ((i = getchar()) != ‘\n’ && i != EOF);
} // sfarsit switch
if (i = = EOF) break;
} // sfarsit for
} // sfarsit main

8.5. Câmp
Limbajul C permite utilizatorului definirea şi prelucrarea datelor pe biţi. Utilizarea
datelor pe biţi este legată de folosirea indicatorilor care de obicei sunt date care iau numai
două valori 0 sau 1.
Nu este justificat ca un astfel de indicator să fie păstrat ca un întreg pe 16 biţi şi nici
măcar pe un octet. Indicatorul poate fi păstrat pe un singur bit. În acest scop, limbajul C oferă
posibilitatea de a declara date care să se aloce pe biţi (unul sau mai mulţi biţi). Acest lucru îşi
găseşte aplicare în programele de sistem. Astfel, de exemplu, atributele variabilelor dintr-o
tabelă de simboluri pot fi păstrate pe biţi, ceea ce conduce la o economisire substanţială a
memoriei ocupate de tabela respectivă.
Prin câmp înţelegem un şir de biţi adiacenţi conţinuţi într-un cuvânt calculator.
Câmpurile se grupează formând o structură.
Un câmp se declară ca şi o componentă a unei structuri şi el are tipul unsigned (întreg
fără semn). Totodată în declaraţia câmpului se indică şi dimensiunea lui în biţi.
În general, o structură cu componente câmpuri are forma:

struct
{ camp1;
...
campn;
} nume;

unde campi (i=1,...,n) are unul din formatele de mai jos:


unsigned nume : lungime_în_biţi
sau : lungime_în_biţi

Exemplu:
struct
{ unsigned a:1;
unsigned b:1;
unsigned c:2;
unsigned d:2;
unsigned e:3;
} indicatori;

81
Data indicatori se alocă într-un cuvânt calculator, adică pe 16 biţi. Componentele ei
sunt:
- a un bit;
- b un bit;
- c doi biţi;
- d doi biţi;
- e trei biţi.

La câmpuri ne putem referi la fel ca şi la componentele oricărei structuri. Deci la


indicatorii de mai sus ne putem referi prin următoarele construcţii:

- indicatori.a
- indicatori.b
- indicatori.c
- indicatori.d
- indicatori.e

Alocarea biţilor este dependentă de calculator. De obicei biţii se alocă de la


dreapta spre stânga ca în figura de mai jos:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0

a
b
c

d
e

Observaţii:
1o. Dacă un câmp nu poate fi alocat în limitele unui cuvânt, el se alocă în întregime în
cuvântul următor.
2o. Nici un câmp nu poate avea o dimensiune mai mare decât 16 biţi.
3o. Formatul fără nume (al doilea format) pentru câmp se foloseşte pentru cadraje. Acest lucru
este util atunci când sunt zone de biţi neutilizate în cadrul unui cuvânt. De asemenea,
utilizarea formatului cu lungime egală cu zero permite ca alocarea câmpurilor următoare lui
să se facă în cuvântul următor.
4o. O structură care are şi componente câmpuri poate avea şi componente obişnuite.
5o. Nu se pot defini tablouri de câmpuri.
6o. Unui câmp nu i se poate aplica operatorul adresă.

Câmpurile se utilizează frecvent la scrierea unor programe de sistem, cum ar fi : drivere


pentru periferice, compilatoare, etc.
Utilizarea câmpurilor poate conduce la programe cu o portabilitate redusă. Totodată,
accesul la date pe biţi conduce la creşterea numărului de operaţii, fiind necesare deplasări şi
operaţii pe biţi suplimentare, fapt ce poate conduce atât la creşterea timpului de execuţie a
programelor, cât şi la creşterea memoriei utilizate. Ori datele pe biţi se folosesc chiar în ideea
de a economisi memorie.

82
8.6. Tipul enumerat
Tipul enumerat permite utilizatorului să folosească în program nume sugestive în
locul unor valori numerice. De exemplu, în locul numărului unei luni calendaristice, se poate
folosi denumirea ei:
1 ian
2 feb
3 mar

În locul valorilor 0 şi 1 se pot folosi cuvintele FALS şi ADEVĂRAT.


Prin aceasta, se introduce o mai mare claritate în programe, deoarece valorile numerice
sunt înlocuite prin diferite sensuri atribuite lor.
Un tip enumerat se introduce printr-o declaraţie de forma:

enum nume {nume0, nume1, . . . , numen};

Prin această declaraţie se defineşte tipul enumerat nume, iar numei are valoarea i. O
formă mai generală a declaraţiei de mai sus permite programatorului să forţeze valorile
numelor din acoladă. În acest scop, se pot folosi construcţii de forma:

numei= eci

unde eci este o expresie constantă de tip int.


Cu alte cuvinte, unui nume i se poate atribui o valoare sau valoarea lui coincide cu a
numelui precedent mărită cu 1. Dacă primului nume din acoladă nu i se atribuie o valoare, el
are valoarea 0. Numele nume0, nume1,. . . , numen trebuie să fie nume diferite. Ele sunt
constante şi valoarea lor se stabileşte prin declaraţia în care au fost scrise. Domeniul lor de
valabilitate este definit de domeniul de valabilitate al declaraţiei prin care se definesc:
- instrucţiunea compusă care conţine declaraţia;
- fişierul sursă în care este scrisă declaraţia, dacă este externă oricărei funcţii.
Valorile atribuite lui nume0, nume1, . . . , numen sunt de obicei diferite, dar unele pot
să şi coincidă.
După ce s-a introdus un tip enumerat, se pot declara date de tipul respectiv
printr-o declaraţie de forma:

enum nume lista_de_variabile;

Datele de tip enumerat se consideră de tip int şi se pot utiliza în program oriunde este
legal să apară o dată de tip int.

Observaţii:
1o. Se pot utiliza, ca şi în cazul structurilor, construcţii de forma:
enum nume {nume0, nume1,. . . , numen} lista_de_variabile;
sau
enum { nume0, nume1,. . . , numen} lista_de_variabile;

2o. De asemenea, se poate utiliza construcţia typedef pentru a atribui un nume unui tip
enumerat:
typedef enum nume {nume0, nume1,. . . , numen} NUME;

83
În continuare se pot declara date de tipul NUME, astfel:

NUME lista_de_variabile;

Exemple:
1) enum luna{ian=1,feb,mar,apr,mai,iun,iul,aug,sep,oct,nov,dec};
enum luna luna_calendaristica

Prin prima declaraţie se introduce tipul enumerat luna. Mulţimea de valori asociate
acestui tip este formată din numerele întregi 1,2, . . . , 12. Se pot utiliza denumirile:
ian ia valoarea 1
feb ia valoarea 2
...
dec ia valoarea 12

A doua construcţie declară data luna_calendaristica de tipul luna. Ei i se pot atribui


valori prin expresii de atribuire de forma:

luna_calendaristica = mar sau


luna_calendaristica = mai + 4

2) typedef enum {luni, marti,miercuri,joi,vineri,sambata,duminica} ZI;


ZI z;

Variabila z este de tip ZI. Se poate utiliza în expresii de forma:


z=marti;
if(z<sambata) // trateaza ziua de lucru
else // trateaza zi de odihna

84
9. Liste
9.1. Date structurate definite recursiv
Limbajul C permite definirea de tipuri structurate recursiv (autoreferenţiate). Acest
lucru se face cu ajutorul pointerilor, şi anume un element al structurii poate să fie un pointer
spre tipul de dată introdus prin structura respectivă:

struct nume
{ declaratii
struct nume *p;
declaratii
};

Un tip definit ca mai sus se spune că este un tip autoreferit sau recursiv. O dată
structurată declarată printr-un astfel de tip se spune că este autoreferită sau recursivă. Datele
structurate recursive au numeroase aplicaţii în prelucrarea listelor înlănţuite şi arborescente.
Două tipuri structurate t1 şi t2 pot să conţină fiecare un pointer spre celalalt. În acest
caz se va proceda ca mai jos:

struct t1; // o declaratie inainte fara de care nu se poate


// declara tipul t2
struct t2
{ declaratii
struct t1 *pt1;
declaratii
};
struct t1
{ declaratii
struct t2 *pt2;
declaratii
};

9.2. Liste înlănţuite


Datele structurate se pot organiza în tablouri sau în structuri recursive introducând în
tipul structurat unul sau mai mulţi pointeri spre tipul structurat respectiv. Astfel se stabileşte o
relaţie de ordine (uneori chiar mai multe) între elementele mulţimii de date structurate; de
asemenea, mulţimea rescpectivă se poate organiza în mod dinamic, adăugând elemente noi
sau suprimându-le pe cele care nu mai sunt necesare.
Definiţie O mulţime dinamică de structuri recursive de acelaşi tip şi care satisfac una sau mai
multe relaţii de ordine introduse prin pointeri se numeşte listă înlănţuită. Elementele listei
se mai numesc noduri.
Cele mai utilizate tipuri de listă sunt:
- lista simplu înlănţuită;
- lista circulară simplu înlănţuită;
- lista dublu înlănţuită;
- lista circulară dublu înlănţuită;.

Cele patru tipuri de liste sunt exemplificate grafic astfel:


85
capul

listă liniară simplu înlănţuită;

capul

listă liniară circulară simplu înlănţuită;

capul

listă liniară dublu înlănţuită;

capul

listă liniară circulară dublu înlănţuită;

9.3. Lista liniară simplu înlănţuită


O listă simplu înlănţuită; este o listă înlănţuită; ale cărei noduri satisfac o singură
relaţie de ordine introdusă prin pointeri.
Tipul unui nod dintr-o listă simplu înlănţuită; se poate declara în două moduri:
a) struct tnod
{ declaratii
struct tnod *urmator;
declaratii
};

b) typedef struct tnod

86
{ declaratii
struct tnod *urmator;
declaratii
} TNOD;

Observaţii
1o. Varianta b) este mai folosită.
2o. Pointerul următor introduce o relaţie de ordine între nodurile de tip TNOD.
3o. Ultimul nod al listei va avea pointerul urmator = NULL.
4o. Pentru nodurile interioare ale listei pointerul urmator va avea valori adrese; dacă
urmator din nodul a pointează spre nodul b, spunem că nodul b este succesorul lui a.
Operaţiile ce se pot efectua asupra unei liste simplu înlănţuită;
- crearea listei;
- accesul la un nod al listei;
- inserarea unui nod înlănţuită;
- ştergerea unui nod dintr-o listă;
- ştergerea unei liste.

Memorarea listelor se poate face:


- dinamic (în memoria internă);
- static (în fişiere).

Pentru modul dinamic se va folosi funcţia malloc la crearea listei cât şi la inserarea de
noduri, iar la ştergerea de noduri funcţia free.

9.4. Crearea şi afişarea unei liste


Vom crea o listă ce conţine în noduri informaţii despre numere întregi şi pătratele lor.
Avem două funcţii: una de creare care întoarce adresa capului listei şi o funcţie de afişare a
informaţiei din noduri. Vom comenta instrucţiunile importante din program.

#include <stdio.h>
#include <alloc.h>
typedef struct nod // definirea tipului NOD
{ int nr;
int patrat;
struct nod* leg;
} NOD;

NOD *creaza(void) // functia de creare, intoarce adresa capului


{ NOD *cap,*p,*pc;
int i,lung;
printf("\n\n\n\ creare lista simplu inlantuita\n\n");
lung = sizeof(NOD);
pc=(NOD *)malloc(lung); // pc este un pointer curent, in el se vor pune adresel noi
cap=pc;
printf("dati informatia elementului : ");
scanf ("%d",&i);

while (i>0) // crearea listei se termina la numar negativ

87
{ p=pc; // pointer ce pastreaza adresa noua
p->nr=i; // incarcarea informatiei
p->patrat=i*i;
pc=(NOD *)malloc(lung); // se cere o noua adresa de memorie
p->leg=pc; // se leaga pointerul leg la noua adresa
printf("dati informatia elementului : ");
scanf ("%d",&i);
}
p->leg=NULL; // ultimul nod al listei are pointerul leg = NULL
free(pc); // eliberarea ultimei adrese care de fapt nu face parte din lista
return cap; // returneaza adresa capului listei
}

void afisare(NOD *p) // functia de afisare a listei


{
while (p != NULL) // cat timp n-am ajuns la ultimul nod
{ printf ("\n numarul %d si patratul sau %d", p->nr,p->patrat);
p=p->leg; // trecerea la urmatorul nod al listei
}
}

void main (void) // functia principala


{ NOD *capul;
clrscr();
capul=creaza(); // lista e cunoscuta prin adresa capului
afisare(capul);
getch();
}

88
10. Prelucrarea fişierelor
10.1. Fişiere
În general, prin fişier înţelegem o colecţie ordonată de elemente numite înregistrări,
care sunt păstrate pe diferite suporturi de memorie externă. Suportul de memorie externă cel
mai folosit este suportul magnetic (de obicei discuri sub forma de flopy şi hardiscuri sau
bandă magnetică care e din ce în ce mai rar folosită). Suportul magnetic este reutilizabil
deoarece zona utilizată pentru a păstra înregistrările unui fişier poate fi ulterior reutilizată
pentru a păstra înregistrările altui fişier.
Datele introduse de la un terminal se consideră că formează un fişier de intrare.
Înregistrarea în acest caz, de obicei, este formată din datele tastate la terminal pe un rând deci
caracterul de rând nou (newline) este terminator de înregistrare. În mod analog, datele care se
afişează pe terminal formează un fişier de ieşire. Înregistrarea poate fi formată din
caracterele unui rând.
Un fişier are o înregistrare care marchează sfârşitul de fişier. În cazul fişierelor de
intrare de la tastatură sfârşitul de fişier se generează prin:

CTRL/Z

El poate fi pus în evidenţă folosind constanta simbolică EOF definită în fişierul stdio.h.
Prelucrarea fişierelor implică un număr de operaţii specifice acestora. Două operaţii
sunt absolut necesare la prelucrarea oricărui fişier:
- deschiderea fişierului;
- închiderea fişierului.
Aceste operaţii de deschidere şi închidere a unui fişier se pot realiza prin intermediul unor
funcţii speciale din biblioteca standard a limbajului C. Alte operaţii privind prelucrarea
fişierelor sunt:
- crearea unui fişier;
- consultarea unui fişier;
- actualizarea unui fişier;
- adăugarea de înregistrări într-un fişier;
- poziţionarea într-un fişier;
- ştergerea unui fişier.
Prelucrarea fişierelor se poate face la două nivele. Primul nivel face apel direct la sistemul
de operare şi se numeşte nivelul inferior de prelucrare al fişierelor. Cel de-al doilea nivel de
prelucrare se realizează prin utilizarea unor proceduri specializate în prelucrarea fişierelor
care, printre altele, pot rezerva şi gestiona automat zone tampon necesare realizării operaţiilor
de intrare/ieşire, şi se numeşte nivelul superior de prelucrare al fişierelor

10.2. Nivelul inferior de prelucrare al fişierelor


La acest nivel de prelucrare se folosesc 5 funcţii:
- open (creat)- pentru deschiderea fişierelor;
- read - pentru citirea din fişier;
- write - pentru citirea din fişier;
- lseek - pentru poziţionarea în fişier;
- close - pentru închiderea fişierului.

89
10.2.1. Deschiderea unui fişier

Orice fişier înainte de a fi prelucrat trebuie deschis. Această operaţie se realizează prin
intermediul funcţiei open al cărui prototip este următorul:
int open (const char *cale, int acces);
unde:
- cale este un pointer spre un şir de caractere care defineşte calea spre fişierul care se
deschide (în cea mai simplă formă este numele fişierului dacă se află în directorul curent)
- acces este o variabilă de tip întreg care poate lua una din valorile:
- O_RDONLY - fişierul se deschide numai în citire (consultare);
- O_WRONLY - fişierul se deschide numai în scriere (creare);
(sau O_CREAT)
- O_RDWR - fişierul se deschide în citire/scriere;
- O_APPEND- fişierul se deschide la sfârşit pentru adăugare;
- O_BINARY - fişierul se prelucrează binar;
- O_TEXT - fişierul este de tip text.

Unele valori din cele de mai sus se pot combina cu ajutorul operatorului |. De exemplu
O_RDWR | O_BINARY pentru deschiderea fişierului în scriere/citire binară.

Observaţii:
1o. Funcţia open întoarce descriptorul de fişier care este o valoare intreagă ce va identifica
fişierul în toate celelalte operaţii care se vor realiza asupra lui. Dacă deschiderea unui fişier
nu reuşeşte (de obicei unul din parametrii este eronat) atunci funcţia open returnează valoarea
–1.
2o. Pentru a putea utiliza funcţia open trebuie incluse fişierele header io.h şi fcntl.h.
3o. Pentru a crea un fişier nou se va folosi funcţia creat în locul funcţiei open cu prototipul:

int creat (const char *cale, int mod);


unde:
- cale are aceeaşi semnificaţie ca şi la funcţia open;
- mod este un întreg care poate fi definit prin constantele simbolice de mai jos:
S_IREAD - se poate citi fişierul;
S_IWRITE - se poate scrie în fişier;
S_IEXEC - se poate executa programul conţinut în fişier.

Utilizarea funcţiei presupune includerea fişierelor io.h şi stat.h


o
4 . Implicit fişierul se consideră că este de tip text.

Exemple:
1) char nume_fisier[ ]=”fis1.dat”;
int df;
df = open (nume_fisier, O_RDONLY);

Prin apelul de mai sus se deschide în citire fişierul fis1.dat din directorul curent.

2) int df;
df = open (“c:\\borlandc\\help.txt”,O_APPEND);

Se deschide în adăugare fişierul help.txt din directorul borlandc de pe discul C.

90
10.2.2. Citirea dintr-un fişier (consultare)

Funcţia folosită pentru operaţia de citire dintr-un fişier în memorie se numeşte read
şi are prototipul următor:

int read (int df, void *buf, unsigned lung);

unde:
- df este descriptorul de fişier a cărui valoare a fost definită la deschidere;
- buf este pointerul spre zona de memorie în care se recepţionează înregistrarea care se
citeşte;
- lung este lungimea în octeţi a inregistrării citite.

Observaţii:
1o. La fiecare apel funcţia returnează înregistrarea curentă. La primul apel se citeşte prima
înregistrare din fişier, la al doilea apel se citeşte a doua, etc. Ordinea înregistrărilor în fişier
este cea definită la crearea fişierului.
2o. La un apel al funcţiei read se citesc cel mult lung octeţi înregistrarea având definită
lungimea la scrierea în fişier. Funcţia reîntoarce numărul de octeţi citiţi, 0(zero) la sfârşit de
fişier, sau –1 la eroare. De obicei se folosesc frecvent înregistrări de 512 octeţi sau chiar mai
mari.
3o. Funcţia read poate fi folosită pentru a citi de la intrarea standard. În acest caz,
descriptorul de fişier este 0 (stdin are 0, stdout are 1, stderr are 2 stdprn are 3 stdaux are 4).
Programatorul nu trebuie să deschidă fişierele standard deoarece ele sunt deschise automat la
lansarea în execuţie a programului.
4o. Utilizarea funcţiei read, presupune includerea fişierului io.h.

10.2.3. Scrierea într-un fişier (creare, actualizare, adăugare)

Pentru a scrie într-un fişier se foloseşte funcţia write. Se presupune că fişierul este
deschis în prealabil prin funcţia creat sau open. Ea este asemănătoare cu funcţia read, doar că
realizează transferul invers, adică din memorie în fişier şi are prototipul:

int write (int df, void *buf, unsigned lung);

Observaţii:
1o. Funcţia returnează numărul octeţilor scrişi în fişier. Acesta este egal cu lung şi defineşte
lungimea înregistrării scrise în fişier. În cazul în care numărul returnat de funcţia write diferă
de parametrul lung scrierea a fost eronată şi se reîntoarce valoarea –1.
2o. Utilizarea funcţiei write implică includerea fişierlui io.h.

10.2.4. Poziţionarea într-un fişier

Pentru a avea acces aleator la înregistrările unui fişier se foloseşte o funcţie de


poziţionare în fişier pe anumite înregistrări dorite. Pe fişierele care au suporturi magnetice
este posibilă poziţionarea cu ajutorul funcţiei lseek care are prototipul următor:

long lseek (int df, long deplasament, int origine)


unde:

91
- df este descriptorul de fişier;
- deplasament defineşte numărul de octeţi peste care se va deplasa capul de
scriere/citire al discului;
- origine are una din valorile:
0 deplasamentul se consideră de la începutul fişierului;
1 deplasamentul se consideră din poziţia curentă a capului de
scriere/citire;
2 deplasamentul se consideră de la sfârşitul fişierului.

Observaţii:
1o. Funcţia returnează poziţia capului de citire/scriere faţă de începutul fişierului în număr de
octeţi sau –1L la eroare.
2o. Funcţia nu realizează nici un transfer de informaţie ci doar poziţionează capul de
citire/scriere în fişier. Deci pentru transfer e nevoie de funcţiile read sau write.
3o. Utilizarea funcţiei presupune includerea fişierului io.h.
4o. Apelul lseek (df, 0L, 0) permite o poziţionare la început de fişier,
iar apelul lseek (df, 0L, 2) permite o poziţionare la sfârşit de fişier.

10.2.5. Închiderea unui fişier

La sfârşitul prelucrării unui fişier acesta trebuie închis. Acest lucru se realizează
automat dacă programul se termină prin apelul funcţiei exit. Programatorul poate închide un
fişier folosind funcţia close. Se recomandă închiderea unui fişier de îndată ce s-a terminat
prelucrarea lui. Aceasta din cauză că numărul fişierelor ce pot fi deschise simultan este
limitat. Limita este dependentă de sistemul de operare şi ea variază, de obicei, în intervalul
15-25. De obicei numărul de buffere (zone tampon) asociate fişierelor se precizează în
fişierul autoexec.bat. Menţionăm că fişierele standard din limbajul C nu se închid de
programator.
Funcţia close are prototipul următor:
int close (int df);
unde
df este descriptorul fişierului care se închide.

Observaţii:
1o. La o închidere normală, funcţia returnează valoarea 0 şi –1 în caz de incident.
2o. Utilizarea funcţiei close implică includerea fişierului io.h.
Exemple
1) Vom deschide fişierul “fis1.dat” în creare şi vom scrie în el două înregistrări.

#include<io.h>
#include<fcntl.h>
void main (void)
{ int df,i;
df = open("fis1.dat", O_CREAT); // se deschide fisierul fis1.dat in creare
if (df != -1) // se testeaza daca fiserul s-a deschis corect
{ write (df,"cioban vasyle\n", 14); // se scriu doua inregistrari
write (df,"cioban andrei\n", 14);
} else printf (“nu s-a deschis fisierul);
close (df); // se inchide fisierul
}

2) Se afişează conţinutul fişierului “fis1.dat”

92
#include<io.h>
#include<fcntl.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{int df;
char s[14];
df = open("fis1.dat", O_RDONLY); // se deschide fisierul în citire
if (df != -1) // se testeaza daca deschiderea e corecta
{ read (df, s, 14);
printf ("%.14s",s);
read (df, s, 14);
printf ("%.14s",s);
} else printf (“nu s-a deschis fisierul);
close (df);
getch(); // se asteapta un caracter de la tastatura
}

3) programul următor copiază intrarea standard la ieşierea standard folosind o zonă tampon
de 80 de caractere:

#define LZT 80
#include <io.h>
void main (void) // copiaza intrarea standard la iesirea standard
{ char zona_tampon[LZT];
int i;
while ((i = read (0, zona_tampon, LZT)) > 0) write (1, zt, i);
}

10.3. Nivelul superior de prelucrare a fişierelor


După cum am amintit, la acest nivel fişierele se prelucrează cu ajutorul unor
proceduri specializate.

10.3.1. Deschiderea unui fişier

Funcţia fopen se utilizează pentru deschiderea unui fişier. Ea returnează un pointer


spre tipul FILE (tipul fişier), tip definit în fişierul stdio.h. Tipul FILE este un tip structurat şi
el depinde de sistemul de operare. În caz de eroare, funcţia fopen returnează pointerul NULL.
Prototipul funcţiei fopen este următorul:

FILE *fopen (const char *cale, const char *mod);


unde:
- cale are aceeaşi semnificaţie ca şi în cazul funcţiilor open şi creat.
- mod este un pointer spre un şir de caractere care defineşte modul de prelucrare al
fişierului după deschidere. Acest şir de caractere se defineşte în felul următor:
- “r” - deschidere în citire (read);
- “w” - deschidere în scriere (write);
- “a” - deschidere pentru adăugare;
- “r+” - deschidere pentru modificare (citire sau scriere);
- “rb” - citire binară;
- “wb” - scriere binară;
- “r+b” - citire/scriere binară.

93
Observaţii:
1o. Dacă se deschide un fişier inexistent cu modul “w” sau “a”, atunci el este deschis în
creare.
2o. Dacă se deschide un fişier existent cu modul “w”, atunci conţinutul vechi al fişierului se
pierde şi se va crea unul nou cu acelaşi nume.
3o. Menţionăm că, stdin, stdout, stderr, stdaux şi stdprn sunt pointeri spre tipul FILE şi
permit ca funcţiile de nivel superior de prelucrare a fişierelor să poată trata intrarea standard
şi ieşirile standard pe terminal şi imprimantă la fel ca şi fişierele pe celelalte suporturi.
Singura deosebire constă în aceea că aceste fişiere nu se deschid şi nici nu se închid de către
programator. Ele sunt deschise automat la lansarea în execuţie a programului şi se închid la
apelul funcţiei exit.
4o. Apelul funcţiei se realizează prin construcţia:
FILE *pf;
pf = fopen (“FIS1.DAT”,”w”);

10.3.2. Prelucrarea pe caractere a unui fişier

Fişierele pot fi scrise şi citite caracter cu caracter, folosind două funcţii simple:
- putc pentru scriere;
- getc pentru citire.

Funcţia putc are prototipul:


int putc (int c, FILE *pf);
unde:
- c este codul ASCII al caracterului care se scrie în fişier;
- pf este pointerul spre tipul FILE a cărui valoare a fost returnată de funcţia fopen la
deschiderea fişierului în care se scrie; pf poate fi şi stdout, sdterr, stdaux, stdprn.
Funcţia putc returnează valoarea lui c respectiv –1 în caz de eroare.

Funcţia getc are prototipul:


int getc (FILE *pf);
unde:
- pf este pointerul spre tipul FILE a cărui valoare a fost returnată de funcţia fopen la
deschiderea fişierului; în particular pf poate fi stdin.
Funcţia getc returnează codul ASCII al caracterului citit sau EOF la sfârşit de fişier sau
eroare.

10.3.3. Închiderea unui fişier

Închiderea unui fişier se realizează cu ajutorul funcţiei fclose care are prototipul:

int fclose (FILE *pf);


unde:
- pf este pointerul spre tipul FILE a cărui valoare a fost definită la deschiderea
fişierului prin intermediul funcţiei fopen.

Funcţia fclose returnează:


- 0 la închiderea normală a fişierului;
- 1 în caz de eroare.
94
Exemple:

1) Programul următor copiază intrarea standard la ieşirea standard stdout, folosind funcţiile
getc şi putc.
#include <stdio.h>
void main (void)
{ int c;
while (( c = getc (stdin)) != EOF) putc (c, stdout);
}

2) Programul următor copiază intrarea standard la imprimantă.


#include <stdio.h>
void main (void)
{ int c;
while (( c = getc (stdin)) != EOF) putc (c, stdprn);
}

3) Programul următor scrie la ieşirea stdout caracterele unui fişier a cărui cale este
argumentul din linia de comandă. Dacă nu există un argument în linia de comandă, atunci
se citeşte de la intrarea standard.

#include <stdio.h>
void main (int argc, char *argv[ ] )
{ FILE *pf;
int c;
if (argc = = 1)
pf = stdin; // nu exista argument in linia de comanda
else // se deschide fisierul a carui cale se afla in argv[1]
if (( pf = fopen (*++argv,”r”)) = = NULL)
{ printf (“nu se poate deschide fisierul %s\n”,*argv);
exit (1);
}
while (( c = getc (pf)) != EOF) putchar(c);
exit (0);
}

10.3.4. Operaţiile de intrare-ieşire cu format

Biblioteca standard a limbajului C conţine funcţii care permit realizarea operaţiilor de


intrare/ieşire cu format. Astfel se pot utiliza funcţiile fscanf şi fprintf, prima pentru citire cu
format dintr-un fişier, iar a doua pentru scriere cu format într-un fişier.
Funcţia fscanf este asemănătoare cu funcţia scanf. Ea are un parametru în plus faţă de
scanf. Acest parametru este un pointer spre tipul FILE şi el defineşte fişierul din care se face
citirea. Acest pointer este primul parametru al funcţiei fscanf. Funcţia poate fi apelată printr-o
expresie de atribuire de forma:

nr = fscanf (pf, control, lista_de_parametrii );

unde :
- pf este un pointer spre tipul FILE şi valoarea lui a fost definită prin apelul funcţiei fopen;
defineşte fişierul din care se face citirea;
- ceilalţi parametri sunt identici cu cei utilizaţi la apelul funcţiei scanf.

95
Funcţia fscanf, ca şi funcţia scanf, returnează numărul câmpurilor citite din fişier. La
întâlnirea sfârşitului de fişier se returnează valoarea EOF definită în fişierul stdio.h. Pentru pf
= stdin, funcţia fscanf este identică cu scanf.
Funcţia fprintf , ca şi funcţia printf, returnează numărul caracterelor scrise în fişier
sau –1 în caz de eroare. Pentru pf = stdout, funcţia fprintf este identică cu printf. De
asemenea, se pot utiliza pointerii standard obisnuiţi: stderr, stdaux, stdprn.
Exemplu:
Vom scrie un program cu ajutorul căruia se va crea un fişier cu numele "FIS.DAT" şi care
conţine înregistrări cu numele, prenumele şi adresa unor persoane.

#include <stdio.h>
void main(void)
{
int n=0, i=1; // n este numarul de înregistrari ce se va scrie in fisier
char nume[25], prenume[30], adresa[50];
FILE *pf;
printf("\n Dati numarul de inregistrari n= ");
scanf("%d",&n);
pf=fopen("FIS.DAT","w");
if (pf = = NULL)
{ printf ("Eroare la deschidere");
return;
}
do
{
printf("\n Nume : ");
scanf("%s",nume);
printf("\n Prenume : ");
scanf("%s",prenume);
printf("\n Adresa : ");
scanf("%s",adresa);
fprintf(pf,"%s %s %s", nume, prenume, adresa);
i++;
} while (i<=n);
fclose(pf);
}

10.3.5. Intrări-ieşiri de şiruri de caractere

Biblioteca standard a limbajului C conţine funcţiile fgets şi fputs care permit citirea
respectiv scrierea într-un fişier ale cărui înregistrări sunt şiruri de caractere.
Funcţia fgets are prototipul:

char *fgets (char *s, int n, FILE *pf);


unde:
- s este pointerul spre zona în care se face citirea caracterelor;
- n-1 este numărul maxim de caractere care se citesc;
- pf este pointerul spre tipul FILE care defineşte fişierul din care se face citirea.

De obicei s este numele unui tablou de tip char de dimensiune cel puţin n. Şirul se
termină cu ‘\0’ (caracterul NUL). La întâlnirea caracterului ‘\n’, citirea se opreşte. În acest
caz, în zona receptoare se transferă caracterul ‘\n’ şi apoi caracterul NUL (‘\0’).
În mod normal, funcţia returnează valoarea pointerului s. La întâlnirea sfârşitului de
fişier se returnează valoarea NULL.

96
Funcţia fputs scrie într-un fişier un şir de caractere care se termină prin ‘\0’. Ea are
prototipul:
int fputs (const char *s, FILE *pf);
unde:
- s este pointerul spre zona care conţine şirul de caractere care se scrie;
- pf este pointerul spre zona care conţine şirul de caractere care se scrie.
Funcţia fputs returnează codul ASCII al ultimului caracter scris sau –1 în caz de
eroare.
Aceste funcţii sunt realizate folosind funcţia getc pentru fgets şi putc pentru fputs.
Pentru a citi de la intrarea standard stdin, se poate folosi funcţia gets, care nu mai are
parametrii pf şi n. Parametrul pf este implicit stdin. Funcţia gets citeşte caracterele de la
intrarea standard până la întâlnirea caracterului ‘\n’ care nu mai este păstrat în zona spre care
pointează s. Şirul de caractere citit se termină şi în acest caz prin ‘\0’.
În mod analog, pentru a scrie la ieşirea standard stdout se poate folosi funcţia puts,
care nu mai are parametrul pf, acesta fiind implicit stdout. În rest, funcţia puts este la fel ca şi
funcţia fputs.

10.3.6. Poziţionarea într-un fişier

Cu ajutorul funcţiei fseek se poate deplasa capul de citire/scriere al discului în vederea


prelucrării înregistrărilor fişierului într-o ordine oarecare, diferită de cea secvenţială (acces
aleator). Această funcţie este asemănătoare cu funcţia lseek. Ea are prototipul următor:

int fseek (FILE *pf, long deplasament, int origine);


unde:
- pf este pointerul spre tipul FILE care defineşte fişierul în care se face poziţionarea capului
de citire/scriere;
- deplasament şi origine au aceeaşi semnificaţie ca şi în cazul funcţiei lseek.

Funcţia fseek returnează valoarea zero la poziţionare corectă şi o valoare diferită de zero
în caz de eroare.
Funcţia ftell indică poziţia capului de citire în fişier. Ea are prototipul:

long ftell (FILE *pf);


unde:
- pf este pointerul spre tipul FILE care defineşte fişierul în cauză.

Funcţia returnează o valoare de tip long care defineşte poziţia curentă a capului de
citire/scriere, şi anume reprezintă deplasamentul în octeţi a poziţiei capului faţă de începutul
fişierului.

10.3.7. Prelucrarea fişierelor binare

Fişierele organizate ca date binare (octeţii nu sunt consideraţi ca fiind coduri de


caractere) pot fi prelucrate la acest nivel folosind funcţiile fread şi fwrite. În acest caz se
consideră că înregistrarea este o colecţie de date structurate numite articole. La o citire, se
transferă într-o zonă specială, numită zonă tampon, un număr de articole care se presupune

97
că au o lungime fixă. În mod analog, la scriere se transferă din zona tampon un număr de
articole de lungime fixă. Cele două funcţii au prototipurile de mai jos:

unsigned fread (void *ptr, unsigned dim, unsigned nrart, FILE *pf);

unde:
- ptr este pointerul spre zona tampon ce conţine articolele citite (înregistrarea citită);
- dim este un întreg ce reprezintă lungimea unui articol;
- nrart este un întreg ce reprezintă numărul articolelor care se transferă;
- pf este un pointer spre tipul FILE care defineşte fişierul din care se face citirea.

unsigned fwrite (const void *ptr, unsigned dim, unsigned nrart, FILE *pf);

Parametrii funcţiei fwrite au aceeaşi semnificaţie ca şi parametrii funcţiei fread.


Ambele funcţii returnează numărul articolelor transferate sau –1 în caz de eroare.

Exemplu:
Programul următor citeşte de la intrarea standard stdin datele ale căror formate sunt definite
mai jos şi le scrie în fişierul miscari.dat din directorul curent.

tip denumire um cod pret cantitate


1 TELVIZOR buc 0001 3000000 200
#include <stdio.h>
#define MAX 60
typedef struct { char tip[2];
char den[MAX];
int val;
char unit[3];
long cod;
float pret;
float cant;
} ARTICOL;

union { // 6 articole in zona tampon

ARTICOL a[6];
char zt[6*sizeof(ARTICOL)];
}buf;

int cit (ARTICOL *str) // citeste datele de la intrarea standard si le scrie in


{ // structura spre care pointeaza str
int nr,c;
float x,y;
while ((nr = scanf("%1s %60s %3d %2s %ld", str -> tip, str ->den,
&str ->val, str -> unit,&str -> cod)) != 5|| scanf("%f %f", &x,&y) != 2)

{
if(nr = = EOF) return (EOF);
printf ("rind eronat ; se reia\n");
// avans pina la newline
while ((c = getchar ()) != '\n' && c != EOF);
if (c == EOF) return (EOF);
} // sfarsit while
str -> pret = x;
98
str -> cant = y;
return (nr);
} // sfarsit cit

void main (void) // creaza fisierul miscari.dat cu datele citite de la intrarea standard
{
FILE *pf;
ARTICOL a;
int i,n;
if((pf = fopen("miscari.dat", "wb")) == NULL)
{
printf("nu se poate deschide in creare fisierul miscari.dat\n");
exit(1);
}
for ( ; ; )
{ // se umple zona tampon a fisierului
for (i = 0 ;i<6 ; i++)
{
if((n=cit(&a)) == EOF) break;
buf.a[i]=a;
}
// se scrie zona tampon daca nu este vida
if(i !=0 )
if(i!=fwrite(buf.zt, sizeof(ARTICOL), i, pf))
{ printf("eroare la scrierea in fisier\n");
exit(1);
}
if(n = = EOF) break;
}
fclose(pf);
}

10.4. Ştergerea unui fişier


Un fişier poate fi şters apelând funcţia unlink. Aceasta are prototipul:

int unlink (const char *cale);


unde:
- cale este un pointer spre un şir de caractere identic cu cel utilizat la crearea fişierului în
funcţia creat sau fopen.
Funcţia unlink returnează valoarea zero la o ştergere reuşită, respectiv -1 în caz de
eroare.

99
11. Funcţii standard
Vom descrie câteva clase de funcţii, numite clase de funcţii standard aflate în
bibliotecile mediului BORLAND C. Funcţiile şi macrourile utilizate frecvent în majoritatea
aplicaţiilor se pot grupa în următoarele clase:

a) funcţii de prelucrare a fişierelor;


b) funcţii de alocare dinamică a memoriei;
c) macrouri de clasificare;
d) macrouri de transformare a simbolurilor;
e) funcţii care realizează conversii;
f) funcţii de prelucrare a şirurilor de caractere;
g) funcţii de calcul;
h) funcţii pentru controlul proceselor;
i) funcţii de gestiune a datei şi a orei;
j) funcţii de gestiune a ecranului.

Funcţiile de la punctele a) şi b) au fost descrise în lecţii anterioare.

11.1. Macrouri de clasificare


În această clasă distingem un număr de macrouri simple care au o utilizare largă în
prelucrarea simbolurilor. Definiţiile acestor macrouri se află în fişierul ctype.h
Unul dintre macrouri este denumit isascii şi are prototipul:
int isascii (int c);
Macroul returnează o valoare diferită de zero dacă valoarea lui c aparţine intervalului
de numere întregi [0,127] şi zero în caz contrar. Acest macrou permite să se testeze dacă
valoarea parametrului său reprezintă un cod ASCII sau nu.

Celelalte macrouri au prototipul următor:

int nume (int c);

unde nume este unul din următoarele:

isalpha dacă c este codul unei litere;


isalnum dacă c este codul unei litere sau cifre;
isdigit dacă c este codul unei cifre;
isgraph dacă c este codul unui caracter imprimabil inclusiv spaţiul;
islower dacă c este codul unei litere mici;
isprint dacă c este codul unui caracter imprimabil inclusiv spaţiu;
isspace dacă c reprezintă spaţiu, tabulator, retur de car, rând nou,
tabulator vertical, salt la început de pagină de imprimantă.
isupper dacă c este codul unei litere mari;
isxdigit dacă c este codul unei cifre hexazecimale.

100
Exemplu:

Programul următor citeşte un fişier şi îl rescrie schimbând literele mari cu litere mici.
Căile spre cele două fişiere (sursă şi destinaţie) sunt argumente în linia de comandă:

argv[1]este un pointer spre fişierul sursă;


argv[2]este un pointer spre fişierul destinaţie.
#include <stdio.h>
#include <ctype.h>
void main ( int argc, char *argv[ ] )
{ FILE *pf1, *pf2;
int c;
if (argc != 3)
{ printf (“linia de comanda eronata\n”);
exit(1);
}
if (( pf1 = fopen (argv[1],”r”) ) = = NULL
{ printf (“nu se poate deschide fisierul”%s\n”, argv[1]);
exit(1);
}
if (( pf2 = fopen (argv[2],”w”) ) = = NULL
{ printf (“nu se poate deschide fisierul”%s\n”, argv[2]);
exit(1);
}
while (( c = getc (pf1)) != EOF)
if (isascii(c) && isupper (c))
putc (c - ‘A’ + ‘a’, pf2); // c este codul unei litere mari
else
putc(c, pf2); // c este codul unei litere mici
fclose (pf1);
fclose (pf2);
// afisarea fisierului destinatie
if (( pf2 = fopen (argv[2], “r”) = = NULL)
{ printf (“nu se poate deschide in citire:%s\n”, argv[2]);
exit(1);
}
while ((c = getc(pf2)) != EOF) putchar (c);
fclose (pf2);
}

11.2. Macrouri de transformare a simbolurilor


În această clasă distingem macrouri definite tot în fişierul ctype.h. Prototipurile
acestor macrouri sunt:
int toascii (int c); returnează ultimii 7 biţi ai lui c (care reprezintă un
cod ASCII);
int tolower (int c); transformă pe c din literă mare în literă mică;
int toupper (int c); transformă pe c din literă mică în literă mare.

101
11.3. Conversii
O dată are un format extern şi un format intern. Prin conversie înţelegem o
transformare a unei date dintr-un format al ei în celălalt. Conversiile se pot face sub controlul
unui format sau fără format. Dintre funcţiile care realizează conversii sub controlul
formatelor amintim:
printf;
fprintf;
scanf;
fscanf;

Aceste funcţii au fost descrise în lecţiile anterioare. Vom da în continuare câteva


funcţii care realizează conversii fără format şi care sunt utilizate mai frecvent. Aceste funcţii
au prototipurile în fişierul stdlib.h.
Funcţia atoi are prototipul:
int atoi (const char *ptr);
unde:
- ptr este un pointer spre o zonă de tip caracter ce conţine cifre zecimale care
sunt, eventual, precedate de semnul minus;

Efectul:
- şirul de cifre spre care pointează ptr este convertit din întreg zecimal în întreg
binar de tip int.
Observaţie:
1o. Funcţia returnează rezultatul acestei conversii.

Funcţia atol are prototipul:


long atol (const char *ptr);
unde:
- ptr este un pointer spre o zonă de tip caracter ce conţine cifre zecimale care
sunt, eventual, precedate de semnul minus;

Efectul:
- şirul de cifre spre care pointează ptr este convertit din întreg zecimal în întreg
binar de tip long.

Observaţie:
1o. Funcţia returnează rezultatul acestei conversii.

Funcţia atof are prototipul:


double atof (const char *ptr);
unde:
- ptr este un pointer spre o zonă de tip caracter ce conţine cifre zecimale care
sunt, eventual, precedate de semnul minus (poate conţine marca zecimală);

Efectul:
- şirul de cifre spre care pointează ptr este convertit în virgulă flotantă dublă
precizie.
Observaţie:

102
1o. Funcţia returnează rezultatul acestei conversii.

Funcţia itoa are prototipul:


char *itoa (int val, char *sir, int baza)
Efectul:
- valoarea parametrului val se converteşte din întreg binar de tip int în baza de
numeraţie definită de parametrul baza şi se păstrează în zona spre care pointează
sir.
Observaţie:
1o. Funcţia returnează pointerul sir.

Funcţia ltoa are prototipul:


char *ltoa (long val, char *sir, int baza)
Efectul:
- valoarea parametrului val se converteşte din întreg binar de tip long în baza de
numeraţie definită de parametrul baza şi se păstrează în zona spre care pointează
sir.
Observaţie:
1o. Funcţia returnează pointerul sir.

11.4. Funcţii de prelucrare a şirurilor de caractere


Funcţiile din această clasă implică includerea fişierului string.h. Indicăm mai jos
funcţiile din această clasă, utilizate mai frecvent. O parte din aceste funcţii au mai fost
utilizate în diferite exemple din lecţiile anterioare.

Funcţii de copiere:
char *strcpy (char *dest, const char *sursa);

char *strncpy (char *dest, const char *sursa, unsigned n);

- prima funcţie copiază şirul de caractere spre care pointează sursa în zona spre care
pointează dest;
- a doua funcţie realizează acelaşi lucru, dar copiază cel mult primii n octeţi din
sursă;
- ambele funcţii returnează valoarea pointerului dest.

Funcţii de concatenare:
int strcmp (const char *dest, const char *sursa);

char *strncat (const char *dest, const char *sursa, unsigned n);
- prima funcţie copiază şirul spre care pointează sursa la sfârşitul şirului din zona
spre care pointează dest;
- a doua funcţie realizează acelaşi lucru, dar se copiază cel mult primii n octeţi din
zona spre care pointează sursa;
- ambele funcţii returnează valoarea pointerului dest.

Funcţii de comparare:
int strcmp (const char *sir1, const char *sir2);

103
int stricmp (const char *sir1, const char *sir2);

int strncmp (const char *sir1, const char *sir2, unsigned n);

int strnicmp (const char *sir1, const char *sir2, unsigned n);

- aceste funcţii compară şirurile de caractere din zonele spre care pointează pointerii
sir1 şi sir2;

- ele returnează o valoare:


- negativă, dacă şirul spre care pointează sir1 este mai mic decât cel spre care
pointează sir2;
- zero, dacă cele două şiruri sunt egale;
- pozitivă, dacă şirul spre care pointează sir1, este mai mare decât cel spre care
pointează sir2;
- prezenţa literei i (ignore) în numele funcţiei înseamnă că nu se face distincţie între
literele mari şi mici;
- prezenţa literei n în numele funcţiei înseamnă că se realizează comparaţia pe cel
mult n octeţi.

Observaţie:
1o. Fie şirurile s1 şi s2 de lungime l1 şi l2. Atunci cele două şiruri sunt egale dacă:
l1=l2 ( au aceeaşi lungime);
s1[k] = s2 [k] pentru k=0,1,...,l 1
2o. Şirul s1 este mai mic decât şirul s2 dacă există un j, j  0 şi j  min (l1, l2), astfel încât:
s1[j] < s2[j];
s1[k] = s2[k], pentru k=0,1, . . . , j-1.

3o. Şirul s1 este mai mare decât şirul s2 dacă există un j, j  0 şi j  min(l1, l2), astfel incât:

s1[j] > s2[j];


s1[k] = s2[k], pentru k=0,1, . . . , j-1.

Funcţia lungime:
unsigned strlen (const char *sir);

- returnează lungimea şirului de caractere spre care pointează sir;


- caracterul NUL care termină şirul nu este numărat.

11.5. Funcţii de calcul


Majoritatea funcţiilor matematice au prototipurile în fişierul math.h. Multe dintre
acestea se utilizează la calculul valorilor funcţiilor elementare şi au prototipul:

double nume (double x);


unde nume este unul din următoarele:
acos -arccos;
asin -arcsin;
atan -arctg;

104
cos -cos;
sin -sin;
exp -ex;
log -ln;
log10 -lg;
sqrt -rădăcina pătrată;
ceil -returnează cel mai mare întreg mai mare sau egal cu x (partea întreagă);
floor -returnează cel mai mare întreg mai mic sau egal cu x;
fabs -valoarea absolută;
sinh -sinus hiperbolic;
cosh -cosinus hiperbolic;
tanh -tangentă hiperbolică;

Alte funcţii:
double atan2 (double y, double x); - returnează arctg(y/x);
double pow (double x, double y); - returnează xy;
double cabs (struct complex z); - returnează modulul numărului
complex;
double poly (double x, int n, double c[ ] ) - returnează valoarea polinomului
de grad n în punctul x, coeficienţii
sunt c[0], . . . c[n].

Funcţiile care urmează au prototipul în fişierele stdlib.h şi math.h:


int abs (int n); - returnează valoarea absolută din întregul n;
long labs (long n); - returnează valoarea absolută din întregul long n;

11.6. Funcţii pentru controlul proceselor


Aceste funcţii au prototipurile în fişierul stdlib.h şi în process.h şi realizează controale
asupra programelor:
void abort (void); - termină un program în caz de eroare;
void exit (int stare) - termină un program şi returnează o stare; stare este
0 pentru terminare normală şi diferită de zero
pentru o terminare anormală; videază buferele
fisierelor, închide toate fişierele;

int system (const char *comanda) - execută o comandă DOS definită prin sirul
de caractere spre care pointează comanda;
returnează 0 la succes si –1 la eroare.
Aceste funcţii au prototipurile în stdlib.h şi în process.h.

11.7. Funcţii pentru gestiunea datei şi orei


Dăm mai jos prototipurile a patru funcţii pentru citirea/setarea datei şi orei. Ele
implică includerea fişierului dos.h.
105
void getdate(struct date *d); - încarcă structura de tip date spre care pointează d
cu datele corespunzătoare furnizate de sistemul de
operare;
void gettime(struct time *t); - încarcă structura de tip time spre care pointează t
cu datele corespunzătoare furnizate de sistemul de
operare;
void setdate (struct date *t); - setează data curentă în conformitate cu datele de
tip date;
void settime (struct time *t); - setează ora curentă în conformitate cu datele de tip
time spre care pointează t.

Tipurile date şi time sunt definite în fişierul dos.h. astfel:

struct date {
int da_year;
int da_day;
int da_mon;
};

struct time {
unsigned char ti_min;
unsigned char ti_hour;
unsigned char ti_hund;
unsigned char ti_sec;
};

Exemplu:
void main (void) // afiseaza data si ora
{ struct date d;
struct time t;
getdate (&d);
gettime (&t);
printf (“\n\t%02d/%02d/%04d”,d.da_day, d.da_mon, d.da_year);
printf (“\tora %02d:%02:%02\n”, t.ti_hour, t.ti_min, t.ti_sec);
}

11.8. Alte funcţii diverse de uz general


void clrscr (void); - şterge fereastra activă sau tot ecranul; prototipul în conio.h
void delay(unsigned i); - suspendă execuţia programului pentru o perioadă de i
milisecunde;
void sleep(unsigned i); - suspendă execuţia programului pentru o perioadă de i
secunde;
void nosound (void); - opreşte difuzorul calculatorului;
void sound(unsigned h); - porneşte difuzorul calculatorului cu un ton egal cu h Hz.

106
12. Gestiunea ecranului în mod text
Biblioteca standard a limbajelor C şi C++ conţine funcţii pentru gestiunea ecranului.
Acesta poate fi gestionat în 2 moduri:
- modul text şi
- modul grafic.

Modul text presupune că ecranul este format dintr-un număr de linii şi coloane. De obicei
există două variante:

25 de linii x 80 de coloane = 2000 de caractere sau


25 de linii x 40 de coloane = 1000 de caractere.

Poziţia pe ecran a unui caracter se defineşte printr-un sistem de coordonate întregi


(x,y)
unde:
x - reprezintă numărul coloanei în care este situat caracterul;
y - reprezintă numărul liniei în care este situat caracterul.

Colţul din stânga sus are coordonatele (1,1) iar colţul din dreapta jos (80,25) sau
(40,25).

În mod implicit funcţiile de gestiune a ecranului în mod text au acces la tot ecranul.
Accesul poate fi limitat la o parte din ecran utilizând aşa numitele ferestre. Fereastra este un
dreptunghi care este o parte a ecranului şi care poate fi gestionată independent de restul
ecranului.
Un caracter de pe ecran, pe lângă coordonate, mai are şi următoarele atribute:
- culoarea caracterului afişat;
- culoarea fondului;
- clipirea caracterului.

Aceste atribute sunt dependente de adaptorul grafic utilizat. Cele mai utilizate adaptoare
sunt:
- placa MDA, care este un adaptor monocrom;
- placa HERCULES, care este un adaptor color;
- placa CGA, care este un adaptor color;
- placa EGA, care este un adaptor color;
- placa VGA, care este un adaptor color de mare performanţă.

Pentru adaptoarele de mai sus se pot utiliza 8 culori de fond şi 16 culori pentru afişarea
caracterelor.
Atributul unui caracter se defineşte cu ajutorul formulei:

atribut = 16 * culoare_fond + culoare_caracter + clipire (*)

unde:
- culoare_fond (background) = cifră între 0 şi 7; (8 culori)
- culoare_caracter (foreground) = întreg între 0 şi 15; (16 culori)
- clipire = 128 (clipire) sau 0 (fără clipire)
Tabel cu numele culorilor:
107
Culoare Constantă simbolică Valoare

negru BLACK 0
albastru BLUE 1
verde GREEN 2
turcoaz CYAN 3
roşu RED 4
purpuriu MAGENTA 5
maro BROWN 6
gri deschis LIGHTGRAY 7
gri închis DARKGRAY 8
albastru deschis LIGHTBLUE 9
verde deschis LIGHTGREEN 10
turcoaz deschis LIGHTCYAN 11
roşu dechis LIGHTRED 12
purpuriu magenta LIGHTMAGENTA 13
galben YELLOW 14
alb WHITE 15
clipire BLINK 128

12.1. Setarea ecranului în mod text


Setarea (punerea) se realizează cu ajutorul funcţiei textmode care are prototipul:

void textmode (int modtext);


unde
modtext poate fi exprimat numeric sau simbolic în felul următor:

Modul text activat Constantă simbolică Valoare

alb/negru 40 coloane BW40 0


color 40 coloane C40 1
alb/negru 80 coloane BW80 2
color 80 coloane C80 3
monocrom MONO 7
color 43 linii pentru EGA
şi 50 linii pentru VGA C4350 64
modul precedent LASTMODE -1

Modul MONO se poate seta pe un adaptor monocolor. Celelalte moduri se pot seta pe
adaptoare color.

12.2. Definirea unei ferestre


Dacă dorim să partajăm ecranul în zone care să poată fi gestionate independent trebuie
să definim ferestre.
O fereastră este o zonă dreptunghilară de pe ecran care se poate defini cu funcţia
window al cărei prototip este:
void window (int stanga, int sus, int dreapta, int jos);
108
unde:
-(stanga, sus) = coordonatele colţului din stânga al ferestrei;
-(dreapta, jos) = coordonatele colţului din dreapta jos.

La un moment dat o singură fereastră este activă şi anume aceea definită de ultimul
apel al funcţiei window. Funcţiile de gestionare a ecranului în mod text acţionează
întotdeauna asupra ferestrei active. După setarea modului text cu ajutorul funcţiei textmode
este activ tot ecranul.
Menţionăm că funcţia window nu are nici un efect dacă parametrii de la apel sunt
eronaţi.

12.3. Ştergerea unei ferestre


Fereastra activă se poate şterge cu ajutorul funcţiei clrscr cu prototipul:

void clrscr(void);
- fereastra activă (sau tot ecranul) devine activă; fondul are culoarea definită prin culoarea
de fond curentă;
- clrscr poziţionează cursorul în poziţia (1,1) a fiecărei ferestre.

12.4. Gestiunea cursorului


Programatorul poate plasa cursorul pe un caracter al ferestrei folosind funcţia gotoxy
al cărei prototip este:

void gotoxy (int coloana, int linie);


unde:
- (coloana, linie) sunt coordonate relative la fereastra activă, dacă sunt înafara ferestrei
active apelul este ignorat;

Poziţia cursorului se poate determina cu ajutorul funcţiilor wherex şi wherey care au


prototipurile:

int wherex (void); -returnează numărul coloanei în care se află cursorul


int wherey (void); -returnează numărul liniei în care se află cursorul.

Există cazuri când se doreşte ascunderea cursorului. Acest lucru se poate realiza printr-o
secvenţă specială în care se utilizează funcţia geninterrupt.

void ascundcursor (void) // face invizibil cursorul


{ _AH = 1;
_CH = 0x20;
geninterrupt (0x10);
}

Cursorul poate fi reafişat apelând funcţia următoare:

void afiscursor (void) // face vizibil cursorul


{ _AH = 1;
109
_CH = 6;
_CL = 7;
geinterrupt (0x10);
}

- AH, CH, CL reprezintă unii din regiştrii microprocesorului.

12.5. Determinarea parametrilor ecranului


Utilizatorul are posibilitatea să obţină parametrii curenţi ai ecranului prin apelarea
funcţiei gettextinfo al cărui prototip este:

void gettextinfo (struct text_info *p);

unde structura text_info este definită în fişierul conio.h astfel:

struct text_info
{ unsigned char winleft; // amplasarea colturilor ferestrei
unsigned char wintop;
unsigned char winright;
unsigned char winbottom;
unsigned char attribute; // culoarea fondului, a caracterelor si
unsigned char normattr; // clipirea
unsigned char currmode;
unsigned char screenheight; // dimensiunea ecranului
unsigned char screenwidth;
unsigned char curx; // pozitia cursorului
unsigned char cury;
};

12.6. Modurile video alb/negru


Există două moduri (intens şi normal) care se pot activa cu funcţiile highvideo şi
lowvideo ale căror prototipuri sunt:

void highvideo (void);


void lowvideo (void);

Intensitatea iniţială este de obicei cea normală. Se poate reveni la intensitatea normală
dacă se apelează funcţia normvideo cu acelaşi prototip ca al celorlalte două funcţii.

Exemplu:
Vom scrie un program care afişează parametrii ecranului:
#include <stdio.h>
#include <conio.h>

void main (void)


{ struct text_info par_ecran;
gettextinfo (&par_ecran) ;
printf (“\nstanga: %u sus: %u dreapta: %u jos: %u\n”,
par_ecran.winleft,
par_ecran.wintop,

110
par_ecran.winright,
par_ecran.winbottom);
printf (“atribut: %d mod curent: %d\n”,
par_ecran.normattr, par_ecran.currmode);
printf (“inaltimea ecranului: %d latimea ecrnului: %d\n”,
par_ecran.screenheight, par_ecran.screenwidth);
printf (“coloana cursorului: %d linia cursorului: %d\n”,
par_ecran.curx, par_ecran.cury);

12.7. Setarea culorilor


Culoarea fondului se setează cu ajutorul funcţiei textbackground cu prototipul
următor:
void textbackground (int culoare);
unde:
- culoare este un întreg cuprins între 0 şi 7 cu semnificaţia din tabelul anterior al culorilor.

Culoarea caracterelor se setează cu ajutorul funcţiei textcolor cu prototipul următor:


void textattr (int culoare);
unde:
- culoare este un întreg între 0 şi 15.

Se pot seta ambele culori, precum şi clipirea caracterului folosind funcţia textattr de
prototip:
void textattr (int atribut);
unde:
- atribut e definit cu ajutorul relaţiei (*) de la începutul lecţiei.

12.8. Gestiunea textelor


Pentru afişarea caracterelor colorate în conformitate cu atributele definite prin relaţia:

atribut = 16 * culoare_fond + culoare_caracter + clipire

se pot folosi funcţiile:


putch - pentru afişarea unui caracter;
cputs - pentru afişarea color a unui şir de caractere (acelaşi prototip ca puts);
cprintf - pentru afişarea color sub controlul formatelor.

Alte prototipuri de funcţii:


void insline (void); - inserează o linie cu spaţii în fereastră, liniile de sub
poziţia cursorului se deplasează în jos cu o poziţie;
void clreol (void) - şterge sfârşitul liniei începând cu poziţia cursorului;
void delline (void) - şterge toată linia pe care este poziţionat cursorul;

int movetext ( int stanga, int sus, int dreapta, int jos,
int stanga_dest, int dreapta_dest );

111
- copiază un text dintr-o poziţie în alta;
- returnează: 1 dacă textul s-a copiat cu succes şi 0 în caz de eroare.

Textele dintr-o zonă dreptunghiulară a ecranului pot fi salvate sau citite dintr-o zonă
de memorie cu ajutorul funcţiilor puttext şi gettext şi au prototipurile:

int gettext (int stanga, int sus, int dreapta, int jos, void *destinatie);

unde
- primii patru parametrii definesc fereastra unde se află textul de salvat;
- destinatie este pointerul spre zona de memorie în care se salvează textul.
şi
int puttext (int stanga, int sus, int dreapta, int jos, void *sursa);

unde
- primii patru parametrii definesc fereastra unde se va scrie pe ecran textul preluat din
memorie;
- sursa este pointerul spre zona de memorie din care se transferă textul.

Ele returnează:
- 1 la copiere cu succes;
- 0 la eroare.
Observaţie:
1o. Fiecare caracter de pe ecran se păstrează pe doi octeţi:
- pe un octet codul caracterului;
- pe octetul următor atributul caracterului.

Exemple:

1) Programul următor setează o fereastră şi modurile video alb/negru.

#include <conio.h>
void main (void)
{ textmode (BW80);
window (10,4,60,4);
clrscr ();
lowvideo ();
cputs(”lowvideo”);
normvideo ();
cputs (“normvideo”);
textmode (LASTMODE);
cprintf (“\n\r Acionati o tasta pentru a continua:”);
getch ();
}

2) Programul următor afişează toate combinaţiile de culori posibile pentru fond şi caractere
(adaptor EGA/VGA).

#include <conio.h>

112
#include <stdio.h>
void main (void)
{ static char *tculoare [ ] = { “0 BLACK negru”,
“1 BLUE albastru”,
“2 GREEN verde”,
“3 CYAN turcoaz”,
“4 RED rosu”,
“5 MAGENTA purpuriu”,
“6 BROWN maro”,
“7 LIGHTGRAY gri deschis”,
“8 DARKGRAY gri inchis”,
“9 LIGHTBLUE albastru deschis”,
“10 LIGHTGREEN verde deschis”,
“11 LIGHTCYAN turcoaz deschis”,
“12 LIGHTRED rosu dechis”,
“13 LIGHTMAGENTA purpuriu magenta”,
“14 YELLOW galben“,
“15 WHITE alb”};
int i,j,k;
struct text_info atribut;
gettextinfo (&atribut);
for (i = 0; i < 8; i++ ) // i alege culoarea fondului
{ window (3,2,60,20);
k=2;
textbackground (i);
clrscr();
for (j=0; j <10; j++, k++) // j alege culoarea caracterului
{ textcolor (j);
gotoxy (2,k);
if (i = = j) continue;
cputs (tculoare[j]);
}
gotoxy (1,18);
printf (“actionati o tasta pentru contiuare\n”);
getch();
}
window (atribut.winleft, atribut.wintop, atribut.winright, atribut.winbottom);
textattr (atribut. attribute);
clrscr();
}

113
13. Gestiunea ecranului în mod grafic
Modul grafic presupune că ecranul este format din “puncte luminoase” (pixeli).
Numărul acestora depinde de adaptorul grafic şi se numeşte rezoluţie. O rezoluţie este cu atât
mai bună cu cât este mai mare. Adaptorul CGA are o rezoluţie de 200 rânduri x 640 de
coloane iar EGA de 350 de rânduri x 640 de coloane. Adaptorul VGA şi SVGA oferă
rezoluţii în funcţie de mărimea ecranului. Astfel adaptorul VGA şi SVGA oferă rezoluţii de
până la 768 de rânduri x 1024 de coloane pentru ecranul de 14” (inches).
Pentru gestiunea ecranului în mod grafic se pot utiliza peste 60 de funcţii standard
aflate în biblioteca sistemului. Aceste funcţii au prototipurile în fişierul graphics.h. Totodată
ele folosesc pointeri de tip far (de 32 de biţi).

13.1. Setarea modului grafic


Modul grafic se setează cu ajutorul funcţiei initgraph. Această funcţie poate fi
folosită singură sau împreună cu o altă funcţie numită detectgraph care determină parametrii
adaptorului grafic. Prototipul ei este:

void far detectgraph (int far *graphdriver, int far *graphmode);


unde:
- în zona spre care pointează graphdriver se păstrează una din valorile:

valoare simbol

1 CGA
2 MCGA
3 EGA
4 EGA64
5 EGAMONO
6 IBM8514
7 HERCMONO
8 ATT400
9 VGA
10 PC3270

- în zona spre care pointează graphmode se memorează una din valorile:

pentru CGA
valoare simbol

0 CGAC0
1 CGAC1
2 CGAC2
3 CGAC3
corespunzând toate pentru o rezoluţie de 320*200 de pixeli şi permit 4 culori
4 CGAHI
are o rezoluţie de 640*200 puncte şi lucrează numai alb/negru.

pentru EGA
114
valoare simbol

0 EGALO are 640*200 i 16 culori


1 EGAHI are 640*350

pentru VGA
valoare simbol

0 VGALO are 640*200


1 VGAMED are 640*350
2 VGAHI are 640*480

Valorile spre care pointeză graphdriver definesc nişte funcţii standard corespunzătoare
adaptorului grafic. Aceste funcţii se numesc drivere. Ele se află în subdirectorul BGI.
Funcţia detectgraph detectează adaptorul grafic prezent la calculator şi păstrează
valoarea corespunzătoare acestuia în zona spre care pointează graphdriver. Modul grafic se
defineşte în aşa fel încât el să fie cel mai performant pentru adaptorul grafic curent. Cele mai
utilizate adaptoare sunt de tip EGA (calculatoare mai vechi) şi VGA şi SVGA (calculatoarele
mai noi).
Apelul funcţiei detectgraph trebuie să fie urmat de apelul funcţiei initgraph. Aceasta
setează modul grafic în conformitate cu parametrii stabiliţi de apelul prealabil al funcţiei
detectgraph.
Funcţia initgraph are prototipul:

void far initgraph (int far * graphdriver, int far *graphmode, char far *cale);

unde:
- graphdriver şi graphmode sunt pointeri cu aceeaşi semnificaţie ca în cazul
funcţiei detectgraph;
- cale este un pointer spre şirul de caractere care definesc calea subdirectorului BGI
(Borland Graphic Interface), de exemplu:
“C:\\BOLANDC\\BGI”
Exemplu:
Setarea în mod implicit a modului grafic:

int driver, mod_grafic;


...
detectgraph (&driver, &mod_grafic);
initgraph(&driver, &mod_grafic, “C:\\BORLANDC\\BGI”);

După apelul funcţiei initgraph se pot utiliza celelalte funcţii standard de gestiune
grafică a ecranului.
Din modul grafic se poate ieşi apelând funcţia closegraph care are prototipul:

void closegraph (void);

Funcţia initgraph mai poate fi apelată şi folosind constanta simbolică DETECT astfel:

int driver, mod_grafic;


...
115
driver = DETECT;
initgraph (&driver, &mod_grafic, “C:\\BORLANDC\\BGI”);

Constanta simbolică DETECT este definită în fişierul graphics.h, alături de celelalte


constante simbolice care definesc driverul. Aceasta are valoarea zero.
Prin apelul de mai sus, funcţia initgraph apelează funcţia detectgraph pentru a defini
parametrii impliciţi ai adaptorului grafic.
Utilizatorul poate defini el însuşi parametri pentru iniţializarea modului grafic.

13.2. Gestiunea culorilor


Adaptoarele grafice sunt prevăzute cu o zonă de memorie în care se păstrează date
specifice gestiunii ecranului. Această zonă de memorie poartă denumirea de memorie video.
În mod grafic, ecranul se consideră format, după cum am precizat, din puncte
luminoase numite pixeli. Poziţia pe ecran a unui pixel se defineşte printr-un sistem de
coordonate (x,y) cu x coloana şi y linia în care este afişat pixelul.
În cazul adaptoarelor color, unui pixel îi corespunde o culoare. Culoarea pixelilor se
păstrează pe biţi în memoria video. Memoria video necesară pentru a păstra starea ecranului
setat în mod grafic, se numeşte pagină video. Adaptoarele pot conţine mai multe pagini
video. Gestiunea culorilor este dependentă de tipul de adaptor grafic existent la
microprocesor.
Numărul maxim de culori pentru adaptoarele EGA este de 64 (numerotate de la 0 la
63). Cele 64 de culori nu pot fi afişate simultan pe ecran. În cazul adaptorului EGA se pot
afişa simultan 16 culori. Mulţimea culorilor care pot fi afişate simultan pe ecran se numeşte
paletă. Culorile din componenţa unei palete pot fi modificate de utilizator prin intermediul
funcţiilor standard. La iniţializarea modului grafic se setează o paletă implicită.
Pentru adaptorul EGA există un tablou de 64 de culori (cu coduri între 0 şi 63) din
care se selectează cele 16 culori pentru paletă. Există şi constante simbolice foarte sugestive
cu numele culorilor în engleză. Funcţiile de gestiune a culorilor pot avea ca parametri nu
numai codurille culorilor ci şi aceste constante simbolice.
Culoarea fondului este întotdeauna cea corespunzătoare indicelui 0 din paletă.
Culoarea pentru desenare este cea corespunzătoare indicelui 15.
Culoarea de fond poate fi modificată cu ajutorul funcţiei setbkcolor care are
prototipul:
void far setbkcolor (int culoare);
unde:
- culoare = index în tabloul care defineşte paleta.

Exemplu: setbkcolor (BLUE); setează culoarea de fond pe albastru.

Pentru a cunoaşte culoarea de fond curentă se poate apela funcţia getbkcolor de


prototip:
int far getbkcolor (void);

Ea returnează indexul în tabloul care defineşte paleta pentru culoarea de fond.

Culoarea pentru desenare poate fi modificată folosind funcţia setcolor de prototip:

void far setcolor(int culoare);


116
unde:
- culoare = index în tabloul care defineşte paleta.

Exemplu: setcolor (YELLOW); setează culoarea pentru desenare în galben.

Culoarea pentru desenare se poate determina apelând funcţia getcolor de prototip:

int far getcolor (void);

Ea returnează indexul în tabloul care defineşte paleta relativ la culoarea pentru


desenare.
Paleta curentă poate fi modificată cu funcţiile setpalette şi setallpalette. Prima
funcţie se foloseşte pentru a modifica o culoare din paleta curentă şi are prototipul:

void far setpalette (int index, int cod);

unde:
- index este un întreg din {0,. . . , 15} şi reprezintă indexul în tabloul care defineşte
paleta pentru culoarea care se modică;
- cod este un întreg din intervalul {0, 1,. . . , 63} şi reprezintă codul culorii care o
înlocuieşte în paletă pe cea veche.

Exemplu: setpalette (DARKGRAY, 45); modifică culoarea corespunzătoare


indicelui DARKGRAY (adică 8) prin
culoarea de cod 45.

Funcţia setallpalette permite modificarea simultană a mai multor culori din


compunerea paletei şi are prototipul:

void far setallpalette (struct palettetype far *paleta);

unde:
- palettetype este un tip definit în fişierul graphics.h astfel
struct palettetype
{ unsigned char size;
unsigned char colors [MAXCOLOR+1];
};
cu - size dimensiunea paletei
- colors tablou ale cărui elemente au ca valori codurile
componente ale paletei care se defineşte.

Modificarea paletei curente cu ajutorul funcţiilor setpalette şi setallpalette conduce la


schimbarea corespunzătoare a culorilor afişate pe ecran în momentul apelului funcţiilor
respective.
Pentru a determina codurile culorilor componente ale paletei curente se va folosi
funcţia getpalette de prototip:

void far getpalette (struct palettetype far *paleta);

Paleta implicită poate fi determinată folosind funcţia getdefaultpalette de prototip:

117
struct palettetype *far getdefaultpalette(void);

Numărul culorilor dintr-o paletă poate fi obţinut apelând funcţia getmaxcolor de


prototip:
int far getmaxcolor (void);

Funcţia returnează numărul maxim de culori diminuat cu 1. Deci în cazul adaptorului


EGA funcţia returnează valoarea 15.

O altă funcţie care determină dimensiunea paletei este funcţia getpalettesize cu


prototipul:
int far getpalettesize (void);

Funcţia returnează numărul culorilor componente ale paletei. În cazul adaptorului


EGA funcţia returnează valoarea 16.

Exemplu:
Programul următor afişează codurile culorilor pentru paleta implicită:
#include <graphics.h>
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{ int gd = DETECT, gm, i;
struct palettetype far *pal = (void *) 0;
initgraph (&gd, &gm, “C:\\BORLANDC\\BGI”);
pal = getdefaultpalette ();

for (i=0; i<16; i++)


{ printf (“colors[%d]=%d\n”, i, pal -> colors[i]);
getch ();
}
closegraph();
}

13.3. Starea ecranului


În mod grafic ecranul se compune din n*m pixeli. Pixelul din stânga-sus are
coordonatele (0,0), iar pixelul din dreapta-jos are coordonatele (n-1,m-1).
În BGI există mai multe funcţii care permit utilizatorului să obţină informaţii despre:
- coordonata maximă pe orizontală;
- coordonata maximă pe verticală;
- poziţia curentă (pixelul curent);
- etc.

int far getmaxx (void); returnează abscisa maximă;


int far getmaxy (void); returnează ordonata maximă;
int far getx (void); returnează poziţia pe orizontală (abscisa) pixelului curent;
int far gety (void); returnează poziţia pe verticală (ordonata) pixelului curent.

Exemplu:
#include <graphics.h>
118
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
void main (void)
{ int gd = DETECT, gm, cul_fond, cul_desen, curent_x,curent_y, maxx, maxy;
initgraph (&gd, &gm, “C:\\BORLANDC\\BGI”);
cul_fond = getbkcolor();
cul_desen = getcolor();
maxx = getmaxx();
maxy = getmaxy();
curent_x = getx();
curent_y = gety();
closegraph();
printf (“culoarea fondului = %d\n”, cul_fond);
printf (“culoare desenare = %d\n”, cul_desen);
printf (“abscisa maxima = %d\n”, maxx);
printf (“ordonata maxima = %d\n”, maxy);
printf (“abscisa curenta = %d\n”, curent_x);
printf (“ordonata curenta = %d\n”, curent_y);
printf (“acionai o tasta pentru terminare”);
getch();
}

119
14. Probleme diverse
De multe ori suntem puşi în faţa unor probleme pe care le înţelegem uşor dar nu ştim
cum să le rezolvăm cât mai simplu şi elegant. Vă propunem câteva metode care bine însuşite pot
duce, uneori, la o rapidă rezolvare a problemelor dificile. Evident că, nu toate problemele pot fi
încadrate în aceste tipare propuse dar fiecare programator poate să-şi formeze un astfel de
"portofoliu" de metode cu care să poate aborda orice problemă. Metodele prezentate în
continuare pot constitui un început.

14.1. Generarea combinărilor


Fie o mulţime oarecare de n elemente care poate fi pusă într-o corespondenţă biunivocă
cu mulţimea A={1,...,n}. Ne propunem să determinăm toate m-combinările (mn) ale mulţimii
A (submulţimi de m elemente ale mulţimii A). Vom rezolva problema, nerecursiv, pornind de la
m-combinarea V=(1,2,...,m) determinând succesiv toate m-combinările ordonate lexicografic
crescător.
Fie V=(v1,v2,...,vm) o m-combinare oarecare, atunci m-combinarea următoare în ordine
lexicografică crescătoare se obţine astfel:
a) se determină cel mai mare indice i satisfăcând relaţiile:

vi<n-m+i, vi+1=n-m+i+1,..., vm-1=n-1, vm=n. (1)

b) se trece la vectorul următor:

(v1, ..., vi-1,vi+1,vi+2, ..., vi+n-i+1);

c) dacă nu există un astfel de indice i (care să satisfacă relaţiile (1)) înseamnă că vectorul V
conţine ultima m-combinare şi anume: (n-m+1,n-m+2, ...,n).

Dăm în continuare o funcţie C care generează o m-combinare cu n elemente având ca


parametru cod o variabilă booleană care pentru valoarea 0 generează prima m-combinare iar
pentru valoarea 1 generează următoarea m-combinare. Funcţia comb reîntoarce valoarea 1 dacă
s-a generat o m-combinare oarecare şi valoarea 0 dacă s-a generat ultima m-combinare (în acest
caz cod are valoarea 0). Se va folosi un vector global v în care se generează m-combinările.
#define dim 50
#include <stdio.h>
int v[dim+1], n, m;
// functia generatoare a m-combinarilor
int comb(cod)
int cod;
{
int i,j; // generarea primei m-combinari 1,...,m
if (cod == 0)
{
for (i=1; i<=m; v[i]=i++);
return (1);
}
i=m+1;

do { i-- } // cautarea indicelui i pentru satisfacerea relatiilor (1)


120
while (v[i] >= n-m+i);
if (i)
{
v[i]=v[i]+1;
for (j=i+1; j<=m; v[j]=v[j-1]+1,j++);
return (1);
}
else return (0);
}

void main(void)
{
int kod,i;
printf("\ndati n: ");
scanf ("%d",&n);
printf("\ndati m: ");
scanf ("%d",&m);
comb(0);
kod=1;
while (kod)
{ printf("\n");
for (i=1;i<=m;printf ("%3d",v[i++]));
kod = comb(kod);
}
getche();
}
14.2. Metoda greedy
Se aplică problemelor în care se dă o mulţime A conţinând n date (de orice natură şi
structură) de intrare cerându-se să se determine o submulţime B (BA) care să îndeplinească
anumite condiţii pentru a fi acceptată. Cum, în general, există mai multe astfel de submulţimi
(numite soluţii posibile) se mai dă şi un criteriu conform căruia dintre aceste submulţimi să
alegem una singură (numită soluţie optimă) ca rezultat final. Foarte multe probleme de căutare
se înscriu în acest tip.
Menţionăm că, în general, soluţiile posibile au următoarele proprietăţi:
- se presupune că  este soluţie posibilă;
- dacă B este soluţie posibilăţi CB atunci şi C este soluţie posibilă;
Vom da în continuare o variantă a tehnicii greedy (denumire care în traducere înseamnnă
lăcomie, înghiţire) în care se porneşte de la mulţimea vidă. Apoi se alege pe rând, într-un anumit
fel, un element din A neales încă. Dacă adăugarea lui la soluţia parţial anterior construită
conduce la o soluţie posibilă, atunci se adaugă, altfel se alege un nou element. Tot acest procedeu
se repetă pentru toate elementele din A. Dăm în continuare în pseudocod o procedură:
procedure GREEDY (n,A,B)
B:=;
for i=1,n do
call ALEGE (A,i,x);
call POSIBIL (B,x,cod);
if cod=1 then
call ADAUG (B,x);
endif;
endfor;
return;
end procedure
Despre procedurile apelate din GREEDY precizăm următoarele:

121
- procedura ALEGE furnizează în x un element din A a j{ai,...,an} şi interschimbă ai cu aj;
dacă la fiecare pas se cercetează ai atunci procedura se simplifică;
- procedura POSIBIL verifică dacă elementul x poate fi adăugat sau nu mulţimii parţiale
construită până la pasul curent furnizând o valoare booleană cod cu semnificaţia:
cod = 1, dacă B U {x}, este soluţie posibilă
cod = 0, altfel
- procedura ADAUG înlocuieşte pe B cu B{x}.

Obs. Procedurile de mai sus nu sunt necesare întotdeauna, acest fapt depinzând de complexitatea
problemei. Oricum trebuie reţinut cadrul de rezolvare al acestui tip de problemă.

Problemă rezolvată

Se dă o mulţime de valori reale X={x1, . . .,xn}. Se cere submulţimea YX astfel ca  y /yY,
să fie maximă.

Evident că problema este foarte simplă (în Y trebuie introduse elementele strict pozitive
din X; evităm să mai construim procedurile ALEGE, POSIBIL, ADAUG) şi vom da
rezolvarea ei în pseudocod:

procedure SUMA (n,X,Y,k)


integer n,X(n),Y(n),k
k:=0;
for i=1,n do
if x(i) > 0 then k:=k+1;
y(k):=x(i)
endif;
endfor;
return;
end procedure

Probleme propuse

14.2.1.
Se dau n şiruri S1,S2,...,Sn ordonate crescător, de lungimi L1,L2, ...,Ln. Să se obţină un şir S de
lungime L1+L2+...+Ln cu toate elementele celor n şiruri ordonate crescător (problemă de
interclasare).

Indicaţie: Vom interclasa succesiv câte două şiruri în final obţinând şirul ordonat crescător.
Complexitatea interclasării a două şiruri A şi B de lungimi a şi b depinde direct proporţional de
a+b (pentru că se fac a+b deplasări de elemente). Se pune problema găsirii ordinii optime în
care trebuie efectuate aceste interclasări astfel ca numărul total de deplasări să fie minim.
Vom da un exemplu pentru a lămuri mai bine lucrurile:
fie 3 şiruri de lungimi (90,40,10). Interclasăm şirul S1 cu S2 apoi şirul rezultat cu S3; putem nota
acest lucru formal prin (S1+S2)+S3. Se obţin (90+40) + (130+10)=270 deplasări. Dacă vom
interclasa şirurile în ordinea (S 2+S3)+S1 vom obţine (40+10)+ (50+90)=190 de deplasări. De aici
concluzia că întotdeauna vom interclasa şirurile de lungimi minime din şirurile rămase.

14.2.2.
122
Găsiţi tripletele de numere pitagorice din Nn x Nn x Nn (prin Nn notat mulţimea {0,1,2,...,n}).
Încercaţi optimizarea timpului de execuţie a programului.

14.2.3.
Fie mulţimea m-combinărilor luate din n elemente şi fie k<m un număr natural. Să se dea un
algoritm şi apoi să se scrie un program C astfel încât să se determine o submulţime de m-
combinări cu proprietatea că oricare două m-combinări au cel mult k elemente comune.

14.2.4.
Să se determine mulţimile interior stabile (MIS) ale unui graf oarecare dat prin matricea sa de
adiacenţă.

14.3. Metoda backtracking (căutare cu revenire)


Această metodă se aplică problemelor ce pot fi reprezentate sub forma unui arbore finit
iar căutarea soluţiei presupune parcurgerea arborelui în adâncime (DF=Depth First).
Problemele de acest tip au în general soluţia de forma x=(x1, . . . ,xn)  S1x . . . xSn, fiecare Sk
fiind o mulţime finită. Mai facem câteva precizări preliminare:
a) pentru fiecare problemă sunt date anumite relaţii între componentele x1, . . . ,xn ale lui
x numite condiţii interne;
b) produsul cartezian S=S1x...xSn se mai numeşte spaţiul soluţiilor posibile, iar soluţiile
posibile care satisfac condiţiile interne se numesc soluţii rezultat;
c) în general se cer două lucruri în astfel de probleme:
- determinarea tuturor soluţiilor rezultat;
- determinarea doar a acelor soluţii care optimizează o funcţie obiectiv dată.

Cum rezolvăm astfel de probleme? Există două modalităţi de rezolvare:


1) tehnica directă (numită şi tehnica forţei brute) prin care se generează toate elementele
spaţiului de soluţii posibile şi apoi se verifică fiecare dacă este sau nu o soluţie rezultat; această
tehnică necesită un timp prohibitiv (dacă fiecare S i are doar 2 componente complexitatea este
O(2n); totodată ar fi necesară imbricarea a n cicluri cu n aprioric necunoscut).
2) backtracking care evită generarea tuturor soluţiilor posibile.
Să dăm în continuare câteva repere ale rezolvării:
- soluţia este construită progresiv, componentă cu componentă;
- lui xk i se atribuie o valoare (evident că numai din Sk) dacă şi numai dacă x1, . . . ,xk-1 au deja
valori;
- se verifică condiţiile de continuare (strâns legate de condiţiile interne) dacă are sens
trecerea la xk+1;
1. dacă nu are sens (adică condiţiile de continuare nu sunt îndeplinite atunci
facem o nouă alegere pentru xk sau dacă am epuizat valorile din Sk atunci k:=k-1
şi se face o nouă alegere pentru xk-1; ş.a.m.d.
2. dacă are sens atunci k:=k+1 şi se testează dacă k>n:
21) dacă inegalitatea este adevărată atunci se afişează soluţia astfel determinată
şi k:=k-1 continuând procesul de la punctul 1;
22) dacă inegalitatea este falsă se continuă procesul de la punctul 1.

Procedura rezolvării unor astfel de probleme este:


procedure BT(n,x)

123
integer n;
array x(n);
k:=1;
while k>0 do
cod:=0;
while ([mai exist o valoare α din Sk netestat] and cod=0)
x(k):=α;
if k(x(1),...,x(k)) then cod:=1;
endif;
endwhile;
if cod=0 then
k:=k-1
else
if k=n then write (x(1),...,x(n))
else k:=k+1
endif
endif;
endwhile;
return;
end procedure

Vom face câteva precizări:


1o. Predicatul k(x1, . . . , xk) reprezintă condiţiile de continuare pentru x1, . . . , xk;
2o. Cod este o valoare ce indică îndeplinirea/neîndeplinirea condiţiilor de continuare;
3o. Dacă predicatul k(x1, . . . , xk) este adevărat  k  {1,...,n} atunci se vor afla toţi vectorii
din S;
4o. Backtracking poate fi uşor reprezentat pe un arbore construit astfel:
- nivelul 1 conţine rădăcina;
- din orice nod de pe nivelul k pleacă sk muchii spre nivelul k+1, etichetate cu cele
sk elemente ale lui Sk;
- nivelul n+1 va conţine s1*s2*. . .* sn noduri frunză;
- pentru fiecare nod de pe nivelul n+1 etichetele muchiilor conţinute în drumul ce
leagă rădăcina de acest nod reprezintă o soluţie posibilă;

Dacă mulţimile Sk reprezintă progresii aritmetice atunci algoritmul general se modifică astfel:

procedure BTA(n,a,b,h)
integer n;
array primul(n),ultimul(n),ratia(n),x(n);
k:=1;
x(1):=primul(1)-ratia(1);
while k>0 do
cod:=0;
while ( x(k)+ratia(k)  ultimul(k) and cod=0 )
x(k):=x(k)+ratia(k);
if k(x(1),...,x(k)) then cod:=1 endif;
endwhile;
if cod=0 then
k:=k-1
else

124
if k=n then write (x(1),...,x(n))
else k:=k+1
x(k):=a(k)-h(k)
endif
endif;
endwhile;
return;
end procedure

unde:
- primul(n) reprezintă vectorul primilor termeni ai progresiilor aritmetice;
- ultimul(n) reprezintă vectorul ultimilor termeni ai progresiilor aritmetice;
- ratia(n) reprezintă vectorul raţiilor progresiilor aritmetice;

De reţinut cele două avantaje ale acestui procedeu:


- evitarea imbricării unui număr oarecare de cicluri aprioric variabil (în algoritmul propus se
imbrică doar două cicluri pretestate while);
- evitarea construirii spaţiului tuturor soluţiilor posibile S1xS2x . . . xSn.

Problemă rezolvată

În câte moduri se pot aranja 8 dame pe tabla de şah astfel încât să nu se "bată" reciproc. Să se
folosească al doilea algoritm dintre cei menţionaţi anterior.

Prima variantă

Acest program respectă algoritmul anterior cu unele mici modificări. Facem precizarea
că vectorul x conţine în componenta x[i] numărul coloanei de pe tabla de şah pe care se va afla
dama de pe linia i. Tipărirea va reprezenta o permutare (din cele 8! soluţii posibile). Se vor afla
92 de soluţii. Lăsăm pe seama cititorului să deducă analogiile şi diferenţele între algoritm şi
program.

#include <stdio.h>
#include <math.h>
void main (void)
{ int x[9],cod,k,i,nr;
k=1; x[1]=0;nr=0;
while (k>0)
{ cod=0;
while (((x[k]+1)<=8)&&(cod= =0))
{ x[k]++;
if ((k= =1) && (x[k]= =1)) cod=1;
else { i=1; cod=1;
while ((cod==1)&&(i<k))
{ if ((x[i]==x[k])||(abs(x[i]-x[k])==k-i)) cod=0;
i++; } // sfarsit while
} // sfarsit if
} // sfarsit while

if (cod= =0) k--;


else {if (k= =8)
{ printf("\n%3d. ",++nr);
for (i=1;i<9;printf("%d ",x[i++]));
}}
125
else x[++k]=0;
}
}
}

A doua variantă:

#include <stdio.h>
#include <math.h>
#define n 100
int x[100],cod,k,nc,nsol;

int Verifica(void)
{ int i,cod1; // cod1=1 conditiile de continuare
cod1=1; // sunt verificate
if (k>1) // cod1=0 in caz contrar;
for(i=1; i<= (k-1); i++)
{ if (x[k]= =x[i]) cod1=0;
if (abs(x[k]-x[i]) = = k-i) cod1=0; // (*)
}
return cod1;
}

void ScrieSolutie(void)
{ int i;
printf("\n%3d. ",++nsol);
for (i=1; i<=nc; printf("%3d",x[i++]));
}

void CitesteDate(void)
{ int i;
nsol=0;
clrscr();
nc=n+1;
while ((nc>n) || (nc<0))
{ printf ("Dati n = ");
scanf ("%d",&nc);
};
}
void main (void)
{ CitesteDate();
k=1; x[1]=0;
while (k>0)
{ cod=0;
while (((x[k]+1) <= nc) && (cod= =0))
{x[k]++;
cod=Verifica();
}
if (cod= =0) k--;
else {if (k==nc) ScrieSolutie();
else x[++k]=0;
}
}
}

A doua variantă este modulară, mai uşor de înţeles şi generalizează problema damelor
până la tabla de şah de 100x100. Lăsăm pe seama cititorului modificarea funcţiei ScrieSolutie
pentru a afişa în mod grafic tabla de şah.
Dacă în funcţia Verifica se şterge instrucţiunea notată cu (*) atunci se obţin toate
permutările de n obiecte.
126
Probleme propuse

14.3.1
Să se rezolve problema turelor de şah după al doilea algoritm. În câte moduri se pot aranja n
turnuri pe tabla de şah astfel încât să nu se "bată" reciproc.

14.3.2.
Să se afişeze poziţiile succesive ale unui cal pe tabla de şah, pornind dintr-o poziţie dată, astfel
încât să fie atinse toate căsuţele tablei de şah.

14.3.3.
Având un fişier cu o mulţime de cuvinte din limba română de aceeaşi lungime k să se scrie un
program C care afişează toate careurile rebusiste fără puncte negre. ( Problema e fascinantă
implicând şi cunoştinţe gramaticale dar şi cunoscând faptul că nu s-au construit careuri de 10x10
fără puncte negre manual şi nici cu ajutorul calculatorului; se poate încerca apoi şi cu
k:=11,12, . . .).

14.3.4.
Un intreprinzător dispune de un capital C şi are n variante de investiţii. Pentru fiecare investiţie i
cunoaşte fondurile de investiţie fi precum şi beneficiile bi. Se cere un program care să deducă
toate variantele posibile de investiţii al intreprinzătorului. Se mai dau şi condiţiile Cci  i
{1, . . . ,n}.

14.3.5.
Având un graf neorientat caracterizat prin matricea costurilor să se determine prin bactraking
circuitul de cost minim pornind dintr-un vârf dat.

14.3.6.
Având un graf neorientat caracterizat prin matricea de incidenţă a vârfurilor să se determine prin
bactraking mulţimile interior stabile maximale.

14.3.7.
Să se determine toate cuvintele binare de lungime 10 care conţin exact 5 cifre de 1.

14.3.8.
Să se determine toate cuvintele binare de lungime 10 care conţin cel mult 5 cifre de 1.

14.3.9.
Să se determine toate cuvintele din {a,b,c} * (mulţimea tuturor cuvintelor peste alfabetul Σ se
notează cu Σ* ) de lungime 10 care conţin exact 2 simboluri 'a'; 3 simboluri 'b' şi 5 simboluri 'c'.

14.3.10.
Să se determine toate cuvintele din {a,b,c}* de lungime n care conţin exact na simboluri 'a'; nb
simboluri 'b' şi nc simboluri 'c' (cu condiţia n=na+nb+nc).

14.3.11.
Să se determine toate tripletele (x1,x2,x3) de numere astfel ca:
x1+x2+x3suma
127
x1*x2*x3produs
cu valorile suma şi produs date iar x1S1, x2S2 şi x3S3 ; S1, S2 şi S3 fiind trei progresii
aritmetice date deasemenea.

14.3.12.
Să se determine toate variantele de pronosport cu 13 rezultate din {1,x,2} care conţin exact n1
simboluri '1'; nx simboluri 'x' şi n2 simboluri '2' (cu condiţia n1+nx+n2=13).

14.3.13.
Să se determine toate variantele de pronosport cu 13 rezultate din {1,x,2} care conţin cel mult n1
simboluri '1'; cel mult nx simboluri 'x' şi simboluri '2' în rest (cu condiţia n1+nx13).

14.4. Metoda divide et impera (divide şi stăpâneşte)


Această modalitate de elaborare a programelor constă în împărţirea repetată a unei
probleme de dimensiune mai mare în două sau mai multe subprobleme de acelaşi tip urmată de
combinarea soluţiilor subproblemelor rezolvate pentru a obţine soluţia problemei iniţiale.
Se dă un vector A=(a1,...,an) şi trebuie efectuată o prelucrare oarecare asupra elementelor
sale.
Presupunem că:
 p,q{1,...,n} cu 1 p < q   m{p,p+1,...,q-1} a.î. prelucrarea secvenţei {ap,...,aq} se poate
face prelucrând subsecvenţele:
{ap,...,am} şi {am+1,...,aq} şi apoi combinând rezultatele pentru a obţine prelucrarea întregii
secvenţe {ap,...,aq}.
Dacă se reuşeşte o astfel de formalizare a problemei atunci ea poate fi rezolvată cu ajutorul
acestei metode.
Vom da în continuare o procedură recursivă în pseudocod:

procedure DI (p,q,α)
if q-p  eps then
call PREL (p,q,α)
else
call IMPARTE (p,q,m) ;
call DI (p,m,β);
call DI (m+1,q,γ);
call COMB (β,γ,α);
endif;
return;
end procedure

Câteva precizări se impun:


- procedura trebuie apelată prin call DI (1,n,α) în α obţinându-se rezultatul final;
- eps este lungimea maxim a unei secvene {ap,...,aq} notată prin (p,q) pentru care se face
prelucrarea directă fără a mai fi necesară împărţirea în subprobleme;
- procedura PREL realizează prelucrarea directă a secvenţelor (p,q);
- procedura COMB realizează combinarea rezultatelor β şi γ ale prelucrării a două secvenţe
vecine (p,m) şi (m+1,q) obţinând rezultatul α al prelucrării întregii secvenţe (p,q);
- prin procedura IMPARTE se obţine valoarea lui m.
128
Vom da ca exemplu problema sortării crescătoare a unui şir de întregi prin interclasare.
- deoarece secvenţele (i,i+1) sunt uşor de ordonat acestea vor constitui secvenţele ce se vor
prelucra, deci eps = 1;
- m se va calcula ca (p+q)/2, deci nu mai e nevoie de procedura specială IMPARTE;
- procedura COMB va interclasa întotdeauna două secvenţe (p,m) şi (m+1,q) ordonate
crescător;
- vom folosi un vector x drept structură globală şi vom face toate prelucrările pe elementele
sale nemaiavând nevoie de zonele α,β;
- pentru zona γ vom folosi un vector local y în procedura COMB acesta conţinând
elementele corespondente din x dar ordonate crescător; tot în procedura COMB se vor
copia apoi elementele lui y din porţiunea (p,q) în x;
- evident că procedurile din schema generală a algoritmului sunt funcţii C cititorul făcând
analogiile necesare.
#include <stdio.h>
#include <conio.h>
#define nrelem 100
int x[nrelem];
int n;

void PREL (int p, int q)


{int aux;
if ((p<q) && (x[p] > x[q])) {
aux=x[p];
x[p]=x[q];
x[q]=aux;
}
}

void COMB (int inf, int mijloc, int sup)


{int i,j,k,l;
int y[nrelem];
i=k=inf;
j=mijloc+1;
while (i<=mijloc && j<=sup)
{ if (x[i] <= x[j])
y[k++]=x[i++];
else
y[k++]=x[j++];
}
for(l=i; l<=mijloc; y[k++]=x[l++]);
for(l=j; l<=sup; y[k++]=x[l++]);
for(k=inf; k<=sup; x[k++]=y[k]);
}
void DI (int p, int q)
{ int m;
if ((q-p) <= 1)
PREL (p,q);
else
{m=(p+q)/2;
DI (p,m);
DI (m+1,q);
COMB (p,m,q);
}
return;
}

void main(void)
{int i;
129
clrscr();
printf ("dati nr de elemente\n");
scanf ("%d",&n);
for (i=1; i<=n; i++)
{printf("x[%d]=",i);
scanf("%d",&x[i]);
}
DI (1,n);
printf("\nsirul sortat crescator este:\n");
for (i=1; i<=n; i++) printf("x[%d]=%d\n",i,x[i]);
getch();
}

14.5. Metoda programării dinamice


Această metodă este aplicabilă problemelor de optim în care soluţia poate fi privită ca
rezultatul unui şir de decizii d1, . . . ,d n. Fiecare decizie depinde de deciziile deja luate şi nu este
unic determinată (spre deosebire de tehnica greedy unde fiecare decizie care se ia trebuie să fie
unică). Totodată este necesar să fie satisfăcută una din variantele principiului optimalităţii pentru
a putea fi aplicată această metodă.
Vom formaliza acest principiu al optimalităţii:
- fie d1,...,dn un şir optim de decizii (SOD) care transformă starea so în starea sn, trecând prin
stările intermediare s1, . . . ,sn-1; vom nota acest fapt prin (d1,dn) este SOD pentru perechea de
stări (so,sn);
- grafic procesul este descris ca în figura următoare:

d1 d2 dn
*----->*---->*------>* . . . *------>*
so s1 s2 sn-1 sn

Vom da acum mai multe variante ale principiului optimalităţii:

1) dacă (d1,dn) este SOD pentru (so,sn) atunci (d2,dn) este SOD pentru (s1,sn);

2) dacă (d1,dn) este SOD pentru (so,sn) atunci  i din {1, . . . ,n-1} avem
a) (d1,di) este SOD pentru (so,si) SAU
b) (di+1,dn) este SOD pentru (si,sn).

3) dacă (d1,dn) este SOD pentru (so,sn) atunci  i din {1, . . . ,n-1} avem
a) (d1,di) este SOD pentru (so,si) ŞI
b) (di+1,dn) este SOD pentru (si,sn).

Ultima variantă este cea mai generală şi mai completă. Odată verificat o formă a
principiului optimalităţii, metoda programării dinamice constă în a scrie relaţiile de recurenţă şi
apoi de a le rezolva. În general relaţiile de recurenţă sunt de 2 tipuri :

1) fiecare decizie di depinde de di+1,...,dn - relaţii de recurenţă înainte, deciziile vor fi luate în
ordinea dn, dn-1, . . . ,d1;
2) fiecare decizie di depinde de d1, . . . ,d i-1 - relaţii de recurenţă înapoi, deciziile vor fi luate în
ordinea d1, d2, . . . , dn.

Problemă rezolvată

130
Fie G=(X,Γ) un 1-graf orientat căruia îi ataşăm matricea costurilor, (fiecare arc (i,j)Γ
este etichetat cu o valoare reală strict pozitivă). Se pune problema găsirii drumului de valoare
minim (DVM) între oricare 2 vârfuri i şi j.

Rezolvare

Verificăm prima variantă a principiului optimalităţii:


- fie i, i1, i2, . . . , im, j DVM între i şi j atunci evident că i1, . . . , im, j este DVM între i1 şi
j;
- notăm prin L(i,k) lungimea DVM dintre i şi k,  kX;
- notăm deasemenea dkj valoarea arcului (k,j);
- ecuaţiile de recurenţă sunt:

L(i,j) = min {L(i,k) + dkj)}


(k,j)Γ
#include<stdio.h>
#include<values.h>
#define nn 10
int d[nn+1][nn+1],i,j,n;

int L(int i,int j)


{
int m=MAXINT;
int k,x=0;
if (i= =j) m=0;
else
for (k=1;k<=n;k++)
if (d[k][j] < MAXINT)
{ x=L(i,k)+d[k][j];
if (x<m) m=x;
}
return m;
}

void citestematrice(void)
{int i,j;
printf("\ndati dimensiunea matricii distantelor : ");
scanf("%d",&n);

for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{ printf("d[%d,%d]= ",i,j);
scanf ("%d",&d[i][j]);
}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) if (d[i][j]= = -1) d[i][j]=MAXINT;
}

void citeste(void)
{ printf("\ndati varful initial : ");
scanf("%d",&i);
printf("\ndati varful final : ");
scanf("%d",&j);
}

void afiseaza(int val)


131
{ printf("\nvdlm intre varful %d si %d este %d",i,j,val);
}

void main(void)
{ int vdlm;
clrscr();
citestematrice();
citeste();
vdlm=L(i,j);
afiseaza(vdlm);
getch();
}

Probleme propuse

14.5.1.
Se dă o matrice A=(aij), i=1,...,n; j=1,...,m cu elementele din mulţimea {0,1,2}. Două elemente
din A aij şi akl se numesc 4-vecine dacă i-k+j-l = 1.
Notăm cu So, S1 şi S2 submulţimile formate din elementele matricii egale cu 0, 1 respectiv 2.
Submulţimea S1 se împarte în grupuri astfel: aij şi akl fac parte din acelaşi grup dacă sunt 4-vecine
sau dacă  apq  S1 : aij şi apq sunt 4-vecine iar apk şi akl sunt din acelasi grup. Un element akl  So
îl vom numi spion al grupului G  S1 dac  aij  G a.î. akl şi aij s fie vecine. Un spion este
perfect dac are toţi vecinii din G.
Se cere:
a) cel mai numeros grup (S1) care are un singur spion (dacă există).
b) toate grupurile care au cel puţin doi spioni perfecţi.

14.5.2.
Se dă o matrice cu elemente care au valori din {d,o}, care reprezintă un teren cu drumuri {d} şi
obstacole {o}. În acest teren se află un tanc şi o ţintă. Acest tanc poate primi comenzi pentru
rotirea ţevii (cu 90o în ambele sensuri), pentru deplasare (în direcţia ţevii cu o linie sau cu o
coloană) sau pentru tragere (în direcţia ţevii pentru a nimeri ţinta) în cazul în care între tanc şi
ţintă nu există nici un obstacol. Considerând că deplasarea nu se poate efectua prin obstacole, se
cere cel mai scurt drum necesar tancului pentru a putea distruge ţinta şi şirul comenzilor
efectuate în cazul în care exist un astfel de drum.

14.5.3.
Se d o matrice cu elemente din {0,1,2,3}, reprezentând o pădure cu capcane (0) şi cărări (1). În
această pădure se află o vulpe (2) şi mai mulţi lupi (3). Fiecare lup încearcă să se apropie de
vulpe fără a şti unde se află capcanele, iar în cazul în care cade într-o capcană, moare. Vulpea
încearcă să se îndepărteze de cel mai apropiat lup, având însă posibilitatea să descopere şi
capcanele şi să le ocolească. Atât vulpea cât şi lupii îi pot modifica poziţia doar cu o linie sau cu
o coloană în fiecare moment. Să se spună dacă vulpea reuşeşte să scape de lupi.

14.5.4.
Se consideră un teren dreptunghiular sub forma unei matrici A de m linii şi n coloane.
Elementele aij ale matricii conţin cotele (înălţimile) diferitelor porţiuni astfel obţinute. Se
presupune că o bilă se găseşte pe o porţiune (io,jo) având cota a(io,jo).

132
Se cere un program care să precizeze toate traseele (începând cu (i o,jo) ) posibile pe care le poate
urma bila spre a ieşi din teren, ştiind că bila se poate deplasa în orice porţiune de teren 4-
învecinat cu o cotă strict inferioară cotei terenului pe care se găseşte bila.

14.5.5.
Se cere un program care rezolvă problema labirintului (nerecursiv).
O matrice de m linii şi n coloane reprezintă un labirint dacă:
 a(i,j) = o - semnificând culoar;
 a(i,j) = 1 - semnificând zid.
Un traseu de ieşire pornind de la o porţiune (io,jo) trebuie să fie o succesiune de perechi (io, jo),
(i1, j1) . . . (ik, jk) astfel încât 2 perechi învecinate să fie 4-vecine şi a(ip,jp)=0
 p=0, . . . ,k.

TEMĂ:

Să se scrie programe C care rezolvă 5 probleme propuse din capitolul 14.

133
PARTEA II

Limbajul C++

134
15. Facilităţi noi în limbajul C++
15.1. Extinderea limbajului C
Programele scrise în limbajul C pot fi executate şi folosind compilatorul C++
(eventual cu modificări nesemnificative). Deci limbajul C++ conţine toate instrucţiunile
limbajului C, dar în plus oferă şi facilităţi noi. Cele mai importante avantaje ale limbajului C+
+ faţă de C sunt următoarele. S-a introdus noţiunea de clasă prin care se defineşte un tip
abstract de date, astfel programatorul are posibilitatea folosirii metodei de programare prin
abstractizarea datelor. Clasele pot fi organizate într-o ierarhie de clase, deci este posibilă şi
programarea prin metode orientate obiect.
În continuare prezentăm nişte elemente noi ale limbajul C++, iar în secţiunile care
urmează vor fi tratate facilităţi noi, mai importante, referitoare la operatori, structuri,
reuniuni, tipuri enumerare şi funcţii.
Există un nou tip de comentarii în limbajul C++ de forma: // comentariu
În acest caz, tot ce se află după caracterele // se va ignora de către compilator, până la
sfârşitul rândului.
În limbajul C o instrucţiune compusă este de forma:
{
declaraţii
instrucţiuni
}

Deci toate declaraţiile trebuie să fie înainte de prima instrucţiune. În limbajul C++
declaraţiile şi instrucţiunile pot fi în orice ordine în interiorul unei instrucţiuni compuse.
Nu există instrucţiuni de intrare/ieşire în limbajul C, şi nici în C++. În schimb în
limbajul C există biblioteci standard de funcţii pentru operaţii de intrare/ieşire. Ele pot fi
folosite şi în limbajul C++, dar în afară de aceste biblioteci, mai există şi ierarhii de clase
pentru realizarea operaţiilor de intrare/ieşire. Ele vor fi tratate pe larg în capitolul 4, dar
pentru a putea utiliza aceste clase, prezentăm aici nişte exemple simple, ilustrative.
Dispozitivului standard de intrare respectiv ieşire s-au ataşat aşa numitele streamuri
standard: cin pentru stdin şi cout pentru stdout. Operaţiile de intrare se vor efectua aplicând
operatorul >> streamului standar cin, iar cele de ieşire aplicând operatorul << streamului
standard cout.
Prezentăm în continuare exemple de operaţii de intrare/ieşire folosind streamurile
standard cin şi cout. Pentru fiecare exemplu se va specifica în formă de comentariu
echivalentul operaţiei, folosind funcţii din biblioteca standard a limbajului C.
Ierarhia de clase pentru operaţii de intrare/ieşire poate fi utilizată numai dacă se
include fişierul iostream.h. În continuare prezentăm exemple pentru operaţii de afişare pe
dispozitivul standard de ieşire:
#include <iostream.h>
int main() {
...
int x;
...
cout << x; // printf("%d", x);
...
double y;
...
cout << y; // printf("%lg", y);
...

135
char z;
...
cout << z; // printf("%c", z);
...
char t[] = "exemplu";
...
cout << t; // printf("%s", t);
...
}

Exemple pentru operaţii de citire de la dispozitivul standard de intrare:


#include <iostream.h>
int main() {
...
int x;
...
cin >> x; // scanf("%d", &x);
...
double y;
...
cin >> y; // scanf("%lf", &y);
...
char z[20];
...
cin >> z; // scanf("%s", z);
...
}

Fără a intra acum în detalii menţionăm că operatorii << respectiv >> pot fi aplicaţi
înlănţuit. Deci de exemplu
int w;
...
cout << "w = " << w + 25 << '\n';

este echivalent cu
int w;
...
printf("w = %d\n", w);

Trecerea la linie nouă se poate efectua şi cu


cout << endl;

Deci în loc de apelarea funcţiei printf de mai sus, s-ar fi putut scrie şi:
cout << "w = " << w + 25 << endl;

Există o diferenţă minoră dintre limbajele C şi C++, referitoare la constantele de tip


caracter. Ele sunt memorate pe doi octeţi în limbajul C şi au tipul int. În limbajul C++ însă
există două feluri de constante de tip caracter. Memorate pe un octet ele au tipul char, iar pe
doi octeţi au tipul int.
Să considerăm următorul exemplu. Fişierul caracter.cpp:
#include <iostream.h>
#include <conio.h>

136
int main() {
clrscr();
cout << 'A' << " cod: " << int('A') << " octeti: "
<< sizeof 'A' << endl;
cout << 'B' << " cod: " << int('B') << " octeti: "
<< sizeof 'B' << endl;
cout << 'AB' << " octeti: " << sizeof 'AB' << endl;
cout << "\'B\' * 256 + \'A\' = " << 'B' * 256 + 'A';
return 0;
}

După executarea programului se obţine:

A cod: 65 octeti: 1
B cod: 66 octeti: 1
16961 octeti: 2
'B' * 256 + 'A' = 16961

Observăm că într-adevăr caracterul 'AB' este de tip int şi în octetul mai semnificativ
este memorat codul ASCII al caracterului 'B', iar în cel mai puţin semnificativ codul ASCII al
caracterului 'A'.
În limbajul C utilizarea operatorului de conversie explicită se face sub forma:
(tip) expresie

În limbajul C++ operatorul de conversie explicită se poate aplica şi în modul următor:


tip(expresie)

Să considerăm următorul exemplu. Fişierul conv1.cpp:


#include <iostream.h>
#include <conio.h>

typedef double* p_double;

int main() {
double x = 4.0 / 3;
void* p;
clrscr();
cout << x << endl;
cout << int( x ) << endl; // int( x ) este echivalenta cu
// (int) x
p = &x;
cout << *p_double(p) << endl; // *p_double(p) este
// echivalenta cu
// *(double *) p
return 0;
}

După executarea programului obţinem:


1.333333
1
1.333333

137
Rezultă că într-adevăr s-a realizat conversia variabilei x de la tipul double la tipul int,
şi conversia variabilei p de la tipul void* la tipul double*. Menţionăm că în acest caz,
introducerea tipului p_double a fost obligatorie. Dacă s-ar fi folosit conversia explicită
(double*) p, atunci nu ar fi fost necesar definirea tipului p_double.
În limbajul C++ putem defini şi conversii utilizator ataşate unor tipuri definite de
programator. Conversiile utilizator se vor trata în secţiunea 16.8.

15.2. Operatori
15.2.1. Operatorul de rezoluţie

Dacă o dată globală se redefineşte într-o funcţie, atunci referirea la data globală se
poate face cu ajutorul operatorului de rezoluţie. Numele datei globale trebuie precedată de
caracterele ::, deci de operatorul de rezoluţie.
Prezentăm în continuare un exemplu. Fişierul rezol.cpp:
#include <iostream.h>
#include <conio.h>

int x = 65;

int main() {
clrscr();
char x = 'A';
cout << "Variabila globala: " << ::x << endl;
cout << "Variabila locala: " << x << endl;
return 0;
}

După execuţie se obţine:


Variabila globala: 65
Variabila locala: A

Deci se observă că referirea la variabila globală x de tip int s-a făcut cu ajutorul
operatorului de rezoluţie (::x ). Menţionăm că operatorul de rezoluţie se va folosi şi în cazul
în care se face referire la datele respectiv funcţiile membru ale unei clase.

15.2.2. Folosirea operatorului adresă pentru definirea tipului referinţă

În limbajul C operatorul & se foloseşte pentru a determina adresa unei variabile. În


limbajul C++ acest operator mai este folosit şi pentru definirea tipului referinţă. Am văzut că
în cazul pointerilor construcţia

tip *

reprezintă un tip pointer. La fel şi în cazul de faţă prin

tip &

138
se va înţelege un tip referinţă. Cu ajutorul tipului referinţă putem defini nume alternative
pentru date deja cunoscute. Aceste nume alternative pot fi utilizate în continuare în locul
datelor respective. Deasemenea acest tip de date se va folosi şi pentru realizarea apelului prin
referinţă în limbajul C++.
Dacă se foloseşte ca şi parametru formal într-o funcţie, atunci tipul referinţă va fi de
forma:
tip & parametru_formal

iar declararea unei variabile de tip referinţă se va face sub forma


tip & nume = dată;

Exemplu. Fişierul refer1.cpp.


#include <iostream.h>
#include <conio.h>

void afiseaza(int x[], int y, int* z)


{
cout << "x[0] = " << x[0] << " "
<< "y = " << y << " "
<< "*z = " << *z << endl;
}

int main() {
clrscr();
int x[4] = {10, 20, 30, 40};
int& y = x[0]; // y este referinta la primul element
// al vectorului x
int* z = x; // z este pointer catre vectorul x
afiseaza(x, y, z);
y = 50;
afiseaza(x, y, z);
*z = 60;
afiseaza(x, y, z);
return 0;
}

Prin execuţie se obţine:


x[0] = 10 y = 10 *z = 10
x[0] = 50 y = 50 *z = 50
x[0] = 60 y = 60 *z = 60

Observăm că aceeaşi zonă de memorie este accesată în trei feluri. Primul element al
vectorului x este x[0], y este referinţă la acest element, deci un nume sinonim pentru x[0], iar
z este un pointer către vectorul x, astfel *z reprezentând acceaşi zonă de memorie ca şi x[0].
Rezultă că valoarea oricărei dintre aceste trei elemente s-ar schimba, valorile celorlalte se
schimbă în mod automat.
Există posibilitatea definirii unei referinţe şi la o funcţie. Acest lucru este prezentat în
exemplul următor. Fişierul refer2.cpp:
#include <iostream.h>
#include <math.h>
#include <conio.h>

139
typedef double TipFunctie(double);

int main() {
clrscr();
const double Pi = 3.141592653;
TipFunctie& sinus = sin;
cout << sinus( Pi / 2 ); // se afiseaza valoarea 1
return 0;
}

În exemplul de mai sus s-a declarat o referinţă la funcţia standard de bibliotecă sin,
deci numele sinus poate fi folosit în locul funcţiei sin pentru a calcula sinusul unui număr
real.
Tipul referinţă este cel mai des folosit pentru realizarea apelului prin referinţă, ceea ce
se tratează în paragraful următor.

15.2.3. Apelul prin referinţă

În limbajul C apelul este prin valoare. Acest lucru înseamnă că parametrii actuali şi
cei formali sunt memorate în diferite locaţii de memorie. La apelarea funcţiei parametrii
actuali îşi transmit valoarea lor parametrilor formali corespunzătoare. De aceea modificarea
parametrilor formali nu are nici un efect asupra parametrilor actuali. Următorul exemplu scris
în limbajul C ilustrează acest lucru. Fişierul valoare1.cpp:
#include <stdio.h>
#include <conio.h>

void nu_modifica( int x )


{
x += 20; // valoarea parametrului formal x
} // se mareste cu 20

int main () {
int y;
y = 10;
clrscr();
nu_modifica( y ); // parametrul actual y nu se modifica
printf("y = %d", y);
return 0;
}

Constatăm că într-adevăr parametrul actual y nu se modifică, şi programul afişează


valoarea 10.
Totuşi şi în limbajul C se poate realiza modificarea parametrilor actuali, cu ajutorul
pointerilor. Acest lucru este prezentat în următorul exemplu.
Fişierul valoare2.cpp:
#include <stdio.h>
#include <conio.h>

void modifica( int *x ) // parametrul formal x este un


{ // pointer catre un intreg
*x += 20; // intregul spre care pointeaza x
} // se mareste cu 20

140
int main () {
int y;
y = 10;
clrscr();
modifica( &y ); // La apelare adresa lui y se transmite
// prin valoare parametrului formal x,
// deci intregul spre care pointeaza x
// adica *x, va fi chiar y.
// In consecinta modificarea valorii lui
// *x ne conduce la modificarea
// valorii lui y.
printf("y = %d", y);
return 0;
}

În exemplul de mai sus s-a realizat modificarea valorii parametrului actual y, dar şi în
acest caz s-a folosit de fapt apel prin valoare. S-a transmis valoarea adresei lui y pointerului
x. Deci prin intermediul pointerilor apelul prin valoare s-a transformat în apel prin referinţă.
Menţionăm că numele unui vector de elemente este un pointer către primul element al
vectorului. În consecinţă, dacă un parametru actual este un vector, atunci valorile elementelor
acestui vector pot fi modificate în interiorul funcţiei.
Această modalitate de realizare a apelului prin referinţă, prin intermediul pointerilor,
de multe ori îngreunează munca programatorului, de aceea în limbajul C++ s-a introdus şi
apelul prin referinţă propriu zis.
Apelul prin referinţă se realizează cu ajutorul tipului referinţă. În cazul folosirii tipului
referinţă parametrul formal este un nume alternativ pentru parametrul actual, deci ele
reprezintă aceeaşi zonă de memorie şi ca atare orice modificare a parametrului formal
conduce la modificarea parametrului actual corespunzător. Iată cum se modifică exemplul de
mai sus folosind apelul prin referinţă. Fişierul refer3.cpp:
#include <iostream.h>
#include <conio.h>

void modifica( int& x ) // parametrul formal x este o


{ // referinta la un intreg
x += 20; // valoarea parametrului formal x
} // se mareste cu 20

int main () {
int y;
y = 10;
clrscr();
modifica( y ); // Parametrul formal x este un nume
// alternativ pentru y, deci modificarea
// valorii lui x conduce la modificarea
// valorii lui y.
cout << "y = " << y;
return 0;
}

Putem constata că apelul prin referinţă este mult mai simplu de utilizat, decât
realizarea ei prin intermediul pointerilor.

141
15.2.4. Operatori pentru alocare şi dezalocare dinamică a memoriei

Pentru alocare şi dezalocare de memorie în limbajul C pot fi folosite funcţiile malloc


şi free. În limbajul C++ există şi operatori pentru realizarea acestui lucru.
Alocarea memoriei se poate face folosind operatorul new, în unul din următoarele trei
moduri:
a) new tip
b) new tip(expresie_1)
c) new tip[expresie_1]

În toate cele trei cazuri construcţiile de mai sus formează o expresie pe care o vom
nota cu expresie_2. Valoarea expresiei expresie_2 este egală cu adresa de început a zonei de
memorie alocate, şi este zero în cazul unei erori.
În cazul a) se alocă zonă de memorie pentru o variabilă de tipul exprimat prin tip. În
cazul b) zona de memorie alocată se iniţializează cu valoarea expresiei expresie_1, iar în
cazul c) se alocă memorie un număr de expresie_1 de zone de memorie de tipul menţionat.
Deci în cazul a) şi b) se vor aloca un număr de sizeof(tip) octeţi, iar în cazul c)
expresie_1 * sizeof(tip) octeţi.
În tabelul 1 prezentăm trei exemple de secvenţe de programe echivalente, în care se
foloseşte operatorul new în diferite moduri.
Eliberarea zonei de memorie alocate cu operatorul new se va face cu ajutorul
operatorului delete. Să considerăm pointerul q pentru care s-a alocat memorie folosind
operatorul delete.
Dacă alocarea zonei de memorie s-a făcut cu una din variantele a) sau b) atunci
dezalocarea se va face în forma:
delete q;

iar dacă alocarea s-a făcut prin varianta c) atunci eliberarea zonei de memorie se va face cu
delete [expresie_1] q;

unde expresie_1 este expresia care s-a folosit la alocare. De obicei ea poate să lipsească.

Nr. crt. C++ C sau C++


1. double* d; double *d;
d = new double; d = (double *) malloc(sizeof(double));
2. int* i; int* i;
i = new int(20); i = new int;
*i = 20;
3. double* p; d = (double *) malloc(10 * sizeof(double));
p = new double[10];

Tabelul 1: folosirea operatorului new

15.3. Structuri, reuniuni şi tipuri enumerare


În limbajul C++ numele după cuvântul cheie struct, union respectiv enum poate fi
folosit pentru identificarea structurii, reuniunii respectiv a tipului enumerare. De exemplu în
limbajul C struct nume formează un tip de date, în limbajul C++ nu mai este nevoie de
cuvântul cheie struct pentru a se referi la structura deja declarată.
De exemplu să considerăm structura:

142
struct Persoana {
char nume[15];
char prenume[15];
long telefon;
};

Atunci următoarea declaraţie este corectă în C++ şi greşită în C.


Persoana Adalbert, Marta;

O altă proprietate importantă este că în limbajul C++ o funcţie poate avea structuri ca
şi parametrii. Deasemenea şi valoarea returnată de o funcţie poate fi o structură. În consecinţă
valoarea returnată de funcţie şi parametrii unei funcţii pot fi structuri, pointeri către structuri
şi referinţe la structuri.
Se pot efectua instrucţiuni de atribuire cu structurile de acelaşi tip. De exemplu:
Persoana A, Adalbert;
A = Adalbert;

În interiorul unei structuri se poate declara o reuniune anonimă în limbajul C++ sub
forma:
struct nume {
declaraţii
union {
declaraţii
};
declaraţii
} lisă_de_nume;

În acest caz, referirea la elementele declarate în interiorul reuniunii anonime, se face


ca şi referirea la elementele structurii.
În limbajul C++ primul element al unei reuniuni poate fi iniţializat.
Considerăm următorul exemplu. Fişierul union1.cpp:
#include <iostream.h>
#include <conio.h>

union date {
int x[8];
double y[2];
};

int main() {
clrscr();
date D = {1, 2, 3, 4, 5, 6, 7, 8}; // initializarea lui x
for(int i=1; i <= 8; i++)
cout << "x[" << i-1 << "]=" << D.x[i-1]
<< ( i%4 ? " " : "\n");
D.y[0] = 1.5;
D.y[1] = 2.4;
cout << "\ny[0] = " << D.y[0]
<< " y[1] = " << D.y[1] << endl;
return 0;
}

Prin execuţie se obţine:


x[0]=1 x[1]=2 x[2]=3 x[3]=4
143
x[4]=5 x[5]=6 x[6]=7 x[7]=8

y[0] = 1.5 y[1] = 2.4

Rezultă că primul element (vectorul x) poate fi iniţializat la fel cum s-ar fi procedat în
cazul iniţializării elementului în sine (adică a unui vector).
În limbajul C++ variabilelor de tip enumerare se pot atribui numai elementele
componente ale tipului. Numerele asociate cu aceste elemente nu se pot atribui variabilelor de
tip enumerare. Exemplu
enum culori {rosu, galben, verde};
culori Semafor;
...
Semafor = 1; // corect in C, gresit in C++
Semafor = galben; // corect

În continuare ne vom ocupa de facilităţi noi oferite de limbajul C++ referitoare la


funcţii.

15.4. Funcţii
15.4.1. Iniţializarea parametrilor formali ai funcţiilor

Parametrii formali ai unei funcţii pot fi iniţializaţi în limbajul C++. Vom spune că
parametrii iniţializaţi au o valoare implicită. În lista parametrilor formali elementele
iniţializate vor fi declarate în felul următor:
tip nume = expresie

Totuşi există anumite restricţii în acest sens. Dacă există atât parametri formali
iniţializaţi cât şi neiniţializaţi, atunci cei neiniţializaţi trebuie să fie înainte de cei iniţializaţi în
lista parametrilor formali.
Prezentăm un exemplu în continuare. Fişierul init1.cpp:
#include <iostream.h>
#include <conio.h>

void intilniri(char* nume, char* loc,


int ora = 12, int minut = 0)
{
cout << "Nume: " << nume << endl
<< "Loc: " << loc << endl
<< "Ora: " << ora << endl
<< "Minut: " << minut << "\n\n";
}

int main() {
clrscr();
cout << "Intilniri:\n\n";
intilniri("Daniel", "Facultatea de Litere");
intilniri("Eva", "Scoala Generala", 14);
intilniri("bunica", "acasa", 17, 20);
return 0;
}

144
Dacă se execută programul se obţine:
Intilniri:

Nume: Daniel
Loc: Facultatea de Litere
Ora: 12
Minut: 0

Nume: Eva
Loc: Scoala Generala
Ora: 14
Minut: 0

Nume: bunica
Loc: acasa
Ora: 17
Minut: 20

Observăm că paramerii formali ora şi minut sunt iniţializaţi cu valorile implicite 12,
respectiv 0. Dacă parametrii formali iniţializaţi nu au parametrii actuali corespunzători, atunci
se vor folosi valorile lor implicite, în caz contrar se va ignora valoarea implicită şi se vor
folosi parametrii actuali corespunzători.
Este interesant de remarcat că la iniţializare se poate folosi orice expresie, deci de
exemplu şi apeluri de funcţii. În următorul exemplu apar parametrii formali iniţializaţi în
acest fel. Fişierul init2.cpp:

#include <iostream.h>
#include <conio.h>
#include <dos.h>

char ziua()
{
date* d;
getdate( d );
return d->da_day;
}

char luna()
{
date* d;
getdate( d );
return d->da_mon;
}

int anul()
{
date* d;
getdate( d );
return d->da_year;
}

void scrie_data( char z = ziua(),


char l = luna(),
int a = anul() )
{
cout << "ziua: " << int(z)
<< " luna: " << int(l)
<< " anul: " << a << endl;
145
}
int main() {
clrscr();
scrie_data();
scrie_data(11);
scrie_data(18,4);
scrie_data(27,7,1991);
return 0;
}

În exemplul de mai sus s-a folosit că în fişierul dos.h este declarată o structură
pentru memorarea datelor sub forma:
struct date {
int da_year; // Anul exprimat printr-un întreg.
char da_day; // Caracterul se converteşte în int
// şi astfel se obţine ziua.
char da_mon; // Caracterul se converteşte în int
// şi astfel se obţine luna.
};

Funcţia getdate are un parametru, care este un pointer către structura de mai sus, în
care se va returna data curentă.
Fişierul se poate compila cu un compilator C++ sub sistemul de operare Dos sau
Windows, şi după execuţie se obţine un rezultat care poate varia în funcţie de data curentă.
De exemplu (data curentă: 24 august 1998):
ziua: 24 luna: 8 anul: 1998
ziua: 11 luna: 8 anul: 1998
ziua: 18 luna: 4 anul: 1998
ziua: 27 luna: 7 anul: 1991

Din rezultate reiese că în cazul în care nu s-a specificat parametru actual, s-a folosit
valoarea implicită, deci data curentă.

15.4.2. Funcţii care returnează tipuri referinţă

În limbajul C++ tipul returnat de o funcţie poate fi şi tip referinţă. În acest caz antetul
funcţiei va fi de forma:
tip& nume(lista_parametrilor_formali)

Funcţiile care returnează tipuri referinţă pot apare atât pe partea dreaptă cât şi pe
partea stângă a operatorilor de atribuire. Se spune că ele pot fi atât operanzi lvalue (left value)
cât şi rvalue (right value). Un operand lvalue poate să apară pe partea stângă, iar unul rvalue
pe partea dreaptă a operatorilor de atribuire.
Să considerăm următorul exemplu. Într-o urnă se află bile albe şi negre. Se extrage un
număr de bile din urnă la întâmplare. Să se scrie un program pentru aflarea numărului de bile
albe, respectiv negre extrase. Fişierul refer4.cpp:
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>
#include <time.h>
146
long& alb_negru( long& ref_alb, long& ref_negru)
{
if ( random(2) )
return ref_alb;
return ref_negru;
}

void main() {
long nr_alb = 0;
long nr_negru = 0;
long nrbile;
clrscr();
cout << "Numarul de bile extrase: ";
cin >> nrbile;
randomize();
for( long i = 0; i < nrbile; i++ )
alb_negru( nr_alb, nr_negru )++;
cout << "Bile albe: " << nr_alb
<< "\nBile negre: " << nr_negru;
}

După compilarea sub sistemul de operare Dos sau Windows cu Borland C++, şi
executarea programului, se obţine un rezultat care depinde de numărul total de bile, număr
citit de la intrarea standard. Numărul de bile albe respectiv negre extrase se determină în mod
aleator. Deci rezultatul poate fi:
Numarul de bile extrase: 50
Bile albe: 27
Bile negre: 23

Menţionăm că s-a evidenţiat numărul citit. Observăm că funcţia alb_negru returnează


o referinţă către unul din parametrii nr_alb sau nr_negru în funcţie de faptul că bila extrasă
este albă sau neagră. Este important de menţionat că parametrii formali ref_alb şi ref_negru ai
funcţiei alb_negru trebuie să fie declarate ca şi referinţe la tipul long. În caz contrar, în cadrul
funcţiei s-ar încerca returnarea unei referinţe către o variabilă locală (unul din parametrii
formali), ceea ce este nepermis.
În cadrul funcţiei main s-a apelat funcţia alb_negru şi totodată s-a folosit şi operatorul
de incrementare. Acest lucru este posibil datorită faptului că funcţia returnează o referinţă la o
variabilă declarată în funcţia main, şi ca atare apelul funcţiei se comportă ca şi un nume de
variabilă.
Bineînţeles această problemă simplă s-ar fi putut rezolva şi fără folosirea unei funcţii,
care returnează o referinţă. În varianta fără funcţia care returnează o referinţă, operatorul de
incrementare s-ar fi apelat de două ori, pentru două variabile distincte (nr_alb şi nr_negru).
În cazul unei probleme mai complexe, de exemplu dacă operatorul de incrementare s-ar fi
înlocuit cu operaţii mai complicate, utilizarea unei funcţii, care returnează o referinţă, poate
simplifica considerabil programul.
În capitolele care urmează, în cadrul definirii claselor, ne vom întâlni cu situaţii, în
care apare necesitatea declarării unor funcţii, care returnează o referinţă către clasa curentă.

15.4.3. Supraîncărcarea funcţiilor

În limbajul C numele funcţiilor distincte trebuie să fie diferit. În limbajul C++ însă
putem defini funcţii diferite având numele identic. Această proprietate se numeşte
147
supraîncărcarea funcţiilor. De obicei funcţiile se vor supraîncărca în cazul în care nişte
operaţii asemănătoare sunt descrise de mai multe variante ale unor funcţii. În general aceste
funcţii se referă la proprietăţi asemănătore, aplicate pentru parametrii formali diferiţi.
Pentru a putea face distincţie între funcţiile supraîncărcate, antetul acestor funcţii
trebuie să fie diferit. Astfel tipul parametrilor actuali va determina funcţia care se va apela.
Prezentăm un exemplu legat de variantele funcţiei trigonometrice atan, pentru
calcularea numărului π. Fişierul sup_fun1.cpp:
#include <iostream.h>
#include <conio.h>
#include <math.h>
#include <complex.h>

/*
double atan(double x);
double atan2(double y, double x);
long double atanl(long double (x));
long double atan2l(long double (y), long double (x));
complex atan(complex x);
*/

int main() {
clrscr();
cout.precision(18); // numerele reale vor fi afisate
// cu 18 zecimale
cout << 4 * atan( 1 ) << endl;
cout << 4 * atanl( 1 ) << endl;
cout << atan2(0, -1) << endl;
cout << atan2l(0, -1) << endl;
cout << real( 4 * atan( complex(1, 0) ) ) << endl;
return 0;
}

În fişierul de mai sus, în formă de comentariu s-au specificat cele cinci variante ale
funcţiei atan. Primele patru sunt declarate în fişierul math.h, ele au numele distincte şi se
referă la parametrii formali de tip real. În Borland C++ există funcţia atan, cu acelaşi nume ca
şi prima funcţie referitoare la parametrii de tip real. Această funcţie este declarată în fişierul
complex.h , şi se apelează în cazul unui parametru actual de tip complex.
După executarea programului obţinem:
3.141592653589793116
3.141592653589793238
3.141592653589793116
3.141592653589793238
3.141592653589793116

Observăm că în cazul în care nu s-au folosit variabile de tip long double (funcţiile
atan cu parametrii de tip double respectiv complex, şi funcţia atan2) primele 15 zecimale sunt
corecte, iar pentru funcţiile atanl şi atan2l toate cele 18 zecimale obţinute sunt corecte.
Exemplul de mai sus se poate modifica astfel încât toate cele cinci variante ale
funcţiei atan să aibă acelaşi nume. Pentru a realiza acest lucru vom scrie trei funcţii cu
numele atan, care vor apela funcţiile atan2, atanl respectiv atan2l. Deoarece antetul funcţiilor
va fi diferit, se poate efectua supraîncărcarea lor. Prezentăm în continuare această variantă
modificată a programului. Fişierul sup_fun2.cpp:
148
#include <iostream.h>
#include <conio.h>
#include <math.h>
#include <complex.h>

//double atan(double x);

double atan(double y, double x)


{
return atan2(y, x);
}

long double atan(long double (x))


{
return atanl(x);
}

long double atan(long double (y), long double (x))


{
return atan2l(y, x);
}

//complex atan(complex x);

int main() {
clrscr();
cout.precision(18);
cout << 4 * atan( 1.0 ) << endl;
cout << 4 * atan( 1.0L ) << endl;
cout << atan(0.0, -1.0) << endl;
cout << atan(0.0L, -1.0L) << endl;
cout << real( 4 * atan( complex(1, 0) ) ) << endl;
return 0;
}

Dacă se execută programul, se obţine acelaşi rezultat ca şi pentru programul


precedent. În acest caz numele tuturor funcţiilor este acelaşi, deci funcţia care se va apela, se
determină după numărul şi tipul parametrilor actuali. De aceea trebuie specificat cu exactitate
tipul parametrilor formali (de exemplu 1.0 este constantă de tip double, şi 1.0L este constantă
de tip long double).
În exemplul de mai sus se poate determina întotdeauna cu exactitate funcţia care se va
apela, deoarece există o funcţie pentru care atât numărul cât şi tipul parametrilor formali este
acelaşi ca şi pentru funcţia respectivă. Nu este însă întotdeauna în aşa fel. De exemplu, dacă
în funcţia main din exemplul de mai sus am fi scris
cout << 4 * atan( 1 ) << endl;

atunci deoarece parametrul actual al funcţiei atan este de tip întreg, nici unul din cele cinci
funcţii nu are un antet corespunzător. În astfel de cazuri se vor lua în considerare următoarele
reguli. Etapele determinării funcţiei care se va apela:

1. Dacă există o funcţie pentru care tipul parametrilor formali coincide cu tipul
parametrilor actuali corespunzătoare (deci şi numărul parametrilor formali
coincide cu numărul parametrilor actuali), atunci această funcţie se va apela.

149
2. Altfel, funcţia se va determina folosind conversia implicită pentru tipurile standard
(de exemplu tipul int se va converti în double). În acest caz nu vor fi pierderi de
informaţii.
3. Dacă nu s-a reuşit determinarea unei funcţii, atunci se încearcă o conversie pentru
tipuri standard cu eventuale pierderi de informaţii (de exemplu conversie din tipul
long în tipul int).
4. Dacă nici în acest fel nu s-a reuşit determinarea funcţiei, atunci se încearcă
aplicarea unor conversii şi tipurilor definite de programator.

Rezultă că în cazul expresiei cout << 4 * atan( 1 ) << endl; se va încerca aplicarea etapei
a doua a determinării funcţiei, dar deoarece s-ar putea aplica atât o conversie de la tipul int la
double, cât şi la long double, apare o ambiguitate care nu poate fi rezolvată de compilator, şi
se va afişa un mesaj de eroare la compilare.
Dacă însă revenim la exemplul din fişierul sup_fun1.cpp putem constata că la
compilarea expresiei amintite nu au apărut mesaje de eroare. Acest lucru se datorează faptului
că funcţia atan a fost supraîncărcat în aşa fel încât au existat numai două variante ale ei (una
cu parametru formal de tip double, iar alta de tip complex). În acest caz există o singură
conversie implicită posibilă pentru tipuri standard, deci se va aplica conversia de la tipul int la
double, şi se va apela funcţia atan cu un singur parametru de tip double.

15.4.4. Funcţii inline

Apelarea unei funcţii se face printr-un salt la adresa unde este memorată corpul
funcţiei, iar după executarea instrucţiunilor funcţiei, se face o revenire la locul apelării. În
cazul în care funcţia este foarte simplă operaţiile necesare apelării pot fi mai complicate decît
instrucţiunile funcţiei. În limbajul C această problemă se poate rezolva prin folosirea unor
macrouri. Ele se definesc în modul următor:
#define nume(listă_de_parametrii) şir_de_caractere

În faza de preprocesare apelurile macrourilor vor fi înlocuite cu şirul de caractere


specificat, în care parametrii formali vor fi înlocuiţi cu parametrii actuali. Observăm că
apelarea macrourilor seamănă cu apelarea funcţiilor. Totuşi există multe diferenţe. De
exemplu apelarea unei funcţii se face printr-un salt la adresa corpului funcţiei după care se
revine la locul apelării, iar apelarea macrourilor se face prin înlocuirea cu expresia
corespunzătoare în faza de preprocesare. Diferenţa cea mai importantă este că în cazul
apelării unei funcţii, se verifică corespondenţa dintre tipurile parametrilor formali şi cei
actuali, iar dacă este necesar se face şi o conversie la tipul corespunzător. Astfel de verificări
nu se fac în cazul macrourilor, ceea ce este un dezavantaj fiindcă poate să conducă la apariţia
unor erori. Dacă sunt folosite în mod corect, avantajul macrourilor este că ele pot fi apelate
pentru expresii de tipuri diferite.
În următorul exemplu vom defini trei macrouri pentru calculul valorii absolute a unui
număr, şi vom atrage atenţia asupra posibilităţii apariţiei unor erori. Fişierul macro1.cpp:
#include <iostream.h>
#include <conio.h>

#define Val_abs(x) ((x) > 0 ? (x) : -(x))


#define Val_abs_eronata_1(x) ( x > 0 ? x : -x )
#define Val_abs_eronata_2(x) (x) > 0 ? (x) : -(x)

int main() {
int y = 10;
150
long z = -50000;
clrscr();
cout << Val_abs( y ) << endl;
cout << Val_abs( z ) << endl;
cout << Val_abs(4000L * y + z ) << endl;
cout << Val_abs_eronata_1(4000L * y + z ) << endl;
cout << 15 + Val_abs( -y ) << endl;
cout << 15 + Val_abs_eronata_2( -y ); cout << endl;
cout << Val_abs( y++ ) << endl;
cout << "y = " << y << endl;
return 0;
}

După execuţie se obţine:


10
50000
10000
-90000
25
5
11
y = 12

Dacă parantezele ar lipsi din definirea macroului Val_abs(x), în unele expresii ar


putea să apară erori. Într-adevăr prin expresia
cout << Val_abs_eronata_1(4000L * y + z ) << endl;

se obţine rezultat necorespunzător (-90000 în loc de 10000), deoarece în faza de preprocesare


macroul Val_abs_eronata_1 s-a înlocuit cu:
( 4000L * y + z > 0 ? 4000L * y + z : -4000L * y + z )

şi rezultatul va fi: –4000L * y + z = -90000. Nici în cazul expresiei


cout << 15 + Val_abs_eronata_2( -y );

nu se obţine rezultat corect (5 în loc de 25), deoarece macroul Val_abs_eronata_2 se


înlocuieşte cu
(-y) > 0 ? (-y) : -(-y)
şi expresia va deveni:
cout << 15 +(-y) > 0 ? (-y) : -(-y);

Operatorul de adunare fiind mai prioritar decât operatorul ?:, se evaluează mai întâi
expresia 15 + (-y), şi se va afişa valoarea 5. În sfârşit, menţionăm că şi în cazul în care
macroul este definit corect pot să apară situaţii în care rezultatul nu va fi cel aşteptat. Dacă
apelarea macroului s-ar face ca şi în cazul funcţiilor, atunci prin evaluarea expresiei
cout << Val_abs( y++ ) << endl;

s-ar afişa valoarea 10, iar valoarea parametrului y după evaluarea expresiei ar fi 11. În cazul
nostru însă s-a afişat valoarea 11, iar după evaluarea expresiei s-a obţinut valoarea y = 12.
Explicaţia constă şi de această dată în modul de apelare a macrourilor. În faza de preprocesare
macroul Val_abs se înlocuieşte cu următorul şir de caractere:

151
((y++) > 0 ? (y++) : -(y++))

După evaluarea expresiei (y++) > 0, se incrementează valoarea lui y, deci rezultatul
expresiei condiţionale va fi egală cu 11, după care se incrementează încă o dată valoarea
variabilei y. În consecinţă după evaluarea expresiei se obţine y = 12.
Aceste neajunsuri ale macrourilor pot fi înlăturate cu ajutorul funcţiilor inline.
Apelarea funcţiilor inline se face în mod asemănător cu apelarea macrourilor. Apelul funcţiei
se va înlocui cu corpul funcţiei, deci operaţia de salt la adresa corpului funcţiei şi revenirea la
locul apelării nu sunt necesare. Comparativ cu macrouri, funcţia inline are avantajul, că se va
verifica corespondenţa dintre tipurile parametrilor formali şi cei actuali, şi se va face o
conversie de tipuri, dacă este necesar. Astfel posibilitatea apariţiei unor erori este diminuată.
Declararea sau definirea unei funcţii inline se face în mod obişnuit, dar antetul funcţiei se va
începe cu cuvântul cheie inline.
Exemplul de mai sus se poate transcrie folosind o funcţie inline, în modul următor.
Fişierul inline1.cpp:

#include <iostream.h>
#include <conio.h>

inline long Val_abs( long x )


{
return x > 0 ? x : -x;
}

int main() {
int y = 10;
long z = -50000;
clrscr();
cout << Val_abs( y ) << endl;
cout << Val_abs( z ) << endl;
cout << Val_abs(4000L * y + z ) << endl;
cout << 15 + Val_abs( -y ) << endl;
cout << Val_abs( y++ ) << endl;
cout << "y = " << y << endl;
return 0;
}

După executarea programului se obţine:


10
50000
10000
25
10
y = 11

Putem constata că, în acest caz, toate rezultatele obţinute sunt corecte. Menţionăm că
o funcţie inline nu se poate folosi numai într-un singur modul, funcţia nu poate să conţină
operaţii mai sofisticate, de exemplu nici instrucţiuni de ciclare. În cazul definirii claselor ne
vom întâlni foarte des cu funcţii inline.

152
16. Noţiunea de clasă
16.1. Realizarea protecţiei datelor prin metoda programării
modulare
Dezvoltarea programelor prin programare procedurală înseamnă folosirea unor funcţii
şi proceduri pentru scrierea programelor. În limbajul C lor le corespund funcţiile care
returnează o valoare sau nu. Însă în cazul aplicaţiilor mai mari ar fi de dorit să putem realiza
şi o protecţie corespunzătoare a datelor. Acest lucru ar însemna că numai o parte a funcţiilor
să aibă acces la datele problemei, acelea care se referă la datele respective. Programarea
modulară oferă o posibilitate de realizare a protecţiei datelor prin folosirea clasei de memorie
static. Dacă într-un fişier se declară o dată aparţinînd clasei de memorie statică în afara
funcţiilor, atunci ea poate fi folosită începînd cu locul declarării până la sfârşitul modulului
respectiv, dar nu şi în afara lui.
Să considerăm următorul exemplu simplu referitor la prelucrarea vectorilor de numere
întregi. Să se scrie un modul referitor la prelucrarea unui vector cu elemente întregi, cu
funcţii corespunzătoare pentru iniţializarea vectorului, eliberarea zonei de memorie ocupate şi
ridicarea la pătrat, respectiv afişarea elementelor vectorului. O posibilitate de implementare a
modulului este prezentată în fişierul vector1.cpp:
#include <iostream.h>

static int* e; //elementele vectorului


static int d; //dimensiunea vectorului

void init(int* e1, int d1) //initializare


{
d = d1;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = e1[i];
}

void distr() //eliberarea zonei de memorie ocupata


{
delete [] e;
}

void lapatrat() //ridicare la patrat


{
for(int i = 0; i < d; i++)
e[i] *= e[i];
}

void afiseaza() //afisare


{
for(int i = 0; i < d; i++)
cout << e[i] << ' ';
cout << endl;
}

Modulul se compilează separat obţinînd un program obiect. De exemplu sub sistemul


de operare Unix folosind comanda
CC –c vector1.cpp

153
obţinem programul obiect vector1.o. Un exemplu de program principal este prezentat în
fişierul vector2.cpp:
extern void init( int*, int);//extern poate fi omis
extern void distr();
extern void lapatrat();
extern void afiseaza();
//extern int* e;

int main() {
int x[5] = {1, 2, 3, 4, 5};
init(x, 5);
lapatrat();
afiseaza();
distr();
int y[] = {1, 2, 3, 4, 5, 6};
init(y, 6);
//e[1]=10; eroare, datele sunt protejate
lapatrat();
afiseaza();
distr();
return 0;
}

Sub sistemul de operare Unix prin comanda:


CC vector2.cpp vector1.o –o vector2
obţinem fişierul executabil vector2. Observăm că deşi în programul principal se lucrează cu
doi vectori nu putem să le folosim împreună, deci de exemplu modulul vector1.cpp nu
poate fi extins astfel încât să realizeze şi adunarea a doi vectori. În vederea înlăturării acestui
neajuns s-au introdus tipurile abstracte de date.

16.2. Tipuri abstracte de date


Tipurile abstracte de date realizează o legătură mai strînsă între datele problemei şi
operaţiile (funcţiile) care se referă la aceste date. Declararea unui tip abstract de date este
asemănătoarea cu declararea unei structuri, care în afară de date mai cuprinde şi declararea
sau definira funcţiilor referitoare la acestea.
De exemplu în cazul vectorilor cu elemente numere întregi putem declara tipul
abstract:
struct vect {
int* e;
int d;
void init(int* e1, int d1);
void distr() { delete [] e; }
void lapatrat();
void afiseaza();
};

Funcţiile declarate sau definite în interiorul structurii vor fi numite funcţii membru iar
datele date membru. Dacă o funcţie membru este definită în interiorul structurii (ca şi funcţia
distr din exemplul de mai sus), atunci ea se consideră funcţie inline. Dacă o funcţie membru
se defineşte în afara structurii, atunci numele funcţiei se va înlocui cu numele tipului abstract
urmat de operatorul de rezoluţie (::) şi numele funcţiei membru. Astfel funcţiile init, lapatrat
şi afiseaza vor fi definite în modul următor:
154
void vect::init(int *e1, int d1)
{
d = d1;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = e1[i];
}

void vect::lapatrat()
{
for(int i = 0; i < d; i++)
e[i] *= e[i];
}

void vect::afiseaza()
{
for(int i = 0; i < d; i++)
cout << e[i] << ' ';
cout << endl;
}

Deşi prin metoda de mai sus s-a realizat o legătură între datele problemei şi funcţiile
referitoare la aceste date, structurile ca tipuri abstracte de date nu ne permit protejarea datelor,
deci ele pot fi accesate de orice funcţie utilizator, nu numai de funcţiile membru. Acest
neajuns se poate înlătura cu ajutorul claselor.

16.3. Declararea claselor


Un tip abstract de date clasă se declară ca şi o structură, dar cuvântul cheie struct se
înlocuieşte cu class. Ca şi în cazul structurilor referirea la tipul de dată clasă se face cu
numele după cuvântul cheie class (numele clasei). Protecţia datelor se realizează cu
modificatorii de protecţie: private, protected şi public. După modificatorul de protecţie se
pune caracterul ‘:’. Modificatorul private şi protected reprezintă date protejate, iar public date
neprotejate. Domeniul de valabilitate a modificatorilor de protecţie este până la următorul
modificator din interiorul clasei, modificatorul implicit fiind private.
De exemplu clasa vector se poate declara în modul următor:
class vector {
int* e; //elementele vectorului
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
~vector() { delete [] e; }
void lapatrat();
void afiseaza();
};

Se observă că datele membru e şi d au fost declarate ca date de tip private (protejate),


iar funcţiile membru au fost declarate publice (neprotejate). Deci programatorul nu are acces
la datele membru numai prin funcţiile membru. Bineînţeles o parte din datele membru pot fi
declarate publice, şi unele funcţii membru pot fi declarate protejate, dacă natura problemei
cere acest lucru. În general datele membru protejate nu pot fi accesate numai de funcţiile
membru ale clasei respective şi eventual de alte funcţii numite funcţii prietene (sau funcţii
friend).
155
O funcţie prieten pentru o clasă, se declară în mod obişnuit, dar declaraţia trebuie să
înceapă cu cuvântul cheie friend. Declaraţia trebuie să fie în interiorul clasei pentru care
funcţia respectivă va fi funcţie prieten. Funcţia prieten se defineşte în mod obişnuit, fără
cuvântul cheie friend.
Funcţiile membru ale altor clase pot fi funcţii prietene pentru o clasă. Dacă dorim ca
toate funcţiile membru ale unei clase Clasa_1 să fie funcţii prietene pentru o clasă Clasa_2,
atunci clasa Clasa_1 se va declara ca şi clasă prietenă pentru clasa Clasa_2. Acest lucru se
poate realiza în modul următor:

class Clasa_1; // Clasa Clasa_1 se defineşte în mod incomplet


// în prealabil, pentru a se putea referi la ea.
// Ea va fi definită în mod complet ulterior.
class Clasa_2 {
...
friend Clasa_1; // Clasa_1 este clasă prietenă pentru Clasa_2
...
}

Este însă de dorit să se evite utilizarea funcţiilor şi claselor prietene pentru a obţine o
protecţie mai bună a datelor.
În secţiunile 18.2.5. şi 18.3.3. vom da exemple de funcţii prietene, şi totodată vom
prezenta o modalitate de a evita aceste funcţii prietene.
O altă observaţie importantă referitoare la exemplul de mai sus este că iniţializarea
datelor membru şi eliberarea zonei de memorie ocupată s-a făcut prin funcţii membru
specifice.
Datele declarate cu ajutorul tipului de dată clasă se numesc obiectele clasei, sau
simplu obiecte. Ele se declară în mod obişnuit în forma:
nume_clasă listă_de_obiecte;

De exemplu un obiect de tip vector se declară în modul următor:


vector v;

Iniţializarea obiectelor se face cu o funcţie membru specifică numită constructor. În


cazul distrugerii unui obiect se apeleză automat o altă funcţie membru specifică numită
destructor. În cazul exemplului de mai sus
vector(int* e1, int d1);
este un constructor, iar
~vector() { delete [] e; }
este un destructor.
Tipurile abstracte de date de tip struct pot fi şi ele considerate clase cu toate
elementele neprotejate. Observăm că constructorul de mai sus este declarat în interiorul
clasei, dar nu este definit, iar destructorul este definit în interiorul clasei. Rezultă că
destructorul este o funcţie inline. Definirea funcţiilor membru care sunt declarate, dar nu sunt
definite în interiorul clasei se face ca şi la tipuri abstracte de date de tip struct, folosind
operatorul de rezoluţie.

16.4. Referirea la elementele claselor. Pointerul this

156
Referirea la datele respectiv funcţiile membru ale claselor se face cu ajutorul
operatorilor . sau -> ca şi în cazul referirii la elementele unei structuri. De exemplu dacă se
declară:
vector v;
vector* p;
atunci afişarea vectorului v respectiv a vectorului referit de pointerul p se face prin:
v.afiseaza();
p->afiseaza();
În interiorul funcţiilor membru însă referirea la datele respectiv funcţiile membru ale
clasei se face simplu prin numele acestora fără a fi nevoie de operatorul punct ( . ) sau săgeată
( -> ). De fapt compilatorul generează automat un pointer special, pointerul this, la fiecare
apel de funcţie membru, şi foloseşte acest pointer pentru identificarea datelor şi funcţiilor
membru.
Pointerul this va fi declarat automat ca pointer către obiectul curent. În cazul
exemplului de mai sus pointerul this este adresa vectorului v respectiv adresa referită de
pointerul p.
Dacă în interiorul corpului funcţiei membru afiseaza se utilizează de exemplu data
membru d, atunci ea este interpretată de către compilator ca şi this->d.
Pointerul this poate fi folosit şi în mod explicit de către programator, dacă natura
problemei necesită acest lucru.
În continuare prezentăm un exemplu complet de clasă vector, cu funcţii membru
pentru ridicarea la pătrat respectiv afişarea elementelor vectorului, precum şi o funcţie
membru pentru adunarea a doi vectori. Fişierul vector3.cpp (conţine clasa vector):
#include <iostream.h>
#include <stdlib.h>

class vector {
private:
int* e; //elementele vectorului
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
~vector() { delete [] e; }
void lapatrat();
vector suma(vector& v1);
void afiseaza();
};

vector::vector(int* e1, int d1)


{
d = d1;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = e1[i];
}

void vector::lapatrat()
{
for(int i = 0; i < d; i++)
e[i] *= e[i];
}

vector vector::suma(vector& v1)


{
if (d != v1.d)
{

157
cerr << "Eroare: dimensiune diferita";
exit(1);
}
int* x = new int[d];
for(int i = 0; i < d; i++)
x[i] = e[i] + v1.e[i];
vector y(x, d);
delete [] x;
return y;
}

void vector::afiseaza()
{
for(int i = 0; i < d; i++)
cout << e[i] << ' ';
cout << endl;
}

Fişierul vector4.cpp (conţine funcţia main):


#include "vector3.cpp"

int main() {
int x[] = {1, 2, 3, 4, 5};
vector v(x, 5);
int y[] = {2, 4, 6, 8, 10};
vector t(y, 5);
v.suma(t).afiseaza();
return 0;
}

16.5. Constructorul
16.5.1. Iniţializarea obiectelor prin constructor

Iniţializarea obiectelor se face cu o funcţie membru specifică numită constructor.


Numele constructorului trebuie să coincidă cu numele clasei. O clasă poate să aibă mai mulţi
constructori. În acest caz aceste funcţii membru au numele comun, ceea ce se poate face
datorită posibilităţii de supraîncărcare a funcţiilor. Bineînţeles în acest caz numele şi/sau tipul
parametrilor formali trebuie să fie diferit, altfel compilatorul nu poate să aleagă constructorul
corespunzător.
Constructorul nu returnează o valoare. În acest caz nu este permis nici folosirea
cuvântului cheie void.
Prezentăm în continuare un exemplu de tip clasa cu mai mulţi constructori, având ca
date membru numele şi prenumele unei persoane, şi cu o funcţie membru pentru afişarea
numelui complet. Fişierul pereche1.cpp (conţine definiţia clasei persoana):
#include <string.h>
#include <iostream.h>

class persoana {
char* nume;
char* prenume;
public:
persoana(); //constructor implicit
158
persoana(char* n, char* p); //constructor
persoana(const persoana& p1); //constructor
//de copiere
~persoana(); //destructor
void afiseaza();
};

persoana::persoana()
{
nume = prenume = 0;
cout << "Apelarea constructorului implicit" << endl;
}

persoana::persoana(char* n, char* p)
{
nume = new char[strlen(n)+1];
prenume = new char[strlen(p)+1];
strcpy(nume, n);
strcpy(prenume, p);
}

persoana::persoana(const persoana& p1)


{
nume = new char[strlen(p1.nume)+1];
strcpy(nume, p1.nume);
prenume = new char[strlen(p1.prenume)+1];
strcpy(prenume, p1.prenume);
cout << "Apelarea constructorului de copiere" << endl;
}

persoana::~persoana()
{
delete[] nume;
delete[] prenume;
}

void persoana::afiseaza()
{
cout << prenume << ' ' << nume << endl;
}

Fişierul pereche2.cpp (conţine funcţia main):


#include "pereche1.cpp"

int main() {
persoana A; //se apeleaza constructorul implicit
A.afiseaza();
persoana B("Stroustrup", "Bjarne");
B.afiseaza();
persoana *C = new persoana("Kernighan","Brian");
C->afiseaza();
delete C;
persoana D(B); //echivalent cu persoana D = B;
//se apeleaza constructorul de copire
D.afiseaza();
return 0;

159
}

Observăm prezenţa a doi constructori specifici: constructorul implicit şi constructorul


de copiere. Dacă o clasă are constructor fără parametri atunci el se va numi constructor
implicit. Constructorul de copiere se foloseşte la iniţializarea obiectelor folosind un obiect de
acelaşi tip (în exemplul de mai sus o persoană cu numele şi prenumele identic). Constructorul
de copiere se declară în general în forma:
nume_clasă(const nume_clasă& obiect);

Cuvântul cheie const exprimă faptul că argumentul constructorului de copiere nu se


modifică.

16.5.2 Apelarea constructorilor în cazul în care datele membru sunt obiecte

O clasă poate să conţină ca date membru obiecte ale unei alte clase. Declarând clasa
sub forma:
class nume_clasa {
nume_clasa_1 ob_1;
nume_clasa_2 ob_2;
...
nume_clasa_n ob_n;
...
};
antetul constructorului clasei nume_clasa va fi de forma:
nume_clasa(lista_de_argumente):
ob_1(l_arg_1), ob_2(l_arg_2), ..., ob_n(l_arg_n)

unde lista_de_argumente respectiv l_arg_i reprezintă lista parametrilor formali ai


constructorului clasei nume_clasa respectiv ai obiectului ob_i.
Din lista ob_1(l_arg_1), ob_2(l_arg_2), ..., ob_n(l_arg_n) pot să lipsească
obiectele care nu au constructori definiţi de programator, sau obiectul care se iniţializează cu
un constructor implicit, sau cu toţi parametrii impliciţi.
Dacă în clasă există date membru de tip obiect atunci se vor apela mai întâi
constructorii datelor membru, iar după aceea corpul de instrucţiuni al constructorului clasei
respective. Fişierul pereche3.cpp:

#include "pereche1.cpp"

class pereche {
persoana sot;
persoana sotie;
public:
pereche() //definitia constructorului implicit
{ //se vor apela constructorii impliciti
} //pentru obiectele sot si sotie
pereche(persoana& sotul, persoana& sotia);
pereche(char* nume_sot, char* prenume_sot,
char* nume_sotie, char* prenume_sotie):
sot(nume_sot, prenume_sot),
sotie(nume_sotie, prenume_sotie)
{
}
void afiseaza();

160
};

inline pereche::pereche(persoana& sotul, persoana& sotia):


sot(sotul), sotie(sotia)
{
}

void pereche::afiseaza()
{
cout << "Sot: ";
sot.afiseaza();
cout << "Sotie: ";
sotie.afiseaza();
}

int main() {
persoana A("Pop", "Ion");
persoana B("Popa", "Ioana");
pereche AB(A, B);
AB.afiseaza();
pereche CD("C","C","D","D");
CD.afiseaza();
pereche EF;
EF.afiseaza();
return 0;
}

Observăm că în cazul celui de al doilea constructor, parametrii formali sot şi sotie au


fost declaraţi ca şi referinţe la tipul persoana. Dacă ar fi fost declaraţi ca parametri formali de
tip persoana, atunci în cazul declaraţiei:
pereche AB(A, B);

constructorul de copiere s-ar fi apelat de patru ori. În astfel de situaţii se crează mai întâi
obiecte temporale folosind constructorul de copiere (două apeluri în cazul de faţă), după care
se execută constructorii datelor membru de tip obiect (încă două apeluri).

16.6. Destructorul
Destructorul este funcţia membru care se apelează în cazul distrugerii obiectului.
Destructorul obiectelor globale se apelează automat la sfârşitul funcţiei main ca parte a
funcţiei exit. Deci nu este indicat folosirea funcţiei exit într-un destructor, pentru că acest
lucru duce la un ciclu infinit. Destructorul obiectelor locale se execută automat la terminarea
blocului în care s-au definit. În cazul obiectelor alocate dinamic, de obicei destructorul se
apelează indirect prin operatorul delete (obiectul trebuie să fi fost creat cu operatorul new).
Există şi un mod explicit de apelare a destructorului, în acest caz numele destructorului
trebuie precedat de numele clasei şi operatorul de rezoluţie.
Numele destructorului începe cu caracterul ~ după care urmează numele clasei. Ca şi
în cazul constructorului, destructorul nu returnează o valoare şi nu este permisă nici folosirea
cuvântului cheie void. Apelarea destructorului în diferite situaţii este ilustrată de următorul
exemplu. Fişierul destruct.cpp:
#include <iostream.h>
#include <string.h>

161
class scrie { //scrie pe stdout ce face.
char* nume;
public:
scrie(char* n);
~scrie();
};

scrie::scrie(char* n)
{
nume = new char[strlen(n)+1];
strcpy(nume, n);
cout << "Am creat obiectul: " << nume << '\n';
}

scrie::~scrie()
{
cout << "Am distrus obiectul: " << nume << '\n';
delete nume;
}

void functie()
{
cout << "Apelare functie" << '\n';
scrie local("Local");
}

scrie global("Global");

int main() {
scrie* dinamic = new scrie("Dinamic");
functie();
cout << "Se continua programul principal"
<< '\n';
delete dinamic;
return 0;
}

16.7. Supraîncărcarea operatorilor


16.7.1. Metoda generală de supraîncărcare

Tipurile abstracte de date au avantajul de a îngloba datele şi operaţiile referitoare la


aceste date. Operaţiile sunt exprimate cu ajutorul funcţiilor membru sau cu funcţii prietene.
Totuşi ar fi mai avantajos dacă s-ar putea folosi şi operatori pentru exprimarea acestor
operaţii. În exemplul referitor la clasa vector am văzut că putem scrie o funcţie membru care
calculează suma a doi vectori, respectiv una care afişează vectorul, dar apelarea funcţiilor se
face destul de complicat, în următoarea formă:
v.suma(t).afiseaza();

Ar fi mult mai simplu şi mai expresiv, dacă am putea scrie:


cout << v + t;

Pentru acesta trebuie supraîncărcat operatorul + pentru adunarea a doi vectori, şi


operatorul << pentru afişarea unui vector la dispozitivul standard de ieşire stdout.

162
În limbajul C++ nu se pot defini operatori noi, dar există posibilitatea supraîncărcării
operatorilor existenţi. Există şi câteva excepţii (de exemplu nu pot fi supraîncărcaţi următorii
operatori: . :: şi ?:). Prin supraîncărcare nu se poate modifica faptul că operatorul este unar
sau binar, nici prioritatea operatorilor şi nici direcţia de evaluare (asociativitatea) lor.
Supraîncărcarea operatorilor se face cu funcţii membru sau prietene specifice. Ele se
comportă ca orice altă funcţie membru sau prieten, dar numele funcţiei trebuie să fie format
din cuvântul cheie operator, după care pot fi eventual şi caractere albe, şi care va fi urmat de
operatorul respectiv. Reamintim că în general spaţiile, taburile ('\t') şi caracterele de trecere
la linie nouă ('\n') se numesc caractere albe.
Numărul parametrilor funcţiilor cu care se supraîncarcă operatorul se poate determina
cu exactitate din faptul că operatorul este unar sau binar şi dacă funcţia este funcţie membru
sau prieten. De exemplu dacă operatorul este binar şi supraîncărcarea se face printr-o funcţie
membru atunci ea va avea un singur parametru, dacă se foloseşte o funcţie prieten atunci va
avea doi parametri.
În cazul clasei vector operatorul + se poate supraîncărca în modul următor:
class vector {
private:
int* e; //elementele vectorului
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
~vector() { delete [] e; }
vector operator +(vector& v1); //supraincarcarea
//operatorului +
void afiseaza();
};

vector vector::operator +(vector& v1)


{
if (d != v1.d)
{
cerr << "Eroare: dimensiune diferita";
exit(1);
}
int* x = new int[d];
for(int i = 0; i < d; i++)
x[i] = e[i] + v1.e[i];
vector y(x, d);
delete [] x;
return y;
}

Observăm că de fapt nu s-a făcut altceva decât s-a înlocuit numele funcţiei cu
operator  +. Operatorul se apelează în modul următor:
int main() {
int x[] = {1, 2, 3, 4, 5};
vector v(x, 5);
int y[] = {2, 4, 6, 8, 10};
vector t(y, 5);
(v+t).afiseaza();
return 0;
}

163
16.7.2. Supraîncărcarea operatorilor de atribuire

În genereal se poate folosi operatorul de atribuire (=) şi pentru obiecte. Prezentăm în


continuare un exemplu pentru calcule cu fracţii. Fişierul fractie1.cpp:
#include <iostream.h>

class fractie {
int numarator;
int numitor;
public:
fractie(int numarator1, int numitor1);
fractie operator *( fractie& r); //calculeaza
//produsul
//a doua fractii
//nu simplifica
void afiseaza();
};

inline fractie::fractie(int numarator1 = 1,


int numitor1 = 0)
{
numarator = numarator1;
numitor = numitor1;
}

inline fractie fractie::operator *(fractie& r)


{
return fractie(numarator*r.numarator,
numitor*r.numitor);
}

inline void fractie::afiseaza()


{
if ( numitor )
cout << numarator << " / " << numitor << endl;
else
cerr << "Fractie incorecta";
}

int main() {
fractie x(3,4);
fractie y(5, 7);
fractie z;
z = x * y;
z.afiseaza();
return 0;
}

Observăm că operatorul de atribuire se poate utiliza şi rezultatul este cel dorit, deci se
afişează produsul celor două fracţii. Din păcate însă nu este întotdeauna aşa. Prezentăm un alt
exemplu legat de clasa vector pentru ilustrarea acestui lucru. Fişierul vector5.cpp:
#include <iostream.h>
#include <stdlib.h>

class vector {
private:
int* e; //elementele vectorului
164
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
~vector();
void operator++(); //incrementarea elementelor
//vectorului
void afiseaza(char* text); //afiseaza adresa
//primului element,
//dupa care afiseaza
//elementele
};

vector::vector(int* e1, int d1)


{
d = d1;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = e1[i];
}

vector::~vector()
{
cout
<< "S-a eliberat zona de memorie de dimensiune "
<< d*sizeof(int)
<< " incepind de la adresa: " << e << endl;
delete[] e;
}

void vector::operator++()
{
for(int i = 0; i < d; i++)
e[i]++;
}

void vector::afiseaza(char* text)


{
cout
<< "Vectorul " << text
<< " (adresa primului element si elementele):\n";
cout << e << endl;
for(int i = 0; i < d; i++)
cout << e[i] << ' ';
cout << endl;
}

int main() {
int x[] = {1, 2, 3, 4, 5};
vector v(x, 5);
int y[] = {20, 40, 60, 80, 100};
vector t(y, 5);
v.afiseaza("v");
t.afiseaza("t");
t = v;
cout << "Dupa atribuirea t = v:\n";
t.afiseaza("t");
++v;
cout
<< "Dupa incrementarea elementelor "
<< "vectorului v:" << endl;
v.afiseaza("v");
t.afiseaza("t");
165
return 0;
}

După o execuţie a programului rezultatele vor fi de exemplu:


Vectorul v (adresa primului element si elementele):
0x222a8
1 2 3 4 5
Vectorul t (adresa primului element si elementele):
0x222c8
20 40 60 80 100
Dupa atribuirea t = v:
Vectorul t (adresa primului element si elementele):
0x222a8
1 2 3 4 5
Dupa incrementarea elementelor vectorului v:
Vectorul v (adresa primului element si elementele):
0x222a8
2 3 4 5 6
Vectorul t (adresa primului element si elementele):
0x222a8
2 3 4 5 6
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x222a8
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x222a8

Se observă că nu s-a obţinut ceea ce s-a dorit, deoarece prin instrucţiunea t = v s-a
obţinut atribuirea adresei primului element al vectorului v datei membru e, corespunzătoare
vectorului t şi nu s-au atribuit elementele în sine. De aceea orice modificare a elementelor
vectorului v duce în continuare la modificarea elementelor vectorului t (în cazul nostru prin
incrementarea elementelor vectorului v se incrementează şi elementele vectorului t).
Un alt neajuns este că nu s-a eliberat zona de memorie alocată iniţial elementelor
vectorului t dar s-a eliberat de două ori cea rezervată pentru elementele vectorului v.
Explicaţia rezultatelor de mai sus constă în faptul că operatorul de atribuire (=) este
supraîncărcat în mod implicit astfel încît să realizeze o copiere bit cu bit a datelor membru. În
exemplul referitor la fracţii prin copierea bit cu bit se obţin rezultatele dorite dar în al doilea
exemplu însă, nu. În general prin copierea bit cu bit nu se obţin rezultate corespunzătoare
atunci când cel putin una dintre datele membru este un pointer. În acest caz supraîncărcarea
operatorului de atribuire se poate face în modul următor:
class vector {
private:
int* e; //elementele vectorului
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
vector(const vector& v1);
~vector();
void operator++(); //incrementarea elementelor
//vectorului
vector& operator =(const vector& v1);
//supraincarcarea
//operatorului
//de atribuire
void afiseaza(char* text); //afiseaza adresa

166
//primului element,
//dupa care afiseaza
//elementele
};

vector& vector::operator =(const vector& v1)


{
if (this != &v1)
{
delete[] e;
d = v1.d;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = v1.e[i];
}
return *this;
}

Operatorii de atribuire +=, -=, *=, /= nu sunt supraîncărcaţi în mod implicit, deci
trebuie supraîncărcaţi de către programator. De exemplu operatorul += pentru clasa vector
poate fi supraîncărcat în modul următor:
vector& vector::operator +=(vector& v1)
{
return *this = *this + v1;
}

Atragem atenţia asupra deosebirii dintre apelul operatorului de atribuire şi a


constructorului implicit. De exemplu dacă se declară un obiect ob1 aparţinând clasei
nume_clasa, în felul următor
nume_clasa ob1; //se apeleaza constructorul
//implicit, sau constructorul cu
//toti parametrii impliciti
atunci prin declaraţia
nume_clasa ob2 = ob1; //apelarea constructorului
//de copiere
se va apela constructorul de copiere şi nu operatorul de atribuire. Construcţia de mai sus este
echivalentă cu:
nume_clasa ob2(ob1); //apelarea constructorului
//de copiere
Exisă totuşi asemănarea dintre operatorul de atribuire şi constructorul de copiere că, în
cazul în care nu există constructor de copiere definit de programator, se va apela un
constructor de copiere implicit care va realiza o iniţializare a obiectului printr-o copiere bit cu
bit. Constructorul de copiere se va apela în general şi în următoarele două situaţii:
- dacă un parametru al unei funcţii este un obiect
- dacă o funcţie returnează un obiect
De aceea în cazul în care copierea bit cu bit nu dă rezultate corespunzătoare, este
recomandat ca programatorul să definească un constructor de copiere, chiar şi în cazul în care
nu se doreşte iniţializarea unui obiect printr-un alt obiect. Prezentăm un exemplu pentru
ilustrare. Fişierul vector6.cpp:

#include <iostream.h>
#include <stdlib.h>

class vector {

167
private:
int* e; //elementele vectorului
int d; //dimensiunea vectorului
public:
vector(int* e1, int d1);
vector(vector& v1);
~vector();
vector operator +(vector v1); //supraincarcarea
//adunarii
vector& operator =(const vector& v1);
//supraincarcarea
//operatorului
//de atribuire

void afiseaza(char* text); //afiseaza adresa


//primului element,
//dupa care afiseaza
//elementele
};

vector::vector(int* e1, int d1)


{
d = d1;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = e1[i];
cout
<< "Apel constructor.\n"
<< "Adresa primului element: " << e << endl;
}

vector::vector(vector& v1)
{
d = v1.d;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = v1.e[i];
cout << "Apel constructor de copiere.\n"
<< "Adresa primului element: " << e << endl;
}

vector::~vector()
{
cout
<< "Apel destructor.\n"
<< "S-a eliberat zona de memorie de dimensiune "
<< d*sizeof(int)
<< " incepind de la adresa: " << e << endl;
delete[] e;
}

vector vector::operator +(vector v1)


{
if (d != v1.d)
{
cerr << "Eroare: dimensiune diferita";
exit(1);
}
int* x = new int[d];
for(int i = 0; i < d; i++)
x[i] = e[i] + v1.e[i];
vector temp(x, d);
168
temp.afiseaza("temp");
return temp;
//return vector(x, d);
}

vector& vector::operator =(const vector& v1)


{
if (this != &v1)
{
cout
<< "Apel operator de atribuire ( = ).\n"
<< "S-a eliberat zona de memorie "
<< "de dimensiune " << d*sizeof(int)
<< " incepind de la adresa: "
<< e << endl;
delete[] e;
d = v1.d;
e = new int[d];
for(int i = 0; i < d; i++)
e[i] = v1.e[i];
}
return *this;
}

void vector::afiseaza(char* text)


{
cout
<< "Vectorul " << text
<< " (adresa primului element si elementele):\n";
cout << e << endl;
for(int i = 0; i < d; i++)
cout << e[i] << ' ';
cout << endl;
}

Fişierul vector7.cpp:
#include "vector6.cpp"

int main() {
int x[] = {1, 2, 3, 4, 5};
vector vx(x, 5);
int y[] = {20, 40, 60, 80, 100};
vector vy(y, 5);
int z[] = {300, 600, 900, 1200, 1500};
vector vz(z, 5);
vx.afiseaza("vx");
vy.afiseaza("vy");
vz.afiseaza("vz");
vz = vx + vy;
cout << "Dupa instructiunea vz = vx + vy:\n";
vz.afiseaza("vz");
return 0;
}

Prin execuţie se obţine de exemplu:


Apel constructor.
169
Adresa primului element: 0x22cd8
Apel constructor.
Adresa primului element: 0x22cf8
Apel constructor.
Adresa primului element: 0x22d18
Vectorul vx (adresa primului element si elementele):
0x22cd8
1 2 3 4 5
Vectorul vy (adresa primului element si elementele):
0x22cf8
20 40 60 80 100
Vectorul vz (adresa primului element si elementele):
0x22d18
300 600 900 1200 1500
Apel constructor de copiere.
Adresa primului element: 0x22d38
Apel constructor.
Adresa primului element: 0x22d78
Vectorul temp (adresa primului element si elementele):
0x22d78
21 42 63 84 105
Apel constructor de copiere.
Adresa primului element: 0x22d98
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22d78
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22d38
Apel operator de atribuire ( = ).
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22d18
Dupa instructiunea vz = vx + vy:
Vectorul vz (adresa primului element si elementele):
0x22d18
21 42 63 84 105
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22d98
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22d18
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22cf8
Apel destructor.
S-a eliberat zona de memorie de dimensiune 20 incepind de la adresa:
0x22cd8

Se observă că s-a apelat constructorul de copiere de două ori. Prima dată pentru
crearea parametrului operatorului de adunare prin copierea obiectului vy. Destructorul acestui
obiect s-a apelat automat după ce s-a părăsit funcţia membru corespunzătoare operatorului de
adunare. Constructorul de copiere s-a apelat a doua oară atunci când s-a creat obiectul anonim
vx+vy. Destructorul acestui obiect anonim s-a apelat însă numai la părăsirea funcţiei main. De
aceea dacă elementele iniţiale a vectorului vz nu se folosesc, ar fi mai convenabil dacă
vectorul vz s-ar iniţializa printr-un constructor. De exemplu cu funcţie main din fişierul
vector8.cpp:

#include "vector6.cpp"
170
int main() {
int x[] = {1, 2, 3, 4, 5};
vector vx(x, 5);
int y[] = {20, 40, 60, 80, 100};
vector vy(y, 5);
vx.afiseaza("vx");
vy.afiseaza("vy");
vector vz = vx + vy;
cout << "Dupa instructiunea vz = vx + vy:\n";
vz.afiseaza("vz");
return 0;
}

În acest caz constructorul de copiere se apelează tot de două ori, dar nu se mai crează
obiectul anonim vx+vy.

16.7.3. Supraîncărcarea operatorilor de incrementare şi decrementare

În exemplul din fişierul vector5.cpp s-a supraîncărcat operatorul de incrementare.


În funcţia main s-a executat instrucţiunea
++v;
pentru un obiect v al clasei vector. Dacă în locul acestei instrucţiuni am fi scris v++; atunci
în faza de compilare ar fi apărut un mesaj de avertisment. De exemplu în Borland C++ apare
mesajul: "Overloaded prefix 'operator ++' used as a postfix operator", deci operatorul ++
prefixat s-a folosit ca şi operator postfixat. Acelaşi mesaj de avertisment apare şi în următorul
exemplu. Fişierul increm1.cpp:
#include <iostream.h>
#include <conio.h>

class Clasa {
int x;
public:
Clasa() { x = 0; }
Clasa& operator++();
void scrie_x();
};

Clasa& Clasa::operator++()
{
++x;
cout << "S-a apelat operatorul ++ prefixat\n";
return *this;
}

void Clasa::scrie_x()
{
cout << "x = " << x << endl;
}

int main() {
Clasa cl;
clrscr();
cl.scrie_x();
cl++;
cl.scrie_x();
(++cl)++;
171
cl.scrie_x();
return 0;
}

În faza de compilare, mesajul de avertisment de mai sus apare de două ori, pentru cele
două apeluri ale operatorului de incrementare postfixat. Prin executarea programului se
obţine:
x = 0
S-a apelat operatorul ++ prefixat
x = 1
S-a apelat operatorul ++ prefixat
S-a apelat operatorul ++ prefixat
x = 3

Din rezultatul de mai sus nu reiese că s-a apelat operatorul ++ postfixat de două ori.
Operatorul de incrementare postfixat poate fi supraîncărcat cu o funcţie membru, care are un
parametru de tip int. La apelarea unui operator postfixat, acest parametru va lua în mod
automat valoarea zero. Deoarece valoarea parametrului nu se va folosi în corpul funcţiei
membru, numele lui poate fi omis. Exemplul de mai sus se poate modifica în felul următor.
Fişierul increm2.cpp:
#include <iostream.h>
#include <conio.h>

class Clasa {
int x;
public:
Clasa() { x = 0; }
Clasa& operator++();
Clasa& operator++( int );
void scrie_x();
};

Clasa& Clasa::operator++()
{
++x;
cout << "S-a apelat operatorul ++ prefixat\n";
return *this;
}

Clasa& Clasa::operator++( int )


{
x++;
cout << "S-a apelat operatorul ++ postfixat\n";
return *this;
}

void Clasa::scrie_x()
{
cout << "x = " << x << endl;
}

int main() {
Clasa cl;
clrscr();
cl.scrie_x();
cl++;
172
cl.scrie_x();
(++cl)++;
cl.scrie_x();
return 0;
}

În acest caz nu apar mesaje de avertisment la compilare. Rezultatul obţinut prin


execuţie este:
x = 0
S-a apelat operatorul ++ postfixat
x = 1
S-a apelat operatorul ++ prefixat
S-a apelat operatorul ++ postfixat
x = 3

Într-adevăr s-a afişat de fiecare dată mesajul corespunzător apelului operatorului


prefixat respectiv postfixat. Menţionăm că operatorului de decrementare prefixat, respectiv
postfixat se poate supraîncărca în mod analog.

16.8. Conversii definite de programator


16.8.1. Efectuarea conversiilor

În mod automat în limbajele C şi C++ se aplică o conversie în următoarele trei situaţii:

- dacă un operator se referă la operanzi de tipuri diferite;


- dacă tipul parametrului actual al unei funcţii diferă de tipul parametrului formal
corespunzător;
- dacă tipul valorii returnate de o funcţie, tip specificat în antetul funcţiei, diferă de
tipul expresiei din instrucţiunea return.

În primul caz, dacă operatorul nu este de atribuire, se va folosi regula conversiilor


implicite. Deci mai întâi tipurile char şi enum se convertesc în int, după care tipurile
"inferioare" se convertesc spre cele "superioare" (de exemplu int în double). Dacă operatorul
este de atribuire, atunci tipul expresiei din partea dreaptă se converteşte spre tipul expresiei
din partea stângă. În cazul al doilea tipul parametrului actual se converteşte spre tipul
parametrului formal. În cazul al treilea tipul expresiei din instrucţiunea return se converteşte
spre tipul specificat în antetul funcţiei.

16.8.2. Conversii implicite definite de programator

Considerăm următorul exemplu referitor la clasa numerelor raţionale. Clasa va avea


două date membru de tip long pentru memorarea numărătorului şi numitorului. Obiectele
clasei se vor iniţializa printr-un constructor. Se va supraîncărca operatorul ~, care va efectua
simplificarea fracţiei, şi operatorul *, pentru înmulţirea a două fracţii. Deasmenea, se va
defini o funcţie membru afiseaza, pentru afişarea unei fracţii. Fişierul conv2.cpp:

173
#include <iostream.h>
#include <stdio.h>
#include <conio.h>

long cmmdc( long a, long b);

class fractie {
long numarator;
long numitor;
public:
fractie(long a, long b);
fractie& operator~ (); // simplificare fractie
fractie operator *(fractie r);
void afiseaza();
};

inline fractie::fractie(long a, long b)


{
numarator = a;
numitor = b;
}

fractie& fractie::operator~()
{
long d = cmmdc(numarator, numitor);
numarator /= d;
numitor /= d;
return *this;
}

inline fractie fractie::operator *(fractie r)


{
return ~fractie(numarator*r.numarator,
numitor*r.numitor);
}

inline void fractie::afiseaza()


{
if ( numitor )
cout << numarator << " / " << numitor << endl;
else
cerr << "Fractie incorecta";
}

long cmmdc( long a, long b)


{
long r;
while ( b )
{
r = a % b;
a = b;
b = r;
}
return a;
}

int main() {
clrscr();
fractie x(2,5);
fractie y(15, 8);
fractie z(0, 1);
fractie w(4, 1);
174
z = x * y;
z.afiseaza();
z = x * w; // z = x * 4; ar fi gresit
z.afiseaza();
return 0;
}

Prin executarea programului se obţine:


3 / 4
8 / 5

Observăm că operatorul de înmulţire se poate folosi numai pentru înmulţirea a două


numere raţionale. Deci în cazul unei expresii de forma următoare
z = x * 4;

compilatorul ar semnala o eroare. Totuşi ar fi de dorit ca expresiile de forma de mai sus, să


fie corecte. Pentru a realiza acest lucru, trebuie definită o conversie implicită din tipul long în
tipul fractie.
În general, conversiile dintr-un tip standard într-un tip abstract se realizează cu
ajutorul unui constructor al tipului abstract. Constructorul trebuie să aibă un parametru, care
aparţine tipului standard, iar dacă sunt şi alţi parametri, ei trebuie să fie iniţializaţi.
În cazul de mai sus constructorul, care realizează conversia din tipul long în tipul
fractie, poate fi declarat sub forma:
fractie(long a, long b = 1);
sau
fractie(long a = 0, long b = 1);
sau
fractie(long a);

De exemplu, dacă fişierul conv2.cpp se modifică astfel încât constructorul să fie


declarat în forma:
fractie(long a = 0, long b = 1);
atunci expresia
z = x * 4;
este corectă şi se obţine numărul raţional 8 / 5. Al doilea operand al operatorului de înmulţire
trebuie să aparţină tipului fracţie. Cu ajutorul constructorului se va crea un obiect anonim
fractie(4, 1) , care se va folosi în locul operandului al doilea. Menţionăm, că de fapt s-au
făcut două conversii. Constanta 4 este de tipul int, deci la apelarea constructorului s-a făcut
o conversie de la tipul int la tipul long, după care s-a făcut o conversie din tipul long în tipul
fractie, prin crearea obiectului anonim.
Totuşi compilatorul semnalizează o eroare la întâlnirea unei expresii de forma
următoare:
z = 4 * x;
Explicaţia este, că în cazul expresiei x * 4, se apelează funcţia membru operator*
pentru obiectul x. Însă în cazul expresiei 4 * x, se încearcă apelarea unei funcţii membru
(operatorul de înmulţire) pentru constanta 4, ceea ce conduce la eroare.
O soluţie ar putea fi ca operatorul de înmulţire să se defineasă printr-o funcţie prieten,
de exemplu în modul următor:
class fractie {
175
long numarator;
long numitor;
public:
...
friend fractie operator*(fractie p, fractie q);
...
};

fractie operator*(fractie p, fractie q)


{
return ~fractie(p.numarator*q.numarator,
p.numitor*q.numitor);
}

Dacă operatorul de înmulţire se defineşte în modul de mai sus, atunci ambele expresii
x * 4 şi 4 * x vor fi corecte. Dezavantajul acestei metode este, că prin utilizarea unei
funcţii prieten, gradul de protecţie a datelor scade. În continuare prezentăm o altă metodă de
înlăturare a erorii de mai sus, astfel încât să nu fie nevoie de introducerea unei funcţii prieten.
Vom defini o funcţie membru inmultire, care se va apela de către funcţia, care supraîncarcă
operatorul de înmulţire.
class fractie {
long numarator;
long numitor;
public:
...
fractie inmultire(fractie r);
...
};

inline fractie fractie::inmultire(fractie r)


{
return ~fractie(numarator*r.numarator,
numitor*r.numitor);
}

fractie operator*(fractie p, fractie q)


{
return p.inmultire( q );
}

Observăm că şi în acest caz, ambele expresii sunt corecte şi nu s-a folosit funcţie
prieten.
Definirea adecvată a constructorului poate să conducă şi la conversii dintr-un tip
abstract într-un alt tip abstract. Prezentăm un exemplu pentru măsurarea unei lungimi, în care
sunt definite două clase, folosind două unităţi de măsură diferite. Cu ajutorul clasei
Lungime_m se va memora lungimea în metri (mm, cm, dm şi m), iar folosind clasa
Lungime_inch memorarea lungimii se va realiza în inch (line, inch, foot şi yard). Relaţiile
dintre cele două unităţi de măsură sunt următoarele:

1 line = 2.54 mm
1 inch = 10 lines = 2.54 cm
1 foot = 12 inches = 30.48 cm
1 yard = 3 feet = 91.44 cm

Deasemenea, programul va realiza conversia din inch în metri. Fişierul lung1.cpp:

176
#include <iostream.h>
#include <conio.h>

class Lungime_inch {
int line;
int inch;
int foot;
int yard;

public:
Lungime_inch(int l = 0, int i = 0, int f = 0, int y = 0);
long line_lungime();
void afisare();
};

class Lungime_m {
int mm;
int cm;
int dm;
int m;
double rest; // restul, care ramane la conversie
public:
Lungime_m( int mm_1 = 0, int cm_1 = 0,
int dm_1 = 0, int m_1 = 0);
Lungime_m(Lungime_inch L); // conversie
void afisare();
};

Lungime_inch::Lungime_inch( int l, int i, int f, int y)


{
line = l;
inch = i;
foot = f;
yard = y;
}

long Lungime_inch::line_lungime()
{
return line + 10 * inch + 120 * foot + 360 * yard;
}

void Lungime_inch::afisare()
{
if ( line )
cout << line << " line ";
if ( inch )
cout << inch << " inch ";
if ( foot )
cout << foot << " foot ";
if ( yard )
cout << yard << " yard";
cout << endl;
}

Lungime_m::Lungime_m( int mm_1, int cm_1, int dm_1, int m_1)


{
mm = mm_1;
cm = cm_1;
dm = dm_1;
m = m_1;
rest = 0;
}
177
Lungime_m::Lungime_m(Lungime_inch L)
{
rest = 2.54 * L.line_lungime();
long mm_lung = rest;
rest -= mm_lung;
m = mm_lung / 1000;
mm_lung %= 1000;
dm = mm_lung / 100;
mm_lung %= 100;
cm = mm_lung / 10;
mm_lung %= 10;
mm = mm_lung;
}

void Lungime_m::afisare()
{
if ( m )
cout << m << " m ";
if ( dm )
cout << dm << " dm ";
if ( cm )
cout << cm << " cm ";
if ( mm + rest )
cout << mm + rest << " mm ";
cout << endl;
}

int main() {
clrscr();
Lungime_inch y(5, 2, 1, 3);
y.afisare();
cout << y.line_lungime() << " line" << endl;
cout << 2.54 * y.line_lungime() << " mm" << endl;
Lungime_m x;
x = y;
x.afisare();
return 0;
}

Prin executarea programului se obţine:


5 line 2 inch 1 foot 3 yard
1225 line
3111.5 mm
3 m 1 dm 1 cm 1.5 mm

Observăm, că şi în acest caz s-a folosit un constructor pentru realizarea conversiei din
tipul Lungime_inch în tipul Lungime_m. Clasa Lungime_inch s-a declarat înainte de clasa
Lungime_m, deoarece constructorul, care realizează conversia, foloseşte tipul Lungime_inch.

16.8.3. Supraîncărcarea operatorului de conversie explicită

Conversia dintr-un tip abstract într-un tip standard se poate realiza prin
supraîncărcarea operatorului de conversie explicită. Pentru a realiza conversia din tipul
abstract Clasa în tipul standard tip_standard este necesară o funcţie membru de forma
următoare:
178
class Clasa {
...
public:
...
operator tip_standard(); // declaraţie
...
};
Clasa::operator tip_standard()
{
...
return expresie; // se returnează o expresie având tipul
// tip_standard
}

Obiectul de tip Clasa se va converti în valoarea expresiei returnate de către funcţia


membru, care supraîncarcă operatorul de conversie explicită. Menţionăm că în declaraţia
funcţiei membru, care supraîncarcă operatorul de conversie explicită, nu trebuie specificat
tipul, care se returnează, deoarece acest tip va fi întotdeauna tipul standard la care se face
conversia. Dacă se încearcă specificarea tipului returnat, atunci compilatorul va semnala
eroare. În următorul exemplu se va efectua conversia unui număr raţional într-un număr de
tip double. Fişierul conv3.cpp:
#include <iostream.h>
#include <stdio.h>
#include <conio.h>

class fractie {
long numarator;
long numitor;
public:
fractie(long a, long b);
operator double();
fractie operator* (fractie r); //calculeaza produsul
//a doua fractii,
//nu simplifica
void afiseaza();
};

inline fractie::fractie(long a, long b)


{
numarator = a;
numitor = b;
}

fractie::operator double()
{
return double(numarator) / numitor;
}

inline fractie fractie::operator* (fractie r)


{
return fractie(numarator*r.numarator,
numitor*r.numitor);
}

inline void fractie::afiseaza()


{
if ( numitor )
cout << numarator << " / " << numitor << endl;

179
else
cerr << "Fractie incorecta";
}

int main() {
clrscr();
fractie x(2,5);
double y;
y = x * 4.5;
cout << y << endl;
y = 4.5 * x;
cout << y << endl;
return 0;
}

Prin executarea programului obţinem:


1.8
1.8

Menţionăm că dacă parametrul formal b al constructorului clasei fractie ar fi fost


iniţializat, atunci ar fi apărut o eroare la compilare. Într-adevăr, în acest caz expresia
x * 4.5 se poate evalua în următoarele două moduri:
- conversia variabilei x în tipul double şi efectuarea înmulţirii a două date de tip
double;
- conversia constantei de tip double 4.5 în tipul long (cu trunchiere), urmată de
conversia în tipul abstract fractie prin apelarea constructorului, şi efectuarea
înmulţirii a două numere raţionale.
Deoarece evaluarea expresiei x * 4.5 se poate face în două moduri diferite,
compilatorul nu poate evalua expresia, şi se va semnala o eroare.
Supraîncărcarea operatorului de conversie explicită se poate folosi şi pentru conversii
dintr-un tip abstract într-un alt tip abstract. În continuare se va relua exemplul referitor la
măsurarea unei lungimi în metri, respectiv în inch. Se va supraîncărca operatorul de conversie
explicită din inch în metri. Fişierul lung2.cpp:
#include <iostream.h>
#include <conio.h>

class Lungime_m {
int mm;
int cm;
int dm;
int m;
double rest; // restul, care ramane la conversie
public:
Lungime_m( int mm_1 = 0, int cm_1 = 0,
int dm_1 = 0, int m_1 = 0, double rest_1 = 0);
void afisare();
};

class Lungime_inch {
int line;
int inch;
int foot;
int yard;
public:

180
Lungime_inch(int l = 0, int i = 0, int f = 0, int y = 0);
operator Lungime_m();
long line_lungime();
void afisare();
};

Lungime_m::Lungime_m( int mm_1, int cm_1,


int dm_1, int m_1, double rest_1)
{
mm = mm_1;
cm = cm_1;
dm = dm_1;
m = m_1;
rest = rest_1;
}

void Lungime_m::afisare()
{
if ( m )
cout << m << " m ";
if ( dm )
cout << dm << " dm ";
if ( cm )
cout << cm << " cm ";
if ( mm + rest )
cout << mm + rest << " mm ";
cout << endl;
}

Lungime_inch::Lungime_inch( int l, int i, int f, int y)


{
line = l;
inch = i;
foot = f;
yard = y;
}

Lungime_inch::operator Lungime_m()
{
double rest_1 = 2.54 * line_lungime();
long mm_1 = rest_1;
rest_1 -= mm_1;
long m_1 = mm_1 / 1000;
mm_1 %= 1000;
long dm_1 = mm_1 / 100;
mm_1 %= 100;
long cm_1 = mm_1 / 10;
mm_1 %= 10;
return Lungime_m(mm_1, cm_1, dm_1, m_1, rest_1);
}

long Lungime_inch::line_lungime()
{
return line + 10 * inch + 120 * foot + 360 * yard;
}

void Lungime_inch::afisare()
{
if ( line )
cout << line << " line ";
if ( inch )
cout << inch << " inch ";
181
if ( foot )
cout << foot << " foot ";
if ( yard )
cout << yard << " yard";
cout << endl;
}

int main() {
clrscr();
Lungime_inch y(5, 2, 1, 3);
y.afisare();
cout << y.line_lungime() << " line" << endl;
cout << 2.54 * y.line_lungime() << " mm" << endl;
Lungime_m x;
x = y;
x.afisare();
return 0;
}

Observăm, că rezultatul obţinut după execuţie este identic cu cel al fişierului


lung1.cpp. Menţionăm că în acest caz clasa Lungime_m trebuie declarată înainte declasa
Lungime_inch, deoarece tipul Lungime_m se foloseşte în cadrul clasei Lungime_inch.
În principiu conversia dintr-un tip abstract într-un alt tip abstract se poate realiza în
două moduri: cu ajutorul unui constructor şi prin supraîncărcarea operatorul de conversie
explicită. Însă în cazul unei aplicaţii concrete, numai unul din aceste două moduri poate fi
folosit. În caz contrar compilatorul va semnala o eroare.

182
17. Metoda programării orientate obiect
17.1. Bazele teoretice ale metodei programării orientate obiect
Prin folosirea tipurilor abstracte de date, se crează un tot unitar pentru gestionarea
datelor şi a operaţiilor referitoare la aceste date. Cu ajutorul tipului abstract clasă se realizează
şi protecţia datelor, deci elementele protejate nu pot fi accesate numai de funcţiile membru
ale clasei respective. Această proprietate a obiectelor se numeşte încapsulare (encapsulation).
În viaţa de zi cu zi însă ne întîlnim nu numai cu obiecte separate, dar şi cu diferite
legături între aceste obiecte, respectiv între clasele din care obiectele fac parte. Astfel se
formează o ierarhie de clase. Rezultă a doua proprietate a obiectelor: moştenirea
(inheritance). Acest lucru înseamnă că se moştenesc toate datele şi funcţiile membru ale
clasei de bază de către clasa derivată, dar se pot adăuga elemente noi (date membru şi funcţii
membru) în clasa derivată. În cazul în care o clasă derivată are mai multe clase de bază se
vorbeşte despre moştenire multiplă.
O altă proprietate importantă a obiectelor care aparţin clasei derivate este că funcţiile
membru moştenite pot fi supraîncărcate. Acest lucru înseamnă că o operaţie referitoare la
obiectele care aparţin ierarhiei are un singur identificator, dar funcţiile care descriu această
operaţie pot fi diferite. Deci numele funcţiei şi lista parametrilor formali este aceeaşi în clasa
de bază şi în clasa derivată, dar descrierea funcţiilor diferă între ele. Astfel în clasa derivată
funcţiile membru pot fi specifice referitoare la clasa respectivă, deşi operaţia se identifică prin
acelaşi nume. Această proprietate se numeşte polimorfism.
Noţiunea de polimorfism ne conduce în mod firesc la problematica determinării
funcţiei membru care se va apela în cazul unui obiect concret. Să considerăm următorul
exemplu. Declarăm clasa de bază baza, şi o clasă derivată din acestă clasă de bază, clasa
derivata. Clasa de bază are două funcţii membru: functia_1 şi functia_2. În interiorul funcţiei
membru functia_1 se apelează functia_2. În clasa derivată se supraîncarcă funcţia membru
functia_1, dar funcţia membru functia_2 nu se supraîncarcă. În programul principal se declară
un obiect al clasei derivate şi se apelează funcţia membru functia_2 moştenită de la clasa de
bază. În limbajul C++ acest exemplu se scrie în următoarea formă. Fişierul virtual1.cpp:
#include <iostream.h>
#include <conio.h>
class baza {
public:
void functia_1();
void functia_2();
};

class derivata : public baza {


public:
void functia_1();
};

void baza::functia_1()
{
cout << "S-a apelat functia membru functia_1"
<< " a clasei de baza" << endl;
}
void baza::functia_2()
{
cout << "S-a apelat functia membru functia_2"
<< " a clasei de baza" << endl;
functia_1();

183
}

void derivata::functia_1()
{
cout << "S-a apelat functia membru functia_1"
<< " a clasei derivate" << endl;
}

int main() {
clrscr();
derivata D;
D.functia_2();
return 0;
}

Prin execuţie se obţine următorul rezultat:


S-a apelat functia membru functia_2 a clasei de baza
S-a apelat functia membru functia_1 a clasei de baza

Însă acest lucru nu este rezultatul dorit, deoarece în cadrul funcţiei main s-a apelat
funcţia membru functia_2 moştenită de la clasa de bază, dar funcţia membru functia_1
apelată de functia_2 s-a determinat încă în faza de compilare. În consecinţă, deşi funcţia
membru functia_1 s-a supraîncărcat în clasa derivată nu s-a apelat funcţia supraîncărcată ci
funcţia membru a clasei de bază. De fapt şi din rezultatul execuţiei programului se obţine
acelaşi lucru.
Acest neajuns se poate înlătura cu ajutorul introducerii noţiunii de funcţie membru
virtuală. Dacă funcţia membru este virtuală, atunci la orice apelare a ei, determinarea funcţiei
membru corespunzătoare a ierarhiei de clase nu se va face la compilare ci la execuţie, în
funcţie de natura obiectului pentru care s-a făcut apelarea. Această proprietate se numeşte
legare dinamică, iar dacă determinarea funcţiei membru se face la compilare, atunci se
vorbeşte de legare statică.

17.2. Declararea claselor derivate


O clasă derivată se declară în felul următor:
class nume_clasă_derivată : lista_claselor_de_bază {
//date membru noi şi funcţii membru noi
};

unde lista_claselor_de_bază este de forma:


elem_1, elem_2, ..., elem_n
şi elem_i pentru orice 1 ≤ i ≤ n poate fi
public clasă_de_bază_i
sau
protected clasă_de_bază_i
sau
private clasă_de_bază_i

Cuvintele cheie public, protected şi private se numesc şi de această dată modificatori


de protecţie. Ele pot să lipsească, în acest caz modificatorul implicit fiind private. Accesul la
elementele din clasa derivată este prezentată în tabelul 2.

184
Observăm că elementele de tip private ale clasei de bază sunt inaccesibile în clasa
derivată. Elementele de tip protected şi public devin de tip protected, respectiv private dacă
modificatorul de protecţie referitor la clasa de bază este protected respectiv private, şi rămân
neschimbate dacă modificatorul de protecţie referitor la clasa de bază este public. Din acest
motiv în general datele membru se declară de tip protected şi modificatorul de protecţie
referitor la clasa de bază este public. Astfel datele membru pot fi accesate, dar rămân
protejate şi în clasa derivată.

Accesul la elementele Modificatorii de Accesul la elementele


din clasa de bază protecţie referitoare la din clasa derivată
clasa de bază
public public public
protected public protected
private public inaccesibil
public protected protected
protected protected protected
private protected inaccesibil
public private private
protected private private
private private inaccesibil

Tabelul 2: accesul la elementele din clasa derivată

17.3. Funcţii membru virtuale


Revenim la exemplul din secţiunea 17.1. Am văzut că dacă se execută programul
virtual1.cpp se apelează funcţiile membru functia_1 şi functia_2 ale clasei de bază. Însă
funcţia membru functia_1 fiind supraîncărcată în clasa derivată, ar fi de dorit ca funcţia
supraîncărcată să fie apelată în loc de cea a clasei de bază.
Acest lucru se poate realiza declarând functia_1 ca funcţie membru virtuală. Astfel
pentru orice apelare a funcţiei membru functia_1, determinarea acelui exemplar al funcţiei
membru din ierarhia de clase care se va executa, se va face la execuţie şi nu la compilare.
Această proprietate se numeşte legare dinamică.
În limbajul C++ o funcţie membru se declară virtuală în cadrul declarării clasei
respective în modul următor. Antetul funcţiei membru se va începe cu cuvântul cheie virtual.
O funcţie membru se declară virtuală în clasa de bază. Supraîncărcările ei se vor
considera virtuale în toate clasele derivate ale ierarhiei.
În cazul exemplului de mai sus declararea clasei de bază se modifică în felul următor.
class baza {
public:
virtual void functia_1();
void functia_2();
};

Rezultatul obţinut prin execuţie se modifică astfel:


S-a apelat functia membru functia_2 a clasei de baza
S-a apelat functia membru functia_1 a clasei derivate

185
Deci într-adevăr se apelează funcţia membru functia_1 a clasei derivate. Prezentăm în
continuare un alt exemplu în care apare necesitatea introducerii funcţiilor membru virtuale. În
acest caz funcţia membru virtuală va fi de fapt supraîncărcarea unui operator.
Să se definească clasa fractie referitoare la numerele raţionale, având ca date membru
numărătorul şi numitorul fracţiei. Clasa trebuie să aibă un constructor, valoarea implicită
pentru numărător fiind zero iar pentru numitor unu, precum şi doi operatori: * pentru
înmulţirea a două fracţii şi *= pentru înmulţirea obiectului curent cu fracţia dată ca şi
parametru. Deasemenea clasa fractie trebuie să aibă şi o funcţie membru pentru afişarea unui
număr raţional. Folosind clasa fractie ca şi clasă de bază se va defini clasa derivată
fractie_scrie, pentru care se va supraîncărca operatorul de înmulţire * astfel încât concomitent
cu efectuarea înmulţirii să se afişeze pe stdout operaţia respectivă. Operaţia *= nu se va
supraîncărca, dar operaţia efectuată trebuie să se afişeze pe dispozitivul standard de ieşire şi
în acest caz. În limbajul C++ clasele se definesc în modul următor. Fişierul fractie2.cpp:
#include <conio.h>
#include <iostream.h>

class fractie {
protected:
int numarator;
int numitor;
public:
fractie(int numarator1, int numitor1);
fractie operator *( fractie& r); //calculeaza
//produsul
//a doua fractii
//nu simplifica
fractie& operator *=( fractie& r);
void afiseaza();
};

fractie::fractie(int numarator1 = 1,
int numitor1 = 0)
{
numarator = numarator1;
numitor = numitor1;
}

fractie fractie::operator *(fractie& r)


{
return fractie(numarator*r.numarator,
numitor*r.numitor);
}

fractie& fractie::operator *=( fractie& q )


{
*this = *this * q;
return *this;
}

void fractie::afiseaza()
{
if ( numitor )
cout << numarator << " / " << numitor;
else
cerr << "Fractie incorecta";
}

186
class fractie_scrie: public fractie{
public:
fractie_scrie( int numarator1 = 0, int numitor1 = 1 ):
fractie(numarator1, numitor1) {}
fractie operator *( fractie& r);

};

fractie fractie_scrie::operator * (fractie& q)


{
fractie r = fractie(*this) * q;
cout << "(";
this->afiseaza();
cout << ") * (";
q.afiseaza();
cout << ") = ";
r.afiseaza();
cout << endl;
return r;
}

int main()
{
clrscr();
fractie p(3,4), q(5,2), r;
r = p *= q;
p.afiseaza();
cout << endl;
r.afiseaza();
cout << endl;
fractie_scrie p1(3,4), q1(5,2);
fractie r1, r2;
r1 = p1 * q1;
r2 = p1 *= q1;
p1.afiseaza();
cout << endl;
r1.afiseaza();
cout << endl;
r2.afiseaza();
cout << endl;
return 0;
}

Prin execuţie se obţine:


15 / 8
15 / 8
(3 / 4) * (5 / 2) = 15 / 8
15 / 8
15 / 8
15 / 8

Observăm că rezultatul nu este cel dorit, deoarece operaţia de înmulţire s-a afişat
numai o singură dată, şi anume pentru expresia r1 = p1 * q1. În cazul expresiei r2 = p1 *= q1
însă nu s-a afişat operaţia de înmulţire. Acest lucru se datorează faptului că funcţia membru
operator *= nu s-a supraîncărcat în clasa derivată. Deci s-a apelat operatorul *= moştenit de
la clasa fractie. În interiorul operatorului *= s-a apelat funcţia membru operator *, dar
deoarece această funcţie membru s-a determinat încă în faza de compilare, rezultă că s-a
187
apelat operatorul de înmulţire referitor la clasa fractie şi nu cel referitor la clasa derivată
fractie_scrie. Deci afişarea operaţiei s-a efectuat numai o singură dată.
Soluţia este, ca şi în exemplul anterior, declararea unei funcţii membru virtuale, şi
anume operatorul * se va declara virtuală. Deci declararea clasei de bază se modifică în felul
următor:
class fractie {
protected:
int numarator;
int numitor;
public:
fractie(int numarator1, int numitor1);
virtual fractie operator *( fractie& r);
fractie& operator *=( fractie& r);
void afiseaza();
};

După efectuarea acestei modificări prin executarea programului obţinem:


15 / 8
15 / 8
(3 / 4) * (5 / 2) = 15 / 8
(3 / 4) * (5 / 2) = 15 / 8
15 / 8
15 / 8
15 / 8

Deci se observă că afişarea operaţiei s-a făcut de două ori, pentru ambele expresii.
Funcţiile virtuale, ca şi alte funcţii membru de fapt, nu trebuie neapărat supraîncărcate în
clasele derivate. Dacă nu sunt supraîncărcate atunci se moşteneşte funcţia membru de la un
nivel superior.
Determinarea funcţiilor membru virtuale corespunzătoare se face pe baza unor tabele
construite şi gestionate în mod automat. Obiectele claselor care au funcţii membru virtuale
conţin şi un pointer către tabela construită. De aceea gestionarea funcţiilor membru virtuale
necesită mai multă memorie şi un timp de execuţie mai îndelungat.

17.4. Clase virtuale


Din cauza moştenirii multiple se poate întâmpla ca o clasă de bază să fie prezentă în
mai multe exemplare într-o clasă derivată. Să considerăm următorul exemplu:

Figura 1. Date membru moştenite în două exemplare

În acest caz datele membru ale clasei animal vor fi moştenite în două exemplare de
către clasa câine. Primul exemplar se moşteneşte prin clasa domestic iar cel de al doilea prin
clasa mamifer. Aceste date membru pot fi accesate folosind operatorul de rezoluţie precedată
de numele clasei prin care se face moştenirea. Fişierul animal1.cpp:

188
#include <iostream.h>
#include <string.h>
#include <conio.h>

class animal {
protected:
char nume[20];
public:
animal(char* n);
};

class mamifer : public animal {


protected:
int greutate;
public:
mamifer(char* n, int g);
};

class domestic : public animal {


protected:
int comportament;
public:
domestic(char* n, int c);
};

class caine : public mamifer, public domestic {


protected:
int latra;
public:
caine(char* n, int g, int c, int l);
void scrie_date();
};

animal::animal(char* n)
{
strcpy(nume, n);
}

mamifer::mamifer(char* n, int g): animal(n)


{
greutate = g;
}

domestic::domestic(char* n, int c): animal(n)


{
comportament = c;
}

caine::caine(char* n, int g, int c, int l): mamifer(n, g),


domestic(n, c)
{
latra = l;
}

void caine::scrie_date()
{
cout << "Numele (mostenit prin clasa mamifer): "
<< mamifer::nume << endl;
cout << "Numele (mostenit prin clasa domestic): "
<< domestic::nume << endl;
189
cout << "Greutatea: " << greutate << endl;
cout << "Comportmentul: " << comportament << endl;
if ( latra )
cout << "Latra: da" << endl;
else
cout << "Latra: nu" << endl;
}
int main() {
clrscr();
caine B("boxer", 12, 9, 1);
B.scrie_date();
return 0;
}

Observăm că data membru nume s-a moştenit în două exemplare de către clasa câine,
şi referirea la aceste date s-a făcut prin mamifer::nume respectiv domestic::nume. Prin
execuţie se va afişa data membru nume de două ori, moştenit prin cele două clase.
Deasemenea se va afişa greutatea şi comportamentul câinelui, şi faptul că latră sau nu.
Ar fi de dorit însă ca numele să fie memorat numai într-un singur exemplar în
obiectele clasei câine. Acest lucru se poate realiza cu ajutorul claselor virtuale.
Dacă nu se doreşte ca datele membru a unei clase de bază să fie prezente în mai multe
exemplare într-o clasă derivată, atunci se folosesc clase virtuale. Clasele de bază devin
virtuale prin moştenire, dacă se specifică acest lucru prin plasarea cuvântului cheie virtual în
faţa numelui clasei, în lista claselor de bază. Astfel clasa de bază respectivă va deveni virtuală
referitor la clasa derivată.
Exemplul de mai sus se modifică în modul următor. Fişierul animal2.cpp:

#include <iostream.h>
#include <string.h>
#include <conio.h>

class animal {
protected:
char nume[20];
public:
animal(char* n);
};

class mamifer : virtual public animal { //clasa animal va fi


protected: //virtuala referitor
int greutate; //la clasa mamifer
public:
mamifer(char* n, int g);
};

class domestic : virtual public animal { //clasa animal va fi


protected: //virtuala referitor
int comportament; //la clasa domestic
public:
domestic(char* n, int c);
};

class caine : public mamifer, public domestic {


protected:
int latra;
public:
caine(char* n, int g, int c, int l);
void scrie_date();
190
};

animal::animal(char* n)
{
strcpy(nume, n);
}

mamifer::mamifer(char* n, int g): animal(n)


{
greutate = g;
}

domestic::domestic(char* n, int c): animal(n)


{
comportament = c;
}

caine::caine(char* n, int g, int c, int l):


animal(n), //trebuie apelat constructorul clasei animal
mamifer(n, g),//in mod explicit, deoarece ea fiind virtuala
domestic(n, c)//constructorul nu se va apela prin unul din
{ latra = l; } //clasele mamifer sau domestic

void caine::scrie_date()
{
cout << "Numele: " << nume << endl;
cout << "Greutatea: " << greutate << endl;
cout << "Comportmentul: " << comportament << endl;
if ( latra )
cout << "Latra: da" << endl;
else
cout << "Latra: nu" << endl;
}
int main() {
clrscr();
caine B("boxer", 12, 9, 1);
B.scrie_date();
return 0;
}

Într-o ierarhie complicată de clase unele clase de bază pot fi moştenite în mai multe
exemplare într-o clasă derivată. Între aceste exemplare pot fi virtuale şi nevirtuale. Să
considerăm următorul exemplu referitor la acest lucru :

Figura 2. Ierarhie de clase

În această ierarhie de obiecte clasa de bază A se moşteneşte în mod virtual de către


clasele B şi C şi în mod nevirtual de către clasa D. Clasele de bază ale clasei G sunt B, C, D,
E şi F, toate moştenirile fiind nevirtuale cu excepţia clasei F. Se pot pune atunci următoarele
probleme. În ce ordine vor fi executate constructorii în cazul în care se crează un obiect al
191
clasei G? Dacă o clasă de bază este moştenită în mai multe exemplare într-o clasă derivată
printre care pot fi atât virtuale cât şi nevirtuale, atunci de câte ori se va apela constructorul
clasei de bază? Pentru a da răspuns la aceste întrebări să considerăm următorul program.
Fişierul virtual2.cpp:
#include <iostream.h>
#include <string.h>
#include <conio.h>

class A {
public:
A(char* a) {
cout << "Se apeleaza constructorul clasei A"
<< " cu parametrul " << a << endl;
}
};

class B: virtual public A {


public:
B(char* b): A(b) {
cout << "Se apeleaza constructorul clasei B"
<< " cu parametrul " << b << endl;
}
};

class C: virtual public A {


public:
C(char* c): A(c) {
cout << "Se apeleaza constructorul clasei C"
<< " cu parametrul " << c << endl;
}
};

class D: public A {
public:
D(char* d): A(d) {
cout << "Se apeleaza constructorul clasei D"
<< " cu parametrul " << d << endl;
}
};

class E {
public:
E(char* e) {
cout << "Se apeleaza constructorul clasei E"
<< " cu parametrul " << e << endl;
}
};

class F {
public:
F(char* f) {
cout << "Se apeleaza constructorul clasei F"
<< " cu parametrul " << f << endl;
}
};

class G: public B, public C, public D, public E, virtual public F {


public:
G(char* g): A(g), B(g), C(g), D(g), E(g), F(g) {
192
cout << "Se apeleaza constructorul clasei G"
<< " cu parametrul " << g << endl;
}
};

int main() {
clrscr();
G ob("obiect");
return 0;
}

Prin execuţie se obţine următorul rezultat:


Se apeleaza constructorul clasei A cu parametrul obiect
Se apeleaza constructorul clasei F cu parametrul obiect
Se apeleaza constructorul clasei B cu parametrul obiect
Se apeleaza constructorul clasei C cu parametrul obiect
Se apeleaza constructorul clasei A cu parametrul obiect
Se apeleaza constructorul clasei D cu parametrul obiect
Se apeleaza constructorul clasei E cu parametrul obiect
Se apeleaza constructorul clasei G cu parametrul obiect

Acest rezultat se datorează următoarelor reguli.


 În cazul în care se crează un obiect al clasei derivate, mai întâi vor fi executate
constructorii claselor de bază virtuale în ordinea din lista_claselor_de_baza
(vezi declaraţia claselor derivate), apoi constructorii claselor de bază nevirtuale în
ordinea din lista_claselor_de_baza.
 Dacă într-o ierarhie de clase, o clasă de bază se moşteneşte în mai multe
exemplare într-o clasă derivată, atunci la crearea unui obiect al clasei derivate se
va executa constructorul clasei de bază o dată pentru toate exemplarele virtuale, şi
încă de atâtea ori câte exemplare nevirtuale există.
Deci în cazul exemplului de mai sus, mai întâi se execut constructorii claselor A şi F
(ele fiind virtuale), apoi constructorii claselor B, C, D şi E (clase nevirtuale). Constructorul
clasei D apelează mai întâi constructorul clasei A (ea fiind nevirtuală de această dată). De
aceea ordinea de apelare a constructorilor va fi: A, F, B, C, A, D, E şi G.

17.5. Clase abstracte. Funcţia membru virtuală pură


În cazul unei ierarhii de clase mai complicate, clasa de bază poate avea nişte
proprietăţi generale despre care ştim, dar nu le putem defini numai în clasele derivate. De
exemplu să considerăm ierarhia de clase din figura 3.
Observăm că putem determina nişte proprietăţi referitoare la clasele derivate. De
exemplu greutatea medie, durata medie de viaţă şi viteza medie de deplasare. Aceste
proprietăţi se vor descrie cu ajutorul unor funcţii membru. În principiu şi pentru clasa animal
există o greutate medie, durată medie de viaţă şi viteză medie de deplasare. Dar aceste
proprietăţi ar fi mult mai greu de determinat şi ele nici nu sunt importante pentru noi într-o
generalitate de acest fel. Totuşi pentru o tratare generală ar fi bine, dacă cele trei funcţii
membru ar fi declarate în clasa de bază şi redefinite în clasele derivate. În acest scop s-a
introdus noţiunea de funcţie membru virtuală pură.

193
Figura 3. Ierarhie de clase referitoare la animale

Funcţia virtuală pură este o funcţie membru care este declarată, dar nu este definită în
clasa respectivă. Ea trebuie definită într-o clasă derivată. Funcţia membru virtuală pură se
declară în modul următor. Antetul obişnuit al funcţiei este precedată de cuvântul cheie
virtual, şi antetul se termină cu = 0. După cum arată numele şi declaraţia ei, funcţia membru
virtuală pură este o funcţie virtuală, deci selectarea exemplarului funcţiei din ierarhia de clase
se va face în timpul execuţiei programului.
Clasele care conţin cel puţin o funcţie membru virtuală pură se vor numi clase
abstracte.
Deoarece clasele abstracte conţin funcţii membru care nu sunt definite, nu se pot crea
obiecte aparţinând claselor abstracte. Dacă funcţia virtuală pură nu s-a definit în clasa
derivată atunci şi clasa derivată va fi clasă abstractă şi ca atare nu se pot defini obiecte
aparţinând acelei clase.
Să considerăm exemplul de mai sus şi să scriem un program, care referitor la un
porumbel, urs sau cal determină dacă el este gras sau slab, rapid sau încet, respectiv tiner sau
bătrân. Afişarea acestui rezultat se va face de către o funcţie membru a clasei animal care nu
se supraîncarcă în clasele derivate. Fişierul abstract.cpp:
#include <conio.h>
#include <iostream.h>

class animal {
protected:
double greutate; // kg
double virsta; // ani
double viteza; // km / h
public:
animal( double g, double v1, double v2);
virtual double greutate_medie() = 0;
virtual double durata_de_viata_medie() = 0;
virtual double viteza_medie() = 0;
int gras() { return greutate > greutate_medie(); }
int rapid() { return viteza > viteza_medie(); }
int tiner()
{ return 2 * virsta < durata_de_viata_medie(); }
void afiseaza();
};

animal::animal( double g, double v1, double v2)


{
greutate = g;
virsta = v1;
viteza = v2;
}

void animal::afiseaza()
{
cout << ( gras() ? "gras, " : "slab, " );
cout << ( tiner() ? "tiner, " : "batran, " );
194
cout << ( rapid() ? "rapid" : "incet" ) << endl;

class porumbel : public animal {


public:
porumbel( double g, double v1, double v2):
animal(g, v1, v2) {}
double greutate_medie() { return 0.5; }
double durata_de_viata_medie() { return 6; }
double viteza_medie() { return 90; }
};

class urs: public animal {


public:
urs( double g, double v1, double v2):
animal(g, v1, v2) {}
double greutate_medie() { return 450; }
double durata_de_viata_medie() { return 43; }
double viteza_medie() { return 40; }
};

class cal: public animal {


public:
cal( double g, double v1, double v2):
animal(g, v1, v2) {}
double greutate_medie() { return 1000; }
double durata_de_viata_medie() { return 36; }
double viteza_medie() { return 60; }
};

int main() {
clrscr();
porumbel p(0.6, 1, 80);
urs u(500, 40, 46);
cal c(900, 8, 70);
p.afiseaza();
u.afiseaza();
c.afiseaza();
return 0;
}

Observăm că deşi clasa animal este clasă abstractă, este utilă introducerea ei, pentru că
multe funcţii membru pot fi definite în clasa de bază şi moştenite fără modificări în cele trei
clase derivate.

195
18. Ierarhii de clase pentru operaţii de intrare/ieşire
18.1. Streamuri
În secţiunea 15.1 am prezentat nişte cunoştinţe de bază referitoare la ierarhiile de clase
existente în C++ pentru realizarea operaţiilor de intrare/ieşire. Am văzut că nu există
instrucţiuni de intrare/ieşire în limbajul C, şi nici în C++. În schimb în limbajul C s-au definit
funcţii standard de bibliotecă, iar în C++ ierarhii de clase pentru operaţii de intrare/ieşire. În
continuare ne vom ocupa de cele două ierarhii de clase definite în C++ în vederea efectuării
operaţiilor de intrare/ieşire.
Operaţiile de intrare/ieşire sunt realizate de către cele două ierarhii de clase cu ajutorul
noţiunii de stream. Printr-un stream vom înţelege un flux de date de la mulţimea datelor sursă
(tastatură, fişier sau zonă de memorie) la mulţimea datelor destinaţie (monitor, fişier sau zonă
de memorie). Cele două ierarhii de clase sunt declarate în fişierul iostream.h, deci acest fişier
va trebui inclus de fiecare dată, când se lucrează cu ierarhiile de clase pentru intrare/ieşire.
Prima ierarhie de clase este:

Figura 4. Ierarhia de clase cu rădăcina în streambuf

Clasa streambuf se poate folosi pentru gestionarea zonelor tampon şi pentru operaţii
de intrare/ieşire simple. A doua ierarhie de clase este mai complicată. Prezentăm în
continuare o parte a ei.

Figura 5. Ierarhia de clase cu rădăcina în ios

Legătura dintre cele două ierarhii de clase s-a realizat printr-o dată membru a clasei
ios, care este un pointer către clasa streambuf. Clasa ios este clasă de bază virtuală atât pentru
clasa istream, cât şi pentru ostream. Astfel elementele definite în clasa ios vor fi prezente
numai într-un singur exemplar în clasa iostream.
Clasa istream realizează o conversie din caracterele unui obiect de tip streambuf,
conform unui format specificat. Folosind clasa ostream se poate efectua o conversie
conform unui format specificat, în caractere memorate într-un obiect de tip streambuf, iar
clasa iostream permite conversii în ambele direcţii.
196
Clasele istream_withassign, ostream_withassign şi iostream_withassign sunt clase
derivate, având clasele de bază istream, ostream respectiv iostream. În plus operatorul de
atribuire (=) este supraîncărcat în două moduri, de aceste clase.
Clasele derivate din clasa istream sau ostream se vor numi clase stream, iar obiectele
claselor derivate din clasa ios se vor numi streamuri. Există următoarele patru streamuri
standard definite în fişierul iostream.h:

Stream Obiect al clasei Corespunde fişierului standard


cin istream_withassign stdin
cout ostream_withassign stdout
cerr ostream_withassign stderr (fără zone tampon)
clog ostream_withassign stderr (cu zone tampon)

Tabelul 3. Streamuri standard

În continuare ne vom ocupa de folosirea streamurilor standard, pentru realizarea


operaţiilor de intrare/ieşire conform unui format specificat.

18.2. Ieşiri formatate


18.2.1. Operatorul de inserare

Operaţiile de scriere pe dispozitivul standard de ieşire, într-un fişier, sau într-o zonă
de memorie se pot efectua cu ajutorul operatorului <<, care în acest caz se va numi operator
de inserare.
Operandul din partea stângă al operatorului << trebuie să fie un obiect al clasei
ostream (bineînţeles şi obiectele claselor derivate din clasa ostream se consideră obiecte ale
clasei ostream). Pentru operaţiile de scriere pe dispozitivul standard de ieşire se va folosi
obiectul cout.
Operandul din partea dreaptă al operatorului << poate fi o expresie. Pentru tipul
corespunzător expresiei, trebuie să fie supraîncărcat operatorul <<. În cazul tipurilor standard
operatorul de inserare este supraîncărcat cu o funcţie membru de forma:
ostream& operator << (nume_tip_standard);

Menţionăm că pentru tipurile abstracte programatorul poate supraîncărca operatorul


de inserare. Să considerăm acum din nou exemplul prezentat în secţiunea 15.1, într-o formă
mai detaliată, pentru a ilustra asemănarea cu utilizarea funcţiei printf. Fişierul stream1.cpp:
#include <stdio.h>
#include <iostream.h>
#include <conio.h>

int main() {
clrscr();
int x = 230;
cout << x << '\n';
printf("%d\n", x);
double y = 543.67;
cout << y << '\n';
printf("%lg\n", y);
char z = 'a';
cout << z << '\n';

197
printf("%c\n", z);
char t[] = "exemplu";
cout << t << '\n';
printf("%s\n", t);
int *v = &x;
cout << v << '\n';
printf("%p", v);
return 0;
}

După executarea programului se obţine:


230
230
543.67
543.67
a
a
exemplu
exemplu
0xfff4
FFF4

Observăm că rezultatul afişării şirurilor de caractere şi datelor de tip int, double şi


char este acelaşi. În cazul afişării unui pointer, valoarea afişată este identică, dar formatul
diferă (dacă se scrie cu printf se folosesc litere mari şi nu se afişează baza, iar în cazul scrierii
cu cout se folosesc litere mici şi se afişează baza).
Deoarece operatorul << returnează o referinţă către clasa curentă, operatorul se poate
aplica în mod înlănţuit. Acest lucru este prezentat în exemplul următor. Fişierul
stream2.cpp:

#include <stdio.h>
#include <iostream.h>
#include <conio.h>

int main() {
clrscr();
int x = 10;
cout << "x (dupa incrementare) = " << x
<< "\nx (inainte de incrementare) = " << x++;
x = 10;
cout << "\n";
printf("x (dupa incrementare) = %d\
\nx (inainte de incrementare) = %d", x, x++);
return 0;
}

Prin execuţie obţinem:


x (dupa incrementare) = 11
x (inainte de incrementare) = 10
x (dupa incrementare) = 11
x (inainte de incrementare) = 10

198
În afară de faptul că operatorul de inserare se poate aplica în mod înlănţuit, se observă
că evaluarea expresiilor afişate s-a făcut în ordine inversă comparativ cu afişarea. Numai în
acest fel se poate explica faptul că valoarea afişată mai repede este deja incrementată, iar cea
afişată mai târziu are valoarea iniţială. De fapt şi pentru funcţia printf este valabilă acelaşi
lucru.
Ordinea de evaluare a expresiilor, care se afişează folosind operatorul de inserare în
mod înlănţuit, este ilustrată mai explicit de următorul exemplu. Fişierul stream3.cpp:
#include <iostream.h>
#include <conio.h>

char* f1()
{
cout << "Evaluare functie f1\n";
return "afisare 1\n";
}

char* f2()
{
cout << "Evaluare functie f2\n";
return "afisare 2\n";
}

int main() {
clrscr();
cout << f1() << f2();
return 0;
}

După executarea programului obţinem:


Evaluare functie f2
Evaluare functie f1
afisare 1
afisare 2

Deci evaluarea funcţiei f2 s-a făcut înainte de evaluarea funcţiei f1.

18.2.2. Funcţia membru setf

În limbajul C funcţia printf ne permite afişarea datelor conform unui format specificat
de programator. Acest lucru se poate realiza şi cu ajutorul ierarhiilor de clase definite în
limbajul C++. În clasa ios s-a declarat o dată membru x_flags, care se referă la formatul cu
care se vor efectua operaţiile de intrare/ieşire. Tot în clasa ios s-a definit un tip enumerare cu
care se pot face referiri la biţii datei membru x_flags. Tipul enumerare este următorul:
class ios {
public:
...
enum {
skipws = 0x0001, // se face salt peste caractere albe la citire
left = 0x0002, // la scriere se cadrează la stânga
right = 0x0004, // la scriere se cadrează la dreapta
internal = 0x0008, // caracterele de umplere vor fi după

199
// semn, sau după bază
dec = 0x0010, // se face conversie în zecimal
oct = 0x0020, // se face conversie în octal
hex = 0x0040, // se face conversie în hexazecimal
showbase = 0x0080, // se afişează şi baza
showpoint = 0x0100, // apare şi punctul zecimal în cazul
// numerelor reale
uppercase = 0x0200, // în hexazecimal se afişează litere mari

showpos // numerele întregi pozitive sunt afişate


= 0x0400,
// cu semnul în faţă
scientific= 0x0800, // afişare numere reale cu exponent
fixed = 0x1000, // afişare numere reale fără exponent
unitbuf = 0x2000, // se videază zonele tampon după scriere
stdio = 0x4000 // după scriere se videază stdout şi stderr
};
...
};

Data membru x_flags are o valoare implicită pentru fiecare tip standard. Astfel
rezultatul afişării datelor care aparţin tipurilor standard, va fi în mod implicit identic cu
rezultatul afişării cu funcţia printf, folosind specificatori de format care corespund tipurilor
respective. Prezentăm această legătură între tipuri şi specificatori de format, în tabelul 4.
Dacă se modifică biţii corespunzători datei membru x_flags, atunci şi rezultatul
afişării se va schimba, conform formatului specificat. Acest lucru se poate realiza cu ajutorul
unor funcţii membru.

Tip Specificator de format


corespunzător
int %d
long %ld
unsigned %u
long unsigned %lu
float %g
double %lg
long double %Lg
char %c
şir de caractere %s

Tabelul 4. Legătura dintre tipuri şi specificatori de format

Pentru a putea lucra mai uşor cu biţii corespunzători formatului, s-au determinat trei
grupe ale biţilor datei membru x_flags. Fiecare grupă are un nume, care este de fapt numele
unei constante statice de tip long declarate în clasa ios. Aceste grupe sunt:
- adjustfield (right, left şi internal), pentru modul de cadrare;
- basefield (dec, oct şi hex), pentru determinarea bazei;
- floatfield (scientific şi fixed), pentru scrierea numerelor reale.
Fiecare grupă are proprietatea că numai un singur bit poate fi setat în cadrul grupei. Biţii datei
membru x_flags pot fi setaţi cu ajutorul funcţiei membru setf al clasei ios. Funcţia membru
setf are două forme:
long setf(long format);

200
şi
long setf(long setbit, long grupa);

Prima variantă a funcţiei membru setf setează biţii corespunzător parametrului de tip
long: format. Dacă un bit din format este egal cu unu, atunci bitul corespunzător din x_flags
va fi unu, iar dacă bitul din format este egal cu zero, atunci bitul corepunzător din x_flags
rămâne neschimbat.
A doua variantă a funcţiei membru setf setează un bit din una dintre cele trei grupe
adjustfield, basefield sau floatfield. Cu ajutorul parametrului setbit se determină bitul care se
va seta. În locul unde se află bitul trebuie să fie unu, iar în rest zero. În parametrul al doilea
trebuie specificat numele grupei. În acest caz se anulează biţii corespunzători grupei după
care se setează biţii din setbit. Ambele variante ale funcţiei setf returnează valoarea datei
membru x_flags înainte de modificare.
Referirea la biţii datei membru x_flags se face cu numele clasei ios, urmată de
operatorul de rezoluţie şi numele bitului din tipul enumerare. Referirea la numele unei grupe
se face în mod analog, înlocuid numele din tipul enumerare cu numele grupei. Utilizarea
funcţiei membru setf este ilustrată de următorul exemplu. Fişierul stream4.cpp:
#include <stdio.h>
#include <iostream.h>
#include <conio.h>

void binar_c( long x )


{
printf("x_flags: ");
for( int i = 8*sizeof(long)-1; i >= 0; i--)
{
printf("%d", (x >> i)& 1);
if ( !(i % 8) )
printf(" ");
}
printf("\n");
}

void nume_bit_1( char* s[], long x )


{
for( int i = 0; i < 15; i++)
if ( (x >> i)& 1 )
printf("%-16s", s[i]);
printf("\n");
}

void afisare( char* s[], long x )


{
binar_c( x );
nume_bit_1( s, x);
}

int main() {

char* nume_enum[] = {
"skipws",
"left",
"right",
"internal",
"dec",
"oct",
"hex",

201
"showbase",
"showpoint",
"uppercase",
"showpos",
"scientific",
"fixed",
"unitbuf",
"stdio"
};

clrscr();
afisare( nume_enum, cout.flags() );
cout.setf(ios::oct, ios::basefield);
afisare( nume_enum, cout.flags() );
cout << 36 << '\n';
afisare( nume_enum, cout.flags() );
cout.setf(ios::showbase);
cout.setf(ios::hex, ios::basefield);
afisare( nume_enum, cout.flags() );
cout << 36 << '\n';
afisare( nume_enum, cout.flags() );
return 0;
}

Prin executarea programului obţinem:


x_flags: 00000000 00000000 00100000 00000001
skipws unitbuf
x_flags: 00000000 00000000 00100000 00100001
skipws oct unitbuf
44
x_flags: 00000000 00000000 00100000 00100001
skipws oct unitbuf
x_flags: 00000000 00000000 00100000 11000001
skipws hex showbase unitbuf
0x24
x_flags: 00000000 00000000 00100000 11000001
skipws hex showbase unitbuf

Funcţia afisare, din exemplul de mai sus, afişează mai întâi biţii datei membru
x_flags, după care se scriu numele biţilor cu valoarea egală cu unu. Mai întâi s-a setat bitul
oct şi s-a afişat valoarea constantei de tip întreg 36, folosind o conversie în octal. Astfel s-a
obţinut valoarea 44. După aceea s-au setat biţii hex şi showbase, şi s-a afişat valoarea
constantei încă o dată, astfel obţinând valoarea 0x24. Observăm că folosind a doua variantă a
funcţiei membru setf pentru setarea bitului hex, s-a obţinut şi anularea bitului oct, aşa cum am
dorit.
Menţionăm că numele şi valoarea tuturor biţilor tipului enumerare s-ar fi putut afişa
de exemplu cu următoarea funcţie:
void nume_bit( char* s[], long x )
{
for( int i = 0; i < 15; i++)
printf("%-11s:%2d\n", s[i], (x >> i)& 1);
}

202
O altă observaţie este următoarea. Pentru afişarea biţilor datei membru x_flags s-a
folosit funcţia printf şi nu ierarhia de clase declarată în fişierul iostream.h. Deşi în general o
funcţie de forma
void binar_stream( long x )
{
for( int i = 8*sizeof(long)-1; i >= 0; i-- )
cout << ((x >> i)& 1) << (i%8 ? "": " ");
cout << '\n';
}

ar afişa corect biţii datei membru x_flags, vor apare erori în cazul în care este setat
bitul showbase şi unul dintre biţii oct sau hex. În aceste cazuri se va afişa şi baza setată, deci
numerele întregi vor fi precedate de zero în cazul conversiei în octal, şi de 0x sau 0X în cazul
conversiei în hexazecimal.

18.2.3. Funcţiile membru width, fill şi precision

Ca şi în cazul funcţiei printf, se poate determina lungimea minimă a câmpului în care


se va afişa data respectivă. Această valoare este memorată în data membru x_width a clasei
ios. Valoarea implicită a datei membru x_width este zero, ceea ce înseamnă că afişarea se va
face pe atâtea caractere câte sunt necesare.
Valoarea datei membru x_width poate fi determinată sau modificată cu funcţia
membru width a clasei ios. Ea are următoarele două forme:
int width();
şi
int width( int lungime );

Prima formă a funcţiei membru width returnează valoarea datei membru x_width. A
doua variantă modifică valoarea datei membru x_width la valoarea determinată de lungime, şi
returnează vechea valoare a lui x_width.
Este foarte important de menţionat că după orice operaţie de intare/ieşire valoarea
datei membru x_width se va reseta la valoarea zero. Deci dacă nu se determină o lungime a
câmpului înainte de o operaţie de inserare, atunci se va folosi valoarea implicită.
Dacă lungimea câmpului, în care se face afişarea, este mai mare decât numărul de
caractere, care vor fi afişate, atunci cadrarea se va face în mod implicit la dreapta, şi spaţiul
rămas se va completa cu caractere de umplere. În mod implicit caracterele de umplere sunt
spaţii, dar ele pot fi modificate cu ajutorul funcţiei membru fill a clasei ios. Clasa ios are o
dată membru x_fill în care se memorează caracterul de umplere. Funcţia membru fill are
următoarele două forme:
char fill();
şi
char fill( char car );

Prima variantă returnează caracterul de umplere curent. A doua formă a funcţiei


membru fill modifică data membru x_fill la caracterul car, şi returnează vechea valoare a
caracterului de umplere.
În cazul în care se afişează valoarea unor numere reale, se poate determina precizia,
adică numărul de zecimale, care se va folosi la scrierea datelor. Clasa ios are o dată membru
203
x_precision, care are valoarea implicită egală cu zero. În mod implicit datele de tip real se vor
afişa cu şase zecimale. Funcţia membru precision are următoarele două forme:

int precision();
şi
int precision( int p );

Prima variantă a funcţiei membru precision returnează valoarea curentă a datei


membru x_precision. A doua variantă atribuie valoarea parametrului p datei membru
x_precision şi returnează valoarea anterioară. Funcţia membru precision s-a folosit în fişierul
sup_fun1.cpp pentru a determina numărul de cifre care vor fi afişate în cazul calculării
numărului π. Prezentăm în continuare un alt exemplu, în care se vor folosi funcţiile membru
din acest paragraf. Fişierul stream5.cpp:
#include <iostream.h>
#include <stdio.h>
#include <conio.h>

const double pi = 3.141592653;

void scrie_width_precision_c()
{
printf("x_width: %d\n", cout.width() );
printf("x_precision: %d\n", cout.precision() );
}

void afisare_pi_c()
{
printf("*");
cout << pi;
printf("*\n");
}

int main() {
clrscr();
scrie_width_precision_c();
afisare_pi_c();
cout.width(7);
cout.precision(2);
scrie_width_precision_c();
afisare_pi_c();
scrie_width_precision_c();
afisare_pi_c();
cout.width(7);
cout.fill('@');
scrie_width_precision_c();
afisare_pi_c();
return 0;
}

După executarea programului obţinem:


x_width: 0
x_precision: 0
*3.141593*
x_width: 7
x_precision: 2
204
* 3.14*
x_width: 0
x_precision: 2
*3.14*
x_width: 7
x_precision: 2
*@@@3.14*

Caracterele '*' s-au afişat pentru a evidenţia câmpul în care se face scrierea datelor.
Observăm că într-adevăr valoarea implicită a datelor membru x_width şi x_precision este
zero, deci afişarea se va face cu şase zecimale.
După modificarea acestor date membru la valorile x_width=7 respectiv
x_precision=2, afişarea se face în câmpul de şapte caractere, cu două zecimale, numărul fiind
cadrat la dreapta. După operaţia de scriere valoarea datei membru x_width devine zero, dar
valoarea datei membru x_precision nu se modifică.
Folosind funcţia membru width, se atribuie datei membru x_width din nou valoarea
şapte. În continuare se foloseşte funcţia membru fill pentru a determina un caracter de
umplere diferit de caracterul blanc.
Menţionăm că dacă în loc de apelarea funcţiei printf, s-ar fi folosit ierarhia de clase
declarată în fişierul iostream.h, atunci nu s-ar fi obţinut ceea ce s-a dorit. De exemplu, dacă în
loc de funcţiile scrie_width_precision_c şi afisare_pi_c s-ar fi folosit funcţiile:

void scrie_width_precision_stream()
{
cout << "x_width: " << cout.width() << '\n';
cout << "x_precision: " << cout.precision() << '\n';
}

void afisare_pi_stream()
{
cout << "*" << pi << "*\n";
}

atunci s-ar fi obţinut:


x_width: 0
x_precision: 0
*3.141593*
x_width: 7
x_precision: 2
*3.14*
x_width: 0
x_precision: 2
*3.14*
x_width: 7
x_precision: 2
*3.14*

În acest caz, de fiecare dată, valoarea lui π se afişează pe un câmp de lungime egală cu
numărul de caractere afişate. Explicaţia este că datei membru x_width după prima operaţie de
inserare s-a atribuit valoarea zero, prima operaţie fiind afişarea şirului de caractere
"x_width:     ", şi nu scrierea valorii lui π.
205
Există o legătură strânsă între funcţiile membru prezentate în acest paragraf şi
formatările din cadrul funcţiei printf. Acest lucru este ilustrat de următorul exemplu. Fişierul
stream6.cpp:

#include <iostream.h>
#include <stdio.h>
#include <conio.h>
int main() {
clrscr();
const double x = 987.65432;
cout << "*";
cout.width(11);
cout.precision(4);
cout.fill('0');
cout << x << "*\n";
printf("*%011.4lf*\n", x);
return 0;
}

Prin execuţie obţinem:


*000987.6543*
*000987.6543*

În exemplul de mai sus s-a afişat valoarea constantei de tip real x în două moduri: cu
ajutorul streamurilor, şi folosind funcţia printf. Putem să constatăm că s-a obţinut acelaşi
rezultat. Deşi în acest caz scrierea cu funcţia printf este mai compactă, afişarea cu streamuri
este mai generală, deoarece caracterul de umplere poate fi orice alt caracter, nu numai '0'.

18.2.4. Manipulatori

Biţii datei membru x_flags, care corespund conversiei, pot fi setaţi şi într-un alt mod,
folosind funcţii membru speciale, numite manipulatori. Avantajul manipulatorilor este că ei
returnează o referinţă la un stream, deci apelurile acestor funcţii membru pot fi înlănţuite.
O parte a manipulatorilor este declarată în fişierul iostream.h, iar celelalte în fişierul
iomanip.h. Manipulatorii declaraţi în fişierul iostream.h sunt:

endl trecere la linie nouă şi vidarea zonei tampon


corespunzătoare streamului
ends inserarea caracterului '\0'
flush vidarea zonei tampon a unui obiect al clasei ostream
dec conversie în zecimal
hex conversie în hexazecimal
oct conversie în octal
ws setarea bitului skipws

Tabelul 5. Manipulatorii declaraţi în fişierul iostream.h

Manipulatorii declaraţi în fişierul iomanip.h sunt:

setbase(int b) setarea bazei sistemului de numeraţie

206
corespunzătoare conversiei, la valoarea
bє{0,8,10,16}
resetiosflags(long x) ştergerea biţilor specificaţi în parametrul x,
din data membru x_flags
setiosflags(long x) setarea biţilor din data membru x_flags,
specificaţi în parametrul x
setfill(int f) datei membru x_fill i se atribuie valoarea
parametrului f
setprecision(int p) datei membru x_precision i se atribuie
valoarea parametrului p
setw(int w) datei membru x_width i se atribuie valoarea
parametrului w

Tabelul 6. Manipulatorii declaraţi în fişierul iomanip.h

Folosind manipulatori exemplul din fişierul stream6.cpp se poate transcrie în


următorul mod. Fişierul stream7.cpp:
#include <iostream.h>
#include <iomanip.h>
#include <stdio.h>
#include <conio.h>

int main() {
clrscr();
const double x = 987.65432;
cout << "*" << setw(11) << setprecision(4)
<< setfill('0') << x << "*" << endl;
printf("*%011.4lf*\n", x);
return 0;
}

Rezultatul obţinut este identic cu cel al programului anterior. Manipulatorii s-au apelat
în mod înlănţuit, deci programul devine mai simplu. În următorul exemplu se va afişa
valoarea datei membru x_flags în binar, hexazecimal, octal şi zecimal. Fişierul stream8.cpp:
#include <iostream.h>
#include <conio.h>
#include <stdio.h>

void binar_c( long x )


{
printf("x_flags: ");
for( int i = 8*sizeof(long)-1; i >= 0; i--)
{
printf("%d", (x >> i)& 1);
if ( !(i % 8) )
printf(" ");
}
printf("\n");
}

int main() {
clrscr();
binar_c( cout.flags() );
cout << "Hexazecimal: " << hex << cout.flags() << endl

207
<< "Octal: " << oct << cout.flags() << endl
<< "Zecimal: " << dec << cout.flags() << endl;
return 0;
}

Rezultatul obţinut este următorul:


x_flags: 00000000 00000000 00100000 00000001
Hexazecimal: 2001
Octal: 20001
Zecimal: 8193

În acest caz nu a fost necesară includerea fişierului iomanip.h, deoarece


manipulatorii hex, oct şi dec sunt declarate în fişierul iostream.h.
Dacă se schimbă modul de conversie cu un manipulator, ea rămâne valabilă în
continuare, până la o nouă modificare a conversiei.

18.2.5. Supraîncărcarea operatorului de inserare

În paragrafele de mai sus ne-am ocupat de modul de folosire a operatorului de inserare


pentru afişarea datelor care aparţin tipurilor standard. Ar fi de dorit ca operatorul << să fie
utilizabil şi în cazul tipurilor abstracte de date. De exemplu în cazul clasei numerelor
raţionale
class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
...
};

afişarea unei fracţii ar trebui să se efectueze în forma

cout << f;

unde f este un obiect al clasei fracţie.


Acest lucru se poate realiza, dacă se supraîncarcă operatorul de inserare pentru
afişarea obiectelor de tip Fractie.
Deoarece operandul din stânga al operatorului << este un stream, supraîncărcarea
operatorului se poate efectua cu o funcţie prieten a clasei Fractie. Pentru o clasă oareacare cu
numele Clasa, operatorul de inserare se poate supraîncărca cu următoarea funcţie prieten:
class Clasa {
...
friend ostream& operator<<(ostream&, Clasa);
...
};

Pentru clasa Fractie supraîncărcarea operatorului de inserare printr-o funcţie prieten


se poate face în felul următor. Fişierul fractie2.cpp:
#include <iostream.h>
#include <conio.h>

208
class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
friend ostream& operator<<( ostream& s, Fractie f);
};

ostream& operator<<( ostream& s, Fractie f)


{
s << f.numarator << " / " << f.numitor;
return s;
}

int main() {
clrscr();
Fractie f1(3,5);
cout << f1 << endl;
return 0;
}

Dacă se execută programul se obţine:


3 / 5

Deci se afişează numărul raţional, aşa cum am dorit. Apelarea operatorului de inserare
se poate aplica în mod înlănţuit, deoarece funcţia prieten a clasei Fractie returnează o
referinţă la streamul curent.
Deşi metoda de mai sus este simplă şi dă rezultat corect, ea are dezavantajul că
utilizarea unei funcţii prieten micşorează gradul de protecţie a datelor. Deci datele membru
protejate pot fi modificate în interiorul funcţiei prieten, care supraîncarcă operatorul de
inserare. În continuare prezentăm o modalitate de supraîncărcare a operatorului <<, fără a
introduce o funcţie prieten. Pentru o clasă oarecare cu numele Clasa, acest lucru poate fi
realizat cu ajutorul unei funcţii membru afisare, care se va apela de către funcţia care
supraîncarcă operatorul de inserare. Deci:
class Clasa {
...
public:
ostream& afisare(ostream& s);
...
};

ostream& Clasa::afisare(ostream& s)
{
...
return s;
}

ostream& operator<<(ostream& s, Clasa c1)


{
return c1.afisare(s);
}

În cazul clasei Fractie supraîncărcarea operatorului de inserare fără utilizarea unei


funcţii prieten se poate efectua în modul următor. Fişierul fractie3.cpp:
209
#include <iostream.h>
#include <conio.h>

class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
ostream& afisare( ostream& s );
};

ostream& Fractie::afisare( ostream& s )


{
s << numarator << " / " << numitor;
return s;
}

ostream& operator<<( ostream& s, Fractie f)


{
return f.afisare( s );
}

int main() {
clrscr();
Fractie f1(3,5);
cout << f1 << endl;
return 0;
}

Rezultatul obţinut prin executarea acestui program este identic cu cel al fişierului
fractie2.cpp, dar în acest caz nu s-a folosit funcţia prieten.

18.3. Intrări formatate


18.3.1. Operatorul de extragere

Operaţiile de intrare se realizează cu ajutorul operatorului >>, care în acest caz se va


numi operator de extragere.
Operandul din stânga al operatorului de extragere trebuie să fie un obiect al clasei
istream, sau a unei clase derivate din clasa istream. Operandul din dreapta va fi o expresie,
care poate aparţine atât unui tip standard, cât şi unui tip abstract. În cazul tipurilor standard se
va apela o funcţie membru a clasei istream de forma:
istream& operator>>( tip_standard& );

În cazul tipurilor abstracte programatorul poate supraîncărca operatorul de extragere.


Pentru a prezenta legătura cu funcţia scanf, revenim la exemplul din secţiunea 15.1, în
următoarea formă mai detaliată. Fişierul stream9.cpp:
#include <iostream.h>
#include <conio.h>

int main() {
clrscr();
int x;

210
cout << "x (int) = ";
cin >> x; // scanf("%d", &x);
double y;
cout << "y (double) = ";
cin >> y; // scanf("%lf", &y);
char z[20];
cout << "z (sir de caractere) = ";
cin >> z; // scanf("%s", z);
cout << "Datele citite sunt:\n"
<< "x = " << x << endl
<< "y = " << y << endl
<< "z = " << z << endl;
return 0;
}

Prin executarea programului obţinem următorul rezultat (s-au evidenţiat caracterele


citite de la intrare).
x (int) = 123
y (double) = 45.67
z (sir de caractere) = exemplu
Datele citite sunt:
x = 123
y = 45.67
z = exemplu

Acelaşi rezultat se obţine şi prin executarea programului următor. Fişierul stream10.cpp:


#include <stdio.h>
#include <conio.h>

int main() {
clrscr();
int x;
printf("x (int) = ");
scanf("%d", &x);
double y;
printf("y (double) = ");
scanf("%lf", &y);
char z[20];
printf("z (sir de caractere) = ");
scanf("%s", z);
printf("Datele citite sunt:\
\nx = %d\ny = %lg\nz = %s\n", x, y, z);
return 0;
}

Rezultă, că în cazurile de mai sus, nu există nici o diferenţă între citirea datelor de la
intrarea standard folosind operatorul de extragere, respectiv cu funcţia scanf. Totuşi în
anumite cazuri pot să apară diferenţe. De exemplu secvenţa

char c;
...
cin >> c;

nu este identică cu
211
char c;
...
scanf("%c", &c);

Diferenţa apare în cazul în care la intrare caracterul curent este un caracter alb, deci
spaţiu, tab sau caracter newline (trecere la rând nou). Prezentăm în continuare un exemplu,
din care rezultă, că într-adevăr cele două secvenţe de program nu sunt identice.
Vom defini o clasă c_alb pentru gestionarea caracterelor, şi vom supraîncărca
operatorul de inserare pentru această clasă. Supraîncărcarea se va face astfel încât caracterele
albe să fie afişate în mod vizibil (' ' pentru spaţii, '\t' pentru taburi şi '\n' pentru caractere
newline). Caracterele diferite de cele albe vor fi afişate nemodificate. Fişierul stream11.cpp:
#include <iostream.h>
#include <stdio.h>
#include <conio.h>

class c_alb {
char c;
public:
c_alb( char c_1 ) { c = c_1; }
ostream& afisare( ostream& s);
};

ostream& c_alb::afisare( ostream& s)


{
switch ( c ) {
case '\n': s << "\'\\n\'"; break;
case '\t': s << "\'\\t\'"; break;
case ' ' : s << "\' \'"; break;
default:
s << c;
}
return s;
}

ostream& operator<<(ostream& s, c_alb car)


{
return car.afisare( s );
}

int main() {
clrscr();
char v;
cout << "Citire caracter (cu operatorul >>). Intrare = ";
cin >> v;
cout << "Caracterul citit (cu operatorul >>) este: "
<< c_alb(v) << endl;
printf("Citire caracter (cu functia scanf). Intrare = ");
scanf("%c", &v);
cout << "Caracterul citit (cu functia scanf) este: "
<< c_alb(v) << endl;
return 0;
}

După executarea programului, obţinem un rezultat de forma:


Citire caracter (cu operatorul >>). Intrare = q
Caracterul citit (cu operatorul >>) este: q
212
Citire caracter (cu functia scanf). Intrare = q
Caracterul citit (cu functia scanf) este: '\t'

La intrare s-a tastat mai întâi un caracter tab, după aceea caracterul 'q', urmat de
trecerea la un rând nou. Acelaşi lucru s-a repetat şi în cazul citirii cu scanf. La scriere,
caracterul v s-a convertit mai întâi într-un obiect anonim de tip c_alb, pentru a obţine o
afişare corespunzătoare a caracterelor albe.
Din cele de mai sus rezultă că prin citirea cu operatorul de extragere s-a obţinut
primul caracter diferit de caracterele albe. Dacă s-a apelat funcţia scanf, s-a citit caracterului
curent, indiferent dacă el a fost caracter alb sau nu.
Menţionăm că diferenţa dintre cele două modalităţi de citire se poate înlătura, dacă se
anulează bitul skipws al datei membru x_flags a clasei ios (valoarea acestui bit în mod
implicit este unu).
În cazul funcţiei scanf, câmpul din care se face citirea începe cu primul caracter diferit
de caracterele albe şi se termină, dacă următorul caracter este alb, sau caracterul respectiv nu
mai corespunde formatului.
În cazul citirii unui şir de caractere, lungimea maximă a câmpului, din care se face
citirea, se determină cu funcţia membru width a clasei ios. Este valabil tot ce s-a spus referitor
la funcţia membru width în secţiunea 18.2.3, dar în acest caz valoarea datei membru x_width
se interpretează ca şi lungimea maximă a câmpului, din care se face citirea, în loc de
lungimea minimă a câmpului în care se face afişarea. Un avantaj important al funcţiei
membru width, comparativ cu utilizarea funcţiei scanf, este că parametrul actual al funcţiei
width poate fi orice expresie, în timp ce această valoare în cazul funcţiei scanf poate fi numai
o constantă. Acest lucru se poate folosi pentru înlăturarea erorilor, care ar putea să apară din
cauza citirii unui număr mai mare de caractere, decât zona de memorie alocată. Considerăm
următorul exemplu pentru ilustrare. Fişierul stream12.cpp:
#include <iostream.h>
#include <conio.h>

int main() {
clrscr();
char* t;
int max;
cout << "max = ";
cin >> max;
t = new char[max]; // cel mult max-1 caractere si
// caracterul nul
cout << "Citire cel mult " << max-1 << " caractere): ";
cin.width(max);
cin >> t;
cout << "Caracterele citite sunt: " << t;
delete [] t;
return 0;
}

După executarea programului obţinem următorul rezultat (s-au evidenţiat datele de intrare).
max = 5
Citire cel mult 4 caractere: abcdefghij
Caracterele citite sunt: abcd

213
Se observă, că deşi la intrare s-au tastat mai multe caractere, nu s-a citit decât numărul
de caractere, care se poate memora în spaţiul alocat şirului.
În cazul operaţiilor de extragere se pot folosi şi manipulatorii definiţi în paragraful
18.2.4, cu excepţia manipulatorilor endl, ends, flush şi setbase. Manipulatorul ws poate fi
folosit numai în operaţii de extragere. Se pot folosi şi celelalte funcţii membru ale clasei ios,
de exemplu funcţia membru setf.

18.3.2. Starea de eroare

Dacă citirea nu s-a terminat cu succes, atunci streamul ajunge într-o stare de eroare,
despre care putem obţine informaţii cu ajutorul datei membru state a clasei ios.
Starea de eroare este caracterizată de biţii datei membru state. Data membru state este
de tipul int, iar referirea la biţii ei se poate face cu ajutorul tipului enumerare io_state, definit
în clasa ios în modul următor.

class ios {
public:
...
enum io_state {
goodbit = 0x00, // operaţie de intrare/ieşire corectă
eofbit = 0x01, // s-a ajuns la sfârşit de fişier
failbit = 0x02, // ultima operaţie de intrare/ieşire s-a
// terminat cu insucces
badbit = 0x04, // operaţie invalidă
hardfail = 0x80 // eroare irecuperabilă
};
...
};

Biţii eofbit, failbit, badbit şi hardfail se vor numi biţi de eroare. Valorile biţilor datei
membru state pot fi determinate folosind următoarele funcţii membru ale clasei ios.

int good(); // goodbit este setat, deci nici unul din biţii de eroare
// nu este setat
int eof(); // bitul eofbit este setat
int fail(); // cel puţin unul din biţii failbit, badbit sau hardfail este setat
int bad(); // cel puţin unul din biţii badbit sau hardfail este setat

Aceste funcţii membru returnează valori diferite de zero, dacă condiţiile scrise sub
formă de comentarii sunt îndeplinite. Funcţia membru
int rdstate();

returnează valoarea datei membru state. Modificarea valorilor biţilor datei membru state se
poate efectua cu ajutorul funcţiei membru clear a clasei ios, funcţie declarată în următorul
mod:
void clear(int = 0);

Dacă se apelează funcţia membru clear fără parametru, atunci toţi biţii de eroare se
vor anula, în afară de bitul hardfail, care nu se poate anula. Dacă parametrul activ este
prezent, atunci data membru state va lua valoarea parametrului. Pentru a seta un anumit bit se
va folosi numele bitului precedat de numele clasei ios şi operatorul de rezoluţie. Dacă se
foloseşte o construcţie de forma
214
cin.clear( ios::badbit | cin.rdstate() );
ceilalţi biţi vor rămâne nemodificaţi. În acest caz s-a setat numai bitul badbit, iar ceilalţi au
rămas nemodificaţi. În următorul exemplu este prezentat modul de utilizare al funcţiei clear.
Fişierul stare1.cpp:
#include <iostream.h>
#include <conio.h>

void binar( int x )


{
for( int i = 8*sizeof(int)-1; i >= 0; i-- )
cout << ((x >> i)& 1) << (i%8 ? "": " ");
cout << endl;
}

void nume_bit_1( char* s[], int x )


{
int good_setat = 1;
for( int i = 0; i < 5; i++)
if ( (x >> i)& 1 ) {
good_setat = 0;
cout.width(16);
cout.setf(ios::left, ios::adjustfield);
cout << s[i+1];
}
if ( good_setat ) {
cout.width(16);
cout.setf(ios::left, ios::adjustfield);
cout << s[0];
}
cout << endl;
}

void afisare( char* s[], int x )


{
cout << "Data membru state: ";
binar( x );
nume_bit_1( s, x);
}

int main() {
char* nume_state[] = {
"goodbit",
"eofbit",
"failbit",
"badbit",
"hardfail"
};
char t[255];
clrscr();
afisare( nume_state, cin.rdstate() );
int x;
cout << "x = ";
cin >> x;
cout << "fail() = " << cin.fail() << endl;
afisare( nume_state, cin.rdstate() );
cout << " Setarea bitului badbit\n";
cin.clear(ios::badbit | cin.rdstate() );
afisare( nume_state, cin.rdstate() );
cout << " Anularea bitilor de eroare\n"
<< " si vidarea zonei tampon la intrare\n";
215
cin.clear(); // anularea bitilor de eroare
cin.getline(t, 255); // vidarea zonei tampon la intrare
afisare( nume_state, cin.rdstate() );
int y;
cout << "y = ";
cin >> y;
cout << "fail() = " << cin.fail() << endl;
afisare( nume_state, cin.rdstate() );
return 0;
}

Dacă se execută programul, se obţine următorul rezultat (sunt evidenţiate caracterele citite).
Data membru state: 00000000 00000000
goodbit
x = a
fail() = 2
Data membru state: 00000000 00000010
failbit
Setarea bitului badbit
Data membru state: 00000000 00000110
failbit badbit
Anularea bitilor de eroare
si vidarea zonei tampon la intrare
Data membru state: 00000000 00000000
goodbit
y = 3140
fail() = 0
Data membru state: 00000000 00000000
goodbit

Funcţia binar afişează valoarea parametrului actual de tip int, în modul în care este
memorat, iar funcţia nume_bit_1 afişează numele biţilor cu valoarea unu. Funcţia afisare
apelează mai întâi funcţia binar, iar după aceea funcţia nume_bit_1. Deci un apel de forma
afisare( nume_state, cin.rdstate() );

afişează mai întâi data membru state în forma în care este memorată în calculator, iar după
aceea numele tuturor biţilor setaţi ai datei membru state.
Observăm că în cazul citirii variabilei de tip întreg x, la intrare nu s-a aflat un număr
întreg (s-a tastat caracterul a). De aceea streamul cin a intrat în stare de eroare, şi s-a setat
bitul failbit. Funcţia fail a returnat o valoare diferită de zero (valoarea 2 corespunzătoare
bitului failbit). După aceea s-a setat bitul badbit de către programator folosind funcţia
membru clear. În continuare, anularea biţilor de eroare s-a făcut tot cu funcţia membru clear.
Înainte de o nouă citire trebuie vidat zona tampon corespunzătoare intrării standard.
Acest lucru se poate efectua prin citirea unui şir de caractere. Dacă citirea s-ar fi făcut cu
ajutorul operatorului de extragere, atunci şirul de caractere citit s-ar fi terminat la primul
caracter alb. De aceea s-a folosit funcţia membru getline a clasei istream, funcţie care este
declarată în următoarele forme:
istream& getline(signed char* z, int n, char c = '\n');
şi
istream& getline(unsigned char* z, int n, char c='\n');

216
Funcţia membru getline citeşte din streamul clasei istream un număr de cel mult n-1
caractere. Citirea se termină la întâlnirea caracterului c, sau dacă s-au citit toate cele n-1
caractere. Menţionăm că nu se iau în considerare formatările referitoare la streamul din care
se citeşte.
Deoarece funcţia membru getline citeşte şi caracterele albe, ea poate fi folosită pentru
vidarea zonei tampon şi în cazul în care la intrare s-au tastat mai multe caractere albe.
Observăm că într-adevăr după anularea biţilor de eroare şi vidarea zonei tampon, se poate citi
şi valoarea altor date, în acest caz valoarea variabilei y.
Faptul că un stream se află în stare de eroare sau nu, poate fi verificat şi în
următoarele două moduri:
- folosind supraîncărcarea operatorului '!';
- folosind conversia streamului într-un pointer de tip void*.
Operatorul '!' este supraîncărcat cu funcţia membru
int operator !();

a clasei ios. Valoarea returnată va fi diferită de zero, dacă cel puţin unul din biţii failbit,
badbit sau hardfail este setat, deci dacă funcţia membru fail() returnează o valoare diferită de
zero.
Operatorul '!' se va folosi în următorul exemplu. Să se definească o clasă pentru
prelucrarea datelor de tip întreg. Pentru clasa respectivă se va defini o funcţie membru citeste,
care va realiza citirea unei date de tip întreg. În cazul unei eventuale erori, citirea se va repeta
până când se va citi o dată corectă, sau se ajunge la sfârşit de fişier. Pentru scrierea datei de
tip întreg se va supraîncărca operatorul de inserare. Fişierul stare2.cpp:
#include <iostream.h>
#include <string.h>
#include <conio.h>

class Intreg {
int x; // data de tip intreg
// care se prelucreaza
char* afisare_text; // textul afisat inainte de citire
char* mesaj_eroare; // mesajul care va apare in cazul
// unei erori
public:
Intreg( int x_1 = 0, char* scrie = "Intreg = ",
char* mesaj_eroare = "Eroare la citire.\n");
istream& citeste(istream& s);
ostream& afiseaza(ostream& s);
};

Intreg::Intreg( int x_1, char* scrie, char* mesaj)


{
x = x_1;
afisare_text = new char[strlen(scrie)+1];
strcpy(afisare_text, scrie);
mesaj_eroare = new char[strlen(mesaj)+1];
strcpy(mesaj_eroare, mesaj);
}

istream& Intreg::citeste(istream& s)
{
char t[255];
do {
cout << afisare_text;
s >> x;

217
if ( s.eof() )
return s;
if ( !s ) {
s.clear(); // anularea bitilor de eroare
s.get(t, 255); // vidarea zonei tampon la intrare
if ( s.get() == EOF ) //citeste '\n' sau EOF
return s;
cout << mesaj_eroare;
}
else return s;
} while ( 1 );
}

ostream& Intreg::afiseaza(ostream& s)
{
s << x;
return s;
}

ostream& operator <<(ostream& s, Intreg n)


{
return n.afiseaza(s);
}

int main() {
clrscr();
Intreg i(0, "i = ");
i.citeste( cin );
if ( cin.good() )
{

cout << "Valoarea citita este: " << i;


}
return 0;
}

Prin executarea programului obţinem următorul rezultat (s-au evidenţiat caracterele citite).
i = abcd
Eroare la citire.
i = efg123
Eroare la citire.
i = 45
Valoarea citita este: 45

Clasa Intreg are următoarele trei date membru: întregul propriu zis; textul, care se va
afişa înainte de citire şi mesajul, care va apare în cazul unei erori înainte de a relua citirea.
Observăm că în corpul funcţiei membru citeste s-a folosit operatorul '!' pentru a testa
dacă a apărut o eroare la citire sau nu. În cazul în care nu s-a tastat un întreg, streamul intră în
stare de eroare, deci biţii de eroare trebuie anulaţi şi trebuie vidată zona tampon. Anularea
biţilor de eroare s-a făcut cu funcţia membru clear.
Vidarea zonei tampon s-a realizat cu ajutorul funcţiei membru get a clasei istream. Ea
are mai multe forme, din care amintim următoarele:
int get();
istream& get(signed char*, int, char = '\n');

218
istream& get(unsigned char*, int, char = '\n');

Prima formă a funcţiei membru get extrage următorul caracter din streamul curent. În
caz de sfârşit de fişier returnează EOF, deci valoarea -1. A doua, respectiv a treia formă a
funcţiei membru get este asemănătoare cu cele două forme a funcţiei membru getline.
Diferenţa este că în cazul funcţiei get caracterul terminal, determinat prin parametrul al
treilea, nu se extrage din streamul curent, iar în cazul funcţiei getline se extrage şi caracterul
terminal.
Vidarea zonei tampon s-ar fi putut efectua ca şi în cazul exemplului din fişierul
stare1.cpp, folosind funcţia membru getline în modul următor:

s.getline(t, 255);

Dacă înlocuim secvenţa


s.get(t, 255); // vidarea zonei tampon la intrare
if ( s.get() == EOF ) //citeste '\n' sau EOF
return s;

din fişierul stare1.cpp, cu apelarea funcţiei getline, obţinem un rezultat asemănător, dar pot
să apară şi diferenţe, de exemplu în cazul următor.
Rezultatul obţinut prin executarea programului stare2.cpp, varianta cu funcţia
membru getline este
i = abc^Z
Eroare la citire.
i =

şi varianta cu funcţia membru get este


i = abc^Z

Deşi ambele rezultate pot fi considerate corecte, credem că în acest caz nu mai este
necesară afişarea mesajului de eroare şi a textului pentru reluarea citirii. De aceea varianta cu
funcţia membru get este cea preferată.
O altă modalitate de a verifica faptul că streamul este în stare de eroare sau nu, este
conversia spre tipul void*. Rezultatul conversiei este pointerul NUL, deci valoarea zero, dacă
cel puţin unul din biţii failbit, badbit sau hardfail este setat, adică funcţia membru fail()
returnează o valoare diferită de zero. În caz contrar se obţine un pointer diferit de zero. De
obicei această conversie se va efectua în cazul construcţiilor de forma următoare:
if ( stream >> data )
...

Rezultatul expresiei stream >> data este o referinţă la obiectul stream, care aparţine
clasei istream. În cazul de mai sus rezultatul acestei expresii se va converti în mod automat
într-un pointer de tip void*. Astfel se poate testa, dacă un bit de eroare, diferit de bitul eofbit,
este setat sau nu.
De exemplu funcţia membru citeste a clasei Intreg se poate scrie în următoarea formă:
istream& Intreg::citeste(istream& s)
{
char t[255];
do {
cout << afisare_text;
if ( (s >> x) || (s.eof()) )
219
return s;
s.clear(); // anularea bitilor de eroare
s.get(t, 255); // vidarea zonei tampon la intrare
if ( s.get() == EOF ) //citeste '\n' sau EOF
return s;
cout << mesaj_eroare;
} while ( 1 );
}

şi rezultatul obţinut este identic cu cel al programului din fişierul stare2.cpp.

18.3.3. Supraîncărarea operatorului de extragere

Ca şi în cazul operatorului de inserare, operatorul de extragere se poate supraîncărca


pentru tipuri abstracte definite de programator. Pentru o clasă oareacare cu numele Clasa,
operatorul de extragere se poate supraîncărca cu următoarea funcţie prieten:
class Clasa {
...
friend istream& operator>>(istream&, Clasa&);
...
};

În vederea măririi gradului de protecţie a datelor se va evita utilizarea funcţiilor


prieten. De exemplu, se poate folosi o funcţie membru citire, care se va apela de către funcţia,
care supraîncarcă operatorul de extragere, în modul următor:

class Clasa {
...
public:
istream& citire(istream& s);
...
};

istream& Clasa::citire(istream& s)
{
...
return s;
}

istream& operator>>(istream& s, Clasa& c1)


{
return c1.citire(s);
}

De exemplu, în cazul fişierului stare2.cpp operatorul de extragere se va


supraîncărca în modul următor:
istream& operator >>(istream& s, Intreg& n)
{
return n.citeste(s);
}

220
Dacă se modifică fişierului stare2.cpp, astfel încât să fie supraîncărcat operatorul de
extragere, atunci expresia
i.citeste( cin );

din funcţia main poate fi scrisă în forma:


cin >> i;

Observăm că funcţia, care supraîncarcă operatorul de extragere, nu are acces la datele


protejate ale clasei Intreg. Deci s-a realizat o protecţie mai bună a datelor, decât prin folosirea
unei funcţii prieten.

221
19. Anexă

19.1. Un memento al sintaxei limbajului C

Sintaxa limbajului C

În descrierea sintaxei limbajului vom folosi următoarele notaţii:


a) ::= cu semnificaţia "prin definiţie este";
b)  pentru a separa alternativele unei definiţii sintactice;
c) < > pentru definirea unor metavariabile;
d) [ ] pentru delimitarea elementelor opţionale;
e) .. pentru a indica elementele care se pot repeta.

Exemplu: Pentru a defini un "întreg_zecimal" ca o succesiune de cifre semnificative vom scrie:


<Întreg_zecimal> ::= <Cifră_nenulă> [<Cifră>] ..

A. Atomi lexicali

<Cuvinte_cheie> ::= autobreakcasecharconstcontinuedefaultdodouble| elseenum


externfloatforgotoifint|longregister | returnshortsignedsizeof
static | struct switchtypedefunionunsignedvoidvolatilewhile

<Nume> ::= <Literă>[<Literă><Cifră>] ..

<Literă> ::= abcdefghijklmnopqrstuvwxyz


ABCDEFGHIJKLMNOPQRS
TUVWXYZ_

<Cifră> ::= 0123456789

<Constantă> ::= <Constantă_întreagă><Constantă_reală>


<Constantă_enumerare><Constantă_caracter>

<Constantă_întreagă> ::= <Număr_întreg>[<Sufix_întreg>

<Număr_întreg> ::= <Întreg_zecimal><Întreg_hexazecimal><Întreg_octal>

<Întreg_zecimal> ::= <Cifră_nenulă>[<Cifră>] ..

<Cifră_nenulă> ::= 123456789

<Întreg_hexazecimal> ::= 0x<Cifră_hexa> ..

<Cifră_hexa> ::= <Cifră>abcdefABCDEF

<Întreg_octal> ::= 0[<Cifră_octală>] ..

<Cifră_octală> ::= 01234567

222
<Sufix_întreg> ::= <Sufix_unsigned>[<Sufix_long>]<Sufix_long>[<Sufix_unsigned>]

<Sufix_unsigned> ::= Uu

<Sufix_long> ::= Ll

<Constantă_reală> ::= <Constantă_fract>[<Exponent>][<Sufix_real>]


<Cifră> .. <Exponent>[<Sufix_real>]

<Constantă_fract> ::= [<Cifră>] .. . <Cifră>..<Cifră>.. .

<Exponent> ::= Ee[+-]<Cifră> ..

<Sufix_real> ::= FfLl

<Constantă_enumerare> ::= <Nume>

<Constantă_caracter> ::= '<Caracter_C> ..'L'<Caracter_C>'

<Caracter_C> ::= <Orice_caracter_tipăribil_cu excepţia: ' i \> 


<Secvenţă_escape>

<Secvenţă_escape> ::= \"\'\?\\\a\b\f\n\r\t\v


\<Cifră_octală>[<Cifră_octală>[<Cifră_octală>]]
\x<Cifră_hexa> ..

<Şir_de_caractere> ::= "[<Caracter_S> ..]"

<Caracter_S> ::= <Orice_caracter_tipăribil_cu excepţia: ' şi \><Secvenţa_escape>

<Operatori_şi_semne_de_punctuaţie >::= +-*/%^&~!=->+=-=*=


/=%=^=&=!=<<>><<=>>== =<=
>=<>&&!!++--,()[]{};?:...

B. Declaraţii

<Unitate_de_compilare> ::= <O_declaraţie> ..

<O_declaraţie> ::= <Def_funcţie><Declaraţie>;

<Declaraţie> ::= [<Specificatori>][<Listă_declaratori>]

<Specificatori> ::= <Specificator> ..

<Specificator> ::= <Clasă_memorare><Tip>typedef

<Clasă_memorare> ::= autoregisterstaticextern

223
<Tip> ::= <Tip_simplu><Nume_typedef><Calificator>
<Descriere_tip_enumerare><Descriere_tip_neomogen>

<Tip_simplu> ::= charshortintlongsignedunsignedfloatdouble void

<Nume_typedef> ::= <Nume>

<Descriere_tip_enumerare> ::= enum<Nume>enum[<Nume>]{<Lista_enum>}

<Lista_enum> ::= <Enumerator>[,<Enumerator>] ..

<Enumerator> ::= <Nume>[=<Expr_constant_>{]

<Descriere_tip_neomogen> ::= <Tip_neomogen><Nume>


<Tip_neomogen>[<Nume>]{<List__câmpuri>}

<Tip_neomogen> ::= structunion

<Listă_câmpuri> ::= <Decl_câmpuri>;[<Decl_câmpuri>;] ..

<Decl_câmpuri> ::= [<Specificatori>][<Câmp>[,<Câmp>] ..]

<Câmp> ::= <Declarator>[<Nume>]:<Expr_constantă>

<Listă_declaratori> ::= <Decl_init>[,<Decl_init>] ..

<Decl_init> ::= <Declarator>[<Iniializare>]

<Declarator> ::= [<Indirect>..]<Denumire>[<Decl_F_T>]

<Indirect> ::= *[<Calificator> ..]

<Denumire> ::= <Nume>(<Declarator>)

<Decl_F_T> ::= ([<L_declar_par>])<Decl_tablou> ..

<Decl_tablou> ::= [][<Expr_constantă>]

<L_declar_par> ::= <Decl_par>[,<Decl_par>] .. [, ..]

<Decl_par> ::= <Tip>..<Declarator><Decl_tip>

<Decl_tip> ::= <Tip>..[<Indirect>..][(<Decl_tip>)][<Decl_F_T>]

<Iniţializare> ::= =<Expr> ={<Listă_init>[,]}

<Listă_init>::=<Expr>[,<Listă_init> ..{<Listă_init>}[,<Listă_init>] ..

<Def_funcţie> ::= [<Specif_funcţie> ..]<Declarator>[<Declaraţie>;] ..<Bloc>

224
<Specif_funcţie> ::= externstatic<Tip>

C. Expresii

<Expr> ::= <Expresie>[,<Expresie>] ..

<Expresie> ::= <Expr_condiţională>|<Expr_unară><Oper_atribuire><Expresie>

<Oper_atribuire> ::= +*=/=%=+=-=<<=>>=&=^=!=

<Expr_condiţională> ::= <Expr_SAU_logic>


<Expr_SAU_logic> ? <Expr> : <Expr_condiţională>

<Expr_SAU_logic> ::= <Expr_ŞI_logic><Expr_SAU_logic> !! <Expr_ŞI_logic>

<Expr_ŞI_logic> ::= <Expr_SAU><Expr_ŞI_logic> && <Expr_SAU>

<Expr_SAU> ::= <Expr_SAU_exclusiv><Expr_SAU><Expr_SAU_exclusiv>

<Expr_SAU_exclusiv> ::= <Expr_ŞI><Expr_SAU_exclusiv> ^ <Expr_ŞI>

<Expr_ŞI> ::= <Expr_egalitate><Expr_ŞI> & <Expr_egalitate>

<Expr_egalitate> ::= <Expr_relaională><Expr_egalitate>==Expr_relaţională>


<Expr_egalitate> != <Expr_relaţională>

<Expr_relaţională> ::= <Expr_deplasare> <Expr_relaţională> < <Expr_deplasare>


<Expr_relaţională> > <Expr_deplasare>
<Expr_relaţională> <= <Expr_deplasare>
<Expr_relaţională> >= <Expr_deplasare>

<Expr_deplasare> ::= <Expr_aditivă> <Expr_deplasare> << <Expr_aditivă>


<Expr_deplasare> >> <Expr_aditivă>

<Expr_aditivă> ::= <Expr_multiplicativă><Expr_aditivă> + <Expr_multiplicativă>


<Expr_aditivă> - <Expr_multiplicativă>

<Expr_multiplicativă>::= <Expr_multiplicativă> * <Expr_prefixată>


<Expr_multiplicativă> / <Expr_prefixată>
<Expr_multiplicativă> % <Expr_prefixată>

<Expr_prefixată> ::= <Expr_unară>(<Decl_tip>)<Expr_prefixată>

<Expr_unară> ::= <Expr_postfixată><Op_unar><Expr_prefixată>

225
++<Expr_unară> --<Expr_unară>sizeof<Expr_unară>
sizeof(<Decl_tip>)

<Op_unar> ::= &*+-~!

<Expr_postfixată> ::= <Termen><Expr_postfixată>[<Expr>]


<Expr_postfixată>(<Listă_Expresii>) <Expr_postfixată> . <Nume>
<Expr_postfixată> -> <Nume><Expr_postfixată> ++ <Expr_postfixată>--

<Termen> ::= <Nume><Constantă><Şir_de_caractere>(<Expr>)


<Listă_Expresii> ::= [<Expr>] ..

<Expr_constantă> ::= <Expr_condiţională>

D. Instrucţiuni

<Instr>::= <Instr_etichetată><Instr_compusă> <Instr_expresie>Instr_de_selecţie>


<Instr_de_ciclare><Instr_de_salt>

<Instr_etichetată> ::= <Nume>:<Instr>case <Expr_constantă { :<Instr>


default : <Instr>

<Instr_compusă> ::= <Bloc>

<Bloc> ::= {[<Declaraţie>;] .. [<Instr>] ..}

<Instr_expresie> ::= [<Expr>];

<Instr_de_selecţie> ::= if (<Expr>) <Instr>if (<Expr>) <Instr> else <Instr>


switch (<Expr>) <Instr>

<Instr_de_ciclare> ::= while (<Expr>)<Instr>;


do <Instr> while (<Expr>);
for ( [<Expr>];[<Expr>];[<Expr>] ) [<Instr>];

<Instr_de_salt> ::= goto <Nume>;continue;break;return [<Expr>];

226
19.2. Lista programelor C++
Prezentăm lista programelor împreună cu numărul secţiunii în care se află programul
respectiv.

Nr. Nr.
Nume fişier Secţiune Nume fişier Secţiune
crt. crt.
1. caracter.cpp 15.1. 30. increm1.cpp 16.7.3.
2. conv1.cpp 15.1. 31. increm2.cpp 16.7.3.
3. rezol.cpp 15.2.1. 32. conv2.cpp 16.8.2.
4. refer1.cpp 15.2.2. 33. lung1.cpp 16.8.2.
5. refer2.cpp 15.2.2. 34. conv3.cpp 16.8.3.
6. valoare1.cpp 15.2.3. 35. lung2.cpp 16.8.3.
7. valoare2.cpp 15.2.3. 36. virtual1.cpp 17.1.
8. refer3.cpp 15.2.3. 37. fractie2.cpp 17.3.
9. union1.cpp 15.3. 38. animal1.cpp 17.4.
10. init1.cpp 15.4.1. 39. animal2.cpp 17.4.
11. init2.cpp 15.4.1. 40. virtual2.cpp 17.4.
12. refer4.cpp 15.4.2. 41. abstract.cpp 17.5.
13. sup_fun1.cpp 15.4.3. 42. stream1.cpp 18.2.1.
14. sup_fun2.cpp 15.4.3. 43. stream2.cpp 18.2.1.
15. macro1.cpp 15.4.4. 44. stream3.cpp 18.2.1.
16. inline1.cpp 15.4.4. 45. stream4.cpp 18.2.2.
17. vector1.cpp 16.1. 46. stream5.cpp 18.2.3.
18. vector2.cpp 16.1. 47. stream6.cpp 18.2.3.
19. vector3.cpp 16.4. 48. stream7.cpp 18.2.4.
20. vector4.cpp 16.4. 49. stream8.cpp 18.2.4.
21. pereche1.cpp 16.5.1. 50. fractie2.cpp 18.2.5.
22. pereche2.cpp 16.5.1. 51. fractie3.cpp 18.2.5.
23. pereche3.cpp 16.5.2 52. stream9.cpp 18.3.1.
24. destruct.cpp 16.6. 53. stream10.cpp 18.3.1.
25. fractie1.cpp 16.7.2. 54. stream11.cpp 18.3.1.
26. vector5.cpp 16.7.2. 55. stream12.cpp 18.3.1.
27. vector6.cpp 16.7.2. 56. stare1.cpp 18.3.2.
28. vector7.cpp 16.7.2. 57. stare2.cpp 18.3.2.
29. vector8.cpp 16.7.2.

227
20. Bibliografie:
[1] ***: Borland C++. Getting Started. Borland International, 1991.
[2] ***: Borland C++. User's Guide. Borland International, 1991.
[3] ***: Borland C++. Programmer's Guide. Borland International, 1991.
[4] ***: Borland C++. Library Reference. Borland International, 1991.
[5] Benkő T., Moré G.: Object Windows, ComputerBooks, Budapest, 1993.
[6] Benkő T., Poppe A., Benkő L.: Bevezetés a Borland C++ programozásba,
ComputerBooks, Budapest, 1991.
[7] Decker R., Hirschfield S.: The Object Concept. An Introduction to Computer
Programming Using the C++, PWS Publishing Company, 1995.
[8] Horowitz E., Sahni S., Mehta D.: Fundamentals od Data Structures in C++,
Computer Science Press, New York, 1995.
[9] Horstmann C.S.: Practical Object-Oriented Development in C++ and Java,
John Wiley and Sons, 1997.
[10] Brian W.Kerninghan, Dennis M. Ritchie, The C Programming Language, INC.
Englewood Cliffs, New Jersey, 1978.
[11] G. Moldovan, M. Lupea, V.Cioban, Probleme pentru programare în limbajul C,
Litografia Universităţii Babeş-Bolyai, Cluj-Napoca, 1995.
[12] Negrescu L.: Limbajele C şi C++ pentru începători. Limbajul C++, Editura
Microinformatica, 1994.
[13] Stroustrup B.: The C++ Programming Language, Second Edition, AT&T Bell
Telephone Laboratories, 1991.

228

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