Chinaunix首页 | 论坛 | 博客
  • 博客访问: 139455
  • 博文数量: 22
  • 博客积分: 1326
  • 博客等级: 中尉
  • 技术积分: 258
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-02 00:52
文章分类
文章存档

2012年(1)

2011年(21)

分类: C/C++

2011-03-09 21:21:17

      赋值表达式也可以包括复合的赋值运算符。例如:
         int a = 12 ;
           a+=a-=a*a
      也是一个赋值表达式。如果a的初值为12,此赋值表达式的求解步骤如下:
           ①先进行“a-=a*a”的运算,它相当于a=a-a*a,a的值为12-144=-132。
           ②再进行“a+=-132”的运算,相当于a=a+(-132),a的值为-132-132=-264。
     ————谭浩强 ,《C程序设计》(第四版),清华大学出版社,2010年6月,p62
 
  首先需要说明的是,这段文字中的“int a=12;”那行是笔者添加的。因为在不交代“a”的定义(变量还是常量?数据类型?)的前提下,那段讨论本身就是毫无意义的。为了把那段错误的文字提升到值得讨论的水平,增加“int a=12;”这个前提条件是必要的。
  这段文字中的错误隐藏的比较深,绝大多数C语言学习者都很难发现其中的问题。为了彻底弄清其中的问题,首先需要建立或澄清几个基本概念。
表达式(Expression)
  什么是表达式?
  简单地说,表达式就是由运算符(operator)与操作数(operand)构成的一个序列。例如:
  2 + 4
  当然,有的表达式只有操作数而没有运算符。例如:
  6
  并非任何由运算符与操作数构成的序列都是合理合法的表达式。例如
  (a=3*5)=4*3
  就是一个具有明显错误的表达式。
  然而运算符与操作数应该按照什么样的规则才能构成一个合理合法的表达式,则不是一两句话能说清的。所以这里暂时就不展开细说了。
  某个表达式可能是另一个表达式的一部分,这时称前者是后者的子表达式(subexpression)。
表达式的效应
  代码中的表达式最多可以有三种效应:要求计算机计算一个值;指明一个对象或函数;产生副效应(side effect)。
  “3 + 5”这个表达式明显是要求计算机求值的。而对于
  int i;
来说,在表达式“i = 5”中的“i”这个子表达式是用来指明名字为“i”的那个int类型的数据对象(object)的。
  所谓“数据对象”,就是指某个数据的存储区间。换句话说,这里的“i”就是指一个特定的存储数据的空间范围及其内容。
“i = 5”与“3 + 5”这两个表达式有一个显著的不同之处,那就是前者有两种效应:求表达式的值(这个值就是5);副效应——“i”对象被存储了一个值5。
  由此可见,C语言中一个表达式可能有多种效应。有的时候我们只用到其中一个效应,也有的时候我们同时用到几个效应,这样可以使代码变得非常简洁。例如在下面的代码段中
  int i ;
  printf( "%d\n" ,  i= 5 );
就用到了“i = 5”这个表达式的两种效应:求“i = 5”的值,i被赋值为5。这种写法显然比
  int i ;
  i = 5 ;
  printf( "%d\n" ,  i);
要简洁漂亮得多。
“a+=a-=a*a”
  了解了表达式的各种效应,就可以分析一下“a+=a-=a*a”这个表达式了。
  这个表达式中有三个运算符:“+=”、“-=”和“*”。其中“*”的优先级最高,“+=”和“-=”优先级相同且都低于“*”的优先级。
  对于存在相同优先级运算符的表达式,要考察这些相同优先级运算符的结合性才能确定表达式想要表达的真正含义。“+=”和“-=”的结合性为从右向左。这样,就可以知道各个运算符的运算对象了。
首先,“*”的优先级最高,因此它的操作数是“a*a”中的“a”和“a”。“a*a”这个子表达式只有一个效应:求值。
  其次,“+=”和“-=”的结合性为从右向左,所以要先考虑确定“-=”的操作数。“-=”是一个二元运算符,因此其运算对象是“a-=a*a”中的前一个“a”和子表达式“a*a”。不难看出“-=”这个运算并非只有一个效应,而是有两个:求值,使“a”所代表的内存中的值变成“a*a”的值。
  最后,可以确定“+=”的操作数是“a+=a-=a*a”中的最左面的“a”和子表达式“a-=a*a”。其效应也是求值和是使“a”所代表的那块内存中的值变成“a-=a*a”这个表达式的值。
  因此,原来的表达式可以等价地写为:
  a += ( a -= ( a * a ) )
其中添加的括号更清楚地表明了各个运算符的操作数。这个表达式所要求的全部效应可以归纳如下:
    求子表达式“a*a”的值;
    求子表达式“a-=a*a”的值;
    求子表达式“a+=a-=a*a”的值;
    使“a”所代表的内存中的值变成子表达式“a*a”的值。
    使“a”所代表的内存中的值变成子表达式“a-=a*a”的值。
  前三项都是求值,后两项是表达式的副效应。
  可以确定的是,计算机一定会先求“a*a”的值,否则无法求子表达式“a-=a*a”的值。还可以确定的是,计算机一定会先求“a-=a*a”的值,否则无法求子表达式“a+=a-=a*a”的值。
  但是无法确定的是副效应在何时完成——C语言并没有规定后两项副效应发生的时间。C语言只要求这两个副效应在下一个序点(sequence point)之前完成。序点的概念这里暂时不给出精确的定义。简单地说,如果这个表达式构成了一个表达式语句:
  a += ( a -= ( a * a ) ) ;
  那么“;”就是一个序点。其含义是前面的运算动作(包括副效应)到这个“;”这里必须全部完成。
  这样,这两个副效应产生的时间,如果因为不同编译器的不同安排,就会产生无论是整个表达式的值,还是变量“a”最终的值,在不同的编译方式下产生各不相同的互相矛盾的结果。然而每一种编译“安排”,只要是在“;”之前完成了副效应,却都没有违背C语言的要求。
  这也就是说,在不违背C语言原则的情况下,表达式“ a += ( a -= ( a * a ) )”可以有多种解释方式,得到并不唯一的结果。这叫做“二义性”或“多义性”。
  程序设计语言和程序是不可以有二义性的,这一点和我们平时使用的自然语言截然不同。
为此,C语言特别规定:在两个相临的序点之间,同一个数据对象中保存的值最多只可以通过表达式求值改变一次。表达式“a += ( a -= ( a * a ) )”的内部并没有序点,因此必定在其前面和后面拥有这“两个相临的序点”。然而在“a += ( a -= ( a * a ) )”中,“+=”、“-=”运算的副效应又都是改变同一个数据对象“a”的值,这明显违反了“同一个数据对象中保存的值最多只可以通过表达式求值改变一次”这个C语言对表达式的基本要求,因此这个表达式是一个错误的表达式。
  但是这种错误不同于语法错误(违背C语言硬性限制(constraint)的错误),编译器会承认这样的表达式不违背C语言的限制(constraint),因而可以进行编译,并且可能不会把这个错误作为语法错误报告给Coder。最多,好的编译器可能会给出一个“警告”——它嗅出了这里疑似有错。
  然而,如果你在代码中写出了“a += ( a -= ( a * a ))”这样的表达式,尽管编译器可以继续编译,但是严重的问题却在于,你自己都不知道你写出的是要达到什么样效果的表达式,C语言和编译器同样也都不知道,编译器会按照它自己的“理解”擅自胡乱编译一通。C语言把这样代码的行为叫做“未定义行为”(undefined behavior)。未定义行为就是一种错误行为。只有初学者才会误以为编译通过就是代码正确。
      举个更通俗的例子,“反对误人子弟的书”,这话在语法上是说得通的,但是却有歧义。因为可以把它理解为“反对——误人子弟的书”,也可以理解为“反对误人子弟——的书”。在汉语中这叫病句,在C语言中也有类似的现象。C语言并没有规定“反对误人子弟的书”究竟是“反对——误人子弟的书”的意思还是“反对误人子弟——的书”的意思,而把这种现象归为“未定义行为”。
 
  令人扼腕的是,这样一只巨大无比的BUG竟然以教科书的面目误人子弟已经长达二十年了。今天,也许已经到了必须埋葬它的时候了。
  为此,特撰此文,提前致悼。非为悼念,是祈悼忘。
 
          
阅读(3733) | 评论(12) | 转发(1) |
给主人留下些什么吧!~~

幻の上帝2011-08-20 14:22:20

Demon--Hunter: 个人浅见 “a += ( a -= ( a * a ))”严格来说是 未指定行为(unspecified behavior),虽然“a += ( a -= ( a * a ))”结果跟编译器相关,但可能出现的结果
是可以.....
unspecified behavior和undefined behavior的区别在于,前者可以是对的,后者只要产生就是错的(除非完全无视可移植性,不过那就不是C语言了,只能是一种方言)。具体的用法引起unspecified behavior还是undefined behavior有明确规定,要点可以参考ISO C99 Annex J(informal)。
“可以预见”只是从一方面解释标准为什么要这么规定而已。从标准委员会的立场看,可能某些用法无论怎么明确或者把它当作unspecified实在太愚蠢了,所以直接规定undefined,明示用户不要使用。

Demon--Hunter2011-08-18 23:58:12

个人浅见 “a += ( a -= ( a * a ))”严格来说是 未指定行为(unspecified behavior),虽然“a += ( a -= ( a * a ))”结果跟编译器相关,但可能出现的结果
是可以预见的。

pmerofc2011-07-17 12:13:17

下面这个表达式的行为也是未定义的。
                    x = f() + g();
=======================
这个一般来说是unspecified行为(这很普通,几乎在任何代码中都有)
只要结果不依赖于计算次序
那就是对的
如果结果依赖与计算次序
那么通常就是undefined行为
是错的

pmerofc2011-07-17 12:07:59

过奖
C语言的定义不是很严格,不敢苟同。
我的看法是,C的定义应该说是严格的,只是许多东西不定义而已,程序员应该避免涉及这些没有定义的东西,否则就是一种错误

xparmenides2011-07-16 12:48:29

看得出你对C语言规范研究很深,C语言的定义不是很严格,所以尽量避免具有“未定义行为”的表达式可以避免很多程序中的错误。但是,C语言也是一种灵活的语言,所以过于苛责严格的写法就掩盖了其特点。如果严格要求的话,下面这个表达式的行为也是未定义的。
                    x = f() + g();
但是我们平常还是能够接受这种写法的,只不过心里有一个意识,即这里可能会有副作用,遇到问题再具体处理。语言的规范不能解决所有的实际问题。

C语言最初就是为书写系统程序而设计的,所以其中的一些概念(例如强制类型转换、指针等)从程序设计语言的角度看是很不严格的,但的确为操作系统这类程序的开发带来了很大便利。不同语言有它们自身的特点以适应不同的开发需要。如果开发应用程序,那么Java的比C要规范的多,写出来的程序逻辑上不容易产生歧义。

这些问题是由C语言本身造成的