CONSTRUCTOR DE COPIA Y ASIGNACIÓN DE OBJETOS EN C++ Cuando se pasa un objeto a un función, o esta lo devuelve, se pueden presentar dificultades. El modo de evitar estos problemas consiste en definir un constructor de copia. Cuando se pasa un objeto a una función, se realiza una copia bit a bit de ese objeto y se guarda en el parámetro de la función que recibe el objeto. Sin embargo, hay casos en los que no es deseable la copia exacta del objeto. Por ejemplo, si el objeto contiene un puntero a la memoria asignada, la copia apuntará a la misma memoria que el objeto original. Por consiguiente, si la copia realiza un cambio sobre el contenido de esta memoria, esta última también cambiará para el objeto original. Cuando la función finaliza se destruye la copia mediante una llamada a su destructor, lo cual puede ocasionar efectos colaterales que afectarían al objeto original. Se produce una situación parecida cuando una función devuelve un objeto. Normalmente el compilador genera un objeto temporal que contiene una copia del valor devuelto por la función. Éste desaparece una vez que se devuelve su valor a la rutina que provocó la llamada, mediante una llamada al destructor temporal. Sin embargo, si el destructor elimina alguna información necesaria por esa rutina (por ejemplo, liberar memoria asignada dinámicamente) continuarán los problemas. Estos problemas derivan de la copia bit a bit, de tal forma que para prevenirlos, necesitamos definir de forma exacta que sucede cuando se hace una copia de un objeto de manera que podamos evitar estos efectos colaterales. La forma de llevar a cabo esto es mediante la creación de un constructor de copia. C++ define dos tipos distintos de situaciones en las que se da el valor de un objeto a otro. La primera es la asignación. La segunda, la inicialización, que puede tener lugar de tres formas: <!--[if !supportLists]-->-<!--[endif]--> Cuando se usa un objeto para inicializar otro en una sentencia de declaración <!--[if !supportLists]-->-<!--[endif]--> Cuando se pasa un objeto como parámetro a una función <!--[if !supportLists]-->-<!--[endif]--> Cuando se crea un objeto temporal para ser usado como el valor devuelto por una función El constructor de copias solo se aplica a la inicialización, no a la asignación. Veamos este ejemplo: #include <iostream.h> class info{ private: int a,b; public: void obtener(int *,int *); info(int dato1,int dato2){ a=dato1; b=dato2; }; }; void info::obtener (int *dato1,int *dato2){ *dato1=a; *dato2=b; } int main(){ info i1(1,2); info i2=i1; int d1,d2; i1.obtener(&d1,&d2); cout << “Atributos i1: a= ” << d1 << ” b= ” << d2 << endl; i2.obtener(&d1,&d2); cout << “Atributos i2: a= ” << d1 << ” b= ” << d2 << endl; return 0; } Toda clase contiene un constructor de copia implícito que realiza una copia bit a bit del objeto origen al objeto destino como citamos anteriormente. Es lo que sucede en el ejemplo anterior. Por supuesto, es posible crear un nuevo constructor que oculte al existente por defecto. Para definir un constructor de copia debemos seguir estrictamente la siguiente sintaxis: nombre_clase::nombre_clase (const nombreclase &) Supongamos una clase mi_cadena. En ella, el constructor de copia por defecto crearía problemas. El puntero a carácter del objeto recién creado apuntaría a la misma cadena que el objeto existente. Al borrar cualquiera de los dos se liberaría por la cadena, por lo que el puntero del otro objeto apuntará a una zona de memoria disponible para el programa. Cuando se procediese a borrar el segundo objeto, se produciría un claro problema. El siguiente código evitaría males mayores: mi_cadena::mi_cadena(const mi_cadena &origen){ clave=origen.clave; cadena=new char [strlen(origen.cadena)+1]; strcpy(cadena, origen.cadena); } Por supuesto, aparte del código anterior, habría que añadir a la parte pública la siguiente línea: mi_cadena (const mi_cadena&); También es posible hacer una copia de un objeto en otro mediante el siguiente procedimiento: clase objeto1,objeto2; … objeto2=objeto1; En este caso, entra en acción el operador “=”, que por defecto, también hace una copia miembro a miembro. Por fortuna, puede ser sobrecargado para conseguir el comportamiento adecuado. Veamos un ejemplo: #include <iostream.h> class miclase{ int a,b; public: void asigna(int i,int j){a=i,b=j;} void muestra(){cout << a << „ „ << b << endl;} }; main() { miclase o1,o2; o1.asigna(10,4); // asigna 01 a 02 o2=o1; o1.muestra(); o2.muestra(); return 0; } Aquí el objeto o1 tiene sus variables miembro a y b fijadas con los valores 10 y 4 respectivamente. A continuación, o1 se asigna a o2. Esto hace que el valor actual de o1.a se asigne a o2.a y o1.b se asigne a o2.b. Debemos tener en cuenta que una asignación entre dos objetos simplemente hace que los datos de esos objetos sean idénticos. Los dos objetos están completamente separados. Por ejemplo, después de la asignación, la llamada a o1.muestra() para establecer el valor de o1.a no tiene efecto en o2 o en su valor a. Sólo se pueden usar objetos del mismo tipo en una sentencia de asignación. Si los objetos no son del mismo tipo, se informa de un error en tiempo de compilación. No es suficiente, además, con que los tipos sean físicamente similares (mismos metodos, variables miembro…) han de tener el mismo nombre de tipo. Es importante entender que todos los miembros de un objeto se asignan a otro cuando realizamos la copia, incluyendo arrays. Imaginemos un objeto pila s1 al que hemos introducido ya tres elementos; si una vez hecho esto, creamos otros objeto copia de s1, ya contendrá esos tres elementos. Si la copia la hacemos antes de introducir los elementos, no los contendrá la copia. #include <iostream> #define TAM 10 using namespace std; // Declara una clase pila de caracteres class pila { char pil[TAM]; // guarda la pila int cab; // índice de la cabeza de la pila public: pila(); void push(char ch); char pop(); }; // Inicializa la pila pila::pila() { cout << “Construyendo una pila\n”; cab=0; } void pila::push(char ch) { if (cab==TAM){ cout << “La pila está llena\n”; return; } pil[cab]=ch; cab++; } char pila::pop() { if (cab==0){ cout << “La pila está vacía\n”; return 0; } cab–; return pil[cab]; } main() { pila s1,s2; int i; s1.push(‟a'); s1.push(‟b'); s1.push(‟c'); s2=s1; // ahora s1 y s2 son idénticos for (i=0;i<3;i++) cout << “Saca de s1: ” << s1.pop() << endl; for (i=0;i<3;i++) cout << “Saca de s2: ” << s2.pop() << endl; system(”PAUSE”); return 0; } Debemos tener cuidado al asignar un objeto a otro y asegurarnos de no destruir información que pueda necesitarse posteriormente. Veamos otro ejemplo que ilustra la necesidad de un constructor de copia. Este programa crea un tipo restringido de array de enteros “seguro” que previene que se sobrepasen sus límites. Se asigna el espacio de cada array usando new y dentro de cada objeto array se mantiene un puntero a la memoria. #include <iostream> using namespace std; class matriz { int *p; int tam; public: matriz(int ta){ p= new int[ta]; if (!p) exit(1); tam=ta; cout << “Uso del constructor „normal‟\n”; } ~matriz(){delete [] p;} // constructor de copia matriz (const matriz &a); void put(int i, int j){ if (i>=0 && i<tam) p[i]=j; } int get(int i){ return p[i]; } }; /* En el caso siguiente se asigna especificamente memoria para la copia, y la dirección de esta memoria se asigna a p. Por tanto, p no está apuntando a la misma memoria asignada dinámicamente al objeto original */ matriz::matriz(const matriz &a){ int i; p=new int [a.tam]; // asignación de memoria para la copia if (!p) exit(1); for (i=0; i<a.tam;i++) p[i]=a.p[i]; // contenido de la copia cout << “Uso del constructor de copia\n”; } int main () { matriz num(10); // esta sentencia llama al constructor “normal” int i; // colocación de algunos valores en el array for (i=0; i<10; i++) { num.put(i,i); } // presentación de num for (i=9;i>=0;i–) { cout << num.get(i);} cout << “\n”; // creación de otro array e inicialización con num matriz x = num; // esta entencia invoca al constructor de copia // presentación de x for (i=0; i<10; i++) { cout << x.get(i);} cout << “\n”; system(“PAUSE”); return 0; } Cuando num se usa para inicializar x, se llama al constructor de copia, se asigna memoria para el nuevo array y se almacena en x.p y el contenido de num se copia en el array de x. De esta forma, x y num tienen arrays que contienen los mismos valores, pero cada array es independiente y distinto, es decir, num.p y x.p no apuntan a la misma zona de memoria. Si el constructor de copia no hubiera sido creado, entonces la inicialización bit a bit matriz x = num habría dado lugar a que los arrays de x y num compartieran la misma memoria. Como ya dijimos antes, el constructor de copia solo es llamado para las inicializaciones. Por ejemplo, la siguiente secuencia no llama al constructor de copia definido en el ejemplo anterior: matriz a(10); matriz b(10); b=a; // no llama al constructor de copia En este caso, b=a realiza la operación de asignación. Veamos de nuevo el ejemplo de tipo cadena que nos muestra como el constructor de copia ayuda a prevenir algunos problemas sobrevenidos con el paso de tipos de objetos a funciones, observemos el siguiente programa (incorrecto): // Este programa tiene un error #include <iostream> using namespace std; class tipocad{ char *p; public: tipocad(char *s); ~tipocad(){delete [] p;} char *obtener() {return p;} }; tipocad::tipocad(char *s) { int l; l=strlen(s); p=new char[l]; if (!p){ cout << “Error de asignación\n”; exit(1); } strcpy(p,s); } void mostrar(tipocad x) { char *s; s=x.obtener(); cout << s << endl; } int main() { tipocad a(“Hola”),b(“mundo”); mostrar(a); mostrar(b); system(“PAUSE”); return 0; } En este programa, cuando un objeto tipocad se pasa a mostrar(), se hace una copia bit a bit y se guarda en el parámetro x. De este modo, cuando finaliza la función, x pierde su valor y se elimina. Esto, por supuesto, da lugar a una llamada al destructor de x, que libera x.p. Sin embargo, la memoria que se ha liberado es la misma que todavía está siendo utilizada por el objeto empleado para llamar a una función. Esto conduce a un error. La solución consiste en definir un constructor de copia para la clase tipocad que asigne memoria a la copia cuando se cree. Este es el enfoque usado en el siguiente programa corregido: /* Este programa usa un constructor de copia para permitir que los objetos tipocad sean pasados a funciones */ #include <iostream> using namespace std; class tipocad{ char *p; public: tipocad(char *s); tipocad(const tipocad &o); // constructor de copia ~tipocad(){delete [] p;} char *obtener() {return p;} }; tipocad::tipocad(char *s) { int l; l=strlen(s); p=new char[l]; if (!p){ cout << “Error de asignación\n”; exit(1); } strcpy(p,s); } // Constructor de copia tipocad::tipocad(const tipocad &o) { int l; l=strlen(o.p); p=new char[l]; // asigna memoria para la nueva copia if (!p){ cout << “Error de asignación\n”; exit(1); } strcpy(p,o.p); // copia de la cadena en la copia } void mostrar(tipocad x) { char *s; s=x.obtener(); cout << s << endl; } int main() { tipocad a(“Hola”), b(” mundo”); mostrar(a); mostrar(b); system(“PAUSE”); return 0; } Ahora, cuando finaliza mostrar() y x pierde su valor, la memoria apuntada por x.p (que se liberará) no es la misma que la usada por el objeto pasado a la función. Vamos a proponer un experimento. ¿Qué sucede si un objeto de una clase derivada se asigna a otro objeto de la misma clase derivada?. ¿Se copia también la información asociada con la clase base?. La respuesta es sí, la información de la clase base también se copia cuando un objeto de una clase derivada se asigna a otro. El siguiente ejemplo lo demuestra: #include <iostream> using namespace std; class base { int a; public: void carga_a(int n) {a=n;} int obtiene_a() {return a;} }; class derivada : public base { int b; public: void carga_b(int n) {b=n;} int obtiene_b() {return b;} }; int main() { derivada ob1, ob2; ob1.carga_a(5); ob1.carga_b(10); // asigna ob1 a ob2 ob2=ob1; cout << “Aquí está a y b de ob1: “; cout << ob1.obtiene_a() << „ „ << ob1.obtiene_b() << “\n”; cout << “Aquí está a y b de ob2: “; cout << ob2.obtiene_a() << „ „ << ob2.obtiene_b() << “\n”; // Como es de suponer, la salida es igual para ambos system(”PAUSE”); return EXIT_SUCCESS;