Programare modulară. Funcţii în limbajul C. Dezvoltarea algoritmilor folosind funcţii

Obiective

În urma parcurgerii acestui laborator, studentul va fi capabil:

  • să declare şi să definească o funcţie în limbajul C
  • să apeleze funcţii definite în acelaşi fişier sursă, cât şi funcţii din alte fişiere sursă sau biblioteci
  • să distingă între parametrii formali şi cei efectivi, între cei transmişi prin valoare şi cei transmişi prin adresa de memorie
  • să explice rolul funcţiei main() într-un program
  • să folosească clasele de stocare în declaraţiile unor funcţii

Noţiuni teoretice

Funcţ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.

Definirea şi apelul unei funcţii în C

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:

declarare.c
int min(int x, int y);
definire.c
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));
}

Transmiterea parametrilor

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.

Transmiterea parametrilor prin valoare

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

Până acum aţi folosit în programele voastre funcţii care trimit valorile atât prin valoare (de exemplu 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.

Funcţii recursive

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).

Funcţia main

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

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;
}

Această declaraţie nu este similară cu următorul caz:

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.

Clase de stocare. Fişiere antet vs. biblioteci

Această secţiune este importantă pentru înţelegerea modului de lucru cu mai multe fişiere sursă şi cu bibliotecile oferite de GCC. Deşi în continuare sunt discutate în contextul funcţiilor, lucrurile se comportă aproximativ la fel şi în cazul variabilelor globale (a căror utilizare este, oricum, descurajată).

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.

Exerciții Laborator CB/CD

  1. Primul exercitiu presupune modificarea/adaugarea de instructiuni unui cod existent pentru a realiza anumite lucruri. In momentul actual programul afiseaza suma cifrelor unui numar.
    • Nu uitati ca trebuie sa utilizam un coding style adecvat atunci cand scriem sursele.
ex1.c
#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:

  • Scrieti o functie care realizeaza tot suma cifrelor, insa intr-un mod nerecursiv.
  • Modificati functia recursiva astfel incat sa realizeze doar suma cifrelor impare.
  • Modificati functia recursiva astfel incat la prima cifra impara sa nu mai mearga in recursivitate si sa intoarca suma realizata pana in acel moment.
  • Luati urmatoarea arhiva si observati cum sunt structurate fisierele. Rulati make pentru a crea executabilul si make clean pentru stergerea fisierelor generate.

Următoarele două probleme vă vor fi date de asistent în cadrul laboratorului.

Checker si teste laborator 4

Cum se foloseste checkerul

Cum se foloseste checkerul

Pentru utilizarea checkerului:

  • Se va scrie cate un fisier sursa pentru fiecare problema;
  • La finalul fiecarui printf utilizat pentru afisarea rezultatului trebuie sa existe un newline;
  • Sursa nu trebuie sa contina alte printf-uri in afara de cele care scriu rezultatul asteptat la stdout.
  • Se va dezarhiva arhiva specifica exercitiului;
  • In directorul curent se afla checkerul, executabilul generat, folderele de input si output specifice problemei;
  • Se va rula “bash checker.sh <executabil>” unde <executabil> este numele executabilului generat;

Exerciţii de Laborator

  1. [2 pct]: Implementați o funcție int factorial_iterativ(int n) care returnează n! = 1 * 2 * 3 * … * (n – 1) * n calculat iterativ. Implementați funcția int factorial_recursiv(int n) care are același scop, dar implementarea este recursivă.
  2. [2 pct]: Scrieți o funcție recursivă și una iterativă care returnează suma cifrelor unui număr natural int suma_cifre_recursiv(int n); respectiv int suma_cifre_iterativ(int n);.
  3. [1 pct]: Scrieți o funcție care determină dacă un număr este prim sau nu (întoarce 1 dacă numărul este prim sau 0 în caz contrar): int este_prim(int n);.
  4. [1.5 pct]: Scrieți o funcție care determină dacă un număr natural este palindrom sau nu (returnează 1 în cazul în care este palindrom sau 0 în caz contrar): int este_palindrom(int n);
  5. [1.5 pct]: Scrieți o funcție care citește de la tastatură un număr natural și calculează câți divizori sunt numere palindrom. Se va afișa la consolă divizorii care sunt palindrom, precum și numărul acestora. Alegeți un nume funcției implementate și apelați această funcție din main pentru a o testa.
  6. [2 pct]: Scrieți o funcție recursivă care citește de la tastatură câte un număr natural (citirea unui număr negativ duce la ignorarea sa) și incrementează un contor de fiecare data când este tastat un număr prim. Citirea se încheie când se întâlnește numărul 0. Afișați contorul după finalizarea citirii de numere de la tastatură.

Bonus

  1. [2 pct]: Folosind declarații si definiri de variabile și funcții, creați două fișiere main.c și autentificare.c, și apleați din funcția main din fișierul main.c o funcție void login(int username, int password) definită în autentificare.c. Funcția login va afișa mesajul “Autentificare cu success!”, respectiv “Autentificare esuata!” în urma verificării celor două argumente primite care trebuie să fie numere prime între ele, pentru ca autentificarea să aibă loc cu succes. De asemenea, sursa autentificare.c va cuprinde o variabilă numită status care va fi setată de funcția login cu 1 în cazul autentificării cu success, respective 0 în caz contrar. Afișați în main, valoarea acestei variabile. Compilați fișierele împreună și executați programul rezultat. Scriți fișierul autentificare.h cu antetul funcției implementată în autentificare.c și NU uitați să îl includeți în main.c.
gcc main.c autentificare.c; ./a.out

Referinţe

programare/laboratoare/lab04.txt · Last modified: 2021/10/24 14:12 by andrei.traistaru99
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0