Tabla de contenidos
El concepto de constante (expresado con la palabra reservada const) se creó para permitir a los programadores marcar la diferencia entre lo que puede cambiar y lo que no. Esto facilita el control y la seguridad en un proyecto de programación en C++.
Desde su origen, const ha sido utilizada para diferentes propósitos. Mientras tanto, se fue filtrando de vuelta al lenguaje C, donde su significado cambió. Todo esto puede parecer un poco confuso al principio, y en este capítulo aprenderá cuándo, por qué y cómo usar la palabra reservada const. Hacia el final se expone una disertación sobre volatile, que es pariente cercano de const (ambos se refieren a los cambios) y tiene una sintaxis idéntica.
El primer motivo para la creación de const parece que fue eliminar el uso de la directiva del preprocesador #define para sustitución de valores. Desde entonces se usa para punteros, argumentos de funciones, tipos de retorno, objetos y funciones miembro. Todos ellos tienen pequeñas diferencias pero su significado es conceptualmente compatible. Se tratarán en las siguientes secciones de este capítulo.
Cuando se programa en C, se usa libremente el preprocesador para crear macros y sustituir valores. El preprocesador simplemente hace un reemplazo textual y no realiza ninguna comprobación de tipo. Por ello, la sustitución de valores introduce problemas sutiles que se pueden evitar en C++ usando valores constantes.
En C, el uso típico del preprocesador para sustituir nombres por valores es algo como:
#define BUFSIZE 100
BUFSIZE es un nombre que sólo existe durante el preprocesado. Por tanto, no ocupa memoria y se puede colocar en un fichero de cabecera para ofrecer un valor único a todas las unidades de traducción que lo utilicen. Es muy importante para el mantenimiento del código el uso de sustitución de valores en lugar de los llamados «números mágicos». Si usa números mágicos en su código, no solamente impedirá al lector conocer su procedencia o significado, sino que, si decide cambiar un valor, tendrá que editarlo a mano y no tendrá ningún rastro que seguir para asegurarse de que no olvida ninguno de sus valores (o de que no cambia por accidente uno que no debería).
La mayor parte del tiempo, BUFSIZE se comportará como una variable ordinaria, pero no siempre. Además, no tiene información de tipo. Eso puede esconder errores muy difíciles de localizar. C++ utiliza const para eliminar estos problemas llevando la sustitución de valores al terreno del compilador. Ahora, puede escribir:
const int bufsize = 100;
Puede colocar bufsize en cualquier lugar donde se necesite conocer el valor en tiempo de compilación. El compilador utiliza bufsize para hacer plegado de constantes[64], que significa que el compilador reduce una expresión constante complicada a un valor simple realizando los cálculos necesarios en tiempo de compilación. Esto es especialmente importante en las definiciones de arrays:
char buf[bufsize];
Puede usar const con todos los tipos predefinidos (char, int, float y double) y sus variantes (así como con objetos de clase, como verá más adelante en este capítulo). Debido a los errores sutiles que puede introducir el preprocesador, deberá utilizar siempre const en lugar de #define para la sustitución de valores.
Para poder usar const en lugar de #define, debe ser posible colocar las definiciones const en los ficheros de cabecera como se hacía con los #define. De este modo, puede colocar la definición de una constante en un único lugar y distribuirla incluyendo el fichero de cabecera en las unidades de traducción que la necesiten. Una constante en C++ utiliza por defecto enlazado interno, es decir, es visible sólo desde el fichero donde se define y no puede verse en tiempo de enlazado desde otras unidades de traducción. Deberá asignar siempre un valor a las constantes cuando las defina, excepto cuando explícitamente use la declaración extern:
extern const int bufsize;
Normalmente el compilador de C++ evita la asignación de memoria para las constantes y, en su lugar, guarda la definición en su tabla de símbolos. Cuando se utiliza extern con una constante, se fuerza el alojamiento en memoria (esto también ocurre en otros casos, como cuando se solicita la dirección de una constante). Es necesario reservar memoria porque extern dice «usa enlazado externo», es decir, que varias unidades de traducción deben ser capaces de hacer referencia al elemento, algo que requiere su almacenamiento en memoria.
Por lo general, cuando extern no forma parte de la definición, no se pide memoria. Cuando la constante se utiliza, simplemente se incorpora en tiempo de compilación.
El objetivo de no almacenar en memoria las constantes tampoco se cumple con estructuras complicadas. Cuando el compilador se ve obligado a pedir memoria no puede realizar plegado de constantes (ya que el compilador no tiene forma de conocer con seguridad qué valor debe almacenar; si lo conociese, no necesitaría pedir memoria).
Como el compilador no siempre puede impedir el almacenamiento para una constante, las definiciones de constantes deben utilizar por defecto enlazado interno, es decir, se enlazan sólo dentro de la unidad de traducción en que se definen. En caso contrario, se producirían errores de enlace con las constantes complicadas, ya que causarían petición de almacenamiento en varios ficheros cpp. Entonces, el enlazador vería la misma definición en múltiples ficheros objeto, lo que causaría un error en el enlace. Como las constantes utilizan por defecto enlazado interno, el enlazador no intenta enlazar esas definiciones entre unidades de traducción, y así no hay colisiones. Con los tipos predefinidos, que son los que se ven involucrados en la mayoría de los casos de expresiones constantes, el compilador siempre puede realizar plegado de constantes.
El uso de las constantes no se limita a sustituir los #define en expresiones constantes. Si inicializa una variable con un valor que se produce en tiempo de ejecución y sabe que no cambiará durante la vida de la variable, es una buena práctica de programación hacerla constante para que de ese modo el compilador produzca un mensaje de error si accidentalmente alguien intenta modificar dicha variable. Aquí hay un ejemplo:
//: C08:Safecons.cpp // Using const for safety #include <iostream> using namespace std; const int i = 100; // Typical constant const int j = i + 10; // Value from const expr long address = (long)&j; // Forces storage char buf[j + 10]; // Still a const expression int main() { cout << "type a character & CR:"; const char c = cin.get(); // Can't change const char c2 = c + 'a'; cout << c2; // ... } ///:~
Listado 8.1. C08/Safecons.cpp
Puede ver que i es una constante en tiempo de compilación, pero j se calcula a partir de i. Sin embargo, como i es una constante, el valor calculado para j sigue procediendo de una expresión constante y es en sí mismo otra constante en tiempo de compilación. En la siguiente línea se necesita la dirección de j y por lo tanto el compilador se ve obligado a pedir almacenamiento para j. Aun así, eso no impide el uso de j para determinar el tamaño de buf porque el compilador sabe que j es una constante y que su valor es válido aunque en algún punto del programa se haya asignado almacenamiento para guardarlo.
En main(), aparece un tipo diferente de constante en el identificador c, porque el valor no puede ser conocido en tiempo de compilación. Eso significa que se requiere almacenamiento, y por eso el compilador no intenta mantener nada en la tabla de símbolos (el mismo comportamiento que en C). La inicialización debe ocurrir, aun así, en el punto de la definición, y una vez que ocurre la inicialización, el valor ya no puede ser cambiado. Puede ver que c2 se calcula a partir de c y además las reglas de ámbito funcionan para las constantes igual que para cualquier otro tipo, otra ventaja respecto al uso de #define.
En la práctica, si piensa que un valor no debería cambiar, debería hacer que fuese una constante. Esto no sólo da seguridad contra cambios inadvertidos, sino que también permite al compilador generar código más eficiente ahorrando espacio de almacenamiento y lecturas de memoria en la ejecución del programa.
Es posible usar constantes para los agregados, pero puede estar prácticamente seguro de que el compilador no será lo suficientemente sofisticado para mantener un agregado en la tabla de símbolos, así que le asignará espacio de almacenamiento. En estas situaciones, const significa «un conjunto de datos en memoria que no pueden modificarse». En cualquier caso, sus valores no pueden usarse en tiempo de compilación porque el compilador no está obligado a conocer en ese momento el contenido de esa memoria. En el código siguiente puede ver las sentencias ilegales:
//: C08:Constag.cpp // Constants and aggregates const int i[] = { 1, 2, 3, 4 }; //! float f[i[3]]; // Illegal struct S { int i, j; }; const S s[] = { { 1, 2 }, { 3, 4 } }; //! double d[s[1].j]; // Illegal int main() {} ///:~
Listado 8.2. C08/Constag.cpp
En la definición de un array, el compilador debe ser capaz de generar código que mueva el puntero de pila para dar cabida al array. En las definiciones incorrectas anteriores, el compilador se queja porque no puede encontrar una expresión constante en la definición del tamaño del array.
Las constantes se introdujeron en las primeras versiones de C++ mientras aún se estaba terminando la especificación del C estándar. Aunque el comité a cargo de C decidió entonces incluir const en C, por alguna razón, vino a significar para ellos «una variable ordinaria que no puede cambiarse». En C, una constante siempre ocupa espacio de almacenamiento y su ámbito es global. El compilador C no puede tratar const como una constante en tiempo de compilación. En C, si escribe:
const int bufsize = 100; char buf[bufsize];
aparecerá un error, aunque parezca algo razonable. Como bufsize está guardado en algún sitio, el compilador de C no puede conocer su valor en tiempo de compilación. Opcionalmente puede escribir:
const int bufsize;
en C, pero no en C++, y el compilador C lo acepta como una declaración que indica que el almacenamiento se ha reservado en otra parte. Como C utiliza por defecto enlazado externo para las constantes, esa semántica tiene sentido. C++ utiliza normalmente enlazado interno, así que, si quiere hacer lo mismo en C++, debe indicar expresamente que se use enlazado externo usando extern.
extern const int bufsize; // es declaración, no definición
Esta declaración también es válida en C.
En C++, const no implica necesariamente almacenamiento. En C, las constantes siempre necesitan almacenamiento. El hecho de que se necesite almacenamiento o no depende de cómo se use la constante. En general, si una constante se usa simplemente para reemplazar un nombre por un valor (como haría con #define), entonces no requiere almacenamiento. Si no se crea almacenamiento (algo que depende de la complejidad del tipo de dato y de la sofisticación del compilador), los valores pueden expandirse en el código para conseguir mayor eficiencia después de la comprobación de los tipos, no antes, como ocurre con #define. Sin embargo, si se toma la dirección de una constante (incluso sin saberlo, al pasarla a una función que recibe un argumento por referencia) o se define como extern, entonces se crea almacenamiento para la constante.
En C++, una constante que esté definida fuera de todas las funciones tiene ámbito de fichero (es decir, es inaccesible fuera del fichero). Esto significa que usa por defecto enlazado interno. Esto es muy diferente del resto de identificadores de C++ (¡y de las constantes en C!), que utilizan por defecto enlazado externo. Por eso, si declara una constante con el mismo nombre en dos ficheros diferentes y no toma su dirección ni define ese nombre como extern, el compilador C++ ideal no asignará almacenamiento para la constante, simplemente la expandirá en el código. Como las constantes tienen implícito el ámbito de fichero, puede ponerlas en ficheros de cabecera de C++ sin que originen conflictos en el enlace.
Dado que las constantes en C++ utilizan por defecto enlazado interno, no puede definir una constante en un fichero y referenciarla como extern desde otro. Para conseguir enlazado externo para la constante y así poder usarla desde otro fichero, debe definirla explícitamente como extern, algo así:
extern const int x = 1; // definición, no declaración
Fíjese en que, al darle un valor inicial y decir que es extern, se fuerza el almacenamiento para la constante (aunque el compilador sigue teniendo la opción de hacer el plegado de constantes en ese punto). La inicialización establece que la sentencia es una definición, no una declaración. La declaración:
extern const int x;
en C++ significa que la definición existe en algún sitio (mientras que en C no tiene por qué ocurrir así). Ahora puede ver por qué C++ requiere que las definiciones de constantes incluyan la inicialización: la inicialización diferencia una declaración de una definición (en C siempre es una definición, así que no es necesario inicializarla). Con una declaración extern const, el compilador no puede hacer plegado de la constante porque no conoce su valor.
El enfoque de C para las constantes es poco útil, y si quiere usar un valor simbólico en una expresión constante (que deba evaluarse en tiempo de compilación) casi está obligado a usar #define.