C語言中文網 目錄

位操作及其使用注意事項,C語言位操作及其使用方法詳解

我們知道,程序中的所有數據在計算機內存中都是以二進制的形式進行存儲的,數據的位是可以操作的最小數據單位,位操作就是直接對整數在內存中的二進制位進行操作。因此,在理論上,我們可以通過“位運算”來完成所有的運算和操作,從而有效地提高程序運行的效率。

C 語言中提供了 &(與)、|(或)、^(異或)、~(取反)、>>(右移)、<<(左移)6 種位操作符。我們可以在程序中合理地使用這些位操作符號來提高程序的運行效率,例如,對于下面的示例代碼:
int x=0;
int y=0;
x = 257 /8;
y = 456 % 32;
我們可以通過位操作符將其修改成如下形式:
int x=0;
int y=0;
x = 257 >>3;
y = 456 - (456 >> 4 << 4);
這樣就可以使程序在性能上得到一定提升。

盡量避免對未知的有符號數執行位操作

在 C 語言中,如果在未知的有符號數上執行位操作,很可能會導致緩沖區溢出,從而在某些情況下導致攻擊者執行任意代碼,同時,還可能會出現出乎意料的行為或編譯器定義的行為。

下面來看一個簡單的示例:
#include <stdio.h>
int main (void)
{
    int x=0;
    int y=0x80000000;
    char buf[sizeof("128")];
    x=sprintf(buf,"%u",y>>24);
    if(x==-1||x>=sizeof(buf))
    {
     // 錯誤處理
    }
    printf(buf);
    return 0;
}
代碼中,y>>24 的執行結果為 4294967168,而 sizeof(buf) 的結果為 4。當我們將 y>>24 的結果值轉換為字符串“4294967168”時,超出了 buf 范圍,所以結果值無法完全存儲在 buf 中。因此,在執行語句“x=sprintf(buf,"%u",y>>24)”時,sprintf 方法在進行寫操作時就會越過 buf 的邊界,從而產生緩沖區溢出。

如果在編譯器 VC++ 中執行這段程序,將會產生如圖 1 所示的錯誤報告。


圖 1

在 C99 中,要修正這樣的錯誤,最好利用 snprintf 方法來代替 sprintf 方法。因為 snprintf 方法最多從源串中復制 n-1 個字符到目標串中,然后再從后面加一個 0。因此,如果目標串的大小為 n,將不會產生溢出。

當然,如果將變量 y 聲明成為無符號類型,即將:
int y=0x80000000;
修改為:
unsigned int y = 0x80000000;
那么這種緩沖區溢出錯誤將不會發生。

在右移中合理地選擇 0 或符號位來填充空出的位

在右移運算中,空出的位用 0 還是符號位進行填充呢?

其實答案由具體的 C 語言編譯器實現來決定。在通常情況下,如果要進行移位的操作數是無符號類型的,那么空出的位將用 0 進行填充;如果要進行移位的操作數是有符號類型的,則 C 語言編譯器實現既可選擇 0 來進行填充,也可選擇符號位進行填充。

因此,如果很關心一個右移運算中的空位,那么可以使用 unsigned 修飾符來聲明變量,這樣空位都會被設置為 0。同時,如果一個程序采用了有符號數的右移位操作,那么它就是不可移植的。

移位的數量必須大于等于 0 且小于操作數的位數

如果被移位的操作數的長度為 n,那么移位的數量必須大于等于 0 且小于 n。因此,在一次單獨的操作中不可能將所有的位從變量中移出。例如,一個 int 型的整數是 32 位,并且 n 是一個 int 型整數,那么 n<<31 和 n<<0 是合法的,但 n<<32 和 n<<-1 是不合法的。因此,我們在進行移位運算的時候必須做相關測試。示例代碼如下所示:
unsigned int x;
unsigned int y;
unsigned int result;
/*初始x,y,result*/
if(y>=sizeof(unsigned int) * CHAR_BIT)
{
    // 錯誤處理
}
else
{
    result=x>>y;
}
這里還需要說明的是,對于變量 x 與 y,C99 規定:
  • 對于 x<y 可以用結果類型表示,那么這個表達式就是結果值,否則,其行為是未定義的;如果 x 是無符號類型,則 x< y,是根據“結果類型可以表達的最大值加 1”進行求模運算得到的結果。需要注意的是,盡管在 C99 中指定了無符號整數的取模行為,無符號整數溢出還是常常導致出乎意料的值以及因此產生的潛在安全風險。
  • 對于 x>>y,如果 x 是無符號類型或非負值的有符號類型,那么 x>>y 的移位結果為 x/2y 的商的整數部分;如果 x 是有符號類型的負值,那么 x>>y 的移位結果是由編譯器所定義的。因此,對一個帶符號整數進行右移運算和將它除以 2 的某次冪不一定是等價的。要證明這一點很容易,考慮 (-1)>>1 的值,它的執行結果不可能為 0,而在大多數 C 語言編譯器中 (-1)/2 的結果都是 0。

盡量避免在同一個數據上執行位操作與算術運算

雖然位操作在很大程度上可以提高程序的執行效率,但在同一個變量上執行位操作和算術運算會模糊程序員的意圖,削弱代碼的可移植性與可讀性,還會導致安全審核員或代碼維護人員難以確定應該執行什么檢查以消除安全缺陷,保證數據的完整性。示例代碼如下:
unsigned int x=10;
x+=(x<<3)-5;
雖然上面的代碼是一條合法的優化語句,但是它的確嚴重地破壞了程序的可讀性。因此,建議采用下面的方式來書寫代碼:
unsigned int x=10;
x=x*9-5;
或許這時候有讀者會問,這樣寫代碼不就降低了程序的執行效率嗎?

其實不然,有些優化編譯器會比我們做得更好。以整數乘法為例,如果目標系統有乘法指令,硬件的乘法比自己用移位操作實現的乘法要快得多;如果目標系統沒有乘法指令,編譯器會自動用移位等操作來優化乘法,示例代碼如下:
unsigned int x=8;
unsigned int y=x*4;
unsigned int z=x/2;
上面的代碼在 Microsoft Visual Studio 2010 集成開發環境 VC++ 的 Debug 模式下將生成如下匯編代碼:
unsigned int x=8;
00DB139E  mov         dword ptr [x],8
unsigned int y=x*4;
00DB13A5  mov         eax,dword ptr [x]
00DB13A8  shl         eax,2
00DB13AB  mov         dword ptr [y],eax
unsigned int z=x/2;
00DB13AE  mov         eax,dword ptr [x]
00DB13B1  shr         eax,1
00DB13B3  mov         dword ptr [z],eax
從上面的匯編代碼可以看出,編譯器會自動在匯編代碼中用移位操作來優化乘除法運算。因此,完全沒有必要用手工進行這種優化,編譯器會自動完成。我們應該把精力放在改進程序的算法上,一個好的算法可以使程序運行效率大大提高。當然,如果除數為 2 的冪,那么在進行除法運算時可以適當地采用移位算法來實現乘除法。

除此之外,采用移位操作還需要注意不要超過該數據類型的精度范圍(數據范圍),示例代碼如下:
#include <stdio.h>
int main (void)
{
    int x=-2147483647;
    x=x<<1;
    printf("%d \n",x);
    return 0;
}
在上面的代碼中就要注意精度問題,在 32 位系統中,int 類型占 4 個字節,精度范圍為“-2147483647~2147483647”。其中,數據“-2147483647”的原碼為“11111111111111111111111111111111”,補碼為“10000000000000000000000000000001”。現在將“10000000000000000000000000000001”左移 1 位,最高位的1沒有了,最低位左移一位,得到的結果為 2。

精美而實用的網站,提供C語言C++STLLinuxShellJavaGo語言等教程,以及socketGCCviSwing設計模式JSP等專題。

Copyright ?2011-2018 biancheng.net, 陜ICP備15000209號

底部Logo