2.2.4 Tipos básicos: representación interna, rango
§1 Sinopsis
El ANSI C reconoce que el tamaño y rangos de valor numérico de los tipos básicos
y sus varias permutaciones ( 2.2.1)
dependen de la implementación y generalmente derivan de la arquitectura del
ordenador. La tabla adjunta muestra los tamaños y rangos de los tipos
numéricos de 32-bits de Borland C++ [1].
Nota: precisamente esta circunstancia, que el Estándar C++ impone relativa libertad
en cuanto al tamaño de los tipos, es la responsable de que aún adhiriéndose estrictamente al Estándar, puedan existir problemas
de portabilidad entre diversas plataformas de los programas C++ (algo que no ocurre con otros lenguajes de definición más estricta.
Por ejemplo Java).
Ver en
2.2.4c
unas notas adicionales sobre los tipos básicos.
§2 Almacenamiento y rango
Las explicaciones siguientes muestran como se representan internamente estos tipos (en negrita los tipos básicos). Los ficheros de cabecera <climits> y <float.h> contienen definiciones de los rangos de valor de todos los tipos fundamentales.
Tipo | bits | Rango / Tipo de uso |
unsigned char | 8 | 0 <= X <= 255 Números pequeños y juego caracteres del PC. |
char (signed) | 8 |
-128 <= X <= 127 Números muy pequeños y juego de caracteres ASCII [5] |
short (signed) | 16 | -32,768 <= X <= 32,767 Números muy pequeños, control de bucles pequeños |
unsigned short | 16 |
0 <= X <= 65,535 Números muy pequeños, control de bucles pequeños |
unsigned (int) | 32 | 0 <= X <= 4,294,967,295. Números grandes |
int (signed) | 32 |
-2,147,483,648 <= X <= 2,147,483,647 Números pequeños, control de bucles |
unsigned long | 32 | 0 <= X <= 4,294,967,295 Distancias astronómicas |
enum | 32 |
-2,147,483,648 <= X <= 2,147,483,647 Conjuntos de valores ordenados |
long (int) | 32 | -2,147,483,648 <= X <= 2,147,483,647 Números grandes |
float | 32 | 1.18e-38 <= |X| <= 3.40e38 Precisión científica ( 7-dígitos) |
double | 64 | 2.23e-308 <= |X| <= 1.79e308 Precisión científica (15-dígitos) |
long double | 80 | 3.37e-4932 <= |X| <= 1.18e4932 Precisión científica (18-dígitos) |
Nota: las cuestiones de almacenamiento interno, como se
almacenan los datos en memoria y cuanto espacio necesitan, están influidas
por otros factores además de los señalados. Estas son las que podríamos denominar "necesidades mínimas" de
almacenamiento. En ocasiones, especialmente en estructuras y uniones, el compilador realiza determinados "ajustes" o
"alineaciones" con los datos, de forma que las direcciones de memoria
se ajustan a determinadas pautas. El resultado es que en estos casos, el
tamaño de por ejemplo una estructura, no corresponde con lo que
teóricamente se deduce de la suma de los miembros (
4.5.9). Las características de esta "alineación" pueden ser controladas
mediante opciones del compilador (
4.5.9a).
§3 Enteros
En C++ 32-bit, los tipos int y long son equivalentes, ambos usan 32 bits [3]. Las variedades con signo son todas almacenadas en forma de complemento a dos usando el bit más significativo como bit de signo (0 positivo y 1 negativo), lo que explica los rangos indicados en la tabla. En las versiones sin signo, se usan todos los bits, con lo que el número de posibilidades es 2n, y el rango de valores está entre 0 y 2n-1, donde n es el número de bits de la palabra del procesador, 8, 16 o 32 (uno, dos, o cuatro octetos).
El estándar ANSI C no define el tamaño de almacenamiento de los diversos tipos, solamente indica que la serie short, int y long no es descendente, es decir: short <= int <= long. De hecho, legalmente los tres tipos pueden ser del mismo tamaño.
En muchas (pero no todas) las implementaciones de C y C++ un long es mayor que un int. Actualmente, la mayoría de las aplicaciones de oficina y personales, con entornos como Windows o Linux, corren sobre plataformas hardware de 32 bits, de forma que la mayoría de los compiladores para estas plataformas utilizan un int de 32 bits (del mismo tamaño que el long).
En cualquier caso, los rangos vienen indicados por las constantes que se señalan (incluidas en <limits.h>):
signed short: SHRT_MIN <= X <= SHRT_MAX.
Siendo: SHRT_MIN <= -32767 y SHRT_MAX >= 32767. Algunas implementaciones hacen SHRT_MIN = -32768 pero no es exigido por el estándar.
unsigned short: 0 <= X <= USHRT_MAX.
Siendo: USHRT_MAX >= 65535. Las variedades short deben contener al menos 16 bits para que pueda cubrirse el rango de valores exigidos.
En la mayoría de los compiladores un short es menor que un int, de forma que algunos programas que deben almacenar grandes matrices de números en memoria o en ficheros pueden economizar espacio utilizando short en lugar de int, pero siempre que se cumplan dos condiciones:
1. En la implementación un short es realmente menor que un int.
2.- Los valores caben en un short.
En algunas arquitecturas el código empleado para manejar los short es más largo y lento que el correspondiente para los int. Esto es particularmente cierto en los procesadores Intel x86 ejecutando código de 32 bits en programas para Windows (NT/95/98), Linux y otras versiones Unix. En estos códigos, cada instrucción que referencia a un short es un byte más larga y generalmente necesita tiempo extra de procesador para ejecutarse.
signed int:
INT_MIN <= X <= INT_MAX.
Siendo: INT_MIN <= -32767 y INT_MAX >= 32767. Algunas implementaciones utilizan un valor INT_MIN = -32768 pero no es exigido en el estándar.
unsigned int:
0 <= X <= UINT_MAX.
Siendo: UINT_MAX >= 65535. Para cubrir esta exigencia, los int deben ser de 16 bits por lo menos.
El rango exigido para signed int y unsigned int es idéntico que
para los signed short y unsigned short. En compiladores
para procesadores de 8 y 16 bits (incluyendo los Intel x86 ejecutando código
en modo 16 bits, como bajo MS DOS), normalmente un int es de 16 bits,
exactamente igual que un short. En los compiladores para
procesadores de 32 bit y mayores (incluyendo los Intel x86 ejecutando código
de 32 bits como Windows o Linux) generalmente un int es de 32 bits, exactamente igual que un long.
signed long:
LONG_MIN <= X <= LONG_MAX.
Siendo: LONG_MIN <= -2147483647 y LONG_MAX >= 2147483647. Existen implementaciones que hacen LONG_MIN = -2147483648 pero no es exigido por el estándar.
unsigned long:
0 <= X <= ULONG_MAX.
Siendo: ULONG_MAX >= 4294967295. Para poder cubrir este rango, los tipos long deben ser de al menos 32 bits.
§4 Nuevos tipos numéricos
Los rangos previstos para los nuevos tipos (
3.2.3d) long long. que se proyectan incluir en el estándar son:
signed long long: LLONG_MIN
<= X <= LLONG_MAX.
Siendo: LLONG_MIN <= -9223372036854775807 y LLONG_MAX >= 9223372036854775807. Algunas implementaciones hacen LLONG_MIN = -9223372036854775808 pero no es exigido.
unsigned long long:
0 <= X <= ULLONG_MAX.
Siendo: ULLONG_MAX >= 18446744073709551615. Las variedades long deben ser de al menos 64 bits para poder cubrir el rango exigido.
La diferencia entre enteros con signo y sin signo (signed y unsigned) es que en los primeros el bit más significativo se usa para guardar el signo (0 positivo, 1 negativo), esto hace que los enteros con signo tengan un rango de valores posibles distinto que los unsigned. Véase al respecto el rango de int y unsigned int.
Los enteros sin signo se mantienen en valores 0 ó positivos, dentro de la aritmética de numeración de módulo base 2, es decir 2n, donde n es el número de bits de almacenamiento del tipo, de forma que, por ejemplo, si un int se almacena en 32 bits, unsigned int tiene un rango entre 0 y 232 -1 = 4,294,967,295 (el valor 0 ocupa una posición de las 4.294.967.295 posibles).
§5 Carácter
La cabecera <limits.h> contiene una macro, CHAR_BIT, que se expande a una constante entera que indica el número de bits de un tipo carácter (char), que se almacenan en 1 byte, es decir, siempre ocurre que sizeof(char) == 1. Esta es la definición ANSI de byte en C/C++, es decir, la memoria requerida para almacenar un carácter, pero este byte no corresponde necesariamente con el byte de la máquina.
El valor de CHAR_BIT es al menos 8; la mayoría de los ordenadores
modernos usan bytes de 8 bits (octetos), pero existen algunos con otros tamaños, por ejemplo 9 bits. Además
algunos procesadores, especialmente de señal (Digital Signal Processors),
que no pueden acceder de forma eficiente a la memoria en tamaños menores que
la palabra del preprocesador, tienen un CHAR_BIT distinto, por ejemplo
24. En estos casos, los tipos char, short e int son todos
de 24 bits, y long de 48 bits. Incluso son más comunes actualmente
procesadores de señal donde todos los tipos enteros incluyendo los long son de 32 bits.
signed char: Valores entre:
SCHAR_MIN <= X <= SCHAR_MAX,
Siendo: SCHAR_MIN <= -127 y SCHAR_MAX >= 127. La mayoría de los compiladores utilizan un valor SCHAR_MIN de -128, pero no es exigido por el estándar.
unsigned char: Valores
entre 0 <= x <= UCHAR_MAX.
Se exige que UCHAR_MAX >= 255. si CHAR_BIT es mayor que 8, se exige que UCHAR_MAX = 2 CHAR_BIT - 1. De forma que una implementación que utilice un carácter de 9 bits puede almacenar valores entre 0 y 511 en un unsigned char.
char (valor carácter a secas "plain
char"). Valores entre CHAR_MIN <= X <= CHAR_MAX
Si los valores char son considerados signed char por defecto: CHAR_MIN == SCHAR_MIN y CHAR_MAX == SCHAR_MAX.
Si los valores char son considerados unsigned char por defecto: CHAR_MIN == 0 y CHAR_MAX == UCHAR_MAX.
Por ejemplo, un trozo del fichero limits.h que acompaña al compilador Microsoft Visual C++ 2008, tiene el siguiente aspecto:
#define CHAR_BIT 8
/* number of bits in a char */
#define SCHAR_MIN (-128) /* minimum signed char value */
#define SCHAR_MAX 127 /* maximum signed char value */
#define UCHAR_MAX 0xff /* maximum unsigned char value */
§6 Fraccionarios
La representación y rango de valores de los números fraccionarios depende del compilador. Es decir, cada
implementación de C++ es libre para definirlos. La mayoría utiliza el formato estándar de la IEEE (Institute of Electrical and
Electronics Engineers) para este tipo de números (
2.2.4a).
float y double son tipos fraccionarios de 32 y 64 bits respectivamente. El modificador long puede utilizarse con el tipo double, declarando entonces un número fraccionario de 80 bits. En C++Builder las constantes fraccionarias, que pueden ser float, double y long double, tienen los rangos que se indican:
Tipo | bits | Rango |
float | 32 | 1.17549e-38 <= |X| <= 3.40282e+38 |
double | 64 | 2.22507e-308 <= |X| <= 1.79769e+308 |
long double | 80 | 3.37e-4932 <= |X| <= 1.18e4932 |
Generalmente los compiladores C++ incluyen de forma automática la librería matemática de punto flotante
si el programa utiliza valores fraccionarios [4].
Builder utiliza los siguientes límites, definidos en el fichero <values.h>
float. Valores entre:
MINFLOAT <= |X| <= MAXFLOAT (valores actuales entre paréntesis)
MINFLOAT (1.17549e-38)
MAXFLOAT (3.40282e+38)
FEXPLEN Número de bits en el exponente (8)
FMAXEXP Valor máximo permitido para el exponente (38)
FMAXPOWTWO Máxima potencia de dos permitida (127)
FMINEXP Valor mínimo permitido para el exponente (-37)
FSIGNIF Número de bits significativos. (24)
double. Valores entre
MINDOUBLE <= |X| <= MAXDOUBLE
MINDOUBLE (2.22507e-308)
MAXDOUBLE (1.79769e+308)
DEXPLEN Número de bits en el exponente (11)
DMAXEXP Valor máximo permitido para el exponente (308)
DMAXPOWTWO Máxima potencia de dos permitida (1023)
DMINEXP Valor mínimo permitido para el exponente (-307)
DSIGNIF Número de bits significativos (53)
§7 La clase numeric_limits
La Librería Estándar C++ contiene una clase numeric_limits que contiene información sobre los escalares. Existen subclases para cada tipo fundamental, enteros (incluyendo los booleanos) y fraccionarios. Esta clase engloba la información contenida en los ficheros de cabecera <climits> y <cfloat>; además de incluir información que no está contenida en ninguna otra cabecera. A continuación se incluye un ejemplo que muestra el espacio disponible (bits) para codificar el valor de los tipos fundamentales (mantisa), así como la salida proporcionada en un caso concreto.
#include <cstdlib>
#include <iostream>
#include <limits>
int main(int argc, char *argv[]) {
std::cout << "Mantisa de un char: "
<< std::numeric_limits<char>::digits << '\n';
std::cout << "Mantisa de un unsigned char: "
<< std::numeric_limits<unsigned char>::digits << '\n';
std::cout << "Mantisa de un short: "
<< std::numeric_limits<short>::digits << '\n';
std::cout << "Mantisa de un unsigned short: "
<< std::numeric_limits<unsigned short>::digits << '\n';
std::cout << "Mantisa de un int: "
<< std::numeric_limits<int>::digits << '\n';
std::cout << "Mantisa de un unsigned int: "
<< std::numeric_limits<unsigned int>::digits << '\n';
std::cout << "Mantisa de un long: "
<< std::numeric_limits<long>::digits << '\n';
std::cout << "Mantisa de un unsigned long: "
<< std::numeric_limits<unsigned long>::digits << '\n';
std::cout << "Mantisa de un float: "
<< std::numeric_limits<float>::digits << '\n';
std::cout << "Longitud de un double: "
<< std::numeric_limits<double>::digits << '\n';
std::cout << "Longitud de un long double: "
<< std::numeric_limits<long double>::digits << '\n';
std::cout << "Mantisa de un long long: "
<< std::numeric_limits<long long>::digits << '\n';
std::cout << "Mantisa de un unsigned long long: "
<< std::numeric_limits<unsigned long long>::digits << '\n';
return 0;
}
Salida en una máquina Intel con el compilador GNU g++ 3.4.2 para Windows:
Mantisa de un char: 7
Mantisa de un unsigned char: 8
Mantisa de un short: 15
Mantisa de un unsigned short: 16
Mantisa de un int: 31
Mantisa de un unsigned int: 32
Mantisa de un long: 31
Mantisa de un unsigned long: 32
Mantisa de un float: 24
Longitud de un double: 53
Longitud de un long double: 64
Mantisa de un long long: 63
Mantisa de un unsigned long long: 64
Temas relacionados:
Operador sizeof (
4.9.13)
Alineación interna (
4.6.1).
[1]
Dado que Borland C++ ha sido desarrollado para la familia de PCs IBM (y
compatibles), la arquitectura de los procesadores Intel 8088 y 80x86 es la
que gobierna las posibilidades de representación interna de los diversos
tipos. En (
4.9.13) se indican los tamaños para otros compiladores.
[3] La única asunción es que un entero (int) tiene la misma longitud que la palabra del procesador.
[4] Los valores fraccionarios son denominados "De punto flotante" en la literatura inglesa.
[5] En cierta ocasión un lector me envió la siguiente pregunta: "Tengo una duda, por qué el caso del tipo char incluye -128 ? Si tenemos el bit de signo el número más pequeño no sería -127?" La cuestión planteada tiene cierta lógica, porque si reservamos 1 bit para el signo, la mayor cantidad que puede representarse con 7 bits es 127, de forma que el patrón de bits 0111 1111 correspondería a +127 y 1111 1111 equivaldría -127.
Una forma de ver la respuesta es considerar que el fabricante del compilador puede adoptar la convención de que el bit de signo tiene un doble significado: signo y valor, de forma que el octeto 1000 0000 puede interpretarse como -128 en lugar de -0, reservando 0000 0000 para el ±0. Como puede verse, la consigna de apurar al máximo las posibilidades del almacenamiento, es tomada muy en serio en esta rama de la tecnología.
Otra forma de entender el asunto sería considerarlo desde la óptica de la
representación interna (binaria), que en la mayoría de máquinas actuales es
en forma de complemento a dos. Veremos que en este sistema (
2.2.4a), los números positivos se representan
reservando el bit más significativo (que debe ser cero) para el signo, mientras
que la representación de los números positivos se consigue cambiando los bits que serían 0 por 1 y
viceversa en el valor positivo del número y sumando uno al resultado.
Por ejemplo, aplicando este razonamiento para representar el número -127, tendríamos que el patrón de bits 0111 1111, que corresponde a +127, se transformaría en 1000 0000, al que sumamos 1, de forma que el patrón 1000 0001 corresponde al -127. De forma análoga, la operación inversa permitiría obtener el valor absoluto de cualquier patron de bits que comience por 1 (negativo), así que el proceso para obtener el valor absoluto del patrón de bits 1000 0000 sería: restarle uno -> 0111 1111 + cambiar ceros por unos -> 1000 0000 = 128.
No obstante lo anterior, no olvidar que por debajo de cualquier convención que se adopte, con 8 bits solo es posible representar 255 valores distintos (256 si incluimos el cero), de forma que el rango de magnitudes con signo y sin signo (signed y unsigned) puede ser arbitrario, pero debe respetar esta limitación. En el caso que nos ocupa, el rango de los valores positivos, x <= +127 incluye 128 valores distintos si incluímos el cero, de forma que nos quedan 128 posibilidades para los valores negativos y el rango elegido es desde -1 a -128 inclusive.