《C专家编程》一1.10 “安静的改变”究竟有多少安静

简介:

本节书摘来自异步社区《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)却将它解释为无符号数,也就是变成了正数。


f0472e205e7f7dfa45fc8105bed9768cfd7e7efc

一个微妙的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)


4abae6a85ecdec41ab7c664441e756c5fea15eb7

对无符号类型的建议

尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而用它来表示数量。

尽量使用像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章,进入本书的中心内容。

相关文章
|
人工智能 运维 数据可视化
程序员养家活口接私活必备网站(顺便用技术改变世界)
程序员接私活的原因很多种(挣钱、养家糊口、提升技术等等)。下面整理了一下网站送给最有潜能的你。 提前准备好自己的笔记本和技术呦。
473 0
|
架构师 程序员
码农代码之外的生存指南,不要等到而立之年再后悔
  程序员不应该只会写代码。   我觉得在程序员群体中,很多人有个误区,就是觉得作为码农,每天安安静静的敲代码、默默的做好技术就可以了,其他的事情都不用去思考,以为一切都会水到渠成。但是还没等到水到渠成,却先等来了中年焦虑。慢慢的等到自己到了三十多岁的时候,发现公司里年轻且更能加班码农越来越多,带来了职业发展上的焦虑,同时上有老下有小且薪资也慢慢遇到职业瓶颈,又加重了生活财务的焦虑。   最近又翻了翻之前看过的一本书《软技能-代码之外的生存指南》,再次来读,又有一番新的理解。   作为一名程序员,我们的职业与生活中,不能仅仅只关注代码和技术。
181 0
|
传感器
和12岁小同志搞创客开发:检测按键状态的两件法宝
和12岁小同志搞创客开发:检测按键状态的两件法宝
和12岁小同志搞创客开发:检测按键状态的两件法宝
|
小程序 搜索推荐 程序员
程序员的路是一行一行走出来的,分享给想要进入这个行业的人
  01、大学   回想十年前,我还是一名大学生,傻乎乎的对未来没有任何的规划。每天就知道混日子,打打游戏、谈谈恋爱。至于毕业后能不能找到一份工作,心里完全没个底。   有时间的时候读读《代码大全》、《人月神话》、《人件》、《代码的整洁之道》等等经典名著,这将对你的未来大有裨益。如果文笔还不错的话,还可以在各大论坛上写写博客,把读书的心得分享出来,记录自己的同时分享给更多的人。   如果时间还有一点剩余的话,在 GitHub 上开源一些项目吧!比如说,搞个抢票的软件、微信读书组队的小程序。假如这些项目星标超过 5K,那么找工作的时候不能说轻而易举吧,至少能亮瞎面试官的双眼。   坚持
110 0
|
移动开发 安全 关系型数据库
黑客马拉松经验谈:一个周末你能做出有趣、有用的服务吗?
“黑客马拉松”(Hackathon),是黑客 + 马拉松(Hack + Marathon)的组合字,大致上就是几个人聚在一起以马拉松的方式进行一段长时间的 Hack 活动,像是台湾 Yahoo! 办过 Open Hack Day、台湾微软办过 HTML5 或 IE 浏览器的黑客松活动,这类型的活动,实际进行的时间从半天、一天到两天一夜的长度都有。
389 0
黑客马拉松经验谈:一个周末你能做出有趣、有用的服务吗?
|
算法 安全 网络安全
一周前,一群有才华有“钱途”的人做了一件可能改变未来的事
时间回到9月21日,在杭州云栖大会现场的极客区,有一群身着统一黑色T恤的小伙儿不停的在电脑上敲着天书般的代码。从这些小伙儿的外表来看绝对想不到他们每个人都身怀绝技,对算法有独到而深入的研究,更是身负滴滴、爱奇艺、微软、京东、第四范式等知名互联网公司的标签,具有丰富的实战经验,身价不菲。
1569 0
|
程序员
工作一年的程序员,离开编程还有出路吗?
工作一年的程序员,离开编程还有出路吗?
2033 0