【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
}

posted on 2014-08-29 22:32  卞爱华  阅读(1290)  评论(5编辑  收藏  举报

导航