首页 > 代码库 > 【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 - 声明和定义