4.14. Programarea procedurala, functii, apelul si revenirea din ele

2019/02/19 in Programare in C

Incepand cu primele limbaje de programare de nivel inalt s-a utilizat programarea procedurala.

Aceasta s-a dezvoltat din necesitatea de a utiliza intr-un program aceeasi secventa de calcul de mai multe ori. Pentru a evita o astfel de repetitie, secventa de instructiuni corespunzatoare se organizeaza ca o parte distincta si se face un salt la ea, ori de cate ori este nevoie in procesul de calcul respectiv. Acest salt este cu revenire la instructiunea urmatoare instructiunii care a facut saltul si de aceea el difera de salturile realizate prin instructiunea goto. Secventa de instructiuni organizata in acest fel are diferite denumiri in diferite limbaje de programare: subprogram, subrutina, procedura etc. Pentru inceput consideram denumirea de procedura.

Uneori procedura trebuie sa exprime acelasi proces de calcul, dar care se realizeaza cu date diferite. In acest caz, procedura trebuie realizata generala, facand abstractie de datele respective. De exemplu, pentru a evalua expresia: (1) 410 - 320 putem construi o procedura pentru ridicarea la putere, care sa fie generala, si care face abstractie de valorile efective pentru baza si exponent. Aceasta generalizare se realizeaza considerand ca fiind variabile atat baza cat si exponentul, iar valorile lor se precizeaza la fiecare apel al procedurii implementate astfel.

Aceste variabile utilizate pentru a putea implementa o procedura generala si care se concretizeaza la fiecare apel al procedurii, se numesc parametri formali.

In felul acesta, procedura apare ca un rezultat al unui proces de generalizare necesar implementarii ei. Ca orice proces de generalizare, ea presupune o abstractizare care se realizeaza prin utilizarea parametrilor formali.

Valorile de apel ale parametrilor formali se numesc parametri efectivi.

Programarea procedurala are la baza utilizarea procedurilor, iar acestea, la randul lor, realizeaza o abstractizare prin parametri.

O procedura se poate asemana cu o cutie neagra (eng. "black box"), la care i se transfera date de apel, iar acestea furnizeaza rezultatele la revenirea din ea. In momentul apelului se face abstractie de metoda de prelucrare a datelor de intrare in rezultate care se mai numesc si date de iesire.

In toate limbajele de programare se considera doua categorii de proceduri:

  1. proceduri care definesc o valoare la revenire;
  2. proceduri care nu definesc o valoare la revenire;

Procedurile din prima categorie de obicei se numesc functii. Valoarea de revenire se mai numeste si valoare de intoarcere sau valoarea returnata de functie.

Procedura pentru calculul ridicarii la putere este un exemplu de functie. Ea are ca parametri baza si exponentul, iar ca valoare de intoarcere sau returnata, rezultatul ridicarii valorii bazei la valoarea exponentului, valori care sunt definite la apel.

In limbajele C si C++ atat procedurile din categoria 1, cat si cele din categoria 2 se numesc functii. Deci, in aceste limbaje distingem functii care returneaza o valoare la revenirea din ele, precum si functii care nu returneaza nicio valoare.

Intr-un program, o functie are o definitie si mai multe apeluri, atatea cate sunt necesare.

O definitie de functie are formatul:

antet
corp

unde:

antet are formatul: tip nume(lista declaratiilor parametrilor formali);
corp este o instructiune compusa.

Tip este cuvantul cheie void pentru functii care nu returneaza nicio valoare la revenirea din ele. (detalii la capitolul 1.4.).

Apelul unei functii trebuie sa fie precedat de definitia sau de prototipul ei.

Prototipul unei functii contine informatii asemanatoare cu cele din antetul ei:

El poate avea acelasi format ca si antetul functiei, in plus este urmat de punct si virgula. (detalii la capitolul 1.11.).

Limbajul C vine cu o serie de functii care au o utilizare frecventa in programe. Ele se pastreaza intr-un fisier special in format OBJ, adica compilat si se adauga la fiecare program in faza de editare. Aceste functii sunt numite functii standard de biblioteca. Ele au prototipurile in diferite fisiere de extensie .h. Exemple de astfel de functii sunt cele de intrare/iesire (printf, scanf, puts, gets, getch, putch etc.), cele pentru calculul functiilor elementare (sqrt, sin, cos, atan, log, pow etc.) sau functiile de conversii (sscanf, sprintf etc.) etc.

Prototipurile functiilor standard se includ in program inaintea apelurilor lor folosind constructia #include tratata prin preprocesor (vezi capitolul 1.12.).

Exemple de fisiere cu prototipuri:

stdio.h pentru functiile printf, scanf, gets, puts etc.
conio.h pentru functiile putch, getch, getche etc.
math.h pentru functiile elementare sqrt, sin, cos etc.

In toate exemplele de pana acum au fost apelate numai functii standard.

Apelul unei functii care nu returneaza nicio valoare se realizeaza printr-o instructiune de apel. Aceasta are formatul:

nume(lista parametrilor efectivi);

unde:

nume este numele functiei care se apeleaza;
lista parametrilor efectivi este fie vida, cand functia nu are parametri, fie o expresie sau mai multe separate prin virgula.

Deci un parametru efectiv este o expresie.

Parametrii efectivi de la apel se corespund cu cei formali prin ordine. Trebuie amintit ca parametrii unei functii (formali sau efectivi) se mai numesc si argumente.

O functie care returneazaa o valoare poate fi apelata fie printr-o instructiune de apel, fie ca operand al unei expresii. In cazul in care functia se apeleaza printr-o instructiune de apel se pierde valoarea returnata. Cand functia se apeleaza ca operand al unei expresii, valoarea returnata de ea se utilizeaza la evaluarea expresiei respective.

De exemplu, sa consideram functia getch. Ea a fost apelata in exemplele de pana acum atat ca operand in expresii de atribuire de forma c = getch() cat si printr-o instructiune de apel de forma getch();.

In primul caz, valoarea codului ASCII citit de la tastatura se atribuie variabilei c. La cel de-al doilea apel, valoarea codului ASCII al caracterului citit de la tastatura nu este utilizata. In acest caz, apelul se face cu scopul de a afisa ecranul utilizator si de a bloca executia programului pana la actionarea unei taste oarecare, corespunzatoare caracterelor ASCII.

La apelul unei functii, valorile parametrilor efectivi se atribuie parametrilor formali corespunzatori, apoi executia continua cu prima instructiune din corpul functiei respective.

In cazul in care tipul unui parametru efectiv difera de tipul parametrului formal care-i corespunde, in limbajul C se converteste automat valoarea parametrului efectiv spre tipul parametrului formal respectiv. In C++ se utilizeaza o regula mai complexa pentru apelul functiilor si de aceea se recomanda sa nu se foloseasca nici in limbajul C regula de conversie implicita. In acest scop se poate utiliza operatorul de conversie explicita (tip), adica asa numitele expresii cast.

De exemplu, daca functia f are un parametru de tip double si n este o variabila de tip int, atunci in locul apelului: f(n); se recomanda a se folosi apelul cu conversie explicita a lui n spre tipul double: f((double)n);. Acest al doilea apel se realizeaza identic in C si C++.

La revenirea dintr-o functie apelata printr-o instructiune apel se va continua cu executia instructiunii urmatoare celei care a facut apelul. In cazul in care o functie este apelata ca un operand al unei expresii, la revenirea din ea se continua cu evaluarea expresiei respective.

Revenirea dintr-o functie se poate realiza in unul din urmatoarele doua moduri:

Instructiunea return este instructiunea de revenire dintr-o functie. Ea are formatele:

  1. return; sau
  2. return expresie;

Cel de-al doilea format se foloseste in corpul unei functii care returneaza o valoare la revenirea din ea. Valoarea expresiei din instructiunea return este chiar valoarea returnata de functie.

In cazul in care tipul acestei expresii difera de tipul care precede numele din antetul functiei, valoarea expresiei se converteste automat spre tipul din antet, inainte de a se reveni din functie.

Formatul 1 al instructiunii de revenire se utilizeaza numai in corpul unei functii care nu returneaza nicio valoare. Dintr-o astfel de functie se mai poate reveni si dupa executia ultimei instructiuni din corpul ei.

Faptul ca instructiunea return (in ambele formate) poate fi scrisa in orice punct al corpului functiei permite o mai mare flexibilitate in programare.

Exercitii:

4.27. Sa se scrie o functie care are ca parametru un intreg n din intervalul [0, 170], calculeaza si afiseaza pe n!

Numim factorial aceasta functie. Ea returneaza o valoare flotanta in dubla precizie. Rezulta ca functia are antetul:

double factorial(int n)

La inceput, functia verifica daca n apartine intervalului [0, 170]. In cazul in care n nu apartine intervalului, functia va retrna valoarea -1. Metoda de calcul este aceeasi cu cea utilizata in Programul 071.

double factorial(int n)
{
    double f;
    int i;

    if ( n < 0 || n > 170)
      return -1.0;
    for ( i = 2, f = 1.0; i <= n; i++)
      f *= i;
    return f;
}

4.28. Sa se scrie un program care calculeaza si listeaza pe m! pentru m = 0, 1, 2, ..., 170.

Acest program apeleaza Functia factorial definita mai sus pentru a-l calcula pe m! pentru o valoare data a lui m.

Definitia functiei factorial si functia principala care o apeleaza pot fi editate in acelasi fisier sursa. Cele doua functii pot fi editate in orice ordine. In cazul in care definitia functiei factorial se afla in fisierul sursa dupa functia principala, apelul functiei factorial din functia principala trebuie sa fie precedat de prototipul ei.

double factorial(int); /* prototipul functiei factorial */
#include <stdio.h>
#include <stdlib.h>

main()
{
    int m;

    for(m = 0 ; m < 171 ; m++) {
	  printf("m = %d\tm! = %g\n", m, factorial(m));
	  if((m + 1)%23 == 0) {
	    printf("actionati o tasta pentru a continua\n");
	    getch();
	  }
    }
}

double factorial(int n)
{
    double f;
    int i;

    if ( n < 0 || n > 170)
      return -1.0;
    for ( i = 2, f = 1.0; i <= n; i++)
      f *= i;
    return f;
}

Observatii:

  1. Functia factorial este apelata ca parametru efectiv al functiei printf. La apelul functiei factorial, valoarea parametrului ei efectiv m se atribuie parametrului formal n. Aceasta valoare este apoi folosita in corpul functiei factorial pentru a calcula pe n! si deci si pe m!. Valoarea respectiva se obtine ca valoare a lui f. La revenirea din functie se returneaza valoarea lui f, adica chiar m!, care se afiseaza folosind specificatorul de format %g.
  2. Cele doua functii pot fi editate in fisiere separate. De exemplu, presupunem ca functia factorial se editeaza in fisierul FUNCTIA081.C, iar functia principala in fisierul Programul 081 A - Factorial cu functia factorial/main.c. In acest caz, putem include fisierul FUNCTIA081.C folosind constructia #include pentru a compila impreuna cele doua functii. Includerea se poate face inainte de definitia functiei main, sau dupa ea. In cazul in care facem includerea in fata functiei main, nu mai este necesar sa indicam prototipul functiei factorial, deoarece apelul ei este precedat chiar de definitia functiei. Procedand in acest fel se obtine varianta de mai jos:

    • Programul 081 A - Factorial cu functia factorial
    #include <stdio.h>
    #include <stdlib.h>
    #include "FUNCTIA081.C"
    
    main()
    {
        int m;
    
        for(m = 0 ; m < 171 ; m++) {
    	  printf("m = %d\tm! = %g\n", m, factorial(m));
    	  if((m + 1)%23 == 0) {
    	    printf("actionati o tasta pentru a continua\n");
    	    getch();
    	  }
        }
    }
    

    NOTA: Fisierul FUNCTIA081.C trebuie sa fie in acelasi director cu fisierul main.c sau, altfel, trebuie precizata calea.

  3. O alta posibilitate de compilare si link-editare a functiilor unui program, editate in mai multe fisiere, este aceea de a utiliza un fisier de tip Project (cu extensia .prj). Un astfel de fisier, care contine numele fiecarui fisier (impreuna cu extensia lui) care se compileaza si link-editeaza in vederea obtinerii fisierului executabil al programului. In acest fisier pot fi indicatenu numai fisiere de tip sursa (cu extensia .c), ci si fisiere de tip obiect (cu extensia .OBJ), sau chiar fisiere de biblioteci de functii, altele decat cele ale sistemului.

    Avantajele fisierelor de tip Project constau in aceea ca, la fiecare lansare se compileaza in mod automat numai sursele in care s-au facut modificari. De aceea, in cazul programelor sursa mari se recomanda impartirea lor in mai multe fisiere sursa si compilarea lor prin utilizarea fisierelor de tip Project. De obicei, intr-un fisier sursa se grupeaza functii "inrudite", adica functii care prelucreaza in comun subseturi de date sau sunt logic legate intre ele.

    Intrucat exercitiile prezentate aici nu sunt de dimensiuni mari, vom utiliza frecvent includerile de fisiere folosind constructia #include a preprocesorului.

4.29. Sa se scrie o functie care are ca parametri doi intregi x si y, calculeaza si returneaza numarul aranjamentelor de x obiecte luate cate y.

La inceput, functia testeaza daca x apartine intervalului [1, 170], iar y intervalului [1, x]. In caz de eroare, functia returneaza valoarea -1.

Daca notam cu A(x,y) numarul aranjamentelor de x obiecte luate cate y, atunci:

A(x,y) = x*(x-1)*(x-2)*...*(x-y+1)
double aranjamente(int x, int y)
{
    double a;
    int i;

    if ( x < 1 || x > 170)
      return -1.0;
    if ( y < 1 || y > x)
      return -1.0;
    a = 1.0;
    i = x - y + 1;
    while ( i <= x )
      a *= i++;
    return a;
}

4.30. Sa se scrie un program care calculeaza si afiseaza numarul aranjamentelor de n obiecte luate cate k, pentru n = 1, 2, ..., 170 si k = 1, 2, ..., n.

Acest program apeleaza Functia aranjamente definita in exemplul precedent.

#include <stdio.h>
#include <stdlib.h>
#include "FUNCTIA082.C" /* include functia aranjamente */

main()
{
    int k, n;

    for(n = 0 ; n <= 170 ; n++) {
	  printf("actionati o tasta pentru a continua\n");
	  getch();
	  printf("\nn = %d\n", n);
	  for (k = 1; k <= n; k++) {
	    printf ("k = %d\tA(n,k) = %g\n", k, aranjamente(n,k));
	    if (k % 23 == 0) {
	      printf ("actionati o tasta pentru a continua\n");
	      getch();
	    }
	  }
    }
}

4.31. Sa se scrie o functie care are ca parametri doi intregi x si y, calculeaza si returneaza numarul combinarilor de x elemente luate cate y.

Functia testeaza daca x apartine intervalului [1, 170], iar y intervalului [0, x]. In caz de eroare, functia returneaza valoarea -1.

Pentru calculul combinarilor de x elemente luate cate y se determina raportul dintre numarul aranjamentelor de x elemente luate cate y si y!

In acest scop, functia de fata apeleaza functiile aranjamente si factorial (FUNCTIA081.C si FUNCTIA082.C).

#include "FUNCTIA081.C"
#include "FUNCTIA082.C"

double combinari(int x, int y)
{
    if ( x < 1 || x > 170)
      return -1.0;
    if ( y < 0 || y > x)
      return -1.0;
    if ( y == 0 || y == x)
      return 1.0;
    return aranjamente(x,y) / factorial(y);
}

Observatii:

  1. Un calcul mai rapid se realizeaza daca se tine cont de relatia: c(x,y) = c(x, x-y), unde prin c(x,y) s-a notat numarul combinarilor de x elemente luate cate y. Aceasta relatie este util sa se aplice pentru y >= x/2. In acest scop ultima instructiune se inlocuieste cu:

    if (y >= x/2)
      return aranjamente (x, x-y) / factorial (y)
    else
      return aranjamente (x,y) / factorial (y);
  2. O alta varianta pentru calculul numarului combinarilor este relatia:

    c(x,y) = (x/1)*((x-1)/2)*((x-2)/3)*...*((x-y+1)/y)
    In acest caz se evita calculul valorilor A(x,y) si y! care cresc rapid odata cu cresterea valorilor lui x si y. Aceasta se realizeaza cu ajutorul functiei de mai jos:
double combinari(int x, int y)
{
    int i;
    double c;
	
    if ( x < 1 || x > 170)
      return -1.0;
    if ( y < 0 || y > x)
      return -1.0;
    if ( y == 0 || y == x)
      return 1.0;
    for( i =1; i <= y; i++)
      c = (c * (x - i + 1)) / i;
    return c;
}

4.32. Sa se scrie un program care calculeaza si afiseaza numarul combinarilor de n obiecte luate cate k, pentru n = 1, 2, ..., 170 si k = 1, 2, ..., n.

Acest program apeleaza functia FUNCTIA083A.C.

#include <stdio.h>
#include <stdlib.h>
#include "FUNCTIA083A.C"

main()
{
    int k, n;

    for(n = 1 ; n <= 170 ; n++) {
	  printf("actionati o tasta pentru a continua\n");
	  getch();
	  printf("\nn = %d\n", n);
	  for (k = 0; k <= n; k++) {
	    printf ("k = %d\tC(n,k) = %g\n", k, combinari(n,k));
	    if (k % 23 == 0) {
	      printf ("actionati o tasta pentru a continua\n");
	      getch();
	    }
	  }
    }
}

4.33. Sa se scrie o functie care ridica la o putere intreaga nenegativa un numar flotant de tip long double.

Functia utilizeaza metoda din Programul 073.

long double ldrp(long double x, int n)
{
    float f, p;

    for (p = x, f = 1.0L; n; n >>= 1) {
      if (n & 1)
        f *= p;
      if (n > 1)
        p *= p;
    }
    return f;
}

4.34. Sa se scrie un program care tabeleaza puterile intregi ale unui numar flotant de tip long double, exponentul variind cu pasul 1, intre doua limite m si n.

Acest program apeleaza functia FUNCTIA084.C.

#include <stdio.h>
#include <stdlib.h>
#include "FUNCTIA084.C"

main()
{
    char t[255];
    float f, g;
    int i, m, n;

    do {
        printf("tastati numarul care se ridica la puteri\n");
        if (gets(t) == NULL) {
            printf("s-a tastat EOF\n");
            exit(1);
        }
        if (sscanf(t, "%f", &f) == 1)
            break;
        printf("nu s-a tastat un numar\n");
    } while (1);
    do {
        do {
            printf("tastati limita inferioara a exponentului\n");
            if (gets(t) == NULL) {
                printf("s-a tastat EOF\n");
                exit(1);
            }
            if (sscanf(t, "%d", &m) == 1)
                break;
            printf("nu s-a tastat un intreg\n");
        } while (1);

        do {
            printf("tastati limita superioara a exponentului\n");
            if (gets(t) == NULL) {
                printf("s-a tastat EOF\n");
                exit(1);
            }
            if (sscanf(t, "%d", &n) == 1)
                break;
            printf("nu s-a tastat un intreg\n");
        } while (1);

        if (m <= n)
            break;
        printf("limita inferioara %d o depaseste pe cea superioara %d\n", m, n);
    } while (1);

    if (f == 0 && m < 0) {
        printf("putere negativa pentru zero\n");
        exit(1);
    }
    for (i = m; i <= n; i++) {
        if (i < 0)
            g = 1.0/ldrp(f,-i);
        else
            g = ldrp(f,i);
        printf("%g la puterea %d este: %g\n", f, i, g);
    }
}

4.15. Apel prin valoare si apel prin referinta