1. Bytes y tipos
La memoria de una computadora es una larga fila de bytes, cada uno con su dirección, y cada byte tiene 8 bits: puede guardar un número entre 0 y 255. Nada en la memoria dice qué significan esos bytes. Este programa toma cuatro bytes fijos y los copia, sin cambiarlos, en tres variables de tipos distintos:
#include <stdio.h>
#include <string.h>
int main(void)
{
unsigned char bytes[4] = {0x00, 0x00, 0x80, 0xbf};
int entero;
unsigned int sin_signo;
float real;
memcpy(&entero, bytes, 4);
memcpy(&sin_signo, bytes, 4);
memcpy(&real, bytes, 4);
printf("como int: %d\n", entero);
printf("como unsigned int: %u\n", sin_signo);
printf("como float: %f\n", real);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o mismosbytes mismosbytes.c && ./mismosbytes
como int: -1082130432
como unsigned int: 3212836864
como float: -1.000000
$ printf '\x00\x00\x80\xbf' | od -A d -t x1 -t d4 -t u4 -t f4
0000000 00 00 80 bf
-1082130432
3212836864
-1.0000000
0000004
memcpy, de string.h, copia bytes de un lugar a otro sin interpretarlos. Los tres resultados son correctos, y los tres son el mismo dato: un entero con signo muy negativo, un entero sin signo de más de tres mil millones, o el número real −1. La segunda orden lo confirma sin escribir C: od tiene opciones para leer los mismos bytes como cada uno de esos tipos. Esa es la idea central del tema:
El tipo de un dato determina cuántos bytes ocupa en la memoria y cómo se interpretan esos bytes: qué valores representan y qué operaciones se pueden hacer con ellos. En C, cada variable tiene un tipo fijo, que el programador declara y el compilador conoce al compilar.
En Bash, todo era texto. En Python, el intérprete elige la representación y la cambia cuando hace falta: un entero crece sin límite. En C, el tipo es una decisión del programador, y tiene consecuencias. Un sistema operativo necesita esa precisión: una cabecera de un paquete de red, una entrada de una tabla de archivos o un registro de un dispositivo tienen un formato exacto, byte por byte, y el programa tiene que poder describirlo.
2. Los tipos enteros
C tiene cinco tipos enteros básicos, de menor a mayor, y cada uno en versión con signo y sin signo:
| Tipo | Mínimo que garantiza el estándar | En Linux x86-64 | Uso habitual |
|---|---|---|---|
char | 8 bits | 1 byte | Caracteres y bytes sueltos. |
short | 16 bits | 2 bytes | Raro hoy; formatos de archivo y de red. |
int | 16 bits | 4 bytes | El entero de uso general: contadores, índices pequeños, códigos de retorno. |
long | 32 bits | 8 bytes | Valores grandes; en Linux, del tamaño de una dirección. |
long long | 64 bits | 8 bytes | Cuando hacen falta al menos 64 bits en cualquier plataforma. |
Lo que el estándar promete es poco: un tamaño mínimo para cada tipo, y que cada uno sea al menos tan grande como el anterior. Un int de 16 bits es perfectamente legal, y existió en las computadoras de los años ochenta; hoy sigue existiendo en microcontroladores pequeños. Que en Linux un int tenga 32 bits es una decisión de la plataforma, no del lenguaje. Esto tiene una consecuencia directa: un programa que supone que int tiene 4 bytes es correcto en Linux, pero no es portable a cualquier sistema.
Cada tipo se puede escribir de varias formas equivalentes. short es short int, long es long int, unsigned solo es unsigned int, y signed es redundante salvo con char, como veremos. En el curso usamos las formas cortas.
3. El operador sizeof
sizeof da el tamaño en bytes de un tipo o de una expresión. Este programa mide los tipos básicos en nuestra plataforma:
#include <stdio.h>
int main(void)
{
printf("char %zu\n", sizeof(char));
printf("short %zu\n", sizeof(short));
printf("int %zu\n", sizeof(int));
printf("long %zu\n", sizeof(long));
printf("long long %zu\n", sizeof(long long));
printf("float %zu\n", sizeof(float));
printf("double %zu\n", sizeof(double));
printf("long double %zu\n", sizeof(long double));
printf("void * %zu\n", sizeof(void *));
printf("size_t %zu\n", sizeof(size_t));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o tamanos tamanos.c && ./tamanos
char 1
short 2
int 4
long 8
long long 8
float 4
double 8
long double 16
void * 8
size_t 8
long y una dirección de memoria ocupan lo mismo: 8 bytes.Algunos detalles de sizeof que conviene conocer desde ahora:
- Es un operador, no una función. Con un tipo, los paréntesis son obligatorios:
sizeof(int). Con una expresión, son opcionales:sizeof edad, como enmemoria.cdel tema 1. Preferirsizeof variableasizeof(tipo)evita errores: si el tipo de la variable cambia, el tamaño cambia con él. - Se calcula al compilar. El resultado es una constante que el compilador conoce, y la expresión no se ejecuta:
sizeof(x++)vale 4 y no incrementax. - Devuelve un
size_t, un entero sin signo que en Linux tiene 8 bytes, y se imprime con%zu. Con%d, el compilador advierte:format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’. sizeof(char)vale 1 por definición. Todos los tamaños se miden en múltiplos de unchar, sea cual sea su cantidad de bits.
$ cat noeval.c
#include <stdio.h>
int main(void)
{
int x = 5;
size_t t = sizeof(x++);
printf("t = %zu, x = %d\n", t, x);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o noeval noeval.c && ./noeval
t = 4, x = 5
4. 32 y 64 bits: los modelos de datos
Cada plataforma decide el tamaño de int, long y los punteros, y esa combinación se llama modelo de datos. Los nombres indican qué tipos tienen 32 o 64 bits: LP64 significa que Long y Punteros tienen 64.
| Modelo | int | long | long long | puntero | Dónde |
|---|---|---|---|---|---|
| ILP32 | 4 | 4 | 8 | 4 | Linux y Windows de 32 bits, muchos sistemas embebidos. |
| LP64 | 4 | 8 | 8 | 8 | Linux, macOS y los demás Unix de 64 bits. |
| LLP64 | 4 | 4 | 8 | 8 | Windows de 64 bits. |
La diferencia que más problemas causa es la de long: un programa que guarda una dirección o un tamaño de archivo en un long funciona en Linux de 64 bits y falla en Windows, donde long sigue teniendo 32. Y en todos los modelos, int tiene 32 bits, aunque la máquina sea de 64: ampliarlo habría desperdiciado memoria en millones de programas que no lo necesitaban. La solución a esta incertidumbre son los tipos de stdint.h, de la sección 10.
Con el paquete gcc-multilib instalado, la opción -m32 compila para el modelo ILP32 en la misma máquina, y tamanos.c muestra entonces 4 bytes para long y para los punteros. Es una forma barata de probar si un programa depende de los tamaños de su plataforma.
5. Con y sin signo
Un tipo sin signo (unsigned) usa todos sus bits para la magnitud: con 8 bits, va de 0 a 255. Un tipo con signo tiene que representar también los negativos, y todas las computadoras actuales lo hacen de la misma forma, el complemento a dos, que C23 convirtió en obligatoria. La regla es simple: el bit más alto vale lo mismo que en el número sin signo, pero negativo. En 8 bits, ese bit vale −128 en lugar de 128. Este programa muestra los bits de varios valores de un byte, leídos de las dos formas:
#include <stdio.h>
/* muestra los 8 bits de un byte, del más alto al más bajo */
static void bits(unsigned char b)
{
for (int i = 7; i >= 0; i--)
putchar((b >> i) & 1 ? '1' : '0');
}
int main(void)
{
signed char valores[] = {0, 1, 2, 127, -128, -2, -1};
for (int i = 0; i < 7; i++) {
signed char v = valores[i];
unsigned char u = (unsigned char) v;
bits(u);
printf(" con signo: %4d sin signo: %3d\n", v, u);
}
return 0;
}
$ ./signo
00000000 con signo: 0 sin signo: 0
00000001 con signo: 1 sin signo: 1
00000010 con signo: 2 sin signo: 2
01111111 con signo: 127 sin signo: 127
10000000 con signo: -128 sin signo: 128
11111110 con signo: -2 sin signo: 254
11111111 con signo: -1 sin signo: 255
La función bits usa operadores que veremos en el tema 8; lo que importa ahora es la tabla. Mientras el bit más alto está apagado, las dos lecturas coinciden. Cuando se enciende, la lectura con signo resta 256: 11111111 es 255 sin signo y −1 con signo. Por eso −1 tiene «todos los bits encendidos», como vimos con return -1 en el tema 3, que llegaba a la shell como 255. Y por eso el rango con signo es asimétrico: de −128 a 127, porque el cero ocupa uno de los valores no negativos.
La regla vale para cualquier tamaño: con n bits, un tipo sin signo va de 0 a 2n − 1, y uno con signo de −2n−1 a 2n−1 − 1. La otra gran diferencia entre ambos es qué pasa al salirse del rango:
$ ./wrap
0 4294967295
Los tipos sin signo dan la vuelta, y está garantizado: la aritmética sin signo es aritmética módulo 2n. Restar 1 a 0 da el mayor valor posible. En los tipos con signo, en cambio, el desbordamiento es comportamiento indefinido, como vimos con INT_MAX + 1 en el tema 1. Mezclar los dos tipos en una misma expresión tiene además una trampa, que el tema 7 explica en detalle:
$ gcc -std=c17 -Wall -Wextra -o comp comp.c
comp.c: In function ‘main’:
comp.c:6:11: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]
6 | if (i < u)
| ^
$ ./comp
-1 >= 1 ?!
Al comparar un int con un unsigned int, C convierte el primero a sin signo, y −1 se convierte en 4294967295, que es mayor que 1. El compilador lo advirtió gracias a -Wextra. La regla práctica: usa tipos sin signo para lo que no puede ser negativo por naturaleza, como tamaños, bytes o máscaras de bits, y no mezcles tipos con y sin signo en comparaciones.
6. Los límites
Como los tamaños dependen de la plataforma, el encabezado limits.h define constantes con el rango de cada tipo. Un programa que necesita saber el mayor int no debería escribir 2147483647, sino INT_MAX:
$ ./limites
CHAR_BIT = 8
SCHAR_MIN = -128
SCHAR_MAX = 127
UCHAR_MAX = 255
CHAR_MIN = -128
SHRT_MIN = -32768
SHRT_MAX = 32767
USHRT_MAX = 65535
INT_MIN = -2147483648
INT_MAX = 2147483647
UINT_MAX = 4294967295
LONG_MIN = -9223372036854775808
LONG_MAX = 9223372036854775807
ULONG_MAX = 18446744073709551615
Los nombres siguen un patrón: el prefijo del tipo (SCHAR, UCHAR, SHRT, INT, LONG, LLONG, con U delante para los sin signo) y el sufijo _MIN o _MAX. Los tipos sin signo no tienen _MIN, porque es siempre 0. CHAR_BIT es la cantidad de bits de un byte: 8 en cualquier sistema que vayas a usar, aunque el estándar admite más.
Las constantes no salen de la nada. El limits.h de glibc las toma del compilador, que las conoce como macros predefinidas, las que listamos con gcc -dM -E en el tema 4:
$ grep -n 'define INT_MAX' /usr/lib/gcc/x86_64-linux-gnu/15/include/limits.h
120:#define INT_MAX __INT_MAX__
$ gcc -dM -E - < /dev/null | grep '__INT_MAX__'
#define __INT_MAX__ 0x7fffffff
0x7fffffff es un 0 seguido de 31 unos: el mayor número con signo de 32 bits. Leer los límites en hexadecimal ayuda a ver su forma: INT_MIN es 0x80000000, el bit más alto solo, y UINT_MAX es 0xffffffff, todos los bits encendidos.
7. El tipo char
char es un entero de un byte, y su nombre engaña: no guarda «caracteres» en un sentido especial, sino números, que por convención se interpretan como códigos de caracteres. 'A' es simplemente otra forma de escribir 65, el código ASCII de la letra A:
#include <stdio.h>
int main(void)
{
char letra = 'A';
char alto = (char) 0xe9; /* el byte 233 */
printf("'A' vale %d, y sizeof 'A' es %zu\n", letra, sizeof 'A');
printf("letra + 1 = %d, que es '%c'\n", letra + 1, letra + 1);
printf("el byte 0xe9 guardado en un char vale %d\n", alto);
if (alto < 0)
printf("en esta plataforma, char tiene signo\n");
else
printf("en esta plataforma, char no tiene signo\n");
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o caracter caracter.c && ./caracter
'A' vale 65, y sizeof 'A' es 4
letra + 1 = 66, que es 'B'
el byte 0xe9 guardado en un char vale -23
en esta plataforma, char tiene signo
$ gcc -std=c17 -Wall -Wextra -funsigned-char -o caracter caracter.c && ./caracter
caracter.c: In function ‘main’:
caracter.c:11:14: warning: comparison is always false due to limited range of data type [-Wtype-limits]
11 | if (alto < 0)
| ^
'A' vale 65, y sizeof 'A' es 4
letra + 1 = 66, que es 'B'
el byte 0xe9 guardado en un char vale 233
en esta plataforma, char no tiene signo
Hay tres sorpresas en esa salida:
- Las letras son números.
'A' + 1es 66, que mostrado con%cesB. Esto permite cosas como convertir un dígito en su valor conc - '0'. 'A'es unint, no unchar. En C, una constante de carácter tiene tipoint, ysizeof 'A'vale 4. (En C++, en cambio, vale 1.)- No se sabe si
chartiene signo. El estándar deja que cada plataforma lo decida. En x86-64,chartiene signo, y el byte 233 se lee como −23; en Linux sobre ARM, como una Raspberry Pi, no lo tiene, y se lee como 233. La opción-funsigned-charimita a ARM, y el compilador hasta advierte quealto < 0ya no puede ser cierto.
Por eso C tiene en realidad tres tipos de un byte: char, signed char y unsigned char. La convención es usar char para texto y unsigned char para bytes crudos, como los de un archivo binario o un paquete de red, donde un valor de 0 a 255 es lo natural. Con eso, las letras con tilde del UTF-8, cuyos bytes están por encima de 127, como vimos con od en el tema 3, no se vuelven negativas al procesarlas byte por byte.
8. El tipo de las constantes
Los números escritos en el código también tienen tipo. Por omisión, un entero es un int; si no cabe en int, el compilador elige el primer tipo más grande en el que quepa. Los sufijos fuerzan otro tipo:
$ ./lit
4 8 8 4 4
| Constante | Tipo | Comentario |
|---|---|---|
100 | int | El tipo por omisión. |
100L | long | Sufijo L; LL para long long. |
3000000000 | long | No cabe en un int, y el compilador lo agranda solo. |
100u | unsigned int | Sufijo U; se combina: 100UL. |
0x1F, 017, 0b101 | int | Hexadecimal, octal (¡un cero adelante!), y binario desde C23. |
1.5, 1.5f | double, float | Con punto decimal es double; el sufijo f lo hace float. |
'A' | int | Una constante de carácter. |
El octal merece una advertencia: un cero adelante cambia la base. 017 no es diecisiete, sino quince. Es la notación que usa chmod para los permisos, como 0644, y la usaremos así en el curso de archivos; fuera de ese caso, nunca escribas un número con ceros a la izquierda.
9. El orden de los bytes
Un int ocupa cuatro bytes consecutivos, pero ¿en qué orden se guardan? El estándar no lo dice, y los procesadores no se ponen de acuerdo. Este programa guarda el número 0x12345678, cuyos bytes son fáciles de reconocer, y los muestra en el orden en que quedaron en la memoria:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
int main(void)
{
uint32_t numero = 0x12345678;
unsigned char bytes[4];
memcpy(bytes, &numero, sizeof numero);
printf("numero = 0x%08x\n", numero);
for (int i = 0; i < 4; i++)
printf("byte %d (dirección más %d): 0x%02x\n", i, i, bytes[i]);
return 0;
}
$ ./orden
numero = 0x12345678
byte 0 (dirección más 0): 0x78
byte 1 (dirección más 1): 0x56
byte 2 (dirección más 2): 0x34
byte 3 (dirección más 3): 0x12
$ lscpu | grep -i 'byte order'
Byte Order: Little Endian
0x78.En x86-64 el byte menos significativo, 0x78, queda primero, en la dirección más baja: es el orden little-endian, «el extremo pequeño primero». Otros procesadores usan el orden inverso, big-endian, que es el que parece natural al leer el número de izquierda a derecha. Explica también lo que vimos en mismosbytes.c: los bytes 00 00 80 bf forman el número 0xbf800000, leído al revés.
Mientras un programa solo use sus datos en la memoria, el orden no se nota. Importa cuando los bytes salen de la máquina: al escribir un archivo binario que otra computadora leerá, o al enviar datos por la red. Los protocolos de internet usan big-endian, el llamado orden de red, y POSIX ofrece funciones para convertir, como htonl («host to network long»), que aparecerán en el curso de redes.
10. Los tipos exactos de stdint.h
Cuando el tamaño importa, «un int, que en Linux tiene 32 bits» no alcanza. C99 agregó stdint.h, con tipos cuyo nombre dice exactamente su tamaño:
| Tipo | Significa | Para qué |
|---|---|---|
int8_t, int16_t, int32_t, int64_t | Exactamente 8, 16, 32 o 64 bits, con signo. | Formatos binarios, protocolos, registros de hardware. |
uint8_t … uint64_t | Lo mismo, sin signo. | Bytes, máscaras, direcciones IP, sumas de verificación. |
int_least16_t | El tipo más pequeño de al menos 16 bits. | Ahorrar memoria en arreglos grandes, con portabilidad total. |
int_fast16_t | El tipo más rápido de al menos 16 bits. | Contadores en bucles críticos. |
intptr_t, uintptr_t | Un entero capaz de guardar una dirección. | Hacer cuentas con direcciones, en el bloque 4. |
intmax_t, uintmax_t | El entero más grande disponible. | Imprimir tipos del sistema de tamaño desconocido. |
No son tipos nuevos, sino alias de los básicos, creados con typedef, que veremos en el tema 26. Cada plataforma los define según sus tamaños:
$ grep -n 'typedef.*int32_t;\|typedef.*int64_t;' /usr/include/x86_64-linux-gnu/bits/stdint-intn.h
26:typedef __int32_t int32_t;
27:typedef __int64_t int64_t;
$ grep -n '__int32_t;\|__int64_t;' /usr/include/x86_64-linux-gnu/bits/types.h | head -n 2
41:typedef signed int __int32_t;
44:typedef signed long int __int64_t;
En esta plataforma, int32_t es un int e int64_t es un long; en Windows, int64_t sería un long long. El programa escribe int64_t y funciona igual en los dos. Imprimirlos con printf tiene una dificultad: el formato correcto depende de qué tipo básico hay detrás, %ld o %lld. inttypes.h resuelve el problema con macros que se pegan a la cadena de formato:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void)
{
int8_t pequeno = INT8_MAX;
uint16_t puerto = 8080;
int64_t grande = INT64_MAX;
uint64_t bytes = UINT64_C(5000000000);
printf("int8_t %zu byte, máximo %" PRId8 "\n", sizeof pequeno, pequeno);
printf("uint16_t %zu bytes, puerto %" PRIu16 "\n", sizeof puerto, puerto);
printf("int64_t %zu bytes, máximo %" PRId64 "\n", sizeof grande, grande);
printf("uint64_t %zu bytes, valor %" PRIu64 "\n", sizeof bytes, bytes);
printf("int_fast16_t ocupa %zu bytes\n", sizeof(int_fast16_t));
printf("intptr_t ocupa %zu bytes\n", sizeof(intptr_t));
return 0;
}
$ ./exactos
int8_t 1 byte, máximo 127
uint16_t 2 bytes, puerto 8080
int64_t 8 bytes, máximo 9223372036854775807
uint64_t 8 bytes, valor 5000000000
int_fast16_t ocupa 8 bytes
intptr_t ocupa 8 bytes
PRId64 es una macro que vale "ld" en Linux de 64 bits, y la cadena "máximo %" PRId64 "\n" se convierte, por la unión de cadenas literales del tema 3, en "máximo %ld\n". Es incómodo de escribir, pero correcto en cualquier plataforma. Los límites siguen el mismo patrón que en limits.h: INT8_MAX, UINT64_MAX; y las macros como UINT64_C dan a una constante el tipo correcto. Fíjate también en int_fast16_t: en x86-64 ocupa 8 bytes, porque glibc considera que las operaciones de 64 bits son las más rápidas en ese procesador.
11. Los tipos del sistema
Las funciones de la biblioteca y las llamadas al sistema usan sus propios nombres de tipos, que también son alias. Aparecen en el SYNOPSIS de casi toda página de las secciones 2 y 3 del manual, y conviene conocerlos antes de llegar a ellos:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
int main(void)
{
size_t largo = strlen("sistema operativo");
ssize_t escritos = write(1, "hola\n", 5);
pid_t yo = getpid();
time_t ahora = time(NULL);
printf("size_t %zu bytes: strlen devolvió %zu\n", sizeof largo, largo);
printf("ssize_t %zu bytes: write devolvió %zd\n", sizeof escritos, escritos);
printf("pid_t %zu bytes: mi pid es %jd\n", sizeof yo, (intmax_t) yo);
printf("off_t %zu bytes\n", sizeof(off_t));
printf("uid_t %zu bytes\n", sizeof(uid_t));
printf("time_t %zu bytes: ahora es %jd\n", sizeof ahora, (intmax_t) ahora);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -o sistema sistema.c && ./sistema
hola
size_t 8 bytes: strlen devolvió 17
ssize_t 8 bytes: write devolvió 5
pid_t 4 bytes: mi pid es 560
off_t 8 bytes
uid_t 4 bytes
time_t 8 bytes: ahora es 1792101965
| Tipo | Representa | Lo usan | Formato |
|---|---|---|---|
size_t | Un tamaño o una cantidad de bytes; sin signo. | sizeof, strlen, malloc, read | %zu |
ssize_t | Un tamaño, o −1 para indicar error; con signo. Es de POSIX. | read, write | %zd |
pid_t | Un identificador de proceso. | getpid, fork, kill | %jd con (intmax_t) |
uid_t, gid_t | Identificadores de usuario y de grupo. | getuid, chown | %ju con (uintmax_t) |
off_t | Una posición dentro de un archivo. | lseek, stat | %jd con (intmax_t) |
time_t | Un instante, en segundos desde el 1 de enero de 1970. | time, stat | %jd con (intmax_t) |
La forma de write resume por qué existen dos tipos para tamaños: le pasamos cuántos bytes escribir como size_t, que no puede ser negativo, y nos devuelve cuántos escribió como ssize_t, que puede ser −1 si falla, la convención del tema 2. Para los tipos cuyo tamaño POSIX no fija, como pid_t, no existe un formato propio; lo portable es convertirlos al mayor entero, intmax_t, e imprimirlo con %jd.
Usar estos nombres en lugar de int o long no es un capricho. Le dice al lector qué representa el valor, y deja que cada sistema elija el tamaño adecuado. time_t es el ejemplo clásico: en los sistemas de 32 bits era un entero de 32 bits con signo, que se desborda el 19 de enero de 2038, el llamado «problema del año 2038». Los programas que usaban time_t se arreglan recompilando con un time_t de 64 bits; los que guardaron la hora en un int, no.
12. Punto flotante y bool
Para números con decimales, C tiene float (4 bytes), double (8 bytes) y long double (16 bytes en x86-64, aunque usa solo 10). Se guardan en punto flotante, una especie de notación científica en binario, según el estándar IEEE 754, y eso tiene consecuencias que sorprenden a todo programador alguna vez:
#include <stdio.h>
#include <float.h>
int main(void)
{
double suma = 0.1 + 0.2;
float grande = 16777217; /* 2^24 + 1 */
printf("0.1 + 0.2 = %.17f\n", suma);
printf("¿es igual a 0.3? %s\n", suma == 0.3 ? "sí" : "no");
printf("16777217 en float = %.1f\n", grande);
printf("float: %d dígitos, épsilon %g\n", FLT_DIG, FLT_EPSILON);
printf("double: %d dígitos, épsilon %g\n", DBL_DIG, DBL_EPSILON);
printf("1.0 / 0.0 = %f\n", 1.0 / 0.0);
return 0;
}
$ ./flotante
0.1 + 0.2 = 0.30000000000000004
¿es igual a 0.3? no
16777217 en float = 16777216.0
float: 6 dígitos, épsilon 1.19209e-07
double: 15 dígitos, épsilon 2.22045e-16
1.0 / 0.0 = inf
0,1 no tiene representación exacta en binario, igual que 1/3 no la tiene en decimal, y la suma acumula el error: nunca se comparan números de punto flotante con ==. Un float tiene unos 7 dígitos significativos, y ya no distingue 16777217 de 16777216. Y la división por cero no detiene el programa, sino que da infinito. En la programación de sistemas el punto flotante se usa poco: tamaños, direcciones, contadores y tiempos se guardan en enteros. Cuando haga falta, usa double, no float.
Para los valores de verdad, C99 agregó el tipo _Bool, que solo guarda 0 o 1, y el encabezado stdbool.h, que define los nombres cómodos bool, true y false. En C23 los tres son palabras clave y el encabezado ya no hace falta. Un bool ocupa un byte, y cualquier valor distinto de cero que se le asigne se convierte en 1: bool b = 42; guarda 1. Por tradición, las funciones de C, incluidas las del sistema, siguen usando int para las condiciones, con 0 como falso y cualquier otro valor como verdadero.
13. Cada tipo con su formato
printf no sabe qué tipos recibe: el formato se lo dice, y si el formato no coincide con el argumento, el resultado es comportamiento indefinido. -Wall detecta casi todos los desacuerdos, como vimos con aviso.c en el tema 2. Esta tabla reúne los formatos de los tipos de este tema; el tema 10 explica las opciones de ancho y precisión:
| Tipo | Formato | Tipo | Formato |
|---|---|---|---|
int | %d | unsigned int | %u, %x |
long | %ld | unsigned long | %lu, %lx |
long long | %lld | unsigned long long | %llu |
char como letra | %c | signed char como número | %hhd |
size_t | %zu | ssize_t | %zd |
intmax_t | %jd | uintmax_t | %ju |
int64_t | "%" PRId64 | uint32_t | "%" PRIu32 |
double | %f, %g | una dirección | %p |
14. Elegir el tipo correcto
Con tantas opciones, unas pocas reglas cubren casi todos los casos que aparecerán en el curso:
| Para guardar… | Usa | Por qué |
|---|---|---|
| Un número entero cualquiera, de rango moderado | int | Es el tipo natural del procesador, y el que esperan la mayoría de las funciones. |
| Un tamaño, una longitud, un índice de un arreglo | size_t | Es lo que devuelven sizeof y strlen, y cubre cualquier tamaño posible en memoria. |
Un resultado de read o write | ssize_t | Puede ser −1. |
| Un campo de un formato binario o de red | uint8_t, uint16_t, uint32_t… | El tamaño es parte del formato, y no puede depender de la plataforma. |
| Bytes crudos | unsigned char o uint8_t | Valores de 0 a 255, sin sorpresas de signo. |
| Texto | char | Es lo que esperan todas las funciones de cadenas. |
| Un identificador o un tiempo del sistema | pid_t, uid_t, time_t, off_t | El sistema decide su tamaño, y el nombre documenta el significado. |
| Un valor de verdad | bool o int | bool es más claro; int, más tradicional. |
| Un número con decimales | double | Más precisión que float, sin costo apreciable. |
15. Resumen
- La memoria guarda bytes sin significado; el tipo determina cuántos bytes ocupa un dato y cómo se interpretan.
- Los enteros básicos son
char,short,int,longylong long. El estándar fija solo tamaños mínimos; en Linux x86-64 miden 1, 2, 4, 8 y 8 bytes. sizeofes un operador que se calcula al compilar, no evalúa su expresión, y devuelve unsize_t, que se imprime con%zu.- Linux de 64 bits usa el modelo LP64 (
longy punteros de 8 bytes); Windows de 64 bits usa LLP64, conlongde 4. - Los enteros con signo usan complemento a dos: el bit más alto resta. −1 tiene todos los bits encendidos.
- Los tipos sin signo dan la vuelta de forma garantizada; desbordar un tipo con signo es comportamiento indefinido. Comparar con y sin signo convierte el negativo en un número enorme.
limits.hdefine los rangos:INT_MAX,UINT_MAX,CHAR_BIT.chares un entero de un byte cuyo signo depende de la plataforma;'A'es unintque vale 65. Para bytes crudos se usaunsigned char.- Las constantes tienen tipo:
intpor omisión, con sufijosL,Uyf; un cero adelante significa octal. - x86-64 es little-endian: el byte menos significativo va primero. La red usa big-endian.
stdint.hofrece tipos de tamaño exacto, comoint32_tyuint64_t, que se imprimen con las macros deinttypes.h, comoPRId64.- El sistema usa alias con significado:
size_t,ssize_t,pid_t,off_t,time_t. Los de tamaño no fijo se imprimen convertidos aintmax_t. - Los números de punto flotante son aproximados y no se comparan con
==;boolguarda 0 o 1.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. En mismosbytes.c, ¿cómo pueden los mismos cuatro bytes valer −1082130432, 3212836864 y −1.0?
Porque la memoria solo guarda bytes, y es el tipo el que dice cómo interpretarlos. Leídos como entero con signo, el bit más alto encendido los hace negativos; como entero sin signo, todos los bits suman; y como float, siguen el formato IEEE 754, en el que esos bytes representan −1.
2. ¿Es correcto un compilador en el que int ocupa 2 bytes? ¿Qué garantiza el estándar?
Sí. El estándar solo garantiza que int tenga al menos 16 bits, y que cada tipo entero sea al menos tan grande como el anterior: char ≤ short ≤ int ≤ long ≤ long long. Que en Linux tenga 32 bits es una decisión de la plataforma.
3. ¿Por qué sizeof(x++) no incrementa x?
Porque sizeof se resuelve al compilar, a partir del tipo de la expresión, sin ejecutarla. El compilador solo necesita saber que x++ es un int para reemplazar todo por la constante 4.
4. ¿Qué diferencia hay entre los modelos LP64 y LLP64, y qué problema puede causar?
En LP64 (Linux, macOS), long y los punteros tienen 64 bits; en LLP64 (Windows de 64 bits), los punteros tienen 64 pero long sigue teniendo 32. Un programa que guarda una dirección o un tamaño grande en un long funciona en Linux y pierde información en Windows.
5. ¿Qué valor tiene el byte 11111111 con signo y sin signo, y por qué?
Sin signo, 255: la suma de todos los bits. Con signo, en complemento a dos, −1: el bit más alto vale −128 en lugar de 128, y −128 + 127 = −1.
6. ¿Por qué -1 < 1u es falso?
Porque al comparar un int con un unsigned int, C convierte el int a sin signo, y −1 se convierte en el mayor valor sin signo, 4294967295, que es mayor que 1. -Wextra lo advierte con -Wsign-compare.
7. ¿Por qué se recomienda unsigned char para procesar bytes crudos, y no char?
Porque el signo de char depende de la plataforma: en x86-64 tiene signo, y los bytes de 128 a 255 se leen como negativos, lo que rompe comparaciones y cuentas; en ARM no lo tiene. unsigned char da siempre valores de 0 a 255.
8. ¿Cuánto vale sizeof 'A' en C, y por qué?
4 en Linux, porque en C una constante de carácter como 'A' tiene tipo int, no char. Es solo otra forma de escribir el número 65.
9. En x86-64, ¿en qué orden quedan en la memoria los bytes de 0x12345678, y cuándo importa?
En orden little-endian: 78 56 34 12, el byte menos significativo en la dirección más baja. Importa cuando los bytes salen de la máquina, al escribir archivos binarios o enviar datos por la red, donde el orden estándar es big-endian.
10. ¿Por qué existe PRId64, en lugar de imprimir un int64_t con %ld?
Porque int64_t es un alias de distintos tipos básicos según la plataforma: long en Linux de 64 bits, long long en Windows o en sistemas de 32 bits, y cada uno necesita un formato distinto. PRId64 es una macro que se expande al formato correcto en cada plataforma.
11. ¿Por qué write recibe un size_t pero devuelve un ssize_t?
La cantidad de bytes a escribir no puede ser negativa, y size_t es sin signo. El resultado, en cambio, tiene que poder indicar un error con −1, y para eso hace falta un tipo con signo del mismo tamaño: ssize_t.
12. ¿Por qué 0.1 + 0.2 == 0.3 es falso?
Porque 0,1, 0,2 y 0,3 no tienen representación exacta en binario, y se guardan aproximados. La suma de las aproximaciones de 0,1 y 0,2 da 0,30000000000000004, que no es exactamente la aproximación de 0,3. Los números de punto flotante no se comparan con ==.
Para practicar
- Agrega a
tamanos.clos tiposunsigned short,_Bool,int_least8_t,int_fast32_t,ssize_t,pid_tyoff_t. ¿Alguno te sorprende? - Modifica
signo.cpara que muestre los 16 bits de unshort, con los valores 0, 1, −1, 32767, −32768 y 1000. - Escribe un programa que sume 1 a
UCHAR_MAX,USHRT_MAXyUINT_MAX, guardando cada resultado en una variable del mismo tipo, y muestra los resultados. Luego haz lo mismo conINT_MAXy compila con-fsanitize=undefined. - Cambia
orden.cpara que use unuint64_tcon el valor0x0102030405060708. Predice el orden de los 8 bytes antes de ejecutarlo. - Crea un archivo de 4 bytes con
printf '\x01\x00\x00\x00' > uno.biny averigua conod -t d4 uno.binqué número entero representa en tu máquina. Luego crea otro con los bytes en orden inverso,'\x00\x00\x00\x01': ¿qué número es ahora, y por qué? - Imprime
getuid(),getppid()y el tamaño degid_tcon los formatos portables de la sección 11. Busca enman 2 getuidel encabezado que necesitas. - Escribe un bucle que sume 0,1 diez veces en un
floaty en undouble, e imprime los resultados con%.10f. ¿Cuál se aleja más de 1?