Let's start by talking about declaration syntax in general (I'll be using C terminology, although C++ is largely similar). In both C and C++, a declaration contains a sequence of one or more declaration specifiers followed by a comma-separated list of zero or more declarators.
Declaration specifiers include type specifiers (int
, double
, char
, unsigned
, etc.), type qualifiers (const
, volatile
, etc.), storage class specifiers (static
, register
, typedef
, etc.), struct
and union
specifiers, and a few other things we won't get into here.
Declarators include the name of the thing being declared, along with information about that thing's pointer-ness, array-ness, or function-ness (in C++ you also have reference-ness).
When you declare a function, such as
void foo( int, double );
void
is the declaration specifier (type specifier), and foo( int, double )
is the declarator. The type of foo
is fully specified by the combination of the declaration specifier and declarator:
foo -- foo
foo( ) -- is a function taking
foo( ) -- unnamed parameter
foo( int ) -- is an int
foo( int, ) -- unnamed parameter
foo( int, double ) -- is a double
void foo( int, double ) -- returning void
In plain English, the type of foo
is "function taking an int
and double
parameter and returning void
."
You can declare pointers to functions as well:
fptr -- fptr
(*fptr) -- is a pointer to
(*fptr)( ) -- function taking
(*fptr)( ) -- unnamed parameter
(*fptr)( int ) -- is an int
(*fptr)( int, ) -- unnamed parameter
(*fptr)( int, double ) -- is a double
void (*fptr)( int, double ) -- returning void
Again, the sole declaration specifier is void
, and the declarator is (*fptr)( int, double )
.
For syntactic purposes, typedef
is grouped with the storage class specifiers (static
, auto
, register
), but it doesn't behave like other storage class specifiers - instead of affecting the storage or visibility of the thing being declared, it makes the identifier in the declarator an alias for the type. If we stick typedef
on the front of the above declaration:
typedef void (*fptr)( int, double );
then it reads as
fptr -- fptr
typedef fptr -- IS AN ALIAS FOR THE TYPE
typedef (*fptr) -- pointer to
typedef (*fptr)( ) -- function taking
typedef (*fptr)( ) -- unnamed parameter
typedef (*fptr)( int ) -- is an int
typedef (*fptr)( int, ) -- unnamed parameter
typedef (*fptr)( int, double ) -- is a double
typedef void (*fptr)( int, double ) -- returning void
IOW, fptr
is an alias (typedef
name) for the type "pointer to function taking an int
and double
parameter and returning void
", and you can use it to declare pointer objects of that type:
fptr fp1, fp2;
You can do the same thing with other pointer types1:
typedef int *intp; // intp is an alias for the type "pointer to int";
typedef double (*arr)[10]; // arr is an alias for the type "pointer to 10-element array of double"
Declarators can get pretty complex. You can have pointers to functions:
T (*ptr)();
pointers to arrays:
T (*ptr)[N];
arrays of pointers to functions:
T (*ptr[N])();
functions returning pointers to arrays:
T (*foo())[N];
arrays of pointers to functions returning pointers to arrays:
T (*(*arr[N])())[M];
and on and on and on, and sticking typedef
in front of any of them will work:
typedef T (*(*arr[N])())[M];
means arr
is an alias for the type "N-element array of pointers to functions returning pointers to M-element arrays of T
".
- As a rule, you do not want to hide pointers to scalar types behind typedefs unless you can guarantee that the programmer using that type will never have to be aware of the underlying pointer-ness of that type (i.e., will never have to explicitly dereference it with
*
or try to print its value with %p
or anything like that).