The logic behind C declarations
and the only correct way to declare pointers
2026-07-25
Every once in a while, I stumble upon people on the Internet arguing about which of the following is the “correct” way to write C variable declarations:
int *variable;
int* variable;with the obligatory third person who will mention these alternatives as a joke to make fun of the discussion:
int * variable;
int*variable;Of course, the correct answer is “Match the style of the existing codebase”; still, which style should one adopt when starting a new codebase?
Those rooting for int* variable will say
that * is part of the type (“pointer to
int”). Those rooting for
int *variable will respond stating that, in the
declaration int* a, b, c, a will
end up being a pointer, while b and
c will be ints, therefore
* is strictly bound to the variable that sits
after it. Then someone will argue that this makes no sense
and it’s the reason why we should never declare more than
one variable in a single statement.
The correct answer is int *variable, but the
reason is a bit more nuanced than this.
How C declarations work
The syntax of C declarations is more logical than it seems; you just need to look at it from a different point of view that is not obvious at a glance.
Every declaration is structured like this:
type <expression>[, expression [...]];
typeis a type, such asintorstruct sockaddr_in(a primitive, a struct, a union or something declared viatypedef);expressionis an expression that contains exactly one identifier that doesn’t already exist.
What the declaration expresses is: “Declare a new
variable such that the expression expression
results in a value of type type”.
For example:
// Declare "variable" such that the
// expression "variable" is of type "int".
int variable;
// Declare "variable" such that the
// expression "*variable" is of type "int".
int *variable;When you type int *variable, you aren’t
saying that variable is an int*.
You are saying that *variable is an
int. That’s why the asterisk should be attached
to variable rather than to
int.
By the way, this logic is valid for any kind of C declaration:
// Declare "variable" such that the
// expression "variable[10]" is of
// type "int".
int variable[10];This is the reason why we write
int variable[] and not
int[] variable like they do in some other
languages like Java.
You may argue that using variable[10] after
the declaration above will result in undefined behavior, as
the last index of the array is 9, but that is
beyond the point. From a purely syntactical point of view,
variable[10] is an int
and will be treated as an int by the compiler.
The undefined behavior is purely a runtime concern.
Take a look at the following two declarations as well:
// Declare "variable1" such that the
// expression "*variable1[10]" is of
// type "int".
int *variable1[10];
// Declare "variable2" such that the
// expression "(*variable2)[10]" is of
// type "int".
int (*variable2)[10];variable1 will be an array of pointers to
int; variable2 will be a pointer
to an array of ints.
Lastly, let’s consider functions.
// Declare "function" such that the
// expression "function(a, b)" is of
// type "int".
int function(int a, float b);This slightly breaks the logic, because you call the
function as function(a, b), not
function(int a, float b), but there
needs to be a way to specify the types of
parameters, and that was the most reasonable place from a
practical point of view.
Also note that in historical (pre-ANSI) C a function was defined differently:
int function(a, b)
int a;
float b;
{
return a + b;
}This is sounder from a theoretical point of view, but it
makes things annoying to define, so ANSI went for
int function(int a, float b).
Function pointers are declared just like functions, but with an asterisk to indicate that you need to dereference them before calling:
// Declare "variable" such that the
// expression "(*variable)(a, b)" is of
// type "int".
int (*variable)(int a, float b);In reality, to call a function pointer you can skip the
asterisk and just use variable(a, b); this
shortcut can’t be carried over to the declaration, because
it would make it ambiguous with a function declaration.
Here’s a complex example that puts everything together:
// An array of function pointers that
// return a pointer to int
int *(*variable[10])(int a, float b);Specifying types on their own
There are situations where you need to name a type on its own, outside of a declaration. The most obvious example is type casting. Based on the rules above alone, there is no meaningful way to specify the name of a complex type (like a pointer or an array) on its own.
The solution C chose is: simply use the syntax you’d use to declare a variable of that type, but omit the identifier.
// Cast "a" to an int
int b = (int)a;
// Cast "a" to a pointer to int
int *b = (int *)a;
// Cast "a" to a function pointer
int (*b)(int, float) =
(int (*)(int, float))a;You get the gist.
Based on this, you could in fact say that, in the
declaration int* variable,
variable is an int*; that
is true, but what the declaration is truly
expressing is that *variable is an
int. This is the logical flow:
int *variable:*variableis anint;- Since
*variableis anint,variableis a pointer to anint; - Thus we can formally say that the type “pointer to an
int” is calledint *, i.e.int *variablewith the identifiervariableomitted; - Therefore
variableis anint *.
Even if you go against this and decide to use
int* variable, because you prefer the syntax
<type_name> <variable_name>, you’re
bound to write inconsistent code, as you can’t apply this
principle to arrays, functions or function pointers. The
following three are illegal:
// Array of 10 ints
int[10] variable;
// Function pointer
int(*)(int, float) variable;
// Function (look at how funny this is!)
int(int a, float b) function;and typedeffing those away won’t solve the
issue, because the inconsistency will hide in the
typedefs. You can, however, solve this in C++
via using int10 = int[10], but you’ll spend
your day defining new types.
C++ references
While this declaration system is almost perfectly logical, it doesn’t always work. In fact, it completely breaks apart when declaring a C++ reference:
// Declare "variable" such that the
// expression &variable is of type "int".
int &variable = something;This is complete nonsense: there is no situation in which
&variable can ever result in an
int (the address of variable would
have to be an int; variable would
have to be some sort of “reverse pointer”). Unfortunately,
looking at the declaration above as we have been doing
through this article doesn’t work.
This is not C++’s fault. There is no sound way to declare
a reference, because a reference to type
is of type type. The only difference
between a reference to an int and an
int is at declaration time:
int a = 10;
int b = a;
int &c = a;b and c are really both
ints; the difference is that c is
the same int as a, while
b is a copy.
Note that, similarly to pointers, &
attaches to the identifier that follows it:
// a is a reference to b; c is a copy of d.
int &a = b, c = d;Therefore, references should still be declared as
int &variable rather than
int& variable, although the argument is
weaker here.
Other languages
Most other languages got rid of the C declaration system. They tend to follow the philosophy we mentioned earlier (type specifier followed by variable name) or a variation of it.
Java did this while keeping the C syntax as-is where possible, and added a new way to declare arrays:
// a and b are both arrays
int[] a, b;
// Only a is an array
int a[], b;Style guides discourage the second C-like syntax.
C# and most other languages fully did away with any remnants of the C system:
int a[], b; // Illegal
int* a, b; // Both a and b are pointersRust specifies the type after the identifier name and follows the same philosophy. Like in C, the identifier can be an expression, but with a completely different goal: pattern matching.
// Array of i32 with size 3:
let x: [i32; 3] = [3, 7, 8];
// a = 3; b = 7; c = 8;
let [a, b, c]: [i32; 3] = x;Note that Rust can infer types, so the above can be shortened to:
let x = [3, 7, 8];
let [a, b, c] = x;Takeaway
In C, int *variable is more correct than
int* variable. The reason why
int* variable is even allowed by the language
is that whitespace is discarded once the lexer has done its
job. No matter what you type, the parser will feed this to
the compiler:
int: resulting type*variable: declaration expression:*: dereference operatorvariable: identifier that is being declared
In all honesty, the solution adopted by other languages is more intuitive and scales better to more complex type systems. However, the C syntax is not really as insane as people make it out to be.