2026. 09. 13.
Where Should the Star Rest? A C Programmer's Quiet Holy War

By the third release candidate, the declaration no longer looked suspicious.
It had survived code review, compilation, tests, and two earlier candidates. The line was short enough to pass beneath the eye without resistance.
const INTPTR p = &value;
One programmer read it as “a pointer to a constant integer.” The compiler read it as “a constant pointer to an integer that could still be modified.”
Both interpretations sounded reasonable in English. Only one of them was C.
The code compiled because the compiler saw a perfectly legal declaration. The tests passed because the branch that depended on the distinction had never been reached. Three release candidates later, that branch finally ran and exposed the assumption hidden inside the line.
The bug had started with a star no one could see.
To explain how it got there, I need to go back a few years.
On a lazy afternoon, sipping a sweet paper cup of Maxim instant coffee, I was staring at some old C code. Familiar stars floated above the lines on the screen: asterisks that meant multiplication in one place, dereferencing in another, and an entirely different kind of trouble inside a declaration.
To developers who grew up with garbage collectors quietly sweeping the floor and references arriving safely wrapped by the language, a C pointer can look like a relic from a less supervised age. A pointer holds the address of an object. Dereferencing it allows the program to read or modify whatever is stored at that address.
If the address is invalid, stale, or simply not the address the programmer thought it was, the program may end with a Segmentation Fault. The less considerate bugs skip the crash and corrupt something quietly.
C gives programmers a powerful key and very little supervision over which door they open. Around that key, C programmers have fought a long and remarkably persistent holy war.
The argument begins with a question that appears too small to deserve one.
Where should the star rest?
int* p;
int *p;
The Type faction places the star beside int. To them, p is not an integer but an integer pointer, so int* should look like a single type. The declaration reads naturally from left to right.
The Variable faction places the star beside p. This is the form commonly seen in K&R, and it follows the way C declarators work. If the expression *p has type int, then the declaration is written as int *p.
Both declarations produce exactly the same type. The compiler ignores the space. The argument exists entirely for the humans who must read the code later, which has never prevented programmers from taking it personally.
While writing a book on data structures and algorithms in C, I found myself caught between the two camps. I preferred the visual elegance of the Type faction. Then I would type this:
int* p, q;
The line looked as though it declared two integer pointers. It did not.
The declaration begins with the base type int, followed by two separate declarators. The declarator *p makes p a pointer to int. The declarator q contains no star, so q is an ordinary int.
Whitespace never joined the star to the type. My eyes did that on their own.
I would stop typing and grab the back of my neck. For a few seconds I blamed the compiler. The compiler had not betrayed me. It had followed the declaration exactly and declined to treat typography as part of the type system.
If both variables were meant to be pointers, the declaration had to say so.
int *p, *q;
Or, better for the next programmer who had not yet had coffee:
int *p;
int *q;
Years later, while reviewing legacy code at a telecom equipment company, I found a third style.
int * p;
There was one space on each side of the star. It belonged visually to neither the type nor the variable.
I stopped at the line longer than the line deserved. Was this a peace treaty between the two factions? Had the original programmer refused to take sides? Or had everyone become tired of the argument and allowed the star to drift into neutral territory?
The compiler, as usual, had no interest in our politics. int* p, int *p, and int * p were the same declaration.
“The code isn’t the product. The system is.”
That phrase came back to me during the review. The position of the space could not change the running system, but it could change how quickly the next programmer understood the declaration. The debate was not completely meaningless. It was simply being judged by the wrong machine.
Eventually, some codebases tried to end the argument by hiding the star.
#define INTPTR int *
At first, the result looked cleaner.
INTPTR p;
But the preprocessor does not create a type. It replaces tokens. The moment a second variable appears, the old problem returns.
INTPTR p, q;
After macro expansion, the compiler sees this:
int *p, q;
p is a pointer. q is an integer.
The star had disappeared from the declaration, but its rules had not changed. Hiding the star only prevented the programmer from seeing where those rules would apply.
A typedef behaves differently.
typedef int *INTPTR;
INTPTR p, q;
Here, INTPTR is a name for the pointer type. Both p and q are pointers to int. For a moment, this appears to settle the argument. The pointer has finally become a complete type with a clean name, and the star no longer has to choose a side.
Then const enters the room.
Consider the macro version:
int value = 0;
#define INTPTR int *
const INTPTR p = &value;
The preprocessor expands the declaration to this:
const int *p = &value;
Here, p is a changeable pointer to an integer that cannot be modified through p. The program may point p somewhere else, but an assignment through *p is not allowed.
Now consider the same declaration with a typedef:
int value = 0;
typedef int *INTPTR;
const INTPTR p = &value;
This time, INTPTR already names the complete type “pointer to int.” The const qualifier therefore applies to the pointer itself. The declaration is equivalent to:
int * const p = &value;
Now p cannot be pointed somewhere else, but the integer may still be modified through *p.
The declaration on the screen had barely changed. Its meaning had reversed.
With the macro, const protected the integer through that pointer. With the typedef, const protected the pointer and left the integer mutable. The difference did not come from whitespace. It came from whether INTPTR was a sequence of substituted tokens or the name of a complete pointer type.
That was the distinction hidden in the third release candidate.
A fix on my team used a const pointer that someone had read as a pointer to const data. The declaration compiled because a constant pointer to mutable data is valid C. The mistake remained invisible because the relevant code path was not covered by the tests. Three release candidates later, the system reached that path and proved that the compiler had been following a different sentence from the one we had been reading.
Nobody had been careless. The programmer, the reviewer, and the compiler had simply entered the line with different models of what INTPTR meant. The compiler’s model was the only one that affected the executable.
After that incident, I became less interested in winning the spacing war.
My ceasefire is deliberately boring. I declare one variable per line. I do not use macros to pretend that token sequences are types. I use pointer typedefs only when the abstraction is worth hiding the star, and I check how const applies before trusting the alias.
When the distinction matters, I would rather write the two types plainly:
const int *data; /* pointer to const int */
int * const fixed = &value; /* const pointer to int */
Decades later, I still do not know which faction won. The compiler never cared whether the star sat beside int or beside p. It cared about the declarator, the type alias, and the exact object qualified by const.
The holy war was never really about whitespace. It was about which sentence a programmer expected to be true when returning to the code a year later: “this variable has a pointer type,” or “dereference this variable and you will find an integer.”
The star may rest beside the type or beside the variable; the compiler will produce the same type either way. What matters is whether the next programmer can tell what the star belongs to, and what const will lock, before the answer costs three release candidates.