Subprograme (proceduri și funcții)
Introducerea conceptului de modularizare a programelor prin utilizarea procedurilor și funcțiilor.
De ce împărțim programul în subprograme
Pe măsură ce programele cresc, scrierea întregului cod în funcția main devine greu de citit, greu de corectat și plină de bucăți repetate. Soluția se numește modularizare: împărțim programul în subprograme — bucăți de cod cu nume, care rezolvă fiecare câte o sarcină bine delimitată și pot fi apelate (chemate) ori de câte ori e nevoie.
Avantajele modularizării, așa cum se cer la teste:
- Evitarea repetării codului: scrii o singură dată calculul, îl apelezi de zece ori. Dacă găsești o greșeală, o corectezi într-un singur loc;
- Lizibilitate: un main care doar apelează citeste_date(), prelucreaza(), afiseaza_rezultat() se citește ca o poveste;
- Testare mai ușoară: fiecare subprogram se poate verifica separat, cu date alese de tine;
- Lucrul în echipă: fiecare membru poate scrie alt subprogram.
În C++ toate subprogramele se numesc funcții; în Pascal ele se împart în proceduri (nu întorc un rezultat) și funcții (întorc un rezultat). Aceeași distincție există și în C++, doar că procedura se scrie ca funcție cu tipul de retur void.
De altfel, ai folosit deja subprograme fără să știi: main este o funcție, iar cin și cout lucrează cu cod scris de alții și adus în program prin #include. Ideea de a folosi cod gata scris și verificat — bibliotecile — este una dintre cele mai importante din informatică.
Funcții care întorc un rezultat
O funcție primește date, face un calcul și returnează un rezultat. Definiția are patru părți: tipul rezultatului, numele, lista de parametri și corpul:
int suma(int a, int b) { return a + b; }
- int din față este tipul rezultatului — ce fel de valoare întoarce funcția;
- suma este numele, ales de programator după regulile identificatorilor;
- (int a, int b) sunt parametrii — datele de intrare ale funcției;
- return a + b; calculează rezultatul și îl trimite înapoi la locul apelului. return face două lucruri deodată: stabilește valoarea returnată și încheie imediat execuția funcției — orice instrucțiune scrisă după return-ul executat nu mai rulează.
Apelul se scrie exact ca folosirea unei variabile: x = suma(3, 5); sau cout << suma(3, 5); Valoarea returnată trebuie folosită — memorată, afișată, comparată. Un apel de funcție cu rezultat ignorat (suma(3, 5); pe o linie de sine stătătoare) compilează, dar rezultatul se pierde — o eroare de logică des întâlnită.
O funcție poate avea mai multe instrucțiuni return (de exemplu pe ramuri diferite ale unui if), dar la o execuție se atinge exact unul. Exemplu clasic — maximul a două numere:
int maxim(int a, int b) { if (a > b) return a; return b; }
Definițiile funcțiilor se scriu înainte de main (sau se anunță printr-un prototip), pentru că la compilare numele trebuie cunoscut înainte de primul apel.
Proceduri: subprograme fără rezultat returnat
Nu orice sarcină produce o valoare de calculat: uneori vrem doar să facem ceva — să afișăm un meniu, să desenăm o linie de steluțe, să afișăm un vector. Pentru asta folosim procedura — în C++, o funcție cu tipul de retur void (care înseamnă „nimic”):
void linie(int n) { for (int i = 1; i <= n; i++) cout << '*'; cout << endl; }
Apelul unei proceduri este o instrucțiune de sine stătătoare: linie(10); — nu se atribuie nimănui, pentru că nu există valoare de atribuit. Aici e simetria de reținut:
- funcție cu rezultat → apelul se folosește într-o expresie: x = f(a);
- procedură (void) → apelul stă singur: p(a);
Încercarea x = linie(10); este eroare de compilare — nu ai ce valoare să iei de la o funcție void.
Într-o procedură poți folosi return; simplu (fără valoare) ca să închei execuția mai devreme, dar nu return cu valoare.
Cum alegi între funcție și procedură? Regula practică: dacă răspunsul sarcinii este o valoare (o sumă, un maxim, un adevărat/fals), scrie o funcție care o returnează; dacă sarcina este o acțiune (afișare, desenare), scrie o procedură. La teste, cerința „scrieți o funcție care returnează...” îți impune funcție cu return, nu afișare! Confuzia dintre a returna și a afișa este penalizată: o funcție care face cout în loc de return nu returnează nimic și nu poate fi folosită în expresii.
Parametri formali și parametri efectivi
Parametrii formali sunt cei din definiția subprogramului — nume „de rezervă” care așteaptă valori. Parametrii efectivi (actuali) sunt valorile concrete trimise la apel. La int suma(int a, int b), a și b sunt formali; la apelul suma(3, x + 2), valorile 3 și x + 2 sunt efective.
Regulile de corespondență, verificate de compilator și de examinator:
- corespondența se face pe poziții: primul parametru efectiv merge în primul formal, al doilea în al doilea — nu după nume;
- numărul parametrilor efectivi trebuie să fie egal cu al celor formali;
- tipurile trebuie să se potrivească (sau să fie convertibile).
Modul standard de transmitere este prin valoare: subprogramul primește o copie a valorii, iar modificările făcute asupra parametrului formal nu afectează variabila din apel. Dacă funcția face a = 0; în corpul ei, variabila trimisă la apel rămâne neschimbată. Acesta e comportamentul implicit în C++ și sursa unei confuzii majore: mulți elevi cred că schimbarea parametrului în funcție schimbă și variabila de afară.
Când chiar vrem ca subprogramul să modifice variabila apelantului, folosim transmiterea prin referință, marcată în C++ cu & la parametrul formal: void dubleaza(int &x) { x = x * 2; } — acum modificarea se vede și în exterior. (În Pascal, echivalentul este cuvântul var.) Exemplul tipic: o procedură de interschimbare a două variabile funcționează doar cu parametri prin referință; cu transmitere prin valoare, se interschimbă copiile și variabilele reale rămân la fel.
Variabile locale și globale
Variabilele locale sunt declarate în interiorul unui subprogram (inclusiv în main) și există doar cât timp acesta se execută: se creează la intrare, dispar la ieșire. Ele sunt vizibile doar în subprogramul lor — main nu vede variabilele locale ale funcției suma și invers.
Variabilele globale sunt declarate în afara tuturor subprogramelor, de obicei sus, sub #include. Ele sunt vizibile din orice subprogram și trăiesc pe toată durata programului. Variabilele globale numerice sunt inițializate automat cu 0; cele locale, nu — pornesc cu valori reziduale imprevizibile.
Două subtilități cerute la teste:
- Două variabile locale cu același nume din subprograme diferite sunt complet independente — un x din main și un x din funcția f nu au nicio legătură, sunt zone de memorie diferite;
- Dacă o variabilă locală are același nume cu una globală, în interiorul subprogramului local o ascunde pe cea globală: numele se referă la varianta locală.
Care e stilul bun? Folosește variabile locale și parametri; evită globalele pe cât posibil. O funcție care primește tot ce îi trebuie prin parametri și întoarce rezultatul prin return este ușor de înțeles și de testat singură. Un program în care zece subprograme modifică aceleași globale devine imposibil de urmărit — nu mai știi cine a schimbat ce și când.
La exercițiile de urmărire a execuției cu subprograme, disciplina e aceeași ca la bucle: tabel cu variabilele, dar ținute pe coloane separate pentru fiecare subprogram, ca să nu amesteci localele unora cu ale altora — exact aici se pierd punctele.
De reținut
- subprogram
- secvență de instrucțiuni cu nume, care rezolvă o sarcină bine delimitată și poate fi apelată de oriunde din program, de oricâte ori
- modularizare
- împărțirea unui program în subprograme, pentru a evita repetarea codului, a-l face mai citeț și mai ușor de testat
- funcție
- subprogram care returnează un rezultat prin instrucțiunea return; apelul ei se folosește într-o expresie: x = f(a)
- procedură (funcție void)
- subprogram care execută o acțiune fără a returna o valoare; apelul ei este o instrucțiune de sine stătătoare
- return
- instrucțiune care stabilește valoarea returnată de funcție și încheie imediat execuția acesteia
- parametri formali
- parametrii din definiția subprogramului, care primesc valori la fiecare apel
- parametri efectivi
- valorile concrete transmise subprogramului la apel; corespondența cu cei formali se face pe poziții, ca număr și tip
- transmitere prin valoare
- modul implicit: subprogramul lucrează pe o copie, iar modificarea parametrului formal nu afectează variabila din apel
- transmitere prin referință
- marcată cu & în C++ (var în Pascal): subprogramul lucrează direct pe variabila apelantului, iar modificările se păstrează după apel
- variabilă locală / globală
- locala există și e vizibilă doar în subprogramul care o declară; globala, declarată în afara subprogramelor, e vizibilă peste tot și inițializată automat cu 0
Greșeli frecvente
Greșit: Funcție care afișează rezultatul cu cout în loc să îl returneze cu return
Corect: Cerința „returnează” impune return; o funcție care doar afișează nu poate fi folosită în expresii și nu respectă cerința — afișarea și returnarea sunt operații diferite
Greșit: Așteptarea ca modificarea unui parametru transmis prin valoare să se vadă și în variabila din apel
Corect: Prin valoare, subprogramul primește o copie; variabila apelantului rămâne neschimbată. Modificarea se propagă doar la transmiterea prin referință (cu &)
Greșit: Scrierea unei proceduri de interschimbare cu parametri transmiși prin valoare
Corect: Se interschimbă doar copiile, iar variabilele reale rămân la fel; parametrii trebuie transmiși prin referință: void schimba(int &a, int &b)
Greșit: Atribuirea rezultatului unei funcții void: x = afiseaza(n);
Corect: O funcție void nu returnează nimic, deci nu există valoare de atribuit — apelul ei stă singur, ca instrucțiune: afiseaza(n);
Greșit: Presupunerea că variabilele locale cu același nume din subprograme diferite sunt aceeași variabilă
Corect: Fiecare subprogram are propriile variabile locale, în zone de memorie diferite; un x din main și un x din funcția f sunt complet independente
Test — 6 întrebări ca la examen
1. Care este avantajul principal al împărțirii unui program în subprograme?
- programul se execută întotdeauna mai repede
- codul repetat se scrie o singură dată și se apelează ori de câte ori e nevoie
- programul ocupă întotdeauna mai puțină memorie la rulare
- dispare nevoia de variabile
Vezi răspunsul
codul repetat se scrie o singură dată și se apelează ori de câte ori e nevoie. Modularizarea elimină duplicarea codului și face programul mai citeț și mai ușor de corectat. Distractorul cu viteza e tentant, dar subprogramele nu accelerează execuția — apelul în sine chiar adaugă un cost minuscul; câștigul este de organizare, nu de viteză.
2. Ce afișează programul: int f(int x) { return x * x + 1; } cu apelul cout << f(3);?
- 10
- 7
- 9
- 16
Vezi răspunsul
10. Parametrul formal x primește valoarea 3, iar funcția returnează 3 * 3 + 1 = 10. Distractorul 7 apare la citirea expresiei ca x * (x + 1) fără respectarea priorității sau ca 2x + 1; ordinea operațiilor rămâne cea de la matematică: întâi înmulțirea.
3. La definiția void p(int a, int b) și apelul p(x, 5), parametrii formali, respectiv efectivi, sunt:
- formali: x și 5; efectivi: a și b
- formali: a și b; efectivi: x și 5
- toți patru sunt formali
- formali: a și x; efectivi: b și 5
Vezi răspunsul
formali: a și b; efectivi: x și 5. Formalii sunt cei din definiție (a, b), efectivii sunt valorile de la apel (x, 5), iar corespondența se face pe poziții: x merge în a, 5 în b. Prima variantă inversează exact cele două noțiuni — confuzia standard de terminologie.
4. Se dă: void f(int a) { a = a + 10; } și secvența: int x = 5; f(x); cout << x; Ce se afișează?
- 15
- 10
- 5
- 0
Vezi răspunsul
5. Parametrul a este transmis prin valoare: funcția modifică o copie, iar x din main rămâne 5. Răspunsul 15 — distractorul principal — ar fi corect doar cu transmitere prin referință: void f(int &a). Exact această diferență o testează întrebarea.
5. O funcție care trebuie să întoarcă maximul a două numere este scrisă: void maxim(int a, int b) { cout << (a > b ? a : b); } De ce NU respectă cerința „funcția returnează maximul”?
- pentru că folosește doi parametri
- pentru că afișează rezultatul în loc să îl returneze, deci nu poate fi folosită în expresii
- pentru că numele funcției nu are voie să fie maxim
- pentru că maximul a două numere nu se poate calcula într-o funcție
Vezi răspunsul
pentru că afișează rezultatul în loc să îl returneze, deci nu poate fi folosită în expresii. A returna înseamnă return cu valoare și tip de retur nevid (int, nu void); afișarea cu cout este o acțiune, nu o valoare întoarsă — apelul x = maxim(2, 7) ar fi imposibil. Ceilalți distractori sunt afirmații false: doi parametri și numele ales sunt perfect legale.
6. Un program are variabila globală int s = 0; și funcția void f() { int s = 7; } După apelul f(), ce valoare are variabila globală s în main?
- 7
- 0
- valoare imprevizibilă
- programul nu compilează, numele s fiind deja folosit
Vezi răspunsul
0. int s = 7; din f declară o variabilă LOCALĂ nouă, care ascunde globala doar în interiorul funcției și dispare la ieșire; globala rămâne 0. Distractorul 7 ar fi corect doar dacă f scria s = 7; fără int — atunci ar fi modificat globala. Redeclararea locală a unui nume global este permisă, deci nu e eroare de compilare.
Deschide varianta interactivă — cu AI care îți explică