6.2 惯用句法
6.2.1 结构体声明
对于这种习惯用法,可能会有不同的观点。但是我在声明结构体的时候,必定会同时给出 typedef
定义。
typedef struct {
int a;
int b;
} Hoge;
此外,在没有特别需要的情况下,我一般不写 tag
*。在需要写 tag 的时候,一般像下面这样,在定义的类型名称后面追加_tag
。
* 这是由于,在特别需要的情况下写 tag
,tag
的存在表明了“正在被前方引用”这层意思。也有人主张“因为不知道什么时候需要,所以必须写 tag
”这样的观点。
typedef struct Hoge_tag {
int a;
int b;
} Hoge;
还有,对于结构体、共用体、枚举的 tag
名,因为持有和一般的标识符不同的命名空间,所以它们也可以写成下面这样:
typedef struct Hoge {
int a;
int b;
} Hoge;
因为写成这样则与 C++意思相同,所以也有很多人喜欢这种写法。
在声明结构体的时候,虽然可以同时定义这个结构体类型的变量,但我不会这么做。一想到 typedef
,就完全没有了这么写的冲动,因为类型的声明和变量的定义根本就是不同的概念,所以我认为还是分开书写比较好。
/* 我不会这样写 */
struct Hoge_tag {
int a;
int b;
} hoge; ←声明struct Hoge_tag 类型的变量
随便再提一下,虽然声明结构体的成员时,可以和声明一般的变量一样,一次性声明多个变量,但是我不会这么做。
/* 我不会这样写 */
typedef struct {
int a, b;
} Hoge;
6.2.2 自引用型结构体
为了构造链表和树,我们会声明包含指向相同类型的指针的结构体。
这样的结构体,好像一般都称为“自引用型结构体”——为什么是“好像”?C 语言的入门书籍中倒是这么称呼的,但是在开发现场,我从没听到过有人使用这个称呼。称之为“自引用型结构体”,其实没有任何特殊的理由。
但是,对于这种结构体的声明,还是有必须要留意的地方。
typedef struct Hoge_tag {
int a;
int b;
struct Hoge_tag *next;
} Hoge;
在这种情况下,在声明成员 next
的时候,typedef
还没有结束,类型 Hoge
还不能被使用,所以 next
还只能声明成 struct Hoge_tag*
。
或者也可以写成下面这样:
typedef struct Hoge_tag Hoge;
struct Hoge_tag {
int a;
int b;
Hoge *next;
};
6.2.3 结构体的相互引用
在 3.2.10 节中我们说过,对于相互引用的结构体,应该像下面这样只是将 tag
提前声明:
在下面的代码中,Man
持有指向“妻”的指针,Woman
持有指向“夫”的指针。
typedef struct Woman_tag Woman; ←提前对tag 进行类型定义
typedef struct {
┊
Woman *wife; /* 妻 */
┊
} Man;
struct Woman_tag {
┊
Man *husband; /* 夫 */
┊
};
以前,一提出上面这种写法,肯定就有人这么想:
哇嘎嘎,那就把所有的 tag
全部提前声明,然后就可以按照任意的顺序声明结构体了。
于是,就有了下面这样的代码,
typedef struct Polyline_tag Polyline; /*(以下3行)将所有tag 提前声明*/
typedef struct Shape_tag Shape;
...
struct Shape_tag {
ShapeType type;
union {
Polyline polyline; ←只声明了tag,使用了Polyline 的实体!
Rectangle rectangle;
Circle circle;
} u;
};
struct Polyline_tag {
...
};
上面的代码是无法通过编译的。
在只是声明 tag
的情况下,其类型是“不完全类型”。对于不完全类型,只能取得其指针(参照 3.2.10 节)。
因为还不知道不完全类型的长度,所以编译器无法确定结构体成员的偏移量。
6.2.4 结构体的嵌套
将结构体作为另一个结构体的成员时,可以像下面这样使用已经声明的结构体:
typedef struct {
int a;
int b;
} Hoge;
typedef struct {
┊
Hoge hoge; ←将结构体Hoge 放到Piyo 的成员中
} Piyo;
也可以声明另一个结构体类型,并且同时将其声明为成员:
typedef struct {
struct Hoge_tag {
int a;
int b;
} hoge;
} Piyo;
此处声明的 struct Hoge_tag
在之后还是可以使用的。但是,因为可以省略 tag
,所以也可以写成:
typedef struct {
struct {
int a;
int b;
} hoge;
} Piyo;
但这种情况下,之后就不能重复使用相同的类型了。
我一般不在结构体声明中再声明结构体,倒是经常在结构体中声明共用体。这个在后一小节中说明。
6.2.5 共用体
共用体几乎总是和结构体、枚举组合使用。
在第 5 章我们定义了下面这样的 Shape
类型,
typedef enum {
POLYLINE_SHAPE,
RECTANGLE_SHAPE,
CIRCLE_SHAPE
} ShapeType;
typedef struct Shape_tag {
ShapeType type;
union {
Polyline polyline;
Rectangle rectangle;
Circle circle;
} u;
} Shape;
Shape
可能是 Polyline
(多点折线),也可能是 Rectangle
(长方形)或者是 Circle
(圆)。此时,我们可以用共用体。
为了表示共用体“此时真正使用的成员是哪一个”,使用了枚举型 ShapeType
的变量 type
。程序员有责任确保枚举的标识和真正被存储的成员之间的整合性。
在某些书籍当中也会出现下面的共用体用法:
typedef union {
char c[4];
int int_value;
} Int32;
在 int
为 4 个字节的情况下,可以以 C 中规定的字节为单位来访问 int_value
中保存的整数值。
可是,其结果完全依赖于环境的字节排序(参照 2.8 节),并且规范本来也没有规定 int
就是 32 位。
我并没有偏执地认为这种写法是完全错误的,但是在使用这种技巧的同时,的确应该意识到程序的可移植性问题。
6.2.6 数组的初始化
一维数组可以通过下面的方式进行初始化:
int hoge[] = {1, 2, 3, 4, 5};
因为编译器会去计算数组元素的个数,所以此时没有必要特别地去定义元素个数。为了防止出现不必要的错误,也最好不要在此处定义元素个数。
二维以上的数组,可以像下面这样初始化:
int hoge[][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
};
除了“最外层”的数组1,其他层包含的数组是不能省略元素个数定义的。请参照 3.5.2 节。
1 维数组的“最外层”是指将其他数组作为元素进行包含的最外侧的数组。——译者注
6.2.7 char数组的初始化
char
数组可以通过下面的方式进行特殊的初始化:
char str[] = "abc";
这其实是
char str[] = {'a', 'b', 'c', '\0'};
的语法糖。
因为末尾加上了'\0'
,所以 str
的元素个数为 4。
此时,多余地加上元素个数的定义,很可能会发生下面的错误:
char str[3] = "abc"; ←忘记了'\0'的存在
为了避免这样的错误,应该省略元素个数的定义,把对元素计数的工作交给编译器来做。
话说回来,其实在实际编程中,使用字符串初始化的 char
的数组的情况并不多,一般写成下面这样就足够了:
char *str = "abc";
两者的不同在于:相对于前者初始化“char
的数组”的内容,后者是利用字符串常量初始化“指向 char
的指针”。字符串常量一般保存在只读的内存区域(参照第 2 章),所以后者不能修改字符串的内容*。
* 在有些环境中,也许是可以修改的。这一点毕竟还是要依赖于环境。
6.2.8 指向char的指针的数组的初始化
在需要几个字符串组成的数组时,一般我们使用“指向 char
的指针的数组”。
char *color_name[] = {
"red",
"green",
"blue",
};
最后的 blue
后面加了一个逗号,这并不是一个错误。
C 语言中,在数组的初始化表达式最后的元素后面,既可以加逗号,也可以不加。
很多人讨厌这个规则,我倒是很喜欢。其实在所有的元素后面都添加逗号,还是挺方便的。对于字符串的情况,倘若在最后一个元素后面不追加逗号,当增加一个元素的时候,容易糊里糊涂地写成下面这样,
char *color_name[] = {
"red",
"green",
"blue" ←忘了加逗号
"yellow"
}
此时,ANSI C 会擅自将相邻的字符串常量连接起来,上面的数组就变成了“red
”、“green
”、“blueyellow
”构成的只有 3 个元素的数组*。
* 很明显,这个问题的起因是“擅自地连接了相邻的字符串”。作为连接字符串的功能,这好像还挺好使的。不过倒不如像 Perl 那样,在字符串之间放入“.
”来连接字符串,就不会发生这样的问题了。
Rationale 中“数组的初始化表达式最后的元素后面,可以加逗号,也可以不加逗号”这样的规则,除了使追加/删除元素更为方便之外,还有一个原因就是,能够更简单地开发自动生成代码的程序(参见 Ratinale 的 3.5.72)。
2 http://www.lysator.liu.se/c/rat/c5.html#3-5-7
但是,枚举的声明中却没有这样的规则,所以我认为这是不完整的规则。
typedef enum {
RED,
GREEN,
BLUE, ←这里不可以加逗号
} Color;
现在,一般称为 ANSI C 的 C(ISO-IEC 9899-1990),在语法上是不允许写成上面这样的*。可是在 ISO C99 中,规范已经修改成可以在最后的元素后面加上逗号。
* 根据编译器的不同,有的编译器可能会忽略这种问题,有的编译器会报出警告。
6.2.9 结构体的初始化
假设有下面这样一个结构体:
typedef struct {
int a;
int b;
} Hoge;
写成下面这样,可以初始化结构体的内容:
Hoge hoge = {5, 10};
在结构体嵌套或者结构体中包含数组的时候
typedef struct {
int a[10];
Hoge hoge;
} Piyo;
只要能够像下面这样,很好地将各成员的内容一一对应,也可以完成初始化:
Piyo piyo = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
{1, 2},
};
6.2.10 共用体的初始化
共用体的初始化比较麻烦。因为编译器无法知道“想要初始化哪一个成员”。
在 ANSI C 中,共用体的初始化是针对第一个成员实施的,这还真是个奇怪的规则呢!但是对于 C 的语法来说,这也是迫不得已的决定。
typedef union {
int int_value;
char *str;
} Hoge;
┊
Hoge hoge = {5}; ←初始化表达式对应于int_value
6.2.11 全局变量的声明
在使用 C 的全局变量的时候,简单地在头文件中像下面这样进行声明:
int global_variable;
然后可以在使用它的(多个的).c
文件中将其#include
。这种用法是比较常见的。
可是,本来应该在整个程序的某一个地方定义全局变量,其他地方使用 extern
声明就可以了。
虽说如此,但是现在的大部分(UNIX 的)C 处理环境中,对于在多个地方进行全局变量的定义,编译器是不会提出任何警告的,这不能不说是 C 的一个缺陷。在多个地方进行不加 extern
的变量定义,本来在连接的时候就应该报出多重定义(multiple define)的错误。
大型的应用程序,会有很多人同时参与开发。这种情况下,对于偶尔出现的全局变量名称冲突的情况,如今的 UNIX 的处理环境是不会提出任何警告的。最终的结果就是,大家总是被“全局变量的值总是在不知不觉中被修改”这样的性质极其恶劣的 bug 所困扰。
当然,在大型应用程序开发中,建立全局变量命名规则是一个常识。但是运用规则的也是人,所以出现疏漏也是不可避免的。所以应该通过一些工具去机械地检查全局变量的命名状况。
这里说一个题外话。我经常看到某些开发工程中对函数名使用了命名规则,却将全局变量命名规则的制定和实施放在一边。对函数名使用命名规则,这自然是件好事。但如果函数名出现冲突,在连接的阶段,毕竟处理环境是会报错的。因此从危险性上来说,全局变量名称的问题应该更多地引起大家的重视。
顺便说一下,C++已经消除了这个缺陷。如果在多个地方定义没有 extern
的变量会被报错。
在第 5 章的 word_count
程序中,头文件 word_manage_p.h 里使用了 extern
声明了全局变量,然后在 initialize.c 中对它们进行了定义。
可是,在两个不同地方进行几乎完全相同的记述,这很容易成为错误的根源。可以使用下面的方法来解决这个问题,
#ifdef GLOBAL_VARIABLE_DEFINE
#define GLOBAL /* 定义"无" */
#else
#define GLOBAL extern
#endif /* GLOBAL_VARIABLE_DEFINE */
GLOBAL int global_variable;
将头文件写成上面这样,然后在程序的某一个地方使用#define
定义 GLOBAL_VARIABLE_DEFINE
,并且包含这个头文件,就能保证定义只存在于一个地方,其他地方都是 extern
。
使用了这个技巧,就无法使用初始化表达式,所以很多人并不喜欢运用这个技巧。对于全局变量,我自己是很少使用初始化表达式的。因为初始化表达式“只能发生一次作用”,所以我习惯写一个像第 5 章例题中 word_initialize()
这样的函数。
但是我们经常需要对数组进行初始化,这种情况下,可以使用下面的方式:
GLOBAL char *color_name[]
#ifdef GLOBAL_VARIABLE_DEFINE
= {
"red",
"green",
"blue",
}
#endif /* GLOBAL_VARIABLE_DEFINE */
;