前自增和后自增运算符问题:

in 积露为波 with 1 comment

今天遇到了一个问题:

int a=0,c=0;
c = (a++)+a;

你觉的c会是什么值?

1908sankmca.png

我的第一反应就是c=0,而结果却是c=1;

之前一直错误地认为++a优先级最高,是最先进行的运算;a++优先级最低,是最后才进行的计算。虽然原来看到书上写的a++和++a同属于较高运算级,但毕竟与自己平时的感觉不太一样,没有特别注意。尤其是遇到 c= (a++)+b 和 c= (++a)+b这类问题时,他们的计算结果与自己预期一致,而这更是加深了这种错误认识。

直到遇到了开头提到的问题,我才开始正式审视这个问题。

多方查证得知a++和++a在单独使用时几乎没有差别。

但是在作为操作数时,a++是将加加之前的值作为操作数,而++a相反,它是将加加之后的值作为操作数。

上述c = (a++)+a,其实相当于tmp = a;a=a+1; c=tmp+a;

而另一个c = (a++)+a,相当于a=a+1; c=a+a;

引用一段在其他地方看到的解释:

假设存在a=1,那么“b=(a++)+a;”将如何计算结果呢?按照正文所述,顺序应该是,1)计算b,2)计算a++(假设值为c)(操作数A),3)计算a(操作数B),4)计算c+a,5)将c+a的结果赋值给b。按照“++”的定义,第2)步中a++的结果依然是1,即c为1,随后a立即增1,因此在执行第3)步时,a的值已经是2。所以b的结果为3。很多初学者会误认为a增1的操作是在表达式计算完毕后执行的。

经过实践,上述表诉是正确的,至少它在C和Java中是对的。

C中的验证

验证的方法很简单,我们知道C是一门高级语言,计算机无法理解,需要用编译器将它编译成汇编语言、机器语言,计算机才能理解。而汇编语言是符号化的机器语言,用一个符号(英文单词、数字)来代表一条机器指令,命令简单,可读性比机器语言好,而且一条指令只做一件事。

所以,我们可以看看编译后的汇编语言是什么样的。再复杂的结构在汇编语言里都会被分解得很小、很细,++a和a++自然也不是问题。

源代码:

c = (a++)+a;
c = (++a)+a;

编译后:

//x86_64 gcc 9.1
mov     eax, DWORD PTR [rbp-4]
lea     edx, [rax+1]
mov     DWORD PTR [rbp-4], edx
mov     edx, DWORD PTR [rbp-4]
add     eax, edx
mov     DWORD PTR [rbp-8], eax
--- 分割线 ---
add     DWORD PTR [rbp-4], 1
mov     eax, DWORD PTR [rbp-4]
add     eax, eax
mov     DWORD PTR [rbp-8], eax

编译后的指令是x86_64指令集,具体是什么我们也不用去管它。当然,如果你能看懂的话还是可以自己仔细分析比较好,有助于加深理解。

a++: add eax, edx

++a: add eax, eax

二者的不同在这里可以看出来,a++作为操作数时它的值为加加之前的值,++a则是加加之后的值。

Java中的验证

不同于C, Java作为一种跨平台语言选择用字节码作为中间媒介。对于它,可以用JDK自带的javap.exe反编译工具进行处理。

源代码:

public class A{
    public static void main(String[] args) {
        
    }
    public void a() {
        int a = 0;
        int c = (a++) + a;
        System.out.println(c);
    }
    public void b() {
        int a = 0;
        int c = (++a) + a;
        System.out.println(c);
    }
}

反编译后的字节码:

public void a();
    Code:
       0: iconst_0
       1: istore_1
       
       2: iload_1
       3: iinc          1, 1
       6: iload_1
       7: iadd
       8: istore_2
       
       9: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;     
      12: iload_2
      13: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      16: return
public void b();
    Code:
       0: iconst_0
       1: istore_1
       
       2: iinc          1, 1
       5: iload_1
       6: iload_1
       7: iadd
       8: istore_2
       
       9: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;     
      12: iload_2
      13: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      16: return

上面是反编译后的字节码,没有学习过jvm指令也没有关系,我们只需要将关注点放到两个函数的不同点上。

这几个指令我们需要简单了解下:

指令作用
iconst_0int型常量值1进栈
istore_1将栈顶int型数值存入第二个局部变量,从0开始计数
iload_1第二个int型局部变量进栈,从0开始计数
iinc 1, 1指定int型变量增加指定值
iadd栈顶两int型数值相加,并且结果进栈

对于函数a中的a++,int c = (a++) + a;

2: iload_1//将a的值入栈,此时a还是原来的值
3: iinc          1, 1//将a加1,并将结果写入a
6: iload_1//将a(第二个操作数)的值入栈,此时a已改变
7: iadd//将栈顶两值相加,并且结果进栈
8: istore_2//将栈顶存放的结果写入c

对于函数a中的++a,int c = (++a) + a;

2: iinc          1, 1//将a加1,并将结果写入a
5: iload_1//将a的值入栈,此时a已改变
6: iload_1//再次将a(第二个操作数)的值入栈,此时a已改变
7: iadd//将栈顶两值相加,并且结果进栈
8: istore_2//将栈顶存放的结果写入c

a++和++a的区别是:

单独的a++;和++a;没有区别他们都被编译成 iinc 1, 1(可以理解为a=a+1);

当二者涉及到其他运算符时他们的区别才体现出来,a++是将加加之前的值作为操作数入栈,++a是将加加之后的值作为操作数入栈。

Responses
  1. Cer

    基本都一样.

    Reply