8: Constantes

Tabla de contenidos

8.1. Sustitución de valores
8.2. Punteros
8.3. Argumentos de funciones y valores de retorno
8.4. Clases
8.5. Volatile
8.6. Resumen
8.7. Ejercicios

El concepto de constante (expresión 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.

Desde su origen, const ha sido utilizada para diferentes propósitos. Mientras tanto FIXME:it trickled back en el lenguaje C en el que su significado cambió. Todo esto puede parecer un poco confuso al principio, y en este capítulo aprenderá cuándo, porqué y cómo usar la palabra reservada const. Hacia el final se expone una disertación sobre volatile, que es familia de const (ambos se refieren a los cambios) y su sintaxis es 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.

8.1. Sustitución de valores

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 pequeños problemas que se pueden evitar usando valores constantes.

El uso más frecuente del preprocesador es la sustitución de valores por nombres, en C 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 que lo utilicen. Es muy importante para el mantenimiento del código el uso de sustitución de valores en lugar de los también 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 si no que complicará innecesariamente la edición del código si necesita cambiar dicho valor.

La mayor parte del tiempo, BUFSIZE se comportará como un valor ordinario, pero no siempre. No tiene información de tipo. Eso puede esconder errores 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 propagación de constantes[61], 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 vectores:

char buf[bufsize];

Puede usar const con todos los tipos básicos(char, int, float y double) y sus variantes (así como clases y todo lo que verá después en este capítulo). Debido a los problemas que introduce el preprocesador deberá utilizar siempre const en lugar de #define para la sustitución de valores.

8.1.1. const en archivos de cabecera

Para poder usar const en lugar de #define, debe ser posible colocar las definiciones const en los archivos 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 archivo de cabecera en las unidades del programa que la necesiten. Una constante en C++ utiliza enlazado interno, es decir, es visible sólo desde el archivo donde se define y no puede verse en tiempo de enlazado por otros módulos. 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, pero en su lugar ocupa una entrada en la 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). El uso de la memoria debe hacerse porque extern dice «usa enlazado externo», es decir, que varios módulos 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 propagación de constantes (ya que el compilador no tiene forma de conocer con seguridad que 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 utilizan enlace interno, es decir, se enlazan sólo con el módulo en que se definen. En caso contrario, los errores de enlace podrían ocurrir con las expresiones constantes complicadas ya que causarían petición de almacenamiento en diferentes módulos. Entonces, el enlazador vería la misma definición en múltiples archivos objeto, lo que causaría un error en el enlace. Como las constantes utilizan enlace interno, el enlazador no intenta enlazar esas definiciones a través de los módulos, y así no hay colisiones. Con los tipos básicos, que son los se ven involucrados en la mayoría de los casos, el compilador siempre realiza propagación de constantes.

8.1.2. constantes seguras

El uso de las constantes no está limitado a la sustitución de los #define por 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 es una expresión constante y es en si 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. Ni siquiera eso 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 se asigne almacenamiento, ya que eso se hace para mantener el valor en algún punto en el programa.

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, aún 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 una variable no debería cambiar, debería hacer que fuese una constante. Esto no sólo da seguridad contra cambios inadvertidos, 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.

8.1.3. Vectores

Es posible usar constantes para los vectores, pero prácticamente está dando por hecho que el compilador no será lo suficientemente sofisticado para mantener un vector 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 puede usarse en tiempo de compilación porque el compilador no conoce en ese momento los contenidos de las variables que tienen espacio asignado. En el código siguiente puede ver algunas declaraciones incorrectas.

//: 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 vector, el compilador debe ser capaz de generar código que mueva el puntero de pila para dar cabida al vector. 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 vector.

8.1.4. Diferencias con C

Las constantes se introdujeron en las primeras versiones de C++ mientras la especificación del estándar C estaba siendo terminada. 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. bufsize está guardado en algún sitio y el compilador no conoce 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 se almacenará en alguna parte. Como C utiliza enlace externo para las constantes, esa semántica tiene sentido. C++ utiliza normalmente enlace interno, así que, si quiere hacer lo mismo en C++, debe indicar expresamente que se use enlace 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 número por un nombre (como hace #define), entonces no requiere almacenamiento. Si es así (algo que depende de la complejidad del tipo de dato y de lo 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 como con #define. Si de todas formas, se necesita la dirección de una constante (aún desconocida, para pasarla a una función como argumento por referencia) o se declara como extern, entonces se requiere asignar almacenamiento para la constante.

En C++, una constante que esté definida fuera de todas las funciones tiene ámbito de archivo (es decir, es inaccesible fuera del archivo). Esto significa que usa enlace interno. Esto es diferente para el resto de identificadores en C++ (y que las constantes en C) que utilizan siempre enlace externo. Por eso, si declara una constante con el mismo nombre en dos archivos diferentes y no toma sus direcciones ni los define 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 a su archivo, puede ponerlas en un archivo de cabecera de C++ sin que origine conflictos en el enlace.

Dado que las constante en C++ utilizan por defecto enlace interno, no puede definir una constante en un archivo y utilizarla desde otro. Para conseguir enlace externo para la constante y así poder usarla desde otro archivo, debe definirla explícitamente como extern, algo así:

extern const int x = 1;  // definición, no declaración

Señalar que dado un identificador, si se dice que es extern, se fuerza el almacenamiento para la constante (aunque el compilador tenga la opción de hacer la expansión 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 porqué ocurrir así). Ahora puede ver porqué 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, aunque no esté inicializada). Con una declaración const extern, el compilador no hace expansión de la constante porque no conoce su valor.

La aproximación de C a 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.



[61] N. del T.: del inglés constant folding