【C語言基礎】->位運算詳細解析->位運算的使用

Ⅰ 位運算的定義

我們知道程序中的所有數據,都是以二進制的方式存儲在計算機中的。位運算就是基於二進制的位進行的運算,直接對整數的二進制位進行操作。因此考慮位運算有兩個錨點:二進制,補碼
注意:計算機中所有的運算都沒有位運算快

Ⅱ 位運算的符號

C語言中有以下六種位運算:

  1. ~ : 按位取反(單目運算)
  2. & : 按位與
  3. | : 按位或
  4. ^ : 按位異或
  5. << : 左移
  6. >> : 右移

Ⅲ 位運算的驗證及分析

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)

以上就爲位運算的分析及應用介紹,之後我會寫利用哈夫曼編碼將文件壓縮及解壓縮的程序,其中便會用到這三個宏。

發佈了27 篇原創文章 · 獲贊 77 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章