Sunteți pe pagina 1din 17

1

1. Noţiunea de subprogram
Prin subprogram se înţelege un ansamblu alcătuit din tipuri de date, variabile şi instrucţiuni
scrise în vederea unei anumite prelucrări (calcule, citiri, scrieri) şi care poate fi utilizat (rulat) doar dacă
este apelat de un program sau de un alt subprogram.
Subprogramul este cea mai mică unitate de program care poate fi compilată separat.
Exemplu: Se citeşte un vector cu n componente numere întregi. Se cere să se tipărească vectorul
sortat.
Această problemă se rezolvă clasic, utilizând funcţia main în care se vor realiza toate
prelucrările necesare pentru rezolvarea problemei:
- citeşte dimensiunea vectorului n
- citeşte elementele vectorului
- sortează vectorul utilizând unul din algoritmii cunoscuţi
- afişează vectorul sortat
De această dată vom încerca să rezolvăm problema utilizând tehnica programării modularizate,
adică ”spărgând” problema în subprobleme mai mici care vor fi rezolvate, fiecare, de câte un
subprogram. În limbajul C, toate subprogramele sunt de tip funcţie, inclusiv main(), care este prima
funcţie care se execută la lansarea în execuţie a unui program. În acest caz, programul ar arăta astfel:
- apelează funcţia care citeşte dimensiunea şi elementele vectorului
- apelează funcţia care sortează vectorul
- apelează funcţia care afişează vectorul
Cele tri funcţii vor fi apelate din main(). Utilizând acestă metodă, vom obţine următoarea structură de
program:

funcţia main()

funcţia citire(v,n) funcţia sortare(v,n) funcţia scriere(v,n)

În structura modulară de mai sus am notat cu v vectorul supus prelucrării, iar cu n dimensiunea
lui, adică numărul de elemente.
În general, o problemă complexă se rezolvă mai uşor dacă o descompunem în altele mai
simple; şansele de a greşi la scrierea unui subprogram sunt mai mici decât acelea de a greşi la scrierea
unui program mare. Acesta din urmă rezultă din asamblarea subprogramelor la care se adaugă,
eventual, câteva linii scrise în programul principal. Avantajele utilizării subprpogramelor sunt:
- reutilizarea codului : o dată scris, un subprogram poate fi utilizat de mai multe programe
- elaborarea algoritmilor prin descompunerea problemei în altele mai simple; în acest fel putem
rezolva mai uşor problema, eventual prin colaborarea unei echipe de programatori
- reducerea numărului de erori care pot apărea la scrierea programelor
- depistarea cu uşurinţă a erorilor : verificăm subprogramele, apoi modul în care le-am asamblat

2. Structura funcţiilor şi apelul lor

2.1 Generalităţi
În esenţă, o funcţie are structura:
antet
instrucţiune compusă
a) Anteul conţine mai multe informaţii importante necesare compilatorului şi anume: numele
funcţiei, lista parametrilor formali, tipul rezultatului.
Structura antetului este:
2

tip nume(lista parametrilor formali)


Lista parametrilor formali este de forma:
parametru1,parametru2,………..,parametrun
Parametrii formali sunt entităţi simbolice folosite pentru a descrie prelucrările realizate de funcţie.
Fiecare parametru are forma:
tip nume
Observaţie: Există şi posibilitatea ca lista parametrilor formali să fie vidă, oricum prezenţa
parantezelor rotunde în antet este obligatorie.
b) Instrucţiunea compusă cuprinde declaraţiile variabilelor locale şi instrucţiunile propriu-zise
care descriu operaţiile realizate de către funcţie.
Observaţii:
- poate fi tip al unei funcţii orice tip de dată cu excepţia tablourilor
- dacă ţinem neapărat, există posibilitatea ca funcţia să întoarcă tablouri, dacă acestea sunt
înglobate în tipuri declarate cu struct, aşa cum se va vedea într-un paragraf ulterior.

Exemple de antete de funcţii:


- int suma(int a,int b) – funcţia se numeşte suma, returnează un rezultat de tip int şi are doi
parametri formali de tip int, numiţi a şi b
- void t(int n,float v[20]) – funcţia se numeşte t, este de tip void (nu returnează rezultat
prin nume), are doi parametri formali, primul numit n, de tip int, al doilea numit v, de tip float*
(pointer la float)
- char* sir(int n,char s[200]) – funcţia se numeşte sir, întoarce un pointer către un şir de
caractere şi are doi parametri formali, unul de tip int, numit n şi altul de tip char* (pointer la şir de
caractere) numit s
- ELEV mana(int n,char a[200]) – funcţie de tip ELEV, unde ELEV este de tipul structură
următor:
struct ELEV{
char nume[20];
int varsta;
};

Încheierea unei funcţii se poate face în mai multe moduri, după tipul rezultatului returnat de
funcţie.
În cazul funcţiilor de tip void (care nu returnează rezultat) încheierea funcţiilor se poate face în
două moduri şi anume:
- la încheierea instrucţiunii compuse care formează corpul funcţiei (încheiere normală)
Exemplu: funcţia următoare afişează elementele vectorului v cu n elemente întregi
void afisare1(int n,int v[])
{
int i;
for(i=1;i<=n;i++)
cout<<v[i]<<” “;
cout<<“\n”;
}
Evident, acestă funcţie se încheie în momentul în care se întâlneşte acolada închisă care încheie
corpul funcţiei.
- la întâlnirea instrucţiunii return; (încheiere forţată)
Exemplu: funcţia următoare afişează doar primele elemente impare dintr-un vector v cu n elemente
întregi. La întâlnirea primei valori pare, execuţia funcţiei se încheie şi se revine în modulul apelator
(care a făcut apelul).
void afisare2(int n,int v[])
{
int i;
for(i=1;i<=n;i++)
if(v[i]%2)
cout<<v[i]<<” “;
else
return;
cout<<“\n”;
}
Dacă vectorul v=(1,-9,13,19,5,-23), funcţia va afişa toate valorile şi se va încheia normal. Dacă vectorul
v=(9,-17,21,6,11,24,9), funcţia va afişa valorile 9,-17,21 şi se va încheia forţat cu return pentru v[4]=6.
3

În cazul funcţiilor care returnează un rezultat, indiferent de natura acestuia, corpul funcţiei
trebuie să conţină cel puţin o instrucţiune de forma return expresie; prin care se returnează
rezultatul calculat de funcţie, şi în care expresie trebuie să fie de acelaşi tip cu tipul funcţiei sau să
poată fi convertit implicit către acesta.
Exemplu: următoarea funcţie verifică dacă numărul întreg x este prim sau nu returnând 1 în caz
afirmativ şi 0 în caz contrar.
int Prim(int x)
{
int d;
if(x==2) return 1;
if(!(x%2)) return 0;
for(d=3;d<=sqrt(x);d+=2)
if(!(x%d)) return 0;
return 1;
}
Evident, funcţia foloseşte algoritmul cunoscut de verificare a primalităţii unui număr întreg, având patru
instrucţiuni return care tratează toate cazurile posibile: x=2, x par diferit de 2, x impar compus, x impar
prim.
Apelul unei funcţii se poate face din main() sau dintr-o altă funcţie. Apelul se poate face de
sine stător sau din interiorul unei expresii.
În cazul funcţiilor de tip void, apelul acestora nu se poate face decât de sine stătător cu o
instrucţiune de apel de forma nume_funcţie(listă-parametri-efectivi); unde lista-
parametrilor-efectivi este lista valorilor concrete cu care se apelează funcţia, şi este desigur vidă
dacă şi lista parametrilor formali este vidă. Spre exemplu, funcţia de afişare a elementelor unui vector
se poate apela din main() cu instrucţiunea afisare1(n,a); unde a este tabloul efectiv ale cărui
elemente se vor afişa.
În cazul funcţiilor care returnează un rezultat acestea se pot apela direct dintr-o expresie, caz
în care mai întâi se apelează funcţia şi apoi rezultatul returnat de aceasta este folosit pentru evaluarea
expresiei. Spre exemplu, următoarea secvenţă de program foloseşte funcţia Prim() descrisă anterior
pentru a afişa elementele prime dintr-un vector a cu n elemente întregi.
for(i=1;i<=n;i++)
if(Prim(a[i])) cout<<“%d “,a[i]);
Apelul funcţiei Prim() se face din condiţia instrucţiunii if (expresie logică). Desigur, dacă dorim, putem
neglija rezultatul returnat de o funcţie şi să o apelăm de sine stătător. Spre exemplu, deşi funcţiile
scanf(), printf(), getch() şi nu numai, returnează rezultate de tip int, uzual le apelăm de sine stătător şi
nu folosim rezultatul pe care îl returnează.

2.2 Declararea variabilelor


2.2.1 Noţiuni generale

Până în prezent am declarat variabile doar în corpul funcţiilor, inclusiv în cel al funcţiei main().
Variabilele astfel declarate se numesc locale. Sistemul de operare alocă fiecărui program trei zone
distincte în memoria internă a calculatorului, în care se găsesc memorate variabilele programului. Harta
simplificată a memoriei interne este prezentată în continuare:

segment de date
segment de stivă
heap
Segmentul de date este folosit pentru stocarea codului programului aflat în execuţie şi pentru
variabilele care sunt folosite de către acesta pe toată durata execuţiei. Codul unui program se încarcă
în segmentul de date numai în moţmentul în care acesta este lansat în execuţie, această operaţie
realizându-se de către o componentă a sistemului de operare numită încărcător (loader). Reamintim că
una din principalele sarcini ale sistemului de operare este gestionarea resurselor fizice şi logice ale unui
sistem de calcul, cele mai importante resurse fiind memoria RAM şi timpul de lucru al procesorului.
Evident, în momentul în carte execuţia programului se încheie, sistemul dealocă memoria din
segmentul de date, programul şi toate variabilele acestuia devenind inaccesibile.
Segmentul de stivă este utilizat pentru a realiza transferul parametrilor la apelul
subprogramelor şi pentru a controla execuţia programului în cazul lucrului cu subprograme. În
4

momentul în care este apelată o funcţie (un subprogram), controlul programului este cedat acesteia.
Funcţia se execută instrucţiune cu instrucţiune până la încheierea normală sau forţată a acesteia. În
acest moment controlul programului revine modulului care a făcut apelul (funcţia main() sau orice altă
funcţie). Pentru ca această revenire să se facă corect, pe stivă se salvează, în momentul apelului,
adresa de revenire (spre exemplu, adresa instrucţiunii imediat următoare instrucţiunii de apel) şi
contextul din care s-a făcut apelul, adică valorile tuturor variabilelor locale modulului apelator. În
momentul în care execuţia funcţiei se încheie, se recuperează de pe stivă aceste informaţii în ordine
inversă, adică se reface contextul din care s-a făcut apelul şi se află adresa de revenire. În acest fel,
execuţia modului apelator va continua corect după execuţia apelului funcţiei. În absenţa segmentului de
stivă nu ar fi posibilă implementarea lucrului cu subprograme. În continuare vom vedea că stiva are şi
alte destinaţii extrem de importante.
Zona heap se numeşte şi zonă de accumulare a variabilelor dinamice şi se foloseşte pentru
alocarea variabilelor dinamice, create la cererea expresă a programatorului, în timpul execuţiei unui
program prin apelul funcţiei malloc(), de exemplu, sau prin utilizarea operatorului new.
De asemenea, în limbajul C, există posibilitatea ca variabilele să fie memorate într-un anumit
registru al microprocesorului. În acest caz, timpul de acces la astfel de variabile este foarte mic, deci se
pot obţine programe optimizate. Numărul variabilelor care pot fi memorate în regiştrii interni este însă
redus.
În general o variabilă se caracterizează prin patru atribute şi anume:
- clasa de memorare
- vizibilitatea
- durata de viaţă
- tipul variabilei, singurul studiat până în acest moment
Clasa de memorare precizează locul unde este memorată variabila respectivă. O variabilă
poate fi memorată în segmentul de date, în cel de stivă, în heap sau într-un registru al
microprocesorului.
Vizibilitatea precizează liniile textului sursă din care variabila respectivă poate fi accesată.
Astfel avem:
- vizibilitate la nivel de bloc (instrucţiune compusă)
- vizibilitate la nivel de fişier, în cazul în care programul ocupă doar un singur fişier sursă (singurul
caz tratat momentan)
- vizibilitate la nivel de clasă, legată de programarea orientată pe obiecte, facilitate specifică în C++
Durata de viaţă reprezintă timpul în care variabila respectivă are alocat spaţiu în memoria
internă (deci există efectiv). Astfel avem:
- durată statică, conform căreia variabila are alocat spaţiu în tot timpul execuţiei programului
- durată locală, conform căreia variabila are alocat spaţiu în timpul în care se execută înstrucţiunile
blocului respectiv (blocul care conţine declaraţia variabilei)
- durată dinamică, caz în care alocarea şi dealocarea spaţiului necesar variabilei respective se face
explicit de către programator, în timpul execuţiei programului, prin folosirea unor operatori şi funcţii
speciale (de exemplu, new, malloc() etc.)
În limbajul C/C++ variabilele pot fi împărţite în trei mari categorii: locale, globale şi dinamice.

2.2.2 Variabile globale

Aceste variabile se declară în afara corpului oricărei funcţii, ca în exemplul următor:


#include<iostream.h>
int a;
void t()
{
a=3;
cout<<a;
}
int b;
void main()
{
b=4;
cout<<b;
t();
}
Variabilele a şi b sunt globale. Ele pot fi utilizate de toate funcţiile care urmează în textul sursă
declaraţiei variabilei respective. Deoarece pot fi utilizate, deci implicit şi modificate, de toate funcţiile
care succed momentul declaraţiei, aceste variabile se numesc globale.
Observaţie: La declarare, variabilele globale sunt iniţializate de către compilator cu 0.
5

Atributele variabilelor globale sunt:


- clasa de memorare este segmentul de date
- vizibilitatea: sunt vizibile în toate funcţiile care le succed; în exemplul anterior, a poate fi acccesată
din corpul oricărei funcţii, în timp ce variabila b poate fi accesată numai din funcţia main
- durata de viaţă este statică, adică au spaţiu de memorie rezervat în tot timpul execuţiei programului

2.2.3 Variabile locale

Aceste variabile sunt declarate în corpul funcţiilor, mai precis, pot fi declarate în orice bloc
(instrucţiune compusă) al acestora.
Observaţie: Variabilele declarate în funcţia main() sunt tot variabile locale, deci sunt vizibile numai în
această funcţie.
În exemplul următor, variabila a este declarată în corpul funcţiei t(), iar variabila b este
declarată în corpul funcţiei main():
void t()
{
int a=3;
cout<<a+2;
}
void main()
{
int b=4;
cout<<b--; t();
}

Atributele variabilelor locale sunt:


- clasa de memorare este, implicit, segmentul de stivă. Există posibilitatea ca acestea să fie alocate
şi în registrele microprocesorului, caz în care declaraţia lor trebuie precedată de cuvântul cheie
register, ca în exemplul următor: register int b=4;
- vizibilitatea variabilelor locale este la nivelul blocului în care au fost declarate.
- durata de viaţă a variabilelor locale este atât timp cât durează execuţia blocului respectiv, adică
este locală
Observaţie: Variabilele locale nu sunt iniţializate implicit de către compilator cu 0, această sarcină
revenind programatorului. În cazul în care iniţializarea este omisă, ele conţin o valoare oarecare numită
valoare reziduală care poate duce la rezultate eronate.
Exemplul 1: În funcţia următoare am declarat două variabile de tip int, numite b şi c. Variabila b este
vizibilă la nivelul funcţiei, dar variabila c este vizibilă doar la nivelul blocului în care a fost declarată.
Ambele sunt variabile locale alocate pe segmentul de stivă.
void f()
{
int b=7; // vizibilă în toată funcţia
{
int c=9; // vizibilă numai în acest bloc (instr.compusă)
cout << b <<“ ”<<c;
}
}
Exemplul 2: În programul următor am declarat trei variabile, toate numite a. Una este globală, iar
două sunt locale, dar declarate în blocuri diferite.
#include<iostream.h>
int a; // variabilă globală,vizibilă în orice punct din program
void f()
{
int a=4; // variabilă locală,vizibilă în toată funcţia
{
int a=3; // variabilă locală,vizibilă numai în acest bloc
cout<<a; // tipăreşte a=3
a+=2;
}
cout<<a; // tipăreşte a=4
a++;
}
void main()
6

{
a=5; // referă variabila globală a
f();
cout<<a; ; // tipăreşte a=5
}
Observaţie: În cazul în care, într-un anumit bloc sunt vizibile (se pot accesa) mai multe variabile cu
acelaşi nume, dar cu domenii de vizibilitate diferite, se accesează variabila cu vizibilitatea cea mai
mică. Este cazul variabilei a din blocul interior funcţiei func() pentru care se consideră cea mai mică
vizibilitate (cea de bloc) şi se tipăreşte valoarea 3.
Observaţie: Durata de viaţă a unei variabile locale poate fi modificată folosind atributul static, astfel
încât, deşi respectiva variabilă nu este recunoscută în afara funcţiei, ea nu moare odată cu revenirea
dintr-un apel. Cu alte cuvinte, apeluri succesive vor regăsi, fiecare, valoarea lăsată de apelul anterior.
Exemplu: În urma execuţiei programului următor, variabila m va avea valoarea 3, deoarece cele două
apeluri succesive ale funcţiei f() vor returna valorile 1, respectiv 2, variabila locală a având atributul
static.
#include<iostream.h>
int f()
{
static int a=1;
return a++;
}
void main()
{
int m;
m=f()+f();
cout<<m;
}
7

2.3 Transmiterea parametrilor


Parametrii care se găsesc în antetul funcţiei se numesc parametri formali, iar cei care se
găsesc în instrucţiunea de apel se numesc parametri efectivi.
Exemplu: Funcţia următoare calculează suma a două numere naturale.
int suma(int a,int b)
{
return a+b;
}
void main()
{
int c=4,d=3;
cout<<suma(2,3)<<endl;
cout<<suma(2+7,3-1*2)<<endl;
cout<<suma(c,d)<<endl;
cout<<suma(1.9,3.3)<<endl;
}
Parametrii formali ai funcţiei sunt a şi b. Funcţia este apelată de mai multe ori. Parametrii
efectivi sunt pe rând:
- 2, 3
- 2+7, 3-1*2
- c, d
- 1.9, 3.3
După fiecare apel al funcţiei se tipăreşte suma obţinută. Între parametrii formali şi cei efectivi
trebuie să existe o anumită concordanţă care este descrisă prin regulile următoare:
- Numărul parametrilor formali trebuie să coincidă cu numărul parametrilor efectivi. La această regulă
există şi excepţii. În exemplul dat numărul parametrilor formali este 2, iar cel al parametrilor efectivi
tot 2.
- Tipul parametrilor efectivi trebuie să coincidă cu tipul parametrilor formali sau tipul parametrilor
efectivi să poată fi convertit implicit către tipul parametrilor formali. La apelul suma(2,3); parametrii
efectivi sunt constante de tip întreg şi coincid cu tipul parametrilor formali. La apelul suma(2+7,3-
1*2); parametrii efectivi sunt expresii de tip întreg. În acest caz, înainte de apel se evaluează
expresiile respective. La apelul suma(c,d); parametrii formali sunt variabile de tip întreg date prin
valorile lor. La apelul suma(1.9,3.3); parametrii efectivi sunt constante de tip real care se
convertesc la tipul int prin trunchiere şi suma calculată este 1+3=4.
Observaţie: Nu este obligatoriu ca numele parametrilor formali să coincidă cu numele parametrilor
efectivi şi, în multe situaţii nici nu este recomandabil, pentru a evita confuziile.
În momentul lansării în execuţie a unei funcţii (la apel), parametrii efectivi se transmit funcţiei
apelate după următoarele reguli:
- Pentru memorarea parametrilor efectivi subprogramele folosesc segmentul de stivă, întocmai ca
pentru variabilele locale.
- Memorarea parametrilor efectivi transmişi se face în ordinea în care aceştia figurează în antet, de
la stânga spre dreapta.
- În cadrul subprogramului, parametrii transmişi în momentul apelului şi memoraţi pe stivă sunt
variabile. Numele lor este cel din lista parametrilor formali. Aceste variabile se comportă ca nişte
variabile locale subprogramului, deci nu sunt vizibile în afara funcţiei şi există cât timp funcţia este
în execuţie.
- La revenirea în blocul apelant, conţinutul variabilelor memorate pe stivă se pierde, durata de viaţă a
variabilelor locale fiind locală.
Există două mecanisme de transmitere a parametrilor, transmiterea prin valoare şi transmiterea prin
referinţă.

2.3.1 Transmiterea prin valoare

Se utilizează atunci când suntem interesaţi ca subprogramul să lucreze cu acea valoare, dar să
nu poată modifica parametrul efectiv corespunzător din blocul apelator.
Se pot transmite prin valoare:
1. Valorile reţinute de variabile. În acest caz parametrii efectivi trebuie să fie numele
variabilelor. Exemplu:
void test(int n)
{
n++;
8

cout<<”n=”<<n; // tipăreşte n=8


}
void main()
{
int n=7;
test(n);
cout<<”n=”<<n; // tipăreşte n=7
}
Parametrul n este transmis prin valoare. În funcţia main() acest parametru este iniţializat cu
valoarea 7. Când apelăm funcţia test(), se rezervă spaţiu pe stivă, spaţiu care are numele
parametrului formal (în acest caz, tot n) şi care este iniţializat cu valoarea memorată de
variabila n a programului principal. Altfel spus, pe stivă se copie valoarea parametrului efectiv
de apel. În funcţie, variabila n (care este locală acestei funcţii) este incrementată şi devine 8,
valoare care va fi tipărită. La ieşirea din funcţie, variabila n din stivă se pierde, adică nu mai are
spaţiu alocat, prin urmare valoarea 8 este pierdută. În main() se tipăreşte valoarea variabilei n
(locală acesteia) care are valoarea 7.
Pentru exemplul anterior, conţinutul stivei, în momentul apelului şi după execuţia
funcţiei test(), este următorul:

n=7, parametru valoare


de apel, local funcţiei
test
n=7 , var. locală funcţiei n=7 , var. locală funcţiei
main() main()
adresa de revenire din adresa de revenire din
funcţia test funcţia test

în momentul apelului după execuţia funcţiei test

Se observă că, în momentul apelului funcţiei test(), pe stivă sunt alocate două variabile cu acelaşi
nume n. Prima variabilă este variabila locală funcţiei main() care se salvează pe stivă în momentul
apelului pentru a putea reface contextul funcţiei main() după încheierea apelului. A doua variabilă
este parametrul formal tip valoare n, vizibil numai în funcţia test() şi iniţializat în momentul apelului
cu valoarea 7. Indiferent ce valori primeşte acest n în corpul funcţiei test(), după încheierea
execuţiei acestei funcţii, spaţiul său este dealocat din stivă, adică variabila respectivă este distrusă.
Din acest motiv, după execuţia funcţiei test(), conţinutul stivei este cel din dreapta. Se reface
contextul din care s-a lansat apelul funcţiei test(), adică se recuperează din stivă valoarea variabilei
locale n=7 şi adresa de revenire, adică adresa instrucţiunii printf.

2. Expresii. În acest caz, parametrii efectivi sunt expresii, care pot conţine şi funcţii şi care mai
întâi se evaluează. Exemplu:
#include<iostream.h>
#include<math.h>
void test(int n)
{
cout<<”n=”<<n;
}
void main()
{
test(5); // se va tipări 5
test(7+(int)sqrt(45)); // se va tipări 13
}
În funcţie se crează o variabilă numită n, reţinută pe stivă, care la primul apel va primi valoarea
5 şi la al doilea apel valoarea 13. La ieşirea din funcţie conţinutul acestei variabile se pierde.
Transmiterea parametrilor prin valoare se utilizează când nu dorim ca subprogramul apelat să poată
modifica parametrii efectivi de apel. Acesta este modul implicit de transmitere a parametrilor în limbajul
C. Dacă nu ar exista decât transmiterea prin valoare, ar fi imposibil să modificăm valoarea anumitor
valori care sunt declarate în blocul apelator. Acest lucru este totuşi posibil dacă lucrăm cu variabile de
tip pointer, care, aşa cum ştim deja, conţin adresele de memorie ale altor variabile din program. Având
acces la locaţia de memorie a unei variabile (prin adresa ei), îi putem desigur modifica conţinutul.
Funcţia următoare realizează interschimbarea valorilor a două variabile din programul principal. Pentru
9

a putea face acest lucru, funcţia primeşte adresele de memorie ale celor două variabile, adică doi
pointeri.
#include<iostream.h>
void schimba(int *x,int *y)
{ int aux;
aux=*x;
*x=*y;
*y=aux;
}
void main()
{ int a=7, b=12;
schimba(&a,&b);
cout<<“a=”<<a<<”\t b=”<<b; // tipăreşte a=12, b=7
}
Se observă că parametrii formali sunt doi pointeri la întreg care în momentul apelului primesc ca valori
adresa variabilei a, respectiv b (obţinute prin aplicarea operatorului &). În limbajul C acesta este
singurul mod prin care se pot transmite parametrii prin adresă, deci modifica de către un subprogram
apelat. Pentru exemplul anterior, evoluţia stivei este:

y=&b, parametru formal de tip


pointer, local funcţiei
schimba()
x=&a, parametru formal de tip b=7, variabilă locală
pointer, local funcţiei funcţiei main()
schimba()
b=12, variabilă locală funcţiei a=12, variabilă locală
main() funcţiei main()
a=7, variabilă locală funcţiei
main() adresa de revenire din
adresa de revenire din funcţia funcţia schimba()
schimba()

în momentul apelului funcţiei după execuţia funcţiei


schimba() schimba()

În cazul transmiterii prin adresă, pe stiva procesorului se depun, ca şi în cazul transmiterii


prin valoare, adresa de revenire şi valorile variabilelor locale funcţiei main(), necesare pentru a reface
contextul din care s-a făcut apelul. Adresele de memorie ale parametrilor efectivi corespunzători nu se
depun pe stivă. Ei sunt transmişi parametrilor formali de tip adresă printr-un mecanism special, şi
anume prin tabela de adrese gestionată de către compilator. Deşi pointerii x şi y sunt locali funcţiei
schimbă() şi sunt dealocaţi de pe stivă în momentul în care execuţia acestei funcţii se încheie, orice
modificare a locaţiilor de memorie adresate de aceşti pointeri se păstrează şi după încheierea apelului.
Din acest motiv, după execuţia funcţiei schimbă(), conţinutul stivei este cel din dreapta. Valorile
variabilelor a şi b, locale funcţiei main(), au fost modificate în timpul execuţiei funcţiei schimba() care a
avut acces la adresele lor de memorie. La revenirea din funcţie, se recuperează din stivă noile valori
ale variabilelor a şi b, adresa instrucţiunii printf şi se continuă execuţia funcţiei main().
Observaţie: Există o excepţie de această regulă, şi anume, când parametrul este un tablou. În C,
numele unui tablou este un pointer către componentele tabloului. Când transmitem prin valoare un
tablou, ceea ce primeşte subprogramul este un pointer către elementele acestuia, adică adresa de
început a tabloului în memorie. Având acces la această adresă, subprogramul poate modifica valorile
reţinute în tablou. Practic, în cazul tablourilor, transmiterea prin valoare devine transmitere prin adresă.
10

2.3.2 Transmiterea tablourilor către funcţii

Fie următorul fragment de program:


void main()
{
int a[10];
......
f(a); // apelul unei funcţii cu parametrul a
.....
}
Dacă o funcţie primeşte un tablou unidimensional, atunci parametrul formal al acesteia poate fi declarat
în trei moduri: ca pointer, ca tablou dimensionat sau ca tabloul nedimensionat. De exemplu, putem
declara funcţia f() în următoarele moduri:
void f(int *x) // parametru formal de tip pointer
{
………
}
sau ca:
void f(int x[10]) // tablou dimensionat
{
…..
}
sau ca:
void f(int x[]) // tablou nedimensionat
{
……
}
Toate cele trei metode de declarare determină rezultate asemănătoare, deoarece indică compilatorului
că urmează să fie primit un pointer la întreg. Dimensiunea tabloului nu contează din punctul de vedere
al funcţiei, deoarece C nu verifică depăşirea limitelor unui tablou. Din punct de vedere funcţional,
declaraţia:
void f(int x[32])
{
......
}
este de asemenea funcţională, deoarece compilatorul C generează un cod care cere funcţiei func() să
primească un pointer la întreg, fără a crea efectiv un tablou cu 32 de elemente.
Metoda de transmitere este aceeaşi şi când un tablou bidimensional este folosit ca argument al
unei funcţii. În acest caz se transmite efectiv numai pointerul către primul element. Cu toate acestea,
parametrul care primeşte un tablou bidimensional trebuie să definească măcar dimensiunea din
dreapta a tabloului. Aceasta deoarece compilatorul C trebuie să cunoască lungimea fiecărui rând
pentru a aplica corect indicii fiecărui termen al matricii. De exemplu, o funcţie care primeşte o matrice
de 10*10 întregi va fi declarată astfel:
void f(int x[][10])
{
....
}
Se poate preciza şi dimensiunea din stânga, dar nu este absolut necesar.

2.3.3 Transmiterea structurilor către funcţii

Când membrul unei structuri este transmis unei funcţii, ceea ce se transmite funcţiei este de
fapt valoarea acelui membru. Ca atare, se transmite doar o variabilă. Fie structura următoare:
struct info
{
char x;
int y;
float z;
char s[10];
}set;
Iată exemple de transmitere ale fiecărui membru către o funcţie:
func(set.x); // transmite valoarea caracter a lui x
11

func(set.y); // transmite val. întreagă a lui y


func(set.z); // transmite val. reală a lui z
func(set.s); // transmite adresa şirului s
func(set.s[3]); // transmite val. tip caracter a lui s[3]
Dacă dorim să transmitem adresa unui anumit membru al unei structuri, inserăm operatorul & înaintea
numelui structurii.
func(&set.x); // transmite adresa caracterului x
func(&set.y); // transmite adresa întregului y
func(&set.z); // transmite adresa variabilei float z
func(set.s); // transmite adresa şirului s
func(&set.s[3]); // transmite adresa caracterului s[3]
Când o structură este folosită ca argument al unei funcţii, se va efectua transmiterea întregii structuri,
folosind metoda standard a apelului prin valoare. Aceasta înseamnă că structura nu va putea fi
modificată. Tipul structurii trebuie să coincidă cu tipul parametrului formal care primeşte structura. Din
acest motiv, declaraţia tipului structurii trebuie să aibă caracter global, pentru a putea fi folosită de către
toate funcţiile din program, ca în exemplul următor:
#include<iostream.h>
struct DATA{
int a,b;
char ch;
}; // declaraţie globală de tip
void f(DATA x) // foloseşte tipul global
{
cout<<x.a);
}
void main()
{
DATA arg; // foloseşte tipul global
arg.a=157;
f(arg);
}
Deoarece limbajul C permite declararea pointerilor către structuri, putem modifica un argument de tip
structură folosind mecanismul transmiterii prin adresă, cu ajutorul pointerilor ca în exemplul următor:
#include<iostream.h>
struct ORA{
int ore;
int minute;
int secuNde;
}; // declaraţie globală
void f(ORA *p) // pointer la structura globală
{
…………….
}
void main()
{
ORA arg;
…....
f(&arg); // transmite adresa structurii
}

Observaţie: Poate fi tip al unei funcţii orice tip de dată cu excepţia tablourilor. Dacă ţinem neapărat,
există posibilitatea ca funcţia să întoarcă tablouri, dacă acestea sunt înglobate în tipuri declarate cu
struct, aşa cum se arată în exemplul următor:
#include<iostream.h>
struct MAT{
float matrice[6][8];
};
MAT citire(int m,int n)
{
MAT a;
int i,j;
for(i=0;i<m;i++)
for(j=0;j<n;j++) cin>>a.matrice[i][j]);
12

return a;
}
void main()
{
int i,j;
MAT mtr=citire(3,2);
for(i=0;i<3;i++)
{
for(j=0;j<2;j++) cout<<mtr.matrice[i][j]<<” ”;
cout<<“\n”;
}
}

2.3.4 Transmiterea prin referinţă

În limbajul C++ există un tip special de date care permite ca o variabilă să fie identificată prin
mai multe nume, tip numit referinţă. Aparent, programul lucrează cu mai multe variabile, dar de fapt toţi
identificatorii respectivi referă aceeaşi locaţie de memorie. Declararea unei variabile de tip referinţă se
face cu sintaxa:
tip& nume variabilă=valoare de iniţializare;
În programul următor, variabila x este o referinţă pentru variabila y. Ele au aceeaşi valoare şi aceeaşi
adresă de memorie, dar poartă nume diferite.
#include<iostream.h>
void main()
{
int x=7;int& y=x; // sau int x=7, &y=a;
cout<<“x=”<<x<<“   si are adresa “<<&x<<endl;
cout<<“y=”<<y<<“   si are adresa “<<&y<<endl;
}
În cazul subprogramelor, parametrii sunt transmişi prin referinţă atunci când ne interesează ca la
revenirea din subprogram variabila transmisă să reţină valoarea stabilită în timpul execuţiei
subprogramului.
În cazul transmiterii prin referinţă parametrii formali trebuie să fie referinţe la variabile. În acest
caz subprogramul având acces la adresa de memorie a parametrului efectiv corespunzător, îl poate
modifica.
Exemplu: Programul următor interschimbă valorile a două variabile transmise prin referinţă.
#include<iostream.h>
void schimba(int &a, int &b)
{
int aux=a; a=b; b=aux;
}
void main()
{
int x=7, y=5;
schimba(x,y);
cout<<” x=”<<x<<”\ty=”<<y;
}

Concluzii:
a) În momentul în care într-un subprogram este întâlnită instrucţiunea de apel a unui alt subprogram,
controlul execuţiei este cedat subprogramului apelat şi pe stivă se depun, în ordine:
- adresa de revenire din subprogramul apelat
- valorile variabilelor locale subprogramului apelator (contextul din care s-a făcut apelul)
- valorile parametrilor transmişi prin valoare
b) Revenirea din subprogramul apelat se face fie la întâlnirea instrucţiunii return, fie la întâlnirea
acoladei care încheie instrucţiunea compusă a subprogramului apelat
c) La încheierea execuţiei subprogramului apelat se recuperează din stivă, în ordine, valorile
variabilelor locale subprogramului apelator şi adresa de revenire, adică se reface contextul din care s-a
făcut apelul.
13

2.4 Definirea şi declararea unui subprogram


Deşi aparent asemănătoare, cele două noţiuni diferă. A defini un subprogram, înseamnă a-l
scrie efectiv (antet şi instrucţiune compusă). Este foarte important locul în care se defineşte un
subprogram. A declara un subprogram, înseamnă a-l anunţa. Un subprogram nedeclarat nu poate fi
folosit. Definiţia unui subprogram ţine loc şi de declaraţie.
Exemplu: Programul următor conţine două funcţii f1 şi f2. Definiţiile ambelor funcţii se găsesc înaintea
funcţiei main() şi din acest motiv ele pot fi apelate din main(). Definiţia funcţiei f1 este înaintea definiţiei
lui f2, deci funcţia f1 poate fi apelată din f2, în schimb, din f1 nu poate fi apelată f2 pentru că declaraţia
acesteia îi succede lui f1.
#include<iostream.h>
void f1()
{
cout<<“functia f1\n”;
}
void f2()
{
f1();
cout<<“functia f2\n”;
}
void main()
{
f1();
f2();
}
Dacă dorim ca f1 să poată apela funcţia f2 folosim prototipul funcţiei care este antetul acesteia urmat
de punct şi virgulă. Prototipul are rolul de a declara o funcţie şi nu conţine definiţia acesteia.
Exemplu:
#include<iostream.h>
void f1(); // prototip f1
void s2(); // prototip f2
void main()
{
f1();
}
void f1() // definiţia lui f1
{
f2();
cout<<“functia f1\n”;
}
void f2() // definiţia lui f2
{
f1();
cout<<“functia f2\n”;
}

Observaţie: În prototipul unei funcţii se poate omite numele parametrilor formali, precizându-se numai
tipul acestora. Spre exemplu, este corect un prototip de forma float func(int, int&); care declară o
funcţie cu rezultat de tip float si doi parametri, unul valoare de tip int şi unul referinţă la int.
În C++ este o practică uzuală să se scrie mai întâi prototipurile tuturor funcţiilor utilizate în
program, fără funcţia main(), iar după funcţia main() să fie definite aceste funcţii. În acest fel, orice
funcţie, mai puţin main(), poate fi apelată din oricare alta.

2.5 Funcţii cu număr variabil de argumente


O facilitate importantă a limbajului C este posibilitatea utilizării funcţiilor cu un număr variabil de
argumente. Este obligatoriu ca cel puţin primul parametru să apară în lista parametrilor formali. Pentru
parametrii variabili antetul va conţine “…”. Parametrii efectivi sunt depuşi, unul după altul, în ordinea
declarării în antet, în stivă. Dacă la adresa primului parametru adunăm 1, obţinem adresa următorului
14

parametru şi aşa mai departe. De regulă, parametrii ficşi conţin informaţii despre cei variabili (numărul
lor, în unele cazuri tipul lor).
În exemplul următor, funcţia f are un număr neprecizat de parametri, aspect semnalat prin
prezenţa celor trei puncte după parametrul fix n în antetul funcţiei. Accesarea parametrilor actuali în
momentul apelului se bazează pe faptul că parametrii se memorează la adrese consecutive în stivă.
Astfel, se pleacă de la adresa ultimului parametru fix, în acest exemplu &n. Valoarea &n+1 reprezintă
adresa următorului parametru, adică a primului parametru variabil, &n+2 este adresa celui de-al doilea
parametru variabil etc. În felul acesta, valorile care vor apărea la apel după parametrii ficşi vor putea fi
accesaţi printr-un calcul obişnuit de adrese şi funcţia va returna maximul dintre elementele vectorului x,
adică 23.
#include<iostream.h>
int f(int n,…)
{
int *x=&n+1, m=x[0], j;
for(j=1;j<n;j++)
x[j]>m? m=x[j]: m=m;
return m;
}
void main()
{
cout<<f(4,12,11,23,2);
}
Un alt aspect important referitor la funcţiile limbajului C este posibilitatea de a iniţializa
parametrii formali ai unei funcţii direct în antetul acesteia. În acestă situaţii, la apelul funcţiei pot să
lipsească unul sau mai mulţi parametri actuali, chiar toţi parametrii. În cazul în care la apel lipseşte un
parametru actual, compilatorul va folosi în funcţie valoarea cu care a fost iniţializat la declarare
parametrul formal aferent.
Exemplu: Programul următor este corect şi va afişa valorile m=6, n=15, p=34 şi q=52.
#include<iostream.h>
int f(int a=1,int b=2,int c=3)
{
return a+b+c;
}
void main()
{
int m,n,p,q;
m=f();
n=f(10);
p=f(12,19);
q=f(11,23,18);
cout<<m << ” ”<<n<<” ”p<<” ”q;
}

2.6 Folosirea funcţiilor ca parametri de apel


Limbajul C permite transmiterea funcţiilor ca parametri de apel pentru alte funcţii. Numele
funcţiei este dat ca parametru prin intermediul unui pointer către funcţie. Considerăm programul
următor:
#include<iostream.h>
#include<math.h>
float ma(int x,int y)
{
return (x+y)/2.;
}
float mg(int x,int y)
{
return sqrt(x*y);
}
void calcul(int x,int y,float (*f)(int,int))
{
cout<<f(x,y);
}
void main()
15

{
int a,b;
scanf(“%d %d”,&a,&b);
if(a>=b) calcul(a,b,ma);
else if(a>0 && b>0)
calcul(a,b,mg);
}

În acest exemplu, funcţia calcul are un antet mai deosebit. Primii doi parametri sunt doi întregi
x şi y. Al treilea parametru, float (*f)(int,int) , reprezintă un pointer către o funcţie f. Această
funcţie f are la rândul ei doi parametri de tipul int. Nu este necesar să se precizeze numele acestor
parametri, ci doar tipul lor. Instrucţiunea cout<<“%.1f”,f(x,y)); din corpul funcţiei f afişează
valoarea întoarsă de funcţie. În funcţia main() se fac apelurile: calcul(a,b,ma);, respectiv
calcul(a,b,mg);. La apel, al treilea parametru din antet (pointerul la funcţie), este înlocuit cu
numele unei funcţii, ma, respectiv, mg. Aceste funcţii au fost scrise anterior. Spre exemplu, dacă se
citesc valorile a=7 şi b=6, identificatorul f este înlocuit cu ma, se calculează media aritmetică a
parametrilor x şi y şi se tipăreşte valoarea 6.5. Dacă se citeşte a=2 şi b=8, identificatorul f se
înlocuieşte cu mg, se calculează media geometrică a parametrilor x şi y şi se tipăreşte valoarea 4.

3. Exemple de aplicaţii care folosesc subprograme


Exemplul 1: Se citesc două numere întregi m şi n. Să se tipărescă numerele prime aflate în intervalul
[m,n].
Pentru a verifica primalitatea unui număr din intervalul dat vom folosi o funcţie simplificată care
returnează 1 dacă numărul respectiv este prim, şi 0 în caz contrar.
#include<iostream.h>
#include<math.h>
int prim(int x)
{
int i;
for(i=2;i<=sqrt(x);i++)
if(!(x%i)) return 0;
return 1;
}
void main()
{
int m,n,i;
cout<<“m,n="); scanf("%d %d",&m,&n);
cout<<“ numerele prime din intervalul ”<< ”[”<<m<<”,”<< n<<”,]"
<<"sunt: "\n";
for(i=m;i<=n;i++)
if(prim(i)) cout<<i<<“ ";;
cout<<“\n";
}

Exemplul 2: Se citeşte un vector cu n componente numere întregi. Se cere să se tipărescă cmmdc al


elementelor vectorului.
Pentru citirea vectorului vom scrie o funcţie care returnează (prin referinţă) numărul n al
elementelor şi elementele propriu-zise ale vectorului, citite de la tastatură. Pentru determinarea cmmdc
al elementelor vectorului, vom scrie o funcţie care calculează şi returnează cmmdc a două numere
întregi a şi b, şi pe care o vom folosi într-o buclă pentru elementele vectorului.
#include<iostream.h>
void citire(int& n, int x[]);
int cmmdc(int a,int b);
void main()
{
int i,n,v[50],cm;
citire(n,v);
cm=cmmdc(v[0],v[1]);
for(i=2;i<n;i++)
cm=cmmdc(cm,v[i]);
cout<<“cmmdc= "<<cm<<”\n”;
16

}
void citire(int& n, int x[])
{
int i;
cout<<“n="); scanf("%d",&n);
for(i=0;i<n;i++)
{
cout<<“el[„<<i<<”]=";
cin>>x[i];
}
}
int cmmdc(int a,int b)
{
while(a!=b)
if(a>b) a-=b;
else b-=a;
return a;
}

Exemplul 3: Se citesc două numere naturale m şi n, m<n. Să se tipărescă toate numerele


palindromice aflate în intervalul [m,n]. Un număr este palindromic dacă citit de la dreapta la stânga şi
de la stânga la dreapta reprezintă aceeaşi valoare.
Pentru a verifica dacă un număr natural x este palindrom, vom scrie o funcţie care vareturna 1
în caz afirmativ şi 0 în caz contrar. Funcţia va construi numărul cu cifrele în ordine inversă şi îl va
compara cu parametrul efectiv primit.
#include<iostream.h>
int palindrom(int x)
{
int inv=0,t=x;
while(x)
{
inv=inv*10+x%10;
x/=10;
}
return inv==t;
}
void main()
{
int m,n,i;
cout<<“m,n=");cin>>m>>n;
cout<<“\t Numere palindromice din intervalul:”<<
”[”<<m<<”,”<< n<<”,]" <<"sunt: "\n";
for(i=m;i<=n;i++)
if(palindrom(i)) cout<<i<<” ”;
cout<<“\n");
}
17

Exemplul 4: Să se scrie o funcţie care citeşte o matrice cu elemente numere întregi dintr-un fişier text
cu numele f.in. Pe prima linie a fişierului se găsesc două valori separate prin spaţii care reprezintă
numărul de linii şi, respectiv, de coloane (m şi n). Următoarele m linii ale fişierului conţin, în ordine,
elementele aflate pe fiecare linie a matricei. Matricea astfel obţinută va fi tipărită în main().
Funcţia de citire a matricii va primi ca parametri de intrare referinţe la variabilele m şi n şi
adresa tabloului citit, care are cel mult 10 linii şi 10 coloane.
#include<fstream.h>
void citire(int& m,int& n,int a[10][10])
{
int i,j;
FILE *f;
f=fopen("f.in","rt");
fscanf(f,"%d %d",&m,&n);
for(i=0;i<m;i++)
for(j=0;j<n;j++)
fscanf(f,"%d",&a[i][j]);
fclose(f);
}
void main()

{
int m,n,a[10][10],i,j;
citire(m,n,a);
cout<<“\n\t Matricea citita este:\n");
for(i=0;i<m;i++)
{
for(j=0;j<n;j++) cout<<a[i][j]<< “ ",;
cout<<“\n";
}
}

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