3.1 解读 C 的声明

3.1.1 用英语来阅读

在 1.2.2 节的补充内容中,我认为像

  1. int *hoge_p;

还有

  1. int hoge[10];

这样的声明方式很奇怪

对于这种程序的声明方式,可能也有很多人感觉不到有什么别扭的地方。那就再看下面的这个例子(经常被使用):

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

这里声明了一个“指向 char 的指针的数组”。

正如 2.3.2 节中介绍的那样,可以像下面这样声明一个“指向将 double 作为参数并且返回 int 的函数的指针”:

  1. int (*func_p)(double);

关于这样的声明,在 K&R 中有下面这样一段说明:

  1. int f(); / f:返回指向 int 指针的函数*/

  1. int (pf)(); / pf 指向返回 int 的函数的指针/

这两个声明最能说明问题。在这里,因为是前置运算符,它的优先级低于(),为了让连接正确地进行,有必要加上括号。

首先,这段文字中有谎言

声明中*()[]并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。

先将这个问题放在一边。如果你老老实实地去读这段文字,该会嘀咕“是不是搞反了”。如果说

  1. int (*pf)();

是指向函数的指针,使用括弧先将星号(指针)括起来岂不是很奇怪?

这个问题的答案,等你明白过来就会觉得非常简单。C 语言本来是美国人 开发的,最好还是用英语来读*

* 在 K&R 中,登载了 dcl 这个解析 C 的声明的程序,同时也记载了程序的输出结果,但是日语版并没有对这一段进行翻译,而是一成不变地转载了英文原文。

以上的声明,如果从 pf 开始以英语的顺序来读,应该是下面这样:

  1. pf is pointer to function returning int

翻译成中文,则为

  1. pf 为指向返回 int 的函数的指针。

要 点

用英语来读 C 的声明。

3.1.2 解读C的声明

在这里,向读者介绍阅读 C 语言声明的方法:机械地向前读。

为了把问题变得更简单,我们在这里不考虑 constvolatile。(3.4 节考虑了 const)接下来遵循以下步骤来解释 C 的声明。

  • 首先着眼于标识符(变量名或者函数名)。

  • 从距离标识符最近的地方开始,依照优先顺序解释派生类型(指针、数组和函数)。优先顺序说明如下,

    • 用于整理声明内容的括弧

    • 用于表示数组的[],用于表示函数的()

    • 用于表示指针的*

  • 解释完成派生类型,使用“of”、“to”、“returning”将它们连接起来。

  • 最后,追加数据类型修饰符(在左边,intdouble 等)。

  • 英语不好的人,可以倒序用日语(或者中文)1解释。

1 这里同样可以倒序用中文解释。——译者注

数组元素个数和函数的参数属于类型的一部分。应该将它们作为附属于类型的属性进行解释。

比如,

  1. int (*func_p)(double);
  • 首先着眼于标识符。
  1. int (*func_p)(double);

英语的表达为:

func_p is

  • 因为存在括号,这里着眼于*
  1. int (*func_p)(double);

英语的表达为:

func_p is pointer to

  • 解释用于函数的(),参数是 double
  1. int (*func_p)(double);

英语的表达为:

func_p is pointer to function(double) returning

  • 最后,解释数据类型修饰符 int
  1. int (*func_p)(double);

英语的表达为:

func_p is pointer to function(double) returning int

  • 翻译成中文:
  1. func_p 是指向返回 int 的函数的指针。

使用和上面相同的方式,我们对各种各样的声明进行解读,如下表(表 3-1)。

表3-1 解读各种各样的C语言声明

C语言 英语表达 中文表达
int hoge; hoge is int hogeint
int hoge[10]; hoge is array(元素个数10) of int hogeint的数组(元素个数10)
int hoge[10][3]; hoge is array(元素个数10) of array(元素个数3) of int hogeint数组(元素个数3)的数组(元素个数10)
int *hoge[10]; hoge is array(元素个数10) of pointer to int hoge是指向int的指针的数组(元素个数10)
int (*hoge)[3]; hoge is pointer to array(元素个数3) of double hoge是指向int的数组(元素个数3)的指针
int func(int a); func is function(参数为int a) returning int func是返回int的函数(参数是 int a)
int (*func_p)(int a) ; func_p is pointer to function(参数为int a) returning int func_p是指向返回int的函数(参数为int a)的指针

正如大家看到的这样,C 语言的声明不能从左往右按顺序解读(无论是英语、中文,还是日语),而是左右来回地解读。

K&R 中指出:在 C 语言中,变量的声明仿效表达式的语法。可是,勉强地去模拟本质上完全不同的事物,结果就是“四不像”。

“使声明的形式和使用的形式相似”是 C(还有从 C 派生的 C++、Java* 等语言)特有的奇怪的语法

* 其实, Java 的大部分 声明语法还是能做到这点的。

K&R 中同时也写道:

C 的声明语法,特别是指向函数指针的语法,受到了严厉的批评。

在 Pascal 中,C 的 int hoge[10]可以这样声明,

  1. var
  2. hoge : array[0..9] of integer;

这种声明,从左向右用英语按顺序解读是完全没有问题的。

顺便说一下,C 的作者 Dennis Ritchie 开发了一种叫 Limbo[7]的语言。Limbo 中各种标记的使用方法,一眼就可以看出来和 C 非常相似*,但是声明语法完全设计成 Pascal 风格。其实作者自身也在反省 C 语言的声明语法。

* 比如,不使用 beginend 或者 ifendif 而是使用中括号。

3.1.3 类型名

在 C 中,除标识符以外,有时候还必须定义“类型”。

具体来说,遇到以下情况需定义“类型”:

  • 在强制转型运算符中

  • 类型作为 sizeof 运算符的操作数

比如,将强制转型运算符写成下面这样:

  1. (int*)

这里指定“int*”为类型名

从标识符的声明中,将标识符取出后,剩下的部分自然就是类型名。

表3-2 类型名的写法

声  明 声明的解释 类 型 名 类型名的解释
int hoge; hogeint int int类型
int *hoge; hoge是指向int的指针 int * 指向int的指针类型
double(*p)[3]; p是指向double的数组(元素个数3)的指针 double(*)[3] 指向double的数组(元素个数3)的指针类型
void(*func)(); func是指向返回void函数的指针 void (*)() 指向返回void函数的指针类型

在表 3-2 最后两个例子中,括起星号的括弧(*)好像有点多余,但是一旦去掉括弧,意思就完全不一样了

(double *[3])是将 double *hoge[3]的标识符去掉后形成的,所以这个类型名被解释成“指向 double 的指针的数组”。

补充 如果将指针后置……

C 的声明语法虽然奇怪,但也有人说 Pascal 风格写起来长得像裹脚布,同样让人感到厌恶。

  1. var
  2. hoge : array[0..9] of integer;

关于这样的声明,array 后面紧接着[],用来表示这是个数组,但是这样让人感觉 array 这个单词太长了。顺手在后面追加的 of 也是多余的。尝试将这些多余的部分去掉,结果就像下面这样:

  1. var
  2. hoge : [0..9] integer;

如果改成 C 的写法:

  1. hoge[10] int;

如果仅仅就 int 前置还是后置的问题来说,感觉和 C 的声明方式也没多大差别。

可是,一旦涉及指针,情况就不一样了。C 的指针运算符*是前置的。

在 Pascal 中,运算符^相当于 C 中*的,而且它是后置的。

如果同样地将 C 的指针运算符*也放在标识符后面,即使兼顾“变量的声明仿效表达式的语法”,声明也会变成下面这样:

  1. int func_p^(double);

如果这个声明表示“指向返回 int 的函数(参数为 double)的指针”,差不多也符合英语的阅读顺序。不过 int 放在前面终究是个问题。

此外,一旦使用后置的^,通过指针引用结构体的成员的时候,就可以不要->运算符了。

C 中,^被作为异或运 算符使用。这里,我们 且不必去关心这一点。

原本

  1. hoge->piyo

只是

  1. (hoge).piyo

的语法糖,所以又可以写成

  1. hoge^.piyo

因此->完全可以不要的。

此外,将解引用后置,可以使包含结构体成员和数组引用的复杂表达式变得简洁

* 另外,如果将指针的强制转型也进行后置,同样也能起到简化表达式的作用。

关于这一点,“The Development of the C Language[5]”中也有说明:

Sethi [Sethi 81] observed that many of the nested declarations and expressions would become simpler if the indirection operator had been taken as a postfix operator instead of prefix, but by then it was too late to change.

请允许我用我这二把刀的英语水平给大家翻译一下:

Sethi 认为,如果将解引用由前置变成后置,嵌套的声明和表达式就会变得更简单。但是,如今想要修正,为时已晚。