Declarators in C
Understanding declarators in C language is essential to be able to manage variables, pointers and functions safely and effectively.
This topic may seem complex due to the multiple combinations of symbols (*, [], ()) and precedence rules. However, with some key principles, it is possible to decipher even the most intricate declarations and use typedef to simplify the code.
Declarators
A declarator consists of an identifier (the name of the variable or function being declared), possibly preceded by the * symbol or followed by [] or (). By combining *, [] and (), we can create declarators of surprising complexity.
Before examining the more complicated declarators, let's review the declarators seen in previous lessons. In the simplest case, a declarator is just an identifier, like i in the following example:
int i;
Declarators can also contain the symbols *, [] and ():
-
A declarator that starts with
*represents a pointer:int *p; -
A declarator that ends with
[]represents an array:int a[10];The brackets can be left empty if the array is a parameter, if it has an initializer or if its storage class is
extern:extern int a[];Since
ais defined elsewhere in the program, the compiler does not need to know its length (in the case of a multidimensional array, only the first pair of brackets can be empty).The C99 standard provides two additional options for what can appear between the brackets in an array parameter declaration:
-
A declarator that ends with
()represents a function:int abs(int i); void swap(int *a, int *b); int find_largest(int a[], int n);
The C language allows parameter names to be omitted in a function declaration:
int abs(int);
void swap(int *, int *);
int find_largest(int [], int);
The parentheses can even be left empty:
int abs();
void swap();
int find_largest();
The declarations in the group above specify the return type of the functions abs, swap and find_largest, but do not provide information about their arguments. Leaving the parentheses empty is not the same as writing void between them, which would indicate the absence of arguments. The empty-parentheses style of function declarations has largely disappeared; it is inferior to the prototype style introduced in C89, since it does not allow the compiler to check whether function calls have the correct arguments.
In summary:
Declarator
A declarator consists of an identifier (the name of the variable or function being declared), possibly preceded by the * symbol or followed by [] or ().
If all declarators were this simple, programming in C would be trivial. Unfortunately, in practice declarators often combine the *, [] and () notations. We have already seen some examples. We know, for example, that
int *ap[10];
declares an array of 10 pointers to integers. We know that
float *fp(float);
declares a function that has a parameter of type float and returns a pointer to float.
In the lesson on function pointers, we saw that
void (*pf)(int);
declares a pointer to function with argument of type int and return value void.
For this reason, it is important to understand how to interpret complex declarators. Let's see how to do it.
Decoding Complex Declarators
Up to this point, we have not had too many problems understanding declarators. But what about declarators like the following?
int *(*x[10])(void);
This declarator combines *, [] and (), so it is not obvious whether x is a pointer, an array or a function.
Fortunately, there are two simple rules that will help us understand any declaration, no matter how convoluted:
- Always read declarators from the inside out. In other words, locate the identifier being declared, then start interpreting the declaration from there outward.
- When there is a choice, always prefer
[]and()over*. If*precedes the identifier and[]follows it, the identifier represents an array, not a pointer. Similarly, if*precedes the identifier and()follows it, the identifier represents a function, not a pointer. Of course, we can always use parentheses to modify the normal precedence of[]and()over*.
Let's see how these rules apply to our simple examples. In the declaration:
int *ap[10];
the identifier is ap. Since * precedes ap and [] follows it, we give precedence to [], so ap is an array of pointers.
In the declaration:
float *fp(float);
the identifier is fp. Since * precedes fp and () follows it, we give precedence to (), so fp is a function that returns a pointer.
The declaration:
void (*pf)(int);
is a bit more complicated. Since *pf is enclosed in parentheses, pf must be a pointer. But (*pf) is followed by (int), so pf must point to a function with argument of type int. The word void represents the return type of this function.
As a last example, understanding a complex declarator often involves a zigzag step from one side of the identifier to the other. Let's look at this:
int *(*x[10])(void);
First, we locate the identifier being declared (x). We see that x is preceded by * and followed by [10]. Since [] has precedence over *, we understand that x is an array. Then we move to the left to discover the type of the array elements (pointers). Then we move to the right to see what type of data they point to (functions with no arguments). Finally, again to the left to discover that each function returns a pointer to int. Graphically, here is the process:
xis an array.- of pointers.
- to functions with no arguments.
- that return pointer to
int.
Some Things Cannot be Declared
Mastering C declarations takes time and practice. The only good news is that there are some things that cannot be declared in C. Functions cannot return arrays:
int f(int)[]; /*** WRONG ***/
Warning Functions cannot return arrays.
Functions cannot return functions:
int g(int)(int); /*** WRONG ***/
Warning Functions cannot return functions.
Arrays of functions are also not possible:
int a[10](int); /*** WRONG ***/
Warning Arrays of functions are not allowed.
In any case, we can use pointers to achieve the desired effect. A function cannot return an array, but it can return a pointer to an array. A function cannot return another function, but it can return a function pointer. Arrays of functions are not allowed, but an array can contain pointers to functions.
Using typedef to Simplify Declarations
Some programmers use typedef to help simplify complex declarations. Let's consider the declaration of x that we examined in this section:
int *(*x[10])(void);
To make the type of x clearer, we could use the following series of type definitions:
typedef int *Fcn(void);
typedef Fcn *Fcn_ptr;
typedef Fcn_ptr Fcn_ptr_array[10];
Fcn_ptr_array x;
If we read these lines from the last to the first, we see that x is of type Fcn_ptr_array: an array of Fcn_ptr values. An Fcn_ptr is a pointer to type Fcn, and an Fcn is a function that has no arguments and returns a pointer to int.
In Summary
In this lesson we learned that:
- A declarator links the identifier to type information (such as pointer, array or function).
- The fundamental rules for interpreting complex declarations are: start from the name of the variable and give precedence to
[]and()over*. - Some combinations are not allowed in C (for example, functions that return arrays or functions), but there are alternative solutions using pointers.
- In many cases,
typedefhelps make code more readable, allowing a complicated declaration to be broken into multiple type definitions. - Clearly understanding declarators avoids compilation errors and simplifies code maintenance and extension.