Sunteți pe pagina 1din 169

1

Funcţii definite în program


O funcţie furnizează o modalitate convenabilă de a încapsula anumite calcule care pot fi
utilizate (apelate) ulterior fără a ne preocupa, în momentul apelului, de modul lor de
implementare.
Limbajul de programare C face folosirea funcţiilor uşoară, convenabilă şi eficientă.

Definiţia unei funcţii are următoarea formă:

tipul-rezultatului numele-functiei(declaratiile-parametrilor)
{
declaratii
instructiuni
}

Programul 1.1: Definirea şi utilizarea unei funcţii.


#include <stdio.h>
int putere(int m, int n);
/* utilizarea functiei putere */

int main()
{
int i;
for (i=0; i<10; ++i)
printf(“%d %d %d\n”, i, putere(2,i), putere(-3,i));
return 0;
}

/* putere: ridica baza la puterea n; n>=0 */


int putere(int baza, int n)
{
int i,p;
p=1;
for (i=1; i<=n; ++i)
p = p * baza;
return p;
}

Prima linie a funcţiei


int putere (int baza, int n)
declară tipurile şi numele parametrilor precum şi tipul rezultatului returnat de funcţie.
În general, vom folosi denumirea de parametru pentru o variabilă enumerată în lista
închisă între paranteze din definiţia unei funcţii şi cea de argument pentru valoarea folosită
într-un apel de funcţie.
Valoarea calculată în funcţia putere este returnată către main de instrucţiunea
return. După return poate urma orice expresie. Nu este obigatoriu ca o funcţie să returneze

7
o valoare. Ea poate fi apelată şi pentru efectele directe pe care le produce (de exemplu o funcţie
care afişează pe ecran o valoare pe care o primeşte ca argument).
Declaraţia
int putere(int m, int n);
aflată înaintea lui main se numeşte prototip de funcţie şi trebuie să concorde cu definiţia
şi apelurile funcţiei putere. Nu este necesar ca numele parametrilor să corespundă. Un
prototip poate fi scris şi omiţând numele parametrilor:
int putere(int, int);
Numele folosite în definiţia efectivă a funcţiei putere pentru parametrii săi sunt
entităţi locale funcţiei putere şi nu sunt vizibile pentru nici o altă funcţie. Acest lucru este
valabil şi pentru variabilele locale i şi p. Aceste variabile se numesc, în C, variabile
automatice.

În C, toate argumentele funcţiilor sunt transmise “prin valoare”. Acest lucru înseamnă
că funcţia apelată primeşte valorile argumentelor sale prin stocare în variabile temporare create
în stivă special în acest scop. În acest fel, funcţia nu are acces la locaţiile de memorie ale
variabilelor originale. În consecinţă, funcţia apelată nu poate modifica direct variabila
corespunzătoare argumentului transmis din funcţia apelantă; ea nu poate modifica decât copia
temporară.
Apelul prin valoare conduce la programe mai compacte, cu mai puţine variabile
neesenţiale, deoarece parametrii pot fi trataţi în rutina apelată ca variabile locale, convenabil
iniţializate ca urmare a apelului.

Când este necesar, se poate face ca o funcţie să modifice o variabilă din funcţia
apelantă. Funcţia apelantă trebuie să furnizeze adresa variabilei ce va fi accesată (un pointer
către acea variabilă), iar funcţia apelată trebuie să declare parametrul ca fiind pointer şi să
acceseze variabila indirect, prin intermediul acestuia (vezi cap. 2 şi cap.4).

O altă excepţie este în cazul tablourilor: când numele unui tablou este folosit ca
argument, valoarea transmisă funcţiei este adresa locaţiei primului element al tabloului.
Folosind această valoare ca pe o variabilă cu indici, funcţia poate accesa şi poate modifica
direct orice element al tabloului vezi şi cap.2.

1.1 Bazele definirii şi utilizării funcţiilor


Există două mari avantaje ale utilizării funcţiilor definite în program:
1) împărţirea unor sarcini de calcul ample în unele mai mici, ceea ce conduce la
scrierea mai simplă a programului în ansamblu;
2) reutilizarea unui cod deja scris sub formă de funcţii pentru programe anterioare (de
exemplu funcţiile de bibliotecă).

Limbajul de programare C a fost special conceput să permită utilizarea simplă şi


eficientă a funcţiilor; programele C sunt, în general, formate din multe funcţii mici şi nu din
câteva funcţii mari.

Programul 1.2: Să se scrie un program care tipăreşte liniile din fişierul de intrare ce
conţin un anumit “şablon” (şir de caractere prestabilit).

8
Algoritmul problemei se împarte în trei părţi:
while (mai exista o linie)
if (linia contine sablonul)
tipareste-o
Deşi ar fi posibil să plasăm întregul cod în funcţia main, o soluţie mai bună este să
facem din fiecare parte a algoritmului o funcţie separată (funcţiile preialinie,
indicesir şi respectiv printf – funcţie de bibliotecă).
Funcţia indicesir(s,t) returnează poziţia sau indicele din şirul s unde începe
şirul t sau -1 dacă s nu îl conţine pe t. Deoarece în C tablourile încep de la poziţia 0, indicii
vor fi zero sau strict pozitivi, o valoare negativă precum -1 fiind convenabilă pentru semnalarea
eşecului.
#include <stdio.h>
#define MAXLINIE 1000 /* lung. max. a liniei de intrare */
int preialinie(char linie[ ], int max);
int indicesir(char sursa[ ], char decautat[ ]);
char sablon[ ] = “ea”; /* sablonul cautat */
/* gaseste toate liniile in care apare sablonul */

int main()
{
char linie[MAXLINIE];
int gasite = 0;
while (preialinie(linie, MAXLINIE) > 0)
if (indicesir(linie, sablon) >= 0) {
printf(“%s”, linie);
gasite++; }
return gasite;
}

/* preialinie: preia linia, o copiaza in s, returneaza lung.*/


int preialinie(char s[ ], int lim)
{
int c, i;
i = 0;
while (--lim > 0 && (c=getchar()) != EOF && c != ‘\n’)
s[ i++ ] = c;
if (c == ‘\n’)
s[ i++ ] = c;
s[ i ] = ‘\0’;
return i;
}

/* indicesir: returneaza indicele lui t din s sau -1 daca t


nu exista in s*/
int indicesir(char s[ ], char t[ ])
{
int i, j, k;
for (i = 0; s[ i ] != ‘\0’; i++) {
for (j=i, k=0; t[ k ]!=‘\0’ && s[ j ]==t[ k ]; j++, k++) ;
if (k > 0 && t[ k ] == ‘\0’)
return i;
}
return -1;
}

9
Revenind acum la forma generală a definiţiei unei funcţii:

tip-rezultat nume-functie(declaratii de parametri)


{
declaratii si instructiuni
}

anumite părţi pot lipsi; o funcţie minimală este:


nimic() { }
care nu face nimic şi nu returnează nimic. O astfel de formă poate fi utilă ca variantă provizorie
pe parcursul dezvoltării programului.

Dacă tipul returnat este omis se presupune că este int.

Un program este o colecţie de definiţii de variabile şi de funcţii. Comunicarea între


funcţiile unui program se efectuează prin:
- parametri (argumente);
- valori returnate;
- variabile externe.
Funcţiile pot apărea în orice ordine în fişierul sursă, iar programul sursă poate fi
împărţit în mai multe fişiere, atât timp cât nici o funcţie nu este divizată.

Mecanismul pentru returnarea unei valori din funcţia apelată către apelanta sa
(rezultatul funcţiei) este instrucţiunea return .
După return poate urma orice expresie :
return expresie;
Dacă este necesar, expresie poate fi convertită la tipul returnat de funcţie. De
asemenea, pentru claritate, expresie poate fi inclusă, opţional, între paranteze. Dacă nu are
nevoie de ea, funcţia apelantă poate să ignore valoarea returnată.
După instrucţiunea return nu este necesar să urmeze vreo expresie; caz în care nu se
returnează nici o valoare apelantei. În această situaţie, apelul funcţiei se justifică doar prin
efectele colaterale pe care le determină: de exemplu, afişarea unei valori. Este o situaţie
similară cu aceea a unei proceduri Pascal.
Dacă se ajunge la acolada închisă } care indică terminarea funcţiei, controlul revine de
asemenea la apelantă fără să transmită nici o valoare. Nu este ilegal, dar poate fi un semnal de
alarmă pentru o posibilă greşală, dacă o funcţie returnează o valoare într-un caz şi nu
returnează nici una în alt caz.

Procedeul pentru compilarea şi încărcarea unui program C care se află în mai multe
fişiere sursă variază de la un sistem la altul. El va fi prezentat în alt capitol.

Programul 1.3: Să se determine maximul dintre trei numere întregi, utilizând o funcţie
care determină maximul dintre două numere întregi.
#include <stdio.h>
int max (int a,int b)
{
if (a > b)
return a;
else
return b;
}

10
main( )
{
int x, y, z;
printf(“Introduceti 3 numere intregi\n”);
scanf(“%d%d%d”, &x, &y, &z);
printf(“Maximul este %d\n”, max(x, max(y,z));
}

1.2 Funcţii ce returnează altfel de valori decât întregi


Exemplele de funcţii de până acum, fie nu au returnat nici o valoare fie au returnat un
int.
Ce se întâmplă dacă o funcţie trebuie să returneze un rezultat de alt tip?
1. Funcţia trebuie să declare tipul valorii pe care o returnează, din moment ce aceasta
nu este int. Numele tipului precede numele funcţiei.
2. În rutina apelantă sau înainte de codul apelantului se declară explicit funcţia
apelată întrucât rutina apelantă trebuie să ştie că funcţia apelată returnează o
valoare ce nu aparţine tipului int.

Programul 1.4: Să se scrie o nouă variantă a funcţiei putere care calculează baza exponent,
unde baza este o valoare reală iar exponent este un număr natural.
#include <stdio.h>
float putere (float baza, int exponent)
{
float p;
int i;
if (exponent==0) return 1;
for (i=1, p=1.0; i<=exponent; i++)
p*=baza;
return p;
}
main()
{
int e;
float b, p;
printf(“Introduceti baza si exponentul \n”);
scanf(“%f%d”, &b, &e);
p=putere(b,e);
printf(“%f la puterea %d = %f\n”, b, e, p);
printf(“5 la puterea 3 = %f\n”, putere(5,3));
printf(“(2 la 3) la 2 = %f\n”, putere(putere(2,3),2));
}

Programul 1.5: Scrierea şi utilizarea funcţiei atof care converteşte şirul de caractere s în
numărul echivalent în reprezentare virgulă mobilă în dublă precizie.
Biblioteca standard include o funcţie atof; este declarată în fişierul antet <stdlib.h>.
#include <ctype.h>
/* atof: converteste sirul de caractere s la tipul double */
double atof(char s[ ])
{
double val, putere;
int i, semn;

11
for (i=0; isspace(s[i]); i++) /* treci peste spatiile albe */
;
semn = (s[i] == ‘-’) ? -1 : 1;
if (s[i] == ‘+’|| s[i] == ‘-’)
i++;
for (val = 0.0; isdigit(s[i]); i++)
val = 10.0 * val + (s[i] –’0’);
if (s[i] == ‘.’)
i++;
for (putere = 1.0; isdigit(s[i]); i++) {
val = 10.0 * val + (s[i] –’0’);
putere *= 10.0;
}
return semn * val / putere;
}

Rutina apelantă este un calculator rudimentar care citeşte numerele câte unul pe linie,
precedate opţional de un semn, le adună şi tipăreşte suma curentă după fiecare număr introdus.
#include <stdio.h>
#define MAXLINIE 100
/* calculator rudimentar */
main()
{
double suma, atof(char [ ]);
char linie[MAXLINIE];
int preialinie(char linie[ ], int max);
suma = 0.0;
while (preialinie(linie, MAXLINIE) > 0)
printf(“\t%g\n”, suma += atof(linie));
return 0;
}

Declaraţia
double suma, atof(char [ ]);
precizează că suma este o variabilă de tip double şi că atof este o funcţie care primeşte un
argument de tip char[] şi returnează un double.

Funcţia atof trebuie declarată, definită şi utilizată în mod concordant. Dacă funcţia
atof şi apelul său din main au, în acelaşi fişier, tipuri care nu concordă, eroarea va fi
detectată de compilator. În schimb, dacă funcţia atof ar fi compilată separat (ceea ce este
posibil) neconcordanţa nu ar fi detectată. Funcţia atof ar returna un double pe care funcţia
main l-ar trata, de exemplu, ca pe un int şi ar apărea rezultate lipsite de înţeles.
Dacă o funcţie nu are prototip, aceasta este declarată implicit de prima sa apariţie într-o
expresie, cum ar fi:
suma += atof(linie)
Dacă un nume care nu fost declarat în prealabil apare într-o expresie, urmat de o
paranteză deschisă, acesta este declarat prin context ca fiind nume de funcţie, despre funcţie se
presupune că este de tip int, iar despre argumentele acesteia nu se presupune nimic. De
asemenea, dacă o declaraţie de funcţie nu include argumente, ca de exemplu:
double atof();

12
atunci acest lucru este de asemenea interpretat ca însemnând că nu se poate deduce nimic
despre argumentele funcţiei atof şi, în consecinţă, nu se efectuează nici o verificare asupra
parametrilor.

Se recomandă ca, într-o declaraţie de funcţie, dacă funcţia acceptă argumente, să


declarăm parametrii corespunzători sau să folosim tipul void dacă funcţia nu acceptă
argumente.

Programul 1.6: Realizarea funcţiei atoi prin apelul funcţiei atof şi conversia
explicită a rezultatului.
/* atoi: converteste sirul s la un intreg folosind atof */
int atoi(char s[ ])
{
double atof(char s[ ]);
return (int) atof(s);
}

Valoarea returnată din primul apel este convertită la tipul funcţiei înainte de a se face
returnarea din noua funcţie. Operatorul cast precizează că operaţia este intenţionată şi elimină
orice avertisment din partea compilatorului.

1.3 Variabile externe


Un program C este format dintr-o colecţie de obiecte externe care sunt variabile sau
funcţii. Adjectivul “extern” este folosit ca antonim pentru “intern” care descrie argumentele şi
variabilele definite în interiorul funcţiilor.
Variabilele externe sunt definite în afara oricărei funcţii şi sunt astfel disponibile mai
multor funcţii.
Funcţiile în sine sunt întotdeauna externe, deoarece limbajul C nu permite definirea
funcţiilor în interiorul altor funcţii.
Variabilele şi funcţiile externe au proprietatea că toate trimiterile către ele prin acelaşi
nume, chiar şi din funcţii compilate separat, constituie trimiteri către acelaşi obiect (standardul
numeşte această proprietatea “legare externă”). Limbajul de programare C permite totui să se
definească variabile şi funcţii externe care să fie vizibile doar dintr-un singur fişier sursă.
Deoarece variabilele externe sunt global accesibile ele pot constitui o alternativă pentru
transmiterea datelor între funcţii, prin opoziţie cu argumentele funcţiilor şi cu valorile
returnate de acestea. Orice funcţie poate accesa o variabilă externă făcând trimitere la aceasta
prin nume, dacă numele a fost declarat în vreun fel.
Dacă mai multe funcţii urmează să folosească în comun un număr mare de variabile,
variabilele externe sunt mai convenabile pentru a reprezenta aceste variabile, decât listele de
argumente lungi. Acest lucru trebuie folosit cu precauţie pentru că poate avea efect dăunător
asupra structurii programului fiind prea multe conexiuni de date între funcţii.
Variabilele externe sunt utile şi datorită domeniului de vizibilitate şi duratei de
existenţă mai mari. Aceste variabilele au caracter permanent. Ele păstrează valorile de la o
invocare de funcţie la următoarea, spre deosebire de variabilele automatice care sunt interne
unei funcţii (sunt create când se intră în funcţie şi dispar când se iese din aceasta).
Astfel, dacă două funcţii trebuie să folosească în comun anumite date, dar nici una
dintre ele nu o apelează pe cealaltă, este adesea convenabil ca datele comune să fie păstrate în
variabile externe în loc să fie transmise şi preluate prin intermediul argumentelor.

13
1.4 Reguli pentru domeniul de vizibilitate
Domeniul de vizibilitate al unui nume este partea programului în care acesta poate fi
folosit, în conformitate cu declaraţia sau definiţia sa.
Pentru o variabilă automatică declarată la începutul unei funcţii, domeniul de
vizibilitate este funcţia în care este declarat numele. Variabilele locale cu acelaşi nume din
diferite funcţii nu au legătură unele cu altele. Acelaşi lucru este valabil şi pentru parametrii
funcţiei, care sunt de fapt variabile locale.
Domeniul de vizibilitate al unei variabile sau funcţii externe începe din locul în care
aceasta este declarată şi ţine până la sfârşitul fişierului în curs de compilare.
main() { … }
int i = 0;
double tab[MAX];

void func1(double f) { … }

double func2(void) { … }

Variabilele i şi tab pot să fie folosite în funcţiile func1 şi func2 prin numirea lor,
fără să mai fie nevoie de alte declaraţii. Ele nu sunt vizibile în main; la fel, nu sunt vizibile în
main nici funcţiile func1 şi func2.

Dacă trebuie să se facă referire la o variabilă externă înainte ca aceasta să fie definită,
sau dacă este definită într-un fişier sursă diferit de cel în care este folosită, atunci este
obligatorie o declaraţie extern.

Care este diferenţa între declaraţia unei variabile şi definiţia sa?

O declaraţie anunţă proprietăţile unei variabile (în primul rând tipul său).

O definiţie anunţă proprietăţile unei variabile şi, în plus, produce alocarea de spaţiu
de memorie pentru stocarea valorii variabilei.

Exemple:

1. Liniile
int i = 0;
double tab[MAX];

din exemplul anterior, apar în afara oricărei funcţii: sunt definiţii de variabile externe (i şi
tab).

2. În schimb, liniile
extern int i;
extern double tab[ ];
declară pentru restul fişierului sursă că i este un int şi că tab este un tablou de tip double,
dar nu crează variabilele şi nu alocă spaţiu de stocare pentru acestea.

14
Trebuie să existe o singură definiţie a unei variabile externe, doar într-unul din fişierele
care compun programul sursă. Pentru a accesa o astfel de variabilă în alte fişiere, aceste fişiere
trebuie să conţină declaraţii extern corespunzătoare.

Dimensiunile tablourilor trebuie specificate la definire. Ele sunt opţionale în cazul


declaraţiilor extern. De asemenea, iniţializarea unei variabile externe se poate face doar la
definire, adică acolo unde ea nu este declarată extern şi are rezervată locaţia de memorie în
care să se înregistreze această valoare.

1.5 Variabile statice


Declaraţia (clasa de memorare) static aplicată unui obiect extern (variabilă sau
funcţie), limitează domeniul de vizibilitate al acelui obiect la restul fişierului în curs de
compilare. Declaraţia static face ca o variabilă externă să nu poată fi accesată de funcţii din
afara fişierului în curs de compilare.

Declaraţia static externă este utilizată cel mai adesea pentru variabile, dar poate fi
aplicată şi funcţiilor, cu acelaşi efect. De obicei, numele de funcţii sunt globale (sunt vizibile
pentru orice parte a programului). Dacă o funcţie este declarată ca fiind static, numele
acesteia nu este vizibil în afara fişierului în care este declarată.

Declaraţia static poate fi aplicată, de asemenea, şi variabilelor interne. Variabilele


interne de tip static sunt locale unei anumite funcţii, la fel ca şi variabilele automatice, dar
ele există în permanenţă. Nu sunt create şi nici nu sunt distruse de fiecare dată când este
activată funcţia. Variabilele interne de tip static au spaţiu de stocare permanent în cadrul
unei funcţii şi, în consecinţă, pot fi folosite pentru transmiterea de valori de la un apel la altul
al aceleiaşi funcţii.
Clasa de memorie static este precizată prefixând declaraţia normală prin cuvântul
static.
Dacă într-un fişierul ce conţine funcţiile func1 şi func2 declarăm static
variabilele i şi tab:

static int i = 0;
static double tab[MAX];

void func1(double f) { … }
double func2(void) { … }

atunci variabilele i şi tab nu pot fi accesate de nici o altă funcţie din alte fişiere sursă, doar de
func1 şi func2.

1.6 Statutul variabilelor - sinteză


Variabilele interne unei funcţii: sunt vizibile doar în interiorul funcţiei respective. Din
punctul de vedere al duratei de viaţă pot fi:
- automatice - sunt create când se intră în funcţie şi dispar când se revine din aceasta;

15
- statice - există pe toată durata programului; nu sunt create şi nici nu sunt distruse la
fiecare activare funcţiei; în consecinţă, pot fi folosite pentru transmiterea de valori de la un apel
la altul al aceleiaşi funcţii.
În mod implicit, variabilele interne sunt automatice. Pentru a deveni statice, ele trebuie
declarate static.

Variabilele externe tuturor funcţiilor: sunt vizibile cel puţin până la sfârşitul fişierului
sursă respectiv. Variabilele externe au caracter permanent: locaţiile lor de memorie există pe
toată durata execuţiei programului. Rezultă că ele pot fi folosite pentru transmiterea de date
între apelurile de funcţii diferite.
Variabilele externe pot fi declarate:
- static – domeniul de vizibilitate se limitează la fişierul în curs de compilare;
- extern – declară proprietăţile unei variabile definită într-un alt fişier sursă; unica
locaţie de memorie pentru variabilă va fi în fişierul în care ea este definită
(în care nu este extern).
Declaraţia static se poate aplica şi funcţiilor, cu acelaşi efect privind vizibilitatea.

1.7 Variabile registru


O declaraţie register avertizează compilatorul că variabila în discuţie va fi intens
folosită. Variabilele de tip register se păstrează în registrele maşinii (care se găsesc chiar în
interiorul unităţii centrale de prelucrare) şi fac programul mai performant.
Compilatorul poate să acceseze o valoare mult mai repede atunci când ea este localizată
într-un registru. Pentru că numărul de registre este limitat, compilatorul nu poate să asocieze
permanent o variabilă unui registru, dar poate să încerce să ţină variabila cât mai mult în
registru.

Exemple:
register int x;
register char c;

Cuvântul register este ignorat în cazul declaraţiilor în exces.


Declaraţia register nu poate fi aplicată decât entităţilor locale: variabile automatice şi
parametrii unei funcţii.

Exemplu:
f(register unsigned m, register long n)
{
register int i;
….
}

Restricţiile referitoare la numărul şi tipurile posibile pentru variabile registru diferă de


la maşină la maşină.

16
1.8 Structura de bloc
C-ul nu este un limbaj structurat pe blocuri ca şi Pascal-ul, deoarece funcţiile nu pot fi
definite în interiorul altor funcţii. În schimb, variabilele pot fi definite în maniera unei structuri
în blocuri în interiorul unei funcţii.

Declaraţiile de variabile pot fi plasate după acolada deschisă care introduce orice
instrucţiune compusă, nu numai după acolada care marchează începutul unei funcţii. Domeniul
de vizibilitate al variabilei este cuprins între cele două acolade ({ … }).
O variabilă automatică declarată şi iniţializată într-un bloc este iniţializată de fiecare
dată când se intră în bloc.
Exemplu:
if (n>0) {
int i=0; /* declara şi initializeaza pe i la
fiecare parcurgere*/
...
}

O variabilă declarată static este iniţializată numai prima dată când se intră în bloc.

1.9 Iniţializarea variabilelor


În absenţa iniţializării explicite este garantată iniţializarea cu zero doar a variabilelor
externe şi statice. Variabilele automatice şi registru au valori iniţiale nedefinite.

Pentru variabilele externe şi statice, valoarea de iniţializare explicită trebuie să fie o


expresie constantă; iniţializarea se efectuează o singură dată, în principiu înainte ca programul
să înceapă execuţia.
În cazul variabilelor automatice şi registru, iniţializarea se efectuează de fiecare dată
când se intră în funcţie sau în bloc. Valoarea de iniţializare poate fi şi o expresie care să
implice valori definite anterior, chiar şi apeluri de funcţii.

Exemplu:
int cautbin(int x, int v[ ], int n) {
int prim = 0;
int ultim = n – 1;
int mijl;
...
}

Un tablou se poate iniţializa punând după declaraţia sa o listă de valori de iniţializare:

int zile[]={31,28,31,30,31,30,31,31,30,31,30,31};

Când este omisă dimensiunea, compilatorul va calcula lungimea numărând valorile de


iniţializare (12 în acest caz).
Dacă sunt mai puţine valori decât dimensiunea specificată, elementele lipsă vor fi zero
atât pentru variabilele externe sau statice cât şi pentru cele automatice. Situaţia inversă: prea
multe valori de iniţializare, constituie o eroare.

17
Tablourile de caractere se pot iniţializa cu un şir de caractere, astfel:

char luna[ ]=“iunie”;

în loc de

char luna[ ]={‘i’,‘u’,‘n’,‘i’,‘e’,‘\0’};

Dimensiunea tabloului în acest caz este 6 (5 caractere plus terminatorul ‘\0’).

Programul 1.7: Evaluarea unei funcţii intr-un punct.


/*Calculul valorii unei functii date intr-un punct
f(x) = |x^2, x <= -1
|x+3, -1 < x <= 1
|arctg(x), x > 1 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
float f(float x) {
if (x <= -1) return x*x;
else if (x <= 1) return x+3;
else return atan(x); }

int main(void) {
float v;
printf("Calculul valorii unei functii pentru ");
printf("un argument citit de la tastatura \n");
printf("Valoarea argumentului este:");
scanf("%f",&v);
printf("Valoarea functiei in punctul %.2f este%.2f \n",v,f(v));
system(“pause”);
return 0;
}

Programul 1.8: Algoritmul lui Euclid de aflare a celui mai mare divizor comun a două numere
întregi.
Formularea în cuvinte a algoritmului este următoarea:
Dacă unul dintre numere este zero, c.m.m.d.c. al lor este celălalt număr.
Dacă nici unul dintre numere nu este zero, atunci c.m.m.d.c. nu se modifică dacă se înlocuieşte
unul dintre numere cu restul împărţirii sale cu celălalt.

/*determinarea cmmdc si cmmmc pentru doua numere intregi*/


#include <stdio.h>
int cmmdc(int m, int n) {
int r;
r=m % n;
while (r != 0) {
m=n;
n=r;
r=m % n;
}
return n;
}

18
int main(void) {
int n1,n2,divizor,multiplu;
printf("Calculul cmmdc si cmmmc cu alg. lui Euclid\n");
printf("Numerele sunt:\n");
printf(“n1 = "); scanf("%d",&n1);
printf(“n2 = "); scanf("%d",&n2);
divizor=cmmdc(n1,n2);
printf("CMMDC = %d\n",divizor);
printf("CMMMC = %d\n",(n1*n2)/ divizor);
return 0;
}

1.10 Întrebări şi probleme


1. Care sunt avantajele utilizării funcţiilor definite în program?
2. Care este forma generală a definiţiei unei funcţii ?
3. Care este tipul returnat implicit ?
4. Cum se poate face comunicarea între funcţii într-un program C ?
5. Care este sintaxa (modul de redactare) şi semantica (efectul) instrucţiunii return ?
6. Ce sunt variabilele externe ? Dar variabilele declarate ca extern ?
7. În ce constă proprietatea de legare externă ?
8. La ce pot fi utilizate variabilele externe ?
9. Ce sunt variabilele automatice ?
10. Ce este domeniul de vizibilitate al unui nume ? Dar durata de viaţă a unei variabile ?
11. Care este diferenţa între declaraţia unei variabile şi definiţia sa?
12. Ce efect are declaraţia static pentru o variabilă externă ? Dar pentru una internă ?
13. Ce efect are o declaraţie register ?
14. Ce înseamnă bloc în limbajul de programare C ?
15. Cum se iniţializează variabilele simple în limbajul de programare C ? Dar tablourile ?
16. Să se citească două numere întregi pozitive, n şi m. Să se calculeze, cu ajutorul a două
funcţii suma(n) şi produs(m) valoarea expresiei
n

E= 
i 1
2i
3i 2  4
 m

( j 2
 1)
j 1

17. Să se citească mai multe numere naturale. Să se calculeze, pentru fiecare număr, cu ajutorul
unei funcţii, suma cifrelor şi să se afişeze.
18. Un număr perfect este egal cu suma divizorilor săi, printre care este considerată valoarea 1,
dar nu şi numărul. Să se găsească, cu ajutorul unei funcţii, toate numerele perfecte mai mici
sau egale cu un număr dat n şi să se afişeze fiecare număr astfel determinat, urmat de suma
divizorilor săi. de exemplu, numărul 6 are divizorii 1, 2 şi 3 şi este număr perfect deoarece
6=1+2+3.

19
19. Să se scrie o funcţie care verfică dacă cifra c aparţine sau nu reprezentării zecimale a
numărului. Să se apeleze această funcţie pentru a determina cifrele comune ale numerelor n
şi np. Valorile n şi p sunt date de intrare (numere întregi).
20. Să se citească un număr natural n. Să se afişeze toate palindroamele mai mici sau egale cu
n. Un număr natural se numeşte palindrom dacă are aceeaşi valoare indiferent dacă cifrele
sale sunt citite de la stânga la dreapta sau de la dreapta la stânga. Să se scrie o funcţie care
testează dacă parametrul ei este palindrom.
21. Fie x un număr natural, x>2. Să se scrie programul care determină cel mai mare număr
prim mai mic decât x şi cel mai mic număr prim mai mare decât x.
22. Fie n un număr natural. Să se determine, prin intermediul unei funcţii, toate numerele
naturale mai mici decât n cu proprietatea că sunt divizibile atât prin suma, cât şi prin
produsul cifelor sale.
23. Fie n un număr natural şi x un număr real, citite de la tastatură. Să se calculeze, cu ajutorul
unei funcţii, valoarea expresiei:

a. E (n, x)  1  1x!  x2
2!
 ...  xn
n!

x 2 n 1
b. E ( n, x )  1x!  x3
3!  x5
5!  ... (1) n ( 2 n 1)!

E (n, x)  1  x2
 x4
 ...  (1) n (x2 n )!
2n
c. 2! 4!

24. Să se citească n numere întregi, pe rând într-o aceeaşi variabilă. Să se calculeze suma
numerelor prime. Să se utilizeze în program o funcţie care determină dacă un număr natural
este prim.
25. Fiind dat un număr natural n, să se determine toate numerele naturale mai mici decât n, cu
proprietatea că suma cuburilor cifrelor lor este egală cu numărul însuşi. Să se scrie o
funcţie care calculează suma cuburilor cifrelor unui număr întreg.
26. Să se citească trei numere reale pozitive. Să se determine, cu ajutorul unei funcţii, dacă pot
reprezenta laturile unui triunghi. Dacă da, să se calculeze perimetrul şi aria triunghiului.
27. Se dă un şir de numere naturale, citite pe rând într-o aceeaşi variabilă. Să se determine, cu
ajutorul unei funcţii, câte elemente sunt pătrate perfecte.
28. Să se scrie programul care, citind o listă de numere întregi, afişează numărul prim cel mai
apropiat de fiecare element al listei. Dacă două numere prime sunt la distanţă egală de un
element, se vor afişa ambele. Să se scrie o funcţie care determină dacă un număr este prim.

20
2
Tablouri şi pointeri.
Alocarea dinamică a memoriei

2.1 Tablourile, definite şi utilizate ca variabile cu indici


Tabloul este un ansamblu format dintr-un număr fix de componente, toate de acelaşi
tip. Este motivul pentru care el se consideră ca fiind o structură omogenă. În general, tablourile
reprezintă în programare variabilele cu indici din matematică.
Exemple: 1) şirul (vectorul) xi , i= 1, n
2) matricea bidimensională aij , i= 1, n , j= 1, m
Structura de tablou a apărut foarte timpuriu în programare (1955-limbajul Fortan) şi
este frecvent utilizată, din următoarele cauze:
- variabilele cu indici sunt modele matematice potrivite pentru multe structuri de date
din problemele economice, tehnice sau de altă natură;
- prin utilizarea unei variabile indexate, în locul unui număr corespunzător de variabile
simple, se generalizează algoritmul de rezolvare a problemei şi se simplifică substanţial
programul (vezi şi prog. 2.1).

Componentele unui tablou se numesc elemente. La definirea tabloului în limbajul de


programare C, se precizează tipul elementelor, numit tip de bază şi valoarea maximă a fiecărui
indice (dimensiunile tabloului), ca în exemplul generic de mai jos:

<tip element> <nume tablou> [ <max indice 1>] [ <max indice 2>] ......

La definirea tabloului ca variabilă, valorile maxime ale indicilor trebuie să fie constante sau
expresii având ca operanzi exclusiv constante. Aceasta înseamnă că tablourile definite astfel
sunt structuri statice: dimensiunea lor se calculează la compilare şi nu se poate schimba la
execuţia programului.

Selectarea unui element din ansamblul reprezentând tabloul se face alăturând numelui
tabloului, valorile corespunzătoare ale indicilor, conform următoarei sintaxe:

<nume tablou> [ <indice 1>] [ <indice 2>] ......

Valorile indicilor sunt expresii pozitive, strict mai mici decât valoarea maximă asociată acelui
indice la declararea tabloului. Se observă şi faptul că fiecare indice se include între perechi de
paranteze drepte distincte.

21
Exemple:
int v[10];

defineşte un vector de 10 întregi. Indicii încep de la zero ceea ce înseamnă că primul element al
vectorului este v[0] şi ultimul este v[9].
int mat[10][10];
reprezintă o matrice cu 10 linii şi 10 coloane; se observă că un tablou multidimensional este un
tablou de tablouri. Corespunzător, elementul de indici (i,j) va fi selectat astfel: mat[i][j] şi
nu mat[i,j] ca în multe alte limbaje (de exemplu, Pascal).

Tablourile pot fi iniţializate la declarare punând după declaraţie o listă de valori de


iniţializare.

Exemple:
int x[ ]={1,2,3};
int zile[ ]={31,28,31,30,31,30,31,31,30,31,30,31};

Când este omisă dimensiunea, compilatorul va calcula lungimea numărând valorile de


iniţializare (3 în primul caz şi 12 în al doilea). Dacă sunt mai puţine valori decât dimensiunea
specificată, elementele lipsă vor fi zero atât pentru variabilele externe sau statice cât şi pentru
cele automatice. Situaţia inversă: prea multe valori de iniţializare, constituie o eroare.

Tablourile de caractere se pot iniţializa cu un şir de caractere, astfel:

char luna[ ] = “iunie”;

în loc de

char luna[ ] = { ‘i’, ‘u’, ‘n’, ‘i’, ‘e’, ‘\0’};

Dimensiunea tabloului este în acest caz 6 (5 caractere propriu-zise plus terminatorul ‘\0’).

Sunt şi alte situaţii în limbajul de programare C în care nu este necesar să se precizeze


dimensiunile talourilor:

1) Parametrii formali tablouri pot fi declaraţi incomplet, fără precizarea primei


dimensiuni:
int f(char l[ ],int m[ ][10]);
Dimensiunile efective ale tablourilor se vor preciza în momentul apelului funcţiei, fiind date de
dimensiunile argumentelor. În acest fel se pot descrie funcţii cu un grad de generalitate ceva
mai mare decât în Pascal, unde este obligatoriu ca dimensiunile parametrului formal şi cele ale
argumentului să fie identice. În cazul unei matrici, numărul de coloane este necesar pentru ca,
în funcţia f, să se poată calcula adresa elementului m[i][j] în zona de memorie alocată
tabloului m.

2) Variabilele declarate ca extern:


extern double val[ ];
declară pentru restul fişierului sursă tabloul val cu elemente de tip double, dar nu crează
variabila şi nu alocă spaţiu de stocare în memorie pentru aceasta. Neefectuând rezervarea de
memorie, nu este necesară precizarea dimensiunii tabloului.

22
Programul 2.1: Să se parcurgă un text şi să se contorizeze numărul de apariţii ale
fiecărei cifre (într-un tablou), ale caracterelor tip spaţiu alb (spaţiu, tabulator, rând nou) şi ale
tuturor celorlalte caractere.
#include <stdio.h>
/* numara cifre,spatii albe,altele */
main()
{
int c,i,nalbe,naltele;
int ncifre[10];
nalbe = naltele = 0;
for (i=0; i<10; ++i)
ncifre[i]=0;
while((c=getchar())!= EOF)
if (c>=‘0’&&c<=‘9’)
++ncifre[c-’0’];
else if(c ==‘ ‘||c ==‘\n’ || c == ‘\t’)
++nalbe;
else
++naltele;
printf(“cifre=”);
for (i=0; i<10; ++i)
printf(“%d”, ncifre[i]);
printf(“, spatii albe = %d, altele = %d\n”, nalbe, naltele);
}
 Observaţii:
 Declaraţia int ncifre[10]; declară variabila ncifre ca fiind un tablou de 10
întregi. În C, indicii tablourilor încep întotdeauna de la 0, deci elementele sunt
ncifre[0],ncifre[1],...,ncifre[9].
 Un indice poate fi orice expresie cu valoare întreagă.

2.1.1. Tablouri unidimensionale


Tablourile cu un singur indice, numite şi tablouri liniare, sunt modele potrivite pentru
reprezenterea în program a vectorilor, şirurilor şi, implicit, a matricilor linie sau coloană.
 Schema de prelucrare a unui tablou unidimensional:

#define N 10
TIP t[N]; /* tablou de N elemente */
int i; /* variabila contor */
for(i=0; i<n; i++)
*prelucrare t[i]

Programul 2.2: Să se citească N numere întregi şi să se memorează în şirul t1. Să se


construiască apoi şirul t2,conţinând elementele lui t1 în ordine inversă.

#include <stdio.h>
#define N 20
void main (void)
{
int t1[N], t2[N];
int i;

23
/* citeste elementele sirului t1 */
printf(“Introduceti cele %d elemente ale sirului \n”, N);
for(i=0; i<N; i++)
scanf(“%d”, &t1[i]);
/* construieste t2 */
for (i=0; i<N; i++)
t2[i]=t1[N–1-i];
/* afiseaza elementele sirului t2 */
printf(“Sirul in ordine inversa este: \n”);
for(i=0; i<N; i++)
printf(“%d ”, t2[i]);
printf(“\n”);
}

Programul 2.3: Operaţii cu vectori.


// Fie a un vector cu n (n<=50) componente de tip int.
// a. Verificati daca exista in vectorul a un palindrom
// b. Verificati daca toate elementele vectorului sunt nr. prime
#include<stdio.h>
#include <stdlib.h>
#include <math.h>

int main() {
int a[50], x, rx, n, i, exista, ok, d, rad;
printf("n="); scanf("%d", &n);
printf("Introduceti elementele vectorului\n");
for(i=0; i<n; i++)
scanf("%d", &a[i]);
for (exista=i=0; !exista && i<n; i++) {
// verific daca a[ i ] este palindrom
x=a[i];
rx=0; // “inversul” lui x – in oglinda
while (x) {
rx=rx*10 + x%10;
x /= 10; }
if (rx == a[ i ]) exista=1;
}
if (exista)
printf("exista un palindrom in vector \n");
else
printf("nu exista un palindrom in vector \n");
for (ok=1, i=0; ok && i<n; i++) {
// verific daca a[ i ] este prim
for (d=2, rad=sqrt(a[ i ]); a[ i ]%d && d<=rad; d++);
if (a[ i ] % d == 0) ok=0; // a[ i ] nu este prim
}
if (ok)
printf("Toate elementele vectorului sunt prime\n");
else
printf("Nu toate elementele vectorului sunt prime\n");
system("pause");
}

24
2.1.2. Tablouri bidimensionale
Reprezintă, în programare, matricile din matematică. Parcurgerea unei astfel de
structuri se obţine prin generalizarea parcurgerii unui tablou liniar: mai multe instrucţiuni for
imbricate (suprapuse), câte una pentru fiecare indice.

Exemple: Diferite variante de parcurgere a unei matrici pătrate.


#define N 10
TIP mat[N][N]; /* matrice pătrată de ordinul N */
int i,j; /* variabile contor */

1) Parcurgerea, linie cu linie, a tuturor elementelor unei matrici:

for(i=0; i<n; i++)


for(j=0; j<n; j++)
*prelucrare mat[i][j]

2) Parcurgerea, coloană cu coloană, a tuturor elementelor unei matrici:

for(j=0; j<n; j++)


for(i=0; i<n; i++)
*prelucrare mat[i][j]

3) Parcurgerea elementelor unei matrici situate dedesuptul diagonalei principale,


inclusiv cele de pe diagonală:

for(i=0; i<n; i++)


for(j=0; j<i; j++)
*prelucrare mat[i][j]

4) Parcurgerea elementelor de pe diagonala principală:

for(i=0; i<n; i++)


*prelucrare mat[i][i]

5) Citirea elementelor unei matrici cu N linii şi M coloane:


int a[N][M]);
printf(“Introduceti elementele matricii: \n”);
for(i=0; i<N; i++)
for(j=0; j<M; j++)
scanf(“%d”, &a[i][j]);

6) Afişarea elementelor unei matrici:

int a[N][M]);
for(i=0; i<N; i++){
for(j=0; j<M; j++)
printf(“%4d”,a[i][j]);
printf(“\n”);
}

25
Programul 2.4: Să se calculeze suma elementelor de sub diagonala principală a unei
matrici pătrate cu n (n≤20) linii şi coloane.

#include <stdio.h>
void main(void)
{
int a[20][20];
int suma, i, j;
printf(“Introduceti dimensiunea matricii (<=20):”);
scanf(“%d”, &n);

/* citirea elementelor matricii */


for(i=0; i<n; i++)
for(j=0; j<n; j++) {
printf(“a[%d][%d]=“, i+1,j+1);
scanf(“%d”, &a[i][j]);
}

/* calculul si afisarea sumei */


for(i=0, suma=0; i<n; i++)
for(j=0; j<i; j++)
suma += a[i][j];
printf(“Suma calculata este: %d\n”, suma);
}

Programul 2.5: Să se scrie un program care calculează suma a două matrici, S1=A+B
precum şi suma S2=S1+A.
#include <stdio.h>
int n, m;

void citire(int a[][10])


{
int i, j;
printf(“Introduceti elementele:”);
for(i=0; i<n; i++)
for(j=0; j<m; j++) {
printf(“a[%d][%d]=“, i+1, j+1);
scanf(“%d”, &a[i][j]); }
}

void suma(int a[][10], int b[][10], int s[][10])


{
int i, j;
for(i=0; i<n; i++)
for(j=0; j<m; j++)
s[i][j]=a[i][j] + b[i][j];
}

26
void tiparire(int a[][10])
{
int i, j;
for(i=0; i<n; i++) {
for(j=0; j<m; j++)
printf(“%8d”, a[i][j]);
}
printf(“\n”);
}

void main(void)
{
int a[10][10], b[10][10], s1[10][10], s2[10][10];
printf(“Introduceti dimensiunile matricilor:”);
scanf(“%d%d”, &n, &m);
/* Citirea matricilor */
printf(“Introduceti elementele matricii a \n”);
citire(a);
printf(“Introduceti elementele matricii b \n”);
citire(b);
/* Tiparirea matricilor */
printf(“Matricea a este :\n”);
tiparire(a);
printf(“Matricea b este :\n”);
tiparire(b);

/* Calculul si afisarea matricilor suma */


suma(a, b, s1);
printf(“Matricea suma dintre a si b este:\n”);
tiparire(s1);
suma(s1, a, s2);
printf(“Matricea suma dintre s1 si a este:\n”);
tiparire(s2);
}

Programul 2.6: Alte operaţii cu matrici.

// Fie a o matrice de intregi cu n linii si m coloane(n, m<=100)


// Numarati cate elemente din matrice au ca vecini numai
// numere pare (doua elemente sunt vecine daca sunt
// adiacente pe verticala sau orizontala

#include<stdio.h>
#include <stdlib.h>
#define NVecini 4
#define DMAX 102

int a[DMAX][DMAX], n, m;
int dl[ NVecini ]={-1, 0, 1, 0};
int dc[ NVecini ]={0, 1, 0, -1};

int main()
{ int i, j, k, nr, total=0;
printf("Introduceti nr de linii si coloane\n");
scanf("%d%d", &n, &m);

27
printf("Introduceti elementele matricii\n");
for (i=1; i<=n; i++)
for (j=1; j<=m; j++) {
printf("a[%d,%d]=", i, j);
scanf("%d", &a[ i ][ j ]); }
for (i=1; i<=n; i++)
for (j=1; j<=m; j++)
{ // parcurg vecinii si-i numar pe cei pari
for (nr=0, k=0; k<NVecini; k++)
if (a[i+dl[k]][j+dc[k]] % 2 == 0) nr++;
// daca toti vecinii sunt pari, numar acest element
if (nr == NVecini) total++;
}
printf("Numarul de elemente cu vecini pari:%d\n", total);
system("pause");
return 0;
}

Programul 2.7: conversia datei din zi a lunii în zi a anului şi invers, prin intermediul a
două funcţii:
zi_a_anului – converteşte ziua şi luna în ziua din an
luna_zi – converteşte ziua din an în lună şi zi.
Argumentele corespunzătoare lunii şi zilei vor fi pointeri.

static char tabzi[2][13]={


{0,31,28,31,30,31,30,31,31,30,31,30,31},
{0,31,29,31,30,31,30,31,31,30,31,30,31}
};

/* zi_a_anului: calculeaza ziua din an avand luna si ziua */


int zi_a_anului (int an, int luna, int zi)
{
int i, bisect;
bisect = an%4 == 0 && an%100 != 0 || an%400 == 0;
for (i=1; i<luna; i++)
zi += tabzi[bisect][i];
return zi;
}

/* luna_zi: calculeaza luna si ziua avand ziua din an */


void luna_zi(int an, int zi_an, int *pluna, int *pzi)
{
int i, bisect;
bisect = an%4 == 0 && an%100 != 0 || an%400 == 0;
for (i = 1; zi_an > tabzi[bisect] [i]; i++)
zi_an -= tabzi[bisect] [i];
*pluna = i;
*pzi = zi_an;
}

Valoarea aritmetică a unei expresii logice (variabila bisect), este fie zero (fals) fie
unu (adevărat), deci poate fi folosită ca indice pentru tabloul tabzi.
S-a folosit tipul char pentru tabloul tabzi, pentru a se ilustra o întrebuinţare
specifică acestui tip, stocarea întregilor mici, chiar dacă nu sunt caractere.

28
În C, un tablou bidimensional este de fapt un tablou unidimensional, ale cărui elemente
reprezintă fiecare câte un vector. Indicii sunt scrişi astfel:

tabzi[i][j] /* [linie][[col] */
şi nu
tabzi[i,j] /* GRESIT */

Elementele se stochează pe linii, deci cel mai din dreapta indice (coloana ), variază cel
mai repede dacă elementele sunt accesate în ordinea în care sunt stocate.

Un tablou se iniţializează cu o listă de valori de iniţializare cuprinsă între acolade,


fiecare linie a unui tablou bidimensional este iniţializată cu sublista corespunzătoare. La
începutul tabloului s-a plasat o coloană de zerouri pentru ca numerele coloanelor să poată varia
între 1 şi 12, în loc de 0 şi 11.

Dacă un tablou bidimensional trebuie transmis unei funcţii, declaraţia de parametru


din funcţie trebuie să conţină, obligatoriu, numărul de coloane; numărul de linii este irelevant,
deoarece ceea ce se transmite este un pointer la un tablou de linii, în care fiecare linie este un
vector de 13 elemente de tip int.
Se transmite un pointer către obiecte care sunt vectori de 13 elemente de tip int.

Dacă tabloul tabzi ar trebui transmis unei funcţii f, declaraţia lui f ar putea fi
f(int tabzi[2][13]) { ... }
dar poate fi şi
f(int tabzi[ ][13]) { ... }
care precizează că parametrul este un pointer la un vector de 13 întregi.

2.1.3. Tablouri de caractere


Este tipul de tablou cel mai des folosit în C.

Programul 2.8: Citeşte un set de linii de text şi o tipăreşte pe cea mai lungă.
Pseudocod:
while (exista o alta linie)
if (este mai lunga decat precedenta)
(salveaz-o)
(salveaza-i lungimea)
tipareste cea mai lunga linie

Programul se împarte în mod natural în mai multe fragmente care vor fi organizate ca
funcţii: preluarea liniei, testarea liniei, salvarea liniei.

#include <stdio.h>
#define MAXLINIE 1000 /* dim. maxima a liniei de intrare */
int preialinie(char linie[ ], int maxlinie);
void copiaza(char in[], char din[]);
/* tipareste cea mai lunga linie de intrare */

29
int main()
{
int lung; /* lungimea liniei curente */
int max; /* lungimea maxima intalnita pana acum */
char linie[MAXLINIE]; /* linia de intrare curenta */
char cea_mai_lunga[MAXLINIE]; /* pentru salvarea liniei
celei mai lungi */
max=0;
while ((lung = preialinie(linie, MAXLINIE))>0)
if (lung > max) {
max=lung;
copiaza(cea_mai_lunga, linie);
}
if (max > 0) /* s-a gasit o linie */
printf(“%s”, cea_mai_lunga);
return 0;
}

/* preialinie: citeste o linie, o stocheaza in “s” si


returneaza lungimea ei */
int preialinie(char s[],int lim)
{
int c, i;
for (i=0; i<lim-1&&(c=getchar())!=EOF&&c!=‘\n’++i)
s[i]=c;
if (c==‘\n’) {
s[i]=c;
++i;
}
s[i]=‘\0’;
return i;
}
/* copiaza: copiaza “din” in “in”; presupunem ca “in” este
suficient de mare */
void copiaza(char in[], char din[])
{
int i;
i=0;
while ((in[i]=din[i])!=‘\0’)
++i;
}

 Observaţii:
 Lungimea tabloului s[] nu este necesară în funcţia preialinie, din moment ce
dimensiunea sa este setată în funcţia main:
char linie[MAXLINIE];
 Tipul int este tipul returnat implicit. Acesta poate fi omis.
 Unele funcţii returnează o valoare utilă; altele, precum copiaza, sunt folosite numai
pentru efectul lor.
 Funcţia preialinie aşează caracterul ‘\0’ (caracterul nul, a cărui valoare este zero)
la sfârşitul tabloului pe care îl crează, pentru a marca sfârşitul şirului de caractere.
 Specificatorul de format %s din printf aşteaptă ca argumentul corespunzător să fie
un şir de caractere terminat cu ‘\0’.

30
2.2 Pointeri şi adrese
Un pointer este o variabilă care conţine adresa unui obiect (altă variabilă sau funcţie).
Orice variabilă are două elemente caracteristice:
valoarea conţinută în variabilă (rvalue) şi
valoarea adresei locaţiei de memorie (lvalue).
Prefixul “r” sau “l” este pus în analogie cu semantica unei variabile situată în
stânga şi respectiv în dreapta operatorului de atribuire.
Exemplu:
a=a+b;
Deşi notaţia care desemnează variabila “a” este aceeaşi în stânga şi în dreapta
operatorului de atribuire, semantica asociată celor două ipostaze este diferită: în dreapta “a”
reprezintă valoarea variabilei iar în stânga este adresa locaţiei de memorie atribuită variabilei.
Pointerii sunt folosiţi mult în C deoarece:
- permit exprimarea unor operaţii la un nivel mai scăzut;
- conduc la un cod mai compact şi mai eficient.
Trebuie totuşi să fie utilizaţi cu grijă pentru a nu afecta claritatea şi simplitatea
programelor.
Adresa locaţiei de memorie în care este stocată o variabilă se poate obţine aplicând
operatorul de adresă (operatorul &) înaintea numelui variabilei (operaţie întâlnită frecvent în
exemplele anterioare, în cazul apelării funcţiei scanf).
Operatorul de adresă poate fi utilizat pentru obţinerea valorii adresei oricărei variabile.
Secvenţa următoare ilustrează acest lucru: se tipăresc valorile adreselor a două variabile, ca
numere întregi fără semn, în reprezentare hexazecimală:
int a;
float x;
...
printf("adresa lui a este %x \n", &a);
printf("adresa lui x este %x \n", &x);
Valorile şi formatul adreselor de memorie depind de arhitectura calculatorului şi de
sistemul de operare sub care rulează. De aceea, din motive de portabilitate a programelor, dacă
se doreşte declararea unei variabile care să conţină o adresă de memorie, nu se vor utiliza
tipurile întregi.
În C s-a definit un specificator de format special: %p, pentru tipărirea valorilor
reprezentând adrese de memorie.

int a;
float x;
printf("adresa lui a este %p \n", &a);
printf("adresa lui x este %p \n", &x);

Se recomandă utilizarea specificatorului de format %p pentru tipărirea adreselor (fiind o


variantă portabilă).

2.2.1 Variabile pointer


Un pointer este o variabilă care are ca valoare o adresă de memorie. Spunem că
variabila pointer indică locaţia de memorie de la adresa pe care o conţine. Cel mai comun caz
este acela în care valoarea unei variabile pointer este adresa unei alte variabile.

31
În C, pointerii se referă la un anumit tip: tipul datelor conţinute în locaţia de memorie
indicată de variabila pointer.
Declararea variabilelor pointer se face în felul următor:
tip *nume_variabila_pointer;
unde:
- variabila nume_variabila_pointer poate conţine adrese de zone de
memorie alocate unor date de tipul tip.
- * semnifică faptul că variabila este pointer la tipul respectiv.
De exemplu,
int *p;
defineşte o variabilă p pointer la întreg. Variabila p poate conţine adrese de locaţii în
care se memorează valori întregi.
=> O variabilă pointer în C este de regulă pointer la un anumit tip.

Declaraţiile variabilelor pointer la un anumit tip pot să apară în aceeaşi listă cu


variabilele obişnuite de acel tip:
int x, *p;
defineşte o variabilă x de tip întreg şi o variabilă p de tip pointer la întreg.

Se pot declara şi pointeri generici, de tip void * ( tipul datelor indicate de ei nu este
cunoscut).
Un pointer de tip void reprezintă doar o adresă de memorie a unui obiect oarecare
şi are următoarele caracteristici:
- dimensiunea zonei de memorie indicate şi interpretarea informaţiei conţinute, nu sunt
definite;
- poate apare în atribuiri, în ambele sensuri, cu pointeri de orice alt tip;
- folosind o conversie explicită de tip cu operatorul cast : (tip *), el poate fi convertit la
orice alt tip de pointer (pe acelaşi calculator, toate adresele au aceeaşi lungime).

2.2.2 Operatorii de adresare şi indirectare


Ca şi în cazul variabilelor de orice tip, şi variabilele pointer declarate şi neiniţializate
conţin valori aleatoare.
Pentru a atribui variabilei p valoarea adresei variabilei x, numită anterior lvalue, se
foloseşte operatorul de adresă într-o expresie de atribuire de forma:
p=&x;
În urma acestei operaţii, se poate spune că pointerul p indică spre variabila x.

Pentru a obţine valoarea obiectului indicat de un pointer, numită anterior rvalue, se


utilizează operatorul de indirectare (dereferenţiere), notat cu *.
Dacă p este o variabilă de tip pointer care are ca valoare adresa locaţiei de memorie a
variabilei întregi x (p indică spre x) atunci expresia *p reprezintă o valoare întreagă (valoarea
variabilei x).

Exemple:
int x,*p;
x=3;
p=&x;
printf("%d", *p);

32
*p=5;
printf("%d", x);
Expresia *p care se afişează cu primul printf reprezintă valoarea obiectului indicat
de p, deci valoarea lui x. Se va afişa valoarea 3.
Atribuirea *p=5; modifică valoarea obiectului indicat de p, adică valoarea lui x.
Ultimul printf din secvenţă afişează 5 ca valoare a lui x.

 Concluzii:
 Întotdeauna când se aplică operatorul * asupra unui pointer ptr declarat ca şi
tip *ptr;
expresia obţinută prin dereferenţiere (*ptr) este de tipul tip.
 În cazul unui pointer generic (de tip void *), dereferenţierea trebuie precedată de
cast (altfel nu ştim ce tip de valori indică pointerul).
 Când se utilizează operatorul de dereferenţiere, trebuie avut grijă ca variabila pointer
asupra căreia se aplică să fi fost iniţializată, adică să conţină adresa unei locaţii de
memorie valide. O secvenţă ca cea de mai jos este incorectă şi poate genera erori
grave la execuţie:
int *p;
*p=3;
 Pentru a indica adresă inexistentă, se utilizează ca valoare a unui pointer, constanta
NULL.
 Variabilele pointer pot fi utilizate în expresii şi direct, fără indirectare, ca în exemplul
de mai jos:
int x, *p1, *p2;
x=3;
p1=&x;
p2=p1; /* atribuire de pointeri */
Atribuirea p2=p1; copiază conţinutul lui p1 în p2, deci p2 va fi făcut să indice
spre acelaşi obiect ca şi p1. În contextul secvenţei, p2 va indica tot spre x.

2.2.3 Expresii implicând pointeri


Să considerăm următoarea secvenţă de instrucţiuni:

int x=1, y=2, z[10];


int *ip; /* ip este un pointer la int */
ip = &x; /* ip indica acum spre x */
y = *ip; /* y este acum 1 */
*ip = 0; /* x este acum 0 */
ip = &z[0]; /* ip indica acum spre z[0] */

Dacă ip indică spre x, atunci *ip poate apărea în orice context în care ar putea
apărea x, deci
*ip = *ip + 10;
îl incrementează pe x cu 10.

33
Operatorii unari * şi & au o precedenţă mai mare decât operatorii aritmetici; în
consecinţă:
y = *ip + 1;
preia obiectul spre care indică ip, îl adună cu 1 şi atribuie rezultatul lui y, iar
*ip += 1;
incrementează obiectul spre care indică ip, ca şi
++ *ip; şi (*ip )++;
Operatorii unari precum * şi ++ se asociază de la dreapta la stânga.

Pointerii fiind variabile, pot fi folosiţi şi fără a fi dereferenţiaţi.


Dacă iq este un alt pointer spre tipul int,
iq = ip
copiază conţinutul lui ip în iq, făcând astfel ca iq să indice spre ceea ce indică ip.
Operatorul de adresă (&) se aplică numai obiectelor din memorie: variabile şi elemente
de tablou. Operatorul & nu poate fi aplicat expresiilor, constantelor sau variabilelor de tip
register.

2.3 Pointerii şi tablourile


Orice operaţie care poate fi realizată cu ajutorul tablourilor (variabilelor cu indici) poate
fi, de asemenea, efectuată cu ajutorul pointerilor.
Declaraţia
int a[10];
defineşte un tablou de dimensiune 10, adică un bloc de 10 elemente consecutive notate a[0],
a[1], ..., a[9] (fig.2.1).

Fig. 2.1 Reprezentarea în memorie a unui tablou

Dacă pa este un pointer spre un întreg, declarat ca


int *pa;
atunci atribuirea
pa = &a[0];
îl setează pe pa să indice spre elementul zero al lui a; mai precis, pa conţine adresa lui a[0]
(fig.2.2).

Fig. 2.2 Accesarea unui tablou print-un pointer

34
Instrucţiunea
x = *pa;
va copia conţinutul lui a[0] în x.

Dacă pa indică spre un anumit element de tablou, atunci, prin definiţie, pa+1 indică spre
următorul element, pa+i indică spre o locaţie aflată la i elemente după pa, iar pa–i indică
spre o locaţie aflată cu i elemente înainte (fig. 2.3).

Fig. 2.3 Accesarea elementelor unui tablou

Remarcile anterioare sunt valabile indiferent de tipul sau mărimea variabilelor din tabloul a.

Înţelesul expresiei adună 1 la un pointer şi, prin extensie, întreaga aritmetică a pointerilor
este că pa+1 indică spre următorul obiect, iar pa+i indică spre al i-lea obiect de după pa.

Prin definiţie, valoarea unei variabile sau expresii de tip tablou este adresa elementului zero al
tabloului.
pa = &a[0];
poate fi scrisă
pa = a;
 O referinţă la a[i] poate fi scrisă şi ca *(a+i); cele două forme sunt echivalente.
 Aplicând operatorul & ambelor părţi ale echivalenţei, rezultă că &a[i] şi a+i sunt, de
asemenea, forme identice.

 Concluzii:
 O expresie cu tablou şi indice este echivalentă cu una scrisă ca pointer şi distanţă de
deplasare.
 Şi invers, dacă pa este un pointer, în expresii acesta poate fi folosit cu un indice;
pa[i] este identic cu *(pa+i).

Există totuşi o diferenţă între un nume de tablou şi un pointer.


 Un pointer este o variabilă:
pa=a şi pa++ sunt expresii legale
 Un nume de tablou nu este o variabilă:
a=pa şi a++ sunt expresii ilegale

Când un nume de tablou este transmis unei funcţii, ceea ce se transmite este o adresă
reprezentând locaţia elementului iniţial. În interiorul funcţiei apelate, acest argument este o
variabilă locală; deci un nume de tablou sub formă de parametru este un pointer, adică o
variabilă care conţine o adresă. În concluzie, în limbajul C, numele unui tablou este echivalent
cu un pointer care conţine adresa primului său element.

35
Fie următoarele declaraţii:
tab un tablou de elemente de tip T şi p un pointer la tipul T:
T tab[N];
T *p;
Cele 3 atribuiri următoare sunt echivalente, si au ca efect faptul că p va indica adresa
primului element al tabloului tab:
p=tab; p=&tab; p=&tab[0];
Atribuirea tab=p; nu este permisă. Numele tabloului indică o adresă primului
element al tabloului este o valoare constantă din momentul în care tabloul a fost alocat static,
deci nu mai poate fi modificată printr-o atribuire ca cea de mai sus.

Programul 2.9: Funcţie pentru determinarea lungimii unui şir de caractere – varianta 1.
/* strlen: returneaza lungimea sirului s */
int strlen(const char *s)
{
int n;
for (n=0; *s!=‘\0’; s++)
n++;
return n;
}

Având funcţia de mai sus, sunt corecte toate apelurile următoare:


strlen(“buna ziua”); /* constanta sir */
strlen(tablou); /* char tablou[100]; */
strlen(ptr); /* char *ptr; */

 Observaţii:
 Ca parametri formali din definiţia unei funcţii, declaraţiile
char s[ ]; şi char *s;
sunt echivalente. Se preferă char *s; deoarece precizează într-un mod mai
explicit faptul că parametrul este un pointer.
 Când numele unui tablou este transmis unei funcţii, funcţia poate considera, după
cum preferă, că i-a fost transmis fie un tablou, fie un pointer. Poate folosi chiar
ambele notaţii dacă acest lucru este considerat potrivit şi clar.
 Se poate transmite funcţiei doar o parte dintr-un tablou: f(&a[2]) sau f(a+2)
În interiorul lui f declaraţia parametrului este, indiferent de apel:
f(int tabl[ ]) {...} sau f(int *tabl) {...}

2.4 Aritmetica adreselor


Toate operaţiile de manipulare a pointerilor iau în considerare în mod automat
dimensiunea obiectului spre care se indică.
Operaţiile permise cu pointeri sunt:
- atribuirea pointerilor de acelaşi tip;
- adunarea unui pointer cu un întreg sau scăderea unui întreg;
- scăderea sau compararea a doi pointeri care indică spre elemente ale aceluiaşi
tablou;
- atribuirea valorii zero (NULL) sau compararea cu aceasta.

36
2.4.1. Incrementarea şi decrementarea pointerilor
Operatorii unari de incrementare şi decrementare se pot aplica asupra variabilelor de
tip pointer, în format prefix şi postfix.
T *p;
p++; p--;
++p; --p;

Operatorul de incrementare aplicat asupra unui operand de tip pointer la tipul T măreşte
adresa conţinută de operand cu numărul de octeţi necesar pentru a păstra o dată de tipul T: se
adună sizeof(T).
T tab[N];
T *p;
int i;
p=&tab[i];
p++;
p va contine adresa elementului tab[i+1];

2.4.2 Adunarea şi scăderea unui întreg la un pointer


Dacă p este un pointer la tipul T şi n o valoare întreagă, expresia p+n măreşte
valoarea lui p cu n*sizeof(T). Analog, expresia p-n micşorează valoarea lui p cu
n*sizeof(T);

Dacă se declară un tablou tab şi se utilizează ca în secvenţa de mai jos:


T tab[N];
T *p;
p=tab;
expresia p+n va indica spre elementul tab[n] iar valoarea elementului tab[n] poate
fi reprezentată şi prin expresia *(p+n).
În concluzie, variabilele cu indici se pot înlocui prin expresii cu pointeri.

2.4.3 Initializarea pointerilor


În general, un pointer poate fi iniţializat ca orice altă variabilă deşi, în mod normal,
singurele valori care au sens sunt zero sau o expresie care implică adresele unor date definite
anterior.
Constanta zero poate fi atribuită unui pointer şi un pointer poate fi comparat cu
constanta zero. Constanta simbolică NULL (definită în <stdio.h>) este adesea folosită în
locul lui zero, ca o modalitate de a indica mai clar că aceasta este o valoare specială pentru un
pointer.

2.4.4 Compararea şi diferenţa a doi pointeri


Operaţiile sunt permise doar dacă cei doi pointeri indică spre elementele aceluiaşi
tablou.

Compararea a doi pointeri


Doi pointeri care indică spre elementele aceluiaşi tablou pot fi comparaţi, folosind
operatorii de relaţie şi de egalitate.

37
Dacă p şi q sunt doi pointeri spre elementele tab[i] respectiv tab[j] ale unui
tablou, comparaţia p<q este adevarată dacă şi numai dacă i<j.

Diferenţa a doi pointeri


Doi pointeri care indică spre elementele aceluiaşi tablou pot fi scăzuţi. Dacă p şi q sunt
doi pointeri spre elementele tab[i] şi respectiv tab[i+n], diferenţa q-p are valoarea
egală cu n.
În concluzie, dacă p şi q indică spre elemente ale aceluiaşi tablou şi p<q, atunci q-
p+1 reprezintă numărul de elemente de la p la q inclusiv.

Programul 2.10: Funcţie pentru determinarea lungimii unui şir de caractere –varianta 2.

/* strlen: returneaza lungimea sirului s */


int strlen(const char *s)
{
char *p = s; /* initializare cu primul element al sirului*/
while (*p != ‘\0’)
p++;
return p-s; /* returneaza nr. de caractere parcurse*/
}

2.4.5 Operaţii interzise


Sunt ilegale următoarele operaţii aritmetice asupra pointerilor:
- adunarea a doi pointeri;
- înmulţirea sau împărţirea pointerilor;
- deplasarea sau aplicarea unor măşti;
- adunarea cu valori de tip float sau double ;
- atribuirea a doi pointeri de tipuri diferite fără a folosi operatorul cast (cu excepţia
tipului void *).

Programul 2.11: Variante de parcurgere a elementelor unui tablou.


void tiparire1(int tab[ ],int N)
{
int i;
for (i=0; i<N; i++)
printf("%d ",tab[i]);
}

void tiparire2(int tab[ ],int N)


{
int * ptr;
for (ptr=tab; ptr<tab+N; ptr++)
printf("%d ", *ptr);
}

void tiparire3(int *tab, int N)


{
int * ptr;
for (ptr=tab; ptr<tab + N; ptr++)
printf("%d ", *ptr);
}

38
void tiparire4(int *tab, int N)
{
int i;
for (i=0; i<N; i++, tab++)
printf("%d ", *tab);
}

Apelurile celor 4 funcţii au acelaşi efect:


void main(void)
{
int a[5]={1,2,3,4,5};
tiparire1(a,5);
tiparire2(a,5);
tiparire3(a,5);
tiparire4(a,5);
}

2.5 Pointeri spre caractere


O constantă şir, scrisă sub forma:
“Eu sunt un sir de caractere”
este un tablou de caractere. În reprezentarea internă, tabloul se termină cu caracterul ‘\0’,
pentru ca în program să se poată găsi sfârşitul şirului. Lungimea spaţiului de stocare este de
aceea cu unu mai mare decât decât numărul de caractere aflate între ghilimele.

Poate cea mai frecventă utilizare a constantelor şir este ca argumente ale funcţiilor:
printf(“Buna ziua\n”);
Accesul la acest şir se face printr-un pointer la tipul caracter; funcţia printf primeşte
un pointer către începutul tabloului de caractere. Pointerul accesează deci primul element al
şirului.

Constantele şir nu trebuie să fie neapărat argumente de funcţii. Dacă pmesaj este
declarat ca:
char *pmesaj;

atunci instrucţiunea
pmesaj = “acum este timpul”;

îi atribuie lui pmesaj un pointer către tabloul de caractere (fig. 2.4).

Fig. 2.4 Accesarea unui şir de caractere

39
Există o deosebire importantă între următoarele definiţii (fig. 2.5):

char tmesaj[ ]= “acum este timpul”; /* un tablou */


char *pmesaj=“acum este timpul”; /* un pointer */

Fig. 2.5 Deosebirea între un tablou de caracter şi un pointer spre caractere

- Caracterele din cadrul tabloului pot fi modificate individual, dar tmesaj va indica
întotdeauna către acelaşi spaţiu de stocare.
- pmesaj este un pointer iniţializat să indice către o constantă şir; poate fi modificat
ulterior astfel încât să indice altă locaţie, dar rezultatul este nedefinit dacă se modifică
conţinutul şirului.

Programul 2.12: Funcţia strcpy, care copiază un şir în alt şir – varianta 1
/* strcpy: copiaza t in s; versiunea cu indici de tablou */
void strcpy(char *s, const char *t)
{
int i;
i = 0;
while ((s[i] =t[i])!= ‘\0’)
i++;
}

În practică, strcpy nu ar fi scrisă ca în exemplele anterioare. Programatorii C


experimentaţi ar prefera:

Programul 2.13: Funcţia strcpy, care copiază un şir în alt şir – varianta 2
/* strcpy: copiaza t in s; versiunea cu pointeri 2*/
void strcpy(char *s, const char *t)
{
while ((*s++ = *t++) != ‘\0’)
;
}
Această versiune mută incrementarea lui s şi t în partea de testare a ciclului. În final se
copiază în s inclusiv terminatorul ‘\0’.

Putem realiza încă o compactare, observând că nu este necesară comparaţia cu ‘\0’


pentru a controla ciclul (întrebarea este doar dacă expresia este zero). Efectul final este
copierea caracterelor din t în s, până la şi inclusiv terminatorul ‘\0’.

Programul 2.14: Funcţia strcpy, care copiază un şir în alt şir – varianta 3
/* strcpy: copiaza t in s; versiunea cu pointeri 3*/
void strcpy(char *s, const char *t)
{
while (*s++ = *t++)
;
}

40
Convenţia de notaţie din această ultimă versiune este demnă de luat în seamă, iar stilul
din acestă variantă trebuie însuşit (este tipic pentru limbajul C şi se întâlneşte frecvent).

Funcţia strcpy se poate implementa şi în varianta în care returnează, la apel, şirul


iniţial:

Programul 2.15: Funcţia strcpy, care copiază un şir în alt şir – varianta 4.
char *strcpy(char *destinatie, const char *sursa)
{
char *initial = destinatie;
while (*destinatie++ = *sursa++)
;
return (initial);
}
 Observaţii:
 Între operatorii *, ++ şi -- pot să apară următoarele combinaţii. De exemplu:
*--p
îl decrementează pe p înainte de a prelua caracterul spre care indică p.
 Perechea de expresii:
*p++ = val;/* introdu val in stiva */
val=*--p;/* scoate varful stivei si copiaza-l in val */
constituie instrucţiunile standard pentru introducerea şi scoaterea unui element
din stivă.

Programul 2.16: Aplicaţie cu şiruri de caractere.

// Se citeste de la tastatura un text format din max.1000 car.


// Textul,format din mai multe linii, se termina cu o linie vida
// Sa se afiseze cuvintele in ordinea crescatoare a lungimiilor
#include<stdio.h>
#include <stdlib.h>
#include<string.h>
char sir[1000]="", *p, separ[ ]=" ,.;:'?!\n\t", cuv[100][40];

void citxt() {
char linie[100];
printf("Introduceti textul\n");
do {
gets(linie);
strcat(sir, linie); strcat(sir, "\n");
} while (strcmp(linie,""));
}
int main() {
char aux[40];
int n=0, i, j;
citxt();
p=strtok( sir, separ);
if(p)
strcpy(cuv [n++] ,p);
while (p) {
p=strtok( NULL , separ);
if (p)

41
strcpy(cuv [n++], p);
}
printf("Numarul de cuvinte este %d\n", n);
for (i=0; i<n; i++)
printf("%s\n", cuv[ i ]);
printf("Afisare ordonata dupa lungimea cuvintelor\n");
for (i=0; i<n-1; i++)
for (j=n-1; j>i; j--)
if (strlen(cuv[ i ]) > strlen(cuv[ j ]) ) {
strcpy (aux, cuv [ i ]);
strcpy (cuv [ i ], cuv [ j ]);
strcpy (cuv [ j ], aux);
}
for( j=0; j<n; j++)
printf("%s %d\n", cuv [ j ], strlen(cuv [ j ]));
system(“pause”);
}

2.6 Câteva funcţii de bibliotecă pentru prelucrarea şirurilor


de caractere
 Fişierul antet <string.h>
char *strcpy(s,ct)-copiază şirul ct în şirul s, inclusiv caracterul ‘\0’; returnează s.

char *strncpy(s,ct,n)-copiază cel mult n caractere din şirul ct în şirul s, inclusiv


caracterul ‘\0’; returnează s. Completează cu caractere nule (‘\0’) dacă ct are mai puţin de
n caractere.

char *strcat(s,ct)- concatenează şirul ct la sfârşitul s; returnează s.

char *strncat(s,ct,n)- concatenează cel mult n caractere din ct la sfârşitul s;


termină şirul s cu \0 ; returnează s.

int strcmp(cs,ct)- compară şirul cs cu şirul ct; returnează o aloare <0 dacă cs<ct,
0 dacă cs==ct sau >0 dacă cs>ct.

int strcncmp(cs,ct,n)- compară cel mult n caractere din şirul cs cu şirul ct;
returnează o valoare <0 dacă cs<ct, 0 dacă cs==ct sau >0 dacă cs>ct.

char *strchr(cs,c)- returnează un pointer spre prima apariţie a lui c în cs, sau NULL
dacă acesta nu apare.

char *strrchr(cs,c)- returnează un pointer spre ultima apariţie a lui c în cs, sau
NULL dacă acesta nu apare.

size_t strlen(cs) returnează lungimea lui cs.

42
2.7 Implementarea unor funcţii de bibliotecă pentru
prelucrarea şirurilor de caractere
Programul 2.17: Adăugarea conţinutului unui şir la alt şir (concatenarea şirurilor).

char *strcat(char *destinatie, const char *sursa)


{
char *initial = destinatie;
while (*destinatie)
destinatie++; /* cauta sfarsitul sirului */
while (*destinatie++ = *sursa++)
;
return (initial);
}

Programul 2.18: Adăugarea a n caractere dintr-un şir la un alt şir.

char *strncat(char *destinatie, const char *sursa, int n)


{
char *initial = destinatie;
int i = 0;
while (*destinatie)
destinatie++;
while ((i++ < n) && (*destinatie++ = *sursa++))
;
if (i > n) *destinatie = NULL;
return (initial);
}

Programul 2.19: Numărarea apariţiilor unui caracter într-un şir.

int charcnt(const char *sir, int litera)


{
int nr = 0;
while (*sir)
if (*sir == litera)
nr++;
return (nr);
}

Programul 2.20: Inversarea conţinutului unui şir.

char *strrev (char *sir) {


char *initial = sir;
char *urmator = sir;
char temp;
while (*sir) sir++;
while (urmator < sir) {
temp = *(--sir);
*sir = *urmator;
*urmator++ = temp; }
return (initial);
}

43
Programul 2.21: Funcţia strcmp, care compară lexicografic două şiruri de caractere (s şi
t) şi returnează o valoare:
- negativă, dacă s < t
- zero, dacă s == t
- pozitivă, dacă s > t.
Valoarea returnată se obţine prin scăderea caracterelor de la prima poziţie în care t şi s diferă.
/* strcmp: compara sirurile de caractere s si t; versiunea cu
indici de tablouri */
int strcmp (char *s, char *t)
{
int;
for (i=0; s[i] == t[i]; i++)
if (s[i] == ‘\0’)
return 0;
return s[i] – t[i];
}
/* strcmp: compara sirurile de caractere s si t; versiunea cu
pointeri */
int strcmp (char *s, char *t)
{
for ( ; *s == *t; s++, t++)
if (*s == ‘\0’)
return 0;
return *s – *t;
}

Programul 2.22: Compararea a două şiruri de caractere, altă variantă.


int strcmp(const char *s1, const char *s2)
{
while ((*s1 == *s2) && (*s1))
{
s1++;
s2++;
}
if ((*s1 == *s2) && (! *s1)) /* aceleasi siruri */
return (0);
else if ((*s1) && (! *s2)) /* aceleasi, dar s1 > s2 */
return (1);
else if ((*s2) && (! *s1)) /* aceleasi, dar s2 > s1 */
return (-1);
else
return ((*s1 > *s2) ? 1 : -1); /* diferite */
}

Programul 2.23: Căutarea unui subşir într-un şir de caractere.


char *strstr(const char *s1, const char *s2)
{
int i, j, k;
for (i=0; s1[i]; i++)
for (j=i, k=0; s1[j] == s2[k]; j++, k++)
if (!s2[k+1])
return (s1 + i);
return (NULL);
}

44
Programul 2.24: Numărul de apariţii ale unui subşir.

int strstr_nr(const char *sir, const char *subsir)


{
int i, j, k, nr = 0;
for (i=0; sir[i]; i++)
for (j=i, k=0; sir [j] == subsir [k]; j++, k++)
if (!subsir[k+1])
nr++;
return(nr);
}

Programul 2.25: Ştergerea unui subşir dintr-un şir de caractere.

char *strstr_rem(const char *sir, const char *subsir)


{
int i, j, k, loc = -1;
for (i=0; sir[i] && (loc == -1); i++)
for (j=i, k=0; sir[j] == subsir[k]; j++, k++)
if (!subsir[k + 1])
loc=i;
if (loc != -1) /* subsirul a fost gasit */
{
for (k=0; subsir[k]; k++)
;
for (j=loc, i=loc+k; sir[i]; j++, i++)
sir[j] = sir[i];
sir[i] = NULL;
}
return (sir);
}

Programul 2.26: Înlocuirea unui subşir cu altul.


#include <string.h>
char *strstr_rep(char *sursa, char *vechi, char *nou)
{
char *initial = sursa;
char temp[256];
int lung_veche = strlen(vechi);
int i, j, k, loc = -1;
for (i=0; sursa[i] && (loc==-1); ++i)
for (j=i, k=0; sursa[j] == vechi[k]; j++, k++)
if (! vechi[k+1])
loc = i;
if (loc != -1) {
for (j=0; j < loc; j++)
temp[j] = sursa[j];
for (i=0; nou[i]; i++, j++)
temp[j] = nou[i];
for (k=loc + lung_veche; sursa[k]; k++, j++)
temp[j] = sursa[k];
temp[j] = NULL;
for(i=0; sursa[i] == temp[i]; i++); }
return (initial);
}

45
2.8 Alocarea dinamică de memorie
Multe aplicaţii pot fi optimizate dacă memoria necesară stocării datelor este alocată
dinamic, în timpul execuţiei programului. Alocarea dinamică de memorie înseamnă alocarea
de zone de memorie şi eliberarea lor în mod explicit, în timpul execuţiei programelor.
Zona de memorie în care se alocă, la cerere, datele dinamice este diferită de zona de
memorie în care se alocă datele statice(stiva de date) şi se numeşte HEAP.
Spaţiul de memorie alocat dinamic, la cerere, este accesibil programatorului printr-un
pointer care conţine adresa sa. Pointerul care indică un obiect din HEAP este de regulă stocat
într-o variabilă alocată static.
Momentele alocării şi cel al eliberării zonei de memorie pot fi aleatorii pe durata
execuţiei programului şi sunt stabilite de programator.
Pentru alocarea şi eliberarea memoriei alocate dinamic există funcţii oferite de
biblioteca C standard. Funcţiile de gestionare a memoriei au prototipurile în fişierele
alloc.h şi stdlib.h. Cele mai utilizate dintre acestea sunt:
malloc – alocarea zonei de memorie
free – eliberarea zonei de memorie ocupată cu malloc

Funcţia de alocare dinamică a memoriei (malloc)


void * malloc(size_t size);

Funcţia alocă în HEAP un bloc de dimensiune size; dacă operaţia reuşeşte


returnează un pointer la blocul alocat, altfel returnează NULL.

Este posibil ca cererea de alocare să nu poată fi satisfăcută, dacă nu mai există


suficientă memorie în zona de HEAP sau nu există un bloc compact de dimensiune cel puţin
egală cu size. Dimensiunea size se specifică în octeţi.

Rezultatul funcţiei malloc este un pointer de tip void. Se va folosi operatorul cast
pentru conversii de tip la atribuirea rezultatului funcţiei către un pointer de un anumit tip.

Exemplu: Alocarea dinamică a unei zone pentru o valoare de tip int :


int *ip;
ip=(int *)malloc(sizeof(int));
*ip=5;

Chiar dacă variabila pointer care indică o astfel de zonă de memorie nu mai există (şi-a
depăşit durata de viaţă) zona alocată rămâne în continuare ocupată, devenind însă inaccesibilă
pentru program. O eroare frecventă de acest gen poate apare la utilizarea variabilelor dinamice
în cadrul funcţiilor:
void fct()
{
int *p;
p=(int *)malloc(10* sizeof(int));
}

La revenirea din apelul funcţiei fct, locaţia de memorie atribuită pointerului p este
desfiinţată iar memoria alocată dinamic nu mai poate fi accesată, deşi ea nu a fost eliberată.

46
Funcţia de eliberare dinamică a memoriei (free)
void free(void *p);
Funcţia eliberează un bloc alocat anterior cu malloc; adresa de început a blocului este
transmisă ca argument, la apelul funcţiei.
Transferul unei valori invalide (un pointer la un bloc de memorie care nu a fost
rezultatul unui apel al funcţiei malloc), poate compromite funcţionarea sistemului de alocare.
Un tablou poate fi alocat dinamic printr-o secvenţă ca cea de mai jos:
TIP *p;
p=(TIP *)malloc(N*sizeof(TIP));
Pointerul p va indica un bloc suficient de mare pentru a conţine N elemente de tipul
TIP. În continuare, variabila p poate fi utilizată ca şi cum ar fi fost declarată ca un tablou de
forma: TIP p[N];
Avantajul alocării dinamice a unui tablou este acela că dimensiunea sa poate fi
specificată doar în timpul execuţiei.

Programul 2.27: Alocarea dinamică de memorie pentru un tablou de n numere întregi:


#include <stdio.h>
#include <stdlib.h>
main(void)
{
int n;
int *tab;
int i;

printf("Introduceti numarul de elemente: \n");


scanf("%d", &n);
if ((tab=(int *)malloc(n*sizeof(int)))==NULL) {
printf("Eroare alocare dinamica memorie !\n");
exit(1);
}
for (i=0; i<n; i++)
scanf("%d", &tab[i]);
for (i=0; i<n; i++)
printf("%d ", tab[i]);
free(tab);
}

Programul 2.28: Să se citească de la tastatură un şir de caractere sir1. Să se creeze o


copie dinamică a acestuia, notată sir2.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
main(void)
{
char sir1[40], *sir2;
printf("introduceti un sir de caractere: \n");
scanf("%40s", sir1);
if ((sir2=(char *)malloc(strlen(sir1)+1))==NULL) {
printf("eroare aloc. dinamica memorie !\n");exit(1);
}
strcpy(sir2, sir1);
printf("copia sirului este: %s \n", sir2); ..........
}

47
Programul 2.29: Definirea si utilizarea unui vector alocat dinamic ( varianta 2 ).

main( )
{
int n,i; int * a;
printf ("n="); scanf ("%d", &n);
a=(int *) calloc (n, sizeof(int));
// sau: a=(int*) malloc (n*sizeof(int));
printf ("componentele vectorului: \n");
for (i=0; i<n; i++)
scanf ("%d", &a[i]); // sau scanf ("%d", a+i);
for (i=0;i; i<n; i++)
printf ("%d ",a[i]); // sau printf ("%d ", *(a+i));
}

Programul 2.30: Alocarea dinamică a unei matrici.

main () {
int **a; int i, j, nl, nc;
printf ("nr. linii="); scanf ("%d",&nl);
printf ("nr. col. ="); scanf ("%d",&nc);
// memorie pentru vectorul de pointeri la linii
a = (int**) malloc(nl*sizeof(int*));
for (i=0; i < nl; i++)
// aloca memorie pentru fiecare linie i
a[i] = (int*) calloc (nc, sizeof(int)); // o linie
..........................
}

Pentru o matrice alocată dinamic, cu elemente întregi, notaţia a[i][j] este


interpretată astfel:
- a[i] conţine un pointer (o adresa b)
- b[j] sau b+j conţine întregul din poziţia j a vectorului cu adresa b.

Programul 2.31: Se citeşte un număr necunoscut de valori întregi într-un vector


extensibil.

#define INCR 100 //cu cat creste vectorul la fiecare realocare


main() {
int n,i,m ;
float x, * v; // v = adresa vector
n=INCR; i=0;
v = (float *)malloc(n*sizeof(float)); //alocare initiala
while (scanf("%f",&x) != EOF) {
if(++i==n)
{ // daca este necesar
n=n+ INCR; //creste dimensiunea vectorului
v=(float *) realloc (v, n*sizeof(float));
}
v[i]=x; // memorarea lui x in vector
}
...........
}

48
Variabilele alocate în HEAP sunt dinamice. Ele se numesc astfel pentru că apar şi pot
să dispară pe parcursul execuţiei programului, în mod dinamic. Alocarea spaţiului necesar
pentru o asemenea variabilă ca şi relocarea (eliberarea) lui, se efectuează în mod explicit în
program, prin apelurile malloc şi free.
Timpul scurs din momentul creării locaţiei unei variabile şi până la desfiinţarea acestei
locaţii se numeşte durata de viaţă a variabilei. Pentru variabilele obişnuite (automatice), durata
de viaţă coincide cu durata de activare a blocului de care aparţin. Pentru variabilele dinamice
apariţia şi dispariţia lor are loc independent de structura textului programului, iar durata de
viaţă este intervalul de timp scurs între apelurile funcţiilor malloc şi free pentru acele
variabile.

2.9 Tablouri de pointeri; pointeri la pointeri


Deoarece pointerii sunt la rândul lor variabile, ei pot fi stocaţi în tablouri, ca şi alte
variabile.

Programul 2.31: sortarea unor linii de text de lungimi diferite (fig. 2.6).
- Dacă liniile de sortat sunt stocate una în continuarea alteia într-un lung şir de
caractere, atunci fiecare linie poate fi accesată printr-un pointer către primul său
caracter.
- Pointerii pot fi stocaţi într-un tablou.
- Două linii pot fi comparate transmiţând pointerii lor funcţiei strcmp. Când
două linii trebuie interschimbate, se interschimbă pointerii acestora din tabloul
de pointeri, nu liniile în sine.

Fig. 2.6 Sortarea unor linii de text accesate prin pointeri

Acest lucru elimină dubla problemă a gestionării complicate a spaţiului de stocare şi a


resurselor mari necesare mutării liniilor în sine.
Procesul de sortare se desfăşoară în trei paşi:
 citeşte toate liniile de la intrare
 sortează-le
 tipăreşte-le în ordine
Împărţim programul în funcţii care să corespundă acestei diviziuni naturale, funcţia
principală controlând celelalte funcţii.

Rutina de citire trebuie:


 să culeagă şi să salveze caracterele de pe fiecare linie şi să creeze un tablou de
pointeri către linii;
 să numere liniile citite, deoarece această informaţie este necesară pentru sortare şi
tipărire;

49
 să returneze, de exemplu -1 (număr de linii greşit), dacă există prea multe date de
intrare.
Rutina de scriere trebuie doar să tipărească liniile în ordinea în care apar acestea în
tabloul de pointeri.

#include <stdio.h>
#include <string.h>
#define MAXLINII 5000 /* nr maxim de linii ce vor fi sortate */
char *ptrlinie[MAXLINII]; /*pointeri catre liniile de text */
int citestelinii(char *ptrlinie[ ], int nlinii);
void tiparestelinii(char *ptrlinie[ ], int nlinii);
void qsort(char *ptrlinie[ ], int stanga, int dreapta);
int main()
{
int nlinii; /* numarul de linii de intrare citite */
if ((nlinii = citestelinii(ptrlinie, MAXLINII) >=0) {
qsort (ptrlinie, 0, nlinii-1);
tiparestelinii(ptrlinie, nlinii);
return 0;
}
else
{
printf(“eroare: prea multe date de sortat\n”);
return 1;
}
}

#define MAXLUNG 1000 /*lungimea maxima a unei linii de intrare*/


int preialinie(char *, int);

/* citestelinii: citeste liniile de intrare */


int citestelinii(char *ptrlinie[ ], int maxlinii)
{
int lung, nlinii;
char *p, linie[ MAXLUNG ];
nlinii = 0;
while ((lung = preialinie(linie, MAXLUNG)) > 0)
if (nlinii>=maxlinii ||(p=(char *)malloc(lung))==NULL)
return -1;
else {
linie[lung-1]=‘\0’; /* sterge caracterul rand nou */
strcpy(p, linie);
ptrlinie[nlinii++]=p;
}
return nlinii;
}

/* tiparestelinii: tipareste liniile de iesire */


void tiparestelinii(char *ptrlinie[ ], int nlinii)
{
int i;
for (i=0; i<nlinii; i++)
printf(“%s\n”, ptrlinie[i]);
}

50
Funcţia preialinie este cea descrisă anterior.

Principala noutate este declaraţia lui ptrlinie:


char *ptrlinie[MAXLINII]
care precizează că:
- ptrlinie este un tablou de MAXLINII elemente, fiecare element fiind un pointer
către char;
- ptrlinie[i] este un pointer spre un caracter, iar *ptrlinie[i] este
caracterul spre care indică pointerul, adică primul caracter al celei de a i-a linii de text salvată.

ptrlinie fiind el însuşi numele unui tablou, poate fi tratat ca un pointer, iar funcţia
tiparestelinii poate fi scrisă şi altfel:

/* tiparestelinii: tipareste liniile de iesire */


void tiparestelinii(char *ptrlinie[ ], int nlinii)
{
while (nlinii -- > 0)
printf(“%s\n”, *ptrlinie++);
}

La început, *ptrlinie indică spre prima linie; fiecare incrementare îl avansează pe


acesta la următorul pointer de linie, în timp ce nlinii descreşte de fiecare dată cu unu.

/*qsort:sorteaza v[stanga]...v[dreapta] in ordine crescatoare*/


void qsort(char *v[ ],int stanga,int dreapta)
{
int i, ultim;
void swap(char *v[ ],int i,int j);
if (stanga>=dreapta) /* nu face nimic daca tabloul*/
return; /* contine mai putin de 2 elemente*/
swap(v,stanga, stanga + dreapta)/2);
ultim=stanga;
for(i=stanga+1; i<=dreapta; i++)
if(strcmp(v[i],v[stanga])<0)
swap(v, ++ultim, i);
swap(v,stanga,ultim);
qsort(v, stanga, ultim-1);
qsort(v, ultim+1, dreapta);
}

/* swap: interschimba v[i] cu v[j] */


void swap(char *v[ ],int i,int j)
{
char *temp;
temp=v[i];
v[i]=v[j];
v[j]=temp;
}

51
2.9.1 Iniţializarea tablourilor de pointeri
Exemplu: o funcţie nume_luna(n), care returnează un pointer la un şir de caractere
ce conţine numele celei de a n-a luni.

char *nume_luna(int n)
{
static char *nume[]={
“Luna inexistenta”,
“Ianuarie”, “Februarie”, “Martie”, “Aprilie”,
“Mai”, “Iunie”, “Iulie”, “August”,
“Septembrie”,“Octombrie”,“Noiembrie”,“Decembrie”
};
return (n<1 || n>12)? nume[0]:nume[n];
}

Valorile de iniţializare ale tabloului de pointeri la caractere, notat nume, este o listă de
şiruri de caractere; fiecare dintre acestea este plasat în poziţia corespunzătoare din tablou.
Caracterele din cel de al i-lea şir sunt plasate undeva în stiva de date şi un pointer la
acest şir este stocat în nume[i].
Deoarece dimensiunea tabloului nu este precizată, compilatorul numără valorile de
iniţializare şi deduce valoarea corectă.

2.9.2 Accesarea prin pointeri a tablourilor multidimensionale


Începătorii în C au uneori nelămuriri cu privire la deosebirea dintre un tablou
bidimensional şi un tablou de pointeri.

Fie declaraţiile:
int a[10][20];
int *b[10];
atunci a[3][4] şi b[3][4] sunt din punct de vedere sintactic referinţe legale către un
singur int.
a este însă un tablou bidimensional veritabil: au fost alocate 200 de locaţii fiecare de
dimensiunea unui int, iar pentru găsirea elementului a[linie][col] se foloseşte formula:
20 x linie + col

Pentru b definiţia alocă doar 10 pointeri (locaţii de tip adresă) şi nu îi iniţializează;


iniţializarea trebuie efectuată fie la definire fie prin cod.
Presupunând că fiecare element al lui b indică spre un tablou de douăzeci de elemente,
atunci se va aloca spaţiu pentru cât pentru 200 de elemente de tip int, plus zece locaţii pentru
pointeri.

Avantajul cel mai important al tablourilor de pointeri este că liniile tabloului pot avea
lungimi diferite, adică nu este necesar ca toate elementele lui b să indice spre câte un vector de
douăzeci de elemente; unele pot indica spre două elemente, altele spre cincizeci şi altele spre
nici unul.

Cea mai frecventă întrebuinţare a tablourilor de pointeri este stocarea şirurilor de


caractere de lungimi diferite, ca în funcţia nume_luna din exemplul anterior.

52
Exemplu 1: tablou de pointeri

char *nume[]={“Luna inexistenta”, “Ian”, “Feb”, “Mar”};

Fig. 2.7 Reprezentarea unui tablou de pointeri

Exemplu 2: tablou bidimensional

char unnume[][17]={“Luna inexistenta”, “Ian”, “Feb”,“Mar”};

Fig. 2.8 Reprezentarea unui tablou bidimensional, clasic

2.10 Întrebări şi probleme


1. Ce este un tablou ?
2. Care este utilitatea tablourilor în programare ?
3. Cum se iniţializează tablourile în limbajul de programare C ?
4. Ce este un pointer ?
5. Ce reprezintă rvalue şi respectiv lvalue ?
6. Care este utilitatea pointerilor în limbajul de programare C ?
7. Care este efectul operatorului de adresă ? Dar al operatorului de indirectare ?
8. Ce variante avem în C, pentru tipărirea pointerilor ?
9. De ce se spune în C, că pointerii se referă la un anumit tip ?
10. Ce este un pointer generic ?
11. Cum se reprezintă în limbajul de programare C o adresă inexistentă ?
12. Căror entităţi li se poate aplica operatorul de adresă ?
13. Ce reprezintă, în C, valoarea unei variabile de tip tablou ?
14. Cu ce este echivalentă, în C, o expresie cu tablou şi indice ?
15. Ce operaţii aritmetice se pot aplica pointerilor ? Dar care sunt operaţiile interzise ?

53
16. În ce situaţie se poate efectua compararea sau diferenţa a doi pointeri ?
17. În ce constă alocarea dinamică a memoriei ?
18. Care sunt principalele funcţii de gestionare a memoriei alocate dinamic ?
19. Care este avantajul alocării dinamice a tablourilor ?
20. Ce este durata de viaţă a unei variabile ?
21. Să se citească un şir format din n valori reale. Să se determine elementul maxim al şirului,
precum şi rangul său.

22. Să se citească de la tastatură un vector cu maximum 50 de elemente numere reale. Să se


calculeze media aritmetică a elementelor şirului. Să se formeze un nou şir care să conţină
elementele mai mici decât media aritmetică.

23. Se dă o matrice cu m linii şi n coloane cu elemente întregi. Se cere:


a) Să se construiască doi vectori conţinând elementele pozitive şi respectiv
negative din matricea dată;
b) Să se afişeze vectorii formaţi.

24. Se dă o matrice cu m linii şi n coloane cu elemente întregi. Se cere:


a) Să se determine elementele numărul elementelor pare de pe fiecare linie a
matricei date şi să se memoreze într-un vector;
b) Să se afişeze vectorul format.
25. Să se citească un şir format din n valori de tip întreg (n<=20).
a) Să se elimine din şir toate elementele pare.
b) Să se insereze în şir după fiecare element egal cu 0 un element cu valoarea 1.
c) Determinaţi cea mai lungă secvenţă formată din elemente egale, existentă în
vector. Afişaţi lungimea secvenţei, poziţia de început şi valoarea care se repetă.

26. Să se citească un şir format din n valori reale. Să se mute la sfârşitul şirului elementele sale
negative.

27. Să se citească două şiruri, ordonate crescător, formate din m, respectiv n elemente. Să se
interclaseze cele două şiruri astfel încât şirul rezultat să fie ordonat crescător.

28. Să se realizeze un program care citeşte de la tastatură un text pe mai multe linii, terminat cu
linie vidă. Să se afişeze numărul şi frecvenţa de apariţie (în procente faţă de numărul total de
litere din text) a fiecărei litere din alfabet.

29. Se citesc de la tastatură cuvinte, câte unul pe linie şi terminate cu CTRL+Z. Pentru fiecare
cuvânt se cere să se precizeze dacă este format numai din litere iar, în caz afirmativ, câte
dintre acestea sunt vocale şi câte sunt consoane.

30. Se citeşte o succesiune de cuvinte, câte unul pe o linie de intrare. Succesiunea se încheie cu
o linie vidă. Se citeşte apoi un nou cuvânt, dat pe o linie distinctă. Se cere să se afişeze toate
cuvintele din succesiunea citită iniţial care rimează cu cuvântul dat (au ultimele trei
caractere identice).

54
31. Se dă o matrice cu m linii şi n coloane (m,n <= 20) de valori reale. Să se citească şi să se
afişeze matricea. Să se determine elementul maxim de pe fiecare coloană a matricii şi să se
memoreze într-un vector. Să se afişeze vectorul obţinut.

32. Se consideră o matrice de valori întregi cu m linii şi n coloane. Se cere:


a) Să se afişeze indicele liniilor ordonate crescător;
b) Să se afişeze toate elementele matricei care sunt numere perfecte.
Observaţie: un număr perfect este egal cu suma divizorilor săi, mai puţin el însuşi
(exemplu: 6=1+2+3 este număr perfect).

33. Se dă o matrice pătrată de dimensiune n (n<=10) de valori întregi. Să se calculeze şi să se


afişeze suma elementelor de sub diagonala principală. Să se verifice dacă deasupra
diagonalei principale există cel puţin un număr par. Spaţiul pentru matrice se va aloca
dinamic.

34. Se consideră o matrice pătrată de ordinul n (n<=20) de valori întregi. Cele două diagonale
determină patru zone notate 1, 2 3 şi respectiv 4, ca în figura de mai jos, care nu includ
elementele de pe diagonale:

1
2 3

Se cere:
a) Să se determine media aritmetică a elementelor pare din zonele 2 şi 3;
b) Să se determine numărul elementelor impare din regiunile 1 şi 4.

35. Fiind dată o matrice cu elemente numere întregi, să se determine punctele-şa din matrice,
adică acele puncte care sunt minime pe linia lor şi maxime pe coloana lor.

36. Se dă o matrice pătrată de dimensiune n (n<=50) cu elemente numere naturale <= 500.
Să se calculeze cmmdc ai elementelor simetrice faţă de diagonala principală. Pentru fiecare
pereche se vor preciza poziţiile elementelor, elementele şi cmmdc.

37. Fie n un număr natural impar (n<=10). Să se construiască o matrice pătrată cu n linii şi
coloane, după modelul următor:

2 2 0 1 1
2 2 0 1 1
0 0 0 0 0
3 3 0 4 4
3 3 0 4 4

55
38. Fie n un număr natural impar (n<=10). Să se construiască o matrice pătrată cu n linii şi
coloane, după modelul următor:

0 1 1 1 0
2 0 1 0 4
2 2 0 4 4
2 0 3 0 4
0 3 3 3 0

39. Să se citească un text terminat cu linie vidă. Să se extragă cuvintele din text, să se
memoreze, să se sorteze alfabetic şi să se afişeze.

40. Se dă un şir format din n valori reale. Să se citească elementele şirului şi să se memoreze
într-o zonă alocată dinamic. Să se sorteze şirul crescător şi să se afişeze. Pentru accesarea
elementelor şirului să se utilizeze doar pointeri.

41. Se dă un şir format din n valori întregi. Să se citească elementele şirului şi să se memoreze
într-o zonă alocată dinamic. Să se formeze un nou şir care să conţină elementele pare din
primul şir. Noul şir să se memoreze de asemenea într-o zonă alocată dinamic.

42. Să se citească un şir format din n (n<=50) valori întregi şi să se memoreze într-o zonă
alocată dinamic. Să se afişeze şirul citit.
a) Să se verifice dacă toate elementele şirului sunt numere prime.
b) Să se verifice dacă există în şir un palindrom (număr care, citit de la stânga la
dreapta şi de la dreapta la stânga, este acelaşi).

43. Se dă un şir format din n (n<=30) valori întregi. Să se citească şi să se memoreze


elementele şirului. Să se insereze după fiecare element egal cu 0 un element cu valoarea 1.

44. Fie a o matrice cu m linii şi n coloane de valori întregi. Să se memoreze matricea într-o
zonă alocată dinamic. Să se afişeze matricea citită. Să se verifice dacă există în matrice o
coloană cu toate elementele nule.

45. Fie a o matrice pătratică cu n linii şi coloane de valori întregi. Să se memoreze matricea
într-o zonă alocată dinamic. Să se afişeze matricea citită. Să se verifice dacă matricea este
simetrică faţă de diagonala principală.

46. Fie a o matrice pătratică cu m linii şi n coloane de valori reale. Să se memoreze matricea
într-o zonă alocată dinamic. Să se afişeze matricea citită. Să se calculeze suma elementelor
de sub diagonala principală si suma elementelor situate pe chenarul exterior al matricii.

47. Să se citească un text de maximum 80 de caractere. Să se determine numărul de vocale din


text şi să se afişeze histograma vocalelor.

Exemplu: pentru textul


astazi este soare şi cald

56
afişarea este
a: ****
e: ***
i: **
o: *
u:

48. Să se citească de la tastaură n cuvinte şi să se memoreze într-un tablou de pointeri la şiruri


de caractere. Fiecare cuvânt citit va fi inserat după citire astfel încât tabloul să conţină în
fiecare moment cuvintele în ordine alfabetică.

49. Să se citească de la tastatură un şir care are foarte multe elemente nule. Să se parcurgă
tabloul utilizând un pointer la int şi să se afişeze primul element nenul utilizând acest
pointer, ca în exemplul de mai jos.

intrare (tastatură) ieşire (ecran)


n=5 Primul elem. nenul este 8, la pozitia 3
a[0] = 0
a[1] = 0
a[1] = 0
a[3] = 0
a[4] = 0

50. Să se citească două texte. Să se determine vocala care apare de cele mai multe ori în fiecare
text.

51. Să se citească şi să se memoreze numele mai multor persoane. Să se afişeze lista


persoanelor în ordine alfabetică.

52. Să se citească o frază şi un cuvânt. Să se caute toate apariţiile cuvântului în cadrul frazei şi
să se afişeze de fiecare dată acea parte din frază aflată după identificarea caracterului.

53. Să se citească un text terminat cu linie vidă şi apoi un cuvânt. Să se şteargă toate apariţiile
cuvântului din text.

57
3
Structuri şi uniuni
O structură este o colecţie de una sau mai multe componente care pot să fie de tipuri
diferite, grupate împreună sub un singur nume pentru uşurinţa manipulării în ansamblu.
Structurile ajută la organizarea datelor complicate, în special în programe ample,
deoarece permit ca un grup de variabile care au ceva în comun să fie tratate ca o unitate, în loc
de a fi considerate entităţi separate.
Un exemplu tradiţional de structură este o înregistrare din statul de plată: un angajat
este descris printr-un set de atribute precum nume, adresă, cod numeric personal, salariu etc.
Unele dintre acestea pot fi, la rândul lor structuri: un nume are mai multe componente, la fel o
adresă şi chiar un salariu.

3.1 Bazele structurilor


Ne propunem să creăm o structură potrivită pentru grafică. Obiectul de bază este un
punct, care are o coordonată x şi o coordonată y, ambele întregi (fig. 3.1).

Fig. 3.1 Reprezentarea unui punct în plan

Cele două componente pot fi plasate într-o structură declarată astfel:

struct punct {
int x;
int y;
};

Cuvântul cheie struct introduce o declaraţie de structură, care este o listă închisă
între acolade. După cuvântul struct poate urma un nume opţional, denumit nume generic al
structurii (structure tag) (punct, în exemplu nostru). Numele generic reprezintă acest
tip de structură şi poate fi folosit ulterior ca prescurtare pentru partea declaraţiei cuprinsă între
acolade.
Variabilele definite într-o structură (componentele sau câmpurile structurii) se numesc
membri, în limbajul de programare C.

59
Un membru al unei structuri sau numele generic al acesteia şi o variabilă obişnuită
(care nu este membru) pot avea acelaşi nume fără a apărea conflicte, deoarece acestea pot fi
oricând deosebite prin context. De asemenea, în structuri diferite pot să apară aceleaşi nume de
membri.

O declaraţie struct defineşte un tip. Ca atare, acolada închisă care încheie lista de
membri poate fi urmată de o listă de variabile, la fel ca în cazul oricărui tip de bază:

struct { ... } x,y,z;

O declaraţie de structură care nu este urmată de o listă de variabile nu alocă spaţiu; doar
descrie un şablon de structură.
Dacă în declaraţie apare numele generic, acest nume poate fi folosit mai târziu în
definirea unor instanţieri ale structurii.
Declaraţia

struct punct pt;

defineşte o variabilă pt care este o structură de tipul struct punct.

O structură poate fi iniţializată plasând după definiţia sa o listă de valori de iniţializare


pentru membri, fiecare valoare fiind o expresie constantă:

struct punct maxpt = {320, 200};

Într-o expresie, un membru al unei structuri poate fi accesat printr-o construcţie de


forma:
nume-structura.membru

Operatorul “.” (punct) de acces la membrii unei structuri leagă numele structurii de
numele membrului.

Exemplu:
printf(“%d,%d”,pt.x,pt.y);
dist=sqrt((double)pt.x * pt.x +(double)pt.y * pt.y);

Structurile pot fi imbricate (incluse unele în altele).


Exemplu: o reprezentare a unui dreptunghi este o pereche de puncte care reprezintă
colţurile diagonal opuse (fig. 3.2).

Fig. 3.2 Reprezentarea, în plan, a unui dreptunghi

60
struct drept {
struct punct pt1;
struct punct pt2;
}
Structura drept conţine două structuri punct.
Dacă declarăm variabila ecran ca:

struct drept ecran;


atunci
ecran.pt1.x
se referă la coordonata x a membrului pt1 din ecran.

Exemple de structuri întâlnite frecvent:


struct data
{
int zi;
int luna;
int an;
};

struct angajat
{
char nume[64];
int varsta;
int categ_salarizare;
float salariu;
unsigned nr_marca;
} angajat_info, nou_angajat, fost_angajat;

3.2 Operaţii asupra unei structuri, în ansamblul ei


Singurele operaţii legale ce pot fi efectuate asupra unei structuri sunt:
- copierea acesteia prin operaţia de atribuire sau prin transmiterea de argumente
funcţiilor şi returnarea de valori din funcţii (structura se consideră ca o unitate);
- invocarea adresei structurii cu operatorul &;
- accesarea membrilor unei structuri.
O structură poate fi iniţializată la definirea sa, printr-o listă de valori constante pentru
membri (vezi iniţializarea maxpt dintr-un exemplu anterior şi exemplul următor); o structură
automatică poate fi, de asemenea, iniţializată printr-o atribuire sau prin apelarea unei funcţii
care returnează o valoare structură de tipul corespunzător.

Exemplu:
struct student {char* nume; float medie;}
candidat = {“Gigel”, 8.6};

Structurile nu pot fi comparate.

61
3.3 Transmiterea structurilor ca argumente
Există 3 abordări posibile pentru transmiterea conţinutului unei structuri prin
mecanismul parametri - argumente, fiecare cu avantajele şi dezavantajele sale:
- transmiterea componentelor separat;
- transmiterea unei întregi structuri;
- transmiterea unui pointer către aceasta.

Programul 3.1: funcţie pentru lucrul cu punctele şi cu dreptunghiurile.

/*creazapunct: creaza un punct din comp. x si y */


struct punct creazapunct(int x, int y)
{
struct punct temp;
temp.x = x;
temp.y = y;
return temp;
}

Nu există nici un conflict între numele argumentului şi membrul cu acelaşi nume;


refolosirea numelor evidenţiază mai clar relaţia dintre aceştia.

Funcţia creazapunct poate fi folosită pentru a iniţializa dinamic orice structură, sau
pentru a furniza unei funcţii argumente de tip structură:
struct drept ecran;
struct punct mijloc;
struct punct creazapunct(int, int);
ecran.pt1 = creazapunct(0, 0);
ecran.pt2 = creazapunct(XMAX, YMAX);
mijloc = creazapunct((ecran.pt1.x + ecran.pt2.x)/2,
(ecran.pt1.y + ecran.pt2.y)/2);

Programul 3.2: funcţie care efectuează operaţii aritmetice cu puncte.

/*ptindrept:returneaza 1 daca p se afla in interiorul


dreptunghiului d si 0 in caz contrar */
int ptindrept(struct punct p, struct drept d)
{
return p.x >= d.pt1.x && p.x < d.pt2.x &&
p.y >= d.pt1.y && p.y < d.pt2.y;
}

Presupunem că dreptunghiul este reprezentat într-o formă standard în care coordonatele


lui pt1 sunt mai mici decît coordonatele lui pt2 (forma canonică).

Programul 3.3: Aducerea unui dreptunghi la forma canonică.


#define MIN(a,b) ((a)<(b) ? (a):(b))
#define MAX(a,b) ((a)>(b) ? (a):(b))

/*canondrept:adu coordonatele dreptunghiului la forma canonica */


struct drept canondrept(struct drept d)
{
struct drept temp;

62
temp.pt1.x = MIN(d.pt1.x, d.pt2.x);
temp.pt1.y = MIN(d.pt1.y, d.pt2.y);
temp.pt2.x = MAX(d.pt1.x, d.pt2.x);
temp.pt2.y = MAX(d.pt1.y, d.pt2.y);
return temp;
}

3.4 Structurile şi pointerii


Dacă trebuie să transmitem unei funcţii o structură voluminoasă, este mai eficient să
transmitem un pointer la structură decât să copiem întreaga structură. Pointerii spre structuri
sunt identici cu pointerii spre orice variabilă.

Declaraţia
struct punct *pp;

precizează că pp este un pointer la o structură punct. Dacă pp indică spre o structură


punct, atunci *pp este structura, iar (*pp).x şi (*pp).y sunt membrii. Pentru a putea
folosi pointerul pp ca mai sus, acesta trebuie mai întâi iniţializat:

Exemplu:
struct punct origine, *pp;
.....
pp = &origine;
.....
printf(“originea este (%d,%d)\n”, (*pp).x, (*pp).y);

În expresia (*pp).x sunt necesare parantezele deoarece precedenţa operatorului de


acces la membrii unei structuri, “.”, este mai mare decât cea a operatorului *.
Există însă şi o notaţie alternativă. Dacă p este un pointer la o structură, atunci

p->membru-al-structurii

face referire la acel membru.


În exemplul de mai sus, am putea scrie cu acelaşi efect:
printf(“originea este (%d,%d)\n”, pp->x, pp->y);
Atât operatorul . cât şi operatorul -> se asociază de la stânga la dreapta.

Exemplu: dacă avem:


struct drept d, *dp = &d;

atunci următoarele patru expresii sunt echivalente:

d.pt1.x
dp->pt1.x
(d.pt1).x
(dp->pt1).x

Operatorii pentru structuri . şi ->, împreună cu operatorul ( ) pentru apelurile de funcţii


şi cu operatorul [] pentru indici, se află în vârful ierarhiei precedenţei şi sunt foarte strâns
legaţi de operanzii lor.

63
Exemple vizând prioritatea operatorilor:

Fiind dată declaraţia


struct {
int lung;
char *str;
} *p;
atunci:
++p->lung incrementează variabila lung, nu variabila p, deoarece asocierea
implicită este ++(p->lung);
(++p)->lung incrementează variabila p înainte de a-l accesa pe lung;
(p++)->lung incrementează variabila p ulterior accesării.

În acelaşi mod:
*p->str preia obiectul spre care indică str;
*p->str++ incrementează pointerul str după accesarea obiectului spre care indică
acesta;
(*p->str)++ incrementează obiectul spre care indică str;
*p ++-> str incrementează variabila p după accesarea obiectului spre care
indică p.

3.5 Tablouri de structuri. Exemplu


Programul 3.4: Vrem să realizăm un program care să numere apariţiile fiecărui cuvânt
cheie din C dintr-un text dat în fişierul de intrare.
Avem nevoie de un tablou de şiruri de caractere care să memoreze numele şi de un
tablou de întregi care să memoreze rezultatele (numărul de apariţii). Cele două tablouri pot fi
reprezentate printr-un vector de structuri.

Definim structura:

struct cheie
{
char *cuvant;
int rez;
} tabchei[NCHEI];

Tabloul poate fi definit şi astfel:


struct cheie
{
char *cuvant;
int rez;
};
struct cheie tabchei[NCHEI];

Deoarece structura tabchei conţine un set constant de nume, cel mai simplu este să o
facem variabilă externă şi să o iniţializăm o dată pentru totdeauna la definire.
Iniţializarea se face printr-o listă de valori de iniţializare sub formă de perechi
corespunzătoare cu membrii structurii, închise între acolade:

64
struct cheie {
char *cuvant;
int rez;
} tabchei[ ] = {
“auto”, 0,
“break”, 0,
“case”, 0,
.....
“while”, 0};
Acoladele interioare ({“auto”, 0}), nu sunt necesare.

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#define MAXCUVANT 100

int preiacuvant(char *, int);


int cautbin(char *, struct cheie *, int);
/* numara cuvintele cheie C dintr-un text */

main()
{
int n;
char cuvant[MAXCUVANT];
while (preiacuvant(cuvant, MAXCUVANT) != EOF)
if (isalpha(cuvant[0]))
if ((n = cautbin(cuvant, tabchei, NCHEI)) >= 0)
tabchei[n].rez++;
for(n=0; n<NCHEI; n++)
if (tabchei[n].rez >0)
printf(“%4d %s\n”,tabchei[n].rez, tabchei[n].cuvant);
return 0;

/* cautbin: cauta cuvantul in tab[0] ... tab[n-1] */


int cautbin(char *cuvant, struct cheie tab[ ], int n)
{
int cond;
int prim, ultim, mijloc;
prim = 0;
ultim = n–1;
while (prim <= ultim)
{
mijl = (prim + ultim) / 2;
if ((cond = strcmp(cuvant, tab[mijl].cuvant)) < 0)
ultim = mijl – 1;
else if (cond > 0)
prim = mijl + 1;
else
return mijl;
}
return -1;
}

65
Entitatea NCHEI reprezintă numărul de cuvinte cheie din tabchei.
Cum o determinăm ? Dimensiunea tabloului este complet determinată în momentul
compilării. Numărul de intrări este dat de expresia:

dimensiunea lui tabchei/dimensiunea lui struct cheie

Limbajul C pune la dispoziţie un operator unar care acţionează în momentul compilării,


numită sizeof, şi care poate fi folosit pentru a a calcula dimensiunea unui obiect.
Expresiile:
sizeof obiect şi
sizeof(nume tip)
furnizează dimensiunea în octeţi a obiectului (variabilă, tablou sau structură) sau a
tipului specificat.

În program, putem calcula numărul de cuvinte alegând una dintre următoarele două
variante:
- împărţind dimensiunea tabloului la dimensiunea unui element, într-o instrucţiune
#define:
#define NCHEI (sizeof tabchei / sizeof(struct cheie))

- împărţind dimensiunea tabloului la dimensiunea unui element precizat:


#define NCHEI (sizeof tabchei / sizeof tabchei[0])
care prezintă avantajul că nu trebuie schimbată dacă se schimbă tipul.

Operatorul sizeof nu poate fi folosit într-o linie #if, deoarece preprocesorul nu


analizează nume de tipuri (expresia din #define nu poate fi evaluată de preprocesor, vezi şi
cap. 5).

3.6 Declaraţia typedef


Limbajul C pune la dispoziţie o facilitate numită typedef pentru crearea de noi nume
de tipuri de date.

Exemple:
Declaraţia
typedef int Lungime;
face numele Lungime sinonim cu int. Tipul Lungime poate fi folosit în declaraţii,
conversii etc., în exact acelaşi mod ca şi int:

Lungime lung, maxlung;


Lungime *lungimi[ ];
Declaraţia
typedef char *sir;
face numele Sir sinonim cu char * ( pointer spre caracter ); acesta poate fi
folosit în declaraţii sau conversii, astfel:
sir p, ptrlinie[MAXLINII], malloc(int);
int strcmp(sir, sir);
p = (sir) malloc(100);

66
Tipul declarat într-o construcţie typedef apare în poziţia unui nume de variabilă şi nu
imediat după cuvântul typedef. Din punct de vedere sintactic, typedef este asemănător cu
clasele de memorie extern, static etc.
O declaraţie typedef nu creează un tip nou. Ea doar atribuie un nume unui tip
existent. Motivele principale pentru utilizarea construcţiilor typedef sunt:
1. Creşterea portabilităţii programelor: dacă se folosesc construcţii typedef pentru
tipuri de date care pot fi dependente de maşină, numai construcţiile typedef
trebuie modificate atunci când programul este mutat pe alt calculator.
2. Furnizarea unei documentaţii mai bune pentru un program.

3.7 Alocarea dinamică implicând structuri


Exemplu: Se considera tipul structurat persoana, definit în felul următor:
struct persoana
{
char nume[30];
int varsta;
};

O variabilă dinamică de tip pointer la tipul structurat persoana se alocă şi se utilizează în


felul urmator:
struct persoana * pp;
pp=(persoana *)malloc(sizeof(persoana));
pp->varsta=20;
strcpy(pp->nume, "ion");

Programul 3.5: Să se citească datele (numele şi vârsta) pentru un număr oarecare n de


persoane şi să se memoreze într-o structură. Să se găsească o structură de date adecvată astfel
încât consumul de memorie să fie optim.

Se defineşte tipul structurat persoana:


typedef struct {
char *nume;
int varsta;
} persoana;
int n;
persoana *tab;

void citire(void) {
int i;
char buf[80];
printf("Introduceti nr de persoane \n");
scanf("%d", &n);
if (!(tab=(persoana *)malloc(n*sizeof(persoana)))) {
printf("Eroare alocare dinamica memorie \n");
exit(1);
}
for (i=0; i<n; i++) {
printf("Introduceti numele persoanei:");
scanf("%s", buf);
if (!(tab[i].nume=(char *)malloc(strlen(buf)+1))) {

67
printf("Eroare alocare dinamica memorie \n");
exit(1);
}
strcpy(tab[i].nume, buf);
printf("Introduceti varsta persoanei:");
scanf("%d", &tab[i].varsta);
}
}

Programul 3.6: Să se scrie un program care gestionează date despre un grup de studenţi.
Pentru fiecare student se memorează numele şi numărul matricol. Programul trebuie să
implementeze următoarele operaţii:

- citirea numărului de studenţi şi a datelor acestora;


- afişarea datelor tuturor studenţilor;
- sortarea listei de studenţi în ordinea alfabetică a numelor;
- sortarea listei de studenţi în ordinea crescătoare a numerelor matricole;
- căutarea unui student pentru care se precizează numele şi afişarea poziţiei pe care o
ocupă acesta în lista ordonată alfabetic după numele studenţilor;
- căutarea unui student pentru care se precizează numărul matricol şi afişarea poziţiei pe
care o ocupă acesta în lista ordonată crescător după numărul matricol al studenţilor;

#include<stdio.h>
#include<string.h>
#include<stdlib.h>

typedef struct {
char *nume;
int nr;
} student;

#define SIZE sizeof(student)

typedef student *pstud;

void eroare(void) {
puts(“\n **** eroare alocare dinamica de memorie ****”);
exit(1);
}

void citeste(int *n, pstud *tab) {


pstud t;
char sir[40];
int i;
printf(“\n dati numarul studentilor:”);
scanf(“%d”, n);
if(!(t=(pstud)malloc((*n)*SIZE))) eroare();
*tab=t;
for (i=0; i<*n; i++, t++) {
printf(“\n nume: “);
scanf(“%s”, sir);
/* aloca dinamic spatiu pentru numele studentului */
if (!(t->nume=(char *) malloc(strlen(sir)+1)))
eroare ();

68
strcpy(t->nume, sir);
printf(“\n numar matricol: “);
scanf(“%d”, &t->nr);
}
}

void afiseaza ( int n, pstud tab)


{
int i;
puts(“\n tabelul cu studenti “);
for (i=0; i<n; i++, tab++)
printf(“\n%-30s %4d”, tab->nume, tab->nr);
}

void elibereaza (pstud tabel, int n) {


int i;
for (i=0; i<n; i++)
free(tabel[ i ].nume);
free(tabel);
}

void meniu(void) {
puts(“\n c, C --- citeste tabel studenti”);
puts(“\n a, A --- afiseaza tabel studenti”);
puts(“\n n, N --- ordoneaza dupa nume”);
puts(“\n r, R --- ordoneaza dupa numar matricol”);
puts(“\n f, F --- cauta dupa nume”);
puts(“\n l, L --- cauta dupa numar matricol”);
puts(“\n x, X --- iesire din program”);
}

void main(void) {
char opt;
int n; /* numarul de studenti */
char nume[30];
student s;
pstud tabel=NULL; /* adresa tabloului cu studenti */
while (1)
{
meniu();
opt=tolower(getchar());
switch(opt) {
case ‘c’:
if(tabel)/* daca a existat anterior un alt tabl. in
mem.*/
elibereaza(tabel, n);
citeste(&n, &tabel);
break;
case ‘a’:
afiseaza(n, tabel);
break;
case ‘n’:
sorteaza_alfabetic(tabel, n);
break;
case ‘r’:

69
sorteaza_dupa_nr_matricol(tabel, n);
break;
case ‘f’:
printf(“\n dati numele:”);
scanf(“%s”, nume);
if (!(s.nume=(char *)malloc(strlen(nume)+1)))
eroare();
strcpy(s.nume, nume);
cauta_dupa_nume(&s, tabel, n);
free(s.nume);
break;
case ‘l’:
printf(“\n dati numarul matricol:”);
scanf(“%d”, &s.nr);
cauta_dupa_nr_matricol(&s, tabel, n); break;
case ‘x’:
exit(0);
default:
puts(“Comanda gresita”);
}
}
}

3.8 Uniuni
O uniune este o structură în care toţi membrii sunt memoraţi la aceeaşi adresă. Aceasta
înseamnă că, la un moment dat, într-o uniune poate exista doar un singur membru.
Tipul de dată union a fost inventat pentru a preveni fragmentarea memoriei în bucăţi de
dimensiuni inutilizabile. Acest lucru se obţine prin crearea unei dimensiuni standard pentru
anumite date, spaţiul alocat pentru uniune fiind egal cu dimensiunea elementului maxim al
uniunii.
De asemenea, uniunile permit ca o porţiune de memorie să fie interpretată ca fiind
tipuri de date diferite, întrucît toate aceste tipuri sunt de fapt în aceeaşi locaţie de memorie dar
la momente de timp diferite.

3.8.1 Declararea unei uniuni


O uniune se declară în acelaşi mod ca şi o structură. Ea conține o listă de membri care
pot să fie de diferite tipuri.

Exemplu general:

union nume_uniune {
tip1 element1;
tip2 element2;
tip3 element3
} nume_obiect;

Declararea unei variabile uniune se face la fel ca şi declararea unei variabile structură.
O uniune nu poate fi iniţializată decât cu o valoare de tipul primului său membru.

70
3.8.2 Utilizarea unei uniuni
Exemplu:
union int_sau_real
{
int membru_int;
float membru_float;
};
union int_sau_real uniune1, uniune2;

Ca şi în cazul structurilor, membrii unei uniuni pot fi accesaţi cu operatorii . şi ->.


Totuşi, spre deosebire de structuri, variabilele uniune1 şi uniune2 pot fi tratate, în timpul
execuţiei programului, fie ca variabile întregi, fie ca variabile reale în virgulă flotantă.
De exemplu, dacă scriem:
uniune1.membru_int = 5;
atunci programul vede uniune1 ca fiind un întreg.
Dacă apoi vom scrie:
uniune1.membru_float = 7.5;
atunci valoarea întreagă a variabilei uniune1 se va pierde şi această variabilă va
reprezenta în continuare un număr real.

Concluzie: o variabilă de tip union nu poate avea, la un moment dat, decât un singur
tip: tipul membrului activ în acel moment.

3.8.3 Exemple de utilizare


O modalitate de a reţine ce tip de valoare este memorată în variabila uniune este aceea
de utiliza pentru fiecare uniune o variabilă fanion. Se recomandă ca tipul variabilei fanion să
fie un tip enumerare.

Exemplul 1: uniunii int_sau_real îi putem asocia următorul tipul enumerare :


enum care_membru {
INT,
FLOAT
};

Variabilele de tip uniune şi de tip enumerare pot fi declarate în pereche, astfel:


union int_sau_real uniune1;
enum care_membru stare_uniune1;

iar pentru prelucrarea uniunii cea mai potrivită va fi o instrucţiune switch:

switch (stare_uniune1) {
case INT:
uniune1.membru_int += 5;
break;
case FLOAT:
uniune1.membru_float += 23.222333;
break;
}

71
În vederea unei mai uşoare utilizări, putem grupa aceste variabile într-o structură:

struct multitip
{
enum care_membru stare;
union int_sau_real numar;
};
struct multitip mt;

iar asignările membrilor structurii le facem în pereche:

mt.stare = INT;
mt.numar.membru_int = 5;

Exemplul 2: Într-o uniune putem grupa tipuri elementare împreună cu tablouri sau
structuri de elemente de dimensiuni mici.

union mixt_t {
long l;
struct {
short x;
short y;
} s;
char c[4];
} mixt;

Structura defineşte trei nume care ne permit să accesăm acelaşi grup de 4 octeţi, la momente de
timp diferite, ca şi tipuri long, short sau char.

Modalităţile de accesare a datelor sunt prezentate în fig. 3.3.

Fig. 3.3 Reprezentarea în memorie a uniunii mixt, în cele trei variante

3.9 Câmpuri de biţi


Printre alte facilităţi la nivel scăzut, C include şi operaţii pe biţi. În acest sens este
firească preocuparea de a include şi reprezentarea datelor pe biţi.
Limbajul C conţine un mecanism de structurare a datelor la nivel de bit: se pot aloca,
unor câmpuri de structuri sau uniuni, biţi individuali sau grupuri de biţi dintr-un octet, numite
câmpuri de biţi care pot fi accesate individual şi unitar, în mod direct.

72
Un câmp de biţi este un şir de biţi adiacenţi conţinuţi într-un cuvânt calculator.
Câmpurile de biţi se grupează în structuri. Se pot defini câmpuri de biţi doar ca şi componente
de tip întreg fără semn ale structurilor. În declaraţia câmpului se specifică şi lungimea
acestuia în biţi.

Exemplu: Descrierea datelor meteorologice. Trebuie să se memoreze data (ziua şi luna),


dacă în respectiva zi a plouat, dacă a fost soare, sau dacă a nins. Se alocă câte un bit pentru
ploaie, soare şi respectiv ninsoare. Ziua şi luna pot fi reprezentate şi ele ca şi câmpuri de biţi,
deoarece au valori într-un interval redus.

struct meteo {
unsigned int ziua : 5;
unsigned int luna : 4;
unsigned int ploaie :1;
unsigned int soare :1;
unsigned int ninsoare :1;
} m;

Principalul avantaj: câmpurile de biţi pot fi accesate în mod direct:

m.ziua=15;
m.luna=5;
m.ploaie=1;
m.soare=1;
m.ninsoare=0;

Utilizarea câmpurilor de biţi implică următoarele restricţii:

- aceştia trebuie definiţi în cadrul unei structuri sau uniuni;


- nu se pot defini tablouri de câmpuri de biţi;
- unui câmp de biţi nu i se poate aplica operatorul de adresă (nu se pot exprima adrese
care să puncteze în interiorul unui octet).

O structură poate conţine atât câmpuri de date normale cât şi câmpuri de biţi.

3.10 Întrebări şi probleme


1. Ce este o structură ? Dar o uniune ?
2. Daţi exemple de structuri.
3. Ce este numele generic al structurii şi la ce se utilizează ?
4. Cum se numesc componentele unei structuri ?
5. Cum se iniţializează o structură ?
6. Cum se accesează membrii unei structuri ? Daţi un exemplu.
7. Ce înseamnă structuri imbricate ? Daţi un exemplu.
8. Ce operaţii se pot efectua cu structuri în ansamblul lor ?
9. Cum se poate transmite valoarea unei structuri către o funcţie ?

73
10. Care este utilitatea pointerilor spre structuri ?
11. Comparaţi operatorii pentru structuri ”.” şi respectiv ”->”.
12. Care este prioritatea operatorilor pentru structuri ? Daţi exemple de operatori de aceeaşi
prioritate.
13. Care este utilitatea declaraţiei typedef ?
14. Care este deosebirea între uniune şi structură ? Dar asemănările ?
15. Cum se poate iniţializa o uniune C
16. Ce sunt câmpurile de biţi ? În ce situaţii este utilă această facilitate ?
17. Ce restricţii trebuie să respecte câmpurile de biţi ?
18. Se dă un text format din mai multe linii, terminat cu linie vidă. Să se citească textul şi să se
memoreze. Să se afişeze liniile textului în ordinea descrescătoare a lungimii lor. Pentru
memorarea datelor se va folosi spaţiul de memorie minim necesar.
19. Se citesc informaţii despre n studenţi (alocaţi dinamic): „Nume | Prenume | Adresa |
Medie”; adresa se va memora dinamic; se vor folosi structuri.
b) Afişaţi toţi studenţii cu datele respective.
c) Afişaţi studentul (sau studenţii) cu media cea mai mare.

20. Se citeşte de la tastatură un număr întreg pozitiv N, iar apoi se citesc N cuvinte. Pentru
fiecare din cele N cuvinte citite, afişaţi literele cuvântului în sens invers (parcurse de la
coadă la cap).

21. Se citesc date (nume şi data naşterii) pentru un număr oarecare de persoane şi se
memorează într-o structură de date. Întâi se citeşte n, numărul de persoane, apoi se citesc
datele celor n persoane. Să se găsească o structură de date adecvată astfel încât consumul
de memorie să fie optim. Să se afişeze persoanele ordonate alfabetic şi apoi în ordinea
descrescătoare a vârstei.
22. Se citesc de la tastatură n cuvinte. Să se memoreze cuvintele în zone de memorie alocate
dinamic. Să se sorteze cuvintele în ordine alfabetică şi să se afişeze. Să se afişeze cuvintele
care încep cu o anumită literă.
23. Dându-se o propoziţie de la tastatură, să se afişeze toate cuvintele din componenta ei, cu
frecventa lor de apariţie, sortate în ordine crescătoare dupa frecvenţă. De exemplu din
propozitia "Ion si Maria si Nelu merg la Ion acasa" rezulta: Maria=1,
Nelu=1, merg=1, la=1, acasa=1, Ion=2, si=3.
24. Se citeşte un text scris pe mai multe rânduri, cuvintele fiind despărţite prin unul din
caracterele “ ,.;!?”. Să se extragă cuvintele din text. Să se afişeze cuvintele în ordine
alfabetică. Dacă un cuvânt apare de mai multe ori va fi scris o singură dată. Să se memoreze
într-o zonă alocată dinamic cuvântul de lungime maximă şi să se afişeze. Pentru extragerea
cuvintelor din text se poate folosi funcţia strtok.
25. Se citeşte un dicţionar de cuvinte, până la CTRL+Z:

cuv1 explicatie1
cuv2 explicatie2

74
Se vor folosi structuri acolo unde se pretează; se consideră că un cuvânt poate avea maxim
20 de caractere dar explicaţia oricâte. Numărul de cuvinte al dicţionarului poate varia mult.
a) Afişaţi dicţionarul.
b) Citiţi un cuvânt de la tastatură şi afişaţi explicaţia lui sau mesajul „cuvânt
inexistent”, după caz.

26. Se citesc de la tastatură cuvinte până la apariţia cuvântului „gata”. Cuvintelor li se atribuie
indici, în ordine începând cu 1. Definiţi o structură în care se memorează un cuvânt,
numărul său de apariţii şi un tablou cu indicii la care a apărut cuvântul. Cuvântul din
structură, precum şi tabloul de indici, vor fi alocaţi dinamic. Construiţi un tablou de astfel
de structuri, alocat dinamic şi el, în care păstraţi informaţia despre cuvintele citite de la
tastatură. Fiecare cuvânt citit va avea o singură structură corespunzătoare lui în tablou. La
final parcurgeţi tabloul şi afişati lista cuvintelor, iar pentru fiecare cuvânt afişaţi numărul de
apariţii şi lista indicilor la care a apărut.

27. Să se gestioneze un dicţionar printr-un tablou de structuri. O structură va contine cuvântul


şi explicaţia. Să se formeze dicţionarul prin citiri succesive de forma
28.
cuvant1 explicatie formata din mai multe cuvinte
cuvant2 explicatie cuv 2
.....

până la EOF. Apoi se va citi un cuvânt, se va căuta în dicţionar şi se va afişa. Dacă nu


există se va afişa cel mai apropiat cuvânt (are cele mai multe caractere comune succesive,
pornind de la începutul cuvantului) .

28. Se citesc coordonatele (reale) a două puncte din plan :P1(x1,y1), P2(x2,y2) ce determină un
dreptunghi (colţurile opuse). Se citesc coordonatele încă unui punct din plan P(x,y). Să se
afişeze dacă acesta este interior sau exterior dreptunghiului; se va folosi operatorul
conditional ?: Pentru a memora punctele se vor folosi structuri.

29. Se citeşte o listă de persoane cu următoarele date: Nume, Prenume, Sex, Inălţime.
Să se creeze o funcţie de citire care citeşte datele unei persoane, le pune într-o structură şi
returnează structura. Pentru Nume şi Prenume se va face alocare dinamică la citire.
Se va crea astfel un tablou de structuri (static sau dinamic, la alegere). Când se introduce o
persoană nouă, se verifică dacă nu a mai fost introdusă anterior.
Să se afişeze lista alfabetică a băieţilor, cu toate informaţiile, sub forma de tabel.
Să se afişeze lista fetelor, în ordinea descrescătoare a înălţimii.

30. Se citeşte o listă de persoane (cu următoarele date: Nume, Prenume, Sex, Adresa) pe rând,
până la CTRL+Z (tabloul va fi alocat dinamic). Se creează un nou tablou cu persoanele de
sex feminin, care se va parcurge şi se va cere pentru fiecare element numele după căsătorie,
iar apoi se afişează ambele tablouri. Se vor utiliza funcţii. Câmpul Adresa se va memora
de asemenea dinamic.

31. Se citesc de la tastatură cuvinte până la introducerea cuvântului "gata" (acesta din urmă
nu va fi prelucrat). Păstraţi cuvintele într-un tablou de şiruri alocate dinamic. Pentru fiecare
cuvânt găsiţi şi afişaţi perechea cea mai potrivită. Perechea cea mai potrivită este acel
cuvânt cu proprietatea că un număr maxim de litere din alfabet apare în ambele cuvinte. Nu

75
se face distincţie între literele mari şi literele mici. Spre exemplu în perechea de cuvinte
"SOARE" şi "cerebel" apar comune două litere din alfabet: 'e' şi 'r'.

32. Se citeşte de la tastatură un text încheiat cu caracterul '.'. Scrieţi un program care plasează
caracterele din textul citit într-o matrice pătratică, caracter cu caracter, în ordine pe linii de
sus în jos şi pe fiecare linie de la stânga la dreapta. Calculaţi dimensiunea minimă necesară
pentru matricea pătratică pentru a păstra toate caracterele din text şi alocaţi dinamic
matricea pătratică de dimensiunea respectivă. Dacă mai rămân poziţii neocupate în matrice,
ele vor fi iniţializate cu caracterul spaţiu. Afişaţi matricea obţinută.

33. Se citesc de la tastatură două şiruri de caractere. Să se insereze în primul şir, la o poziţie
specificată, al doilea şir. Să se extragă cuvintele din şirul obţinut, să se memoreze într-un
tablou, să se sorteze alfabetic şi apoi să se afişeze.
Pentru memorarea şirurilor se va folosi alocarea dinamică.
Inserarea celui de-al doilea şir în primul şir se va face cu ajutorul unei funcţii scrise de
programator.
Separatorii dintre cuvinte pot fi: spaţiu : ; , .
Cuvintele sunt şiruri de caractere formate din orice caracter diferit de separatori. Pentru
extragerea cuvintelor se poate folosi funcţia de bibliotecă strtok.

34. Un campionat de fotbal are înscrise 6 echipe. Realizaţi un program care preia de la tastatură
rezultatele meciurilor disputate şi generează un clasament actualizat după fiecare meci,
clasament care va fi afişat pe ecran. Citirea de la tastatură se face specificând numele celor
două adversare şi a scorului înregistrat.
Victoria aduce 3 puncte, meciul egal câte un punct iar meciul pierdut niciun punct.
Clasamentul va fi ordonat după numărul punctelor acumulate şi va conţine următoarele
date pentru fiecare echipă, păstrate într-un tip structurat: numele, punctajul, numărul de
victorii, numărul de meciuri remizate, numărul de înfrângeri, numărul de goluri înscrise şi
numărul de goluri primite.

35. Se citesc rânduri de text de maximum 100 de caractere, de la tastatură. Citirea se termină la
detectarea EOF (CTRL-Z de pe tastatură). Cuvintele de pe rândurile citite se presupun a fi
separate de unul din separatorii: spaţiu, virgulă, punct (puteti folosi functia strtok pentru
extragerea cuvintelor). Cuvintele din text se pun în trei şiruri diferite, alocate dinamic.
Cuvintele care conţin mai multe vocale decât consoane se pun într-un şir, cele care au
număr egal de consoane şi de vocale în altul şi cele care au mai multe consoane decât
vocale se pun în al treilea şir. În cele trei şiruri, cuvintele se separa între ele printr-un
spaţiu. Introducerea cuvintelor în listă se face astfel încât şirurile să fie ordonate (cuvintele
din fiecare şir să fie în ordine alfabetică). Spaţiul alocat fiecăruia dintre cele trei şiruri va fi
menţinut în permanenţă la minimum (folositi realloc()). La final afişaţi conţinutul
şirurilor şi numărul de cuvinte din fiecare. Dezalocaţi memoria.

36. Citiţi de la tastatură perechi de numere întregi pozitive nenule până la introducerea perechii
0,0. Stocaţi aceste informaţii într-un tablou cu elemente de tip structură, fiecare element
având trei câmpuri: nr1, nr2, rest. Numărul maxim de perechi introduse nu va
depăşi 100. Scrieţi trei funcţii, câte una pentru citire, afişare, prelucrări. Prelucrările
constau în calcularea restului împărţirii întregi a lui nr1 la nr2. Restul se va calcula prin
scăderi succesive (scădeţi din nr1 pe nr2 până cand nu se mai poate scădea. Rezultatul îl
reprezintă chiar restul împărţirii.). Nu se vor folosi variabile globale.

76
37. Scrieti un program calendar care generează zilele unui an astfel: un tablou cu 12 elemente
de tip structură, câte unul pentru fiecare lună. Structura luna are următoarele câmpuri: nr
de zile, nume lună, un pointer spre un tablou de caractere (sir de caractere fără terminatorul
de şir) Aceste tablouri vor fi alocate dinamic cu dimensiunea egală cu nr de zile al fiecărei
luni şi apoi se vor completa elementele cu literele L, M, M, J, V, S, D pentru zilele
săptămânii. Se vor afişa apoi toate zilele de joi care sunt în data de 17 a lunii şi ce zi a
săptămânii şi în ce lună este a o suta douăzeci şi una zi din an. La final se va dezaloca
memoria alocată dinamic. Crearea şi completarea informaţiilor pentru fiecare lună se va
face într-o funcţie ce va avea ca parametru tabloul cu elemente de tip structură. Numărul de
zile al fiecarei luni se va lua dintr-un tablou cu elemente constante, declarat în funcţia de
creare. La fel şi numele lunilor. Observaţie: anul nu este bisect. Nu folosiţi variabile
globale.

38. Se citesc de la tastatură linii conţinând informaţii despre consumul lunar al anumitor
resurse. Liniile au structura: nume_resursa cantitate nume_luna. Ultima linie
introdusă va conţine doar cuvântul gata. Construiţi în memorie un tablou de înregistrări în
care fiecare înregistrare conţine numele unei resurse şi cantitatea totală consumată din
resursa respectivă. Pe măsură ce se citesc liniile de la tastatură actualizaţi acest tablou,
astfel încât în final să puteţi afişa o situaţie privind consumul total al fiecărei resurse. Spre
exemplu dacă de la tastatură se introduce:

lemne 1000 noiembrie


gaz 3400 noiembrie
lemne 1120 decembrie
gaz 4090 decembrie
sare 320 decembrie
gata

veţi afişa pe ecran:


lemne 2120
gaz 7490
sare 320

Pentru a obţine punctaj maxim pe această problemă, se impune rezolvarea ei folosind


structuri. Pentru a memora numele resurselor veţi folosi cantitatea minimă de memorie
necesară. În finalul programului se va elibera toată memoria alocată dinamic.

77
4
Proiectarea şi dezvoltarea
sistematică a programelor de mari
dimensiuni

Marea majoritate a programelor care se elaborează în prezent sunt programe mari,


alcătuite din mii de linii sursă. La astfel de programe lucrează, de obicei, mai mulţi proiectanţi
şi programatori. Toate aceste aspecte fac din programare o activitate complexă care trebuie
abordată în mod sistematic, pe faze şi etape, cu tehnologii specifice fiecărei faze.
Aplicarea unei tehnologii adecvate trebuie să conducă la obţinerea unui program de
calitate, în timp scurt şi la un preţ cât mai mic. Tehnologiile pentru realizarea programelor sunt
astăzi elemente componente ale ingineriei programării şi detalierea lor nu face obiectul acestei
lucrări.
Un mediu integrat de dezvoltare a programelor oferă tehnici şi instrumente care
asistă programatorul în toate fazele elaborării programului: editare, codificare, compilare,
testare etc. Într-un astfel de caz se poate aprecia că o parte din tehnologia care urmează să fie
aplicată este intrinsecă mediului de dezvoltare.
Un alt element care concură la realizarea eficientă a unor programe de calitate este stilul
de programare.

4.1 Ce înţelegem printr-un bun stil de programare


Principalele elemente care evidenţiază stilul de programare sunt următoarele:
 aspectul general al programului;
 claritatea şi lizibilitatea programului;
 structurarea şi modularizarea programului în conformitate cu funcţiile şi operaţiile
care se implementează;
 robusteţea programului: calitatea sa de a continua chiar şi la apariţia unor erori;
 întreţinerea programului: uşurinţa cu care poate fi modificat şi îmbunătăţit ulterior.
Pentru a atinge aceste obiective programatorii au la dispoziţie mai multe pârghii prin
intermediul cărora pot acţiona atât la fazele de proiectare cât şi la cele de programare, testare
etc. Acestea sunt:
 Organizarea programului: definirea subprogramelor în conformitate cu princi-palele
activităţi şi operaţii desprinse din problema care urmează a fi rezolvată.

79
 Organizarea datelor: trebuie să concorde cu structura obiectelor din realitatea problemei
care se rezolvă.
 Comunicarea între subprograme: se recomandă să se realizeze exclusiv prin intermediul
parametrilor. Astfel se acordă subprogramului un mai mare grad de generalitate şi efectele
sale se limitează la textul subprogramului: sunt mai uşor de urmărit la citirea
programului. În cazul în care comunicarea între subprograme se face şi prin intemediul
variabilelor globale, apar aşa numitele efectele laterale care îngreunează urmărirea şi
înţelegerea programelor.
 Testarea prin program a unor categorii de erori, de exemplu cele de intrare/ ieşire. Unele
limbaje, inclusiv C, permit astfel de operaţii ceea ce determină îmbunătăţirea robusteşii
programelor.
 Alegerea numelor simbolice (identificatorilor) din program astfel încât să se facă o
legătură directă cu semnificaţia utilităţii respective în problema de rezolvat. În acest fel se
poate uşura urmărirea şi înţelegerea programului.
 Utilizarea comentariilor: reprezintă cea mai simplă metodă de documentare a
programelor, utilă chiar şi autorilor programului, dacă îl parcurg după un anumit timp. În
practica programării se utilizează linii de comentarii distincte prin care se descriu funcţiile
programului, funcţia fiecărui subprogram, datele de intrare şi cele de ieşire, indicaţii de
utilizare a programului etc. De asemenea, se obişnuieşte ca prelucrările mai importante sau
mai dificile din program să fie însoţite de comentarii explicative.
 Formatul liber de redactare al liniilor sursă. Această facilitate, prezentă în majoritatea
limbajelor de programare moderne, pune în concordanţă textul programului cu organizarea
şi semnificaţia sa. În acest fel creşte lizibilitatea şi claritatea programului.

4.2 Elaborarea programului prin metoda detalierii în paşi


succesivi
Cu cât problema este mai complexă, trecerea de la enunţul problemei la program se face
mai anevoios. În aceste situaţii este util ca atât elaborarea algoritmului cât şi scrierea
programului să se facă treptat, într-un proces de detaliere succesivă a rezolvării problemei.
Stilul de lucru corespunzător s-a concretizat într-o metodă de proiectare-programare
descendentă (top-down) numită metoda detalierilor în paşi succesivi (step wise refirement).
În această metodă se porneşte de la general, elaborând mai întâi programul principal
(pasul 1). Astfel se proiectează şi se descriu, în limbajul de implementare ales, principalele date
ale programului (cele de la nivelul programului principal), iar funcţiile apelate din programul
principal sunt doar enunţate: numele şi lista de parametrii formali. În continuare, prin detalieri
(rafinări) succesive (paşii 2, 3, …) se dezvoltă funcţiile apelate direct din programul principal
(pasul 2), apoi funcţiile apelate din acestea din urmă (pasul 3) ş.a.m.d., până la elaborarea în
amănunt a întregului program.
Caracteristica esenţială a acestui mod de rafinare a programului este următoarea: în fiecare
pas se dezvoltă funcţiile care au apărut (fiind doar enunţate) în pasul anterior; dacă operaţiile
implementate în pasul curent sunt suficient de complexe, este posibil să fie enunţate noi funcţii
care vor fi dezvoltate (detaliate) în pasul următor.

80
4.3 Exemplu
Programul 4.1: Să se scrie un program pentru realizarea unui top al melodiilor.
Persoanele care participă la alcătuirea topului se împart în 4 categorii, după sex şi vârstă (mai
tineri de 20 de ani şi peste 20 de ani). Fiecare persoană nominalizează, în ordine, 5 melodii
preferate, fiecare identificată prin titlu. Datele privind persoanele participante la realizarea
topului se citesc de pe mediul de intrare (de la tastatură). Datele pentru o persoană vor fi de
forma:
nume prenume sex(m sau f ) vârstă
melodie1
melodie2
melodie3
melodie4
melodie5

Să se afişeze:

1. Lista melodiilor în ordinea popularităţii lor


2. Lista câştigătorilor (cei care au ghicit melodiile câştigătoare), şi anume: 4 liste separate
cuprinzând numele şi prenumele persoanelor care au menţionat pe prima poziţie a
preferinţelor lor una din cele 3 melodii cele mai solicitate la categoria lor.

Pasul 1
La primul pas vom defini structura datelor „principale” şi funcţiile principale ale
programului. Pentru problema noastră, sunt specificate clar cele 2 funcţii principale, dar e
posibil ca la unele probleme să trebuiască să analizăm cerinţele cu atenţie pentru a desprinde
cerinţele de bază.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// datele despre o persoana


typedef struct pers{
char * nume; // numele persoanei
char * prenume; //prenumele
char s; // sexul, f sau m
int v; // varsta
char * melodii[5]; // tablou de 5 pointeri
// catre cele 5 melodii preferate preferinte
} persoana;

// datele despre o melodie


typedef struct m {
char * titlu; // titlul melodiei
int punctaj; // punctaj=cate persoane au votat-o
} melodie;

persoana * citire(void){
persoana *plista;
// citeste datele si le pune in lista

81
// va returna pointerul catre lista de persoane // creata
}

melodie * top_melodii(persoana *plista){


// creaza o lista cu melodiile si voturile aferente fiecarei
// melodii
// ordoneaza lista de melodii descrescator, dupa de numarul
// voturilor
// afiseaza lista de melodii
// returneaza un pointer la lista de melodii, pentru
// a fi folosit de alte functii
}

void castigatori(melodie *ptop, persoana *plista){


// afiseaza cele 4 liste cerute
}

void main() {
melodie *ptop;
persoana *plista;
lista=citire(); //citeste datele din fisier si le pune in lista
top=top_melodii(plista); //top un tablou ordonat al
//melodiilor
castigatori(ptop, plista); //afiseaza cele 4 liste cu
//castigatorii pe categorii
}

Sunt încă multe decizii de luat în pasul următor; încă nu am decis daca citirea se va face
de la tastatură sau din fişier, sau cum vom implementa cele 2 funcţii, top_melodii şi
castigatori.

Pasul 2
Modificările operate în acest pas vor fi evidenţiate prin îngroşare.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// datele despre o persoana


typedef struct pers{
char * nume; // numele persoanei
char * prenume; //prenumele
char s; // sexul, f sau m
int v; // varsta
char * melodii[5]; // un tablou de 5 pointeri catre cele 5
// melodii preferate preferinte
} persoana;

// datele despre o melodie


typedef struct m {
char * titlu; // titlul melodiei 
int punctaj; // punctaj=cate persoane au votat-o
} melodie;

82
persoana * citire(int *pi){
persoana * plista=NULL;
// citeste datele si le pune in lista
// va returna pointerul catre lista de persoane creata
char aux[30];
int i=0,j;
while (!feof(stdin)) { /* cat timp avem date la stdin (implicit
tastatura); trebuie tastat CTRL+z IMEDIAT
dupa ultima data introdusa (nu ENTER sau alt
spatiu alb) */
/* realocam memorie in heap pentru (i+1) structuri de tip
persoana, adica pentru cele i pe care le-am introdus pana acum +
1 pentru cea care urmeaza sa fie introdusa acum.*/
if (! (plista=(persoana *)realloc(plista,
(sizeof(persoana)*(i+1))) ) ){
// pentru plista==NULL, realloc are acelasi efect ca si malloc
printf("Memorie insuficienta \n");
exit(1);
}
scanf("%29s ", aux); /* citim in var. auxiliara aux numele
pers deoarece nu stim lungimea, deci nu
putem direct aloca memorie pt. nume */
/* alocam memorie pentru nume si adresa zonei alocate se depune
in pointerul lista[i].nume */
if (! (plista[i].nume = (char *) malloc(strlen(aux)+1) ) ) {
printf("Memorie insuficienta \n");
exit(1);
}
else
strcpy(plista[i].nume, aux); //copiem numele din aux
//in zona alocata
//alocam memorie in mod identic
//si pentru prenume in lista[i].prenume
scanf("%29s ", aux);
if (! (plista[i].prenume = (char *) malloc(strlen(aux)+1) ) ) {
printf("Memorie insuficienta \n");
exit(1);
}
else
strcpy(plista[i].prenume, aux);
// citim sexul si varsta si le punem la adresa(&) coresp.
scanf("%c %d", &(plista[i].s), &(plista[i].v));
// citim, pe rand, in aux, cate o melodie, alocam loc pt. ea
// si o copiem
for (j=0; j<NMEL; j++) {
scanf("%29s", aux);
if (! (plista[i].melodii[j] =
(char *) malloc(strlen(aux)+1))) {
printf("Memorie insuficienta \n");
exit(1);
}
strcpy(plista[i].melodii[j], aux);
}
i++;

83
}
// copiem numarul de persoane la adresa stocata in parametrul pi
*pi=i;
//returnam pointerul spre tabloul creat,
//pentru a putea fi folosit de alte functii
return plista;
}

melodie *creaza_lista_melodii(persoana *lista,int i){


// creaza o lista cu meodiile si voturile aferente fiecarei melodii
// si o returneaza
}

melodie *sorteaza_lista_melodii(melodie *ptop, int i){


// creaza o lista ordonata descrescator, functie de nr. voturilor,
// din lista de melodii primita ca parametru
// returneaza lista ordonata
}

afiseaza_top(melodie *ptop,i){
// afiseaza lista de melodii primita ca parametru
}

melodie * top_melodii(persoana *plista, int np){


melodie *ptop;
int nm;
// creaza o lista cu meodiile si voturile aferente fiecarei melodii
ptop=creaza_lista_melodii(plista, np, &nm);
// ordoneaza lista de melodii descrescator, dupa numarul voturilor
sorteaza_lista_melodii(ptop,nm);
// afiseaza lista de melodii
afiseaza_top(ptop,nm);
// returneaza un pointer la lista de melodii, pentru a fi folosit
// de alte functii
return ptop;
}

void afiseaza_castigatori(persoana *plista,int np,melodie * ptop,


char s, int v1, int v2){
// se parcurge lista de persoane plista si se afiseaza persoanele
// cu sexul s si varsta cuprinsa intre v1 si v2, adica
// plista[i].s==s si v1<=lista[i].v<=v2 (pseudocod)
int i;
printf("\n Lista castigatorilor de sex %c cu varsta cuprinsa
intre %d si %d",s,v1,v2);
for(i=0; i<np; i++)
if ((plista[i].s==s) && (plista[i].v>=v1) && (plista[i].v<=v2))
if ( !strcmp(plista[i].melodii[0],ptop[0].titlu) ||
!strcmp(plista[i].melodii[0],ptop[1].titlu) ||
!strcmp(plista[i].melodii[0],ptop[2].titlu) )
printf("\n%s %s %s", plista[i].nume, plista[i].prenume,
plista[i].melodii[0]);
}

84
void castigatori(melodie *ptop, persoana *plista, int i){
// afiseaza cele 4 liste cerute
// se apeleaza functia afiseaza_castigatori pentru cele 4 categorii
// dorite; avantajul este ca se pot modifica parametrii listelor
// fosrte usor, de exemplu vom putea imparti in 3 grupe de varsta
// fiecare sex doar apeland de 6 ori functia afiseaza_castigatori
afiseaza_castigatori(plista,i,ptop,'f',1,19);
afiseaza_castigatori(plista,i,ptop,'f', 20,150);
afiseaza_castigatori(plista,i,ptop,'m',1,19);
afiseaza_castigatori(plista,i,ptop,'m', 20,150);
/* o alta metoda consta in parcurgerea listei de persoane o singura
data, creand in acest timp cele 4 liste mai mici, pe care sa le
afisam ulterior*/
}

void afisare_test(persoana *plista, int n){


// afisarea de test este foarte importanta, intrucat stim ca pornim
// prelucrarile de la niste date corecte
int i,j;
for(i=0; i<n; i++){
printf("\n persoana %d",i);
printf("\n Nume: %s Prenume: %s s:%c v:%d \n",plista[i].nume,
plista[i].prenume, plista[i].s,plista[i].v);
for(j=0; j<5; j++)
printf("%s ",plista[i].melodii[j]);
}
}
/*de asemenea am putea reutiliza afisarea (intrucat este
parametrizata) pentru afisarea celor 4 liste, daca am alege sa
cream listele si apoi sa le afisam (vezi functia castigatori) */

void main() {
int i,c;
melodie *ptop;
persoana *plista=NULL; // lista persoanelor
lista=citire(&i); //citeste datele din fisier si le pune in lista
afisare_test(plista, i);
top=top_melodii(plista, i); //top un tablou ordonat al melodiilor
castigatori(ptop, plista, i); //afiseaza cele 4 liste cu
//castigatorii pe categorii
}

Pasul 3
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NMEL 5

// datele despre o persoana


typedef struct pers{
char * nume; // numele persoanei
char * prenume; //prenumele
char s; // sexul, f sau m
int v; // varsta

85
char * melodii[5]; // un tablou de 5 pointeri catre cele 5
// melodii preferate preferinte
} persoana;

// datele despre o melodie


typedef struct m {
char * titlu; // titlul melodiei
int punctaj; // punctaj=cate persoane au votat-o
} melodie;

persoana * citire(int *pi){


persoana * plista=NULL;
// citeste datele si le pune in lista
// va returna pointerul catre lista de persoane creata
char aux[30];
int i=0,j;
while (!feof(stdin)) { /* cat timp avem date la stdin (implicit
tastatura); trebuie tastat CTRL+z IMEDIAT
dupa ultima data introdusa (nu ENTER sau alt
spatiu alb) */
/* realocam memorie in heap pentru (i+1) structuri de tip
persoana, adica pentru cele i pe care le-am introdus pana acum +
1 pentru cea care urmeaza sa fie introdusa acum.*/
if (! (plista=(persoana *)realloc(plista,
(sizeof(persoana)*(i+1))) ) ){
// pentru plista==NULL, realloc are acelasi efect ca si malloc
printf("Memorie insuficienta \n");
exit(1);
}
scanf("%29s ", aux); /* citim in var. auxiliara aux numele
pers deoarece nu stim lungimea, deci nu
putem direct aloca memorie pt. nume */
/* alocam memorie pentru nume si adresa zonei alocate se depune
in pointerul lista[i].nume */
if (! (plista[i].nume = (char *) malloc(strlen(aux)+1) ) ) {
printf("Memorie insuficienta \n");
exit(1);
}
else
strcpy(plista[i].nume, aux); //copiem numele din aux
//in zona alocata
//alocam memorie in mod identic
//si pentru prenume in lista[i].prenume
scanf("%29s ", aux);
if (! (plista[i].prenume = (char *) malloc(strlen(aux)+1) ) ) {
printf("Memorie insuficienta \n");
exit(1);
}
else
strcpy(plista[i].prenume, aux);
// citim sexul si varsta si le punem la adresa(&) coresp.
scanf("%c %d", &(plista[i].s), &(plista[i].v));
// citim, pe rand, in aux, cate o melodie, alocam loc pt. ea
// si o copiem
for (j=0; j<NMEL; j++) {

86
scanf("%29s", aux);
if (! (plista[i].melodii[j] =
(char *) malloc(strlen(aux)+1))) {
printf("Memorie insuficienta \n");
exit(1);
}
strcpy(plista[i].melodii[j], aux);
}
i++;
}
// copiem numarul de persoane la adresa stocata in parametrul pi
*pi=i;
//returnam pointerul spre tabloul creat,
//pentru a putea fi folosit de alte functii
return plista;
}

int in_top(melodie *ptop, int n, char *melodie){


int k;
for(k=0; k<n; k++)
if ( strcmp(ptop[k].titlu,melodie)==0 ) return k;
// daca titlul melodiei de pe poz i este egal cu al melodiei
// parametru, returnam pozitia la care se gaseste acea
// melodie in tabloul ptab
return -1;
// daca s-a terminat for-ul inseamna ca nu s-a gasit respectiva
// meodie in ptab si deci se returneaza -1
}

melodie *creaza_lista_melodii(persoana *lista,int n, int *nm){


// creaza o lista cu meodiile si voturile aferente fiecarei melodii
// si o returneaza
melodie *ptop=NULL;
int i, j, k=0, poz=-1;
for(i=0; i<n; i++){
for(j=0; j<NMEL; j++)
if ( (poz=in_top(ptop, k, plista[i].melodii[j]))<0){
printf("\n mel %d ptp %d", (k+1)*sizeof(melodie),
sizeof(ptop)+sizeof(melodie));
if (! (ptop=realloc(ptop,(k+1)*sizeof(melodie)) ) ){
printf("Memorie insuficienta \n");
exit(1);
}
ptop[k].punctaj=1;
// varianta1:
//ptop[k].titlu=malloc( strlen( plista[i].melodii[j] )+1 );
//alocam memorie
//strcpy(ptop[k].titlu,plista[i].melodii[j]);
//copiem melodia
// varianta2:
ptop[k].titlu=plista[i].melodii[j];
//se copiaza adresa la care se afla titlul melodie (adresa
//care se gaseste in plista[i].melodie[j])
//ATENTIE!! daca se dezaloca tabloul plista nu mai stim ce
//se afla la adresa respectiva (ar putea fi o variabila
//locala i, de exemplu)
k++;

87
}
else ptop[poz].punctaj++;
}
*nm=k;
return ptop;
}

int comp(const void *a,const void*b){


return ((melodie*)b)->punctaj-((melodie*)a)->punctaj;
}

void sorteaza_lista_melodii(melodie *ptop, int i){


// creaza o lista ordonata descrescator, functie de nr. voturilor,
// din lista de melodii primita ca parametru
int c;
qsort((void *)ptop,i,sizeof(melodie),comp);
//dupa apelarea acestei functii ptop va pointa catre lista ordonata
}

void afiseaza_top(melodie *ptop, int n){


// afiseaza lista de melodii primita ca parametru
int i;
printf("Topul melodiilor");
for(i=0; i<n; i++){
printf("\n %d. %s - %dp ",i+1, ptop[i].titlu, ptop[i].punctaj);
}
}

melodie * top_melodii(persoana *plista, int np){


melodie *ptop;
int nm;
// creaza o lista cu meodiile si voturile aferente fiecarei melodii
ptop=creaza_lista_melodii(plista, np, &nm);
// ordoneaza lista de melodii descrescator, dupa numarul voturilor
sorteaza_lista_melodii(ptop,nm);
// afiseaza lista de melodii
afiseaza_top(ptop,nm);
// returneaza un pointer la lista de melodii, pentru a fi folosit
// de alte functii
return ptop;
}

void afiseaza_castigatori(persoana *plista,int np,melodie * ptop,


char s, int v1, int v2){
// se parcurge lista de persoane plista si se afiseaza persoanele
// cu sexul s si varsta cuprinsa intre v1 si v2, adica
// plista[i].s==s si v1<=lista[i].v<=v2 (pseudocod)
int i;
printf("\n Lista castigatorilor de sex %c cu varsta cuprinsa
intre %d si %d",s,v1,v2);
for(i=0; i<np; i++)
if ((plista[i].s==s) && (plista[i].v>=v1) && (plista[i].v<=v2))
if ( !strcmp(plista[i].melodii[0],ptop[0].titlu) ||
!strcmp(plista[i].melodii[0],ptop[1].titlu) ||
!strcmp(plista[i].melodii[0],ptop[2].titlu) )
printf("\n%s %s %s", plista[i].nume, plista[i].prenume,

88
plista[i].melodii[0]);
}

void castigatori(melodie *ptop, persoana *plista, int i){


// afiseaza cele 4 liste cerute
// se apeleaza functia afiseaza_castigatori pentru cele 4 categorii
// dorite; avantajul este ca se pot modifica parametrii listelor
// fosrte usor, de exemplu vom putea imparti in 3 grupe de varsta
// fiecare sex doar apeland de 6 ori functia afiseaza_castigatori
afiseaza_castigatori(plista,i,ptop,'f',1,19);
afiseaza_castigatori(plista,i,ptop,'f', 20,150);
afiseaza_castigatori(plista,i,ptop,'m',1,19);
afiseaza_castigatori(plista,i,ptop,'m', 20,150);
/* o alta metoda consta in parcurgerea listei de persoane o singura
data, creand in acest timp cele 4 liste mai mici, pe care sa le
afisam ulterior*/
}

void afisare_test(persoana *plista, int n){


// afisarea de test este foarte importanta, intrucat stim ca pornim
// prelucrarile de la niste date corecte
int i,j;
for(i=0; i<n; i++){
printf("\n persoana %d",i);
printf("\n Nume: %s Prenume: %s s:%c v:%d \n ",plista[i].nume,
plista[i].prenume, plista[i].s,plista[i].v);
for(j=0; j<5; j++)
printf("%s ",plista[i].melodii[j]);
}
}
/*de asemenea am putea reutiliza afisarea (intrucat este
parametrizata) pentru afisarea celor 4 liste, daca am alege sa
cream listele si apoi sa le afisam (vezi functia castigatori) */

void main() {
int i,c;
melodie *ptop;
persoana *plista=NULL; // lista persoanelor
lista=citire(&i); //citeste datele din fisier si le pune in lista
afisare_test(plista, i);
top=top_melodii(plista, i); //top un tablou ordonat al melodiilor
castigatori(ptop, plista, i); //afiseaza cele 4 liste cu
//castigatorii pe categorii
}

4.4 Întrebări şi probleme


1. Care sunt principalele elemente ce evidenţiază stilul de programare?
2. Ce se înţelege printr-un bun stil de programare?
3. Ce modalitate de comunicare între funcţii este de preferat din punctul de vedere al unui
bun stil de programare ? De ce ?
4. Care este utilitatea comentariilor ?

89
5. Prin ce se caracterizează elaborarea programelor prin rafinări succesive ?

Să se aplice metoda de elaborare a programelor prin detaliere în paşi succesivi pentru


rezolvarea următoarelor probleme:

6. Să se scrie un program C format din trei proceduri: citeşte, calculează şi scrie, care
rezolvă expresia matriceală E = A × B + CT – D, unde A, B, C, D se citesc cu procedura
citeste apelată de patru ori, E se află cu procedura calculeaza şi se afişează cu procedura
scrie. Toate procedurile auxiliare vor fi declarate în interiorul celor trei proceduri principale.
Toate procedurile vor acţiona numai asupra unor date locale lor, cu excepţia dimensiunii
matricelor N × N, care este globală.

7. Să se determine numărul de apariţii a cuvintelor dintr-un text dart. Textul se dă de la


tastatură şi se termină cu o linie vidă. În final, se vor afişa cuvintele detectate, ordonate
alfabetic, împreună cu numărul de apariţii. Se consideră că un cuvânt este format din orice
şir de caractere tipăribile, cu excepţia spaţiilor, a sfârşitului de linie, a tab-ului, virgulei şi a
punctului (care sunt separatori).

8. Despre un student se reţin următoarele informaţii:


 nume;
 media;
 data naşterii: an, lună, zi;
 număr livret dacă sex = M, respectiv nume anterior dacă sex = F.

Într-o grupă există maxim 30 de studenţi.


Să se scrie un program care să realizeze următoarele operaţii:
a) citeşte datele despre studenţi (mai întâi numărul de studenţi din grupă şi apoi datele
despre fiecare);
b) afişarea studentului cu media maximă;
c) afişarea celui mai bătrân student;
d) afişarea datelor complete ale studenţilor pentru care se citeşte numele (numele trebuie să
coincidă cu numele studentului sau numele anterior);
e) afişarea tuturor studenţilor în ordinea alfabetică a numerelor.

9. Să se implementeze un program tip „agenda” care conţine informaţii despre persoane fizice
şi firme, memorate pe câte un „card”. Pentru o persoană fizică se va memora numele,
numărul de telefon şi data naşterii, iar pentru firme se va memora numele, numărul de
telefon şi codul fiscal.
a) adăugare card;
b) căutare card după nume (persoană sau firmă);
c) afişarea tuturor persoanelor care îşi sărbătoresc ziua într-o anumită lună;
d) ieşire program.

90
10. Să se realizeze un program C ce ţine evidenţa CD-urilor unei biblioteci multimedia.
Pentru fiecare CD din evidenţă se vor memora următoarele informaţii:

denumire – şir de caractere de lungime 20


preţ_achiz – întreg
tip_cd – caracter; poate avea una din valorile:
’A’ - CD audio,
’D’- CD-ROM

11. Se consideră un tablou cu elemente de tip struct cu următoarea structură:


- nume: string[25];
- nr_aparitii: integer;
Să se scrie un program care gestionează conţinutul tabloului astfel:
 citeşte câte un cuvânt de la tastatură;
 caută cuvântul citit în intrările ocupate ale tabloului;
 dacă se găseşte o astfel de intrare se incrementează valoare câmpului nr_aparitii şi se
deplasează întreaga intrare, astfel încât tabloul să fie permanent ordonat după valorile
descrescătoare ale câmpului nr_aparitii;
 dacă în tablou nu se găseşte cuvântul citit, atunci acesta se plasează în prima intrare
liberă, punând nr_aparitii pe 1;
 dacă cuvântul citit este “end” se afişează conţinutul întregului tablou şi se încheie
execuţia programului.

12. Se generează aleator o mulţime de numere întregi de cardinalitate maxim 100, cu valori
cuprinse între 1 şi 10000. Să se afle suma, media, minimul şi maximul acestei mulţimi.
Numărul de elemente va fi generat şi el aleator.

13. X si Zero
Scrieţi un program C care să implementeze binecunoscutul joc "X si Zero".
Programul trebuie să permită ca doi jucători umani să joace unul împotriva
celuilalt.
Tabla de joc va arăta în felul următor (va fi desenată în mod text):

X 0

La pornirea programului se va afişa tabla fără nici o piesă pe ea.


Primul jucator care mută este întotdeauna jucatorul "X".
Programul va citi de la tastatură linia şi coloana unde doreşte să mute jucătorul care este
la rând. În caz că valorile introduse nu sunt corecte (fie sunt in afara tablei, fie căsuţa

91
respectivă este deja ocupată) se va afişa un mesaj de eroare şi se va repeta citirea pentru acelaşi
jucător.
Dacă valorile sunt corecte, se va pune o piesă la poziţia indicată şi se va trece la celălalt
jucător.
În momentul în care unul din jucători a câştigat, programul va anunţa acest lucru printr-
un mesaj pe ecran.
În timpul rulării, programul trebuie sa dea lase o impresie plăcută. Tabla de joc trebuie
să apară mereu în aceeaşi poziţie pe ecran, eventualele mesaje de eroare să nu rămână vizibile
de la o mutare la alta, etc.
Sugestie pentru rezolvare.
Se recomandă abordarea programului în următorii paşi:
1) definirea unui tip de date în care să se poată memora configuraţia pieselor pe tabla de joc şi
jucătorul care este la rând la mutare;
2) definirea unei/unor funcţii pentru desenarea tablei de joc;
3) definirea unei funcţii care verifică dacă o mutare introdusă de utilizator este corectă;
4) definirea unei funcţii care verifică dacă există un câştigător pe o anumită configuraţie a
tablei de joc;
5) în programul principal bucla care citeşte mutările jucătorilor, le verifică, desenează tabla de
joc, anunţă câştigătorul atunci când e cazul, etc.

92
5
Elemente de programare avansată în
limbajul C

5.1 Funcţii de conversie


Bibliotecile C standard furnizează implementări ale unor operaţii utilizate frecvent în
programele C.

Clasificări şi conversii de caractere

În <ctype.h> este definit un set de funcţii (de fapt macrodefiniţii) care testează natura
unui caracter şi fac diverse conversii ale caracterelor.

Există următoarele funcţii care clasifică un caracter c:

isalpha(c) - returnează adevărat (valoare nenulă) dacă caracterul c este o literă, respectiv
fals (zero) în caz contrar.

isupper(c) - returnează adevărat dacă c este o literă mare

islower(c) - returnează adevărat dacă c este o literă mică

isdigit(c) - returnează adevărat dacă c este o cifră

isalnum(c) - returnează adevărat dacă c este literă sau cifră

isspace(c) - returnează adevărat dacă c este un caracter de spaţiere (spaţiu, tab, linie
nouă).

Există şi următoarele funcţii de transformare a caracterelor:

toupper(c) - transformă caracterul c din literă mică în literă mare

tolower(c) - transformă caracterul c din literă mare în literă mică

93
Funcţii de transformare a şirurilor

Un grup de funcţii de conversii ale şirurilor în valori numerice se găseşte în


<stdlib.h>. Conversia se face de la un şir de caractere la o valoare al cărei tip este specificat
pentru fiecare funcţie.

Următoarele funcţii realizează conversia unui şir de caractere la real, întreg, respectiv
long:

double atof(char *s);


int atoi(char *s);
long atol(char *s);

Aceste funcţii presupun că şirul de caractere dat ca şi parametru reprezintă o constantă


numerică în formatul corespunzător. Dacă şirul nu poate fi convertit, funcţiile returnează valoarea
0.

Există şi funcţii care realizează conversia inversă, din format numeric în şir de caractere.

char * itoa(int val, char *s, int baza);


char * ltoa(long val, char *s, int baza);

Aceste funcţii produc în şirul s o reprezentare în baza specificată a valorii val.

În <stdio.h> există funcţii de transfer cu format din şiruri de caractere:

int sscanf(char *s, char *format, adresa1, adresa2, ...);


int sprintf(char *s, char *format, arg1, arg2, ...);

Aceste funcţii sunt similare funcţiilor scanf şi printf, dar transferul de date se face
dintr-un şir de caractere şi nu de la intrarea sau ieşirea standard.

De exemplu, fie următoarea secvenţă:

char s[80]="23.5 abc 3";

char sir[10];
int n;
float x;

sscanf(s, "%f %s %d", &x, sir, &n);


sprintf(s, "2*%f=%f",x, 2*x);

În urma execuţiei secvenţei anterioare, variabilele x, sir şi n au valorile: x=23.5


sir=abc şi n=3. Şirul de caractere s va fi în final ”2*23.5=47”

94
5.2 Manipularea datelor la nivel de biţi
5.2.1 Operatori la nivel de bit
În C există şi categoria operatorilor pe biţi. Specific acestora este faptul că îşi interpretează
operanzii ca şiruri de biţi, fiecare bit fiind tratat independent de ceilalţi. Operanzii pot fi de orice
tip întreg. Tabelul următor prezintă operatorii pe biţi:

Operator Denumire Utilizare

~ negare ~e

& şi e1 & e2

^ sau exclusiv e1 ^ e2

| sau e1 | e2

>> deplasare dreapta e1 >> n

<< deplasare stânga e1 << n

Operatorul de negaţie inversează valorile biţilor operandului: fiecare bit de 1 devine 0 şi


fiecare bit de 0 devine 1. De exemplu, fie declaraţia
unsigned char x=0x97;

x 1 0 0 1 0 1 1 1

~x 0 1 1 0 1 0 0 0

Valoarea expresiei ~x este deci 0x68.

Operatorii de deplasare >> şi << realizează o deplasare, la dreapta sau la stânga, a biţilor
expresiei e1 care este primul operand cu un număr de poziţii egal cu valoarea n a operandului al
doilea.

În cazul deplasării la dreapta, biţii cei mai puţin semnificativi se pierd. În faţă se inserează
biţi 0 dacă operandul e1 este fără semn, sau în cazul operanzilor cu semn se multiplică bitul de
semn. O deplasare la dreapta cu o poziţie este echivalentă cu o împărţire la 2.

În cazul deplasării la stânga, se pierd cei mai semnificativi biţi iar în partea stângă se
completează cu biţi 0. O deplasare la stânga cu o poziţie este echivalentă cu o înmulţire cu 2.

95
Dacă variabila x declarată anterior se deplasează la dreapta, respectiv la stânga cu un
număr de poziţii, rezultatele sunt:

x >> 2 0 0 1 0 0 1 0 1

x << 1 0 0 1 0 1 1 1 0

Operatorii şi (&), sau (|) şi sau exclusiv(^) pe biţi prelucrează perechile de biţi ai celor doi
operanzi pe care îi primesc. Au o semnificaţie diferită de operatorii logici && şi ||, care tratează
valorile operanzilor în totalitatea lor ca valori logice.

Operatorul şi pe biţi (&) calculează fiecare bit al rezultatului prin operaţia şi pe perechea de
biţi corespunzătoare din operanzi: bitul rezultat este 1 dacă ambii biţi sunt 1, şi este 0 în caz
contrar.

Operatorul sau pe biţi (|) calculează fiecare bit al rezultatului prin operaţia sau pe
perechea de biţi corespunzătoare din operanzi: bitul rezultat este 1 dacă cel puţin unul din biţi este
1, şi este 0 dacă ambii sunt 0.

Operatorul sau exclusiv pe biţi (^) calculează fiecare bit al rezultatului prin operaţia sau
exclusiv pe perechea de biţi corespunzătoare din operanzi: bitul rezultat este 1 dacă exact unul din
biţi este 1.

Se consideră următoarele declaraţii:

unsigned char x=0x97;


unsigned char y=0x34

Expresiile x&y, x|y, x^y sunt următoarele:

x 1 0 0 1 0 1 1 1

y 0 0 1 1 0 1 0 0

x&y 0 0 0 1 0 1 0 0

x | y 1 0 1 1 0 1 1 1

x ^y 1 0 1 0 0 0 1 1

Toţi operatorii pe biţi pot fi utilizaţi în operatori combinaţi de atribuire: &=, ^=, |=, >>=,
<<=.

96
Variabilele utilizate ca şi şiruri de biţi (vectori de biţi) pot servi la reprezentarea unor
informaţii de stare de tip da/nu pentru o mulţime de elemente. Fiecare bit este asociat cu un anumit
element şi valoarea sa (0 sau 1) codifică starea elementului respectiv.

Programul 5.1: Să se implementeze un tip de date abstract mulţime de litere. Se va utiliza


un vector de biţi în care fiecare poziţie este asociată unei litere, cu semnificaţia că dacă bitul
respectiv este pe 1, litera este prezentă în mulţime, respectiv dacă bitul este 0 litera nu este
prezentă în mulţime.

Deoarece sunt 27 de litere, este necesar ca vectorul de biţi să fie de cel puţin 4 octeţi. Se
poate deci reprezenta pe o variabilă de tip unsigned long:

typedef unsigned long multime;

Fiecare literă va avea asociat un bit. Convenim ca litera \u{a}' să fie asociată bitului cel mai
puţin semnificativ, urmând apoi în ordine alfabetică celelalte litere.

Adăugarea unei litere c la mulţime presupune setarea bitului corespunzător pe 1. Această


operaţie se poate face printr-un sau pe biţi dintre vechea mulţime şi o mască ce conţine un bit de 1
pe poziţia literei. Masca se obţine deplasând constanta lungă 1 (conţine un singur bit de 1 pe
poziţia cea mai puţin semnificativă) cu c-\u{a}' poziţii spre stânga.

multime adauga(multime m, char c) {


return m | (1L<<(c-\u{a}'));
}

Scoaterea unei litere din mulţime presupune resetarea bitului corespunzător la 0. Această
operaţie se poate face printr-un şi pe biţi între vechea mulţime şi o mască ce conţine toţi biţii pe 1,
mai puţin cel de pe poziţia literei. Această mască se obţine prin negarea binară a măştii utilizate
anterior.

multime scoate(multime m, char c) {


return m & ~(1L<<(c-\u{a}'));
}

Testul dacă o anumită literă este prezentă în mulţime implică verificarea valorii bitului
corespunzător.

long prezent(multime m, char c) {


return m & (1L<<(c-\u{a}'));
}

97
5.2.2 Tabelul precedenţei operatorilor
Următorul tabel prezintă ordinea precedenţei tuturor operatorilor limbajului C (tab. 5.1).
Există 15 nivele diferite de precedenţă, prioritatea maximă revine operatorilor de pe nivelul 1.

Nivel Operatori Asociativitate


1 () (apel funcţie) stânga
[] (indexare)
. -> (op. de selecţie)
2 + - (op. unari de schimbare de semn) dreapta
++ --
~ ! (negaţie bit cu bit, resp. logică)
(tip) , sizeof
* (adresare indirectă), & (adresa)
3 * / % (op. binari multiplicativi) stânga
4 + - (op. binari aditivi) stânga
5 >> << (op. deplasare) stânga
6 < > <= >= (op. comparaţie) stânga
7 == != stânga
8 & (şi pe biţi) stânga
9 ^ stânga
10 | stânga
11 && (şi logic) stânga
12 || stânga
13 ?: dreapta
14 = dreapta
+= -= *= /= %=
&= ^= |= >>= <<=
15 , stânga

Tab. 5.1 Prioritatea operatorilor în C

98
5.3 Argumente în linia de comandă

Declaraţia generală a unei funcţii main este:

int main(int argc, char **argv);

Funcţia main are un tip (int) şi poate avea parametri. Atât rezultatul funcţiei main cât
şi parametrii ei interesează în condiţiile în care se utilizează programul ca şi o comandă lansată
din linia de comandă a sistemului de operare sau când programul este lansat în execuţie de
un alt program.

Valoarea întreagă returnată de main este disponibilă pentru programul care a lansat în
execuţie programul curent. Se obişnuieşte ca valoarea returnată de un program să fie 0 în caz de
terminare corectă şi o valoare nenulă în caz contrar. Pentru ieşirea forţată din program în caz de
erori grave se recomandă funcţia exit cu parametrul 1. Spre deosebire de o simplă instrucţiune
return 1; această funcţie realizează în plus şi alte operaţii, precum închiderea tuturor fişierelor
deschise de program.
Funcţia main are doi parametri, cu următoarele semnificaţii: primul parametru, argc
(argument corect), este un întreg având semnificaţia de număr al parametrilor daţi în linia de
comandă, iar al doilea parametru, argv (argument vector), este un vector de şiruri de caractere,
reprezentând parametrii din linia de comandă.

Prin convenţie, argv[0] este numele prin care programul a fost apelat.

argv[arcc] este NULL (Sfârşitul argumentului)

Programul 5.2: Program care îşi afişează parametrii primiţi în linia de comandă.

Fie următorul program, salvat şi compilat în fişierul cu numele prog:

#include <stdio.h>
int main(int argc, char **argv) {
int i;
printf(“numele programului:%s\n”,argv[0]);
if(argc==1) printf(“nu are parametri\n”);
else
for (i=1; i<argc; i++)
printf("parametrul %d: %s \n", i, argv[i]);
return 0; /*codul returnat de program*/
}

99
Lansarea în execuţie a programului prin linia de comandă:

prog arg1 arg2 arg3

produce ieşirea:

numele programului: prog


parametrul 1: arg1
parametrul 2: arg2
parametrul 3: arg3

Observaţie:
Redirectările de intrare şi ieşire specificate în linia de comandă nu se consideră parametri în
linia de comandă.

5.4 Pointeri la funcţii


Din punct de vedere al conţinutului zonei de memorie indicate, există următoarele categorii
de pointeri:

 pointeri de date, care conţin adresa unei variabile


 pointeri generici, pointeri void *, pot conţine adresa unui obiect oarecare, de orice tip.
Se utilizează atunci când nu se cunoaşte precis tipul de date care va fi instanţiat în mod
dinamic la execuţie.
 pointeri de funcţii, care conţin adresa codului executabil al unei funcţii. În C, o funcţie nu
este o variabilă, dar este posibil a se defini pointeri la funcţii care pot fi atribuiţi, plasaţi în
tablouri, transmişi ca parametri altor funcţii, etc.

Exemple schematice de utilizare:


 Declaraţia de funcţie: tip_rez f(....);
 Declaraţia de pointer la funcţie: tip_rez(*pf)(....);
 Atribuiri echivalente: pf=f; pf=&f;
 Apeluri echivalente: pf(....); pf)(....);

 Observaţii:
int *f(void); //o funcţie ce returnează un pointer la int
int (*f)(void); //pointer la o funcţie ce returnează un int

Exemplu: un tablou de pointeri de funcţii


typedef void (*pf)(void);
void f1(void); void f2(void); /*.....*/ void
f10(void);
pf ftab[10]={f1, f2, ...,f10};
... int k; ... ftab[k]; ...

100
Programul 5.3: Tabelarea unei funcţii.
 În C, unica manieră de transmitere a unui parametru, este prin valoare. Ea permite însă
programatorului să realizeze efectul transmiterii prin adresă şi al transmiterii ca argumente
a altor funcţii. Ultima posibilitate se realizează transmiţând ca parametru pointerul la
funcţie.
 În exemplul de mai sus funcţia tab, tabelează valorile oricărei funcţii reale de un
argument întreg, între două limite precizate. Funcţia, împreună cu limitele se transmit ca
parametri.

void tab ( double(* f )(int ), int j,int i){


for(; j<=i; j++)
printf(“%d%f\n“,j,(*f)(j));
}

double f1(int x){


return 2*3.14*x;
}

double fact(int x){


double f=1; int i;
for(i=1; i<=x; i ++)
f*=i;
return f ;
}

-----------------------------
tab ( f1, -10, 10 ) ;
tab ( fact, 0, 10 ) ;
-----------------------------

Programul 5.4: Se citesc de pe mediul de intrare date(numele şi vârsta) despre un număr


oarecare de personae şi se doreşte generarea listei persoanelor, ordonată întâi în ordine alfabetică
şi apoi lista ordonată în ordinea crescătoare a vârstei.

Se defineşte tipul structurat persoana:

typedef struct {
char * nume;
int varsta;
} persoana;

Citirea datelor se face într-o funcţie citire, declarată în felul următor:

void citeste (int * n, persoana ** tab)

101
Funcţia citeşte date despre n persoane şi construieşte un tablou alocat dinamic. Parametrii
funcţiei sunt pointeri pentru a forţa transferul lor prin referinţă: n este numărul de persoane şi se
citeste în funcţie, iar tab este tabloul în care sunt memorate cele n persoane. Tabloul tab se
alocă dinamic în cadrul funcţiei de citire, deci se modifică însăşi adresa de început a tabloului, de
aceea se pune parametru pointer la tablou.

void citeste (int * n, persoana ** tab) {


persoana * t;
char sir[41];
int i;

printf("Introduceti numarul de persoane\n");


scanf("%d", n);

/* aloca spatiu pentru n structuri de tip persoana */


if (!(t=(persoana *) malloc ((*n) * sizeof(persoana)))) {
printf("Eroare alocare dinamica memorie \n");
exit(1);
}

/* atribuie spatiul alocat tabloului de persoane */


*tab=t;

/* citeste datele despre fiecare persoana */


for (i=0; i<*n; i++, t++) {
printf("\n nume: ");
scanf("%40s", sir);
/* aloca memorie pentru nume */
if (!(t->nume=(char *)malloc (strlen(sir)+1))) {
printf("Eroare alocare dinamica memorie \n");
exit(1);
}
strcpy(t->nume, sir);
printf("\n varsta: ");
scanf("%d", &t->varsta);
}
}

Pentru cele 2 sortări cerute ale listei de persoane, ceea ce diferă este criteriul de sortare. O
sortare se descompune în 3 tipuri de operaţii :
- o comparaţie care determină ordinea relativa a 2 elemente
- o interschimbare a unei perechi de elemente
- un algoritm care efectuează operaţii de comparare şi interschimbare până când elementele
sunt în ordinea dorită.

102
Algoritmul de sortare este independent de operaţiile de comparare şi interschimbare.
Utilizând funcţii diferite de comparare şi interschimbare, se pot realiza sortări dupa diverse criterii
(numeric, alfabetic) sau a diferite tipuri de date.

Pentru compararea a 2 persoane în funcţie de diferite criterii, se vor utiliza funcţii separate
ce primesc ca şi parametri 2 persoane p1 şi p2, şi returnează o valoare întreagă pozitivă dacă
p1 > p2, o valoare întreagă negativă dacă p1< p2, respectiv zero dacă între cele 2 persoane
nu se poate face o ordonare pe baza criteriului de comparaţie dat.

typedef int (*fct_cmp) (persoana, persoana);

void sortare (persoana *sir, int n, fct_cmp f) {


int i,j;
persoana temp;

for (i=0; i<n-1; i++)


for (j=i+1; j<n; j++)
if ((*f)(sir[i], sir[j])<0) {
temp=sir[i];
sir[i]=sir[j];
sir[j]=temp;
}
}

int fc1(persoana p1, persoana p2) {


return strcmp(p1.nume, p2.nume);
}

int fc2(persoana p1, persoana p2) {


return p1.varsta-p2.varsta;
}

void main(void) {
int n;
persoana *tabel=NULL;
citeste(&n, &tabel);
afiseaza(n, tabel);
sortare(tabel, n, fc1);
printf("\n Lista in ordine alfabetica:");
afiseaza(n, tabel);
sortare(tabel, n, fc2);
printf("\n Lista in ordinea varstei:");
afiseaza(n, tabel);
}

103
Se observă că s-a transmis criteriul de comparare ca parametru de tip funcţie către
algoritmul de sortare. În primul rând se declară fct_cmp ca pointer cu tipul ”funcţie cu rezultatul
de tip întreg şi 2 parametri de tip persoana”. Funcţia sortare are un parametru f de acest tip
fct_cmp. Funcţia f este apelată în interiorul corpului funcţiei sortare, în forma
(*f)(sir[i], sir[j]). La apelul funcţiei sortare, locul acestui parametru formal f este
luat de funcţiile fc1 şi respectiv fc2.

5.5 Preprocesorul
Preprocesarea este o fază care precede compilarea. Preprocesorul limbajului C este relativ
simplu şi în principiu execută substituţii de texte. Prin intermediul lui se realizează:

 Includeri de fişiere sursă (antet)


 Definiţii şi apeluri de macrodefiniţii
 Compilare condiţionată

Directivele de preprocesare au caracterul # la început de linie. Prezentarea acestor directive


a fost preluată din [5].

5.5.1 Fişiere antet


O directivă #include este înlocuită în timpul preprocesării cu conţinutul integral al
fişierului specificat, care se inserează în locul ei.

Forma directivei include este

#include "nume_de_fisier" sau

#include <nume_de_fisier>

Diferenţa între cele două moduri de scriere este următoarea: dacă numele fişierului de
inclus este dat între ghilimele, preprocesorul caută fişierul pornind de la directorul curent, cel în
care se află programul. Dacă fişierul nu e găsit, sau dacă numele este dat între paranteze
unghiulare, căutarea fişierului se face în continuare după reguli definite de implementarea
compilatorului de C. În funcţie de compilatorul utilizat, se pot specifica prin opţiuni de configurare
care sunt directoarele unde se face căutarea.

 Un fişier inclus poate conţine la rândul său directive #include.

 Utilizarea directivei #include e o modalitate de a utiliza aceleaşi declaraţii în cazul


programelor care sunt împărţite pe mai multe fişiere. Dacă se modifică un fişier inclus,
trebuie recompilate toate fişierele care îl includ pe acesta direct sau indirect.

104
 Un fişier antet poate conţine: definiţii de tipuri, declaraţii de funcţii, declaraţii de
variabile, definiţii de constante, macrodefiniţii, directive de includere
 Un fişier antet bine proiectat nu trebuie să conţină definiţii de funcţii şi definiţii de
date! În principiu un fişier antet poate fi inclus de către mai multe module diferite ale
aceleiaşi aplicaţii. În cazul în care acest modul antet conţine o definiţie de funcţie sau o
definiţie de variabilă globală, prin includerea multiplă se realizează definirea multiplă a
acestor entităţi, fapt care constituie o eroare.

5.5.2 Macrodefiniţii
Definiţia constantelor simbolice prin directiva #define e un caz particular de
macrodefiniţie. O macrodefiniţie (un macro) are la bază operaţia de substituţie. Un macro are o
definiţie şi poate fi apelat de un număr oarecare de ori. La definiţia unui macro se specifică de fapt
textul care urmează a se substitui la fiecare apel al său. Acest text poate fi variabil, în funcţie de
anumiţi parametri.

Forma generală a unei macrodefiniţii este:

#define nume(p1, p2, ,pn) tex_de_substituit


nume = numele macroului
p1, p2, , pn = parametrii macroului
text = textul cu care este înlocuit macroul la fiecare apel

În cazul unei macrodefiniţii cu parametri, între numele macroului şi paranteza deschisă


trebuie să nu existe spaţii, altfel macroul se va considera fără parametri şi parantezele împreună cu
parametrii vor fi considerate ca făcând parte din textul de substituţie. Între paranteza închisă şi
textul de substituţie trebuie să existe cel puţin un caracter spaţiu. Textul de substituţie poate
conţine parametrii p1, p2, ..., pn. Apelul unui macro constă din numele lui, urmat de lista valorilor
parametrilor între paranteze. Parametrii de la apel se substituie în locul parametrilor formali în
textul de substituţie al macroului apelat şi apoi textul rezultat se substituie apelului.

Programul 5.5: Un macro pentru calcului maximului a două numere poate fi definit ca mai
jos:

#define MAX(x,y) ((x)>(y) ? (x) : (y))

Un exemplu simplu de apel al acestui macro este:

float a,b,c;
c=MAX(a,b);

La preprocesare linia c=MAX(a,b); se înlocuieste cu

c=((a) > (b) ? (a) : (b));

105
Un alt exemplu de apel al macro-ului MAX:

int i,j,k;
k=MAX(i+j, i-j);

La preprocesare linia k=MAX(i+j, i-j); va fi înlocuită cu

k=(i+j) > (i-j) ? (i+j) (i-j))

Avantajele unui macro faţă de o funcţie care să implementeze acelaşi lucru sunt
următoarele: În primul rând,se evită apelurile ineficiente din punct de vedere al regiei pentru
funcţiile simple care se reduc la una sau două instrucţiuni. În aceste cazuri este recomandabil să nu
se mai construiască funcţii care să fie apelate, ci instrucţiunile respective să fie scrise direct în
locurile unde sunt necesare. Macrourile tratate de preprocesorul limbajului C realizează generări in
line a funcţiilor. În al doilea rând, macrourile pot fi mai generale decât funcţiile, nedepinzând de
tipul parametrilor. În cazul macroului de determinare a maximului, acesta e general şi nu depinde
de tipul parametrilor, care pot fi întregi sau reali.

Există şi anumite pericole legate de utilizarea necorespunzătoare a macrodefiniţiilor, care


pot avea efecte secundare ascunse. De exemplu, dacă se apelează macroul de determinare a
maximului cu următorii parametri:

MAX(i++, j++)

acesta va incrementa de două ori valoarea variabilei care e mai mare, pentru că expandarea
macro-ului se face în felul următor:

(i++) > (j++) ? (i++) : (j++)

Un alt exemplu unde apare această eroare este macroul de ridicare a unui număr la pătrat

#define PATRAT(x) x*x

Acesta dă rezultate eronate în cazul în care este apelat de exemplu pentru x+1:
PATRAT(x+1) este expandat în x+1*x+1, ceea ce, având în vedere precedenţa operatorilor, nu
calculează pătratul expresiei (x+1). Pentru a fi corect un macro de ridicare la pătrat ar trebui scris
utilizând paranteze:

#define PATRAT(x) (x)*(x)

106
Programul 5.6: Câteva exemple de macrodefiniţii.

Macro pentru transformarea unui caracter din literă mică în literă mare:

#define UPPER(c) ((c)-\u{a}'+\U{A}')

Macro pentru definirea unui ciclu infinit:

#define FOREVER for(;;)

Macro pentru interschimbarea a două numere

#define SCHIMBA(X,Y) {
int t;
t=X;
X=Y;
Y=t;
}

Apelul SCHIMBA(a,b) este substituit cu secvenţa:

{ int t;t=a; a=b; b=t; }

Variabila t există numai în interiorul instrucţiunii compuse generată de preprocesor!

O facilitate interesantă a macrodefiniţiilor este că permit parametrizarea unei operaţii cu un


nume de tip, ceea ce se poate folosi ca o facilitate de realizare a unor funcţii generice primitive.
Macro-ul de interschimbare a două elemente poate fi rescris astfel încât să fie parametrizat şi cu
tipul elementelor de interschimbat:

#define SWAP(TIP, X, Y) {\
TIP t;\
t=X;\
X=Y;\
Y=t;
}

Macrodefiniţia SWAP se poate apela cu parametri de orice tip pe care este definită operaţia =.

int a, b;

SWAP(int, a, b);
float x, y;
SWAP(float, x, y);

107
Macro pentru alocarea dinamică a unui bloc de memorie de n elemente de un anumit tip:

#define ALOCA(tip, n) (tip *)malloc(sizeof(tip)*n)

void main(void) {
int *ip;
float *fp;
ip=ALOCA(int, 10);
fp=ALOCA(float, 10);
}

Macroexpandarea poate fi oricând suspendată cu directiva undef:

#undef nume

La întâlnirea acestei directive, macroul cu numele respectiv nu mai este expandat în


continuarea fişierului.

În mod normal, preprocesorul nu substituie parametrii formali în interiorul şirurilor de


caractere între ghilimele. Însă dacă în textul de înlocuire se plasează caracterul # înaintea unui
nume de parametru formal, această combinaţie se expandează într-un şir între ghilimele, conţinând
argumentul respectiv.

#define AFISEAZA(expr) printf(#expr "=%g\n", expr)

AFISEAZA(x/y);

Se expandează în

printf("x/y""=%g\n",x/y);

5.5.3 Compilare condiţionată


Compilarea condiţionată permite să se aleagă dintr-un text general părţile care se
compilează împreună.
Compilarea condiţionată se realizează folosind construcţiile #if, #ifdef şi #ifndef

În cele ce urmează, se notează cu expr o expresie constantă (valoarea ei poate fi evaluată


de preprocesor la întâlnirea ei).

108
#if expr
text
#endif

Dacă expr are valoarea adevărat (o valoare diferită de zero), atunci text se supune
preprocesării, altfel se continuă cu ceea ce urmează după #endif.

#if expr
text1
#else
text2
#endif

În interiorul unei directive #if, expresia defined(nume) are valoarea 1 dacă nume a
fost deja definit de o directivă define, sau zero în caz contrar.

De exemplu, pentru a asigura faptul că se include o singură dată conţinutul unui fişier antet
antet1.h, conţinutul fisierului se poate plasa într-o construcţie condiţională de forma:

#if !defined(ANTET1)

#define ANTET1

... /* continutul fisierului antet */

#endif

Prima includere a fişierului antet1.h defineşte numele ANTET1. În cazul în care ar mai
exista includeri ulterioare, chiar şi indirect, prin alte fişiere, preprocesorul vede că numele e deja
definit şi sare direct la #endif. Această facilitate este utilă în situaţii în care un fişier antet ar fi
altfel inclus de mai multe ori, direct sau indirect, ca în următoarea situaţie de exemplu: Se
consideră un fişier antet a.h şi un fişier antet b.h care conţine o directivă de includere a lui a.h.
Dacă un alt fişier main.c conţine directive de includere a ambelor fişiere a.h şi b.h, rezultă că
fişierul a.h va fi inclus de două ori. Această includere dublă poate conduce la erori datorită
redefinirii unor variabile şi constante conţinute în a.h.

Există şi formele #ifdef şi #ifndef, forme specializate ale directivei #if, care
testează dacă un nume este sau nu este definit.

#ifndef(ANTET1)
#define ANTET1

/* continutul fisierului antet */


#endif

109
O altă formă de exploatare a facilităţii de compilare condiţionată este de includere a unor
fişiere diferite în funcţie de anumite variabile sistem:

#if SYSTEM==SYSV
#define ANTET "sysv.h"
#elif SYSTEM==BSD
#define ANTET "bsd.h"
#elif SYSTEM==MSDOS
#define ANTET "msdos.h"
#else
#define ANTET "default.h"
#endif
#include ANTET

Un exemplu în care e necesară facilitatea de includere condiţionată sunt definirea de tipuri


mutual recursive: se consideră două tipuri structurate T1 şi T2, fiecare dintre acestea conţine un
câmp de tip pointer la celălalt tip.

typedef struct {
T2 a;
int b;} *T1;

typedef struct {
T1 a;
int b;} *T2;

În acest caz, se pune problema ordinii corecte în care trebuie să fie definite aceste tipuri.
Dacă T1 e definit înaintea lui T2, compilatorul nu îl cunoaşte pe T2 când întâlneşte definiţia lui
T1, care are un câmp (a) de acest tip. Problema există şi invers, dacă T2 ar fi definit înaintea lui
T1. Pentru a rezolva problema referirilor reciproce, se utilizează declaraţii incomplete de structuri:

typedef struct t_T1 * T1;


typedef struct t_T2 *T2;

struct t_T1 {
T2 a;
int b;
};
struct t_T2 {
T1 a;
int b;
};

110
În cazul în care cele 2 tipuri, T1 şi T2, sunt definite în fişiere separate t1.h şi t2.h:

/* fisierul t1.h */
#if !defined(tip_T1)
#define tip_T1
typedef struct t_T1 *T1;
#include "t2.h"
struct t_T1 {
T2 a;
int b;
};
#endif

/* fisierul t2.h */
#if !defined(tip_T2)
#define tip_T2
typedef struct t_T2 *T2;
#include "t1.h"
struct t_T2 {
T1 a;
int b;
};
#endif

/* fisierul main.c */
#include "t1.h"
#include "t2.h"

void main() {

5.6 Exerciţii şi probleme


1. Se dau următoarele definiţii:

unsigned int a=3, b=7;

Să se precizeze valorile următoarelor expresii: a&b, a&&b, a^b, a|b, a||b, ~a,
a>>1&b, ~0>>4, ~!a, !~a.

111
2. Să se scrie o funcţie care primeşte ca şi parametru un şir de caractere reprezentând un
număr binar şi returnează valoarea numărului. Pentru construirea valorii numărului se vor
utiliza operaţii pe biţi.

3. Să se scrie o funcţie care primeşte un număr întreg fără semn ca parametru şi afişează
reprezentarea binară a numărului. Se vor utiliza operaţii pe biţi.

4. Să se afişeze un tabel cu primele 16 puteri ale lui 2 (20, 21, ...215). Se vor utiliza operaţii pe
biţi pentru calculul puterilor lui 2.

5. Se citesc două numere întregi, x şi n, unde n are valori între 0 şi 15. Se cere să se afişeze:

- bitul n din numărul x

- numărul x în care se inversează bitul n

6. Se citeşte valoarea unui întreg lung. Se cere să se afişeze valoarea fiecărui octet din
reprezentarea sa.

7. Se citeşte un număr întreg x. Să se afişeze numărul obţinut prin schimbarea între ei a


octeţilor lui x.

8. Se citesc numerele întregi x, n, p. Se cere să se afişeze:


- numărul format din cei n biţi începând cu poziţia p
- numărul obţinut prin setarea celor n biţi începând cu poziţia p
- numărul obţinut prin ştergerea celor n biţi începând cu poziţia p
- numărul obţinut prin inversarea celor n biţi începând cu poziţia p

9. Să se scrie o macrodefiniţie cu 2 parametri MAXIM(a, b) pentru determinarea maximului


dintre valorile parametrilor săi a şi b.

10. Să se scrie o funcţie int maxim(int a, int b) pentru determinarea maximului


dintre 2 întregi.

11. Comparaţi macrodefiniţia MAXIM cu funcţia maxim. Să se apeleze ambele în următoarele


secvenţe, pentru care se consideră declaraţiile:

int x=2, y=3, z=4;


char c1='b', c2=\c{s}';
1. calculează maximul dintre x şi y
2. calculează maximul dintre x+y şi x-y
3. tipăreşte x, tipăreşte y; calculează maximul dintre x++ şi y++; tipăreşte x, tipăreşte y
4. calculează maximul dintre c1 şi c2

112
12. Să se definească un macro pentru calculul valorii absolute care să fie apoi apelat într-un
program pentru evaluarea expresiilor:

i=(|a|+|b|)/(1+|a*b|), unde a,b sunt numere intregi

z=(|x+y|-|x-y|)/(1+|x+y|*|x-y|), unde x, y, z sunt numere reale

13. Să se definească două tipuri structurate T1 şi T2 mutual recursive (T1 conţine un câmp de
tip pointer la o structură de tip T2 iar T2 conţine un câmp care este pointer la o structură de tip
T1.) Pe lângă aceste câmpuri, fiecare din cele 2 tipuri structurate mai conţin alte câmpuri de tip
int şi char *.

14. Să se definească cele 2 tipuri în 2 fişiere separate, t1.h şi t2.h. Să se scrie un program
care include t1.h şi t2.h şi defineşte variabilele v1 şi v2 de tipurile T1 şi T2.

113
6
Fişiere

Toate programele scrise până acum au fost interactive, primindu-şi datele de intrare de la
tastatură, iar afişarea rezultatelor s-a făcut întotdeauna pe ecran. De multe ori însă, când volumul
datelor de intrare este mare sau formatul lor complicat, este de dorit ca acestea să fie editate
înainte de rularea programului şi salvate într-un fişier de unde să fie preluate de program în timpul
execuţiei. În acest fel, nu mai este nevoie ca datele să fie reintroduse de fiecare dată când se
execută programul. De asemenea, rezultatele calculate de un program pot să fie scrise într-un
fişier. În acest fel, rezultatele unui program pot fi imediat utilizate ca date de intrare pentru alt
program.

În general, prin fişier se înţelege o colecţie ordonată de înregistrări care sunt păstrate pe
diferite suporturi externe (în general discuri). Un fişier are o înregistrare care marchează sfârşitul
de fişier. Sfârşitul de fişier poate fi referit prin constanta simbolică EOF (End Of File).

Prelucrarea fişierelor prin program implică de obicei o secvenţă de operaţii specifice. Orice
fişier trebuie deschis înainte de a putea fi prelucrat. Prelucrările care se fac cel mai frecvent asupra
fişierelor sunt consultarea ( citirea înregistrărilor din fişier) şi crearea sau actualizarea fişierului
prin scrierea de noi înregistrări în fişier. La sfârşitul prelucrărilor, fişierul trebuie închis. Toate
aceste operaţii se realizează prin funcţii din biblioteca standard a limbajului C.

Prelucrarea fişierelor în limbajul C se poate face la două nivele: nivelul inferior de


prelucrare a fişierelor, care utilizează direct funcţii ale sistemului de operare, şi nivelul superior de
prelucrare care conţine funcţii specializate specifice limbajului C. În acest capitol vor fi tratate
doar funcţiile de prelucrare a fişierelor din această ultimă categorie. Aceste funcţii oferă mai multe
facilităţi decât funcţiile de nivel inferior şi sunt mai uşor de utilizat.

În funcţie de modul de interpretare a datelor, fişierele pot fi prelucrate ca fişiere text sau ca şi
fişiere binare.

115
6.1 Fişiere text
6.1.1 Redirectarea intrării şi ieşirii standard
Datele introduse de la un terminal se consideră că formează fişierul standard de intrare. În
mod analog, datele care se afişează pe terminal se consideră că formează fişierul standard de
ieşire. În cazul fişierelor de intrare de la tastatură, sfârşitul de fişier se generează prin <CTRL>Z.

Intrarea şi ieşirea standard pot fi redirectate. În felul acesta, toate funcţiile de intrare-ieşire
din biblioteca stdio (scanf, printf, putchar, getchar, etc.) pot fi folosite pentru a
realiza operaţii de intrare-ieşire cu alte periferice decât terminalul standard. Se poate redirecta
ieşirea standard, în loc de afişare pe ecran, spre un fişier de pe disc.

De exemplu, dacă avem un program în fişierul executabil prog.exe, acesta poate fi


lansat în execuţie din linia de comandă, cu redirectarea ieşirii în fisierul fis_out în felul
următor:

prog > fis_out

Redirectarea intrării din fişierul fis_in se face:

prog < fis_in

Redirectarea ieşirii unui program prog1 ca intrare pentru un alt program prog2:

prog1|prog2

Programul 6.1: Redirectarea intrării şi ieşirii standard. Programul copiaza.c realizează


copierea, caracter cu caracter, a intrării standard la ieşirea standard. Programul poate fi folosit
pentru realizarea unei copii a unui fişier f1, dacă intrarea standard a programului e redirectată către
acesta, iar ieşirea standard către fişierul copie care se crează.

/* copiaza.c */
#include <stdio.h>

void main() {
int c;
while ((c=getchar())!=EOF)
putchar(c);
}

În linia de comandă, se va da comanda:

116
copiaza <f1 >copie

Rezultatul va fi crearea fişierului cu numele copie, având acelaşi conţinut ca şi fişierul f1.

6.1.2 Deschiderea unui fişier


Redirectarea intrării şi ieşirii standard, descrisă în paragraful anterior, este o metodă simplă
de a face un program să lucreze cu fişiere. De multe ori însă este nevoie ca un program să lucreze
simultan atât cu intrarea şi cu ieşirea standard cât şi cu mai multe fişiere din care citeşte şi în care
scrie scrie date. În acest caz, lucrul cu fişiere trebuie specificat în program.

La nivelul utilizatorului şi al sistemului de operare, un fişier este identificat prin numele


său. La nivelul interfeţei de programare C, un fişier este referit printr-o structură de tip FILE cu
informaţiile necesare pentru accesul la fişier. Pentru a putea manipula un fişier prin program,
trebuie să se realizeze mai întâi operaţia de deschidere a fişierului, operaţie care asociază numelui
fişierului o structură de tip FILE şi îl pregăteşte pentru prelucrări ulterioare.

Pentru deschiderea unui fişier se utilizează funcţia fopen:

FILE * fopen(char * nume, char * mod);

Parametrii acestei funcţii sunt:

nume = un şir de caractere care reprezintă numele fişierului. Numele poate include şi
specificarea căii.

mod = un şir de caractere care defineşte modul de prelucrare a fişierului după deschidere.
Posibilele valori ale acestui parametru sunt: ”r” ”w” ”a” ”r+”.

Modul ”r” (read) înseamnă deschiderea fişierului cu numele specificat pentru citire. Dacă
se încearcă deschiderea în modul ”r” a unui fişier inexistent, funcţia fopen va returna pointerul
NULL.

Modurile ”w” (write) şi ”a” (append) înseamnă deschiderea fişierului pentru scriere. Dacă
se deschide un fişier inexistent cu modul ”w” sau ”a” atunci acesta este creat. Dacă se deschide un
fişier existent cu modul ”w” conţinutul vechi al fişierului se pierde şi fişierul este rescris, în timp
ce cu modul ”a” se scrie în continuarea vechiului conţinut.

Pentru fişiere binare, se adaugă caracterul ”b” la specificarea modului de prelucrare: ”rb”
”wb” ”ab” ”r+b”.

Întotdeauna trebuie să se testeze dacă rezultatul operaţiei de deschidere este un pointer


nenul. Dacă rezultatul este pointerul NULL înseamnă că deschiderea fişierului nu a reuşit şi nu pot
fi efectuate prelucrări ulterioare asupra sa.

117
De exemplu, dacă se doreşte deschiderea fişierului ”date.txt” pentru citire, se poate utiliza o
secvenţă ca şi cea de mai jos:

FILE *fs;
...
if ((fs=fopen("date.txt", "r"))==NULL) {
printf("nu se poate deschide fisierul !\n");
exit(1);
}

Există variabilele predefinite stdin, stdout, stderr care sunt pointeri spre tipul FILE
şi permit ca funcţiile de nivel superior de prelucrare a fişierelor să poată trata intrarea standard,
ieşirea standard şi ieşirea standard pentru erori la fel ca şi restul fişierelor. Singura deosebire e că
programatorul nu trebuie să deschidă sau să închidă aceste fişiere, ele fiind deschise automat la
lansarea în execuţie a programului şi închise la apelul funcţiei exit la terminarea programului.

6.1.3 Închiderea unui fişier


După terminarea prelucrării unui fişier acesta trebuie închis. Se recomandă ca
programatorul să închidă orice fişier de îndată ce s-a terminat prelucrarea lui, deoarece numărul de
fişiere ce poate fi deschis simultan e limitat de sistemul de operare.

int fclose(FILE * pf);

Parametrul acestei funcţii, FILE * pf este un pointer către structura FILE care a fost
obţinut la deschiderea fişierului. Funcţia returnează valoarea 0 dacă operaţia a decurs normal sau -
1 dacă operaţia nu a reuşit.

6.1.4 Prelucrarea pe caractere a unui fişier


Cele mai simple funcţii de prelucrare a fişierelor sunt cele pe caractere, getc şi putc.

int getc(FILE * pf);


int putc(int c, FILE * pf);

Funcţiile primesc ca şi parametru un pointer pf la un fişier deschis în modul


corespunzător: pf trebuie să fi fost deschis pentru citire (în modul ”r”) pentru getc, respectiv
pentru scriere (în modul ”w” sau ”a”) pentru putc.

Funcţia getc returnează următorul caracter din fişierul indicat ca şi parametru. În caz de
eroare sau când ajunge la sfârşitul fişierului returnează valoarea EOF.

Funcţia putc scrie caracterul c în fişierul indicat de parametrul pf şi returnează


caracterul scris sau EOF în caz de eroare.

118
Funcţiile cunoscute getchar şi putchar pentru citirea unui caracter de la tastatură,
respectiv pentru afişarea unui caracter, sunt de fapt apeluri ale acestor funcţii mai generale:
getchar este echivalent cu getc(stdin) iar putchar(c) este echivalent cu
putc(c, stdout). Getchar şi putchar sunt realizate ca simple macrodefiniţii pornind de
la getc şi putc.

Programul 6.2: Un program care crează un fişier copie caracter cu caracter a unui alt fişier dat.
Programul copiază fişierul cu numele nume_sursa, creaînd fişierul cu numele nume_dest.

Se declară doi pointeri de tip FILE, fs şi fd, pentru prelucrarea acestor două fişiere.
Fişierul sursă fs este deschis în mod ”r” pentru citire, iar fişierul copie fd este deschis în modul
”w” pentru scriere. Dacă a existat anterior un alt fişier cu acelaşi nume nume_dest acesta va fi
rescris. Pentru fiecare operaţie de deschidere de fişier se verifică faptul că operaţia a reuşit
(pointerul la FILE returnat este diferit de NULL).

#include <stdio.h>
#include <stdlib.h>

void main() {
int c;
char nume_sursa[80], nume_dest[80];
FILE *fs,*fd;

printf("Introduceti numele fisierului sursa \n");


gets(nume_sursa);
printf("Introduceti numele fisierului copie\n");
gets(nume_dest);

if ((fs=fopen(nume_sursa, "r"))==NULL) {
printf("Nu se poate deschide fisierul %s \n",
nume_sursa);
exit(1);
}

if ((fd=fopen(nume_dest, "w"))==NULL) {
printf("Nu se poate deschide fisierul %s \n",
nume_dest);
exit(1);
}

while ((c=getc(fs))!=EOF)
putc(c, fd);

fclose(fs);
fclose(fd);
}

119
Copierea se face citind câte un caracter din fişierul fs, atâta timp cât acesta este diferit de
marcajul de sfârşit de fişier, EOF. Caracterul este imediat scris în fişierul fd. La sfârşit se închid
ambele fişiere.

6.1.5 Aplicaţii de prelucrare a textelor


Numeroase aplicaţii necesită o procesare a textelor. Exemplul următor ilustrează o manieră
simplă de analiză a unui text.

Programul 6.3: Să se scrie un program care citeşte un text conţinut într-un fişier şi afişează
fiecare cuvănt întâlnit pe o nouă linie pe ecran. Se consideră că două cuvinte pot fi despărţite
printr-un număr oarecare de separatori. Separatorii sunt caracterele spaţiu, tab şi linie nouă. Orice
altă grupare de caractere se consideră a fi un cuvânt.

Se definesc funcţiile separator şi incuvant care testează dacă un caracter dat este
separator sau poate face parte din cuvinte.

Principiul algoritmului de găsire a cuvintelor, realizat de funcţia cuvinte, este următorul:

atata timp cat nu s-a terminat fisierul


* elimina separatorii
* colecteaza caracterele unui cuvant

Programul este dat în continuare:

#include <stdio.h>
#include <stdlib.h>

#define MAXLIT 80

int separator(char c) {
/* testeaza daca c este separator */
if ((c==' ')||(c=='\t')||(c=='\n')) return 1;
return 0;
}

int incuvant(char c) {
/* testeaza daca c poate apare in interiorul unui cuvant */

return !separator(c);
}

120
void cuvinte(FILE * fp){
char s[MAXLIT], c;
int l;
c=getc(fp);
while (c!=EOF) {
while (separator(c)) {
/* elimina separatorii */
c=getc(fp);
}
if (c=EOF)
break;
/* incepe un cuvant */
l=0; /* initializeaza contorul literelor */
while ((c!=EOF)&&incuvant(c)) {
s[l++]=c; /* depune litera */
c=getc(fp);
}
s[l]='\0'; /* depune terminatorul de sir */
printf("Cuvantul: %s \n", s);
}
}

void main(void) {
char numefis[80];
FILE *fp;
printf("Introduceti numele fisierului \n");
scanf("%80s", numefis);
if ((fp=fopen(numefis, "r"))==NULL) {
printf("Eroare deschidere fisier ! \n");
exit(1);
}
cuvinte(fp);
fclose(fp);
}

Funcţia cuvinte primeşte ca parametru un pointer la structura FILE asociată fişierului


care va fi prelucrat şi care a fost deschis pentru citire. Fişierul este citit caracter cu caracter. Întâi
se elimină separatorii, apoi se colectează caracterele care formează un cuvânt. Primul caracter
diferit de un separator, care a determinat ieşirea din ciclul while (separator(c)), poate fi
primul caracter parte din cuvânt, sau poate fi EOF dacă s-a ajuns la sfârşit de fişier. Dacă s-a ajuns
la sfârşit de cuvânt, instrucţiunea break determină ieşirea din ciclul while deci se termină
funcţia cuvinte. Altfel, caracterul c este primul caracter al cuvântului care urmează şi se
iniţializează l, contorul literelor cuvântului curent. Atâta timp cât c este un caracter care nu este
separator şi nu s-a ajuns la sfârşit de fişier, se depune c în tamponul colector s. Când s-a ajuns la
sfârşitul cuvântului, se adaugă la poziţia curentă din s şi terminatorul de şir \0. Procedeul
continuă atâta timp cât nu s-a ajuns la sfârşit de fişier.

121
6.1.6 Operaţiile de intrare-ieşire cu format
Asupra fişierelor se pot realiza operaţii de intrare-ieşire cu format utilizând funcţiile
fscanf şi fprintf. Aceste funcţii sunt similare funcţiilor scanf şi printf, având în plus un
parametru de tip pointer la fişier:

int fscanf (FILE *fis, char *format , adresa1, adresa2, ...);

int fprintf (FILE *fis, char *format, arg1, arg2, ...);

Pentru funcţia fscanf, primul parametru este un pointer la un fişier care a fost deschis
pentru citire, iar pentru funcţia fprintf primul parametru este un pointer la un fişier care a fost
deschis pentru scriere.

Programul 6.4: Secvenţă care ilustrează ieşirea cu format într-un fişier:

#include <stdio.h>

void main(void) {
FILE * fp;
char nume_fis[15]="fis.txt";
int i=5;
char c='z';
float x=7.56;

if ((fp=fopen(nume_fis, "w"))==NULL) {
printf("Nu se poate deschide fisierul %s \n", nume_fis)
;
exit(1);
}

fprintf(fp, "%d %c %f \n", i, c,x);

fclose(fp);
}

Ca urmare a execuţiei programului, se crează fişierul cu numele fis.txt având


conţinutul:

5 z 7.56

122
6.1.7 Intrări-ieşiri de şiruri de caractere
Pentru intrări şi ieşiri de şiruri de caractere se pot utiliza funcţiile fgets şi fputs,
similare funcţiilor cunoscute gets şi puts.

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

Funcţia fgets citeşte maxim n-1 caractere sau până la '\n' inclusiv, şi le depune în s,
adaugă la sfârşit terminatorul '\0' şi returnează adresa şirului. La eroare întoarce valoarea NULL.

int fputs(char *s, FILE *fis);

Funcţia fputs scrie şirul în fişier, fără a scrie şi caracterul '\0'. La eroare întoarece
EOF.

6.1.8 Testul de sfârşit de fişier


Pentru a testa dacă parcurgerea unui fişier a ajuns la sfârşitul acestuia se poate utiliza
funcţia feof:

int feof(FILE * fp);

Funcţia returnează o valoare nenulă (adevărat) dacă s-a atins sfârşitul de fişier.

Programul 6.5: Dintr-un fişier text se citesc rezultatele la examen ale unei grupe de
studenţi. Fişierul este organizat pe linii de forma:

nume prenume nota

Se cere să se afişeze câti studenţi au note de 9 şi 10, câţi au nota între 6 şi 8, câţi au 5 şi câţi au
note sub 5.

Deoarece nu se cunosc de dinainte numărul de studenţi din fişier, prelucrarea (citirea)


datelor se face atâta timp cât nu s-a ajuns la sfârşitul fişierului de date de intrare, ca în programul
de mai jos.

#include <stdio.h>
#include <stdlib.h>

void main(void) {
FILE * pf;
char numefis[100];
char nume[80],pren[80];

123
int nota;
int n1=0, n2=0, n3=0, n4=0;

printf("Introd numele fisierului");


gets(numefis);

pf=fopen(numefis, "r");
if (pf==NULL) {
printf("Nu pot deschide fisierul %s \n", numefis);
exit(1);
}

while (!feof(pf)) {
fscanf(pf, "%s %s %d", nume, pren, &nota);
if ((nota==10)||(nota==9)) n1++;
else if ((nota >=6)&&(nota <=8)) n2++;
else if (nota ==5) n3++;
else n4++;
}

fclose(pf);

printf("%d %d %d %d", n1, n2, n3, n4);


}

6.2 Prelucrarea fişierelor binare


Într-un fişier text, octeţii sunt consideraţi coduri de caractere. Scrierea unei valori numerice
într-un fişier text presupune o operaţie de conversie a numărului în şirul de caractere care
reprezintă numărul conform formatului de ieşire ales. De exemplu, dacă fp este un pointer la un
fişier text deschis pentru scriere, secvenţa următoare determină scrierea în fişier a textului
”25640”.

int i=25640;
...
fprintf(fp, "%d", i);

Valoarea întregului i este reprezentată în fişierul text printr-un şir de caractere de lungime 5,
ocupând 5*1 octeţi. Se observă că pentru scrierea valorii numerice în fişierul de tip text se
consumă mai mult spaţiu decât pentru reprezentarea în formatul intern în memorie (unde variabila
de tip int ocupă doar 2 octeţi). De asemenea, pentru scrierea şi citirea valorilor numerice cu
format se fac operaţii de conversie între formatul intern de reprezentare a numerelor şi şiruri de

124
caractere. Rezultă că fişierele de tip text nu sunt adecvate pentru a stoca pe disc baze de date
eficiente.

În fişierele organizate ca date binare, octeţii nu sunt consideraţi ca fiind coduri de


caractere. Se consideră că o înregistrare este o colecţie de date structurate numite articole. La o
citire se transferă într-o zonă tampon un număr de articole care au lungime fixă. Analog, la scriere
se transferă din zona tampon un număr de articole de lungime fixă.

6.2.1 Funcţii pentru citirea şi scrierea în fişiere binare


În C, fişierele binare pot fi prelucrate folosind funcţiile fread şi fwrite.

unsigned fread(void *ptr, unsigned dim, unsigned nrart,


FILE *pf);

Funcţia fread citeşte dintr-un fişier un număr de articole de aceeaşi dimensiune şi le


stochează într-o zonă de memorie alocată pentru aceasta. Parametrii funcţiei sunt:

ptr = pointerul spre zona tampon ce va conţine articolele citite

dim = lungimea unui articol

nrart = numărul articolelor care se transferă

pf = pointer spre structura FILE care defineşte fişierul din care se face citirea. Acest fişier
trebuie să fi fost deschis în modul ”rb”.

unsigned fwrite(void *ptr, unsigned dim, unsigned nrart,


FILE *pf);

Parametrii funcţiei fwrite au semnificaţii analoage cu parametrii funcţiei fread.


Fişierul pf trebuie să fi fost deschis pentru scriere în mod binar, în modurile ”wb” sau ”ab”.

Funcţiile returnează numărul articolelor transferate sau -1 în caz de eroare. Numărul de


octeţi transferaţi este dim*nrart.

Dacă în fişierul binar se transferă valorile unor variabile, locaţiile de memorie


corespunzătoare sunt tratate ca o zonă tampon de scriere:

int x=5;
double y=9.3;
...
fwrite(&x, sizeof(int), 1, fp);
fwrite(&y, sizeof(double), 1, fp);

125
Următorul program ilustrează modul de manipulare al fişierelor binare.

Programul 6.6: Copierea unui fişier.

Este mai eficient să se transfere odată blocuri de dimensiuni mai mari decât un singur
caracter, ca în programul următor în care se defineşte MAX numărul maxim de octeţi transferaţi
odată.

#include <stdio.h>
#include <stdlib.h>

#define MAX 500

void main() {
int c;
char nume_sursa[80], nume_dest[80];
FILE *fs,*fd;
char buf[MAX];
int dim;

printf("Introduceti numele fisierului care se copiaza \n");

gets(nume_sursa);
printf("Introduceti numele fisierului copie \n");
gets(nume_dest);

if ((fs=fopen(nume_sursa, "rb"))==NULL) {
printf("Nu pot deschide fisierul %s \n", nume_sursa);
exit(1);
}

if ((fd=fopen(nume_dest, "wb"))==NULL) {
printf("Nu pot deschide fisierul %s \n", nume_dest);
exit(1);
}

while (!feof(fs)) {
dim=fread(buf, 1, MAX, fs);
fwrite(buf, 1, dim, fd);
}

fclose(fs);
fclose(fd);
}

126
Se alocă o zonă de memorie buf capabilă să conţină MAX octeţi. Operaţia de citire din
fişierul sursă este realizată de instrucţiunea

dim=fread(buf, 1, MAX, fs);

Aceasta transferă din fişierul fs cel mult MAX articole de 1 octet înspre zona buf. În general,
operaţia de citire va transfera exact MAX octeţi, mai puţin în ultima iteraţie a ciclului când este
posibil ca numărul octeţilor care au mai rămas de transferat să fie mai mic. Numărul exact de
octeţi ciţiţi este returnat de funcţia fread în variabila dim. Funcţia fwrite va scrie primii dim
octeţi din zona buf în fişierul destinaţie.

6.2.2 Poziţionarea într-un fişier


Operaţiile de citire şi de scriere într-un fişier se execută secvenţial. La fiecare apel al
funcţiilor de citire se citeşte înregistrarea curentă din fişier, iar la scriere se scrie întotdeauna la
poziţia curentă. Dacă se doreşte accesul aleator, se poate utiliza funcţia de poziţionare în fişier,
fseek.

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


Parametrii funcţiei au următoarele semnificaţii:

pf = pointer către structura FILE care defineşte fişierul în care se face poziţionarea
deplas = diferenţa în octeţi între poziţia de referinţă faţă de care se face deplasarea, indicată
de origine, şi noua poziţie care se doreşte a fi obţinută.
origine = un cod reprezentând poziţia de referinţa faţa de care se face deplasarea. Această
poziţie de referinţa poate fi: începutul fişierului, codificat cu 0; poziţia curentă,
cofificată cu 1; sfârşitul fişierului, codificat cu 2.

Funcţia fseek returnează valoarea 0 dacă poziţionarea a fost corectă şi o valoare nenulă
în caz contrar.

De exemplu, următoarea instrucţiune poziţionează capul de citire/scriere la începutul


fişierului:

fseek(pf, 0, 0);

Pentru aflarea poziţiei curente în fişier se poate utiliza funcţia ftell:

long ftell(FILE *fp);

Funcţia returnează o valoare reprezentând deplasamentul, în octeţi, a poziţiei curente a


capului de citire/scriere faţă de începutul fişierului.

127
6.3 Exerciţii şi probleme
1. Să se scrie un program care codifică un fişier text în felul următor: fiecare caracter este
înlocuit cu caracterul care îi succede în tabela de coduri ASCII. Ultimul caracter ASCII va
deveni în urma codificării primul caracter.

2. Într-un fişier cod.dat este memorată o tabelă de codificare, dată pe linii de forma:

caracter cod

Să se preia un text din fişierul f1, să se codifice conform codului, iar textul obţinut să se
scrie în fişierul f2. Dacă în fişierul f1 apar caractere ale căror coduri nu au fost definite, se
va afişa un mesaj de eroare.

3. Să se scrie un program care citeşte un fişier text şi îl copiază în alt fişier în care adaugă şi o
numerotare a liniilor în stânga fiecărei linii.

4. Să se scrie un program care citeşte un fişier text şi afişează liniile acestuia în ordine inversă
(ultima linie din fişier se afişează prima, etc).

5. Să se scrie un program care citeşte un fişier text şi numără de câte ori apare fiecare vocală
în text.

6. Să se scrie un program care citeşte un fişier şi îl afişează pe ecran. Afişarea se va face în


pagini de câte 20 de linii. După fiecare pagină, programul se opreşte şi aşteaptă ca
utilizatorul să introducă o comandă. Aceasta poate fi de continuare a afişarii sau de ieşire
din program.

7. Să se scrie un program care citeşte un fişier text şi înlocuieşte secvenţele de mai multe
spaţii consecutive printr-un singur spaţiu.

8. Să se scrie un program care citeşte un fişier text şi numără câte cuvinte distincte conţine
acesta. Se consideră că separatorii între cuvinte sunt caracterele spaţiu, tab şi linia nouă.
Între două cuvinte pot fi oricâţi separatori.

9. Să se scrie un program care elimină comentariile dintr-un program C conţinut într-un fişier.
Se va avea în vedere că secvenţele /* şi */ pot apare şi în interiorul unor constante şiruri de
caractere, caz în care trebuie ignorate.

10. Să se scrie un program care serveşte la generarea automată a unor cereri şi formulare tip.
Sablonul cererii tip este dat într-un fişier text în care sunt indicate rubricile în care se
introduc informaţiile personalizate. Aceste rubrici sunt indicate printr-un text între acolade.
La întâlnirea unei rubrici, programul va afişa un mesaj prin care solicită utilizatorului
introducerea informaţiei necesare. Mesajul afişat va fi Introdu <text>, unde <text>

128
este textul conţinut între acolade. Programul va genera un fişier text în care rubricile sunt
completate cu informaţiile introduse.

De exemplu, şablonul unei cereri de concediu poate fi un fişier cu următorul conţinut:

Domnule Director,

Subsemnatul {Nume} {Prenume} va rog sa-mi aprobati


plecarea in concediu de odihna in perioada
{data start}- {data sfarsit}.

Cu respect,

La rularea programului, va avea loc următorul dialog cu utilizatorul:

Introduceti Nume:
Popescu
Introduceti Prenume:
Ion
Introduceti data start:
11.07.2004
Introduceti data sfarsit:
01.08.2004

Fişierul rezultat va avea următorul conţinut:

Domnule Director,

Subsemnatul Popescu Ion va rog sa-mi aprobati


plecarea in concediu de odihna in perioada
11.07.2004 - 01.08.2004.

Cu respect,

11. Să se scrie un program care adaugă într-un fişier text personal.txt datele despre
angajaţii unei firme. Datele se citesc de la tastatură şi conţin următoarele: numele,
prenumele, adresa, data nasterii, data angajării, funcţia, salariul.

12. Să se scrie un program care afişează datele păstrate în fişierul personal.txt creat în
exerciţiul anterior.

129
13. Se citesc dintr-un fişier text numele şi notele obţinute de studenţi la examen. Fişierul
conţine linii de forma

nume nota

Să se calculeze media notelor din an.

14. Se citeşte dintr-un fişier text lista produselor care se găsesc într-un magazin. Fişierul e
organizat pe linii de forma

denumire_produs pret

Se citeşte de la tastatură o valoare reprezentând o sumă de bani de care dispune un client.


Se cere să se afişeze care e cel mai scump produs pe care îl poate cumpăra în limita sumei
de care dispune.

15. Se consideră două fişiere care conţin numere întregi în ordine strict crescătoare. Să se
creeze un al treilea fişier care să conţină toate numerele din cele două fişiere, în ordine
strict crescătoare. Dacă un număr apare în ambele fişiere de intrare el va apare o singură
dată în fişierul rezultat. Prelucrarea se va face direct de pe disc, fără a încărca fişierele de
intrare integral în memorie.

16. Să se scrie un program care crează un fişier în care scrie valorile tuturor numerelor întregi
între 1000 şi 2000. Programul va fi realizat în două variante: în prima variantă lucrează cu
un fişier text iar în a doua variantă cu un fişier binar. Se vor compara cele două fişiere.

17. Să se scrie un program pentru crearea unui fişier binar având articole structuri cu
următoarele câmpuri: denumire produs, preţ unitar, cantitate în stoc. Datele se introduc de
la tastatură. Să se afişeze conţinutul fişierului.

130
7
Recursivitate

7.1 Recursivitatea ca şi concept


Un obiect sau un fenomen se defineşte în mod recursiv dacă în definiţia sa se face referire la
el însuşi.
Exemple:
1) Definiţia numerelor naturale:
a) 0  N
b) dacă i  N  succ(i)  N
2) Diferite funcţii matematice:
fact: N  N (factorial)
fact(n) = 1 dacă n  0,
n  fact(n-1) dacă n  0
ac: N  N  N (Ackermann)
n1 dacă m  0,
ac(m, n) = ac(m-1, 1) dacă n  0,
ac(m-1, ac(m, n-1)) dacă m  0 şi n  0.
fib: N  N (Fibonacci)
fib(n) = 1 dacă n  0 sau n  1,
fib(n-2)  fib(n-1) dacă n  1.
Utilitatea practică a recursivităţii rezultă din posibilitatea de a defini o infinitate de obiecte
printr-o singură relaţie sau printr-un set de relaţii finit.
Recursivitatea a fost introdusă în programare în 1960, în limbajul Algol.
O funcţie este recursivă atunci când executarea ei implică cel puţin încă un apel către ea
însăşi. Aceasta se poate realiza practic permiţând ca o funcţie să se apeleze direct (recursivitate
directă). sau prin intermediul mai multor funcţii care se apelează circular (recursivitate indirectă
sau mutuală).

131
7.2 Recursivitatea directă în C
În limbajul C, identificatorul unei funcţii se consideră definit pe nivelul înconjurător blocului
subprogramului respectiv, ceea ce însemnă că, în corpul funcţiei, el este global şi poate fi invocat. Din
acest motiv funcţiile C se pot apela pe ele însele, adică sunt direct recursive.
Exemplu schematic:
void p (listă de parametri formali){
... a, b;
...
if cond p(...) ... sau:
p(...);  while cond { ...p(...) } sau:
do ... p(...)... while cond
}
Apelul recursiv al unei funcţii trebuie să fie obligatoriu condiţionat de o decizie (cond din
exemplul de mai sus) care, la un moment dat în cursul execuţiei, va împiedica apelul în continuare,
provocând revenirea. Dacă acest lucru nu este prevăzut sau este prevăzut greşit, se obţine un apel
în cascadă, fără oprire (infinit) care, în cele din urmă, va conduce la eroare de program: Depăşirea
stivei de date. Condiţia care trebuie testată este, evident, specifică problemei de rezolvat.
Programatorul trebuie să o identifice în fiecare situaţie concretă şi, pe baza ei, să redacteze corect
apelul recursiv.

Unei funcţii i se acordă, la apel, o zonă de memorie numită înregistrare de activare în care,
printre altele, se rezervă locaţiile pentru variabilele locale funcţiei: variabilele automatice.
Aceasta înseamnă că, la fiecare apel, se va crea o nouă înregistrare de activare, incluzând un
nou set de variabile locale, într-o zonă de memorie numită stiva de date (fig. 7.1).

variabile locale
apel 3 p a b înreg. de activ 3

variabile locale
apel 2 p a b înreg. de activ 2

variabile locale
apel 1 p a b înreg. de activ 1
(din programul
principal)

Fig. 7.1. Stiva de date a programului pe parcursul unui apel recursiv

Deşi aceste variabile poartă acelaşi nume ele reprezintă, de la un apel la altul, entităţi
diferite care, în mod firesc, au valori diferite. Pe parcursul unui anumit apel nu sunt accesibile

132
decât variabilele locale create la începutul apelului respectiv (nu şi cele din apelurile anterioare) şi
care se desfiinţează la revenirea din apel. În acelaşi mod se tratează parametrii formali şi locaţia de
memorie prin care se returnează rezultatul unei funcţii, informaţii care sunt şi ele prezente în
înregistrarea de activare şi diferă de la un apel la altul.

Programul 7.1: Se cere să se scrie programul care să citească un cuvânt de pe mediul de intrare şi
să-l afişeze atât normal cât şi în oglindă (în ordinea inversă a literelor). Cuvântul va fi urmat de
spaţiu. Citirea se va efectua caracter cu caracter, într-o variabilă de tip char.
Exemplu: Linia de intrarea: ABC enter
Linia de ieşire: ABC CBA

#include <conio.h>
#include <stdio.h>
void function Invers(){
char ch;
getchar(ch); putchar(ch);
if (ch    ) Invers();
putchar(ch)
}
void main(){
Invers();
getch();
}

apel 4 ch 

apel 3 chC

apel 2 chB

apel 1 chA
(p.p)
Fig. 7.2. Dinamica stivei la execuţia programului pentru linia de intrare dată ca exemplu

Programul 7.2: Calculul recursiv al factorialului.


double function factrec (n: int){
if (ch 1)
return 1;
else
return nf(n-1);
}

133
factrec 1
(1)
factrec 21

factrec 32

factrec 4 6
(4)

Fig. 7.3. Dinamica stivei de date în cazul apelului factrec(4)

7.3 Relaţia între recursivitate şi iteraţie


Recursivitatea este strâns legată de iteraţie.
Iteraţia reprezintă execuţia repetată a unei porţiuni de program, până în momentul în care
se îndeplineşte o condiţie. Fiecare execuţie se duce până la capăt, se verifică îndeplinirea condiţiei
şi, în funcţie de răspuns, se reia execuţia de la început sau se termină iteraţia.
Exemple: structurile repetitive while, do while şi for.
Programul 7.3: Calculul iterativ al factorialului.
double factiter(int x)
double f=1 ; int i ;
for(i=1;i<=x; i++)
f*=i;
return f;

Ca şi iteraţia, recursivitatea presupune, de asemenea, execuţia repetată a unei porţiuni de


program. Spre deosebire de iteraţie, în cazul recursivităţii, porţiunea de program care se repetă
este organizată ca funcţie, iar condiţia este verificată pe parcursul executării acesteia (nu la
sfârşit). În funcţie de rezultatul condiţiei, funcţia se poate apela din nou, fără ca apelul precedent
să se fi încheiat. În momentul satisfacerii condiţiei de revenire, se revine în ordine inversă, pe
lanţul de apeluri, reluându-se şi încheindu-se apelurile suspendate.

134
iterativ recursiv
for (i1;i<=n;i++) void function p (... ){
p (... ); .....
if (...) p (...);
.....
}

Se pune întrebarea firească: ce este mai indicat de utilizat, apelul recursiv sau calculul
iterativ? Deşi, aparent, cele două variante sunt echivalente, se impune totuşi următoarea remarcă
generală: dacă algoritmului de calcul îi corespunde o formulă iterativă este de preferat să se
folosească aceasta în locul apelului recursiv, întrucât viteza de prelucrarea este mai mare şi
necesarul de memorie mai mic. De exemplu, calculul recursiv al numerelor lui Fibonacci este
complet ineficient întrucât numărul de apeluri creşte exponenţial odată cu n (pentru n = 5 sunt
necesare 15 apeluri).
În principiu, orice algoritm recursiv poate fi transformat într-unul iterativ. Această
transformare însă se poate realiza mai uşor sau mai greu. Sunt situaţii în care, în varianta iterativă,
programatorul trebuie să gestioneze, în mod explicit, stiva apelurilor, salvându-şi singur valorile
variabilelor de la un ciclu la altul, ceea ce este dificil şi îngreunează mult înţelegerea algoritmului
(exemplu, poate nu cel mai potrivit datorită simplităţii lui, programul de inversare de caractere).
Pornind de la performanţele calculatoarelor actuale şi de la faptul că principala resursă a
devenit timpul programatorului, recursivitatea are astăzi domeniile ei bine definite, în care se
aplică cu succes. În general se apreciază că algoritmii a căror natură este recursivă trebuie
formulaţi ca proceduri recursive: preluarea structurilor de date definite recursiv (liste, arbori),
descrierea proceselor şi fenomenelor în mod intrinsec recursive.
S-au elaborat de asemenea metode recursive cu mare grad de generalitate în rezolvarea
problemelor de programare (exemplu: backtracking) pe care programatorii experimentaţi le aplică
cu multă uşurinţă, ca nişte scheme, aproape în mod automat.

Programul 7.4: Algoritmul lui Euclid de aflare a celui mai mare divizor comun (c.m.m.d.c.) a
două numere întregi.
Formularea recursivă, în cuvinte, a algoritmului este următoarea:
1) Dacă unul dintre numere este zero, c.m.m.d.c. al lor este celălalt număr.
2) Dacă nici unul dintre numere nu este zero, atunci c.m.m.d.c. nu se modifică dacă se
înlocuieşte unul dintre numere cu restul împărţirii sale cu celălalt.
Algoritmul poate fi implementat sub forma următoarei funcţii recursive:

int function cmmdc (int m,n){


if (n=0) return m
else return cmmdc(n, m mod n)
}

135
Exemplu numeric: cmmdc (18, 27)

m n cmmdc
18 18 cmmdc (27, 18 mod 27)
27 27 cmmdc (18, 27 mod 18)
18 18 cmmdc (9, 18 mod 9)
9 9 cmmdc = 9

Programul 7.5: Se consideră o secvenţă de n numere naturale distincte. Se cere să se genereze toate
secvenţele reprezentând permutările celor n numere.
Se porneşte de la primele n numere naturale ordonate crescător într-un tablou ai, i=1,n. Prin
apelul recursiv al procedurii Permutare se reduc în cascadă permutări (k) la permutări (k-1),
efectuându-se următoarele operaţii:
 păstrând elementul ak pe locul său, permută primele k-1 elemente;
for(i=1; i<k-1; i++){
 schimbă între ele elementele ai cu ak;
 păstrând elementul ak pe locul său, permută primele k-1 elemente;
 schimbă între ele elementele ai cu ak pentru a reface situaţia iniţială a variabilei globale a,
absolut necesară la revenirea dintr-un lanţ de apeluri recursive.
}
Programul complet este următorul:

#include <stdio.h>
#include <conio.h>

#define N 4

int a[N+1];

void Tipareste()
{
int i;
for (i=1; i<=N; i++)
printf("%d ", a[i]);
printf("\n");
}

void Permutare(int k)
{
int i,x;

136
if (k == 1)
Tipareste();
else
{
Permutare(k-1);
for (i=1; i<=k-1; i++)
{
x = a[i]; a[i] = a[k]; a[k] = x;
Permutare(k-1);
x = a[i]; a[i] = a[k]; a[k] = x;
} //for
} //else
}

int main(int argc, char *argv[])


{
int i;
for (i=1; i<=N; i++)
a[i] = i;
Permutare(N);
getch();
return 0;
}
Se observă că programul conţine chiar operaţiile descrise mai sus în pseudocod, la care se
adaugă condiţia de oprire a apelării recursive (k = 1, caz în care s-a obţinut o secvenţă finală şi se
tipăreşte). În concluzie, descrierea trecerii de la un pas la pasul următor, calitativ identic, împreună
cu condiţia de oprire, sunt suficiente pentru implementarea în program a unui algoritm recursiv.
În partea dreaptă a programului sunt prezentate secvenţele finale, în ordinea afişării lor din
program. Pentru fiecare grup de 6 respectiv 2 secvenţe se observă ideea de bază a algoritmului:
păstrarea elementului ak (k = 4 şi respectiv k = 3) pe locul său şi permutarea primelor k-1
elemente.
Pentru a parcurge în mod corect şi complet toate permutările prin înlocuirea fiecărui element
ak cu toate elementele ai (i  1,k  1) , la revenirea din cel de-al doilea apel recursiv trebuie refăcută
starea iniţială a tabloului a. Această situaţie sau situaţii similare apar frecvent în programele
recursive, atunci când procedura recursivă efectuează modificări asupra unor variabile globale.

7.4 Întrebări şi probleme

1. Prin ce se caracterizează definiţia recursivă a unui obiect sau fenomen?


2. Care este utilitatea practică a recursivităţii?

137
3. În ce constă apelul recursiv al unei proceduri sau funcţii?
4. Ce particularitate a regulilor de vizibilitate a identificatorilor permite, în Pascal, recursivitatea
directă?
5. De ce trebuie condiţionat un apel recursiv?
6. Ce informaţii conţine înregistrarea de activare şi care este durata de viaţă a acestor informaţii, la
execuţie programului?
7. Care sunt asemănările şi deosebirile între un apel recursiv şi un calcul iterativ? Care din cele
două variante este mai eficientă?
8. Ce categorii de aplicaţii sunt implementate în prezent prin algoritmi recursivi?

9. Să se redacteze programul recursiv care determină şi afişează toate numerele lui Fibonacci mai
mici sau egale cu un n dat.

10. Să se calculeze în mod recursiv valoarea funcţiei lui Ackermann pentru m şi n citiţi de la
tastatură.

11. Să se elaboreze o soluţie nerecursivă pentru funcţia lui Ackermann.

12. Să se elaboreze o soluţie nerecursivă pentru generarea de permutări (programul 7.5)

13. Să se implementeze calculul recursiv al numărului de combinări C pn pentru n şi p date de intrare,


pe baza următoarelor relaţii:
Cm0  1
Cmm  1
Cmk  Cmk 1  Cmk 11 pentru m  k  0

14. Se consideră polinomul de gradul n:


P ( x)  c0  x n  c1  x n 1  ...  cn
Se cere să se calculeze recursiv valorile polinoamelor P(x) şi P’(x) într-un punct a, pe baza
următoarelor relaţii recurente:
c0 pentru k = 0
Pk ( x)  
 Pk 1 ( x)  ck pentru k  0

şi

138
pentru k = 0
c0
Pk ( x)   pentru k  0
 Pk 1 ( x)  ck

15. Se consideră funcţia f: I  R de forma:


1
f ( x)  cu n  N*
sin n ( x)
Domeniul I este astfel ales încât sin x  0 x  I.
Fie In(x) o primitivă a funcţiei f cu proprietatea:
π
In    0
2
Se cere să se calculeze recursiv In(x), pentru n şi x date de intrare, pe baza următoarelor relaţii
recurente:
 x pentru x = 1
 tg
Pk ( x)   2
 pentru x  2

16. Să se realizeze un program care, citind o secvenţă de n caractere diferite, afişează toate
grupurile de caractere care se pot obţine pe baza aranjamentelor luate câte m a celor n
caractere.

17. Similar cu 18, dar grupurile de caractere se vor construi pa baza aranjamentelor cu repetiţie,
combinărilor sau combinărilor cu repetiţie.

18. Turnurile din Hanoi: Se dau trei tije notate cu A, B, C şi n discuri de diametre diferite, stivuite
pe tija A în ordinea descrescătoare a diametrelor, formând un turn, ca în fig. 3.4.
Se cere să se realizeze programul care determină secvenţa de mutări a tuturor discurilor
de pe tija A pe tija C, în aceeaşi ordine, utilizând eventual tija B ca intermediar. Mutările se vor
efectua cu următoarele restricţii:
 la fiecare mişcare se va muta un singur disc;
 un disc nu poate fi suprapus peste altul mai mic decât el.

139
8

Metode generale de proiectare a


algoritmilor şi programelor

În teoria şi practica programării există un număr foarte mare de probleme pentru care
trebuie găsite rezolvări. Din totalitatea acestor probleme se disting clase de probleme similare.
Pentru toate problemele dintr-o asemenea clasă se poate aplica una şi aceeaşi metodă generală de
rezolvare, evident cu mici ajustări în funcţie de natura problemei concrete.

În timp s-au distilat mai multe metode generale de rezolvare a problemelor. Programatorii
experimentaţi stăpânesc foarte bine aceste metode generale şi le aplică în mod automat atunci când
au posibilitatea. Vom prezenta în continuare trei asemenea metode, şi anume Greedy,
Backtracking şi Divide and Conquer. Se recomandă studiul foarte atent al acestor metode,
înţelegerea contextului în care ele se pot aplica şi însuşirea lor în asemenea mod încât aplicarea lor
să devină un automatism.

Prezentarea fiecăreia din cele trei metode generare de rezolvare a problemelor va începe cu
analiza numelui metodei. Numele sunt foarte sugestive şi din simpla analiză a lor putem avea o
bună înţelegere despre caracteristicile esenţiale ale metodelor. Se recomandă traducerea numelor
din limba engleză, chiar folosirea a mai multe dicţionare diferite, pentru a realiza sensul lor
profund.

Apoi va fi prezentat contextul în care metoda se poate aplica, urmat de modul de lucru al
metodei. Se va explica implementarea metodei, furnizându-se scheme de lucru în pseudocod. Pe
urmă se vod analiza exemple concrete de probleme la care se va face întâi o analiza teoretică şi
apoi se vor furniza soluţii sub formă de programe în limbajul C.

141
8.1 Metoda Greedy
8.1.1 Consideraţii teoretice
Explicarea numelui
În limba engleză cuvântul greedy înseamnă lacom. Algoritmii de tip greedy sunt algoritmi
lacomi. Ei vor să construiască într-un mod cât mai rapid soluţia problemei, fără a sta mult pe
gânduri.

Algoritmii de tip greedy se caracterizează prin luarea unor decizii rapide care duc la
găsirea unei soluţii a problemei. Nu întotdeauna asemenea decizii rapide duc la o soluţie optimă,
dar vom vedea că există anumite tipuri de probleme unde se pot obţine soluţii optime sau foarte
apropiate de optim.

Aplicabilitatea algoritmilor de tip Greedy


Algoritmii de tip Greedy se aplică la acele probleme unde datele de intrare sunt organizate
sub forma unei mulţimi A şi se cere găsirea unei submulţimi BA care să îndeplinească anumite
condiţii astfel încât să fie acceptată ca soluţie posibilă.

În general pot să existe mai multe submulţimi BA care să reprezinte soluţii posibile ale
problemei. Dintre toate aceste submulţimi B se pot selecta, conform unui anumit criteriu, anumite
submulţimi B* care reprezintă soluţii optime ale problemei. Scopul este de a găsi, dacă este posibil,
una din mulţimile B*. Dacă acest lucru nu este posibil, atunci scopul este găsirea unei mulţimi B
care să fie cât mai aproape de mulţimile B*, conform criteriului de optimalitate impus.

Modul de lucru al algoritmilor de tip Greedy


Construirea mulţimii B se face printr-un şir de decizii. Iniţial se porneşte cu mulţimea vidă
(B = Ø). Fiecare decizie constă în alegerea unui element din mulţimea A, analiza lui şi eventual
introducerea lui în mulţimea B. În funcţie de modul în care se iau aceste decizii, mulţimea B se va
apropia mai mult sau mai puţin de soluţia optimă B*. În cazul ideal vom avea B = B*.

Algoritmii de tip greedy nu urmăresc să determine toate soluţiile posibile şi să aleagă


dintre ele, conform criteriului de optimalitate impus, soluţiile optime. După cum spune şi numele,
algoritmii de tip greedy sunt caracterizaţi prin lăcomie şi nu au răbdarea să investigheze toate
variantele posibile de alegere a soluţiei. Ei încep construirea unei soluţii pornind de la mulţimea
vidă, apoi lucrează în paşi, într-un mod cât se poate de hotărât: la fiecare pas se ia câte o decizie şi
se extinde soluţia cu câte un element.

La fiecare pas se analizează câte un element din mulţimea A şi se decide dacă să fie sau nu
inclus în mulţimea B care se construieşte. Astfel se progresează de la Ø cu un sir de mulţimi
intermediare (Ø, B0, B1, B2, ...), până când se obţine o soluţie finală B.

142
8.1.2 Implementare
Ca şi schemă generală de lucru, există două variante de implementare a algoritmilor de tip
Greedy.

Prima variantă foloseşte două funcţii caracteristice: alege şi posibil. alege este o funcţie
care are rolul de a selecta următorul element din mulţimea A care să fie prelucrat. Funcţia posibil
verifică dacă un element poate fi adăugat soluţiei intermediare Bi astfel încât noua soluţie Bi+1 care
s-ar obţine să fie o soluţie validă. Prezentăm în continuare pseudocodul pentru această primă
variantă greedy. Se consideră că numărul de elemente al mulţimii A este n.

B = multimea vida
for (i=0; i<n; i++)
{
x = alege(A)
if (posibil(B,x))
* adauga elementul x la multimea B
}

Dificultatea la această primă variantă constă în scrierea funcţiei alege. Dacă funcţia alege
este bine concepută, atunci putem fi siguri că soluţia B găsită este o soluţie optimă. Dacă funcţia
alege nu este foarte bine concepută, atunci soluţia B găsită va fi doar o soluţie posibilă şi nu va fi
optimă. Ea se poate apropia însă mai mult sau mai puţin de soluţia optimă B*, în funcţie de
criteriul de selecţie implementat.

A doua variantă de implementare diferă de prima prin faptul că face o etapă iniţială de
prelucrare a mulţimii A. Practic se face o sortare a elementelor mulţimii A, conform unui anumit
criteriu. După sortare, elementele vor fi prelucrate direct în ordirea rezultată. Prezentăm în
continuare pseudocodul pentru această a doua variantă greedy.

B = multimea vida
prelucreaza(A)
for (i=0; i<n; i++)
{
x = A[i]
if (posibil(B,x))
* adauga elementul x la multimea B
}

La a doua variantă, dificultatea funcţiei alege nu a dispărut, ci s-a transferat funcţiei


prelucreaza. Dacă prelucrarea mulţimii A este bine făcută, atunci se va ajunge în mod sigur la o
soluţie optimă. Altfel se va obţine doar o soluţie posibilă, mai mult sau mai puţin apropiată de
optim.

143
8.1.3 Exemple
Programul 8.1: Submulţime de sumă maximă.

Enunţ Fie o mulţime X = {x0, x1, ..., xn} cu elemente reale. Se cere să se determine o
submulţime YX astfel încât suma elementelor lui Y (yY y) să fie maximă.

Rezolvare Submulţimea de sumă maximă se obţine dacă se aleg toate elementele pozitive
din mulţimea X. Vom reda aici doar o schiţă de demonstraţie. Pentru o demonstraţie completă se
poate consulta literatura de specialitate. Să notăm cu Ypoz submulţimea care conţine toate
elementele pozitive din X. Fie Spoz suma elementelor lui Ypoz (Spoz = yYpozy). Dacă vrem să
eliminăm un element oarecare yi din mulţimea Ypoz, atunci vom obţine o nouă mulţime Y* = Y-
{yi} care va avea suma S* = Spoz-yi. Deoarece toate elementele din Y, deci şi yi, sunt pozitive, vom
avea S* < Spoz.
Dacă vrem să adaugăm un element oarecare la mulţimea Ypoz, atunci vom obţine o nouă
mulţime Y** = Y+{xi} care va avea suma S** = Spoz+xi. Cum toate elementele din X care ar mai
putea fi adăugate la Y, deci şi xi, sunt negative, rezultă că S** < Spoz.
Deci în oricare din cele două variante am obţine o mulţime cu suma elementelor mai mică
decât mulţimea Ypoz.

Implementare Procedeul greedy ne spune să începem construirea soluţiei pornind de la


mulţimea vidă. Cu alte cuvinte vom porni de la Y = Ø. Pe urmă vom parcurge pe rând elementele
mulţimii X şi la fiecare element parcurs vom decide dacă să îl adăugăm sau nu mulţimii Y.
Să considerăm un exemplu concret. Fie X = {5, -3, 1, 12, -6, -34, 37, 0}. Iniţial soluţia
noastră este mulţimea vidă Ø. Primul element parcurs din X este 5. Deoarece este pozitiv îl
adaugăm la soluţie şi obţinem mulţimea Y0 = {5}. Următorul element parcurs este -3. Deoarece
este negativ, îl ignorăm. Urmează 1 care este pozitiv şi se adaugă la soluţie, obţinându-se
mulţimea Y1 = {5, 1}. Urmează 12 care este pozitiv şi ne conduce la noua soluţie Y2 = {5, 1, 12}.
-6 şi -34 sunt negative şi le ignorăm. 37 este pozitiv şi ne conduce la Y3 = {5, 1, 12, 37}. 0 nu ne
influenţează în nici un fel. Am putea la fel de bine să îl adăugăm sau să îl lăsăm deoparte. Alegem
să îl ignorăm. Soluţia finală este Y = Y3.
Deşi problema în sine este trivială, ea ilustrează foarte bine toate elementele tipice unui
algoritm de tip greedy: construirea pas cu pas a soluţiei prin adăugarea de elemente dintr-o
mulţime iniţială, precum şi modul în care se ia decizia de a adăuga elemente la soluţie, în mod
direct, fără a se analiza mai multe soluţii posibile în paralel.

Cod sursă 1 În continuare prezentăm prima variantă de implementare a soluţiei, folosind


funcţia alege.

#include <stdio.h>

/* Aceasta este multimea noastra initiala.


Aici am definit-o ca si constanta, pentru
simplitate. Alternativ ea poate fi citita
de la tastatura sau din fisier. */
#define N 8

144
int x[N] = {5, -3, 1, 12, -6, -34, 37, 0};

/* Aici vom memora solutia problemei,


submultimea Y. */
int y[N];

/* Acesta este un tablou folosit de functia


"alege" pentru a sti care elemente au fost
deja alese din multimea X. */
int ales[N];

/* Aceasta este functia "alege" care preia cate


un element din multimea A pentru prelucrare.
*/

int alege()
{
int i;

/* Parcurgem multimea X si cautam elemente


strict pozitive si care nu au fost inca
alese. */
for (i=0; i<N; i++)
{
/* In momentul cand am intalnit un
element strict pozitiv si care nu
a mai fost ales, il marcam ca fiind
ales si il returnam. */
if ((x[i] > 0) && !ales[i])
{
ales[i] = 1;
return x[i];
}
}

/* Daca am ajuns in acest punct, atunci nu


mai avem elemente strict pozitive nealese.
In aceasta situatie vom returna un numar
negativ. */
return -1;
}

int main(void)
{
int x_ales, i, k, suma;

/* Afisam multimea X. */
printf("Multimea X are %d elemente.\n", N);
for (i=0; i<N; i++)
printf("%d ", x[i]);

/* Initial nici un element din multimea X nu

145
a fost ales. */
for (i=0; i<N; i++)
ales[i] = 0;

/* Pentru parcurgerea multimii X nu vom folosi


un ciclu "for", ci o bucla "do...while",
deoarece este posibil ca numarul de elemente
strict pozitive sa fie mai mic decat N si in
acest caz nu are rost sa facem parcurgerea
tuturor celor N elemente. */
k = 0;
suma = 0;
do
{
/* Alegem un nou element din multimea X. */
x_ales = alege();

/* Functia "posibil" este triviala in cazul


nostru. Din moment ce functia "alege"
returneaza elementele pozitive, singurul
test ce trebuie facut este daca rezultatul
primit de la "alege" este pozitiv. In caz
contrar inseamna ca s-au epuizat elementele
pozitive din multimea X. */
if (x_ales > 0)
{
/* Urmatoarele doua linii adauga
elementul ales din X la multimea Y. */
y[k] = x_ales;
k++;

/* Calculam si suma elementelor lui Y. */


suma += x_ales;
}
} while (x_ales > 0);

/* Afisam multimea Y si suma obtinuta. */


printf("\nSubmultimea Y are %d elemente.\n", k);
for (i=0; i<k; i++)
printf("%d ", y[i]);
printf("\nSuma elementelor lui Y este %d.\n", suma);

return 0;
}

Cod sursă 2 Prezentăm şi o implementare conform celei de-a doua variante a algoritmilor
de tip Greedy. De data aceasta avem o funcţie prelucrare care sortează în ordine descrescătoare
elementele mulţimii X, pentru a uşura alegerea lor în vederea prelucrării.

146
#include <stdio.h>

#define N 8
int x[N] = {5, -3, 1, 12, -6, -34, 37, 0};
int y[N];

/* Aceasta este functia de prelucrare a


multimii initiale X. Practic se face
o sortare descrescatoare a multimii,
astfel incat elementele strict pozitive
sa fie primele. */

void prelucrare()
{
int i, j, aux;

/* Pentru sortare am folosit bubblesort. */


for (i=0; i<N-1; i++)
for (j=i+1; j<N; j++)
if (x[i] < x[j])
{
aux = x[i];
x[i] = x[j];
x[j] = aux;
}
}

int main(void)
{
int x_ales, i, k, suma;

printf("Multimea X are %d elemente.\n", N);


for (i=0; i<N; i++)
printf("%d ", x[i]);

/* La inceput de tot facem prelucrarea


multimii X. */
prelucrare();

/* Parcurgem multimea pana cand se epuizeaza


toate elementele sau pana cand intalnim un
element mai mic sau egal cu zero. */
k = 0;
suma = 0;
for (i=0; (i<N) && (x[i]>0); i++)
{
x_ales = x[i];
y[k] = x_ales;
k++;

suma += x_ales;

147
}

printf("\nSubmultimea Y are %d elemente.\n", k);


for (i=0; i<k; i++)
printf("%d ", y[i]);
printf("\nSuma elementelor lui Y este %d.\n", suma);

return 0;
}

Programul 8.2: Conectarea oraşelor cu cost minim.

Enunţ Se consideră n oraşe. Pentru diferite perechi de oraşe (i, j), 0<i<n, 0<j<n se
cunoaşte costul conectării lor directe ci,j. Nu toate perechile de oraşe pot fi conectate; pentru
perechile care nu pot fi conectate nu se precizează costul. Se cere să se construiască o reţea prin
care oricare două oraşe să fie conectate între ele direct sau indirect şi costul total al conectării să
fie minim.

Rezolvare Se poate arăta că reţeaua de conectare cerută este un arbore. Problema mai este
cunoscută şi ca problema determinării arborelui parţial de cost minim într-un graf. Pentru această
problemă există un algoritm greedy de rezolvare numit algoritmul lui Prim. În literatura de
specialitate există argumentarea matematică a faptului că acest algoritm găseşte întotdeauna
soluţia optimă de conectare a oraşelor.

Se construieşte arborele parţial minim în manieră greedy, adăugând câte un nod la fiecare
pas. La început de tot arborele parţial este vid, nu conţine nici un nod. Primul pas constă în
adăugarea unui nod arbitrar în arbore. Pe urmă, la fiecare pas se caută muchia de cost minim care
porneşte dintr-un nod deja adăugat la arbore şi ajunge într-un nod care nu este în arbore. Se adaugă
în arbore nodul în care sfârşeşte muchia găsită.
Să considerăm spre exemplu o reţea de 7 oraşe numerotate de la 0 la 6. Costurile de
conectare a oraşelor sunt redate în fig. 8.1.

Fig. 8.1 Costuri de conectare a oraşelor pentru problema


conectării oraşelor cu cost minim

148
Arborele minim este redat cu linii îngroşate. El a fost construit pas cu pas, conform
procedeului descris mai sus. Iniţial arborele a fost vid. La primul pas s-a adăugat un nod arbitrar, şi
anume nodul 0.
Pe urmă s-a ales muchia de cost minim care pleacă din nodul 0 către celelalte noduri.
Muchia de cost minim a fost (0,2) de cost 10. Nodul 2 a fost adăugat în arbore.
La următorul pas s-a ales muchia de cost minim care pleacă din nodurile 0 sau 2 către
celelalte noduri. Muchia aleasă a fost (2,1) de cost 9. Nodul 1 a fost adăugat în arbore.
La următorul pas s-a ales muchia de cost minim care pleacă din nodurile 0, 2 sau 1 către
nodurile încă neintroduse în arbore. Muchia aleasă a fost (1,5) de cost 3. Nodul 5 a fost adăugat în
arbore.
Următoarea muchie aleasă a fost (5,4) de cost 2. Nodul 4 a fost adăugat în arbore. Apoi a
fost aleasă muchia (4,6) de cost 2 şi nodul 6 a fost adăugat şi el în arbore.
Pe urmă a fost aleasă muchia (1,3) de cost 4 şi nodul 3 a fost introdus în arbore. În acest
moment algoritmul s-a încheiat deoarece toate oraşele au fost conectate la reţea. Costul total al
conectării a fost 10 + 9 + 3 + 2 + 2 + 4 = 30.

Implementare Matricea costurilor, C, se reţine într-un tablou bidimensional. Pentru


perechile de oraşe între care nu se poate face legătură se va trece în matricea costurilor valoarea 0.
În termeni Greedy, mulţimea noastră iniţială de elemente A este muţimea tuturor perechilor de
oraşe între care se poate stabili legătură directă. Adică A={(i, j) | ci,j>0}. Pentru graful din figura 1,
mulţimea A va conţine elementele {(0,1), (0,2), (1,2), (1,3), (1,5), (2,3), (4,5), (4,6), (5,6)}.
Submulţimea B pe care o căutăm va conţine o parte din perechile aflate în mulţimea A. Se
poate demonstra că soluţia optimă B* conţine n-1 perechi atunci cănd numărul de oraşe este n şi se
poate construi o reţea care să conecteze toate oraşele (graful este conex).
Pentru construirea mulţimii B, vom selecta oraşele rând pe rând pentru a le adăuga la reţea.
Vom spune că un oraş este selectat atunci când el a fost conectat la reţeaua de oraşe printr-o
muchie care face parte din mulţimea B.
În implementare nu vom lucra cu mulţimea A sub forma explicită de perechi, ci vom folosi
matricea costurilor C. Vom eticheta liniile şi coloanele matricei costurilor după cum urmează.
Atunci cănd un oraş oi este selectat, linia i din matrice se marchează, iar coloana i din matrice se
şterge.
Pentru a alege următorul element din mulţimea A care să fie prelucrat, căutăm cel mai mic
cost din matricea costurilor din liniile marcate şi coloanele care nu sunt şterse. Să zicem că cel mai
mic cost a fost găsit ca fiind elementul ci_min,j_min. Atunci următorul element din mulţimea A care
va fi prelucrat este perechea (i_min,j_min).
Ştergerea coloanelor din matricea costurilor nu va însemna o ştergere fizică, ci doar una
logică. Vom folosi doi vectori prin care vom memora care linii sunt marcate şi care coloane sunt
şterse.

Cod sursă Prezentăm în continuare codul sursă în limbaj C care implementează algoritmul
lui Prim descris mai sus.
#include <stdio.h>
#include <conio.h>

/* Numarul maxim de noduri din graf. */


#define N_MAX 30

149
/* Constanta care ne ajuta la gasirea minimului in
matrice. Inainte de parcurgerea matricii
initializam variabila care va pastra minimul cu
aceasta valoare foarte mare. */
#define MINIM 10000

/* Numarul de orase. */
int n;

/* Matricea costurilor de conectare a oraselor. */


int c[N_MAX][N_MAX];

/* Vector care indica liniile marcate din matricea


costurilor. marcat[k] va fi 1 pentru liniile
marcate si 0 pentru liniile nemarcate. */
int marcat[N_MAX];

/* Vector care indica coloanele sterse din matricea


costurilor. sters[k] va fi 1 pentru coloanele
sterse si 0 pentru coloanele nesterse. */
int sters[N_MAX];

/* Functie care alege urmatorul element care sa


fie prelucrat din multimea A, adica o pereche
de orase intre care sa se construiasca drum.
Se parcurg liniile marcate si coloanele
nesterse din matricea costurilor si se
alege costul minim.
Se returneaza costul minim gasit, si linia si
coloana unde apare el.
Daca nu mai exista linii coloane nesterse, sau
daca nu mai exista legaturi inspre orasele
neselectate, atunci se va returna ca si minim
valoarea MINIM definita mai sus ca si constanta. */

void alege(int* min, int *i_min, int* j_min)


{
int i, j;

*min = MINIM;
*i_min = -1;
*j_min = -1;
for (i=0; i<n; i++)
if (marcat[i])
{
for (j=0; j<n; j++)
if (!sters[j])
{
if ((c[i][j] != 0) &&
(c[i][j] < *min))
{

150
*min = c[i][j];
*i_min = i;
*j_min = j;
}
}
}
}

int main(void)
{
FILE *fin;
int i, j;
int cost, count, gata;
int min, i_min, j_min;
int arbore[N_MAX][2];

/* Deschidem fisierul pentru citire. */


fin = fopen("prim.in", "rt");
/* Ne asiguram ca fisierul s-a deschis
cu succes. */
if (!fin)
{
printf("Nu pot deschide fisierul.\n");
return -1;
}

/* Citim din fisier numarul de noduri din graf. */


fscanf(fin, "%d", &n);
/* Citim din fisier matricea costurilor. */
for (i=0; i<n; i++)
for (j=0; j<n; j++)
fscanf(fin, "%d", &(c[i][j]));

/* Afisam datele citite din fisier. */


printf("Graful are %d noduri.\n", n);
printf("Matricea costurilor este:\n");
for (i=0; i<n; i++)
{
for (j=0; j<n; j++)
printf("%d ", c[i][j]);
printf("\n");
}
printf("\n");

/* Initial nici o linie nu este marcata si


nici o coloana nu este stearsa. */
for (i=0; i<n; i++)
{
marcat[i] = 0;
sters[i] = 0;
}

151
/* Pornim de la varful "0", motiv pentru care
marcam linia 0 si stergem coloana 0. */
marcat[0] = 1;
sters[0] = 1;

/* Deocamndata nu am gasit nici o muchie in


arbore. Costul total al arborelui este zero. */
count = 0;
gata = 0;
cost = 0;

/* Cat timp mai avem noduri neparcurse. */


while (!gata)
{
alege(&min, &i_min, &j_min);

/* Daca am gasit o valoare minima, inseamna


ca mai putem adauga o muchie la arbore.
Este posibil si sa nu mai existe muchii
care pornesc din liniile marcate (intr-o
asemenea situatie variabila "min" va ramane
la valoarea MINIM pe care a fost setata
inainte de parcurgerea matricii costurilor. */
if (min < MINIM)
{
printf("S-a ales muchia (%d, %d) de cost %d.\n",
i_min, j_min, c[i_min][j_min]);

/* Marcam linia noului nod si stergem


coloana lui. */
marcat[j_min] = 1;
sters[j_min] = 1;

/* Adaugam muchia la arbore. */


arbore[count][0] = i_min;
arbore[count][1] = j_min;

/* Actualizam costul total. */


cost += c[i_min][j_min];

/* Incrementam numarul de muchii gasite. */


count++;
}

/* Daca variabila "min" nu s-a modificat fata


de valoarea MINIM, inseamna ca nu mai exista
muchii care pornesc din liniile marcate spre
coloanele sterse. Acest lucru se poate
intampla fie pentru ca am parcurs toate
nodurile din graf, fie pentru ca graful nu
este conex si am epuizat componenta conexa

152
curenta. In oricare din situatii vom incheia
algoritmul. */
else
{
gata = 1;
}
}

/* Afisam arborele partial minim pe care l-am gasit. */


printf("\nAPM are costul %d si este:\n", cost);
for (i=0; i<count; i++)
printf("(%d, %d) ", arbore[i][0], arbore[i][1]);
printf("\n");

return 0;
}

Fisier cu date de intrare pentru graful din fig. 8.1 este următorul:

7
0 20 10 0 0 0 0
20 0 9 4 0 3 0
10 9 0 10 0 0 0
0 4 10 0 0 0 0
0 0 0 0 0 2 2
0 3 0 0 2 0 3
0 0 0 0 2 3 0

Programul 8.3: Problema comis-voiajorului.

Enunţ Se consideră n oraşe. Se cunosc distanţele dintre oricare două oraşe. Un comis-
voiajor trebuie să treacă prin toate cele n oraşe. Se cere să se determine un drum care porneşte
dintr-un oraş, trece exact o dată prin fiecare din celelalte oraşe şi apoi revine la primul oraş, astfel
încât lungimea drumului să fie minimă.

Rezolvare Pentru găsirea unei soluţii optime la această problemă este nevoie de algoritmi
cu timp de rulare foarte mare (de ordin exponenţial O(2n)). În situaţiile practice asemenea
algoritmi cu timp foarte mare de rulare nu sunt acceptabili. Ca urmare se face un compromis şi se
acceptă algoritmi care nu găsesc soluţia optimă ci doar o soluţie aproape de optim, dar au în
schimb un timp de rulare mic. Propunem în continuare o soluţie greedy la această problemă. Ideea
este următoarea. Se porneşte dintr-un oraş oarecare. Se caută drumul cel mai scurt care pleacă din
oraşul respectiv către oraşe nevizitate încă. Se parcurge acel drum şi se ajunge într-un alt oraş.
Aici din nou se caută cel mai scurt drum către oraşele nevizitate încă. Se parcurge şi acest drum,
ajungându-se într-un nou oraş. Repetănd aceşti paşi se parcurg toate oraşele. La final se parcurge
drumul care duce înapoi spre primul oraş.

153
Să considerăm exemplul din fig. 8.2. Avem 4 oraşe cu distanţele reprezentate în figură.

Fig. 8.2 Reţea de oraşe pentru problema comis-voiajorului

Pornim vizitarea oraşelor din oraşul 0. De aici alegem drumul cel mai scurt către oraşele
nevizitate, şi anume (0,2) de lungime 2. Ajunşi în oraşul 2, alegem din nou drumul cel mai scurt
spre oraşele nevizitate, şi anume (2,3) de lungime 1. Din oraşul 3 mai avem doar un singur oraş
nevizitat, 1, aşa că alegem drumul spre el (3,1) de lungime 1. În acest moment am parcurs toate
oraşele şi ne reîntoarcem în oraşul 0 pe drumul (1,0) de lungime 4. Drumul rezultat este 0, 2, 3, 1,
0, iar distanţa totală de parcurs este 2 + 1 + 1 + 4 = 8.

Implementare Distanţele între oraşe le memorăm într-un tablou bidimensional D. Distanţa


între oraşele (i,j) va fi memorată în elementul di,j al matricii. În termeni Greedy, mulţimea iniţială
A este mulţimea tuturor perechilor de oraşe. Pentru reţeaua de oraşe din figura 2 mulţimea A
conţine elementele {(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)}. Mulţimea B care trebuie găsită va
conţine o parte din aceste perechi de oraşe, şi anume acele perechi care înlănţuite să formeze un
drum ce trece prin toate oraşele. Dacă avem un număr de n oraşe, atunci mulţimea B va conţine n
perechi de oraşe.
În implementare nu vom lucra cu mulţimea A sub această formă explicită de perechi de
oraşe, ci vom folosi matricea distanţelor D. De asemenea drumul comis-voiajorului nu îl vom
păstra sub formă de perechi de oraşe, ci sub forma unui sir al oraşelor.
Pentru a memora drumul parcurs de comis-voiajor, folosim un tablou unidimensional
drum. În acest tablou vom memora indicii oraşelor parcuse, în ordinea parcurgerii.
Pentru a şti care oraşe au fost parcurse, facem o marcare logică a oraşelor folosind un
tablou unidimensional vizitat. Elementele din acest tablou care au valoarea 1 reprezintă oraşe
vizitate.

Cod sursă În continuare este prezentat codul sursă în limbajul C care implementează
algoritmul descris mai sus.

154
#include <stdio.h>

/* Numarul maxim de orase. */


#define N_MAX 30

/* Constanta care se foloseste ca valoare


de initializare la cautarea minimului. */
#define MINIM 10000

/* Numarul de orase. */
int n;

/* Matricea distantelor dintre orase. */


int d[N_MAX][N_MAX];

/* Drumul comis voiajorului. Contine


indicii oraselor in ordinea in care
sunt ele parcurse. */
int drum[N_MAX];

/* Vector care memoreaza care orase au


fost vizitate. vizitat[k] va fi 1 daca
orasul k a fost vizitat, 0 altfel. */
int vizitat[N_MAX];

/* Functie care alege urmatorul element care


sa fie prelucrat din multimea oraselor.
Primeste ca parametru ultimul oras care
a fost vizitat, si returneaza urmatorul
oras care sa fie vizitat precum si lungimea
drumului catre acesta. */
void alege(int ultimul, int *min, int *j_min)
{
int j;

/* Cautam drumul minim de la ultimul


oras pana la orasele neparcurse inca. */
*min = MINIM;
*j_min = -1;
for (j=0; j<n; j++)
if (!vizitat[j])
{
if (d[ultimul][j] < *min)
{
*min = d[ultimul][j];
*j_min = j;
}
}
}

int main(void)

155
{
FILE *fin;
int i, j;
int count, cost, min, j_min;

/* Deschidem fisierul pentru citire in mod text. */


fin = fopen("comis.in", "rt");
if (!fin)
{
printf("Eroare: nu pot deschide fisierul.\n");
return -1;
}

/* Citim datele din fisier. */


fscanf(fin, "%d", &n);
for (i=0; i<n; i++)
for (j=0; j<n; j++)
fscanf(fin, "%d", &(d[i][j]));

/* Afisam pe ecran datele preluate din fisier. */


printf("Avem %d orase.\n", n);
printf("Distantele dintre orase sunt:\n");
for (i=0; i<n; i++)
{
for (j=0; j<n; j++)
printf("%d ", d[i][j]);
printf("\n");
}
printf("\n");

/* Initial nici un oras nu este vizitat. */


for (i=0; i<n; i++)
vizitat[i] = 0;

/* Primul oras vizitat este cel cu numarul "0".


Costul total este zero deocamdata. */
drum[0] = 0;
vizitat[0] = 1;
count = 1;
cost = 0;

/* Parcurgem restul de n-1 orase. */


for (i=0; i<n-1; i++)
{
/* Alegem urmatorul oras care sa fie vizitat. */
alege(drum[count-1], &min, &j_min);

/* Parcurgem drumul minim gasit si vizitam


un nou oras. */
printf("Am ales drumul (%d, %d) de cost %d.\n",
drum[count-1], j_min, min);
drum[count] = j_min;

156
vizitat[j_min] = 1;
count++;
cost += min;
}

/* Parcurgem drumul de ultimul oras vizitat


catre primul oras si actualizam costul
total. */
cost += d[drum[n-1]][0];

/* Afisam drumul parcurs. */


printf("\nDrumul are costul %d si este:\n", cost);
for (i=0; i<n; i++)
printf("%d ", drum[i]);
printf("0\n");
return 0;
}

Fişierul cu date de intrare pentru reţeaua de oraşe din fig. 8.2 este următorul:

4
0 4 2 7
4 0 2 1
2 2 0 1
7 1 1 0

Îmbunătăţiri Algoritmul greedy prezentat se poate îmbunătăţi pentru a furniza soluţii mai
aproape de soluţia optimă. O variantă de îmbunătăţire este să nu se pornească doar din primul oraş
la parcurgerea drumului. Se poate relua calculul având ca punct de pornire fiecare oraş pe rând şi
se poate memora minimul global astfel obţinut.

8.1.4 Observaţii
Este foarte important ca atunci când aplicăm algoritmi de tip greedy pentru rezolvarea de
probleme să ne asigurăm că algoritmii pe care îi alegem duc la soluţii corecte. Dacă este nevoie să
găsim şi soluţii optime, atunci trebuie să ne asigurăm şi de faptul că algoritmii aleşi sunt capabili
să găsească o soluţie optimă.
Pentru un număr mare de probleme există deja algoritmi greedy consacraţi care găsesc
soluţii optime. Aceşti algoritmi sunt deja demonstraţi în literatura de specialitate şi îi putem prelua
ca atare, acordându-le toată încrederea.
Atunci când nu găsim în literatura de specialitate algoritmi care să ne ajute la rezolvarea
problemelor şi compunem noi înşine algoritmi, este necesară demonstrarea în mod riguros
matematic a faptului că soluţia obţinută este optimă. Se poate întâmpla ca pe unele cazuri
particulare de date de intrare algoritmul să găsească soluţie optimă, dar pe cazul general să nu
găsească soluţii optime. Asemenea situaţii nu sunt întotdeauna evidente, deci o fundamentare
matematică a soluţiei este necesară.

157
8.2 Metoda Backtracking
8.2.1 Consideraţii teoretice
Explicarea numelui
Pentru a înţelege semnificaţia cuvântului backtracking vom reda trei definiţii diferite:
 retrace one’s steps; reverse one’s previous position or opinion1
 to go back along a path you have just followed2
 to retrace one’s course; to go back to an earlier point in a sequence3
Ideea de bază este aceea de revenire pe cale. Algoritmii de tip backtracking explorează
spaţiul soluţiilor în mod exhaustiv, pe toate căile posibile. Atunci când pe calea curentă de
explorare se constată că nu mai sunt şanse să se ajungă la o soluţie validă, se revine cu un pas
înapoi şi se abordează o altă cale de explorare.

Aplicabilitatea algoritmilor de tip Backtracking

Algoritmii de tip backtracking se aplică la problemele unde spaţiul soluţiilor S este de


forma unui produs cartezian S = S0 × S1 × ... × Sn-1. Orice element x S din spaţiul soluţiilor va fi
un vector de forma x = (x0, x1, ..., xn-1), cu xi Si, 0<i<n.
Nu toate elementele xS sunt soluţii valide ale problemei. Doar acele elemente x care
satisfac anumite condiţii impuse de problemă vor fi soluţii valide. Definim condiţiile care trebuie
satisfăcute sub forma unei funcţii booleene Solutie(x0,x1,...,xn-1). Un element x=(x0, x1, ..., xn-1)  S
este soluţie a problemei dacă funcţia Solutie aplicată componentelor lui x va returna valoarea true.
Scopul este de a găsi acei vectori xS pentru care funcţia Solutie returnează true.

Modul de lucru al algoritmilor de tip Backtracking

Modul de lucru al algoritmului este următorul: se alege un element x00 din S0, apoi un
element x10 din S1, apoi un element x20 din S2, ş.a.m.d, până când se va fi ales un element xn-10 din
Sn-1. În acest moment vom avea un vector x = (x00, x10, ..., xn-20, xn-10)  S. Evaluăm rezultatul
funcţiei Solutie pentru acest vector. Dacă obţinem rezultatul true atunci avem o soluţie corectă a
problemei. Dacă obţinem rezultatul false, soluţia nu este corectă şi continuăm căutarea.
Aplicăm principiul revenirii pe cale, adică ne întoarcem cu un pas în urmă, înainte de
alegerea elementului xn-10 Sn-1. De aici continuăm alegând un alt element xn-11 Sn-1, xn-11 ≠ xn-10.
Vom obţine un nou vector x' = (x00,x10,...,xn-20,xn-11) S asupra căruia aplicăm din nou funcţia
Solutie. Dacă rezultatul este false, revenim din nou cu un pas în urmă şi continuăm cu alegerea
unui alt element xn-12 Sn-1, xn-12 ≠ xn-10 şi xn-12 ≠ xn-11. Repetăm aceşti paşi până la epuizarea
tuturor elementelor din Sn-1.

1
Oxford Online Dictionary, http://www.askoxford.com/
2
Cambridge Online Dictionary, http://dictionary.cambridge.org/
3
Merriam-Webster Online Dictionary, http://www.m-w.com/

158
După ce am epuizat toate elementele mulţimii Sn-1, vom fi la faza în care am ales
elementele x00, x00, ..., xn-20. Ar trebui să alegem un element din Sn-1, dar cum toate elementele din
Sn-1 au fost deja alese, revenim cu încă un pas în urmă pe cale, şi alegem un alt element xn-21 Sn-2.
Pe urmă reîncepem alegerea elementelor din Sn-1 cu primul dintre ele, xn-10 Sn-1. Vom avea
vectorul x = (x00, x10,..., xn-30, xn-21, xn-10).
Procedând în acest mod, vom ajunge practic să construim toţi vectorii x S, adică vom
explora întreg spaţiul soluţiilor posibile. Există în principiu două categorii de probleme: unele care
cer găsirea unei singure soluţii şi unele care cer găsirea tuturor soluţiilor. Atunci când se cere
găsirea unei singure soluţii, putem opri căutarea după găsirea primului vector x pentru care funcţia
Solutie returnează valoarea true. La cealaltă categorie de probleme este nevoie să parcurgem întreg
spaţiul soluţiilor pentru a găsi toate soluţiile.
Parcurgerea întregului spaţiu al soluţiilor posibile este foarte mare consumatoare de timp.
În general se poate accelera această parcurgere prin următoarea optimizare: pornind de la funcţia
Solutie(x0, x1, ..., xn-1), definim o funcţie Continuare(x0, x1, ..., xk) care să ne spună dacă, având
alese la un moment dat elementele x0, x1, ..., xk există o şansă de a se ajunge la o soluţie. Această
optimizare este foarte utilă deoarece de multe ori după alegerea câtorva elemente x0, x1, ..., xk ne
putem da seama că nu vom putea găsi nici o soluţie validă care conţine elementele respective. În
asemenea situaţii nu mai are rost să continuăm alegerea de elemente până la xn-1, ci putem direct să
revenim cu un pas în urmă pe cale şi să încercăm cu un alt element xk'.

8.2.2 Implementare
Algoritmul backtracking redactat sub formă de pseudocod arată în felul următor:
k = 0;

while (k >= 0)
{
do
{
* alege urmatorul x[k] din multimea S[k]
* evalueaza Continuare(x[1], x[2], ..., x[k])
}
while ( !Continuare(x[1], x[2], ..., x[k]) &&
(* mai sunt elemente de ales din multimea S[k]) )

if (Continuare(x[1], x[2], ..., x[k]))


{
if (k == n-1)
{
if (Solutie(x[1], x[2], ..., x[n]))
* afiseaza solutie
}
else
{
k = k + 1;
}

159
}
else
{
k = k - 1;
}
}

Dacă analizăm modul de funcţionare al algoritmului backtracking, vom vedea ca la orice


moment de timp ne este suficient un tablou de n elemente pentru a memora elementele x0, x1, ...,
xn-1 alese. Ca urmare în implementare declarăm un tablou x de dimensiune n. Tipul de date al
tabloului depinde de problemă, de tipul mulţimilor S.
Variabila k ne indică indicele mulţimii din care urmează să alegem un element. La început
de tot trebuie să alegem un element din mulţimea S0, de aceea îl iniţializăm pe k cu valoarea 0. Pe
urmă k se modifică în funcţie de modul în care avansăm sau revenim pe calea de căutare.
Pentru alegerea următorului xk din mulţimea Sk, ne bazăm pe faptul că între elementele
fiecărei mulţimi Sk există o relaţie de ordine. Dacă încă nu s-a ales nici un element din mulţimea
Sk atunci îl alegem pe primul conform relaţiei de ordine. Dacă deja s-a ales cel puţin un element,
atunci îl alegem pe următorul neales, conform relaţiei de ordine.

8.2.3 Exemple
Datorită faptului că algoritmul backtracking explorează întregul spaţiu al soluţiilor, orice
problemă se poate rezolva în ultimă instanţă folosind backtracking.
Un lucru care nu este întotdeauna evident este cum să formulăm problema în termenii
necesari pentru a o putea rezolva folosind backtracking.
Vom da în continuare două exemple de probleme care se pot rezolva cu backtracking.
Pentru fiecare din cele două exemple vom da întâi enunţul problemei, apoi vom arăta cum trebuie
el reformulat pentru a-l adapta unei rezolvări de tip backtracking, iar apoi vom prezenta codul
sursă care rezolvă problema.

Programul 8.4: Reginele pe tabla de şah.

Enunţ Avem o tablă de şah de dimensiune 8x8. Să se aşeze pe această tablă de şah 8
regine astfel încât să nu existe două regine care se atacă între ele.

Rezolvare Pentru ca să nu existe două regine care se atacă între ele, este evident că nu
avem voie să plasăm două regine pe aceeaşi linie. Deci pe fiecare din cele 8 linii ale tablei de şah
vom avea exact o singură regină. Ca urmare putem codifica o modalitate de plasare a reginelor pe
tablă în felul următor: pentru fiecare din cele 8 linii precizăm coloana în care se plasează regina de
pe respectiva linie. Spre exemplu o codificare pentru o modalitate de plasare a reginelor este: (linia
0, coloana 0), (linia 1, coloana 4), (linia 2, coloana 7), (linia 3, coloana 5), (linia 4, coloana 2),
(linia 5, coloana 6), (linia 6, coloana 1), (linia 7, coloana 3).
Cum pentru fiecare linie avem posibilitatea de a alege coloana din 8 variante posibile,
putem identifica elementele tipice unei probleme de tip backtracking. Să notăm cu C mulţimea {0,
1, 2, 3, 4, 5, 6, 7} din care putem alege coloanele pe care plasăm regine. Pentru fiecare linie de la 0

160
până la 7 trebuie să alegem câte un număr din mulţimea C. Cu alte cuvinte soluţia problemei
noastre o putem scrie sub forma unui vector c = (c0, c1, c2, c3, c4, c5, c6, c7), unde ci C, 0 < i < 8.
Spaţiul soluţiilor posibile este produsul cartezian S = C × C × C × C × C × C × C × C.
Funcţia Solutie trebuie să verifice dacă nu cumva există regine care se află pe aceeaşi
coloană sau dacă nu cumva există regine care se atacă pe diagonală. Verificarea este simplă.
Trebuie să verificăm că între elementele (c0, c1, c2, c3, c4, c5, c6, c7) nu există două care au aceeaşi
valoare. Aceasta ar însemna că avem două regine pe aceeaşi coloană. Pe urmă mai trebuie să
verificăm că i,j {0, 1, 2, 3, 4, 5, 6, 7}, |i-j| ≠ |ci-cj|. Aceasta este condiţia ca să nu existe două
regine care se atacă pe diagonală.
Funcţia Continuare va face aceleaşi verificări, dar nu pentru toate 8 reginele, ci doar pentru
cele k care au fost alese până la un moment dat. Principiul pe care ne bazăm este următorul: dacă
atunci când plasăm o regină pe tablă ea atacă una dintre reginele deja plasate, atunci în mod sigur
nu putem ajunge la o soluţie validă. Dacă în schimb noua regină nu atacă nici una din reginele de
pe tablă, atunci avem şanse să ajungem la o soluţie validă a problemei.

Cod sursă Redăm în continuare codul sursă în limbajul C pentru rezolvarea acestei
probleme.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

#define N 8
#define INVALID -1

int main(void)
{
/* In acest vector vom memora coloanele in care
se plaseaza reginele pentru fiecare linie.
Regina de pe linia "k" va fi plasata in
coloana "c[k]". */
int c[N];

int k, i;
int continuare, ataca;

/* Numaram si cate solutii sunt gasite. Pentru


o tabla de 8x8 exista 92 de solutii. */
int count = 0;

/* Initializam toate elementele din vectorul


"c" cu o valoare invalida, care nu apare
in multimea "C". Facem asta pentru a sti
ca inca nu am ales nici un element din nici
una din multimile produsului cartezian. */
for (i=0; i<N; i++)
c[i] = INVALID;

/* "k" indica indicele multimii din produsul


cartezian din care urmeaza sa alegem un

161
element. Prima alegere se face din multimea
"C[0]". */
k = 0;

/* Cat timp indicele "k" mai indica spre una


din multimile produsului cartezian, mai
avem elemente de ales. Cand "k" devine "-1",
inseamna ca am epuizat toate caile de cautare. */
while (k >= 0)
{
/* Alegem pe rand elemente din multimea "C[k]"
si evaluam pentru fiecare alegere functia
"Continuare". Ne vom opri fie atunci cand
am epuizat toate elementele din multimea
"C[k]", fie atunci cand intalnim un element
pentru care functia returneaza "true". */
do
{
/* Daca elementul "c[k]" este setat pe
INVALID, inseamna ca nu am ales inca
nici un element din multimea "C[k]"
(adica nu s-a incercat plasarea reginei
de pe linia "k"). Incepem prin a alege
primul element (plasam regina de pe
linia "k" pe coloana 0). */
if (c[k] == INVALID)
c[k] = 0;
/* Daca "c[k]" nu este setat pe INVALID,
inseamna ca am ales deja cel putin un
element din multimea "C[k]", deci
continuam alegerea cu urmatorul element
(incercam sa plasam regina pe urmatoarea
coloana de pe linia "k"). */
else
c[k]++;

/* Daca nu am depasit numarul de coloane de


pe linie, atunci trecem la evaluarea
functiei "Continuare". */
if (c[k] < N)
{
/* Ne asiguram ca regina pe care vrem
sa o plasam pe linia "k" si coloana
"c[k]" nu ataca nici una din reginele
deja plasate. */
ataca = 0;
for (i=0; !ataca && (i<k); i++)
{
/* Verificam daca mai exista o regina
pe aceeasi coloana. */
if (c[i] == c[k])
ataca = 1;

162
/* Verificam daca exista o regina pe
care noua regina o ataca pe
diagonala. */
else if (abs(i-k) == abs(c[i]-c[k]))
ataca = 1;
}

/* Daca noua regina nu ataca nici una din


reginele plasate anterior, atunci
functia "Continuare" returneaza "true",
adica 1. */
if (!ataca)
continuare = 1;
/* Altfel functia returneaza "false",
adica 0. */
else
continuare = 0;
}

/* Daca s-a depasit numarul coloanelor de pe


o linie, functia "Continuare" returneaza
automat "false". */
else
{
continuare = 0;
}
}
while (!continuare && (c[k] < N));

/* Daca am obtinut "true", adica "1", din functia


"Continuare", atunci consideram regina plasata
si continuam algoritmul. */
if (continuare)
{
/* Daca am plasat toate N reginele, atunci
afisam solutia gasita. Nu mai este nevoie
sa calculam rezultatul functiei "Solutie",
deorece rezultatele cumulate ale functiilor
"Continuare" ne garanteaza ca solutia este
corecta. */
if (k == N-1)
{
for (i=0; i<N; i++)
printf("%d ", c[i]);
printf("\n");
count++;
}

/* Daca nu am plasat toate reginele, trecem cu


un pas inainte, la urmatoarea multime din

163
produsul cartezian, si incercam plasarea
urmatoarei regine. */
else
{
k++;
}
}

/* Daca functia "Continuare" a returnat "false",


inseamna ca s-au epuizat toate variantele de
plasare a reginei de pe linia "k". Revenim cu
un pas inapoi pe calea de cautare, la regina
"k-1". Inainte de revenire marcam cu INVALID
elementul "c[k]". Facem aceasta pentru ca atunci
cand vom ajunge din nou la regina "k" sa stim
sa incepem plasarea ei de la coloana 0. */
else
{
c[k] = INVALID;
k--;
}
}

printf("%d solutii\n", count);


return 0;
}

Programul 8.5: Săritura calului pe tabla de şah.

Enunţ Avem o tablă de şah de dimensiune 8x8. Să se gasească toate modalităţile de a


deplasa un cal pe această tablă, astfel încât calul să treacă prin toate căsuţele de pe tablă exact o
dată.

Rezolvare Pentru a parcurge fiecare căsuţă de pe tabla de şah exact o dată, calul va trebui
să facă exact 8 × 8 = 64 de paşi. La fiecare pas el poate alege oricare din cele 64 de căsuţe de pe
tablă. Să codificăm căsuţele de pe tabla de şah în modul următor: căsuţa de la linia i şi coloana j o
notăm prin perechea (i,j). Să notăm mulţimea tuturor căsuţelor de pe tablă cu C: C = {(0,0), (0,1),
..., (0,7), (1,0), ..., (7,7)}.
O soluţie a problemei o putem nota printr-un vector x = (x0, x1, ..., x63), unde xS = C × C
× C × ... × C (produs cartezian în care mulţimea C apare de 64 de ori), iar xi C,  i {0, 1, ...,
63}.
Cu aceste elemente putem vedea că se poate aplica o rezolvare de tip backtracking. Funcţia
Solutie va verifica să nu existe două elemente ci şi cj care au aceeaşi valoare, deoarece asta ar
însemna că s-a trecut de două ori prin aceeaşi căsuţă. În plus funcţia mai trebuie să verifice faptul
că i {0, 2, ..., 62, 63} calul poate sări de la căsuţa ci la căsuţa ci+1. Asta înseamnă că fie ci şi ci+1
se află la două linii distanţă şi la o coloană distanţă, fie ele se află la o linie distanţă şi la două
coloane distanţă.

164
Funcţia Continuare trebuie să facă exact aceleaşi verificări ca şi funcţia Solutie, dar nu
pentru toate 64 de căsuţe ci pentru cele k căsuţe care au fost alese până la un moment dat.

Cod sursă În continuare prezentăm codul sursă pentru rezolvarea acestei probleme.
#include <stdio.h>
#include <conio.h>
#include <stdlib.h>

/* Dimensiunea tablei de sah definita ca si constanta.


Pentru o tabla de dimensiune 8x8 gasirea solutiilor
dureaza foarte mult, de aceea lucram pe o tabla de
5x5 unde solutiile sunt gasite mult mai repede. */
#define N 5

#define INVALID -1

int main(void)
{
/* Pentru o tabla de dimensiune N vom memora
solutiile intr-un vector de dimensiune N*N.
Fiecare element din vector va fi la randul lui
un vector cu doua elemente, primul element va
memora linia de pe tabla, iar al doilea element
va memora coloana de pe tabla. */
int c[N*N][2];

int k, i;
int pe_tabla, continuare;
int delta_l, delta_c;

/* Numaram si cate solutii sunt gasite. */


int count = 0;

/* Pentru inceput marcam toate elementele


vectorului "c" cu INVALID, semn ca nu am
ales nici un element din multimile
produsului cartezian. */
for (i=0; i<N*N; i++)
{
c[i][0] = INVALID;
c[i][1] = INVALID;
}

k = 0;
while (k >= 0)
{
/* Incercam sa plasam mutarea "k" a
calului in fiecare casuta a tablei
de joc, pe rand. Evaluam la fiecare
alegere functia "Continuare". Ne

165
oprim fie atunci cand am incercat
toate casutele de pe tabla, fie
atunci cand gasim o casuta unde
functia "Continuare" returneaza
"true". */
do
{
/* Aici alegem urmatorul element
din multimea "C[k]". Daca elementul
"c[k]" este setat pe INVALID,
inseamna ca inca nu am ales nici
un element din multimea curenta,
deci alegem primul element (plasam
calul in casuta de la linia 0 si
coloana 0). */
if (c[k][0] == INVALID)
{
c[k][0] = 0;
c[k][1] = 0;
pe_tabla = 1;
}
/* Daca elementul "c[k]" nu este setat
pe invalid, inseamna ca deja am ales
o casuta din multimea "C[k]". Acum
alegem urmatoarea casuta de pe tabla.
Cu alte cuvinte incercam sa plasam
calul in urmatoarea casuta. Daca este
posibil incercam sa ramanem pe aceeasi
linie si sa ne deplasam cu o coloana
spre dreapta. */
else if (c[k][1] < N-1)
{
c[k][1]++;
pe_tabla = 1;
}
/* Daca cumva eram chiar la ultima casuta
din linie, atunci alegem prima casuta
din linia urmatoare. Ne asiguram ca nu
eram cumva si pe ultima linie a
tablei, caz in care am epuizat toate
casutele. */
else if (c[k][0] < N-1)
{
c[k][1] = 0;
c[k][0]++;
pe_tabla = 1;
}
/* Daca eram pe ultima linie a tablei,
atunci am epuizat toate casutele.
Marcam acest lucru setand variabila
"pe_tabla" pe 0. */
else

166
{
pe_tabla = 0;
}

/* Daca casuta "c[k]" aleasa este valida


(se afla pe tabla de joc), atunci
trecem la calculul functiei
"Continuare". */
if (pe_tabla)
{
/* Daca suntem la prima mutare a
calului, atunci mutarea este
valida oriunde ar fi ea pe
tabla. */
if (k == 0)
continuare = 1;
/* Daca nu suntem la prima mutare,
atunci trebuie sa facem o serie
de verificari. */
else
{
/* In primul rand verificam daca
de la pozitia precedenta a
calului pe tabla ("c[k-1]")
se poate ajunge in pozitia
aleasa acum printr-o mutare.
*/
delta_l = abs(c[k-1][0]-c[k][0]);
delta_c = abs(c[k-1][1]-c[k][1]);
continuare = (((delta_l == 1) &&
(delta_c == 2)) ||
((delta_l == 2) &&
(delta_c == 1)));

/* Si apoi verificam daca nu


cumva s-a mai trecut prin
casuta aleasa acum. */
for (i=0; continuare && (i<k); i++)
{
if ((c[i][0] == c[k][0]) &&
(c[i][1] == c[k][1]))
continuare = 0;
}
}
}
/* Daca casuta "c[k]" aleasa este in
afara tablei de sah, atunci functia
"Continuare" va returna automat
"false". */
else
{
continuare = 0;

167
}
}
while (!continuare && pe_tabla);

/* Daca am obtinut rezultat pozitiv in urma


verificarilor de "Continuare", atunci
consideram piesa asezata la pozitia "c[k]"
si continuam cautarea. */
if (continuare)
{
/* Daca s-a parcurs toata tabla de sah
atunci afisam solutia. */
if (k == N*N - 1)
{
for (i=0; i<N*N; i++)
printf("(%d,%d) ", c[i][0],
c[i][1]);
printf("\n");
count++;
}
/* Daca nu s-a parcurs inca toata tabla
atunci trecem cu un pas inainte pe
calea de cautare. */
else
{
k++;
}
}

/* Daca casuta aleasa nu este valida, atunci


marcam elementul "c[k]" cu INVALID si
revenim cu un pas inapoi pe calea de
cautare. */
else
{
c[k][0] = INVALID;
c[k][1] = INVALID;
k--;
}
}

printf("%d solutii\n", count);


return 0;
}

Optimizare Putem face o optimizare la programul prezentat, pornind de la faptul că se


cunosc regulile după care se poate deplasa calul pe tabla de şah. La alegerea din mulţimea Ck, în
loc să alegem pe rând fiecare căsută şi apoi să verificăm dacă s-ar putea face mutare în căsuţa
respectivă, e mai eficient să alegem direct dintre căsuţele în care se poate face mutare. Pentru a
putea determina aceste căsuţe, folosim doi vectori care definesc mutările posibile ale calului
(numărul de căsuţe cu care se poate deplasa pe orizontală şi pe verticală). Prezentăm mai jos codul

168
sursă care implementează această optimizare. Implementarea este una recursivă, pentru a arăta că
modul de lucru al algoritmului backtracking se pretează în mod natural la implementări recursive.

#include <stdio.h>
#include <conio.h>

#define N 5

int dx[8] = {-1, -2, -2, -1, 1, 2, 2, 1};


int dy[8] = {-2, -1, 1, 2, 2, 1, -1, -2};

int c[N*N][2];
int count = 0;

void back(int pas)


{
int i, j, continuare;

if (pas == N*N)
{
for (i=0; i<pas; i++)
printf("(%d,%d) ", c[i][0], c[i][1]);
printf("\n");
count++;
}
else
{
for (i=0; i<8; i++)
{
c[pas][0] = c[pas-1][0] + dy[i];
c[pas][1] = c[pas-1][1] + dx[i];

if ((c[pas][0]>=0) && (c[pas][0]<N) &&


(c[pas][1]>=0) && (c[pas][1]<N))
{
continuare = 1;
for (j=0; continuare && (j<pas); j++)
{
if ((c[j][0] == c[pas][0]) &&
(c[j][1] == c[pas][1]))
continuare = 0;
}

if (continuare)
back(pas+1);
}
}
}
}

169
int main(void)
{
int i,j;
for (i=0; i<N; i++)
for (j=0; j<N; j++)
{
c[0][0] = i;
c[0][1] = j;
back(1);
}
printf("%d solutii\n", count);
return 0;
}

8.3 Metoda Divide and Conquer


8.3.1 Consideraţii teoretice
Explicarea numelui
Numele acestei metode de rezolvare a problemelor de programare provine din dictonul
latin divide et impera. Semnificaţia este următoarea: atunci când avem de rezolvat o problemă pe
care, din diverse motive, o considerăm greu de rezolvat, o împărţim în mai multe subprobleme
care să se poată rezolva mai uşor. După rezolvarea subproblemelor vom combina soluţiile lor
pentru a obţine soluţia întregii probleme.

Aplicabilitatea algoritmilor de tip Divide and Conquer


Metoda de rezolvare Divide and Conquer se poate aplica la problemele care se pot
descompune în subprobleme de aceeaşi natură cu problema principală, dar de dimensiuni mai
mici.
La unele probleme această posibilitate de descompunere în subprobleme de acelaşi tip este
evidentă. Vom vedea însă că sunt şi probleme unde o asemenea descompunere nu apare de la
prima vedere.

Modul de lucru al algoritmilor de tip Divide and Conquer


Se poate pune întrebarea ”cum rezolvăm subproblemele?”. Răspunsul este: ”în acelaşi mod
în care am rezolvat problema principală”. Metoda Divide and Conquer se pretează foarte bine la
implementări recursive. Din moment ce ştim să împărţim problema principală în subprobleme, ce
ne opreşte să facem acelaşi lucru cu fiecare subproblemă în parte? Putem împarţi fiecare
subproblemă în subsubprobleme, pe care la răndul lor le impărţim în subsubsubprobleme, ş.a.m.d.
Când ne oprim cu aceste împărţiri recursive? Atunci când ajungem la subprobleme de
dimensiuni atât de mici încât rezolvarea lor este trivială.

170
8.3.2 Implementare
În general implementarea metodei Divide and Conquer se face prin funcţii recursive. De
regulă vom avea o singură funcţie care primeşte ca parametri informaţiile necesare pentru a
rezolva o subproblemă şi returnează rezultatele pentru subproblema respectivă.
Funcţia va determina dacă subproblema este una trivială, caz în care va calcula direct
soluţia pentru ea. Dacă subproblema nu este una trivială, atunci funcţia va împărţi subproblema în
subsubprobleme şi se va auto-apela în mod recursiv pentru fiecare din ele. Pe urmă va combina
rezultate obţinute pentru subsubprobleme şi va găsi soluţia pentru subproblemă.

Prezentăm schema generală de lucru a unei asemena funcţii, în pseudocod:


function divide(* parametri care definesc o subproblema)
{
if (* subproblema este una triviala)
{
* rezolva subproblema in mod direct
* returneaza rezultatele
}
else
{
* imparte subproblema in subsubprobleme
* pentru fiecare subsubproblema
* apeleaza divide(subsubproblema)

* combina rezultatele subsubproblemelor


* returneaza rezultatele pentru subproblema
}
}

Pentru a rezolva problema principală, tot ce trebuie făcut este să se apeleze funcţia
recursivă cu acei parametri care definesc problema principală.

8.3.3 Exemple
Programul 8.6: Turnurile din Hanoi.

Enunţ Fie trei tije notate cu a, b şi c. Pe tija a se află n discuri de dimensiuni diferite,
aşezate în ordinea descrescătoare a diametrelor, astfel încât discul cu diametrul cel mai mare se
află cel mai jos, iar discul cu diametrul cel mai mic se află în vârful stivei. Să se găsească o
modalitate de a muta toate cele n discuri de pe tija a pe tija b, folosind tija intermediară c, atfel
încât în final discurile să fie ordonate tot descrescător. În timpul operaţiilor care se fac, este
interzisă plasarea unui disc mai mare peste un disc mai mic.

171
Rezolvare Problema noastră iniţială este să mutăm n discuri de pe tija a pe tija b, folosind
tija intermediară c. O putem codifica în felul următor: (n,a,b,c). Dacă am găsi o modalitate de a
muta n-1 discuri de pe tija a pe tija intermediară c, atunci am putea să mutăm discul cel mai mare
de pe tija a pe tija b. Pe urmă ar trebui să aducem cele n-1 discuri de pe tija c pe tija b şi problema
ar fi rezolvată. Pentru a muta n-1 discuri de pe tija a pe tija c, putem folosi ca tijă intermediară tija
b. La fel, pentru a muta înapoi cele n-1 discuri de pe tija c pe tija b, putem folosi ca tijă
intermediară tija a.
Putem reformula cele zise mai sus în felul următor: problema (n,a,b,c) se rezumă la
problema (n-1,a,c,b), urmată de mutarea discului de diametru maxim de pe a pe b, urmată de
problema (n-1,c,b,a).

Implementare Implementarea se face printr-o funcţie recursivă. Funcţia primeşte patru


parametri: numărul de discuri de pe tija iniţială, tija initială, tija finală şi tija intermediară. Se
descompune problema in subprobleme, în modul descris mai sus. Cazul trivial este acela când
avem de mutat un singur disc şi în această situaţie discul este mutat direct.

Cod sursă Prezentăm în continuare codul sursă în limbajul C care rezolvă problema:

#include <stdio.h>

/* Functie care muta n discuri de pe tija initiala


pe tija finala, folosind o tija intermediara.
Rezolvarea se face in maniera Divide and Conquer. */
void hanoi(int n, char t_initial, char t_final,
char t_intermediar)
{
/* Daca avem mai mult de o tija de mutat,
atunci descompunem problema in subprobleme. */
if (n > 1)
{
hanoi(n-1, t_initial, t_intermediar, t_final);
printf("%c -> %c\n", t_initial, t_final);
hanoi(n-1, t_intermediar, t_final, t_initial);
}
/* Daca avem un singur disc de mutat, atunci
il mutam direct. La acest nivel problema are
o rezolvare triviala. */
else
{
printf("%c -> %c\n", t_initial, t_final);
}
}

int main(void)
{
/* Numarul de discuri. */
int n;

/* Citim numarul de discuri de la tastatura. */


printf("Introduceti numarul de discuri:");

172
scanf("%d", &n);

/* Apelam functia recursiva. */


hanoi(n, 'a', 'b', 'c');

return 0;
}

Programul 8.7: Determinarea minimului şi maximului dintr-un şir de numere.

Enunţ Se dă un şir de n numere reale {x 0, x1, ..., xn-1}. Să se determine valoarea minimă şi
valoarea maximă din acest şir de numere.

Rezolvare Metoda imediată de rezolvare este parcurgerea întregului şir şi inspectarea


fiecărui element de două ori, o dată pentru aflarea minimului şi a doua oară pentru aflarea
maximului. Codul sursă în limbajul C pentru această metodă imediată este:
#include <stdio.h>

/* Declaram sirul de numere direct din cod. Alternativ


el poate fi citit de la tastatura sau din fisier. */
#define N 10
int x[] = {10, 5, 23, -11, 4, 2, 0, -6, 66, 40};

int main(void)
{
/* Folosim doua variabile pentru a stoca minimul
si maximul gasite. */
int min, max;

/* Vom contoriza numarul de comparatii care se fac


pentru gasirea minimului si maximului. */
int comp = 0;

int i;

/* Afisam sirul de numere. */


printf("Avem %d numere.\n", N);
for (i=0; i<N; i++)
printf("%d ", x[i]);
printf("\n\n");

/* Initializam minimul si maximul cu prima valoare


din sir. */
min = x[0];
max = x[0];

/* Parcurgem intreg sirul si actualizam minimul si


maximul atunci cand e cazul. */
for (i=1; i<N; i++)

173
{
/* Facem o comparatie pentru minim. */
comp++;
if (min > x[i])
min = x[i];

/* Si o comparatie pentru maxim. */


comp++;
if (max <x[i])
max = x[i];
}

/* Afisam rezultatele. */
printf("Minimul este %d.\n", min);
printf("Maximul este %d.\n", max);
printf("Comparatii facute: %d.\n", comp);

return 0;
}

Dacă analizăm metoda de mai sus, vom vedea că ea face comparaţii inutile, deoarece orice
element care este candidat pentru minim nu poate fi în acelaşi timp candidat pentru maxim, şi
invers. Deci este redundant să testăm fiecare element în parte atât pentru minim cât şi pentru
maxim.
Putem aplica tehnica Divide and Conquer, împărţind şirul de numere în două părţi.
Determinăm minimul şi maximul pentru fiecare din cele două părţi, iar pe urmă determinăm
maximul global prin compararea celor două maxime parţiale, iar minimul global prin compararea
celor două minime parţiale.

Implementare Pentru implementare vom defini o funcţie recursivă ce va căuta minimul şi


maximul într-o secvenţă a şirului. Iniţial vom apela această funcţie pentru întregul şir. Funcţia se
va apela pe ea însăşi, recursiv, pentru jumătarea stângă şi pentru jumătatea dreaptă a secvenţei.

Cod sursă Prezentăm în continuare codul sursă în limbajul C pentru rezolvarea problemei.
#include <stdio.h>

/* Declaram sirul de numere direct din cod. Alternativ


el poate fi citit de la tastatura sau din fisier. */
#define N 10
int x[] = {10, 5, 23, -11, 4, 2, 0, -6, 66, 40};

/* Numaram cate comparatii se fac in total. */


int comp = 0;

/* Functie care determina minimul si maximul dintr-o


secventa a sirului de numere. Secventa este
delimitata de indicii "st" si "dr". Valorile minime
si maxime gasite vor fi returnate prin pointerii
"min" si "max" primiti ca si parametru. */

174
void minmax(int st, int dr, int *min, int *max)
{
int mijloc, min_st, max_st, min_dr, max_dr;

printf("Caut in secventa [%d..%d].\n", st, dr);

/* Daca secventa contine un singur numar, atunci


el este atat minim cat si maxim. */
if (st == dr)
{
*min = x[st];
*max = x[st];
}
/* Daca secventa contine doua numere, atunci
facem o comparatie pentru a gasi minimul si
maximul. */
else if (st == dr - 1)
{
comp++;
if (x[st] < x[dr])
{
*min = x[st];
*max = x[dr];
}
else
{
*min = x[dr];
*max = x[st];
}
}
/* Daca avem mai multe numere, atunci divizam
problema in subprobleme. */
else
{
/* Divizare. */
mijloc = (st + dr) / 2;
minmax(st, mijloc, &min_st, &max_st);
minmax(mijloc+1, dr, &min_dr, &max_dr);

/* Combinarea rezultatelor partiale.


Comparam minimele partiale intre ele
si maximele partiale intre ele. */
comp++;
if (min_st < min_dr)
*min = min_st;
else
*min = min_dr;
comp++;
if (max_st > max_dr)
*max = max_st;
else
*max = max_dr;

175
}
}

int main(void)
{
int min, max;
int i;

/* Afisam sirul de numere. */


printf("Avem %d numere.\n", N);
for (i=0; i<N; i++)
printf("%d ", x[i]);
printf("\n\n");

/* Apelam functia recursiva. */


minmax(0, N-1, &min, &max);

/* Afisam rezultatele. */
printf("\n");
printf("Minimul este %d.\n", min);
printf("Maximul este %d.\n", max);
printf("Comparatii facute: %d.\n", comp);

return 0;
}

Dacă rulăm în paralel cele două programe, vom vedea că întradevăr pentru acelaşi şir de
numere metoda Divide and Conquer face mai puţine comparaţii decât metoda clasică.

8.3.4 Observaţii
După cum am văzut şi în exemplele prezentate, metoda Divide and Conquer se poate
implementa într-un mod foarte elegant prin funcţii recursive.
Este posibil să implementăm metoda şi fără a folosi funcţii recursive, dar atunci avem
nevoie de structuri de date suplimentare (stive) pentru a memora rezultatele subproblemelor. La
implementările nerecursive eleganţa codului scade.

8.4 Întrebări şi probleme


1. Care este proprietatea comună a soluţiilor posibile în metoda Greedy?
2. În ce constă esenţa metodei Backtracking?
3. Care este principiul de bază al tehnicii divizării?
4. Să se realizeze programul de găsire a tuturor soluţiilor de plasare a 8 turnuri pe tabla de şah,
astfel încât nici un turn să nu fie în priza altuia.

176
5. Să se scrie un program care citeşte o listă de n persoane care trebuie cazate în m camere de
hotel. Pentru fiecare cameră se specifică numărul de paturi. Să se afişeze toate posibilităţile de
cazare ale celor n persoane în cele m camere.
6. Se citesc, de pe mediul de intrare, două numere întregi M şi N, cu N maxim 20. În continuare, se
mai citesc încă N perechi de numere întregi (Xi şi Yi), reprezentând valoarea unei monede şi
respectiv numărul de monede. Să se realizeze acoperirea sumei M cu monede dintre cele
existente. Dacă nu există soluţie, se va afişa un mesaj corespunzător. Se recomandă utilizarea
unui algoritm cu revenire combinat cu o strategie de tip Greedy.
7. În vederea deplasării la un turneu, trebuie stabilită modalitatea de plasare a parti-cipanţilor în
vehiculele cu care se face deplasarea. Se precizează ca date iniţiale:
 numărul de participanţi şi numele fiecărui participant;
 numărul de vehicule, iar pentru fiecare dintre acestea, numărul de înmatriculare şi numărul de
locuri disponibile.
Se cere să se afişeze toate posibilităţile de plasare a pasagerilor în vehicule.
8. Se consideră o matrice de 55 elemente. Ea poate fi completată cu valorile 0 sau 1. Să se
completeze în aşa fel încât suma elementelor de pe linii şi de pe coloane să fie 1 pentru orice
linie sau coloană. Se vor afişa toate soluţiile posibile.
9. Se consideră un grup de N ţări care apar pe aceeaşi foaie de hartă. Se cere să se scrie programul
care stabileşte culoarea asociată fiecărei ţări, astfel încât două ţări vecine să nu fie colorate la
fel.
Ca date de intrare se vor da:
 numărul N de ţări;
 lista culorilor posibile;
 lista celor N ţări (denumire, listă vecini).

10. Un dresor trebuie să scoată din arenă M lei şi N tigri, astfel încât să nu scoată doi tigri unul
după altul. Scrieţi un program care citeşte valorile M şi N, şi determină toate posibilităţile de
soluţionare a problemei dresorului.
11. Se citesc informaţii despre n băieţi şi n fete. Acestea conţin: nume, culoare preferată (roşu,
portocaliu, galben, verde, albastru, indigo, violet), muzica preferată (operă, simfonică, pop,
rock, country), temperament (flegmatic, melancolic, sanguin, coleric).
Să se scrie un program care stabileşte perechile pentru un concurs de dans, astfel încât
băiatul şi fata care compun o pereche să aibă aceleaşi preferinţe la muzică şi culori şi
temperamente diferite. Dacă nu se poate, atunci să fie respectate numai două criterii sau cel
puţin unul.
12. Se dă un fişier “LABIRINT.DAT” cu 24 de linii, fiecare formată din 80 de caractere, care
reprezintă harta unui labirint. Semnificaţia caracterelor este următoarea:
 “P” – perete;
 “.” – loc liber.

177
Să se scrie un program recursiv pentru găsirea unui drum de ieşire din labirint, plecând
din punctul (12, 40).
Pentru memorarea drumului se va folosi un tablou.
13. O matrice cu M linii şi N coloane descrie starea de ocupare a unei parcări, la care lipsesc,
deocamdată, marcajele pentru culoarele de circulaţie (adică fiecare maşină poate parca oriunde
găseşte loc liber). Valorile pentru M şi N nu depăşesc 100, iar elementele matricii pot fi 0 sau 1, cu
semnificaţia de “loc liber” şi respectiv “loc ocupat”. Accesul în parcare (intrarea şi ieşirea) se
poate face numai prin dreptul locaţiei de coordonate (1,1). Să se realizeze programul care citeşte,
dintr-un fişier, matricea de ocupare şi determină numărul şi poziţia maşinilor blocate (care nu
pot ieşi imediat) din parcare.
14. Se consideră un triunghi format din linii, pe fiecare linie i sunt i numere întregi pozitive, ca în
exemplul de mai jos:

7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

Să se scrie un program care calculează cea mai mare sumă a numerelor aflate pe un drum
care leagă vârful de sus al triunghiului cu baza. Drumul este astfel construit încât la fiecare pas
se coboară pe diagonală, spre stânga sau spre dreapta.
Exemplu: pentru triunghiul de mai sus, drumul căutat este:
7 – 3 – 8 – 7 –5
15. La o grădiniţă s-au strâns din sponsorizări diferite obiecte în vederea alcătuirii unor pachete cu
cadouri pentru copii. Deoarece obiectele sunt foarte variate, se pune problema găsirii unei
modalităţi optime de alcătuire a pachetelor de cadouri, astfel încât să se realizeze o alcătuire
echitabilă. Fiecare pachet trebuie să conţină obiecte cu o valoare totală stabilită. De asemenea, se
stabileşte un număr maxim de obiecte care pot fi incluse într-un pachet, pentru a evita
includerea într-un pachet a unui număr foarte mare de obiecte de valoare mică.
Să se scrie un program care citeşte dintr-un fişier text următoarele date de intrare:
 pe prima linie, numărul N de obiecte;
 pe următoarele N linii, valoarea fiecărui obiect;
 valoarea totală a unui pachet;
 numărul maxim de obiecte care pot fi incluse într-un pachet.
Programul va afişa toate variantele de alcătuire a pachetelor cu cadouri. Pentru fiecare
variantă, se va afişa numărul de pachete rezultate şi numărul de obiecte rămase neincluse în
pachete.

16. Să se implementeze următoarea optimizarea la problema comis-voiajorului: se va considera


fiecare oraş pe rând ca oraş de start şi se va păstra cea mai bună soluţie obţinută astfel.

178
17. Partiţionarea unui număr în sumă de numere: Se dă un număr natural N. Să se găsească toate
modalităţile de partiţiona numărul N în sumă de numere naturale.

Spre exemplu numărul N = 4 se poate partiţiona în 4 moduri diferite: 4 = 1 + 1 + 1 + 1, 4 =


2 + 1 + 1, 4 = 2 + 2, 4 = 3 + 1.

Atenţie Când veţi încerca să reformulaţi problema în termenii specifici unei rezolvări
backtracking, veţi vedea că vectorii x nu vor avea lungime fixă. Chiar sî în exemplul dat vedem că
există partiţionări cu 4, cu 3 şi cu 2 elemente. Acesta este un exemplu de problemă în care vectorii
soluţie x au lungime variabilă.

18. Turnurile din Hanoi nerecursiv. Să se scrie o implementare nerecursivă pentru soluţia la
problema turnurilor din Hanoi prezentată în § 8.3.

19. Determinarea nerecursivă a minimului şi maximului. Să se scrie o implementare nerecursivă


pentru determinarea minimului şi a maximului dintr-un şir de numere. Se va utiliza tehnica Divide
and Conquer prezentată în § 8.3, cu singura diferenţă că nu este permisă utilizarea funcţiilor
recursive.

179

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