Disponible la versión 6 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


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).

  Inicio.


[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.