29 Constructores

Los constructores son funciones miembro especiales que sirven para inicializar un objeto de una determinada clase al mismo tiempo que se declara.

Los constructores son especiales por varios motivos:

  • Tienen el mismo nombre que la clase a la que pertenecen.
  • No tienen tipo de retorno, y por lo tanto no retornan ningún valor.
  • No pueden ser heredados.
  • Por último, deben ser públicos, no tendría ningún sentido declarar un constructor como privado, ya que siempre se usan desde el exterior de la clase, ni tampoco como protegido, ya que no puede ser heredado.

Sintaxis:

class <identificador de clase> {
    public:
        <identificador de clase>(<lista de parámetros>) [: <lista de constructores>] {
            <código del constructor>
        }
        ...
}

Añadamos un constructor a nuestra clase pareja:

#include <iostream> 
using namespace std;
 
class pareja {
   public:
      // Constructor
      pareja(int a2, int b2);
      // Funciones miembro de la clase "pareja"
      void Lee(int &a2, int &b2);
      void Guarda(int a2, int b2);
   private:
      // Datos miembro de la clase "pareja"
      int a, b; 
};

pareja::pareja(int a2, int b2) {
   a = a2;
   b = b2;
}

void pareja::Lee(int &a2, int &b2) {
   a2 = a;
   b2 = b;
}

void pareja::Guarda(int a2, int b2) {
   a = a2;
   b = b2;
}

int main() {
   pareja par1(12, 32);
   int x, y;
   
   par1.Lee(x, y);
   cout << "Valor de par1.a: " << x << endl;
   cout << "Valor de par1.b: " << y << endl;
   
   return 0;
}

Si no definimos un contructor el compilador creará uno por defecto, sin parámetros, que no hará absolutamente nada. Los datos miembros del los objetos declarados en el programa contendrán basura.

Si una clase posee constructor, será llamado siempre que se declare un objeto de esa clase. Si ese constructor requiere argumentos, como en este caso, es obligatorio suministrarlos.

Por ejemplo, las siguientes declaraciones son ilegales:

pareja par1;
pareja par1();

La primera porque el constructor de "pareja" requiere dos parámetros, y no se suministran.

La segunda es ilegal por otro motivo más complejo. Aunque existiese un constructor sin parámetros, no se debe usar esta forma para declarar el objeto, ya que el compilador lo considera como la declaración de un prototipo de una función que devuelve un objeto de tipo "pareja" y no admite parámetros.

Cuando se use un constructor sin parámetros para declarar un objeto no se deben escribir los paréntesis.

Y las siguientes declaraciones son válidas:

pareja par1(12,43);
pareja par2(45,34);

Constructor por defecto

Cuando no especifiquemos un constructor para una clase, el compilador crea uno por defecto sin argumentos. Por eso el ejemplo del capítulo anterior funcionaba correctamente. Cuando se crean objetos locales, los datos miembros no se inicializarían, contendrían la "basura" que hubiese en la memoria asignada al objeto. Si se trata de objetos globales, los datos miembros se inicializan a cero.

Para declarar objetos usando el constructor por defecto o un constructor que hayamos declarado sin parámetros no se debe usar el paréntesis:

pareja par2();

Se trata de un error frecuente cuando se empiezan a usar clases, lo correcto es declarar el objeto sin usar los paréntesis:

pareja par2;

Inicialización de objetos

Hay un modo específico para inicializar los datos miembros de los objetos en los constructores, que consiste en invocar los constructores de los objetos miembro antes de las llaves de la definición del constructor.

En C++ incluso las variables de tipos básicos como int, char o float son objetos. En C++ cualquier variable (u objeto) tiene, al menos un constructor, el constructor por defecto, incluso aquellos que son de un tipo básico.

Sólo los constructores de las clases admiten inicializadores. Cada inicializador consiste en el nombre de la variable miembro a inicializar, seguida de la expresión que se usará para inicializarla entre paréntesis. Los inicializadores se añadirán a continuación del paréntesis cerrado que encierra a los parámetros del constructor, antes del cuerpo del constructor y separado del paréntesis por dos puntos ":".

Por ejemplo, en el caso anterior de la clase "pareja" teníamos este constructor:

pareja::pareja(int a2, int b2) {
   a = a2;
   b = b2;
}

Podemos (y debemos) sustituir ese constructor por este otro:

pareja::pareja(int a2, int b2) : a(a2), b(b2) {}

Por supuesto, también pueden usarse inicializadores en línea, dentro de la declaración de la clase.

Ciertos miembros es obligatorio inicializarlos, ya que no pueden ser asignados, por ejemplo las constantes o las referencias. Es preferible usar la inicialización siempre que sea posible en lugar de asignaciones, ya que frecuentemente, es menos costoso y más predecible inicializar objetos en el momento de la creación que usar asignaciones.

Veremos más sobre este tema cuando veamos ejemplos de clases que tienen como miembros objetos de otras clases.

Sobrecarga de constructores

Los constructores son funciones, también pueden definirse varios constructores para cada clase, es decir, el constructor puede sobrecargarse. La única limitación (como en todos los casos de sobrecarga) es que no pueden declararse varios constructores con el mismo número y el mismo tipo de argumentos.

Por ejemplo, añadiremos un constructor adicional a la clase "pareja" que simule el constructor por defecto:

class pareja {
   public:
      // Constructor
      pareja(int a2, int b2) : a(a2), b(b2) {}
      pareja() : a(0), b(0) {}
      // Funciones miembro de la clase "pareja"
      void Lee(int &a2, int &b2);
      void Guarda(int a2, int b2);
   private:
      // Datos miembro de la clase "pareja"
      int a, b;
};

De este modo podemos declarar objetos de la clase pareja especificando los dos argumentos o ninguno de ellos, en este último caso se inicializarán los dos datos miembros con cero.

Constructores con argumentos por defecto

También pueden asignarse valores por defecto a los argumentos del constructor, de este modo reduciremos el número de constructores necesarios.

Para resolver el ejemplo anterior sin sobrecargar el constructor suministraremos valores por defecto nulos a ambos parámetros:

class pareja {
   public:
      // Constructor
      pareja(int a2=0, int b2=0) : a(a2), b(b2) {}
      // Funciones miembro de la clase "pareja"
      void Lee(int &a2, int &b2);
      void Guarda(int a2, int b2);
   private:
      // Datos miembro de la clase "pareja"
      int a, b; 
};

Asignación de objetos

Probablemente ya lo imaginas, pero la asignación de objetos también está permitida. Y además funciona como se supone que debe hacerlo, asignando los valores de los datos miembros.

Con la definición de la clase del último ejemplo podemos hacer lo que se ilustra en el siguiente:

#include <iostream>
    using namespace std;
    
int main() {
   pareja par1(12, 32), par2;
   int x, y;
   
   par2 = par1;
   par2.Lee(x, y);
   cout << "Valor de par2.a: " << x << endl;
   cout << "Valor de par2.b: " << y << endl;
   
   return 0;
}

La línea "par2 = par1;" copia los valores de los datos miembros de par1 en par2.

En realidad, igual que pasa con los constructores, el compilador crea un operador de asignación por defecto, que copia los valores de todos los datos miembro de un objeto al otro. Veremos más adelante que podemos redefinir ese operador para nuestras clases, si lo consideramos necesario.

Constructor copia

Un constructor de este tipo crea un objeto a partir de otro objeto existente. Estos constructores sólo tienen un argumento, que es una referencia a un objeto de su misma clase.

En general, los constructores copia tienen la siguiente forma para sus prototipos:

tipo_clase::tipo_clase(const tipo_clase &obj);

De nuevo ilustraremos esto con un ejemplo y usaremos también "pareja":

class pareja {
   public:
      // Constructor
      pareja(int a2=0, int b2=0) : a(a2), b(b2) {}
      // Constructor copia:
      pareja(const pareja &p);

      // Funciones miembro de la clase "pareja"
      void Lee(int &a2, int &b2);
      void Guarda(int a2, int b2);
   private:
      // Datos miembro de la clase "pareja"
      int a, b; 
};

// Definición del constructor copia:
pareja::pareja(const pareja &p) : a(p.a), b(p.b) {}

Para crear objetos usando el constructor copia se procede como sigue:

int main() {
   pareja par1(12, 32)
   pareja par2(par1); // Uso del constructor copia: par2 = par1
   int x, y;
   
   par2.Lee(x, y);
   cout << "Valor de par2.a: " << x << endl;
   cout << "Valor de par2.b: " << y << endl;

   return 0;
}

Aunque pueda parecer confuso, el constructor copia en otras circunstancias:

int main() {
   pareja par1(12, 32)
   pareja par2 = par1; // Uso del constructor copia
...

En este caso se usa el constructor copia porque el objeto par2 se inicializa al mismo tiempo que se declara, por lo tanto, el compilador busca un constructor que tenga como parámetro un objeto del tipo de par1, es decir, busca un constructor copia.

Tanto es así que se invoca al constructor copia aunque el valor a la derecha del signo igual no sea un objeto de tipo pareja.

Disponemos de un constructor con valores por defecto para los parámetros, así que intentemos hacer esto:

int main() {
   pareja par2 = 14; // Uso del constructor copia
...

Ahora el compilador intenta crear el objeto par2 usando el constructor copia sobre el objeto 14. Pero 14 no es un objeto de la clase pareja, de modo que el compilador usa el constructor de pareja con el valor 14, y después usa el constructor copia.

También para cualquier clase, si no se especifica ningún constructor copia, el compilador crea uno por defecto, y su comportamiento es exactamente el mismo que el del definido en el ejemplo anterior. Para la mayoría de los casos esto será suficiente, pero en muchas ocasiones necesitaremos redefinir el constructor copia.

Comentarios de los usuarios (7)

Joaquín
2011-10-15 17:23:50

Tengo una duda enorme con delegaciones de constructores, que veo (por lo que vengo leyendo en éste excelente curso) no está tratado, solamente sobrecarga. Desde C++11 que se permite la delegación de constructores (similar a Java al que estoy más que acostumbrado). Pero tengo errores "insalvables" que no logro comprender.

Tengo una clase template que tiene ésta forma:

template<class T, int ROWS, int COLUMNS>
class Matrix {
public:
    //Constructor para llenar de ceros la matriz
    Matrix() :
            mat(ROWS, vector(COLUMNS, 0)) {
    }
    //Constructor copia
    Matrix(const Matrix<T, ROWS, COLUMNS> &m) :
            mat(m.mat) {
    }

...

protected:
    vector<vector<T> > mat;
};

De esa clase que funciona bastante bien (es segura, en el sentido de que toma todos los errores en tiempo de compilación). Derivan 3 clases que especializan ese template. Dejo sólo los headers:

class RotationMatrix : public Matrix<double, 3, 3> { ... }
class HomogeneousMatrix : public Matrix<double, 4, 4> { ... }
class HomogeneousVector : public Matrix<double, 4, 1> { ... }

En particular el constructor de HomogeneousMatrix no me deja inicializar el miembro 'mat' porque dice que "HomogeneousMatrix no tiene un miembro 'mat'". Sin embargo en las otras clases lo inicializo de la misma manera sin problemas. ¿Alguna idea?

Steven R. Davidson
2011-10-15 19:34:29

Hola Joaquín,

Primeramente, no hablamos de la delegación de constructores, pero sí existe algo parecido en el estándar anterior de C++ (C++ 98). Sin embargo, se suele usar para invocar a constructores de las clases base, como se explica en el capítulo 38 ( http://c.conclase.net/curso/?cap=036b#038_constructores ).

En cuanto a tu código, la verdad es que no he tenido muchos problemas con ello - bueno, la parte que nos presentas. El único error importante era el uso de 'vector' en la lista inicializadora del constructor sin parámetros. Escribes:

template< class T, int ROWS, int COLUMNS >
class Matrix
{
public:
  //Constructor para llenar de ceros la matriz
  Matrix() : mat(ROWS, vector(COLUMNS, 0)) {}
...
protected:
  vector< vector<T> > mat;
};

El compilador me lanza un error con 'vector' en la línea:

mat(ROWS, vector(COLUMNS, 0))

Esto es porque no sabe qué tipo usar para 'vector'.

El código corregido es:

template< typename T, int ROWS, int COLUMNS >
class Matrix
{
public:
  //Constructor para llenar de ceros la matriz
  Matrix() : mat(ROWS, vector<T>(COLUMNS, 0)) {}
...
protected:
  vector< vector<T> > mat;
};

Ahora bien, en cuanto a problemas de no poder encontrar 'mat' dentro de 'HomogeneousVector', no te puedo decir sin ver más código fuente. Hice algunas pruebas y no he tenido problemas. Por ejemplo,

class HomogeneousVector : public Matrix<double, 4, 1>
{
public:
  // Para no escribir tanto y ser más legible
  typedef Matrix<double, 4, 1> base;

  HomogeneousVector() : base()
  {
    cout << mat.size() << endl;
  }

  HomogeneousVector( const HomogeneousVector &ref ) : base(ref)
  {
    cout << mat.size() << endl;
  }
};

Espero que esto te oriente un poco.

Steven

Anónimo
2011-10-18 18:23:17

@Steven: Muchísimas gracias por la respuesta. Finalmente lo resolví a la antigua, con una combinación de typedefs (por problemas con el template de Matriz<T, R, C> en la especialización) y parámetros por defecto. Pasa que es una práctica muy común en Java (que lo tengo más a la mano) el usar delegaciones de constructores para dar parámetros por defecto.

Lo de la inicialización del vector me había saltado ya y se solucionó fácil.

En otro momento experimentaré un poco más, ahora estoy apretado con éste programa que tiene que funcionar a como dé lugar.

Mil gracias y realmente una felicitación por el curso. es un material de consulta imprescindible. De poder arreglar la muestra de algunos códigos (que aparecen mal, con slashes que ocultan inicializaciones muchas veces) se volvería perfecto.

Morillas
2011-10-21 22:51:19

Sobre constructores privados:

Existen muchos diseños donde subyace una motivación razonable para declarar un constructor privado o protegido. Dos ejemplos:

En ocasiones es útil hacer que una interfaz defina una cierta implementación por defecto para todas sus funciones miembro, de tal manera que las subclases redefinan sólo aquellas que les interese, pero al no declarar la interfaz como abstracta se corre el riesgo de que los clientes la instancien. Utilizar un constructor protegido tiene sentido. Ejemplo:

//Interfaz A que por lo que sea queremos que se comporte como si fuera abstracta.
class A {
public:
	virtual void metodo_1( ){ //implementacion por defecto } 
	virtual void metodo_2( ){ //implementacion por defecto } 
	...
	virtual void metodo_n( ){ { //implementacion por defecto } 
protected:
	A( );
}
	
class B : public A {
	...
	void metodo_i{ //redefinición del metodo }
	...
}

Un caso típico de esto es dejar las implementaciones por defecto vacías, de forma que sólo tengamos que redefinir las funciones miembro que nos interesen (en contraposición a declararlas como virtuales puras, que exigirán, más tarde o más temprano, una implementación en todas las subclases finales).

Otro ejemplo típico del uso privado de contructores puede verse en el patrón Singleton, donde se pretende garantizar que sólo haya una instacia de una determinada clase (por ejemplo, es posible que en un programa sólo deba haber un único objeto "Sistema","Pantalla","DespachadorDeEventos"...) Ejemplo:

ClaseUnica{
public:
	static ClaseUnica* getInstancia( );
	void metodo_1( );
	...
	void metodo_n( );
private:
	ClaseUnica( );
	static ClaseUnica *_unicaInstancia; 
}
//------------------------------------------Implementación:
ClaseUnica::_unicaInstancia = 0;
	
ClaseUnica* ClaseUnica::getInstancia( ){
	if( !_unicaIntancia )
		_unicaIntacia = new ClaseUnica;	//PERMITIDO, ya que estamos dentro de ClaseUnica. (A esto se le llama "inicialización perezosa").
	return _unicaIntancia;
}
//Resto de la implementación...
	
//------------------------------------------Uso:
ClaseUnica *miObjetoUnico = new ClaseUnica; //ERROR. ClaseUnica( ) es privado!
ClaseUnica *miObjetoUnico = ClaseUnica::getInstancia( );	//Ahora sí.
ClaseUnica *miOtroObjetoUnico = ClaseUnica::getInstancia( ); //miObjetoUnico y miOtroObjetoUnico son en realidad el mismo objeto.
miObjetoUnico->metodo_i();

Se puede cambiar la visibilidad del constructor de privado a protegido y permitir la especialización de ClaseUnica, aunque personalmente, es un enfoque que a mí no me gusta.

En conclusión, hay casi tantos diseños de clases donde declarar un contructor privado o protegido tenga todo el sentido del mundo como se nos ocurran.

Un saludo y felicidades por la página.

Javier R.
2013-06-09 21:14:33

Hola. Primero que nada decir que esta estupenda esta web, ya que nos enseña muchos aspectos de lenguajes como C++.

Tengo una duda que ojala me puedan resolver. Tengo esto:

Este es el código de mi cabecera.

#ifndef PRUEBA_H
#define PRUEBA_H

#include <iostream>
#include <cstring>

using namespace std;

class cadena{
  public:
   cadena();
   cadena(const char *c);
   cadena(int n);
   cadena(const cadena &Cad);
   ~cadena();
   void DevDir();
   void Asignar(const char *dest);
   char *Leer(char *c);
  private:
    char *cad;
    short valor;
    friend void tec(cadena obj);
};

#endif

Este el de mi archivo .cpp

#include "prueba.h"

cadena::cadena():cad(NULL),valor(128){}

cadena::cadena(const char *c){
  cad = new char[strlen(c)+1];
  strcpy(cad,c);
}

cadena::cadena(int n){
  cad = new char[n+1];
  cad[0] = 0;
}

cadena::cadena(const cadena &Cad){
  cad = new char[strlen(Cad.cad)+1];
  strcpy(cad,Cad.cad);
}

cadena::~cadena(){
  delete[] cad;
}

void cadena::DevDir(){
  cout << "dir valor: " << &cad << endl;
}

void cadena::Asignar(const char *dest){
  delete[] cad;
  cad = new char[strlen(dest)+1];
  strcpy(cad,dest);
}

char *cadena::Leer(char *c){
  strcpy(c,cad);
  return c;
}

void tec(cadena obj){
  cout << obj.valor << endl;
}

Y este el de main.

#include "prueba.h"

int main(){
    cadena Cadena1;

    tec(Cadena1);

    cin.get();
    return 0;
}

Si lo compilo no hay problema, pero al ejecutarlo marca un error. He problema detectado que el problema está en la delaración del constructor copia, ya que en el constructor por defecto estoy inicializando cad a null. Y al pasar esto al constructor copia, en la parte de strlen, si recibe null ahí esta el fallo.

Bien pero, si en la función main sólo estoy llamando al constructor por defecto, ¿por qué mete su "cucharota" el constructor copia?

C++ llama a todos los contructores o ¿Qué pasa?

Muchas gracias por su respuesta.

Saludos.

Steven R. Davidson
2013-06-09 23:45:31

Hola Javier,

Se invoca el constructor copia al invocar a 'tec()'. Su prototipo es:

void tec( cadena obj );

Esto significa que al invocar a 'tec()',

tec( Cadena1 );

Pasamos 'Cadena1' por copia (o por valor) a 'tec()'. Básicamente, hacemos esto:

cadena obj = Cadena1;
cout << obj.valor << endl;
...

Como puedes ver, al instanciar 'obj', automáticamente se invoca el constructor copia.

Podríamos evitar esta copia si pasamos el objeto por referencia; esto es,

void tec( const cadena &obj );

Espero que esto aclare la duda.

Steven

Javier R.
2013-06-10 00:39:55

Muchísimas gracias por tu estupenda explicación. Se ha resuelto mi problema.

De nuevo gracias.

:-)