Tabla de contenidos
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.
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.
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.
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.
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.
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
.