⏱️ 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 doesn’t exist: it’s -- followed by >. That misunderstanding sums up the point 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
- Going 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’ll understand why the –> operator in C, with over 10,000 votes on Stack Overflow, is really — followed by >.
- You’ll learn why NULL and nullptr are not interchangeable in C++ overload resolution since C++11.
- You’ll be able to compare the for loop syntax in K&R C, C99, C++11, and C++20 with real code examples.
- You’ll 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’ll get to know Duff’s device, the 1983 technique that combines switch and do-while to unroll loops.
- You’ll learn to detect a null pointer deref with gcc’s undefined behavior sanitizer.
- You’ll be able to tell 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 of 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 didn’t require function prototypes, it assumed types by convention rather than enforcing them, and it left gaps where the compiler would raise an error today. Understanding null pointers and the current form of the for loop first requires understanding where the language came from.
The ANSI committee standardized C in 1989 (C89), fixing a good part 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 prototype-less functions
In K&R C, if you didn’t declare a return type, the compiler assumed int. And a function’s parameters were typed on a separate line, after the list of names:
/* K&R style: parameter types go after the list */
suma(a, b)
int a, b;
{
return a + b;
}
This code still compiles on many compilers if you ask for K&R mode or gnu89 with relaxations. There’s 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 won’t compile.
The operator that doesn’t 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 C’s 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’s the absence of spaces that creates the visual illusion of an arrow.
⚠️ Heads up: the fact that the compiler accepts
n --> 0doesn’t mean you should write it that way in real code. It’s a good example for understanding tokenization, but in production it wrecks readability with no performance benefit.
Duff’s device: the switch that reaches 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 copying pixels 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 dividing by 8, and from there the do-while keeps running the remaining copies. This made it possible to unroll a copy loop without writing separate repetitive code.
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 doesn’t 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++ that doesn’t work the same way: the language doesn’t 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); // on many compilers this calls procesar(int)
procesar(nullptr); // always calls 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 code or very old C++ | Compatibility with legacy codebases | Can be mistaken for an integer in overloads |
| 0 literal | Never, to represent a pointer | None over NULL or nullptr | Ambiguous: unclear whether it’s a number or a pointer |
| nullptr (C++11) | All C++11 code onward | Its own type, no overload ambiguity | Doesn’t exist in standard C (only in C++) |
💭 Key point: many compilers optimize by assuming that a pointer already dereferenced earlier in the code can’t be null later on, and they remove NULL checks that the programmer did expect to run. It’s a direct consequence of dereferencing a null pointer being undefined behavior.
The evolution of the for loop up to C++20
K&R C’s for loop already had the form for (initialization; condition; increment) that we still use today. What changed over the years is where and how the control variable is declared.
/* K&R / C89: the variable must exist before the for */
int i;
for (i = 0; i < 10; i++) {
procesar_dato(i);
}
/* C99: declare the variable inside the for */
for (int i = 0; i < 10; i++) {
procesar_dato(i);
}
In K&R C and in 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 with 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 an auxiliary variable (like 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:
# Compile as classic K&R / C89
gcc -std=c89 -Wall -Wextra ejemplo.c -o ejemplo_c89
# Compile with C99 (declare variables inside the for)
gcc -std=c99 -Wall -Wextra ejemplo.c -o ejemplo_c99
# Compile C++20 with support for range-for with init-statement
g++ -std=c++20 -Wall -Wextra ejemplo.cpp -o ejemplo_cpp20
# Detect null pointer deref with the undefined behavior sanitizer
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 the compiler itself defines:
#include <stdio.h>
int main(void) {
printf("__STDC_VERSION__ = %ld\n", (long)__STDC_VERSION__);
return 0;
}
With -std=c99 you’ll 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 gnu89 mode still show up in decades-old codebases, 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 is still relevant in any C or C++ code that manages 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 extra counting logic, like processing data alongside 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’s 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 out: that only happens in K&R style; since C99, the variable declared in the for dies when leaving the loop.
- Ignoring the undefined behavior sanitizer: a null pointer deref 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 from 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’s best to set nullptr as the team standard and ban NULL in new code reviews.
Going deeper: what happens under the hood
The technical reason dereferencing a null pointer is undefined behavior, and not a guaranteed error, is that the standard doesn’t require the null address to physically be memory location 0x0 on every architecture. In practice, almost 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 that same pointer was already dereferenced earlier in the function: the compiler reasons that, if the earlier code already dereferenced it without checking, then it can’t 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’s why it’s legal for a case to land in the middle of a do-while: the switch doesn’t 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’s 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’s never a single three-character operator in C’s grammar.
What’s 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 overload ambiguity.
Since which version of C can you declare the for variable inside the for itself?
Since C99. In K&R C and in C89 the variable had to be declared before the loop.
What did C++20 add to the for loop?
The ability to add an init-statement to a range-based for, to declare an auxiliary variable like a counter without its scope extending outside the loop.
Is Duff’s device legal code in modern C?
Yes, it’s still syntactically valid in C99, C11, and later versions, although in practice today’s 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
Featured image: Photo by Fotis Fotopoulos on Unsplash
0 Comments