Declarations


Declaration Contexts and Levels · Outer Declaration · Member Declaration · Function Definition · Parameter Declaration · Block Declaration · Prototype Declaration · Type-Name Declaration · Declaration Levels · Visibility and Name Spaces · Name Spaces · Scope · Linkage and Multiple Declarations · Linkage · Tags · Type Definition · Object Declaration · Function Declaration · Reading Declarations · Object Initializers


A translation unit consists of one or more declarations, each of which can:

Declarations can contain other declarations in turn.

This document describes how to use declarations to construct a C program. It describes how to create names and how to use the same name for distinct purposes. It also shows how to write object initializers to specify the initial values stored in objects. The behavior of functions is described separately.

Declaration Contexts and Levels

You can write declarations in different contexts. The syntax of an arbitrary declaration (other than a function definition) is:

The presentation that follows shows graphically how each context restricts the declarations that you can write, by eliminating from this syntax diagram those parts that are not permitted in a given context. It also describes when you must write a name within the declarator part of a declaration and when you must not.

Here is a sampler of all possible declaration contexts:

struct stack {               outer declaration
    int top, a[100];         member declaration
    } stk = {0};

void push(val)               function definition
    int val;                 parameter declaration
    {
    extern void oflo(        block declaration
        char *mesg);         prototype declaration
    if (stk.top < sizeof a /
        sizeof (int))        type-name declaration
        stk.a[stk.top++] = val;
    else
        oflo("stack overflow");
    }

Outer Declaration

You write an outer declaration as one of the declarations that make up a translation unit. An outer declaration is one that is not contained within another declaration or function definition:

You can omit the declarator only for a structure, union, or enumeration declaration that declares a tag. You must write a name within the declarator of any other outer declarator.

Member Declaration

You write a member declaration to declare members of a structure or union, as part of another declaration.

A bitfield can be unnamed. If the declarator is for a bitfield that has zero size, do not write a name within the declarator. If the declarator is for a bitfield that has nonzero size, then you can optionally write a name; otherwise, you must write a name.

Function Definition

You write a function definition as one of the declarations that make up a translation unit. You cannot write a function definition within another declaration or function definition.

This is the only context where you can omit both the storage class and any type part. You must write a name within the declarator.

Parameter Declaration

You write a parameter declaration as part of a function definition whose function declarator contains a list of parameter names. You must write a parameter name within the declarator.

Block Declaration

You write a block declaration as one of the declarations that begin a block within a function definition.

You can omit the declarator only for a structure, union, or enumeration declaration that declares a tag. Otherwise, you must write a name within the declarator.

Prototype Declaration

You write a prototype declaration within a declarator as part of a function decoration to declare a function parameter.

If the prototype declaration declares a parameter for a function that you are defining (it is part of a function definition), then you must write a name within the declarator. Otherwise, you can omit the name.

Type-Name Declaration

You write a type-name declaration within an expression, eihher as a type cast operator or following the sizeof operator. Do not write a name within the declarator.

Declaration Levels

You use member declarations and type-name declarations only to specify type information. You declare the functions and objects that make up the program in the remaining five contexts shown above. These contexts reside at three declaration levels:

How the translator interprets a declaration that you write depends on the level at which you write it. In particular, the meaning of a storage class keyword that you write (or the absence of a storage class keyword) differs considerably among the declaration levels.

Visibility and Name Spaces

You use names when you declare or define different entities in a program (possibly by including a standard header). The entities that have names are:

The program can declare or define some of these entities by including standard headers. The program can implicitly declare a function by calling the function within an expression.

Each entity is visible over some region of the program text. You refer to a visible entity by writing its name. A macro, for example, is visible from the define directive that defines it to any undef directive that removes the definition or to the end of the translation unit. An object that you declare within a block is visible from where you declare it to the end of the block (except where it is masked, as described below).

Name Spaces

You can sometimes mask an entity by giving another meaning to the same name. An object that you declare within an inner block, for example, can mask a declaration in a containing block (until the end of the inner block). You can use an existing name for a new entity only if its name occupies a different name space from the entity it masks. You can specify an open-ended set of name spaces.

The following diagram shows the relationship between various name spaces:

Each box in this diagram is a separate name space. You can use a name only one way within a given name space. The diagram shows, for example, that within a block you cannot use the same name both as a structure tag and as a union tag.

union x {int i; float f;};
struct x {... };         INVALID: same name space

Each box in this diagram masks any boxes to its right. If the translator can interpret a name as designating an entity within a given box, then the same name in any box to its right is not visible. If you define a macro without parameters, for example, then the translator will always take the name as the name of the macro. The macro definition masks any other meaning.

extern int neg(int x);
#define neg(x) (-(x))
y = neg(i + j);          macro masks function

You introduce two new name spaces with every block that you write. One name space includes all functions, objects, type definitions, and enumeration constants that you declare or define within the block. The other name space includes all enumeration, structure, and union tags that you define within the block. You can also introduce a new structure or union tag within a block before you define it by writing a declaration without a declarator, as in:

{                        new block
struct x;                new meaning for x
struct y {
    struct x *px;        px points to new x

A structure or union declaration with only a tag (and no definition or declarator) masks any tag name declared in a containing block.

The outermost block of a function definition includes in its name space all the parameters for the function, as object declarations. The name spaces for a block end with the end of the block.

You introduce a new goto label name space with every function definition you write. Each goto label name space ends with its function definition.

You introduce a new member name space with every structure or union whose content you define. You identify a member name space by the type of left operand that you write for a member selection operator, as in x.y or p->y. A member name space ends with the end of the block in which you declare it.

Scope

The scope of a name that you declare or define is the region of the program over which the name retains its declared or defined meaning. A name is visible over its scope except where it is masked:

A macro name is in scope from the point where it is defined (by a define directive) to the point where its definition is removed (by an undef directive, if any). You cannot mask a macro name.

Linkage and Multiple Declarations

You can sometimes use the same name to refer to the same entity in multiple declarations. For functions and objects, you write declarations that specify various kinds of linkage for the name you declare. By using linkage, you can write multiple declarations:

In either of these two cases, you can have the declarations refer to the same function or object.

You can use the same enumeration, structure, or union tag in multiple declarations to refer to a common type. Similarly, you can use a type definition to define an arbitrary type in one declaration and use that type in other declarations.

Linkage

A declaration specifies the linkage of a name. Linkage determines whether the same name declared in different declarations refers to the same function or object. There are three kinds of linkage:

The names of functions always have either external or internal linkage.

There are separate rules for determining linkage of objects and functions. In either case, however, do not declare the same name with both internal linkage and external linkage in the same translation unit.

Whenever two declarations designate the same function or object, the types specified in the two declarations must be compatible. If two such declarations are in the same name space, the resulting type for the second declaration is the composite type.

For example, a valid combination of declarations is:

extern int a[];          external linkage
extern int a[10];        type is compatible

Tags

You use enumeration, structure, and union tags to designate the same integer, structure, or union type in multiple declarations. You provide a definition for the type (enclosed in braces) in no more than one of the declarations. You can use a structure or union tag (but not an enumeration tag) in a declaration before you define the type, to designate an incomplete structure or incomplete union type. When you later provide a definition for the incomplete structure or union type, it must be in the same name space.

For example:

struct node {            begin definition of node
    int type, value;
    struct node *L, *R;  valid: although node incomplete
    } *root = NULL;      node now complete

Here, a declaration that refers to the structure whose tag is node appears before the structure type is complete. This is the only way to declare a structure that refers to itself in its definition.

Type Definition

You use type definitions to designate the same arbitrary type in multiple declarations. A type definition is not a new type; it is a synonym for the type you specify when you write the type definition.

For example:

typedef int I, AI[], *PI;
extern int i, ai[10], *pi;
extern I i;              valid: compatible type
extern AI ai;            valid: compatible type
extern PI pi;            valid: compatible type

You can write any type in a type definition. You cannot, however, use a type definition in a function definition if the parameter list for the function being defined is specified by the type definition.

For example:

typedef void VOID;       valid type definition
typedef VOID VF(int x);  valid type definition
VF *pf;                  valid use of type definition
VF f {                   INVALID use of type definition

The parameter list for a function must appear explicitly as a function decoration in the declarator part of a function definition, as in:

VOID f(int x) {          valid use of type definition

A type definition behaves exactly like its synonym when the translator compares types. (The type definition and its synonym are compatible.)

Object Declaration

You declare the objects that the program manipulates at file level, at parameter level (within a function definition), or at block level. The storage class keyword you write (if any) determines several properties of an object declaration. The same storage class can have different meanings at the three declaration levels.

The properties you specify by writing a given storage class at a given declaration level are linkage, storage duration, form of initialization, and object definition status. An object declaration can specify that a name has:

Some declarations accept the previous linkage of a declaration that is visible at file level for the same name (with external or internal linkage). If such a declaration is not visible, then the previous linkage is taken to be external linkage.

An object declaration can specify that the declared object has one of two storage durations:

A type definition for an object type has no duration because duration has no meaning in this case.

An object declaration can permit one of two forms of initialization:

You must write no initializer in some cases.

Each of the four kinds of object definition status of a declaration determines whether the declaration causes storage for an object to be allocated:

The following table summarizes the effect of each storage class at each declaration level on object declarations:

Storage   File-Level            Parameter-Level   Block-Level
Class     Declaration           Declaration       Declaration

none      external linkage      no linkage        no linkage
          static duration       dynamic duration  dynamic duration
          static initializer    no initializer    dynamic initializer
          tentative definition  definition        definition

auto      --                    --                no linkage
                                                  dynamic duration
                                                  dynamic initializer
                                                  definition

extern    previous linkage      --                previous linkage
          static duration                         static duration
          static initializer                      no initializer
          not a definition                        not a definition

register  --                    no linkage        no linkage
                                dynamic duration  dynamic duration
                                no initializer    dynamic initializer
                                definition        definition

static    internal linkage      --                no linkage
          static duration                         static duration
          static initializer                      static initializer
          tentative definition                    definition

typedef   no linkage            --                no linkage
          no duration                             no duration
          no initializer                          no initializer
          type definition                         type definition

The table specifies the definition status assuming that you do not write an initializer. In all cases, if you write an initializer (where permitted), then the declaration allocates storage for the object. (It is a definition.) For example, the following two declarations both name the same object:

static int abc;          internal linkage, tentative definition
extern int abc;          previous linkage, no definition

Function Declaration

You declare the functions that a program calls at file level or at block level. The translator alters any declaration you write at parameter level with type function returning T to type pointer to function returning T, which is an object type.

The properties you specify by writing a given storage class at a given declaration level are linkage and function definition status. A function declaration can specify that a name has:

Some declarations accept the previous linkage of a declaration that is visible at file level for the same name (with external or internal linkage). If such a declaration is not visible, then the previous linkage is taken to be external linkage

The function definition status of a declaration determines whether you can write a function definition in that context. You have one of three possibilities:

The following table summarizes the effect of each storage class, at each declaration level, on function declarations:

Storage   File-Level            Parameter-Level   Block-Level
Class     Declaration           Declaration       Declaration

none      previous linkage      (becomes pointer  previous linkage
          can define            to function)      cannot define
    
auto      --                    --                --

extern    previous linkage      --                previous linkage
          can define                              cannot define

register  --                    (becomes pointer  --
                                to function)

static    internal linkage      --                --
          can define

typedef   no linkage            --                no linkage
          type definition                         type definition

For example, the following declarations both name the same function:

static int f(void);      internal linkage
extern int f(void);      previous linkage

Reading Declarations

Reading a declaration is not always easy. Proceed with caution any time:

The following provides some simple guidelines for writing and reading complex declarations.

When you write a declaration, avoid redundant parentheses. In particular, never write parentheses around a name, as in int (x), because it is easy for you or others to misread the parenthesized name as a parameter list, and the type changes if you omit the name.

You must omit the name when you write a type cast operator. You can omit the name in a declarator when you write a function parameter declaration that is not part of a function definition. If you omit the name in the example above, you get int (), which specifies type function returning int, not type int.

Avoid writing a declaration that masks a type definition. If you must mask a type definition, write at least one type part in the masking declaration that is not a type qualifier. The translator assumes that a name visible as a type definition is always a type part if that is a valid interpretation of the source text, even if another interpretation is also valid.

For example:

typedef char Small;
int g(short Small);      valid: Small has new meaning
int f(Small)             Small taken as type definition
    short Small;         INVALID: not a parameter name

To read a declaration, you must first replace the name if it has been omitted. You determine where to write the name by reading the declaration from left to right until you encounter:

You write the name immediately to the left of this point.

For example:

int                      becomes    int x
void (*)()               becomes    void (*x)()
char []                  becomes    char x[]
long ()                  becomes    long x()

You read a complex declaration by first locating the name (using the previous rules). Then you:

  1. Read the array or function decorations from left to right, beginning with the name, until you come to the end of the declarator or to a right parenthesis.
  2. Read the pointer decorations from right to left, beginning back at the name, until you come to the beginning of the declarator, to a type part, or to a left parenthesis.
  3. If you encounter a left parenthesis, repeat the first two steps (treating the parenthesized declarator as if it were the name).
  4. Read the type specified by the type parts.

The following diagram can also help:

d7 d6 ( d4 d3 NAME d1 d2 ) d5

Read the decorations in increasing numeric order, beginning with d1 and ending with the type parts (d7). It is often sufficient simply to remember that, in the absence of parentheses (or within a pair of grouping parentheses), you read the pointer decorations as the last part of the type.

For example:

int *fpi(void)           is    function returning pointer to int
int (*pfi)(void)         is    pointer to function returning int
unsigned int *(* const *name[5][10])(void)
                         is    array with 5 elements of
                                  array with 10 elements of
                                  pointer to
                                  pointer which is constant to
                                  function (no parameters) returning
                                  pointer to
                                  unsigned int

Object Initializers

You can specify an initial value for an object by writing an initializer. The type of the object and the declaration context constrain how you write an initializer.

You initialize an object with static duration by writing a static initializer. A static initializer for an object with scalar type consists of a single expression (possibly enclosed in braces) that the translator can evaluate prior to program startup. A static initializer for an object with array, structure, or union type consists of a list of one or more initializers separated by commas and enclosed in braces.

For example:

extern char *first = NULL;
static short February[4] = {29, 28, 28, 28};

You initialize an object with dynamic duration by writing a dynamic initializer. For other than array types, any rvalue expression that is assignment compatible with the type of the object can serve as a dynamic initializer. You can also write a dynamic initializer in the same form as a static initializer.

For example:

auto int bias =
    {RAND_MAX/2};        static form initializer
auto int val =
    rand() < bias;       dynamic form initializer

The initializers that you write within a list separated by commas are inner initializers. You write an inner initializer the same way you write a static initializer, except that you can omit the outermost braces:

Some examples are:

struct complex {
    float real, imag;
    } neg_one = {-1, 0}; values for real and imag
union {
    struct complex *p;
    float value;
    } val_ptr =
        {&neg_one};      initializes pointer member
int a23[2][3] =
    {{00, 01, 02},       all braces present
    {10, 11, 12}};       on inner initializers
int a32[3][2] =
    {00, 01,             braces omitted
    10, 11,              on inner initializers
    20, 21};

If you do not provide as many initializers as there are members or elements to initialize, the translator initializes any remaining members or elements to the value zero. Do not provide excess initializers. You can initialize an object of incomplete array type, in which case the number of element initializers you write determines the repetition count and completes the array type.

For example:

double matrix[10][10] =
    {1.0};               rest set to 0
int ro[] = {1, 5, 10,
    50, 100, 500};       6 elements

You can initialize an array of any character type by writing a string literal, or an array of wchar_t by writing a wide character string literal, as shorthand for a sequence of character constants. The translator retains the terminating null character only when you initialize an object of incomplete array type. (An object of complete array type is padded as needed with trailing zero initializers.)

For example:

char fail[6] = "fail";   same as {'f', 'a', 'i', 'l', 0, 0}
char bad[] = "bad";      same as {'b', 'a', 'd', '\0'}
wchar_t hai[3] = L"hai"; same as {L'h', L'a', L'i'}

But note:

wchar_t hai[3] = {L'h', L'a', L'i',
    '\0'};               INVALID

The following table summarizes the various constraints on initializer expressions or initializer lists, depending on context and the type of the object:

              Dynamic           Static            Inner
Type          Initializer       Initializer       Initializer

arithmetic    { arithmetic      { arithmetic      { arithmetic
              rvalue }          constant          constant
                                expression }      expression }

              arithmetic        arithmetic        arithmetic
              rvalue            constant          constant
                                expression        expression

pointer       { assignment-     { address         { address
              compatible        constant          constant
              rvalue }          expression }      expression }

              assignment-       address           address
              compatible        constant          constant
              rvalue            expression        expression

structure     { inner           { inner           { inner
              initializer list  initializer list  initializer list
              for members }     for members }     for members }

              compatible                          inner
              structure                           initializer list
              rvalue                              for members

union         { inner           { inner           { inner
              initializer list  initializer list  initializer list
              for first         for first         for first
              member }          member }          member }

              compatible                          inner
              union                               initializer list
              rvalue                              for first
                                                  member

array         { inner           { inner           { inner
              initializer list  initializer list  initializer list
              for elements }    for elements }    for elements }

                                                  inner
                                                  initializer list
                                                  for elements

array of      { "..." }         { "..." }         { "..." }
character     "..."             "..."             "..."

array of      { L"..." }        { L"..." }        { L"..." }
wchar_t       L"..."            L"..."            L".."

This table shows you, for example, that you can write an arbitrary arithmetic rvalue expression as the initializer for an object with arithmetic type and dynamic duration. You can write an arithmetic constant expression, with or without braces, anywhere you initialize an object with arithmetic type.

The table also shows you that you can initialize the elements of an object of array type, in any context, by writing a list of initializers in braces. You can omit the braces only for a string literal initializer or for a list you write as an inner initializer for some containing initializer.


See also the Table of Contents and the Index.

Copyright © 1989-1996 by P.J. Plauger and Jim Brodie. All rights reserved.