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.2d   Construcción y destrucción de objetos

§1  Sinopsis:

Recordemos que los objetos pueden ser creados y destruidos bajo diversas circunstancias:

  • Los objetos automáticos son creados cada vez que el proceso encuentra una declaración y destruidos cuando la ejecución sale del bloque en que son declarados ( 4.1.5).

  • Los objetos persistentes son creados con el operador new y destruidos con delete ( 4.1.5)

  • Un miembro no estático de un objeto es creado y destruido cuando el objeto del que es miembro es a su vez creado y destruido.

  • Un objeto estático local es creado la primera vez que se encuentra la declaración durante la ejecución del programa, y es destruido cuando el programa finaliza ( 4.1.5).

  • Un objeto estático global es creado al principio del programa, durante la ejecución del módulo inicial ( 1.5) y destruido cuando el programa finaliza.

  • Un objeto temporal puede ser creado como parte de la evaluación de una expresión y destruido al final de la evaluación misma.

Las condiciones enunciadas de destrucción de objetos son las que podríamos considerar "normales";  en el sentido que ocurren durante la ejecución normal del programa. Pero existen otras causas de destrucción de objetos que acontecen durante el proceso de limpieza de pila ("Stack unwinding" 1.6) que forma parte del mecanismo C++ de excepciones.

§2  Unas funciones muy especiales

La adecuada creación y destrucción de los objetos es cuestión de la mayor importancia para la correcta ejecución de un programa, y una de las causas más frecuentes de errores en los programas C++. Como botón de muestra, recordemos que al tratar de la declaración de punteros advertíamos lo fácil que resulta causar un desastre con un simple error en su inicialización ( 4.2.1a). Para evitar este tipo de inconvenientes, C++ implementa mecanismos que garantizan en lo posible, la correcta creación, inicialización y destrucción de objetos.

En la definición de clases se utilizan ciertas funciones-miembro un tanto particulares que son responsables de la creación, inicialización, copia y destrucción de los objetos de la clase. Son los constructores (4.11.2d1) y destructores (4.11.2d2). De los primeros hay dos variedades: los que crean un objeto desde cero (especificando sus propiedades), y los que lo crean e inicializan como imagen de otro ya existente que sirve de modelo, son los denominados constructor-copia ( 4.11.2d4).  Estas funciones gozan de las características del resto de las funciones-miembro; son declaradas y definidas dentro de la clase (o declaradas dentro y definidas fuera); como la mayoría de las funciones C++, los constructores pueden tener argumentos por defecto, o utilizar listas de argumentos de inicialización (los destructores no reciben argumentos), pero unos y otros tienen características que los hace especiales. En atención a esta singularidad, el Estándar las denomina funciones-miembro especiales ("special member funcitons").


§2.1  Denominación
:  La primera singularidad está en el nombre de estas funciones: los constructores adoptan el mismo nombre que la clase a que pertenecen; por su parte los destructores adoptan el nombre precedido por la tilde ~.

Ejemplo:

class C {          // definición de una clase
   ...
   C() { /* definición de constructor */ }
   ~C() { /* definición del destructor */ }
};


Evidentemente, caso de existir varios constructores, lo que es muy frecuente, se trata de versiones sobrecargadas de la misma función, porque al compartir todos el mismo identificador, se diferencian solo en el número y tipo de los argumentos que aceptan. En este caso, son las reglas de resolución de sobrecarga de funciones ( 4.4.1a) las que deciden cual de ellos será invocado para construir el objeto.

Puesto que el destructor no acepta argumentos y tiene un nombre específico la consecuencia inmediata es que existe un solo destructor para cada clase (no existe posibilidad de sobrecarga).


§2.2
  Su declaración no especifica ningún valor devuelto (ni siquiera void). En este sentido, constructores y destructores son un caso especialísimo de funciones!!

Nota: el hecho que no puedan devolver ningún valor, hace que, por lo general, no existan procedimientos sencillos o elegantes para tratar los errores que pudieran producirse durante la fase de creación del objeto.


§2.3
  Estos miembros no pueden ser heredados, aunque una clase derivada puede invocar a los constructores y destructores de su clase-base si son accesibles. Es decir: si no han sido declarados privados o protegidos ( 4.11.2d1).

§2.4  Un constructor no puede ser friend ( 4.11.2a1) de ninguna otra clase. Tampoco pueden ser declarados virtual ( 4.11.2d2), static, const o volatile.

Nota: aunque el lenguaje no contempla la posibilidad de declarar constructores virtuales, sí ofrece soporte para algunas técnicas que permiten simular este comportamiento ( 4.13.5).


§2.5
  No pueden obtenerse sus direcciones, por lo que no pueden definirse punteros a este tipo de funciones miembro. La sentencia del ejemplo es ilegal

int main (void)  {
  ...
  void* ptr = base::base;    // ilegal
  ...
}


§2.6 
Recuerde que, como en el resto de las funciones miembro no estáticas, el primer argumento (oculto) de constructores y destructores es el puntero this ( 4.11.6), a través del cual la función sabe sobre que objeto debe actuar para inicializarlo o destruirlo.

§2.7  Tenga en cuenta que un objeto que tenga constructor o destructor no puede ser utilizado como miembro de una unión ( 4.6).

§3  Invocación explícita de constructores y destructores

Al margen de la particularidad que representan sus invocaciones implícitas, en general su invocación sigue las pautas del resto de los métodos. Ejemplos:

X x1;            // Ok. Invocación implícita del constructor

X::X();          // Error: invocación ilegal del constructor

X x1 = X::X()    // Error: invocación ilegal del constructor

X x1 = X();      // Ok. Invocación legal del constructor

X x1();          // Ok. Variación sintáctica del anterior [2]

x1.X();          // Error: no se puede invocar el constructor de un

                     objeto después de creado

....

X* p = &x1;      // p es puntero-a-X señalando al objeto x1

...
p->~X();         // Ok: Invocación legal del destructor del objeto x1

p–>X::~X();      // Ok: variación sintáctica del anterior

x1.~X()          // Ok: otra posible invocación


  Recordar que cuando se trata de iniciar o destruir objetos de tipos definidos por el usuario (clases), los operadores new y delete pueden realizar invocaciones implícitas a los constructores y destructores de tales clases. A su vez, constructores y destructores pueden realizar invocaciones explícitas a los operadores new ( 4.9.20) y delete ( 4.9.21) si se requiere espacio persistente para algún miembro del objeto (ver ejemplo 4.11.2d1).


§3.1
  El compilador invoca los constructores y destructores correspondientes cuando se definen y destruyen objetos (estas invocaciones pueden ser explícitas o implícitas). El constructor correspondiente crea un objeto y lo inicia. Después, cuando estos objetos deben ser destruidos, sus destructores invierten el proceso destruyendo los objetos creados.

Lo mismo que ocurre con los tipos simples, los objetos abstractos pueden ser creados en memoria dinámica o ser automáticos. En el primer caso su destrucción debe realizarse explícitamente. En caso de ser automáticos, los destructores son invocados por el compilador en el momento que los objetos salen de ámbito. Son las situaciones esquematizadas en el siguiente ejemplo:

{

  C c1;             // objeto automático

  C* cpt = new C;   // objeto persistente (anónimo) + objeto cpt automático

  delete cpt;       // destrucción explícita del objeto anónimo

}                   // destrucción implícita de c1 y cpt


§3.2
  Si en una clase X no se ha definido ningún constructor para aceptar un tipo de argumento particular, el compilador no se realizará ningún intento para encontrar otro constructor, o alguna conversión para convertir un valor asignado a un tipo aceptable para algún constructor de dicha clase. Esta regla se aplica solo a constructores con un parámetro y sin iniciadores, que utilice la sintaxis de asignación “=”.  Por ejemplo:

class X {

  ...

  X(int);    // constructor

};

class Y {

  ...

  Y(X);      // constructor

};

Y a = 1;     // ilegal: No es transformado a Y(X(1))


§3.3 
Cualquiera que sea el método de invocación del constructor (implícito o explícito) para crear un objeto, si la clase contiene miembros abstractos ADTs ( 2.2), el constructor invoca a su vez los constructores de estos miembros. Como el constructor de los tipos escalares reserva espacio en memoria, pero no realiza ningún tipo de inicialización concreta, los miembros de tipo escalar quedan sin una correcta inicialización a menos que esta sea proporcionada explícitamente por el programador.

Como consecuencia de la regla anterior, en el diseño de constructores no es generalmente necesario preocuparse de la iniciación de los miembros abstractos, ya que sus constructores serán invocados automáticamente y (suponemos) han sido correctamente establecidos al definir sus clases. En cambio, dado que los tipos escalares (tipos simples preconstruidos en el lenguaje) no reciben automáticamente una correcta iniciación, es probable que sus contenidos iniciales (basura) puedan ocasionar problemas, por lo que es generalmente necesario proporcionarles una correcta inicialización en el constructor.

Nota: algunos autores sostienen que en estos casos, la iniciación de los miembros escalares debe realizarse en la lista de iniciadores ( 4.11.2d3a), dejando el cuerpo del constructor para cualquier lógica adicional que sea necesaria durante la construcción [1]. La razón argumentada es que al agruparlas así, se facilita la legibilidad de código y el manejo de excepciones en los procesos de creación de objetos.  El consejo llega al extremo de recomendar que, si es imprescindible realizar al alguna manipulación de estos miembros en el cuerpo del constructor, al menos se inicien con un valor adecuado en la lista de iniciadores. Valor que será actualizarlo después en el cuerpo del constructor.

La destrucción sigue el proceso inverso, de forma que la destrucción de estos objetos implica a su vez la invocación de los destructores de los objetos contenidos.

Ejemplo:

class Coordenada { public: int x; int y; };

class Triangulo {

  public:

  int color;

  Coordenada verticeA; Coordenada verticeB; Coordenada verticeC;

};

...

Triangulo T1;


Al instanciar el objeto T1, el constructor de la clase Triangulo invocará tres veces al constructor de la clase Coordenada. Así mismo, el destructor de T1 también invocará al destructor de Coordenada. Sin embargo, tanto la propiedad Triangulo::color como los miembros x e y de los vértices, permanecerán sin una inicialización específica, aunque desde luego, serán destruidos cuando el objeto T1 sea destruido.

§4  Tipos de constructores

Una vez que se ha definido un tipo abstracto C (una clase), su utilización supone una operatoria mínima que puede ser esquematizada en las siguientes sentencias:

{

  C c1, c2;     // Creación

  C c3 = c1;    // Creación a partir de un modelo

  c2 = c1;      // Asignación

}               // destrucción

La primera exige la existencia de un constructor;  en este caso un constructor que se encargue de crear los objetos con las inicializaciones por defecto. La segunda exige la presencia de un constructor capaz de crear un objeto a imagen de otro tomado como referencia. La tercera exige la presencia de un operador de asignación capaz de realizar la asignación de los miembros del Rvalue en el Lvalue. Finalmente se precisa de destructores que garanticen la correcta destrucción de los objetos y que puedan ser invocados automáticamente por el compilador al llegar al final del ámbito.

Nota: se podría argumentar que la segunda puede ser sustituida por la creación de un objeto c3 desde "scratch" (caso primero) seguido de una asignación c3 = c1 (caso tercero). Sin embargo, esta no es la solución adoptada. Todas las consideraciones de diseño del lenguaje han gravitado alrededor de la eficiencia del código, de forma que se ha preferido mantener una sola operación. Es importante entender que una sentencia como esta no implica ninguna asignación. Es una forma de expresar la utilización de un constructor especial que acepta un objeto como argumento, objeto que será utilizado como modelo para la creación del nuevo; algo como: C::C(c1);.


Los diseñadores del lenguaje decidieron que estas utilidades eran imprescindibles, y además pretendieron simplificar el trabajo del usuario, de forma que, aunque dejaron al programador facultad para definir sus propios constructores, destructores y operadores, decidieron que, en caso de no hacerlo explícitamente, el compilador debería proporcionarlos por defecto. Estos algoritmos se denominan oficiales o de oficio para distinguirlos de los creados por el programador, a los que llamaremos explícitos. Cualquier constructor, destructor u operador de asignación "oficial" (generado por el compilador) es público.

Nota: es importante distinguir estos conceptos; "de oficio" (generados por el compilador) y "explícitos" (definidos por el usuario), y no confundirlos con el concepto "por defecto" (que puede ser invocado sin argumentos 4.11.2d1). Desgraciadamente son confundidos y/o no suficientemente enfatizadas sus diferencias en la mayoría de los textos. Otra cosa es que, por ejemplo, el constructor "oficial" sea además "por defecto" (puede ser invocado sin argumentos).


  Si el programador no define explícitamente ningún constructor, el compilador genera un constructor oficial o de oficio ( 4.11.2d1), que hace posible expresiones como:

C c1;

Sin embargo, si el programador proporciona un constructor explícito (con o sin argumentos) el constructor oficial no es generado. En consecuencia, si se proporciona un constructor explícito con argumentos y además se desea un constructor por defecto (sin argumentos), este último debe ser proporcionado por el programador de la clase.

Si el programador no define ningún constructor adecuado, el compilador genera un constructor-copia oficial ( 4.11.2d4) que hace posible que puedan utilizarse expresiones como:

C c2 = c1;

Si no se define explícitamente una versión de la función operator=() para miembros de la clase, el compilador genera un operador de asignación oficial que hace posible expresiones como:

c3 = c1;

Finalmente, si el programador no define explícitamente un destructor, el compilador proporciona un destructor oficial.


Todos estos algoritmos "oficiales" suministrados por el compilador cuando no hay ninguno explícito, tienen un comportamiento predefinido que será comentando más adelante. Por ahora indicaremos que cuando existen versiones explícitas, el compilador aporta automáticamente algunos detalles si la versión explícita los omite. Estos detalles tienden a garantizar un comportamiento correcto del algoritmo desde el punto de vista lógico y varían en función del algoritmo (constructor, constructor-copia o destructor).

Es significativo que, aparte de las invocaciones explícitas o implícitas a los constructores, que ocurren cuando se instancia deliberadamente un objeto, el compilador también crea infinidad de objetos temporales (cuya existencia pasa más o menos inadvertida) utilizando el mencionado constructor-copia. Considere el siguiente ejemplo:

class UnaClase { ... };

UnaClase func (UnaClase) {

  ...

  return UnaClase;

}

...

{                                // Bloque B.

  UnaClase obj1;                 // L.1

  UnaClase obj2 = obj1;          // L.2

  UnaClase obj3 = func(obj1);    // L.3

  onj2 = onj3;                   // L.4

}                                // L.5


Estas sentencias provocan las siguientes invocaciones  (ver en la página adjunta un ejemplo ejecutable de verificación ejemplo):

L.1:  Invocación al constructor por defecto (sin argumentos).  Este constructor puede ser oficial o explícito, según el diseño de la clase.

L.2:  Invocación al constructor-copia (crea un objeto obj2 con el mismo contenido que obj1).

L.3a.-  Invocación de la función: Es invocado el constructor-copia para crear un objeto temporal tmp local a la función, e igual que el objeto obj1 pasado como argumento. Al terminar la función, el objeto tmp es finalmente destruido, junto con el resto de objetos locales, mediante la invocación a su destructor.

       b.-  Invocación del constructor-copia para crear un objeto obj3, con el mismo contenido que el valor devuelto por la función.

L.4:  No se invoca ningún constructor.  La asignación es totalmente distinta de la construcción y de la construcción-copia (se realiza entre objetos ya creados).

L.5:  Invocación de los destructores de los objetos obj1, obj2 y obj3.

  Tamas relacionados:
  • Constructores de conversión ( 4.11.2d1)
  • Operadores de conversión ( 4.9.18k)
  • Control de recursos ( 4.1.5a)

  Inicio.


[1]  "C++ Cookbook" por D. Ryan Stephens, Christopher Diggins, Jonathan Turkamis y Jeff Cogswell. O'Relly Media Inc 2a Ed. 2006.

[2]  Evidentemente, el hecho que X x1 = X::X(); no sea correcta, es una excepción en las reglas generales de la gramática C++. Por su parte, la expresión X x1 = X(); es correcta precisamente porque el compilador puede deducir por el contexto, que el Rvalue es X::X().