1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | C/C++ 快問快答: 令 double x = (double)(-9223372036854775808LL); 試問 x < 0.0 結果為何? (假設 long long 是 64-bit 的二補數, double 是 IEEE-754 的倍精確度浮點數) 解答: 其實有一個問題問到重點了: 這個東西確實是想要直接寫出 std::numeric_limit<long long>::min 的值 (也就是 - 2^63), 但是這裡發生了許多事使得最後這個結果可以是 (1) Compile Error (2) true (3) false 三種答案。 首先,一個很多人不知道的細節是:** C/C++語言裡沒有負的 literal! ** 所有我們寫成 -1, -2, -689, -9.2 的東西,對編譯器來說都是一個負號跟著一個正 literal, 然後編譯時再以 constant folding 轉化成負的常數存進程式裡的。 我們都知道,整數 literal 的型態當數字後面沒有附記號時是 signed int,有 U 變成 unsigned,有 L 變成 long, 如果是 LL 那就變成 long long,因此上面這東西確實是想要寫出一個 long long 常數的。 但是問題來了:剛才說過 C/C++ 沒有負 literal, 所以上面這實際上是負號跟著 9223372036854775808LL 這個常數, 只是 long long 並不能表示 9223372036854775808 這個數。 (它比 std::numeric_limit<long long>::max 還大 1) 一般來說碰到 literal 型態裝不下這個 literal 時,標準規定要往上找能裝得下的型態來裝, signed / unsigned 不變,int 往上可以找 long 跟 long long,long 往上可找 long long,long long 就沒了。 (除非平台有支援更高等級的整數型態,例如 __int128 之類的那就可以再往上,但不是必要) 例如直接寫 3456789012,它比 2^31-1 = 2147483647 還大, 在 int / long 都是 32-bit 的平台上它會自動變成 long long 型態的 literal。 說到這裡就可以解釋其中一個答案了: 當編譯器發現 long long 裝不下 9223372036854775808 的時候, 由於再上面沒有型態能裝了,編譯器可以直接報錯。 那編譯通過又是怎麼一回事? 原來,在 C90/C++03 的規則之下,這個升級的規定是不一樣的: 如果有號數升到最高級還是裝不下,那還會再試一個型態:最高級的無號數, 若那個裝的下就用了,裝不下再報錯。 因此如果這邊使用舊規則,那麼 long long 上面還有一個 unsigned long long 可以試, 然後發現它裝的下了,因此 9223372036854775808LL 這個 literal 就被升級成 unsigned long long。 如果你是用 gcc/g++ 編譯,它的警告訊息就是告訴你:這個常數只在 C90 規則下是 unsigned。 升級成 unsigned long long 之後,接著要取負號。 標準規定,無號數取負號的結果一律是 2^N 減去原數,其中 N 是該型態的有效位元數。 於是 9223372036854775808 這個 unsigned long long 取負號之後還是它自己。 不過不管結果,因為現在它是無號數了,轉成 double 之後自然成了非負數,因此 x < 0.0 自然就得到 false 了。 那那邊那個跟直覺一樣得到 true 的又是怎麼回事? 好問題,不過問題裡已經有答案了:「跟直覺一樣」 這是當編譯器它看到 -9223372036854775808 這樣的東西時, 直接把這兩個部份合起來,認為你要的就是這個負常數,因此就產生一個 signed long long 其值為 - 2^63。 這確實是有編譯器是如此實作的:沒錯,不理標準出名的微軟 Visual Studio 系列即是如此! 如果有人要問轉型成 double 是在做什麼,答案是那是要唬你的 XD 轉型成 double 是為了把這個問題中很重要的一個訊息 (這個大常數的型態) 給隱藏起來, 跟什麼 FPU 的轉型或進位模式什麼的並沒有關係。 (再說 - 2^63 根本連 float 都能表示得出來 XD) 事實上,因為 constant folding 的關係,這個轉型的動作在編譯期就已經完成了, 實際寫入程式的就只有轉型後的數值而已。 這個細節也連帶的影響了 C 的 INT_MIN / LONG_MIN / LLONG_MIN 的定義, gcc/g++ 的標頭檔裡對它們的定義都是間接的, 像是 LLONG_MIN 就定義成 (-9223372036854775807LL - 1LL), 這樣前一個大數字保證沒問題,又因為 constant folding 所以可以得到我們想要的最小值。 不過,這個題目還有延伸題:若令 double y = (double)(-0x8000000000000000LL); 那 y < 0.0 的結果又是如何? (沒錯,就只是換寫成十六進位而已,還是那個數,0x8 後面十五個 0) 捲到下面看答案! 如果寫在那邊的是十六進位常數,那以上的論述只有一個地方不一樣: 即使是現今的 C99/C++11 標準,八進位跟十六進位有號常數在試著升級時會先試同等級的無號型態再往上升級。 也就是說,int 會依序往上試 unsigned int、long、unsigned long、long long、unsigned long long, 都不行才報錯。 那麼這個時候,這個常數確實會在 unsigned long long 找到歸宿, 因此答案便不會有編譯錯誤的選項,而就直接是上面當成無號數的結果:false。 這甚至不需要到 long long 的極值就能觀察到了: double z = (double)(-0x80000000); //0x8 後面七個 0 所得到的也是一個正數,根據的便是同樣的理由。 這裡寫十進位的結果就不一樣了: double w = (double)(-2147483648); 得到的是負數。這裡 2147483648 升級成了高一級的有號數,因此取負號就依然是負的。 當然,以上講了依然排除 Visual Studio XD 由於同樣的理由所以 VS 還是得到 true。 不過這裡倒是有一件事很有趣:如果是上幾行的那個 z 那 VS 倒是正確給出了正數的結果。 到底是怎麼回事這我無法猜測就是了。 那麼,以上就是本次快問快答的詳解,我們下次再見!:D |
Direct link: https://paste.plurk.com/show/2283249