Funcții

Funcțiile permit structurarea programelor în secvențe mai mici de cod care îndeplinesc anumite sarcini.

În C++, o funcție este un grup de instrucțiuni căruia i se dă un nume și care poate fi apelat dintr-un alt punct al programului. Cea mai utilizată sintaxă pentru definirea unei funcții este:

tip nume ( parametru1, parametru2, ...) { instructiuni }

unde:
- tip este tipul valorii returnate de funcție.
- nume este identificatorul cu care funcția poate fi apelată.
- parameteri (în număr necesar): fiecare parametru constă într-un tip urmat de un identificator și fiind separat de următorul prin virgulă. Fiecare parametru seamănă foarte mult cu o declarație obișnuită de variabilă (de exemplu: int x) și, de fapt, acționează în interiorul funcției ca o variabilă obișnuită care este locală (în funcție). Scopul parametrilor este de a permite transmiterea de argumente către funcție din locația de unde este apelată.
- instructiuni formează corpul funcției. Este un bloc de instrucțiuni cuprinse între acolade { } care precizează ceea ce să facă funcția.

Să ne uităm la un exemplu:

// exemplu functie
#include <iostream>
using namespace std;

int adunare (int a, int b)
{
  int r;
  r=a+b;
  return r;
}

int main ()
{
  int z;
  z = adunare (5,3);
  cout << "Rezultatul este " << z;
}
Rezultatul este 8

Acest program este împărțit în doua funcții: adunare și main. Să ne amintim că indiferent de ordinea în care au fost definite, un program C++ începe întotdeauna prin apelarea lui main. De fapt, main este singura funcție apelată automat, iar codul din oricare altă funcție este executat numai dacă funcția respectivă este apelată din main (direct sau indirect - dintr-o altă funcție apelată, la rândul său).

În exemplul de mai sus, main începe cu declararea variabilei z de tip int, iar imediat după aceasta execută primul apel de funcție: apelează funcția adunare. Apelul unei funcții are o structură asemănătoare cu declarația. În exemplul de mai sus, apelul funcției adunare poate fi comparat cu definiția acesteia care se găsește cu câteva linii mai sus:


Parametrii din definiția funcției au corespondență clară cu argumentele transmise la apelul funcției. Apelul transmite funcției două valori, 5 și 3; acestea corespund parametrilor a și b, declarați pentru funcția adunare.

În punctul în care funcția este apelată din interiorul lui main, controlul este predat funcției adunare: aici se oprește execuția lui main și va fi reluată la terminarea funcției adunare. În momentul apelului funcției, valorile ambelor argumente (5 și 3) sunt copiate în variabilele locale int a și int b din interiorul funcției.

Apoi, în funcția adunare, se declară o altă variabilă locală (int r) și, cu ajutorul expresiei r=a+b, rezultatul lui a plus b este atribuit lui r; care, în acest caz, unde a este 5 și b este 3, r va avea valoarea 8.

Ultima instrucțiune din funcție:

1
return r;

termină funcția adunare și redă controlul punctului în care funcția a fost apelată; în acest caz, funcției main. Exact în acest moment, programul își reia cursul în main revenind exact în același punct în care a fost întrerupt de apelul funcției adunare. În plus, deoarece funcția adunare are un tip returnat, apelul este evaluat ca având o valoare și această valoare este cea precizată în instrucțiunea return de la sfârșitul funcției adunare: în acest caz particular, valoarea variabilei locale r, care are valoarea 8 la punctul return.


De aceea, apelul lui adunare este o expresie având valoarea returnată de funcție și, în acest caz, valoarea 8 este atribuită lui z. Este ca și cum întregul apel al funcției (adunare(5,3)) ar fi înlocuit cu valoarea pe care aceasta o returnează (adică 8).

Funcția main pur și simplu afișează această valoare prin apel:

1
cout << "Rezultatul este " << z;

De fapt, o funcție poate fi apelată de mai multe ori într-un program, iar apelul nu este restricționat doar la constante:

// exemplu functie
#include <iostream>
using namespace std;

int scadere (int a, int b)
{
  int r;
  r=a-b;
  return r;
}

int main ()
{
  int x=5, y=3, z;
  z = scadere (7,2);
  cout << "Primul rezultat este " << z << '\n';
  cout << "Al doilea rezultat este " << scadere (7,2) << '\n';
  cout << "Al treilea rezultat este " << scadere (x,y) << '\n';
  z= 4 + scadere (x,y);
  cout << "Al patrulea rezultat este " << z << '\n';
}
Primul rezultat este 5
Al doilea rezultat este 5
Al treilea rezultat este 2
Al patrulea rezultat este 6

Analog funcției adunare din exemplul anterior, acest exemplu definește funcția scadere, care returneaza diferența dintre cei doi parametri ai săi. De această dată, main apelează funcția de câteva ori, arătând mai multe variante în care poate fi apelată o funcție.

Să examinăm fiecare dintre aceste apeluri, având în vedere că fiecare apel de funcție reprezintă o expresie a cărei valoare este chiar valaorea pe care o returnează. Repetăm: ne putem gândi la un apel de funcție ca și cum îl putem înlocui cu valoarea returnată:

1
2
z = scadere (7,2);
cout << "Primul rezultat este " << z;

Dacă înlocuim apelul funcției cu valoarea pe care o returnează (în acest caz, 5), obținem:

1
2
z = 5;
cout << "Primul rezultat este " << z;

În aceeași manieră,putem interpreta:
1
cout << "Al doilea rezultat este " << scadere (7,2);

ca:
1
cout << "Al doilea rezultat este " << 5;

deoarece 5 este valoarea returnată de scadere (7,2).

În cazul:

1
cout << "Al treilea rezultat este " << scadere (x,y);

Argumentele transmise funcției scadere sunt variabile, nu constante. Este perfect valid și funcționează. Funcția este apelată cu valorile x și y pe care le au la momentul apelului: 5, respectiv 3, returnând 2 ca rezultat.

Și al patrulea apel este similar:

1
z = 4 + scadere (x,y);

Singura mențiune este aceea că, de data aceasta, apelul funcției este și el un operand într-o operație de adunare. Și acum, rezultatul este ca și cum am înlocui apelul funcției cu rezultatul ei: 6. Să observăm că, datorită proprietății de comutativitate a adunării, cele de mai sus pot fi scrise astfel:

1
z = scadere (x,y) + 4;

cu exact același rezultat. De asemenea, să observăm că punct și virgulă nu este necesar după apelul funcției, dar la sfârșitul întregii instrucțiuni este obligatoriu (ca de obicei). Subliniem, logica din spate poate fi înțeleasă mai ușor dacă înlocuim apelurile de funcții cu valorile returnate de ele:

1
2
z = 4 + 2;    // la fel ca z = 4 + scadere (x,y);
z = 2 + 4;    // la fel ca z = scadere (x,y) + 4; 

Funcții fără tip. Folosirea lui void

Sintaxa de mai sus pentru funcții:

tip nume ( argument1, argument2 ...) { instructiuni }

cere ca definiția să înceapă cu un tip. Acesta este tipul valorii returnate de funcție. Dar dacă funcția nu trebuie să returneze o valoare? În acest caz, scriem void, care este un tip special ce reprezintă absența unei valori. De exemplu, o funcție care doar afișează un mesaj nu are nevoie să returneze o valoare:

// exemplu de functie void
#include <iostream>
using namespace std;

void afiseaza_mesaj ()
{
  cout << "Eu sunt o functie!";
}

int main ()
{
  afiseaza_mesaj ();
}
Eu sunt o functie!

void poate fi folosi, de asemenea, ca listă de parametri ai funcției pentru a preciza explicit că, de fapt, funcția nu are nevoie de niciun parametru. De exemplu, am fi putut defini afiseaza_mesaj și astfel:

1
2
3
4
void afiseaza_mesaj (void)
{
  cout << "Eu sunt o functie!";
}

În C++, se poate folosi, cu același înțeles, o listă de parametri vidă în loc de void, dar folosirea lui void în locul listei de argumente a fost popularizată de limbajul C, în care este obligatorie.

Parantezele care urmează numelui funcției nu sunt opționale, nici la definiție, nici la apel. Chiar și atunci când funcția nu are parametri, o pereche de paranteze trebuie să fie adăugată după numele funcției. Să vedem cum a fost apelată afiseaza_mesaj în exemplul anterior:

1
afiseaza_mesaj ();

Parantezele sunt cele care diferențiază funcțiile de alte tipuri de declarații sau instrucțiuni. Ceea ce urmează nu este un apel de funcție:

1
afiseaza_mesaj;

Valoarea returnată de main

Probabil ați observat că tipul returnat de main este int, dar în cele mai multe exemple dinainte nu s-a returnat, practic, nici o valoare din main.

Ei bine, explicația este următoarea: dacă execuția lui main se termină normal, fără a întâlni vreo instrucțiune return, compilatorul presupune că funcția se termină cu o instrucțiune return implicită:

1
return 0;

Țineți minte că aceasta se aplică numai funcției main, din motive istorice. Toate celelalte funcții cu tip returnat trebuie să se termine cu o instrucțiune return potrivită, care să includă o valoare, chiar și dacă valoarea respectivă nu este folosită deloc.

Când main returnează zero (fie implicit sau explicit), mediul intrepretează că programul s-a terminat corect. main poate returna și alte valori, iar unele medii permit accesul la acele valori, deși nu este nici necesar, nici portabil acest comportament între platforme. Valorile pentru main care, garantat, sunt interpretate în același fel pe toate platformele sunt:

ValoareDescriere
0Programul s-a terminat corect.
EXIT_SUCCESSProgramul s-a terminat corect (la fel ca mai sus).
Această valoare este definită în header-ul <cstdlib>.
EXIT_FAILUREProgramul a eșuat.
Această valoare este definită în header-ul <cstdlib>.

Deoarece instrucțiunea return 0; implicită pentru main este o excepție delicată, unii autori consideră o bună practică să scrie explicit această instrucțiune.

Argumente transmise prin valoare și prin referință

În funcțiile de mai înainte, argumentele au fost transmise mereu prin prin valoare. Aceasta înseamnă că, atunci când se apelează o funcție, ceea ce se transmite sunt valorile pe care le au argumentele în momentul apelului, valori care sunt copiate în variabilele reprezentate de parametrii funcției. De exemplu, să luăm:

1
2
int x=5, y=3, z;
z = adunare ( x, y );

În acest caz, funcția adunare preia 5 și 3, care sunt copiate în x, respectiv y. Aceste valori (5 și 3) sunt folosite pentru a inițializa variabilele date ca parametri în definiția funcției, însă orice modificare a acelor variabile în interiorul funcției nu are efect asupra variabilelor x și y din afara ei, deoarece în apel nu au fost transmise chiar x și y, ci numai valorile lor în acel moment.


În anumite cazuri, totuși, poate fi util accesul la variabile externe din interiorul unei funcții. Pentru aceasta, argumentele pot fi transmise prin referință și nu prin valoare. De exemplu, funcția duplicat în această secvență de cod duplică valorile celor trei argumente ale sale, făcând ca variabilele folosite drept argumente să fie modificate chiar de apel:

// transmiterea parameterilor prin referință
#include <iostream>
using namespace std;

void duplicat (int& a, int& b, int& c)
{
  a*=2;
  b*=2;
  c*=2;
}

int main ()
{
  int x=1, y=3, z=7;
  duplicat (x, y, z);
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}
x=2, y=6, z=14

Pentru a obține accesul la argumentele sale, funcția declară parametrii ca referințe. În C++, referințele sunt indicate de un simbol ampersand (&) imediat după tipul parametrilor, ca la parametrii lui duplicat în exemplul de mai sus.

Când o variabilă este transmisă prin referință, nu se mai face o copie, ci chiar variabila însăși va fi identificată prin parametrul funcției, asociindu-se cu argumentul transmis; orice modificare asupra variabilelor locale funcției se reflectă în variabilele transmise ca argumente la apel.



De fapt, a, b și c devin alias-uri ale argumentelor transmise în apelul funcției (x, y și z) și orice schimbare asupra lui a în interiorul funcției devine schimbare a variabilei x în afara funcției. Orice schimbare a lui b modifică pe y și orice schimbare asupra lui c îl modifică pe z. Aceasta înseamnă că atunci când, în exemplul de mai sus, funcția duplicat modifică valorile variabilelor a, b și c, vor fi afectate și valorile lui x, y și z.

Dacă în definiția:

1
void duplicat (int& a, int& b, int& c)

nu s-ar fi folosit simbolul ampersand:

1
void duplicat (int a, int b, int c)

variabilele nu ar fi fost transmise prin referință, ci prin valoare, creându-se copii ale acelor valori. În acest caz, datele de ieșire ale programului x, y și z ar fi avut valorile nemodificate (adică 1, 3, respectiv 7).

Despre eficiență și referințe constante

Apelarea unei funcții cu parametri dați prin valoare creează copii ale valorilor date. Aceasta este o operație relativ necostisitoare pentru tipuri de date fundamentale precum int, dar dacă parametrul are un tip compus larg (care necesită mult spațiu), poate rezulta o oarecare supraîncărcare. De exemplu, să considerăm funcția următoare:

1
2
3
4
string concateneaza (string a, string b)
{
  return a+b;
}

Această funcție are două string-uri ca parametri (prin valoare) și returnează rezultatul concatenării lor. Transmițând argumentele prin valoare, funcția forțează ca a și b să fie copii ale argumentelor transmise funcției la apel. Și, dacă acestea sunt șiruri lungi, poate însemna copierea unei mari cantități de date doar pentru apelul funcției.

Putem evita acest lucru dacă parametrii sunt dați ca referințe:

1
2
3
4
string concateneaza (string& a, string& b)
{
  return a+b;
}

Argumentele prin referință nu necesită copie. Funcția operează direct cu alias-urile string-urilor transmise ca argumente și, cel mult, poate însemna transferul anumitor pointeri funcției. În acest sens, versiunea cu referințe a funcției concateneaza warw mai eficientă decât versiunea cu parametri prin valoare, deoarece nu este necesară copierea costisitoare a unor string-uri.

Pe de altă parte, funcțiile cu parametri prin referință sunt percepute ca funcții care modifică argumentele transmise, fiind motivul pentru care au fost definiți parametrii prin referință.

Pentru ca parametrii dați prin referință să nu poată fi modificați de către funcție, îi marcăm ca și constante:

1
2
3
4
string concateneaza (const string& a, const string& b)
{
  return a+b;
}

Precedându-i cu const, funcției i se interzice să modifice atât valoarea lui a, cât și pe cea a lui b, dar poate accesa valorile lor ca referințe (aliasurile argumentelor), fără a trebui să realizeze copii ale string-urilor.

De aceea, referințele const asigură funcționarea asemănătoare transmiterii argumentelor prin valoare, dar cu o eficiență crescută pentru parametrii costisitori. Acesta este motivul pentru care sunt foarte folosite în C++ în cazul tipurilor costisitoare. Reținem, totuși, că pentru tipurile fundamentale de date nu are importanță deosebită din punct de vedere al eficienței, ba chiar, în unele cazuri, pot fi chiar mai puțin eficiente!

Funcții inline

În general, apelarea unei funcții cauzează o supraîncărcare (stivuirea argumentelor, salturi, etc...), și, deci, pentru funcții foarte scurte, ar fi mult mai eficient doar să inserăm codul funcției acolo unde este apelată, în loc de toate formalitățile apelării funcției.

Începând definiția funcției cu specificatorul inline informăm compilatorul că, în locul mecanismului uzual de apelare a unei funcții, se preferă expandarea inline pentru o anumită funcție. Acest fapt nu va schimba comportamentul unei funcții; pur și simplu se sugerează compilatorului că secvența de cod generată de corpul funcției ar trebui inserată în fiecare punct în care se apelează, renunțându-se la un apel obișnuit.

De exemplu, funcția concateneaza de mai sus ar putea fi declarată inline astfel:

1
2
3
4
inline string concateneaza (const string& a, const string& b)
{
  return a+b;
}

Aceasta informează compilatorul că, atunci când este apelată concateneaza, programul să expandeze funcția inline, în loc de executarea unui apel obișnuit. inline se precizează numai la definiția funcției, nu și când este apelată.

Facem observația că majoritatea compilatoarelor deja optimizează codul pentru generarea de funcții inline când ar îmbunătăți eficiența, chiar dacă nu au fost marcate cu specificatorul inline. De aceea, specificatorul pur și simplu precizează compilatorului că se preferă forma inline a acelei funcții, deși compilatorul poate să nu o trateze așa și să optimizeze codul altfel. În C++, optimizarea este o sarcină a compilatorului, care poate să genereze orice fel de cod, atât timp cât comportamentul rezultat este cel specificat de cod.

Valori implicite pentru parametri

În C++, funcțiile pot avea și parametri opționali, pentru care nu sunt necesare argumente la apel, astfel încât, de exemplu, o funcție cu trei parametri poate fi apelată cu doar doi. În acest scop, funcția ar trebui să includă o valoare implicită pentru ultimul său parametru, valoare care să fie folosită atunci când funcția este apelată cu mai puține argumente. De exemplu:

// valori implicite in functii
#include <iostream>
using namespace std;

int imparte (int a, int b=2)
{
  int r;
  r=a/b;
  return (r);
}

int main ()
{
  cout << imparte (12) << '\n';
  cout << imparte (20,4) << '\n';
  return 0;
}
6
5

În acest exemplu, avem două apeluri ale funcției imparte. În primul:

1
imparte (12)

apelul transmite doar un argument funcției, chiar dacă a fost definită cu doi parametri. În acest caz, funcția presupune că al doilea parametru este 2 (observați definiția funcției, care declară al doilea parametru ca int b=2). De aceea, rezultatul este 6.

În al doilea apel:

1
imparte (20,4)

se transmit funcției două argumente. De aceea, este ignorată valoarea implicită pentru b (int b=2) și b ia valoarea transmisă ca argument, adică 4, rezultând valoarea 5.

Declararea funcțiilor

În C++, identificatorii pot fi folosiți în expresii numai după ce au fost declarați. De exemplu, o variabilă x nu poate fi folosită înainte să apară într-o instrucțiune de declarație, cum ar fi:

1
int x;

Același principiu se aplică și funcțiilor. Funcțiile nu pot fi apelate înainte să fi fost declarate. Acesta este motivul pentru care în exemplele anterioare au fost definite mai întâi celelalte funcții și apoi funcția main, care este funcția din care erau apelate alte funcții. Dacă main ar fi fost definită înaintea celorlalte, s-ar fi încălcat regula ca funcția să fie declarată înainte de a fi folosită și, deci, programele nu ar fi compilat.

Prototipul unei funcții poate fi declarat fără a se defini complet funcția, dar dă suficiente detalii care să permită cunoașterea tipurilor implicate de apelul funcției. Evident, funcția ar trebui definită altundeva, poate mai târziu în program. Dar, astfel, putem să apelăm funcția înainte de a-i fi dat definiția completă.

Declarația ar trebui să includă toate tipurile implicate (tipul returnat și tipul argumentelor sale), folosind aceeași sintaxă ca și la definiția funcției, dar înlocuind corpul funcției (blocul de instrucțiuni) cu punct și vurgulă la sfârșitul liniei de declarație (prototipul se termină cu ; spre deosebire de antetul din definiție).

u este nevoie ca lista de parametri să conțină și numele acestora (sunt opționale, dar pot să apară și nu este obligatoriu să coincidă cu cele din definiția funcției), ci numai tipul lor. De exemplu, prototipul unei funcții denumite protofunctie cu doi parametri poate fi declarat cu oricare dintre instrucțiunile:

1
2
int protofunctie (int first, int second);
int protofunctie (int, int);

În orice caz, includerea unui nume pentru fiecare parametru îmbunătățește întotdeauna lizibilitatea declarației.

// declararea functiilor prin prototipuri
#include <iostream>
using namespace std;

void impar (int x);
void par (int x);

int main()
{
  int i;
  do {
    cout << "Va rugam, tastati un numar (0 pentru iesire): ";
    cin >> i;
    impar (i);
  } while (i!=0);
  return 0;
}

void impar (int x)
{
  if ((x%2)!=0) cout << "Este impar.\n";
  else par (x);
}

void par (int x)
{
  if ((x%2)==0) cout << "Este par.\n";
  else impar (x);
}
Va rugam, tastati un numar (0 pentru iesire): 9
Este impar.
Va rugam, tastati un numar (0 pentru iesire): 6
Este par.
Va rugam, tastati un numar (0 pentru iesire): 1030
Este par.
Va rugam, tastati un numar (0 pentru iesire): 0
Este par.

Acesta nu este chiar un exemplu de eficiență. Probabil că puteți scrie, pentru acest program, o versiune care să conțină numai jumătate din numărul de linii de cod. În orice caz, acest exemplu ilustrează cum se pot declara funcțiile, înainte de a fi definite:

Următoarele linii:

1
2
void impar (int a);
void par (int a);

declară prototipurile funcțiilor. Conțin tot ce este necesar pentru apeluri: numele funcțiilor, tipurile argumentelor și tipul returnat (void în acest caz). Declarând aceste prototipuri, funcțiile pot fi apelate înainte de a fi definite complet (adică din funcția main).

Declararea funcțiilor înainte de definire nu este utilă numai în reorganizarea codului. În unele cazuri (precum acesta), este necesară cel puțin o declarație, deoarece impar și par se apelează reciproc; avem un apel la par în interiorul lui impar și un apel al lui impar în par. Și, de aceea, nu am putea găsi o metodă de structurare a codului astfel încât impar să fie definit înainte de par și par înainte de impar.

Recursivitate

Recursivitatea este proprietatea unei funcții de a se apela pe ea însăși. Este utilă în unele sarcini precum sortarea elementelor sau calcularea numerelor factoriale. De exemplu, pentru a obține factorialul unui număr (n!), formula matematică este:

n! = n * (n-1) * (n-2) * (n-3) ... * 1
Mai exact, 5! (factorial de 5) ar fi:

5! = 5 * 4 * 3 * 2 * 1 = 120
și o funcție recursivă pentru acest calcul în C++ ar putea fi:

// calculare factorial
#include <iostream>
using namespace std;

long factorial (long a)
{
  if (a > 1)
   return (a * factorial (a-1));
  else
   return 1;
}

int main ()
{
  long numar = 9;
  cout << numar << "! = " << factorial (numar);
  return 0;
}
9! = 362880

Observați că în funcția factorial am inclus un apel la ea însăși, dar numai dacă argumentul transmis este mai mare decât 1, căci, altfel, funcția ar executa o buclă recursivă infinită, în care ajunși la 0, s-ar continua înmulțirea cu toate numerele negative (provocând, probabil, o depășire a stivei la un moment dat, pe parcursul execuției).
Index
Index