Unlike the usual teaching method, I suggest you start from the outside in. Recognize the general structure of the declaration and proceed by refinement: replace the coarse-grained "blobs" that you see with more detailed ones. In other words, traverse the syntax from the root of the syntax tree to the leaves, rather than trying to pick out the right leaf and working bottom up.
The general structure of a declaration (that has a single declarator) is:
TYPE WHATEVER;
WHATEVER
is being declared, related to TYPE
.
Now note that TYPE
is int
, so WHATEVER
is of type int
. And WHATEVER
has the general shape (W1)(W2)
: two syntactic units in parentheses, whatever 1 and whatever 2:
int (W1)(W2);
Here, W1
is what is being declared, and it is a function returning int
, which takes a W2
parameter list. The latter actually denotes int *
:
int (W1)(int *);
Thus W1
is a function returning int
which takes int *
. But W1
is actually *W3
, which makes W3
a pointer to W1
's type.
int (*W3)(int *);
W3
is a pointer to a function returning int
which takes int *
.
And W3
is actually f[]
, so f
is an array of unspecified size of W3
's type:
int (*f[])(int *);
f
is an array of unspecified size of pointers to function returning int
which takes int *
.
Tip: How do we know that W1
isn't actually W3[]
where W3
is then *f
? This is because the type construction operator [...]
is syntactically similar to the postfix array indexing operator [...]
and the type constructing *
is similar to the dereferencing unary operator *
. The unary operator has a lower predecence than the postfix operator. When we see *X[]
we know it means *(X[])
and not (*X)[]
. The symbols *X
do not form a syntactic unit in that phrase. If we want the second meaning, we have to use the parentheses.
The syntax tree is:
declaration
| |
+------------+ +----------------+
| |
specifier-qualifier list -- TYPE declarator -- WHATEVER
| | |
int +-----+ |
| |
function -- W1 params -- W2
| |
pointer -- W3 (int *)
|
array -- f
The helpful symbols like W3
are just labels for the nodes as we traverse from root to leaves. The name being declared in a C
declaration is somewhere at the bottom of the tree, unlike in some other languages. As go deeper into the tree, we are actually emerging out of the type, so finally when we arrive at the bottom, we know the most important things: f
is being declared, and overall it is an array of something.