Declaration Specifiers

The declaration specifiers name a base type and can also include specifiers that alter the behavior of the entire declaration. These may appear in any order, but a simplified grammar is shown below,

declaration-specifiers:

[ storage-class-specifier ]

[ function-specifier ]

type-specifier…

[ type-qualifier… ]

Type Specifiers

Every declaration must name a base type, which is specified by a sequence of type specifiers. These may name one of the scalar types, a type alias, or denote a structure, union, or enumeration type as described below:

struct

tag-name

{ member-declaration-list }

union

tag-name

{ member-declaration-list }

enum

tag-name

{ enumerator-list }

This results in a declaration of tag-name with the given definition. Either tag-name or the definition may be omitted, as described in User-Defined Types

Importantly, neither pointer, array, nor function types can be specified as part of type specifiers–these are instead derived from the base type in each declarator, individually.

Type Qualifiers

The base type denoted in the type specifiers may additionally be cvr-qualified using the const, volatile and restrict keywords, all of which affect the compiler’s ability to optimize accesses to qualified objects.

Const Qualification

The const qualifier denotes a type which produces undefined behavior when modified by the program. A compiler might optimize accesses to const-qualified objects under the assumption that their values do not change. Additionally, const-qualified objects may be placed in read-only memory, which is faster to access on some systems.

Volatile Qualification

The volatile qualifier denotes a type which produces side effects when its value is accessed. This implies that its value may “spontaneously” change between accesses, such as in an object which represents memory-mapped I/O in a device driver. This prevents the compiler from assuming that it knows the value of an object and from performing related optimizations.

Restrict Qualification

The restrict qualifier denotes a pointer which does not alias another pointer. A compiler might optimize accesses through a restrict-qualified pointer under the assumption that no modifications through another pointer could have changed the value of the pointed-at object. Many library functions restrict-qualify their parameters.

These qualifiers are often mistaken for compiler-enforced constraints, with disastrous results. While modern compilers may warn of constraint violations in some situations, most cases are impossible for the compiler to detect. It is the programmer’s responsibility to obey cvr-qualification. Modifying a const-qualified object, aliasing a restrict-qualified pointer, or failing to volatile-qualify an externally modified object can all lead to undefined behavior.

Storage Class Specifiers

The storage class specifier modifies the overall behavior of a declaration. There are five storage class specifiers, auto, register, typedef, static, and extern. The validity and effects of storage class specifiers are context dependent, based on the location of the declaration, and whether the declaration is for an object, or function.

No Storage Class

File scope object declarations have external linkage and static storage duration. All other object declarations have no linkage and automatic storage duration. Function declarations have external linkage.

Auto Storage Class

The auto specifier may be applied to declarations of block-scope objects only, and its effects are redundant, so it should not be used. It is a legacy from earlier versions of C that supported an implied type specification, such that declarations like auto x; were once valid.

Register Storage Class

The register specifier may be applied to block-scope object declarations and function parameter declarations only. It is an error to access the address of objects declared with the register storage class, which enables the compiler to store these objects in registers rather than memory, for faster access. Unlike the cvr-qualifiers mentioned above, register is enforced by the compiler.

Typedef Storage Class

The typedef specifier may be applied to block- or file-scope declarations, and modifies the entire declaration so that each declarator declares a type alias, rather than an object or function. For example, int x, f(); declares x as an object of type int, and f as a function returning int. On the other hand, typedef int x, f(); declares x as an alias for int, and f as an alias for int().

Static Storage Class

The static specifier may only be used for file-scope declarations of objects and functions, and for block-scope declarations of objects. At file-scope, static changes the linkage of declared identifiers from external to internal linkage. At block-scope, it changes the declared object’s storage duration from automatic to static, but the object still has no linkage.

Extern Storage Class

The extern specifier may be used for block- or file-scope declarations to specify external linkage of declared identifiers.

The subsections of Practical Declarations each cover how these specifiers are used in practice.

Function Specifiers

There is only one function specifier, inline, and it is used to provide an alternative function definition for an otherwise externally linked function. This allows the compiler to directly inline the function, which means it replaces the function call with the actual code of the function, which is generally faster. Function inlining is an advanced topic that will not be covered in more detail in this class.