7 Normas para la notación

Que no te asuste el título. Lo que aquí trataremos es más simple de lo que parece.

En este capítulo hablaremos sobre las reglas que rigen cómo se escriben las constantes en C++ según diversos sistemas de numeración y el uso tiene cada uno.

Constantes int

En C++ se usan tres tipos de numeración para la definición de constantes numéricas: la decimal, la octal y la hexadecimal; según se use la numeración en base 10, 8 ó 16, respectivamente.

Por ejemplo, el número 127, se representará en notación decimal como 127, en octal como 0177 y en hexadecimal como 0x7f.

Cualquier cantidad puede expresarse usando números de diferentes bases. Generalmente, los humanos, usamos números en base 10, pero para los ordenadores es más cómodo usar números en bases que sean potencias de 2, como 2 (numeración binaria), 8 (numeración octal) ó 16, (numeración hexadecimal).

El valor de cualquier cantidad se puede calcular mediante la siguiente fórmula:

N = Σ(di * basei)

Donde di es cada uno de los dígitos, empezando con i=0 para el dígito más a la derecha, y base es el valor de la base de numeración usada.

Por ejemplo, si el número abcd expresa una cantidad en base n, el valor del número se puede calcular mediante:

N = d*n0 + c*n1 + b*n2 + a*n3

En notación octal se necesitan sólo ocho símbolos, y se usan los dígitos del '0' al '7'. En hexadecimal, se usan 16 símbolos, los dígitos del '0' al '9', que tienen el mismo valor que en decimal; para los seis símbolos restantes se usan las letras de la 'A' a la 'F', indistintamente en mayúsculas o minúsculas. Sus valores son 10 para la 'A', 11 para la 'B', y sucesivamente, hasta 15 para la 'F'.

Según el ejemplo anterior, el valor 0177 expresa una cantidad en notación octal. En C++, el prefijo '0' indica numeración octal. Tendremos, por lo tanto, el número octal 177, que según nuestra fórmula vale:

N = 7*80 + 7*81 + 1*82 = 7*1 + 7*8 + 1*64 = 7 + 56 + 64 = 127

Análogamente, en el número 0x7f, "0x" se usa como prefijo que indica que se trata de un número en notación hexadecimal. Tenemos, por lo tanto, el número 7F. Aplicando la fórmula tenemos:

N = F*160 + 7*161 = 15*1 + 7*16 = 15 + 112 = 127

Por último, aunque parezca obvio, el número 127 estará expresado en base 10, y también podemos aplicar la fórmula:

N = 7*100 + 2*101 + 1*102 = 7*1 + 2*10 + 1*100 = 7 + 20 + 100 = 127
Nota:

Si no tienes demasiado fresco el tema de las potencias, recordaremos que cualquier número elevado a 0 es 1.

Hay que tener mucho cuidado con las constantes numéricas, en C++ no es el mismo número el 0123 que el 123, aunque pueda parecer otra cosa. El primero es un número octal y el segundo decimal.

La ventaja de la numeración hexadecimal es que los valores enteros requieren dos dígitos por cada byte para su representación. Así, un byte puede tomar valores hexadecimales entre 0x00 y 0xff, dos bytes entre 0x0000 y 0xffff, etc. Además, la conversión a binario es casi directa, cada dígito hexadecimal se puede sustituir por cuatro bits (cuatro dígitos binarios), el '0x0' por '0000', el '0x1' por '0001', hasta el '0xf', que equivale a '1111'. En el ejemplo el número 127, ó 0x7f, sería en binario '01111111'.

En la notación binaria usamos como base el 2, que es la base más pequeña que se puede usar. En este sistema de numeración sólo hay dos dígitos: 0 y 1. Por supuesto, también podemos aplicar nuestra fórmula, de modo que el número 01111111 vale:

N = 1*20 + 1*21 + 1*22 + 1*23 + 1*24 + 1*25 + 1*26 + 0*27
  = 1*1 + 1*2 + 1*4 + 1*8 + 1*16 + 1*32 + 1*64 + 0*128= 1 + 2+ 4 + 8 + 16 + 32 + 64 = 127

Con la numeración octal sucede algo similar a lo que pasa con la hexadecimal, salvo que cada dígito octal agrupa tres bits (tres dígitos binarios). Así un byte puede tomar valores octales entre 0000 y 0377, dos bytes entre 0000000 y 0177777, etc. Además, la conversión a binario también es es casi directa, ya que cada dígito octal se puede sustituir por tres bits, el '0' por '000', el '1' por '001', hasta el '7', que equivale a '111'. En el ejemplo el número 127, ó 0177, sería en binario '01111111'.

Aún no hemos hablado de los operadores de bits, pero podemos adelantar que C++ dispone de tales operadores, que básicamente realizan operaciones con números enteros bit a bit. De este modo, cuando trabajemos con operadores de bits, nos resultará mucho más sencillo escribir los valores de las constantes usando la notación hexadecimal u octal, ya que es más directa su conversión a binario.

Constantes long

Cuando introduzcamos valores constantes long debemos usar el sufijo "L", sobre todo cuando esas constantes aparezcan en expresiones condicionales, y por coherencia, también en expresiones de asignación. Por ejemplo:

long x = 123L;
if(0L == x) Imprimir("Valor nulo");

A menudo recibiremos errores del compilador cuando usemos constantes long sin añadir el sufijo L, por ejemplo:

if(1343890883 == x) Imprimir("Número long int");

Esta sentencia hará que el compilador emita un error ya que no puede usar un tamaño mayor sin una indicación explícita.

Hay casos en los que los tipos long e int tienen el mismo tamaño, en ese caso no se producirá error, pero no podemos predecir que nuestro programa se compilará en un tipo concreto de compilador o plataforma.

Constantes long long

En el caso de valores constantes long long tendremos que usar el sufijo "LL", tanto cuando esas constantes aparezcan en expresiones condicionales, como cuando lo hagan en expresiones de asignación. Por ejemplo:

long long x = 16575476522787LL;
if(1LL == x) Imprimir("Valor nulo");

A menudo recibiremos errores del compilador cuando usemos constantes long long sin añadir el sufijo LL, por ejemplo:

if(16575476522787 == x) Imprimir("Número long long");

Esta sentencia hará que el compilador emita un error ya que no puede usar un tamaño mayor sin una indicación explícita.

Constantes unsigned

Del mismo modo, cuando introduzcamos valores constantes unsigned debemos usar el sufijo "U", en las mismas situaciones que hemos indicado para las constantes long. Por ejemplo:

unsigned int x = 123U;
if(3124232U == x) Imprimir("Valor encontrado");

Constantes unsigned long

También podemos combinar en una constante los modificadores unsigned y long, en ese caso debemos usar el sufijo "UL", en las mismas situaciones que hemos indicado para las constantes long y unsigned. Por ejemplo:

unsigned long x = 123456UL;
if(3124232UL == x) Imprimir("Valor encontrado");

Constantes unsigned long long

En una constante también podemos usar los modificadores unsigned y long long, para esos casos usaremos el sufijo "ULL", en todas las situaciones que hemos indicado para las constantes long long y unsigned. Por ejemplo:

unsigned long long x = 123456534543ULL;
if(3124232ULL == x) Imprimir("Valor encontrado");

Constantes float

Del mismo modo, existe una notación especial para las constantes en punto flotante. En este caso consiste en añadir ".0" a aquellas constantes que puedan interpretarse como enteras.

Se puede usar el sufijo "f". En ese caso, se tratará de constantes en precisión sencilla, es decir float.

Por ejemplo:

float x = 0.0;
if(x <= 1.0f) x += 0.01f;

Constantes double

Por defecto, si no se usa el sufijo, el compilador tratará las constantes en precisión doble, es decir double.

Por ejemplo:

double x = 0.0;
if(x <= 1.0) x += 0.01;

Constantes long double

Si se usa el sufijo "L" se tratará de constantes en precisión máxima, es decir long double.

Por ejemplo:

long double x = 0.0L;
if(x <= 1.0L) x += 0.01L;

Constantes enteras

En general podemos combinar los prefijos "0" y "0x" con los sufijos "L", "U", y "UL".

Aunque es indiferente usar los sufijos en mayúsculas o minúsculas, es preferible usar mayúsculas, sobre todo con la "L", ya que la 'l' minúscula puede confundirse con un uno '1'.

Constantes en punto flotante

Ya hemos visto que podemos usar los sufijos "f", "L", o no usar prefijo. En este último caso, cuando la constante se pueda confundir con un entero, debemos añadir el ".0".

Para expresar constantes en punto flotante también podemos usar notación exponencial, por ejemplo:

double x = 10e4;
double y = 4.12e2;
double pi = 3.141592e0;

El formato exponencial consiste en un número, llamado mantisa, que puede ser entero o con decimales, seguido de una letra 'e' o 'E' y por último, otro número, en este caso un número entero, que es el exponente de una potencia de base 10.

Los valores anteriores equivalen a:

x = 10 x 104 = 100000
y = 4,12 x 102 = 412
pi = 3.141592 x 100 = 3.141592

Al igual que con los enteros, es indiferente usar los sufijos en mayúsculas o minúsculas, pero es preferible usar mayúsculas, sobre todo con la "L", ya que la 'l' minúscula puede confundirse con un uno '1'.

Constantes char

Las constantes de tipo char se representan entre comillas sencillas, por ejemplo 'a', '8', 'F'.

Después de pensar un rato sobre el tema, tal vez te preguntes ¿cómo se representa la constante que consiste en una comilla sencilla?. Bien, te lo voy a contar, aunque no lo hayas pensado.

Secuencias de escape

Existen ciertos caracteres, entre los que se encuentra la comilla sencilla, que no pueden ser representados con la norma general. Para eludir este problema existe un cierto mecanismo, llamado secuencias de escape. En el caso comentado, la comilla sencilla se define como '\'', y antes de que preguntes te diré que la barra descendente se define como '\\'.

Pero además de estos caracteres especiales existen otros. El código ASCII, que es el que puede ser representado por el tipo char, consta de 128 ó 256 caracteres. Y aunque el código ASCII de 128 caracteres, 7 bits, ha quedado prácticamente obsoleto, ya que no admite caracteres como la 'ñ' o la 'á'; aún se usa en ciertos equipos antiguos, en los que el octavo bit se usa como bit de paridad en las transmisiones serie. De todos modos, desde hace bastante tiempo, se ha adoptado el código ASCII de 256 caracteres, 8 bits. Recordemos que el tipo char tiene siempre un byte, es decir 8 bits, y esto no es por casualidad.

En este conjunto existen, además de los caracteres alfabéticos, en mayúsculas y minúsculas, los numéricos, los signos de puntuación y los caracteres internacionales, ciertos caracteres no imprimibles, como el retorno de línea, el avance de línea, etc.

Veremos estos caracteres y cómo se representan como secuencia de escape, en hexadecimal, el nombre ANSI y el resultado o significado.

Escape Hexad ANSI Nombre o resultado
  0x00 NULL Carácter nulo
\a 0x07 BELL Sonido de campanilla
\b 0x08 BS Retroceso
\f 0x0C FF Avance de página
\n 0x0A LF Avance de línea
\r 0x0D CR Retorno de línea
\t 0x09 HT Tabulador horizontal
\v 0x0B VT Tabulador vertical
\\ 0x5c \ Barra descendente
\' 0x27 ' Comilla sencilla
\" 0x22 " Comillas
\? 0x3F ? Interrogación
\O   cualquiera O=tres dígitos en octal
\xH   cualquiera H=número hexadecimal
\XH   cualquiera H=número hexadecimal

Los tres últimos son realmente comodines para la representación de cualquier carácter. El \O sirve para la representación en notación octal, la letra 'O' se debe sustituir por un número en octal. Para la notación octal se usan tres dígitos, recuerda que para expresar un byte los valores octales varían de 0 a 0377.

También pueden asignarse números decimales a variables de tipo char. Por ejemplo:

char A;
A = 'a';
A = 97;
A = 0x61;
A = '\x61';
A = 0141;
A = '\141';

En este ejemplo todas las asignaciones son equivalentes y válidas.

Nota: Una nota sobre el carácter nulo. Este carácter se usa en C++ para terminar las cadenas de caracteres, por lo tanto es muy útil y de frecuente uso. Para hacer referencia a él se usa frecuentemente su valor decimal, es decir char A = 0, aunque es muy probable que lo encuentres en libros o en programas como '\000', es decir en notación octal.

Sobre el carácter EOF, del inglés "End Of File", este carácter se usa en muchos ficheros como marcador de fin de fichero, sobre todo en ficheros de texto. Aunque dependiendo del sistema operativo este carácter puede cambiar, por ejemplo en MS-DOS es el carácter "0x1A", el compilador siempre lo traduce y devuelve el carácter EOF cuando un fichero se termina. El valor usado por el compilador está definido en el fichero "stdio.h", y es 0.

¿Por qué es necesaria la notación?

C++ es un lenguaje pensado para optimizar el código y conseguir un buen rendimiento por parte del ordenador cuando ejecute nuestros programas. Esto nos obliga a prestar atención a detalles de bajo nivel (cercanos al hardware), detalles que en otros lenguajes de alto nivel no se tienen en cuenta.

Esto tiene un precio: debemos estar más atentos a los detalles, pero las ventajas compensan, ya que nuestros programas son mucho más rápidos, compactos y eficientes.

En todos estos casos que hemos visto en este capítulo, especificar el tipo de las constantes tiene el mismo objetivo: evitar que se realicen conversiones de tipo durante la ejecución del programa, obligando al compilador a hacerlas durante la fase de compilación.

Esto es una optimización, ya que generalmente, los programas se ejecutan muchas más veces de las que se compilan, así que parece razonable invertir más esfuerzo en minimizar el tiempo de ejecución que el de compilación.

Si en el ejemplo anterior para float hubiéramos escrito if(x <= 1)..., el compilador almacenaría el 1 como un entero, y durante la fase de ejecución se convertirá ese entero a float para poder compararlo con x, que es float. Al poner "1.0" estamos diciendo al compilador que almacene esa constante como un valor en coma flotante, con lo cual nos evitamos la conversión de tipo cada vez que se evalúe la condición de la sentencia if. Lo mismo se aplica a las constantes long, unsigned y char.