3.1 解读 C 的声明
3.1.1 用英语来阅读
在 1.2.2 节的补充内容中,我认为像
int *hoge_p;
还有
int hoge[10];
这样的声明方式很奇怪。
对于这种程序的声明方式,可能也有很多人感觉不到有什么别扭的地方。那就再看下面的这个例子(经常被使用):
char *color_name[] = {
"red",
"green",
"blue",
};
这里声明了一个“指向 char
的指针的数组”。
正如 2.3.2 节中介绍的那样,可以像下面这样声明一个“指向将 double
作为参数并且返回 int
的函数的指针”:
int (*func_p)(double);
关于这样的声明,在 K&R 中有下面这样一段说明:
- int f(); / f:返回指向 int 指针的函数*/
和
- int (pf)(); / pf: 指向返回 int 的函数的指针/
这两个声明最能说明问题。在这里,因为是前置运算符,它的优先级低于
()
,为了让连接正确地进行,有必要加上括号。
首先,这段文字中有谎言。
声明中*
、()
和[]
并不是运算符。在语法规则中,运算符的优先顺序是在别的地方定义的。
先将这个问题放在一边。如果你老老实实地去读这段文字,该会嘀咕“是不是搞反了”。如果说
int (*pf)();
是指向函数的指针,使用括弧先将星号(指针)括起来岂不是很奇怪?
这个问题的答案,等你明白过来就会觉得非常简单。C 语言本来是美国人 开发的,最好还是用英语来读*。
* 在 K&R 中,登载了 dcl
这个解析 C 的声明的程序,同时也记载了程序的输出结果,但是日语版并没有对这一段进行翻译,而是一成不变地转载了英文原文。
以上的声明,如果从 pf 开始以英语的顺序来读,应该是下面这样:
pf is pointer to function returning int
翻译成中文,则为
pf 为指向返回 int 的函数的指针。
要 点
用英语来读 C 的声明。
3.1.2 解读C的声明
在这里,向读者介绍阅读 C 语言声明的方法:机械地向前读。
为了把问题变得更简单,我们在这里不考虑 const
和 volatile
。(3.4 节考虑了 const
)接下来遵循以下步骤来解释 C 的声明。
首先着眼于标识符(变量名或者函数名)。
从距离标识符最近的地方开始,依照优先顺序解释派生类型(指针、数组和函数)。优先顺序说明如下,
用于整理声明内容的括弧
用于表示数组的
[]
,用于表示函数的()
用于表示指针的
*
解释完成派生类型,使用“
of
”、“to
”、“returning
”将它们连接起来。最后,追加数据类型修饰符(在左边,
int
、double
等)。英语不好的人,可以倒序用日语(或者中文)1解释。
1 这里同样可以倒序用中文解释。——译者注
数组元素个数和函数的参数属于类型的一部分。应该将它们作为附属于类型的属性进行解释。
比如,
- int (*func_p)(double);
- 首先着眼于标识符。
- int (*func_p)(double);
英语的表达为:
func_p
is
- 因为存在括号,这里着眼于
*
。
- int (*func_p)(double);
英语的表达为:
func_p
is pointer to
- 解释用于函数的(),参数是
double
。
- int (*func_p)(double);
英语的表达为:
func_p
is pointer to function(double)
returning
- 最后,解释数据类型修饰符
int
。
- int (*func_p)(double);
英语的表达为:
func_p
is pointer to function(double)
returning int
- 翻译成中文:
func_p 是指向返回 int 的函数的指针。
使用和上面相同的方式,我们对各种各样的声明进行解读,如下表(表 3-1)。
表3-1 解读各种各样的C语言声明
C语言 | 英语表达 | 中文表达 |
---|---|---|
int hoge; | hoge is int | hoge 是int |
int hoge[10]; | hoge is array(元素个数10) of int | hoge 是int 的数组(元素个数10) |
int hoge[10][3]; | hoge is array(元素个数10) of array (元素个数3) of int | hoge 是int 数组(元素个数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]
可以这样声明,
var
hoge : array[0..9] of integer;
这种声明,从左向右用英语按顺序解读是完全没有问题的。
顺便说一下,C 的作者 Dennis Ritchie 开发了一种叫 Limbo[7]的语言。Limbo 中各种标记的使用方法,一眼就可以看出来和 C 非常相似*,但是声明语法完全设计成 Pascal 风格。其实作者自身也在反省 C 语言的声明语法。
* 比如,不使用 begin
~end
或者 if
~endif
而是使用中括号。
3.1.3 类型名
在 C 中,除标识符以外,有时候还必须定义“类型”。
具体来说,遇到以下情况需定义“类型”:
在强制转型运算符中
类型作为
sizeof
运算符的操作数
比如,将强制转型运算符写成下面这样:
(int*)
这里指定“int*
”为类型名。
从标识符的声明中,将标识符取出后,剩下的部分自然就是类型名。
表3-2 类型名的写法
声 明 | 声明的解释 | 类 型 名 | 类型名的解释 |
---|---|---|---|
int hoge; | hoge 是int | 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 风格写起来长得像裹脚布,同样让人感到厌恶。
- var
- hoge : array[0..9] of integer;
关于这样的声明,
array
后面紧接着[]
,用来表示这是个数组,但是这样让人感觉array
这个单词太长了。顺手在后面追加的of
也是多余的。尝试将这些多余的部分去掉,结果就像下面这样:
- var
- hoge : [0..9] integer;
如果改成 C 的写法:
- hoge[10] int;
如果仅仅就
int
前置还是后置的问题来说,感觉和 C 的声明方式也没多大差别。可是,一旦涉及指针,情况就不一样了。C 的指针运算符
*
是前置的。在 Pascal 中,运算符^相当于 C 中
*
的,而且它是后置的。如果同样地将 C 的指针运算符
*
也放在标识符后面,即使兼顾“变量的声明仿效表达式的语法”,声明也会变成下面这样:
- int func_p^(double);
如果这个声明表示“指向返回
int
的函数(参数为double
)的指针”,差不多也符合英语的阅读顺序。不过int
放在前面终究是个问题。此外,一旦使用后置的
^
,通过指针引用结构体的成员的时候,就可以不要->
运算符了。C 中,
^
被作为异或运 算符使用。这里,我们 且不必去关心这一点。原本
- hoge->piyo
只是
- (hoge).piyo
的语法糖,所以又可以写成
- 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 认为,如果将解引用由前置变成后置,嵌套的声明和表达式就会变得更简单。但是,如今想要修正,为时已晚。