⏱️ Lectura: 13 min

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.

📑 En este artículo
  1. TL;DR
  2. Qué es K&R C y por qué sigue importando
  3. Las rarezas de K&R C que todavía sorprenden
    1. Int implícito y funciones sin prototipo
    2. El operador que no existe: –>
    3. Duff’s device: el switch que se mete en un bucle
  4. El problema de los punteros nulos
  5. La evolución del bucle for hasta C++20
  6. Cómo empezar: probá estas rarezas vos mismo
  7. Casos de uso reales
  8. Errores comunes y buenas prácticas
  9. Comparativa con alternativas
  10. Profundizando: lo que pasa por dentro
  11. Preguntas frecuentes
    1. ¿Qué significa “K&R C”?
    2. ¿Por qué el operador –> no existe realmente?
    3. ¿Qué diferencia hay entre NULL y nullptr?
    4. ¿Desde qué versión de C se puede declarar la variable del for dentro del propio for?
    5. ¿Qué aportó C++20 al bucle for?
    6. ¿Es Duff’s device código legal en C moderno?
  12. Referencias

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;
}

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 <stdio.h>

int main(void) {
    int n = 5;

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

    return 0;
}

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);
    }
}

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.

Fragmento de código C mostrando Duff's device con switch y do-while
Duff’s device de 1983 combina switch y do-while en una sola función. Foto de Brett Jordan en Unsplash

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 <iostream>

void procesar(int valor) { std::cout << "version int\n"; }
void procesar(char *puntero) { std::cout << "version puntero\n"; }

int main() {
    procesar(NULL);     // en muchos compiladores llama a procesar(int)
    procesar(nullptr);  // siempre llama a procesar(char*)
}

Como NULL suele ser un entero literal, el compilador puede preferir la sobrecarga de int en lugar de la de puntero, algo que casi nunca es lo que el programador quiere. Para resolver esto de raíz, C++11 introdujo la palabra clave nullptr, con su propio tipo std::nullptr_t, que solo puede convertirse a tipos puntero.

Opción Cuándo usarla Ventaja Limitación
NULL (macro heredada de C) Código C puro o C++ muy antiguo Compatibilidad con bases de código legadas Puede confundirse con un entero en sobrecargas
0 literal Nunca, para representar un puntero Ninguna sobre NULL o nullptr Ambiguo: no queda claro si es número o puntero
nullptr (C++11) Todo código C++11 en adelante Tipo propio, sin ambigüedad de sobrecarga No 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.

/* K&R / C89: la variable debe existir antes del for */
int i;
for (i = 0; i < 10; i++) {
    procesar_dato(i);
}

/* C99: declarar la variable dentro del for */
for (int i = 0; i < 10; i++) {
    procesar_dato(i);
}

En K&R C y en C89, i tenía que existir antes del bucle y seguía viva después de que este terminara. C99 permitió declarar la variable directamente en el encabezado del for, con alcance limitado al propio bucle.

// C++11: range-based for
for (auto valor : lista_de_datos) {
    procesar_dato(valor);
}

// C++20: range-based for con init-statement
for (auto contador = 0; auto valor : lista_de_datos) {
    procesar_dato(valor, contador++);
}

C++11 sumó el range-based for, que itera directamente sobre un contenedor sin manejar índices. C++20 fue más allá y permite un init-statement dentro del range-based for, para declarar una variable auxiliar (como un contador) sin ensuciar el alcance exterior al bucle.

Estándar Sintaxis del for Qué cambia
K&R C (1978) for (i = 0; i < n; i++) La variable debe declararse antes, fuera del for
ANSI C89 (1989) for (i = 0; i < n; i++) Formaliza la sintaxis, sigue exigiendo declaración externa
C99 for (int i = 0; i < n; i++) Permite declarar la variable dentro del for
C++11 for (auto x : contenedor) Itera sobre contenedores sin índices manuales
C++20 for (auto c = 0; auto x : contenedor) Agrega init-statement al range-based for
Comparación de sintaxis del bucle for en distintos estándares de C y C++
C++20 permite declarar un contador junto al elemento del range-based for. Foto de Nick Fewings en Unsplash

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

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

#include <stdio.h>

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

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.

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.

Imagen destacada: Foto de Fotis Fotopoulos en Unsplash


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.