1. Una unión: varios nombres para los mismos bytes
Una unión (union) se declara igual que una estructura, pero todos sus miembros ocupan el mismo lugar de la memoria: empiezan en el desplazamiento 0, y la unión mide lo que el miembro más grande, redondeado a su alineación. Escribir un miembro modifica los bytes que los demás leen.
#include <stdio.h>
#include <stddef.h>
union numero {
int i;
float f;
unsigned char b[4];
};
union mixta {
char texto[13];
int i;
double d;
};
static void bytes(const union numero *n)
{
for (int k = 0; k < 4; k++)
printf(" %02x", n->b[k]);
}
int main(void)
{
union numero n;
printf("sizeof(union numero) = %zu, alignof = %zu\n", sizeof n, _Alignof(union numero));
printf("&n = %p, &n.i = %p, &n.f = %p, &n.b = %p\n",
(void *) &n, (void *) &n.i, (void *) &n.f, (void *) &n.b);
n.f = 1.0f;
printf("n.f = %g n.i = 0x%08x bytes:", n.f, (unsigned) n.i);
bytes(&n);
printf("\n");
n.i = 0x41424344;
printf("n.i = 0x%08x bytes:", (unsigned) n.i);
bytes(&n);
printf(" como texto: %c%c%c%c\n", n.b[0], n.b[1], n.b[2], n.b[3]);
n.i = -1;
printf("n.i = %d n.f = %g (todos los bytes en 0xff)\n", n.i, n.f);
union mixta m;
printf("sizeof(union mixta) = %zu, alignof = %zu, sizeof m.texto = %zu\n",
sizeof m, _Alignof(union mixta), sizeof m.texto);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o basica basica.c
$ ./basica
sizeof(union numero) = 4, alignof = 4
&n = 0x7ffca4fcfc90, &n.i = 0x7ffca4fcfc90, &n.f = 0x7ffca4fcfc90, &n.b = 0x7ffca4fcfc90
n.f = 1 n.i = 0x3f800000 bytes: 00 00 80 3f
n.i = 0x41424344 bytes: 44 43 42 41 como texto: DCBA
n.i = -1 n.f = -nan (todos los bytes en 0xff)
sizeof(union mixta) = 16, alignof = 8, sizeof m.texto = 13
La salida muestra las propiedades de una unión:
- Todas las direcciones coinciden.
&n.i,&n.fy&n.bson la misma, la de la unión. Unoffsetofde cualquier miembro vale 0. - Mide lo que el miembro más grande.
union numeroocupa 4 bytes, no 12. Pero el tamaño se redondea a la alineación de la unión, la del miembro más exigente: enunion mixta, el arreglo de 13 caracteres es el más largo, y eldoublepide alineación 8, así que ocupa 16. - Escribir uno cambia los otros. Al asignar
n.f = 1.0f,n.ipasa a ser0x3f800000: no se convierte el valor 1 en el entero 1, se leen como entero los bytes con que se representa 1.0 como real. Los bytes00 00 80 3fson ese número en orden little endian. - Es una forma de ver la representación. Con
n.i = 0x41424344, el arreglobmuestra los bytes en memoria:44 43 42 41, es decir «DCBA»: el byte menos significativo está primero. Es la prueba del little endian que vimos en el tema 5. - Los bytes no tienen por qué formar un valor válido. Con todos los bytes en
0xff,n.fes un NaN (not a number), queprintfmuestra como-nan.
Una unión se inicializa por nombre, union numero n = {.f = 1.5f};, o con un valor entre llaves, que inicializa su primer miembro. Nunca más de un miembro a la vez.
struct | union | |
|---|---|---|
| Los miembros están | Uno a continuación del otro. | Todos en el mismo lugar. |
| Desplazamiento de cada miembro | 0, 4, 8… (con relleno). | Siempre 0. |
| Tamaño | La suma de los miembros, más relleno. | El del miembro más grande, redondeado a la alineación. |
| Valores a la vez | Todos los miembros. | Uno solo tiene sentido. |
| Sirve para | Agrupar datos que van juntos. | Alternativas excluyentes, o ver unos bytes de varias formas. |
2. Reinterpretar bits y el aliasing estricto
Leer un miembro distinto del último escrito es lo que se llama type punning: reinterpretar los bytes de un valor como si fueran de otro tipo. C lo permite expresamente con las uniones (C++ no). Es útil para inspeccionar la representación de los números. Este programa descompone números reales en los tres campos del formato IEEE 754 de 32 bits: el signo, el exponente y la mantisa:
#include <stdio.h>
#include <stdint.h>
#include <math.h>
static void descomponer(float x)
{
union {
float f;
uint32_t u;
} v = {.f = x};
unsigned signo = v.u >> 31;
unsigned exponente = (v.u >> 23) & 0xFF;
unsigned mantisa = v.u & 0x7FFFFF;
printf("%-6g 0x%08x signo=%u exponente=%3u (%+4d) mantisa=0x%06x\n",
x, (unsigned) v.u, signo, exponente, (int) exponente - 127, mantisa);
}
int main(void)
{
descomponer(1.0f);
descomponer(0.1f);
descomponer(-2.5f);
descomponer(0.0f);
descomponer(INFINITY);
descomponer(NAN);
return 0;
}
$ ./bits
1 0x3f800000 signo=0 exponente=127 ( +0) mantisa=0x000000
0.1 0x3dcccccd signo=0 exponente=123 ( -4) mantisa=0x4ccccd
-2.5 0xc0200000 signo=1 exponente=128 ( +1) mantisa=0x200000
0 0x00000000 signo=0 exponente= 0 (-127) mantisa=0x000000
inf 0x7f800000 signo=0 exponente=255 (+128) mantisa=0x000000
nan 0x7fc00000 signo=0 exponente=255 (+128) mantisa=0x400000
Con los operadores de bits del tema 8 se leen los campos. El 1.0 tiene el exponente 127, que representa 2⁰ (el exponente se guarda con un sesgo de 127) y la mantisa en cero, porque el 1 inicial está implícito. El 0.1 no se puede representar exactamente en binario: 0x3dcccccd es el número más cercano, y la mantisa 4ccccd es la secuencia periódica 1100 1100… redondeada, que es la causa de que 0.1 + 0.2 != 0.3. El −2.5 tiene el signo encendido. El infinito y el NaN usan el exponente 255, y se distinguen por la mantisa. Es el tipo de inspección que un depurador de bajo nivel o una herramienta de formato binario necesita, y con una unión es directa.
Hay otra forma de obtener los mismos bits que se ve a menudo, y es incorrecta: convertir la dirección. Este programa compara las tres:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
uint32_t con_cast(float f)
{
return *(uint32_t *) &f; /* viola el aliasing estricto */
}
uint32_t con_union(float f)
{
union {
float f;
uint32_t u;
} v = {.f = f};
return v.u;
}
uint32_t con_memcpy(float f)
{
uint32_t u;
memcpy(&u, &f, sizeof u);
return u;
}
int main(void)
{
float x = 0.1f;
printf("cast: 0x%08x\n", (unsigned) con_cast(x));
printf("union: 0x%08x\n", (unsigned) con_union(x));
printf("memcpy: 0x%08x\n", (unsigned) con_memcpy(x));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -O2 -o punning punning.c
punning.c: In function ‘con_cast’:
punning.c:7:13: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
7 | return *(uint32_t *) &f; /* viola el aliasing estricto */
| ^~~~~~~~~~~~~~~
$ ./punning
cast: 0x3dcccccd
union: 0x3dcccccd
memcpy: 0x3dcccccd
$ gcc -std=c17 -O2 -S -fno-asynchronous-unwind-tables -fcf-protection=none -o - punning.c | grep -A2 -E '^con_'
con_cast:
movd %xmm0, %eax
ret
con_union:
movd %xmm0, %eax
ret
con_memcpy:
movd %xmm0, %eax
ret
Las tres dieron el mismo resultado, y las tres compilaron a la misma instrucción, movd, que copia el registro de coma flotante a uno entero. Pero no son iguales ante el estándar. La regla del aliasing estricto dice que un objeto solo se puede acceder a través de un puntero a su tipo (o a char): el compilador puede suponer que un uint32_t * y un float * nunca apuntan al mismo lugar, y reordenar o eliminar lecturas y escrituras en consecuencia. con_cast rompe esa suposición, y es comportamiento indefinido aunque aquí funcione; gcc lo avisa con -Wstrict-aliasing cuando optimiza. En un programa más grande, donde el compilador sí reordena, produce resultados que cambian con el nivel de optimización. La unión y memcpy son correctas. memcpy no cuesta nada, porque el compilador la reconoce y no la ejecuta (con_memcpy es una sola instrucción), funciona entre cualquier par de tipos y se entiende en C++; es la forma que conviene preferir, y la que ya usamos en los temas 5 y 18.
3. Uniones etiquetadas
El uso más importante de las uniones no es inspeccionar bytes, sino guardar un valor de uno de varios tipos posibles sin gastar lugar para todos. Una unión no recuerda cuál de sus miembros es el válido, así que el programa tiene que guardarlo aparte: en una estructura, junto a la unión, hay un campo que dice cuál es. Es una unión etiquetada (tagged union), el mecanismo con que se implementan los valores de tipo variable de los intérpretes, los mensajes de los protocolos y los nodos de los árboles sintácticos:
#include <stdio.h>
enum tipo { T_ENTERO, T_REAL, T_TEXTO, T_NINGUNO };
struct valor {
enum tipo tipo; /* la etiqueta: qué miembro vale */
union { /* unión anónima (C11) */
long entero;
double real;
const char *texto;
};
};
static struct valor entero(long n) { return (struct valor){.tipo = T_ENTERO, .entero = n}; }
static struct valor real(double x) { return (struct valor){.tipo = T_REAL, .real = x}; }
static struct valor texto(const char *s) { return (struct valor){.tipo = T_TEXTO, .texto = s}; }
static struct valor ninguno(void) { return (struct valor){.tipo = T_NINGUNO}; }
static void mostrar(const struct valor *v)
{
switch (v->tipo) {
case T_ENTERO:
printf("entero %ld\n", v->entero);
break;
case T_REAL:
printf("real %g\n", v->real);
break;
case T_TEXTO:
printf("texto \"%s\"\n", v->texto);
break;
case T_NINGUNO:
printf("ninguno\n");
break;
}
}
int main(void)
{
struct valor lista[] = {entero(42), real(2.5), texto("sistema"), ninguno(), entero(-7)};
for (size_t i = 0; i < sizeof lista / sizeof lista[0]; i++)
mostrar(&lista[i]);
printf("sizeof(struct valor) = %zu\n", sizeof(struct valor));
return 0;
}
$ ./etiquetada
entero 42
real 2.5
texto "sistema"
ninguno
entero -7
sizeof(struct valor) = 16
Varias cosas de este diseño importan:
- La etiqueta es una disciplina, no una garantía. El compilador no impide leer
v->realcuandov->tipovaleT_ENTERO: lee los bytes del entero como real. Para evitarlo, el programa lee los miembros solo después de mirar la etiqueta, y los crea solo con funciones,entero,real,texto, que fijan los dos a la vez. Los literales compuestos del tema 24 lo hacen cómodo. - La unión anónima es una característica de C11: un
unionsin nombre dentro de una estructura hace que sus miembros se usen directamente,v->enteroy nov->datos.entero. También hay estructuras anónimas. - Ocupa 16 bytes para cualquiera de los cuatro casos: 4 de la etiqueta, 4 de relleno por la alineación 8 de la unión, y 8 para el miembro más grande. Una estructura con los tres campos por separado habría ocupado 32.
- El
switchsobre unenumes la forma de recorrerla, y tiene una ventaja que se aprecia al agregar un caso nuevo:
$ gcc -std=c17 -Wall -Wextra -g -o falta falta.c
falta.c: In function ‘nombre’:
falta.c:7:5: warning: enumeration value ‘T_NINGUNO’ not handled in switch [-Wswitch]
7 | switch (t) {
| ^~~~~~
Cuando un switch sobre un valor de tipo enumerado no cubre todas las constantes, gcc avisa con -Wswitch, incluido en -Wall. Al agregar una constante a la enumeración, el compilador señala cada switch que hay que actualizar. Ese beneficio desaparece si se escribe una rama default, porque entonces el compilador supone que el caso está cubierto: conviene dejar default afuera cuando se quiere cubrir todas las constantes, y manejar los valores inválidos con una comprobación aparte, como en la sección 12.
4. Las uniones del sistema
Las uniones son frecuentes en las interfaces que el núcleo expone, porque muchas llamadas llevan un dato opaco que el programa decide cómo interpretar. Dos ejemplos que veremos en los cursos siguientes: epoll avisa qué descriptores están listos, y devuelve junto a cada aviso el dato que el programa registró; y las señales con datos llevan un valor que puede ser un entero o un puntero. Este programa mira los tamaños:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stddef.h>
#include <signal.h>
#include <sys/epoll.h>
int main(void)
{
printf("sizeof(epoll_data_t) = %zu, alignof = %zu\n",
sizeof(epoll_data_t), _Alignof(epoll_data_t));
printf("sizeof(struct epoll_event) = %zu, alignof = %zu\n",
sizeof(struct epoll_event), _Alignof(struct epoll_event));
printf(" events en %zu, data en %zu\n",
offsetof(struct epoll_event, events), offsetof(struct epoll_event, data));
printf("sizeof(union sigval) = %zu\n", sizeof(union sigval));
struct epoll_event e = {.events = EPOLLIN, .data.fd = 7};
printf("e.data.fd = %d, e.data.u64 = 0x%llx\n", e.data.fd, (unsigned long long) e.data.u64);
return 0;
}
$ ./epoll
sizeof(epoll_data_t) = 8, alignof = 8
sizeof(struct epoll_event) = 12, alignof = 1
events en 0, data en 4
sizeof(union sigval) = 8
e.data.fd = 7, e.data.u64 = 0x7
$ grep -n -A7 'typedef union epoll_data' /usr/include/x86_64-linux-gnu/sys/epoll.h
77:typedef union epoll_data
78-{
79- void *ptr;
80- int fd;
81- uint32_t u32;
82- uint64_t u64;
83-} epoll_data_t;
La unión epoll_data_t ofrece cuatro formas de usar los mismos 8 bytes: un puntero, un descriptor de archivo, un entero de 32 bits o uno de 64. El núcleo no mira lo que contiene: lo guarda al registrar el descriptor y lo devuelve intacto con cada aviso. Un programa que maneja pocos descriptores guarda el número en data.fd; uno que maneja muchas conexiones guarda en data.ptr un puntero a la estructura que describe cada una. Es un parámetro de contexto, el mismo patrón de void * que vimos en qsort_r en el tema 20, con un tipo propio que evita el cast. union sigval, de signal.h, es lo mismo para las señales con datos.
Hay algo más para notar, que enlaza con el tema 25. struct epoll_event mide 12 bytes y tiene alineación 1: data está en el desplazamiento 4, aunque contiene un puntero de alineación 8. La estructura está declarada con __attribute__((packed)) en x86-64, como muestra la macro __EPOLL_PACKED, porque debe coincidir bit a bit con la que usa el núcleo desde las primeras versiones, y eso incluye a los programas de 32 bits que corren sobre un núcleo de 64. Es una compatibilidad binaria que el programa de usuario recibe sin saberlo. Y la última línea muestra el riesgo de las uniones: el programa escribió data.fd y leyó data.u64, que incluye 4 bytes que nadie escribió. Dio 7 porque el compilador puso en cero el resto al inicializar, no porque sea cierto en general; leer un miembro distinto del escrito es lo que la etiqueta de la sección 3 existe para evitar.
5. Enumeraciones
Una enumeración (enum) es un tipo entero cuyas constantes tienen nombre. Se declara con una lista de identificadores: el primero vale 0, y cada uno siguiente vale uno más que el anterior, salvo que se le asigne un valor explícito.
#include <stdio.h>
enum color { ROJO, VERDE = 5, AZUL, AMARILLO = AZUL + 10 };
enum signo { NEGATIVO = -1, CERO, POSITIVO };
enum { TAM = 512 }; /* sin nombre: solo una constante */
int main(void)
{
printf("ROJO=%d VERDE=%d AZUL=%d AMARILLO=%d\n", ROJO, VERDE, AZUL, AMARILLO);
printf("sizeof(ROJO) = %zu (la constante es un int)\n", sizeof ROJO);
printf("sizeof(enum color) = %zu, sizeof(enum signo) = %zu\n",
sizeof(enum color), sizeof(enum signo));
printf("(enum color) -1 > 0: %d (enum signo) -1 > 0: %d\n",
(enum color) -1 > 0, (enum signo) -1 > 0);
enum color c = 42; /* C no lo impide */
printf("c = %d (no es ningún color)\n", c);
char buffer[TAM];
printf("sizeof buffer = %zu\n", sizeof buffer);
return 0;
}
$ ./enumeraciones
ROJO=0 VERDE=5 AZUL=6 AMARILLO=16
sizeof(ROJO) = 4 (la constante es un int)
sizeof(enum color) = 4, sizeof(enum signo) = 4
(enum color) -1 > 0: 1 (enum signo) -1 > 0: 0
c = 42 (no es ningún color)
sizeof buffer = 512
Lo que hay que saber de las enumeraciones de C, que son menos estrictas de lo que parecen:
- Los valores siguen una regla simple.
ROJOvale 0;VERDEse fijó en 5;AZULcontinúa con 6;AMARILLOusa una expresión constante, 16. Dos constantes pueden tener el mismo valor. - Las constantes son de tipo
int, como muestrasizeof ROJO, y son constantes de compilación: se pueden usar donde se pide una expresión constante, como el tamaño de un arreglo,char buffer[TAM]. Por esoenum { TAM = 512 }se usa como constante entera sin nombre de tipo, la alternativa a#defineque mencionamos en el tema 6. - El tipo enumerado es un entero cuyo tipo elige el compilador.
gccusaunsigned intsi todas las constantes son no negativas, eintsi alguna es negativa: eso explica que(enum color) -1 > 0sea verdadero y(enum signo) -1 > 0, falso. El estándar no fija ese tipo. C23 permite elegirlo,enum estado : uint8_t { … }, lo que sirve para formatos binarios. - C no limita los valores:
enum color c = 42;compila sin ninguna advertencia. Una variable de tipoenum colorpuede valer cualquier entero que quepa en el tipo. Cuando el valor viene de afuera, de un archivo, de la red o de un argumento, hay que validarlo antes de convertirlo, como hace la sección 12. - Las constantes no tienen ámbito propio. Viven en el mismo espacio de nombres que las variables y las funciones: dos enumeraciones del mismo bloque no pueden usar el mismo nombre, y un nombre como
ROJOchoca con cualquier otro del programa o de una cabecera. Por eso se acostumbra un prefijo,T_ENTERO,EV_TECLA,E_ZOMBI, que es también el estilo de las cabeceras del sistema:O_RDONLY,SIGINT,EPOLLIN. - Mezclar enumeraciones sí tiene un aviso:
$ gcc -std=c17 -Wall -Wextra -g -o mezcla mezcla.c
mezcla.c: In function ‘main’:
mezcla.c:8:20: warning: implicit conversion from ‘enum animal’ to ‘enum color’ [-Wenum-conversion]
8 | enum color c = GATO;
| ^~~~
-Wenum-conversion, incluida en -Wextra para C, detecta que se asigna una constante de una enumeración a una variable de otra. Es la única comprobación de tipos que el lenguaje ofrece para las enumeraciones, y no cubre el caso de un entero cualquiera.
Frente a #define, la enumeración agrupa constantes relacionadas, no es una sustitución de texto que ocurre antes de compilar, y gdb conoce sus nombres: muestra EV_TECLA y no el número 0 (sección 13). Las macros siguen siendo necesarias para lo que no es un entero, como una cadena o un valor de coma flotante.
6. Usos habituales de enum
Tres patrones aparecen una y otra vez. Los tres están en este programa, que lee el estado de procesos desde /proc/PID/stat y lo traduce a una enumeración:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <limits.h>
enum estado { E_EJECUTANDO, E_DURMIENDO, E_DISCO, E_ZOMBI, E_DETENIDO, E_OTRO, N_ESTADOS };
static const char *const nombres[N_ESTADOS] = {
[E_EJECUTANDO] = "ejecutando",
[E_DURMIENDO] = "durmiendo",
[E_DISCO] = "esperando disco",
[E_ZOMBI] = "zombi",
[E_DETENIDO] = "detenido",
[E_OTRO] = "otro",
};
static enum estado clasificar(char letra)
{
switch (letra) {
case 'R':
return E_EJECUTANDO;
case 'S':
case 'I':
return E_DURMIENDO;
case 'D':
return E_DISCO;
case 'Z':
return E_ZOMBI;
case 'T':
case 't':
return E_DETENIDO;
default:
return E_OTRO;
}
}
/* lee "pid (nombre) E ..." de /proc/PID/stat; 0 si lo logró, -1 si no */
static int leer_estado(long pid, char comm[32], char *letra)
{
char ruta[64], linea[512];
int n = snprintf(ruta, sizeof ruta, "/proc/%ld/stat", pid);
if (n < 0 || (size_t) n >= sizeof ruta)
return -1;
FILE *f = fopen(ruta, "r");
if (f == NULL)
return -1;
char *ok = fgets(linea, sizeof linea, f);
fclose(f);
if (ok == NULL)
return -1;
char *abre = strchr(linea, '(');
char *cierra = strrchr(linea, ')'); /* el nombre puede contener paréntesis */
if (abre == NULL || cierra == NULL || cierra[1] != ' ' || cierra[2] == '\0')
return -1;
snprintf(comm, 32, "%.*s", (int) (cierra - abre - 1), abre + 1);
*letra = cierra[2];
return 0;
}
int main(int argc, char *argv[])
{
int estado = 0;
for (int i = 1; i < argc; i++) {
char *fin;
errno = 0;
long pid = strtol(argv[i], &fin, 10);
if (fin == argv[i] || *fin != '\0' || errno == ERANGE || pid < 1 || pid > INT_MAX) {
fprintf(stderr, "%s: PID inválido: '%s'\n", argv[0], argv[i]);
estado = 2;
continue;
}
char comm[32], letra;
if (leer_estado(pid, comm, &letra) != 0) {
fprintf(stderr, "%s: no se pudo leer el proceso %ld\n", argv[0], pid);
estado = 1;
continue;
}
enum estado e = clasificar(letra);
printf("%6ld %-16s %c %s (%d)\n", pid, comm, letra, nombres[e], e);
}
return estado;
}
$ sleep 30 &
[1] 538
$ ./estados $! $$ 1 99999 abc; echo $?
538 sleep S durmiendo (1)
338 bash S durmiendo (1)
1 systemd S durmiendo (1)
./estados: no se pudo leer el proceso 99999
./estados: PID inválido: 'abc'
2
$ kill -STOP $!; ./estados $!
538 sleep T detenido (4)
Los tres patrones del programa:
- La constante final
N_ESTADOSno es un estado sino la cantidad de estados, gracias a que los valores arrancan en 0 y son consecutivos. Se usa como tamaño del arreglo y como límite de los recorridos, y se actualiza sola cuando se agrega un estado antes de ella. - Un arreglo indexado por la enumeración, con inicialización designada,
[E_ZOMBI] = "zombi", del tema 24. Es la tabla de nombres para mostrar, y también la forma de la tabla de despacho de la sección 12. Si se agrega una constante y se olvida su nombre, el elemento queda enNULL: conviene comprobarlo, o fijarlo con unstatic_assertsobreN_ESTADOS. - Un
switchque clasifica una entrada del sistema, aquí la letra de estado que Linux escribe en/proc, en un conjunto chico y propio. Los estados reales (R, S, D, Z, T, I…) están enman 5 proc, y eldefaultcubre los que el programa no distingue.
El resto repasa lo que aprendimos en el bloque: strrchr encuentra el último ) porque el nombre del proceso, que puede ser cualquier texto de hasta 15 caracteres, es el que aparece entre paréntesis y puede contener paréntesis; strtol valida el argumento; y snprintf arma la ruta con una comprobación del recorte.
El segundo uso grande de enum son las banderas: constantes que son potencias de 2, para combinar con operadores de bits, como los permisos de archivos o las opciones de open:
#include <stdio.h>
enum permiso {
P_LEER = 1 << 0,
P_ESCRIBIR = 1 << 1,
P_EJECUTAR = 1 << 2,
};
static void texto(unsigned p, char salida[4])
{
salida[0] = p & P_LEER ? 'r' : '-';
salida[1] = p & P_ESCRIBIR ? 'w' : '-';
salida[2] = p & P_EJECUTAR ? 'x' : '-';
salida[3] = '\0';
}
int main(void)
{
unsigned usuario = P_LEER | P_ESCRIBIR;
char t[4];
texto(usuario, t);
printf("usuario = %u %s\n", usuario, t);
usuario |= P_EJECUTAR; /* encender */
usuario &= ~P_ESCRIBIR; /* apagar */
texto(usuario, t);
printf("usuario = %u %s\n", usuario, t);
for (unsigned p = 0; p <= 7; p++) {
texto(p, t);
printf("%u=%s ", p, t);
}
printf("\n");
return 0;
}
$ ./banderas
usuario = 3 rw-
usuario = 5 r-x
0=--- 1=r-- 2=-w- 3=rw- 4=--x 5=r-x 6=-wx 7=rwx
La variable que guarda la combinación es un unsigned, no un enum permiso: un valor como 3 no es ninguna de las tres constantes, y declararlo del tipo enumerado sería mentir. La enumeración solo da nombre a los bits; las operaciones son las del tema 8: | para combinar, & para probar, |= para encender, &= ~ para apagar. Es el patrón de S_IRUSR, O_CREAT y casi todas las opciones de las llamadas al sistema.
7. typedef: un nombre nuevo para un tipo
La palabra typedef declara un alias: un nombre nuevo para un tipo que ya existe. Se escribe igual que la declaración de una variable de ese tipo, con typedef delante, y el nombre que sería el de la variable pasa a ser el del tipo. Es lo que vimos en el tema 20 con los punteros a funciones:
| Declaración de una variable | Con typedef delante | Resultado |
|---|---|---|
unsigned long contador; | typedef unsigned long contador_t; | contador_t es otro nombre de unsigned long. |
int (*f)(int); | typedef int (*funcion_t)(int); | funcion_t es «puntero a función de int en int». |
char nombre[32]; | typedef char nombre_t[32]; | nombre_t es un arreglo de 32 caracteres. |
struct proceso p; | typedef struct proceso proceso_t; | proceso_t es struct proceso. |
Lo más importante de typedef es lo que no hace: no crea un tipo distinto. El alias y el original son exactamente el mismo tipo, y el compilador los mezcla sin quejarse. Es una diferencia con las estructuras, que sí son tipos distintos, como vimos en el tema 24:
#include <stdio.h>
typedef unsigned long contador_t;
typedef int metros_t;
typedef int pies_t;
struct metros { double valor; };
struct pies { double valor; };
int main(void)
{
contador_t a = 5;
unsigned long *p = &a; /* es el mismo tipo: sin conversión */
metros_t m = 100;
pies_t f = m; /* son el mismo tipo: compila sin aviso */
int *q = &f;
printf("a=%lu *p=%lu m=%d f=%d *q=%d\n", a, *p, m, f, *q);
printf("sizeof(contador_t) = %zu, sizeof(struct metros) = %zu\n",
sizeof(contador_t), sizeof(struct metros));
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o alias alias.c && ./alias
a=5 *p=5 m=100 f=100 *q=100
sizeof(contador_t) = 8, sizeof(struct metros) = 8
Asignar un metros_t a un pies_t es perfectamente válido, porque los dos son int; asignar un struct metros a un struct pies, no. Por eso un typedef sirve para documentar y centralizar, no para proteger. Sirve para tres cosas:
- Dar significado.
contador_tdice qué guarda la variable, más queunsigned long. - Centralizar una decisión. Si el programa usa
contador_ten todas partes, cambiarlo aunsigned long longes una sola línea. Es la razón por la que existensize_topid_t(sección 11). - Acortar declaraciones difíciles, sobre todo con punteros a funciones:
typedef int (*comparador_t)(const void *, const void *).
Si se quiere que el compilador distinga metros de pies, se envuelve cada valor en una estructura de un solo campo, como hicimos con struct metros y struct pies: tiene el mismo tamaño y no cuesta nada en tiempo de ejecución.
8. typedef y las estructuras
El uso más común es evitar escribir struct cada vez. Hay tres formas de combinarlos:
/* 1. etiqueta y alias: se puede usar struct proceso o proceso_t */
typedef struct proceso {
int pid;
char nombre[16];
} proceso_t;
/* 2. estructura anónima: solo existe el alias */
typedef struct {
int x, y;
} punto_t;
/* 3. declarar el alias primero: permite referirse al tipo antes de definirlo */
typedef struct nodo nodo_t;
struct nodo {
int valor;
nodo_t *siguiente; /* con la forma 2 no se podría */
};
La forma 1 es la más flexible. La forma 2 es la más corta, pero una estructura sin etiqueta no puede referirse a sí misma, ni declararse por adelantado. La forma 3, la de las listas, usa el alias para el puntero al propio tipo. También se aplica a las uniones y las enumeraciones, como en la sección 12: typedef enum { … } tipo_evento_t;.
Si typedef evita escribir struct, ¿por qué seguimos escribiéndolo en los temas 24 y 25? Porque es una cuestión de estilo en la que hay dos escuelas:
- La convención del núcleo Linux desaconseja el
typedefde estructuras: verstruct procesoen el código indica de inmediato que es una estructura, que se pasa preferentemente por puntero, que tiene campos que se pueden mirar. Un nombre comoproceso_toculta todo eso. - La convención de muchas bibliotecas usa un alias para toda estructura que forme parte de la interfaz pública, sobre todo si es opaca (sección 10).
En este curso se usa struct x para las estructuras con campos visibles, y typedef para los punteros a funciones, las enumeraciones usadas como tipos, los tipos numéricos con significado y los tipos opacos. Ambas formas son correctas; lo que importa es ser coherente en cada programa. Los nombres que terminan en _t son un caso aparte: POSIX los reserva para las implementaciones, de modo que un programa que define su propio proceso_t en un entorno POSIX puede chocar con un tipo del sistema en el futuro; en un programa chico no ocurre nada, y los proyectos grandes usan otro sufijo o un prefijo propio.
9. La trampa de typedef con punteros
Un typedef no es una sustitución de texto, como un #define. El alias es un tipo completo, y eso cambia el significado de const y del asterisco cuando el tipo es un puntero:
#include <stdio.h>
typedef char *cadena;
int main(void)
{
char buffer[] = "hola";
char otro[] = "chau";
const char *a = buffer; /* puntero a char constante */
const cadena b = buffer; /* ¿lo mismo? */
a = otro; /* permitido: a no es constante */
b[0] = 'H'; /* permitido: ¡los char no son constantes! */
b = otro; /* error: es el puntero lo que es constante */
printf("%s %s\n", a, b);
return 0;
}
$ gcc -std=c17 -Wall -Wextra -g -o trampa trampa.c
trampa.c: In function ‘main’:
trampa.c:15:7: error: assignment of read-only variable ‘b’
15 | b = otro; /* error: es el puntero lo que es constante */
| ^
Con un #define CADENA char *, const CADENA b se expandiría a const char * b. Pero cadena es «puntero a char» como un solo tipo, y un const aplicado a ese tipo vuelve constante el puntero: const cadena b es char *const b, la segunda forma del tema 19. El compilador acepta modificar b[0] y rechaza reasignar b, lo contrario de lo que se pretendía. Es una trampa que atrapa incluso a quien conoce bien const. De ahí la regla: no se ocultan punteros dentro de un typedef. Si hace falta un alias, se define para el tipo apuntado, typedef char caracter_t;, y el asterisco y el const se escriben donde se ven: const caracter_t *. Las excepciones son los punteros a funciones, donde la sintaxis exige el alias para ser legible, y los tipos opacos que se pasan siempre por puntero, como veremos.
10. Tipos incompletos y tipos opacos
Un tipo incompleto es uno que se declaró pero todavía no se definió: el compilador sabe que existe, pero no cuánto mide ni qué campos tiene. Sobre un tipo incompleto se puede declarar un puntero, que mide siempre lo mismo, y pasarlo; no se puede declarar una variable, ni usar sizeof, ni acceder a un campo.
#include <stdio.h>
typedef struct contador contador; /* declaración: el tipo existe, pero está incompleto */
static void mostrar(const contador *c); /* se pueden pasar punteros a un tipo incompleto */
static int leer(const contador *c)
{
return c->valor; /* ...pero no mirar adentro */
}
struct contador {
int valor;
};
static void mostrar(const contador *c)
{
printf("valor = %d (sizeof = %zu)\n", c->valor, sizeof *c);
}
int main(void)
{
contador c = {5};
mostrar(&c);
return leer(&c) == 5 ? 0 : 1;
}
$ gcc -std=c17 -Wall -Wextra -g -o incompleto incompleto.c
incompleto.c: In function ‘leer’:
incompleto.c:9:13: error: invalid use of incomplete typedef ‘contador’
9 | return c->valor; /* ...pero no mirar adentro */
| ^~
El error está en leer, que mira un campo antes de que la estructura se defina. mostrar, en cambio, está después de la definición y compila bien. Esto permite una técnica de diseño llamada tipo opaco: un módulo publica en su cabecera solo la declaración incompleta, typedef struct cola cola_t;, y las funciones que operan sobre un cola_t *. La definición de la estructura queda en el archivo fuente del módulo, y quien lo usa no puede ver los campos ni declarar variables de ese tipo: solo recibir y devolver punteros. La ventaja es que el módulo puede cambiar su implementación sin que nadie tenga que recompilar, y los usuarios no pueden depender de los detalles internos. La contrapartida es que los objetos tienen que crearlos las funciones del módulo, con memoria dinámica, que veremos en el tema 28.
Los tipos opacos están en toda la biblioteca de C. FILE es el más conocido:
$ grep -n -B1 'typedef struct _IO_FILE FILE' /usr/include/x86_64-linux-gnu/bits/types/FILE.h
6-/* The opaque type of streams. This is the definition used elsewhere. */
7:typedef struct _IO_FILE FILE;
El programa trabaja siempre con un FILE *, que devuelve fopen y reciben fgets y fclose. En glibc la estructura sí se define en otra cabecera, y mide 216 bytes, pero el comentario dice «opaque» por una razón: el estándar no promete nada sobre sus campos, y ningún programa portable los toca. Lo mismo ocurre con DIR, para recorrer directorios, y con pthread_t, cuya representación POSIX tampoco especifica. Es el diseño que conviene imitar al crear módulos.
11. Los tipos del sistema
Los tipos que vimos usar a lo largo del curso, size_t, pid_t, off_t, time_t, no son tipos propios del lenguaje: son alias, definidos con typedef en las cabeceras del sistema. Este programa pregunta por el tamaño y el signo de los más comunes:
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <time.h>
#include <sys/types.h>
#define MOSTRAR(T) \
printf("%-9s %zu bytes, %s\n", #T, sizeof(T), ((T) -1 < 1) ? "con signo" : "sin signo")
int main(void)
{
MOSTRAR(size_t);
MOSTRAR(ssize_t);
MOSTRAR(ptrdiff_t);
MOSTRAR(pid_t);
MOSTRAR(uid_t);
MOSTRAR(gid_t);
MOSTRAR(mode_t);
MOSTRAR(off_t);
MOSTRAR(ino_t);
MOSTRAR(time_t);
MOSTRAR(clock_t);
MOSTRAR(uint32_t);
return 0;
}
$ ./sistema_tipos
size_t 8 bytes, sin signo
ssize_t 8 bytes, con signo
ptrdiff_t 8 bytes, con signo
pid_t 4 bytes, con signo
uid_t 4 bytes, sin signo
gid_t 4 bytes, sin signo
mode_t 4 bytes, sin signo
off_t 8 bytes, con signo
ino_t 8 bytes, sin signo
time_t 8 bytes, con signo
clock_t 8 bytes, con signo
uint32_t 4 bytes, sin signo
Para saber qué es cada uno, se sigue la cadena de alias en las cabeceras. pid_t no es un int directamente, sino el final de una cadena:
$ grep -n 'typedef.*\bpid_t\b' /usr/include/x86_64-linux-gnu/sys/types.h
97:typedef __pid_t pid_t;
$ grep -n '__pid_t;' /usr/include/x86_64-linux-gnu/bits/types.h
154:__STD_TYPE __PID_T_TYPE __pid_t; /* Type of process identifications. */
$ grep -n '__PID_T_TYPE' /usr/include/x86_64-linux-gnu/bits/typesizes.h
53:#define __PID_T_TYPE __S32_TYPE
$ grep -n 'typedef.*size_t;' /usr/lib/gcc/x86_64-linux-gnu/*/include/stddef.h | head -1
229:typedef __SIZE_TYPE__ size_t;
pid_t es __pid_t, definido por una macro de la implementación como un entero de 32 bits con signo, es decir, int. size_t lo define el compilador mismo con __SIZE_TYPE__, que en esta plataforma es unsigned long. Tanta indirección tiene un propósito: que el código fuente compile y funcione en cualquier sistema, aunque los tipos reales cambien. Un programa que declara pid_t p = fork(); compila con el tipo que cada sistema eligió; si mañana un núcleo necesita PID de 64 bits, se cambia la cabecera y los programas se recompilan sin modificarse. Es lo que pasó con time_t: en los sistemas de 32 bits fue de 32 bits y se agota en enero de 2038, y los sistemas actuales lo ampliaron a 64 sin tocar los programas que lo usan correctamente.
De la salida salen cuatro reglas para usarlos:
- Usar el tipo, no el que parece ser. Un PID se guarda en
pid_t, no enint; un tamaño de archivo, enoff_t; un tiempo, entime_t. Es lo que dicen las funciones que los devuelven. - No suponer el signo.
pid_ttiene signo, y por esoforkpuede devolver −1;uid_tno, y el valor «ningún usuario» se escribe(uid_t) -1, el más grande. Comparar con 0 sin mirar el signo da resultados distintos para cada tipo. - No suponer el tamaño al imprimir.
%dcon unpid_tfunciona hoy y deja de hacerlo si el tipo se ensancha. El truco habitual es convertir a un tipo ancho:printf("%jd\n", (intmax_t) pid). Parasize_texiste%zu, como vimos en el tema 5; para los enteros destdint.h, las macrosPRId64y compañía. - En los formatos binarios, los tipos de tamaño exacto de
stdint.h, como en el tema 25: los del sistema cambian entre plataformas por diseño.
12. Todo junto: una tabla de despacho de eventos
El último programa combina las tres herramientas con lo visto antes: una enumeración para el tipo de cada evento, una unión etiquetada para sus datos, una estructura que las agrupa, y una tabla de punteros a funciones indexada por la enumeración, como la de calc.c del tema 20. Cada evento se procesa con la función que le corresponde:
#include <stdio.h>
typedef enum {
EV_TECLA,
EV_TIEMPO,
EV_SENAL,
EV_FIN,
N_EVENTOS
} tipo_evento_t;
typedef struct {
tipo_evento_t tipo;
union {
char tecla;
struct {
long segundos;
long nanos;
} tiempo;
int senal;
};
} evento_t;
typedef void (*manejador_t)(const evento_t *);
static void en_tecla(const evento_t *e)
{
printf("tecla '%c'\n", e->tecla);
}
static void en_tiempo(const evento_t *e)
{
printf("tiempo %ld.%09ld s\n", e->tiempo.segundos, e->tiempo.nanos);
}
static void en_senal(const evento_t *e)
{
printf("señal %d\n", e->senal);
}
/* la tabla se indexa con el enum; EV_FIN no tiene manejador */
static const manejador_t manejadores[N_EVENTOS] = {
[EV_TECLA] = en_tecla,
[EV_TIEMPO] = en_tiempo,
[EV_SENAL] = en_senal,
};
static int despachar(const evento_t *e)
{
if ((unsigned) e->tipo >= N_EVENTOS || manejadores[e->tipo] == NULL)
return -1;
manejadores[e->tipo](e);
return 0;
}
int main(void)
{
const evento_t cola[] = {
{.tipo = EV_TECLA, .tecla = 'q'},
{.tipo = EV_TIEMPO, .tiempo = {.segundos = 3, .nanos = 250000000}},
{.tipo = EV_SENAL, .senal = 15},
{.tipo = (tipo_evento_t) 9},
{.tipo = EV_FIN},
};
printf("sizeof(evento_t) = %zu\n", sizeof(evento_t));
for (size_t i = 0; i < sizeof cola / sizeof cola[0]; i++) {
if (cola[i].tipo == EV_FIN) {
printf("fin\n");
break;
}
if (despachar(&cola[i]) != 0)
printf("evento inválido: %d\n", (int) cola[i].tipo);
}
return 0;
}
$ ./eventos
sizeof(evento_t) = 24
tecla 'q'
tiempo 3.250000000 s
señal 15
evento inválido: 9
fin
Cada decisión de diseño se apoya en un punto del tema:
- Los alias.
tipo_evento_t,evento_tymanejador_tevitan escribirenum,structy la declaración del puntero a función. Los tres son de los usos recomendados de la sección 8. - La unión anónima con una estructura anónima dentro:
e->tiempo.segundoses un miembro de una estructura que a su vez es miembro de una unión. El evento mide 24 bytes: 4 de la etiqueta, 4 de relleno, y 16 de la variante más grande, el tiempo. - La tabla indexada por el
enumreemplaza a unswitchde cuatro ramas: agregar un evento es escribir su función y una línea en la tabla, como encalc.c.EV_FINno tiene manejador, y su posición queda enNULL. - La validación. El cuarto evento tiene tipo 9, que ninguna constante representa; C lo permite sin avisar, y el programa lo recibe de una cola, que podría venir de afuera.
despacharcomprueba que el índice esté en rango, y que haya un manejador, antes de llamar a la función: sin esa comprobación, indexar la tabla con 9 leería memoria fuera del arreglo y llamaría a una dirección cualquiera. El(unsigned)evita comparar un valor posiblemente negativo. - Sin
defaulty con-Wswitchcomo aliado: si el programa usara unswitchsobretipo_evento_ten lugar de la tabla, el compilador avisaría de cada evento nuevo sin manejar.
13. Uniones y enumeraciones en gdb
El depurador muestra las constantes por su nombre y la unión con todos sus miembros a la vez, lo que deja ver, de un vistazo, que son los mismos bytes:
$ gdb -q -batch -ex 'break despachar' -ex run -ex 'p *e' -ex 'p e->tipo' -ex 'p/d e->tipo' \
-ex 'p (tipo_evento_t) 2' -ex 'p/d N_EVENTOS' -ex 'ptype evento_t' -ex 'whatis manejadores' \
-ex continue -ex 'p e->tiempo' -ex 'p sizeof(evento_t)' ./eventos
Breakpoint 1, despachar (e=0x7fffffffd760) at eventos.c:49
49 if ((unsigned) e->tipo >= N_EVENTOS || manejadores[e->tipo] == NULL)
$1 = {tipo = EV_TECLA, {tecla = 113 'q', tiempo = {segundos = 113, nanos = 0}, senal = 113}}
$2 = EV_TECLA
$3 = 0
$4 = EV_SENAL
$5 = 4
type = struct {
tipo_evento_t tipo;
union {
char tecla;
struct {...} tiempo;
int senal;
};
}
type = const manejador_t [4]
Breakpoint 1, despachar (e=0x7fffffffd778) at eventos.c:49
49 if ((unsigned) e->tipo >= N_EVENTOS || manejadores[e->tipo] == NULL)
$6 = {segundos = 3, nanos = 250000000}
$7 = 24
p *e muestra los tres miembros de la unión con el mismo contenido: tecla = 113 'q', senal = 113, y los segundos = 113 del tiempo, porque el carácter 'q' es el byte 113 y los demás bytes de la unión son cero. Es la forma más directa de entender qué es una unión. p e->tipo imprime EV_TECLA y no 0, y p/d fuerza el número; p (tipo_evento_t) 2 convierte un entero a su nombre; ptype muestra la estructura con su unión anónima, y whatis manejadores el tipo del arreglo de punteros a funciones. Con ptype /o evento_t, del tema 25, se ven los desplazamientos: la etiqueta en 0, un hueco de 4 bytes, y la unión de 16 bytes en el 8.
14. Errores frecuentes
| Error | Qué pasa | Corrección |
|---|---|---|
| Leer de una unión un miembro distinto del escrito, sin etiqueta | Se reinterpretan los bytes; con bytes no escritos, el resultado es basura. | Una unión etiquetada, que se consulta antes de leer. |
| Olvidar actualizar la etiqueta al cambiar el miembro | La etiqueta miente: se lee el miembro equivocado. | Constructores o funciones que fijen las dos cosas a la vez. |
Reinterpretar con *(uint32_t *) &f | Viola el aliasing estricto: comportamiento indefinido. | memcpy o una unión. |
| Suponer que el tamaño de una unión es el del miembro mayor | Se redondea a la alineación: 16, no 13. | sizeof(union …). |
Contar con que una variable enum solo vale sus constantes | C acepta cualquier entero; un índice inválido sale de la tabla. | Validar los valores que vienen de afuera. |
default en un switch que debe cubrir todo el enum | Oculta el aviso -Wswitch cuando se agrega una constante. | Sin default; validar aparte. |
| Nombres de constantes sin prefijo | Chocan con otros del programa o del sistema. | EV_, E_, un prefijo por enumeración. |
Guardar una combinación de banderas en una variable enum | El valor no es ninguna constante del tipo. | Un unsigned con las operaciones de bits. |
Contar con que typedef crea un tipo distinto | metros_t y pies_t se mezclan sin aviso. | Envolver el valor en una estructura. |
typedef char *cadena; y const cadena | Es char *const, no const char *. | No ocultar punteros en un typedef. |
Estructura anónima con typedef y un puntero a sí misma | No tiene nombre: no se puede referir a sí misma. | Una etiqueta: typedef struct nodo nodo_t;. |
| Acceder a un campo de un tipo incompleto | invalid use of incomplete typedef. | Definir la estructura antes del uso, o usar solo punteros. |
Imprimir pid_t, off_t, time_t con %d o %ld | Depende de la plataforma. | Convertir a intmax_t y usar %jd. |
15. Resumen
- Una unión pone todos sus miembros en el mismo lugar: cada uno está en el desplazamiento 0, y la unión mide lo del miembro mayor, redondeado a su alineación. Escribir uno cambia los demás.
- Leer un miembro distinto del escrito reinterpreta los bytes; con una unión es válido en C y sirve para inspeccionar la representación de los números.
*(tipo *) &xviola el aliasing estricto y es comportamiento indefinido;memcpyy las uniones son correctas, ymemcpyno cuesta nada.- Una unión etiquetada junta una enumeración y una unión en una estructura: la etiqueta dice qué miembro es el válido, y se consulta siempre antes de leer. Las uniones anónimas, de C11, simplifican el acceso.
- El núcleo usa uniones para datos opacos de contexto:
epoll_data_tyunion sigval. - Una enumeración da nombre a constantes enteras: parten de 0 y crecen de a uno, salvo que se asigne un valor. Sus constantes son
int, no tienen ámbito propio, y el compilador elige el tipo. - C no restringe los valores de una variable enumerada: hay que validar los que vienen de afuera.
-Wswitchavisa de unswitchincompleto, y-Wenum-conversion, de mezclar enumeraciones; ambos pierden valor con undefault. - Patrones de
enum: una constante final con la cantidad, arreglos indexados por la enumeración, y banderas de potencias de 2 combinadas con operadores de bits. typedefcrea un alias, no un tipo nuevo: documenta y centraliza, pero no protege. Se escribe como la declaración de una variable contypedefdelante.- No se ocultan punteros en un
typedef:const cadenacontypedef char *cadenaeschar *const. - Un tipo incompleto admite punteros pero no acceso a campos ni
sizeof; sobre él se construyen los tipos opacos, comoFILE. - Los tipos del sistema,
size_t,pid_t,off_t,time_t, sontypedefde las cabeceras: se usan por nombre, sin suponer su signo ni su tamaño, y se imprimen convertidos a un tipo ancho.
16. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuánto mide union { char texto[13]; int i; double d; }, y por qué?
Mide 16 bytes. El miembro más grande es el arreglo de 13 caracteres, pero la alineación de la unión es la del miembro más exigente, el double, que es 8; el tamaño se redondea hasta un múltiplo de 8, que es 16.
2. Si se asigna n.f = 1.0f en una unión con un miembro int i, ¿por qué n.i no vale 1?
Porque no hay conversión: los dos miembros comparten los mismos bytes. n.i lee, como entero, los bytes con que se representa 1.0 como número de coma flotante, que forman 0x3f800000, es decir, 1 065 353 216.
3. ¿Cuál es la diferencia entre el type punning con una unión y con un cast de punteros?
La unión es válida en C: leer otro miembro reinterpreta los bytes. El cast de puntero, *(uint32_t *) &f, accede a un float a través de un uint32_t *, lo que viola el aliasing estricto y es comportamiento indefinido, aunque a veces funcione. memcpy es la tercera forma válida y la más portable.
4. ¿Para qué sirve la etiqueta de una unión etiquetada?
Para saber cuál miembro de la unión tiene un valor válido. La unión no guarda esa información; el programa la guarda en un campo aparte, normalmente una enumeración, y la consulta antes de leer cualquier miembro. Sin ella no hay forma de distinguir un entero de un real que ocupan los mismos bytes.
5. ¿Por qué struct epoll_event mide 12 bytes y no 16?
Porque en x86-64 está declarada con __attribute__((packed)), sin relleno: events ocupa 4 bytes y data, de 8, empieza en el desplazamiento 4. Es por compatibilidad binaria con el diseño que el núcleo usa desde sus primeras versiones, incluida la de los programas de 32 bits.
6. ¿Qué valor tiene cada constante en enum { A, B = 5, C, D = C + 10 }?
A vale 0; B vale 5, por la asignación explícita; C continúa con 6; y D es C + 10, es decir, 16.
7. ¿Qué pasa si se asigna 42 a una variable de tipo enum color que solo tiene tres constantes?
Nada: C lo acepta sin avisar, porque el tipo enumerado es un entero. La variable puede valer cualquier entero que quepa en el tipo. Por eso los valores que vienen de afuera se validan antes de usarlos como índice o como entrada de un switch.
8. ¿Qué avisa -Wswitch, y por qué no conviene poner default en ese caso?
Avisa de que un switch sobre un valor enumerado no cubre todas las constantes. Es útil al agregar una constante nueva, porque el compilador señala cada switch que falta actualizar. Una rama default cubre todos los casos para el compilador y anula el aviso.
9. ¿Por qué una variable de banderas se declara unsigned y no del tipo enum?
Porque una combinación como P_LEER | P_ESCRIBIR vale 3, que no es ninguna de las constantes de la enumeración. La enumeración solo nombra los bits; la variable guarda el resultado de operaciones con ellos.
10. ¿Un typedef int metros_t; y un typedef int pies_t; son tipos distintos?
No. Un typedef crea otro nombre para el mismo tipo: los dos son int y se mezclan sin aviso. Para que el compilador los distinga hay que envolverlos en estructuras de un campo, como struct metros y struct pies.
11. Con typedef char *cadena;, ¿qué significa const cadena s;?
Significa char *const s: un puntero constante a char modificable. Se puede escribir s[0] = 'x' y no se puede reasignar s, lo contrario de const char *s. Por eso no se ocultan punteros dentro de un typedef.
12. ¿Qué ventaja da un tipo opaco, y cómo se declara?
Se declara en la cabecera solo como tipo incompleto, typedef struct cola cola_t;, con funciones que usan cola_t *; la definición de los campos queda en el archivo fuente del módulo. Quien lo usa no puede mirar los campos ni declarar variables, así que el módulo puede cambiar su implementación sin afectar a nadie. FILE y DIR son ejemplos.
13. ¿Por qué las cabeceras definen pid_t con una cadena de typedef en lugar de usar int?
Para que el mismo código fuente compile en cualquier sistema, aunque el tipo real cambie. Si un sistema necesita PID más grandes, se cambia la definición en la cabecera, y los programas se recompilan sin modificarse. Lo mismo ocurrió con time_t, ampliado a 64 bits.
14. En eventos.c, ¿por qué despachar comprueba el índice antes de usar la tabla?
Porque un valor de tipo enumerado puede valer cualquier entero, y un evento recibido de afuera puede traer un tipo que no existe, como el 9. Indexar la tabla con ese valor leería memoria fuera del arreglo y llamaría a una dirección cualquiera. La comprobación del rango y del NULL convierte ese caso en un error manejable.
Para practicar
- Escribe una unión que permita ver un
uint32_tcomo cuatrouint8_ty úsala para decidir si tu máquina es little endian. Escribe después una funciónuint32_t invertir(uint32_t)que invierta el orden de sus bytes con la unión, y compárala con__builtin_bswap32. - Extiende
bits.cparadouble(64 bits: 1 de signo, 11 de exponente, 52 de mantisa) y muestra el valor de0.1,0.2y0.1 + 0.2. ¿En qué se diferencian los bits de0.1 + 0.2y0.3? - Agrega a
etiquetada.cun tipoT_LISTAcuyo miembro sea un puntero a un arreglo destruct valory su cantidad. Escribemostrarde forma recursiva. Comprueba que-Wswitchte avisa antes de agregar el caso. - Escribe una función
int igual(const struct valor *a, const struct valor *b)que compare dos valores etiquetados, y explica por quémemcmpno sirve aquí (piensa en el relleno y en los miembros de distinto tamaño). - Amplía
estados.cpara que, sin argumentos, lea todos los procesos de/proc(conopendiryreaddir,man 3 readdir) y muestre cuántos hay en cada estado, usando el arreglo indexado por la enumeración. - Define una enumeración de banderas
enum opcion { OPT_VERBOSE = 1, OPT_FORZAR = 2, OPT_RECURSIVO = 4 }, procesa congetopt(tema 23) las opciones-v,-fy-r, y muestra qué combinación quedó encendida. - Escribe un tipo opaco
contador_ten dos archivos: una cabecera con la declaración incompleta y las funcionescrear,incrementar,valorydestruir, y un archivo fuente con la definición. Hazlo con memoria dinámica cuando termines el tema 28, y compila los dos archivos por separado. - Agrega a
eventos.cun eventoEV_RATONcon dos coordenadas y comprueba cuántas líneas hay que tocar. ¿Qué pasa si te olvidas de agregar su manejador a la tabla?