Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.11.2d4  Copiar objetos

§1  Preámbulo

Al reseñar los mecanismos generales de creación y destrucción de objetos ( 4.11.2d), indicamos que cuando se manejan objetos abstractos (instancias de clases), el operador de asignación = tiene un sentido por defecto que supone la asignación miembro a miembro del operando derecho en el izquierdo. Dicho en otras palabras: cuando se define una clase, el compilador define automáticamente una función-operador operator=() que realiza una asignación miembro-a-miembro de los elementos del operando derecho en el izquierdo. Por ejemplo, si c1 e c2 son objetos de la clase C, la asignación

c1 = c2;      // S1:

supone la asignación de todos los miembros de c2 en sus homónimos de c1. Sin embargo, cuando el Lvalue implica la declaración de un nuevo objeto, la expresión

C c1 = c2;    // S2:

no implica ninguna asignación. En realidad la sintaxis anterior es una convención para representar la invocación de un constructor especial, denominado constructor-copia que acepta una referencia al objeto a copiar c2 (Rvalue de la expresión) y produce un nuevo objeto c1 que es un clon del anterior. Podríamos suponer que la sentencia S2 es equivalente a una invocación del tipo:

C::C(c2);

y que el objeto producido se identifica con el nombre c1 (recordemos que los constructores no "devuelven" nada).

§2  El constructor-copia

El constructor-copia de la clase X es aquel que puede ser invocado con un argumento, que es precisamente una referencia a la propia clase X como en cualquiera de las definiciones siguientes:

X::X(X&) { /*...*/ };
X::X(const X&){ /*...*/ };
X::X(const X&, int i) { /*...*/ };


El programador es libre para asignar al constructor-copia el significado que desee. De hecho, siempre que se respeten las reglas de sobrecarga de funciones, pueden existir múltiples versiones de estos constructores, cada uno con su propia finalidad. Como puede verse en la tercera sentencia, el constructor-copia también puede tener otros argumentos, pero en cualquier caso, la referencia a la propia clase debe ser el primero de ellos.

Como sugiere su nombre, el constructor-copia se utiliza para copiar objetos. Sin embargo, el concepto "copia" puede tomarse con diversos significados en C++: existen copias ligeras y profundas; también copias lógicas ("Shadow copy") y físicas.

En general "copiar" un objeto supone la generación física de otro objeto, pero no siempre el nuevo es necesariamente una réplica exacta del original (aunque pueda parecerlo). En ocasiones el nuevo objeto es un manejador "handle" del original, de forma que el usuario percibe "como si" estuviera ante una copia real. Este tipo de copia, denominada lógica o ligera, presenta la ventaja de su rapidez y economía de espacio. La duplicación completa de objetos grandes puede ser costosa en términos de espacio y tiempo, además de no ser siempre estrictamente necesaria o conveniente.

Las copias profundas, también denominadas físicas, suponen la replicación completa del objeto original, reproduciendo todos sus miembros ("Memberwise") a todos los niveles de anidamiento (los objetos copiados pueden ser tipos abstractos cuyos miembros pueden ser a su vez tipos abstractos, cuyos miembros pueden ser a su vez tipos abstractos, y así sucesivamente con cualquier nivel de anidación).


§2.1  Este tipo de constructores son invocados automáticamente por el compilador en muchas circunstancias (siempre que se presenta la necesidad de crear un nuevo objeto igual a otro existente):

  • Cuando se pasa un objeto (por valor) como argumento a una función; en cuyo caso se crea una copia del objeto en la función invocada ( 4.4.5). Ejemplo:

    class C { .... } c1;
    void func(C c) { ...; } // función que acepta un objeto
    ...
    func(c1);

  • Cuando una función devuelve un objeto, en cuyo caso se crea una copia del objeto en la función invocante ( 4.4.7).  Ejemplo:

    class C { ... } c1;
    C func(C& ref) {    // función que devuelve un objeto
      ...
      return ref;
    }
    ...
    {
      ...
      c2 = func(c1);    // se crea una copia local del objeto ref
    }

  • Cuando se lanza una excepción ( 1.6). Ver Ejemplo.

  • Cuando se inicia un objeto; típicamente cuando se declara e inicia un objeto mediante asignación de otro de la misma clase. Es la situación de las sentencias L.2/3 (ver Ejemplo):

    class C { .... };
    ...
    C c1;          // L.1 invocación implícita al constructor por defecto
    C c2 = c1;     // L.2 invocación implícita al constructor-copia
    C c3(c1);      // L.3 ídem (variación sintáctica del anterior)
    c3 = c2;       // L.4 invocación del operador de asignación


§2.2  El compilador crea un constructor-copia oficial para cualquier clase en la que no se hubiese definido ningún otro explícitamente (un constructor que pueda ser invocado con un solo argumento que sea una referencia al propio tipo). Este copiador de oficio realiza un clon exacto del original replicando todos los miembros a cualquier profundidad, y permite programar de forma segura con muchos tipos abstractos. Sin embargo, como veremos a continuación , cuando el programa crea tipos abstractos por agregación de otros tipos complejos como clases, estructuras, matrices y objetos persistentes que se manejan mediante punteros, puede ser necesario definir un constructor-copia explícito. También si se sobrecarga el operador de asignación (§3 ).

Como todos los constructores, el constructor-copia se define como una función que no devuelve nada (ni siquiera void) y su primer parámetro es una referencia constante a un objeto de la clase [1]. Puede aceptar más parámetros; tantos como sean necesarios, siempre que se respeten las reglas de sobrecarga de funciones, de forma que no exista ambigüedad entre dos definiciones. Por ejemplo:

class X {

  int i;

  char c;
  public:

  X(const X&): i(0), c('X') {}
  X(const X& ref, int n = 0); {  // Error! ambigüedad con el anterior

    i = n;

    c = ref.c;
};


class Y {

  int i;

  char c;
  public:

  Y(const Y&): i(0), c('X') {}
  Y(const Y& ref, int n); {      // Ok.

    i = n;

    c = ref.c;
};


Cuando se define explícitamente un constructor-copia, el compilador no necesita crear ninguna versión oficial. En tal caso, las sentencias en que se requiere la utilización de un constructor-copia utilizarán la versión explícita. Pero entonces es responsabilidad del programador cubrir todos los detalles pertinentes. En todos los casos en que, en la definición de una versión explícita, se omita especificar la asignación de una propiedad de la clase, el compilador incluirá automáticamente una invocación al constructor por defecto del miembro. Observe que esto implica que no se realizará copia de los miembros omitidos, cuyos valores corresponderán a objetos de nueva construcción. Supone también que si los miembros omitidos son tipos simples (cuyo constructor por defecto no los inicializa con ningún valor concreto), los miembros del objeto resultante contendrán basura ( 4.11.2d1). La definición por el programador de un constructor-copia explícito, supone que este hará algo más que una mera copia de los objetos-miembro de la clase, ya que esto lo hace por sí mismo el constructor-copia oficial; supone así mismo que hará algo más que copiar un puntero e incrementar un contador, porque eso es justamente lo que hace un shared_ptr.

La página adjunta muestra un ejemplo que pone de manifiesto las implicaciones de lo indicado en párrafo anterior ( Ejemplo)

§2.3  Ejemplo:

A continuación exponemos el ejemplo de clase para la que se define un constructor-copia (un perfeccionamiento de la clase Punto ya comentada 4.11.2d2). Aunque se trata de un caso sencillo, ilustra con claridad la técnica seguida, así como el proceso de invocación automática de dichos métodos realizada por el compilador.

#include <iostream>
using namespace std;
#define Abcisa coord[0]
#define Ordenada coord[1]

class Punto {
  public:
  int* coord;
  Punto(int x = 1, int y = 2) {   // construtor por defecto
    coord = new int[2];
    Abcisa = x; Ordenada = y;
    cout << "Creado objeto: X == "
         << Abcisa << "; Y == " << Ordenada << endl;
  }
  ~Punto() {                       // destructor
    cout << "Objeto (" << Abcisa << ", " << Ordenada << ") destruido!" << endl;
    delete [] coord;
  }
  Punto(const Punto& ref) {       // construtor-copia
    coord = new int[2];
    Abcisa = ref.Abcisa; Ordenada = ref.Ordenada;
    cout << "Copiado objeto: X == "
         << Abcisa << "; Y == " << Ordenada << endl;
  }
};

int main() {             // ======================
  Punto p1;              // L.25: p1 instancia de Punto
  Punto p2(3, 4);        // L.26: p2 instancia de Punto
  Punto p3 = p2;         // L.27: p3 instancia de Punto
  return 0;              // L.28: fin del programa
}

Salida:

Creado objeto: X == 1; Y == 2
Creado objeto: X == 3; Y == 4
Copiado objeto: X == 3; Y == 4
Objeto (3, 4) destruido!
Objeto (3, 4) destruido!
Objeto (1, 2) destruido!

Comentario

Tenga en cuenta los "defines" Abcisa y Ordenada, sustituyéndolos mentalmente por los valores respectivos.

En L.25 se instancia el objeto p1; se invoca el constructor por defecto y se utilizan los argumentos por defecto.

En L.26 se instancia el objeto p2; en este caso se realiza una invocación implícita del constructor pasándole dos argumentos.

En L.27 se instancia el objeto p3; en esta ocasión se produce una invocación implícita del constructor copia

Al alcanzarse en L.28 el final del programa, son invocados automáticamente los destructores de los objetos creados en el ámbito de la función main. En este momento se invocan los destructores de los objetos p3, p2 y p1 (en orden inverso a su creación).

Ver otro ejemplo de la necesidad y utilización del constructor-copia ( 4.9.18d1).

§3  El operador de asignación oficial

Si en la definición de la clase no se define ninguna versión explícita de la función operator=(), el compilador proporciona una versión oficial, que puede ser utilizada con miembros de la clase. Esta versión oficial realiza una copia profunda (miembro a miembro) del operando derecho en el izquierdo. Es el que permite utilizar expresiones como

c1 = c2;

con objetos de tipo C aunque no se haya definido explícitamente este operador para miembros de la clase. Su funcionamiento es análogo al del constructor-copia oficial, aunque en este caso ambos operandos deben ser objetos creados previamente. Recordaremos que esta función también recibe una referencia a un objeto de la clase, aunque se diferencia del constructor-copia en que devuelve una referencia (los constructores no devuelven nada, simplemente crean un objeto).

C& operator= (const C&  c);   // operador de asignación


El constructor-copia y el operador de asignación no tienen porqué coincidir en su diseño. De hecho suelen existir diferencias substanciales, debido a que el constructor-opia está relacionado con la creación de objetos, de forma que implicará la asignación de recursos (por ejemplo memoria). Por contra, la asignación maneja objetos ya construidos, lo que implica la reasignación de recursos y eventualmente, desasignación de los anteriores. En cualquier caso, el mecanismo de copiado "clónico" de la asignación oficial presenta los mismos inconvenientes que el constructor-copia oficial (ver a continuación §4 ), pero el programador tiene libertad para definir su propia versión.

La descripción del operador de asignación operator= y la forma de sobrecargarlo, ha sido discutida con detalle en el apartado correspondiente ( 4.9.18a), pero debemos advertir que si se proporciona una versión explícita, es responsabilidad del usuario cubrir los detalles pertinentes, ya que solo se efectuarán aquellas asignaciones que sean especificadas de forma explícita. Los miembros para los que no exista una asignación explícita mantendrán sus antiguos valores. La página adjunta incluye un ejemplo que muestra claramente este comportamiento ( Ejemplo).

§4  Inconvenientes de la copia miembro-a-miembro

En ocasiones, la réplica miembro a miembro efectuada por el constructor-copia oficial, o por el operador de asignación =, presenta riesgo de resultados indeseados cuando algunos miembros son punteros. También en los procesos de destrucción de tales objetos, cuando son invocados los destructores al finalizar su ámbito. Para ilustrarlo con un ejemplo, supongamos una clase Cliente para manejar los clientes de un hotel:

class Cliente {

  public:

  char* nombre;

  ...

  Cliente(char*) {       // constructor

    nombre = new char[30];

    ...

  }

  ~Cliente() {           // destructor

    delete[] nombre;

  }

};

Si construimos dos instancias de la clase:

void foo() {

  Cliente c1( "Pau Casals" );

  Cliente c2 = c1;

}

En el primer caso se ha realizado una llamada implícita al constructor con los argumentos situados en la lista de inicialización. En el segundo, se ha realizado una llamada implícita al constructor-copia oficial .

Cualquiera que sea el método de construcción seguido, resulta evidente que los miembros del objeto c2 serán exactamente iguales que los de c1. Existe duplicidad de miembros entre ambos objetos pero la matriz alfanumérica es compartida. Concretamente el puntero nombre de ambas instancias señala la misma dirección del montón, donde se aloja la cadena "Pau Casals" que fue creada por c1 (al que consideramos "Propietario" del contenido). Cualquier modificación del contenido de esta cadena desde un objeto tiene repercusión en el otro, que "ve" el nuevo contenido. Pero si c1 cambia totalmente contenido y situación. Por ejemplo, cambiando el nombre de cliente actual por otro más largo, lo que supone rehusar la matriz actual y crear otra de mayor dimensión en otra zona del montón, el puntero de c2 queda descolgado. Cualquier intento de acceder al nombre del cliente desde c2 proporcionará basura.

la situación es aún más conflictiva al finalizar la función foo. Ya sabemos (4.1.5) que las variables locales son automáticamente destruidas al finalizar su ámbito.; En este caso serán invocados automáticamente los destructores de c2 y c1 que destruirán los objetos. Cuando le llegue el turno a c2, su destructor intentará realizar un delete[] utilizando un puntero inválido, lo que probablemente producirá un error fatal de runtime.

Como puede verse, las copias miembro-a-miembro, ya sean originadas por un constructor-copia o por el operador de asignación, no son siempre deseables. Incluso pueden resultar peligrosas, en especial cuando en los constructores se utilizan punteros y/o objetos persistentes creados con new.

Tema relacionado:
  • El operador de asignación con miembros de clases ( 4.9.2a).

  Inicio.


[1]  Por lo general el primer parámetro del constructor-copia es una referencia constante a un objeto de la clase porque el objeto copiado (Rvalue) no debe ser modificado. Sin embargo esto no es siempre así. Al tratar de los objetos-puntero ilustramos un ejemplo en el que el constructor-copia modifica también el objeto copiado ( 4.13.3).