⏱️ 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
  1. TL;DR
  2. What K&R C is and why it still matters
  3. The K&R C quirks that still surprise
    1. Implicit int and prototype-less functions
    2. The operator that doesn’t exist: –>
    3. Duff’s device: the switch that reaches into a loop
  4. The null pointer problem
  5. The evolution of the for loop up to C++20
  6. Getting started: try these quirks yourself
  7. Real-world use cases
  8. Common mistakes and best practices
  9. Comparison with alternatives
  10. Going deeper: what happens under the hood
  11. Frequently Asked Questions
    1. What does “K&R C” mean?
    2. Why doesn’t the –> operator really exist?
    3. What’s the difference between NULL and nullptr?
    4. Since which version of C can you declare the for variable inside the for itself?
    5. What did C++20 add to the for loop?
    6. Is Duff’s device legal code in modern C?
  12. 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 --> 0 doesn’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.

C code snippet showing Duff's device with switch and do-while null pointers
Duff’s device from 1983 combines switch and do-while in a single function. Photo by Brett Jordan on Unsplash

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
Comparison of null pointers and for loop syntax across C and C++ standards
C++20 lets you declare a counter alongside the range-based for element. Photo by Nick Fewings on Unsplash

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 nullptr over NULL or 0 to 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=undefined during 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.

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

📱 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


Andrés Morales

Developer and AI researcher. Writes about language models, frameworks, developer tooling, and open source releases. Covers ML papers, the tech startup ecosystem, and programming trends.

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.