【C++再学习】【07】自增自减操作符背后的秘密

来源:互联网 发布:淘宝试用成功后要钱吗 编辑:程序博客网 时间:2024/05/09 10:30

首先来看一个问题:

Y=i++;

Y=++i;

        上边两行语句我们要讨论的问题就是Y是否一样,可能大家都知道结果,但是我还是要说说这其中的道理,因为i++和++i都是表达式,表达式都是有值的,而前增量和后增量的结果是不同的。++i是先去做i=i+1,然后再把i作为表达式的结果;而i++是先把i作为表达式的结果,然后再去做i=i+1。当两种情况作为单独的完整表达式时,效果是一样没什么区别,但是如果两者作为子表达式的值参与进一步运算时,两者的执行速度可能有细微的差别,对于内置类型和当代编译器而言,这看似不是什么问题。然而C++允许程序员针对类定义这些操作符。对于后缀版本首先复制一个拷贝,将其加1,然后将复制的拷贝返回。因此,前缀版本的效率比后缀版本的效率要高。

        问题就这么简单吗?我们来继续挖掘一下。

 

        从最基本的说起,语言必不可少的一样东西是表达式,通过表达式我们才能完成我们想要的运算,到底什么是表达式,用运算符将运算对象连接形成的式子就是表达式,表达式有一些基本的构成规则:

 l ) 单个的常量、变量、函数调用都是表达式;
2 ) “前缀单目运算符 表达式”是表达式;
3 ) “表达式 后缀单目运算符”是表达式;
4 ) “表达式 双目运算符 表达式”是表达式;
5 ) “表达式 ? 表达式 : 表达式”是表达式;
6 ) 有限次使用上述规则获得的运算式也是表达式。

        表达式主要分为:算术表达式、关系表达式、逻辑表达式、条件表达式、赋值表达式和逗号表达式。我们接下来主要讨论算术表达式和赋值表达式。

X+Y;2、X*10;3、X++;4、++Y;5、Y+=10

        上边是几个表达式的例子,表达式也是可以分类的,根据我们的讨论需要,我们从有无副作用角度分为:有副作用表达式和无副作用表达式,表达式的本质就是变量或者常量间通过运算符进行操作,在这个过程中,我们一方面关注表达式所确定的计算过程,另一方面我们还要关注表达式对环境的影响。这里所说的对环境的影响就是表达式的副作用,它的本质就是表达式在计算的过程中要改变表达式中作为操作数的某个变量的值。那么很自然的我们想知道为什么会产生副作用呢?原因很简单,就是因为那几个特殊具有副作用的操作符:赋值操作符(=)、复合赋值操作符(+=,*=,/=,%=,!=,&=等等)、自增自减操作符(前++,后++,前--,后--)。

        好了,了解上边副作用操作之后,来看下一个问题,如果程序里一条语句通过表达式的副作用修改了一个变量的值,那什么时候从该变量能够取到修改后的新值?有人可能觉得这不是什么问题,会认为修改完了再去取值就是新的值了,那么看看下边这条语句你就知道了这还是很有问题的。

int i =0;

i = ++i + 1;

        我们要讨论的核心问题就是如果程序语句中的表达式(或者子表达式)有副作用,那么这种副作用什么时候能真正体现到相关变量中。比如说上边例子中的++操作符对i的副作用会不会在=操作符之前起作用呢?

         下边再来一个概念(貌似一会说了好多概念),顺序点:一个顺序点(sequence point)是程序执行中的一点,在该点处,所有的副作用都在进入下一步前被计算。也就是说,在顺序点那里可以确定某些动作已经完成,且其他动作尚未开始,相当于是一个承前启后的点。所以当程序执行到一个顺序点时,在此之前发生的所用副作用都必须实现,而在此之后的都还没实现,而在两个顺序点之间的程序是无法保证副作用一定实现。

        那C++都有哪些顺序点呢?

1)分号;

2)未重载的逗号运算符的左操作数赋值之后(即','处);

3)未重载的'||'运算符的左操作数赋值之后(即'||'处);

4)未重载的'&&'运算符的左操作数赋值之后(即"&&"处);

5)三元运算符'? : '的左操作数赋值之后(即'?'处);

6)在函数所有参数赋值之后但在函数第一条语句执行之前;

7)在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前;

8)每个基类和成员初始化之后;

9)在每一个完整的变量声明处有一个顺序点,例如int i, j;中逗号和分号处分别有一个顺序点;

10)for循环控制条件中的两个分号处各有一个顺序点。

        下边是一个规定,在两个顺序点之间,子表达式求值和副作用实现是不完全一致的,如果程序的结果与求值以及副作用实现顺序相关,那这样的程序将会产生不确定的行为(unspecified behavior)。此外,假如在此期间对一个内建类型执行一次以上的写操作也会产生未定义的行为。知道这个之后,我们看看下边若干例子。

 int i=0;

 int m=(++i)+(++i)+(++i)+(++i); //在两个分号之间有5个副作用,这5个副作用与子表达式的求值顺序是未定义的,对于不同的编译器会得出不同的结果。

 n = n++;          //两个序列点(分号)之间两次修改n
 a[i] = i++; //求值顺序是不确定的

 n = f1() + f2() + f3();  //f1,f2,f3谁先被调用,谁后被调用,这是不一定的,C语言标准没有对此作规定。

 int i = 7;  

         printf("%d\n", i++ * i++);  //第一个自增操作和第二个自增操作以及乘法操作的顺序是不一定的,所以结果根本无法确定

int i = 1;
i++, (i == 2);  //最后的值就是1,因为逗号表达式的前半部分i++的副作用(i自增1)在逗号之前已经生成,所以当执行到(i == 2)的时候,i的值已经是2了,所以i == 2成立,(i == 2)的值便作为整个逗号表达式的值。

        y = (4 + x++) + (6 + x++); //表达式 4 + x++ 不是一个完整表 达式。因此,C++不保证x的值在计算子表达式 4 + x++ 后立刻增加1。在 这个语句中,整条赋值语句时一个完整表达式,而分号标示了顺序点,因此 C++只保证程序执行到下一条语句之前,x的值将被递增两次。C++没有规定 是在计算每个子表达式之后将x的值递增,还是在整个表达式计算完毕后才 将x的值递增。

      总结一下,通过上边的了解我们发现,以前我们讨论的很多问题可能会变得毫无意义,因为C++规定了那些问题就是不确定的,所以在以后编写程序的过程中,我们要尽量避免那些模棱两可的不怎么友好的代码,提高程序可阅读性的同时保证程序的健壮性。

0 0
原创粉丝点击