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 para las clases [64], de modo que debe comprender las opciones adecuadas para crear miembros 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 temporarios). Pero preservar la consistencia de un objeto constante es más complicado. El compilador puede asegurar la consistencia de las variables de los tipos del lenguaje pero no puede vigilar la complejidad de una clase. Para garantizar dicha consistencia se emplean las funciones miembro constantes; que son las únicas que un objeto constante puede invocar.

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 vector en una clase y se quiere usar const en lugar de #define para establecer el tamaño del vector y para usarlo al calcular datos concernientes al vector. El tamaño del vector 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, algo que const permite corregir de forma adecuada consiguiendo 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 para cada variable y representa un valor que es inicializado y ya no se puede cambiar. El uso de una constante dentro de una clase significa «Esto es constante durante la vida del objeto». Por otra parte, 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. 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.

Un punto especial de inicialización es la llamada «lista de inicialización del constructor» y fue pensada en un principio para su uso en herencia (tratada en el [FIXME:XREF: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 antes de abrir la llave del cuerpo del constructor. Se hace así para recordarle que las inicialización de la lista sucede antes de ejecutarse el constructor. Ese 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 del lenguaje como si tuvieran constructores.

Constructores para los tipos del lenguaje

Durante el desarrollo del lenguaje se puso más esfuerzo en hacer que los tipos definidos por el programador se pareciesen a los tipos del lenguaje, pero a veces, cuando se vio útil se hizo 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 del lenguaje 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 del lenguaje (que simplemente significan asignación) al caso general que es por lo que la definición float funciona en el código anterior. A menudo es útil encapsular un tipo del lenguaje 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


El vector de enteros declarado en main() se inicializa automáticamente a cero. Esta inicialización no es necesariamente más costosa que un bucle for o memset(). Muchos compiladores lo optimizan fácilmente como un proceso muy rápido.



[64] N. del T.: Esto se conoce como polisemia del lenguaje