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.

Problemas

  1. Problemas de sudokus para resolver con clases:

    A partir de este capítulo, y a lo largo de los siguientes, iremos creando una estructura de clases para resolver Sudokus (enlace del juego en Wikipedia).

    Este problema se ajusta bien a un enfoque POO, ya que es lo suficientemente complejo (ya iremos viendo algunos algoritmos para resolución), y en la estructura del juego es relativamente sencillo localizar objetos.

    Empecemos por algunas clases auxiliares que usaremos más adelante y que nos ayudarán a clasificar distintas capas del juego.

    Lo más evidente, cuando se ve un Sodoku clásico, es que se trata de una tabla de 9x9 casillas, en total 81. Una vez resuelto, en cada casilla debe haber un número entre 1 y 9, y se deben cumplir tres reglas:

    1. En cada fila deben aparecer los nueve dígitos (1 a 9).
    2. En cada fila deben aparecer los nueve dígitos (1 a 9).
    3. En cada bloque cuadrado de 3x3 casillas deben aparecer los nueve dígitos (1 a 9).

    De momento, el primer problema es diseñar una clase para contener la información de una única casilla. Entre los datos que debe tener, en esta primera versión, están los siguientes:

    • valor: el valor de la casilla, entre 0 y 9. Usaremos un char para este dato.
    • posible: un conjunto de 9 bits, cada uno indica si el número n es un posible valor para la casilla.
    • opciones: número de valores posibles, es decir, un contador de los bits "posible" con valor 1.

    Durante la resolución del Sudoku iremos aplicando reglas que eliminen valores posibles de ciertas casillas. Cuando sólo quede una posibilidad, esa será el valor de la casilla. En ese momento sólo uno de los bits de posible será 1, opciones tendrá el valor 1 y valor tendrá el de la única posibilidad viable.

    También puede suceder, porque queremos ser previsores, que todos los bits de opciones sean 0, y que opciones valga 0. Eso significa que el Sudoku actual no tiene solución.

    Otros datos miembro, que usaremos para aplicar las reglas más tarde de una forma más sencilla son:

    • sigFila: puntero a la siguiente casilla de la misma fila.
    • sigColumna: puntero a la siguiente casilla de la misma columna.
    • sigBloque: puntero a la siguiente casilla del mismo bloque.

    Usaremos estos punteros para recorrer cada fila, columna o bloque cuando nos interese.

    Más adelante añadiremos más métodos, pero en esta primera versión debe tener los siguientes:

    • Un constructor, que recibirá parámetro char con un valor entre 0 y 9, y con un valor por defecto de 0. El cero indica que la casilla está vacía, es decir, que aún no se le ha asignado un valor. Por supuesto, debe asignar valores iniciales para todos los datos miembro de la clase. Los que no se pueda, porque sean desconocidos, seran 0.
    • Un método para modificar el valor de sigFila.
    • Un método para modificar el valor de sigColumna.
    • Un método para modificar el valor de sigBloque.
    • Un método para obtener el valor de sigFila.
    • Un método para obtener el valor de sigColumna.
    • Un método para obtener el valor de sigBloque.

    Definir una segunda clase Tablero, que debe contener las 81 casillas. Para ello, uno de los datos miembro será, precisamente, un array de Casillas de 9x9. De momento, no añadiremos más datos.

    Añadir dos constructores. Uno de ellos sin parámetros, que creará un tablero con todas las casillas vacías. El segundo admitirá como parámetro una cadena de 81 caracteres, como un puntero a char. En este caso se iniciará cada casilla con el valor correspondiente de la cadena, fila a fila.

    Para codificar cada casilla en la cadena se consideran válidos los valores '1' a '9', y para indicar casillas vacías serán válidos los caracteres siguientes: '.', '-', o un espacio. Cualquier error en la cadena, ya sea de longitud (cadenas de más o menos de 81 caracteres), o de contenido (caracteres no permitidos), debe crear un tablero vacío.

    Los constructores deben inicializar los valores de los punteros sigFila, sigColumna y sigBloque de cada una de las 81 casillas.

  2. Crear una clase Fecha que tenga un constructor, con tres parámetros: dia, mes y anno. Para evitar fracasos en la creación de la fecha (fechas no válidas), se añadirá un dato a la clase que indique si la fecha actual es o no válida.

    En siguientes capítulos iremos añadiendo más métodos a esta clase, así que no la pierdas.

  3. Crear una clase para representar números racionales (fracciones). Se debe implementar un constructor, un método para simplificar, y un método privado para calcular el máximo común divisor.

    Consultar Ejemplo 11-4, Ejemplo 11-5 y Ejemplo 24-2 sobre operaciones con fracciones y cálculo del máximo común divisor.

    También se debe crear un método para mostrar el objeto en pantalla, en la forma "N/D", donde N es el numerador y D el denominador.

    Conservar este programa para futuras ampiaciones.