El comportamiento indefinido (UB, por sus siglas en inglés) es un incumplimiento del contrato entre tú y la máquina abstracta de C. Cuando realizas una operación que el estándar no define —como desbordar un entero con signo o dereferenciar un puntero nulo—, dejas de tener control sobre el programa. El compilador, al ver que has roto las reglas, no está obligado a intentar que tu código “funcione”; de hecho, el optimizador moderno (como GCC o Clang con -O2 o -O3) asume que el UB nunca ocurre.
Esta suposición es el motor que permite transformaciones asombrosas. Para optimizar, el compilador construye un grafo de flujo de control (CFG). Si una rama de código lleva a un estado de UB, el compilador la marca como “imposible” y la poda de forma agresiva. Si una comprobación de seguridad (como if (p != NULL)) protege una operación que, según el estándar, solo puede realizarse si el puntero es válido, el compilador simplemente elimina la comprobación por considerarla redundante. Esto es extremadamente eficiente, pero es una trampa mortal: si llegas a ejecutar esa ruta de código, el programa puede comportarse de forma errática o ser despojado de su lógica original.
¿Cuándo deberías usar esto? Nunca. El UB no es una herramienta de optimización para el programador, es un error de diseño. Si lo usas para “ahorrar” una comprobación, estás jugando a la ruleta rusa con el compilador. Si el compilador detecta que una ruta lógica implica UB, lo que “rompe” es la semántica de tu programa: el compilador puede decidir que esa parte del código simplemente no existe, eliminando bloques enteros de lógica que parecen perfectamente válidos a simple vista.
#include <stdio.h>
#include <stdint.h>
/* UB 1: Elusión de comprobación de puntero nulo
* El compilador observa el dereferenciamiento de 'p' al final.
* Como dereferenciar NULL es UB, el compilador asume que 'p'
* nunca puede ser NULL en este contexto. */
void check_null_elision(int *p) {
if (p == NULL) {
/* En -O3, esta sección es código muerto.
El compilador elimina el 'if' porque sabe que 'p' es válido. */
printf("Error: p es NULL (Esto no se imprimirá en release)\n");
return;
}
*p = 100;
}
/* UB 2: Bucle infinito por desbordamiento de signo
* En C, el desbordamiento de un entero con signo es UB.
* El compilador asume que 'i' nunca desbordará hacia un valor negativo.
* Por lo tanto, la condición 'i >= 0' es siempre verdadera. */
void signed_overflow_loop() {
for (int i = 0; i >= 0; i++) {
// En -O3, este bucle se convierte en un 'while(1)' infinito
// porque el compilador descarta la posibilidad de que 'i' sea negativo.
if (i == 2147483647) break;
}
}
/* UB 3: Eliminación de código muerto por UB en lógica de control
* El compilador puede determinar que ciertas condiciones son
* imposibles de alcanzar debido a que activarían un UB. */
int logic_destruction(int x) {
if (x > 1000) {
/* Si el compilador analiza que 'x + 2147483647' causará
desbordamiento de signo, puede asumir que este bloque
nunca se ejecuta para mantener la integridad del modelo. */
x = x + 2147483647;
}
return x;
}
int main(void) {
int val = 0;
int b = 500;
int c = 1001;
printf("--- Probando elusión de NULL ---\n");
check_null_elision(NULL);
printf("--- Probando lógica destruida ---\n");
// Si compilas con -O3, el segundo printf mostrará valores inesperados
// porque el compilador ha "limpiado" el código que considera imposible.
printf("Valor 500 -> %d\n", logic_destruction(b));
printf("Valor 1001 -> %d\n", logic_destruction(c));
printf("\nNota: Ejecuta con -O3 para ver la magia (y el desastre).\n");
printf("Nota: Descomenta 'signed_overflow_loop()' para ver un loop infinito.\n");
return 0;
}
Desglose del comportamiento
Analicemos qué está ocurriendo exactamente en el código anterior:
En check_null_elision, el compilador realiza un análisis de flujo de datos. Al ver la instrucción *p = 100;, el compilador sabe que, por definición de la máquina abstracta de C, p debe ser un puntero válido y no NULL. Al rastrear hacia atrás, el compilador concluye que la comprobación if (p == NULL) es una condición que nunca se cumplirá. Por tanto, elimina el salto (jump) y la llamada a printf, dejando solo la asignación.
En signed_overflow_loop, la clave es la naturaleza del tipo int. A diferencia de los tipos unsigned (cuyo desbordamiento tiene una semántica de vuelta al cero definida), el desbordamiento de un int es indefinido. Si el optimizador ve i >= 0 y sabe que i aumenta, asume que i nunca cruzará el umbral hacia los negativos mediante desbordamiento. El resultado es que el compilador puede transformar ese bucle en un jump incondicional, convirtiéndolo en un bucle infinito que nunca alcanzará el break.
En logic_destruction, el compilador aplica una técnica llamada dead code elimination basada en UB. Si puede probar que una rama de ejecución (como x > 1000 seguido de una suma que desborda) viola las reglas del lenguaje, simplemente elimina esa rama del grafo de ejecución. El valor de retorno de la función se optimiza para devolver directamente el valor de entrada, asumiendo que la rama de desbordamiento es un “camino fantasma”.
El error frecuente
El problema más difícil de depurar es el fenómeno de “funciona en debug, pero falla en release”.
Cuando compilas con -g (sin optimización), el compilador traduce tus líneas de C de forma casi literal. Si tienes un desbordamiento o un puntero nulo, el programa fallará de forma predecible (posiblemente un Segmentation Fault). Sin embargo, al compilar con -O2 o -O3, el optimizador “limpia” tu lógica basándose en sus asunciones de que el UB no existe.
// El error: confiar en la seguridad de un chequeo que el optimizador eliminará
if (ptr != NULL) {
do_something(ptr);
}
ptr->value = 10; // UB: El compilador asume que ptr NO es NULL por la línea anterior.
Si intentas usar AddressSanitizer (-fsanitize=address) o UndefinedBehaviorSanitizer (-fsanitize=undefined), detectarás el error en el momento exacto en que ocurre. Si no los usas y confías en la compilación estándar, el error se manifestará como un comportamiento errático, un bucle infinito que solo aparece en producción, o un valor de retorno “imposible”.
N° 79