58. Expresiones constantes en C
1. Expresiones constantes enteras Ya hemos hablado de las
expresiones constantes enteras en C (ver:
50. Expresiones constantes enteras. Enumeraciones. Tipos enteros).
Una
expresión constante entera se requiere, al menos, en casos como los que siguen:
(1) Para especificar el tamaño de un campo de bits en una estructura con tales campos (es un tema que aún no hemos visto y aquí no explicaremos).
(2) Para especificar el valor de una constante de enumeración.
(3) Para especificar el tamaño de un array (en ciertos casos esto no se exige a algunos arrays).
(4) Para especificar un valor en una constante
case (dentro de un bloque
switch()).
Puede que haya otros posibles usos.
La lista es sólo informativa, y los detalles aparecerán cuando sea (o haya sido) conveniente.
2. Expresiones constantes aritméticas\( \bullet \)
Expresión constante aritmética: Será definida en forma recursiva, así:
(0) Una
expresión constante entera es también considerada una
expresión constante aritmética (1) Una
constante de punto flotante (decimal o hexadecimal [ver párrafos
constantes de punto flotante en C y
Constantes hexadecimales en
19. Números de punto flotante. Estándar C99. Constantes]).
(2) Una expresión entre paréntesis
(c), donde
c es una
expresión constante aritmética, es también una
expresión constante aritmética.
(5) Un
cast explícito (T) c donde
T es un
tipo aritmético, y donde
c es una
expresión constante aritmética, es también una
expresión constante aritmética.
Notemos que no se admiten aquí casts desde tipos escalares no aritméticos, ni tipos derivados, etc.
(5') Un
cast explícito dentro de una operación hecha con
sizeof, cuyo resultado es una expresión constante entera.
(6) Una
operación unaria ~c,
!c,
+c,
-c, donde
c es una
expresión constante aritmética, también es una
expresión constante aritmética.
(7) Una
operación binaria a*b,
a/b,
a%b, donde
a y
b son
expresiones constantes aritméticas, es también una
expresión constante aritmética.
(8) Una
operación binaria a+b,
a-b, donde
a y
b son
expresiones constantes aritméticas, es también una
expresión constante aritmética.
(9) Una
operación de bits a<<b,
a>>b, donde
a y
b son
expresiones constantes aritméticas, es también una
expresión constante aritmética.
(10) Una
operación binaria a<b,
a>b,
a<=b,
a>=b, donde
a y
b son
expresiones constantes aritméticas, es una
expresión constante aritmética.
(11) Una
operación binaria a==b,
a!=b, donde
a y
b son
expresiones constantes aritméticas, es una
expresión constante aritmética.
(12) Una
operación de bits a&b, donde
a y
b son
expresiones constantes aritméticas, es también una
expresión constante aritmética.
(13) Una
operación de bits a^b, donde
a y
b son
expresiones constantes aritmética, es también una
expresión constante aritmética.
(14) Una
operación de bits a|b, donde
a y
b son
expresiones constantes aritmética, es también una
expresión constante aritmética.
(15) Una
operación binaria a&&b, donde
a y
b son
expresiones constantes aritmética, es también una
expresión constante aritmética.
(16) Una
operación binaria a||b, donde
a y
b son
expresiones constantes aritmética, es también una
expresión constante aritmética.
(17) Una
operación ternaria q?a:b, donde
q,
a y
b son
expresiones constantes aritmética, es también una
expresión constante aritmética.
Dada una
expresión constante aritmética, decimos que es una
expresión constante de punto flotante si alguno de los operandos es de punto flotante, y no se trata de una
expresión constante entera.
Para entender bien esto, tenemos que revisar la definición de
expresión constante entera, en la cual puede haber operandos no enteros en algunos casos (por ejemplo, son
sizeof, o un cast de una constante literal de punto flotante a un tipo entero).
Una observación importante es que no pueden haber operandos que sean punteros (ni siquiera constantes), así como tampoco constantes de otros tipos.
Tanto en el caso de
expresiones constantes enteras como en el de
expresiones aritméticas constantes vemos algo de discusión en torno a los casts explícitos y
sizeof (ver (5) y (5')).
Mi opinión (y la de otros también) es que el estándar no es exacto al último detalle en la definición de lo que se supone que es una
expresión constante entera y una
expresión constante aritmética.
El estándar afirma que, salvo en el caso de
VLA (
variable length arrays, o sea: arrays de longitud variable, que aún no hemos explicado), el operador
sizeof arroja como resultado un valor entero que es conocido en la etapa de compilación, y se puede considerar una
constante (en un sentido más amplio que explicamos abajo), que además es entera.
Podemos decir, sin temor a equivocarnos, que el resultado de
sizeof es, en esos casos, una
expresión constante entera.
Las dudas vienen cuando
sizeof es también aplicado a una expresión muy complicada, que puede tener variables de todo tipo.
El estándar establece que, el operador
sizeof no evalúa la expresión a la que afecta, y que sólo "se fija" en los tipos de datos involucrados en la expresión.
Salvo para
VLAs, el tamaño de cualquier tipo de datos (que no esté incompleto), o del tipo de datos de cualquier expresión, puede saberse en la etapa de compilación.
Por esa razón se le considera una
expresión constante entera.
No discutiremos aquí una variedad de sutilezas que sólo desvirtuarían la exposición.
Importante: Las constantes aritméticas que involucran operandos de punto flotante se tienen que evaluar durante la etapa de compilación.
No obstante, los valores de punto flotante pueden tener un comportamiento distinto en el
entorno de traducción que en el
entorno de ejecución.
Para prevenirse de posibles problemas al respecto, el estándar
C establece convenciones de con cuánta precisión, y de qué manera se tienen que evaluar este tipo de constantes.
(*) Si una expresión de punto flotante se evalúa en el
entorno de traducción, tanto la
precisión como el
rango será al menos tan grade como si la expresión fuera evaluada en el
entorno de ejecución.
Esta norma implica que se conocen de antemano algunos detalles del método de evaluación, redondeo y otras minucias.
Por ejemplo, se tiene que conocer de antemano el valor de la macro
FLT_EVAL_METHOD.
El método de redondeo es
to-nearest.
3. Constantes de dirección Las
constantes de dirección (o de referencia) se definen mediante las siguientes especificaciones:
(1) Un
puntero nulo es una
constante de dirección.
(2) Un puntero a un
lvalue que designa un objeto de duración
static.
(3) Un puntero a un designador de función.
Los casos (2) y (3) merecen más explicación, y no es tema de esta sección, sino de secciones futuras.
Observemos que no se habla de "constantes escalares", porque las constantes aritméticas no se incluyen en la lista.
Este tipo de constantes no necesariamente tienen un valor predecible en la etapa de compilación.
(4) Este tipo de constantes se debe obtener mediante el operador de dirección
& (es decir, tomando la dirección de un objeto mediante ese operador);
(5) o bien convirtiendo una
constante entera (un literal entero) a un puntero mediante un cast explícito;
(6) o implícitamente por el uso de un expresión de tipo array o de tipo función.
El caso (4) se refiere a los puntos (2) y (3), cuando sea aplicable.
El caso (5) es llamativo, pues nos permite especificar una dirección explicita de la memoria RAM de la computadora. Esto en general no tiene mucho sentido, porque los sistemas operativos en los que nuestros programas correrán son quienes controlan las direcciones de memoria RAM, y a veces prohíben el acceso a determindas direcciones.
Pero eso no tiene nada que ver con la sintaxis del lenguaje: el punto (5) es una manera más de definir una
constate de dirección.
Su uso tiene sentido en contextos
free-standing, vale decir, donde seguramente no hay un sistema operativo como
Windows o
Linux. Por ejemplo, en la programación de micros, o en cualesquiera casos donde sea posible acceder a determinadas zonas de memoria que se conocen predeterminadas por alguna razón (dispositivos con una memoria ROM ó
Read Only Memory).
El punto (6) se refiere a que el resultado de la expresión tiene tipo de datos de un array o de una función.
(7) Para crear la
constante de dirección pueden usarse expresiones involucrando los operadores que hemos visto para las
expresiones constante enteras y aritméticas, pero también pueden usarse los siguientes:
[],
.,
->,
&,
*, y casts explícitos a tipos puntero.
Sin embargo, en ningún caso se debe acceder (ni siquiera con truquitos) al valor del objeto mediante aprovechando combinaciones de estos operadores.
El índice pasado a
[] tendrá que ser una
expresión constante entera para que todo funcione correctamente.
Se observa que no están permitidas
constantes de dirección apuntando a la dirección de estructuras o uniones, pero sí está permitido apuntar a la dirección de un puntero a una estructura o unión.
4. Expresiones constantes en/para inicializadores Una
expresión constante en/para inicializadores es una de las siguientes:
(1) Una
expresión aritmética constante.
(2) Una constante puntero nulo.
(3) Una constante de dirección.
(4) Una constante de dirección para un tipo de objeto (o sea, no apunta a un tipo "función") más o menos una expresión constante entera.
Esto último puede expresarse como
A + N donde
A denota la constante de dirección (definida en párrafo 4), y
N denota una expresión constante entera.
Como su nombre lo indica, este tipo de constantes son admitidas cuando se inicializa una variable. Aún no hemos tratado este tema...
5. Expresiones constantes Una
expresión constante es una expresión que involucra operadores y operandos aritméticos tales que:
(0) La expresión dada contiene los operadores listados, por ejemplo, en los puntos (1) a (15) de la Sección 50, en el párrafo dedicado a las
expresiones constantes enteras.
(1) Ninguno de los operadores es de asignación, incremento (
++), decremento (
--), operador coma (
,), o una llamada a una función.
Se exceptúa el caso de
subexpresiones que por alguna razón nunca serían evaluadas.
(2) Es posible determinar su valor en la etapa de compilación.
Esto quiere decir que el compilador es capaz de determinar el valor de la expresión, y ponerlo directamente en la versión ejecutable del programa.
Que "sea capaz" no quiere decir que efectivamente lo haga. El compilador puede negarse a hacerlo.
Pero "ser capaz" de esto es lo que determina qué tipo de expresiones son las consideradas constantes.
Para cumplir un requisito como éste, ninguno de los operandos de la expresión, idealmente, tendría que ser un objeto o una llamada a función.
La excepción serían algunas de las expresiones afectadas por el operador
sizeof (sólo aquellas que permiten anticipar el tamaño de un
objeto durante la etapa de compilación),
o ser parte de una subexpresión que nunca es evaluada.
(Estas misteriosas expresiones nunca evaluadas son las afectadas por
sizeof y también algunos casos de "corto-circuito" en expresiones booleanas, asunto que analizaremos en posts futuros).
(3) Las expresiones constantes pueden usarse en cualquier sitio del programa donde una constante (literal) podría usarse.
(4) Una
expresión constante tiene un valor, calculado a partir de sus operandos y operadores, que ha de ajustarse al tipo de datos resultante de aplicar dichas operaciones (este tema lo veremos en futuras secciones).
Este valor debe estar en el
rango de valores asociado al tipo de datos de la expresión.
Las constantes literales, obviamente, son un caso especial de
expresión constante.
Las
expresiones constantes enteras son un caso especial de
expresiones constantes aritméticas.
A su vez las
expresiones aritméticas son un caso especial de
expresiones constantes.
En la Sección 50 nosotros nos hemos restringido a ciertos operadores específicos para construir las
expresiones constantes enteras.
Los mismos operadores hemos usado aquí para construir las
expresiones constantes aritméticas.
Hemos descartado los operadores unarios:
& * . ->, el operador binario
[] y las expresiones del tipo
literal compuesto (aún no hemos tratado este tema).
Se puede demostrar que estos operadores no pueden estar en las
expresiones constantes enteras.
El operador
& requeriría, si fuera aplicable, crearía una referencia, es decir, un valor de tipo
puntero a, pero sólo se permiten operandos enteros en una
expresión constante entera.
El operador
* requeriría aplicarse a un objeto, el cual tiene una dirección de memoria asociada. Esta información no puede saberse en la etapa de compilación.
Los operadores
. y
-> requieren ser aplicados a objetos tipo estructura, o bien a referencias (que apuntan a estructuras). En cualquier caso, el operando al que se aplicarían no sería un valor entero.
El operador binario
[] se usa con arrays, y requeriría entonces que uno de sus operandos sea de tipo
array que, de nuevo, no es un entero.
(El único caso en que se permite un operando no entero es un valor de punto flotante inmediatamente precedido por un cast a tipo entero).
De este modo, la definición dada por el estándar coincide con la nuestra.
Lo mismo se puede hacer para las
expresiones constantes aritméticas.
Dado que los
objetos requieren una posición en memoria determinada durante la ejecución del programa, no es completamente determinable en la etapa de compilación.
Por lo tanto, las variables y cualesquiera otros objetos no pueden aparecer en una
expresión constante.
Un ejemplo poco intuitivo de esto son las
cadenas literales. Ellas son "constantes" de tipo
array de caracteres, por definición, pero tienen el inconveniente de que un
array no es sólo un valor, sino también un
objeto.
Esto implica que tiene asociada una dirección de memoria, y es por eso que pueden, por ejemplo, aparecer en expresiones que requieren un
puntero a char.
O sea que las
cadenas literales no se pueden considerar como una
expresión constante.
Existe un tipo de expresión en
C99 llamada
literal compuesto que crea un objeto en memoria de un determinado tipo.
Aunque en general este objeto se inicializa con valores constantes, el hecho de que sea un
objeto en memoria hace que no pueda formar parte de una
expresión constante.
Las expresiones constantes son una subclase de las expresiones constantes en/para inicializadores.
Las constantes de dirección no son una subclase de las expresiones constantes.
6. Expresiones constantes enteras para inclusión condicional Todavía existe una clase de
expresiones constantes aún más restringidas, que son una subclase de las
expresiones constantes enteras,
las cuales serían idóneas para usar con la directiva
#if.
Aquí no explicaremos los detalles de
#if, sino que sólo detallaremos el tipo de
expresiones constantes que admite.
(0) Una
expresión constante entera para inclusión condicional es, sintácticamente, una
expresión constante entera, sujeta a estas restricciones:
(1) No puede haber operadores
cast explícitos.
En particular, no pueden aparecer constantes de punto flotante, porque en las
expresiones constantes enteras éstas sólo se aceptaban precedidas por un cast explícito a un tipo entero.
(2) No puede haber constantes de enumeración. (O sea, declaradas con
enum).
Más generalmente, no puede haber identificadores, salvo aquellos que designan
macros.
Este requerimiento obedece a que
#if es una directiva analizada por el preprocesador en una etapa temprana de las acciones del compilador.
Se procesa dicha directiva en una etapa en la que se sólo es posible saber si un identificador denota una
macro o no.
Luego, otro tipo de identificadores, directamente no se sabe lo que son.
(3) No puede aparecer el operador
sizeof.
El problema es que
sizeof es una
palabra clave del lenguaje, y las
palabras clave no son interpretadas por el preprocesador en etapas demasiado tempranas.
Sin embargo, la palabra "sizeof" sí que puede aparecer en una declaración
#if, pero será considerada un identificador que no ha sido asociado a macro alguna, y por lo tanto tenrá un valor constante de 0.
7. Conclusión. Las
expresiones constantes de distintos tipos son requeridas en ciertos contextos.
En general hacen se trata de valores que no cambian a lo largo de un programa, y que pueden determinarse en la etapa de compilación.
Aquí conviene reflexionar un poco sobre las consecuencias de predeterminar valores en la etapa de compilación.
Puede ocurrir que el cálculo del valor de ciertas constantes sea distinta en el
entorno de traducción que en el
entorno de ejecución.
Por ejemplo, esto podría pasar con los valores de punto flotante, o el rango de los valores enteros.
Sin embargo, el estándar
C99 obliga a los compiladores a trabajar de forma coherente, asegurando que las evaluaciones de
expresiones constantes en el
entorno de traducción se comporten de igual modo que en el
entorno de ejecución.
O sea que los valores obtenidos en la etapa de compilación serán los mismos que los del programa ejecutable.
Puede haber excepciones, pero en ese caso lo que ocurre es que en la etapa de compilación se realizan cálculos más precisos, de manera que no se pierda información o precisión al llevar el valor resultanto a la versión ya ejecutable.
En resumen, no habrá sorpresas al utilizar constantes pues los valores obtenidos con expresiones constantes serán transparentes al programa en ejecución, comportándose como si hubieran sido calculados allí.
Pero todavía queda algo en el tintero: las
constantes de caracter.
Como hemos discutido asiduamente en varios posts anteriores, en que estudiamos la codificación de caracteres y sus vicisitudes, bien puede ocurrir que la codificación de caracteres en el
entorno de traducción sea distinta al del
entorno de ejecución.
Así, una constante de caracter como
'A', ¿qué valor tendría?
Supongamos que la compilación se hace en un entorno con
EBCDIC, y que el programa ejecuta en un entorno con
ASCII.
El código de la letra A en
EBCDIC es 193, mientras que en
ASCII es 65.
Según el estándar
C99, los valores numéricos de las
constantes de caracter en el
entorno de traducción se transforman en "otro" valor en el
entorno de ejecución, según una fórmula definida por el compilador.
Si bien no es demasiado claro en el documento estándar, cada caracter se transforma en su equivalente. O sea que nuestro ejemplo de 193 se transformaría en 65.
Por lo tanto, podemos esperar que las
constantes de caracter, que adquieren un valor entero, al formar parte de una
expresión constante entera, van a tener el valor que tendrían al ejecutar el programa. Así que 'A' valdría 65.
Pero esta transformación ocurre en una etapa algo tardía de las etapas del preprocesador.
La etapa que procesa las declaraciones
#if es anterior, y entonces, si en una tal declaración tenemos una
expresión constante enterna para inclusión condicional, no podemos asegurar que el valor de 'A' sea 65.
Podría ser que, en tal caso, mantenga el valor del código de A en el
entorno de traducción, o sea 193.
La moralaja es que 'A' puede tener dos valores distintos como expresión constante, en contextos distintos de nuestro programa. Y una observación más. Esto que acabamos de observar no necesariamente implica que, en nuestro ejemplo, tengamos obligadamente los dos valores distintos de 193 y 65 para 'A'.
En lo que se refiere a las
cinclusiones condicionales #if nunca es claro qué camino va a tomar el compilador:
(a) ¿Elegirá darle a 'A' el mismo valor que tendría en el
entorno de ejecución? (nuestro 65).
(b) ¿Elegirá darle a 'A' el valor del código de la letra A en el
entorno de traducción? (nuestro 193).
(c) ¿Elegirá darle a 'A' algún otro valor, basado en quién sabe qué caprichos? (qué sé yo, ¿un 49 te va?)
El estándar
C99 nada especifica al respecto, y esto nos obliga a ser muy cuidadosos en el modo en que usamos
expresiones constantes enteras que involucren constantes de caracter.
Si las usamos dentro de
inclusiones condicionales #if podemos tener valores diferentes a los que tendrían en otros contextos.
Si no, al menos tenemos la certeza de que tendrán siempre un comportamiento bien definido, y determinado por los valores de los códigos de los caracteres en el
entorno de ejecución.
Pero esto no nos produce valores siempre predecibles, porque no son valores universales, sino que dependen de qué
entorno de ejecución tengamos.
(Nota: El
entorno de ejecución no es exactamente el sistema operativo en el que se nos ocurre ejecutar nuestro programa, sino más bien el sistema en el que el compilador cree que será ejecutado nuestro programa. Esta información se la debemos proveer en el momento de la compilación de nuestro programa, o sea, avisarle para qué sistema queremos que compile).
OrganizaciónComentarios y Consultas