⏱️ Lectura: 12 min
A Stack Overflow question about the --> operator in C has racked up more than 10,000 votes, and the answer is that this operator does not exist: it is -- followed by >. That misunderstanding sums up the topic of this article nicely.
📑 En este artículo
- TL;DR
- What K&R C is and why it still matters
- The K&R C quirks that still surprise
- The null pointer problem
- The evolution of the for loop up to C++20
- Getting started: try these quirks yourself
- Real-world use cases
- Common mistakes and best practices
- Comparison with alternatives
- Digging deeper: what happens under the hood
- Frequently Asked Questions
- References
K&R C left behind syntax rules that survive almost half a century later, from ambiguities in null pointers to four distinct redesigns of the for loop, the last of them formalized in C++20.
TL;DR
- You will understand why the –> operator in C, with more than 10,000 votes on Stack Overflow, is really — followed by >.
- You will learn why NULL and nullptr are not interchangeable in C++ overload resolution since C++11.
- You will be able to compare the for loop syntax in K&R C, C99, C++11, and C++20 with real code examples.
- You will be able to compile the same program with -std=c89, -std=c99, and -std=c++20 and see the differences with your own eyes.
- You will get to know Duff’s device, the 1983 technique that combines switch and do-while to unroll loops.
- You will learn to detect a null pointer dereference with gcc’s undefined behavior sanitizer.
- You will tell apart when declaring the for variable outside (K&R) or inside (C99) changes its actual scope.
What K&R C is and why it still matters
K&R C is the informal name for the C dialect described in the book by Brian Kernighan and Dennis Ritchie, published in 1978. There was no official standard yet: the book itself served as the de facto specification for thousands of programmers.
That dialect is more permissive than modern C. It did not require function prototypes, it assumed types by convention instead of enforcing them, and it left gaps where the compiler would raise an error today. Understanding null pointers and the current shape of the for loop requires first understanding where the language comes from.
The ANSI committee standardized C in 1989 (C89), correcting many of those ambiguities. But some quirks from the K&R era survived as syntax curiosities, and others, like the null pointer problem, were never fully resolved: they simply got a safer layer of syntax placed on top.
The K&R C quirks that still surprise
Implicit int and functions without a prototype
In K&R C, if you did not declare a return type, the compiler assumed int. And a function’s parameters were typed on a separate line, after the list of names:
/* estilo K&R: los tipos de los parametros van despues de la lista */
suma(a, b)
int a, b;
{
return a + b;
}
This code still compiles in many compilers if you ask for K&R mode or gnu89 with relaxations. There is no explicit return type (int is assumed), and the types of a and b are declared on a separate line before the opening brace. With -std=c99 or higher, this syntax simply does not compile.
The operator that does not exist: –>
The most cited example of C’s syntactic quirks is this loop:
#include <stdio.h>
int main(void) {
int n = 5;
while (n --> 0) {
printf("%d\n", n);
}
return 0;
}
There is no --> (“goes to”) operator in the C grammar. The compiler reads n-- (post-decrement) followed by > 0 (comparison). The result is a perfectly valid loop that prints 4, 3, 2, 1, and 0. It is the absence of spaces that creates the visual illusion of an arrow.
⚠️ Watch out: the compiler accepting
n --> 0does not mean you should write it that way in real code. It is a good example for understanding tokenization, but in production it breaks readability with no performance benefit whatsoever.
Duff’s device: the switch that slips into a loop
The most extreme quirk of the K&R era is Duff’s device, written by Tom Duff in 1983 to speed up pixel copying on a graphics terminal:
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);
}
}
The trick interleaves a switch with a do-while: the initial case jumps straight into the middle of the loop body, based on the remainder of the division by 8, and from there the do-while keeps running the remaining copies. This allowed unrolling a copy loop without writing repetitive code separately.
The null pointer problem
The null pointer problem in C starts with a simple design decision: a null pointer represents “points to no valid object,” but the language does not stop you from dereferencing it. Doing so is undefined behavior, not a guaranteed error.
In C, the NULL macro usually expands to ((void*)0), and the compiler implicitly converts that void* to any pointer type. In C++ it does not work the same way: the language does not allow the implicit conversion of void* to another pointer type, so NULL was historically defined as 0 or 0L, an integer disguised as a pointer.
That difference created a real overload resolution problem in 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*)
}
Since NULL is usually an integer literal, the compiler may prefer the int overload over the pointer one, which is almost never what the programmer wants. To fix this at the root, C++11 introduced the nullptr keyword, with its own type std::nullptr_t, which can only convert to pointer types.
| Option | When to use it | Advantage | Limitation |
|---|---|---|---|
| NULL (macro inherited from C) | Pure C or very old C++ code | Compatibility with legacy code bases | Can be confused with an integer in overloads |
| 0 literal | Never, to represent a pointer | None over NULL or nullptr | Ambiguous: unclear whether it is a number or a pointer |
| nullptr (C++11) | All C++11 code and later | Own type, no overload ambiguity | Does not exist in standard C (C++ only) |
💭 Key point: many compilers optimize by assuming that a pointer already dereferenced earlier in the code cannot be null later on, and they eliminate NULL checks the programmer did expect to run. It is a direct consequence of dereferencing a null pointer being undefined behavior.
The evolution of the for loop up to C++20
The for loop in K&R C already had the for (initialization; condition; increment) form that we still use today. What changed over the years was where and how the control variable is declared.
/* 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);
}
In K&R C and C89, i had to exist before the loop and stayed alive after it ended. C99 allowed declaring the variable directly in the for header, with scope limited to the loop itself.
// 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 added the range-based for, which iterates directly over a container without handling indices. C++20 went further and allows an init-statement inside the range-based for, to declare a helper variable (such as a counter) without polluting the scope outside the loop.
| Standard | for syntax | What changes |
|---|---|---|
| K&R C (1978) | for (i = 0; i < n; i++) | The variable must be declared beforehand, outside the for |
| ANSI C89 (1989) | for (i = 0; i < n; i++) | Formalizes the syntax, still requires external declaration |
| C99 | for (int i = 0; i < n; i++) | Allows declaring the variable inside the for |
| C++11 | for (auto x : contenedor) | Iterates over containers without manual indices |
| C++20 | for (auto c = 0; auto x : contenedor) | Adds an init-statement to the range-based for |
Getting started: try these quirks yourself
Everything above can be reproduced with a standard compiler and the right flags. These commands use gcc and g++, but clang accepts the same -std flags:
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
To confirm which standard is actually active during compilation, you can print the macro that the compiler itself defines:
#include <stdio.h>
int main(void) {
printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
return 0;
}
With -std=c99 you will see the value 199901, and with -std=c11 the value 201112. In C++, the equivalent is printing __cplusplus: under -std=c++20 it should show 202002 or higher.
Real-world use cases
The K&R dialect or the gnu89 mode still shows up in decades-old code bases, embedded systems with old compilers, and historical parts of the Linux kernel that for years required strict compatibility with GCC in extended C89 mode.
The null pointer problem remains relevant in any C or C++ code that handles memory manually: parsers, drivers, database engines. And the evolution of the for loop up to C++20 matters mostly in new code that iterates over ranges with additional counting logic, like processing data together with its index without declaring variables outside the loop.
Common mistakes and best practices
- Confusing NULL with 0: in C++, always prefer
nullptroverNULLor0to avoid overload ambiguity. - Using the –> trick in real code: it is valid, but a code reviewer will flag it as unreadable. Write
n--; if (n > 0)or just restructure the loop. - Assuming that declaring inside the for leaks the variable outside: that only happens in K&R style; since C99, the variable declared in the for dies when the loop exits.
- Ignoring the undefined behavior sanitizer: a null pointer dereference may not crash on every platform. Compiling with
-fsanitize=undefinedduring development catches these cases before production. - Copying Duff’s device without need: modern compilers already unroll loops automatically with optimizations like
-O2; rewriting that technique by hand rarely adds anything today.
Comparison with alternatives
The table in the previous section on NULL, 0, and nullptr sums up the most common decision a modern C++ developer faces when representing a null pointer. In pure C, the choice is simpler because only the NULL macro exists, but in C++ it is best to set nullptr as the team standard and ban NULL in new code reviews.
Digging deeper: what happens under the hood
The technical reason dereferencing a null pointer is undefined behavior, rather than a guaranteed error, is that the standard does not require the null address to physically be memory location 0x0 on every architecture. In practice, nearly all modern systems do implement it as all zeros, but the standard leaves the door open for a compiler to optimize by assuming that access never happens.
That compiler assumption is what can eliminate an if (puntero != NULL) check written after the same pointer has already been dereferenced earlier in the function: the compiler reasons that, if the earlier code already dereferenced it without checking, then it cannot be null, and it removes the later check as dead code.
Duff’s device also illustrates a deep detail of the language: in C, a switch is really a jump (a disguised goto) to a label inside a block, not an independent control structure. That is why it is legal for a case to land in the middle of a do-while: the switch does not require its case labels to be at the same nesting level as a separate loop.
📖 Summary on Telegram: View summary
Your next step: compile the for loop example with gcc -std=c89 and then with gcc -std=c2x to see which different warnings appear between the two standards.
Frequently Asked Questions
What does “K&R C” mean?
It is the C dialect described in Kernighan and Ritchie’s 1978 book, predating the 1989 ANSI standardization.
Why doesn’t the –> operator really exist?
Because the compiler tokenizes it as -- (post-decrement) followed by > (greater than); there is never a single three-character operator in the C grammar.
What is the difference between NULL and nullptr?
NULL usually expands to an integer (0 or 0L), while nullptr has its own type std::nullptr_t that only converts to pointers, avoiding ambiguity in overloads.
From which version of C can you declare the for variable inside the for itself?
Since C99. In K&R C and C89 the variable had to be declared before the loop.
What did C++20 bring to the for loop?
The ability to add an init-statement to a range-based for, to declare a helper variable such as a counter without its scope extending outside the loop.
Is Duff’s device legal code in modern C?
Yes, it is still syntactically valid in C99, C11, and later versions, although in practice current compilers already unroll loops automatically with optimizations.
References
- Stack Overflow: the original question about the –> operator in C/C++, with more than 10,000 votes.
- cppreference: nullptr: reference documentation on the keyword introduced in C++11.
- cppreference: range-for: documentation on the range-based for and the C++20 init-statement.
- Wikipedia: The C Programming Language: history of the Kernighan and Ritchie book published in 1978.
- Wikipedia: Duff’s device: origin and explanation of the technique written by Tom Duff in 1983.
📱 Enjoying this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day. @programacion
Imagen destacada: Foto de Fotis Fotopoulos en Unsplash
0 Comments