CONSTRUCTOR DE COPIA Y ASIGNACIÓN DE OBJETOS EN C++

Anuncio
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;
Descargar