Constructores (Programación)

Constructores (Programación)
Información sobre la plantilla
Concepto:Son un tipo especial de función miembro, estrechamente relacionados con los destructores

Constructores (Programación). Es un tipo especial de función miembro, estrechamente relacionados con los destructores.

La construcción de objetos tiene tres fases:

  1. Instanciación, que aquí representa el proceso de asignación de espacio al objeto, de forma que este tenga existencia real en memoria.
  2. Asignación de recursos. Por ejemplo, un miembro puede ser un puntero señalando a una zona de memoria que debe ser reservada; un "handle" a un fichero; el bloqueo de un recurso compartido o el establecimiento de una línea de comunicación.
  3. Iniciación, que garantiza que los valores iniciales de todas sus propiedades sean correctos.

La correcta realización de estas fases es importante, por lo que el creador del lenguaje decidió asignar esta tarea a un tipo especial de funciones (métodos) denominadas constructores. En realidad la consideraron tan importante, que como veremos a continuación, si el programador no declara ninguno explícitamente, el compilador se encarga de definir un constructores de oficio, encargándose de utilizarlo cada vez que es necesario. Aparte de las invocaciones explícitas que pueda realizar el programador, los constructores son frecuentemente invocados de forma implícita por el compilador.

Descripción

Para entender como funciona, observe este sencillo ejemplo en el que se definen sendas clases para representar complejos; en una de ellas definimos explícitamente un constructor; en otra dejamos que el compilador defina un constructor de oficio:

#include <iostream>
using namespace std;
 class CompleX {            // Una clase para representar complejos
  public:
  float r; float i;        // Partes real e imaginaria
  CompleX(float r = 0, float i = 0) {  // L.7: construtor explícito
    this->r = r; this->i = i;
    cout << "c1: (" << this->r << "," << this->i << ")" << endl;
  }
};
class CompX {              // Otra clase análoga
  public:
  float r; float i;        // Partes real e imaginaria
};
void main() {              // ======================
  CompleX c1;              // L.18:
  CompleX c2(1,2);         // L.19:
  CompX c3;                // L.20:
  cout << "c3: (" << c3.r << "," << c3.i << ")" << endl;
}

Salida:

c1: (0,0)
c2: (1,2)
c3: (6.06626e-39,1.4013e-45)

Comentario

En la clase CompleX definimos explícitamente un constructor que tiene argumentos por defecto, no así en la clase CompX en la que es el propio compilador el que define un constructor de oficio.

La utilización explícita del puntero this en la definición del constructor (L.8/L.9). Ha sido necesario hacerlo así para distinguir las propiedades i, j de las variables locales en la función-constructor (hemos utilizado deliberadamente los mismos nombres en los argumentos, pero desde luego, podríamos haber utilizado otros.

En la función main se instancian tres objetos; en todos los casos el compilador realiza una invocación implícita al constructor correspondiente. En la declaración de c1, se utilizan los argumentos por defecto para inicializar adecuadamente sus miembros; los valores se comprueban en la primera salida.

La declaración de c2 en L.19 implica una invocación del constructor por defecto pasándole los valores 1 y 2 como argumentos. Es decir, esta sentencia equivaldría a:

c2 = CompleX::CompleX(1, 2); // Hipotética invocación explícita al constructor

El resultado de L.19 puede verse en la segunda salida.

Finalmente, en L.20 la declaración de c3 provoca la invocación del constructor de oficio construido por el propio compilador. Aunque la iniciación del objeto con todos sus miembros es correcta, no lo es su inicialización. En la tercera salida vemos como sus miembros adoptan valores arbitrarios. En realidad se trata de basura existente en las zonas de memoria que les han sido adjudicadas.

El corolario inmediato es deducir lo que ya señalamos en la página anterior: aunque el constructor de oficio inicia adecuadamente los miembros abstractos , no hace lo mismo con los escalares. Además, por una u otra causa, en la mayoría de los casos de aplicaciones reales es imprescindible la definición explícita de uno o varios de estos constructores.

Técnicas de buena construcción

Un objeto no se considera totalmente construido hasta que su constructor ha concluido satisfactoriamente. En los casos que la clase contenga sub-objetos o derive de otras, el proceso de creación incluye la invocación de los constructores de las subclases o de las super-clases en una secuencia ordenada que se detalla más adelante.

Los constructores deben ser diseñados de forma que no puedan (ni aún en caso de error) dejar un objeto a medio construir. En caso que no sea posible alistar todos los recursos exigidos por el objeto, antes de terminar su ejecución debe preverse un mecanismo de destrucción y liberación de los recursos que hubiesen sido asignados. Para esto es posible utilizar el mecanismo de excepciones.

Invocación de constructores

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;            // L.1: Ok. Invocación implícita del constructor
X::X();          // Error: invocación ilegal del constructor 
X x2 = X::X()    // Error: invocación ilegal del constructor
X x3 = X();      // L.4: Ok. Invocación legal del constructor  
X x4();          // L.5: Ok. Variación sintáctica del anterior 

Como ocurre con los tipos básicos (preconstruidos en el lenguaje), si deseamos crear objetos persistentes de tipo abstracto (definidos por el usuario), debe utilizarse el operador new . Este operador está íntimamente relacionado con los constructores. De hecho, para invocar la creación de un objeto a traves de él, debe existir un constructor por defecto. Si nos referimos a la clase CompleX definida en el ejemplo, las sentencias:

{
  CompleX* pt1 = new(CompleX);
  CompleX* pt2 = new(CompleX)(1,2);
}

provocan la creación de dos objetos automáticos, los punteros pt1 y pt2, así como la creación de sendos objetos (anónimos) en el montón. Observe que ambas sentencias suponen un invocación implícita al constructor. La primera al constructor por defecto sin argumentos, la segunda con los argumentos indicados. En consecuencia producirán las siguientes salidas:

c1: (0,0)
c1: (1,2)

Observe también, y esto es importante, que los objetos pt1 y pt2 son destruidos automáticamente al salir de ámbito el bloque. No así los objetos señalados por estos punteros (ver comentario al respecto.

Propiedades de los constructores

Aunque los constructores comparten muchas propiedades de los métodos normales, tienen algunas características que las hace ser un tanto especiales. En concreto, se trata de funciones que utilizan rutinas de manejo de memoria en formas que las funciones normales no suelen utilizar.

1- Los constructores se distinguen del resto de las funciones de una clase porque tienen el mismo nombre que esta. Ejemplo:

class X {         // definición de la clase X
  public:
  X();            // constructor de la clase X
};

2- No se puede obtener su dirección, por lo que no pueden declararse punteros a este tipo de métodos.

3- No pueden declararse virtuales

4- Se declaran sin devolver nada, ni siquiera void, lo que no es óbice para que el resultado de su actuación (un objeto) sí pueda ser utilizado como valor devuelto por una función:

class C { ... };
...
C foo() {
   return C();
}

5- No pueden ser heredados, aunque una clase derivada puede llamar a los constructores y destructores de la superclase siempre que hayan sido declarados public o protected. Como el resto de las funciones (excepto main), los constructores también pueden ser sobrecargados; es decir, una clase puede tener varios constructores.

En estos casos, la invocación (incluso implícita) del constructor adecuado se efectuará según los argumentos involucrados. Es de destacar que en ocasiones, la multiplicidad de constructores puede conducir a situaciones realmente curiosas; incluso se ha definido una palabra clave, explicit, para evitar los posibles efectos colaterales.

6- Un constructor no puede ser friend de ninguna otra clase.

7- Una peculiaridad sintáctica de este tipo de funciones es la posibilidad de incluir iniciadores, una forma de expresar la inicialización de variables fuera del cuerpo del constructor

8- Como en el resto de las funciones, los constructores pueden tener argumentos por defecto

9- Cuando se definen constructores deben evitarse ambigüedades

10- Los constructores de las variables globales son invocados por el módulo inicial antes de que sea llamada la función main y las posibles funciones que se hubiesen instalado mediante la directiva #pragma startup.

11- Los objetos locales se crean tan pronto como se inicia su ámbito. También se invoca implícitamente un constructor cuando se crea, o copia, un objeto de la clase (incluso temporal).

El hecho de que al crear un objeto se invoque implícitamente un constructor por defecto si no se invoca ninguno de forma explícita, garantiza que siempre que se instancie un objeto será inicializado adecuadamente.

12- El constructor de una clase no puede admitir la propia clase como argumento (se daría lugar a una definición circular).

13- Los parámetros del constructor pueden ser de cualquier tipo, y aunque no puede aceptar su propia clase como argumento, en cambio sí pueden aceptar una referencia a objetos de su propia clase, en cuyo caso se denomina constructor-copia (su sentido y justificación lo exponemos con más detalle en el apartado correspondiente).

Constructor oficial

Si el programador no define explícitamente ningún constructor, el compilador proporciona uno por defecto al que llamaremos oficial o de oficio. Es público, "inline", y definido de forma que no acepta argumentos. Es el responsable de que funcionen sin peligro secuencias como esta:

class A {
 int x;
};         // C++ ha creado un constructor "de oficio"
...
A a;       // invocación implícita al constructor de oficio

Constructor trivial

Un constructor de oficio se denomina trivial si cumple las siguientes condiciones: La clase correspondiente no tiene funciones virtuales y no deriva de ninguna superclase virtual.

Todos los constructores de las superclases de su jerarquía son triviales Los constructores de sus miembros no estáticos que sean clases son también triviale

Constructor por defecto

Constructor por defecto de la clase X es aquel que "puede" ser invocado sin argumentos, bien porque no los acepte, bien porque disponga de argumentos por defecto. Como hemos visto en el epígrafe anterior, el constructor oficial creado por el compilador si no hemos definido ningún constructor, es también un constructor por defecto, ya que no acepta argumentos.

Tenga en cuenta que diversas posibilidades funcionales y sintácticas de C++ precisan de la existencia de un constructor por defecto (explícito u oficial). Por ejemplo, es el responsable de la creación del objeto x en una declaración del tipo X x;.

== Orden de construcción==

Dentro de una clase los constructores de sus miembros son invocados antes que el constructor existente dentro del cuerpo de la propia clase. Esta invocación se realiza en el mismo orden en que se hayan declarado los elementos. A su vez, cuando una clase tiene más de una clase base (herencia múltiple), los constructores de las clases base son invocados antes que el de la clase derivada y en el mismo orden que fueron declaradas. Por ejemplo en la inicialización:

class Y {...}
class X : public Y {...}
X one;

los constructores son llamados en el siguiente orden:

Y();   // constructor de la clase base
X();   // constructor de la clase derivada

En caso de herencia múltiple:

class X : public Y, public Z
X one;

los constructores de las clase-base son llamados primero y en el orden de declaración:

Y();  // constructor de la primera clase base
Z();  // constructor de la segunda clase base
X();  // constructor de la clase derivada

Los constructores y las funciones virtuales

Debido a que los constructores de las clases-base son invocados antes que los de las clases derivadas, y a la propia naturaleza del mecanismo de invocación de funciones virtuales, el mecanismo virtual está deshabilitado en los constructores, por lo que es peligroso incluir invocaciones a tales funciones en ellos, ya que podrían obtenerse resultados no esperados a primera vista.

Considere los resultados del ejemplo siguiente, donde se observa que la versión de la función fun invocada no es la que cabría esperar en un funcionamiento normal del mecanismo virtual.

Constructores de conversión

Normalmente a una clase con constructor de un solo parámetro puede asignársele un valor que concuerde con el tipo del parámetro. Este valor es automáticamente convertido de forma implícita en un objeto del tipo de la clase a la que se ha asignado. Por ejemplo, la definición:

class X {
 public:
 X();                      // constructor C-1
 X(int);                   // constructor C-2
 X(const char*, int = 0);  // constructor C-3
};

Véase también

Fuente