Subprograme
Noțiunea de subprogram, funcții și proceduri, parametri formali și actuali, mecanisme de transmitere a parametrilor.
Bacalaureat
Ce este un subprogram și de ce îl folosim
Un subprogram este o porțiune de program cu nume propriu, care rezolvă o sarcină bine delimitată și poate fi apelată ori de câte ori e nevoie. În loc să copiezi de trei ori codul care calculează cel mai mare divizor comun, scrii o singură dată subprogramul cmmdc și îl apelezi de trei ori.
Avantajele care se cer la teorie, dar se simt și în practică:
- evitarea repetării codului – o corectură se face într-un singur loc;
- lizibilitate – programul principal devine o listă de pași cu nume sugestive;
- testare ușoară – fiecare subprogram se verifică separat, pe cazuri proprii;
- reutilizare – același subprogram servește mai multe programe.
În C++ subprogramele se numesc generic funcții, dar tradițional se disting două categorii:
- funcții operand (de tip rezultat) – întorc o valoare printr-o instrucțiune return și se folosesc în expresii: s = cmmdc(a, b) + 1;
- proceduri (în C++ funcții void) – nu întorc o valoare prin nume, ci produc un efect: afișează ceva sau modifică parametrii; se apelează ca instrucțiune de sine stătătoare.
Confuzia clasică: o funcție void nu poate apărea într-o expresie aritmetică — nu are ce valoare să ofere.
Antetul funcției: parametri formali și actuali
Antetul unui subprogram declară tot ce trebuie știut pentru a-l apela: tipul rezultatului, numele și lista de parametri. De exemplu: int suma(int a, int b).
- Parametrii formali sunt cei din definiția subprogramului (a și b din antet) — nume generice, care primesc valori abia la apel.
- Parametrii actuali (efectivi) sunt cei din apel: suma(3, x + 2) are parametrii actuali 3 și x + 2.
Regula de corespondență se aplică pozițional: primul parametru actual ajunge în primul formal, al doilea în al doilea și așa mai departe. Numărul lor trebuie să coincidă, iar tipurile trebuie să fie compatibile. Numele nu contează deloc: parametrul formal se poate numi a, iar cel actual x — nu e nicio legătură de nume între ei, doar de poziție.
Instrucțiunea return expresie; face două lucruri deodată: stabilește valoarea întoarsă și încheie imediat execuția funcției — tot ce urmează după return nu se mai execută. De aici un idiom util: într-o funcție care verifică o proprietate, poți scrie return 0; direct din interiorul buclei, la prima dovadă că proprietatea nu se respectă.
La subiectul III de bacalaureat antetul este de regulă impus — respectarea lui întocmai (tip, nume, ordinea parametrilor) este condiție de punctaj.
Transmiterea prin valoare și prin referință
Mecanismul de transmitere a parametrilor decide dacă subprogramul poate modifica variabilele apelantului.
- Prin valoare (implicit în C++ pentru variabile simple): parametrul formal primește o copie a valorii actuale. Subprogramul lucrează pe copie, iar la revenire originalul este neschimbat. void f(int x) { x = 10; } nu afectează variabila trimisă la apel.
- Prin referință (marcată cu & în antet): parametrul formal devine un alt nume pentru variabila originală. Orice modificare se vede la apelant. void f(int &x) { x = 10; } chiar schimbă variabila trimisă.
Când alegi referința? În două situații: când subprogramul trebuie să întoarcă mai mult de un rezultat (return poate întoarce o singură valoare, dar poți avea oricâți parametri prin referință) și când vrei să modifici efectiv argumentul (o procedură de interschimbare: void swap(int &a, int &b)).
Restricție de reținut: prin referință se pot transmite doar variabile — apelul swap(3, 5) e greșit, pentru că 3 și 5 nu sunt zone de memorie modificabile.
Tablourile sunt cazul special: se transmit întotdeauna prin adresă, deci se comportă ca și cum ar fi prin referință — modificările asupra elementelor rămân, fără să scrii vreun &. Este exact invers decât la variabilele simple și este capcana preferată a itemilor de examen.
Variabile locale și globale
Variabilele locale sunt declarate în interiorul unui subprogram (inclusiv parametrii formali): există doar pe durata execuției acelui apel și sunt vizibile doar acolo. Variabilele globale sunt declarate în afara oricărui subprogram: există pe toată durata programului și sunt vizibile din orice funcție.
Reguli care apar în itemii de examen:
- două subprograme pot avea fiecare o variabilă locală numită i — sunt variabile complet diferite, în zone de memorie diferite;
- dacă o variabilă locală are același nume cu una globală, în interiorul subprogramului locala ascunde globala: orice referire la acel nume țintește variabila locală;
- variabilele globale de tip numeric sunt inițializate automat cu 0; cele locale au valori nedefinite (reziduale) până la prima atribuire — folosirea lor neinițializate este o eroare de logică frecventă;
- modificarea unei variabile globale dintr-un subprogram se vede peste tot — comod, dar periculos: programele mari devin greu de urmărit când orice funcție poate schimba orice.
Stilul recomandat (și implicit cerut de proiectarea modulară): subprogramele comunică prin parametri și valori returnate, nu prin variabile globale. Excepția pragmatică din practica de bacalaureat: tablourile mari se declară deseori global, ca să nu ocupe stiva.
Recursivitate — primele noțiuni
Un subprogram este recursiv dacă se apelează pe el însuși. Ideea pare circulară, dar funcționează dacă respectă două condiții obligatorii:
- există un caz de bază — o situație în care funcția întoarce rezultatul direct, fără alt apel;
- fiecare apel se face pe o problemă mai mică, astfel încât șirul apelurilor să ajungă garantat la cazul de bază.
Exemplul canonic este factorialul: n! = n (n-1)!, cu cazul de bază 0! = 1. În C++: long f(int n) { if (n == 0) return 1; return n f(n - 1); }. Apelul f(3) declanșează f(2), care declanșează f(1), apoi f(0) — abia acolo lanțul se oprește și rezultatele se compun la întoarcere: 1, 1, 2, 6.
Fiecare apel are propriile variabile locale și propriii parametri, memorate pe stivă; de aceea apelurile nu se încurcă între ele. Fără caz de bază (sau cu o problemă care nu scade), apelurile se adâncesc la nesfârșit și programul se oprește cu depășire de stivă.
Pentru urmărirea execuției — cerință tipică de examen — desenează lanțul apelurilor în jos și compunerea rezultatelor în sus. Atenție la funcțiile care afișează ceva înainte, respectiv după apelul recursiv: ordinea afișărilor se inversează între cele două cazuri, iar acesta este itemul-capcană preferat.
De reținut
- subprogram
- porțiune de program cu nume propriu, care rezolvă o sarcină delimitată și poate fi apelată ori de câte ori este nevoie
- funcție
- subprogram care întoarce o valoare prin instrucțiunea return și poate fi folosit ca operand în expresii
- procedură (funcție void)
- subprogram care nu întoarce o valoare prin numele său; produce efecte precum afișări sau modificarea parametrilor și se apelează ca instrucțiune
- parametri formali
- parametrii din antetul (definiția) subprogramului, care primesc valori la fiecare apel
- parametri actuali (efectivi)
- valorile sau variabilele scrise la apelul subprogramului, puse în corespondență pozițională cu parametrii formali
- transmitere prin valoare
- mecanism în care parametrul formal primește o copie a valorii; modificările din subprogram nu afectează variabila apelantului
- transmitere prin referință
- mecanism (marcat cu & în C++) în care parametrul formal este un alt nume al variabilei originale; modificările rămân după apel
- variabilă locală
- variabilă declarată în interiorul unui subprogram, care există doar pe durata apelului și este vizibilă doar în acel subprogram
- variabilă globală
- variabilă declarată în afara subprogramelor, vizibilă din tot programul; cele numerice sunt inițializate automat cu 0
- subprogram recursiv
- subprogram care se apelează pe el însuși; corectitudinea cere un caz de bază și apeluri pe probleme din ce în ce mai mici
Greșeli frecvente
Greșit: O funcție void ar putea fi folosită într-o expresie: s = f(x) + 1 cu f de tip void
Corect: Funcțiile void nu întorc nicio valoare prin nume, deci nu pot apărea ca operand; doar funcțiile cu tip de rezultat (int, double etc.) se folosesc în expresii
Greșit: Parametrii actuali s-ar potrivi cu cei formali după nume
Corect: Corespondența este strict pozițională: primul actual ajunge în primul formal, indiferent de nume; două variabile cu același nume în apelant și în subprogram sunt entități diferite
Greșit: Modificarea unui parametru transmis prin valoare s-ar vedea și la apelant
Corect: Prin valoare subprogramul lucrează pe o copie: variabila apelantului rămâne neschimbată; ca modificarea să rămână, parametrul trebuie declarat prin referință (int &x)
Greșit: După instrucțiunea return s-ar mai executa restul funcției
Corect: return încheie imediat execuția funcției; instrucțiunile de după el (pe aceeași ramură) nu se mai execută niciodată — de aceea un return pus greșit într-o buclă oprește căutarea prea devreme
Greșit: O funcție recursivă fără caz de bază s-ar opri singură când n devine mic
Corect: Fără caz de bază apelurile continuă la nesfârșit până la depășirea stivei; oprirea trebuie programată explicit printr-o condiție care întoarce rezultatul fără alt apel recursiv
Test — 6 întrebări ca la examen
1. În definiția int putere(int b, int e), b și e sunt:
- parametri actuali
- parametri formali
- variabile globale
- constante
Vezi răspunsul
parametri formali. Parametrii din antetul definiției sunt formali; ei primesc valori abia la apel. Parametrii actuali sunt cei scriși la apel, de exemplu putere(2, 10) — confuzia dintre cele două denumiri este itemul de teorie cel mai des întâlnit.
2. Se definește void f(int x) { x = x * 2; }. În programul principal: a = 5; f(a);. Ce valoare are a după apel?
- 10
- 5
- 0
- nedefinită
Vezi răspunsul
5. x este transmis prin valoare, deci f lucrează pe o copie a lui a; dublarea copiei nu atinge originalul, iar a rămâne 5. Răspunsul 10 ar fi corect doar dacă antetul ar fi void f(int &x) — cu transmitere prin referință.
3. Care antet este potrivit pentru un subprogram care interschimbă valorile a două variabile întregi?
- void swap(int a, int b)
- int swap(int a, int b)
- void swap(int &a, int &b)
- int swap(int &a, int b)
Vezi răspunsul
void swap(int &a, int &b). Interschimbarea trebuie să modifice ambele variabile ale apelantului, deci ambii parametri se transmit prin referință. Primul antet — capcana clasică — compilează fără eroare, dar interschimbă doar copiile: la revenire variabilele originale rămân neschimbate.
4. O variabilă globală int n; neinițializată explicit și o variabilă locală int m; neinițializată au valorile:
- amândouă 0
- n = 0, m nedefinită
- amândouă nedefinite
- n nedefinită, m = 0
Vezi răspunsul
n = 0, m nedefinită. Variabilele globale numerice sunt inițializate automat cu 0; cele locale primesc o zonă de memorie cu valoare reziduală, deci nedefinită până la prima atribuire. Varianta amândouă 0 e tentantă pentru că unele compilatoare par să dea 0 local — dar limbajul nu garantează asta.
5. Pentru long f(int n) { if (n == 0) return 1; return n * f(n - 1); }, câte apeluri ale funcției f se execută în total pentru f(4), inclusiv apelul inițial?
- 4
- 5
- 24
- 3
Vezi răspunsul
5. Se apelează f(4), f(3), f(2), f(1) și f(0) — cinci apeluri, ultimul fiind cazul de bază. Varianta 4 uită tocmai apelul f(0), iar 24 este valoarea returnată (4!), nu numărul de apeluri — distractorul care testează dacă ai citit întrebarea.
6. Se definește void g(int n) { if (n > 0) { g(n - 1); cout << n; } }. Ce afișează apelul g(3)?
- 321
- 123
- 333
- nimic
Vezi răspunsul
123. Afișarea este pusă DUPĂ apelul recursiv, deci se execută la întoarcere: mai întâi coboară g(3) → g(2) → g(1) → g(0), apoi afișează în ordinea revenirii 1, 2, 3. Varianta 321 ar fi corectă dacă afișarea ar sta înaintea apelului — exact inversarea pe care o testează acest tip de item.
Deschide varianta interactivă — cu AI care îți explică