On the subject of pointer syntax
Vahan March 29, 2026 Updated: April 04, 2026 #C #ProgrammingThe C programming language was initially designed by Dennis Ritchie and one of his key design principles was that declaration syntax should mimic expression syntax1.
// Declarations
char *p;
int ;
// Expressions
return *p;
return ;
The declaration char *p; means that the expression *p will be of type char.
The declaration int f(); means that the expression f() will be of type int.
This principle also applies to declarations that have multiple declarators:
int
int
In the program above, the declaration int *p = &x, f(); means that both *p and f() will be expressions of type int.
However, we can see that this principle is not an absolute rule:
int
int
In the example above, the declaration int add(int, int); does not mean that add(int, int) will be an expression of type int.
It is not considered a well-formed expression to begin with.
The way to think about it is that add(X1, X2) (X1 and X2 being expressions of type int) is an expression of type int.
Array declaration syntax is an even worse offender:
int a = ;
Here, applying the "declaration syntax mimics expression syntax" is not only wrong but could also be harmful.
The expression a[5] would cause an out-of-bounds access error.
Nonetheless, understanding the spirit of this design principle can help us decipher more complex declarations:
;
Declarations in C should be read inside-out starting with the name of the declared entity (foo in this example).
Prefix operators (such as *) have lower precedence compared to postfix operators (such as the function call operator()).
- Knowing this fact allows us to establish that
foois a function which has a parameter of typeint. - We know that it returns a pointer since the value returned by
foocan be dereferenced (*foo(int a)). - We know that the returned pointer points to a function that has a parameter of type
doubleand a parameter of typechar *((*foo(int a))(double, char *)). - That function returns a pointer (
*(*foo(int a))(double, char *)). - That pointer points to a function that has a parameter of type
longand returns a value of typeint.
It is obviously not obvious at a glance that foo is a function which returns a pointer to a function which returns a pointer to a function which returns a value of type int.
In fact, declarations can become so hard to read that the maintainer of the Fish shell created a website that deciphers C declarations for you.
Even though declarations in C are notoriously hard to read and write, expressions can also be tricky due to their many nested parentheses.
My goal here is not to write a C language tutorial or explain in depth all the implications of this principle but to discuss language design.
A postfix indirection operator
In The Development of the C Language, Dennis Ritchie mentions that declarations and expressions involving pointers would be more readable if the indirection operator was a postfix operator.
Let's imagine for a moment that ! is the postfix indirection operator.
int
The declaration of foo would now look like the following:
int !!;
It can almost be read from left to right. We just have to go back at the beginning to read the type specifier (int).
It is clearer that foo is a function that takes an int and returns a pointer to a function that takes a double and a char ! and returns a pointer to a function that takes a long and returns an int.
Ideally, the type specifier should go at the end but we would need a keyword to introduce declarations:
let p !int = &x;
let a int = ;
let !! int;
A postfix indirection operator also removes the need for the arrow operator ->.
// Without arrow operator
return .property;
// Arrow operator
return pointer->property;
// Postfix indirection operator combined with the '.' operator
return pointer!.property;
Another issue that the prefix * operator causes occurs during parsing2.
Consider the following code snippet:
a * b;
It is ambiguous during parsing whether a * b; is an expression statement or a declaration of a variable named b of type "pointer to a".
This ambiguity complicates and slows down tools such as formatters, linters and syntax highlighters.
Other languages
There are many other languages which have pointers and it's interesting to see how familiarity and popularity shape programming language design more than anything else.
Pascal
Pascal and C emerged around the same period but Pascal used ^ as a postfix indirection operator from the very beginning.
The syntax to access fields of a record then becomes trivial: p^.field.
Ada
Ada emerged a few years later than C and Pascal.
It has "access types"3 which are roughly equivalent to pointers.
Ada uses .all as a postfix indirection operator.
It also has an implicit dereferencing feature to access fields of a record which means it is possible to write Ptr.Field instead of Ptr.all.Field.
Rust
Rust uses * as a prefix indirection operator just like C and has implicit dereferencing just like Ada.
Rust also has implicit borrowing when calling methods.
Declaration syntax is not tied to expression syntax so declarations stay readable.
Despite its implicit borrowing/dereferencing features and its .deref() method, there are still discussions about adding postfix borrowing and dereferencing operators4.
Go
Go also uses the same operators as C and has implicit dereferencing. It changes the syntax of declarations to make them readable.
Zig
Zig uses .* as a postfix indirection operator.
Odin
Odin uses the same operator as Pascal.
Hare
Hare is similar to Go and Rust in this regard.
C3
C3 uses the same operators as C.
V
V uses the same operators as C.
Conclusion
Some would argue that language designers should completely avoid unary operators5. Others would argue that all unary operators should have the same fixity. My stance is not that extreme.
However, Pascal appeared in the 1970s and Dennis Ritchie mentioned in 1993 that a postfix indirection operator would be better. I find it unfortunate that the language designers of Go, Rust, Hare, C3, and V made the mistake of disregarding this insight.
When designing a new language (especially a spiritual successor to C) which breaks source retrocompatibility, avoiding simple improvements like making a prefix operator postfix because of familiarity6 is bad design.