位運算
Ⅰ 位運算的定義
我們知道程序中的所有數據,都是以二進制的方式存儲在計算機中的。位運算就是基於二進制的位進行的運算,直接對整數的二進制位進行操作。因此考慮位運算有兩個錨點:二進制,補碼。
注意:計算機中所有的運算都沒有位運算快
Ⅱ 位運算的符號
C語言中有以下六種位運算:
- ~ : 按位取反(單目運算)
- & : 按位與
- | : 按位或
- ^ : 按位異或
- << : 左移
- >> : 右移
Ⅲ 位運算的驗證及分析
a.按位取反 ~
以下爲測試代碼:👇
#include <stdio.h>
int main() {
int a = 1;
int b = ~a;
printf("a:%d b:%d\n", a, b);
return 0;
}
我們令a = 1,對其取反,得到結果如下👇
可以看到,對1取反結果是-2,爲什麼會這樣?我們通過二進制和補碼兩個錨點做以下分析。
1爲int類型,即32位,所以1的補碼爲:
(0000 0000 0000 0000 0000 0000 0000 0001)補
對其按位取反,得到以下補碼:
(1111 1111 1111 1111 1111 1111 1111 1110)補
將這個補碼化成原碼,第一位爲字符位,其餘按位取反末位加1:
(1000 0000 0000 0000 0000 0000 0000 0010)原 = -2
這便是-2的來歷。
b.按位與 &
以下爲測試代碼👇
#include <stdio.h>
int main() {
int a = 1;
int b = 0;
printf("a & b = %d\n", a & b);
printf("~a & b = %d\n", ~a & b);
printf("a & ~b = %d\n", a & ~b);
printf("~a & ~b = %d\n", ~a & ~b);
return 0;
}
結果如下👇
對於前三個結果我想大家應該都比較清楚,最後這個-2就很迷惑了,按照慣常想法,1和0的與結果要麼是0要麼是1,那我就再對這個結果進行以下分析👇
根據第一個按位取反,我們可以直接寫出~1和 ~0的結果:
(~1) 補 = (1111 1111 1111 1111 1111 1111 1111 1110)補
(~0) 補 = (1111 1111 1111 1111 1111 1111 1111 1111)補
將這兩個補碼進行按位與,得
(1111 1111 1111 1111 1111 1111 1111 1110)補
將其化成原碼得
(1000 0000 0000 0000 0000 0000 0000 0010)原 = -2
c.按位或 |
以下爲測試代碼👇
#include <stdio.h>
int main() {
int a = 1;
int b = 0;
printf("a | b = %d\n", a | b);
printf("~a | b = %d\n", ~a | b);
printf("a | ~b = %d\n", a | ~b);
printf("~a | ~b = %d\n", ~a | ~b);
return 0;
}
結果如下👇
運算規則和上面一樣,我們可以發現 1 | ~0 = -1,所以位運算和邏輯運算是很不一樣的,即使( | )和 ( || )樣子差別不大,但是用法差別很大,這點需要注意,驗證過程我不再贅述。
d.按位異或 ^
驗證代碼如下👇
#include <stdio.h>
int main() {
int a = 1;
int b = 0;
printf("a ^ b = %d\n", a ^ b);
printf("!a ^ b = %d\n", !a ^ b);
printf("a ^ !b = %d\n", a ^ !b);
printf("!a ^ !b = %d\n", !a ^ !b);
return 0;
}
這次我們將~變成!,我們只看他們的邏輯關係,其本質還是一樣的,是按照補碼進行按位異或,結果如下👇
e.左移 <<
測試代碼如下👇
#include <stdio.h>
int main() {
int a = 8;
int b;
int c;
b = a << 2;
c = a << 3;
printf("8 << 2 = %d\n", b);
printf("8 << 3 = %d\n", c);
return 0;
}
結果如下👇
可以看到 8 << 2 = 32 , 8 << 3 = 64,分析如下👇
(8)補= (0000 0000 0000 0000 0000 0000 0000 1000)補
8 << 2 = (0000 0000 0000 0000 0000 0000 0010 0000)補
=32
所以結論是,當把一個數左移n時, 實質上是對這個數乘以2n。即,a << b = a * 2b
f.右移 >>
測試代碼如下👇
#include <stdio.h>
int main() {
int a = 7;
int d = -32;
int b;
int c;
b = a >> 2;
c = d >> 2;
printf("7 >> 2 = %d\n", b);
printf("-32 >> 2 = %d\n", c);
return 0;
}
結果如下👇
根據左移的規則可以得出:a >> b = a / 2b
但是右移有一個需要注意的地方,即右移的時候,在原來位置需要補的是符號位,即是正數則補0,是負數則補1,但是有幾種特殊情況,比如單片機的程序,右移時在原來位置補0, 且恆補0,在這種場合中,可以將相關變量或者值,定義或強轉成無符號數。
我們以 -32 >> 2 = -8爲例,說明右移。
(-32)原 = (1000 0000 0000 0000 0000 0000 0010 0000)原
(-32)補 = (1111 1111 1111 1111 1111 1111 1110 0000)補
-32 >> 2 = (1111 1111 1111 1111 1111 1111 1111 1000)補 = -8
/* 注意,我們在原來位置補的是1,因爲符號位爲1 */
Ⅳ 位運算的技巧
a.與運算
與運算的一個應用場合:子網掩碼
與運算可以從一個字節中取出指定位的數據。
比如x的補碼爲(b7 b6 b5 b4 b3 b2 b1 b0)補, 要取出b2 ,b1, b0的數據,只需要x & 7。即(b7 b6 b5 b4 b3 b2 b1 b0)補 & (0000 0111),結果就可以得到b2 ,b1, b0的數據。
b.或運算
或運算可以將一個字節中的指定位設置爲1
還是用x來舉例,比如我們要將x的b6,b3 ,b1設置爲1,則
(b7 b6 b5 b4 b3 b2 b1 b0) | (0100 1010)
= ((b7 1 b5 b4 1 b2 1 b0)
c.異或運算
異或運算可以將一個字節中的指定位設置爲其反
還是用x來舉例,比如我們要將x的b6,b3 ,b1設置爲其反,則
(b7 b6 b5 b4 b3 b2 b1 b0) ^ (0100 1010)
= (b7 !b6 b5 b4 !b3 b2 !b1 b0)
d.左移右移
計算機中,所有的運算都不如位運算快。
這裏提一個概念,計算機硬件中存在指令週期,一條指令的全過程稱爲一個指令週期,是由若干個時鐘週期組成,不同指令的指令週期不同。位運算指令週期基本都是1時鐘週期,四則運算基本都是16~32 時鐘週期,乘法運算大概是加減運算的2-4倍。
比如 x * 94 = x * (64 + 32 - 2) = x * 64 + x * 32 - x * 2
= (x << 6) + (x << 5) - (x << 1)
所需要的時鐘週期從16*3 = 48 變成了 1 + 1 + 1 + 16 + 16 = 35,雖然是微小的差距,但是在解決極致問題時能提供很多時間的節省空間。
Ⅴ 位運算的重要應用
位運算有三個基本操作,置位,清位及取位。其在比如哈夫曼壓縮解壓縮中十分重要,我會分別進行分析並將其寫成代碼,以便以後的使用。
a.置位
置位即將某一字節的指定位上置爲1。
根據之前的內容可知,我們可以用按位或運算來進行置位。設index爲需要置爲1的位,value爲需要置位的字節的十進制值,t爲value通過其置位的補碼的十進制值。
下標 | 0123 4567 |
---|---|
xxxx xxxx |
按位或👇
對下標爲0置位 | 1000 0000 |
---|---|
對下標爲1置位 | 0100 0000 |
對下標爲2置位 | 0010 0000 |
對下標爲3置位 | 0001 0000 |
以上的表就是對各個位置位的規律, 即(value | 1 << (7-index)),其中,
7-index = index ^ 7,各位可以自行驗證。所以最終,value |= 1 << (index ^ 7),即可完成value中對index位的置位。
我們可以將其寫成一個宏,寫到自己常用的頭文件裏。
#define SET(value, index) (value |= 1 << (index ^ 7))
b.清位
清位即將某一字節的指定位置爲0。
下標 | 0123 4567 |
---|---|
xxxx xxxx |
按位與👇
對下標爲0清位 | 0111 1111 |
---|---|
對下標爲1清位 | 1011 1111 |
對下標爲2清位 | 1101 1111 |
對下標爲3清位 | 1110 1111 |
同樣我們找出下標index與t的關係,可以得到:
value &= ~(1 << (index ^ 7))
#define CLEAR(value, index) (value &= ~(1 << (index ^ 7)))
c.取位
取位即得到某個字節的指定位的數據。
下標 | 0123 4567 |
---|---|
xxxx xxxx |
按位與👇
對下標爲0取位 | 1000 0000 |
---|---|
對下標爲1取位 | 0100 0000 |
對下標爲2取位 | 0010 0000 |
對下標爲3取位 | 0001 0000 |
所以取位所用的t和置位是一樣的,我們只需要增加一個邏輯判斷,即
(value) & (1 << (index ^ 7)) != 0
前面的位運算可以得到該位的值,後面!= 0的邏輯判斷,便可以將這個表達式的值變得和所要的位的值相同。
#define GET(value, index) (((value) & (1 << ((index) ^ 7))) != 0)
以上就爲位運算的分析及應用介紹,之後我會寫利用哈夫曼編碼將文件壓縮及解壓縮的程序,其中便會用到這三個宏。