În urma parcurgerii acestui laborator, studentul va fi capabil:
main()
într-un programFuncţiile împart taskuri complexe în bucăţi mici mai uşor de înţeles şi de programat. Acestea pot fi refolosite cu alte ocazii, în loc să fie rescrise de la zero. De asemenea, funcţiile sunt utile pentru a ascunde detalii de funcţionare ale anumitor părţi ale programului, ajutând la modul de lucru al acestuia. Utilizând funcţii, care reprezintă unitatea fundamentală de execuţie a programelor C, se obţine o divizare logică a programelor mari şi complexe.
Împărţirea programelor în funcţii este arbitrară şi depinde de modul de gândire a celui care le creează. De obicei, funcţiile cuprind o serie de instrucţiuni care efectuează un calcul, realizează o acţiune, implementează un algoritm, etc. Crearea funcţiilor trebuie să se bazeze pe următoarele principii: claritate
, lizibilitate
, uşurinţă în întreţinere
, reutilizabilitate
.
Caracteristicile definitorii ale unei funcţii în C sunt: numele, parametrii de apel şi valorea returnată. Sintaxa standard de declarare a unei funcţii este:
tip_returnat nume_functie (tip_param1 nume_param1 , tip_param2 nume_param2, ...);
Această declarare poartă numele de antetul funcţiei (function signature sau simplu signature). Lista de parametri poate lipsi.
Odată declarată, o funcţie trebuie definită, în sensul că trebuie expandat corpul acesteia cu instrucţiunile pe care trebuie să le execute.
Definirea unei funcţii are forma:
tip_returnat nume_functie(tip_param1 nume_param1, tip_param2 nume_param2, ...) { declaratii de variabile si instructiuni; return expresie; }
Limbajul C permite separarea declaraţiei unei funcţii de definiţia acesteia (codul care o implementează). Pentru ca funcţia să poată fi folosită, este obligatorie doar declararea acesteia înainte de codul care o apelează. Definiţia poate apărea mai departe în fişierul sursă, sau chiar într-un alt fişier sursă sau bibliotecă.
Diferite părţi din definirea unei funcţii pot lipsi. Astfel, o funcţie minimală este:
dummy() {}
Funcţia de mai sus nu face absolut nimic, nu întoarce nici o valoare şi nu primeşte nici un argument, însă din punct de vedere al limbajului C este perfect validă.
Tipul returnat de o funcţie poate fi orice tip standard sau definit de utilizator (struct-uri - acoperite într-un laborator următor), inclusiv tipul void (care înseamnă că funcția nu returnează nimic).
Orice funcţie care întoare un rezultat trebuie să conţină instrucţiunea:
return expression;
Expresia este evaluată şi convertită la tipul de date care trebuie returnat de funcţie. Această instrucţiune termină şi execuţia funcţiei, indiferent dacă după aceasta mai urmează sau nu alte instrucţiuni. Dacă este cazul, se pot folosi mai multe instrucţiuni return pentru a determina mai multe puncte de ieşire din funcţie, în raport cu evoluţia funcţiei.
Exemplu:
int min(int x, int y);
int min(int x, int y) { if (x < y) { return x; } return y; }
Apelul unei funcţii se face specificând parametrii efectivi (parametrii care apar în declararea funcţiei se numesc parametri formali).
int main() { int a, b, minimum; //........... x = 2; y = 5; minimum = min(x, 4); printf("Minimul dintre %d si 4 este: %d", x, minimum); printf("Minimul dintre %d si %d este: %d", x, y, min(x, y)); }
Apelul unei funcţii se face specificând parametrii care se transmit acesteia. În limbajul C, dar şi în alte limbaje de programare există 2 moduri de transmitere a parametrilor. Deoarece nu avem încă cunoștințele necesare pentru a înțelege ambele moduri, astăzi vom studia doar unul, urmând ca în laboratorul 8 să revenim și să îl explicăm și pe al doilea.
Funcţia va lucra cu o copie a variabilei pe care a primit-o şi orice modificare din cadrul funcţiei va opera asupra aceste copii. La sfârşitul execuţiei funcţiei, copia va fi distrusă şi astfel se va pierde orice modificare efectuată.
Pentru a nu pierde modificările făcute se foloseşte instrucţiunea return, care poate întoarce, la terminarea funcţiei, noua valoare a variabilei. Problema apare în cazul în care funcţia modifică mai multe variabile şi se doreşte ca rezultatul lor să fie disponibil şi la terminarea execuţiei funcţiei.
Exemplu de transmitere a parametrilor prin valoare:
min(x, 4); // se face o copie lui x
printf()
) cât şi prin intermediul adresei de memorie (de exemplu scanf()
). Mecanismul de transfer al valorilor prin intermediul adresei de memorie unde sunt stocate va fi complet „elucidat” în laboratorul de pointeri.
O funcţie poate să apeleze la rândul ei alte funcţii. Dacă o funcţie se apelează pe sine însăşi, atunci funcţia este recursivă. Pentru a evita un număr infinit de apeluri recursive, trebuie ca funcţia să includă în corpul ei o condiţie de oprire, astfel ca, la un moment dat, recurenţa să se oprească şi să se revină succesiv din apeluri.
Condiţia trebuie să fie una generică, şi să oprească recurenţa în orice situaţie. Această condiţie se referă în general a parametrii de intrare, pentru care la un anumit moment, răspunsul poate fi returnat direct, fără a mai fi necesar un apel recursiv suplimentar.
Exemplu
: Calculul recursiv al factorialului
int fact(int n) { if (n == 0) { return 1; } else { return n * fact(n - 1); } }
sau, într-o formă mai compactă:
int fact(int n) { return (n >= 1) ? n * fact(n - 1) : 1; }
Întotdeauna trebuie avut grijă în lucrul cu funcţii recursive deoarece, la fiecare apel recursiv, contextul este salvat pe stivă pentru a putea fi refăcut la revenirea din recursivitate. În acest fel, în funcţie de numărul apelurilor recursive şi de dimensiunea contextului (variabile, descriptori de fişier, etc.) stiva se poate umple foarte rapid, generând o eroare de tip stack overflow (vezi şi Infinite recursion pe Wikipedia).
Orice program C conţine cel puţin o funcţie, şi anume cea principală, numită main()
. Aceasta are un format special de definire:
int main(int argc, char *argv[]) { // some code return 0; }
Primul parametru, argc
, reprezintă numărul de argumente primite de către program la linia de comandă, incluzând numele cu care a fost apelat programul. Al doilea parametru, argv
, este un pointer către conţinutul listei de parametri al căror număr este dat de argc
. Lucrul cu parametrii liniei de comandă va fi reluat într-un laborator viitor.
Atunci când nu este necesară procesarea parametrilor de la linia de comandă, se poate folosi forma prescurtată a definiţiei funcţiei main
, şi anume:
int main(void) { // some code return 0; }
În ambele cazuri, standardul impune ca main
să întoarcă o valoare de tip întreg, care să reprezinte codul execuţiei programului şi care va fi pasată înapoi sistemului de operare, la încheierea execuţiei programului. Astfel, instrucţiunea return
în funcţia main
va însemna şi terminarea execuţiei programului.
În mod normal, orice program care se execută corect va întoarce 0, şi o valoare diferită de 0 în cazul în care apar erori. Aceste coduri ar trebui documentate pentru ca apelantul programului să ştie cum să adreseze eroarea respectivă.
Tipul de date void
are mai multe întrebuinţări.
Atunci când este folosit ca tip returnat de o funcţie, specifică faptul că funcţia nu întoarce nici o valoare. Exemplu:
void print_nr(int number) { printf("Numarul este %d", number); }
Atunci când este folosit în declaraţia unei funcţii, void
semnifică faptul că funcţia nu primeşte nici un parametru. Exemplu:
int init(void) { return 1; }
int init() { return 1; }
În cel de-al doilea caz, compilatorul nu verifică dacă funcţia este într-adevăr apelată fără nici un parametru. Apelul celei de-a doua funcţii cu un număr arbitrar de parametri nu va produce nici o eroare, în schimb apelul primei funcţii cu un număr de parametri diferit de zero va produce o eroare de tipul:
too many arguments to function
.
După cum se ştie, într-un fişier sursă (.c) pot fi definite un număr oarecare de funcţii. În momentul în care programul este compilat, din fiecare fişier sursă se generează un fişier obiect (.o), care conţine codul compilat al funcţiilor respective. Aceste funcţii pot apela la rândul lor alte funcţii, care pot fi definite în acelaşi fişier sursă, sau în alt fişier sursă. În orice caz, compilatorul nu are nevoie să ştie care este definiţia funcţiilor apelate, ci numai semnătura acestora (cu alte cuvinte, declaraţia
lor), pentru a şti cum să realizeze instrucţiunile de apel din fişierul obiect. Acest lucru explică de ce, pentru a putea folosi o funcţie, trebuie declarată
înaintea codului în care este folosită.
Fişierele antet
conţin o colecţie de declaraţii de funcţii, grupate după funcţionalitatea pe care acestea o oferă. Atunci când includem un fişier antet (.h) într-un fişier sursă (.c), compilatorul va cunoaşte toate semnăturile funcţiilor de care are nevoie, şi va fi în stare să genereze codul obiect pentru fiecare fişier sursă în parte. (NOTĂ: Astfel nu are sens includerea unui fişier .c în alt fişier .c; se vor genera două fişiere obiect care vor conţine definiţii comune, şi astfel va apărea un conflict de nume la editarea legăturilor).
Cu toate acestea, pentru a realiza un fişier executabil, trebuie ca fiecare funcţie să fie definită
. Acest lucru este realizat de către editorul de legături; cu alte cuvinte, fiecare funcţie folosită în program trebuie să fie conţinută în fişierul executabil. Acesta caută în fişierele obiect ale programului definiţiile funcţiilor de care are nevoie fiecare funcţie care le apelează, şi construieşte un singur fişier executabil care conţine toate aceste informaţii. Bibliotecile
sunt fişiere obiect speciale, al căror unic scop este să conţină definiţiile funcţiilor oferite de către compilator, pentru a fi integrate în executabil de către editorul de legături.
Clasele de stocare
intervin în acest pas al editării de legături. O clasă de stocare aplicată unei funcţii indică dacă funcţia respectivă poate fi folosită şi de către alte fişiere obiect (adică este externă
), sau numai în cadrul fişierului obiect generat din fişierul sursă în care este definită (în acest caz funcţia este statică
). Dacă nu este specificată nici o clasă de stocare, o funcţie este implicit externă.
Cuvintele cheie extern
şi static
, puse în faţa definiţiei funcţiei, îi specifică clasa de stocare. De exemplu, pentru a defini o funcţie internă, se poate scrie:
static int compute_internally(int, int);
Funcţia compute_internally
nu va putea fi folosită decât de către funcţiile definite în acelaşi fişier sursă şi nu va fi vizibilă de către alte fişiere sursă, în momentul editării legăturilor.
#include <stdio.h> int sum_recursive(int n) { if (n == 0) { return 0; } return n % 10 + sum_recursive(n / 10); } int main(void) { int nr; scanf("%d", &nr); printf("%d\n", sum_recursive(nr)); return 0; }
Cerinte:
Următoarele două probleme vă vor fi date de asistent în cadrul laboratorului.
gcc main.c autentificare.c; ./a.out