The advice I usually see about reading C declarations is to read them from the inside out. This method works fine for me, but I recently found a simpler method based on the idea that a declaration in C mirrors the use of the declaration.
Let us take for example the declaration of a pointer to a function returning an integer:
int (*f)(); /* declaration of f */
My new way of reasoning about what the above line means is to imagine that I were to use the declaration, inspired by Dennis Ritchies comment on similar examples in The development of the C programming language:
In all these cases the declaration of a variable resembles its usage in an expression whose type is the one named at the head of the declaration.
In other words, the use of the declaration would look nearly the same, in fact just remove the
int and you get
(*f)(); /* use of f */
This may not seem like a big difference, but I believe that to people who are used to reading C code this is immediately recognizable as dereferencing a pointer and calling the result as a function. The usage example thus explains what the declaration means!
It seems to me that this method is useful because reading the declaration as a usage leads intuitively to an inside-out interpretation of the syntax. This might be because you actually imagine the execution of an expression, rather than the abstract meaning of a declaration. C programmers have this inside-out parsing technique internalized in the context of expressions, while in the context of declarations, at least to me, it is a more conscious effort.
Below are some more examples of different kinds of C declarations, and their corresponding as-if-used interpretation.
int *f(); /* declaration */ *f(); /* use */
The above declaration is for a function returning a pointer to an integer. Normally one would not dereference a pointer without using the result, so you could imagine that the use occurred as the right-hand side of an assignment.
void (*f(int, float))(); /* declaration */ (*f(3, 1.4f))(); /* use */
In this case there are parameter types which should be substituted by literals of corresponding type in the use example. From the use example one can conclude that
f(3, 1.4f) is a call of function
f, which returns a pointer to a function.
float *a; *a;
Arbitrary array indexes should be added in place of empty square brackets. The use case here shows that the declaration is an array of float-pointers. One could make the declaration slightly more readable by adding parenthesis around
void (*a)(int); (*a)(0);
Now this example I find a bit difficult to parse using the inside-out method, yet reading the use case leads me on the right track faster. The declaration is an array of function pointers (array of