3.5 其他
3.5.1 函数的形参的声明
C 语言可以像下面这样声明函数的形参:
void func(int a[])
{
┊
}
对于这种写法,无论怎么看都好像要向函数的参数传递数组。
可是,在 C 中是不能够将数组作为函数的参数进行传递的。无论如何,在这种情况下,你只能传递指向数组初始元素的指针。
在声明函数形参时,作为类型分类的数组,可以被解读成指针。
void func(int a[])
{
┊
}
可以被自动地解读成
void func(int *a)
{
┊
}
此时,就算你定义了数组的元素个数,也会被无视。
必须引起注意的是,在 C 语言中,只有在这种情况下,int a[]
和 int *a
才具有相同的意义。请同时参照 3.5.2 节。
要 点
【非常重要!!】
只有在声明函数形参的情况下,int a[]和 int *a 才具有相同的意义。
下面是一个稍微复杂一点的形参声明的例子:
void func(int a[][5])
a
的类型为“int
的数组(元素个数 5)的数组(元素个数不明)”,因此它可以解读成“指向 int
数组(元素个数 5)的指针”。因此,上面的声明本来的意思是:
void func(int (*a)[5])
补充 K&R 中关于函数形参声明的说明
K&R 的 p.121 中,有下面这样一段记述:
作为函数定义的形参,
- char s[];
和
- char s;
是完全相同的。我们认为写成后面这样比较好,因为这种写法能更加明确地表示这里的参数是一个指针。
这段文字本身可能并没有什么问题。可是在 K&R 中,这段文字是在说明了
\
(pa + i)
和pa[i]
之后唐突地出现的。因此,读者非常容易读漏掉前面的“作为函数定义的形参”这个前提条件。此外,原书中“作为函数定义的形参”( As formal parameters in a function definition)这句话正好到了页尾,这又增大了读者漏读的可能性。
此外,在这个例子中,为什么右边加上了分号?ANSI C 中,定义形参的时候一般是不加分号的,莫非早期的 C 语言就是这样的?还是忘了修正第一版的内容?
这应该不是翻译过程中的问题,原书中就已经加上了分号。
更让人费解的是,在 K&R 中,接着还有下面这段文字,
在向函数传递数组名的时候,函数会根据情况判断它是作为数组传入的,还是作为指针传入的,并进行相应的操作。
至少对于我来说,真的是完全不明白这段文字的意思。
真相应该是:
对于 C 语言,在表达式中的数组可以被解读成“指向初始元素的指针”
函数的参数也是表达式,所以,此时的数组也可以被解读成“指向初始元素的指针”
因此,向函数传递的往往是指针。
C 也不具备“函数会根据情况判断它是作为数组传入的,还是作为指针传入的,并进行相应的操作”这么神的超能力。只是指针经常被作为参数向函数传递罢了。
实际上,对于刚才那段引用中的
我们认为写成后面这样比较好,因为这种写法能更加明确地表示这里的参数是一个指针。
这段话,让人费解的是:
如果 C 语言的作者认为后面的写法比较好,为什么还故意加上“只有在函数形参中,数组的声明才可以被解读为指针”这么个奇怪的规则呢?
关于这一点,“The Development of the C Language”[5]中有这样一段说明:
Moreover, some rules designed to ease early transitions contributed to later confusion. For example, the empty square brackets in the function declaration
- int f(a) int a[]; { … }
are a living fossil, a remnant of NB's way of declaring a pointer;
翻译成中文是:
为了容易地进行早期的移植而设计的几个规则,之后带来了一些混乱。比如函数声明的空方括号,
这是 ANSI C 以前的方式。
- int f(a) int a[]; { … }
就是一个活化石,NB(New B)的指针声明方法留下的后遗症。
3.5.2 关于空的下标运算符[]
在 C 语言中,遇到以下情况下标运算符[]可以将元素个数省略不写。
对于这些情况,不同编译器会有各自特别的解释,所以不能作为普遍的规则来使用。
- 函数形参的声明
正如 3.5.1 节中说明的那样,对于函数的形参,最外层的数组会被解读成指针,即使定义了元素个数也会被无视。
- 根据初始化表达式可以确定数组大小的情况
在下面的情况下,编译器可以根据初始化表达式来确定元素的个数,所以可以省略最外层数组的元素个数。
int a[] = {1, 2, 3, 4, 5};
char str[] = "abc";
double matrix[][2] = {{1, 0}, {0, 1}};
char *color_name[] = {
"red",
"green",
"blue",
};
char color_name[][6] = {
"red",
"green",
"blue",
};
在初始化数组的数组的时候,如果有初始化表达式,貌似即使不是最外层的数组,编译器也应该能够确定其元素个数。可是,在 C 语言中,允许下面这样不整齐的数组初始化,因此还是不能简单地确定最外层数组以外的元素个数。
int a[][3] = { /* int a[3][3]的省略形式*/
{1, 2, 3},
{4, 5},
{6}
};
char str[][5] = { /* char str[3][5]的省略形式*/
"hoge",
"hog",
"ho",
};
似乎可以考虑让编译器选择一个最大值,但 C 的语法并没有这么做。
如果这么做是为了排查程序员的编程失误,那为什么没有把上面“不整齐的数组”也规定为错误?对于这种现象,我至今百思不得其解(莫非只是因为疏忽?)。
顺便说一下,在初始化上面这样不整齐的数组的时候,没有对应的初始化表达式的元素会被初始化为 0。
- 使用
extern
声明全局变量的情况
全局变量在多个编译单元(.c 文件)中的某一个中定义,然后从其他代码文件通过 extern
进行声明。
在定义的时候还是需要元素个数的,但是在使用 extern
进行声明的时候,在连接的时候编译器可以确定实际的数组大小,所以可以省略最外层数组的元素个数。
正如前面说明的那样,只有在声明函数形参的时候,数组的声明才可以被解读成指针。
像下面这样进行全局变量声明的时候,将数组和指针混在一起,除了程序不能正常运行之外,编译器通常也不会报告任何警告或者错误。这一点需要引起注意*。
- 如今的链接器,有时也会报错。
关于file_1.c中……
int a[100];
关于file_2.c中……
extern int *a;
补充 定义和声明
在 C 语言中,“声明”在规定变量或者函数的实体的时候被称为“定义”。 比如,像下面这样声明全局变量的行为,就是“定义”。
准确地说,
int a;
这样的定义属于暂时定义(tentative definition);int a = 0;
这样的加上了初始化表达式的定义属于“外部定义”。
- int a;
以下的
extern
的声明,意味着“使在某处声明的对象能够在当前的地方使用”,因此它不是“定义”。
- extern int a;
同样地,函数的原型是“声明”,函数的“定义”是指写着函数的实际执行代码的部分。
自动变量的情况下,区别定义和声明是没有意义的,因为此时声明必然伴随着定义。
3.5.3 字符串常量
使用""
包围起来的字符串被称为字符串常量。
字符串常量的类型是“char
的数组”,因此在表达式中,它可以解读为指针。
char *str;
str = "abc"; ←将「指向"abc"的初始元素的指针」赋给str
可是,char
数组的初始化是个例外。此时的字符串常量,作为在花括号中分开书写的初始化表达式的省略形式,编译器会进行特殊处理。
char str[] = "abc";
和
char str[] = {'a', 'b', 'c', '\0'};
具有相同的含义。
以前C语言只有标量,所以不能初始化自动变量的数组。因此,
char str[] = {'a', 'b', 'c', '\0'};
必须写成下面这样:
static char str[] = {'a', 'b', 'c', '\0'};
同样地,
char str[] = "abc";
这样的写法也是不允许的,你必须写成下面这样:
static char str[] = "abc";
可是,从 ANSI C 开始,即使是自动变量的数组,也可以被整合来进行初始化。
char str[] = "abc";
正因为如此,上面的写法是合法的。所以,
char str[4];
str = "abc";
这样的写法是非法的。
你是不是有点晕了?下面的例子不是初始化 char
的数组,而是初始化指针,所以也是合法的:
char *str = "abc";
此时的“abc
”就是普通的“char
的数组”,在表达式中被解释成“指向 char
的指针”,然后被赋给 str
。
只要按顺序对标识符的声明和初始化表达式中花括号的对应关系进行一步步分析,更复杂的例子也一定能够解释。
char *color_name[] = {
"red",
"green",
"blue",
};
此时,标识符 color_name
的类型为“指向 char
的指针的数组”,类型分类“数组”对应初始化表达式的最外层的花括号。因此,“red
”也好,“blue
”也好,它们都是“指向 char
的指针”。
char color_name[][6] = {
"red",
"green",
"blue",
};
这个例子中的 color_name
的类型为“char
的数组(元素个数 6)的数组”,同样地,类型分类“数组”,对应于初始化表达式的最外层的花括号,因此,无论是“red
”,还是“blue
”,它们都是“char
的数组(元素个数 6)”。所以,上面的声明和
char color_name[][6] = {
{'r', 'e', 'd', '\0'},
{'g', 'r', 'e', 'e', 'n', '\0'},
{'b', 'l', 'u', 'e', '\0'},
};
具有相同的意思。
通常,字符串常量保存在只读的内存区域(准确地说,实际的保存方式还是要依赖处理环境的具体实现的)。但如果在初始化 char
的数组的时候,采取将原本在花括号中分开书写的初始化表达式的省略形式,并且不给数组自身指定 const
,字符串常量就是可写的。
char str[] = "abc";
str[0] = 'd'; ←可写
但如果写成下面这样,就会报错:
char *str = "abc";
str[0] = 'd'; ←在大部分的处理环境中会报错
补充 字符串常量就是
char
的数组字符串常量的类型是“
char
的数组”。可是,在表达式中它可以被解释成“指向
char
的指针”。是不是有很多同学认为字符串常量本来就是“指向
char
的指针”呢?通过以下的代码,可以证明字符串常量本质还是数组:
- printf("size..%d\n", sizeof("abcdefghijklmnopqrstuvwxyz"));
3.5.4 关于指向函数的指针引起的混乱
正如 2.3.2 节中说明的那样,对于 C 语言,表达式中的函数可以被解读成“指向函数的指针”。
在信号处理、事件驱动的程序中,这种特性往往以回调函数的形式被使用。
/*如果发生SIGSEGV(Segmentation falut),回调函数segv_handler */
signal(SIGSEGV, segv_handler);
可是,如果基于之前说明过的 C 语言声明规则,int func()
这样的声明会被解释为“返回 int
的函数”,如果函数在表达式中,只是取出 func
解释成“指向返回 int
函数的指针”,是不是感觉很怪异?如果一定要使用指向函数的指针,必须要写成&func
。
对于上面信号处理的函数,写成
signal(SIGSEGV, &segv_handler);
这样,实际上也能顺利地执行。
相反,像
void (*func_p)();
这样,变量 func_p
声明为指向函数的指针,进行函数调用的时候,可以写成
func_p();
但是像 int func()
这种声明,都是用 func()
这样的方式进行调用的,从对称性的角度考虑,对于 void (*func_p)()
,必须要写成
- (*func_p)();*
* 早期的 C 语言中,好像也只能这么写……
这样也是能毫无问题地执行的。
是不是感觉 C 语言的关于指向函数的指针的语法比较混乱?
混乱产生的原因就是:“表达式中的函数可以解读成‘指向函数的指针’”这个意图不明的规则(难道就是为了和数组保持一致?)。
为了照顾到这种混乱,ANSI C 标准对语法做了以下例外的规定:
表达式中的函数自动转换成“指向函数的指针”。但是,当函数是地址运算符
&
或者sizeof
运算符的操作数时,表达式中的函数不能变换成“指向函数的指针”。函数调用运算符
()
的操作数不是“函数”,而是“函数的指针”。
如果对“指向函数的指针”使用解引用*
,它暂时会成为函数,但是因为在表达式中,所以它会被瞬间地变回成“指向函数的指针”。
结论就是,即使对“指向函数的指针”使用*
运算符,也是对牛弹琴,因为此时的运算符*
发挥不了任何作用。
因此,下面的语句也是能顺利执行的,
(**********printf)("hello, world\n"); ←无论如何,*就是什么也没做
3.5.5 强制类型转换
cast
是将某类型强制地转换成其他类型的运算符,它写成下面这样:
(类型名称)
简单地说,强制类型转换有两种使用方式。
其一是基本类型的强制转换,比如像下面这样想要将 int
作为 double
来使用的情况:
int hoge, piyo;
┊
printf("hoge / piyo..%f\n", (double)hoge / piyo);
在 C 中,无论怎样,int
的除法运算的结果还是 int
*,如果想要得到小数部分,上面除法运算符的某一边(或者是双方)的操作数必须转换成 double
。
* 这可是一个很大的陷阱。
此时,强制类型转换将 int
类型的值转换成实际的 double
类型。编译器在大多数情况下,会生成强制转换对应的机器代码。
另外一个强制转换的方式就是指针类型的强制转换。
C 语言编译器对于指针类型,根据其指向的类型的不同,分别采取不同的对待方式。在运行时,无论是指向 int
的指针,还是指向 double
的指针,从机器语言的角度来看,它们在大多数的处理环境中都只是地址。所谓的指针的强制类型转换,就是对指针进行强制读取转换。
比如像下面这样的指针强制类型转换:
double double_var;
int *int_p;
int_p = (int*)&double_var; ←将指向 double 的指针转换成指向 int 的指针
一旦将“指向 double
指针”强制地转换为“指向 int
的指针”,就无法追踪指针原本指向什么对象了。
因此,写成*int_p
,取出的数据类型为 int
类型,对 int_p
加 1,指针前移 sizeof(int)
。
如果想要开发出可移植性高的程序,就应该避免对指针进行强制类型转换。规范的编程是不会草率地对指针进行强制类型转换的*。
* 以前,malloc()
的返回值是必须要进行强制转型的,但到了 ANSI C 的时候就不需要这么做了。
可是也有一些例外,比如对于一个通用的 GUI 类库程序,界面上的按钮等控件可能会被关联各种类型的数据。此时,姑且先让控件关联到 void*
,之后根据需要再将其强制转换到关联数据的本来的类型。现实中的指针强制类型转换的场景,大致也就是这种程度。
“不知道为什么编译器提示了警告,姑且先来一把强制转型”→“只要不再出现警告,就随它去了……”——这种恶习是绝对需要避免的*。
* 经常能看到这种事。
编译器是不会无端地给出警告的,强制类型转换只是暂时掩盖了问题。就算通过了编译,程序也很有可能不会正常运行,要不就是虽然在当前的环境中能正常运行,一拿到别的环境中就跑不起来了。
要 点
不要使用强制类型转换来掩盖编译器的警告。
3.5.6 练习——挑战那些复杂的声明
应该是小试牛刀的时候了。
在 ANSI C 的标准库中,有一个 atexit()
函数。如果使用这个函数,当程序正常结束的时候,可以回调一个指定的函数。
atexit()
的原型定义如下:
- int atexit(void (*func)(void));
- 首先着眼于标识符。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is
- 解释用于函数的()。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is function() returning
- 函数的参数部分比较复杂,所以先解析这部分。同样地,先着眼于标识符。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is function(func
is) returning
- 因为有括号,所以这里解释*。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is function(func
is pointer to
) returning
- 解释用于函数的
()
。这里的参数还是比较简单的,是void
(无参数)。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is function(func
is pointer to function (void)
returning) returning
- 解释类型指定符
void
。这样就结束了atexit
的参数部分的解释。
- int atexit(void (*func)(void));
英语的表达为:
atexit
is function(func
is pointer to function(void)
returning void
) returning
- 解释数据类型修饰符
int
。
int atexit(void (*func)(void));
英语的表达为:
atexit
is function (func
is pointer to function (void)
returning void
) returning int
- 翻译成中文……
atexit
是返回 int
的函数(参数是,指向返回 void
没有参数的函数的指针)。
下面是一个更加复杂的例子。
标准库中有一个 signal()
函数,它的原型声明如下,
- void (*signal(int sig, void (*func)(int)))(int);
- 首先着眼于标识符。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is
- 相比
*
,()
的优先顺序更高,所以先解释这部分。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function() returning
- 解释参数部分。这里有两个参数,第一参数是
int sig
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig
is int
,) returning
- 着眼另外一个参数。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig
is int
, func
is) returning
- 因为有括号,所以这里解释
*
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig
is int
, func
is pointer to) returning
- 解释表示函数的(),参数为
int
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function
(sig
is int
, func
is pointer to function(int
) returning) returning
- 解释数据类型修饰符
void
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig is int
, func
is pointer to function(int
) returning void
) returning
- 参数部分已经解释结束。接着因为有括号,所以这里解释
*
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig
is int
, func
is pointer to function(int
) returning void
) returning pointer to
- 解释表示函数的(),参数为
int
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig is int
, func
is pointer to function(int
) returning void
) returning pointer to function(int
) returning
- 最后,添上
void
。
- void (*signal(int sig, void (*func)(int)))(int);
英语的表达为:
signal
is function(sig is int
, func
is pointer to function(int
) returning void
) returning pointer to function(int
) returning void
- 翻译成中文……
signal
是返回“指向返回 void
参数为 int
的函数的指针”的函数,它有两个参数,一个是 int
,另一个是“指向返回 void
参数为 int
的函数的指针”。
如果能读懂这种难度的声明,我想应该不会再有什么让你畏惧的 C 声明了。
下面的说明可能会让你对 C 语言感到更加不快。
signal()
是用于注册信号处理(当中断发生时被调用的函数)的函数。此函数的返回值是之前注册的处理当前信号中断的函数。
也就是说,其中的一个参数和返回值,它们都是相同的类型——指向信号处理函数的指针。在一般的语言中,同样的表现模式出现两次并不会让你感到不适,但是解释 C 语言声明的过程是“一会儿向左一会儿向右”,因此,表示返回值的部分散落了在左右两侧。
此时,运用 typedef
可以让声明变得格外得简洁。
/*摘录于FreeBSD 的man page */
typedef void(*sig_t)(int);
sig_t signal(int sig, sig_t func);
sig_t
代表“指向信号处理函数的指针”这个类型。