6: Inicialización y limpieza

Tabla de contenidos

6.1. Inicialización garantizada por el constructor
6.2. Limpieza garantizada por el destructor
6.3. Eliminación del bloque de definiciones
6.4. Stash con constructores y destructores
6.5. Stack con constructores y destructores
6.6. Inicialización de tipos agregados
6.7. Constructores por defecto
6.8. Resumen
6.9. Ejercicios

El Capítulo 4 supuso una mejora significativa en el uso de librerías tomando los diversos componentes de una librería C típica y encapsulándolos en una estructura (un tipo abstracto de dato, llamado clase a partir de ahora).

Esto no sólo permite disponer de un único punto de entrada en un componente de librería, sino que también oculta los nombres de las funciones dentro del nombre de la clase. En el Capítulo 5 se presentó el control de acceso (la ocultación de la implementación). Esto le da al diseñador de la clase la posibilidad de establecer límites claros que determinan qué cosas puede hacer el programador cliente y qué queda fuera de sus límites. Eso significa que los mecanismos internos de las operaciones sobre los tipos de datos están bajo el control y la discreción del diseñador de la clase, y deja claro a qué miembros puede y debe prestar atención el programador cliente.

Juntos, la encapsulación y el control de acceso representan un paso significativo para aumentar la sencillez de uso de las librerías. El concepto de «nuevo tipo de dato» que ofrecen es mejor en algunos sentidos que los tipos de datos que incorpora C. El compilador C++ ahora puede ofrecer garantías de comprobación de tipos para esos tipos de datos y así asegura un nivel de seguridad cuando se usan esos tipos de datos.

Sin embargo, en lo que se refiere a la seguridad, el compilador puede hacer mucho más de lo que ofrece C. En éste y en próximos capítulos verá posibilidades adicionales que se han incluido en C++ y que hacen que los errores en sus programas casi salten del programa y le agarren, a veces antes incluso de compilar el programa, pero normalmente en forma de advertencias y errores en el proceso de compilación. Por este motivo, pronto se acostumbrará a la extraña situación en que, a menudo, un programa C++ que compila funciona bien a la primera.

Dos de esas cuestiones de seguridad son la inicialización y la limpieza. Gran parte de los errores de C se deben a que el programador olvida inicializar o liberar una variable. Esto sucede especialmente con las librerías C, cuando el programador cliente no sabe cómo inicializar una estructura, o ni siquiera sabe que debe hacerlo. (A menudo las librerías no incluyen una función de inicialización, de modo que el programador cliente se ve forzado a inicializar la estructura a mano). La limpieza es un problema especial porque los programadores C están acostumbrados a olvidarse de las variables una vez que han terminado con ellas, de modo que omiten cualquier limpieza que pudiera ser necesaria en alguna estructura de la librería.

En C++, el concepto de inicialización y limpieza es esencial para facilitar el uso de las librerías y eliminar muchos de los errores sutiles que ocurren cuando el programador cliente olvida realizar estas tareas. Este capítulo examina las posibilidades de C++ que ayudan a garantizar una inicialización y limpieza apropiadas.

6.1. Inicialización garantizada por el constructor

Tanto la clase Stash como la Stack definidas previamente tienen una función llamada initialize(), que, como indica su nombre, se debería llamar antes de usar el objeto de cualquier otra forma. Desafortunadamente, esto significa que el programador cliente debe asegurar una inicialización apropiada. Los programadores cliente son propensos a olvidar detalles como la inicialización cuando tienen prisa por hacer que la librería resuelva sus problemas. En C++, la inicialización es demasiado importante como para dejársela al programador cliente. El diseñador de la clase puede garantizar la inicialización de cada objeto facilitando una función especial llamada constructor. Si una clase tiene un constructor, el compilador hará que se llame automáticamente al constructor en el momento de la creación del objeto, antes de que el programador cliente pueda llegar a tocar el objeto. La invocación del constructor ni siquiera es opcional para el programador cliente; la realiza el compilador en el punto en el que se define el objeto.

El siguiente reto es cómo llamar a esta función. Hay dos cuestiones. La primera es que no debería ser ningún nombre que pueda querer usar para un miembro de la clase. La segunda es que dado que el compilador es el responsable de la invocación del constructor, siempre debe saber qué función llamar. La solución elegida por Stroustrup parece ser la más sencilla y lógica: el nombre del constructor es el mismo que el de la clase. Eso hace que tenga sentido que esa función sea invocada automáticamente en la inicialización.

Aquí se muestra una clase sencilla con un constructor:

class X {
  int i;
public:
  X();  // Constructor
};

Ahora, cuando se define un objeto,

void f() {
  X a;
  // ...
}

ocurre lo mismo que si a fuese un entero: se reserva espacio para el objeto. Pero cuando el programa llega al punto de ejecución en el que se define a, se invoca el constructor automáticamente. Es decir, el compilador inserta discretamente la llamada a X::X() para el objeto a en el punto de la definición. Como cualquier método, el primer argumento (secreto) para el constructor es el puntero this - la dirección del objeto al que corresponde ese método. En el caso del constructor, sin embargo, this apunta a un bloque de memoria no inicializado, y el trabajo del constructor es inicializar esa memoria de forma adecuada.

Como cualquier función, el constructor puede tomar argumentos que permitan especificar cómo ha de crearse el objeto, darle valores de inicialización, etc. Los argumentos del constructor le proporcionan una forma de garantizar que todas las partes del objeto se inicializan con valores apropiados. Por ejemplo, si una clase Tree[57] tiene un constructor que toma como argumento un único entero que indica la altura del árbol, entonces debe crear un objeto árbol como éste:

Tree t(12);  // árbol de 12 pies

Si Tree(int) es el único constructor, el compilador no le permitirá crear un objeto de otro modo. (En el próximo capítulo verá cómo crear múltiples constructores y diferentes maneras de invocarlos.)

Y realmente un constructor no es más que eso; es una función con un nombre especial que el compilador invoca automáticamente para cada objeto en el momento de su creación. A pesar de su simplicidad, tiene un valor excepcional porque evita una gran cantidad de problemas y hace que el código sea más fácil de escribir y leer. En el fragmento de código anterior, por ejemplo, no hay una llamada explícita a ninguna función initialize() que, conceptualmente, esté separada de la definición. En C++, la definición y la inicialización son conceptos unificados: no se puede tener el uno sin el otro.

Constructor y destructor son tipos de funciones muy inusuales: no tienen valor de retorno. Esto es distinto de tener valor de retorno void, en cuyo caso la función no retorna nada pero usted sigue teniendo la posibilidad de hacer que retorne otra cosa. Constructores y destructores no retornan nada y no hay otra posibilidad. El acto de traer un objeto al programa, o sacarlo de él, es algo especial, como el nacimiento o la muerte, y el compilador siempre hace él mismo las llamadas a estas funciones, para asegurarse de que ocurren. Si hubiera un valor de retorno, y usted pudiera elegir uno propio, el compilador tendría que saber de algún modo qué hacer con el valor retornado, o el programador cliente tendría que invocar explícitamente a constructores y destructores, lo que eliminaría su seguridad.



[57] árbol