Sunday, December 20, 2015

Limbajul C++. Functii. Transfer prin valoare, referinta, adresa


Stim ca la transmiterea prin valoare a argumentelor unei functii, prelucrarea nu se face asupra variabilelor-argument, ci asupra unor copii. Copiile sunt variabile interne blocului functiei, de aceea durata lor de viata este limitata la blocul functiei. Reguli:

1) nu folosim transmisia prin valoare in cazul unor variabile de mari dimensiuni, pentru ca dimensiunea trebuie replicata pentru copiile folosite in corpul functiei;

2) nu folosim transferul prin valoare cand vrem ca variabilele transmise ca argument sa fie modificate.

Pentru cazurile mentionate anterior putem folosi transferul prin referinta (alias al variabilei-argument, permis doar in C++) sau transferul prin adresa (cu pointeri, permis in C/C++). 

Transferul prin adresa este, de fapt, tot un transfer prin valoare.
Ce se transfera in parametrul functiei este o copie a pointerului. O copie a pointerului indirecteaza aceeasi variabila (sau, altfel spus, un pointer-copie are drept valoare aceeasi adresa de variabila ca pointerul "original"). Modificarea variabilei indirectate de pointerul-copie in corpul (blocul) functiei se face in urma aplicarii operatorului de dereferentiere. Dereferentierea copiei unui pointer da acelasi rezultat ca dereferentierea originalului. Nu e nimic mistic aici, suntem, in mod absolut formal, tot in cazul transferului prin valoare :-)

Revenind la referinte si la transferul lor ca parametri de functie, tipul de return, daca exista, poate fi o valoare, o referinta sau un pointer, cu specificarea, in ultimele doua cazuri, ca nu putem returna referinta sau adresa unei variabile locale functiei, deoarece aceasta isi inceteaza existenta odata cu iesirea din blocul functiei.

In exemplul urmator, este definita o functie de alocare dinamica a unui tablou, pe care o apelam din functia main. Atribuim rezultatul unui pointer (cu tip corespunzator). Initializam si efectuam prelucrari asupra elementelor tabloului, de ex. depozitam in fiecare element de index i suma numerelor naturale pana la i, inclusiv. La final, afisam tabloul si eliberam spatiul ocupat de tablou.

Remarca importanta: spatiile de memorie alocate dinamic NU au durata de viata, precum au variabilele locale ale unei functii oarecare. De aceea, memoria alocata dinamic trebuie elibarata manual.

//Functii cu transferul argumentelor prin valoare/adresa

#include <iostream>

using namespace std;

int* CreezTablou(int);

void PrelucrezTablou(int*, int);

void AfisezTablou(int*, int);

int main()
{
    int n;
    cout << "\nDimensiunea tabloului n = ";
    cin >> n;
    int* q = CreezTablou(n);
    PrelucrezTablou(q, n);
    AfisezTablou(q, n);
    delete [] q;
    return 0;
}

int* CreezTablou(int n)
{
    return new int [n];
}

void PrelucrezTablou(int* p, int n)
{
     for(int i = 0; i < n; i++)
     {
          *(p + i) = 0; 
//  echivalent: p[i] = 0; oricarui pointer i se poate aplica operatorul []
          for (int j = 1; j <= i; j++) *(p + i) += j;  
     }

// am initializat elementele din p[i] cu suma elementelor pana la indecsii i

void AfisezTablou(int* p, int n)
{
     cout << "\n_________________________\n";
     for(int i = 0; i < n; i++)
     {
         cout << "p[" << i << "] = " << *(p + i) << "\n";
     }
}



--------------------------------------------------------
Valori implicite in prototipul unei functii

int Volum( int lungime, int latime, int inaltime = 10); // definitie Volum
/..../

int Volum ( int lungime, int latime, int inaltime = 10) //declaratie Volum
{
            return lungime*latime*inaltime;
}

Reguli:

1) Apelul Volum(10, 10) returneaza 1000. Apelul Volum(10, 10, 10) returneaza 1000. Apelul (10, 10, 1) returneaza 100, altfel spus, transferul tuturor argumentelor in parametrii formali are intaietate in cazul in care exista parametri cu valoare implicita.

2) Daca in prototipul unei functii apare un parametru cu valoare implicita, eventualii parametri care urmeaza in prototip trebuie, de asemenea, sa aiba specificate valori implicite. Altfel spus, parametrii cu valori implicite sunt ultimii in prototipul unei functii.

Ex: int Volum1(int lungime, int latime = 10, int inaltime = 11);

C++ permite supraincarcarea functiilor sau crearea unor functii cu acelasi nume dar cu tip return si parametri diferiti. Ex:

int Multiply(int x, int y);

double Multiply(double z, double w);

Functii inline

Cuvantul inline specificat in fata definitiei unei functii produce urmatorul efect: cand numele functiei este intalnit in cod, nu se mai realizeaza apelul, ci pur si simplu este copiata functia respectiva in locul unde ar trebui sa fie apelata. 

Acesta este un mecanism de optimizare, care elimina apelul de functie (procesarea se face local) si este recomandat doar pentru functiile de mici dimensiuni, cum este functia Volum din exemplul anterior.

Alt exemplu: operatii cu numere complexe folosind structuri si transfer prin referinta

// Adunarea, scaderea si conjugatul numerelor complexe

#include <iostream>

using namespace std;

struct Complex
{
 double _re;
 double _im;
};

void Ad(const Complex &c1, const Complex &c2, Complex &c3);

// primele doua argumente nu sunt modificate de functie
// de aceea parametrii formali corespunzatori sunt referinte constante

void Dif(const Complex &c1, const Complex &c2, Complex &c3);

void Conj(const Complex &c1, Complex &c2);

void Show(Complex c);

int main()

{
    Complex c1, c2, c3, c4;
    cout << "\nPartea reala primul numar = ";
    cin >> c1._re;
    cout << "\nPartea imaginara primul numar = ";
    cin >> c1._im;
    cout << "\nPartea reala al doilea numar = ";
    cin >> c2._re;
    cout << "\nPartea imaginara al doilea numar = ";
    cin >> c2._im;
    Ad(c1, c2, c3);
    Show(c3);
    Dif(c1, c2, c4);
    Show(c4);
    Conj(c1, c3);
    Show(c3);
    Conj(c2, c4);
    Show(c4);  
    return 0;
}
void Ad(const Complex &a, const Complex &b, Complex &c)
{
     c._re = a._re + b._re;
     c._im = a._im + b._im;
}
void Dif(const Complex &a, const Complex &b, Complex &c)
{
     c._re = a._re - b._re;
     c._im = a._im - b._im;
}
void Conj(const Complex &a, Complex &c)
{
     c._re = a._re;
     c._im = -a._im;
}
void Show(Complex d)

{
     cout << "\n" << d._re;
     if(d._im >= 0)
     {
           cout << " + " << "i*(" << d._im << ").";
     }
     else 
     {
           cout << " - " << "i*(" << -d._im << ").";
     }
}


-------------------------------------------------------
Apelul recursiv

O functie poate primi ca argument valoarea de return a functiei insasi, adica se poate autoapela. Pentru ca numarul de apelari sa fie finit trebuie impusa o conditie corespunzatoare de stop in definitia functiei.

De pilda, in cazul functiei factorial se poate impune conditia ca apelurile sa se opreasca la valoarea 2. In cazul in care valorile permise sunt inferioare lui 2 (adica 0 sau 1) functia returneaza 1, conform definitiei factorialului: 1! = 1 si 0! = 1.

//Factorial Recursiv

int RecFactor(int n)
{
 if(n < 2) return 1;
 return n*RecFactor(n - 1);
}
.....
Sa presupunem, pentru claritate, ca vrem sa calculam factorial de 4. Valoarea ceruta nu se poate returna in aceasta etapa dar putem expanda expresia: 

return 4*RecFactor(3) <==> 4*(3*RecFactor(2)) <==>  4*3*(2*RecFactor(1)) <==> 4*3*2*1. 

Dupa apelul initial, au loc trei autoapeluri ale RecFactor(n), pentru n = 3, 2, 1. Conditia fixata pentru n < 2 impiedica autoapelurile in continuare.

Codul urmator implementeaza apelul recursiv pentru calculul numerelor Fibonacci, date, dupa cum se stie, de f(n) = f(n - 1) + f(n - 2), cu f(1) = 1 si f(2) = 1.

// Fibonacci recursiv

#include <iostream>

using namespace std;

int RecFib(int);

void Show(int);

int main()
{
    int n;
    cout << "Numere Fibonacci pana la n = ";
    cin >> n;
    Show(n);
    return 0;
}

int RecFib(int n)
{
    if(n == 1 || n == 2)
    {
         return 1;
    }
    else
    {
        return (RecFib(n - 1) + RecFib(n - 2));
    }
}

void Show(int n)
{
     cout << "\n________________________\n";
     for(int i = 1; i <= n; i++)
     {
         cout << "\nAl " << i <<"-lea nr. Fibonacci este " << RecFib(i);
     }
}

La fel ca in cazul factorialului, expresia return RecFib(n - 1) + RecFib(n - 2) este expandata prin autoapelare pana la intalnirea valorilor de stop, adica 1 si 2. 



Merita reamintita, cu aceasta ocazie, implementarea nerecursiva a numerelor Fibonacci.

//Fibonacci nerecursiv

#include <iostream>

using namespace std;

int FibNum(int);

void Show(int);

int main()
{
    cout << "\nNumere Fibonacci pana la n = ";
    int n;
    cin >> n;
    Show(n);
    return 0;
}

int FibNum(int n)
{
    if(n == 1 || n == 2) return 1;

    int a = 1;
    int b = 1;
    int rez;
    for(int i = 3; i <= n; i++)
      {
          rez = b + a;
          a = b;
          b = rez;
      }
    return rez;
}

void Show(int n)
{
     cout << "\n______________________\n";
     for(int i = 1; i <= n; i++)
     {
           cout << "\nAl " << i << "-lea numar Fibonacci: " << FibNum(i);
     }
}
         
         
La prima vedere, implementarea nerecursiva pare sa aiba codul un pic mai "stufos". La apel se vede insa adevarata diferenta.

Fibonacci nerecursiv afiseaza cvasi-instantaneu primele 45 de numere (ultimul rezultat permis in domeniul intregilor cu semn este pentru n = 46) dar Fibonacci recursiv are nevoie de zeci de secunde (!!!) pentru a obtine acelasi rezultat pe un sistem P6100/2 GHz la 4 GB memorie instalata.

No comments:

Post a Comment