Metode de Evaluare
Metode de Evaluare
Metode de Evaluare
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.
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ă):
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
...
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++.
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 “;”
{ 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;
}
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).
1.3.2. Nume
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.
Î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
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ă.
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:
‘A’ 65
‘B’ 66
‘a’ 97
‘0’ 48
‘9’ 58
‘*’ 77
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
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ă.
1.3.7. Tablouri
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:
t
adresa lui
t[0]
t[0] t[1] t[2] t[3] t[4]
1.3.8. Comentariu
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ă.
#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).
17
#define nume succesiune_de_caractere
Exemple:
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.
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.
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.
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 */
...
}
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ă.
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;
Exemplu:
void f(int n)
{ int i=10;
int k=i+n;
...
}
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;
Exemple:
1) int itab[] = {1,2,3,4,5} // tabloul de tip intreg are 5 elemente itab[0] = 1,. . . itab[4] = 5
Î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;
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) << >> & ^ |
,
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)
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.
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.
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.
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.
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.
& 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.
Practic s-a obţinut valoarea biţilor 8,7,6 a lui x (numerotaţi de la dreapta începând cu 0).
(v este fie o variabilă simplă, fie variabilă cu indici sau un element de structură).
O expresie de forma:
v1=(v=expresie);
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;
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)
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]
(tip) operand
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.
sizeof (data)
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.
Exemple:
int i, j, k;
float a, b;
double x, y;
unsigned p;
long r;
char c;
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
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
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).
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:
%[-][d..d][.d..d][l1]l2
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.
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
}
2)
#define sir “PC WORLD”
void main (void)
{ printf(“*%10s*“,sir);
printf(“*%-10s*“,sir);
printf(“*%10.5s*“,sir);
printf(“*%-10.5s*“,sir);
}
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.
&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.
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
}
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:
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.
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.
40
5.1.3. Programare modulară
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ă).
41
a) secvenţa;
b) iteraţia pretestată;
c) alternativa simplă.
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.
42
5.1.6. Programarea orientată spre obiecte
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ă.
expresie;
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
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);
}
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.
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);
}
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
}
switch (exp)
{ case c1: sir1
break;
case c2: sir2
break;
...
case cn: sirn
break;
default: sir
}
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
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;
...
}
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.
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.
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 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.
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
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;
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;
dintr-o declaraţie de pointer reprezintă tip dintr-o declaraţie obişnuită. De aceea, construcţia
tip *
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;
Să presupunem că această funcţie determină lungimea unui şir de caractere şi se poate apela
prin:
56
l=lungime(tablou);
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;
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
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.
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:
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
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
58
funcţia lungime
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
}
}
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:
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.
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:
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:
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();
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ă:
#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);
}
62
double sinxp (double x)
{ return sin (x*x); }
Exemplu:
Considerăm că la lansarea programului prog s-au furnizat parametrii:
31 MARTIE 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]);
}
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.
}
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:
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:
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;
}
}
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:
Î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>
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.
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>
68
4) Programul determina maximul dintr-un vector de numere astfel:
#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);
}
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);
}
}
#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 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.
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;
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:
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:
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;
};
dc.ziua
dc.anul
dc.luna (atenţie este pointer spre caractere)
tdc[i].ziua
tdc[i].anul
tdc[i].luna (este pointer)
tdc[i].luna[0], tdc[i].luna[1], . . . , tdc[i].luna[11]
74
void functie(struct data_calendaristica *p)
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ă.
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];
int i, j, tablou[10];
Analog:
REAL x, y, z;
float x, y, z;
PI p;
care este echivalentă cu:
int *p;
4) Declaraţia
typdef struct
{ double real;
double imaginar;
} 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
union u *p;
atunci construcţia
p -> i
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
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.
78
break;
default:
// eroare
}
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.
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;
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;
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.
- indicatori.a
- indicatori.b
- indicatori.c
- indicatori.d
- indicatori.e
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ă.
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
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
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
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:
capul
capul
capul
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.
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.
#include <stdio.h>
#include <alloc.h>
typedef struct nod // definirea tipului NOD
{ int nr;
int patrat;
struct nod* leg;
} NOD;
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
}
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
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:
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);
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:
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.
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:
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.
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.
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
}
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);
}
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”);
Fişierele pot fi scrise şi citite caracter cu caracter, folosind două funcţii simple:
- putc pentru scriere;
- getc pentru citire.
Închiderea unui fişier se realizează cu ajutorul funcţiei fclose care are prototipul:
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);
}
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);
}
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);
}
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:
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.
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:
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.
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);
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.
ARTICOL a[6];
char zt[6*sizeof(ARTICOL)];
}buf;
{
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);
}
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:
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ă:
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;
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.
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.
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ţii de copiere:
char *strcpy (char *dest, const char *sursa);
- 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;
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:
Funcţia lungime:
unsigned strlen (const char *sir);
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].
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.
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);
}
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:
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:
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
Modul MONO se poate seta pe un adaptor monocolor. Celelalte moduri se pot seta pe
adaptoare color.
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.
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.
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.
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;
};
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>
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);
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.
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:
#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 Acionati 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).
valoare simbol
1 CGA
2 MCGA
3 EGA
4 EGA64
5 EGAMONO
6 IBM8514
7 HERCMONO
8 ATT400
9 VGA
10 PC3270
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
pentru VGA
valoare simbol
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:
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:
Funcţia initgraph mai poate fi apelată şi folosind constanta simbolică DETECT astfel:
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.
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.
117
struct palettetype *far getdefaultpalette(void);
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 ();
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 (“acionai 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.
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).
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 (BA) 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 CB 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 YX astfel ca y /yY,
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:
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ţă.
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
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;
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
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 Cci 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+x3suma
127
x1*x2*x3produs
cu valorile suma şi produs date iar x1S1, x2S2 şi x3S3 ; 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+nx13).
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
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();
}
d1 d2 dn
*----->*---->*------>* . . . *------>*
so s1 s2 sn-1 sn
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
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 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Ă:
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);
...
}
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);
Deci în loc de apelarea funcţiei printf de mai sus, s-ar fi putut scrie şi:
cout << "w = " << w + 25 << endl;
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;
}
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
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;
}
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;
}
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.
tip *
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
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;
}
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.
Î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>
int main () {
int y;
y = 10;
clrscr();
nu_modifica( y ); // parametrul actual y nu se modifica
printf("y = %d", y);
return 0;
}
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>
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
Î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ă.
142
struct Persoana {
char nume[15];
char prenume[15];
long telefon;
};
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;
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;
}
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
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>
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;
}
Î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ă.
Î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
Î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>
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;
}
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.
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
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;
}
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>
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;
}
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>
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;
}
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.
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;
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();
};
void vector::lapatrat()
{
for(int i = 0; i < d; i++)
e[i] *= e[i];
}
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;
}
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
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()
{
delete[] nume;
delete[] prenume;
}
void persoana::afiseaza()
{
cout << prenume << ' ' << nume << endl;
}
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
}
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)
#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
};
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;
}
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;
}
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();
};
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
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();
};
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()
{
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]++;
}
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;
}
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
};
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;
}
#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
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;
}
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;
}
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.
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;
}
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;
}
173
#include <iostream.h>
#include <stdio.h>
#include <conio.h>
class fractie {
long numarator;
long numitor;
public:
fractie(long a, long b);
fractie& operator~ (); // simplificare fractie
fractie operator *(fractie r);
void afiseaza();
};
fractie& fractie::operator~()
{
long d = cmmdc(numarator, numitor);
numarator /= d;
numitor /= d;
return *this;
}
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;
}
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);
...
};
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
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();
};
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;
}
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;
}
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.
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
}
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();
};
fractie::operator double()
{
return double(numarator) / numitor;
}
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;
}
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();
};
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::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;
}
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();
};
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;
}
Î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ă.
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ă.
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;
}
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);
};
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;
}
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();
};
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.
Î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);
};
animal::animal(char* n)
{
strcpy(nume, n);
}
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);
};
animal::animal(char* n)
{
strcpy(nume, n);
}
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 :
class A {
public:
A(char* a) {
cout << "Se apeleaza constructorul clasei A"
<< " cu parametrul " << a << 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;
}
};
int main() {
clrscr();
G ob("obiect");
return 0;
}
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();
};
void animal::afiseaza()
{
cout << ( gras() ? "gras, " : "slab, " );
cout << ( tiner() ? "tiner, " : "batran, " );
194
cout << ( rapid() ? "rapid" : "incet" ) << endl;
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:
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.
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:
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);
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;
}
#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;
}
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;
}
Î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
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.
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>
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;
}
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.
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 );
int precision();
şi
int precision( int p );
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;
}
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";
}
Î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;
}
Î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:
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
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>
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;
}
cout << f;
208
class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
friend ostream& operator<<( ostream& s, Fractie f);
};
int main() {
clrscr();
Fractie f1(3,5);
cout << f1 << endl;
return 0;
}
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;
}
class Fractie{
int numarator;
int numitor;
public:
Fractie( int a, int b) { numarator = a; numitor = b;}
ostream& afisare( ostream& 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.
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;
}
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);
};
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;
}
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.
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>
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);
};
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;
}
int main() {
clrscr();
Intreg i(0, "i = ");
i.citeste( cin );
if ( cin.good() )
{
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);
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 =
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 );
}
class Clasa {
...
public:
istream& citire(istream& s);
...
};
istream& Clasa::citire(istream& s)
{
...
return 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 );
221
19. Anexă
Sintaxa limbajului C
A. Atomi lexicali
222
<Sufix_întreg> ::= <Sufix_unsigned>[<Sufix_long>]<Sufix_long>[<Sufix_unsigned>]
B. Declaraţii
223
<Tip> ::= <Tip_simplu><Nume_typedef><Calificator>
<Descriere_tip_enumerare><Descriere_tip_neomogen>
<Listă_init>::=<Expr>[,<Listă_init> ..{<Listă_init>}[,<Listă_init>] ..
224
<Specif_funcţie> ::= externstatic<Tip>
C. Expresii
225
++<Expr_unară> --<Expr_unară>sizeof<Expr_unară>
sizeof(<Decl_tip>)
D. Instrucţiuni
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