本节书摘来自异步社区《C专家编程》一书中的第1章,第1.10节,作者 【美】Perter Van Der Linde,更多章节内容可以访问云栖社区“异步社区”公众号查看
1.10 “安静的改变”究竟有多少安静
标准所作的修改并非都如原型那样引人注目。ANSI C作了其他一些修改,目的是使C语言更加可靠。例如,“寻常算术转换(usual arithmetic conversion)”在旧式的K&R C和ANSI C中的意思就有所不同。Kernighan和Ritchie当初是这样写的:
第6.6节:算术转换
许多运算符都会引发转换,以类似的方式产生结果类型。这个模式称为“寻常算术转换”。
首先,任何类型为char或short的操作数被转换为int,任何类型为float的操作数被转换为double。其次,如果其中一个操作数的类型是double,那么另一个操作数被转换成double,计算结果的类型也是double。再次,如果其中一个操作数的类型是long,那么另一个操作数被转换成long,计算结果的类型也是long。或者,如果其中一个操作数的类型是unsigned,那么另一个操作数被转换成unsigned,计算结果的类型也是unsigned。如果不符合上面几种情况,那么两个操作数的类型都作为int,计算结果的类型也是int。
ANSI C手册重新编写了有关内容,填补了其中的漏洞:
第6.2.1.1节 字符和整型(整型升级)
char, short int或者int型位段(bit-field),包括它们的有符号或无符号变型,以及枚举类型,可以使用在需要int或unsigned int的表达式中。如果int可以完整表示源类型的所有值[7],那么该源类型的值就转换为int,否则转换为unsigned int。这称为整型升级。
第6.2.1.5节 寻常算术转换
许多操作数类型为算术类型的双目运算符会引发转换,并以类似的方式产生结果类型。它的目的是产生一个普通类型,同时也是运算结果的类型。这个模式称为“寻常算术转换”。
首先,如果其中一个操作数的类型是long double,那么另一个操作数也被转换为long double。其次,如果其中一个操作数的类型是double,那么另一个操作数也被转换为double。再次,如果其中一个操作数的类型是float,那么另一个操作数也被转换为float。否则,两个操作数进行整型升级(第6.2.1.1节描述整型升级),执行下面的规则:
如果其中一个操作数的类型是unsigned long int,那么另一个操作数也被转换为unsigned long int。其次,如果其中一个操作数的类型是long int,而另一个操作数的类型是unsigned int,如果long int能够完整表示unsigned int的所有值[8],那么unsigned int类型操作数被转换为long int,如果long int不能完整表示unsigned int的所有值[9],那么两个操作数都被转换为unsigned long int。再次,如果其中一个操作数的类型是long int,那么另一个操作数被转换为long int。再再次,如果其中一个操作数的类型是unsigned int,那么另一个操作数被转换为unsigned int。如果所有以上情况都不属于,那么两个操作数都为int。
浮点操作数和浮点表达式的值可以用比类型本身所要求的更大的精度和更广的范围来表示,而它的类型并不因此改变。
采用通俗语言(当然存有漏洞,而且不够精确),ANSI C标准所表示的意思大致如下:
当执行算术运算时,操作数的类型如果不同,就会发生转换。数据类型一般朝着浮点精度更高、长度更长的方向转换,整型数如果转换为signed不会丢失信息,就转换为signed,否则转换为unsigned。
K&R C所采用无符号保留(unsigned preserving)原则,就是当一个无符号类型与int或更小的整型混合使用时,结果类型是无符号类型。这是个简单的规则,与硬件无关。但是,正如下面的例子所展示的那样,它有时会使一个负数丢失符号位。
ANSI C标准则采用值保留(value preserving)原则,就是当把几个整型操作数像下面这样混合使用时,结果类型有可能是有符号数,也可能是无符号数,取决于操作数的类型的相对大小。
下面的程序段分别在ANSI C和K&R C编译器中运行时,将打印出不同的信息:
main(){
if(-1 < (unsigned char)1
printf("-1 is less than (unsigned char)1: ANSI semantics ");
else
printf("-1 NOT less than (unsigned char)1: K&R semantics");
}
程序中的表达式在两种编译器下编译的结果不同。-1的位模式是一样的,但一个编译器(ANSI C)将它解释为负数,另一个编译器(K&R C)却将它解释为无符号数,也就是变成了正数。
一个微妙的Bug
虽然规则作了修改,但微妙的Bug依然存在。在下面这个例子里,变量d比程序所需的下标值小1,这段代码的目的就是处理这种情况。但if表达式的值却不是真。为什么?是不是有Bug:
int array[] = { 23, 34, 12, 17, 204, 99, 16 };
#define TOTAL_ELEMENTS (sizeof(array)/sizeof(array[0]))
main( )
{
int d = -1, x;
/* ... */
if(d <= TOTAL_ELEMENTS - 2)
x = array[d+1];
/* ... */
}
TOTAL_ELEMENTS所定义的值是unsigned int类型(因为sizeof()的返回类型是无符号数)。if语句在signed int和unsigned int之间测试相等性,所以d被升级为unsigned int类型,-1转换成unsigned int的结果将是一个非常巨大的正整数,致使表达式的值为假。这个bug在ANSI C中存在,而如果K&R C的某种编译器的sizeof()的返回值是无符号数,那么这个Bug也存在。要修正这个问题,只要对TOTAL_ELEMENTS进行强制类型转换即可:
if(d <= (int)TOTAL_ELEMENTS – 2)
对无符号类型的建议
尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而用它来表示数量。
尽量使用像int那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如-1被翻译为非常大的正数)。
只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型。
这听起来是不是有点诡异,是不是令人吃惊?确实如此!用前面一页所说的规则完成上面这个例子。
最后,为了不让The Elements of Programming Style[10]未来的版本把这段代码作为不良风格的实例,我最好解释一下其中的一些代码。我使用了下面这条语句:
#define TOTAL_ELEMENTS (sizeof(array) / sizeof(array[0]))
而不是:
#define TOTAL_ELEMENTS (sizeof(array) / sizeof(int))
因为前者可以在不修改#define语句的情况下改变数组的基本类型(比如,把int变成char)。
Sun公司的ANSI C编译器小组认为从“无符号保留”转到“值保留”对于C语言的语义而言完全没有必要,只会让偶尔遇到这方面问题的人感到吃惊和沮丧。因此,在“尽量不让人误会”的原则下,Sun编译器认可并编译ANSI C的特性,除非该特性在K&R C里另有解释。如果碰到后面这种情况,编译器在缺省情况下使用K&R C的标准,并给出一条警告信息。如果碰到上面这个例子,程序员应该使用强制类型转换告诉编译器最终所希望的类型。在Sun公司运行Solaris 2.x的工作站上只要打开编译器的-Xc开关,就可以使编译器严格遵循ANSI C标准的语义。
在K&R C的许多特性中,有许多在ANSI C中进行了更新,包括许多所谓“安静的转变”。在这种情况下,代码在两种编译器里都能通过编译,但具体含义稍有差别。当程序员发现这种情况时,他们的反应可想而知。因此,这种转变事实上应该称作“讨厌的转变”。总的来说,ANSI委员会试图进行尽可能少的改动,与原先存在的但确实需要改进的特性保持一致。
对于ANSI C族系背景知识的讨论已经够多了。因此,在下面的“轻松一下”一节过后,让我们驶向第2章,进入本书的中心内容。