Índex
Miguel A. Almarza
Departament d'Informàtica
IES Mare de Deu de la Merce

       

 
Capítol 3

Funcions en C++. Pas de paràmetres. Constructors de còpia. Funcions amigues.

  1. Assignació d'objectes.
    1. L'assignació dinàmica de memòria i l'assignació d'objectes.
  2. Pas d'un objecte com a paràmetre d'una funció.
    1. L'assignació dinàmica de memòria i el pas d'objectes com a paràmetre.
  3. Devolució d'objectes per una funció.
    1. L'assignació dinàmica de memòria i la tornada d'objectes d'una funció.
  4. Els constructors de còpia. Solució als problemes anteriors.
    1. Constructor de còpia per a la classe String.
    2. Ús del constructor de còpia per a la declaració d'un objecte String.
    3. Ús del constructor de còpia per passar un objecte a una funció.
    4. Ús del constructor de còpia per tornar un objecte una funció.
  5. Funcions amigues.
    1. Funcions independents amigues d'una classe.
    2. Funcions membre d'una classe amigues d'una altra classe.
    3. Totes les funcions membre d'una classe són amigues d'una altra classe.
  6. Exercicis.


3.1. Assignació d'objectes.

L'assignació d'un objecte a un altre consisteix en la còpia de les dades de les propietats d'un objecte a l'altre objecte. És un mecanisme similar a l'assignació de dades en les estructures del llenguatge C.

Recordem la classe Punt del primer capítol que tenia com a propietats x, y i Caracter.

Examinant l'exemple següent veiem que declarem el punt A deixant que el constructor per defecte li assigni dades a les propietats.

Assignació d'un objecte
main()
{
    Punt A;
    Punt C(3,10,'X');

    A=C;
    A.ImprimeixDades();
}

Creem el Punt C amb els valors 3 i 10 per a x i y, així com el caràcter X per a la propietat Caracter.

Fem l'assignació A = C; i ara totes les dades de C es copien al punt A.

3.1.1 L'assignació dinàmica de memòria i l'assignació d'objectes.

Quan en un objecte d'una classe fem assignació de memòria per a les seves dades hem de treballar amb cura en el moment d'assignar les seves dades a un altre objecte.

Examinem am detall el problema de la classe string, que per tal de recordar-la l'hem posada a continuació:

Assignació d'un objecte amb assignació dinàmica de memòria.
#include <iostream.h>
#include <alloc.h>
#include <string.h>
#include <stdlib.h>

class String
{
    char * Cadena;
    int Longitud;
public:
    String();
    String(char * Punter);
    ~String();
    void Posa(char * Punter);
    void ImprimeixDades();
}

String::String(char * Punter)
{
    if ((Cadena = new char [strlen(Punter)+1]) == NULL)
    {
        cout << "Falta memòria per a aquest string\n";
        exit(1);
    }
    strcpy(Cadena,Punter);
    Longitud=strlen(Cadena);
}

String::String()
{
    Cadena = NULL;
    Longitud=0;
}

String::~String()
{
    if (Cadena) delete Cadena;
}

void String::Posa(char * Punter)
{
    delete Cadena;
    if ((Cadena = new char [strlen(Punter)+1]) == NULL)
    {
        cout << "Falta memòria per a aquest string\n";
        exit(1);
    }
    strcpy(Cadena,Punter);
    Longitud=strlen(Cadena);
}

void String::ImprimeixDades()
{
    cout << "Objecte string.\n";
    cout << "Cadena:   " << Cadena << "\n";
    cout << "Longitud: " << Longitud << "\n";
}

main()
{
    String Nom1("Lopez Perez, Pere");
    String Nom2("Bru Pi, Pau");
    Nom2 = Nom1;
}

Veiem que el constructor de la classe string, quan rep una cadena de caràcters per crear un nou objecte, fa una reserva de memòria per posar aquesta cadena, i podem dir que aquesta memòria està reservada per a l'objecte creat.

Així les instruccions

String Nom1("Lopez Perez, Pere");
String Nom2("Bru Pi, Pau");


creen els objectes Nom1 i Nom2 i fan aparèixer una memòria per a dades dels dos objectes com la de la figura següent.

La instrucció Nom2 = Nom1; copia les dades de Nom1 a Nom2, es a dir, copia l'adreça de la Cadena de Nom1 i el valor 17 per a la longitud de la cadena.

Tenim doncs que els dos punters apunten a la posició de memòria on es troba Lopez Perez, Pere i cap dels dos punters apunta a Bru Pi, Pau.

Es a dir, que el nostre programa ha perdut el control sobre aquesta zona de memòria (la de Bru Pi, Pau). És un errada que pot arribar a ser greu i a fer trencar l'execució del programa.


3.2. Pas d'un objecte com a paràmetre d'una funció.

Un objecte es pot passar com a paràmetre a una funció.

Examinem l'exemple següent. Suposem que volem afegir al nostre programa de punts una funció que ens torni el mòdul d'un punt, entenent que el mòdul és la longitud des de l'origen de coordenades fins al punt en qüestió.

Així farem una funció i un us d'ella com en el programa següent:

Objectes com a paràmetres.
float Modul(Punt P)
{
    int x,y;
    x= P.TornaX();
    y= P.TornaY();
    return sqrt(x*x + y*y);
}

main()
{
    Punt D(3,4);
    cout << Modul(D) << '\n';
}

Veiem que en el programa principal declarem un punt D que es passa a la funció Modul. Per a aquesta funció es crea un nou punt P i es copien les dades de D al punt P. Es a dir que no s'executa cap constructor pel fet de tenir una declaració del tipus Punt P en la declaració de la funció. Així tenim, mentre s'està executant la funció Modul, dos punts el punt original D i una còpia seva P.

Quan la funció Modul acaba el destructor dels Punts és cridat de forma automàtica i destrueix el punt P.

IMPORTANT: Quan passem un objecte a una funció es crea una còpia de l'objecte per a la funció sense cridar el constructor corresponent. Quan acaba d'executar-se la funció es crida el destructor per destruir l'objecte passat.

3.2.1 L'assignació dinàmica de memòria i el pas d'objectes com a paràmetre.

De la mateixa manera que a l'assignació d'objectes, tenim un problema quan es passen com a paràmetres objectes que tenen assignació dinàmica de memòria. El problema és similar, però no és igual.

Veiem que passa amb un objecte de la classe string que estem treballant. Fem una funció, que es diu ComptaAs, que rep un objecte string com a paràmetre i que torna el número de lletres A que té aquest string.

Quan cridem a la funció amb les instruccions

String Tira("AlA AlA");
cout << ComptaAs(Tira) << '\n';

es crea una còpia de l'objecte Tira, còpia per a la funció, que es diu S. Fins aquí tot va bé, s'executa la funció però quan la funció acaba es crida el destructor per a l'objecte S, i aquest destructor si ens recordem desassigna la memòria que té assignat el punter a les dades. Com veieu a la figura aquesta memòria és la mateixa que la de l'objecte Tira i per tant Tira ha perdut l'accés a les seves dades. Errada greu.

Objectes com a paràmetres.
class String
{
    char * Cadena;
    int Longitud;
public:
    String();
    String(char * Punter);
    ~String();
    void Posa(char * Punter);
    void ImprimeixDades();
    char * TornaPunter();
};

char * String::TornaPunter()
{
    return Cadena;
}

int ComptaAs(String S) 
{   /*Aquesta funció té una errada greu*/
    int i=0;
    char * Punter;
    Punter = S.TornaPunter();
    while (*Punter)
    {
        if (*Punter == 'A') i++;
        Punter ++;
    }
    return i;
}

main()
{
    String Tira("AlA AlA");
    cout << ComptaAs(Tira) << '\n';
}

3.3 Devolució d'objectes per una funció.

Una funció pot tornar un objecte de la mateixa manera que pot rebre objectes.

Si examinem el programa següent amb la classe Punt veiem que la funció CanviaXY rep un punt i torna un altre amb les coordenades permutades respecte a les de l'objecte rebut.

Devolució d'un punt per una funció.
#include <iostream.h>
#include <conio.h>

class Punt
{
    int x;
    int y;
    char Caracter;

 public:
    Punt() { x=y=0; Caracter = ' '; };
    Punt(int n1,int n2)
    {
       x=n1;
       y=n2;
       Caracter = ' ';
    };
    Punt(int n1,int n2,char Car)
    {
       x=n1;
       y=n2;
       Caracter = Car;
    };

    ~Punt(){cout << "Destructor \n";};

    void PosaCaracter(char c) {Caracter = c;};
    void PosaCoordenades(int n1, int n2)
    {
       x = n1;
       y = n2;
    };
    void Dibuixat()
    {
       gotoxy(x,y);
       cout << Caracter;
    };
    int TornaX() {return x;};
    int TornaY() {return y;};
    int TornaCaracter(){return Caracter;};
    void ImprimeixDades()
    {
       cout << "x = " << x;
       cout << ", y = " << y;
       cout << ", Caracter = " << Caracter << "\n";
    };
};

Punt CanviaXY(Punt Q)
{
    Punt P=Q;
    int x,y;
    x = P.TornaX();
    y = P.TornaY();
    P.PosaCoordenades(y,x);

    return P;
}

main ()
{
    Punt A(1,5,'A');
    Punt B;
    B = CanviaXY(A);
    B.ImprimeixDades();
}


El mecanisme de devolució d'un objecte és una mica més complicat. Just abans de acabar la funció es crea automàticament un objecte provisional que és el que servirà per copiar les dades.

Així es crearà un objecte provisional per a la devolució del punt P just abans d'acabar la funció CanviaXY. Aquest objecte provisional serveix per copiar les dades al punt B de la funció main.

Una vegada copiades les dades adients aquest objecte provisional es destruït pel destructor de la seva classe. En el nostre exemple pel destructor de la classe Punt que només escriu un petit missatge en pantalla.

A la figura següent veiem quantes vegades ha estat executat aquest destructor en el nostre programa i els moments en que ha estat executat:

Sortida per pantallaS'executa:
DestructorEl destructor pel punt Q de la funció CanviaXY
DestructorEl destructor pel punt P de la funció CanviaXY
DestructorEl destructor pel punt provisional després de l'assignació al punt B de la funció main
x=1,y=5,Caracter=AB.ImprimeixDades()
DestructorEl destructor pel punt B de la funció main
DestructorEl destructor pel punt A de la funció main

3.3.1 L'assignació dinàmica de memòria i la tornada d'objectes d'una funció.

Com hem dit abans al tornar un objecte es crea un objecte provisional on es copien les dades de l'objecte a tornar abans d'acabar la funció.

Suposem que aquests objecte fa us de memòria dinàmica. Com que, després es copien les dades a l'objecte definitiu (instrucció que ha cridat a la funció) estem fent una assignació amb punters i quan es crida el destructor per a l'objecte provisional s'allibera aquesta memòria apuntada pels dos objectes. Errada greu.

A l'exemple següent veiem que hi ha una funció anomenada AlReves, que rep un objecte String i que torna un altre objecte String amb l'ordre dels caràcters canviats.

En aquesta funció es fa la errada que acabem d'explicar. També es fa la de l'apartat 321.

Devolució d'un punt per una funció.
/*Aquesta funció té una errada greu*/
String AlReves(String S)
{
    int Lon;
    char C;
    char * Punter;

    Lon= S.TornaLongitud();
    Punter = S.TornaPunter();
    for(int i=0; i< Lon/2; i++)
    {
        C=Punter[i]; Punter[i]=Punter[Lon-i-1];Punter[Lon-i-1]=C;
    }

    return S;
}

main()
{
    String Tira1("ABCDEFG");
    String Tira2;
    Tira2 = AlReves(Tira1);
    Tira2.ImprimeixDades();
}


3.4. Els constructors de còpia. Solució als problemes anteriors.

La solució als problemes anteriors està en el ús de constructors de còpia, una forma del constructor que rep una referència a un objecte de la classe i que s'executa de forma automàtica en els tres casos de la taula:

Declaració i inicialització en la mateixa instruccióMiclasse Ob1 = Ob2;
Crida a una funció amb un objecte com a paràmetre... Funcio(Ob);
Devolució d'un objecte per part d'una funcióOb = Funcio(...);


Cal dir que aquest constructor de còpia no s'executa en una assignació normal entre objectes, assignació del tipus a = b, car en aquest cas els dos objectes estan construïts i es fa una còpia bit a bit de les dades de b en a. Cal evitar fer assignacions d'aquest tipus quan hi ha assignació dinàmica de memòria, encara que existeix la solució de la sobrecàrrega de l'operador = que estudiarem en el capítol 4.

Un constructor de còpia té la forma genèrica
NomClasse (const NomClasse &Objecte)
{
    Instruccions del constructor.
}

3.4.1 Constructor de còpia per a la classe String.

Constructor de còpia per a la classe String.
class String
{
    char * Cadena;
    int Longitud;
public:
    String(){Cadena = NULL;Longitud=0;};
    String(char * Punter);
    String(String &S);
    ~String(){if (Cadena) delete Cadena;};
    void Posa(char * Punter);
    void ImprimeixDades();
    char * TornaPunter(){return Cadena;}
    int TornaLongitud(){return Longitud;};
};

String::String(String &S)
{
    if ((Cadena = new char [S.TornaLongitud()]) == NULL)
    {
        cout << "Falta memòria per a aquest string\n";
        exit(1);
    }
    strcpy(Cadena,S.TornaPunter());
    Longitud=strlen(Cadena);
    cout << "Executat el constructor de còpia\n";
}
Veiem que en aquest constructor el que es fa és crear memòria per a un nou objecte on copiarà les dades rebudes sobre l'objecte S.

Per tant no és fa una copia de les dades a nivell de bit com abans i de forma automàtica, sinó que és el programador el que decideix com ha de fer-se la còpia.


3.4.2 Ús del constructor de còpia per a la declaració d'un objecte String.

Ús del constructor de còpia per a la declaració d'un objecte String.
main()
{
    String Tira1("ABCDEFG");
    String Tira2(Tira1);
    Tira2.ImprimeixDades();
}
La instrucció de declaració de l'objecte Tira2 i inicialització amb les dades de l'objecte Tira1 és

String Tira2(Tira1);

Veiem que quan s'executa aquesta instrucció s'executa automàticament el constructor de còpia que hem fet i per tant l'objecte Tira2 té la seva pròpia memòria assignada. No hi ha cap errada.

Sortida per pantalla


3.4.3 Ús del constructor de còpia per passar un objecte a una funció.

Ús del constructor de còpia per passar un objecte a una funció.
int ComptaAs(String S)
{
    int i=0;
    char * Punter;
    Punter = S.TornaPunter();
    while (*Punter)
    {
        if (*Punter == 'A') i++;
        Punter ++;
    }
    return i;
}

main()
{
    String Tira("AlA AlA");
    cout << ComptaAs(Tira) << '\n';
}
La funció ComptaAs rep com a paràmetre un objecte de la classe String que ha de copiar les seves dades a l'objecte S.

S'executa de forma automàtica el constructor de còpia per a posar aquestes dades a l'objecte S.

Així tenim que l'objecte S té la memòria dinàmica assignada diferent de la de l'objecte original i quan acabi l'execució de la funció ComptaAs el destructor de la classe String lliura la memòria de S i no la de l'objecte original.

Veiem que a la sortida per pantalla apareix la frase

Executat el constructor de còpia

en el moment d'execurtar-se aquest constructor per a l'objecte S. Després apareix el número de lletres A, en aquest cas 4 lletres A.
Sortida per pantalla


3.4.4 Ús del constructor de còpia per tornar un objecte una funció.

Ús del constructor de còpia per tornar un objecte una funció.
String AlReves(String S)
{
    int Lon;
    char C;
    char * Punter;

    Lon= S.TornaLongitud();
    Punter = S.TornaPunter();
    for(int i=0; i< Lon/2; i++)
    {
        C=Punter[i]; Punter[i]=Punter[Lon-i-1];Punter[Lon-i-1]=C;
    }

    return S;
}

main()
{
    String Tira1("ABCDEFG");
    String Tira2;
    Tira2 = AlReves(Tira1);
    Tira2.ImprimeixDades();
}
Tenim la funció AlReves que rep un objecte String i que també en torna un altre.

Així el constructor de còpia s'executa dues vegades. La primera per crear l'objecte S que rep la funció. La segona vegada que s'executa aquest constructor és per a l'objecte provisional que es crea automàticament per a tornar l'objecte S i que es copiarà a sobre de l'objecte Tira2.

Per tant, i com veiem a la sortida per pantalla, apareix dues vegades la frase

Executat el constructor de còpia

car la funció constructora ha estat executada dues vegades.
Sortida per pantalla


3.5 Funcions amigues.

Funcions amigues d'una classe són funcions que, sense ser funcions membre de la classe, tenen accés als elements privats i protegits de la classe.

3.5.1 Funcions independents amigues d'una classe.

Per fer que una funció sigui amiga d'una classe hem de fer la declaració de la funció amb la paraula reservada friend dins la declaració de la classe però hem de definir-la fora de la classe, com una funció normal de programa.

Podem fer ús d'aquesta característica per escriure funcions que han de fer tasques simultànies amb diferents objectes de la classe.

Una mateixa funció pot ser amiga de diferents classes, quan hem de relacionar d'alguna manera les propietats d'objectes de diferent classe.

Per comprendre l'us de les funcions amigues veiem l'exemple següent:

Funcions amigues.
#include <iostream.h>
#include <math.h>

class Complex
{
    float a,b;
public:
    Complex(float x, float y){a=x;b=y}
    void Imprimeix(){cout << a << " + "  << b << "i";}

    friend int ModulMajor(Complex z1, Complex z2);
};

int ModulMajor(Complex z1, Complex z2)
{
    float M1 = sqrt(z1.a*z1.a + z1.b*z1.b);
    float M2 = sqrt(z2.a*z2.a + z2.b*z2.b);

    return M1 > M2;
}

main()
{
    Complex z1(2,5), z2(4,8);
    z1.Imprimeix();
    cout << "\n";
    z2.Imprimeix();
    cout << "\n";
    cout << ModulMajor(z2,z1);
}
Declarem una classe Complex que té a i b com a propietats. Hem reduït al mínim possible les funcions membre amb un constructor i la funció Imprimeix.

Després, dins la declaració de la classe Complex, es declara la funció amiga (friend) ModulMajor que segons veiem es defineix fora de la classe com una funció normal.

També veiem que tenim accés a les propietats privades dels objectes de la classe Complex, amb instruccions del tipus z1.a.

Nota: aquesta funció calcula el mòdul (M1 i M2) dels dos complexos z1 i z2 (Distància del punt (a,b) a l'origen de coordenades). Torna Cert si M2 és major que M1 i fals en cas contrari.
Sortida per pantalla

3.5.2 Funcions membre d'una classe amigues d'una altra classe.

Podem fer que una funció Func sigui membre de la classe B i amiga de la classe A. Hem de fer ús de la notació següent.

class A
{
    ......
    friend tipus B::Func(Paramètres);
    ......
};

Així que la funció Func de la classe B pot accedir als membres privats de la classe A.

Funcions membre d'una classe amigues d'una altra classe.
#include <iostream.h>

class A; //Declaració forward

class B
{
    int DatB;
public:
    void PosaDada(int n) {DatB=n;}
    int TornaSuma(A Obj);
};

class A
{
    int DatA;
public:
    void PosaDada(int n){DatA=n;}
    friend int B::TornaSuma(A Obj);
};

int B::TornaSuma(A Obj)
{
    return Obj.DatA + DatB;
};

main()
{
    A Obj1;
    B Obj2;

    Obj1.PosaDada(25);
    Obj2.PosaDada(13);

    cout << "La funció TornaSuma dona: " << Obj2.TornaSuma(Obj1);
}
La funció TornaSuma és una funció membre de la classe B i és amiga de la classe A.

Com que la funció ha de declarar-se dins la classe B hem de posar una declaració forward de la classe A abans.

Observem que la funció es declara com amiga dins la classe A.

També observem que la instrucció

return Obj.DatA + DatB;

d'aquesta funció fa ús de les dades privades de A.

Com veiem a la instrucció ens adonem que la funció és una funció membre de la classe B ja que Obj2 és un objecte d'aquesta classe.

3.5.3 Totes les funcions membre d'una classe són amigues d'una altra classe.

Aquest paràgraf ho deixem per més endavant.

3.6. Exercicis.

  1. Escriu el programa que tens a l'apartat 3.1.1 i respon a les preguntes següents:



  2. Constructors i pas de paràmetres d'objectes a funcions.



  3. Objectes com a paràmetres.



  4. Escriu el programa de l'apartat 3.3 i comprova que quan una funció torna un objecte es crea un objecte provisional i s'executen la funció constructora i destructora per a aquest objecte.

    En quin moment exacte de l'execució del programa es crea i es destrueix aquest objecte? (Pots veure aquest moment si fas un debug amb l'entorn de programació).


  5. Comprova mitjançant el debug que es fan dues errades greus quan executem el programa de l'apartat 3.3.1.


  6. Escriu tots els programes del paràgraf 3.4. Comprova en tots ells que s'executa el constructor de copia i escriu quina instrucció de la funció main és la que provoca l'execució d'aquest constructor de còpia.

    Per fer la comprovació has de fer ús del debug.


  7. Una classe VectorEnters té com a dades un punter a vector d'enters i in enter que indicarà la longitud del vector.

    Fes la classe amb les funcions membre del quadre següent.

    Observa que has de fer un constructor que rep un vector a enters i un constructor que rep un objecte de tipus VectorEnters. Aquest és el constructor de còpia.

    Fes la funció main e indica quantes vegades s'executa el constructor de còpia i en qui moment de l'execució del programa.

    class VectorEnters
    {
        int * Vector;
        int Longitud;
    public:
        VectorEnters(int *Vector, int L);
        VectorEnters(VectorEnters &Obj);
        void Imprimeix();
        int SumaVector();
        float Mitjana();
    	~VectorEnters();
    };
    
    main()
    {
        int V[10] = {1, 3, 5, 7, 9 , 11, 13, 15 , 17, 19}
        int L = 10;
    
        VectorEnters Senars(V,10);
        VectorEnters C(Senars);
        cout << C.SumaVector();
    }
    

  8. Afegeix a la classe Complex del paràgraf 3.5.1 una funció amiga que ens permeti saber si dos objectes d'aquesta classe són iguals.
  9. Escriu l'exercici del paràgraf 3.5.2.


  10. Dos classes diferents comparteixen una impressora. Fes una funció independent, amiga d'aquestes dues classes que rebi un objecte de cada classe i ens torni cert si alguna està fent ús de la impressora i fals en cas contrari.

    Les classes han de tenir una dada que sigui booleana i que es digui ImpresoraEnUs.