Constructores y destructores

Entre los métodos que se pueden definir para una estructura existen unas funciones especiales: constructores y destructores.

Un constructor tiene el mismo nombre que el nombre de la estructura y no devuelve ningún valor (tipo void). El constructor, si está definido, se invocará en el momento de la inicialización para cada nueva instancia de la estructura. Debido a esto, en el constructor se puede calcular el estado inicial de la estructura de una manera especial.

Una estructura puede tener múltiples constructores con diferentes conjuntos de parámetros, y el compilador elegirá el apropiado basándose en el número y tipo de argumentos en el momento de definir la variable.

Por ejemplo, podemos describir un par de constructores en la estructura Result: uno sin parámetros y el segundo, con un parámetro de tipo cadena para establecer el estado.

struct Result
{
   ...
   void Result()
   {
      status = "ok";
   }
   void Result(string s)
   {
      status = s;
   }
};

Por cierto: un constructor sin parámetros se llama constructor por defecto. Si no hay constructores explícitos, el compilador crea implícitamente un constructor por defecto para cualquier estructura que contenga cadenas y arrays dinámicos para rellenar estos campos con ceros.

Es importante que los campos de otros tipos (por ejemplo, todos los numéricos) no se pongan a cero, independientemente de que la estructura tenga un constructor por defecto, por lo que los valores iniciales de los elementos tras la asignación de memoria serán aleatorios. Debería crear constructores o asegurarse de que los valores correctos se asignan en el código inmediatamente después de crear el objeto.

La presencia de constructores explícitos hace imposible utilizar la sintaxis de inicialización agregada. Por eso no se compilará la línea Result r = {}; del método calculate. Ahora tenemos derecho a utilizar sólo uno de los constructores que hemos proporcionado nosotros mismos. Por ejemplo, las siguientes sentencias llaman al constructor sin parámetros:

   Result r1;
   Result r2();

Y crear una estructura con un estado relleno puede hacerse así:

   Result r3("success");

También se llama al constructor por defecto (explícito o implícito) cuando se crea un array de estructuras. Por ejemplo, la siguiente sentencia asigna memoria para 10 estructuras con resultados y las inicializa con un constructor por defecto:

   Result array[10];

Un destructor es una función que se invocará cuando el objeto estructura esté siendo eliminado. El destructor tiene el mismo nombre que el nombre de la estructura, pero va precedido del carácter de tilde '~'. El destructor, al igual que el constructor, no devuelve ningún valor, pero tampoco recibe parámetros.

Sólo puede haber un destructor.

No se puede llamar explícitamente al destructor: lo hace el programa en sí al salir de un bloque de código en el que se ha definido una variable de estructura local, o al liberar un array de estructuras.

El propósito del destructor es liberar cualquier recurso dinámico si la estructura asignó dichos recursos en el constructor. Por ejemplo, una estructura puede tener la propiedad persistence, es decir, guardar su estado en un archivo cuando se descarga de la memoria y restaurarlo cuando el programa la crea de nuevo. En este caso se utiliza un descriptor que debe abrirse y cerrarse en las funciones de archivo integradas.

Vamos a definir un destructor en la estructura Result y a añadir constructores por el camino para que todos estos métodos hagan un seguimiento del número de instancias de objetos (a medida que se crean y se destruyen).

struct Result
{
   ...
   void Result()
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
      status = "ok";
   }
 
   void Result(string s)
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
      status = s;
   }
 
   void ~Result()
   {
      static int count = 0;
      Print(__FUNCSIG__" ", ++count);
   }
};

Hay tres variables estáticas de nombre count que existen con independencia unas de otras: cada una de ellas cuenta en el contexto de su propia función.

Como resultado de la ejecución del script, obtendremos el siguiente registro:

Result::Result() 1
Result::Result() 2
Result::Result() 3
Result::~Result() 1
Result::~Result() 2
0.5 1 ok
1.00000 2.00000 3.00000
Result::Result(string) 1
0.5 1 ok
1.00000 2.00000 3.00000
Result::~Result() 3
Result::~Result() 4

Vamos a ver lo que significa.

La primera instancia de la estructura se crea en la función OnStart, en la misma línea en la que se llama a calculate. Al entrar en el constructor, el valor del contador count se inicializa una vez con cero y luego se incrementa cada vez que se ejecuta el constructor, por lo que para la primera vez, el valor es 1.

Dentro de la función calculate se define una variable local de tipo Result; se registra con el número 2.

La tercera instancia de la estructura no es tan obvia. La cuestión es que, para pasar el resultado de la función, el compilador crea implícitamente una variable temporal en la que copia los datos de la variable local. Es probable que este comportamiento cambie en el futuro, y entonces la instancia local se «moverá» fuera de la función sin duplicarse.

La última llamada al constructor es en un método con un parámetro de cadena, por lo que el recuento de llamadas es 1.

Es importante que el número total de llamadas a ambos constructores sea el mismo que el número de llamadas al destructor: 4.

Hablaremos más sobre constructores y destructores en el capítulo sobre las clases.