DEV Community

Cover image for K&R C: rarezas, punteros nulos y el bucle for hasta C++20
lu1tr0n
lu1tr0n

Posted on • Originally published at elsolitario.org

K&R C: rarezas, punteros nulos y el bucle for hasta C++20

Una pregunta en Stack Overflow sobre el operador --> en C acumula más de 10.000 votos, y la respuesta es que ese operador no existe: es -- seguido de >. Ese malentendido resume bien el problema de este artículo.

K&R C dejó reglas de sintaxis que sobreviven casi medio siglo después, desde ambigüedades en los punteros nulos hasta cuatro rediseños distintos del bucle for, el último de ellos formalizado en C++20.

TL;DR

  • Vas a entender por qué el operador --> en C, con más de 10.000 votos en Stack Overflow, en realidad es -- seguido de >.- Vas a saber por qué NULL y nullptr no son intercambiables en la resolución de sobrecargas de C++ desde C++11.- Vas a poder comparar la sintaxis del bucle for en K&R C, C99, C++11 y C++20 con ejemplos de código reales.- Vas a poder compilar el mismo programa con -std=c89, -std=c99 y -std=c++20 y ver las diferencias con tus propios ojos.- Vas a conocer Duff's device, la técnica de 1983 que combina switch y do-while para desenrollar bucles.- Vas a aprender a detectar un deref de puntero nulo con el sanitizer de undefined behavior de gcc.- Vas a distinguir cuándo declarar la variable del for por fuera (K&R) o por dentro (C99) cambia su alcance real.

Qué es K&R C y por qué sigue importando

K&R C es el nombre informal del dialecto de C descrito en el libro de Brian Kernighan y Dennis Ritchie, publicado en 1978. No había un estándar oficial todavía: el libro mismo funcionaba como especificación de facto para miles de programadores.

Ese dialecto es más permisivo que el C moderno. No exigía prototipos de función, asumía tipos por convención en vez de forzarlos, y dejaba huecos donde hoy el compilador levantaría un error. Entender los punteros nulos y la forma actual del bucle for requiere primero entender de dónde viene el lenguaje.

El comité ANSI estandarizó C en 1989 (C89), corrigiendo buena parte de esas ambigüedades. Pero algunas rarezas de la era K&R sobrevivieron como curiosidades de sintaxis, y otras, como el problema del puntero nulo, nunca se resolvieron del todo: simplemente se les puso una capa de sintaxis más segura encima.

Las rarezas de K&R C que todavía sorprenden

Int implícito y funciones sin prototipo

En K&R C, si no declarabas un tipo de retorno, el compilador asumía int. Y los parámetros de una función se tipaban en una línea aparte, después de la lista de nombres:

/* estilo K&R: los tipos de los parametros van despues de la lista */
suma(a, b)
    int a, b;
{
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Este código compila en muchos compiladores todavía si les pedís el modo K&R o gnu89 con relajaciones. No hay tipo de retorno explícito (se asume int), y los tipos de a y b se declaran en una línea separada antes de la llave de apertura. Con -std=c99 o superior, esta sintaxis directamente no compila.

El operador que no existe: -->

El ejemplo más citado de rareza sintáctica de C es este bucle:

#include 

int main(void) {
    int n = 5;

    while (n --> 0) {
        printf("%d\n", n);
    }

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

No existe un operador --> ("va hacia") en la gramática de C. El compilador lee n-- (post-decremento) seguido de > 0 (comparación). El resultado es un bucle perfectamente válido que imprime 4, 3, 2, 1 y 0. Es la ausencia de espacios la que crea la ilusión visual de una flecha.

⚠️ Ojo: que el compilador acepte n --> 0 no significa que debas escribirlo así en código real. Es un buen ejemplo para entender tokenización, pero en producción rompe la legibilidad sin ninguna ventaja de rendimiento.

Duff's device: el switch que se mete en un bucle

La rareza más extrema de la era K&R es Duff's device, escrita por Tom Duff en 1983 para acelerar la copia de píxeles en una terminal gráfica:

void copiar(short *destino, short *origen, int cuenta) {
    int n = (cuenta + 7) / 8;
    switch (cuenta % 8) {
        case 0: do { *destino++ = *origen++;
        case 7:      *destino++ = *origen++;
        case 6:      *destino++ = *origen++;
        case 5:      *destino++ = *origen++;
        case 4:      *destino++ = *origen++;
        case 3:      *destino++ = *origen++;
        case 2:      *destino++ = *origen++;
        case 1:      *destino++ = *origen++;
                } while (--n > 0);
    }
}
Enter fullscreen mode Exit fullscreen mode

El truco entrelaza un switch con un do-while: el case inicial salta directamente al medio del cuerpo del bucle, según el resto de la división por 8, y desde ahí el do-while sigue ejecutando las copias restantes. Esto permitía desenrollar un bucle de copia sin escribir código repetitivo por separado.
Duff's device de 1983 combina switch y do-while en una sola función.

El problema de los punteros nulos

El problema de los punteros nulos en C empieza con una decisión de diseño simple: un puntero nulo representa "no apunta a ningún objeto válido", pero el lenguaje no impide que lo desreferencies. Hacerlo es comportamiento indefinido (undefined behavior), no un error garantizado.

En C, la macro NULL suele expandirse a ((void*)0), y el compilador convierte implícitamente ese void* a cualquier tipo de puntero. En C++ eso no funciona igual: el lenguaje no permite la conversión implícita de void* a otro tipo de puntero, así que NULL históricamente se definía como 0 o 0L, un entero disfrazado de puntero.

Esa diferencia generó un problema real de resolución de sobrecargas en C++:

#include 

void procesar(int valor) { std::cout OpciónCuándo usarlaVentajaLimitaciónNULL (macro heredada de C)Código C puro o C++ muy antiguoCompatibilidad con bases de código legadasPuede confundirse con un entero en sobrecargas0 literalNunca, para representar un punteroNinguna sobre NULL o nullptrAmbiguo: no queda claro si es número o punteronullptr (C++11)Todo código C++11 en adelanteTipo propio, sin ambigüedad de sobrecargaNo existe en C estándar (solo en C++)
> **💭 Clave:** muchos compiladores optimizan asumiendo que un puntero ya desreferenciado antes en el código no puede ser nulo más adelante, y eliminan chequeos de NULL que el programador sí esperaba que se ejecutaran. Es una consecuencia directa de que desreferenciar un puntero nulo sea undefined behavior.

## La evolución del bucle for hasta C++20

El bucle `for` de K&R C ya tenía la forma `for (inicializacion; condicion; incremento)` que seguimos usando hoy. Lo que cambió con los años fue dónde y cómo se declara la variable de control.

Enter fullscreen mode Exit fullscreen mode

/* K&R / C89: la variable debe existir antes del for */
int i;
for (i = 0; i EstándarSintaxis del forQué cambiaK&R C (1978)for (i = 0; i < n; i++)La variable debe declararse antes, fuera del forANSI C89 (1989)for (i = 0; i < n; i++)Formaliza la sintaxis, sigue exigiendo declaración externaC99for (int i = 0; i < n; i++)Permite declarar la variable dentro del forC++11for (auto x : contenedor)Itera sobre contenedores sin índices manualesC++20for (auto c = 0; auto x : contenedor)Agrega init-statement al range-based forC++20 permite declarar un contador junto al elemento del range-based for.

Cómo empezar: probá estas rarezas vos mismo

Todo lo anterior se puede reproducir con un compilador estándar y las banderas correctas. Estos comandos usan gcc y g++, pero clang acepta las mismas banderas -std:

atCommand:
# Compilar como K&R / C89 clasico
gcc -std=c89 -Wall -Wextra ejemplo.c -o ejemplo_c89

# Compilar con C99 (declarar variables dentro del for)
gcc -std=c99 -Wall -Wextra ejemplo.c -o ejemplo_c99

# Compilar C++20 con soporte para range-for con init-statement
g++ -std=c++20 -Wall -Wextra ejemplo.cpp -o ejemplo_cpp20

# Detectar deref de puntero nulo con el sanitizer de undefined behavior
gcc -std=c99 -fsanitize=undefined -g ejemplo.c -o ejemplo_ubsan
./ejemplo_ubsan
Enter fullscreen mode Exit fullscreen mode

Para confirmar qué estándar está realmente activo en la compilación, podés imprimir la macro que el propio compilador define:

#include 

int main(void) {
    printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Con -std=c99 vas a ver el valor 199901, y con -std=c11 el valor 201112. En C++, el equivalente es imprimir __cplusplus: bajo -std=c++20 debería mostrar 202002 o superior.

Casos de uso reales

El dialecto K&R o el modo gnu89 todavía aparecen en bases de código de décadas, sistemas embebidos con compiladores viejos, y partes históricas del kernel de Linux que durante años exigieron compatibilidad estricta con GCC en modo C89 extendido.

El problema de los punteros nulos sigue siendo relevante en cualquier código C o C++ que maneje memoria manualmente: parsers, drivers, motores de bases de datos. Y la evolución del bucle for hasta C++20 importa sobre todo en código nuevo que itera sobre rangos con lógica de conteo adicional, como procesar datos junto con su índice sin declarar variables fuera del bucle.

Errores comunes y buenas prácticas

  • Confundir NULL con 0: en C++, preferí siempre nullptr en vez de NULL o 0 para evitar ambigüedad de sobrecarga.- Usar el truco --> en código real: es válido, pero un revisor de código lo va a marcar por ilegible. Escribí n--; if (n > 0) o directamente reestructurá el bucle.- Asumir que declarar dentro del for filtra la variable afuera: eso solo pasa en estilo K&R; desde C99, la variable declarada en el for muere al salir del bucle.- Ignorar el sanitizer de undefined behavior: un deref de puntero nulo puede no crashear en todas las plataformas. Compilar con -fsanitize=undefined durante desarrollo detecta estos casos antes de producción.- Copiar Duff's device sin necesidad: los compiladores modernos ya desenrollan bucles automáticamente con optimizaciones como -O2; reescribir esa técnica a mano rara vez aporta algo hoy.

Comparativa con alternativas

La tabla de la sección anterior sobre NULL, 0 y nullptr resume la decisión más común que un desarrollador de C++ moderno enfrenta al representar un puntero nulo. En C puro, la elección es más simple porque solo existe la macro NULL, pero en C++ conviene fijar nullptr como estándar del equipo y prohibir NULL en revisiones de código nuevas.

Profundizando: lo que pasa por dentro

La razón técnica de que desreferenciar un puntero nulo sea undefined behavior, y no un error garantizado, es que el estándar no obliga a que la dirección nula sea físicamente la posición de memoria 0x0 en todas las arquitecturas. En la práctica, casi todos los sistemas modernos sí la implementan como todos ceros, pero el estándar deja la puerta abierta para que un compilador optimice asumiendo que ese acceso jamás ocurre.

Esa suposición del compilador es lo que puede eliminar un chequeo if (puntero != NULL) escrito después de ya haber desreferenciado ese mismo puntero antes en la función: el compilador razona que, si el código anterior ya lo desreferenció sin comprobarlo, entonces no puede ser nulo, y borra la comprobación posterior como código muerto.

Duff's device también ilustra un detalle profundo del lenguaje: en C, un switch es en realidad un salto (goto disfrazado) hacia una etiqueta dentro de un bloque, no una estructura de control independiente. Por eso es legal que un case aterrice en medio de un do-while: el switch no exige que sus case estén al mismo nivel de anidamiento que un bucle separado.

📖 Resumen en Telegram: Ver resumen

Tu próximo paso: compilá el ejemplo del bucle for con gcc -std=c89 y después con gcc -std=c2x para ver qué advertencias distintas aparecen entre ambos estándares.

Preguntas frecuentes

¿Qué significa "K&R C"?

Es el dialecto de C descrito en el libro de Kernighan y Ritchie de 1978, anterior a la estandarización ANSI de 1989.

¿Por qué el operador --> no existe realmente?

Porque el compilador lo tokeniza como -- (post-decremento) seguido de > (mayor que); nunca hay un único operador de tres caracteres en la gramática de C.

¿Qué diferencia hay entre NULL y nullptr?

NULL suele expandirse a un entero (0 o 0L), mientras que nullptr tiene su propio tipo std::nullptr_t que solo convierte a punteros, evitando ambigüedad en sobrecargas.

¿Desde qué versión de C se puede declarar la variable del for dentro del propio for?

Desde C99. En K&R C y en C89 la variable debía declararse antes del bucle.

¿Qué aportó C++20 al bucle for?

La posibilidad de agregar un init-statement a un range-based for, para declarar una variable auxiliar como un contador sin que su alcance se extienda fuera del bucle.

¿Es Duff's device código legal en C moderno?

Sí, sigue siendo sintácticamente válido en C99, C11 y versiones posteriores, aunque en la práctica los compiladores actuales ya desenrollan bucles automáticamente con optimizaciones.

Referencias

📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

Top comments (0)