Chapter 11. Declarations

A declaration determines the significance and properties of one or more identifiers. _Static_assert declarations, introduced in C11, are an exception: these static assertions do not declare identifiers, but only instruct the compiler to test whether a constant expression is nonzero. Static assertions are only classed as declarations because of their syntax.

In other declarations, the identifiers you declare can be the names of objects, functions, types, or other things, such as enumeration constants. Identifiers of objects and functions can have various types and scopes. The compiler needs to know all of these characteristics of an identifier before you can use it in an expression. For this reason, each translation unit must contain a declaration of each identifier used in it.

Labels used as the destination of goto statements may be placed before any statement. These identifiers are declared implicitly where they occur. All other identifiers require explicit declaration before their first use, either outside of all functions or at the beginning of a block. Beginning with C99, declarations may also appear after statements within a block.

After you have declared an identifier, you can use it in expressions until the end of its scope. The identifiers of objects and functions can have file or block scope (see “Identifier Scope”).

There are several different kinds of declarations:

  • Declarations that only declare a structure, union, or enumeration tag, or the members of an enumeration (that is, the enumeration constants)

  • Declarations that declare one or more object or function identifiers

  • typedef declarations, which declare new names for existing types

  • _Static_assert declarations, which instruct the compiler to test an assertion without declaring an identifier (C11)

Declarations of enumerated, structure, and union types are described in Chapters 2 and 10. This chapter deals mainly with object, function, and typedef declarations.

Object and Function Declarations

These declarations contain a declarator list with one or more declarators. Each declarator declares an identifier for an object or a function. The general form of this kind of declaration is:

[storage_class_specifier] type declarator [, declarator [, ...]];

The parts of this syntax are as follows:

storage_class_specifier

No more than one of the storage class specifiers extern, static, _Thread_local, auto, or register, or the specifier _Thread_local in conjunction with extern or static. The exact meanings of the storage class specifiers, and restrictions on their use, are described in “Storage Class Specifiers”.

type

At least a type specifier, possibly with type qualifiers. The type specifier may be any of these:

  • A basic type
  • The type void
  • An enumerated, structure, or union type
  • A name defined by a previous typedef declaration

In a function declaration, type may also include one of the type specifiers inline or _Noreturn.

In an object declaration, type may also contain one or more of the type qualifiers const, volatile, and restrict. In C11 implementations that support atomic objects, an object declaration may declare the object as atomic by using the type qualifier _Atomic, or by using a type specifier of the form _Atomic(type_name). The various type qualifiers are described with examples in “Type Qualifiers”.

The C11 keyword _Alignas allows you to influence the alignment of objects you declare. For more on the alignment of objects, see “The Alignment of Objects in Memory”.

declarator

The declarator list is a comma-separated list containing at least one declarator. A declarator names the identifier that is being declared. If the declarator defines an object, it may also include an initializer for the identifier. There are four different kinds of declarators:

Function declarator

The identifier is declared as a function name if it is immediately followed by a left parenthesis (().

Array declarator

The identifier is declared as an array name if it is immediately followed by a left bracket ([).

Pointer declarator

The identifier is the name of a pointer if it is preceded by an asterisk (*)—possibly with interposed type qualifiers—and if the declarator is neither a function nor an array declarator.

Other

Otherwise, the identifier designates an object of the specified type.

A declarator in parentheses is equivalent to the same declarator without the parentheses, and the rules listed here assume that declarations contain no unnecessary parentheses. However, you can use parentheses intentionally in declarations to control the associations between the syntax elements described. We will discuss this in detail in “Complex Declarators”.

Examples

Let us examine some examples of object and function declarations. We discuss declarations of typedef names in “typedef Declarations”.

In the following example, the declarator list in the first line contains two declarators, one of which includes an initializer. The line declares two objects, iVar1 and iVar2, both with type int. iVar2 begins its existence with the value 10:

int iVar1, iVar2 = 10;
static char msg[] = "Hello, world!";

The second line in this example defines and initializes an array of char named msg with static storage duration (we discuss storage duration in “Storage Class Specifiers”).

Next, you see the declaration of an external variable named status with the qualified type volatile short:

extern volatile short status;

The next declaration defines an anonymous enumerated type with the enumeration constants OFF and ON, as well as the variable toggle with this type. The declaration initializes toggle with the value ON:

enum { OFF, ON } toggle = ON;

The following example defines the structure type struct CharColor, whose members are the bit-fields fg, bg, and bl. It also defines the variable attribute with this type, and initializes the members of attribute with the values 12, 1, and 0.

struct CharColor { unsigned fg:4, bg:3, bl:1; } attribute = {12, 1, 0};

The second line of the next example defines an array named clientArray with 100 elements of type struct Client, and a pointer to struct Client named clientPtr, initialized with the address of the first element in clientArray:

struct Client { char name[64], pin[16]; /* ... */ };
struct Client clientArray[100], *clientPtr = clientArray;

Next you see a declaration of a float variable, x, and an array, flPtrArray, whose 10 elements have the type pointer to float. The first of these pointers, flPtrArray[0], is initialized with &x; the remaining array elements are initialized as null pointers:

float x, *flPtrArray[10] = { &x };

The following line declares the function func1() with the return value type int. This declaration offers no information about the number and types of the function’s parameters, if any:

int func1();

We’ll move on to the declaration of a static function named func2(), whose only parameter has the type pointer to double, and which also returns a pointer to double:

static double *func2( double * );

Last, we define the inline function printAmount(), with two parameters, returning int:

inline int printAmount( double amount, int width )
{ return printf( "%*.2lf", width, amount ); }

Storage Class Specifiers

A storage class specifier in a declaration modifies the linkage of the identifier (or identifiers) declared, and the storage duration of the corresponding objects. (The concepts of linkage and storage duration are explained individually in later sections of this chapter.)

Tip

A frequent source of confusion in regard to C is the fact that linkage (which is a property of identifiers) and storage duration (which is a property of objects) are both influenced in declarations by the same set of keywords—the storage class specifiers. As we explain in the upcoming sections of this chapter, the storage duration of an object can be automatic, static, or allocated, and the linkage of an identifer can be external, internal, or none. Expressions such as “static linkage” or “external storage” in the context of C declarations are meaningless except as warning signs of incipient confusion. Remember: objects have storage duration, not linkage; and identifiers have linkage, not storage duration.

No more than one storage class specifier may appear in a declaration. Function identifiers may be accompanied only by the storage class specifier extern or static. Function parameters may take only the storage class specifier register. The five storage class specifiers have the following meanings:

auto

Objects declared with the auto specifier have automatic storage duration. This specifier is permissible only in object declarations within a function. In ANSI C, objects declared within a function have automatic storage duration by default, and the auto specifier is archaic.

register

You can use the specifier register when declaring objects with automatic storage duration. The register keyword is a hint to the compiler that the object should be made as quickly accessible as possible—ideally, by storing it in a CPU register. However, the compiler may treat some or all objects declared with register the same as ordinary objects with automatic storage duration. In any case, programs must not use the address operator on objects declared with the register specifier.

static

A function identifier declared with the specifier static has internal linkage. In other words, such an identifier cannot be used in another translation unit to access the function.

An object identifier declared with static has either no linkage or internal linkage, depending on whether the object’s definition is inside a function or outside all functions. Objects declared with static always have static storage duration. Thus, the specifier static allows you to define local objects—that is, objects with block scope—that have static storage duration.

extern

Function and object identifiers declared with the extern specifier have external linkage. You can use them anywhere in the entire program. External objects have static storage duration.

_Thread_local

The specifier _Thread_local declares the given object as thread-local, which means that each thread has its own separate instance of the object. Only objects can be declared as thread-local, not functions. If you declare a thread-local object within a function, the declaration must also have either the extern or the static specifier. In expressions, the identifier of a thread-local object always refers to the local instance of the object belonging to the thread in which the expression is being evaluated. For an example, see “Using Thread-Local Objects”.

Type Qualifiers

You can modify types in a declaration by including the type qualifiers const, volatile, restrict, and _Atomic. A declaration may contain any number of type qualifiers in any order. A type qualifier list may even contain the same type qualifier several times, or the same qualifier may be applied repeatedly through qualified typedef names. The compiler ignores such repetitions of any qualifier, treating them as if the qualifier were present only once.

The individual type qualifiers have the following meanings:

const

An object whose type is qualified with const is constant; the program cannot modify it after its definition.

volatile

An object whose type is qualified with volatile may be modified by other processes or events. The volatile keyword instructs the compiler to reread the object’s value each time it is used, even if the program itself has not changed it since the previous access. This is most commonly used in programming for hardware interfaces, where a value can be changed by external events.

restrict

The restrict qualifier is applicable only to object pointer types. The type qualifier restrict was introduced in C99, and is a hint to the compiler that the object referenced by a given pointer, if it is modified at all, will not be accessed in any other way except using that pointer, whether directly or indirectly. This feature allows the compiler to apply certain optimization techniques that would not be possible without such a restriction. The compiler may ignore the restrict qualifier without affecting the result of the program.

_Atomic

An object declared with the type qualifier _Atomic is an atomic object. Arrays cannot be atomic. Support for atomic objects is optional: C11 implementations may define the macro __STDC_NO_ATOMICS__ to indicate that programs cannot declare atomic objects. For more information about atomic objects, see “Atomic Objects”.

The compiler may store objects qualified as const but not volatile, in a read-only segment of memory. It may also happen that the compiler allocates no storage for such an object if the program does not use its address.

Objects qualified with both const and volatile, such as the object ticks in the following example, cannot be modified by the program itself but may be modified by something else, such as a clock chip’s interrupt handler:

extern const volatile int ticks;

Here are some more examples of declarations using qualified types:

const int limit = 10000;                    // A constant int object.
typedef struct { double x, y, r; } Circle;  // A structure type.
const Circle unit_circle = { 0, 0, 1 };     // A constant Circle object.
const float v[] = { 1.0F, 0.5F, 0.25F };    // An array of constant
                                            // float elements.
volatile short * restrict vsPtr;            // A restricted pointer to
                                            // volatile short.

With pointer types, the type qualifiers to the right of the asterisk qualify the pointer itself, and those to the left of the asterisk qualify the type of object it points to. In the last example, the pointer vsPtr is qualified with restrict, and the object it points to is qualified with volatile. For more details, including more about restricted pointers, see “Pointers and Type Qualifiers”.

Declarations and Definitions

You can declare an identifier as often as you want, but only one declaration within its scope can be a definition. Placing the definitions of objects and functions with external linkage in header files is a common way of introducing duplicate definitions and is therefore not a good idea.

An identifier’s declaration is a definition in the following cases:

  • A function declaration is a definition if it contains the function block. Here is an example:

    int iMax( int a, int b );   // This is a declaration, not a
                                // definition.
    int iMax( int a, int b )    // This is the function's definition.
    { return ( a >= b ? a : b ); }
  • An object declaration is a definition if it allocates storage for the object. Declarations that include initializers are always definitions. Furthermore, all declarations within function blocks are definitions unless they contain the storage class specifier extern. Here are some examples:

    int a = 10;               // Definition of a.
    extern double b[];        // Declaration of the array b, which is
                              // defined elsewhere in the program.
    void func()
    {
      extern char c;          // Declaration of c, not a definition.
      static short d;         // Definition of d.
      float e;                // Definition of e.
      /* ... */
    }

    If you declare an object outside of all functions, without an initializer and without the storage class specifier extern, the declaration is a tentative definition. Here are some examples:

    int i, v[];                // Tentative definitions of i, v and j.
    static int j;

    A tentative definition of an identifier remains a simple declaration if the translation unit contains another definition for the same identifier. If not, then the compiler behaves as if the tentative definition had included an initializer with the value zero, making it a definition. Thus, the int variables i and j in the previous example, whose identifiers are declared without initializers, are implicitly initialized with the value 0, and the int array v has one element, with the initial value 0.

Complex Declarators

The symbols (), [ ], and * in a declarator specify that the identifier has a function, array, or pointer type. A complex declarator may contain multiple occurrences of any or all of these symbols. This section explains how to interpret such declarators.

The basic symbols in a declarator have the following meanings:

()

A function whose return value has the type…

[ ]

An array whose elements have the type…

*

A pointer to the type…

In declarators, these symbols have the same priority and associativity as the corresponding operators would have in an expression. Furthermore, as in expressions, you can use additional parentheses to modify the order in which they are interpreted. Here is an example:

int *abc[10];    // An array of 10 elements whose
                 // type is pointer to int.
int (*abc)[10];  // A pointer to a array of 10
                 // elements whose type is int.

In a declarator that involves a function type, the parentheses that indicate a function may contain the parameter declarations. The following example declares a pointer to a function type:

int (*fPtr)(double x);    // fPtr is a pointer to a function that has
                          // one double parameter and returns int.

The declarator must include declarations of the function parameters if it is part of the function definition.

When interpreting a complex declarator, always begin with the identifier. Starting from there, repeat the following steps in order until you have interpreted all the symbols in the declarator:

  1. If a left parenthesis (() or bracket ([) appears immediately to the right, then interpret the pair of parentheses or brackets.

  2. Otherwise, if an asterisk (*) appears to the left, interpret the asterisk.

Here is an example:

extern char *(* fTab[])(void);

Table 11-1 interprets this example bit by bit. The third column is meant to be read from the top row down, as a sentence.

Table 11-1. Interpretation of extern char *(* fTab[ ])(void);
Step Symbols interpreted Meaning (read this column from the top down, as a sentence)
1. Start with the identifier. fTab fTab is…
2. Brackets to the right. fTab[] an array whose elements have the type…
3. Asterisk to the left. (* fTab[]) pointer to…
4. Function parentheses (and parameter list) to the right. (* fTab[])(void) a function, with no parameters, whose return value has the type…
5. Asterisk to the left. *(* fTab[])(void) pointer to…
6. No more asterisks, parentheses, or brackets: read the type name. char *(* fTab[])(void) char.

fTab has an incomplete array type because the declaration does not specify the array length. Before you can use the array, you must define it elsewhere in the program with a specific length.

The parentheses around *fTab[] are necessary. Without them, fTab would be declared as an array whose elements are functions—which is impossible.

The next example shows the declaration of a function identifier, followed by its interpretation:

float (* func())[3][10];
The identifier func is...
a function whose return value has the type...
pointer to...
an array of three elements of type...
array of ten elements of type...
float.

In other words, the function func returns a pointer to a two-dimensional array of 3 rows and 10 columns. Here again, the parentheses around * func() are necessary, as without them the function would be declared as returning an array—which is impossible.

Type Names

To convert a value explicitly from one type to another using the cast operator, you must specify the new type by name. For example, in the cast expression (char *)ptr, the type name is char * (read: “char pointer” or “pointer to char”). When you use a type name as the operand of sizeof, it appears the same way, in parentheses. Function prototype declarations also designate a function’s parameters by their type names, even if the parameters themselves have no names.

The syntax of a type name is like that of an object or function declaration, but with no identifier (and no storage class specifier). Here are two simple examples to start with:

unsigned char

The type unsigned char

unsigned char *

The type “pointer to unsigned char

In the examples that follow, the type names are more complex. Each type name contains at least one asterisk (*) for “pointer to,” as well as parentheses or brackets. To interpret a complex type name, start with the first pair of brackets or parentheses that you find to the right of the last asterisk. (If you were parsing a declarator with an identifier rather than a type name, the identifier would be immediately to the left of those brackets or parentheses.) If the type name includes a function type, then the parameter declarations must be interpreted separately:

float *[]

The type “array of pointers to float.” The number of elements in the array is undetermined.

float (*)[10]

The type “pointer to an array of ten elements whose type is float.”

double *(double *)

The type “function whose only parameter has the type pointer to double, and which also returns a pointer to double.”

double (*)()

The type “pointer to a function whose return value has the type double.” The number and types of the function’s parameters are not specified.

int *(*(*)[10])(void)

The type “pointer to an array of ten elements whose type is pointer to a function with no parameters which returns a pointer to int.”

typedef Declarations

The easy way to use types with complex names, such as those described in the previous section, is to declare simple synonyms for them. You can do this using typedef declarations.

A typedef declaration starts with the keyword typedef, followed by the normal syntax of an object or function declaration, except that no storage class or _Alignas specifiers and no initializers are permitted.

Each declarator in a typedef declaration defines an identifier as a synonym for the specified type. The identifier is then called a typedef name for that type. Without the keyword typedef, the same syntax would declare an object or function of the given type. Here are some examples:

typedef unsigned int UINT, UINT_FUNC();
typedef struct Point { double x, y; } Point_t;
typedef float Matrix_t[3][10];

In the scope of these declarations, UINT is synonymous with unsigned int, and Point_t is synonymous with the structure type struct Point. You can use the typedef names in declarations, as the following examples show:

UINT ui = 10, *uiPtr = &ui;

The variable ui has the type unsigned int, and uiPtr is a pointer to unsigned int.

UINT_FUNC *funcPtr;

The pointer funcPtr can refer to a function whose return value has the type unsigned int. The function’s parameters are not specified:

Matrix_t *func( float * );

The function func() has one parameter, whose type is pointer to float, and returns a pointer to the type Matrix_t.

Example 11-1 uses the typedef name of one structure type, Point_t, in the typedef definition of a second structure type.

Example 11-1. typedef declarations
typedef struct Point { double x, y; } Point_t;
typedef struct { Point_t top_left; Point_t bottom_right; } Rectangle_t;

Ordinarily, you would use a header file to hold the definitions of any typedef names that you need to use in multiple source files. However, you must make an exception in the case of typedef declarations for types that contain a variable-length array. Variable-length arrays can only be declared within a block, and the actual length of the array is calculated anew each time the flow of program execution reaches the typedef declaration. Here is an example:

int func( int size )
{
  typedef float VLA[size]; // A typedef name for the type "array of
                           // float whose length is the value of size."
  size *= 2;
  VLA temp;                // An array of float whose length is the
                           // value that size had
                           // in the typedef declaration.
  /* ... */
}

The length of the array temp in this example depends on the value that size had when the typedef declaration was reached, not the value that size has when the array definition is reached.

One advantage of typedef declarations is that they help to make programs more easily portable. Types that are necessarily different on different system architectures, for example, can be called by uniform typedef names. typedef names are also helpful in writing human-readable code. As an example, consider the prototype of the standard library function qsort():

void qsort( void *base, size_t count, size_t size,
            int (*compare)( const void *, const void * ));

We can make this prototype much more readable by using a typedef name for the comparison function’s type:

typedef int CmpFn( const void *, const void * );
void qsort( void *base, size_t count, size_t size, CmpFn *compare );

_Static_assert Declarations

The _Static_assert declaration, introduced in C11, is a special case among declarations. It is only an instruction to the compiler to test an assertion, and does not declare an identifier at all. A static assertion has the following syntax:

_Static_assert( constant_expression , string_literal );

The assertion to be tested, constant_expression, must be a constant expression with an integer type (see “Integer Constants”). If the expression is true—that is, if its value is not 0—the _Static_assert declaration has no effect. If the evaluation of the expression yields the value 0, however, the compiler generates a error message containing the specified string literal. The string literal should contain only characters of the basic source character set, as extended characters are not necessarily displayed. In the following example, a static assertion ensures that objects of the type int are bigger than two bytes:

_Static_assert( sizeof(int) > 2 , "16-bit code not supported");

If the type int is only two bytes wide, the compiler’s error message may look like this:

demo.c(10): fatal error:
Static assertion failed: "16-bit code not supported".

If you include the header assert.h in your program, you can also use the synonym static_assert in place of the keyword _Static_assert.

The new capability of testing an assertion at compile time is an addition to the two related techniques:

  • The macro assert, described in Chapter 18, which tests an assertion during the program’s execution

  • The preprocessor directive #error, described in Chapter 15, which makes the preprocessor exit with an error message on a condition specified using an #if directive

Linkage of Identifiers

An identifier that is declared in several translation units, or several times in the same translation unit, may refer to the same object or function in each instance. The extent of an identifier’s identity in and among translation units is determined by the identifier’s linkage. The term reflects the fact that identifiers in separate source files need to be linked if they are to refer to a common object.

Identifiers in C have either external, internal, or no linkage. The linkage is determined by the declaration’s position and storage class specifier, if any. Only object and function identifiers can have external or internal linkage.

External Linkage

An identifier with external linkage represents the same function or object throughout the program. The compiler presents such identifiers to the linker, which resolves them with other occurrences in other translation units and libraries.

Function and object identifiers declared with the storage class specifier extern have external linkage, with one exception: if an identifier has already been declared with internal linkage, a second declaration within the scope of the first cannot change the identifier’s linkage to external.

The compiler treats function declarations without a storage class specifier as if they included the specifier extern. Similarly, any object identifiers that you declare outside all functions and without a storage class specifier have external linkage.

Internal Linkage

An identifier with internal linkage represents the same object or function within a given translation unit. The identifier is not presented to the linker. As a result, you cannot use the identifier in another translation unit to refer to the same object or function.

A function or object identifier has internal linkage if it is declared outside all functions and with the storage class specifier static.

Identifiers with internal linkage do not conflict with similar identifiers in other translation units. If you declare an identifier with internal linkage in a given translation unit, you cannot also declare and use an external identifier with the same spelling in that translation unit.

No Linkage

All identifiers that have neither external nor internal linkage have no linkage. Each declaration of such an identifier therefore introduces a new entity. Identifiers with no linkage include the following:

  • Identifiers that are not names of variables or functions, such as label names, structure tags, and typedef names

  • Function parameters

  • Object identifiers that are declared within a function and without the storage class specifier extern

Here are a few examples:

int func1( void );         // func1 has external linkage.
int a;                     // a has external linkage.
extern int b = 1;          // b has external linkage.
static int c;              // c has internal linkage.

static void func2( int d ) // func2 has internal linkage; d has no
                           // linkage.
{
  extern int a;            // This a is the same as that above, with
                           // external linkage.
  int b = 2;               // This b has no linkage, and hides the
                           // external b declared above.
  extern int c;            // This c is the same as that above, and
                           // retains internal linkage.
  static int e;            // e has no linkage.
  /* ... */
}

As this example illustrates, an identifier with external or internal linkage is not always visible. The identifier b with no linkage, declared in the function func2(), hides the identifier b with external linkage until the end of the function block (see “Identifier Scope”).

Storage Duration of Objects

During the execution of the program, each object exists as a location in memory for a certain period, called its lifetime. There is no way to access an object before or after its lifetime. For example, the value of a pointer becomes invalid when the object that it references reaches the end of its lifetime.

In C, the lifetime of an object is determined by its storage duration. Objects in C have one of four kinds of storage duration: static, thread, automatic, or allocated. The C standard does not specify how objects must be physically stored in any given system architecture, but typically, objects with static or thread storage duration are located in a data segment of the program, and objects with automatic storage duration are located on the stack. Allocated storage is memory that the program obtains at runtime by calling the malloc(), calloc(), and realloc() functions. Dynamic storage allocation is described in Chapter 12.

Static Storage Duration

Objects that are defined outside all functions, or within a function and with the storage class specifier static, have static storage duration. These include all objects whose identifiers have internal or external linkage.

All objects with static storage duration are generated and initialized before execution of the program begins. Their lifetime spans the program’s entire runtime.

Thread Storage Duration

Objects defined with the storage class specifier _Thread_local are called thread-local objects and have thread storage duration. The storage duration of a thread-local object is the entire runtime of the thread for which it is created. Each thread has its own separate instance of a thread-local object, which is initialized when the thread starts.

Automatic Storage Duration

Objects defined within a function and with no storage class specifier (or with the unnecessary specifier auto) have automatic storage duration. Function parameters also have automatic storage duration. Objects with automatic storage duration are generally called automatic variables for short.

The lifetime of an automatic object is delimited by the braces ({}) that begin and end the block in which the object is defined. Variable-length arrays are an exception: their lifetime begins at the point of declaration, and ends with the identifier’s scope—that is, at the end of the block containing the declaration, or when a jump occurs to a point before the declaration.

Each time the flow of program execution enters a block, new instances of any automatic objects defined in the block are generated (and initialized, if the declaration includes an initializer). This fact is important in recursive functions, for example.

Initialization

You can explicitly specify an object’s initial value by including an initializer in its definition. An object defined without an initializer either has an undetermined initial value, or is implicitly initialized by the compiler.

Implicit Initialization

Objects with automatic storage duration have an undetermined initial value if their definition does not include an initializer. Function parameters, which also have automatic storage duration, are initialized with the argument values when the function call occurs. All other objects have static storage duration, and are implicitly initialized with the default value 0, unless their definition includes an explicit initializer. Or, to put it more exactly:

  • Objects with an arithmetic type have the default initial value 0.

  • The default initial value of pointer objects is a null pointer (see “Initializing Pointers”).

The compiler applies these rules recursively in initializing array elements, structure members, and the first members of unions.

Explicit Initialization

An initializer in an object definition specifies the object’s initial value explicitly. The initializer is appended to the declarator for the object’s identifier with an equals sign (=). The initializer can be either a single expression or a list of initializer expressions enclosed in braces.

For objects with a scalar type, the initializer is a single expression:

#include <string.h>                // Prototypes of string functions.
double var = 77, *dPtr = &var;
int (*funcPtr)( const char*, const char* ) = strcmp;

The initializers here are 77 for the variable var, and &var for the pointer dPtr. The function pointer funcPtr is initialized with the address of the standard library function strcmp().

As in an assignment operation, the initializer must be an expression that can be implicitly converted to the object’s type. In the previous example, the constant value 77, with type int, is implicitly converted to the type double.

Objects with an array, structure, or union type are initialized with a comma-separated list containing initializers for their individual elements or members:

short a[4] = { 1, 2, 2*2, 2*2*2 };
Rectangle_t rect1 = { { -1, 1 }, { 1, -1 } };

The type Rectangle_t used here is the typedef name of the structure we defined in Example 11-1, whose members are structures with the type Point_t.

The initializers for objects with static storage duration must be constant expressions, as in the previous examples. Automatic objects are not subject to this restriction. You can also initialize an automatic structure or union object with an existing object of the same type:

#include <string.h>            // Prototypes of string functions.
/* ... */
void  func( const char *str )
{
  size_t len = strlen( str );  // Call a function to initialize len.
  Rectangle_t rect2 = rect1;   // Refers to rect1 from the previous
                               // example.
  /* ... */
}

More details on initializing arrays, structures, and unions, including the initialization of strings and the use of element designators, are presented in “Initializing Arrays”, “Initializing Structures”, and “Initializing Unions”.

Objects declared with the type qualifier const ordinarily must have an initializer, as you can’t assign them the desired value later. However, a declaration that is not a definition, such as the declaration of an external identifier, must not include an initializer. Furthermore, you cannot initialize a variable-length array.

void func( void )
{
  extern int n;            // Declaration of n, not a definition.
  char buf[n];             // buf is a variable-length array.
  /* ... */
}

The declarations of the objects n and buf cannot include initializers.