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 ():

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:

Definition

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:

  1. Always read declarators from the inside out. In other words, locate the identifier being declared, then start interpreting the declaration from there outward.
  2. 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:

  1. x is an array.
  2. of pointers.
  3. to functions with no arguments.
  4. that return pointer to int.
Complex Declaration Example
Picture 1: Complex Declaration Example

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 ***/
Note

Warning Functions cannot return arrays.

Functions cannot return functions:

int g(int)(int);  /*** WRONG ***/
Note

Warning Functions cannot return functions.

Arrays of functions are also not possible:

int a[10](int);  /*** WRONG ***/
Note

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, typedef helps 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.