8.4. Clases

Esta sección muestra la forma en la que se puede usar el especificador const con las clases. Puede ser interesante crear una constante local a una clase para usarla en expresiones constantes que serán evaluadas en tiempo de compilación. Sin embargo, el significado del especificador const es diferente dentro de las clases, de modo que debe comprender las opciones adecuadas para crear atributos constantes en una clase.

También se puede hacer que un objeto completo sea constante (y como se ha visto, el compilador siempre hace constantes los objetos temporales). Pero preservar la constancia de un objeto constante es más complicado. El compilador puede asegurar la constancia de las variables de los tipos predefinidos pero no puede vigilar la complejidad de una clase. Para garantizar la constancia de un objeto se emplean las funciones miembro constantes: sólo se puede invocar una función miembro constante sobre un objeto constante.

8.4.1. const en las clases

Uno de los lugares donde interesa usar const es para expresiones constantes dentro de las clases. El ejemplo típico es cuando se define un array en una clase y se quiere usar const en lugar de #define para establecer el tamaño del array y para usarlo al calcular datos concernientes al array. El tamaño del array es algo que desea mantener oculto en la clase, así que si usa un nombre como size, por ejemplo, se podría usar el mismo nombre en otra clase sin que ocurra un conflicto. El preprocesador trata todos los #define de forma global a partir del punto donde se definen, así que con ellos no se consigue el efecto deseado.

Se podría pensar que la elección lógica es colocar una constante dentro de la clase. Esto no produce el resultado esperado. Dentro de una clase, const recupera un poco su significado en C. Asigna espacio de almacenamiento dentro de cada objeto y representa un valor que se inicializa una vez y ya no se puede cambiar. El uso de una constante dentro de una clase significa «Esto es constante durante la vida del objeto». Sin embargo, en cada objeto la constante puede contener un valor diferente.

Por eso, cuando crea una constante ordinaria (no estática) dentro de una clase, no puede darle un valor inicial. Esta inicialización debe ocurrir en el constructor, por supuesto, pero en un lugar especial del constructor. Como la constante se debe inicializar en el punto en que se crea, en el cuerpo del constructor la constante debe estar ya inicializada. De otro modo, le quedaría la opción de esperar hasta algún punto posterior en el constructor, lo que significaría que la constante no tendría valor por un momento. Y nada impediría cambiar el valor de la constante en varios sitios del constructor.

La lista de inicialización del constructor

Ese punto especial de inicialización se llama «lista de inicialización del constructor» y fue pensada en un principio para su uso en herencia (tratada en el Capítulo 14). La lista de inicialización del constructor (que como su nombre indica, sólo aparece en la definición del constructor) es una lista de «llamadas a constructores» que aparece después de la lista de argumentos del constructor y de dos puntos, y antes de abrir la llave del cuerpo del constructor. Se hace así para recordarle que la inicialización de la lista sucede antes de que se ejecute el código principal del constructor. Éste es el lugar donde poner las inicializaciones de todas las constantes de la clase. El modo apropiado para colocar las constantes en una clase se muestra a continuación:

//: C08:ConstInitialization.cpp
// Initializing const in classes
#include <iostream>
using namespace std;

class Fred {
  const int size;
public:
  Fred(int sz);
  void print();
};

Fred::Fred(int sz) : size(sz) {}
void Fred::print() { cout << size << endl; }

int main() {
  Fred a(1), b(2), c(3);
  a.print(), b.print(), c.print();
} ///:~

Listado 8.9. C08/ConstInitialization.cpp


El aspecto de la lista de inicialización del constructor mostrada arriba puede crear confusión al principio porque no es usual tratar los tipos predefinidos como si tuvieran constructores.

Constructores para los tipos predefinidos

A medida que el lenguaje se desarrollaba y se ponía más esfuerzo en hacer que los tipos definidos por el programador se pareciesen a los tipos predefinidos, se hizo evidente que a veces era útil hacer que los tipos predefinidos (built-in) se pareciesen a los definidos por el programador. En la lista de inicialización del constructor, puede tratar a los tipos predefinidos como si tuvieran un constructor, como aquí:

//: C08:BuiltInTypeConstructors.cpp
#include <iostream>
using namespace std;

class B {
  int i;
public:
  B(int ii);
  void print();
};

B::B(int ii) : i(ii) {}
void B::print() { cout << i << endl; }

int main() {
  B a(1), b(2);
  float pi(3.14159);
  a.print(); b.print();
  cout << pi << endl;
} ///:~

Listado 8.10. C08/BuiltInTypeConstructors.cpp


Esto es especialmente crítico cuando se inicializan atributos constantes porque se deben inicializar antes de entrar en el cuerpo de la función. Tiene sentido extender este «constructor» para los tipos predefinidos (que simplemente significa asignación) al caso general, que es por lo que la definición float pi(3.14159) funciona en el código anterior. A menudo es útil encapsular un tipo predefinido en una clase para garantizar la inicialización con el constructor. Por ejemplo, aquí hay una clase Integer:

//: C08:EncapsulatingTypes.cpp
#include <iostream>
using namespace std;

class Integer {
  int i;
public:
  Integer(int ii = 0);
  void print();
};

Integer::Integer(int ii) : i(ii) {}
void Integer::print() { cout << i << ' '; }

int main() {
  Integer i[100];
  for(int j = 0; j < 100; j++)
    i[j].print();
} ///:~

Listado 8.11. C08/EncapsulatingTypes.cpp


Todos los elementos del array de Integer declarado en main() se inicializan automáticamente a cero. Esta inicialización no es necesariamente más costosa que un bucle for o memset(). Muchos compiladores la optimizan fácilmente para convertirla en un proceso muy rápido.