Disponible la versión 6 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.2d1  Constructores

§1  Sinopsis

Podemos imaginar que 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 (no contengan basura).

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.

Es significativo señalar que las fases anteriores se realizan en un orden, aunque todas deben ser felizmente completadas cuando finaliza la labor del constructor.

§2  Descripción

Para empezar a entender como funciona el asunto, 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.

Es de destacar la utilización explícita del puntero this ( 4.11.6) 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

Nota: en realidad esta última sentencia es sintácticamente incorrecta; se trata solo de un recurso pedagógico, ya que no es posible invocar de esta forma al constructor de una clase ( 4.11.2d). Una alternativa correcta a la declaración de L.19 sería:

CompleX c2 = CompleX(1,2);

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 ( 4.1.2). 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 ( 4.11.2d), 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 .

§3  Técnicas de buena construcción

Recordar que 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.

§4  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 [4]

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

X x3 = X();      // L.4: Ok. Invocación legal del constructor  [5]

X x4();          // L.5: Ok. Variación sintáctica del anterior [6]


Nota: observe como la única sentencia válida con invocación explícita al constructor (L.4) es un caso de invocación de función miembro muy especial desde el punto de vista sintáctico (esta sintaxis no está permitida con ningún otro tipo de función-miembro, ni siquiera con funciones estáticas o destructores).  La razón es que los constructores se diferencian de todos los demás métodos no estáticos de la clase en que no se invocan sobre un objeto (aunque tienen puntero this 4.11.6).  En realidad se asemejan a los dispositivos de asignación de memoria, en el sentido que son invocados desde un trozo de memoria amorfa y la convierten en una instancia de la clase [7].


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 ( 4.9.20).  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 4.11.2d2).

§5  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.

§5.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
};


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

§5.3  No pueden declararse virtuales ( 4.11.8a). Ejemplo:

class C {

   ...

   virtual C();         // Error !!

};

La razón está en la propia idiosincrasia de este tipo de funciones.  En efecto, veremos que declarar que un método es virtual ( 4.11.8a) supone indicar al compilador que el modo concreto de operar la función será definido más tarde, en una clase derivada. Sin embargo, un constructor debe conocer el tipo exacto de objeto que debe crear, por lo que no puede ser virtual.


§5.4  Otras peculiaridades de los constructores es que 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.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 ( 4.11.2a).  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 .

§5.5  Un constructor no puede ser friend ( 4.11.2a1) de ninguna otra clase.


§5.6  Una peculiaridad sintáctica de este tipo de funciones es la posibilidad de incluir iniciadores (4.11.2d3), una forma de expresar la inicialización de variables fuera del cuerpo del constructor. Ejemplo:

class X {

  const int i;

  char c;
  public:
  X(int entero, char caracter): i(entero), c(caracter) { };
};


§5.7  Como en el resto de las funciones, los constructores pueden tener argumentos por defecto.  Por ejemplo, el constructor:

X::X(int, int = 0)

puede aceptar uno o dos argumentos.  Cuando se utiliza con uno, el segundo se supone que es un cero int.

De forma análoga, el constructor

X::X(int = 5, int = 6)

puede aceptar dos, uno, o ningún argumento. Los valores por defecto proporcionan la información necesaria cuando faltan datos explícitos.

  Observe que un constructor sin argumentos, como X::X(), no debe ser confundido con X::X(int=0), que puede ser llamado sin argumentos o con uno, aunque en realidad siempre tendrá un argumento.  En otras palabras:  que una función pueda ser invocada sin argumentos no implica necesariamente que no los acepte.


§5.8  Cuando se definen constructores deben evitarse ambigüedades. Es el caso de los constructores por defecto del ejemplo siguiente:

class X  {
  public:
  X();
  X(int i = 0);
};


int main() {
   X uno(10);  // Ok; usa el constructor X::X(int)
   X dos;      // Error: ambigüedad cual usar? X::X() o X::X(int = 0)
   return 0; 
}


§5.9
  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 ( 1.5).


§5.10  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.

En el ejemplo que sigue se muestra claramente como se invoca el constructor tan pronto como se crea un objeto.

#include <iostream>
using namespace std;

class A {             // definición de una clase
  public:
  int x;
  A(int i = 1) {      // constructor por defecto
    x = i;
    cout << "Se ha creado un objeto" << endl;
  }
};

int main() {          // =========================
  A a;                // se instancia un objeto
  cout << "Valor de a.x: " << a.x << endl;
  return 0;
}

Salida:

Se ha creado un objeto
Valor de a.x: 1

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

class X {
  public:
  X(X);         // Error: ilegal
};


§5.12  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 4.11.2d4).

Ejemplo:

class X {
  public:
  X(X&);        // Ok. correcto
};

Aparte del referido constructor-copia, existe otro tipo de constructores de nombre específico: el constructor oficial y el constructor por defecto .

§6  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" ( 4.11.2a), 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


Recordemos que el constructor de oficio invoca implícitamente los constructores de oficio de todos los miembros.  Si algunos miembros son a su vez objetos abstractos, se invocan sus constructores. Así sucesivamente con cualquier nivel de complejidad hasta llegar a los tipos básicos (preconstruidos en el lenguaje 2.2) cuyos constructores son también invocados.  Recordar que los constructores de los tipos básicos inician (reservan memoria) para estos objetos, pero no los inicializan con ningún valor concreto. Por lo que en principio su contenido es impredecible (basura) [1]. Dicho en otras palabras: el constructor de oficio se encarga de preparar el ambiente para que el objeto de la clase pueda operar,  pero no garantiza que los datos contenidos sean correctos.  Esto último es responsabilidad del programador y de las condiciones de "runtime". Por ejemplo:

struct Nombre {

  char* nomb;

};

struct Equipo {

  Nombre nm;

  size_t sz;

};

struct Liga {

  int year;

  char categoria;

  Nombre nLiga;

  Equipo equipos[10];

};

...

Liga primDiv;


En este caso la última sentencia inicia primDiv mediante una invocación al constructor por defecto de Liga, que a su vez invoca a los constructores por defecto de Nombre y Equipo para crear los miembros nLiga y equipos (el constructor de Equipo es invocado diez veces, una por cada miembro de la matriz). A su vez, cada invocación a Equipo() produce a su vez una invocación al constructor por defecto de Nombre (size_t es un tipo básico y no es invocado su constructor 4.9.13). Los miembros nLiga y equipos son iniciados de esta forma, pero los miembros year y categoria no son inicializados ya que son tipos simples, por lo que pueden contener basura.

Si el programador define explícitamente cualquier constructor, el constructor oficial deja de existir. Pero si omite en él la inicialización de algún tipo abstracto, el compilador añadirá por su cuenta las invocaciones correspondientes a los constructores por defecto de los miembros omitidos ( Ejemplo).

§6.1  Constructor trivial

Un constructor de oficio se denomina trivial si cumple las siguientes condiciones:

  • La clase correspondiente no tiene funciones virtuales ( 4.11.8a) 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 triviales
§7  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 ( 4.4.5).

  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;.

§8  Un constructor explícito puede ser imprescindible

En el primer ejemplo , el programa ha funcionado aceptablemente bien utilizando el constructor de oficio en una de sus clases, pero existen ocasiones en que es imprescindible que el programador defina uno explícitamente, ya que el suministrado por el compilador no es adecuado.

Consideremos una variación del citado ejemplo en la que definimos una clase para contener las coordenadas de puntos de un plano en forma de matrices de dos dimensiones:

#include <iostream>

using namespace std;

class Punto {
  public: int coord[2];
};

 

int main() {                // ==================

  Punto p1(10, 20);         // L.8:
  cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl;
}

Este programa produce un error de compilación en L.8. La razón es que si necesitamos este tipo de inicialización del objeto p1 (utilizando una lista de argumentos), es imprescindible la existencia de un constructor explícito ( 4.11.2d3). La versión correcta del programa seria:

#include <iostream>

using namespace std;


class Punto {
  public: int coord[2];

  Punto(int x = 0, int y = 0) {      // construtor explícito
    coord[0] = x; coord[1] = y;      // inicializa
  }

};

 

int main() {                // ==================

  Punto p1(10, 20);         // L.8:  Ok.
  cout << "Punto p1; X == " << coord[0] << "; Y == " << coord[1] << endl;
}


§8.1  La anterior no es por supuesto la única causa que hace necesaria la existencia de constructores explícitos.  Más frecuente es el caso de que algunas de las variables de la clase deban ser persistentes. Por ejemplo, supongamos que en el caso anterior necesitamos que la matriz que almacena las coordenadas necesite este tipo de almacenamiento. En este caso, puesto que la utilización del especificador static aplicado a miembros de clase puede tener efectos colaterales indeseados ( 4.11.7), el único recurso es situar el almacenamiento en el montón ( 1.3.2), para lo que utilizamos el operador new (4.9.20) en un constructor definido al efecto.  La definición de la clase tendría el siguiente aspecto [8]:

class Punto {
  public: int* coord;
  Punto(int x = 0, int y = 0) {      // construtor por defecto
    coord = new int[2];              // asigna espacio
    coord[0] = x; coord[1] = y;      // inicializa
    cout << "Creado punto; X == "
         << coord[0] << "; Y == " << coord[1] << endl;
  }
};


Posteriormente se podrían instanciar objetos de la clase Punto mediante expresiones como:

Punto p1;                       // invocación implícita

Punto p2(3, 4);                 // invocación implícita con argumentos
Punto p3 = Punto(5, 6);         // invocación explícita con argumentos
Punto* ptr1 = new(Punto)        // invocación implícita sin argumentos
Punto* ptr2 = new(Punto)(7, 8)  // invocación implícita con argumentos

§9  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 4.11.2c), 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

Nota: al tratar de la destrucción de objetos ( 4.11.2d2), veremos que los destructores son invocados exactamente en orden inverso al de los constructores.


§9.1  Los constructores de clases base virtuales ( 4.11.8a) son invocados antes que los de cualquier clase base no virtual.  Si la jerarquía contiene múltiples clases base virtuales, sus constructores son invocados en el orden de sus declaraciones. A continuación de invocan los constructores del resto de las clase base, y por último el constructor de la clase derivada.


§9.2  Si una clase virtual deriva de otra no virtual,  primero se invoca el constructor de la clase base (no virtual), de forma que la virtual (derivada) pueda ser construida correctamente. Por ejemplo, el código:

class X : public Y, virtual public Z
X one;

origina el siguiente orden de llamada en los constructores:

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


Un ejemplo más complicado:

class base;
class base2;
class level1 : public base2, virtual public base;
class level2 : public base2, virtual public base;
class toplevel : public level1, virtual public level2;
toplevel view;

El orden de invocación de los constructores es el siguiente:

base();      // clase virtual de jerarquía más alta
             // base es construida solo una vez
base2();     // base no virtual de la base virtual level2
             // debe invocarse para construir level2
level2();    // clase base virtual
base2();     // base no virtual de level1
level1();    // otra base no virtual

toplevel();


§9.3  Si una jerarquía de clases contiene múltiples instancias de una clase base virtual, dicha base virtual es construida solo una vez. Aunque si existen dos instancias de la clase base: virtual y no virtual, el constructor de la clase es invocado solo una vez para todas las instancias virtuales y después una vez para cada una de las instancias no virtuales.

§9.4  En el caso de matrices de clases, los constructores son invocados en orden creciente de subíndices.

§10  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 ( 4.11.8a), 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.

#include <string>
#include <iostream>
using namespace std;

class B {                   // superclase
  public:
  virtual void fun(const string& ss) {
    cout << "Funcion-base: " << ss << endl;
  }
  B(const string& ss) {     // constructor de superclase
    cout << "Constructor-base\n";
    fun(ss);
  }
};

class D : public B {        // clase derivada
  string s;                 // private por defecto
  public:
  void fun(const string& ss) { cout << "Funcion-derivada\n"; s = ss; }
  D(const string& ss) :B(ss) {  // constructor de subclase
    cout << "Constructor-derivado\n";
  }
};

int main() {                // =============
  D d("Hola mundo");        // invocación implícita a constructor D
}

Salida:

Constructor-base
Funcion-base: Hola mundo
Constructor-derivado

Nota: la invocación de destructores ( 4.11.2d2) se realiza en orden inverso a los constructores.  Las clases derivadas se destruyen antes que las clases-base [2]. Por esta razón el mecanismo virtual también está deshabilitado en los destructores (lo que no tiene nada que ver con que los destructores puedan ser en sí mismos funciones virtuales 4.11.2d2). Así pues, en la ejecución de un destructor solo se invocan las definiciones locales de las funciones implicadas. De lo contrario se correría el riesgo de referenciar la parte derivada del objeto que ya estaría destruida.

§11  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
};

en la que se han definido dos constructores que pueden ser utilizados con un solo argumento,  permite que las siguientes asignaciones sean legales:

void f() {
  X a;                     // Ok invocado C-1
  X b = X();               // Ok ídem.
  X c = X(1);              // Ok invocado C-2
  X d(1);                  // Ok igual que el anterior
  X e = X("Mexico");       // Ok invocado C-3
  X f("Mexico");           // Ok igual que el anterior
  X g = 1;                 // L.1 Ok.
  X h = "Madrid";          // L.2 Ok.
  a = 2;                   // L.3 Ok.
}

La explicación de las tres últimas sentencias es la siguiente:

En L.1, el compilador intenta convertir el Rvalue (que aquí es una constante numérica entera de valor 1) en el tipo del Lvalue, que aquí es la declaración de un nuevo objeto (una instancia de la clase). Como necesita crear un nuevo objeto, utilizará un constructor, de forma que busca si hay uno adecuado en X que acepte como argumento el tipo situado a la derecha.  El resultado es que el compilador supone un constructor implícito a la derecha de L.1:

  X a = X(1);    // interpretación del compilador para L.1

El proceso se repite en la sentencia L.2 que es equivalentes a:

X B = X("Madrid");    // L.2bis

La situación en L.3 es completamente distinta, ya que en este caso ambos operandos son objetos ya construidos. Para poder realizar la asignación, el compilador intenta convertir el tipo del Rvalue al tipo del Lvalue, para lo cual, el mecanismo de conversión de tipos busca si existe un constructor adecuado en X que acepte el operando derecho. Caso de existir se creará un objeto temporal tipoX que será utilizado como Rvalue de la asignación. La asignación propiamente dicha es realizada por el operador correspondiente (explícito o implícito) de X. La página adjunta incluye un ejemplo que muestra gráficamente el proceso seguido ( Ejemplo)

Este tipo de conversión automática se realiza solo con constructores que aceptan un argumento o que son asimilables (como C-2), y suponen una conversión del tipo utilizado como argumento al tipo de la clase. Por esta razón son denominadas conversiones mediante constructor, y a este tipo de constructores constructores de conversión ("Converting constructor"). Su sola presencia habilita no solo la conversión implícita, también la explícita. Ejemplo:

class X {
  public:
  X(int);        // constructor C-2
};

la mera existencia del constructor C-2 en la clase X,  permite las siguientes asignaciones:

void f() {
  X a = X(1)               // L1: Ok. invocación explícita al constructor
  X a = 1;                 // Ok. invocación implícita X(1)
  a = 2;                   // Ok. invocación implícita X(2)
  a = (X) 2;               // Ok. casting explícito (estlo tradicional)
  a = static_cast<X>(2);   // Ok. casting explícito (estilo C++)
}

Si eliminamos el constructor C-2 de la declaración de la clase, todas estas sentencias serían erróneas.

Observe que en L1 cabría hacerse una pregunta: ¿Se trata de la invocación del constructor, o un modelado explícito al estilo tradicional?. La respuesta es que se trata de una invocación al constructor, y que precisamente el modelado (explícito o implícito) se apoya en la existencia de este tipo de constructores para realizar su trabajo.

  Temas relacionados
  • Operadores de conversión ( 4.9.18k)
  • Conversión automática a tipos simples ( 4.13.6)
§12  Constructor  explicit

El problema es que en ocasiones el comportamiento descrito en el epígrafe anterior puede resultar indeseable y enmascarar errores. Es posible evitarlo declarando el constructor de la clase con la palabra clave explicit, dando lugar a los denominados constructores explicit [3]. En estos casos, los objetos de la clase solo podrán recibir asignaciones de objetos del tipo exacto. Cualquier otra asignación provocará un error de compilación.


§12.1
La sintaxis de utilización es:

explicit <declaración de constructor de un solo parámetro>

Aplicándolo al ejemplo anterior:

class X {
  public:
  explicit X(int);                   // constructor C-2b
  explicit X(const char*, int = 0);  // constructor C-3b
};
...
void f() {
  X a = 1;                 // L.1 Error!!
  X B = "Madrid";          // L.2 Error!!  
  a = 2;                   // L.3 Error!!
}

Ahora los objetos de la clase X, dotada con constructores explicit, solo pueden recibir asignaciones de objetos del mismo tipo:

void f() {
  X a = X(1);              // L.1 Ok.
  X b = X("Madrid", 0);    // L.2 Ok.
  a = (X) 2;               // L.3 Ok.
}

En L.3 se ha utilizado una conversión de tipos ("Casting") explícita ( 4.9.9). Para realizarla, el mecanismo de conversión busca si en la clase X existe un constructor que acepte como argumento el tipo de la derecha, con lo que estaríamos en el caso de L.1.

§13  Constructores privados y protegidos

Cuando los constructores no son públicos (son privados o protegidos 4.11.2b-1), no pueden ser accedidos desde el exterior, por lo que no pueden ser invocados explícita ni implícitamente al modo tradicional (§4 ). Ejemplo:

class C {
  int x;
  C(int n=0): x(n) {}   // privado por defecto
};
...
void foo() {
  C c(1);   // Error!! cannot access private member
}

Además, como las clases derivadas necesitan invocar los constructores de las superclases para instanciar sus objetos, caso de no ser protegidos o públicos también pueden existir limitaciones para su creación. Ejemplo:

class B {
  int x;
  B (): x(10) {}
};

class D : public B {
  ...
};

void foo() {
   D d;    // Error!! no appropriate default constructor available
   ...
}


Puesto que los miembros private o protected no pueden ser accedidos desde el exterior de la clase, este tipo de constructores se suelen utilizar siempre a través de funciones-miembro públicas con objeto de garantizar cierto control sobre los objetos creados. El esquema de utilización sería el siguiente:

class C {
  C(int n) { /* constructor privado */ }
  public:
  static C makeC(int m) {
     ...
     return C(m);
  }
  ...
};
 
void foo() {
   C c = C::makeC(1);   // Ok.
}

Observe que makeC() es estática para que pueda ser invocada con independencia de la existencia de cualquier objeto ( 4.11.7). Observe también que mientras una expresión como:

C c = C(1);

es una invocación al constructor, en cambio, la sentencia 

C c = C::makeC(1);

es una invocación al operador de asignación, ya que la función devuelve un objeto que será tomado como Rvalue de la asignación.

Esta técnica, que utiliza constructores privados o protegidos junto con métodos públicos para accederlos, puede prevenir algunas conversiones de tipo no deseadas, pudiendo constituir una alternativa al uso de constructores explicit (§12 ).

  Inicio.


[1]  Las razones de este trato desigual entre ambos tipos de miembros se debe a la herencia de C y a consideraciones de eficiencia.

[2]  Debe tomarse también en el sentido de que las partes privativas del objeto se destruyen antes que sus partes heredadas (los miembros del espacio de nombres local se destruyen antes que los miembros del espacio de nombres de sus ancestros).

[3]  En vez de llamarlos "constructores explícitos", preferimos mantener aquí la denominación inglesa, manteniendo la primera para aquellos constructores que han sido explícitamente definidos en el programa, distinguiéndolos así de los constructores "de oficio".

[4]  Aún en el caso de que esta invocación al constructor instanciase un objeto, este sería de imposible acceso, ya que carecería de cualquier identificador o puntero. Además el constructor tiene que saber sobre que objeto debe actuar, cosa que no ocurre en esta expresión.

[5]  Observe que aunque válida, esta sentencia es un tanto extraña, habida cuenta que se repite hasta la saciedad que los constructores no devuelven nada "ni siguiera void"; en este caso, la sentencia parece asignar al Lvalue el valor "devuelto" por el constructor.

[6]  Esta forma es utilizada cuando hay que pasar argumentos al constructor.

[7]  Stroustrup & Ellis: ACRM  §12.1

[8]  En aras de la simplicidad del ejemplo, hemos omitido intencionadamente cualquier referencia al destructor de la clase, que en este caso, exigiría evidentemente una definición explícita para liberar la zona de memoria reservada en el montón.