Mattia Moffa

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 [...]];

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:

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 pointers

Rust 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:

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.