第八章 为什么程序员无法分清万圣节和圣诞节
8.1 Portsebie度量衡系统
8.2 根据位模式构筑图形
8.3 在等待时类型发生了变化
printf(“%d”, sizeof ‘A’);
输出的结果是4(或者是你机器上int的长度)。字符常量的类型是int,根据提升规则,它由char转换为int。
这个特性被称为类型提升。当它发生于整型类型时称为“整型提升”。
char c1, c2;
/* … */
c1 = c1 + c2;
“整型提升”规则要求把每个变量的值提升为int的长度,然后对两个int值执行加法运算,然后再对运算结果进行裁剪。如果两个char的加法运算结果不会发生溢出异常,那么在实际执行时只需要产生char类型的运算结果,可以省略类型提升。
float f1, f2;
double d;
/* … */
f1 = f2 * d;
如果编译器可以确定用float进行运算的结果跟转换为double后进行运算的结果一样,那么可以使用float来进行乘法运算。
整型提升就是char、short int和位段类型(无论signed或unsigned)以及枚举类型将被提升为int,前提是int能够完整地容纳原先的数据,否则将被转换为unsigned int。ANSI C表示如果编译器能够保证运算结果一致,也可以省略类型提升——这通常出现在表达式中存在常量操作数的时候。
真正值得注意之处——参数也会被提升。
这就是为什么单个的printf()格式符字串%d能适用于几种不同的类型,short、char或int,而不论实际传递的是上述类型的哪一个,函数从堆栈中(或寄存器中)取出的参数总是int类型。
C语言中的类型转换不仅用于操作数上,使操作符两端数据类型一致,同时也提升比规范类型int或double更小的数据类型(即使它们类型匹配)。
8.4 原型之痛
在K&R C中,如果向函数传递一个短语int的整数,函数实际所接收到的是int,如果传递的是一个float,函数实际接收到的是double。在被调用函数的函数体内,这些值会根据函数定义时参数的声明类型自动裁剪为该类型。
如果适用了函数原型,缺省参数提升就不会发生。如果参数声明为char,则实际所传递的也是char。
8.5 原型在什么地方会失败
如果未一个K&R C函数定义增加函数原型,而原型的参数列表中有一个short参数,在参数传递是,这个原型将导致实际传递给原型的就是short类型的参数,而根据函数的定义,它期望接收的是一个int类型的参数。这样,函数从堆栈中抓取4个字节(int)而不是2个字节(short)。
文件1
/*旧风格的函数定义,但它却具有原型*/
olddef(d, i) float d; char i; {
printf(“olddef: float = %f, char = %x \n “, d, i);
}
/*新风格的函数定义,但它却没有原型*/
newdef(float d, char i) {
printf(“newdef: float = %f, char = %x \n “, d, i);
}
文件2
/*旧风格的函数定义,但它却具有原型*/
int olddef ( float d, char i);
main() {
float d=10.0;
char j = 3;
olddef(d, j);
/*新风格的函数定义,但它却没有原型*/
newdef( d, j);
}
实际输出结果:
olddef: float = 524288.000000, char = 4
newdef: float = 2.562500, char = 0
如果把函数的定义放在它们被调用的同一个文件内,也就是文件2,程序的行为就会不一样。编译器将会检测到olddef()的不匹配,因为它现在可以同时看到原型和K&R C的函数定义。如果把newdef()的定义放在它之前,编译器就会平静地执行正确的操作,因为此时函数的定义相当于原型,它保证了声明和定义的一致性。
8.6 不需要按回车键就能得到一个字符
8.7 用C语言实现有限状态机
在C语言中,有好几种方法可以用来表示FSM,但它们绝大多数都是基于函数指针数组。一个函数指针数组可以像下面这样声明:
void ( *state[ MAX_STATES])();
如果知道了函数名,就可以像下面这样对数组进行初始化。
extern int a(), b(), c(), d();
int ( *state[])() = {a, b, c, d};
可以通过数组中的指针来调用函数:
( *state[i])()