How to Understand C's Declaration Syntax
Many programming languages employ a declaration syntax which emphasises the type of the name being declared. But C did something different: its syntax emphasises the type of expressions using the declared name.
This is key to an intuitive understanding of declarations like this (extreme)
example from from FreeBSD's manual page for signal(3)
:
void (*signal(int sig, void(*func)(int)))(int)
It is also the reason that C programmers conventionally say int *x
and not
int* x
(note the spaces), and why declarations like int *x, *y, z
make any
sense.
In a 1996 paper1, Dennis Ritchie wrote about the choice of syntax:
For each object of such a composed type, there was already a way to mention the underlying object: index the array, call the function, use the indirection operator on the pointer. Analogical reasoning led to a declaration syntax for names mirroring that of the expression syntax in which the names typically appear. Thus,
int i, *pi, **ppi;declare an integer, a pointer to an integer, a pointer to a pointer to an integer. The syntax of these declarations reflects the observation that
i
,*pi
, and**ppi
all yield an int type when used in an expression. Similarly,int f(), *f(), (*f)();declare a function returning an integer, a function returning a pointer to an integer, a pointer to a function returning an integer;
int *api[10], (*pai)[10];declare an array of pointers to integers, and a pointer to an array of integers. 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.
More examples should help cement this idea:
int *n1
emphasises that*n1
has the typeint
;double f1(int n)
emphasises thatf1(n)
has the typedouble
;double (*n2)[10]
declares a pointer-to-array. It emphasises that(*n2)
has the typedouble[3]
, and further that(*n2)[i]
has the typedouble
;double(*f2)(int)
declares a pointer-to-function. It emphasises that(*f2)
has the function typedouble(int)
, and of course that(*f2)(1)
has the typedouble
;double(*f3())(int)
declares a function returning a pointer-to-function. It emphasises thatf3()
is adouble(*)(int)
, that*f3()
is adouble(int)
, and that(*f3())(n)
isdouble
; and finallyvoid (*signal(int sig, void(*func)(int)))(int)
emphasises thatsignal(sig, func)
isvoid(*)(int)
, and that the namefunc
is also of typevoid(*)(int)
.
So signal
turns out to be a function returning a pointer-to-function. It
takes two arguments, one of which is also a pointer-to-function.
These rules do not quite hold for C++, which has a "heavier emphasis on types" than its predecessor. For instance, reference variables, declared like
int& x = y;
don't require the use of an &
to get at the referenced object. In this
example the type of the expression x
is int
and x
is indistinguishable
from y
, see [expr.type].
Also worth mentioning is https://cdecl.org, which can be used to convert C declarations into a psuedo-English description – occasionally useful for checking your understanding of a complicated declaration.
Footnotes:
Dennis Ritchie. History of Programming Languages-II. Thomas J. Bergin, Jr. and Richard G. Gibson, Jr. ACM Press (New York) and Addison-Wesley (Reading, Mass), 1996; ISBN 0-201-89502-1