首页 > 代码库 > 【C】 05 - 声明和定义

【C】 05 - 声明和定义

  仅从形式上看,C程序就是由各种声明和定义组成的。它们是程序的骨架和外表,不仅定义了数据(变量),还定义了行为(函数)。规范中的纯语言部分,声明和定义亦花去了最多的篇幅。完全说清定义的语法比较困难,这里也只是个人的理解。

1. 标识属性

  对C编译器而言,标识(identifier)包括对象名、函数名、复合类型及枚举tag、typedef类型名、label和枚举常量。标识的各种属性构成了C的复杂功能,理清这些概念对C的高级使用尤其重要。

  域(scope)可以看做是标识的活动范围,一个编译单元中该范围是层次结构的。最外层当然就是整个编译单元(file scope),它其中包含了各种块(block scope)和子块,这些构成了域的层次结构。这里的块是个宽泛的概念(非规范定义),包括类型定义、函数声明和函数定义。每一个域中的按照标识类型又可以划分为不同的名字空间(name space),同一名字空间的标识不可重名(冲突)。一般情况下,一个父域中的标识可以在子域中活动,但当子域中有与其冲突的标识时,该标识即覆盖父域的标识,使其不可见。大部分标识的可见范围是从其完整定义到其所处域的结尾,tag的可见范围是从其标识结束即开始的,枚举常量从其定义结束可见,label的可见范围是整个函数(function scope)。另外,函数定义不可以出现在块域中,而函数声明、结构和联合则可以。

typedef struct S {         // file scope    struct S* p;           // S can be used, p block scope    struct T {int i;} t;   // ok} S;                       // file scopeenum {    RED,                   // file scope    BLACK = RED            // ok};void f(int i);             // f file scope, i block scopeint a;                     // file scopevoid f(int i)              // i block scope{LABEL:                     // function scope    goto LABEL;    int m;                 // ok in C99    int a;                 // ok, cover global a    void fun(void) {}      // illegal    extern void fun(void); // ok    struct T {int i;} t;   // ok}

  名字空间可分为:label、tag、其它(非规范定义)。这样的分类是按照标识在语法出现的位置决定的,label总是后面跟冒号,tag前面总有关键字struct、union或enum,所以他们总是可以区分出来的。而其它标识可能出现在相同语境中,重名会造成歧义。

typedef struct S {int i;} S; // okstruct T {     int T;                  // ok} S;                         // name conflictenum {    S,                       // name conflict    T                        // ok}void F(void){F:                           // ok    int F;                   // name conflict}

  域的概念一般出现在编译过程中,而链接属性(linkage)出现在链接过程中,它只针对对象和函数。所有extern修饰的对象和函数都有外部链接(external linkage)的属性,所有文件域的标识都有链接属性,其中含有static修饰符的是内部链接(internal linkage),不含static的是外部链接,其它所有标识皆没有链接属性。外部链接的标识会存储在编译结果中,链接过程负责把标识替换为真实地址,而内部链接的标识符外部不可用。对于未初始化的外部链接对象,如果含有extern则仅是声明,不分配空间,否则是定义。

struct S {int i;};           // no linkageextern int a[];              // external, declare onlystatic int b;                // intenalextern int m = 0;            // defineint n;                       // definestatic void f1(void) {}      // internalvoid f2(void)                // external{    extern void fun(void);   // external, declare only    static int m;            // no linkage    int n;                   // no linkage}

  存储类型(storage class)表示运行时对象的存储方法,分为静态存储(static)、动态存储(dynamic)和自动存储(aotomatic)。所有文件域和有static标识符的对象都是静态存储的,它们在程序生命周期中一直存在。动态存储是通过库函数产生的对象,它们存储在特殊的区域(堆),由用户负责申请和释放。块域的对象在进入块时分配空间(新规范支持在块中间定义),在退出块时释放空间,唯一的例外是变长数组在定义时才分配空间。自动存储的对象默认由auto修饰,也可以由register修饰,它建议把对象存放在寄存器中。但其实现代编译器的优化可以做到这一点,所以register已经过时。

int m;                      // staticstatic int n;               // staticregister int r;             // illegalvoid f(void){                           // alloc a    n = m;    static int s;           // static    auto int a;             // automatic, the same as int a    {                       // alloc b        a = s;        register int b[2];  // automatic        int c[a];           // alloc c    }                       // free b, c}                           // free a

  C中对对象有一些限定符(qualifier),它们一般起到限制和优化的作用。const所修饰的对象不可通过相应变量直接修改,所以只能在定义时初始化,编译器可能把它当常量使用。volatile所修饰的对象可能被外部改变(系统时间),提醒编译器每次使用时从内存加载。restrict修饰符只作用于指针(受限指针),表示在该指针生命周期内,所指对象(可以是多个)只能直接或间接通过该指针修改,编译器可以对此进行优化。受限指针可以由子域中的另一个受限指针“接管”,而没有未定义行为。

const int c = 0;const int *pc;int *pi;volatile int v = 0;const volatile long long sys_tick;  // system timeretrict int n;                      // illegalc = 1;                              // illegalpc = &c;                            // okpi = &c;                            // illegalpi = (int*)&c;                      // ok*pc = 1;                            // illegal*pi = 1;                            // okv;                                  // may be not 0void f(restrict int* p){    restrict int *q = p;            // undefind    {        restrict int *s = p;        // ok    }}

  函数修饰符(function specifier)作用于函数,包括inline和_Noreturn,它们不能用于main函数。inline建议编译器优化函数代码,它一般在编译单元内部使用,但也可以外部访问(对外是函数形式)。_Noreturn表示函数不会返回,一般直接结束程序(比如abort)。对齐修饰符(alignment specifier)限定对象的对齐方式(已介绍过),当作用于组合类型时,它对每一个成员其作用。

// file 1extern void f(void);_Noreturn void fun(void){    f();                  // function call    return;               // illegal}// file 2inline void f(void) {}_Noreturn int main(void)  // illegal{    f();                  // inline    abort();}

2. 一般声明

  首先说明一下,规范中的“声明”包括了定义,下文中一般名词性的“声明”包含定义,其它都是我们常用的单纯“声明”。

  结构(联合)的定义一般叫模板,结构、联合和枚举的类型名叫tag。结构(联合)可以声明(incomplete type),可用于头文件中隐藏具体细节,亦可用于结构(联合)间的互相应用。结(联合)在花括号之后成为完整类型,相互赋值时空隙部分并不赋值。可以有长度为0位域,它结束当前word,但不可以有名字。当成员是结构(联合)且没有变量名,它称为匿名结构(联合),其成员成为上层结构(联合)的成员。枚举不可以声明,它的定义中可有一个多余的逗号。枚举常量的类型为int型,枚举变量为整型(基于实现),枚举变量的值在调试时可显示为枚举常量(比普通整型好)。

// head filestruct S;                       // declarationvoid f(struct S* p);            // okvoid f(struct S s);             // illegalstruct S2;struct S1 {    struct S2 *ps2;             // ok    struct S1 s1;               // illegal} s1;                           // okstruct S2 {struct S1 *ps1;};struct S {    char c;    int a   : 20;    int b   : 0;                // illegal    int     : 0;                // end the word} s1, s2;s1 = s2;                        // not the same as memcpystruct S {    struct T {        int m;        struct {int n};         // anonymous    };                          // anonymous    int m, n;                   // name conflict} s;s.m + s.n;                      // okenum E;                         // illegalenum E {ONE,};                  // okenum E e = ONE;

  函数和栈结构可以及时释放临时变量,提高内存利用率。函数可以递归调用(包括main),但会消耗更多的内存和时间。函数返回类型的修饰符会被忽略,只需有函数修饰符,函数不返回数组或函数。函数本地参数叫形参(parameter),调用者传递的值叫实参(argument),该定义同样适用于宏。参数列表由逗号隔开是来源于旧规范(见示例),其实分号做分隔符更好。函数参数格式上可以是数组或函数(提示作用),但它们等价于对应的指针形式,register是唯一可以使用的存储类型修饰符。C支持变长参数列表,仅需在最后一个参数后加三个点,之后不可再有参数定义。必须要有显式参数,用来获得变长参数列表的起始地址。标准库<stdarg.h>提供了使用变长参数的方法,但三个点本身不需要库支持。值得一提的是,变长参数列表只能由调用者自己清理(calling convention),而windows中默认是函数清理栈(__stdcall),需要使用__cdecl修饰符支持变长参数列表。函数原型(prototype)可协助进行编译时类型检查,仍支持旧规范中的空参数,它表示参数不确定(在定义中表示没有参数)。函数原型中的参数名可与定义中不同,甚至可以没有参数名。函数参数和函数体属于同一个block,注意名字冲突。

int main();                              // old C, not know parameterint main()                               // old C, no parameter{    static s = 4;    if (0 == s) return 0;    else return main() + s--;            // ok}int fun(x, y) int x; int y; {return 1;}  // old Cvoid fun(int x, y);                      // illegalvoid fun(int);                           // okvoid fun(register int m);                // okvoid fun (int a) {int a;}                // name conflictconst int f(int a[2]) {return 1;}        // the same as int f(int* a)void g(int f(int*)) {}                   // the same as void g(int (*f)(int*))int m = f((int[]){1, 2 ,3});             // okg(f);                                    // okvoid fun(...);                           // illegalvoid fun(int, ..., int);                 // illegalvoid fun(int, ...);                      // ok, no need <stdarg.h>void __cdecl fun(int, ...);              // necessary in windows

  新规范中数组参数可以有更丰富的形式,array_specifier是数组参数方括号里的内容。本节的语法仍只是说明性的,非规范定义,除特别说明方括号表示可选。

array_specifier:    [type_qulifier_list] [assignment_exp]    static [type_qulifier_list] assignment_exp    type_qulifier_list static assignment_exp    [type_qulifier_list] *

    当然这里的数组是指第一维的,它会转换成指针,指针的类型限定符可以写到方括号里。这里的表达式(不可以是逗号表达式)一般没有实际意义,static修饰符被复用,用来提醒调用者数组长度至少为表达式的值。对高维数组的其它维,方括号里仅可以是表达式或星号,其中星号仅用于函数原型。

void f(int a[1, 2]);            // illegalvoid f(int a[const 2]);         // the same as const int* avoid f(int a[const static 2]);  // okvoid f(int a[static const 2]);  // okvoid f(int a[static 2 const]);  // illegalvoid f(int a[const *]);         // okvoid f(int a[*][*]);            // okvoid f(int n, int a[n][n]);     // okvoid f(int a[*][*]) {}          // illegal

  一般数组长度必须为常整型,const变量也不行。新规范中支持变长数组(VLA,variable length arrary),VLA定义时数组长度是整型表达式(非常数)。VLA只能在块域,它在每次定义时确定长度并分配空间,但一旦确定长度,在生命周期内不会改变。VLA和任何数组是类型兼容的,其指针可互相赋值。指向VLA的指针一般叫VM(variably modified),它也必须在块域并且无连接。VM可以是静态存储,而VLA则不可以,VM和VLA都不可以出现在结构或联合里。

int const n = 2;int a[n];                 // illegal, static VLAextern (*p)[n];           // illegal, linkage VMint b[2];struct S {    int a[n];             // illegal, VLA in struct    int (*p)[n];          // illegal, VM in struct};int main(void){    int m = 2;    int a[m++];           // ok, auto VLM    sizeof(a);            // sizeof(int) * 2    static int b[n];      // illegal, static VLA    extern int b[n];      // illegal, linkage VLA    extern (*p)[n];       // illegal, linkage VM    static (*p)[n] = &b;  // ok, no linkage block VM}

3. 复杂声明

declare:    declare_specifier [init_declarator_list]declare_specifier:    storage_class_specifier [declare_specifier]    alignment_specifier [declare_specifier]    function_specifier [declare_specifier]    type_qualifier [declare_specifier]    type_specifier [declare_specifier]    init_declarator:    declarator [= initializer]storage_class_specifier:    one of {typedef, extern, static, auto, register}
alignment_specifier: _Alignas(type or int_const)
function_specifier: one of {
inline, _Noreturn}
type_qualifier: one of {const, volatile, restrict}
type_specifier: one of {void, char, short, int, long, float, double, signed, unsigned, _Bool, _Complex} struct_union_specifier enum_specifier typedef_name

    声明语句由声明修饰(declare_specifier)和声明列表(init_declarator_list)组成,其中声明列表用逗号作分割符。一个完整的声明可以粗略分为三个部分(非规范定义):属性、类型和扩展(非规范定义)。扩展部分包含附加操作、声明对象名和初始化,附加操作是对声明对象的类型补充(见下段)。属性包括存储类型(storage_class_specifier)、对齐(alignment_specifier)和函数性质(function_specifile),它们所修饰的是最终声明对象。声明中只能有一个存储类型,typedef在使用形式上与存储类型一致,所以也统一到该类中。类型包括类型限定(type_qualifier)和类型修饰(type_specifier),它们都可以看做是类型的一部分,所修饰的是整个扩展部分(非声明对象,也不互相修饰)。类型限定可以组合使用,类型修饰要么是定义中第一类的组合,要么是后三者之一(四类不可组合使用)。从定义中可以看出,所有属性和类型的的顺序是随意的,但建议按习惯的顺序使用。

typedef static int INT;                  // illegal, 2 storage_class_specifiertypedef int INT;                         // okconst restrict int *p;                   // ok, 2 type_qualifier, all apply on *p, not pconst struct S {int i;} s1;struct S s2;s1.i = 1;                                // illegal, s1 is consts2.i = 1;                                // ok, S is not constunsigned INT a;                          // illegal, 2 type_specifierstatic _Alignas(4) unsigned long int a;  // ok, good stylestatic int _Alignas(4) a;                // okint static a;                            // okint typedef INT;                         // okstatic int long unsigned a;              // oklong static int unsigned a;              // okunsigned int long typedef UL;            // ok

  以下是扩展部分的附加操作和声明对象名语法,除[array_specifier]外方括号皆表示可选。

declarator:    identifier    (declarator)    * [type_qualifier_list] declarator    declarator[array_specifier]    declarator(parameter_list)

  附加操作也是声明对象类型的一部分,包括指针、数组和函数。类型顺序和操作优先级一致,因为后缀操作有限级高,有时指针需要加括号。指针后面可以跟类型限定符,它同样与指针都是类型的一部分。扩展部分是可选的,但仅用于模板定义中。没有扩展时结构和联合必须有tag,枚举可以没有tag。

int *a[2];             // array of pointerint (*a)[2];           // pointer to arrayint f(void)[2];        // illegal, function return arrayint *a[2](void);       // illegal, array of functionint (*a[2])(void);     // ok, array of function pointerint (*f(void))(void);  // ok, function return function pointerconst int *p;          // *p is constint *const p;          // p is constint *const *pp;        // *pp is conststruct {int i;} s;     // okstruct S {int i;};     // ok, definestruct {int i;};       // illegal, but ok as a memberenum {ONE};            // ok

  以下是结构和联合的定义语法。

struct_union_specifier    struct_or_union [identifier] {struct_declare_list}    struct_or_union identifierstruct_declare:    type_specifier_qualifier_list [struct_declarator_list];struct_declarator:    declarator    [declarator]: int_const

  不管是tag还是整个定义,结构(联合)都是作为类型使用的。成员不可以是函数或不完全类型,当然在花括号之前定义本身也不完全。成员只能用类型,不能有属性修饰符。成员可以用列表形式,但不能初始化。

int n = 2;struct S {int i;} const static s1;  // okstruct S {    void f(void);                   // illegal, funtion member    int a[];                        // illegal, incomplete type    struct S s;                     // illegal, incomplete type    int m = 0;                      // illegal, can‘t init    const int a, *b;                // ok    static _Alignas(4) int c;       // illegal};

  在有些场合只需要类型而不需要实例,比如强制转换、复合常量、函数原型。大部分场合类型只需声明中的类型和附加操作两部分,需要找到原本声明对象所在的位置来确定最终类型。大部分情况可以根据优先级找到类型起点,当出现空的圆括号时当函数看待。

(int(*)[2])0;          // cast to pointer to array(int*[]){NULL, NULL};  // pointer array literal(int(*[])()){NULL};    // functon pointer array literal void f(int());         // convert to int(*)()void f(int(*)[*]);     // VM

  对于复杂类型,最好使用typedef重命名类型,以使定义更清晰。typedef不可与其它属性一起使用,但可以包括类型限定符。它可以重命名不完全的结构(联合),但在完整定义前仅能用其指针,不可以重命名不完全的枚举。重命名的不完全数组,可在数组定义时确定数组长度,重命名的VLA和VM在每次使用时确定数组长度。typedef仅是重命名,不改变原有定义的性质。

void (*signal(int id, void(*hdl)(int)))(int);  // from <signal.h>typedef void (*sig_t)(int);sig_t signal(int num, sig_t hdl);              // much more cleartypedef static int si;                         // illegaltypedef _Alignas(4) int ai;                    // illegaltypedef const int ci;                          // oktypedef struct S S;                            // oktypedef enum E E;                              // illegaltypedef char Array[] ;                         // okArray a = {1}, b = {1, 2};                     // sizeof(a) = 1, sizeof(b) = 2void f(void){    int n = 1;    typedef char VLA[n], (*VM)[n];             // ok    n++;    VLA vla;                                   // sizeof(vla) = 2    VM vm = &vla;                              // ok}typedef int T;struct S {    T t        : 10;                           // signed or unsigned     unsigned T : 10;                           // unsigned member named T    const T    : 10;                           // anonymous member};

4. 初始化

initilizer:    assignment_exp    {initilizer_list}    {initilizer_list,}

  初始化包含在变量定义中,它为对象提供初始值。初始化列表含有一对花括号,以及其中由逗号分隔的初始化项,每个初始化项可以是初始化列表,也可以是表达式。由于逗号已经用作分隔符,所以初始化表达式不能是逗号表达式,列表末尾的逗号无意义。如果对象是度量值或浮点数,花括号可省略。如果对象是复合类型,也可直接互相赋值。对字符串类型,可直接用字符串赋值(可带花括号)。

int a = 1, 2;                        // illegalint a = {1};                         // the same as int a = 1int a[] = {1, 2, };                  // only two membersstruct S {int i} s1 = {1}, s2 = s1;  // okchar str[] = "hi";                   // the same as {"hi"}char str[2] = "hi";                  // ok, sizeif(str) = 2

  组合类型的初始化列表依次初始化成员,如遇到组合类型成员则同样初始化其每个成员。位域中的匿名成员不参加初始化,联合只初始化第一个成员,其它成员若有空隙则按比特置零。若列表不足,则剩余的成员按类型置零(整数为0、浮点数为0.0、指针为空指针)。长度不定的数组以初始化列表的长度为准,否则以数组长度为准,列表长度不可以超过数组长度(字符串除外)。

typedef struct S {int a[2]; int b:20; int :12; int c, d;} S;typedef union U {char c; int i;} U;S s = {1, 2, 3, 4};                   // s.a[0] = 1, s.a[1] = 2, s.b = 3, s.c = 4, s.d = randU u = {a, 1};                       // illegalU u = {a};                          // u.c = ‘a‘, other bits to 0char* strs[2] = {"hi"};               // strs[1] = NULLchar str[2] = {h, i, \0};       // illegal

  一对花括号对应一个当前对象,即使正在初始化成员的成员,当前对象仍然是花括号所对应的对象。对成员也可以使用初始化列表,这时的当前对象切换为该成员,一切规则以当前对象执行。当前成员切换出来时,从下一个成员继续执行。新规范还支持指定初始化(designated initilization),可以在初始化列表中指定具体成员(及其成员)初始化,此过程也进行当前对象切换,切换回来后从被指定成员(当前对象的)的下一个成员继续执行。当然指定初始化可能覆盖之前的初始化,也可改变当前成员。

typedef struct S {int a[2][2]; int b} S;S s = {1, 2, 3, 4, 5};                       // cur_obj not changeS s = {{1, 2, 3,}, 4};                       // cur_obj s -> s.a -> sS s = {{[0] = {1, 2}, [1][1] = 3}, .b = 4};  // okS s = {.a[0][0] = 1, 2};                     // b = 2, cur_obj s -> s.a[0][0] -> sS s = {.a = {[0][0] = 1, 2}};                // a[1][0] = 2, cur_obj s ->s.a -> s.a[0][0] -> s.a -> sS w[] = {{1}, 2};                            // w[0].a[0][0] = 1, w[1].[0][0] = 2, cur_obj w -> w[0] -> w

  静态存储的对象的值存储在可执行文件的数据区,需要编译时确定,所以只能用常数初始化,自动存储的对象无此限制。静态存储变量只在运行初被初始化一次,未显示初始化的也按类型置为0。可变长数组和结构尾部的可变数组不可以初始化。初始化列表从左向右执行,但其中没有序列点,可能有不确定行为。

typedef struct S {struct S *p;} S;typedef struct V {int i; int a[]} V;int  i = 1;int a[2] = {i++, i++};                  // a[1] = 1 or 2V v = {1, 2};                           // illegalS s1, *ps = &s1;                        // ok, s1.p = NULLS s2 = {ps};                            // illegalvoid f(void){    static S s3 = {&s3};                // ok, only once    S s4 = {ps};                        // ok    int a[i] = {1};                     // illegal}
 

【C】 05 - 声明和定义