C 語言常見誤解/整數/表示法與位元運算

整數(一)表示法和位元運算

編輯

有號整數表示法中的位元分三種:正負號、值和填充。正負號和值的格式可以是二補數(補碼)、一補數(反碼)或正負號加上大小(原碼)三種格式其中之一。無號整數表示法則少了正負號位元。(參考 N1256 6.2.6.2p1~p2 、以及 C Defect Report #069

並不是所有的位元組合都能表示合理的數字,存取某些位元組合在某些機器上可能會造成嚴重錯誤,此種組合稱作陷阱表示法(trap representation)。除非使用位元運算或是違反標準其他規定(如溢位),一般的運算不可能產生陷阱表示法。標準明確允許實作自行決定在以下兩種狀況下是否是陷阱表示法:

  • 型態為有號整數且正負號及值位元為特定組合時(三種格式各有一特殊組合)。
  • 填充位元為某些組合時。(參考 N1256 註腳# 44, 45

位元運算會忽略填充位元,因此(等級不輸給 unsigned int 的)無號整數可安心使用。為了最大可攜性,位元運算不應該用在有號整數上。

uintN_tintN_t 保證沒有填充位元,intN_t 一定是二補數,而且 intN_t 不可能有陷阱表示法,堪稱是最安全的整數型態。實作可能不提供這些型態,但一旦提供就要保證這些好性質。(參考標準 N1256 7.18.1.1p1~p3)


問:int 剛好 32 位元不是嗎?

答:不一定。整數的寬度(正負號和值位元的數量)沒有上界,只要能表示標準規定的數字範圍即可。更何況除了寬度之外,可能還有其他填充位元。同理 short 也不一定是 16 位元,long long 也不一定是 64 位元。想要固定寬度請使用 int32_t


問:那 int 至少 32 位元吧?

答:也不一定。因為 int 只保證能存下 -215+1 (-32767) 到 215-1(32767) 之間的整數,16 位元已經足夠。int_least32_tint_fast32_t 可以保證存下至少 -231+1到231-1之間的整數(由於不一定是沒有陷阱表示法的二補數,所以保證範圍的下限不是-231而是-231+1)。


我想要有一個 300 乘 300 的 double 陣列,malloc(300*300*sizeof(double)) 有什麼問題? 300*300 可能超出 INT_MAX,而且 300*300*sizeof(double) 可能超出 SIZE_MAXINT_MAX(看實作決定轉型成哪個型態)。實際上比較危險的狀況是有可能 malloc 實際上只給了一塊很小的記憶體,但程式卻當作一塊很大的記憶體使用,造成可能的緩衝區溢位漏洞。為了要安全可攜可以做兩件事情:第一個是盡量從頭到尾維持型態 size_t 或範圍更大的無號整數型態,所以 sizeof 擺前面(順序很重要),而且中途所有數字都是等級在 unsigned int 以上的無號整數(如常量尾巴加上 u);第二個要保證運算結束後不會超過 size_t 的範圍。一個可能寫法如下:

  if (SIZE_MAX / 300u / 300u < sizeof(double)) {
     p = NULL;
  } else {
     p = malloc(sizeof(double)*300u*300u) ;
  }

(參考只寫一半的 clc FAQ 7.16, clang 的檢查


問:到底要怎麼用 int 才不會超出範圍?!

答:int 保證可以存下 -215+1 到 215-1 之間的整數。更一般的寫法是使用 INT_MAXINT_MIN 得知真正的範圍。其他整數型態都可用類似的方法得到範圍。


問:假如 sizeof(int) 為 4 或是確定 int 佔 32 位元,是不是代表 int 剛好可以儲存 -231 到 231-1 之間的整數?

答:不一定。首先一個位元組不一定是 8 位元(見此問題)。即使是,int 表示法中不一定每個位元都會用來表示值(這種位元稱作填充位元)。退萬步言,即使寬度(不含填充位元)剛好為 32 位元,int 的格式可能也不是二補數,所以不一定是從 -231 開始算。再退萬步言,縱使用二補數,特定組合可能是陷阱表示法,所以可能還是無法表示-231。用 int32_t 可以避開以上所有問題,滿足所有需求,除了 sizeof(int32_t) 不一定是 4.


問:假如 i 的型態是整數。能不能用 memset(&i, 0, sizeof(i)) 歸零?

答:標準委員會已決定向廣大程式碼妥協。注意只有 0 有特赦條款保證。(參考 C Defect Report #263 看標準如何妥協,以及 N1256 6.2.6.2p5)


問:假設 ab 兩個變數有相同的整數型態,a^=b; b^=a; a^=b; 是否可讓兩數交換?

答:不保證。因為 a^b 可能會產生陷阱表示法。(參考陷阱表示法)


問:該不會 | ^ & ~ 四種位元運算都可能產生陷阱表示法(trap representation)?

答:沒錯。例如在有號整數上都可能產生陷阱表示法(其他某些型態也有可能)。(參考陷阱表示法)


問:那應該如何安全的使用位元運算?

答:使用等級不輸給 unsigned int 的無號整數可高枕無憂。


問:假設 a 的型態是 unsigned int 且寬度洽為 32, a<<32 結果會是 0 嗎?

答:不保證(參考 N1256 6.5.7p3)。實際上有些處理器結果會是 a 而不是 0, 因為在那些機器上 a << b 實際上是 a << (b % 32).(參考 KennyTM 舉的現實例子


問:要如何區辦是 little-endianness 還是 big-endianness?

答:世界上有機器兩者都不是,理論上也不可能有(簡單的)可攜寫法可以判斷,請仔細考慮是否真的需要判斷機器怎麼存數字。網路傳資料時請用系統提供之轉換函式。(參考 middle-endian 和 bi-endian)