Previous Page
Next Page

11.1. General Syntax

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

Declarations of enumerated, structure, and union types are described in Chapter 2 and Chapter 10. This chapter deals mainly with object, function, and typedef declarations. These declarations contain a declarator list with one or more declarators. Each declarator declares a typedef name or an identifier for an object or a function. The general form of this kind of declaration is:

    [typedef | 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, auto, or register. A typedef declaration cannot include a storage class specifier. The exact meanings of the storage class specifiers, and restrictions on their use, are described in "Storage Class Specifiers," later in this section.


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, the type specifier inline may also appear.

type may also contain one or more of the type qualifiers const, volatile, and restrict.


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 qualifiersand 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," later in this chapter.

11.1.1. Examples

Let us examine some examples of object and function declarations. We discuss declarations of typedef names in "typedef Declarations," later in this chapter.

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 the following section).

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]; /* ... */ };   // A structure type.
    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 ); }

11.1.2. 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.)

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 keywordsthe 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 four 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 possibleideally, 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 objectsthat is, objects with block scopethat 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.

11.1.3. Type Qualifiers

You can modify types in a declaration by including the type qualifiers const, volatile, and restrict. 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.


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.

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, while 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 with volatile. For more details, including more about restricted pointers, see "Pointers and Type Qualifiers" in Chapter 9.

11.1.4. 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. 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. 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. 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.

11.1.5. 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. 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 functionswhich 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 arraywhich is impossible.


Previous Page
Next Page