6.2 惯用句法

6.2.1 结构体声明

对于这种习惯用法,可能会有不同的观点。但是我在声明结构体的时候,必定会同时给出 typedef 定义。

  1. typedef struct {
  2. int a;
  3. int b;
  4. } Hoge;

此外,在没有特别需要的情况下,我一般不写 tag*。在需要写 tag 的时候,一般像下面这样,在定义的类型名称后面追加_tag

* 这是由于,在特别需要的情况下写 tagtag 的存在表明了“正在被前方引用”这层意思。也有人主张“因为不知道什么时候需要,所以必须写 tag”这样的观点。

  1. typedef struct Hoge_tag {
  2. int a;
  3. int b;
  4. } Hoge;

还有,对于结构体、共用体、枚举的 tag 名,因为持有和一般的标识符不同的命名空间,所以它们也可以写成下面这样:

  1. typedef struct Hoge {
  2. int a;
  3. int b;
  4. } Hoge;

因为写成这样则与 C++意思相同,所以也有很多人喜欢这种写法。

在声明结构体的时候,虽然可以同时定义这个结构体类型的变量,但我不会这么做。一想到 typedef,就完全没有了这么写的冲动,因为类型的声明和变量的定义根本就是不同的概念,所以我认为还是分开书写比较好。

  1. /* 我不会这样写 */
  2. struct Hoge_tag {
  3. int a;
  4. int b;
  5. } hoge; ←声明struct Hoge_tag 类型的变量

随便再提一下,虽然声明结构体的成员时,可以和声明一般的变量一样,一次性声明多个变量,但是我不会这么做。

  1. /* 我不会这样写 */
  2. typedef struct {
  3. int a, b;
  4. } Hoge;

6.2.2 自引用型结构体

为了构造链表和树,我们会声明包含指向相同类型的指针的结构体。

这样的结构体,好像一般都称为“自引用型结构体”——为什么是“好像”?C 语言的入门书籍中倒是这么称呼的,但是在开发现场,我从没听到过有人使用这个称呼。称之为“自引用型结构体”,其实没有任何特殊的理由。

但是,对于这种结构体的声明,还是有必须要留意的地方。

  1. typedef struct Hoge_tag {
  2. int a;
  3. int b;
  4. struct Hoge_tag *next;
  5. } Hoge;

在这种情况下,在声明成员 next 的时候,typedef 还没有结束,类型 Hoge 还不能被使用,所以 next 还只能声明成 struct Hoge_tag*

或者也可以写成下面这样:

  1. typedef struct Hoge_tag Hoge;
  2. struct Hoge_tag {
  3. int a;
  4. int b;
  5. Hoge *next;
  6. };

6.2.3 结构体的相互引用

在 3.2.10 节中我们说过,对于相互引用的结构体,应该像下面这样只是将 tag 提前声明:

在下面的代码中,Man 持有指向“妻”的指针,Woman 持有指向“夫”的指针。

  1. typedef struct Woman_tag Woman; ←提前对tag 进行类型定义
  2. typedef struct {
  3. Woman *wife; /* 妻 */
  4. } Man;
  5. struct Woman_tag {
  6. Man *husband; /* 夫 */
  7. };

以前,一提出上面这种写法,肯定就有人这么想:

哇嘎嘎,那就把所有的 tag 全部提前声明,然后就可以按照任意的顺序声明结构体了。

于是,就有了下面这样的代码,

  1. typedef struct Polyline_tag Polyline; /*(以下3行)将所有tag 提前声明*/
  2. typedef struct Shape_tag Shape;
  3. ...
  4. struct Shape_tag {
  5. ShapeType type;
  6. union {
  7. Polyline polyline; ←只声明了tag,使用了Polyline 的实体!
  8. Rectangle rectangle;
  9. Circle circle;
  10. } u;
  11. };
  12. struct Polyline_tag {
  13. ...
  14. };

上面的代码是无法通过编译的。

在只是声明 tag 的情况下,其类型是“不完全类型”。对于不完全类型,只能取得其指针(参照 3.2.10 节)。

因为还不知道不完全类型的长度,所以编译器无法确定结构体成员的偏移量。

6.2.4 结构体的嵌套

将结构体作为另一个结构体的成员时,可以像下面这样使用已经声明的结构体:

  1. typedef struct {
  2. int a;
  3. int b;
  4. } Hoge;
  5. typedef struct {
  6. Hoge hoge; ←将结构体Hoge 放到Piyo 的成员中
  7. } Piyo;

也可以声明另一个结构体类型,并且同时将其声明为成员:

  1. typedef struct {
  2. struct Hoge_tag {
  3. int a;
  4. int b;
  5. } hoge;
  6. } Piyo;

此处声明的 struct Hoge_tag 在之后还是可以使用的。但是,因为可以省略 tag,所以也可以写成:

  1. typedef struct {
  2. struct {
  3. int a;
  4. int b;
  5. } hoge;
  6. } Piyo;

但这种情况下,之后就不能重复使用相同的类型了。

我一般不在结构体声明中再声明结构体,倒是经常在结构体中声明共用体。这个在后一小节中说明。

6.2.5 共用体

共用体几乎总是和结构体、枚举组合使用。

在第 5 章我们定义了下面这样的 Shape 类型,

  1. typedef enum {
  2. POLYLINE_SHAPE,
  3. RECTANGLE_SHAPE,
  4. CIRCLE_SHAPE
  5. } ShapeType;
  6. typedef struct Shape_tag {
  7. ShapeType type;
  8. union {
  9. Polyline polyline;
  10. Rectangle rectangle;
  11. Circle circle;
  12. } u;
  13. } Shape;

Shape 可能是 Polyline(多点折线),也可能是 Rectangle(长方形)或者是 Circle(圆)。此时,我们可以用共用体。

为了表示共用体“此时真正使用的成员是哪一个”,使用了枚举型 ShapeType 的变量 type。程序员有责任确保枚举的标识和真正被存储的成员之间的整合性。

在某些书籍当中也会出现下面的共用体用法:

  1. typedef union {
  2. char c[4];
  3. int int_value;
  4. } Int32;

int 为 4 个字节的情况下,可以以 C 中规定的字节为单位来访问 int_value 中保存的整数值。

可是,其结果完全依赖于环境的字节排序(参照 2.8 节),并且规范本来也没有规定 int 就是 32 位。

我并没有偏执地认为这种写法是完全错误的,但是在使用这种技巧的同时,的确应该意识到程序的可移植性问题。

6.2.6 数组的初始化

一维数组可以通过下面的方式进行初始化:

  1. int hoge[] = {1, 2, 3, 4, 5};

因为编译器会去计算数组元素的个数,所以此时没有必要特别地去定义元素个数。为了防止出现不必要的错误,也最好不要在此处定义元素个数。

二维以上的数组,可以像下面这样初始化:

  1. int hoge[][3] = {
  2. {1, 2, 3},
  3. {4, 5, 6},
  4. {7, 8, 9},
  5. };

除了“最外层”的数组1,其他层包含的数组是不能省略元素个数定义的。请参照 3.5.2 节。

1 维数组的“最外层”是指将其他数组作为元素进行包含的最外侧的数组。——译者注

6.2.7 char数组的初始化

char 数组可以通过下面的方式进行特殊的初始化:

  1. char str[] = "abc";

这其实是

  1. char str[] = {'a', 'b', 'c', '\0'};

的语法糖。

因为末尾加上了'\0',所以 str 的元素个数为 4。

此时,多余地加上元素个数的定义,很可能会发生下面的错误:

  1. char str[3] = "abc"; ←忘记了'\0'的存在

为了避免这样的错误,应该省略元素个数的定义,把对元素计数的工作交给编译器来做。

话说回来,其实在实际编程中,使用字符串初始化的 char 的数组的情况并不多,一般写成下面这样就足够了:

  1. char *str = "abc";

两者的不同在于:相对于前者初始化“char 的数组”的内容,后者是利用字符串常量初始化“指向 char 的指针”。字符串常量一般保存在只读的内存区域(参照第 2 章),所以后者不能修改字符串的内容*

* 在有些环境中,也许是可以修改的。这一点毕竟还是要依赖于环境。

6.2.8 指向char的指针的数组的初始化

在需要几个字符串组成的数组时,一般我们使用“指向 char 的指针的数组”。

  1. char *color_name[] = {
  2. "red",
  3. "green",
  4. "blue",
  5. };

最后的 blue 后面加了一个逗号,这并不是一个错误。

C 语言中,在数组的初始化表达式最后的元素后面,既可以加逗号,也可以不加。

很多人讨厌这个规则,我倒是很喜欢。其实在所有的元素后面都添加逗号,还是挺方便的。对于字符串的情况,倘若在最后一个元素后面不追加逗号,当增加一个元素的时候,容易糊里糊涂地写成下面这样,

  1. char *color_name[] = {
  2. "red",
  3. "green",
  4. "blue" ←忘了加逗号
  5. "yellow"
  6. }

此时,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

但是,枚举的声明中却没有这样的规则,所以我认为这是不完整的规则。

  1. typedef enum {
  2. RED,
  3. GREEN,
  4. BLUE, ←这里不可以加逗号
  5. } Color;

现在,一般称为 ANSI C 的 C(ISO-IEC 9899-1990),在语法上是不允许写成上面这样的*。可是在 ISO C99 中,规范已经修改成可以在最后的元素后面加上逗号。

* 根据编译器的不同,有的编译器可能会忽略这种问题,有的编译器会报出警告。

6.2.9 结构体的初始化

假设有下面这样一个结构体:

  1. typedef struct {
  2. int a;
  3. int b;
  4. } Hoge;

写成下面这样,可以初始化结构体的内容:

  1. Hoge hoge = {5, 10};

在结构体嵌套或者结构体中包含数组的时候

  1. typedef struct {
  2. int a[10];
  3. Hoge hoge;
  4. } Piyo;

只要能够像下面这样,很好地将各成员的内容一一对应,也可以完成初始化:

  1. Piyo piyo = {
  2. {0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
  3. {1, 2},
  4. };

6.2.10 共用体的初始化

共用体的初始化比较麻烦。因为编译器无法知道“想要初始化哪一个成员”。

在 ANSI C 中,共用体的初始化是针对第一个成员实施的,这还真是个奇怪的规则呢!但是对于 C 的语法来说,这也是迫不得已的决定。

  1. typedef union {
  2. int int_value;
  3. char *str;
  4. } Hoge;
  5. Hoge hoge = {5}; ←初始化表达式对应于int_value

6.2.11 全局变量的声明

在使用 C 的全局变量的时候,简单地在头文件中像下面这样进行声明:

  1. 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 中对它们进行了定义。

可是,在两个不同地方进行几乎完全相同的记述,这很容易成为错误的根源。可以使用下面的方法来解决这个问题,

  1. #ifdef GLOBAL_VARIABLE_DEFINE
  2. #define GLOBAL /* 定义"无" */
  3. #else
  4. #define GLOBAL extern
  5. #endif /* GLOBAL_VARIABLE_DEFINE */
  6. GLOBAL int global_variable;

将头文件写成上面这样,然后在程序的某一个地方使用#define 定义 GLOBAL_VARIABLE_DEFINE,并且包含这个头文件,就能保证定义只存在于一个地方,其他地方都是 extern

使用了这个技巧,就无法使用初始化表达式,所以很多人并不喜欢运用这个技巧。对于全局变量,我自己是很少使用初始化表达式的。因为初始化表达式“只能发生一次作用”,所以我习惯写一个像第 5 章例题中 word_initialize()这样的函数。

但是我们经常需要对数组进行初始化,这种情况下,可以使用下面的方式:

  1. GLOBAL char *color_name[]
  2. #ifdef GLOBAL_VARIABLE_DEFINE
  3. = {
  4. "red",
  5. "green",
  6. "blue",
  7. }
  8. #endif /* GLOBAL_VARIABLE_DEFINE */
  9. ;