顯微鏡下的 i++ 與 ++i

注意,以下討論的語言是 Java

這個問題被網上的好多文章寫爛了,但基本重複度很高,我看過後的感覺是,大部分都是錯誤的、誤導讀者的。

隨便百度一下,我們打開第一條。

上來先說個結論

i++ 先賦值在運算,例如 a=i++,先賦值a=i,後運算i=i+1,所以結果是a==1
++i 先運算在賦值,例如 a=++i,先運算i=i+1,後賦值a=i,所以結果是a==2

然後給了成噸的例子來說明


    
    
    
public  class Test3 {
  public static void main(String[] args) {
   int y= 0
   //注意"="是賦值,"=="纔是相等
   //這裏的y=++y 是先運算在賦值
  y=++y; // y==0,++y==y+1; 結果y=++y == y+1 == 0+1 ==1
  y=++y; // y==1,++y==y+1; 結果y=++y == y+1 == 1+1 ==2
  y=++y; // y==2,++y==y+1; 結果y=++y == y+1 == 2+1 ==3
  y=++y; // y==3,++y==y+1; 結果y=++y == y+1 == 3+1 ==4
  y=++y; // y==4,++y==y+1; 結果y=++y == y+1 == 4+1 ==5
  System.out.println( "y="+y); //5
   int i = 0;
   // i==0,i++==0; 結果i=i++ == (記住先賦值後運算)
  i=i++;
  i=i++;
  i=i++;
  i=i++;
  i=i++;
  System.out.println( "i="+i); //0
  System.out.println( "================"); //1
 }
}

首先這個例子沒有任何代表性

其次得出的結論也是極其誤導人的;

但最關鍵的是,這無法幫助你真正理解 i++ 和 ++i 的本質是什麼

所以 i++ 和 ++i 的區別請聽我說

先忘掉什麼“先賦值、後運算”

彆着急,慢慢來,忍住看到最後

i++ 和 ++i 字節碼

查看字節碼用 javap 命令,或者直接用 idea 的插件,這裏不做過多介紹

在某方法裏寫上這樣一段代碼

public void ipp() {
    int i = 1;
    
i++;
}

查看其字節碼

iconst_1
istore_1
iinc 1 1
return

然後我們在寫上這樣一段代碼

public void ipp() {
    int i = 1;
    
++i;
}

查看其字節碼

iconst_1
istore_1
iinc 1 1
return

發現沒,完全一樣。也就是說,在沒有賦值操作時,i++ ++i 編譯成字節碼後,都是

iinc 1 1

完全一樣

有多少人之前的理解是 i++ 和 ++i 本身孤零零地放在那是有區別的呢?

看 iinc 字節碼的定義

找到 JVM 官方手冊

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.iinc

看到 iinc 字節碼指令的格式爲

iinc index const

index 表示局部變量表的索引,const 表示將其值加上多少

所以上面的

iinc 1 1

就表示

將局部變量表索引爲1位置的值,加上1

局部變量表索引 0 位置處是 this,索引 1 位置處的值,是int i = 1 這段代碼設置的,也就是 1。把這個值加 1,就變成了了 2

再來回顧下上面的代碼

public void ipp() {
    int i = 1;
    i++;
    System.out.println(i);
}

public void ppi() {
    int i = 1;
    ++i;
    System.out.println(i);
}

如果打印 i 的值,很容易知道,兩個都是 2

所以很簡單,i++ 和 ++i 本身在字節碼指令中的體現都是 iinc,就是單純把 i 所在的局部變量表那個位置的值,+1

稍稍複雜一點

我們把上面的代碼稍稍複雜一點,++操作後,再重新賦值給 i

public void ipp() {
    int i = 1;
    i = i++;
}

public void ppi() {
    int i = 1;
    i = ++i;
}

你猜 i 的值分別是多少

別急,再次查看字節碼

void ipp() --> i = i++;

iconst_1
istore_1
iload_1
iinc 1 1

istore_1
return

void ppi() --> i = ++i;

iconst_1
istore_1
iinc 1 1
iload_1

istore_1
return

這回看到不一樣了,但字節碼指令都相同,只是順序不同

i = i++ 就是 先 iload_1 iinc 1 1

i = ++i 就是 先 iinc 1 1 iload_1

所以也很簡單,i++ 和 ++i 只有在最終賦值給某變量時(實際上是因爲參與了運算,因爲直接賦值也是一種無運算符號的運算),字節碼指令是不同的,而且也只是順序上的不同。

那順序的不同會導致結果怎樣呢?下面我們通過觀察 虛擬機棧 來細化整個過程

觀察虛擬機棧中的變化

但你得先知道虛擬機棧是什麼,也就得知道 JVM 的內存劃分,這塊就不幫你複習了哈,直接上 ipp() 方法入虛擬機棧後,這個方法棧幀裏的初始構造

看 i = i++

下面一步步執行 ipp() 方法的字節碼

iconst_1
istore_1
iload_1
iinc 1 1
istore_1
return

注意局部變量表 0 處表示的是 this,這裏爲了簡化沒有寫出,然後棧幀的“幀”字寫錯啦,我就任性一下不改了哈
iconst_1:將立即數 1 壓棧

istore_1:操作數棧頂 -> 局部變量表 1 位置
iload_1:局部變量表 1 位置 -> 操作數棧頂

iinc 1 1:局部變量表 1 位置的值 +1

istore_1:操作數棧頂 -> 局部變量表 1 位置

所以,最後 i 的值,也就是局部變量表中 1 位置處的值,就是 1

我們用動畫再演示一遍

你可以感受到,i = i++ 這種寫法, iinc 1 1 這一步是完全沒有用的,因爲最後局部變量表 1 位置處的值,在最後一步賦值操作時,會被操作數棧頂處的值覆蓋,所以之前的 +1 完全沒用

所以 idea 也會提示你,這裏的 i++ 沒用

the value changed at 'i++' is never used

再看 i = ++i

相信這個你自己也可以推到出來了
iconst_1
istore_1
iinc 1 1
iload_1
istore_1
return
  • iconst_1:將立即數 1 壓棧
  • istore_1:操作棧頂 -> 局部變量表 1 位置
  • iinc 1 1:局部變量表 1 位置的值 +1
  • iload_1:局部變量表 1 位置 -> 操作棧頂
  • istore_1:操作棧頂 -> 局部變量表 1 位置

所以,最後 i 的值,也就是局部變量表中 1 位置處的值,就是 2

我們直接用動畫演示一遍

本質區別

所以看出本質區別是什麼了麼?

區別就是

是 "先把局部變量表中的值 +1,再放到操作數棧中"

還是 "先放到操作數棧中,再把局部變量表中的值 +1"

僅此而已

所以網上普遍的說法,i++ 表示 先 賦值運算

  • 賦值就是 壓入操作數棧頂
  • 運算就是 局部變量表 +1 操作

反正這倆詞我是對應不上...

還有的說法是,i++ 是先把 i 拿出來使用,然後再+1

還有的說法是,i++ 先賦值在自增

還有的 ... ...

哥誒,咱別用自己造的詞誤導讀者了好不?

所以最後用我的話總結一個沒有任何歧義的

  • i++:先將局部變量表中的 i 放入操作數棧中,再將局部變量表中的 i 值 +1
  • ++i:先局部變量表中的 i 值 +1,再將i放入操作數棧中

來點難的

當你從這個角度理解了之後,再做類似的複雜一點的題,也不在話下,大不了在腦子裏從頭推導一遍即可

看題

int i = 2;
int y = i++ + ++i;
y = ?

int a = 2;
a = a++ + ++a;
a = ?

int b = 2;
b = b++ + (++b + ++b) + (b += 2);
b = ?



答案

y = 6

a = 6

b = 18

你做對了麼?

我把最難的那個題的字節碼展示出來

按照黃色的字可以到操作數棧的變化(從左到右就是操作數棧從棧底到棧頂),自己腦補一下動畫吧,不想做了有點懶哈哈哈~


    
    
    
int b = 2;
b = b++ + (++b + ++b) + (b += 2);

iconst_2    ;操作數棧  2
istore_1    ;局部變量表 b=2
iload_1     ;操作數棧  2
iinc 1 by 1 ;局部變量表 b=3
iinc 1 by 1 ;局部變量表 b=4
iload_1     ;操作數棧  2 4
iinc 1 by 1 ;局部變量表 b=5
iload_1     ;操作數棧  2 4 5
iadd        ;操作數棧  2 9 (=4+5)
iadd        ;操作數棧  11 (=2+9)
iinc 1 by 2 ;局部變量表 b=7
iload_1     ;操作數棧  11 7
iadd        ;操作數棧  18(=11+7)
istore_1    ;局部變量表 b=18

再難的,我覺得就有些無聊了,大家自己給自己出題吧~

如果你對這裏的入棧順序有困惑,比如你感覺加了()數學上不是先進行運算麼?怎麼不是先入棧參與運算呢?

那其實這和 i++ 與 ++i 的知識就不相關了,你需要了解的是 前綴、中綴、後綴表達式,這裏只舉個例子不展開講解。

簡單說就是如何將數學表達式,轉換成一種格式,按照這個順序可以方便通過棧來實現計算

比如

b++ + (++b + ++b) + (b += 2)

在轉成後綴表達式過程中 ++ 操作根本不受影響,先簡化成

b + (b + b) + b

轉換成後綴表達式後就是

b b b + + b +

照着這個順序壓棧,就是字節碼中指令的順序啦,比如 b 壓棧就是 iload_1,運算符(+)壓棧就是 iadd再回過去證明一下哦~

而這裏的每一個 b 的值,就是壓棧那一時刻的 b 的值

最後憤怒地再說兩句

所以,網上關於 ++ 的題目,其實是兩個知識點

  • i++ 與 ++i 參與運算時的字節碼指令

  • 將數學表達式轉換爲棧操作的後綴表達式

而網上的講解,大部分都不是從最直接的字節碼指令說,還將兩個知識點混爲一談,我覺得是不負責任的。

回過頭來再看開頭說的那篇文章的結論

i++ 先賦值在運算,例如 a=i++,先賦值a=i,後運算i=i+1,所以結果是a==1
++i 先運算在賦值,例如 a=++i,先運算i=i+1,後賦值a=i,所以結果是a==2
先不說它沒有用字節碼來說明問題,你有沒有發現這說的本身就是錯的,有很大的誤導性。
先賦值a=i,後運算i=i+1
其實根本沒有先賦值吧,只是把 i 丟到操作數棧中等待被運算而已,然後局部變量表 i=i+1,最後操作數棧中的 i 出棧並寫入局部變量表中 a 的位置,這時才叫賦值。
總之,這類文章還是少看爲好


本文分享自微信公衆號 - 五分鐘學算法(CXYxiaowu)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章