这真的是很无聊的问题啊。
引用一段<狂人C>里的话来回答你的问题,希望对你有所帮助。
在现实的运用中,这个两个“++”运算符还是会经常让我们感到头痛,甚至很多成熟的编程团队会将其定义为尽量避免使用的运算符。那么,这个谜一样的运算符,为什么会成为不少程序员极力回避的禁忌呢?
我们首先来看看下面这个简单的代码片段。
i = 3;
j = ++ i + i;
按照前面我们讲过的原理,在第二行的表达式里,CPU读取第一个i值(求++i的值)之前,需要完成将i赋值为i+1这个副效应。但问题在于,在前后两个序点之间,CPU需要两次读取i的值,我们并不清楚会先读哪个i,这个次序选择权在于编译器。我们根本无法控制。也就是说,上面那句话可能表示两种不同的运算语意,产生两种完全不同的运算结果。
语意一
完成副效应i=i+1(i的值变为4)
读第一个i值(此时赋值的副效应已经完成,i的值为4)
读第二个i值(i值已经变为了4,这个i值自然也不例外)
两次读得的i值相加,把结果写入j内存(结果即是8)
语意二
读第二个i值(此时i值为3)
完成副效应i=i+1(i的值变为4)
读第一个i值(此时赋值的副效应已经完成,i的值为4,于是出现了第一个i值和第二个i值之间的值并不相等的现象)
两次读得的i值相加,把结果写入j内存(结果竟然是7)
由于C语言并未明确规定这些运算的次序,因此在完全符合C语言语法规则的前提下,竟然能得到两种结果,这就是所谓的“二义性”。
编写程序时候是不可能容忍代码存在这种“二义性”的,否则程序很可能就变成了“鸡同鸭讲”。代码必须具备唯一确定的语意。
除了涉及到序点,C语言没有规定编译器在这种情况下应该究竟选择哪种语意,这样,表达式 j = ++ i + i就成了一种未定义行为。如同前面曾经提到的那样,这种未定义行为尽管不违背C语言的语法规则,但本质上却是一种错误的代码。
以这里讨论的表达式为例,在求“+”运算符右边的i值的时候,从C语言或代码的角度来说,并不能确定i在内存中确切的值。因为在求“+”左面的操作数——表达式“++i”的值的时候可能改变i的值。由于没有规定求“++i”的值和求“+”右边的i值这两个动作之间的次序,于是求表达式“++ i + i”的值就成了一个未定义的行为。
未定义的行为出现在代码中,就是一个“语病”。只不过这里我们说的“语病”不是那种不符合语法要求的语病,而是那种语法上符合要求,但在语言或代码层面却无法确定其唯一含义的语病。比如,有一个大家很熟悉的广告词――“xx皮鞋,足以自豪的皮鞋”,语法上这句话绝对没有问题,但那个“足”字显然是一语双关的,作为广告语这很好,但编程不是做广告,计算机也不会听你忽悠,它只接受具体明确的、不带有“二义性”的指令。而代码中没有语法错误的“二义性”会导致编译器为你“胡乱”选择一种语意。这当然是不可接受的。
根据程序运算结果揣测j = ++ i + i这样未定义行为没有确定含义的表达式的含义是肤浅幼稚的。因为未定义行为不但是不可能预测的,同样也不可以逆向推测。它产生什么样的后果都不奇怪,哪怕让机器死机,关闭电源甚至火山爆发。C语言的学习者之间经常会出现很多类似这样的可笑对话:一个学习者问,为什么这个计算机(编译器)说“足以自豪的皮鞋”里面的“足”字是“脚”的意思,而不是“足够”的意思?另一个学习者立刻反驳,不对!我的计算机(编译器)明明说“足”是足够的意思嘛!
这两个不明就里的学习者也许会争论上好一阵子,却也得不出一个所以然来。本书的读者对此应该有个清晰的认识,能够很轻松地告诉他们代码“二义性”的来龙去脉。
下面,列出了一些C语言中典型的“二义性”例子。
int i = 3,j;
j = (++i)+(++i)+(++i);
(i++)+ (i++)+(i++)
i = i++
printf(“%d %d\n”, i , i++ );
p=(++p>0)?(p++) :p++);
j = (i = 4) + (i = 5) ;
执行 int k = 11 ; k = 1/3*k++;后,k的值是____。
a += a -= a * a
这些例子,都会让编译器陷入那个“足”是脚还是足够的疑惑。写出这种表达式的人,说明其对于运算符的真实含义还是缺乏了解。可惜的是,在现在国内很多专业的C语言论坛中,还是会有不少程序员,在这个问题上疑惑不解。
这些人往往都还有另一个误区,这个误区就是把优先级和结合性与运算次序相混淆,他们难以理解为什么优先级高的反而后计算。比如下面的表达式:
j + i ++
在这个表达式中,“++”的优先级最高,但这个运算却不是最先进行的。这里的优先级只是决定了“++”这个运算符的运算对象是i,而不是“j+i”,即:
j + (i ++ )
也就是说这个表达式的意义是计算“j+ i”的值,再加上一个副效应。而这个副效应发生的时间,我们只知道会是在编译器求完i值之后,但我们无法知道会是发生在计算“j+i”值的之前还是之后。
然而,不少人把优先级理解成了小学里的“先乘除后加减”,这是完全的误解。这里需要再次强调的是,优先级和运算次序完全是两回事!
对于初学者来说,另外一个错误不得不提。就是,++或--(无论前缀或后缀),只能用于左值。比如,int i; “++i”是可以的,因为i是左值;但++(i+1)是一个语法错误。因为(i+1)只有值的含义不可能表示一块连续的具有类型含义的内存(左值),因此(i+1)只是一个右值表达式。在目前这个学习阶段,只有变量名这种初级表达式是左值表达式。
4. 总结
好了,现在我们完全搞清了“++”运算符的来龙去脉。那么,在代码中应该如何避免上面所提到的“二义性”问题呢?
首先,我们需要把握一个原则,即不在两个序点之间更改同一个变量(严格的术语是对象)两次或更多次(a += a -= a * a就是违背了这种原则的错误代码)。如果两个序点之间只写一次同一对象的值,但同时还存在着读这个对象值的情况,那么必须确保写这个对象的值发生在读这个对象值之后。所以,表达式 i = i + 1 的行为是确定的,而表达式++ i + i则属于未定义的行为。
其次,尽量少使用可能引发“二义性”的复杂表达式。熟练的程序员在使用“++”这类运算符时是极其审慎的,在利用“++”的副效应时,一定要确保不会发生出乎自己意料之外的结果。
或许有人会问,这么麻烦干什么,直接取消可恶的“副效应”不久可以了吗?然而,“副效应”真的那么可恶吗?是否取消了副效应就可以一了百了了呢?其实不是的。
副效应不一定是什么坏事。比如前面例子中for语句中的“++”就是利用了其将i值加1的副效应使得代码写得非常简洁,而求得的i值本身倒是没有什么用处的。
而且,没有副效应的表达式语句,在编译器看来是可以不理睬的废话。比如:
2 + 4 ;
这句话,几乎所有的编译器都不会执行。我们最常用的printf()函数,其实多数情况下使用的是它的副效应,而函数调用得到的值几乎很少被用到。编译器对这样有副效应的表达式语句不可能置之不理。
因此,副效应是非常有用的,有时候甚至是必须的。作为一个合格的程序员,应该善于使用副效应。但是在涉及到改变变量在内存中的值的表达式中,一定要慎重,否则就会像前文中那些例子一样,画虎不成反成犬。