Declarations

Overview

A declaration specifies the interpretation and attributes of a set of identifiers. It indicates linkage, storage duration, and part of the type of the entities that the declarators denote. For example, the following declaration has two declarators x and y, both of type const int, declared in file scope with static storage duration.

extern const int x, y;

C declarations were designed so that the declaration of an object looks like the use of the object. This isn't always true, but for the most part this philosophy can be leveraged to read them.

Declarators

A declarator in C is roughly an identifier along with pointers, function brackets, or array indications.

A declarator is said to be full if is not part of another declarator. If any part of a full declarator specifies a variable length array type, the declarator is said to be variably modified. Types containing variably modified declarators are likewise called variably modified types (VMTs).

Definitions

A definition is a declaration that causes storage to be reserved for the object (for object types) or includes the function body (for function types).

Since C23, function definitions may omit parameter names. This mirrors how function declarations work.

Initializers

An initializer is an expression that gives an object a value at time of declaration.

All objects (except VLAs) can be zero-initialized with = { 0 }. Since C23, all objects (including VLAs) can instead be empty initialized with = {}. Such objects are also zeroed.

Prototypes

There exist two ways for a function declaration to use declarators: parameter type lists and identifier type lists. Note that identifier type lists were removed in the C23 standard though. To make the distinction clear, consider the following ways of defining an add function:

int f(int x, int y) { return x + y; }  // Paramter type list
int f(x, y) int x; int y; { return x + y }  // Identifier type list

A function prototype is a function declaration that specifies a function signature. A prototype with empty parentheses (e.g. <T> foo()) is interpreted differently depending on the standard:

Empty identifier lists are invalid. Thus prototypes are only possible with parameter type lists. Examples include:

// C17: Empty identifer list.
//      `foo` takes an unknown specification of arguments.
// C23: Equivalent to `void foo(void)`.
void foo();
// C17: Uses a non-empty identifier list. Compiler error.
void foo(x, y);
// C17: Uses a non-empty identifier list. Compiler error.
void foo(x, y) int x; int y;
// C17: Uses a non-empty identifier list. Definitions allow this.
void foo(x, y) int x; int y; { }
// Uses a non-empty parameter list.
// This prototypes a function `foo` that takes no arguments.
void foo(void);
// Uses a non-empty parameter list.
// This defines a function `foo` that takes no arguments.
void foo(void) {}

Array Syntax

Functions that receive pointers should generally prefer using array syntax for additional documentation and static checking:

void foo(T a[static 1]);
void bar(T a[static N]);
void baz(size_t n, T a[n]);
void qux(T *a);

These generally should be interpreted in the following way:

Additionally, the following pairs of lines are equivalent means of expressing the same parameters:

void foo(T a[const]);
void foo(T *const a);

void bar(const int a[]);
void bar(const int *a);

This holds for other type qualifiers like volatile and restrict.

Function Pointers

Whenever a function pointer cannot be null, we can supply a function pointer parameter without a *. For example, the first line is a non-null variant of the second:

int atexit(void handler(void));
int atexit(void (*handler)(void));

VMT Parameters

Within a function prototype, a parameter can denote a VMT using [*] syntax. Unlike a parameter with suffix [], a parameter with suffix [*] is considered complete (but with unspecified size).

For example, the following prototypes are all (more or less) equivalent:

int sum2d(int  , int  , int a[*][*]);
int sum2d(int n, int  , int a[n][*]);
int sum2d(int  , int m, int a[*][m]);
int sum2d(int n, int m, int a[n][m]);
int sum2d(int  , int  , int a[][*]);
int sum2d(int  , int  , int (*a)[*]);
int sum2d(int  , int m, int (*a)[m]);

Arrays passed to functions still undergo array-to-pointer decay. Therefore array arguments lose their innermost dimension.

Precedence Rules

Declarations can be read by complying with the precedence rules outlined below:

  1. Find the name of the declaration.
  2. Obey the following precedence rules:
    1. Parentheses grouping together parts of a declaration
    2. Postfix operators () and []
    3. Prefix operator: the asterisk * denoting "pointer to"
  3. If const and/or volatile keyword is next to a type specifier, it applies to the type specifier. Otherwise it applies to the pointer asterisk on its immediate left.
Powered by Forestry.md