Sunteți pe pagina 1din 123

Limbajul C++

Tipuri de date, variabile, constante, functii de I/E


Structura unui program C++
Limbajul C, aparut la inceputul anilor 70, a fost creat de Denis Ritchie si Brian Keringhan si are urmatoarele caracteristici: Este dotat cu un set puternic de operatori (inclusiv operatori de acces la nivel de bit); Efectueaza controale sumare asupra atribuirilor, executand de cele mai multe ori si conversiile necesare; Permite folosirea numai a subprogramelor de tip functie, dar o functie poate fi apelata la fel ca o procedura; Este orientat pe pointeri; Imbina caracteristicile limbajelor de ansamblare cu cele ale limbajelor de nivel inalt. Limbajul C++, creat de Bjarne Stroustrup, poate fi privit ca o extensie a limbajului C++ care permite programarea pe obiecte. Un program C++ este alcatuit exclusiv din functii, dintre care una este principala si are numele main. Deci, fiecare program contine cel putin o functie si anume functia main. De exemplu, programul urmator afiseaza textul inclus intre ghilimele:
#include <stdio.h> void main() { printf(Acesta este textul ce se va afisa); }

Prima linie de cod indica faptul ca se va apela o functie, printf (afiseaza o informatie), care se gaseste in fisierul stdio.h. Un astfel de fisier se numeste fisier header (de aici si extensia .h). Fisierele header grupeaza prototipurile functiilor inrudite care pot fi apelate in C++. De exemplu fisierul header stdio.h contine prototipurile functiilor de citire/scriere. Cuvantul rezervat void din fata numelui functiei main indica faptul ca functia nu va returna nici un rezultat. Deoarece, o functie contine cel putin o instructiune bloc (numita si instructiune compusa) este obligatoriu includerea unei perechi de acolade. Fiecare instructiune dintr-un program C++ trebuie sa se termine cu caracterul punct si virgula (;). Limbajul C (C++) nu recunoaste sfarsitul liniei ca terminator. Aceasta inseamna ca nu exista restrictii in ceea ce priveste pozitia unei instructiuni in linie. De asemenea, pe o linie se pot plasa doua sau mai multe instructiuni. In procesul de transformare a textului sursa in program executabil apare o faza noua, numita preprocesare, care se executa inaintea compilarii. In faza de preprocesare fisierele header precizate sunt incluse in textul sursa. Din punct de vedere sintactic, un program C++ este o secventa de caractere ASCII, format din unitati lexicale separate prin spatii. Unitatile lexicale pot fi grupate astfel: Cuvinte cheie cuvinte care au un rol bine stabilit; ele nu pot fi folosite in alt context. if, while, for, do, break, continue etc. sunt cuvinte cheie care desemneaza instructiuni ale limbaului C (C++) si ele nu pot fi utilizate in alte scopuri (de exemplu, ca identificatori). Identificatori se folosesc pentru a denumi variabilele, functii etc. Identificatorii se formeaza cu ajutorul literelor mari si mici ale alfabetului (inclusiv liniuta de subliniere) si a cifrelor. Trebuie sa inceapa cu o litera si sa nu corespunda unui cuvant rezervat. Limbajul face deosebirea dintre literele mari si mici folosite intr-un identificator. Un identificator poate avea un numar nelimitat de caractere dar numai primele 32 caractere 1

sunt semnificative. De exemplu, x, y, pf, factorial, pi, sort_asc sunt identificatori corecti, in timp ce 1sort nu este un identificator valid deoarece incepe cu o cifra. Constante sunt valori numerce (5, 25, 56.7), caractere (A, c, x) si siruri de caractere (Introduceti un numar, Rezultatele sunt;), care nu se pot modifica pe timpul prelucrarilor; Operatori reprezinta simboluri sau cuvinte care precizeaza operatia care se va executa. De exemplu, + este simbolul operatiei de adunare, * este simbolul operatiei de inmultire, ++ este simbolul operatiei de incrementare, sizeof este operatorul pentru determinarea spatiului de memorie necesar unei variabile etc. Un program poate contine si comentarii, care pot fi plasate oriunde in textul sursa, respecatand urmatoarele reguli: Pentru a crea un comenatriu care sa ocupe una sau mai multe linii, se incadreaza textul intre perechile de caractere /* si */. De exemplu:
/* Acesta este o linie de comentariu */

Pentru a introduce un comentariu intr-o linie de program, la sfarsitul acesteia, se plaseaza in fata textului caracterele //.
printf(Rezultatul); // Comentariu pe linia de program

Tipuri de date
Una din primele intrebari pe care trebuie sa ni le punem in legatura cu un limbaj de programare, se refera la tipurile de date utilizabile pentru reprezentarea obiectelor ce intervin in problemele pe care le rezolvam. Aceste obiecte pot fi de complexitate diferita, pornind insa intordeauna de la elemente simple, de baza (numere, caractere, valori de adevar), care pot fi organizate in structuri din ce in ce mai complexe. De aceea raspunsul la intrebarea noastra va contine doua parti: cea referitoare la tipurile de date predefinite (fundamentale) ale limbajului si cea privind mecanismele ce sunt oferite pentru reprezentarea valorilor unor obiecte de alta natura decat cele direct reprezentabile prin intermediul tipurilor predefinite. Principalele tipuri de date predefinite (fundamentale) in limbajul C (C++) sunt tipurile aritmetice: intregi si reale. Aceasta optiune se bazeaza pe faptul ca entitatile de alta natura (caractere, valori de adevar, culori, stari etc.) pot fi codificate numeric, iar operatiile specifice lor pot fi reprezentate prin prelucrari, simple sau compuse, efectuate asupra codurilor numerice adoptate. Astfel, pentru reprezentarea valorilor logice se recurge la o conventie foarte simpla si clara: fals se codifica prin valoarea intreaga 0, iar adevarat printr-o valoare intreaga nenula (in cazul evaluarii unei expresii relationale sau logice, aceasta valoare este 1). Limbajul C++ are o deosebita putere de reprezentare, datorita mecanismelor pe care le ofera pentru declararea de tipuri derivate, pornind de la cele fundamentale. Astfel, pot fi declarate: Tipuri structurate tablouri, structuri, uniuni; Tipuri functie - un tip functie este caracterizat atat prin tipul rezultatului furnizat, cat si prin numarul si tipul argumentelor necesare pentru obtinerea rezultatului; Tipuri pointer - ofera posibilitatea de adresare indirecta la entitati de diferite tipuri, deoarece o valoare de tip pointer reprezinta adresa unei entitati definite in program.

Declaratii de tip
Tratarea corecta a unei entitati presupune cunoasterea tipului sau. Daca, in cazul constantelor explicite, tipul poate fi dedus din modul in care sunt ele precizate (de exemplu 5 este o constanta intreaga, 3.5 este o constanta reala, a este o constanta caracter), in cazul entitatilor identificate prin nume simbolice (variabile, functii, constante simbolice), tipul lor trebuie precizat anterior primei utilizari printr-o declaratie sau definitie corespunzatoare. Printr-un tip de data se intelege un ansamblu format din trei elemente: multimea valorilor admise; modalitatea de reprezentare a acestora in memoria interna; 2

setul de operatii permise cu valorile respective. Declaratiile de variabile sunt de cele mai multe ori, definitii, deoarece pe langa asocierea unui nume determina si alocarea spatiului de memorie necesar pastrarii valorilor. Definitia unei variabile poate fi completata prin specificarea unei valori de initializare. Momentul initializarii variabilelor este insa conditionat de clasa de memorare a acestora. O declaratie se poate referi la una sau mai multe variabile precizand clasa de memorare, tipul de data, identificatorii si eventual valorile de initializare ale acestora, folosind urmatoarea sintaxa:
[clasa_memorare] tip variabila [=valoare] [,variabila [=valoare]]

Intr-o declaratie C++ se poate preciza variabile de tipuri diferite, insa derivate din acelasi tip de baza. De exemplu,
char c=A, *p, s[10]

se refera la trei variabile de tipuri diferite: caracterul c, pointerul p si tabloul s de cel mult 10 caractere.

Tipul aritmetic
In limbajul C++, ca de altfel in majoritatea limbajelor de programare, pentru a reprezenta valorile numerice exista mai multe tipuri de date predefinite (numite si tipuri fundamentale), care se pot grupa in doua categorii: 1. Tipuri intregi Se folosesc pentru reprezentarea numerelor intregi. Tipurile predefinite din limbajul C++ ce pot fi utiliza pentru a reprezenta valorile intregi sunt: unsigned char (caracter fara semn) ocupa 8 biti si poate lua valori cuprinse intre 0 si 255; char (caracter) ocupa 8 biti si poate avea valori intre 128 si 127; unsigned int (intreg fara semn) ocupa 16 biti si poate lua valori intre 0 si 65535; short int (intreg scurt) ocupa 16 biti si permite valori intre 32768 si 32767; int (Intreg) ocupa de regula 16 biti (lungimea poate diferi de la o implementare la alta) si ia valori intre 32768 si 32767; unsigned long (intreg lung fara semn) ocupa 32 de biti si ia valori intre 0 si 4294967295; long (intreg lung cu semn) - ocupa 32 biti (4 bytes) si poate avea valori cuprinse intre 2147483648 si 2147483647.
Nota: Caracterele sunt considerate tot numere intregi, deoarece in memorie ele sunt reprezentate prin valorile numerice ale codului ASCII.

2. Tipuri reale Sunt folosite la reprezentarea numerelor reale. Tipurile fundamentale din limbajul C++ care pot fi utilizate pentru a reprezenta valorile reale sunt: float (virgula mobila simpla precizie) ocupa 32 biti (4 bytes) si ia valori intre 3.4x10-38 si 3.4x1038; double (virgula mobila dubla precizie) ocupa 64 biti (8 bytes) si poate avea valori intre 1.7x10-308 si 1.7x10308; long double (virgula mobila extinsa) ocupa 80 biti (10 bytes) si permite valori intre 3.4x10-4932 si 1.1x104932.

Tipul enumerare
Tipurile enumerare sunt cazuri particulare ale tipurilor intregi. Ele se folosesc pentru a realiza o reprezentare comoda si sugestiva a obiectelor ale caror valori se pot identifica printr-un numar finit de nume simbolice. De exemplu, unitatile de masura ale unui produs pot fi: metru, litru, gram, bucata; mijlocul de transport pentru o deplasare poate fi: trenul, avionul, autocarul, autoturismul. Valorile constante identificate prin nume simbolice specifice problemei trebuie codificate prin entitati admise de limbajul de programare, deci prin constante numerice. Insa prin utilizarea directa a codificarii numerice se pierde din claritatea asigurata de folosirea numelor simbolice. Solutia acestei probleme este oferita de tipurile enumerare, care in esenta, 3

declara o multime de constante simbolice carora li se asociaza coduri numerice de tip intreg, incepand cu 0. Pentru a declara un tip enumerare se foloseste sintaxa:
enum [nume_tip] {lista_nume_constante} [lista_variabile];

unde: nume_tip reprezinta identificatorul tipului enumerare, care poate lipsi, dar in acest caz va fi prezent cel putin un nume de variabila in lista_variabile; lista_nume_constante reprezinta o lista de constante simbolice, separate prin virgula, care specifica identificatorii pentru valorile numerice asociate de compilator. lista_variabile reprezinta o lista formata din una sau mai multe nume de variabile ale tipului enumerare precizat prin nume_tip. Daca aceasta lista lipseste, atunci variabilele tipului enumerare vor fi precizate ulterior. Exemple: 1. Avand declaratia:
enum {ilegal, ianuarie, februarie, martie, aprilie, mai, iunie, iulie, august, septembrie, octombrie, noiembrie, decembrie} luna;

numarul lunii poate fi inlocuit prin denumirea lunii respective. De exemplu, o atribuire de forma:
luna = 3;

se poate inlocui cu una mai sugestiva:


luna = martie;

deoarece, conform declaratiei de mai sus, luna are valoarea 3. 2. Fie declaratia:
enum Boolean {false, true};

atunci pentru a putea folosi acest tip este necesar sa declaram o variabila de forma:
enum Boolean exista;

In continuare se pot folosi expresii de forma:


exista = false;

sau
exista = true; Nota: Compilatorul C nu controleaza datele de tip enumerare si din acest motiv se pot scrie expresii care sa nu corespunda scopului pentru care a fost definit tipul de date. In C, variabilele tipului enumerare sunt tratate ca simple date de tip int.

Asignarea unui nume pentru tipurile de date


In limbajul C (C++) se poate atribui un nume unui tip, indiferent daca el este un tip predefinit sau definit de utilizator: Pentru aceasta se foloseste o constructie de forma:
typedef tip nume_tip;

unde: tip este fie un tip predefinit (fundamental) fie un tip definit de utilizator; nume_tip reprezinta identificatorul care se atribuie tipului precizat prin tip. Dupa atribuirea unui nume tipului de date, identificatorul respectiv se poate folosi pentru a declara variabile ale acelui tip, la fel cum se folosesc in declaratii cuvintele cheie ale tipurilor predefinite: int, float, char etc. Exemple: 1. Fie declaratia:
typdef float Real;

Dupa aceasta declaratie, cuvantul Real poate fi folosit pentru a identifica date de tip float. Deci declaratia:
Real x;

este echivalenta cu:


float x;

2. Daca in program exista declaratia:


typedef int Intreg;

atunci declaratia:
Intreg y;

este echivalenta cu:


int x;

La declararea tipului enumerare se poate folosi cuvantul cheie typedef astfel:


typedef enum {lista_nume_constante} nume_tip;

In continuare, variabilele tipului enumerare pot fi declarate folosind urmatoare:


nume_tip lista_variabile;

Folosind acest mod de declarare, exemplele prezentate mai sus pot fi scrise astfel:
typedef enum {ilegal, ianuarie, februarie, martie, aprilie, mai, iunie, iulie, august, septembrie, octombrie, noiembrie, decembrie} DenLuna; DenLuna luna; typedef enum {false, true} Boolean; Boolean exista;

Constante
In limbajul C++ exista mai multe tipuri de constante, dar in continuare se vor prezenta doar cele corespunzatoare tipurilor definite (fundamentale). 1. Constante intregi. Ele se clasifica astfel: Zecimale; Octale (in baza 8). Se declara precedand numarul de un 0 nesemnificativ. De exemplu, 0143 reprezinta constanta octala 143. Hexazecimale (in baza 16). Ele sunt declarate plasand in fata numarului caracterele 0X sau 0x. De exemplu, 0x7FA este constanta hexazecimala 7FA.
Observatie: O constanta intreaga este pozitiva. Daca se foloseste semnul in fata unei constante, avem de fapt o expresie constanta (- este un operator unar), care va fi evaluata.

Constanta este memorata dupa un mecanism implicit (fara interventia programatorului) sau explicit. Mecanismul implicit tine cont de valoarea constantei. De exemplu, valorile cuprinse intre 0 si 32767 sunt memorate folosind tipul int, in timp ce valorile cuprinse intre 32768 si 2147463647 sunt memorate folosind tipul long. Mecanismul explicit consta in a forta ca o anumita constanta sa fie memorata intr-un anumit tip de data intreaga folosind un sufix. De exemplu, daca vrem ca valoarea 234 sa fie memorata intr-un tip long, se va folosi sufixul L sau l, adica se va scrie 234L sau 234l. 2. Constante caracter. Constanta de tip caracter reprezinta un caracter incadrat intre apostrofuri. De exemplu, A, 5 etc. sunt constante caracter. Constantele caracter pot fi declarate si folosind o secventa escape. Secventa escape incepe cu caracterul backslash (\), si se foloseste in urmatoarele cazuri: Cand vrem sa folosim codul ASCII in octal sau hexazecimal al caracterului in locul caracterului propriu-zis. De exemplu, litera a are codul ASCII 97(10), 141(8) si 61(16) si folosind o secventa escape, constanta a poate fi scrisa sub forma \141 sau \x61 (in cazul folosirii sistemului hexazecimal, valoarea este precedata de caracterul x). In cazul caracterelor care nu au simboluri grafice. De exemplu, caracterul newline (codul 10(10), 12(8) sau A(16)) poate fi declarat \12 sau \xA. Pentru anumite caractere, in C++ exista si notatii speciale sub forma unei secvente escape. De exemplu, constanta newline poate fi scrisa si sub forma \n. 3. Constante sir de caractere. Se compun dintr-o succesiune de mai multe caractere incluse intre ghilimele. Sirul nul (numit si sir de lungime zero) este sirul care nu contine nici un caracter. Constantele sir de caractere pot contine si secvente escape. De exemplu, Acesta este o constanta sir de caractere, Acest sir contine secventa escape \n care va determina scrierea constantei pe doua linii, (este sirul nul). 5

4. Constante reale. Ele se memoreaza implicit folosind tipul double. Pentru a forta memorarea intr-un tip float se foloseste sufixul F sau f, iar pentru tipul long double sufixul L sau l. De exemplu, 3.5, 1.7e10, 1.754, 3.14f sunt constante reale.

Declararea variabilelor si a constantelor cu nume


Intr-un program, pe langa date constante, se folosesc si date variabile, care isi pot modifica valorile pe timpul executiei programului. Cel mai simplu mod de referire a unei date variabile este acela de a denumi data respectiva. Numele datei va permite accesul la valoarea ei si schimbarea valorii atunci cand este necesar. In cazul in care o data declarata nu are legaturi cu alte date (de exemplu, legaturi de ordine), se spune ca ea este o data izolata. Numele datei izolate se spune ca reprezinta o variabila simpla. Unei date izolate ii corespunde un tip. In cursul executiei programului se pot modifica valorile unei variabile simple, dar nu si tipul ei. Daca intre datele ce se prelucreaza se poate satbili o legatura, atunci ele pot fi grupate pentru a fi manipulate mai usor. Grupului de date i se asociaza un nume care va fi folosit la adresarea atat a intregului grup cat si a elementelor ce compun grupul respectiv. Numele grupului de date reprezinta o variabila structurata. Corespondenta dintre numele unei variabile si tipul ei se stabileste printr-o declaratie.

Variabile simple
Declararea unei variabile simple se realizeaza respectand urmatoarea sintaxa:
tip_data nume1 [= constanta][, nume2[= constanta]] ;

unde: tip data - este tipul de data al variabilelor; nume1, nume2 - numele variabilelor; constanta - valoarea de initializare a variabilei. Exemple: int x, y = 1; S-au declarat variabilele x si y de tip intreg. Variabila x nu este initializata, in timp ce variabila y este initializata cu 1; char ok = D, lit = 65; S-au declarat doua variabile de tip caracter ok si lit. Prima variabila este initializata cu codul caracterului D, iar a doua cu valoarea 65 (codul ASCII al literei A). float pi = 4.1415; Variabila pi de tip real simpla precizie a fost initializata cu valoarea 3.1415.
Observatie: La declararea variabilelor, in locul constantelor de initializare se pot folosi expresii constante (expresii formate din constante si operatori), de exemplu: int x = 25 * 3;

Variabile structurate
De foarte multe ori, intr-un program este necesar sa prelucram grupuri de date. In functie de modul de stabilire a legaturilor dintre datele ce vor forma un grup, variabilele structurate pot fi clasificate in tablouri, structuri si reuniuni.

Tablouri
Cel mai simplu procedeu ce se poate utiliza la gruparea datelor de acelasi tip il constituie considerarea lor ca formand o multime ordonata de elemente, care pot fi referite folosind indici. O astfel de grupa se spune ca formeaza un tablou (numit si masiv), caruia i se poate asocia un nume. Tipul comun al elementelor unui tablou este si tipul tabloului. De exemplu, o multime ordonata de intregi, reprezinta un tablou de intregi. Atunci cand elementele care se grupeaza intr-un tablou sunt ele insele tablouri, sunt necesari mai multi indici pentru referirea lor. Daca se foloseste un singur indice pentru referirea la elementele unui tablou, se spune ca tabloul este unidimensional, iar daca pentru la referirea elementelor se folosesc mai multi indici, se spune ca tabloul este n-dimensional. Tablourile unidimensionale se mai numesc si vectori, iar tablourile cu doua dimensiuni se numesc matrici. Un tablou, la fel ca orice variabila simpla, trebuie declarat inainte de a fi utilizat. Declaratia de tablou trebuie sa indice tipul comun al elementelor sale, numele tabloului si numarul maxim de elemente din fiecare dimensiune, inclus intre paranteze drepte. Deci o declaratie de tablou trebuie sa fie de forma:
tip_comun nume_tablou[limita1][limita2][limitaN];

unde: tip_comun - specifica tipul comun al elementelor tabloului. nume_tablou indica numele tabloului. limitaI reprezinta numarul maxim de elemente de pe dimensiunea I; adica al I-lea indice poate lua valorile 0, 1, 2, , limitaI-1. Limitele superioare pot fi constante sau expresii constante. Exemple: int vect[10]; defineste tabloul vect de 10 elemente de tip int. In memorie, acestei variabile i se aloca 20 bytes (10 * 2 bytes). vect este un simbol a carei valoare este adresa primului sau element, adica adresa lui vect[0]. Deci vect[0] are ca valoare valoarea primului element al tabloului, iar vect are ca valoare adresa acestui element. double dmat[10][20]; defineste un tablou bidimensional de tip double. El reprezinta o matrice de 10 linii si 20 coloane fiecare si ocupa 800 bytes (10 * 20 * 4 bytes). Referirea la elementele unui tablou se face folosind o variabila cu indici, care se compune din numele tabloului urmat de valorile indicilor, fiecare indice fiind reprezentat printr-o expresie inclusa intre paranteze drepte.
Atentie: In limbajul C (C++) valoarea inferioara a indicilor este intotdeauna egala cu 0, iar cea superioara este mai mica cu o unitate fata de numarul maxim de elemente precizat pentru dimensiunea respectiva.

Deci, in cazul primului exemplu (care este un vector), elementele se vor referi prin: vect[0], vect[1], , vect[9]. In cazul celui de-al doilea exemplu (care este o matrice) referirea se va face astfel: dmat[0][0], care este primul element al tabloului, dmat[0][1], , dmat[0][19] pentru elementele din prima linie a tabloului, dmat[1][0], , dmat[1][19] pentru elementele din linia a doua a tabloului, respectiv dmat[9][0], , dmat[9][19] pentru elementele din ultima linie a tabloului.

Initializarea tablourilor
Declararea unui tablou poate fi insotita de initializarea sa, caz in care se foloseste un declarator de forma:
nume_tablou[limita1][limitaN] = valoare_initiala;

unde: valoare_initiala este lista valorilor de initializare ale elementelor sale, separate prin virgule si incluse intre acolade. Expresiile folosite pentru initializare trebuie sa fie constante. Exemple: Declaratorul int vect[5] = {34, 56, 89, 90, 75}; indica un tablou de 5 intregi si initializeaza elementele acestuia cu valorile precizate. Declaratorul int matrix[3][3] = {{1,2,3}, {7,8,9}, {4,2,5}}; indica o matrice si initializeaza elementele lui. Fiecare pereche de acolade din interior indica lista valorilor de initializare ale elementelor unei linii.
Observatii: o Daca dimensiunea tabloului este precizata, iar in partea de initializare sunt mai putine valori decat elementele tabloului, atunci restul elementelor sunt initializate cu 0 (toti bitii sunt pusi pe 0), dar interpretarea depinde de tipul de baza al tabloului. o Daca in lista sunt mai multe valori decat numarul de elemente al tabloului se produce o eroare la compilare (se afiseaza mesajul de eroare Too many initializers). o Un caz aparte este cel al tablourilor de caractere, care se pot initializa cu siruri de caractere incluse intre ghilimele. In acest caz fiecare element al sirului, inclusiv caracterul \0 (terminatorul de sir atasat automat de compilator) initializeaza cate un element al tabloului. De exemplu, char tabCh[] = Buna; este echivalent cu char tabCh[5] = {B,u,n,a,\0};

Cand intr-un program este necesara declararea mai multe tablouri avand aceeasi dimensiune si acelasi tip comun de date este indicat sa se declare mai intai un tip utilizator folosind cuvantul cheie typedef si apoi sa declaram variabile ale tipului respectiv. De exemplu, in locul declaratiei:
double a[40], b[40], c[40];

se pot utiliza urmatoarele declaratii:


typedef double Vector[40]; Vector a, b, c; // Declara tipul utilizator Vector // Declara variabile ale tipului Vector

Structuri
Un alt mod de a grupa datele este acela care ia in considerare prezenta unei relatii de inrudire intre datele care se grupeaza. In acest caz datele respective formeaza o structura. Intr-o structura pot fi incluse date de tipuri diferite, realizand-se astfel o grupare de date neomogene din punct de vedere al tipului. Relatia de legatura dintre datele grupate intr-o structura este definita de utilizator cu scopul de a simplifica manipularea respectivei grupe de date pe timpul prelucrarilor. Unei astfel de structuri i se asociaza un identificator, care va reprezenta de fapt un nume de tip de data definit de utilizator. De asemenea, fiecarei componente a structurii i se asociaza un nume, ce se va folosi la referirea acestora. Un exemplu simplu de structura este data calendaristica, care poate fi considerata ca find o grupa formata din trei date inrudite: zi, luna si an. Aceste trei date nu trebuie sa fie neaparat de acelasi tip. De exemplu, ziua si anul pot fi intregi (tipul int), in timp ce luna poate fi de tip nenumeric (caracter), daca ea se preciza prin denumire. Pentru declararea unei date structurate se foloseste urmatorul format:
struct nume_structura { descriere_membru_1; descriere_membru_2;

descriere_membru_n; };

unde: nume_structura - reprezinta numele tipului de data. descriere_membru_i - reprezinta declaratia componentelor ce fac parte din structura, care poate fi o declaratie de variabila sau o alta declaratie de structura. De exemplu, pentru a defini o structura corespunzatoare datelor calendaristice se poate utiliza o declaratie de forma:
struct data_calendar { unsigned int luna, zi; int an; };

Numele structurii se comporta ca un tip special de declarator, definit de utilizator. El nu impune rezervarea de memorie interna, dar permite declararea ulterioara a unor variabile apartinand acestui tip, variabile carora insa li se va rezerva spatiul de memorie necesar. Pentru a declara variabile apartinand unui tip structura se poate folosi unul din urmatoarele formate:
struct data_calendar data1, data2;

sau
data_calendar data1, data2;

Declararea unor variabile de tip structura se poate face concomitent cu declararea tipului de date astfel:
struct data_calendar { unsigned int luna, zi; int an; } data1, data2;

Memoria ocupata de o variabila de tip structura se determina tinand cont de spatiul necesar stocarii fiecarui membru a structurii si a unor eventuale cerinte de aliniere in memorie. Cand este necesara cunoasterea spatiului ocupat de o variabila de tip structura este indicat sa se foloseasca functia sizeof() pentru a determina acest spatiu, deoarece lungimea structurii poate depasi suma dimensiunilor componentelor. Accesul la un membru al unei variabile de tip structura se face dupa urmatorul model:
variabila.membru

Acest mod de adresare se numeste calificare. De exemplu, data1.luna desemneaza luna, iar data1.zi indica ziua din variabila structurata data1. O variabila de tip structura poate fi declarata si ca tablou; tablourile de structuri sunt asociate in general cu conceptul de fisier in memorie, deoarece un element al tabloului va contine un articol (o inregistrare), iar intregul tablou va reprezenta un fisier. De exemplu, definirea:
struct persoana { char nume[25]; int nota[7]; } elev[21];

aloca spatiul necesar pentru stocarea informatiilor (numele si cel mult 7 note) pentru 21 de elevi. O constructie de forma elev[15].nume desemneaza numele celui de-al 16-elev, in timp ce elev[2].note[1] desemneaza a doua nota a celui de-al treilea elev. Un membru al unei structuri poate fi el insasi o structura. Structurile incluse pot fi descrise fie in interiorul structurii de baza fie inaintea acesteia. De exemplu, structura
struct persoana { char nume[25]; struct domiciliu { char strada[30]; char oras[25]

char judet[2]; } adresa; // numele de membru in structura exterioara char studii[15]; } prof;

poate fi scris si sub forma:


struct domiciliu { char strada[30]; char oras[25] char judet[2]; }; // variabila adresa nu mai trebuie declarata aici struct persoana { char nume[25]; domiciliu adresa; char studii[15]; } prof;

Initializarea structurilor
Declaratia unei variabile de tip structura poate fi insotita si de specificarea unor valori de initializare. Valorile de initializarea a unei structuri se precizeaza printr-o lista de valori de initializarea, separate prin virgule, continand cate o valoare pentru fiecare camp (membru) al structurii. Lista se include intre acolade. Exemplu: Fie structura:
struct persoana { char nume[30]; int etate; long retributie; }

atunci Initializarea unei variabile simple de tip persoana se poate realiza astfel:
struct persoana pers = {Alexandru Ion, 35, 3500}; struct persoana grup[] = {{Ionescu Ion, 45, 4500}, {Vasilescu Vasile, 50, 3500}, {Mihai Marin, 35, 2750} };

Initializarea unui tablou cu date de tip persoana se poate face astfel:

Reuniuni
Limbajul C (C++) pune la dispozitia programatorilor facilitati de a pastra intr-o zona de memorie date de tipuri diferite. Din cele prezentate pana acum a rezultat ca unei date i se aloca o zona de memorie corespunzatoare tipului datei respective si ca in zona respectiva se pot pastra numai date ale acelui tip. De exemplu, daca avem declaratia:
long w;

atunci variabilei w i se aloca 4 bytes (32 biti) si in zona respectiva se vor stoca valori numerice intregi reprezentate prin complement fata de 2. De foarte multe ori intr-o aplicatie este de dorit ca in aceeasi zona de memorie sa putem pastra date de tipuri diferite. De exemplu, sa presupunem ca dupa un timp nu mai avem nevoie de variabila w si dorim ca zona de memorie alocata ei sa o utilizam pentru a stoca date apartinand altui tip: int, char sau chiar float. Reutilizarea unor zone de memorie conduce la economisirea memoriei. Folosind o constructie similara datelor de tip struct, insa inlocuind cuvantul cheie struct cu union putem grupa datele carora li se va aloca aceeasi zona de memorie. O astfel de grupare se numeste reuniune. Formatele precizate in cazul datelor de tip struct sunt valabile si pentru datele de tip union. Tipul introdus prin union este un tip definit de utilizator ca si cel definit de struct. 10

Exemple: 1. Fie declaratia:


union numar { int x; long y; double r; char ch; } nr; atunci nr este o reuniune de tip numar. Componentele reuniunii vor fi referite folosind notatia cu punct, ca si in cazul structurilor, adica sub forma: nr.x, nr.y, nr.r si nr.ch. Ele sunt alocate in

aceeasi zona de memorie si din acest motiv, la un moment dat al executiei programului, numai una dintre aceste date este definita (alocata). In faza de compilare, variabilei nr i se va aloca o zona de memorie egala cu cea mai mare zona dintre cele necesare pastrarii componentelor (in cazul exemplului anterior zona alocata va fi 8 bytes, spatiul necesar pastrarii datelor de tip double). 2. Fie declaratiile:
typedef union { char nume[50]; int nrmat; long cod; } Zona; Zona buf; atunci variabila buf este o reuniune de tip Zona pentru care se vor aloca 50 bytes. In aceasta zona se pot pastra siruri de caractere la care ne vom pute referi sub forma buf.nume, buf.nume[0], buf.nume[6]; sau valori numerice intregi de tip int sau long la care ne putem referi prin buf.nrmat respectiv buf.cod.

La utilizarea reuniunilor se pot ivi unele probleme, deoarece programatorul trebuie sa cunoasca, in fiecare moment al executiei, ce componenta a reuniunii se gaseste in zona alocata ei. Pentru a evita erorile ce pot sa apara pe timpul utilizarii unei reuniuni, este indicat sa se foloseasca un indicator care sa defineasca tipul de data pastrat in fiecare moment in zona alocata reuniunii.
Nota: Reuniunile, spre deosebire de structuri, nu pot fi initializate.

Constante simbolice
Pentru a declara constante cu nume (asa numitele constante simbolice) se foloseste urmatoarea sintaxa:
const [tip_data] nume = valoare

unde:
Nota: tip_data este o constructie optionala care reprezinta tipul constantei; daca este absenta, constanta este considerata ca fiind de tip int. nume - reprezinta numele asociat constantei; valoare - reprezinta valoarea constantei. Cuvantul cheie const este numit modificator, deoarece are rolul de a modifica un enunt care, initial, are un alt sens.

Exemple:
const n = 50; const float pi = 3.1415; const ok = D;

11

Clase de memorie
Clasele de memorie sunt asociate variabilelor pentru a evidentia momentul alocarii, tipul memoriei folosite, durata de viata si domeniul de vizibilitate. In limbajul C++ exista urmatoarele clase de memorie: automatic - corespunde variabilelor comune si se defineste implicit (cand nu este precizata o alta clasa) sau explicit prin cuvantul cheie auto. Variabilele din aceasta clasa se definesc numai in interiorul unui bloc. Nu sunt initializate implicit de catre compilator, deoarece ele sunt alocate pe timpul executiei in memoria dinamica. Variabilele automatice sunt recunoscute doar in blocul unde sunt definite, iar spatiul ocupat de ele este eliberat la iesirea din bloc. Pot fi initializate explicit la definire, dar initializarea se va face la fiecare intrare in blocul respectiv. static - corespunde variabilelor permanente pentru un bloc sau o functie. Ele sunt alocate in memoria statica si la compilare sunt initializate automat cu 0, pastrandu-si continutul intre revenirile succesive in blocul respectiv. Clasa static este implicita pentru unele categorii de date (tablouri cu initializari, variabile globale, siruri de caractere). Pentru a declara in mod explicit variabile statice se foloseste cuvantul cheie static. Cand sunt definite in afara oricarui bloc, variabilele statice au caracter global. Cand sunt definite in interiorul unui bloc, domeniul de vizibilitate al variabilelor se reduce la acel bloc; desi ele exista si dupa iesirea din bloc, ele nu pot fi accesate. Initializarea variabilelor statice se poate realiza si in mod explicit, ca in cazul oricarei variabile; de exemplu, static double t = 5.6; dar acest lucru se face tot o singura data, la compilare. extern - se defineste in raport cu impartirea unui program in mai multe fisiere si functii, compilabile separat. De regula variabilele din aceasta clasa sunt definite intr-un fisier si folosite in alt fisier. Se face distinctie intre definirea si declararea variabilelor din aceasta clasa. Pentru a fi vazute in alte functii, variabilele se definesc obligatoriu in afara oricarei functii. Utilizarea lor inainte de definire presupune ca ele sa fie in prealabil declarate prin cuvantul cheie extern. Aceasta declaratie determina ca identificarea efectiva a variabilei sa se faca in faza de linkeditare, cand sunt reunite toate fisierele. Deci, variabila externa apare intotdeauna in doua ipostaze: la definire, cand nu poarta specificatorul extern si la utilizare, cand este insotita de specificatorul extern. Declaratia extern pentru variabile poate sa apara: o in interiorul unui bloc, caz in care ele sunt recunoscute numai acolo; o in afara oricarei alte functii, caz in care ele sunt recunoscute din acel loc pana la sfarsitul fisierului sursa. register - determina ca variabilele sa fie pastrate in registrele calculatorului. Aceasta clasa se poate asocia variabilelor de tip intreg, caracter sau pointer (adresa). Cuvantul cheie prin care se indica apartenenta la aceasta clasa de memorie este register.

12

Intrari/iesiri standard
Atat in C cat si in C++ se foloseste foarte des notiunea de stream. Stream-ul este un concept abstract, prin care se intelege orice flux de date de la o sursa la o destinatie.

Intrari/iesiri C
Stream-ul poate fi de intrare (sursa este un fisier oarecare iar destinatia este memoria) sau de iesire (sursa este memoria iar destinatia este un fisier oarecare). Exista doua cazuri speciale: cand datele sunt afisate pe monitor se considera ca ele sunt scrise intr-un fisier standard numit stdout; cand datele sunt preluate de la tastatura se considera ca ele sunt citite din fisierul standard stdin. Cele doua fisiere sunt asignate automat oricarui program C. Ele sunt deschise automat la lansarea in executie a programului si sunt inchise automat la terminarea executiei programului. Aceste doua fisiere sunt considerate ca fiind fisiere de caractere (fisiere text) si din acest motiv sunt necesare anumite conversii pentru a adapta formatul extern de reprezentare al datelor la formatul intern si invers. Conversiile sunt efectuate sub controlul unor specificatori de format.

Functia printf
Afisarea pe monitor (scrierea in fisierul stdout) se realizeaza prin utilizarea functiei printf. Prototipul functiei se gaseste in fisierul header stdio.h. Sintaxa apelului functiei este:
printf(format[, lista_argumente]);

unde: format - reprezinta un sir de caractere ce defineste textul si formatul datelor de afisat; lista_argumente - este format din una sau mai multe expresii separate prin virgula, ale caror rezultate se vor afisa. Parametrul format contine atat textul care se va afisa ca atare in pozitiile indicate cat si specificatorii de format ce definesc conversiile datelor din formatul intern in formatul extern. Functia printf executa urmatoarele: Accepta un sir de argumente; Aplica fiecarui argument specificatorul de format continut in argumentul format; Afiseaza pe ecran data in formatul specificat. Un specificator de format are urmatoarea forma:
% [indicatori] [lungime] [.precizie] tip

unde:
Component a indicator Ce controleaza sau specifica Parametru optional. Specifica alinierea, semnul numerelor, prefixele octale si hexazecimale. Ca indicatori se pot folosi urmatoarele caractere: Rezultatul este aliniat la stanga, completandu-se cu spatii la dreapta. In mod prestabilit alinierea se face la dreapta, completandu-se la stanga cu spatii sau zerouri. + Valorile numerice vor fi precedate intodeauna de semnul + sau minus. spatiu Daca valoarea nu este negativa, se lasa un spatiu in locul semnului; iar valorile negative sunt precedate de semnul minus. # Indica faptul ca argumentul este convertit intr-un format alternativ. Caracterele permise in aceasta situatie sunt: csdiu Fara efect 0 Adauga 0 la un argument diferit de zero x or X Adauga 0x (sau 0X) argumentului eEf Rezultatul contine intodeauna marca zecimala, chiar daca dupa ea nu urmeaza cifre diferite de 0. In mod normal, marca zecimala apare in aceste

13

lungime

.precizie

tip

rezultate numai daca dupa ea urmeaza o cifra. gG Acelasi ca si E, dar zerourile de la urma nu sunt eliminate. Parametru optional. El poate fi specificat prin una din urmatoarele procedee: 1. direct, prin intermediul unui sir de cifre zecimale: n Vor fi afisate cel putin n caractere. Daca valoarea de iesire are mai putin de n caractere, este completata cu spatii (la dreapta daca se foloseste indicatorul -, la stanga in caz contrar). 0n Vor fi afisate n caractere. Daca valoarea de iesire are mai putin de n caractere, se va completa la stanga cu zerouri. 2. indirect, prin intermediul unui astersic (*). Daca se foloseste asteriscul drept specificator, urmatorul argument din apel (care trebuie sa fie un intreg) specifica lungimea minima a campului de iesire. Parametru optional. Specifica numarul maxim de caractere pentru valorile numerice intregi si numarul minim de cifre la partea zecimala pentru valorile numerice reale. Precizia prestabilita este urmatoarea: 1 pentru tipurile d, i, o, u, x, X; 6 pentru tipurile e, E, f; toate cifrele semnificative pentru tipurile g, G; primul caracter diferit de null pentru tipurile s; nu are efect pentru tipul c. Valorile acestui argument pot fi: 0 Pentru tipurile d, i, o, u, x, precizia este stabilita la valoarea predefinita, iar pentru tipurile e, E, f nu se afiseaza punctul zecimal; .n Sunt afisate n caractere sau n cifre zecimale. Daca valoarea are mai mult de n caractere, ea poate fi trunchiata sau rotunjita, in functie de parametrul tip;. * Indica faptul ca lista de argumente furnizeaza precizia (la fel ca si in cazul paramterului lungime). Parametru obligatoriu. Specifica conversia de tip (vezi tabelul urmator).

Conversiile ce se realizeaza la afisarea datelor sunt indicate de catre ultima sau ultimile litere ale specificatorului de format (parametrul tip). Literele care se pot utiliza pentru conversie sunt prezentate in tabelul urmator:
Caracterul de tip Numerice d sau i o sau u x X f e g E G Caractere c s % Pointeri p Tipul intrarii Intreg Intreg Intreg Intreg Real Real Real Real Real Caractere Pointer sir fara Pointer Formatul iesirii Intreg in zecimal cu semn Intreg octal fara semn Intreg hexazecimal fara semn (cu a, b, c, d, e, f) Intreg hexazecimal fara semn (cu A, B, C, D, E, F) Valoare cu semn in format [-]dddd.dddd. Valoare cu semn in format [-]d.dddd sau e[+/-]ddd Valoare cu semn in format e sau f, in functie de valoarea si precizia data. Zerourile de la sfarsit si punctul zecimal sunt afisate daca este necesar. Acelasi ca si e; avand E pentru exponent. Acelasi ca si g; cu E pentru exponent daca este folosit formatul e. Un singur caracter Afiseaza caracterele pana la intalnirea unui caracter null sau pana la dimensiunea stabilita. Afiseaza caracterul procent (%) Afiseaza argumentul de intrare ca un pointer; formatul depinde de modelul de memorie folosit; care poate fi XXXX:YYYY sau YYYY (numai offset-ul).

Conventiile care se aplica unora dintre specificatorii de format ai functiei printf sunt urmatoarele: Conversia %e sau %E - argumentul este convertit pentru a corespunde stilului 14

[-] d.ddd...e[+/-]ddd
Observatii: o In fata punctulului zecimal exista intotdeauna o cifra; o Numarul cifrelor de dupa punctul zecimal este egal cu argumentul precizie din specificatorul de format; o Exponentul contine intotdeauna cel putin doua cifre.

Conversia %f - argumentul este convertit la notatia zecimala in stil [-] ddd.ddd..., unde numarul de cifre de dupa punctul zecimal este egal cu argumentul precizie, daca s-a transmis o precizie diferita de zero. Conversia %g sau %G - argumentul este afisat in stil e, E sau f, avand numarul de cifre de dupa punctul zecimal specificat de precizie. Zerourile de la sfarsit sunt eliminate din rezultat, iar punctul zecimal apare numai daca acesta este necesar. Argumentul este afisat in stil e sau f (cu anumite limitari) atunci cand caracterul de conversie este g. Stilul e este utilizat numai daca exponentul rezultat din conversie este fie mai mare decat precizie fie mai mic ca 4. Argumentul este afisat in stil E atunci cand G este caracterul de conversie. Conversia %x sau %X - pentru conversiile x, in iesire apar literele a, b, c, d, e si f. Pentru conversiile X, in iesire apar literele A, B, C, D, E si F. Exemple: 1. Fie declaratia: int x = -12; Atunci: printf(x = %d\n, a); afiseaza: x = -12 si trece la linia urmatoare, deorece in parametru format al apelului de functie s-a folosit secventa escape newline (\n); printf(x=%10.7d\n, x); afiseaza x=bb-0000012 (prin b s-a notat spatiul) si trece la linia urmatoare. printf(x=%3.7d\n, x); afiseaza x=-0000012 (argumentul lungime, 3, este ignorat, deoarece data este de tip intreg) si trece la linia urmatoare; printf(x=%x\n, x); afiseaza x=fff4;
Nota: Numarul x este negativ si in memorie el este reprezentat in cod complementar pe 2 bytes. Pentru a-l reprezenta pe 12 in hexazecimal se efectueaza diferenta dintre 216=1000(16) si 12=C(16) si se obtine astfel FFF4.

printf(x=%X\n, x); afiseaza x=FFF4; printf(x=%u\n, x); afiseaza x=65524;


Nota: Numarul reprezentat in memorie este considerat pozitiv si FFF4(16) convertit in zecimal da 65524 (15 x 163 + 15 x 162 + 15 x 16 + 4).

2. Fie declaratiile:
char a = 12, b = a; unsigned char c = 12, d = -12, e = x;

Atunci: printf(%d %c %d %x\n, a, b, b, b); afiseaza -12 a 97 61. Ultimele doua valori au aparut deoarece am solicitat sa se afiseze variabila b ca valoare zecimala (argumentul %d din specificatorul de format), care este 97 (codul ASCII al literei b) si ca valoare hexazecimala. printf(%d %d %c %d, c, d, e, e); afiseaza 12 244 x 120, fara a trece la linie noua. 12 reprezentat pe un byte are valoarea zecimala egala cu 244, iar litera x are codul zecimal ASCII egal cu 120. 3. Fie declaratiile:
float a=67.87; double b=-98.9; long double c = 32.7; long int d = 100000;

Atunci in urma executarii urmatoarei secvente de instructiuni:


printf(a=%+5.1f \nb=%lf\n, a,b); printf(c=%2Lf\n,c);

15

printf( d=%ld\n,d);

se va afisa:
a=+67.8 b=-98.900 c=32.70 d=100000

4. Fie instructiunea printf(\a un sir\bt); La executie se va emite un sunet scurt (secventa escape \a), se va afisa textul un sir, se va sterge caracterul r (secventa escape \b) si se va afisa in locul lui caracterul t, ramanand afisat textul un sit. 5. Functiile din C++ pot fi apelate atat folosind sintaxa apelului de procedura (cum au fost apelate in exemplele anterioare) cat si utilizand sintaxa apelului de functie. Atunci cand functia printf este apelata ca functie, ea returneaza numarul de caractere scrise. De exemplu, urmatorul program:
#include <stdio.h> void main() { int a = 673 printf(\n%d, printf( }

%d,a));

va afisa:
673 4

prima linie este afisata de apelul printf( %d,a), transmis ca argument, iar dupa executie se afiseaza al doilea rand (4), care reprezinta numarul de caractere afisat.

Functia scanf
Se foloseste pentru a citi din fisierul stdin. Prototipul functie se gaseste in fisierul header stdio.h. Sintaxa apelului de functie este urmatoarea:
scanf(format, lista_argumente);

unde: format - reprezinta un sir de caractere ce contine text si specificatorii de format (asemanatori celor pentru functia printf); lista_argumente - este formata dintr-unul sau mai multe argumente separate prin virgule. Fiecare argument din lista reprezinta adresa zonei de memorie unde se va transfera data citita. Adresa zonei de memorie se specifica, de regula, printr-o constructie de forma:
&nume_zona_memorie

Functia scanf executa urmatoarele operatii: Scaneaza, caracter cu caracter, o succesiune de campuri de intrare1; Formateaza fiecare camp in concordanta cu specificatorul de format corespunzator, transmis prin argumentul format; Transfera data convertita in zona de memorie a carei adresa a fost transmisa in lista_argumente. Un specificator de format are urmatoarea forma simplificata:
% [*] [lungime] [F|N] tip

unde:
Component a *
1

Ce este/ce executa (Optional) Suprima atribuirea urmatorului camp de intrare. Daca in

Intr-o functie scanf, prin camp de intrare se intelege: Toate caracterele pana la primul caracter alb (fara a-l include); Toate caracterele pana la primul care nu poate fi convertit folosind specificatorul de format curent (cum ar fi o cifra 8 sau 9 in formatul octal); Toate cele n caractere, unde n este lungimea specificata a campului.

16

lungime F|N tip Caracteru l de tip Numeric d D e, E f g, G o O i l u U x X Caracter s c W %

specificatorul de format dupa caracterul procent (%) urmeaza un asterisc (*), urmatorul camp de intrare este scanat dar nu este atribuit urmatorului argument de adresa. Data de intrare suprimata este presupusa a fi de tipul specificat de caracterul tip ce urmeaza dupa asterisc. (Optional) Specifica numarul maxim de caractere de citit; pot fi citite mai putine caractere daca functia scanf intalneste un caracter alb2 sau neconvertibil. (Optional) Modifica modul de interpretare implicita a adresei argumentului: N = pointer apropiat; F = pointer indepartat. (Obligatoriu) Caracterul tipului de conversie (vezi tabelul urmator). Intrarea asteptata Intreg zecimal Intreg zecimal Real Real Real Intreg octal Intreg octal Intreg zecimal, octal sau hexazecimal Intreg zecimal, octal sau hexazecimal Intreg zecimal fara semn Intreg zecimal fara semn Intreg hexazecimal Intreg hexazecimal Sir caractere Caracter Tipul argumentului Pointer la un intreg (int *arg) Pointer la un intreg lung (long *arg) Pointer la un real (float *arg) Pointer la un real (float *arg) Pointer la un real (float *arg) Pointer la un intreg (int *arg) Pointer la un intreg lung (long *arg) Pointer la un intreg (int *arg) Pointer la un intreg lung (long *arg) Pointer la un intreg fara semn (unsigned int *arg) Pointer la un intreg lung fara semn (unsigned long *arg) Pointer la un intreg (int *arg) Pointer la un intreg (int *arg) Pointer la un tablou de caractere (char arg[]) Pointer la un caracter (char *arg) daca impreuna cu caracterul de tip este data si o lungime de camp (cum ar fi, %5c). Pointer la un tablou de W caractere (char arg[W]) Nu se executa nici o conversie; se stocheaza caracterul %.

Intr-un specificator de format pentru conversie se pot folosi urmatoarele caractere:

Caracterul %

Pointer (adresa) Format hexazecimal Pointer la un obiect (far* sau near*). %p converteste p YYYY:ZZZZ sau ZZZZ dimensiunea implicita a adresei la modelul memoriei. Nota: Informatiile din tabelul anterior se bazeaza pe presupunerea ca in specificatorul de format nu este inclus decat caracterul tipului de conversie.

Conventiile care se aplica unora dintre specificatorii de format prezentati in tabelul anterior sunt: Conversia unui singur caracter (%c): aceasta specifica citirea caracterului ce urmeaza, inclusiv un caracter alb. Pentru a sari un caracter alb si a se citi urmatorul caracter diferit de caracterul alb, se foloseste %1s; Conversia tabloului de caractere (%[W]c): adresa argumentului este un pointer catre un tablou de caractere (char arg[W]). Tabloul contine W elemente;
2

caracter alb (whitespace) caracterele spatiu ( ), tab-ul orizontal (\t), tab-ul vertical (\v), linie noua(\n) si retur de car (\r) sunt considerate caractere albe, deoarece nu au reprezentare grafica.

17

Conversia unui sir (%s): Adresa argumentului este un pointer catre un tablou de caractere (char arg[]). Marimea tabloului trebuie sa fie de cel putin (n+1) bytes, unde n este lungimea, in caractere, a sirului s. Un spatiu sau un caracter newline termina campul de intrare. Un terminator nul este adaugat automat la sir si stocat ca ultim element in tablou; Conversia realului (%e, %E, %f, %g si %G): Numerele reale din campul de intrare trebuie sa fie conform urmatorului format generic: [+/-] ddddddddd [.] dddd [E|e] [+/-] ddd, unde constructia [element] indica faptul ca elementul este optional, iar ddd reprezinta cifre (zecimale, octale sau hexazecimale). In plus, +INF, -INF, +NAN si -NAN sunt recunoscute ca numere reale (prezenta semnului + sau - si scrierea cu majuscule este obligatorie). Conversia tipurilor fara semn (%d, %i, %o, %x, %D, %I, %O, %X, %c, %n): un pointer catre un caracter fara semn, intreg fara semn sau intreg lung fara semn poate fi utilizat in orice conversie unde este permis un pointer catre un caracter, intreg sau intreg lung. Conversia setului de cautare (%[...], %[^...]): setul de caractere dintre parantezele drepte pot fi inlocuite cu tipul s de caractere. Adresa argumentului este un pointer catre un tablou de caractere (char arg[]). Parantezele drepte incadreaza un set de caractere care definesc sirul de cautare. Daca primul caracter din parantezele drepte este simbolul ^, setul de cautare este inversat pentru a include toate caracterele ASCII exceptand pe cele specificate intre parantezele drepte. Campul de intrare este un sir nedelimitat de caractere albe. Functia scanf citeste campul de intrare corespunzator pana la intalnirea primului caracter care nu apare in setul de cautare (sau in inversul setului de cautare). Exemple: %[abcd] Cauta in campul de intrare unul din caracterele a, b, c si d %[^abcd] Cauta in campul de intrare orice caracter diferit de a, b, c si d Setul de caractere poate fi specificat si sub forma unui domeniu de litere sau numerale, folosind o constructie de forma [prmul-ultimul]. Exemple: Pentru a captura toate cifrele zecimal, puteti defini setul de cautare fie sub forma %[0123456789] fie sub forma %[0-9] Pentru a captura caracterele alfanumerice puteti folosi una din urmatoarele formate: %[A-Z] Captureaza toate literele mari; %[0-9A-Za-z] Captureaza toate cifrele si toate literele; %[A-FT-Z] Captureaza literele mari dintre A si F si dintre T si Z; La stabilirea setului de cautare folosind domenii de caractere se va avea in vedere urmatoarele reguli: Caracterul din fata liniutei de unire (-) trebuie sa fie lexical mai mic decat cel de dupa semn. Liniuta de unire nu trebuie sa fie primul semn sau ultimul in setul de cautare. Caracterele de pe oricare parte a liniutei de unire trebuie sa fie capete ale domeniului si nu parti ale unui alt asemenea domeniu.

Oprirea si trecerea la urmatorul camp de intrare


O functie scanf poate opri scanarea unui camp de intrare particular inainte de intalnirea caracterului final normal (spatiul alb) sau ea se poate termina in intregime. Functia scanf opreste scanarea, stocheaza campul de intrare curent si trece la urmatorul camp de intrare daca se intalneste una din urmatoarele situatii: In specificatorul de format apare un caracter pentru suprimarea atribuirii (*) dupa caracterul %, caz in care campul curent este scanat dar nu este stocat; S-au citit numarul de caractere specificat prin argumentul lungime; S-a intalnit un caracter care nu poate fi convertit sub formatul curent (de exemplu, o litera A cand formatul este zecimal); 18

In campul de intrare s-a intalnit un caracter care nu apare in setul de cautare (sau apare in setul de cautare inversat).
Observatie: Cand functia scanf opreste scanarea campului de intrare curent pentru unul dintre motivele prezentate mai sus, urmatorul caracter este presupus necitit si el poate fi un caracter al urmatorului camp de intrare sau primul caracter dintr-o noua operatie de citire.

Terminarea citirii
Functia scanf se va termina in urmatoarele conditii: Urmatorul caracter din campul de intrare este in conflict cu caractererul corespunzator diferit de spatiul alb din sirul de formatare; Urmatorul caracter din campul de intrare este EOF (sfarsit de fisier); S-a terminta sirul de formatare (argumentul format). Exemple: 1. Fie declaratiile int a, b; Atunci: scanf(%d, &a); citeste un intreg si-l atribuie variabilei a; scanf(%1d, &a); citeste prima cifra a numarului tastat si o atibuie valraibilei a, deci daca se tasteaza 4562, variabila a retine doar 4. scanf(%1d %*1d %1d, &a, &b); citeste prima si a treia cifra a numarului introdus si le atribuie variabilelor a si respectiv b. Astfel daca se tasteaza 357, variabila a va contine 3, iar variabila b va memora 7. Cifra 5 este citita dar nu este memorata. 2. Fie declaratiile: char a, b; Atunci: Secventa:
scanf(%c, &a); printf(%c %d\n, a, a);

va afisa:
a 97

daca s-a introdus caracterul a; si


0 48

daca s-a introdus 0. Functia printf afiseaza de doua ori variabila a, prima data caracterul, iar a doua oara codul caracterului introdus. scanf(%c %c, &a, %b); Sa consideram ca se tasteaza caracterele obbbbbj, unde prin b s-a notat prezenta unui spatiu. Dupa executarea citirii variabila a va retine caracterul o, iar variabila b va retine caracterul j. Spatiile din campul de intrare sunt sarite, deoarece cele doua formate de citire sunt separate prin spatiu (caracter alb) si la citire se sar toate spatiile intalnite. scanf(%c%c, &a, &b); Presupunand ca se introduc caracterele obbbbbj, atunci a va retine caracterul o, iar b va retine caracterul (spatiu). Cele doua formate de citire nu sunt separate printr-un spatiu, si deci primul caracter spatiu din campul de intrare este retinut; celelalte caractere din campul de intrare sunt ignorate. scanf(1%c, &a); Daca, se tasteaza 12, atunci a va retine cifra 2 (caracterul 1 este cel asteptat si este sarit), iar daca se introduce 21, atunci a nu se citeste, intrucat primul caracter asteptat este 1 si intrarea contine 2. 3. Fie decalaratiile float a; double b; long double c; Atunci: scanf(%f %lf %LF,&a, &b, &c); Daca se tasteaza 2.5 2.7 4.3, a va retine 2.5, b va retine 2.7 si c va retine 4.3. scanf(%f%lf,&a, &b); Daca se introduc valorile 2 si 3, atunci a retine 2.0 si b retine 3.0; scanf(%3f,&a); Daca se tasteaza 1.25, a retine 1.2; scanf(%f 1 %lf, &a, &b); Daca se tasteaza valorile 1.2 1 -1.7, a retine 1.2 si b retine 1.7; iar daca se tasteaza 1.2 7.8, atunci se citeste numai a. 19

Alte functii de intrare/iesire Functia getchar


Functia getchar() are rolul de a citi un caracter din fisierul stdin si are prototipul in fisierul stdio.h. Functia se apeleaza folosind sintaxa: getchar(); Citirea propriu-zisa a caracterului are loc dupa apasarea tastei Enter. Functia getchar returneaza caracterul citit, dupa convertirea acestuia intr-un intreg fara semn.

Functia putchar
Functia putchar Are rolul de a scrie un caracter in fisierul stdout. Functia are prototipul in fisierul stdio.h si returneaza o valoare de tip intreg care reprezinta caracterul scris. Sintaxa de apelare a functiei este: putchar(variabila);

Functia clreol
Functia clreol are prototipul in fisierul header conio.h si are rolul de sterge caracterele ce se gasesc pe linia curenta (linia in care se afla cursorul) incepand din pozitia cursorului pana la sfarsitul liniei. Dupa stergere, pozitia cursorului nu se modifica. Functia nu returneaza nici o valoare. Sintaxa apelului functiei este: clreol();

Functia clrscr
Functia clrscr are prototipul in fisierul header conio.h si se foloseste pentru a sterge ecranul. Dupa stergere, cursorul se pozitioneaza in coltul din stanga sus (in prima coloana a primei linii punctul de coordonate 1,1). Functia nu returneaza nici o valoare. Sintaxa apelului functiei este:
clrscr();

Functia gotoxy
Functia gotoxy are prototipul in fisierul header conio.h si are permite pozitionarea cursorului in punctul de coordonate specificat prin argumente (primul argument reprezinta coloana, iar al doilea indica linia). Daca coordonatele sunt incorecte (in afara ferestrei), functia nu realizeaza nimic. Functia nu returneaza nici o valoare. Sintaxa apelului functiei este:
gotoxy(coloana, linie);

Functiile wherex si wherey


Functiile wherex si wherey au prototipul in fisierul header stdio.h. Functia wherex returneaza o valoare intreaga din intervalul 1 la 80 reprezintand coordonata x (coloana) a pozitiei curente a cursorului (din fereastra curenta de text), iar functia wherey returneaza o valoare intreaga din intervalul 1 la 25, 1 la 43 sau 1 la 50 (in functie de parametrii stabiliti ai ecranului) care reprezinta coordonata y (linia) a pozitiei curente a cursorului. Sintaxa de apel a functiilor este urmatoarea:
wherex() wherey()

Functiile getch si getche


Prototipurile functiilor getch si getche se gasesc in fisierul header conio.h. Functiile au rolul de a citi un caracter introdus de la tastatura. Caracterul este citit imediat ce a fost tastat (nu se asteapta apasarea tastei Enter). Deosebirea dintre ele contsa in faptul ca functia getch citeste caracterul fara ecou (caracterul tastat nu apare pe ecran), in timp ce functia getche citeste caracterul cu ecou. Sintaxa de apel a functiilor este:
getch() getche()

20

Intrari/iesiri C++
Limbajul C++, la fel ca si limbajul C, nu are instructiuni specifice operatiilor de intrare/iesire. In limbajul C, astfel de operatii se realizeaza cu ajutorul unui set de functii din biblioteca standard a sistemului, asa cum am vazut mai inainte. Aceste functii pot fi utilizate in acelasi mod si in programele scrise in C++. In afara acestor functii, biblioteca standard a limbajului C++ ofera posibilitatea de a folosi stream-urile standard cin (console input) si cout (console output), pentru efectuarea operatiilor de intrare/iesire. Prototipurile acestor stream-uri se gasesc in fisierul header iostream.h.

Iesirea standard
Iesirile standard se pot realiza folosind operatorul <<, numit si operator de inserare. Acest operator a fost supraincarcat pentru toate tipurile predefinite de date. In mod normal, operatorul << este operatorul de deplasare la stanga. Operatorii de deplasare au prioritatea imediat mai mica decat operatorii binari aditivi (+ si -) si imediat mai mare decat operatorii relationali. Operatorii de deplasare se asociaza de la dreapta spre stanga. Aceste reguli raman valabile si pentru operatorul de inserare. Deoarece la supraincarcarea operatorului de inserare se returneaza o referinta la obiectul curent, operatorii de inserare se pot aplica inlantuiti. Exemplu:
#include <iostream.h> #include <stdio.h> void main() { int x = 5; char sir[] = Testare cout; cout << x = << x << << sir << \n; // Are acelasi efect ca si urmatoarea instructiune din C printf(x = %d c% \n, x, sir); }

Intrari standard
Pentru operatiile de intrare se poate utiliza operatorul >>, numit si operator de extragere. Operatorul >> este de fapt operatorul de deplasare la dreapta care a fost supraincarcat pentru operatii de intrare. In mod implicit, operatorul de extragere realizeaza avansul peste caracterele albe pana la primul caracter deferit de un caracter alb. Caracterele citite trebuie sa corespunda tipului de data care formeaza operandul din dreapta operatorului de extragere. In caz contrar, se intra in starea de eroare. Exemplu:
#include <stdio.h> #include <iostream.h> void main() { int x; char sir[10]; cout << "Introduceti un intreg "; cin >> x; cout << "Ati introdus: " << x << endl; printf("Introduceti un intreg: "); scanf("%d", &x); printf("S-a introdus: %d \n", x); cout << Introducet un cauvant: ; cin >> sir; cout << S-a introdus: << sir << endl; printf(Tastati un cuvant: ); scanf(%C, sir); printf(S-a introdus sirul %c\n, sir); }

21

Utilizarea mediului Visual C++


Pentru a utiliza mediul de programare Microsoft Visual C++ la scrierea si testare programelor C (C++) este necesar sa creati un spatiu de lucru si un nou proiect. Operatiile ce trebuie executate pentru crearea si testare unui nou proiect sunt urmatoarele: 1. Se lanseaza in executie mediul de dezvoltare Microsoft Visual C++. Ecranul de deschidere al aplicatiei arata astfel:

2. Se descide meniul File si se selecteaza optiunea New sau se foloseste combinatia de taste Ctrl+N. Pe ecran se va afisa caseta de dialog New. 3. In caseta de text Project name se tasteaza numele proiectului. 4. In caseta de text Location se tasteaza calea dosarului in care se vor salva fisierele proiectului. Stabilirea dosarului in care se va salva proiectului se poate realiza si prin utilizarea butonului cu puncte de suspensie din dreapta casetei de text Location. Executarea unui clic pe acest buton determina afisarea casetei de dialog Change Directory in care folosind caseta combinata Drives se stabileste unitatea de disc pe care se gaseste dosarul si apoi cu caseta combinata Directory name se selecteaza dosarul necesar. Dupa ce s-a stabilit unitatea de disc si dosarul in care se vor salva fisierele proiectului se executa clic pe butonul OK. 5. In pagina Projects a casetei de dialog New se selecteaza tipul de proiect ce se va realiza. Pentru a crea o aplicatie care se ruleaza in linie de comanda (similar aplicatiilor DOS) se selecteaza optiunea Win32 Console Application. Dupa executarea operatiilor prezentate pana acum caseta de dialog New va arata ca in figura urmatoare. 6. Se executa clic pe butonul OK. 7. In caseta de dialog Win32 Console Application care apare pe ecran se selecteaza, daca este necesar, optiunea An empty project si apoi se executa clic pe Finish.

22

8. In caseta de dialog New Project Information, care afiseaza informatiile legate de noul proiect, se executa clic pe butonul OK.

Ecranul mediului Microsoft Visual C++ va arata astfel:

Dupa crearea unui proiect se poate trece la adaugarea codului sursa necesar proiectului. Pentru a crea fisierul ce va contine codul sursa se procedeaza astfel: 1. Se deschide meniul File si se selecteaza optiunea New sau se foloseste combinatia de taste Ctrl+N. Pe ecran se va afisa caseta de dialog New avand activata pagina Files. 2. Se selecteaza optiunea corespunzatoare tipului de fisier ce se va crea. Pentru a crea un fisier sursa se alege optiunea C++ Source File, se tasteaza numele fisierului in caseta de text File name si apoi se executa clic pe butonul OK. 3. In fereastra de editare se tasteaza continutul fisierului sursa. 4. Dupa introducerea codului se salveaza fisierul (deschizand meniul File si selectand optiunea Save sau folosind combinatia de taste Ctrl+S). Daca proiectul necesita si alte fisiere (sursa sau alte tipuri), se repeta algoritmul prezentat. Dupa adaugarea tuturor fisierelor necesare proiectului, se poate incerca construirea proiectului. Construirea implica compilarea si linkeditarea lui intr-un executabil. Operatiile de construire a proiectului sunt urmatoarele: 1. Se deschide meniul Build si se selecteaza optiunea Build <nume proiect> sau se apasa tasta functionala F7 sau se executa clic pe butonul Build Build MiniBar. din bara instrumentele

23

2. Daca in proiect sunt erori, mesajele de eroare ale compilatorului apar in fereastra inferioara. Executati dublu-clic pe mesajul de eroare si linia ce contine eroarea va fi indicata in fereastra codului.

3. Se corecteaza eroare si se reconstruieste proiectul. Aceste operatii se vor repeta pana cand numai exista erori. Dupa ce au fost eliminate toate erorile de compilare si de linkeditare se poate trece la testarea proiectului. Pentru a lansa in executie proiectul se deschide meniul Build si se selecteaza optiunea Execute <nume proiect> sau se foloseste combinatia de taste Ctrl+F5 sau se executa clic pe butonul Execute program .

24

Operatorii si instructiunile limbajului C++


Operatorii limbajului C++
Limbajul C++ are un set puternic de operatori. In tabelul urmator sunt prezentati toti operatorii limbajului C++, in ordinea prioritatii acestora, impartiti in 16 categorii (categoria 1 are cea mai mare prioritate).
Nr.crt . 1. Categorie Operato
() [] -> :: . ! ~ + ++ -& * sizeof new delete .* ->* * / % + << >> < <= > >= == != & ^ | && || ?: = *= /= %= += -= &=

Ce este (sau ce face) Apel de functie Indici de tablou Selector indirect de componenta C++ Accesul/rezolutia domeniului C++ Selector direct de componenta C++ Negare logica (NOT) Complementul fata de 1 Plus unar (nu are nici un efect) Minus unar (schimbare de semn) Preincrementare sau postincrementare Predecrementare sau postdecrementare Adresa Indirectare (returneaza dimensiunea, in bytes, a operandului) (aloca memoria dinamica) (dealoca memoria dinamica) Dereferentiere Dereferentiere Multiplicare Impartire Restul impartirii intregi Adunare Scadere Deplasare stanga Deplasare dreapta Mai mic ca Mai mic ca sau egal cu Mai mare ca Mai mare ca sau egal cu Egal cu Nu este egal cu (diferit de) SI logic pe bit SAU exclusiv pe bit SAU logic pe bit SI logic SAU logic (a ? x : y inseamna "daca a atunci x, altfel y") Atribuire simpla Atribuirea produsului Atribuirea catului impartirii Atribuirea restului impartiri intregi Atribuirea sumei Atribuirea diferentei Atribuire SI logic pe biti

Prioritate maxima

2.

Unar

3. 4. 5. 6 7. 8. 9, 10. 11. 12. 13. 14. 15.

Accesarea membrilor Multiplicativ Aditiv Deplasare Relational Egalitate

Conditional Atribuire

25

16. Nota:

Atribuire SAU exclusiv pe biti Atribuire SAU logic pe biti Atribuirea deplasarii stanga Atribuirea deplasarii dreapta Virgula Evaluare Operatorii unari (categoria 2), conditionali (categoria 14) si de atribuire (categoria15) se asociaza de la dreapta la stanga; toti ceilalti operatori se asociaza de la stanga spre dreapta.

^= |= <<= >>= ,

In continuare se vor detalia o parte dintre acesti operatori.

Operatori aritmetici
Operatiile aritmetice obisnuite adunare, scadere, inmultire si impartire sunt codificate in limbajul C++, ca de altfel in orice alt limbaj de programare, prin operatorii: + adunare - scadere * inmultire / impartire dintre care primii doi pot sa apara si ca operatori unari. Un alt operator aritmetic in C++ este %, care reprezinta restul impartirii a doi intregi. Desi regula de asociativitate pentru operatorii aritmetici binari de aceeasi prioritate este de la stanga spre dreapta, aceasta nu inseamna ca cei doi operanzi sunt neaparat evaluati si ei in aceasta ordine. Astfel, daca cei doi operanzi sunt expresiile e1 si e2, expresia e2 poate fi evaluata inaintea expresiei e1. Acest lucru poate duce la efecte nedorite daca e1 are efecte colaterale pe care se conta in evaluarea expresiei e2.
Sugestie: In toate situatiile in care ordinea de evaluare a operanzilor este importanta, se va recurge la utilizarea unor variabile auxiliare, Observatii: Daca operanzii legati printr-un operator aritmetic sunt de tip diferit, inainte de a se executa operatia se converteste unul dintre operanzi catre tipul celuilalt, care poate retine rezultatul. De exemplu, daca un operand intreg este inmultit cu un operand real, atunci inainte de a se executa inmultirea operandul intreg este convertit la real. Operatorul / (impartire) actioneaza in mod diferit in functie de tipul operanzilor: Daca ambii sunt de tip intreg, rezultatul impartirii este de tip intreg si are semnificatia de impartire intreaga; Daca cel putin unul dintre operanzi este unul din tipurile reale, rezultatul este real (se efectueaza impartirea obisnuita). Operatorul % actioneaza numai asupra operanzilor de tip intreg. Pentru a schimba ordinea de executie a operatiilor (prioritatea) se pot utiliza parantezele rotunde.

Operatori de incrementare/decrementare
Sunt operatori unari si au rolul de a incrementa (aduna 1) sau decrementa (scade 1) continutul unei variabile. Operatorii de incrementare/decrementare din C++ sunt: ++ pentru incrementare; -- (doua semne -) pentru decrementare. Acesti operatori pot fi prefixati (aplicati in fata operandului) sau postfixati (aplicati dupa operand). In functie de pozitia operatorului fata de operand, incrementarea/decrementarea se executa inainte ca valoarea retinuta de variabila sa intre in calcul (pentru operatorii prefixati) sau dupa ce valoarea retinuta de variabila intra in calcul (pentru operatorii postfixati). Exemple:

26

Fie a o variabila de tip intreg (int) care are valoarea 5. In urma evaluarii expresiei 2+a++ se obtine valoarea 7, iar dupa evaluare a va retine valoarea 6 (operator de incrementare postfixat). Daca a si b sunt variabile de tip int care retin valorile 2 si respectiv 6, atunci expresia + +a*b++ produce valoarea 18, iar variabilele a si b retin valorile 3 si respectiv 7, dar incrementarea lor se va produce in momente diferite. Rezultatul evaluarii este 18 deoarece operatia se executa astfel: se incrementeaza variabila a (a retine 3), se evalueaza expresia (3 ori 6 fac 18) si apoi se incrementeaza variabila b (retine 7).

Operatorii relationali si logici


Este aproape imposibil ca in cazul rezolvarii unei probleme sa nu fie necesara testarea uneia sau mai multor conditii. Acestea reflecta relatiile dintre doua entitati, ale caror valori sunt comparate. Operatorii relationali sunt impartiti in doua clase de precedenta (prioritate): Operatori relationali propriu-zisi: < mai mic <= mai mic sau egal > mai mare >= mai mare sau egal Operatori de egalitate: == egal cu != diferit de In principal rezultatul unei comparatii este o valoare de tip logic adevarat sau fals. In limbajul C++ nu exista tipul de data logic si de aceea o valoare intreaga 0 este interpretata ca fals, in timp ce orice valoare diferita de 0 este interpretata ca adevarat. In cazul particular al comparatiilor, valoarea adevarat se codifica prin 1.
Atentie: Una dintre greselile frecvente in programarea C++, care nu este intodeauna usor observabila, consta in folosirea operatorului de atribuire (=) in locul celui de egalitate (==). Aceasta greseala poate avea consecinte neplacute, datorita efectelor colaterale pe care le poate provoca. Astfel, daca in loc de a == b se scrie a = b, rezultatul va fi intotdeauna adevarat, iar valoarea variabilei a va fi alterata.

Operatorii logici admisi de limbajul C++ sunt: ! negare logica; && SI logic (conjunctie); || SAU logic (disjunctie). Datorita conventiei referitoare la codificarea valorilor logice prin valori intregi, operanzii asupra carora se aplica operatorii logici pot fi orice expresie cu rezultat intreg; expresiile relationale reprezentand deci una dintre posibilitati. Rezultatul operatiilor logice este tot de tipul intreg, stabilit conform tabelului urmator.
x == 0 != 0 y == 0 != 0 == 0 != 0 x && y 0 0 0 1 x || y 0 1 1 1 !x 1 0

Anumite conditii pot fi exprimate in diferite variante, cu sau fara utilizarea operatorilor relationali. Astfel: conditia nul poate fi codificata prin x == 0 sau !x; conditia x, y si z diferiti de 0 poate fi codificata x != 0 && y != 0 && z != 0 sau x && y && z.
Observatie: Evaluarea expresiilor logice in programele C++ este intrerupta in momentul in care valoarea rezultatului devine certa, ceea ce poate conduce, in anumite situatii, la efecte colaterale nedorite. De exemplu, in cazul unei expresii logice formata din trei

27

expresii relationale succesive legate prin operatorul && (conjunctie), daca rezultatul evaluarii primei expresii relationale este fals, rezultatul expresiei logice va fi cert fals si ca urmare celelalte expresii nu mai sunt evaluate.

Operatori de atribuire
In limbajul C++, spre deosebire de alte limbaje de programare, exista mai multi operatori de atribuire. Semnificatia atribuirii este aceeasi in toate limbajele de programare: prelucrare al carui efect consta in modificarea valorii unei entitati, prin depunerea unei noi valori in zona de memorie alocata entitatii respective. Pentru a realiza aceasta prelucrare sunt necesare doua informatii: adresa zonei de memorie afectate si valoarea de memorat. Cele mai multe limbaje de programare pun accentul exclusiv pe efectul colateral al atribuirii, aceasta fiind tratata ca instructiune de baza. Spre deosebire de acestea, limbajul C++ trateaza atribuirea ca operatie cu efect colateral, dar care furnizeaza si un rezultat ce poate fi folosit in operatiile ulterioare. De exemplu, cunoscand dimensiunea laturii unui cub (notata cu, dCub), aria unei fete si volumul cubului (notate cu aria si volum) pot fi calculate intr-un program C++ folosind o instructiune de forma:
volum = dCub * (aria = dCub * dCub);

al carui efect este identic cu cel ar urmatoarei secvente de instructiuni (modalitate specifica celorlalte limbaje de programare):
aria = dCub * dCub; volum = dCub * aria;

In afara operatorului de atribuire clasic (semnul egal =), in limbajul C++ au fost introdusi si operatori de atribuire compusi. Rolul acestora este de a permite descrierea mai compacta a atribuirilor de forma:
variabila1 = variabila1 operator variabila2

utilizand numai informatiile strict necesare, sub forma:


variabila1 operator= variabila2

De exemplu, fiind declarate doua variabile, numite suma si element, atunci operatia de atribuire:
suma = suma + element;

poate fi scrisa sub forma prescurtata astfel:


suma += element;

Daca suma are valoarea 10 si element valoarea 15, dupa executarea acestei instructiuni, suma va retine valoarea 25. Operatorii compusi din limbajul C++ sunt urmatorii: += in operandul din stanga se cumuleaza valoarea expresiei din dreapta; -= din valoarea operandului din stanga se scade valoarea expresiei din dreapta; *= valoarea operandului din stanga este inmultita cu cea a expresiei din dreapta si rezultatul se memoreaza in operandul din stanga; /= in variabila din stanga se pastreaza catul impartirii rezultatului expresiei din dreapta la valoarea initiala a variabilei din stanga; %= in variabila din stanga se pastreaza restul impartirii intregi a rezultatului expresiei din dreapta la valoarea initiala a variabilei din stanga.

Operatori logici pe biti


Toate valorile prelucrate pe parcursul executiei unui program sunt reprezentate in calculator prin succesiuni de biti (cifrele binare 0 si 1). Spre deosebire de operatiile discutate pana acum, operatiile la nivel de bit se aplica, asa cum arata si denumirea lor, asupra fiecarui bit din reprezentarea operanzilor, tratandu-l independent de valorile celorlalti biti. Operatorii la nivel de bit (care admit numai operanzi de tip intreg) sunt urmatorii: ~ negare & SI logic 28

^ SAU EXCUSIV | SAU << deplasare stanga >> deplasare dreapta. Rezultatul aplicarii primilor 4 operatori din lista de mai sus este prezentat in tabelul urmator:
b1 0 0 1 1 b2 0 1 0 1 ~a 1 1 0 0 b1 & b2 0 0 0 1 b1 ^ b2 0 1 1 0 b1 | b2 0 1 1 1

In cazul operatorilor de deplasare, cei doi operanzi au roluri diferite: primul operand este cel al carui biti sunt deplasati, iar cel de-al doilea operand indica numarul de biti cu care se face deplasarea. Deci numai primul operand este prelucrat la nivel de bit. La deplasarea spre stanga cu un bit, bitul cel mai semnificativ se pierde, iar cel mai putin semnificativ se completeaza cu 0. Rezulta ca, exceptand cazul producerii depasirii, deplasarea la stanga cu n biti este echivalenta cu inmultirea cu 2n. De exemplu, daca variabila a este de tip intreg si are valoare 15 (in binar 0000000000001111), atunci dupa deplasare spre stanga cu 2 pozitii variabila a va memora valoarea 60 (in binar 0000000000111100). In cazul deplasarii spre dreapta cu un bit, bitul cel mai putin semnificativ se pierde, iar bitul cel mai semnificativ se completeaza cu 0; deci similar impartirii intregi la 2.

Operatorul conditional
Operatorul conditional este un operator ternar (cu trei operanzi). El se foloseste in cazurile in care exista doua variante de obtinere a unui rezultat, dintre care se va alege una singura, in functie de indeplinirea sau nu a unei conditii. Cei trei operanzi, reprezentand conditia testata si cele doua alternative de obtinerea rezultatului, se codifica prin expresii. Sintaxa expresiei conditionale este urmatoarea:
conditie ? varianta1 : varianta2

Rezultatul unei asemenea expresii se stabileste astfel: daca conditie este diferita de 0 (adevarat) atunci rezultatul final este dat de varianta1, in caz contrar rezultatul final este date de varianta2. De exemplu, pentru a obtine valoarea maxima dintre cele ale intregilor a, b si c, se poate folosi instructiunea:
max = a > b ? (a > c ? a : c) : (b > c ? b : c);

iar pentru a afisa relatia dintre valorile a si b se poate folosi instructiunea:


print(d %c %d\n, a , a > b ? >: a < b ? < : =, b);

Operatorul virgula (,)


Operatorul virgula (numit si operator de secventiere) se foloseste atunci cand sintaxa limbajului impune prezenta unei singure expresii, dar prelucrarea necesara presupune evaluarea a doua sau mai multe expresii. Expresiile separate prin operatorul virgula sunt evaluate de la stanga spre dreapta, rezultatul final fiind cel al ultimei expresii. Astfel, expresia
a < b ? (t = b, b = a, a = t) : a are dublu efect: valorile variabilelor a si b sunt ordonate descrescator si ca rezultat se obtine

cea mai mare dintre cele doua valori.

Operatorul de conversie explicita


Un aspect foarte important in ceea ce priveste evaluarea expresiilor se refera la conversiile de tip impuse de structura unei expresii. Cauzele care determina efectuarea conversiilor de tip sunt urmatoarele: 29

Din ratiuni de reducere a variantelor de implementare a operatiilor, operanzii de tipuri intregi mai slabe decat int sunt convertiti la tipul int sau unsigned int, iar cei de tip float la tipul double; Marea majoritate a operatorilor necesita operanzi de acelasi tip. Daca operanzii unei atribuiri nu sunt de acelasi tip, atunci valoarea atribuita trebuie convertita la tipul variabilei careia i se atribuie. Conversia poate fi realizata de compilator (conversie implicita) sau poate fi impusa de programator (conversie explicita). Conversia explicita se indica folosind operatorul conversiei explicite de tip, denumit si cast. O conversie explicita de tip se indica folosind urmatoarea sintaxa:
(declaratie_tip) expresie

Exemple: Fie declaratia: float x=-1.9; atunci (int) x va avea ca rezultat 1. Fie declaratiile: int a=3, b=6; atunci (float) a/b va avea ca rezultat 0.5.

Operatorul sizeof
Rezultatul aplicarii acestui operator unar asupra operandului su este un intreg care specifica spatiul de memorie, exprimat in bytes, necesar stocarii oricarei valori de acelasi tip cu cea a operandului. Deci, operatorul sizeof prelucreaza tipuri (predefinite sau derivate), spre deosebire de ceilalti operatori, care prelucreaza valori. Operatorul sizeof poate fi aplicat direct asupra unui tip, sub forma sizeof(declaratie_tip) sau asupra tipului unei expresii, sub forma
sizeof expresie

Deoarece tipul operatorului este determinat inca din etapa de compilare, sizeof este un operator cu efect la compilare. Aceasta inseamna ca operandul asupra caruia se aplica sizeof nu este evaluat, chiar daca este reprezentat de o expresie. Urmatorul program poate fi utilizat pentru a afla particularitatile de reprezentare, in functie de implementarea C++ utilizata, pentru cateva dintre tipurile predefinite:
#include <stdio.h> void main() { printf( char short int long float double\n%3d%6d%6d%6d%6d%6d\n, sizeof(char), sizeof(short), sizeof(int), sizeof(long), sizeof(float), sizeof(double)); }

Instructiunile limbajului C++


Instructiunea expresie
Se numeste expresie o succesiune de operatori si operanzi legati intre ei, dupa reguli specifice limbajului, in scopul efectuarii unor operatii (calcule, atribuiri, apelari de functii etc.). Ca operanzi intr-o expresie se pot utiliza: constante, variabile si apeluri de functii. Exista putine limbaje de programare care dispun de o instructiune expresie. Limbajul C++ permite scrierea unei instructiuni de forma: 8+7; care este de fapt o expresie. Ea se evalueaza, chiar daca rezultatul ei nu se foloseste in nici un fel. O consecinta importanta a acestei posibilitati o constituie faptul ca o functie poate fi utilizata ca o procedura (de exemplu, utilizarea functiilor de I/E scanf si printf). Instructiunea expresie are urmatoarea forma generala: expresie; unde expresia este evaluata si are efect colateral, deci este reprezentata de regula de o atribuire sau un apel de functie, ca in urmatoarele exemple:
n = a > b ? a: b; m++;

30

Observatie: Expresiile pot fi utilizate si independent, caz in care ele nu sunt terminate cu caracterul punct si virgula (;). De exemplu, printf(x=%d, x * 2); va afisa valoarea variabilei x multiplicata cu 2.

Instructiunea vida
In eleborarea unui program C++ se pot intalni si situatii in care sintaxa impune aparitia unei instructiuni, dar pentru rezolvarea problemei nu este necesara o prelucrare. In astfel de cazuri se foloseste instructiunea vida, care are forma: ; (caracterul punct si virgula).

Instructiunea compusa (bloc)


Pe parcursul elaborarii programelor intervin numeroase situatii in care sintaxa limbajului impune prezenta unei singure instructiuni, insa prelucrarile necesare impune prezenta mai multor instructiuni. Rezolvarea acestor situatii se realizeaza incadrand respectiva secventa de instructiuni intre acolade, obtinandu-se astfel o structura denumita instructiune compusa sau bloc. In cazul general, un bloc poate sa contina pe langa instructiuni si definitii de variabile locale blocului si alte declaratii. Structura generala a unui bloc este:
{ declaratii_si_definitii instructiuni }

Instructiuni de selectie
In majoritatea limbajelor de programare exista instructiuni care permit alegerea unei alternative din doua sau mai multe posibile in functie de anumite conditii. In limbajul C++ exista doua instructiuni care permit acest lucru: Instructiunea de selectie simpla, numita si instructiune decizionala; Instructiunea de selectie multipla.

Instructiunea de selectie simpla


Practic, nu exista problema reala in care sa nu apara necesitatea de a opta pentru o alternativa de prelucrare dintre doua posibile, alegere exprimata literar sub forma: daca este indeplinita conditia impusa, atunci executa prima alternativa, altfel executa a doua alternativa. In orice limbaj de programare, o asemenea structura se codifica folosind o instructiune decizionala. In limbajul C++ instructiunea decizionala are urmatorul format general (parantezele drepte indica faptul ca respectiva constructie poate fi omisa):
if (expresie) instructiune1; [else instructiune2;]

Principiul de executie este urmatorul: Se evalueaza expresia; Daca valoarea produsa este diferita de 0, se executa instructiune1; Daca valoarea produsa este 0 si exista clauza else, se executa instructiune2; in caz contrar nu se executa nimic. Dupa executarea instructiunii instructiune1 sau instructiune2 , se trece la prima instructiune ce urmeaza dupa if. Din format, rezulta ca instructiunea if admite doar o instructiune pentru oricare dintre cele doua alternative, dar ea poate fi si o instructiune compusa. Exemplu: Sa se scrie un program care valideaza data calendaristica introdusa sub forma ll/zz/aaaa (luna/zi/an), tinand cont ca anul poate fi o valoare intre 1900 si 2200. 31

#include <iostream.h> int zi, luna, an, ok = 1; int nrZile[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; void main() { cout << "Se introduce data calendaristica (ll/zz/aaaa):\n"; cout << Luna: ; cin >> luna; cout << Ziua: ; cin >> zi; cout << Anul: ; cin >> an; if(an < 1900 || an > 2200) { cout << "Anul nu este in intervalul [1900, 2200]"; ok = 0; // data calendaristica incorecta } if(luna < 1 || luna > 12) { cout << "Luna = << luna << este eronata\n"; ok = 0; // data calendaristica incorecta } if(luna = 2) nrZile[2] += 1; if(zi < 1 || zi > nrZile[luna] + (luna == 2 && (an % 4 == 0 && an % 100 != 0 || an % 400 == 0) ) { cout << "Ziua = << ziua << este eronata\n"); ok = 0; // data calendaristica incorecta } if(ok) { cout << "Data calendaristica introdusa este corecta:\n"; cout << luna << / << ziua << / << an << endl; } } Observatii: Pentru a adauga o zi la numarul de zile ale lunii februarie daca anul este bisect se foloseste expresia: (luna == 2 && (an % 4 == 0 && an % 100 != 0 || an % 400 == 0))) care va furniza o valoare egala cu 1 daca luna este egala cu 2 si anul este bisect; in caz contrar va furniza 0. Anul este bisect daca este divizibil prin 4 si nu este divizibil prin 100 sau este divizibil prin 400.

Instructiunea de selectie multipla


In structiunea switch permite realizarea unei structuri de selectie multipla (o generalizare a structurii alternative). Sintaxa instructiunii switch este urmatoarea
switch(expresie) { case caz1: instructiuni_1; break; case caz2: instructiuni_2; break; case cazN: instructiuni_N; break; [default: instructiuni;] }

unde:
expresie reprezinta expresia de selectare a alternativei ce se va executa; caz1, caz2, , cazN - sunt constante; instructiuni_1, instructiuni_2, , instructiuni_n, instructiuni - sunt succesiuni de

instructiuni, numite si alternative; 32

Principiu de executie: P1. Se evalueaza expresie; P2. Rezultatul expresiei se compara pe rand cu valorile constantelor caz1, caz2, , cazN: o Daca valoarea rezultatului expresiei expresie corespunde cu una dintre constantele caz1, caz2, , cazn, se executa succesiunea de instructiuni (alternativa) corespunzatoare acelei constante si apoi se trece la prima instructiune ce urmeaza dupa switch; o Daca valoarea rzeultatului expresiei expresie nu corespunde nici uneia dintre constantele caz1, caz2, , cazn, se executa instructiunea corespunzatoare clauzei default, daca exista, si apoi se trece la prima instructiune ce urmeaza dupa switch;
Observatie: Instructiunea break de la sfarsitul alternativelor nu este obligatorie. Daca dintr-o alternativa lipseste instructiunea break, atunci dupa executia succesiunii de instructiuni din compunerea alternativei respective se va trece la executia succesiunii de instructiuni din alternativa urmatoare a aceleeasi instructiuni switch.

Exemplu: Se citeste o cifra din intervalul [1, 7]. Sa se afiseze denumirea zilei din saptamana corespunzatoare cifrei citite.
#include <iostream.h> void main() { int i; do { cout << Introduceti o cifra intre 1 si 7: "); cin >> i; } while ((i < 1) || (i > 7)); switch (i) { case 1: cout << "Luni\n"; break; case 2: cout << "Marti\n"; break; case 3: cout << "Miercuri\n"; break; case 4: cout << "Joi\n"; break; case 5: cout << "Vineri\n"; break; case 6: cout << "Sambata\n"; break; case 7: cout << "Duminica\n"; } } Observatii: Instructiunea switch difera de if prin aceea ca switch testeaza numai egalitatea, in timp ce la if expresia de conditie poate fi de orice tip; Ca expresia selectoare a instructiunii switch se poate utiliza numai o expresie care furnizeaza un rezultat de tip int sau char; Instructiunile asociate cu fiecare case nu trebuie incluse intre acolade, deci ele nu formeaza un bloc de instructiuni; Intr-o instructiune switch sunt permise maximum 257 de clauze case; In acelasi swtich nu pot exista doua sau mai multe constante caz identice; Instructiunile asociate unei clauze case pot lipsi, ceea ce permite ca pentru doua sau mai multe constante caz sa se execute aceleasi instructiuni fara a mai fi nevoie sa fie repetate. Includerea unei instructiuni swtich in interiorul altei instructiuni swtich este permisa, putand exista cel mult 15 niveluri de instructiuni swtich incluse una in alta.

33

Instructiuni de ciclare
In orice limbaj de programare exista instructiuni care permit executarea in mod repetat a unei instructiuni sau grup de instructiuni. Aceste instructiuni se numesc instructiuni de ciclare sau instructiuni repetitive. In limbajul C (C++) exista trei instructiuni de ciclare: while, for si do...while. Toate aceste instructiuni pot controla o singura instructiune, care insa poate fi si instructiune compusa.

Instructiunea while
Instructiunea while permite codificarea unei structuri repetitive conditionata anterior (numita si ciclu cu test initial) si are urmatorul format general:
while (expresie) instructiune;

Principiul de executie este urmatorul: P1. Se evalueaza expresie; P2. Daca valoarea furnizata de expresie este diferita de 0 (adevarat), se executa instructiunea subordonata si se revine la pasul P1; in caz contrar (valoarea furnizata este 0 fals), ciclarea se termina si se trece la instructiunea ce urmeaza dupa while. Exemplu: Se citeste un numar natural. Sa se afiseze in ordine inversa cifrele sale.
#include <iostream.h> void main() { int n, ninv = 0; cout << Introduceti un intreg: ); cin >> n; while (n) { ninv = ninv * 10 + n % 10; n = n / 10; } cout << Numarul inversat este: << ninv; }

Avand in vedere posibilitatile de lucru cu expresii, instructiunea while din acest program poate fi scrisa si sub forma:
while(ninv*=10, ninv += 10, n /= 10); Din aceasta forma a instructiunii while se poate constata ca instructiunea subordonata ei poate fi si o instructiune vida, iar expresie poate fi si o instructiune expresie. Intr-o instructiune expresie evaluarea se face de la stanga la dreapta, iar rezultatul ei este dat de ultima expresie

din instructiune.
Observatie: Instructiunea while trebuie sa contina o instructiune care sa asigure terminarea ciclarii (normal sau fortat). Pentru terminarea normala, in corpul ciclului este necesara prezenta unei instructiuni care sa duca la modificarea conditiei testate (expresie) astfel incat rezultatul furnizat sa devina fals. Daca corpul ciclului este o instructiune vida, atunci in instructiunea expresie ce formeaza conditia de ciclare trebuie sa existe o expresie care sa realizeaza terminarea ciclarii. Terminarea fortata se realizeaza plasand in corpul ciclului o instructiune break.

34

Instructiunea for
Instructiunea for permite codificarea unei structuri repetitive cu contor (numita si ciclu cu numar cunoscut de pasi) si are urmatorul format general:
for(expr_init; expr_test; expr_pas) instructiune;

unde: expr_init este o expresie care furnizeaza valoarea de initializare a variabilei de control a ciclului (numita si contor). Trebuie remarcat faptul ca in aceasta expresie (cu rol special) este posibil chiar si declararea variabilei si a valorii ei initiale. De exemplu, expresia int j = 1; este corecta si declara variabila de control a ciclului j si o initializeaza cu 1. expr_test reprezinta expresia care verifica daca instructiunea subordonata (corpul ciclului) se va executa sau nu. Daca rezulatul evaluarii acestei expresii este diferit de 0 (adevarat), se executa corpul ciclului; in caz contrar (valoarea data de expresie este 0 fals) se termina executia instructiunii for. expr_pas reprezinta o expresie care furnizeaza valoarea de modificare a variabilei de control a ciclului dupa fiecare executie a corpului ciclului. instructiune este o instructiune simpla sau compusa, care se va executa in mod repetat de un numar fix de ori. Formeaza corpul ciclului. Principiul de executie este urmatorul: P1. Se evalueaza expr_init; P2. Se evalueaza expr_test. Daca rezultatul evaluarii este o valoare diferita de 0 (adevarat), se executa corpul ciclului si apoi se trece la pasul P3. Daca rezulatul evaluarii expresiei expr_test este 0 (fals), se termina executarea instructiunii for si se trece la instructiunea ce urmeaza dupa instructiunea for. P3. Se evalueaza expresia expr_pas si se reia executia de la pasul P2.
Observatii: Din analiza modului de executie, rezulta ca structura repetitiva cu numar cunoscut de pasi este de fapt tot o structura conditionat anterior. Dupa fiecare a executiei corpului ciclului variabila de control poate fi majorata sau micsorata cu o valoare intreaga sau reala.

Exemple: 1. Sa se intocmeasca un program care sa calculeze factorialul unui numar natural n introdus de la tastatura.
#include <iostream.h> int n; long fact = 1; void main() { cout << "Introduceti un intreg: "; cin >> n; for(int i=1; i <= n; i++) fact *= i; cout << "n!= " << fact; }

2. Sa se scrie un program care sa afiseaze sub forma tabelara valorile sinusului pentru unghiurile cuprinse intre 0 si 1 radian (cu un pas de incrementare de 0.1).
#include <iostream.h> #include <math.h> void main() { cout << "----------------\n"; cout << "|alfa | sinus |\n"; cout << "|--------------|\n"; for (float radian = 0; radian <= 2; radian += 0.1) cout << "| << radian << | << sin(radian) << |\n; cout << "----------------\n"; } Nota: Fisierul antet <math.h> contine prototipurile functiilor matematice.ale limbajului C++.

35

Instructiunea do...while
Instructiunea do...while permite codificarea structuriilor repetitive conditionate posterior (numite si cicluri cu test final) si are urmatorul format:
do { instructiune } while (expresie);

Principiul de executie este urmatorul: P1. Se executa instructiunea subordonata (corpul ciclului); P2. Se evalueaza expresie. Daca valoarea rezultata este diferita de 0 (adevarat), se reia excutia cu pasul P1: in caz contrar (rezlultatul este 0 fals) executia instructiunii do se termina.
Observatie: Instructiunea do..while trebuie sa contina o instructiune care sa asigure terminarea ciclarii (normal sau fortat). Pentru terminarea normala, in corpul ciclului este necesara prezenta unei instructiuni care sa duca la modificarea conditiei testate (expresie) astfel incat rezultatul furnizat sa devina fals. Daca corpul ciclului este o instructiune vida, atunci in instructiunea expresie ce formeaza conditia de ciclare trebuie sa existe o expresie care sa realizeaza terminarea ciclarii. Terminarea fortata se realizeaza plasand in corpul ciclului o instructiune break.

Exemplu: Se se scrie un program care verifica daca numarul introdus de la tastatura se incadreaza intre doua limite stabilite.
#include <iostream.h> int lim1 = 5, lim2 = 25, n; int x, y; void main() { void main() { do { cout << "Introduceti un intreg [" << lim1 << ", " << lim2 << "]: "; cin >> n; } while (n < 5 || n > 25); }

Instructiunea break
Pentru a forta iesirea dintr-o instructiune de ciclare (for, do sau while) se poate utiliza instructiunea break, care are urmatorul format general:
break;

Exemplu: De la tastatura se introduc mai multe numere intregi. Introducerea se termina la tastarea valorii 0 (nu face parte din sir). Pentru numerele pare se va afisa valoarea lor si numarul de ordine.
#include <iostream.h> void main() { int x = 1, i = 1; do { cout << "Introductei u intreg (0 pentru terminare)= "; cin >> i; if (!x) break; if (!(x % 2)) cout << "Numarul " << x << "din pozitia " << i << " este par\n"; i += 1; } while (1); } Observatie: In cazul mai multor instructiuni repetitive imbricate (incluse una in alta) efectul instructiunii break se refera doar la instructiunea repetitiva care o subordoneaza direct.

Instructiunea continue
36

Instructiunea continue se poate utiliza in instructiunile repetitive pentru a efectua saltul la testul de continuare (in cazul instructiunilor do...while si while) sau saltul la expresia de incrementare (in cazul instructiunii for), fara a se mai executa instructiunile din corpul ciclului de dupa aceasta. Formatul general acestei instructiuni este urmatorul:
continue;

O buna utilizare a instructiunii continue este reluarea executiei unui ciclu atunci cand s-a produs o eroare care poate fi inlaturata de utilizator. De exemplu, urmatorul program insumeaza numerele introduse de la tastatura pana cand utilizatorul tasteaza o valoare nula. Insumarea se produce numai daca numarul introdus este corect. Daca utilizatorul spune ca numarul este incorect, se reia ciclul fara a se insuma numarul eronat.
#include <stdio.h> #include <conio.h> int n, total = 0; char ch; void main() { do { printf("Introduceti un intreg (0 pentru oprire): "); scanf("%d", &n); printf("Numarul %d este corect (D/N)? ", n); ch = getche(); printf("\n"); if(ch == 'n' || ch == 'N') continue; total += n; } while (n); printf("Suma numerelor introduse este %d\n", total); }

37

Programarea procedurala
Definirea si apelarea functiilor
Inca de la primele limbaje de programare de nivel inalt s-a utilizat programarea procedurala. Aceasta a aparut si s-a dezvoltat din necesitatea de a folosi intr-un program, aceeasi secventa de instructiuni de mai multe ori. Pentru a evita scrierea repetata a acestor instructiuni, ele sunt organizate ca o parte distincta a programului, si se executa un salt la aceasta zona ori de cate ori este necesar. Acest salt este un salt cu revenire la instructiunea ce urmeaza dupa cea care a comandat saltul. Secventa de instructiuni organizata in acest fel are diferite denumiri in limbajele de programare: subprogram, subrutina, procedura etc. Pentru inceput o vom numi subprogram. Uneori subprogramul trebuie sa execute aceleasi operatii de prelucrare, dar cu date diferite. In acest caz, subprogramul trebuie realizat la modul general, facandu-se abstractie de datele respective. De exemplu, pentru a evalua expresia:
4**10 3**20

putem crea un subprogram de ridicare la putere, care sa fie general, facandu-se abstractie de valorile concrete ale bazei si exponentului. Pentru a realiza aceasta generalizare se va considera ca atat baza cat si exponentul sunt variabile, iar valorile lor se vor precizeaza la fiecare apel al subprogramului respectiv. Variabilele folosite pentru implementarea unui subprogram general si a caror valori sunt concretizate la fiecare apel se numesc parametri formali (parametri fictivi sau argumente). Valorile transmise la apel parametrilor formali se numesc parametri efectivi (parametri actuali sau reali). Programarea procedurala are la baza utilizarea subprogramelor, iar acestea la randul lor realizeaza o abstractizare prin parametri. In toate limbajele de programare subprogramele sunt considerate a fi de doua categorii: Subprograme care definesc o valoare de revenire, denumite de obicei functii. Valoarea de revenire se mai numeste si valoare de intoarcere sau valoarea returnata a functiei.; Subprograme care nu definesc o valoare de revenire, care in anumite limbaje de programare se numesc subrutine sau proceduri. Limbajul C (C++) permite utilizarea numai a suprogramelor de tip functie, care pot returna sau nu o valoare la revenirea in programul apelant. In plus, functiile limbajului C (C++) pot fi folosite atat independent (apelate ca procedurile in celelalte limbaje) cat si intr-o expresie ca operand (utilizand-se astfel valoarea returnata).

Definirea functiilor
Intr-un program functia are o singura definitie, dar ea poate fi apelata ori de cate ori este necesar in respectivul program. Definitia unei functii are urmatorul format:
tip nume(lista_parametri_formali) { [declaratii] instructiuni }

Primul rand din formatul de mai sus reprezinta antetul functiei, iar partea inclusa intre acolade, impreuna cu acoladele, formeaza corpul functiei. Daca functia returneaza o valoare, atunci tip desemneaza tipul rezultatului prin intermediul unui cuvant cheie (cum ar fi: int, char, float etc.) sau al unui nume de tip definit de utilizator. Daca functia nu returneaza o valoare, atunci ca tip se va folosi cuvantul cheie void, care semnifica lipsa unei valori returnate la revenire. O functie poate avea mai multi parametri sau nici unul. Daca lista_ parametri_formali este vida, atunci functia nu are parametrii formali. In acest caz antetul functiei se reduce la: 38

tip nume()

Lipsa parametrilor formali poate fi mentionata explicit folosind cuvantul cheie void. Deci antetul de mai sus poate fi scris si sub forma:
tip nume(void)

In cazul in care functia are parametrii, adica in antetul functiei este prezenta lista cu parametrii formali, declaratiile parametrilor sunt separate prin virgule. Constructia nume din antet reprezinta numele functiei si trebuie sa respecte conventiile de denumire ale identificatorilor specifice limbajului C (C++). Zona declaratii din corpul functiei se poate folosi pentru definirea variabilelor care se utilizeaza numai in functia respectiva. Aceasta zona poate lipsi. Zona instructiuni contine instructiunile care descriu operatiile de prelucrare pentru care a fost realizata functia si formeaza corpul functiei.

Apelarea functiilor
Pentru a putea utiliza o functie intr-un program, ea trebuie apelata. Apelul de functie trebuie sa fie precedat de definitia sau de prototipul ei. Prototipul unei functii contine informatii similare cu cele din antetul ei: tipul valorii returnate; numele functiei; tipurile parametrilor formali. Prototipul functiei poate avea acelasi format ca si antetul functiei, fiind urmat insa de caracterul punct si virgula (;) sau poate avea un format prescurtat. Deci prototipul unei functii poate fi de forma:
tip nume(lista_parametri_formali);

adica identic cu antetul functiei, sau


tip nume(lista_tipuri_parametri_formali);

o forma prescurtata, in care in lista_tipuri_parametri_formali sunt precizate doar tipurile parametrilor formali. De exemplu, daca intr-un program este definita o functie a carui antet este urmatorul:
long double putere(long double x, int n)

prototipul functie poate fi:


long double putere(long double x, int n);

sau
long double putere(long, int);

Prototipul unei functii este obligatoriu atunci cand functia este definita in acelasi fisier cu apelantul, dar dupa acesta (vezi exemplul de la pagina 41). Limbajul C (C++) se livreaza cu o serie de functii ce au o utilizare frecventa in programe. Ele sunt stocate intr-un fisier in format obiect, adica compilat si se adauga la fiecare program in faza de editare. Aceste functii se numesc functii standard de biblioteca. Prototipurile lor sunt pastrate in diferite fisiere de extensie .h (numite si fisiere header). Prototipurile functiilor standard se includ in program inaintea apelurilor lor folosind constructia #include, care are urmatorul format:
#include <specificator_fisier>

unde: specificator_fisier este format din numele fisierului cu prototipurile functiilor standard necesare, urmat de caracterul punct (.) si de extensia h. De exemplu, pentru a executa intr-un program operatii de intrare/iesire se pot apela functiile printf si scanf, al caror prototipuri se gasesc in fisierul stdio.h. Pentru a le apela este necesar ca inaintea primei utilizari, in codul sursa trebuie sa apara urmatoarea linie: #include <stdio.h>
Observatie: Pentru ca in program sa se includa un fisier sursa, in locul parantezelor unghiulare se folosesc ghilimelele ().

39

De exemplu, linia #include fis1.cpp indica includerea fisierului fis1.cpp aflat in directorul curent, iar linia #include C:\BC31\Programe\Citire.cpp specifica includerea fisierului Citire.cpp din subdirectorul Programe al directorului BC31 din radacina discului C.

Apelul unei functii care nu returneaza o valoare, se realizeaza printr-o instructiune de apel, care are urmatorul format:
nume(lista_parametri_efectivi);

unde: nume este numele functiei cre se apeleaza; lista_parametri_efectivi este fie vida, cand functia a fost declarata fara parametri formali, fie contine una sau mai multe expresii separate prin virgula. O functie care returneaza o valoare, poate fi apelata fie printr-o instructiune de apel, fie ca operand al unei expresii. Daca functia este apelata printr-o instructiune de apel, valoarea returnata de ea se pierde. Cand functia este apelata ca operand al unei expresii, valoarea returnata de ea se utilizeaza la evaluarea expresiei respective. De exemplu, urmatoarea instructiune de atribuire
c = getch();

atribuie variabilei c valoarea codului ASCII al caracterului citit de la tastatura, in timp ce in cazul apelului din urmatoarea linie
getch();

valoarea codului ASCII al caracterului citit de la tastatura nu se foloseste. Aceasta forma de apel a functiei getch() se foloseste pentru a bloca executia programului pana la actionarea unei taste oarecare, astfel incat utilizatorul sa poata citi rezultatele afisate pe ecran. La apelul unei functii, valorile parametrilor efectivi se atribuie parametrilor formali corespunzatori, apoi executia continua cu prima instructiune din corpul functiei apelate. In cazul in care tipul unui parametru efectiv difera de tipul parametrului formal care-i corespunde, in limbajul C se converteste automat valoarea parametrului efectiv spre tipul parametrului formal respectiv. In limbajul C++ se foloseste o regula mai complexa pentru apelul functiilor si de aceea se recomanda utilizarea conversiei explicite, folosind operatorul (tip), adica utilizand asa numitele expresii cast. De exemplu, daca functia f are un parametru formal de tip double si n este o variabila de tip int, atunci in locul apelului:
f(n);

se recomanda utilizarea unui apel cu conversie explicita a lui n la tipul double:


f((double) n);

Din cele prezentate pana in prezent, rezulta ca intre lista parametrilor efectivi ai apelului de functie si lista parametrilor formali ai definitiei functiei trebuie sa existe o concordanta din punct de vedere al ordinii si al tipului parametrilor. De cele mai multe ori aceasta concordanta se extinde si la numarul parametrilor. Revenirea dintr-o functie se poate realiza in unul din urmatoarele moduri: Dupa executia ultimei instructiuni din corpul functiei (s-a ajuns la acolada inchisa care termina corpul functiei). La intalnirea instructiunii return. Instructiunea return poate sa apara oriunde in corpul functiei si are urmatoarele formate:
return; return expresie;

Primul format se foloseste numai in corpul unei functii care nu returneaza o valoare, pentru a forta terminarea executiei functiei si revenirea in programul apelant. Cel de-al doilea format se foloseste in corpul unei functii care returneaza o valoarea la revenirea din ea. Valoarea furnizata de expresie este chiar valoarea returnata de functie. Daca tipul acestei expresii difera de tipul care precede numele din antetul functiei, valoarea expresiei se converteste automat spre tipul din antet, inainte de a se reveni din functie. 40

De exemplu, sa consideram urmatoarea functie, numita factorial, care primeste ca parametru un intreg din intervalul [0, 170] si returneaza factorialul valorii receptionate.
double factorial(int n) { double f; int i; // se verifica daca n este in intervalul [0, 170] if (n < 0 || n > 170) return 1.0; for (i = 2, f = 1.0; i <= n; i++) // Calculeaza n! f *= i; return f }

Functia factorial poate fi editata impreuna cu programul apelant la sfarsitul sau la inceputul acestuia. Daca functia este plasata la sfarsitul programului, atunci inainte de primul apel trebuie sa apara prototipul functie, asa cum se arata in exemplu urmator:
#include <iostream.h> double factorial(int); // Proptotipul functiei factorial void main() { int m; double rezult; cout << "Introduceti un intreg (0 la 170)= "; cin >> m; if ((m < 0) || (m > 170)) cout << "Numarul introdus este < 0 sau > 170\n"; else { rezult = factorial(m); cout << m <<"! = " << rezult << "\n"; } } double factorial (int n) { double f; int i; if ((n < 0) || (n > 170)) return -1.0; for (i = 2, f = 1.0; i <= n; i++) f *= i; return f; }

In cazul in care functia este editata intr-un fisier separat, numit fact.cpp din subdosarul Programe al dosarului BC31 din radacina discului C, atunci in programul apelant trebuie inclusa o directiva #include, care sa specifice numele fisierului ce contine functia si locul unde se gaseste acest fisier, ca in urmatorul exemplu:
#include <iostream.h> #include C:\BC31\Programe\fact.cpp void main() { int m; double rezult; cout << "Introduceti un intreg (0 la 170)= "; cin >> m; if ((m < 0) || (m > 170)) cout << "Numarul introdus este < 0 sau > 170\n"; else { rezult = factorial(m); cout << m <<"! = " << rezult << "\n"; } }

41

Transferul parametrilor
In limbajul C, la apelul unei functii, fiecarui parametru formal i se atribuie valoarea parametrului efectiv care-i corespunde. Datorita faptului ca la apelare se transfera valori, acest mod de transmitere se numeste apel prin valoare. In unele limbaje de programare, printre care si limbajul C++, exista posibilitatea ca in locul valorilor parametrilor efectivi se se transmita adresele acestor valori. Acest mod de transmitere se numeste apel prin referinta. Intre cele doua moduri de apelare exista o mare diferenta si anume: In cazul apelului prin valoare, functia apelata nu modifica parametri efectivi receptionati de la functia apelanta. Adica, functia apelata primeste doar o copie a acestor valori si deci nu are acces la zonele de memorie alocate parametrilor efectivi. In cazul apelului prin referinta, functia apelata poate modifica valorile parametrilor efectivi primiti, deoarece ea primeste adresele zonelor de memorie alocate parametrilor efectivi si nu valoarea acestora. In exemplele prezentate anterior, apelul functiilor s-a realizat prin valoare. Pentru a putea intelege diferenta dintre apelul prin valoare si cel prin referinta vom considera urmatoarea functie:
void permuta(int x, int y) { int aux: aux = x; x = y; y = aux; }

care asigura interschimbarea valorilor parametrilor formali x si y. Considerand declaratia:


int a,b;

atunci apelul:
permuta(a, b);

nu va afecta parametri efectivi, deoarece asa cum este definita functia, apelul se face prin valoare. Pentru ca interschimbarea valorilor sa afecteze si parametri efectivi este necesar sa se transmita adresele zonelor de memorie in care se gasesc valorile respective. Adresele acestor zone se poate transmite prin intermediul pointerilor si deci trebuie sa fie definita astfel:
void ppermuta(int *x, int *y) { int aux: aux = *x; *x = *y; *y = aux; }

in care *x si *y sunt doua variabile de tip pointer catre tipul de date int. In acest caz, functia ppermuta se va apela astfel:
int a, b; ... ppermuta(&a, &b); ...

Prin apelul ppermuta(&a, &b); parametrilor formali de tip pointer x si y li se atribuie adresele parametrilor efectivi a si b, ceea ce inseamna ca x si y acceseza aceleasi zone de memorie ca si a si b. Functia ppermuta nu interschimba valorile pointerilor x si y ci valorile intregilor spre care refera cei doi pointeri, adica chiar valorile variabilelor a si b. In limbajul C++, spre deosebire de limbajul C, se poate defini functia de permutare ca o functie cu parametri formali de tip referinta, astfel:
void rpermuta(int& x, int& y) { int aux: aux = x; x = y;

42

y = aux;

In acest situatie, functia rpermuta se va apela astfel:


int a, b; ... rpermuta(a, b); ...

In acest caz x si y sunt sinonime cu variabilele a si b. Aceasta inseamna ca x si y vor accesa aceleasi date din memorie ca si a si b. Din aceasta cauza permutarea valorilor referite de x si y inseamna permutarea valorilor referite de a si b. Comparand functiile permuta si rpermuta se poate observa ca ele se apeleaza la fel iar sigura diferenta dintre ele consta in modul de declarare al parametrilor formali. In cazul functiei permuta parametri formali sunt date de tip int, iar in cazul functiei rpermuta acestia sunt referinte la date de tip int. Daca transmitem un parametru care este un tablou, atunci transmiterea prin valoare se comporta identic cu transmiterea prin referinta, deci functia va putea sa modifice oricare dintre componentele tabloului. Acest lucru se produce datorita faptului ca numele de tablou reprezinta adresa zonei in care sunt stocate componentele sale. Exemplu urmator foloseste trei functii: CitVect - citeste numarul de componente ale vectorului si valorile componentelor; SortAsc ordoneaza crescator elementele vectorului; SortDesc ordoneaza descrescator elementele tabloului.
#include <iostream.h> typedef double DVector[100]; // Declara DVector ca tip utilizator DVector vector; // Declara o variabila a tipului utilizator int nr; // Numarul de componente ale vectorului void CitVect(DVector); void SortAsc(DVector); void SortDesc(DVector); void main() { int i; char sort; CitVect(vector); // Citeste dimensiunea si componentele vectorului do { cout << "Sortarea va fi ascendenta sau descendenta (A/D)? "; cin >> sort; if(sort >= 'a' && sort <= 'z') sort -=32; if(sort == 'A' || sort == 'D') break; } while(1); cout << "Sirul initial este:\n"; for(i=0; i<nr; i++) cout << vector[i] << " "; cout << "\n"; if(sort == 'A') SortAsc(vector); // Sortare ascendenta else SortDesc(vector); // Sortare descendenta cout << "Sirul sortat este:\n"; for(i=0; i<nr; i++) cout << vector[i] << " "; cout << "\n"; } void CitVect(DVector vec) { // Citeste dimensiunea si componentele vectorului int i; do { cout << "Numarul de componente [2, 100]: "; cin >> nr; if(nr >=2 && nr <=100) break; } while(1); for(i=0; i< nr; i++) { cout << "vector[" << i + 1 << "]= "; cin >> vec[i];

43

} } void SortDesc(DVector vec) { // Sortarea descendenta a vectorului int i, flag = 1; double aux; while (flag) { flag = 0; for (i = 0; i < nr -1; i++) if (vec[i] < vec[i+1]) { flag = 1; aux = vec[i]; vec[i] = vec[i + 1]; vec[i + 1] = aux; } } } void SortAsc(DVector vec) { // Sortarea ascendenta a vectorului int i, j; double aux; for(i=0; i<nr-1; i++) for(j=i+1; j<nr; j++) if (vec[i] > vec[j]) { aux = vec[i]; vec[i] = vec[j]; vec[j] = aux; } }

Se poate constata ca in program a fost definit tipul utilizator DVector, ca fiind un tablou unidimensional de 100 elemente de tip double. Am ales aceasta solutie pentru a simplifica scrierea prototipurilor si antetelor functiilor. Daca nu declaram tipul utilizator DVector, atunci ar fi trebuit sa se foloseasca o declaratie de forma:
double vector[100];

pentru a declara un tablou cu 100 elemente de tip double. In aceasta situatie prototipurile functiilor ar fi trebuit sa fie urmatoarele:
void CitVect(double vec[]); void SortAsc(double vec[]); void SortDesc(double vec[]);

iar antetele de functie ar fi fost urmatoarele:


void CitVect(double vec[]) void SortAsc(double vec[]) void SortDesc(double vec[]) Concluzii: Tablourile sunt transmise prin referinta in mod prestabilit; pentru ca un tablou sa se transmita prin valoare, el trebuie sa fie inclus intr-o structura, care va fi transmisa functiei prin valoare. In general, parametrii efectivi trebuie sa corespunda din punct de vedere al numarului, ordinii si tipului cu parametrii formali. Functia poate returna cel mult o valoare. Pentru a obtine mai multe rezultate la revenirea dintr-o functie, se vor folosi fie tablourile sau structurile pentru a grupa rezultatele de iesire, fie variabile globale, fie transferul prin referinta.

44

Clase de functii
In limbajul C++, spre deosebire de alte limbaje, exista un numar foarte mare de functii de biblioteca, care sunt grupate dupa domeniul prelucrarilor efectuate in mai multe clase de functii astfel: prelucrarea fisierelor, manipularea de siruri, conversii, lucru cu zone de memorie, calcule matematice, alocare dinamica de memorie, grafica etc. Prototipurile acestor functii se gasesc in fisiere de tip header (extensie .h). In tabelul urmator sunt prezentate pe scurt fisierele de tip header din C++.
Nume fisier header
alloc.h assert.h bcd.h bios.h complex.h conio.h ctype.h dir.h direct.h dirent.h dos.h errno.h fcntl.h float.h fstream.h generic.h io.h iomanip.h iostream.h limits.h locale.h math.h mem.h memory.h new.h process.h search.h setjmp.h share.h signal.h stdarg.h stddef.h stdio.h

Destinatie Declara functiile de manipulare a memoriei dinamice(alocare, dealocare etc.). Defineste macrocomanda assert de depanare. Declara clasa bcd din C++ si suprascrie operatorii bcd3 si functiile matematice bcd. Declara diferite functii folosite la apelarea rutinelor IBM-PC ROM BIOS. Declara functiile matematice complexe din C++. Declara diferite functii folosite la apelarea rutinelor I/O ale consolei DOS. Contine informatii folosite de catre macrocomenzile de clasificare caractere si de conversie caractere. Contine structuri, macrocomenzi si functii pentru lucru cu directoare si cai. Defineste structuri, macrocomenzi si functii pentru lucru cu directoare si cai. Declara functii si structuri pentru operatii cu directoare POSIX. Defineste diferite constante si declaratiile necesare pentru apeluri specifice DOS si 8086. Defineste constantele mnemonice pentru codurile de eroare. Defineste constantele simbolice folosite la conectarea cu rutina de biblioteca deschisa. Contine parametrii pentru rutinele in virgula mobila. Declara clasele stream din C++ ce suporta fisierele de intrare si iesire. Contine macrocomenzi pentru declaratii de clase generice. Contine structurile si declaratiile pentru rutinele de intrare/iesire de nivel inferior. Declara manipulatorii de stream-uri de I/O din C++ si contine macrocomenzile pentru crearea manipulatorilor parametrizati. Declara rutinele de baza ale stream-urilor (I/O) din C++ (version 2.0). Contine parametrii mediului, informatiile despre limitarile din timpul compilarii.. Declara functiile care asigura informatiile specifice tarii si limbii. Declara prototipurile functiilor matematice, defineste macrocomanda HUGE_VAL si declara structura utilizata de matherr. Declara functiile de manipulare a memoriei (multe dintre ele sunt definite si in string.h.) Functii de manipularea memoriei. Accesul la operatorul new si newhandler. Contine structurile si declaratiile pentru functiile spawn si exec. Declara functiile de sortare si cautare. Defineste un tip folosit de longjmp si setjmp. Defineste parametrii utilizati in functiile care folosesc fisiere partajate. Defineste constante si declaratii pentru signal si raise. Defineste macrocomenzile folosite pentru citirea listei de argumente din functiile declarate pentru a accepta un numar variabil de argumente. Defineste cateva tipuri de date si macrocomenzi generale. Defineste tipurile si macrocomenzile necesare Standard I/O Package definit in Kernighan si Ritchie si extins sub sistemul UNIX System V. Defineste stream-urile standard de I/O prestabilite: stdin, stdout, stdprn si stderr; declara tutinele de I/O la nivel de stream.

bcd - numar zecimal codificat binar

45

stdiostr.h stdlib.h string.h strstrea.h sys\locking.h sys\stat.h sys\timeb.h sys\types.h time.h utime.h values.h varargs.h

Declara clasele stream C++ (version 2.0) pentru a fi folosite cu structurile FILE pentru stdio. Declara cateva rutine folosite in mod normal: rutine de conversie, rutine de cautare/sortare si alte operatii. Declara cateva rutine de manipulare siruri de caractere. Declara clasele stream C++ pentru folosirea cu tablourile de byte din memorie. Definitii pentru parametrul mode al functiilor de blocare. Defineste constantele simbolice folosite pentru deschiderea si crearea fisierelor. Declara functia ftime si structura timeb ce este returnata de ftime. Declara tipul time_t folosit de functiile de timp. Defineste o structura completata de rutinele de conversie ale timpului si un tip folosit de catre alte rutine de timp; de asemenea asigura prototipurile pentru aceste rutine. Declara functiile utime si structura utimbuf. Defineste constante importante, inclusiv cele dependente de masina pentru compatibilitatea cu UNIX System V. Defineste macrocomenzile de stil vechi pentru procesarea listelor de argumente variabile. Se foloseste stdarg.h

Functii recursive
Una din caracteristicile de baza ale algoritmilor este recursivitatea. Recursivitatea defineste modalitatea de a determina valoarea unei variabile pe baza valorilor ei anterioare si eventual a altor valori cunoscute. O parte din limbajele de programare asigura implementarea algoritmilor recursivi cu ajutorul structurilor repetitive, utilizand tablouri pentru memorarea rezultatelor obtinute in fiecare iteratie si variabile temporare de lucru. Limbajul de programare C++, ca de altfel si alte limbaje de programare, permite descrierea algoritmilor recursivi cu ajutorul functiilor care se apeleaza pe ele insele, fie direct fie prin intermediul altor functii. Pentru exemplificarea recursivitatii vom considera o problema simpla, si anume determinarea sumei elementelor unui vector. Pentru vectorul A = (a0, a1, , an), suma elementelor se poate descrie recursiv astfel:
0, daca n = 0 Sn = Sn-1 + an, daca n>0

Functia care va realiza aceasta operatie are urmatoarea forma:


double sum(double v[], int n) { return n ? v[n-1] + sum(v, n 1) : 0; }

La utilizarea functiilor recursive se va acorda o atentie deosebita controlului finitudinii algoritmului si numarului de apeluri, deoarece apar restrictii datorate dimensiunii stivei. Pentru aceasta in componenta functiei recursive distingem intodeauna conditia de terminare a procesului recursiv (in exemplul anterior, n = 0) si modalitatea prin care se modifica valoarea parametrilor efectivi la apeluri succesive (in exemplul de mai sus, n 1). Practic prin apelul recursiv, functia este impartita in doua: o portiune se executa in mod repetat cu incarcarea in stiva a contextului si a adresei de revenire si o portiune a carei executie repetata incepe numai dupa ce s-a atins nivelul maxim de profunzime a recursivitatii (impus prin conditia de terminare). Un exemplu sugestiv in acest sens il constituie functia care realizeaza afisarea unui numar a, din baza 10 intr-o baza b [2 , 10]. Pentru afisare, trebuie sa se obtina resturile impartirilor succesive la baza in ordinea inversa si deci afisarea cifrei se va face dupa apelul recursiv. Functia de conversie a unui numar din baza 10 intr-o baza oarecare mai mica decat 10 este:
void afis(int a, unsigned b) { if (a < b) cout >> a;

46

else { afis(a / b, b); cout >> a % b; } }

Implicatiile claselor de memorare asociate variabilelor locale din functie sunt importante; astfel, variabilele automatice vor exista in cate un set pentru fiecare apel recursiv (stiva se va incarca cu aceste variabile), in timp ce variabilele statice si externe exista intr-un singur exemplar pentru toate apelurile. Modificarile dintr-un apel nu se regasesc la revenire in cazul variabilelor automatice, in timp ce variabilele statice reflecta efectul cumulat al tuturor modificarilor anterioare. Deci conditia pentru controlul recursivitatii se poate baza pe modificarea argumentelor de apel, pe variabile statice al caror continut se pastreaza intre doua apeluri succesive sau pe variabile globale.

Pointeri
Una dintre cele mai puternice facilitati disponibile pentru un programator C++ este posibilitatea de a manipula direct memoria calculatorului prin intermediul unor variabile care pot contine adrese, numite pointeri. Pentru a intelege pointerii, trebuie reamintite cateva lucruri despre memoria calculatorului. Memoria interna a unui calculator este impartita in locatii de memorie numerotate, incepand cu 0; aceste numere de ordine se numesc adrese. Fiecare variabila a programului este stocata in memorie incepand de la o anumita adresa. In mod normal, nu este necesar ca programatorul sa cunoasca adresa fizica de memorie in care este stocata valoarea unei variabile, deoarece compilatorul manipuleaza aceste detalii. Totusi, daca este necesara cunoasterea acestor adrese, se poate utiliza operatorul de adresa (&), asa cum este ilustrat in urmatorul program:
#include <iostream.h> void main() { unsigned short shortVar=5; unsigned long longVar=65535; long sVar = -65535; cout << "Variabila shortVar= " << shortVar << " are adresa: " << &shortVar << endl; cout << "Variabila longVar= " << longVar << " are adresa: " << &longVar << endl; cout << "Variabila sVar= " << sVar << " are adresa: " << &sVar << endl; }

Rezultatele afisate pe ecran vor arata ca cele din urmatoarea figura:

Nota:

Daca rulati acest program pe calculatorul dumneavoastra, este posibil sa obtineti alte adrese.

Pointerul4 este o variabila care are ca valoare o adresa. Pointerii se folosesc pentru a face referiri la date prin adresa lor. Astfel, daca p este o variabila de tip pointer care are ca valoare adresa lui x, atunci: *p reprezinta chiar valoarea lui x. In constructia *p, caracterul * se considera ca fiind un operator unar (numit si operator de indirectare sau de dereferentiere), care furnizeaza valoarea din zona de memorie a carei
4

In limba romana, pentru pointer, se mai folosesc si alte denumiri, cum ar fi: referinta, localizator, reper, indicator de adresa etc.

47

adresa este continuta in p. Cand valoarea unei variabile este referita printr-un pointer, procesul este numit indirectare. Daca p contine adresa zonei de memorie alocata variabilei x, vom spune ca p refera (pointeaza) spre x. De asemenea, daca p are ca valoare adresa de inceput a unei zone de memorie care contine o data de tipul tip, atunci vom spune ca p refera spre tip. Pentru a atribui o adresa unei variabile de tip pointer se poate folosi operatorul unar & (numit si operator adresa sau de referentiere). Astfel, daca vrem ca p sa refere spre x (adica, sa aiba adresa lui x), vom folosi atribuirea de forma: p = &x. Este permisa folosirea operatorului * in partea stanga a unei operatii de asignare pentru a atrubui unei variabile o noua valoare folosind un pointer. De exemplu, urmatorul program atribuie variabilei q o noua valoare, in mod indirect, utilizand un pointer:
#include <iostream.h> void main() { int *p, q = 235; cout << Valoarea lui q este << q; p = &q; // lui p i se atribuie adresa lui q *p = 275; // in zona referita de p, deci in q, se va stoca 275 cout << Noua valoarea a lui q este << q; }

Notiunea de pointer joaca un rol important, deoarece permite calcule cu adrese, calcule specifice limbajelor de asamblare.

Declararea pointerilor
Pentru a declara un pointer se foloseste sintaxa urmatoare:
tip *nume;

care se interpreteaza astfel: identificatorul nume refera zona de memorie alocata datelor de tipul tip si va putea contine adresa acestei zone. De exemplu, daca dorim sa declaram un pointer p pentru a pastra adresa unor variabile de tip intreg vom folosi urmatoarea declaratie: int *p; Tipul int indica faptul ca p va contine adrese de memorie in care se pastreaza date de tip int. In urma acestei declarari, variabila p nu contine nici o adresa; aceasta trebuie atribuita explicit. De exemplu:
int x; int *p; ... p = &x;

In urma executiei acestei secvente de instructiuni, variabila p, de tip pointer spre tipul int, va contine adresa variabilei x de tip int. Facand comparatie cu declararea unei variabile obisnuite:
tip nume;

putem considera ca:


tip *

dintr-o declaratie de pointeri reprezinta tip dintr-o declaratie obisnuita. Din acest motiv constructia tip * se spune ca reprezinta un tip nou, tipul pointer. Acest tip se spune ca este tipul pointer spre tip. Este foarte important sa facem distinctie intre un pointer, adresa pe care o pastreaza pointerul si valoarea de la adresa retinuta de pointer. Sa consideram urmatoarea secventa de cod:
int Variabila = 5; int *pPointer = &Variabila; Variabila este declarata ca fiind de tip intreg si initializata cu valoarea 5. pPointer este declarat ca fiind un pointer spre un intreg si este initializat cu adresa variabilei Variabila. Valoarea de la adresa pastrata de pPointer este 5.

48

Exista situatii cand dorim ca un pointer sa fie folosit cu mai multe tipuri de date. In aceasta situatie, deoarece nu putem specifica un tip se va folosi cuvantul cheie void:
void *nume;

Exemplu:
int x; float y; char c; void *p; ... p = &x; ... p = &y; ... p = &c; ...

Datorita faptului ca p a fost declarat folosind cuvantul void, lui p i se pot atribui adrese de memorie care pot contine date de tipuri diferite: int, float, char etc. Cand se folosesc pointeri de tip void, sunt necesare conversi explicite prin expresii de tip cast, pentru a preciza tipul de date spre care refera pointerul la un moment dat. De exemplu, fie declaratiile:
void *p; int x;

atribuirea
*p = 10;

este incorecta, deoarece nu este definit tipul datei spre care refera p. Forma corecta a acestei atribuiri este:
*(int *)p = 10;

deoarece valoarea lui p trebuie convertita spre tipul int * si de aceea expresia cast are forma:
(int *)p

Reamintim ca, pentru conversia explicita a unui tip se foloseste urmatoarea forma generala:
(tip) operand

Un pointer poate referi o variabila care se gaseste in acelasi segment cu el, caz in care adresa variabilei va contine doar deplasamentul (offset-ul) fata de inceputul segmentului, adresa de inceput a segmentului fiind implicita. In acest caz pointerul ocupa 2 bytes si se numeste pointer near. Intr-un pointer near se poate reprezenta o adresa de maximum 64 KB, adica in intervalul de adrese ale unui segment (0 la 65535). Pentru pointerii care refera variabile ce se gasesc intr-un segment diferit de cel al pointerului, precum si pe sistemele care folosesc numai adrese pe 32 biti (noile variante de Windows) este nevoie de 4 bytes pentru adresa, iar acesti pointeri se numesc pointeri far. Tipul pointerului se poate cere in mod explicit la declararea unei variabile pointer, prin folosirea cuvintelor cheie near sau far. Deci formatul complet al declaratiei unei variabile pointer este urmatorul:
tip [near | far]*nume;

Pointerii stau la baza adresarii indirecte, permitand construirea de programe cu un inalt grad de generalitate si abstractizare. Ei se folosesc nu numai ca o alternativa de adresare, ci si pentru a realiza unele sarcini specifice cum ar fi: adresarea indexata, specifica lucrului cu tablouri; alocarea si gestiunea memoriei dinamice; adresarea functiilor.

49

Operatii cu pointeri
Datorita faptului ca pointerii sunt un tip de date asupra lor se pot aplica diferite operatii, cum ar fi: incrementarea si decrementarea, adunarea si scaderea unui intreg, compararea, diferenta etc. In continuare sunt prezentate pe scurt opratiile cu pointeri.

Incrementarea si decrementarea pointerilor


Operatorii ++ si -- se pot aplica si asupra operanzilor de tip pointer, la fel ca si in cazul variabilelor, dar modul de executie este diferit. In cazul pointerilor, valoarea incrementului (decrementului) este egala cu numarul de bytes necesari pentru a pastra data de tipul referit de pointer. De exemplu, sa consideram urmatoarele declaratii:
double vec[10]; double *p; int i;

si instructiunea:
p = &vec[i};

pentru i putand lua valori intre 0 si 9, atunci expresiile:


++p

si
p++

majoreaza valoarea lui p cu 8 (pentru stocarea unui numar de tip double sunt necesari 8 bytes), deci p va avea adresa lui vec[i+1]. In mod analog, expresiile:
p--

si
--p

micsoreaza valoarea lui p cu 8, deci p va avea adresa lui vec[i-1].

Adunarea si scaderea unui intreg la/dintr-un pointer


Daca p este un pointer spre tipul t si n un intreg, atunci se pot utiliza expresiile:
p + n

si
p n

Rezultatul acestor operatii consta in marirea (micsorarea) valorii lui p cu produsul r * n, unde prin r s-a notat numarul de bytes necesari pastrarii in memorie a datei de tip t. Fie vec un vector de tipul t. Atunci vec este un pointer constant5 si ca atare o expresie de forma
vec + n

este corecta. Din cele afirmate mai sus, reiese ca rezultatul acestei expresii este chiar adresa elementului vec[n] si ca urmare *(vec + n) va furniza chiar valoarea elementului vec[n]. Deci o atribuire de forma:
y = vec[n];

este echivalenta cu
y = *(vec + n); Sugestie: In general, variabilele cu indici pot fi inlocuite prin expresii cu pointeri. Acest lucru mareste viteza de lucru, deoarece operatiile de inmultire care intervin la evaluarea variabilelor cu indici sunt inlocuite cu adunari.

Numele unui tablou este un pointer constant si contine adresa primului element al tabloui (elementul de indice 0).

50

Compararea a doi pointeri


Doi pointeri care refera acelasi tip de date pot fi comparati, folosind operatorii relationali (<, <=, >, >=) si de egalitate (==, !=). De regula aceste operatii se executa asupra pointerilor care refera elementele unui tablou. Astfel, daca p refera spre elementul vec[i] al tabloului vec (deci contine adresa elementului i) si q refera spre elementul vec[j] al aceluiasi tablou (deci contine adresa elementului j), atunci expresiile: p < q, p <= q, p > q, p >= q, p == q si p != q sunt corecte. De exemplu, expresia:
p < q

are valoarea adevarat (1), daca i < j si fals (0) in caz contrar. In mod analog se evalueaza si celelalte expresii relationale. Operatorii de egalitate pot fi utilizati si pentru a compara pointerii cu o constanta speciala NULL, care este definita in fisierul stdio.h astfel:
#define NULL 0

si ea reprezinta asa numitul pointer null, adica pointerul este definit dar nu contine o adresa.
Sugestie: In limbajul C++ se recomanda sa nu se foloseasca constanta NULL, ci chiar valoarea 0.

Diferenta a doi pointeri


Doi pointeri care refera spre elementele aceluiasi tablou pot fi scazuti. Fie p un pointer catre elementul vec[i] si q un pointer spre elementul vec[i + n] al aceluiasi tablou. In acest caz, diferenta q p are valoare n. Exemplu: a) Sa se scrie o functie care citeste cel mult n elemente de tip double diferite de 0, le pastreaza in zona de memorie a carei adresa de inceput este indicata de valoarea parametrului formal p al functiei si va returna numarul de valori citite. Introducerea unei valori 0 va forta terminarea operatiilor de citire a valorilor
int pnrdblcit(int n, double *p) { double d; char temp[255]; int i = 0; double *q = p + n; // adresa de sfarsit a zonei de memorie while (p < q) { cout << Valoarea elementului= , i); if (gets(temp) == NULL) return i; cin >> temp if (temp) break; *p++ = d; i++; } return i; } Observatii: Parametrul p are ca valoare la apel, adresa de inceput a zonei in care se vor stoca numerele citite. Dimensiunea acestei zone este de 8 * n bytes (o valoare de tip double ocupa 8 bytes) si deci in pointerul q se va stoca adresa ultimei zone de memorie desitinata pastrarii valorilor citite. Instructiunea *p++ = d; memoreaza valoarea citita in zona referita de p, dupa care valoarea lui p este incrementata, astfel ca la urmatoarea iteratie p va referi zona in care se va stoca elementul urmator. Dupa citirea a n valori, p devine egal cu q, deci in acest moment ciclul while trebuie sa se termine, daca nu se forteaza terminarea ciclului (se introduc mai putin de n valori).

51

Modificatorul const
O constanta este definita, de regula, prin caracterele care o compun. Caracterele unei constante pot indica atat tipul cat si valoarea acesteia. De exemplu, 1234 este o constanta de tip int; 1.5 este o constanta de tip double, a este o constanta de tip char, a este o constanta de tip sir de caractere. Cu ajutorul constructiei #define se pot defini constante simbolice. Constructia #define are urmatorul format:
#define nume succesiune_caractere

unde: nume reprezinta identificatorul constantei simbolice; succesiune_caractere reprezinta valoarea constantei simbolice. Folosind aceasta constructie, preprocesarea (faza care se executa in mod automat inaintea compilarii) substituie nume cu succesiune_caractere peste tot in textul sursa care urmeaza constructiei #define respective, exceptand cazul cand nume apare intr-un comentariu sau sir de caractere. O constructie #define autorizeaza substitutia pe care o defineste din punctul in care a fost scrisa si pana la sfarsitul fisierului sau pana la intalnirea unei constructii #undef care o anuleaza. Aceasta are fromatul:
#undef nume

Succesiunea de caractere dintr-o constructie #define poate contine constante simbolice care au fost definite in prealabil prin alte constructii #define. Exemple: a)
#define A 23 #define B A * 5 ... x = 5 + B; // se substitue cu x = 5 + 23 * 5 ...

b)
#define lim_sup 50 ... int vector[lim_sup + 5]; ... // se substituie cu int vector[50 + 5]

Constantele simbolice se folosesc frecvent in locul constantelor obisnuite datorita urmatoarelor avantaje: Permite atribuirea unor nume sugestive constantelor. De exemplu , este mult mai sugestiv sa folosim constanta PI definita prin:
#define PI 3.14159

decat valoarea ei. Permite realizarea unor prescurtari. De exemplu, daca valoarea 3.14159 se foloseste de mai multe ori in program, atunci este mai simplu sa folosim numele PI atribuit ei. Permite inlocuirea simpla a unei constante printr-o alta. Daca o constanta se foloseste de mai multe ori intr-un program si ulterior este necesara modificarea valoarii ei (de exemplu trebuie schimbata precizia constantei), atunci este mai simplu sa schimbam o singura data valoarea in constructia #define, decat sa o inlocuim peste tot in program. In limbajul C++ constantele pot fi definite si prin utilizarea modificatorului const in declaratii. Aceste constante se deosebesc de cele declarate prin constructia #define prin faptul ca ele nu sunt prelucrate la preprocesare; numele lor exista la compilare. Formatele posibile ale unei declaratii cu modificatorul const sunt:
tip const nume = valoare; tip const *nume = valoare; const tip nume = valoare;

52

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

Toate acestea se numesc declaratii de constante. 1. O declaratie de forma:


(1) tip const nume = valoare;

este asemanatoare cu o declaratie de variabila avand o valoare de initializare:


(1) tip nume = valoare;

Diferenta dintre cele doua declaratii consta in aceea ca valoarea lui nume atribuita prin declaratia (1) nu poate fi schimbata folosind o expresie de atribuire de forma:
nume = expresie;

Exemple: a) int const i = 10; - i este o constanta intreaga si are valoarea 10. b) double const pi = 3.14159265; - declara numele pi ca fiind o constanta de tip double si care are valoarea 3.14159265. 2. O declaratie de forma:
(2) tip const *nume = valoare; defineste nume ca un pointer spre o zona constanta. In acest caz valoarea pointerului nume se poate schimba. De exemplu, fie declaratia: char const *s = Sir de caractere;

atunci atribuirea:
s = t;

unde t este un pointer spre tipul char este corecta. In schimb atribuirile:
*s = a; *(s+1) = b;

sunt eronate, deoarece s refera o zona in care se pastreaza o data constanta. Totusi, constanta respectiva poate fi modificata folosind un pointer diferit de s:
char *p; p = (char *)s; *(p+1)=b; // pointerul p refera acum aceeasi zona ca si s

Aceste atribuiri sunt corecte, deoarece p nu mai este un pointer catre o zona constanta. 3. Declaratia de forma:
(3) const tip nume = valoare;

este identica cu declaratia (1) daca tip nu este un tip pointer. Daca tip este un tip pointer, adica declaratia (3) este de forma:
(4) const tip *nume = valoare;

atunci ea este identica cu declaratia (2). De obicei, se utilizeaza formatul (4), in locul formatului (2) pentru a declara un pointer spre o zona constanta. 4. Declaratia de forma:
(5) const tip *nume;

se foloseste pentru a declara un parametru formal. Fie functia f de antet:


tip f(tip *nume)

La apelul functiei f, parametrului formal nume i se atribuie ca valoare o adresa. Acest lucru da posibilitatea functiei f sa modifice data din zona de memorie spre care refera nume folosind o atribuire de forma:
*nume = valoare;

Pentru a proteja datele din zona referita de nume este indicat sa se declare parametrul formal ca un pointer catre o zona constanta, deci antetul functiei f trebuie sa fie de forma:
tip f(const tip *nume)

5. Declaratia de forma:
(6) const nume = valoare;

53

este similara cu declaratia (1), cu deosebirea ca nu se mai specifica tipul constantei. In acest caz tipul constantei se stabileste in functie de valoarea atribuita acesteia. 6. Declaratia de forma:
(7) tip *const nume = valoare;

defineste un pointer constant spre o data care nu este constanta. De exemplu, fie declaratiile:
char *const psir = abc; char *s; const char *sir = test;

Atunci, o atribuire de forma:


sir = s;

este corecta, in timp ce:


psir = s;

nu este permisa, deoarece psir este un pointer constant. In schimb, atribuirile:


*psir = A; *(psir + 1)= B; *(psir + 2)= C;

sunt corecte. 7. Declaratia de forma:


(8) const tip *const nume = valoare;

defineste un pointer constant catre o data constanta.

Apel prin referinta folosind parametrii de tip pointer


In limbajul C apelul functiilor este prin valoare. Aceasta inseamna ca la apelarea unei functii, parametrilor formali li se atribuie valorile parametrilor efectivi ce le corespund. Daca un parametru efectiv este un nume de tablou, apelul prin valoare devine de fapt un apel prin referinta si deci parametrului formal corespunzator lui i se atribuie ca valoare adresa primului element al tabloului. Prin utilizarea pointerilor, apelul prin valoare se poate transforma in apel prin referinta. Astfel, daca x este o variabila simpla, atunci la un apel putem transfera in locul valorii lui x, adresa lui x:
int x; ... h(x); ... g(&x); ... // Se transfera valoarea lui x // Se transfera adresa lui x

In acest caz cele doua functii vor avea antete diferite si anume:
void h(int x);

si respectiv
void g(int *pi);

In cazul functiei g parametrul formal pi este un pointer spre date de tip int. La apel, lui pi i se atribuie ca valoare adresa lui x. Din acest motiv, functia g are posibilitatea de a modifica valoarea lui x. De exemplu, daca in corpul functiei g se foloseste instructiunea:
... *pi = 100; ...

atunci valoarea 100 se atribuie varaibilei x, a carei adresa s-a atribuit la apel lui pi. Exercitii: Sa se scrie o functie care dintr-o data calendaristica definita prin numarul zilei din an si anul respectiv, determina luna si ziua din luna respectiva.
void luna_ziua(int zz, int an, int *zi, int *luna) { /* zz ziua din an an anul; zi pointer a carei valoare este adresa zonei in care se pastreaza ziua

54

*/ // Se determina daca anul este sau nu bisect int bisect = an % 4 == 0 && an % 100 != 0 || an % 400 == 0; int i; extern int nrzile[]; // Tablou definit in apelant cu zilele lunilor for (i = 1; zz > nrzile[i] + (i == 2 && bisect); i++) zz -= (nrzile[i] + (i == 2 && bisect)); *zi = zz; // Valoarea lui zz este pastrata in zona a carei adresa // se transmite prin parametrul efectiv corespunzator // parametrului formal *zi *luna = i; // Valoarea lui i (luna determinata) este stocata in // zona a carei adresa se transmite prin parametrul // efectiv corespunzator parametrului formal *luna }

determinata de functie. luna - pointer a carei valoare este adresa zonei in care se pastreaza ziua determinata de functie.

Expresie lvalue
Pana in prezent s-au folosit expresii de atribuire de forma:
(1) variabila = expresie

unde: variabila se considera ca fiind o variabila simpla, o variabila cu indice (permite accesul la un element de tablou) sau defineste un element de structura. Definitia expresiei de atribuire poate fi completata in momentul de fata prin adaugarea unui nou format:
(2) *ep = expresie

unde: ep este o expresie care defineste un pointer nenul si care nu este un pointer catre o zona constanta. Sa consideram urmatoarele instructiuni de declarare:
int n; double x; void *p; // pointer catre un tip nedefinit de date

In aceste conditii expresiile de atribuire de forma:


n = 123; x = 3.14159;

corespund formatului (1) al expresiei de atribuire. Fie:


p = &n;

atunci
*(int *)p = 123

realizeaza acelasi lucru ca si n = 123, lucru care s-a prezentat mai devreme in acest material. Expresia (int *)p defineste un pointer spre o zona care nu este constanta si deci atribuirea de mai sus corespunde formatului (2) al expresiilor de atribuire. In mod analog, daca
p = &x;

atunci atribuirea:
*(double *)p = 3.14159

realizeaza acelasi lucru ca si expresia


x = 3.14159 Expresiile utilizabile in partea stanga a unei expresii de atribuire se numesc expresii lvalue. Litera l provine dela cuvantul englezesc left. Notiunea de expresie lvalue apartine autorilor limbajului C. Ulterior s-a introdus si notiunea de expresie rvalue, care este o expresie ce se poate utilza in partea dreapta a unei expresii de

atribuire, dar nu si in partea stanga. Din cele prezentate rezulta ca o expresie lvalue poate fi: un nume de variabila simpla; 55

variabila cu indici; constructie ce permite accesul sau modificarea valorii unui element de structura; constructie de forma *ep (unde ep este o expresie care defineste un pointer nenul spre o data care nu este constanta.

Alocarea dinamica a memoriei


n limbajul C++ alocarea dinamica a memoriei si eliberarea ei se pot realiza cu operatorii new si delete. Folosirea acestor operatori reprezinta o metoda superioara, adaptata programarii orientate obiect. Operatorul new este un operator unar care returneaza un pointer la zona de memorie alocata dinamic. n situatia n care nu exista suficienta memorie si alocarea nu reuseste, operatorul new returneaza pointerul NULL. Operatorul delete elibereaza zona de memorie spre care pointeaza argumentul sau. Sintaxa:
tipdata_pointer = new tipdata; tipdata_pointer = new tipdata(val_initializare); tipdata_pointer = new tipdata[nr_elem]; delete tipdata_pointer; delete [nr_elem] tipdata_pointer;

unde: tipdata reprezinta tipul datei (predefinit sau obiect) pentru care se aloca dinamic memorie; tipdata_pointer este o variabila pointer catre tipul tipdata. Pentru a putea afla memoria RAM disponibila la un moment dat, se poate utiliza functia coreleft:
unsigned coreleft(void);

Exercitii: Sa se aloce dinamic memorie pentru o data de tip ntreg:


int *pint; pint=new int; // prelucrari cu *pint delete pint;

Sa se aloce dinamic memorie pentru o data reala, dubla precizie, initializnd-o cu valoarea -7.2.
double *p; p=new double(-7.2); // prelucrari cu *p delete p;

Sa se aloce dinamic memorie pentru un vector de m elemente reale.


double *vector; vector=new double[m]; // prelucrari cu vector de exemplu vector[3]=4 etc. delete [m] vector; // delete vector va dezaloca numai prima componenta

Sa se aloce dinamic memorie pentru o matrice cu 3 linii si 5 coloane de tip double.


double (*a)[5]=new double [3][5]; // prelucrari cu a de exemplu a[2][2]==4.9 etc. delete [3] a;

Sa se aloce dinamic memorie pentru o structura cu doua campuri: unul intreg iar celelat de tip caracter:
struct articol { int nr; char c; }; articol *pa;

56

pa=new articol; cout<<endl<<"nr=";cin>>pa->nr; cout<<endl<<"caracterul ";cin>>pa->c; cout<<endl<<pa->nr<<" "<<pa->c<<" tot la adresa "<<pa<<endl; delete pa;

Structuri dinamice de date


Structurile dinamice de date sunt date structurate ale caror componente se aloca in mod dinamic. Avantajele alocarii dinamice fata de alocarea acelorasi structuri de date in mod static (in segmentul de date) sau volatil (in segmentul de stiva) sunt: memorie suplimentara pentru programe; posibilitatea de a utiliza doar cantitatea de memorie necesara. Alocarea dinamica a componentelor structurii impune un mecanism prin care noua componenta aparuta este legata intr-o succesiune logica de corpul structurii deja format pana atunci. Rezulta ca fiecare componenta, pe langa informatia propriu-zisa pe care o detine, trebuie sa contina si o informatie de legatura cu componenta cu care se leaga logic in succesiune. Aceasta informatie de legatura va fi adresa componentei spre care se realizeaza succesiunea logica, iar mecanismul se mai numeste si alocare inlantuita dupa adrese.

Memoria folosita pentru alocarea dinamica se numeste heap. In heap, structura respectiva va avea zone alocate componentelor sale in locurile gasite disponibile, care nu intotdeauna se succed in ordinea in care este realizata inlantuirea logica. In functie de tipul inlantuirii realizate intre componente, exista urmatoarele tipuri de organizari: structuri liniare (de exemplu o lista care prelucreaza elevii inscrisi la un examen); ele pot fi: liste simplu inlantuite (liniare si circulare) liste dublu inlantuite (liniare si circulare) structuri arborescente (de exemplu reteaua ierarhica a angajatilor dintr-o firma) structuri retea (de exemplu o retea de orase care schimba materiale, combustibili etc).

Liste liniare simplu inlantuite


O componenta a unei liste simplu inlantuite se declara ca o data structurata de tip inregistrare, formata din doua campuri: informatia propriu-zisa (care poate fi de orice tip: numeric, caracter, pointer, tablou, inregistrare) si informatia de legatura (adresa la care e memorata urmatoarea componenta). Ultima componenta va avea informatia de legatura egala cu NULL, avand semnificatia ca dupa ea nu mai urmeaza nimic (retine adresa nici o adresa).

Se poate defini un tip de date structurat numit nod, ale carui campuri le vom numi info si next. De exemplu, pentru o lista de numere reale, putem defini urmatoarea structura:
struct nod { float info; nod *next; };

Componentele listei se vor aloca dinamic. Pentru a putea accesa primul element din lista este necesara o variabila statica sau locala. Fie acesta cap. Valoarea acestei variabile se va atribui dupa fiecare generare de variabila dinamica si astfel ea pastreaza intotdeauna adresa ultimei 57

zone alocate. Cu ajutorul ei putem accesa prima componenta, care contine o legatura catre cea de-a doua etc. si astfel din aproape in aproape putem ajunge la ultimul element al listei (de fapt primul element generat).
nod *cap;

Referirea la informatia primului nod al listei se va face astfel: cap->info Referirea la adresa la care e memorat al doilea nod al listei se va face astfel: cap->next Referirea la informatia celui de-al nod al listei se va face astfel: cap->next->info Operatiile care se pot face utilizand liste simplu inlantuite sunt: crearea listei adaugarea unui element la inceputul listei adaugarea unui element la sfarsitul listei adaugarea unui element in interiorul listei stergerea elementului de la inceputul listei stergerea elementului de la sfarsitul listei stergerea unui element din interiorul listei traversarea listei, cu prelucrarea elementelor acesteia

Exemplu de lista liniare simplu inlantuite


Urmatorul program ofera o modalitate de creare si afisare a unei liste liniare simplu inlantuite care prelucreaza numere intregi:
#include<iostream.h> #include<conio.h> struct nod { int info; nod *next; }; nod *p,*u ; // acceseaza primul respective ultimul nod int n; // numarul de noduri void cre_ad() { // functia de creare si adaugare a unui nou nod nod *c; if(!p) { // daca lista este vida (p==0) se aloca primul nod p = new nod; cout << "valoare primului nod "; cin >> p->info; u = p; // la creare primul si ultimul nod vor fi identici } else { // altfel se adauga un nou nod la sfarsit c = new nod; // se aloca un nou nod cout << "informatia utila :" ; cin >> c->info; u->next = c; // se adauga dupa ultimul nod u = c; // se stabileste noul nod c ca fiind ultimul } u->next = 0; // campul adresa urmatoare a ultimului nod este 0 } void afis() { // Functia parcurge si afiseaza nodurile nod *c; c = p; // se porneste de la primul nod din lista while(c) { // cat timp c retine o adresa nenula cout << c->info< < " "; // se afiseza campul informatie utila c = c->next; // se avanseaza la urmatoarea adresa, la urmatorul nod

58

} cout << endl; } void main() { int b; cout << "n= "; cin >> n; for(int i=1;i<=n;i++) cre_ad(); cout<<endl; afis(); getch(); }

Inserarea si stergerea unui element din lista


Inserarea unui element nou in lista se poate face inainte sau dupa un nod care retine o valoare data (continut util). Evident s-ar putea realiza si inserare pe o pozitie data astfel incat toate elemantele de la acea pozitie incepand se vor deplasa mai la "dreapta". In cazul in care inserarea se face "dupa" se avanseaza cu un nod intermediar c in lista pana la nodul dupa care se va face inserarea, se creaza un nou nod (nodul a) apoi se genereaza legaturile: mai intai de la a la c->next apoi de la c la a. Exista posibilitatea ca ultimul nod sa retina valoarea dupa care se realizeaza inserarea caz in care se modifica ultimul.
c a c a c a c->next c->next c->next

In cazul in care inserarea se face "inainte" se avanseaza cu un nod intermediar c in lista pana la un nod situat inaintea celui cautat , se creaza un nou nod (nodul a) apoi se genereaza legaturile: mai intai de la a la c->next apoi de la c la a. Exista posibilitatea ca primul nod sa retina valoarea inainte de care se realizeaza inserarea caz in care se modifica primul.
c a c a c a c->next c->next c->next

59

In cazul in care se realizeaza stergerea se avanseaza cu un nod intermediar c in lista pana la un nod situat inaintea celui cautat (nodul de sters), se genereaza noile legaturi intre precedentul si urmatorul nodului de sters, se elibereaza spatiul de memorie.
c c->next
c->next->next

c->next

c->next->next

c->next->next

Iata o solutie pentru implementarea functiilor anterioare:


#include<conio.h> #include<fstream.h> struct Nod { int info; Nod *next; }; Nod *prim, *ultim; // Functiile de creare si adaugare se pot comprima intr-un singura care poate // testa daca exista un prim nod si in functie de rezultatul testului va // realiza alocarea primului nod sau se va adauga un nou nod la sfarsit void creare_adaugare() { if(prim == NULL) { prim = new Nod; cout << "introduceti valoarea retinuta in primul nod:"; cin >> prim->info; prim->next = 0; // la crearea listei va exista un singur nod, primul // si prin urmare adresa urmatoare lui este 0 ultim = prim; // evident, avand un singur element acesta va fi si // primul si ultimul } else { Nod *c; c = new Nod; cout << "valoarea de adaugat in lista "; cin >> c->info; ultim->next = c; // se "agata" noul nod c, dupa ultimul din lista ultim = c; // evident noul nod e ultimul... ultim->next = 0; //...si dupa ultimul nu e nimic, deci nici o adresa } } void listare() { // Afisarea datelor din lista Nod *c; c = prim; while(c != 0) { // cat timp mai sunt noduri in lista cout << c->info <<" "; c = c->next; // se avanseza in lista trecand la urmatoarea adresa } cout << endl; } // Inserarea dupa o valoare val transmisa din main() void inserare_dupa(int val) { Nod *c,*a; // Nod *a retine adresa nodului ce se va insera in lista // cu *c se face avansarea in lista pana la nodul ce contine valoarea dupa // care se face inserarea; evident se porneste de la primul;

60

} // Inserarea inainte de o valoare val transmisa din main() void inserare_inainte(int val) { Nod *c, *a; // Nod *a retine adresa nodului ce se va insera in lista // cu *c se face avansarea in lista pana la nodul ce contine valoarea // inainte care se face inserarea; evident se porneste de la primul; c = prim; // pentru ca exista si posibilitatea ca valoarea inainte de care se face // inserarea sa fie retinuta de primul nod se va face un test si in caz // afirmativ se va stabili un nou prim element if(prim->info == val) { c = new Nod; cout << "valoare de inserat "; cin >> c->info; c->next = prim; prim = c; } else { while(c->next->info!=val &&c) //c se pozitioneaza inainte de nodul cautat c = c->next; a = new Nod; cout << "valoarea de inserat "; cin >> a->info; a->next = c->next; c->next = a; } // Stergerea nodului al carui continut a fost transmis ca parametru void stergere(int val) { Nod *c, *a; // a se sterge, c este precedentul sau. Se va genera o noua // legatura intre c si a->next c = prim; if(prim->info==val) { //daca primul nod retine val se sterge primul a = prim; // se retine in a prim = prim->next; // primul va deveni urmatorul element delete a; //se elibereaza memoria } else { while(c->next->info!=val &&c) // se pozitioneaza pe elementul ce c = c->next; // urmeaza a fi sters a = c->next; c->next = a->next; if(a==ultim) ultim = c; delete a; // se elibereaza memoria } } void main() { int i, n, val_info; cout << "Numarul de noduri din lista: "; cin >> n; for(i=1; i<=n; i++) creare_adaugare(); listare();

c = prim; while(c->info != val c = c->next; a = new Nod; cout << "valoarea de a->next = c->next; c->next = a; if(c == ultim) ultim // dupa care

&& c) inserat "; cin >> a->info;

= a; // exista si posibilitatea ca valoarea se face inserarea sa fie retinuta de ultimul nod

61

cout<<"dupa ce valoare din lista se realizeaza inserarea "; cin>>val_info; inserare_dupa(val_info); listare(); creare_adaugare(); listare(); cout<<"valoarea inainte de care se face inserarea "; cin>>val_info; inserare_inainte(val_info); listare(); cout<<endl<<"valoarea ce urmeaza a fi stearsa: "; cin>>val_info; stergere(val_info); listare();

Ordonarea unei liste


Principiul acestei operatii este acelasi ca si la vectori. Ordonarea se poate face prin aceleasi metode. In exemplul urmator este prezentata ordonarea prin metoda Bubble Sort:
#include<iostream.h> #include<conio.h> struct Nod { int info; Nod *next; }; Nod *prim, *ultim; void creare() { prim=new Nod; cout << "Valoarea retinuta in primul nod:"; cin >> prim->info; prim->next = 0; // la crearea listei va exista un singur nod, // primul si prin urmare adresa urmatoare lui este 0 ultim = prim; // evident, avand un singur element acesta va fi // si primul si ultimul } void adaugare() { Nod *c; c = new Nod; cout << "valoarea de adaugat in lista "; cin >> c->info; ultim->next = c; // se "agata" noul nod c, dupa ultimul din lista ultim = c; // evident noul nod e ultimul... ultim->next = 0; //...si dupa ultimul nu e nimic, deci nici o adresa } void ordonare() { Nod *c;int ord,aux; do { c = prim; ord = 1; while(c->next) { if(c->info > c->next->info) { aux = c->info; c->info = c->next->info; c->next->info = aux; ord = 0; } c = c->next;} } while(ord == 0); } void listare() { Nod *c; c = prim; while(c != 0) { // cat timp mai sunt noduri in lista

62

cout << c->info << " "; c = c->next; // se avanseza in lista trecand la urmatoarea adresa } cout<<endl; } void main() { int n, i, val, nr; cout << "cate elemente va avea lista? "; cin >> n; creare(); for(i=2; i<=n; i++) adaugare(); cout << "Elementele listei sunt: "; listare(); ordonare(); cout << endl <<"elementele listei dupa ordonare;" << endl; listare(); getch(); }

Este preferabil ca in unele situatii sa se genereze de la inceput o lista ordonata (crescator de exemplu) si astfel in functia de creare a listei vor exista urmatoarele situatii: inserarea se face inainte de primul nod (informatia mai mica decat p->info) inserarea se face dupa ultimul nod (informatia mai mare decat u->info) inserarea se face in interior Iata o solutie pentru functia de creare ordonata a listei:
#include<iostream.h> #include<conio.h> struct nod { int info; nod* next; }; nod *p, *u; void cre_ord(int x) { // creare lista ordonata nod *c,*a; c = new nod; c->info = x; if(!p) { // test lista vida p = new nod; p->info = x; u = p; u->next = 0; } else if(x <= p->info) { // inserare inainte de primul c->next = p; p = c; } else if(x > u->info) { // inserare dupa ultimul u->next = c; u = c; u->next = 0; } else { // inserare in interior a = p; while(x > a->next->info) a = a->next; c->next = a->next;

63

a->next = c;

} } void afisare() { nod *c; c = p; cout << endl << "continutul listei " << endl; while(c) { cout << c->info << " "; c = c->next; } } void main() { int n, y; cout << "nr de noduri "; cin >> n; for(int i=1; i<=n; i++) { cout << "informatia "; cin >> y; cre_ord(y); } afisare(); getch(); }

Structura de date de tip coada


Aceasta structura de date este un caz particular de lista care functioneaza pe principiul FIFO (first in first out, primul intrat este primul servit). Prin urmare principalele prelucrari care se refera la aceasta structura de date vor fi: creare coada listare coada (parcurgere) adaugare la sfarsir stergere primului element prelucrare primului element Specific acestei structuri de date este faptul ca adaugarea se va face intotdeauna dupa ultimul element in timp ce prelucrarea (sau stergerea) se va face la celalat capat. Pentru a prelucra o structura de tip coada vor fi necesari doi pointeri: unul il vom numi varful cozii (primul nod creat) in timp ce la capatul opus ne vom referi la ultimul element. Functia pune(), creaza structura de tip coada cand aceasta este vida sau adauga un nou element la sfarsit in caz contrar. Functia scoate(), elimina elementul din varful cozii. Fie o structura de tip coada care retine 4 numere intregi: 10,20,30,40
10 20 30 40

In urma adaugarii valorii 50 , prin apelul functiei pune( ) se va obtine:


10 10 20 20 30 30 40 40

50 50

In urma eliminarii primului element,10, retinut de varful cozii, prin apelul functiei scoate se va obtine:
10 20 20 30 30 40 40 50 50

64

In continuare se prezinta o solutie pentru implementarea functiilor anterioare. Structura de tip coada prelucreaza numere intregi.
#include<iostream.h> #include<conio.h> struct nod{ int info; nod *next; }; void pune(nod* &v, nod* &sf, int x) { nod *c; if(!v) { v = new nod; v->info = x; v->next = 0; sf = v; } else { c = new nod; sf->next = c; c->info = x; sf = c; sf->next = 0; } } void afisare(nod *v) { nod *c; c = v; while(c) { cout << c->info << " "; c = c->next; } } void scoate(nod* &v) { nod* c; if(!v) cout << "Coada este vida si nu mai ai ce elimina!!!"; else } c = v; v = v->next; delete c; } } void main() { int n,a; nod *varf = 0,*ultim = 0; // varful si ultimul nod al cozii cout<<"Numarul initial de noduri "; cin>>n; for(int i=1; i<=n; i++) { cout << "valoarea de adaugat in coada "; cin >> a; pune(varf, ultim, a); } cout << endl; afisare(varf); int nre, nra; cout << endl << cate adaugari ?"; cin >> nra; for(i=1; i<=nra; i++) { cout << "Valoarea de adaugat "; cin >> a; pune(varf, ultim, a);

65

} cout << endl << "Dupa adaugare" << endl; n += nra; cout << "coada are " <<n << " elemente" << endl; afisare(varf); cout << endl << "Cate eliminari ?"; cin >> nre; for(i=1;i<=nre;i++) scoate(varf); cout << endl << "Dupa eliminare" << endl; n -= nre; cout << "coada are " << n << " elemente" << endl; afisare(varf); // se prelucreza varful cozii: de exemplu se poate dubla continutul varf->info = 2 * varf->info; cout << endl << "Dupa dublarea valorii varfului " << endl; afisare(varf); getch();

Se observa ca in main se realizeaza prelucrarea (modificarea) informatiei primului nod.

Structura de date de tip stiva


Aceasta structura de date este un caz particular de lista care functioneaza pe principiul LIFO (last in first out, ultimul intrat este primul servit, puteti sa va ganditi la o stiva de materiale pentru care un nou material se va adauga intotdeauna deasupra si se va extrage tot de deasupra). Prin urmare principalele prelucrari care se refera la aceasta structura de date vor fi: creare stiva listare stiva (parcurgere in ordine inversa creerii) adaugare la sfarsit ( peste varful stivei, operatie numita push()) stergere element din varful stivei (operatie numita pop()) prelucrarea varfului stivei Specific acestei structuri de date este faptul ca prelucrarile se fac intotdeauna la elementul de la acelasi capat, element pe care il vom numi varf. Functia push(), creaza stiva cand aceasta este vida sau adauga un nou element in caz contrar. Functia pop(), elimina elementul din varful stivei. Fie o stiva de numere intregi pentru care elementele sunt adaugate in ordinea: 10, 20, 30, 40 La primul pas se creaza stiva caz in care primul element creat va fi varful stivei: vf Apoi se adauga prin operatia push() un nou element, 20: vf 20 20
10 vf In continuare se adauga 30 care va deveni noul varf: 30 30 10 10

vf

20

20

10

10

66

La sfarsit se adauga 40:


40 40 30 30 20 20 10 10

vf

Afisarea se va face in ordine inversa generarii si prin urmare se va afisa: 40, 30, 20, 10. Pentru stergere, prin operatia pop( ) se va sterge elementul din varful stivei, 40, dupa care noul varf va deveni precedentul sau, 30: pop() vf 40 rezulta:
30 20 20 10 10 30

vf

Pentru a prelucra aceasta structura de date va fi suficient un singur pointer, pentru varf. Iata o solutie de implementare:
// Prelucrari stiva : (LIFO) #include<iostream.h> #include<conio.h> struct nod { int info; nod *back; }; nod *varf; void push(nod* &v, int x) { nod *c; if(!v) { v = new nod; v->info = x; v->back = 0; } else { c = new nod; c->back = v; c->info = x; v = c; } } void afisare(nod *v) { nod *c; c = v; while(c) {

67

cout << c->info << " "; c = c->back; } } void pop(nod* &v) { nod* c; if(!v) cout << "stiva este vida si nu mai ai ce elimina!!!"; else { c = v; v = v->back; delete c; } } void main() { int n, a; cout << "numarul initial de noduri "; cin >> n; for(int i=1; i<=n; i++) { cout << "valoarea de adaugat in stiva "; cin >> a; push(varf, a); } cout << endl; afisare(varf); int nre, nra; cout <<endl << "cate adaugari ?"; cin >> nra; for(i=1; i<=nra; i++) { cout << "valoarea de adaugat "; cin >> a; push(varf, a); } cout << endl << "dupa adaugare" << endl; n +=nra; cout << "stiva are " << n << " elemente" << endl; afisare(varf); cout << endl << "cate eliminari ?"; cin >> nre; for(i=1; i<=nre; i++) pop(varf); cout << endl << "dupa eliminare" << endl; n -=nre; cout << "stiva are " << n << " elemente" << endl; afisare(varf); // se prelucreza varful stivei: de exemplu se poate dubla continutul varf->info = 2 * varf->info; cout << endl << "dupa dublarea valorii varfului " << endl; afisare(varf); getch(); }

Liste liniare dublu inlantuite


O componenta a unei liste dublu inlantuite se declara ca o data structurata de tip inregistrare, formata din trei campuri: informatia propriu-zisa (care poate fi de orice tip: numeric, caracter, pointer, tablou, inregistrare) si informatiile de legatura (adresa la care e memorata urmatoarea componenta si adresa la care e memorata precedenta componenta). Ultima componenta va avea informatia de legatura corespunzatoare urmatoarei adrese NULL (sau 0), cu semnificatia ca dupa ea nu mai urmeaza nimic (retine adresa nici o adresa a urmatoarei componente).La fel si in cazul primei componente pentru campul adresa precedenta. 68

0
adr prec informatia utila adr urm

Conform celor enuntate anterior memorarea si prelucrarea acestor structuri de date este similara cu cea a listelor liniare simplu inlantuite, ba chiar unele prelucrari (cum ar fi stergerea si inserarea inainte se simplifica avand intr-un mod facil acces la componenta anterioara). Se poate defini un tip de date structurat numit nod, ale carui campuri le numim info pentru informatia utila, next pentru adresa urmatoare si back pentru adresa precedenta. De exemplu, pentru o lista de numere intregi, putem defini:
struct nod { int info; nod *next, *back; };

Componentele listei se vor aloca dinamic. Prelucrarea listei se va face tot prin intermediul a doi pointeri care acceseaza primul si respectiv ultimul element al listei. Operatiile care se pot face utilizand liste dublu inlantuite sunt: crearea listei adaugarea unui element la inceputul listei adaugarea unui element la sfarsitul listei adaugarea unui element in interiorul listei stergerea elementului de la inceputul listei stergerea elementului de la sfarsitul listei stergerea unui element din interiorul listei traversarea listei, cu prelucrarea elementelor acesteia, intr-un sens sau celalalt Iata o solutie de implementare a functiilor de creare ( si adaugare) si de parcurgere stangadreapta si dreapta-stanga:
#include<iostream.h> #include<conio.h> struct Nod { int info; Nod *next, *back; }; Nod *prim, *ultim; void creare_lista() { Nod *c; c = new Nod; cout << "Informatia "; cin >> c->info; if(!prim) { prim = c; prim->next = 0; prim->back = 0; ultim = prim; } else { ultim->next = c; c->back = ultim; ultim = c; ultim->next = 0; } } void listare_stanga_dreapta() { Nod *c; c = prim;

69

while(c) { cout << c->info << " "; c = c->next; } } void listare_dreapta_stanga() { Nod *c; c = ultim; while(c) { cout << c->info << " "; c = c->back; } } void main() { int n, i; cout << "cate elemente va avea lista? "; cin >> n; for(i=1; i<=n; i++) creare_lista(); cout << endl << "Elementele listei de la stanga la dreapta sunt:" << endl; listare_stanga_dreapta(); cout << endl << "Elementele listei de la dreapta la stanga sunt:" << endl; listare_dreapta_stanga(); getch(); }

Liste circulare
In unele aplicatii este necesar sa se prelucreze structuri de date inlantuite simplu sau dublu circulare. Acestea se obtin din liste liniare printr-o singura operatie daca lista este simpla sau prin doua operatii daca lista este dubla: Fie urmatoarea lista dubla definita astfel:
struct nod { int info; nod *next, *back; }; nod*prim,*ultim;

prim 0 Lista devine circulara prin operatiile:


ultim->next = prim; prim->back = ultim; // urmatorul ultimului devine primul // precedentul primului devine ultimul

ultim 0

Prin urmare nici un nod nu va mai contine pentru campurile *next sau *back valoarea 0 (ultimul si primul in cazul listelor liniare) ceea ce va determina modificarea functiei de parcurgere in scopul prelucrarilor . prim

ultim

70

Iata o solutie de generare a unei liste duble circulare de intregi pornind de la o lista liniara dublu inlantuita si o modalitate de parcurgere a listei circulare astfel obtinute:
#include<iostream.h> #include<conio.h> struct Nod { int info; Nod *next, *back; }; Nod *prim, *ultim; int n; void creare_lista() { Nod *c; c = new Nod; cout << "Informatia: "; cin >> c->info; if(!prim) { prim = c; prim->next = 0; prim->back = 0; ultim = prim; } else { ultim->next = c; c->back = ultim; ultim = c; ultim->next = 0; } } void listare_stanga_dreapta() { Nod *c; c = prim; while(c) { cout << c->info << " "; c = c->next; } } void creaza_lista_circulara() { ultim->next = prim; prim->back = ultim; } void afiseaza_lista_circulara(Nod *c) { // Parcurge lista pornind de la o adresa for(int i=1; i<=n; i++) { cout << c->info << " "; c= c->next; } } void main() { int i; cout << "cate elemente va avea lista? "; cin >> n; for(i=1; i<=n; i++) creare_lista(); cout << endl << "Elementele listei de la stanga la dreapta sunt:" << endl; listare_stanga_dreapta(); creaza_lista_circulara(); cout << endl << "afiseaza lista circulara incepand de la primul:" << endl; afiseaza_lista_circulara(prim); cout << endl << "afiseaza lista circulara incepand cu al doilea:" << endl;

71

afiseaza_lista_circulara(prim->next); cout << endl << "afiseaza lista circulara incepand cu ultimul:" << endl; afiseaza_lista_circulara(ultim); getch();

Prelucrarea sirurilor de caractere


Declararea variabilelor sir de caractere
In limbajul C++ nu exista un tip fundamental pentru sirurile de caractere (string), asa cum exista in alte limbaje de programare (Visual Basic, Turbo Pascal etc.), ele se declara ca tablouri de tip char. De exemplu, declaratia:
char a[10], b[3];

defineste doua tablouri unidimensionale (vectori) a si b de tip char avand 10 si respectiv 3 componente. Ca la orice vector, numele are semnificatia de adresa nemodificabila catre prima componenta (cea de indice 0). La declarare se poate face si initializare, respectand regulile de de initializare ale tablourilor. De exemplu, urmatoarea declaratie:
char a[3] = {a, b, c]; defineste un vector de tip char avand trei componente si le initializeaza astfel: a[0]=a, a[1]=b si a[2]=c.

Mai simplu, vectorul a se poate declara si asa:


char a[] = {a, b, c];

compilatorul determinand numarul de componente dupa valorile de initializare. Totusi, folosirea acestui procedeu pentru declararea sirurilor de caractere implica o munca chinuitoare. Reamintim ca: sirul de caractere este o succesiune de caractere cuprinse intre doua caractere ghilimele (). De exemplu, Acesta este un sir de caractere, Borland C++ etc.
Observatie: Orice sir de caractere se termina prin caracterul NULL (\0). Acesta este pus automat de compilator si ocupa ultimul byte din zona alocata memorarii sirului. Deci sirul Borland C++ va ocupa 12 bytes. La declarare, unui vector cu componente de baza de tip char i se poate atribui un sir de

caractere. De exemplu, urmatoarea declaratie:


char sir[12] = Borland C++; (1) defineste un tablou de tip char cu 12 componente pe care le initializeaza astfel: sir[0]=B, sir[1]=o, , sir[10]=+, sir[11]=\0.

Declaratia anterioara (1) poate fi scrisa si sub forma:


char sir[] = Borland C++;

caz in care compilatorul va determina numarul de bytes necesari pentru memorarea sirului, adaugand unul pentru caracterul NULL.
Observatii: Daca la declarare, numarul de componente este indicat explicit, iar sirul de initializare are un numar de caractere mai mic decat numarul specificat de componente, componentele ramase neinitializate vor fi initializate cu caracterul NULL. Daca sirul are un numar de caractere (fara caracterul NULL) mai mare decat numarul de componente indicat, se semnaleaza eroare de sintaxa. Daca sirul are un numar de caractere (fara caracterul NULL) egal cu numarul de componente specificat, caracterul NULL nu se memoreaza.

Initializarea unei zone de memorie cu un sir de caracter se poate realiza si prin utilizarea pointerilor la tipul char. De exemplu, declaratia anterioara se poate realiza si in maniera:
char *psir = Borland C++; (2)

Ca urmare a acestei declaratii, in faza de compilare se construieste sirul Borland C++ intr-o zona disponibila de memorie, a carei adresa este stocata in pointerul psir. In acest caz, 72

adresarea caracterului r din a doua pozitie a sirului se poate face prin constructia: *(psir+1), care reflecta aritmetica de pointeri pentru siruri. Intre declaratiile (1) si (2) exista deosebiri esentiale in ceea ce priveste dimensiunea memoriei ocupate, precum si posibilitatea de a modifica adrese. In varianta (2) se rezerva 2 sau 4 bytes pentru pointerul psir. In timp ce in varianta (1), odata alocata memoria pentru vectorul sir, adresa acestuia nu poate fi modificata, in varianta (2) pointerul psir poate fi refolosit utlerior pentru a referi alte adrese de variabile de tip caracter (cu riscul pierderii adresei constantei sir, de la initializare). Reprezentarea sirurilor ca tablouri de caractere si modul de interpretare a tablourilor in C asigura ca intr-un tablou cu doua dimensiuni, adresarea cu un indice sa individualizeze un anumit sir din multimea sirurilor, iar adresarea cu doi indici sa localizeze un caracter. De exemplu, programul urmator prezinta modul de initializare a unui tablou bidimensional cu componente de tip char si posibilitatile de adresare, utilizand unul sau doi indici. La rulare se afiseaza zilele saptamanii, in doua variante, precum si prima litera a fiecarei zile.
#include <iostream.h> void main() { char zile[7][10] = {"luni", "marti", "miercuri", "joi", "vineri", "sambata", "duminica"}; int i, j; cout << "Varianta I\n"; // Adresarea cu un singur indice for (i = 0; i < 7; i++) cout << zile[i] << "\n"; cout << "Varianta II\n"; // Adresarea cu doi indici for (i = 0; i < 7; i++) { for (j = 0; (j < 10) && (zile[i][j] != '\0'); j++) cout << zile[i][j]; cout << "\n"; } for (i = 0; i < 7; i++) // Afisarea primei litere a fiecarei zile cout << zile[i][0]; }

Aceeasi probleme rezolvata folosind pointeri evidentiaza si mai pregnant tratarea ca entitate distincta a fiecarui sir, a carei adresa va fi continuta intr-un element al vectorului de pointeri pzile.
#include <stdio.h> void main() { char *pzile[7] = {"luni", "marti", "miercuri", "joi", "vineri", "sambata", "duminica"}; int i, j; cout << "Varianta I\n"; for (i = 0; i < 7; i++) cout << pzile[i]) << "\n"; cout << "Varianta II\n"; for (i = 0;i < 7; i++) { for (j = 0; *(pzile[i] + j) != '\0'; j++) cout << *(pzile[i] + j); cout << "\n"; } for (i = 0; i < 7; i++) cout << *pzile[i] << "\n"; }

73

Functii de intrare pentru siruri de caractere


Pentru realizarea operatiilor de I/E, limbajul C, prin descriptorii %c si %s in functiile scanf si printf, ofera o modalitate comoda de citire si afisare a caracterelor si sirurilor de caractere. Aceste functii au fost prezentate detaliat intr-o lectie anterioara si utilizate in toate exemplele folosite. Din aceste motive in continuare ne vom referi la alte functii ce se pot folosi pentru realizarea operatiilor de I/E. Un inconvenient major al functiei scanf il constituie faptul ca, caracterele spatiu, Tab si NewLine actioneaza implicit ca separatori si deci prin aparitia lor in sirul de intrare forteaza sfarsitul sirului, neputand fi citit intr-o singura variabila un text format din doua sau mai multe cuvinte. Pe langa aceste functii, limbajul C pune la dispozitie si functii de I/E pentru un caracter si respectiv pentru siruri de caractere.

Functii pentru citirea si afisarea caracterelor


Functia getchar() asigura preluarea intr-o variabila de tip int sau char a unui caracter de la terminal; prototipul functiei se gaseste in fisierul stdio.h. Varianta getch() realizeaza acelasi lucru fara a returna si ecoul caracterului citit; in plus, la citirea unui caracter acesta este preluat imediat din zona tampon a tastaturii, fara a se astepta prezenta unui terminator (Enter). Prototipul functiei getch() se gaseste in fisierul conio.h. Echivalentele lor pentru iesire sunt functiile putchar() - prototipul in stdio.h si respectiv putch() - are prototipul in conio.h. De exemplu, urmatoarea secventa de program asigura citirea si scrierea unui caracter:
int c; c = getchar(); putchar(c);

S-a definit c ca fiind de tip int, datorita faptului ca functiile getchar si getch sunt declarate ca fiind de tip int, pentru a putea prelua in afara caracterelor obisnuite si pe cele care transmit doua coduri la apasare (de exemplu tastele sageata) sau coduri asociate functiilor de fisier: New Line, EOF (Ctrl+Z) etc., necesare testarii unor conditii. De exemplu, urmatorul program copiaza din fisierul standard de intrare (stdin), pe fisierul satndard de iesire (stdout), caracter cu caracter, pana la intalnirea sfarsitului de fisier:
#include <stdio.h> void main() { int c; while((c = getchar()) != EOF) putchar(c); }

Caracterele speciale (Tab, Esc, Cr, FF, Backspace etc.) executa diferite functii la imprimare. Daca se doreste simpla lor afisare, fara a fi executate, simbolurile asocaite lor trebuie filtrate si transmise in conformitate ci conventiile limbajului C, ca siruri de caractere aferente secevntelor escape. Un astfel de filtru este realizat in programul urmator:
#include <stdio.h> #include <conio.h> #include <ctype.h> void main() { int c; while ((c = getch()) != 0x1a) switch (c) { case '\n': printf("\\n"); break; case '\r': printf("\\r"); break; case '\t': printf("\\t"); break; case '\v': printf("\\v"); break; // ... alte coduri de control default:

// // // //

Coduri de control al imprimarii

74

} }

if (isprint(c)) putchar(c); // coduri imprimabile else if (c >= 1 && c <= 26) printf("^%c", c + 'A' -1); // ^X coduri CTRL+X else printf("\\%3o", c); // restul, afisare in octal

Programul asigura afisarea corespunzatoare a tuturor codurilor ASCII citite, fara ecou si fara a astepta terminator de introducere. In program s-a apelat o macrocomanda de clasificare, isprint, pentru a verifica apartenenta caracterului introdus (daca este sau nu tiparibil). Limbajul C++ pune la dispozitie mai multe macrocomenzi de clasificare (care se gasesc in fisierul antet ctype.h), pentru a verifica apartenenta la o clasa a unui caracter. Principalele macrocomenzi de clasificare sunt:
Macrocomand a isalnum(c) isalpha(c) isascii(c) isdigit(c) isgraph(c) islower(c) isprint(c) isspace(c) isupper(c) isxdigit(c) Descriere Verifica daca c este o litera (A la Z sau a la z) sau o cifra (0 la 9) Verifica daca c este este o litera (A la Z sau a la z) Verifica daca byte-ul de ordin inferior al lui c este in intervalul 0 la 127(0x00-0x7F) Verifica daca c este o cifra (0 la 9) Verifica daca c este un caracter imprimabil, la fel ca isprint, cu expectia faptului ca spatiul este exclus Verifica daca c este o litera mica (a la z) Verifica daca c este un caracter imprimabil (0x20 la 0x7E) Verifica daca c este un spatiu, tab, retur de car (carriage return), linie noua (new line), tab vertical salt de pagina (formfeed) - 0x09 la 0x0D, 0x20 Verifica daca c este o litera mare (A la Z) Verifica daca c este o cifra hexazecimala (0 la 9, A la F, a la f)

Fiecare macrocomanda este un predicat care returneaza o valoare diferita de zero pentru adevarat si o valoare egala cu 0 pentru fals. In programul prezentat anterior, prin folosirea unei structuri de selectie multipla se impart codurile ASCII in patru clase, astfel: caractere imprimabile pe care le afiseaza ca atare; caractere de control pe care le afiseaza ca doua caractere, din care primul este ^; secvente escape pe care le afiseaza conform conventiilor din limbajul C; alte coduri pe care le afiseaza sub forma secventelor escape in octal.

75

Functii pentru prelucrarea sirurilor de caractere


Biblioteca standard a limbajului contine o serie de functii care permit operatii cu siruri de caractere. Majoritatea acestor functii au prototipul in fisierul string.h. Inainte de a prezenta principalele functii de prelucrare a sirurilor de caractere, vom reaminti cateva dintre caracteristicile sirurilor de caractere: Un sir de caractere se pastreaza intr-o zona de memorie organizata ca tablou unidimensional de tip char. Fiecare caracter se pastreaza pe cate un byte prin codul sau ASCII, care este de fapt o valoare numerica. Dupa ultimul caracter al sirului se pastreaza caracterul NULL (\0). Pentru a opera cu un sir de caractere se poate utiliza numele tabloului in care sunt pastrate caracterele sau pointeri variabili spre sirul respectiv de caractere. Numele tabloului este considerat un pointer constant spre sirul respectiv. Cele mai frecvente prelucrari asupra sirurilor de caractere sunt urmatoarele: Determinarea lungimii sirurilor de carctere. Copierea sirurilor de caractere. Concatenarea sirurilor de caractere. Compararea sirurilor de caractere. Functiile standard prin care se realizeaza aceste operatii au fiecare un nume care incepe cu prefixul str (prescurtare de la string).

Lungimea unui sir de caractere


Lungimea unui sir de caractere se defineste prin numarul de caractere proprii care intra in compunerea sirului respectiv. Caracterul NULL este un caracter impropriu si el nu este luat in considerare la determinarea lungimii unui sir de caractere. Prezenta acestui caracter este necesara, deoarece el indica terminarea sirului si la determinarea lungimii unui sir se numara caracterele sirului pana la intalnirea caracterului NULL. Functia pentru determinarea lungimii unui sir de caractere se numeste strlen si apelarea ei se realizeaza folosind urmatorul format de apel:
strlen(sir)

unde: sir reprezinta numele tabloului ce contine sirul a carui lungime se va determina. Exemple:
char *const p = Borland International; unsigned n; ... n = strlen(p);

Lui n i se atribuie valoarea 21, care reprezinta numarul de caractere proprii din compunerea sirului referit de p.
char tab[] = Acesta este un sir; int n; n = strlen(tab);

Lui n i se atribuie valoarea 18.


int n; n = strlen(Acesta este un sir);

Lui n i se atribuie aceeasi valoare ca si in exemplul anterior.

76

Copierea unui sir de caractere


Pentru a copia un sir de caractere din zona de memorie in care se afla intr-o alta zona de memorie se foloseste functia strcpy. Apelarea functiei strcpy se realizeaza utilizand urmatorul format:
strcpy(destinatie, sursa)

unde: destinatie reprezinta adresa zonei de memorie in care se vor copia caracterele; sursa reprezinta adresa zonei de memorie in care se gasesc caracterele de copiat. Atat sursa cat si destinatie pot fi nume de tablouri de tip char sau pointeri constanti catre zonele de memorie ce contin/vor contine caractere. Functia strcpy copiaza sirul de caractere spre care refera sursa in zona de memorie a carei adresa de inceput este valoarea lui destinatie. Functia copiaza atat caracterele proprii sirului, cat si caracterul NULL de la sfarsitul sirului respectiv. La revenire, functia returneaza adresa de inceput a zonei in care s-a transferat sirul, adica chiar valoarea lui destinatie. Exemple: 1.
char tab[] = Borland International; char sir[sizeof tab]; // Are acelasi numar de elemente ca si tab ... strcpy(sir, tab);

2.
char sir[100]; strcpy(sir, Copiere sir de caractere);

3.
char *p = Sir de caractere; char sir[100]; char *q; q = strcopy(sir, p); // dupa copiere, lui q i se atribuie adresa // zonei in care s-a copiat sirul de caractere.

Pentru a copia cel mult n caractere se foloseste functia strncpy, care se apeleaza respectand urmatoarea sintaxa:
strcpy(destinatie, sursa, numar_caractere)

unde: destinatie reprezinta adresa zonei de memorie in care se vor copia caracterele; sursa reprezinta adresa zonei de memorie in care se gasesc caracterele de copiat. numar_caractere reprezinta numarul de caractere ce se vor copia. Daca numar_caractere este mai mare decat lungimea sirului referit de sursa, atunci toate caracterele sirului respectiv se transfera in zona referita de destinatie.

Concatenarea sirurilor de caractere


Pentru concatenarea (unirea) a doua siruri limbajul dispune de doua functii: strcat si strncat.

Functia strcat
Concatenarea a doua siruri de caractere (adaugarea celui de-al doilea sir la sfarsitul primului sir) se realizeaza cu ajutorul functiei strcat. Formatul de apelare a functiei este:
strcat(destinatie, sursa)

unde: destinatie este adresa zonei sirului de caractere la sfarsitul caruia se va adauga sirul concatenat; sursa este adresa sirului ce se va aduaga. 77

Aceasta functie copiaza sirul de caractere referit de sursa, in zona de memorie ce urmeaza imediat dupa ultimul caracter propriu al sirului referit de destinatie. Se presupune ca zona referita de pointerul destinatie este suficienta pentru a pastra caracterele proprii celor doua siruri care se concateneaza, plus caracterul NULL, care termina sirul rezultat in urma concatenarii. Functia strcat returneaza valoarea pointerului destinatie. Exemple:
char dest[] = Limbajul C++; char sursa[] = este C dezvoltat; strcat(dest, ); // Adauga la sfarsitul sirului dest un spatiu strcat(dest, sursa); // Adauga textul din sursa dupa caracterul // spatiu din sirul dest.

Functia strncat
Functia strncat adauga la sfarsitul primului sir prmele n caractere din cel de-al doilea sir. Forma de apelare a functiei strncat este:
strncat(destinatie, sursa, nr_car)

unde: destinatie este adresa zonei sirului de caractere la sfarsitul caruia se vor adauga cele nr_car din sirul referit de sursa; sursa este adresa sirului in care se gasesc caracterele ce se vor concatena cu sirul referit de destinatie; nr_car este numarul de caractere din sirul referit de sursa ce se vor concatena cu sirul referit de destinatie. Functia strncat concateneaza la sfarsitul sirului referit de destinatie, cel mult nr_car ale sirului referit de sursa. Daca nr_car este mai mare decat lungimea sirului referit de sursa, atunci se concateneaza intregul sir, altfel numai primele nr_car caractere. Exemplu:
char sir1[]=Limbajul E este mai bun decat ; char sir2[] = limbajul C++, care este un superset a lui C; strncat(sir1, sir2, 12);

Dupa revenirea din functie, sir1 contine: Limbajul E este mai bun decat limbajul C++

Compararea sirurilor de caractere


Sirurile de caractere se compara folosind codurile ASCII ale caracterelor din compunerea lor. Fie s1 si s2 doua tabouri unidimensionale de tip caracter utilizate pentru a pastra, fiecare, cate un sir de caractere. Rezultatul compararii celor doua siruri se determina dupa urmatoarele reguli: Cele doua siruri sunt egale daca au lungimi egale si s1[i] = s2[i] pentru toate valorile posibile ale lui i. Sirul s1 este mai mic decat s2, daca exista un indice i astfel incat:
s1[i] < s2[i]

si
s1[j] = s2[j] pentru j = 1, 2, , i 1.

Sirul s1 este mai mare decat s2, daca exista un indice i astfel incat:
s1[i] > s2[i]

si
s1[j] = s2[j] pentru j = 1, 2, , i 1. Compararea sirurilor de caractere se realizeaza folosind urmatoarele functii: strcmp, strncmp, stricmp si strincmp.

78

Functia strcmp
Functia strcmp compara doua siruri de caractere si poate fi apelata folosind urmatoarea sintaxa:
strcmp(sir1, sir2)

unde: sir1 si sir2 reprezinta pointeri catre zonele in care se gasesc caracterele celor doua siruri ce se vor compara. Functia strcmp returneaza: o valoare negativa daca sirul referit de pointerul sir1 este mai mic decat sirul referit de pointerul sir2; zero daca sirul referit de pointerul sir1 este mai mic decat sirul referit de pointerul sir2; o valoare pozitiva daca sirul referit de pointerul sir1 este mai mare decat sirul referit de pointerul sir2;

Functia strncmp
Sintaxa de apelare a functiei strncmp este:
strncmp(sir1, sir2, n)

unde: sir1 si sir2 reprezinta pointeri catre zonele in care se gasesc caracterele celor doua siruri ce se vor compara. n reprezinta numarul maxim de caractere din ambele siruri ce se vor lua in considerare. Daca minimul dintre lungimile celor doua siruri este mai mic decat n, functia strncmp realizeaza aceeasi comparatie ca si functia strcmp. Valorile returnate de functia strncmp sunt acelasi ca si in cazul functie strcmp.

Functia stricmp
Daca la compararea a doua siruri de caractere nu dorim sa se faca distinctie intre literele mari si mici, atunci se va utiliza funtia stricmp. Formatul de apelare este:
stricmp(sir1, sir2)

unde: sir1 si sir2 reprezinta pointeri catre zonele in care se gasesc caracterele celor doua siruri ce se vor compara. Valorile returnate de functia stricmp sunt aceleasi ca si in cazul functiei strcmp.

Functia strincmp
Functia strincmp este similara functiei strncmp, cu deosebirea ca la compararea sirurilor nu se face deosebire intre literele mari si mic. Formatul de apelare este:
strincmp(sir1, sir2, n)

unde: sir1 si sir2 reprezinta pointeri catre zonele in care se gasesc caracterele celor doua siruri ce se vor compara. n reprezinta numarul maxim de caractere din ambele siruri ce se vor lua in considerare. Daca minimul dintre lungimile celor doua siruri este mai mic decat n, functia strncmp realizeaza aceeasi comparatie ca si functia strcmp. Valorile returnate de functia strincmp este acelasi ca si in cazul functie strcmp. Exercitii: a. Sa se scrie un program care citeste o succesiune de cuvinte si-l afiseaza pe cel mai lung dintre ele.
#include <stdio.h> #include <string.h> #define MAX 100 void main() { int max = 0, i;

79

char cuvant[MAX + 1]; char cuv_max[MAX + 1]; while (scanf("%100s", cuvant) != EOF) if (max <(i = strlen(cuvant))) { max = i; strcpy(cuv_max, cuvant); } if (max) printf("%s\n", cuv_max); } Observatii: Se presupune ca un cuvant are cel mult 100 caractere, sunt separate prin spatii si pentru terminare se tasteaza sfarsitul de fisier (Ctrl+Z); Cuvantul citit se pastreaza in tabloul cuvant. Citirea se face utilizand spefificatorul de format %100s, care permite citirea a cel mult 100 caractere diferite de cele albe; Dupa citire se determina lungimea sirului si se compara cu valoarea variabilei max, care pastreaza lungimea maxima curenta; Daca lungimea cuvantului curent citit este mai mare decat lungimea maxima, atunci lui max i se atribuie aceasta valoare, iar cuvantul citi este copiat in cuv_max.

b. Sa se scrie un program care citeste o succesiune de cuvinte, le sorteaza in ordine crescatoare si apoi le afiseaza.
#include <iostream.h> #include <string.h> #define MAXS 30 // Numarul maxim de cuvinte #define MAXC 25 // Numarul maxim de caractere dintr-un cuvant void main() { char tabel[MAXS][MAXC], aux[MAXC]; int i, j, nr; cout << "Numarul de cuvinte de ordonat: "; cin >> nr; cout << "Se introduc sirurile de ordonat:\n"; for (i = 0; i < nr; i++) cin >> tabel[i]; for (i = 0; i < nr - 2; i++) // Se ordoneaza cuvintele for (j = i + 1; j < nr - 1; j++) if (strcmp(tabel[i], tabel[j]) > 0) { // Permutare cuvinte neordonate strcpy(aux, tabel[i]); strcpy(tabel[i], tabel[j]); strcpy(tabel[j], aux); } cout << "Sirurile ordonate alfabetic sunt:\n"; for (i = 0; i < nr; i++) cout << tabel[i] << "\n"; }

80

Prelucrarea fisierelor
Datorita faptului ca limbajul C++ nu dispune de instructiuni de intrare/iesire, aceste operatii se realizeaza prin intermediul unor functii din biblioteca standard a limbajului. Aceste functii asigura o portabilitate buna a programelor, fiind implementate intr-o forma compatibila pe diferite sisteme de operare. Majoritatea operatiilor de I/E se realizeaza in ipoteza ca datele sunt organizate in fisiere6. Cele mai utilizate suporturi pentru fisiere sunt cele magnetice. Ele se numesc si suporturi reutilizabile, deoarece zona utilizata pentru pastrarea inregistrarilor unui fisier poate fi ulterior refolosita pentru pastrarea inregistrarilor altui fisier. Inainte de a incepe prezentarea functiilor de intrare/iesire, este necesar sa intelegem un concept foarte important al limbajului, stream-ul. In limbajul C++, un stream7 este o interfata logica intre calculator si unul dintre diferitele sale periferice. In forma sa cea mai comuna, un stream este o interfata logica pentru un fisier. Dupa cum defineste limbajul C++ termenul de fisier, el se poate referi la un fisier disc, ecran, tastatura etc. Cu toate ca fisierele difera ca forme si proprietati, stream-urile sunt aceleasi. Avantajul acestei aproximari este ca, pentru programator, toate perifericele apar ca fiind la fel. Un stream este asociat unui fisier prin executarea unei operatii de deschidere (open) si este disociat de un fisier prin executarea unei operatii de inchidere (close). Exista doua tipuri de stream-uri: text si binar. Un stream text este utilizat pentru a manipula caractere ASCII. Cand se foloseste un stream text, este posibil sa apara translatarea unor caractere. De exemplu, cand este intalnit caracterul newline, el este convertit intr-o secventa carriage return/line feed. Din acest motiv, este posibil sa nu existe o corespondenta totala intre ceea ce este trimis catre stream si ceea ce contine fisierul. Stream-ul binar poate fi utilizat cu orice tip de data; el nu produce translatari de caractere, iar corespondenta intre ceea ce este transmis catre stream si ceea ce contine fisierul este totala. Un alt concept care trebuie inteles corect este pozitia curenta. Pozitia curenta, numita si locatie curenta, este locul din fisier in care se va produce urmatoarea operatie de intrare/iesire. Datele introduse de la tastatura se considera ca formeaza un fisier de intrare. In acest caz, inregistrarea este formata din datele tastate pe un rand, deci caracterul de rand nou (newline) este terminator de inregistrare. In mod analog, datele ce se afiseaza pe ecran sau se tiparesc la imprimanta formeaza un fisier de iesire. Si in acest caz inregistrarea este formata din caracterele unui rand. Un fisier are o inregistrare care marcheaza sfarsitul de fisier. In cazul fisierelor de intrare de la tastatura, sfarsitul de fisier se genereaza prin secventa Ctrl+Z. Prelucrarea fisierelor implica un numar de operatii specifice acestora. Deschiderea fisierului; Crearea unui fisier; Citirea (consultarea) inregistrarilor unui fisier; Actualizarea unui fisier; Adaugarea de inregistrari intr-un fisier; Pozitionarea intr-un fisier; Stergerea unui fisier; Inchiderea unui fisier.

Fisierul este o colectie ordonata de elemente, numite inregistrari, care sunt pastrate pe diferite suporturi externe de informatii. Stream tradus in limba romana ar insemna flux, insa datorita faptului ca acest cuvant poate avea mai multe intelesuri, specialistii au optat pentru pastrarea termenului din engleza.

81

Tratarea fisierelor se poate face la doua niveluri: La nivel inferior, cand se face apel direct la sistemul de operare; La nivel superior, cand se folosesc proceduri specializate de prelucrare ce utilizeaza structuri speciale de tip FILE. In continuare ne vom ocupa numai de nivelul superior de prelucrare a fisierelor.

Nivelul superior de prelucrare a fisierelor in C


La acest nivel operatiile de prelucrare a fisierelor se executa prin utilizarea unor functii specializate de gestiune a fisierelor, a caror prototipuri se gasesc in fisierul stdio.h. De asemenea, fiecarui fisier i se asociaza o structura de tip FILE, tip definit in fisierul stdio.h.

Deschiderea unui fisier


Pentru a deschide un fisier si a-l asocia unui stream, trebuie sa folosim functia fopen. Aceasta functie returneaza un pointer spre tipul FILE (tipul fisier) sau pointerul NULL in caz de eroare. Valoarea returnata de functia fopen trebuie sa fie atribuita unei variabilei pointer catre tipul FILE (pe care o vom numi in continuare variabila fisier), care se va folosi in continuare in toate apelurile functiilor de prelucrare a respectivului fisier. Sintaxa apelului functiei este:
fopen(cale, mod)

unde: cale reprezinta un sir de caractere care specifica numele si eventual calea fisierului (specificator de fisier) ce se va deschide ; mod este un sir de caractere care defineste modul de prelucrare al fisierului dupa deschidere. Acest sir de caractere poate fi: r deschidere pentru citire (read) a unui fisier existent; w deschidere pentru scriere (write) a unui fisier; a deschidere in adaugare (append); r+ deschidere pentru citire/scriere (read/write) a unui fisier existent; w+ deschiderea unui nou fisier pentru citire/scriere (read/write) ; daca fisierul deja exista, atunci el va fi rescris; a+ deschidere pentru adaugarea de noi inregistrari la sfarsit de fisier. Daca fisierul nu exista, atunci el este creat. Daca operatia de deschidere a fost efectuata cu succes, functia fopen returneaza un pointer catre fisier. Tipul FILE este definit in fisierul antet stdio.h si este de fapt o structura care contine diferite informatii despre fisier, cum ar fi: dimensiunea fisierului, pozitia curenta, modul de acces etc. In general, aceasta structura identifica fisierul. Functia fopen returneaza de fapt un pointer catre structura asociata cu fisierul prin procesul de deschidere.
Observatii: Pentru a specifica faptul ca un fisier este deschis sau creat in modul text se adauga caracterul t la sirul ce indica modul de prelucrare (de exemplu, rt, w+t etc.). Pentru a specifica modul binar, se adauga caracterul b la sirul ce indica modul de prelucrare (de exemplu, wb, a+b etc.). Daca in sirul modului de prelucrare nu se specifica t sau b, se presupune ca fisierul este deschis in modul text. Caracterele t sau b pot fi inserate intre litera si semnul + din sirul modului de prelucrare. De exemplu, rt+ este echvalent cu r+t. Folosind functia fopen puteti deschide un fisier inexistent in modul scriere (w) sau adaugare (a). Daca se deschide un fisier existent in modul scriere (w), atunci el se va crea din nou si vechiul sau continut se va pierde.

82

Deschiderea unui fisier in modul adaugare (a), permite scrierea de noi inregistrari la sfarsitul unui fisier existent. Prin intermediul functiilor din aceasta clasa de prelucrare fisiere se pot trata si fisierele standard. Aceste fisiere nu trebuie deschise, deoarece ele se deschid automat la lansarea in executie a programului. Pentru a utiliza aceste fisiere se vor folosi urmatorii pointeri spre tipul FILE: stdin pentru a citi de la intrarea standard; stdout pentru a afisa pe ecranul de la iesirea standard; stderr pentru afisarea erorilor la iesirea standard; stdprn pentru a tipari la imprimanta paralela; stdaux pentru iesirea la comunicatia seriala.

In aplicatiile de prelucrare fisiere, o foarte mare atentie trebuie acordata testarii existentei fisierului. Adica este foarte important sa se verifice daca functia fopen a returnat un pointer valid sau nu. Pentru a verifica valoarea returnata de functia fopen se poate folosi urmatoarea secventa de instructiuni:
FILE *fp; if((fp = fopen(fisier.txt, r)) == NULL) { printf(Fisierul specificat nu exista.); ... }

Aceasta secventa incearca sa deschida un fisier text cu numele fisier.txt existent in directorul curent. Daca fisierul nu exista, se va afisa mesajul Fisierul specificat nu exista si in continuare se vor executa operatiile corespunzatoare. Daca fisierul exista, atunci el va fi deschis si datele lui pot fi prelucrate. In acelasi mod se poate proceda si pentru fisierele binare. Neverificarea valorii returnate de functia fopen poate avea urmari grave; cea mai importanta fiind pierderea datelor atunci cand se deschide un fisier existent folosind modul w. Exemple: a. Liniile urmatoare declara un pointer catre tipul FILE si ii atribuie valoarea stdin. Fiind un fisier de intrare standard, nu este necesarea deschiderea sau inchiderea lui.
FILE *pf; ... pf=stdin; // pointer catre tipul FILE // citirea se va face de la tastatura

b. Urmatoarea linie declara un pointer catre tipul FILE si-l initializeaza cu valoarea furnizata de apelul functiei fopen. Functia fopen este apelata pentru a deschide un fisier text, numit TEST.TXT, pentru citire. Fisierul se afla in directorul curent.
FILE *pfr = fopen(TEST.TXT, rt);

c. Urmatoarea linie declara un pointer catre tipul FILE si-l initializeaza cu valoarea furnizata de apelul functiei fopen. Functia fopen este apelata pentru a deschide un fisier text, numit TEST.TXT, pentru scriere. Fisierul se afla in subdirectorul Programe din directorul BC31 al radacinii discului C.
FILE *pf = fopen(C:\\BC31\\Programe\\TEST.TXT, wt);

In sirul care precizeaza calea au fost incluse doua caractere backslash (\) in loc de unul singur, deoarece caracterul backslash se foloseste si pentru a include secventa escape intrun sir de caractere.

Inchiderea fisierelor
Dupa terminarea prelucrarii unui fisier, acesta trebuie inchis, Inchiderea unui fisier se realizeaza apeland functia fclose. Apelul functie fclose are urmatoarea sintaxa:
fclose(varaiabila_fisier)

unde: variabila_fisier este un pointer catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului. Functia returneaza urmatoarele valori: 83

0 daca inchiderea fisierului a decurs normal; -1 daca pe timpul inchiderii s-a produs o eroare.

Prelucrarea pe caractere a unui fisier


Pentru a prelucra un fisier caracter cu caracter se folosesc functiile: fputc pentru scriere; fgetc pentru citire.

Functia putc
Sintaxa apelului functiei este:
fputc(caracter, varaiabila_fisier)

unde: caracter este codul ASCII al caracterului care se scrie in fisier; variabila_fisier este un pointer catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului in care se scrie. In particular, variabila_fisier poate fi unul din pointerii: stdout, stderr, stdprn sau stdaux. Functia fputc scrie byte-ul continut de caracter in fisierul asociat cu variabila_fisier ca un unsigned char. Desi in prototipul functiei, caracter este definit ca un int, functia poate fi apelata cu o variabila de tip char aceasta fiind de fapt utilizarea cea mai comuna. Functia fputc returneaza caracterul scris, daca operatia s-a inceiat cu succes, sau EOF, in cazul aparitiei unei erori.

Functia fgetc
Sintaxa apelului functie este:
fgetc(varaiabila_fisier)

unde: variabila_fisier este un pointer catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului din care se citeste. In particular, variabila_fisier poate fi unul din pointerii: stdin sau stdaux. Functia fgetc citeste byte-ul din pozitia curenta ca un unsigned char si-l returneaza ca un intreg. Motivul pentru care byte-ul citit este returnat ca un intreg este acela ca, daca apare o eroare, fgetc returneaza EOF, care este o valoare intreaga. De asemenea, fgetc returneaza EOF si atunci cand este atins sfarsitul de fisier. Functia returneaza valoarea lui caracter sau 1 in caz de eroare. Exemplu:
#include <stdio.h> #include <stdlib.h> char sir[80] = "Testarea scrierii si citirii caracter cu caracter"; FILE *fp; char ch, *p; char numeFis[] = "c:\\Bc31\\Programe\\Fisiere\\Note.txt"; void main() { // Deschiderea fisierului pentru scriere if ((fp = fopen(numeFis, "w")) == NULL) { printf("Fisierul nu poate fi deschis."); exit(1); } // Scrierea fisierului p = sir; while (*p) { if (fputc(*p, fp) == EOF) { printf("Eroare la scriere in fisier.\n"); exit(1); }

84

p++; } fclose(fp); // Deschiderea fisierului pentru citire if((fp = fopen(numeFis, "r")) == NULL) { printf("Nu se poate deschide fisierul.\n"); exit(1); } // Citirea fisierului for( ; ; ) { if ((ch = fgetc(fp)) == EOF) break; putchar(ch); } fclose(fp); }

Functiile feof si ferror


Dupa cum am vazut in exemplu anterior, la citirea/scrierea unui caracter se testeaza daca functiile fgetc/fgetc returneaza EOF, adica daca s-a ajuns la sfarsitul fisierului sau daca s-a produs o eroare. Intrebarea se pune, cum putem afla care dintre cele doua evenimente s-a produs? Mai mult, daca se opereaza asupra unui fisier binar, orice valoare este valida. Aceasta inseamna ca este posibil ca un byte sa aiba aceeasi valoare (cand este evaluat ca un int) ca si EOF. Deci, in ce mod putem afla daca a fost returnata o data valida sau s-a ajuns la sfarsitul fisierului? Solutia acestei probleme o constituie utilizarea functiilor feof si ferror, care au urmatorul format de apelare:
feof(variabila_fisier) ferror(variabila_fisier) unde: variabila_fisier este pointerul catre tipul FILE asociat fisierului la deschiderea acestuia cu functia fopen. Functia feof returneaza o valoare diferita de 0 (adevarat) daca s-a ajuns la sfarsitul fisierului asociat cu variabila_fisier; in caz contrar, ea returneaza 0 (fals). Aceasta functie se poate folosi

atat la fisiere text cat si la fisiere binare. Functia ferror returneaza o valoare diferita de 0 (adevarat) daca in fisierul asociat cu variabila_fisier a aparut o eroare; in caz contrar, ea returneaza 0 (fals). ferror se refera la erorile aparute relativ la ultima operatie de acces la fisier. Pentru a face o verificare totala a erorilor, ea trebuie apelata dupa fiecare operatie asupra fisierului. Exemplu: Urmatorul program realizeaza o copie a unui fisier text al carui cale si nume se introduce de la tastatura.
#include <stdio.h> #include <stdlib.h> #include <string.h> FILE *sursa, *dest; char nume[15], cale[65], specFile[80], specCopie[80], ch; void main() { // Se introduce calea, numele si extensia fisierului de copiat printf("Calea fisierului de copiat (C:\D1\D2...\Dn):\n\t\t"); gets(cale); fflush(stdin); strcat(specFile, cale); strcat(specFile, "\\"); strcpy(specCopie, specFile); printf("Numele si extensia fisierului: "); gets(nume); fflush(stdin); strcat(specFile, nume); strcat(specCopie, "copie.txt");

85

// Se verifica existenta fisierului sursa if((sursa = fopen(specFile, "r")) == NULL) { printf("Fisierul indicat nu exista.\n"); exit(1); } // Se deschide fisierul destinatie if((dest = fopen(specCopie, "w")) == NULL) { printf("Nu se poate deschide fisierul destinatie."); exit(1); } // Copierea fisierului sursa in destinatie while(!feof(sursa)) { ch = fgetc(sursa); if(ferror(sursa)) { printf("Eroare de citire/n"); exit(1); } if(!feof(sursa)) fputc(ch, dest); if(ferror(dest)) { printf("Eroare la scriere.\n"); exit(1); } } printf("Fisierul %s\ns-a copiat in %s", specFile, specCopie); fclose(sursa); fclose(dest); }

Intrari/iesiri fara format


Citirea si scrierea inregistrarilor care sunt siruri de caractere se poate realiza apeland functiile fgets si fputs: fgets citeste un sir de caractere dintr-un fisier; fputs scrie un sir de caractere intr-un fisier. Functiile se pot apela folosind urmatoarele sintaxe:
fgets(sir, numar, variabila_fisier) fputs(sir, variabila_fisier)

unde: sir este un pointer catre zona in care se vor stoca caracterele citite sau unde se gasesc caracterele.care se scriu; numar este dimensiunea, in byte, a zonei in care se citesc caracterele din fisier. La un apel se citesc cel mult numar 1 caractere. variabila_fisier este un pointer catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului din care se citeste sau in care se scrie. Functia fgets citeste caracterele din fisier in sirul a carei adresa este indicata de pointerul sir. Citirea se termina fie cand s-au citit numar 1 caractere, fie cand s-a citit un caracter newline, fie cand s-a ajuns la sfarsitul fisierului, care dintre ele se produce primul.. Functia fgets adauga un caracter null la sfarsitului sirului. Functia fgets lucreaza diferit fata de functia inrudita gets, in sensul ca retine caracterul newline. Functia returneaza pointerul sir daca operatia s-a terminat normal sau un pointer NULL daca s-a produs o eroare. Functia fputs copiaza sirul de caractere sir terminat prin caracterul null in fisierul asociat cu variabila_fisier. Caracterul null de terminare a sirului nu este scris in fisier. Deci functia fputs lucreaza diferit fata de functia inrudita puts, in sensul ca nu adauga automat in fisier secventa carriage return/line feed. Functia fgets returneaza o valoare pozitiva daca operatia de scriere a decurs normal si EOF daca se produce o eroare la scriere. 86

Exemple: a) Folosind functia fputs sa se scrie intr-un fisier liniile tastate. Fiecare linie va constitui un articol in fisier. Transmiterea unei linii goale indica terminarea scrierii in fisier.
#include <string.h> void main() { char linie[100]; int i = 0; FILE *fis = fopen("C:\\BC31\\Programe\\Notite.txt", "wt"); do { gets(linie); // Se testeaza daca nu s-a transmis un rand gol if (strlen(linie)==0) break; if (i > 0) fputs("\n", fis); // Trecerea la o noua inregistrare else i = 1; // Se scriu in fisier caracterele tastate. fputs(linie, fis); fputs("\n", fis); // Trecerea la o noua inregistrare } while (EOF); fclose(fis); } b. Folosind functia fgets sa se afiseze pe ecran continutul fisierului creat anterior. #include <stdio.h> void main() { char linie[100]; FILE *fis = fopen("C:\\BC31\\Programe\\Notite.txt", "rt"); while (!feof(fis)) { fgets(linie, 100, fis); // Citeste un articol din fisier printf("%s", linie); // Scrie articolul citit } fclose(fis); }

Golirea zonei tampon a fisierului


In foarte multe cazuri este necesara golirea (vidarea) zonei tampon (buffer) a unui fisier. Pentru a executa aceasta operatie se apeleaza functia fflush. Sintaxa apelului functiei este:
fflush(variabila_fisier)

unde: variabila_fisier este pointerul catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului a carei zona tampon se goleste.
Observatii: Daca fisierul este deschis in scriere, atunci continutul zonei tampon se scrie in fisierul respectiv. Daca fisierul este deschis in citire, atunci caracterele ramase necitite din zona tampon se pierd, deoarece dupa apelul functiei zona tampon devine goala. Functia returneaza 0 daca operatia s-a terminat normal si EOF daca s-a produs o eroare.

Exemplu: Sa se scrie un program care sa nu permita introducerea unui sir de caractere in locul unei valori numerice.
#include <stdio.h> #include <stdlib.h> void main() { int i, n; do { printf("Valoarea lui n este: "); if ((i = scanf("%d", &n)) == 1) break; printf("Nu s-a tastat un intreg\n");

87

if (i==EOF) // S-a tastat sfarsitul de fisier (Ctrl + Z) exit(1); fflush(stdin); // Se elimina caracterele necitite din zona tampon a // fisierului de intrare standard } while (1); printf("Numarul introdus este: %d\n", n); }

Pozitionarea intr-un fisier


Pentru deplasarea capului de citire/scriere al discului in vederea prelucrarii inregistrarilor (operatie numita si pozitionarea pointerului de fisier) si pentru a determina pozitia curenta a pointerului de fisier, in limbajul C++ exista mai multe functii, dintre care in continuare sunt prezentate doar doua: fseek si ftell.

Functia fseek
Asigura repozitionarea pointerului de fisier. Prototipul functiei se gaseste in fisierul stdio.h. Forma de apelarea a functiei este:
fseek(variabila_fisier, deplasament, origine)

unde: variabila_fisier este pointerul catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului in care se face repozitionarea. deplasament diferenta, in bytes, dintre origine (pozitia pointerului de fisier) si noua pozitie. Pentru fisierele text, deplasament-ul trebuie sa fie 0 sau valoarea returnata de functia ftell. origine una din cele trei localizari SEEK_xxx ale pointerului de fisier (0, 1 sau 2) indicate in tabelul urmator:
Constanta SEEK_SET SEEK_CUR SEEK_END Valoare 0 1 2 Pozitia in fisier Inceputul fisierului Pozitia curenta Sfarsitul fisierului

Functia returneaza 0 la o pozitionare corecta si o valoare diferita de zero in caz de eroare. In cazul cand este aplicata asupra unui fisier nedeschis, functia returneaza un cod de eroare.

Functia ftell
Functia returneaza un intreg lung reprezentand pozitia curenta a pointerului de fisier. Prototipul functiei se gaseste in fisierul stdio.h. Forma de apelare a functiei este:
ftell(variabila_fisier)

unde: variabila_fisier este pointerul catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului in care se determina pozitia curenta a pointerului de fisier.
Observatii: Daca fisierul este binar, deplasamentul este specificat in bytes fata de inceputul fisierului. Valoarea returnata de functie poate fi utilizat intr-un apel ulterior al funtiei fseek.

Functia returneaza 1 in caz de eroare. Exemplu: Sa se intocmeasca o functie care sa determine lungimea, in bytes, a unui fisier text.
long FileSize(FILE *pf) { long pz, lungime; pz = ftell(pf); fseek(pf, 0L, SEEK_END); lungime = ftell(pf); fseek(pf, pz, SEEK_SET); return lungime; } // // // // Se determina pozitia curenta in fisier Pozitionare pe sfarsitul de fisier Determinarea lungimii fisierului Pozitionarea pe inceputul de fisier

88

Prelucrarea fisierelor binare


In fisierele binare (bytes nu sunt considerati ca fiind coduri de caractere), inregistrarea fizica reprezinta o colectie de articole. Articolul este o data de un tip oarecare, predefinit sau definit de utilizator. La citire, intr-o zona speciala, numita zona tampon (buffer), se transfera un numar de articole care se presupun ca au o lungime fixa. In mod analog, la scriere se transfera din zona tampon un numar de articole de lungime fixa. Fisierele binare pot fi prelucrate folosind functiile fread si fwrite.

Functia fread
Functia fread asigura citirea datelor dintr-un fisier binar. Prototipul functiei se gaseste in fisierul stdio.h. Sintaxa apelului functiei este:
fread(variabila, lungime, nr_articole, variabila_fisier)

unde:

variabila este un pointer catre zona tampon ce va contine inregistrarea fizica citita. lungime dimensiunea, in bytes, a unui articol. nr_articole numarul de articole din inregistrarea fizica. variabila_fisier este pointerul catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului. Daca operatia de citire decurge normal, functia fread returneaza numarul de articole (nu

numarul de bytes) citite. Daca valoarea returnata de functie este 0, nu a fost citit nic un articol sau a fost atins sfarsitul de fisier (se pot folosi functiile feof si ferror pentru a afla care dintre aceste evenimente s-a produs).

Functia fwrite
Functia fwrite asigura scrierea articolelor in fisier. Prototipul functiei se gaseste in fisierul stdio.h. Sintaxa apelului functiei este:
fwrite(variabila, lungime, nr_articole, variabila_fisier)

unde:

variabila pointer catre zona tampon ce contine inregistrarea fizica ce va fi scrisa. lungime dimensiunea, in bytes, a unui articol. nr_articole numarul de articole dintr-o inregistrare fizica. variabila_fisier este pointerul catre tipul FILE a carei valoare a fost returnata de functia fopen la deschiderea fisierului. Daca operatia de scriere s-a terminat normal, functia fwrite returneaza numarul de articole (nu numarul de bytes) scrise. In caz de eroare, functia fwrite returneaza 1.

Exemple: a. Sa se creeze un fisier care sa contina numele si prenumele elevului si mediile la cinci materii.
#include <stdio.h> struct catalog { // Structura articolului char nume[30]; float medie[5]; } elev; void main() { char ok; float med; char materie[][11]={"Romana", "Fizica", "Matematica", "Chimie", "Istorie"}; FILE *pf = fopen("C:\\BC31\\Programe\\Catalog.dat", "wb"); do { printf("Numele si prenumele elevului: "); gets(elev.nume); fflush(stdin); printf("\tMediile elevului:\n"); for (int i = 0; i < 5; i++) {

89

} fwrite(&elev, sizeof(elev), 1, pf); // Scrierea articolului printf("\n"); fflush(stdin); printf("Mai sunt date de introdus (D/N)? "); scanf("%c", &ok); fflush(stdin); } while (ok == 'D' || ok == 'd'); fclose(pf); }

printf("%10s: ", materie[i]); scanf("%f", &med); elev.medie[i] = med; fflush(stdin);

b. Sa se realizeze un program care sa citeasca fisierul creat anterior si sa afiseze numele si prenumele elevului si media acestuia.
#include <stdio.h> struct catalog { // Structura articolului char nume[30]; float medie[5]; } elev; void main() { float med; long lung, poz, nrArt; FILE *pf = fopen("C:\\BC31\\Programe\\Catalog.dat", "rb"); // Se determina numarul de articole din fisier poz = ftell(pf); // Determinarea pozitiei curente fseek(pf, 0L, SEEK_END); // Pozitionare pe sfaristul fisierului lung = ftell(pf); // Lungimea fisierului fseek(pf, poz, SEEK_SET); // Repozitionare pe primul articol nrArt = lung / sizeof(elev); // Numar articole din fisier // Se citesc articolele si se afiseaza elevii si mediile lor for (int i = 0; i < nrArt; i++) { med = 0; fread(&elev, sizeof(elev), 1, pf); printf("%30s ", elev.nume); for (int i = 0; i < 5; i++) med += elev.medie[i]/5; printf("%6.2f\n", med); } fclose(pf); }

Nivelul superior de prelucrare a fisierelor in C++


Intrare/Iesire (prescurtat IO de la Input/Output) este procesul de transfer a datelor de pe un periferic al calculatorului pe altul sau dintr-o componenta a calculatorului pe alta. Exista trei categorii de intrari/iesiri: intrari/iesiri standard, intrari/iesiri memorie si intrari/iesiri retea. Intrarile/iesirile standard sunt utilizate frecvent si limbajul C++ furnizeaza stream-urile cin, cout, cerr si clog. IO in C++ sunt realizate prin intermediul claselor stream, care au urmatoarea ierarhie de mostenire:

90

Putem folosi clasele ifstream, ofstream si fstream pentru a efectua intrari/iesiri de fisiere ( (cin este un obiect al clasei istream si cout este un obiect al clasei ostream). IO de fisier reprezinta transferul datelor dintr-o memorie externa (de exemplu, hard-discul) in memoria principala si invers. Schematic fluxul de date poate fi reprezentat astfel:

Memoria RAM Utilizatori

Hard disc

Sagetile indica fluxul de date.

Fisiere text si binare


Limbajul C++, ca si limbajul C, suporta doua tipuri de fisiere: fisiere text fisiere binare Principala diferenta dintre fisierele text si fisierele binare consta in faptul ca in fisierele text se executa diferite translatari ale caracterelor; de exemplu, "\r+\f" (returul de car) este convertit in "\n" (linie noua), in timp ce in fisierul binar nu se fac asemenea translatari. In mod implicit, C++ deschide fisierele in modul text. In tabelele urmatoare sunt prezentate pasii si operatiile ce pot (sau trebuie) fi executate pentru a utiliza fisierele in C++: 1. Crearea sau deschiderea unui fisier
Fisiere text Fisiere binare Pentru scrierea datelor ofstream out ("fis.txt"); ofstream out ("fisier.dat",ios::binary); sau sau ofstream out; ofstream out; out.open("fis.txt"); out.open("fisier.dat", ios::binary); Pentru adaugare (adaugare date la sfarsitul unui fisier existent) ofstream out("fis.txt",ios::app); ofstream out("fis.dat",ios::app|ios::binary); sau sau ofstream out; ofstream out; out.open("fis.txt", ios::app); out.open("fis.dat", ios::app | ios::binary); Pentru citirea datelor ifstream in ("fis.txt"); ifstream in ("fis.dat", ios::binary); sau sau ifstream in ; ifstream in ; in.open("fis.txt"); in.open("fis.dat", ios::binary);

2. Inchiderea fisierelor (dupa terminarea prelucrarii lor)


obiect ofstream "out" out.close();
Data caracter un cuvant mai multe cuvinte obiecte date binare Operatie

obiect Ifstream "in" in.close();


Functii pentru citirea din fisier get(); >> (operator extragere) getline(); read() La fel ca mai sus Functie Descriere Functii pentru scrierea in fisier put(); << (operator inserare) << (operator inserare) write() La fel ca mai sus

3. Citirea/scrierea datelor din/in fisiere

4. Functii ce pot fi utilizate pentru a executa sarcini speciale 91

Verificarea sfarsitului de fisier Verificarea daca operatia a esuat Verificarea daca operatia a esuat Verificarea pentru fisier deschis Numarul de bytes deja cititi Ignorarea caracterelor pe timpul citiri fisierului Verificarea urmatorului caracter Acces aleator (numai pentru fisiere binare)

eof() bad() fail()

Se foloseste pentru a verifica daca s-a intalnit sfarsitul de fisier pe timpul citirii fisierului. Returneaza adevarat daca o operatie de scriere sau citire a esuat.

Returneaza adevarat in aceleasi cazuri ca si bad(), dar si in cazul intalnirii unei erori de format. is_open(); Verifica daca un fisier este sau nu deschis; returneaza adevarat daca fisierul este deschis si flas in caz contrar. gcount() Returneaza numarul de bytes cititi din fisier. ignore() peek() seekg() seekp() tellg() tellp() Ignora n bytes din fisier (pointerul get este pozitionat dupa n caractere) Verifica urmatorul caracter disponibil, fara a muta pointerul get pe urmatorul caracter. In cazul fisierelor binare accesul aleator este realizat cu ajutorul acestor functii. Ele furnizeaza sau seteaza pozitia pointerilor get sau put pe o anumita locatie.

Indicatori speciali
Tabelul urmator prezinta indicatorii speciali ce pot fi utilizati pentru gestionarea fisierelor (acesti indicatori sunt folositi pe timpul deschiderii fisierelor).
Indicator ios::app ios::ate ios::binary ios::in ios::out ios::trunc Descriere Deschide fisierul in modul append (adaugare). Deschide fisierul si seteaza pointerul la sfarsitul fisierului. Deschide fisierul in modul binar. Deschide fisierul pentru citire. Deschide fisieul pentru scriere. Deschide fisierul si truncheaza intregul sau continut.

In continuare prin intermediul unor exemple se va prezenta modul de lucru cu fisierele in C++.

Citirea si scrierea caracterelor in fisierele text


Pentru citirea si scrierea unui caracter in fisierele text se pot folosi functiile get (pentru citire) si put (pentru scriere) al caror format este:
obiect.get(char variabila) obiect.put(char variabila) unde variabila este identificatorul zonei de memorie in care se va citi sau de unde se va scrie fisierul, iar obiect este numele stream-ului de intrare sau de iesire.

Exemplu: Programul urmator scrie pe rand in fisierul test.txt caracterele (tastate de utilizator) atata timp cat raspunsul la intrebarea Continui? este d (de la da). Daca raspunsul este diferit de d, se inchide fisierul. Ultima parte a programului citeste toate caracterele din fisierul test.txt si le afiseaza.
#include<iostream> #include<fstream> using namespace std; void main() { char c, rasp; // codul pentru scrierea caracterelor in fisier rasp='d'; ofstream out ("test.txt"); // Deschiderea unui stream de iesire if(out.is_open()) { // Ciclarea va continua pana se tasteaza un caracter diferit de d while (rasp =='d') { cout <<endl << "Continui? "; cin >> rasp; if(rasp =='d') { cout << endl << "Introduceti un caracter: "; cin >> c; out.put(c);

92

} out.close(); cout << endl; // codul pentru citirea intregului fisier ifstream in("test.txt"); // Deschiderea unui stream de intrare if(in.is_open()) { while(!in.eof()) { c = in.get(); if(!in.eof()) cout << c; } } in.close(); cout << endl; }

Fisiere binare
Copierea unui fisier
Programul urmator copiaza un fisier binar in alt fisier binar. Cele doua fisiere sunt deschise in modul binar, unul pentru citire si celalalt pentru scriere. Dupa deschiderea celor doua fisiere se va copia continutul primului fisier in celalalt fisier.
#include<iostream> #include<fstream> using namespace std; void main() { // Deschiderea stream-ului de intrare si de iesire in modul binar ifstream in("detectiv.jpg",ios::binary); // Stream-ul de intrare ofstream out("copiedetectiv.jpg",ios::binary); // Stream-ul de iesire if(in.is_open() && out.is_open()) { while(!in.eof()) { out.put(in.get()); } } // Inchiderea ambelor fisiere in.close(); out.close(); } Nota: Fisierul detectiv.jpg (sau un alt fisier) trebuie sa existe in calea specificata (in acelasi dosar cu programul executabil).

Citirea si scrierea unui obiect


Pentru a citi/scrie blocuri de date se folosesc functiile read() si write(), care au urmatoarele prototipuri:
obiect.read(char *buf, streamsize num); obiect.write(const char *buf, streamsize num); Pentru functia read(), buf este tabloul de caractere unde va fi memorat blocul citit. Pentru functia write(), buf este tabloul de caractere unde se gasesc datele ce vor fi scrise in fisier. Pentru ambele functii, num este o valoare numerica care defineste cantitatea de date (numarul de caractere) ce se citesc/scriu. Daca se intalneste sfarsitul de fisier inainte de citirea a num caractere, atunci pentru a sti cate caractere au fost citite se poate apela functia gcount().

Aceasta functie returneaza numarul de caractere citite la ultima operatie de intrare neformatata. obiect este numele stream-ului de intrare sau de iesire. 93

Exemplu: Programul va scrie si citi un obiect (obiectul Student) in si dintr-un fisier binar:
#include<iostream> #include<fstream> using namespace std; struct Student { // Definirea structurii inregistrarilor char nume[20]; int cod; } stud; void IntroducereDate() { cout << "Numele studentului: "; cin >> stud.nume; cout << "Codul studentului:"; cin >> stud.cod; } void main() { char rasp='d'; // Fisierul student.dat este deschis in modul append ofstream out("student.dat", ios::app); // Se deschide stream-ul de iesire if(out.is_open()) { // Ciclarea va continua pana cand se raspunde cu ceva diferit de d while( rasp == 'd') { cout << endl << "Continui? "; cin >> rasp; if(rasp == 'd') { IntroducereDate(); out.write((char*) &stud, sizeof(stud)); } } } out.close(); ifstream in("student.dat"); // Se deschide stream-ul de intrare if(in.is_open()) { cout << "Datele studentilor sunt:" << endl; while(!in.eof()) { in.read((char*) &stud, sizeof(stud)); if(!in.eof()) { cout << stud.nume << "\t\t" << stud.cod << endl; } } } in.close(); }

94

Programarea orientata spre obiecte


Programarea orientata spre obiecte realizeaza contopirea celor doua entitati ale prelucrarii informatiilor: datele aplicatiei si codul necesar tratarii lor. Pentru a ajunge la acest deziderat, limabjul de programare pune la dispoziti facilitati deosebite pentru definirea unor tipuri de date proprii si a unor operatori destinati manipularii lor. Acestea se comporta la fel ca tipurile fundamentale (intregi, numere reale, caractere etc.) si operatorii standard (adunare, scadere etc.). Astfel, in functie de ceea ce are nevoie, un programator poate crea tipul complex sau matrice, care sa opereze cu numere complexe sau cu matrici. In aceste conditii, o secventa de genul:
complex matrice ... c = a + m = n * a, b, c; m, n, o; b; o;

poate deveni o realitate, chiar daca cele sase variabile reprezinta numere complexe si matrici, nu numere reale sau intregi. Noile tipuri de date (complex si matrice) se numesc clase, variabilele a, b, c, m, n, o poarta numele de obiecte (sau instantieri) ale claselor complex si matrice, iar operatorii redefiniti + si * se numesc metode ale claselor respective. Din aceasta scurta prezentare rezulta ca o clasa contine atat structurile de date necesare descrierii unui obiect, cat si metodele (functiile) cu ajutorul carora obiectele in cauza pot fi manipulate.

Conceptele de baza ale programarii orientate pe obiecte


Modelul orientat pe obiecte este un model computational ale carui principale abstractii sunt clasele si obiectele. Programarea care are la baza un model orientat pe obiecte poarta numele de programare orientata pe obiecte (OOP Object Oriented Programming) sau programare obiectuala. Notiunile de clasa si obiect impreuna cu relatiile existente intre clase, obiecte, si clase si obiecte reprezinta conceptele fundamentale sau de baza ale programarii orientate pe obiecte.

Clase si obiecte
Conceptele de clasa si obiect sunt inseparabile, iar definitia unuia este acceptata doar in prezenta definitiei celuilalt. Mai mult, existenta unui obiect este strans legata de clasa careia ii apartine. O clasa poate fi definita ca o abstractizare a trasaturilor esentiale ale unei colectii de obiecte inrudite intre ele. Din punct de vedere al limbajului C++, clasa este o entitate statica, declarationala, care descrie o tipologie in termenii definirii structurii si comportamentului pentru toate obiectele care o determina. Desi notiunile de clasa si tip nu se confunda (tipul implica doar consideratii comportamentale), de foarte multe ori termenii se folosesc alternativ, pentru a desemna una si aceeasi notiune. Tipurile predefinite existente in limbajul C++ se numesc tipuri fundamentale, iar tipurile definite de utilizator, in procesul dezvoltarii unei aplicatii, se numesc tipuri utilizator sau clase utilizator. Intre clasa si tip se face distinctie doar in cazul expresiilor tip derivat si clasa derivata, astfel: Prin tip derivat se intelege orice tip obtinut din tipurile fundamentale sau utilizator prin constructii specifice limbajului (pointeri, referinte, tablouri) Notiunea de clasa derivata este strans legata de clasificarea tipurilor. Un obiect poate fi definit ca o materializare (concretizare) a tipologiei descrise de o anumita clasa. De aceea obiectele mai sunt denumite si reprezentanti, indivizi sau instante ale unei 95

clase. Din definitie, rezulta ca obiectul este o entitate dinamica care poate fi creata, utilizata si distrusa. Modelul algoritmic al unui program descrie executarea unor operatii conform diagramei de flux, in timp ce in modelul orientat pe obiecte, programul descrie un sistem de obiecte care interactioneaza. Modul de interactiune al obiectelor este determinat de relatia obiect-obiect existenta intre obiecte si de relatia clasa-clasa existenta intre clasele de care apartin obiectele.

Relatia obiect-obiect
Existenta unui obiect poate fi descrisa prin intermediul a doua caracteristici: caracteristica statica descrie starea interna a unui obiect in termenii unor trasaturi numite atribute sau date membru. Starea interna determina posibilitatile de acumulare a informatiei pe care le detine un sistem de obiecte; insa ea nu confera obiectelor proprietatea de identitate sau individualitate. Deci, un obiect nu poate fi descris doar de starea lui interna, existand posibilitatea ca la un moment dat sa fie mai multe obiecte cu aceeasi stare interna (numite obiecte echivalente). caracteristica dinamica descrie evolutia in timp a starii interne a unui obiect in termenii interactiunii acestuia cu alte obiecte. Interactiunea obiectelor se realizeaza prin schimbul de informatii. Schimbul de informatii este numit simbolic mesaj si este modul de baza de comunicare a obiectelor din cadrul unui sistem. In urma primirii unui mesaj, un obiect se manifesta prin modificarea particulara a starii sale interne si transmiterea de mesaje altor obiecte. Schimbul de mesaje determina tipurile de relatii dintre obiecte si deci si modul de operare asupra obiectelor. Deci exista: obiecte de tip server, asupra carora se opereaza de catre alte obiecte in vederea satisfacerii anumitor cereri; obiecte de tip client, care sunt beneficiarii serviciilor formulate catre obiectele de tip server; obiecte de tip agent, care sunt obiecte mixte, atat de tip server cat si de tip client.

Relatia obiect-clasa
Este de fapt relatia obisnuita dintre o variabila si tipul variabilei.
Definitie: Procesul de definirea a unei clase pornind de la o multime de obiecte se numeste clasificare sau tipizare si este specific fazei de proiectare a unui program. Procesul invers se numeste instantiere si este specific fazei de implementare.

Prin procesul de instantiere a unei clase rezulta o relatie de tip INSTANTA_A (INSTANCE_OF) intre instantele clasei si clasa, relatie care poate fi reprezentata grafic sub forma unui arbore, asa cum se arata in figura urmatoare: oClasa
INSTANTA_A Obiect1 INSTANTA_A Obiect2

Figura 1. Reprezentarea grafica a relatiei obiect-clasa.

96

Relatia clasa-clasa
Relatiile dintre tipuri este o consecinta a modului de definire a unor noi tipuri pornind de la cele existente. Un tip abstractizeaza un segment din lumea inconjuratoare. Modul si nivelul de abstractizare a fiecarui tip depind de cerintele formulate asupra lui pe timpul procesului de modelare. Pentru definirea unui nou tip, exista doua procedee si anume: compunere (agregare) - obiectele instantiate ale noului tip vor exista ca imbinari ale obiectelor corespunzatoare (numite si obiecte inglobate) tipurilor agregate. Cu toate ca starea interna a noilor obiecte este o cumulare a starilor interne ale obiectelor componente, comportarea acestora este descrisa de reguli specificate in mod explicit la definirea tipului. Simbolic, agregarea a doua tipuri poate fi reprezentata cu ajutorul unei scheme ca cea din fig. 2, in care noul tip Calculator, este rezultatul compunerii tipurilor Hard si Soft. Calculator Hard Soft
Figura 2. Reprezentarea simbolica a agregarii tipurilor Hard si Soft pentru obtinerea noului tip Calculator.

Prin procesul de agregare a doua sau mai multe tipuri rezulta o relatie de tip PARTE_DIN (PART_OF) intre tipurile agregate si noul tip. Operatia de agregare afecteaza numai asupra caracteristicilor statice ale tipurilor. specializare prin procesul de specializare a tipurilor se realizeaza dotarea unor tipuri existente cu calitati suplimentare. Este posibila specializarea unui tip din unul sau mai multe tipuri. Noul tip va mosteni trasaturile tipurilor pe care le specializeaza, la care poate contribui in mod optional cu propriile sale caracteristici. Operatia de specializare afecteaza atat asupra caracteristicilor statice cat si asupra caracteristicilor dinamice ale tipurilor angrenate in proces. Operatiile de derivare a claselor si instantierea claselor template sunt cazuri particulare de specializare. Prin procesul de derivare rezulta relatii de tip ESTE_O sau ESTE_UN (IS_A) intre tipul rezultat si tipurile antrenate in proces. Relatiile clasa-clasa obtinuta prin derivarea unei clase se poate reprezenta schematic ca in fig. 3, care se interprezteaza astfel: tipul CalculatorPersonal ESTE_UN AparatElectronic si ESTE_UN BunComercial
AparatElectronic BunComercial

ESTE_UN CalculatorPersonal

ESTE_UN

Figura 3. Relatia clasa-clasa obtinuta prin derivarea unei clase.

generalizare definirea noilor tipuri se realizeaza pornind de la trasaturile comune ale unui set de tipuri. Generalizarea se rasfrange atat asupra structurii cat si asupra comportamentului tipurilor antrenate in proces. Operatiile de factorizare a claselor (opusul derivarii) si definirea claselor template sunt cazuri particulare ale generalizarii. In urma generalizarii rezulta o relatie de tip UN_FEL_DE (KIND_OF) intre tipul rezultat si tipurile angrenate in proces, relatie care poate fi reprezentata schematic ca in figura 97

urmatoare, in care tipul AparatElectronic este UN_FEL_DE Televizor si UN_FEL_DE CalculatoPersonal.


AparatElectronic UN_FEL_DE UN_FEL_DE

Televizor

CalculatorPersonal

Figura 4. Relatia clasa-clasa obtinuta prin generalizarea claselor.

Abstractizare
Abstractizarea reprezinta procesul de ignorare intentionata a detaliilor nesemnificative si retinerea proprietatilor definitorii ale unei entitati. Abstractizarea imbraca urmatoarele aspecte: abstractia procedurala reprezinta ignorarea detaliilor de desfasurare ale proceselor. Programul scris intr-un limbaj care permite abstractia procedurala este format dintr-un set de rutine ce se apeleaza in functie de fluxul evenimentelor. Utilizarea unei rutine nu presupune cunoasterea detaliilor legate de implementarea acesteia. Ca urmare, utilizatorul va gandi in termenii unei operatii logice de program si nu in termenii unor instructiuni sau comenzi de limbaj. abstractia datelor permite ignorarea datelor legate de reprezentarea unui tip de date. De exemplu, utilizarea in Pascal a tipului de data set nu implica cunostinte legate de reprezentarea interna a datelor sau de modul in care compilatorul efectueaza operatiile primitive cu acest tip de data. clasele cele doua abstractii prezentate reprezinta cheia modelului algoritmic de programare, sintetizat de Dijkstra astfel: Algoritmi + structuri de date = programe. In programarea orientata pe obiecte cele doua abstractii se regasesc intr-una singura, clasa. Deci, clasa combina pentru o entitate de nivel inalt atat abstractia procedurala cat si abstractia datelor. La utilizarea obiectelor este suficienta interpretarea obiectului ca entitate; utilizatorul nu trebuie sa cunoasca detaliile legate de datele interne ale clasei si de metodele acesteia. Cea mai puternica forma de abstractizare in procesul de definire a unor noi tipuri este operatia de agregare.

Incapsulare
Incapsularea reprezinta procesul de separare a informatiilor de manipulare ale unei entitati de informatiile de implementare. Incapsularea implica abstractizarea. Clasele pot fi impartite in doua sectiuni distincte: interfata si implementarea. Interactiunea obiectelor se realizeaza cu ajutorul interfetei; implementarea nu este cunoscuta de utilizatorii externi, ea fiind destinata exclusiv uzului intern al obiectelor. Incapsularea este o forma de abstractizare care prin eliminarea accesului ocazional, intamplator, la sectiunea de implementare a claselor promoveaza o vedere exterioara a obiectelor care este determinata de sectiunea de interfata. Obiectivele urmarite prin incapsulare sunt: contopirea datelor cu codul in cadrul claselor; facilizarea detectarii erorilor (intotdeauna cauza unei erori se afla in interiorul unei singure clase); modularizarea problemei de rezolvat (fiecare clasa va rezolva o anumita problema).

98

Mostenire
Mostenirea este o facilitate noua introdusa de limbajele orientate pe obiecte si are la baza doua din operatiile de definire a unor noi tipuri: specializarea si generalizarea. Ea confera limbajului capacitatea de clasificare a tipurilor prin organizarea acestora in taxonomii sau ierarhii de tipuri. In functie de pozitia ocupata in ierarhie, un tip poate fi generalizarea sau specializarea altor tipuri; calitatea de generalitate a unui tip crescand pe masura urcarii in ierarhie. O clasa este superclasa (supertip) sau clasa de baza pentru toate clasele care o specializeaza si subclasa (subtip) sau clasa derivata pentru toate clasele pe care le specializeaza. Daca in cadrul unei ierarhii de clase specializarea se aplica exclusiv unui singur tip spunem ca avem o mostenire simpla. In rest avem o mostenire multipla. Cel mai intuitiv mod de reprezentare a unei ierarhii de tipuri este arborele multicai, in cazul mostenirii simple, sau graficul aciclic directionat in cazul mostenirii multiple. Astfel, exemplele prezentate la specializare si generalizare pot fi unite in cadrul aceleasi ierarhii, rezolvand o mostenire multipla. AparatElectronic
ESTE_UN ESTE_UN

BunComercial
ESTE_UN

Televizor

CalculatorPersonal

Figura 5. Reprezentarea unei mosteniri multiple.

In figura anterioara au fost evidentiate doar relatiile care pot fi deduse din ierarhiile initiale (cu toate ca si tipul Televizor ESTE_UN BunComercial) iar relatiile de tipul UN_FEL_DE au fost transformate in relatii de tipul ESTE_UN. In procesul de proiectare a ierarhiilor de clase se incearca factorizarea caracteristicilor comune fie ele statice sau dinamice ale claselor de pe un anumit nivel de ierarhizare prin implementarea lor pe nivelele superioare. Identificarea unei clase de baza pentru un set de clase este tot o forma de abstractizare. Intr-adevar, generalizarea implica concentrarea atentiei asupra trasaturilor comune si ignorarea particularitatilor caracteristice fiecarei clase in parte. O ierarhie este o clasificare diferentiala a tipurilor. Trasaturile unei clase derivate se exprima in termenii diferentelor fata de trasaturile claselor de baza. De aici rezulta ca o clasa nu are sens in afara ierarhiei de care apartine. Desi contravine modului natural de clasificare a tipurilor, ierarhia de aparteneta a unei clase trebuie specificata in mod unic. Clasificarea diferentiala a tipurilor determina si principalele caracteristici ale unei ierarhii de clase si anume: reutilizarea codului poate fi atinsa prin mostenirea interfetei sau mostenirea codului. Prin mostenirea codului se urmareste implementarea functionalitatii mai multor clase intruna singura care devine astfel clasa de baza. Clasele derivate vor fi vederi externe diferite ale aceleaiasi entitati. Ierarhiile care promoveaza acest gen de mostenire pot fi identificate prin prezenta masiva a codului in clasele de baza si simplitatea relativa a codului claselor derivate. Mostenirea interfetei este, intr-un fel, situatia opusa mostenirii codului. Clasele de baza vor fi modele abstracte ale claselor derivate si vor defini interfata, iar clasele derivate vor oferi versiuni de operare ale claselor de baza prin dezvoltarea interfetei. Ierarhiile care promoveaza acest gen de mostenire pot fi recunoscute prin concentrarea codului in clasele derivate si caracterul generic al codului claselor dr baza. Cele doua cazuri de mostenire nu se exclud reciproc. extensibilitatea si adaptabilitatea sunt caracteristici care fac posibila dezvoltarea si intretinerea unei taxonomii de clase. Oricand este posibila expandarea ierarhiei prin alipirea de noi clase, iar modificarile la care este supusa o clasa de baza se rasfrang si asupra claselor derivate. 99

Polimorfism
Prin definitie, polimorfismul este capacitatea unei entitati de a imbraca mai multe forme. Limbajul C++ suporta urmatoarele forme de polimorfism: polimorfismul parametric apare in cazul aplicarii unei functii pe argumente de tipuri diferite. Este cea mai slaba forma de polimorfism si este mostenita din limbajul C (de fapt este intalnita in majoritatea limbajelor algoritmice) polimorfismul ad-hoc este forma de polimorfism care se bazeaza pe supraincarcarea functiilor si mascarea metodelor in cadrul unei ierarhii de clase. Astfel, aceeasi functie suporta implementari diferite si metodele unei clase de baza pot fi redefinite prin mascare sau pot fi supraincarcate in clasele derivate. polimorfismul de mostenire apare ca o consecinta a clasificarii claselor in ierarhii si presupune manipularea obiectelor de un anumit tip in situatii care cer tipuri diferite de tipul obiectelor. Teoretic, obiectele unei clase D pot fi utilizate in orice situatie care necesita prezenta unor obiecte de tipul B, unde B este o superclasa a clasei D.

Declararea claselor
Pentru a exemplifica modul de declarare a unei clase vom introduce clasa numerelor complexe, presupunand ca intr-o aplicatie sunt necesare foarte multe operatii cu numere complexe. Un numar complex poate fi reprezentat cu ajutorul a doua numere reale, declarate in mod indpendent:
float Re, Im;

sau cu ajutorul unei structuri, evedentiind astfel legatura intrinseca dintre cele doua elemente, partea reala si partea imaginara:
struct Complex { float _re; float _im; };

Trecerea de la structura Complex la clasa CComplex se face foarte usor prin utilizarea cuvantului cheie class in locul cuvantului cheie struct astfel:
// clasa CComplex este echivalenta cu structura Complex class CComplex { public: float _re; float _im; };

Se poate remarca aparitia cuvantului cheie public necesar asigurarii accesului la membrii clasei CComplex: _re si _im. Declararea unei variabile de tipul CComplex, adica a unui obiect, se face similar variabilelor de tipul Complex, adica prin declaratii de instantiere:
CComplex complex1, complex2; // instantierea clasei CComplex

Sa presupunem ca este necesara tiparirea unor numere complexe. Folosind structura Complex, pentru a afisa numarul complex z sub forma (parte_reala, parte_imaginara) se poate folosi o functie numita Tipareste, care poate avea urmatoarea forma:
void Tipareste(Complex z) { printf((%f, %f)\n, z._re, z._im); }

In cazul clasei CComplex functia Tipareste trebuie sa fie declarata chiar in interiorul clasei, pentru a evidentia astfel obiectul operatiei de tiparire a numerelor complexe:
class CComplex { public: float _re; float _im; void Tipareste() {

100

} };

printf((%f, %f)\n, _re, _im);

Functia Tipareste nu mai necesita prezenta argumentului de tipul CComplex acesta fiind determinat de obiectul care necesita tiparirea iar sintaxa de apel a functiei este:
CComplex complex1; // Instantierea clasei complex1.Tipareste(); // Apelarea functiei Tipareste

Functia Tipareste si variabilele _re si _im sunt membrii clasei CComplex. Functia Tipareste este o functie membru sau o metoda, iar variabilele _re si _im sunt date membru sau atribute ale clasei CComplex. Atributele descriu starea interna a obiectului, iar metodele descriu comportarea obiectelor de tipul respectiv in momentul satsfacerii unei cereri de executie.
Sugestie: Functiile membru (metodele) scrise in cadrul clasei sunt privite de catre compilator ca functii inline. In cazul functiilor inline, compilatorul inlocuieste fiecare apel cu codul functiei. De aici rezulta ca o functie va fi descrisa in cadrul clasei numai daca are codul foarte scurt si nu contine instructiuni de ciclare. In caz contrar, clasa trebuie sa contine numai prototipul functiei.

Operatorul de rezolutie
Pentru a nu incarca excesiv declaratia clasei, o metoda poate fi definita si in afara clasei, caz in care in cadrul clasei se va include prototipul functiei. De exemplu, daca vrem ca functia Tipareste sa fie definita in afara clasei, atunci declaratia clasei CComplex ar trebui sa arate astfel:
class CComplex { public: float _re; float _im; void Tipareste(); };

// Prototipul functiei Tipareste

Pentru a specifica apartenenta functiei Tipareste la clasa CComplex in antetul de definire a functiei se va folosi operatorul de rezolutie (::) precedat de numele clasei:
void CComplex::Tipareste() { // Definirea functiei in afara clasei printf((%f, %f)\n, _re, _im); }

Controlul accesului la membrii clasei


Incapsularea nu inseamna numai combinarea datelor cu functiile in cadrul unei singure entitati, ea implica si un anumit grad de limitare a accesului la membrii clasei. In C++ exista trei forme de specificare a accesului la membrii unei clase prin mentionarea cuvintelor cheie public, proteced si private, numite si specificatori de acces. In declaratia unei clase pot fi specificate toate cele trei forme, doar unele sau nici una. Ordinea in care sunt indicate aceste cuvinte cheie nu are importanta, iar repetarea unora in cadrul aceleasi clase este permisa. Tinand cont de specificatorii de acces, sintaxa declararii unei clase poate arata astfel:
class <nume_clasa> { public: // sectiunea publica protected: // sectiunea protejata private: // sectiunea privata proteced: // continuarea sectiunii protejate ...

101

};

Sectiunea publica a unei clase este destinata folosirii neingradite, indiferent de natura potentialului utilizator. De exemplu, clasa CComplex, in forma care a fost definita (vezi pag. 100), permite accesul unui utilizator la oricare din membrii sai:
CComplex z; z._re = 10; z._im = -7.6; z.Yipareste(); float x = z._re;

Sectiunea privata a unei clase este destinata uzului intern al clasei, interzicand accesul extern. De obicei, in sectiunea privata se definesc datele interne si metodele folosite in comun de catre unele metode din sectiunea publica. Forma in care a fost definita clasa CComplex nu este cea mai potrivita, deoarece permite accesul la datele membru _re si _im, care totusi tin de bucataria clasei. Vom modifica definirea clasei trecand datele membru in sectiunea privata si vom defini in sectiunea publica doua noi metode pentru manipularea lor.
class CComplex { public: // Sectiune publica void Tipareste() { printf((%f, %f)\n, _re, _im); } // Metode de modificare a datelor membru void Re(float re) { _re = re; } void Im(float im) { _im = im; } private: // Sectiune privata float _re; float _im; };

In aceste conditii, accesul utilizatorilor la membrii clasei se face astfel:


CComplex z; z.Re = 15; z.Im = -1.6; z.Yipareste(); float x = z._re; // // // // // Instantiere Partea reala ia valoarea 15 Partea imaginara ia valoarea 1.6 Afiseaza (15, -1.6) Genereaza o eroare, membrul este inaccesibil

Din cele prezentate rezulta ca, sectiunea publica a unei clase reprezinta interfata cu exteriorul, iar sectiunea privata corespunde implementarii.
Nota In limbajul C++ limitarea dreptului de acces la membri se face la nivelul clasei si nu al obiectului, ca in unele dintre celelalte limbaje orientate pe obiecte.

Limbajul C++ ofera un mecanism mai rafinat de control al accesului la membri unei clase prin introducerea cuvantului cheie protected, care asigura o diferentiere a modului de tratare a untilizatorilor unei clase in functie de natura acestora. Daca utilizatorul este o subclasa a clasei respective, atunci ea va avea acces la sectiunea protejata pe care o va vedea ca pe o sectiune publica. Daca utilizatorul nu este o subclasa a clasei, atunci nu va avea acces la sectiunea protejata pe care o va vedea ca pe o sectiune privata. In cazul clasei CComplex, daca vrem ca ulterior sa declaram o subclasa, este indicat ca sectiunea privata s-o declaram ca sectiune protejata. In aceste conditii declararea clasei CComplex ar trebui sa aiba urmatoarea forma:
class CComplex { public: // Sectiune publica void Tipareste() { printf((%f, %f)\n, _re, _im); } // Metode de modificare a datelor membru

102

};

void Re(float re) { _re = re; } void Im(float im) { _im = im; } protected: // Sectiune protejata float _re; float _im; Nota: In absenta oricarui specificator de acces, toata clasa este considerata ca fiind sectiune privata.

Indicatorul this
Pentru fiecare instanta a clasei CComplex (definita anterior) compilatorul va rezerva o zona de memorie pentru stocarea atributelor _re si _im. In momentul accesarii unei date membru, compilatorul se va folosi de adresa obiectului, cunoscuta in urma procesului de alocare a memoriei si de pozitia relativa a datei membru fata de inceputul zonei de memorie alocata obiectului, cunoscuta din declaratia clasei. Adresa instantelor clasei CComplex este un pointer de tipul CComplex* const si poarta numele this. Metodele clasei CComplex au un statut special. Exista o singura copie a fiecarei metode definita in cadrul clasei CComplex, iar specificarea instantei de apel se face prin transmiterea suplimentara a indicatorului this intr-un mod transparent pentru utilizator. De exemplu, avand declaratia:
CComplex z;

atunci apelul:
compl.Tipareste();

invoca metoda Tipareste cu parametrul this = &z, iar apelul:


compl.Re(3.7)

invoca metoda Re cu parametrii this = &compl si 3.7. this este de fapt un cuvant cheie al limbajului C++ si poate fi referit in cadrul definitiei unei clase ca adresa obiectului transmisa suplimentar la apelul unei metode:
void CComplex::TiparesteThis() { printf(Adresa obiectului: %p, this); }

Ca pointer de tipul CComplex* const, this va putea fi folosit numai in operatii de citire; orice tentativa de atribuire a unei valori acestuia va determina aparitia unei erori.

Tipuri referinta
Tipurile referinta sunt tipuri derivate introduse ca o alternativa la utilizarea tipurilor indicator. Pentru clasa CComplex tipul referinta se va nota CComplex&, iar variabilele de tip referinta se vor declara astfel:
CComplex z; CComplex& rZ = z; // referinta la obiectul z

Orice operatie efectuata asupra referintei rZ va afecta si entitatea referita, in cazul de fata obiectul z. De asemenea, modificarea obiectului z va avea ca efect actualizarea referintei sale, rZ. Rezulta ca, referinta este un alias, adica un alt nume (pseudonim) pentru una si aceeasi entitate. O entitate poate avea la un moment dat mai multe referinte. Tipurile referinta pot fi utilizate in orice constructie a limbajului care accepta tipuri fundamentale sau tipuri utilizator, cu exceptia unor situatii speciale (referinta la referinta sau pointer la referinta). De asemenea, 103

variabilele de tip referinta pot fi folosite in orice situatie care necesita folosirea entitatii referite. Chiar si adresa variabilei referinta este aceeasi cu a entitatii referite:
CComplex z; CComplex& rZ = z; int b = &z == &rZ; // b !=0 deoarece adresele sunt egale

Deoarece referinta este un alias, ea se poate folosi si ca initializare. Lipsa entitatii referite din cadrul declaratiei unei referinte determina aparitia unei erori:
CComplex z; CComplex& rZ1; CComplex& rZ2 = z; // eroare, declaratie inconsistenta // declaratie cu initializare

Deci singura operatie ce actioneaza direct asupra referintei este initializarea referintei. Restul operatiilor actioneaza asupra entitatii referite, inclusiv atribuirea unei variabile de tipul entitatii referite:
int i = 10, j = 20; int& rI = i; rI++; // rI = i = i + 1 rI = j; // rI = j = 20

Cea mai buna utilizare a tipurilor referinta este folosirea acestora la transmiterea prin referinta a parametrilor de apel al functiilor, nefiind necesare deferentieri pentru a accesa entitatea referita.

Argumente cu valori prestabilite


Exista foarte multe situatii cand este necesar ca unei functii sa i se transmita un numar de parametrii actuali mai mic decat numarul parametrilor formali. In asmenea situatii, la declararea functiei se folosesc argumente cu valori prestabilite (implicite). De exemplu, metoda ReIm prezentata in continuare este o functie cu doua argumente avand valori prestabilite:
void CComplex::ReIm(float re = 0, float im = 0) ( _re = re; _im = im; }

In programe, metoda ReIm poate fi apelata cu un singur argument, cu doua argumente sau cu nici un argument. Lipsa parametrilor actuali din apel va determina atribuirea valorilor prestabilite parametrilor formali:
CComplex z; z.ReIm(7.5, 15); z.ReIm(); z.ReIm(0.5); // _re = 7.5, _im = 15 // _re = 0, _im = 0 // _re = 0.5, _im = 0

Daca functia in cauza este o metoda si este definita in afara clasei, atunci specificarea argumentelor implicite se poate face fie la declarare fie la definire (cazul exemplului prezentat), dar de regula nu in ambele locuri. Nu este obligatoriu ca toate argumentele functiei sa aiba valori prestabilite, dar daca ele exista atunci trebuie sa fie plasati ultimele in lista parametrilor formali. Din cele prezentate rezulta ca, antetul unei functii de forma:
void CComplex::ReIm(float re = 0, float im);

este o eroare, deorece argumentul prestabilit nu este ultimul in lista parametrilor formali. Argumentele implicite se folosesc, de regula, atunci cand vrem sa extindem prelucrarile oferite de o functie fara a fi a modifica codul sursa care apela vechea versiune a functiei.

104

Construirea si distrugerea obiectelor


Scop si vizibilitate
Dupa cum se cunoaste, prin scopul unui identificator se intelege zona din program in care identificatorul ar putea fi accesat, facand abstractie de ceilalti identificatori, iar prin vizibilitatea unui identificator se intelege zona de program in care identificatorul poate fi accesat in prezenta celorlalti identificatori. Rezulta deci, ca scopul unui identificator include si vizibilitatea identificatorului respectiv. Un obiect, la fel ca orice variabila de tip fundamental, poate fi folosit in operatii de atribuire, ca parametru al unei functii sau ca entitate returnata de o functie. Orice alt tip de manipulare a unui obiect, inclusiv inhibarea operatiilor enumerate mai sus, este impus de metodele clasei a carei instanta este. La fel ca in cazul variabilelor de tip fundamental, obiectele pot fi entitati globale, automatice sau alocate si eliberate in mod dinamic, iar numele unui obiect respecta aceleasi reguli de scop si vizibilitate ca si numele variabilelor de tip fundamental. Pe langa categoriile de scop din limbajul C (bloc, functie, prototip de functie si fisier), limbajul C++ introduce o noua categorie de scop, si anume clasa. Toate numele introduse in cadrul unei declaratii de clasa apartin scopului clasei respective. La utilizarea unui nume in afara clasei care l-a introdus se respecta restrictiile impuse de specificatorul de acces sub incidenta careia a fost definit numele in cadrul clasei si regulile de rezolutie a scopului:
class CClasa { public: enum TIP_VAL {VAL1, VAL2, VAL3}; protected: typedef int WORD; private: enum {ANONIM1, ANONIM2}; }; int i = CClasa::VAL1; // corect int j = VAL2; // eroare, scop nespecificat WORD w1; // nume inaccesibil int k = CClasa::ANONIM1; // nume inaccesibil

In exemplul prezentat, numele ANONIM1 si ANONIM2 nu vor putea fi utilizate nici macar in posibilele subclase ale clasei CClasa datorita restrictiilor de acces la membru existente. In cazul unui nume atasat la doua entitati diferite dintre care una are scop global iar cealalta are scop clasa, pentru referirea entitatii globale din cadrul clasei se va folosi operatorul de rezolutie urmat de numele entitatii:
int i =8 // Scop global ... void CClasa::Metoda() { int i =3; // Scop clasa; ... int j = ::i; // Referire la scopul global ... }

Un membru al unei clase poate fi referit in cadrul scopului clasei chiar si inainte de punctul de declarare, proprietate valabila de altfel doar pentru scopul de tip clasa:
class CClasa { ... void Metoda() { ... Metoda Post(); ... } void Post(); };

105

Construirea obiectelor
Sa consideram urmatoarea declaratie de instantiere a unui numar complex:
CComplex z;

In momentul intalnirii declaratiei compilatoarele C++ vor genera codul necesar alocarii spatiului de memorie si pentru apelarea unei functii de construire a obiectului numita constructor. Deoarece in cazul clasei CComplex nu s-a definit nici un constructor, compilatorul va genera un constructor (numit constructor generat implicit) care in mod prestabilit nu executa nici o operatie. Daca la definirea unei clase nu am declarat explicit un constructor, atunci orice incercare de initializare la declararea unui obiect, de exemplu,
CComplex z(1.5, -2.7);

va determina aparitia mesajului de eroare Cannot convert int to CComplex. Constructorul este o functie fara tip avand acelasi nume ca si clasa in care este definita si nu returneaza nici o valoare. Rolul functiei constructor este efectuarea unor operatii de initializare si achizitionarea de resurse de catre instantele clasei. De exemplu, constructorul definit in continuare in clasa CComplex initializeaza cu 0 ambele date membru:
class CComplex { public: CComplex() { // Functie constructor _re = _im = 0; } ... };

Daca se doreste ca in momentul instantierii clasei CComplex sa se poata specifica valori pentru initializarea datelor membru, atunci functia constructor trebuie definita cu parametri:
class CComplex { public: CComplex(float re, float im) { // Functie constructor _re = re; _im = im; } ... }; .. CComplex z(1.5, 3.7); // Initializeaza partea imaginara cu 1.5 // si partea imaginara cu 3.7

Intr-o clasa se pot defini mai multe functii constructor, dar ele trebuie sa difere prin numarul si tipul parametrilor formali. Constructorul care va fi folosit in momentul instantierii va depinde de parametrii indicati. De exemplu, clasa CComplex prezentata in continuare are doi constructori:
class CComplex { public: float _re; float _im; CComplex(float re, float im) { // Primul constructor _re = re; _im = im; } CComplex(float im) { // Al doilea constructor _re = 0; _im = im; } void Tipareste() { // Metoda pentru afisarea valorilor printf("(%f, %f)\n", _re, _im); } };

106

Primul constructor permite programatorului sa specifice valorile de initializare pentru ambele date membru ale clasei si va fi putea fi folosit la instantierea obiectelor sub forma:
CComplex z(1.5, -4.5);

in timp ce al doilea constructor permite programatorului sa specifice valoarea de initializare doar pentru componenta imaginara, componenta reala fiind initializata direct in constructor si va fi putea fi folosit la instantierea obiectelor sub forma:
CComplex z1(7.55); Observatii: Daca o clasa contine una sau mai multe functii constructor care asigura initializarea datelor membru, atunci la instantierea unui obiect al clasei respective este obligatoriu sa se specifice valorile de initializare. In cazul in care vrem sa instantiem obiecte carora sa nu le specificam valoarea de initializare, este necesar ca in clasa sa se defineasca si o functie constructor fara parametri, de forma: CComplex() { } // Constructor care nu excuta nici o operatie De regula, constructorii se pot defini in interiorul clasei, deoarece acestia ar trebui sa aiba putine instructiuni si sa nu contina instructiuni repetitive. Un constructor nu poate avea un parametru formal de tipul clasei din care fac parte.

Distrugerea obiectelor
Exista foarte multe situatii cand la crearea unor obiecte este necesara alocarea unor spatii de memorie, care dupa terminarea folosirii obiectelor respective trebuie sa fie eliberate pentru a le putea refolosi. Pentru a elibera memoria alocata de constructor la crearea obiectelor se folosesc functiile de distrugere asociate clasei respective, numite destructori. Destructorii sunt necesari numai daca la crearea obiectelor s-a alocat spatiu in memoria heap, folosind operatorul new sau orice alta functie de alocare dinamica a memoriei admisa de limabjul C++. In cazul clasei CComplex, folosita pana in prezent pentru exemplificare, nu este necesara o functie de distrugere, deoarece la crearea unui obiect al clasei respective nu se aloca memorie folosind operatorul new. Daca pentru a crea obiectul unei clase se foloseste operatorul new pentru a aloca memorie dinamica, atunci la eliberarea memoriei se poate folosi operatorul delete. Un destructor este o functie care are acelasi nume ca si clasa de care apartine, dar este precedat de caracterul tilda (~). Destructorii nu au argumente. Sa presupunem ca doriti sa creati un tip de date cu numele CString, care sa dispuna de proprietatile sirurilor terminate cu caracterul null din C, dar sa utilizeze o abordare orientata pe obiecte. Altfel spus, ceea ce doriti este un pointer (numit pSir) la un tablou de caractere (avand, sa zicem, cel mult 80 de caractere) si posibilitatea de a stoca in acest tablou siruri in stil C, terminate cu caracterul null. Deoarece aveti nevoie de lungimea sirului, decideti ca ea sa fie stocata ca o variabila membra, numita lung. Este indicat ca definirea clasei sa fie salvata intr-un fisier cu extensia .h, iar definirea functiilor membru intr-un fisier cu extensia .cpp. Definirea clasei CString, salvata in fisierul ClassSir.h este urmatoarea:
// Interfata clasei CString enum Boolean {false, true}; // Defineste un tip boolean class CString { char *pSir; int lung; public: CString(char *s); // Constructor pentru initializarea obiectului // cu pointerul spre sirul de caractere stocat in heap CString(int nrcar = 80); // Constructor care rezerva in heap spatiul // necesar unui sir de cel mult 80 caractere CString(const CString&); // Constructor de copiere ~CString(); // Destructor int retlung(); // Returneaza lungimea sirului

107

void afiseaza(); // Afiseaza sirul de caractere int citeste(); // Citeste un sir de caractere Boolean atribsir(CString *s); // Transfera sirul referit de s in zona // rezervata obiectului curent. Daca zona nu este // suficienta, se truncheaza sirul si se returneaza // false; altfel returneaza true. };

Definirea functiilor membru (metodele) ale clasei CString au fost salvate in fisierul ClassSir.cpp:
// Definirea functiilor membru ale clasei CString . Se verifica daca // fisierele antet string.h si stdio.h sunt sau nu incluse. In caz ca aceste // fisiere nu sunt incluse, atunci ele se vor include. #ifndef _STDIO_h #include <stdio.h> #define _STDIO_H #endif #ifndef _STRING_H #include <string.h> #define _STRING_H #endif #include "ClassSir.h" CString::CString(char *s) { // Constructor - Initializeaza obiectul cu pointerul spre copia // in heap a sirului referit de s lung = strlen(s); // lungimea sireului pSir = new char[lung + 1]; // aloca spatiu in heap strcpy(pSir, s); // transfera sirul in heap } CString::CString(int dim) { // Constructor - Rezerva spatiu in heap pentru siruri de lungime dim lung = dim; pSir = new char[lung + 1]; *pSir = '\0'; // Stocheaza sirul nul } CString::CString(const CString& s) { // Constructor de copiere lung = s.lung; pSir = new char[lung + 1]; strcpy(pSir, s.pSir); } inline CString::~CString() { // Destructor - Elibereaza zona heap a lui pSir delete pSir; } inline int CString::retlung() { // Metoda - returneaza lungimea sirului return lung; } inline void CString::afiseaza() { // Metoda - afiseaza sirul referit de pSir printf(pSir); printf("\n"); } int CString::citeste() { // Metoda - citeste un sir de la intrarea standard si-l stocheaza // in zona heap alocata obiectului curent. Returneaza: // 0 - la sfarsit de fisier // -1 - la trunchierea sirului citit // 1 - la terminare normala. char t[255]; // Variabila temporara pentru citire if(gets(t) == 0) return 0; // S-a intalnit sfarsitul de fisier strncpy(pSir, t, lung);

108

} Boolean CString::atribsir(CString *s) { // Transfera sirul referit de s in zona rezervata sirului obiectului // curent; daca nu este suficient spatiu, se truncheaza sirul referit // de s si se returneaza false; in caz contrar se returneaza true. strncpy(pSir, s->pSir, lung); if(strlen(s->pSir) > lung) return false; return true; }

*(pSir + lung) = '\0'; if(strlen(t) > lung) return -1; else return 1;

// Plaseaza terminatorul de sir // Sirul citit a fost trunchiat

Fisierul ClassSir.h contine de fapt interfata clasei CString, iar fisierul ClassSir.cpp contine implementarea clasei CString. In clasa CString este necesara prezenta unui destructor deoarece se aloca spatiu in memoria heap la crearea unui obiect al clasei, spatiu care trebuie eliberat la distrugerea obiectului. Destructorul este definit astfel:
inline CString::~CString() { // Destructor - Elibereaza zona heap ocupata de pSir delete pSir; }

Operatorul new
In limbajul C, alocarea dinamica a zonelor de memorie in zona heap se poate realiza folosind functii de biblioteca, cum ar fi malloc si alloc. Limbajul C++ permite alocari in zona heap si prin intermediul operatorului new. Acesta este un operator unar si are aceeasi prioritate ca si ceilalti operatori unari. Opeartorul new are ca valoare adresa de inceput a zonei de memorie alocata in heap sau zero (pointerul null) in cazul in care nu poate face alocarea. Operandul operatorului new in forma lui cea mai simpla, este numele unui tip (predefinit sau definit de utilizator). De exemplu, urmatoarele instructiuni:
int *pInt; pInt = new int;

asigura alocarea in memoria heap a unei zone de memorie in care se pot pastra date de tip int si stocheaza adresa de inceput a acestei zone in pointerul pInt. Expresia:
*pInt = 100;

va stoca intregul 100 in zona respectiva. Aceeasi alocare se obtine in limbajul C, folosind functia malloc astfel:
pInt = (int *)malloc(sizeof(int)); new tip(expresie)

Zonele de memorie alocate cu operatorul new pot fi initializate utilizand o expresie de forma: unde: tip este numele unui tip (fundamental sau definit de utilizator), iar expresie este o expresie a carei valoare initializeaza zona de memorie alocata. De exemplu, instructiunea
pInt = new int(100); aloca spatiu in heap pentru un intreg, stocheaza in aceasta zona valoarea 100 si atribuie lui pInt adresa de inceput a zonei respective. Daca vrem sa folosim operatorul new pentru a aloca zona de memorie in heap pentru tablouri,

atunci trebuie sa folosim urmatorul format:


new tip[exp_int]

unde: exp_int este o expresie de tip intreg. Prin aceasta constructie se rezerva, in heap, o zona de memorie de exp_int * sizeof(tip) bytes. 109

Operatorul delete
O zona de memorie alocata folosind operatorul new se elibereaza prin operatorul delete. Daca p este un pointer spre tip:
tip *p;

si
p = new tip;

atunci zona de memorie heap alocata cu ajutorul lui new se elibereaza folosind constructia:
delete p;

Pentru a elibera memoria heap alocata tablourilor cu ajutorul lui new, se foloseste o constructie de forma:
delete[exp_int] p;

unde: exp_int defineste numarul elementelor tabloului (prezenta lui nu este obligatorie).

Functia inline
Dupa cum se cunoaste. la apelul unei functii obisnuite nu se substituie apelul functiei prin corpul ei, ci se realizeaza un salt la zona de memorie in care se pastreaza corpul functiei respective. Dupa terminarea executiei functiei se revine in punctul imediat urmator apelului. Un apel de aceasta forma, numit si apel cu revenire, implica diferite operatii suplimentare. Atunci cand functia este foarte simpla, are 2-3 instructiuni, operatiile implicate de apel si revenire pot fi mai costisitoare decat cele implicate de functia insasi. Din acest motiv, in astfel de situatii, ar fi util ca apelul functiilor sa se realizeze la fel ca apelul de macrocomenzi, adica prin expandare (inlocuirea apelului prin corpul functiei). Acest lucru este posibil daca antetul functiei este precedat de cuvantul cheie inline. O astfel de functie se numeste functie inline. Exemple de functii inline se pot vedea in codul clasei CString prezentat anterior.

Constructorul de copiere
La apelul oricarei functii compilatorul genereaza codul care va asigura copierea argumentelor transmise functiei in stiva programului. In limbajul C copierea consta in multiplicarea configuratiei de biti (operatie numita si copiere la nivel de biti, in engleza bitwise copy) a datelor transmise, indiferent de tipul acestora. Limbajul C++ vede operatia de transmitere a argumentelor ca o operatie de construire de obiecte. Obiectele puse pe stiva sunt construite conform sarcinilor trasate de un constructor special numit constructor de copiere (in engleza, copy constructor). Daca nu este definit nici un constructor de copiere, compilatorul va genera un constructor de copiere implicit (default copy constructor), care va copia obiectele la nivel de membru (memberwise copy). Aceasta inseamna ca datele membru ale unui obiect sunt tratate tot ca niste obiecte. Constructorul de copiere este un constructor obisnuit cu un singur argument de tipul const CClasa&, unde CClasa este numele clasei care-l defineste. Pentru clasa CString definitia constructorului de copiere este urmatoarea:
CString::CString(const CString& s) { lung = s.lung; pSir = new char[lung + 1]; strcpy(pSir, s.pSir); }

In absenta constructorului de copiere, initializarea argumentelor de tipul CString s-ar fi realizat prin copierea datei membru pSir, a obiectului de apel si nu a entitatii referite de acesta. De fapt, ar fi existat doua obiecte care exploateaza una si aceeasi resursa, situatie care nu poate fi acceptata. Chiar mai grav, in momentul distrugerii unuia dintre cele doua obiecte prin apelul destructorului calsei, resursa partajata ar fi fost eliberata, lasand celalalt obiect intr-o stare inconsistenta. La distrugerea celui de-al doilea obiect s-ar fi incercat eliberarea unei resurse inexistente situatie care, de obicei, are ca efect blocarea sistemului. 110

Tot constructorul de copiere este folosit si la copierea valorii returnate de o functie sau la initializarea unor obiecte prin intermediul unor instante de acelasi tip. Prezenta tipului referinta ca tip de apel al constructorului de copiere este necesara pentru a evita situatia de recursivitate infinita creata de existenta unui constructor de forma: CString::CString(CString s). De fapt, incercarea de a defini un astfel de constructor de copiere va fi semnalata ca o eroare la compilare. Un constructor poate promova mai multe argumente, dar argumentele suplimentare trebuie sa fie cu valoare implicita, permitand astfel copierea automata a obiectelor la apelul functiilor si la revenirea din functii. Acest amanunt este util la declararea obiectelor cand se urmareste o initializare selectiva in functie de anumiti indicatori. Exemplu de utilizare a clasei CString: Sa se scrie un program care realizeaza urmatoarele operatii asupra obiectelor de tip CString: initializare; citire de siruri de la intrarea standard; copiere de obiecte de tip CString; atribuire de obiecte de tip CString; afisare siruri de caractere.
#ifndef _STDIO_H #include <stdio.h> #define _STDIO_H #endif #ifndef _STDLIB_H #include <stdlib.h> #define _STDLIB_H #endif #include "ClassSir.cpp" void main() { // Instantieri de obiecte de tip CString CString sir1("C++ este o extensie a limbajului C standard"); CString sir2("C++ suporta stilul de programare:\n\ - prin abstractizarea datelor;\n\ - orientata spre obiecte."); CString sir3; // Instantiere la sir nul CString sir4 = sir1; // Instantiere prin copiere // Afisarea obiectelor create mai sus printf("sir1:\n"); sir1.afiseaza(); printf("sir2:\n"); sir2.afiseaza(); printf("sir3:\n"); sir3.afiseaza(); printf("sir4:\n"); sir4.afiseaza(); // Citirea de siruri de la intrarea standard printf("Introduceti mai multe siruri de la tastatura\n"); printf("Pentru terminare folositi Ctrl + Z\n"); while(sir3.citeste()) sir3.afiseaza(); // Atribuiri de siruri if(sir3.atribsir(&sir1) == false) printf("Trunchiere la atrbuire\n"); sir3.afiseaza(); if(sir3.atribsir(&sir2) == false) printf("Trunchiere la atrbuire\n"); sir3.afiseaza(); }

111

Obiecte globale, automatice, dinamice si temporare


In functie de modul de declarare a obiectelor, ele pot fi clasificate in urmatoarele grupe: Obiecte globale sunt create in segmentul de date al aplicatiei si au ca scop fisierul sau programul, in functie de specifcatiile de linkeditare. Obiectele globale sunt construite la lansarea programului, inainte de intrarea in functia main. Distrugerea obiectelor globale are loc la terminarea programului dupa iesirea din functia main, in ordinea inversa a construirii lor la inceputul programului. Construirea si distrugerea obiectelor se realizeaza prin apelarea constructorilor si respectiv a destructorilor definiti pentru clasele respective. Obiecte automatice sunt create in stiva aplicatiei ai au ca scop functia sau blocul de cod in care sunt definite. Construirea si distrugerea obiectelor automatice are loc in functie de fluxul programului si implica apelul constructorilor si destructorilor definiti in clasa respectiva. Obiecte dinamice sunt create in memoria heap, iar pentru gestionarea lor se folosesc operatorii new si delete. Diferenta dintre acesti operatori si functiile standard de gestiune a memoriei consta in apelarea constructorilor si a destructorilor in cazul operatorilor si ignorarea lor in cazul functiilor standard. Obiecte temporare prin obiect temporar se intelege orice obiect care nu are asociat un nume, iar constructia lui se realizeaza fie prin apelul explicit al constructorilor clasei fie in mod automat de catre compilator. Existenta unui obiect temporar este limitata la expresia care determina instantierea, orice referire ulterioara in afara expresiei este imposibila. De exemplu:
void Funct() { // ... // instantierea unui obiect temporar CString(Programare orientata pe obiecte).Tipareste(); // obiectul temporar nu mai exista // ... }

Functii friend
Dupa cum am vazut una dintre proprietatile de baza a tipurilor abstracte este protectia datelor membru ale tipului respectiv. Elementele protejate constituie asa numita implementare a tipului abstract. Tipurile abstracte se definesc cu ajutorul claselor. In mod obisnuit se spune ca datele protejate ale unui tip abstract sunt incapsulate in clasa care defineste tipul respectiv. Protectia datelor se realizeaza prin faptul ca ele pot fi accesate numai de catre functiile membru ale clasei respective. De asemenea, daca o functie membru este protejata, atunci ea poate fi apelata numai prin intermediul unei functii membru a clasei respective. Acest mod de lucru, cu toate ca asigura o buna protectie a elementelor membru protejate ale unei clase (elemente protejate prin private sau protected), in anumite situatii este considerat ca fiind rigid. Astfel, desi exista functii descrise in C++ care pot fi folosite pentru a prelucra instantieri ale unei clase, ele nu se pot utiliza simplu deoarece nu sunt functii membru ale clasei respective si deci nu au acces la datele membru. Mai mult, o functie membru se apeleaza totdeauna in dependenta cu un obiect, care este numit obiectul curent al apelului. In acest fel, o functie membru se poate apela prin unul din urmatoarele formate:
nume_obiect.nume_functie_membru()

sau
pointer_nume_clasa->nume_functie_membru()

In cazul functiilor obisnuite nu sunt admise astfel de apeluri, toate datele prelucrate de functie fie sunt transferate prin parametrii fie sunt globale. Din acest motiv, o functie obisnuita poate fi 112

folosita la prelucrarea obiectelor unei clase numai daca ea se modifica in asa fel incat sa devina functie membru. Totusi, pentru ca anumite functii care nu sunt functii membru sa poata accesa elementele protejate ale unui clase s-a facut un compromis prin introducerea functiilor friend (prieten). Ele trebuie precizate ca atare in definitia clasei, prin prezenta numai a prototipurilor lor in definitia clasei si sunt precedate de cuvantul cheie friend. De exemplu, sa consideram urmatoroarea definitie a clasei CComplex:
class CComplex { float re; float im; public: CComplex(float x=0, float y=0) { // Constructor re = x; im = y; } // Functie friend pentru calculul modulului unui numar complex friend float modul(CComplex *z); // Functie membru pentru afisarea unui obiect complex void Tipareste(); }; // Definirea functiei membru si si a functiei prieten // Afisarea unui numar complex void CComplex::Tipareste() { printf("(%f, %f)", re, im); } // Determinarea modulului unui numar complex float modul(CComplex *z) { return sqrt(z->re * z->re + z->im * z->im); }

Dupa cum se poate constata, functia modul, prieten al clasei CComplex, are aceeasi definitie ca o orice functie din C si ea va putea fi apelata ca o functie obisnuita. De exemplu, sa consideram urmatoarele declaratii:
CComplex y(5, 5); float f;

atunci executarea instructiunilor:


f = modul(&y); printf(%f, f);

va afisa:
7.071068

Datorita faptului ca functiile friend nu dispun de pointerul implicit this, o functie friend are nevoie de un parametru in plus fata de o functie membru care va avea acelasi efect. Functia friend poate fi o functie obisnuita (ca in cazul functiei modul de ma sus) sau o functie membru a unei alte clase.

Supraincarcarea si redefinirea operatorilor


Limbajul C++ mosteneste integral setul de operatori ai limbajului C la care se adauga operatorul de rezolutie (::), operatorii de acces la un membru (.* si ->*) si operatorii de gestiune a memoriei dinamice (new si delete). O facilitate puternica a limbajului C++ o reprezinta posibilitatea supraincarcarii partiale a setului de operatori pentru un tip anume, astfel incat aplicarea operatorilor redefiniti asupra instantelor sau a tipului respectiv sa rezulte in actiuni definite de programator ca fiind specifice tipului respectiv. Limbajul C++ vede operatorii ca functii obisnuite, iar definirea unui operator este foarte asemanatoare definirii unei functii. 113

Intre supraincarcarea unui operator si redefinirea operatorului respectiv pentru un tip anume exista unele deosebiri: Supraincarcarea unui operator pentru un tip implica posibilitatea folosirii operatorului respectiv in conjunctie cu tipul respectiv sau cu instante ale acestuia si invers; posibilitatea utilizarii operatorului in conjunctie cu un tip sau o instanta a acestuia implica existenta supraincarcarii operatorului pentru tipul respectiv. Redefinirea unui operator pentru un tip este o supraincarcare si implica posibilitatea existentei unei versiuni definite in mod explicit de catre programator a operatorului pentru tipul in cauza. O parte dintre operatorii limbajului C++, si anume: . .* :: ?: sizeof si operatorii de preprocesare # si ##, nu pot fi redefiniti. Ceilalti operatori pot fi redefiniti pentru orice tip utilizator, iar aceste redefinitii pot fi la randul lor supraincarcate de catre alte definitii ale aceluiasi operator, in cadrul aceleasi clase. La definirea unei clase exista automat un subset de operatori gata supraincarcati de compilator pentru tipul respectiv, cum ar fi: ., ->, &, ::, .*, sizeof, =, new si delete. Acesti operatori pot fi folositi in conjunctie cu orice tip sau obiect fara a fi necesare redefiniri din partea programatorului. De asemenea tipurilor fundamentale ale limbajului C++ au gata supraincarcati majoritatea operatorilor existenti. Daca este posibila redefinirea unui operator gata supraincarcat pentru un tip utilizator si exista o definitie data de programator, atunci vechea acceptiune a operatorului, acordata implicit de compilator, va fi inlocuita de noua definitie. La redefinirea unui operator trebuie sa se tina cont de urmatoarele restrictii: Nu este permisa extinderea setului de operatori ai limbajului C++ prin adaugarea de noi simboluri (caractere sau grupuri de caractere). La redefinirea unui operator nu se schimba precedenta operatorului. La redefinirea unui operator asociativitatea operatorului ramane neschimbata. Nu este permisa redefinirea operatorilor pentru tipurile fundamentale ale limbajului C++. Redefinirea operatorilor pentru un tip poate fi realizata, in functie de operator, cu ajutorul metodelor, a functiilor globale sau a metodelor statice, iar in unele cazuri pot exista mai multe versiuni diferite ale unui operator pentru acelasi tip ca functii de tipuri diferite. Toti operatorii redefiniti pentru o clasa, cu exceptia operatorului de atribuire (=), sunt mosteniti de clasele derivate. Modul de redefinire a unui operator este asemanator unei definitii de functii obisnuite, cu deosebirea ca in locul numelui functiei se foloseste cuvantul cheie operator urmat de simbolul operatorului dorit. Deci sintaxa redefinirii unui operator este:
tip_rezultat operator simbol_operator(lista_argumente) { corp_functie }

Pentru a ilustra modul de redefinire a unui operator vom supraincarca operatorul + pentru clasa CComplex:
class CComplex { float re; float im; public: CComplex(float x=0, float y=0) { // Constructor re = x; im = y; } // Functie membru pentru adunarea obiectelor de tip complex CComplex addComplex(CComplex& z); // Functie prieten pentru scaderea obiectelor de tip complex friend CComplex subComplex(CComplex& z1, CComplex& z2); // Supraincarcarea operatorului plus (+) CComplex operator+(CComplex& z); // Functie membru pentru afisarea unui obiect complex

114

void Tipareste(); }; // Definirea functiilor membru si prieten CComplex CComplex::addComplex(CComplex& z) { // Returneaza suma dintre obiectul complex curent si cel referit de z CComplex tmp; // Obiect local de tip complex tmp.re = re + z.re; tmp.im = im + z.im; return tmp; } CComplex CComplex::operator+(CComplex& z) { // Supraincarca operatorul + pentru obiectele de tip CComplex // Returneaza un obiect complex care reprezinta suma dintre // obiectul complex curent si cel referit de z CComplex tmp; tmp.re = re + z.re; tmp.im = im + z.im; return tmp; } CComplex subComplex(CComplex& z1, CComplex& z2) { // Returneaza diferenta dintre obiectele referite de z1 si z2 CComplex tmp; // Obiect local de tip complex tmp.re = z1.re - z2.re; tmp.im = z1.im - z2.im; return tmp; } void CComplex::Tipareste() { // Afisarea unui numar complex printf("(%f, %f)", re, im); }

Presupunand existenta urmatoarei declaratii:


CComplex a(10,20), b(15,25), c;

atunci instructiunile:
c = a.addComplex(b); c.Tipareste();

vor afisa:
(25.0000 45.0000)

instructiunile:
c = subComplex(a, b); c.Tipareste();

vor afisa:
(-5.0000 5.0000)

iar instructiunile:
c = a + b; c.Tipareste();

va afisa:
(25.0000 45.0000)

115

Mostenirea
In functie de numarul tipurilor specializate la definirea unui tip, exista doua tipuri de mostenire: mostenire simpla obtinuta prin specializarea unui singur tip si mostenire multipla obtinuta prin specializarea a cel putin doua tipuri. In cateva cuvinte mecanismul de mostenire se poate exprima astfel: avand o clasa oarecare B, putem defini o alta clasa D care sa preia toate caracteristicile primeia, la care sa pot adauga altele noi, proprii doar acesteia din urma. Prima clasa se va numi clasa de baza, iar cea de a doua clasa derivata.

Clase derivate
Pentru a intelege mai usor mecanismul mostenirii vom incepe prin a prezenta un exemplu. Sa presupunem ca avem definite doua clase disk si hard1 astfel:
enum stare_op {REUSIT, ESEC}; enum stare_prot_scriere {PROTEJAT, NEPROTEJAT}; class disk { protected: stare_prot_scriere ind_prot; int capacitate, nr_sect; public: stare_op Formatare(); stare_op CitestePista(int drive, int sector_start, int numar_sectoare, void *buffer); stare_op ScriePista((int drive, int sector_start, int numar_sectoare, void *buffer); stare_op ProtejeazaScriere(); }; class hard1 { stare_prot_scriere ind_prot; int capacitate, nr_sect; int numar_partitii; public: stare_op ParcareDisc(); stare_op Formatare(); stare_op CitestePista(int drive, int sector_start, int numar_sectoare, void *buffer); stare_op ScriePista((int drive, int sector_start, int numar_sectoare, void *buffer); stare_op ProtejatScriere(); };

Analizand definitiile celor doua clase se poate constata ca o parte din datele si functiilor membru ale clasei disk se regasesc si in clasa hard1. Pentru a folosi avantajele mostenirii proprietatilor clasei disk se poate defini o clasa hard2 astfel:
class hard2:public disk { int numar_partitii; public: stare_op ParcareDisc(); };

Este evident ca procedeul folosit pentru definirea clasei hard2 este mult mai elegant, mai eficace si mai usor de inteles. Antetul folosit la definirea clasei hard2:
class hard2:public disk

are rolul de a indica compilatorului urmatoarele: clasa hard2 va derivata din clasa disk; 116

toti membrii de tip public ai clasei disk vor fi mosteniti (deci vor putea fi folositi) ca public de catre clasa hard2; toti membrii de tip protected ai clasei disk vor putea fi utilizati ca fiind de tip protected in cadrul clasei hard2. Este evident ca eficacitatea nu inseamna doar faptul ca in declaratia clasei derivate nu mai apar informatiile mostenite (ele fiind automat luate in considerare de catre compilator). Apare, in plus, avanatjul ca nu trebuie rescrise nici macar functiile membru ale clasei de baza, ele putand fi refolosite exact in maniera in care au fost definite, insa daca este necesar ele pot fi redefinite pentru a asigura o cu totul alta functionalitate. Dupa cum s-a putu constata din exemplul prezentat, relatia de derivare intre doua clase se exprima la definirea clasei derivate. In acest scop dupa numele clasei derivate, se scrie lista claselor de baza pentru clasa respectiva folosind ca separator caracterul doua puncte (:). In acest fel, o clasa derivata se defineste astfel:
class nume_clasa_derivata: lista_clase_baza { // elemente specifice clasei derivate }; unde lista_clase_baza indica clasele de baza pentru clasa derivata care se defineste. Aceasta

lista contine pentru, fiecare clasa de baza, numele clasei precedat eventual de un modificator de protectie. Clasele de baza din lista_clase_baza se separa prin caracterul virgula (,). Ca modificatori de protectie se pot folosi modificatorii public si private. Exemple:
class cl1:cl{};

Clasa cl1 este o clasa derivata a clasei cl. Clasa cl este singura clasa de baza a clasei cl1.
class cl1:private cl{};

Clasa cl1 este o clasa derivata a clasei cl. Clasa cl este singura clasa de baza a clasei cl1.
class cl1:cl, public cls{};

Clasa cl1 este o clasa derivata a claselor cl si cls. Clasele cl si cls sunt clase de baza a clasei cl1. In mod prestabilit, modificatorul de protectie este private la definirea unei clase derivate. Tabelul prezentat in continuare indica accesul in clasa derivata a elementelor mostenite in functie de protectia fiecarui element mostenit si de modificatorul de protectie utilizat in lista_clase_baza.
Accesul in clasa de baza private protected public private protected public Modificatorul de protectie din lista_clase_baza private private private public public public Accesul in clasa derivata a elementului mostenit inaccesibil privat privat inaccesibil protejat public

Din acest tabel rezulta ca o clasa derivata nu are acces la elementele clasei de baza care au protectia private. In schimb, clasa derivata are acces la elementele clasei de baza care au protectia public sau protected. Daca la definirea clasei derivate se foloseste modificatorul de protectie private, atunci elementele protejate prin protected sau public devin protejate prin protectie private. Din acest motiv, modificatorul private se va folosi in lista_clase_baza numai daca respectiva clasa derivata nu va constitui o clasa de baza pentru o alta derivare. De regula, o ierarhie de clase nu este o ierarhie finala, ea putand fi dezvoltata adaugand clase noi, care deriva din clasele terminale. Acest lucru va putea fi posibil numai daca la definirea claselor derivate se foloseste, in lista_clase_baza, modificatorul public in loc de private. In acest caz, elementele protejate prin protected si public se mostenesc in clasa derivata prin aceeasi protectie. 117

Relatia dintre clasa derivata si clasele ei de baza ridica cateva probleme si anume: Relatia dintre constructorii si destructorii clasei derivate si ai claselor de baza; Conversiile obiectelor claselor derivate si ai claselor de baza, precum si ai pointerilor catre astfel de obiecte; Redeclararea datelor membru si supraincarcarea functiilor membru ale claselor de baza in clasa derivata.

Relatia dintre constructorii si destructorii clasei derivate si ai claselor de baza


Constructorii si destructorii sunt functii membru care nu se mostenesc. La instantierea unui obiect al clasei derivate se apeleaza atat constructorii clasei derivate cat si cei ai claselor de baza. Primii sunt apelati constructorii clasei de baza urmati de constructorul clasei derivate. Ordinea de apelare a constructorilor claselor de baza este ordinea in care apar clasele respective in lista_clase_baza din definitia clasei derivate. La distrugerea unui obiect al clasei derivate, destructorii sunt apelati in ordine inversa. Primul va fi apelat destructorul clasei derivate si apoi cei a calselor de baza in ordine inversa fata de modul in care au fost apelati la instantierea obiectului respectiv. O conditie esentiala pentru a se putea apela constructorii claselor de baza prin intermediul clasei derivate este ca acestia nu trebuie sa aiba protectie de tip private. La instantierea unui obiect se transmit valori tuturor parametrilor constructorilor pentru initializarea datelor membru. In cazul unui obiect al unei clase derivate, o parte din valori se folosesc pentru initializarea datelor membru specifice ale clasei derivate, iar restul pentru initializarea datelor membru ale claselor de baza. Constructorul clasei derivate contine parametrii pentru toate valorile ce se folosesc la initializarea obiectelor. Transferul valorilor datelor membru ale claselor de baza se defineste prin intermediul antetului constructorului clasei derivate. Fie o clasa Cls derivata din clasele de baza Cls_1, Cls_2, , Cls_n:
class Cls:public Cls_1, public Cls_2, ..., public Cls_n { ... Cls(...); // protoptipul constructorului ... };

Constructorul Cls are un antet de forma:


Cls::Cls(...):Cls_1(...),Cls_2(...), ..., Cls_n(...)

Constructorul Cls are o lista de parametrii completa, adica pentru initializrea tuturor datelor membru ale unui obiect de tip Cls. Expresia Cls_i() pentru i=1, 2, , n din antetul constructorului Cls contine, in paranteza, o lista de expresii care definesc valorile initiale pentru datele membru ale clasei Cls_i. Daca o clasa Cls_i nu are constructor, atunci pentru clasa respectiva nu va fi prezenta o expresie de forma Cls_i(...) in antetul constructorului Cls. De asemenea, expresia respectiva nu va fi prezenta in antetul lui Cls daca datele membru ale clasei Cls_i se initializeaza cu valori implicite sau clasa Cls_i nu are date membru. Expresia Cls_i(...) este un apel explicit al unui constructor al clasei Cls_i. Ordinea in care se scriu expresiile Cls_i(...) in antetul constructorului clasei Cls este arbitrara. Constructorii vor fi apelati intotdeauna in ordinea in care sunt scrise clasele in lista_clase_baza. Lista care defineste apelurile constructorilor claselor de baza nu este prezenta in prototipurile constructorilor claselor derivate ci numai in antetele acestora. Exemple: Sa presupunem ca avem o clasa A derivata din clasa B si ambele au nevoie de un constructor. Definitia celor doua clase ar putea arata astfel:
enum Boolean {false, true}; class B { protected:

118

}; class A:public B { protected: Boolean ecran; int abs, ord; public: A(double p=0, double q=0, int lx=758, int ly=357):B(p,q) { // Constructorul clasei A abs = lx; ord = ly; if(0 <= p && p <= lx && 0 <= q && q <= ly) ecran = true; else ecran = false; } ... };

double x, y; public: B(double xx = 0, double yy = 0) { x = xx; y = yy; } ...

// Constructor

Pentru a instantia obiecte ale clasei A se pot folosi instructiuni de forma:


A a; A b(100,400); // a.x=0, a.y=0, a.abs=758, a.ord=357, a.ecran=true // b.x=100, b.y=400,b.abs=758, b.ord=357, b.ecran=false

Datele abs si ord pot fi initializate chiar in antetul constructorului, asa cum se arata in continuare:
A(double p=0,double q=0,int lx=758,int ly=357):B(p,q),abs(lx),ord(ly) { // Constructorul clasei A if(0 <= p && p <= lx && 0 <= q && q <= ly) ecran = true; else ecran = false; } Nota: O clasa derivata trebuie sa aiba cel putin un constructor in cazul in care cel putin una din calsele de baza au un constructor care nu este implicit sau nu are toti parametrii impliciti.

Tot ceea ce s-a prezentat despre relatiile dintre constructorii clasei derivate si ai claselor de baza a avut in vedere constructorii obisnuiti, adica constructorii care nu sunt pentru copiere. In lipsa constructorilor de copiere, compilatorul genereaza in mod automat constrtuctori de copiere impliciti care, atunci cand sunt aplicati, copiaza datele membri ale obiectului sursa (care se copiaza) in datele membru corespunzatoare ale obiectului destinatie (care se creaza). Acest mod de copiere a fost denumita copiere bit cu bit. Copierea bit cu bit nu este suficienta si atunci este necesara definirea unor constructori de copiere. Constructorii de copiere sunt necesari mai ales cand clasele au ca date membru pointeri spre zone alocate dinamic in memoria heap. La definirea constructorilor de copiere a clasei derivate trebuie sa se aiba in vedere doua reguli principale: Apelul constructorului de copiere al clasei derivate nu conduce automat la apelul constructorilor de copiere al claselor de baza. Fie A o clasa derivata din B o clasa de baza a ei. Sa presupunem ca A are un constructor de copiere de antet: A(const A& a) 119

In acest caz, clasa de baza B, fie ca nu are nici un constructor definit de programator, fie ca are un constructor definit de programator care nu are parametrii sau are numai parametri impliciti. La o instantiere de forma: A b=a; unde a este un obiect al clasei A in prealabil declarat, se apeleaza constructorul de copiere al clasei A cu antetul indicat mai sus. Apelul constructorului de copiere al clasei A conduce automat la apelul constructorului implicit al clasei B (cel generat de compilator sau cel definit de programator daca exista un astfel de constructor) sau, in caz contrar, la apelul constructorului clasei B care are toti parametrii impliciti. Daca clasa de baza B are constructori dar nici unul nu este implicit sau cu toti parametri impliciti, atunci constructorul de copiere al clasei derivate A trebuie sa contina apelul explicit al unui constructor al clasei B. Deci constructorul de copiere al clasei A va avea antetul: A(const A& a):B(...)

Conversiile obiectelor claselor derivate si ai claselor de baza


In principiu, conversiile dintr-un tip predefinit intr-un tip abstract se realizeaza cu ajutorul constructorilor, iar conversia inversa cu ajutorul supraincarcarii operatorului cast. Conversia dintr-un tip abstract intr-un alt tip abstract se poate realiza prin ambele metode. Ambele metode se pot aplica implicit cat si explicit. In continuare ne vom referi la conversiile care se pot realiza tinand seama de conceptul de derivare. Fie A o clasa pentru care B este o clasa de baza. Un obiect care este o instantiere a clasei A se converteste in mod implicit intr-un obiect al clasei B. Aceasta, deoarece obiectele clasei A sunt obiecte specializate ale clasei B. Conversia inversa nu se poate realiza fara a fi definita printr-un constructor sau prin supraincarcarea operatorului cast. Conceptul de mostenire impune reguli de conversie si pentru pointeri si referinte la obiecte ale claselor derivate si de baza. Fie declaratiile:
A *pa; A a; B *pb; B b;

unde B este o clasa de baza a clasei A. Compilatorul C++ permite atribuiri de felul celor de mai jos:
pa = &a; pb = &b;

Atribuirile de acest fel se realizeaza pe baza supraincarcarii implicite a operatorului adresa (& unar), care permite ca acesta sa se aplice la o data sau obiecte de acelasi tip. O atribuirea de forma: pb = &a; este acceptata de compilator la fel ca si atribuirea obiectului a la obiectul b: b = a; In mod analog, atribuirea: pb = pa; este corecta. In general, un pointer sau o referinta la un obiect al unei clase derivate se poate atribui la un pointer sau o referinta la un obiect al unei clase de baza a clasei derivate respective. Atribuirile de acest gen se realizeaza prin conversii implicite corespunzatoare. In schimb atribuirile:
pa = &b; pb = pb;

sunt eronate. Ele pot fi acceptate de compiltor daca se utilizeaza expresii cast care sa asigure conversia explicita de pointeri:
pa = (A *)&b; pa = (A *)pb;

Conversiile de acest gen sunt utile cand se creeaza si prelucreaza obiecte de tip colectie, colectia fiind un set de obiecte de diferite tipuri abstracte derivate dintr-un tip abstract de baza. 120

Redefinirea datelor membru ale unei clase de baza intr-o clasa derivata
O data membru a unei clase de baza se poate redefini ca data membru a unei clase derivate. De exemplu:
class B { protected: double x; double y; public: B(double xx=0, double yy=0) { x = xx; y = yy; } ... }; class A:public B { protected: double x; double y; public: A(double dx=0, double dy=0, double bx=0, double by=0):B(bx,by) { x = dx; // x este data membru a clasei derivate y = dy; // y este data membru a clasei derivate } void afis() const; ... };

Functia membri afis se apeleaza pentru obiecte ale clasei derivate. Ea afiseaza datele membru ale clasei derivate, precum si datele membru mostenite de la clasa B. Pentru a face distinctie intre datele membru ale clasei derivate si cele ale clasei de baza, se foloseste operatorul de rezolutie. Astfel: B::x este data membru a clasei B. x este data membru a clasei A. Tinand cont de aceste lucruri, functia afis poate fi definita astfel:
void A::afis() const { printf(x_baza= %g\ty_baza= %g\n, B::x, B::y); printf(x_derivat= %g\ty_derivat= &g\n, x, y); }

Supraincarcare functiilor membru ale unei clase de baza intr-o clasa derivata
Functiile membru ale claselor de baza sunt mostenite de clasele derivate. Astfel, daca B este o clasa de baza pentru clasa A si f este o functie membru a clasei B, atunci f este si o functie membru a clasei A. Deci functia f poate fi utilizata atat cu obiecte ale clasei B, cat si cu obiecte ale clasei A. Astfel daca exista declaratiile:
A a; B b:

atunci apelurile b.f(...) si a.f(...) sunt apeluri corecte ale functiei f. In mod analog, functiile friend ale clasei de baza sunt functii friend si pentru clasa derivata. Majoritatea functiilor membru si friend are ca efect faptul ca supraincarcarea operatorilor pentru clasa de baza este valabila si pentru o clasa derivata a clasei de baza respective. Totusi, nu intotdeauna o functie mostenita de la o clasa corespunde necesitatilor prelucrarilor obiectelor clasei derivate. In astfel de situatii, functia respectiva poate fi supraincarcata pentru clasa derivata. La supraincarcarea pentru clasa derivata se poate pastra nu numai numele 121

functiei, ci chiar si numarul si tipul parametrilor. Functiile supraincarcate in acest fel se selecteaza nu numai dupa numarul si tipul parametrilor ci si dupa obiectul pentru care sunt apelate. Cu alte cuvinte, reluand exemplul de mai sus, la apelul: b.f(...) se apeleaza functia membru f a clasei de baza B, iar la apelul: a.f(...) se apeleaza functia membru f a clasei A daca f este dupraincarcata pentru clasa A si functia membru f a clasei de baza B daca f nu este supraincarcata pentru clasa A. O problema care apare in acest caz este aceea cand in corpul functiei membru f, supraincarcata pentru clasa A, se doreste apelul functiei membru f a clasei B:
A::f(...) { ... // aici este necesar apelarea functiei membru f a clasei B ... };

Daca in corpul functiei f utilizam un apel obisnuit:


f(...)

atunci acesta se realizeaza pentru obiectul curent si conduce la un apel recursiv al functiei f. Pentru a apela functia membru f a clasei de baza B pentru obiectul curent care este o instantiere a clasei derivate A, folosim operatorul de rezolutie:
B::f(...)

Deci corpul functiei s se va defini astfel:


A::f(...) { ... B::f(...) // se apeleaza functia membru f a clasei de baza B pentru // obiectul curent care este instantiere a clasei A derivata din B. ... };

Exemplu:
class B { protected: double x; double y; public: B(double xx=0, double yy=0) { x = xx; y = yy; } void afis() const { printf(bx = %g\tby= %g\n, x, y); } ... }; class A { protected: double xx; double yy; public: A(double dx=0, double dy=0, double bx=0, double by=0):B(bx,by) { xx = dx; yy = dy; } void afis() const; ... };

Functia membru afis, a clasei de baza B, este supraincarcata pentru clasa derivata A. Functia afis aplicata la o instantiere a clasei B va afisa valorile lui x si y. Functia afis aplicata la o instantiere a clasei A va afisa atat valorile lui x si y ale obiectului curent cat si valorile lui xx si yy ale aceluiasi obiect. De aceea la definirea functiei afis pentru clasa A vom apela functia membru a clasei B.
void A::afis() const { B::afis(); // Afiseaza x si y

122

printf(dx= %g\tdy= %g\n, xx, yy);

Exemple de apelare a functiei afis: Considerand urmatoarele declaratii:


B b(1, 2); A a(1, 2, 3, 4); // b.x=1, b.y=2 // a.x=3, a.y=4. a.xx=1, a.yy=2

atunci instructiunea:
b.afis(); // se apeleaza functia membru afis a clasei B

va afisa:
bx= 1 by= 2 // se apeleaza functia membru afis a clasei A

iar instructiunea:
a.afis();

va afisa:
bx= 3 dx= 1 by= 4 dy= 2

Poliformismul
In limbajul C++ polimorfismul se manifesta sub trei forme: parametric, ad-hoc si de mostenire. In continuare se va prezenta doar notiunile generale despre polimorfism. Un exemplu clasic de polimorfism parametric este dat de functiile de iesire formatata a informatieie: printf, fprintf si sprintf. Fiecare dintre aceste functii este o entitate polimorfa parametric, in sensul posibilitatii apelului functiei cu un numar oarecare de argumente de tipuri diferite. Polimorfismul parametric este posibil in limbajele C si C++ datorita modului de incarcare a stivei cu parametrii de apel al functiilor: stiva este incarcata si descarcata de catre entitatea apelanta (apel de functie tip C) nu de catre entitatea apelata, adica functia in cauza (apel de functie tip Pascal8). Ordinea de incarcare a parametrilor actuali in stiva este inversa ordinii de mentionare a acestora in cadrul formulei de apel (primul argument pus in stiva va fi primul in lista). O functie care nu specifica nici un argument de apel nici chiar cuvantul cheie void in cadrul semnaturii sale este exclusa de catre compilatoarele C in procesul de verificare a tipurilor si va putea fi apelata cu un numar arbitrar de parametri, de tipuri oarecare. Specificarea cuvantului void in cadrul listei parametrilor formali ai unei functii, va determina validarea doar a apelurilor neparametrizate pentru functia respectiva. Ca atare, semnatura functiei printf in limbajul C poate avea forma int printf(). In limbajul C++, pentru a suporta functiile polimorfe parametric s-a introdus un nou tip de argument, argumentul orice tip (...) ellipsis, care mentionat intotdeauna ca ultim argument al unei functii va valida apelurile de functie cu un numar oarecare de argumente. Semnatura functiei printf in limbajul C++ are un numar oarecare de argumente. Semnatura functiei printf in limbajul C++ are forma int printf(...) sau int printf(const char*, ...). A doua forma a semnaturii este mai sigura deoarece impune tipul primului argument ca fiind const char*. O semnatura de forma int printf() are cu totul alt inteles in limbajul C++ fata de limbajul C, fiind interpretata cu int printf(void). Pentru a oferi declaratii valide de functii cu un numar oarecare de argumente, indiferent de natura compilatorului folosit (C sa C++), se va compila conditionat, folosind constanta simbolica _cplusplus.

Asemanare cu numele limbajului Pascal este absolut intamplatoare.

123

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