美文网首页Java那些事
不要没事挑战i++和++i,比如x = x++;这种货!

不要没事挑战i++和++i,比如x = x++;这种货!

作者: 程序员必修课 | 来源:发表于2017-12-07 23:14 被阅读10次

    先上结论:

    1. x++和x+1不是一回事!
    2. 这个玩意没有优先级!!!
    3. 这种写法,是C标准严格禁止的。和伸手摸电门一样,写这种代码属于做死
    4. 关于这种写法的结果的一切讨论,都是无意义的。

    一段代码引出的纠结

    先看如下一段代码,猜猜看,输出的是什么?

    int a;
    a = 0;
    a = a++;
    System.out.println("a = " + a);
    

    最终输出的是:a = 0

    为什么是0而不是1?

    查看一下字节码,会发现++操作与+1不一样。

    我们平时总是说前缀表达式优先级高,后缀表达式优先级低,其实并不是,这里根本不存在优先级问题,而一定要说的话,++运算符的优先级高于赋值运算符!

    在程序运行a++时,a的值首先是赋值给一个拷贝或者说临时变量(按值传递,底层实现),即temp = a(即temp = a = 0),然后a执行自增运算(运算后a的值为1),最后将这个拷贝(此时拷贝的值为0)作为(a++)整体的值赋值给a(赋值后a的值有重新从1变为0),所以最终的a的值输出为0。

    即a = a++;语句等价于:

    a=(temp=a,a+=1,temp);
    

    或者我们干脆这么理解:

    int b = a++;
    a = b;
    

    那么x = x++;算什么?

    这在c标准里,这种操作称为未定义行为。

    C99中:
    J.2 Undefined behavior
    Between two sequence an object is modified more than once, or is modified and the prior value is read other than to determine the value to be stored (6.5).

    大致意为在两个序列点(;或,)中,同一变量被修改超过一次的做法是未定义行为。
    C99还规定,未定义行为由编译器自行处理,输出什么结果都可以!输出一个Hello World都是符合标准的。

    x++确切的解释

    i = i + i++之类问题,根本不是优先级的问题。

    简单来说,a = b,和数学课本上的等式完全是两个意思。在计算机领域,它的意思是:先计算出表达式b的值,然后把这个值赋给a。

    表达式的定义为:一个单独的字面值,或者一个单独的变量,或者通过算术/逻辑运算甚至函数调用连接起来的表达式——注意,赋值操作不是什么算术/逻辑运算,也不是函数调用。

    显然,对于表达式b来说,它的运算符优先级有多复杂都不是问题。

    但,因为太重要所以需要再说一遍:请注意,表达式里面不允许出现赋值操作,因为这个操作并不是算术/逻辑运算。

    显然,i++的问题在于,虽然i++看起来只有操作符和操作数组合、而且通常作为表达式使用,但其实它的含义是i=i+1——这根本不是一个表达式,而是“计算表达式i+1的值,并将其赋予变量i”:换句话说,这里面额外有一个赋值操作。

    事实上,i++本身作为一个c/c++语句,是不可删除的;而 2+3、a&&b、!a之类真正的表达式构成的单独语句则可以在编译时直接删除。原因就是i++另外还隐含了一个赋值操作,从而多了个会影响程序状态的“副作用”。

    c/c++里面,类似这个赋值操作的、执行后会影响程序状态的行为,被称为“副作用(side effect)”。

    进一步的,c/c++标准里面对这类有表达式外表、但却另有额外语义的“假”表达式叫做“有side effect的表达式”(关于何谓side effect,c/c++标准有专门定义,请尽量参考这个定义,因为我的转述很可能会有某些瑕疵之处,不可轻信),实质上也是强调了它和原始意义上的表达式的不同之处。

    但是呢,为了写代码的便利,c/c++系语言提供了一个语法糖,允许程序员将i++用到表达式里面,同时规定其含义为:首先取i的值,用这个值代入表达式,供以后求值用;之后,执行i=i+1(执行i=i+1的确切时机不限,在表达式求值之前还是之后都行,只要执行了就对)。

    如此一来,忽略副作用不提的话,i++看起来就像是一个真正的表达式。

    但,必须注意,i++毕竟不是一个表达式,它毕竟还有个副作用藏在里面。粗暴的用某种规定允许它掺乎进去,就必然带来很多棘手的问题。

    比如说,i=i++,这个语句如何解释?

    首先,这显然是一个赋值语句,所以最终i应该存的是等号右侧表达式的值;虽然i++不是表达式,但按照规定,它可以解释为“语句执行前i的取值”;所以,这其实是把语句执行前,i的取值赋给i的一个赋值语句——也就是说,执行后,i的值应该不变。

    但,注意i++还有一个赋值动作。即:把语句执行前的i值加一,然后赋值给变量i——所以,执行后,i的值应该增加了1。

    显然,两个赋值动作的执行结果出现了矛盾。究竟哪个对呢?

    进一步的,i=(i++)+(i++)呢?这里面可有三个针对i的赋值操作啊。

    不仅如此,对于函数调用,如max(i++, i++),这又是什么意义呢?

    很显然,不是表达式的i++,绝对不能和表达式混淆。

    虽然,为了表达简洁,c/c++系列语言允许它在特定场合代替表达式,但这并不等于说,c/c++就认为它和表达式没有差别。相反,c/c++自始至终都认为它是一个赋值操作,只是可以在严格限定的场景替代表达式而已——这个“严格限定”,就是“不允许一个变量在一对序列点之间两次改变其值”(不太严谨的说法)。

    只有满足了这个“严格限定”,程序才不会出现“二义”。

    换句话说,i++本身是一个有着特定内涵(对i赋值)的指令,并不是单纯的数学表达式。把它当基本数学表达式滥用,得到的复合表达式是没有数学意义(因而也没有现实意义)的。

    把它用对,是程序员的责任。

    最后给个面试题的例子,看看就好,毕竟真有人考这玩意……

    public class Test {
        public static void main(String[] args) {
            test1();
            test2();
            test3();
            test4();
            test5();
            test6();
            test7();
        }
    
        private static void test1() {
            int a;
            a = 10;
            a = a++;
            System.out.println("test1 a = " + a);//10
        }
    
        private static void test2() {
            int a, b;
            a = 10;
            a = a + a++;
            System.out.println("test2 a = " + a);//20
        }
    
        private static void test3() {
            int a;
            a = 10;
            a = a++ + a;
            System.out.println("test3 a = " + a);//21
        }
    
        private static void test4() {
            int a;
            a = 10;
            a = a++ + ++a;
            System.out.println("test4 a = " + a); //22
        }
    
    
        private static void test5() {
            int a, b;
            a = 10;
            b = a + a++;
            System.out.println("test5 a = " + a);//11
            System.out.println("test5 b = " + b);//20
        }
    
        private static void test6() {
    
            int a, b;
            a = 10;
            b = a++ + a;
            System.out.println("test6 a = " + a);//11
            System.out.println("test6 b = " + b);//21
        }
    
        private static void test7() {
            int a, b;
            a = 10;
            b = a++ + ++a;
            System.out.println("test7 a = " + a);//12
            System.out.println("test7 b = " + b);//22
        }
    }
    

    参考了作者:invalid s在https://www.zhihu.com/question/23180989/answer/23874381中的描述。

    相关文章

      网友评论

        本文标题:不要没事挑战i++和++i,比如x = x++;这种货!

        本文链接:https://www.haomeiwen.com/subject/nslsixtx.html