【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 scope enum { RED, // file scope BLACK = RED // ok }; void f(int i); // f file scope, i block scope int a; // file scope void 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; // ok struct T { int T; // ok } S; // name conflict enum { 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 linkage extern int a[]; // external, declare only static int b; // intenal extern int m = 0; // define int n; // define static void f1(void) {} // internal void 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; // static static int n; // static register int r; // illegal void 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 time retrict int n; // illegal c = 1; // illegal pc = &c; // ok pi = &c; // illegal pi = (int*)&c; // ok *pc = 1; // illegal *pi = 1; // ok v; // may be not 0 void 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 1 extern void f(void); _Noreturn void fun(void) { f(); // function call return; // illegal } // file 2 inline void f(void) {} _Noreturn int main(void) // illegal { f(); // inline abort(); }
2. 一般声明
首先说明一下,规范中的“声明”包括了定义,下文中一般名词性的“声明”包含定义,其它都是我们常用的单纯“声明”。
结构(联合)的定义一般叫模板,结构、联合和枚举的类型名叫tag。结构(联合)可以声明(incomplete type),可用于头文件中隐藏具体细节,亦可用于结构(联合)间的互相应用。结(联合)在花括号之后成为完整类型,相互赋值时空隙部分并不赋值。可以有长度为0位域,它结束当前word,但不可以有名字。当成员是结构(联合)且没有变量名,它称为匿名结构(联合),其成员成为上层结构(联合)的成员。枚举不可以声明,它的定义中可有一个多余的逗号。枚举常量的类型为int型,枚举变量为整型(基于实现),枚举变量的值在调试时可显示为枚举常量(比普通整型好)。
// head file struct S; // declaration void f(struct S* p); // ok void f(struct S s); // illegal struct S2; struct S1 { struct S2 *ps2; // ok struct S1 s1; // illegal } s1; // ok struct 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 memcpy struct S { struct T { int m; struct {int n}; // anonymous }; // anonymous int m, n; // name conflict } s; s.m + s.n; // ok enum E; // illegal enum E {ONE,}; // ok enum E e = ONE;
函数和栈结构可以及时释放临时变量,提高内存利用率。函数可以递归调用(包括main),但会消耗更多的内存和时间。函数返回类型的修饰符会被忽略,只需有函数修饰符,函数不返回数组或函数。函数本地参数叫形参(parameter),调用者传递的值叫实参(argument),该定义同样适用于宏。参数列表由逗号隔开是来源于旧规范(见示例),其实分号做分隔符更好。函数参数格式上可以是数组或函数(提示作用),但它们等价于对应的指针形式,register是唯一可以使用的存储类型修饰符。C支持变长参数列表,仅需在最后一个参数后加三个点,之后不可再有参数定义。必须要有显式参数,用来获得变长参数列表的起始地址。标准库<stdarg.h>提供了使用变长参数的方法,但三个点本身不需要库支持。值得一提的是,变长参数列表只能由调用者自己清理(calling convention),而windows中默认是函数清理栈(__stdcall),需要使用__cdecl修饰符支持变长参数列表。函数原型(prototype)可协助进行编译时类型检查,仍支持旧规范中的空参数,它表示参数不确定(在定义中表示没有参数)。函数原型中的参数名可与定义中不同,甚至可以没有参数名。函数参数和函数体属于同一个block,注意名字冲突。
int main(); // old C, not know parameter int 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 C void fun(int x, y); // illegal void fun(int); // ok void fun(register int m); // ok void fun (int a) {int a;} // name conflict const 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}); // ok g(f); // ok void fun(...); // illegal void fun(int, ..., int); // illegal void 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]); // illegal void f(int a[const 2]); // the same as const int* a void f(int a[const static 2]); // ok void f(int a[static const 2]); // ok void f(int a[static 2 const]); // illegal void f(int a[const *]); // ok void f(int a[*][*]); // ok void f(int n, int a[n][n]); // ok void 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 VLA extern (*p)[n]; // illegal, linkage VM int 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_specifier typedef int INT; // ok const restrict int *p; // ok, 2 type_qualifier, all apply on *p, not p const struct S {int i;} s1; struct S s2; s1.i = 1; // illegal, s1 is const s2.i = 1; // ok, S is not const unsigned INT a; // illegal, 2 type_specifier static _Alignas(4) unsigned long int a; // ok, good style static int _Alignas(4) a; // ok int static a; // ok int typedef INT; // ok static int long unsigned a; // ok long static int unsigned a; // ok unsigned 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 pointer int (*a)[2]; // pointer to array int f(void)[2]; // illegal, function return array int *a[2](void); // illegal, array of function int (*a[2])(void); // ok, array of function pointer int (*f(void))(void); // ok, function return function pointer const int *p; // *p is const int *const p; // p is const int *const *pp; // *pp is const struct {int i;} s; // ok struct S {int i;}; // ok, define struct {int i;}; // illegal, but ok as a member enum {ONE}; // ok
以下是结构和联合的定义语法。
struct_union_specifier
struct_or_union [identifier] {struct_declare_list}
struct_or_union identifier
struct_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; // ok struct 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 clear typedef static int si; // illegal typedef _Alignas(4) int ai; // illegal typedef const int ci; // ok typedef struct S S; // ok typedef enum E E; // illegal typedef char Array[] ; // ok Array a = {1}, b = {1, 2}; // sizeof(a) = 1, sizeof(b) = 2 void 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; // illegal int a = {1}; // the same as int a = 1 int a[] = {1, 2, }; // only two members struct S {int i} s1 = {1}, s2 = s1; // ok char 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 = 0 U u = {'a', 1}; // illegal U u = {'a'}; // u.c = 'a', other bits to 0 char* strs[2] = {"hi"}; // strs[1] = NULL char 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 change S s = {{1, 2, 3,}, 4}; // cur_obj s -> s.a -> s S s = {{[0] = {1, 2}, [1][1] = 3}, .b = 4}; // ok S s = {.a[0][0] = 1, 2}; // b = 2, cur_obj s -> s.a[0][0] -> s S s = {.a = {[0][0] = 1, 2}}; // a[1][0] = 2, cur_obj s ->s.a -> s.a[0][0] -> s.a -> s S 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 2 V v = {1, 2}; // illegal S s1, *ps = &s1; // ok, s1.p = NULL S s2 = {ps}; // illegal void f(void) { static S s3 = {&s3}; // ok, only once S s4 = {ps}; // ok int a[i] = {1}; // illegal }