Documente Academic
Documente Profesional
Documente Cultură
tipul-rezultatului numele-functiei(declaratiile-parametrilor)
{
declaratii
instructiuni
}
int main()
{
int i;
for (i=0; i<10; ++i)
printf(“%d %d %d\n”, i, putere(2,i), putere(-3,i));
return 0;
}
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.
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;
}
9
Revenind acum la forma generală a definiţiei unei funcţii:
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));
}
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.
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.
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.
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.
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ă.
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.
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.
Exemple:
register int x;
register char c;
Exemplu:
f(register unsigned m, register long n)
{
register int i;
….
}
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.
Exemplu:
int cautbin(int x, int v[ ], int n) {
int prim = 0;
int ultim = n – 1;
int mijl;
...
}
int zile[]={31,28,31,30,31,30,31,31,30,31,30,31};
17
Tablourile de caractere se pot iniţializa cu un şir de caractere, astfel:
în loc de
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.
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;
}
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
<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:
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).
Exemple:
int x[ ]={1,2,3};
int zile[ ]={31,28,31,30,31,30,31,31,30,31,30,31};
în loc de
Dimensiunea tabloului este în acest caz 6 (5 caractere propriu-zise plus terminatorul ‘\0’).
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ă.
#define N 10
TIP t[N]; /* tablou de N elemente */
int i; /* variabila contor */
for(i=0; i<n; i++)
*prelucrare t[i]
#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”);
}
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.
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);
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;
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);
#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.
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.
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.
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;
}
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);
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.
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).
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.
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.
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).
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).
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;
}
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) {...}
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];
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.
Programul 2.10: Funcţie pentru determinarea lungimii unui şir de caractere –varianta 2.
38
void tiparire4(int *tab, int N)
{
int i;
for (i=0; i<N; i++, tab++)
printf("%d ", *tab);
}
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”;
39
Există o deosebire importantă între următoarele definiţii (fig. 2.5):
- 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++;
}
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’.
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).
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ă.
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”);
}
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.
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).
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;
}
44
Programul 2.24: Numărul de apariţii ale unui subşir.
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
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.
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.
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));
}
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
..........................
}
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.
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.
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;
}
}
50
Funcţia preialinie este cea descrisă anterior.
ptrlinie fiind el însuşi numele unui tablou, poate fi tratat ca un pointer, iar funcţia
tiparestelinii poate fi scrisă şi altfel:
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ă.
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
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.
52
Exemplu 1: tablou de pointeri
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.
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.
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).
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.
56
afişarea este
a: ****
e: ***
i: **
o: *
u:
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.
50. Să se citească două texte. Să se determine vocala care apare de cele mai multe ori în fiecare
text.
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.
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ă:
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
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);
60
struct drept {
struct punct pt1;
struct punct pt2;
}
Structura drept conţine două structuri punct.
Dacă declarăm variabila ecran ca:
struct angajat
{
char nume[64];
int varsta;
int categ_salarizare;
float salariu;
unsigned nr_marca;
} angajat_info, nou_angajat, fost_angajat;
Exemplu:
struct student {char* nume; float medie;}
candidat = {“Gigel”, 8.6};
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.
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);
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;
}
Declaraţia
struct punct *pp;
Exemplu:
struct punct origine, *pp;
.....
pp = &origine;
.....
printf(“originea este (%d,%d)\n”, (*pp).x, (*pp).y);
p->membru-al-structurii
d.pt1.x
dp->pt1.x
(d.pt1).x
(dp->pt1).x
63
Exemple vizând prioritatea operatorilor:
Î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.
Definim structura:
struct cheie
{
char *cuvant;
int rez;
} 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
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;
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:
Î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))
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:
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.
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:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
typedef struct {
char *nume;
int nr;
} student;
void eroare(void) {
puts(“\n **** eroare alocare dinamica de memorie ****”);
exit(1);
}
68
strcpy(t->nume, sir);
printf(“\n numar matricol: “);
scanf(“%d”, &t->nr);
}
}
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.
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;
Concluzie: o variabilă de tip union nu poate avea, la un moment dat, decât un singur
tip: tipul membrului activ în acel moment.
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;
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.
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.
struct meteo {
unsigned int ziua : 5;
unsigned int luna : 4;
unsigned int ploaie :1;
unsigned int soare :1;
unsigned int ninsoare :1;
} m;
m.ziua=15;
m.luna=5;
m.ploaie=1;
m.soare=1;
m.ninsoare=0;
O structură poate conţine atât câmpuri de date normale cât şi câmpuri de biţi.
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.
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:
77
4
Proiectarea şi dezvoltarea
sistematică a programelor de mari
dimensiuni
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.
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:
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>
persoana * citire(void){
persoana *plista;
// citeste datele si le pune in lista
81
// va returna pointerul catre lista de persoane // creata
}
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>
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;
}
afiseaza_top(melodie *ptop,i){
// afiseaza lista de melodii primita ca parametru
}
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 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
85
char * melodii[5]; // un tablou de 5 pointeri catre cele 5
// melodii preferate preferinte
} persoana;
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;
}
87
}
else ptop[poz].punctaj++;
}
*nm=k;
return ptop;
}
88
plista[i].melodii[0]);
}
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
}
89
5. Prin ce se caracterizează elaborarea programelor prin rafinări succesive ?
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ă.
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:
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
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
Î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.
isalpha(c) - returnează adevărat (valoare nenulă) dacă caracterul c este o literă, respectiv
fals (zero) în caz contrar.
isspace(c) - returnează adevărat dacă c este un caracter de spaţiere (spaţiu, tab, linie
nouă).
93
Funcţii de transformare a şirurilor
Următoarele funcţii realizează conversia unui şir de caractere la real, întreg, respectiv
long:
Există şi funcţii care realizează conversia inversă, din format numeric în şir de caractere.
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.
char sir[10];
int n;
float x;
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:
~ negare ~e
& şi e1 & e2
^ sau exclusiv e1 ^ e2
| sau e1 | e2
x 1 0 0 1 0 1 1 1
~x 0 1 1 0 1 0 0 0
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.
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.
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:
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.
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.
Testul dacă o anumită literă este prezentă în mulţime implică verificarea valorii bitului
corespunzător.
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.
98
5.3 Argumente în linia de comandă
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.
Programul 5.2: Program care îşi afişează parametrii primiţi în linia de comandă.
#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ă:
produce ieşirea:
Observaţie:
Redirectările de intrare şi ieşire specificate în linia de comandă nu se consideră parametri în
linia de comandă.
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
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.
-----------------------------
tab ( f1, -10, 10 ) ;
tab ( fact, 0, 10 ) ;
-----------------------------
typedef struct {
char * nume;
int varsta;
} persoana;
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.
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.
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ă:
#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.
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.
Programul 5.5: Un macro pentru calcului maximului a două numere poate fi definit ca mai
jos:
float a,b,c;
c=MAX(a,b);
105
Un alt exemplu de apel al macro-ului MAX:
int i,j,k;
k=MAX(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.
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:
Un alt exemplu unde apare această eroare este macroul de ridicare a unui număr la pătrat
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:
106
Programul 5.6: Câteva exemple de macrodefiniţii.
Macro pentru transformarea unui caracter din literă mică în literă mare:
#define SCHIMBA(X,Y) {
int t;
t=X;
X=Y;
Y=t;
}
#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:
void main(void) {
int *ip;
float *fp;
ip=ALOCA(int, 10);
fp=ALOCA(float, 10);
}
#undef nume
AFISEAZA(x/y);
Se expandează în
printf("x/y""=%g\n",x/y);
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
#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
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
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:
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() {
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:
6. Se citeşte valoarea unui întreg lung. Se cere să se afişeze valoarea fiecărui octet din
reprezentarea sa.
112
12. Să se definească un macro pentru calculul valorii absolute care să fie apoi apelat într-un
program pentru evaluarea expresiilor:
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.
Î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.
Redirectarea ieşirii unui program prog1 ca intrare pentru un alt program prog2:
prog1|prog2
/* copiaza.c */
#include <stdio.h>
void main() {
int c;
while ((c=getchar())!=EOF)
putchar(c);
}
116
copiaza <f1 >copie
Rezultatul va fi crearea fişierului cu numele copie, având acelaşi conţinut ca şi fişierul f1.
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”.
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.
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.
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.
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;
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.
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.
#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);
}
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:
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.
#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);
}
fclose(fp);
}
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.
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.
Funcţia fputs scrie şirul în fişier, fără a scrie şi caracterul '\0'. La eroare întoarece
EOF.
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:
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.
#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;
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, ¬a);
if ((nota==10)||(nota==9)) n1++;
else if ((nota >=6)&&(nota <=8)) n2++;
else if (nota ==5) n3++;
else n4++;
}
fclose(pf);
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.
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”.
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.
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>
void main() {
int c;
char nume_sursa[80], nume_dest[80];
FILE *fs,*fd;
char buf[MAX];
int dim;
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
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.
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.
fseek(pf, 0, 0);
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.
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.
Domnule Director,
Cu respect,
Introduceti Nume:
Popescu
Introduceti Prenume:
Ion
Introduceti data start:
11.07.2004
Introduceti data sfarsit:
01.08.2004
Domnule Director,
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
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
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
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)
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 chC
apel 2 chB
apel 1 chA
(p.p)
Fig. 7.2. Dinamica stivei la execuţia programului pentru linia de intrare dată ca exemplu
133
factrec 1
(1)
factrec 21
factrec 32
factrec 4 6
(4)
134
iterativ recursiv
for (i1;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:
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
}
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ă.
şi
138
pentru k = 0
c0
Pk ( x) pentru k 0
Pk 1 ( x) ck
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
Î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.
În general pot să existe mai multe submulţimi BA 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.
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
}
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 YX astfel încât suma elementelor lui Y (yY 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 = yYpozy). 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.
#include <stdio.h>
144
int x[N] = {5, -3, 1, 12, -6, -34, 37, 0};
int alege()
{
int i;
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]);
145
a fost ales. */
for (i=0; i<N; i++)
ales[i] = 0;
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];
void prelucrare()
{
int i, j, aux;
int main(void)
{
int x_ales, i, k, suma;
suma += x_ales;
147
}
return 0;
}
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.
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.
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>
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;
*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];
151
/* Pornim de la varful "0", motiv pentru care
marcam linia 0 si stergem coloana 0. */
marcat[0] = 1;
sters[0] = 1;
152
curenta. In oricare din situatii vom incheia
algoritmul. */
else
{
gata = 1;
}
}
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
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ă.
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.
Cod sursă În continuare este prezentat codul sursă în limbajul C care implementează
algoritmul descris mai sus.
154
#include <stdio.h>
/* Numarul de orase. */
int n;
int main(void)
155
{
FILE *fin;
int i, j;
int count, cost, min, j_min;
156
vizitat[j_min] = 1;
count++;
cost += min;
}
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.
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]) )
159
}
else
{
k = k - 1;
}
}
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.
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;
161
element. Prima alegere se face din multimea
"C[0]". */
k = 0;
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;
}
163
produsul cartezian, si incercam plasarea
urmatoarei regine. */
else
{
k++;
}
}
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 xS = 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>
#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;
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;
}
167
}
}
while (!continuare && pe_tabla);
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 c[N*N][2];
int count = 0;
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 (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;
}
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ă.
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).
Cod sursă Prezentăm în continuare codul sursă în limbajul C care rezolvă problema:
#include <stdio.h>
int main(void)
{
/* Numarul de discuri. */
int n;
172
scanf("%d", &n);
return 0;
}
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.
int main(void)
{
/* Folosim doua variabile pentru a stoca minimul
si maximul gasite. */
int min, max;
int i;
173
{
/* Facem o comparatie pentru minim. */
comp++;
if (min > x[i])
min = 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.
Cod sursă Prezentăm în continuare codul sursă în limbajul C pentru rezolvarea problemei.
#include <stdio.h>
174
void minmax(int st, int dr, int *min, int *max)
{
int mijloc, min_st, max_st, min_dr, max_dr;
175
}
}
int main(void)
{
int min, max;
int i;
/* 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.
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 55 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.
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.
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.
179