C語言:位操作


位操作

運算符:~、&、|、、>>、<<、&=、|=、=、>>=、<<=

二進制、十進制、十六進制

位運算符位字段

_Alignas _Alignof

二進制、位和字節

計算機適用基底為2的數制系統。它用2的冪而不是10的冪。以2為基 底表示的數字被稱為二進制數(binary number)。二進制中的2和十進制中 的10作用相同。例如,二進制數1101可表示為:

1×2^3 + 1×2^2+ 0×2^1+ 1×2^0

以十進制數表示為:1×8 + 1×4 + 0×2 + 1×1 = 13

用二進制系統可以把任意整數(如果有足夠的位)表示為0和1的組合。 由於數字計算機通過關閉和打開狀態的組合來表示信息,這兩種狀態分別用 0和1來表示,所以使用這套數制系統非常方便。

二進制整數

通常,1字節包含8位。C語言用字節(byte)表示儲存系統字符集所需 的大小,所以C字節可能是8位、9位、16位或其他值。不過,描述存儲器芯片和數據傳輸率中所用的字節指的是8位字節(計算機界通常用八位組(octet)這個術語特指8位字節)。

可以從左 往右給這8位分別編號為7~0。在1字節中,編號是7的位被稱為高階位 (high-order bit),編號是0的位被稱為低階位(low-order bit)。每 1位的 編號對應2的相應指數。

這里,128是2的7次冪,以此類推。該字節能表示的最大數字是把所有 位都設置為1:11111111。這個二進制數的值是:
128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255

而該字節最小的二進制數是00000000,其值為0。因此,1字節可儲存0 ~255范圍內的數字,總共256個值。或者,通過不同的方式解釋位組合 (bit pattern),程序可以用1字節儲存-128~+127范圍內的整數,總共還是 256個值。例如,通常unsigned char用1字節表示的范圍是0~255,而signed char用1字節表示的范圍是-128~+127。

有符號整數

如何表示有符號整數取決於硬件,而不是C語言。也許表示有符號數最 簡單的方式是用1位(如,高階位)儲存符號,只剩下7位表示數字本身(假設儲存在1字節中)。用這種符號量(sign-magnitude)表示法,10000001表 示−1,00000001表示1。因此,其表示范圍是−127~+127

這種方法的缺點是有兩個0:+0和-0。這很容易混淆,而且用兩個位組 合來表示一個值也有些浪費。

二進制補碼(two’s-complement)方法避免了這個問題,是當今最常用的系統。

二進制補碼用1字節中的后7 位表示0~127,高階位設置為0。目前,這種方法和符號量的方法相同。另 外,如果高階位是1,表示的值為負。這兩種方法的區別在於如何確定負 值。從一個9位組合100000000(256的二進制形式)減去一個負數的位組 合,結果是該負值的量。例如,假設一個負值的位組合是 10000000,作為 一個無符號字節,該組合為表示 128;作為一個有符號值,該組合表示負值 (編碼是 7的位為1),而且值為100000000-10000000,即 1000000(128)。因此,該數是-128(在符號量表示法中,該位組合表示 −0)。類似地,10000001 是−127,11111111 是−1。該方法可以表示−128~ +127范圍內的數。

要得到一個二進制補碼數的相反數,最簡單的方法是反轉每一位(即0 變為1,1變為0),然后加1。因為1是00000001,那么−1則是11111110+1, 或11111111。

二進制浮點數

浮點數分兩部分儲存:二進制小數和二進制指數。

  1. 二進制小數

一個普通的浮點數0.527,表示如下:

5/10 + 2/100 + 7/1000

從左往右,各分母都是10的遞增次冪。在二進制小數中,使用2的冪作 為分母,所以二進制小數.101表示為:

1/2 + 0/4 + 1/8

用十進制表示法為:

0.50 + 0.00 + 0.125

即是0.625。

許多分數(如,1/3)不能用十進制表示法精確地表示。與此類似,許 多分數也不能用二進制表示法准確地表示。實際上,二進制表示法只能精確 地表示多個1/2的冪的和。因此,3/4和7/8可以精確地表示為二進制小數,但 是1/3和2/5卻不能。

  1. 浮點數表示法

為了在計算機中表示一個浮點數,要留出若干位(因系統而異)儲存二 進制分數,其他位儲存指數。一般而言,數字的實際值是由二進制小數乘以 2的指定次冪組成。例如,一個浮點數乘以4,那么二進制小數不變,其指數 乘以2,二進制分數不變。如果一份浮點數乘以一個不是2的冪的數,會改變 二進制小數部分,如有必要,也會改變指數部分。


其他進制數

八進制

八進制(octal)是指八進制記數系統。該系統基於8的冪,用0~7表示 數字(正如十進制用0~9表示數字一樣)。例如,八進制數451(在C中寫 作0451)表示為:

4×8^2+ 5×8^1+ 1×8^0= 297(十進制)

了解八進制的一個簡單的方法是,每個八進制位對應3個二進制位。

這種關系使得八進制與二進制之間的轉換很容 易。例如,八進制數0377的二進制形式是11111111。即,用111代替0377中 的最后一個7,再用111代替倒數第2個7,最后用011代替3,並舍去第1位的 0。這表明比0377大的八進制要用多個字節表示。這是八進制唯一不方便的 地方:一個3位的八進制數可能要用9位二進制數來表示。注意,將八進制數 轉換為二進制形式時,不能去掉中間的0。

十六進制

十六進制(hexadecimal或hex)是指十六進制記數系統。該系統基於16 的冪,用0~15表示數字。但是,由於沒有單獨的數(digit,即0~9這樣單獨一位的數)表示10~15,所以用字母A~F來表示。例如,十六進制數 A3F(在C中寫作0xA3F)表示為:

10×162+3×161+ 15×16^0= 2623(十進制)

每個十六進制位都對應一個4位的二進制數(即4個二進制位),那么兩 個十六進制位恰好對應一個8位字節。第1個十六進制表示前4位,第2個十六 進制位表示后4位。因此,十六進制很適合表示字節值。

例如,十六進制值0xC2可轉換為 11000010。相反,二進制值11010101可以看作是1101 0101,可轉換為 0xD5。


C按位運算符

C 提供按位邏輯運算符和移位運算符。在下面的例子中,為了方便讀者 了解位的操作,我們用二進制記數法寫出值。但是在實際的程序中不必這 樣,用一般形式的整型變量或常量即可。例如,在程序中用25或031或 0x19,而不是00011001。另外,下面的例子均使用8位二進制數,從左往右 每位的編號為7~0。

按位邏輯運算符

4個按位邏輯運算符都用於整型數據,包括char。之所以叫作按位 (bitwise)運算,是因為這些操作都是針對每一個位進行,不影響它左右兩 邊的位。不要把這些運算符與常規的邏輯運算符(&&、||和!)混淆,常規 的邏輯運算符操作的是整個值。

  1. 二進制反碼或按位取反:~

一元運算符~把1變為0,把0變為1。如下例子所示:

~(10011010) // 表達式
(01100101)  // 結果值

假設val的類型是unsigned char,已被賦值為2。在二進制中,00000010 表示2。那么,~val的值是11111101,即253。注意,該運算符不會改變val 的值,就像3 * val不會改變val的值一樣, val仍然是2。但是,該運算符確實 創建了一個可以使用或賦值的新值:

newval = ~val;
printf("%d", ~val);

如果要把val的值改為~val,使用下面這條語句:

val=~fsval
  1. 按位與:&

二元運算符&通過逐位比較兩個運算對象,生成一個新值。對於每個 位,只有兩個運算對象中相應的位都為1時,結果才為1(從真/假方面看, 只有當兩個位都為真時,結果才為真)。因此,對下面的表達式求值:

(10010011) & (00111101)  // 表達式

由於兩個運算對象中編號為4和0的位都為1,得:

(00010001)  // 結果值

C有一個按位與和賦值結合的運算符:&=。下面兩條語句產生的最終結 果相同:

val &= 0377;
val = val & 0377;
  1. 按位或:|

二元運算符|,通過逐位比較兩個運算對象,生成一個新值。對於每個 位,如果兩個運算對象中相應的位為1,結果就為1(從真/假方面看,如果 兩個運算對象中相應的一個位為真或兩個位都為真,那么結果為真)。因 此,對下面的表達式求值:

(10010011) | (00111101) // 表達式

除了編號為6的位,這兩個運算對象的其他位至少有一個位為1,得:

(10111111) // 結果值

C有一個按位或和賦值結合的運算符:|=。下面兩條語句產生的最終作 用相同:

val |= 0377;
val = val | 0377;
  1. 按位異或:^

二元運算符^逐位比較兩個運算對象。對於每個位,如果兩個運算對象 中相應的位一個為1(但不是兩個為1),結果為1(從真/假方面看,如果兩 個運算對象中相應的一個位為真且不是兩個為同為1,那么結果為真)。因 此,對下面表達式求值:

(10010011) ^ (00111101) // 表達式

編號為0的位都是1,所以結果為0,得:

(10101110)  // 結果值

C有一個按位異或和賦值結合的運算符:^=。下面兩條語句產生的最終 作用相同:

val ^= 0377;
val = val ^ 0377;

用法:掩碼

所謂掩碼指的是一些設置為開 (1)或關(0)的位組合。要明白稱其為掩碼的原因,先來看通過&把一個 量與掩碼結合后發生什么情況。例如,假設定義符號常量MASK為2 (即, 二進制形式為00000010),只有1號位是1,其他位都是0。下面的語句:

flags = flags & MASK;

把flags中除1號位以外的所有位都設置為0,因為使用按位與運算符 (&)任何位與0組合都得0。1號位的值不變(如果1號位是1,那么 1&1得1;如果 1號位是0,那么 0&1也得0)。這個過程叫作“使用掩碼”,因為掩 碼中的0隱藏了flags中相應的位。

可以這樣類比:把掩碼中的0看作不透明,1看作透明。表達式flags & MASK相當於用掩碼覆蓋在flags的位組合上,只有MASK為1的位才可見

下面這條語句是按位與的一種常見用法:

ch &= 0xff; /* 或者 ch &= 0377; */

前面介紹過oxff的二進制形式是11111111,八進制形式是0377。這個掩 碼保持ch中最后8位不變,其他位都設置為0。無論ch原來是8位、16位或是 其他更多位,最終的值都被修改為1個8位字節。在該例中,掩碼的寬度為8位。

用法:打開位(設置為)

有時,需要打開一個值中的特定位,同時保持其他位不變。例如,一台 IBM PC 通過向端口發送值來控制硬件。例如,為了打開內置揚聲器,必須 打開 1 號位,同時保持其他位不變。這種情況可以使用按位或運算符 (|)。

以上一節的flags和MASK(只有1號位為1)為例。下面的語句:

flags = flags | MASK;

把flags的1號位設置為1,且其他位不變。因為使用|運算符,任何位與0 組合,結果都為本身;任何位與1組合,結果都為1。

這種方法根據MASK中為1的位,把flags中對應的位設置為1,其他位不變.

用法:關閉位(清空位)

和打開特定的位類似,有時也需要在不影響其他位的情況下關閉指定的 位。假設要關閉變量flags中的1號位。同樣,MASK只有1號位為1(即,打 開)。可以這樣做:

flags = flags & ~MASK;

由於MASK除1號位為1以外,其他位全為0,所以~MASK除1號位為0 以外,其他位全為1。使用&,任何位與1組合都得本身,所以這條語句保持 1號位不變,改變其他各位。另外,使用&,任何位與0組合都的0。所以無 論1號位的初始值是什么,都將其設置為0。

MASK中為1的位在結果中都被設置(清空)為0。flags中與MASK為0的 位相應的位在結果中都未改變。

簡化:

flags &= ~MASK;

用法:切換位

切換位指的是打開已關閉的位,或關閉已打開的位。可以使用按位異或 運算符(^)切換位。也就是說,假設b是一個位(1或0),如果b為1,則 1b為0;如果b為0,則1b為1。另外,無論b為1還是0,0^b均為b。因此, 如果使用^組合一個值和一個掩碼,將切換該值與MASK為1的位相對應的 位,該值與MASK為0的位相對應的位不變。要切換flags中的1號位,可以使用 下面兩種方法:

flags = flags ^ MASK;
flags ^= MASK;

flags中與MASK為1的位相對應的位都被切換了,MASK為0的位相對應 的位不變。

用法:檢查位的值

有時,需要檢查某位的值。

例如,flags中 1號位是否被設置為1?不能這樣直接比較flags和MASK:

if (flags == MASK)
puts("Wow!"); /* 不能正常工作 */

這樣做即使flags的1號位為1,其他位的值會導致比較結果為假。因此,必須覆蓋flags中的其他位,只用1號位和MASK比較:

if ((flags & MASK) == MASK)
puts("Wow!");

由於按位運算符的優先級比==低,所以必須在flags & MASK周圍加上 圓括號。

為了避免信息漏過邊界,掩碼至少要與其覆蓋的值寬度相同。

移位運算符

移位運算符向左或向右移動位。

  1. 左移:<<

左移運算符(<<)將其左側運算對象每一位的值向左移動其右側運算 對象指定的位數。左側運算對象移出左末端位的值丟失,用0填充空出的位 置。下面的例子中,每一位都向左移動兩個位置:

(10001010) << 2  // 表達式
(00101000)    // 結果值

該操作產生了一個新的位值,但是不改變其運算對象。例如,假設 stonk為1,那么 stonk<<2為4,但是stonk本身不變,仍為1。可以使用左移賦 值運算符(<<=)來更改變量的值。該運算符將變量中的位向左移動其右側 運算對象給定值的位數。如下例:

int stonk = 1;
int onkoo;
onkoo = stonk << 2;  /* 把4賦給onkoo */
stonk <<= 2;     /* 把stonk的值改為4 */
  1. 右移:>>

右移運算符(>>)將其左側運算對象每一位的值向右移動其右側運算 對象指定的位數。左側運算對象移出右末端位的值丟。對於無符號類型,用 0 填充空出的位置;對於有符號類型,其結果取決於機器。空出的位置可用 0填充,或者用符號位(即,最左端的位)的副本填充:

(10001010) >> 2    // 表達式,有符號值
(00100010)       // 在某些系統中的結果值
(10001010) >> 2    // 表達式,有符號值
(11100010)       // 在另一些系統上的結果值
//下面是無符號值的例子:
(10001010) >> 2    // 表達式,無符號值
(00100010)       // 所有系統都得到該結果值
//每個位向右移動兩個位置,空出的位用0填充。

右移賦值運算符(>>=)將其左側的變量向右移動指定數量的位數。如 下所示:

int sweet = 16;
int ooosw;
ooosw = sweet >> 3;  // ooosw = 2,sweet的值仍然為16
sweet >>=3;      // sweet的值為2
  1. 用法:移位運算符

移位運算符針對2的冪提供快速有效的乘法和除法:

number << n    number乘以2的n次冪
number >> n    如果number為非負,則用number除以2的n次冪

這些移位運算符類似於在十進制中移動小數點來乘以或除以10。

移位運算符還可用於從較大單元中提取一些位。例如,假設用一個 unsigned long類型的值表示顏色值,低階位字節儲存紅色的強度,下一個字 節儲存綠色的強度,第 3 個字節儲存藍色的強度。隨后你希望把每種顏色的 強度分別儲存在3個不同的unsigned char類型的變量中。那么,可以使用下面 的語句:

#define BYTE_MASK 0xff
unsigned long color = 0x002a162f;
unsigned char blue, green, red;
red = color & BYTE_MASK;
green = (color >> 8) & BYTE_MASK;
blue = (color >> 16) & BYTE_MASK;

以上代碼中,使用右移運算符將 8 位顏色值移動至低階字節,然后使用 掩碼技術把低階字節賦給指定的變量。

編程示例

用移位運算符來解決把數字轉換為二進制
形式。讀取用戶從鍵盤輸入的整數,將該整數和一個字符串地址 傳遞給itobs()函數(itobs表示interger to binary string,即整數轉換成二進制字 符串)。然后,該函數使用移位運算符計算出正確的1和0的組合,並將其放 入字符串中。

/* binbit.c -- 使用位操作顯示二進制 */
#include <stdio.h>
#include <limits.h> // 提供 CHAR_BIT 的定義,CHAR_BIT 表示每字節 的位數
char * itobs(int, char *);
void show_bstr(const char *);
int main(void)
{
char bin_str[CHAR_BIT * sizeof(int) + 1];
int number;
puts("Enter integers and see them in binary.");
puts("Non-numeric input terminates program.");
while (scanf("%d", &number) == 1)
{
itobs(number, bin_str);
printf("%d is ", number);
show_bstr(bin_str);
putchar('\n');
}
puts("Bye!");
return 0;
}
char * itobs(int n, char * ps)
{
int i;
const static int size = CHAR_BIT * sizeof(int);
for (i = size - 1; i >= 0; i--, n >>= 1)
ps[i] = (01 & n) + '0';
ps[size] = '\0';
return ps;
}
/*4位一組顯示二進制字符串 */
void show_bstr(const char * str)
{
int i = 0;
while (str[i]) /* 不是一個空字符 */
{
putchar(str[i]);
if (++i % 4 == 0 && str[i])
putchar(' ');
}
}

程序使用limits.h中的CHAR_BIT宏,該宏表示char中的位數。 sizeof運算符返回char的大小,所以表達式CHAE_BIT * sizeof(int)表示int類型 的位數。bin_str數組的元素個數是CHAE_BIT * sizeof(int) + 1,留出一個位置 給末尾的空字符。

itobs()函數返回的地址與傳入的地址相同,可以把該函數作為printf()的 參數。在該函數中,首次執行for循環時,對01 & n求值。01是一個八進制形 式的掩碼,該掩碼除0號位是1之外,其他所有位都為0。因此,01 & n就是n 最后一位的值。該值為0或1。但是對數組而言,需要的是字符'0'或字符'1'。 該值加上'0'即可完成這種轉換(假設按順序編碼的數字,如 ASCII)。其結 果存放在數組中倒數第2個元素中(最后一個元素用來存放空字符)。

順帶一提,用1 & n或01 & n都可以。我們用八進制1而不是十進制1,只 是為了更接近計算機的表達方式。

然后,循環執行i--和n >>= 1。i--移動到數組的前一個元素,n >>= 1使n 中的所有位向右移動一個位置。進入下一輪迭代時,循環中處理的是n中新 的最右端的值。然后,把該值儲存在倒數第3個元素中,以此類推。itobs() 函數用這種方式從右往左填充數組。

示例2

編寫一個函數用於切換一個值中的后 n 位, 待處理值和 n 都是函數的參數。

~運算符切換一個字節的所有位,而不是選定的少數位。但是,^運算 符(按位異或)可用於切換單個位。假設創建了一個掩碼,把后n位設置為1,其余位設置為0。然后使用^組合掩碼和待切換的值便可切換該值的最后n 位,而且其他位不變。方法如下:

int invert_end(int num, int bits)
{
int mask = 0;
int bitval = 1;
while (bits–– > 0)
{
mask |= bitval;
bitval <<= 1;
}
return num ^ mask;
}

while循環用於創建所需的掩碼。最初,mask的所有位都為0。第1輪循 環將mask的0號位設置為1。然后第2輪循環將mask的1號位設置為1,以此類 推。循環bits次,mask的后bits位就都被設置為1。最后,num ^ mask運算即得 所需的結果。


位字段

操控位的第2種方法是位字段(bit field)。位字段是一個signed int或 unsigned int類型變量中的一組相鄰的位(C99和C11新增了_Bool類型的位字 段)。位字段通過一個結構聲明來建立,該結構聲明為每個字段提供標簽, 並確定該字段的寬度。例如,下面的聲明建立了一個4個1位的字段:

struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;

根據該聲明,prnt包含4個1位的字段。現在,可以通過普通的結構成員 運算符(.)單獨給這些字段賦值:

prnt.itals = 0;
prnt.undln = 1;

由於每個字段恰好為1位,所以只能為其賦值1或0。變量prnt被儲存在 int大小的內存單元中,但是在本例中只使用了其中的4位。

帶有位字段的結構提供一種記錄設置的方便途徑。許多設置(如,字體 的粗體或斜體)就是簡單的二選一。例如,開或關、真或假。如果只需要使 用 1 位,就不需要使用整個變量。內含位字段的結構允許在一個存儲單元中 儲存多個設置。

有時,某些設置也有多個選擇,因此需要多位來表示。這沒問題,字段不限制 1 位大小。可以使用如下的代碼:

struct {
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 8;
} prcode;

以上代碼創建了兩個2位的字段和一個8位的字段。可以這樣賦值:

prcode.code1 = 0;
prcode.code2 = 3;
prcode.code3 = 102;

如果聲明的總位數超過了一個unsigned int類型的大小會怎樣?會用到下 一個unsigned int類型的存儲位置。一個字段不允許跨越兩個unsigned int之間 的邊界。編譯器會自動移動跨界的字段,保持unsigned int的邊界對齊。一旦 發生這種情況,第1個unsigned int中會留下一個未命名的“洞”。

可以用未命名的字段寬度“填充”未命名的“洞”。使用一個寬度為0的未 命名字段迫使下一個字段與下一個整數對齊:

struct {
unsigned int field1  : 1 ;
unsigned int      : 2 ;
unsigned int field2  : 1 ;
unsigned int      : 0 ;
unsigned int field3  : 1 ;
} stuff;

這里,在stuff.field1和stuff.field2之間,有一個2位的空隙;stuff.field3將 儲存在下一個unsigned int中。

字段儲存在一個int中的順序取決於機器。在有些機器上,存儲的順序是 從左往右,而在另一些機器上,是從右往左。另外,不同的機器中兩個字段 邊界的位置也有區別。由於這些原因,位字段通常都不容易移植。盡管如 此,有些情況卻要用到這種不可移植的特性。例如,以特定硬件設備所用的 形式儲存數據


對齊特性

C11 的對齊特性比用位填充字節更自然,它們還代表了C在處理硬件相 關問題上的能力。在這種上下文中,對齊指的是如何安排對象在內存中的位 置。例如,為了效率最大化,系統可能要把一個 double 類型的值儲存在4 字 節內存地址上,但卻允許把char儲存在任意地址。大部分程序員都對對齊不 以為然。但是,有些情況又受益於對齊控制。例如,把數據從一個硬件位置 轉移到另一個位置,或者調用指令同時操作多個數據項。

_Alignof運算符給出一個類型的對齊要求,在關鍵字_Alignof后面的圓括 號中寫上類型名即可:

size_t d_align = _Alignof(float);

假設d_align的值是4,意思是float類型對象的對齊要求是4。也就是說, 4是儲存該類型值相鄰地址的字節數。一般而言,對齊值都應該是2的非負整 數次冪。較大的對齊值被稱為stricter或stronger,較小的對齊值被稱為 weaker。

可以使用_Alignas 說明符指定一個變量或類型的對齊值。但是,不應該 要求該值小於基本對齊值。例如,如果float類型的對齊要求是4,不要請求 其對齊值是1或2。該說明符用作聲明的一部分,說明符后面的圓括號內包含 對齊值或類型:

_Alignas(double) char c1;
_Alignas(8) char c2;
unsigned char _Alignas(long double) c_arr[sizeof(long double)];


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM