位運算是指按二進制進行的運算。在系統軟件中,常常需要處理二進制位的問題。C語言提供了6個位操作運算符,這些運算只能用於整型操作數,即只能用於帶符號或無符號的char、short、int與long類型。浮點數因為浮點型和整型在計算機內的存儲方式大相徑庭,同樣是32位。但是浮點數是1位表示符號位,23位表示數值部分,8位其他表示指數部分。而整型只是單純32位補碼形式存放的,這就是位運算不能用於浮點數的原因。
1、“按位與”運算符(&)
按位與是指:參加運算的兩個數據,按二進制進行“與”運算。如果兩個相應的二進制位都位1,則該位的結果為1;否則為0。這里的1的可以理解為邏輯中的true,0可以理解為邏輯的false。按位與其實與邏輯上“與”的運算規則一致。邏輯上的“與”,要求運算數全真,結果才為真。若A=true, B=true,則A∩B=true 例如:3&5, 3的二進制編碼是11(2)。(為了區分十進制和其他進制,本文規定,凡是非十進制的數據均在數據后面加上括號,括號中注明其進制,二進制則標記為2,內存儲存數據的基本單位是字節(Byte),一個字節由8個位(bit)所組成。位是用以描述電腦數據量的最小單位。二進制系統中,每個0或1就是一個位。將11(2)補足成一個字節,則是00000011(2)。5的二進制編碼是101(2),將其補足稱一個字節,則00000101(2)。
按位與運算:
0000 0011(2) & 000000101(2) = 00000001(2)
由此可知3&5 = 1。
C語言代碼:
1 #include <stdio.h> 2 3 int main(void) { 4 int a = 3, b = 5; 5 printf("a and b: %d\n", a & b); //0011 & 0101 6 return 0; 7 }
CPU處理位運算的速度是最快的,所以很多操作我們都可以轉換為位運算,以下是用按位與替換取模運算進行奇偶數判斷。
1 /************************************************************************* 2 > File Name: 2.test.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Sun 18 Oct 2020 10:19:55 PM CST 6 ************************************************************************/ 7 #include <stdio.h> 8 9 int main(void) { 10 int n; 11 printf("please input a integer:"); 12 while (scanf("%d", &n) != EOF) { 13 //if(n % 2) { 14 if (n & 1) { 15 printf("Odd number\n"); //奇數 16 } else { 17 printf("Even number\n");//偶數 18 } 19 printf("please input a integer:"); 20 } 21 return 0; 22 }
編譯運行,並輸入數字來測試,結果如下:
ydqun@VM-0-9-ubuntu 20201017 % g++ 2.test.c [0] ydqun@VM-0-9-ubuntu 20201017 % ./a.out [0] please input a integer:3 Odd number please input a integer:2 Even number please input a integer:% ydqun@VM-0-9-ubuntu 20201017 %
14行就是用&運算符去代替%運算符實現奇偶數判斷,這樣效率更快。
2.按位或運算符
兩個相應的二進制位中只要有一個為1,該位的結果值為1。借用邏輯學中或運算的話來說就是,一真為真。
例如 十進制6對應二進制0000 0110與8對應的二進制0000 1000進行或運算,0000 0110 | 0000 1000 = 0000 1110,對應十進制的14。
測試的c語言代碼。
1 /************************************************************************* 2 > File Name: 3.test.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Mon 19 Oct 2020 05:14:44 PM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int main(void) { 11 int n = 8, m = 6; 12 printf("%d | %d = %d\n", n, m, n | m); 13 return 0; 14 }
3.異或運算
異或是一個數學運算,用於邏輯運算。如果a、b兩個值不同,則異或結果為1,否則結果為0,在C語言中是一種強大的基本運算符,有很多巧妙的應用。
例如, A = 14, B = 10;
A = 14,二進制則為1110,B = 10,二進制為1010.
對二進制數進行異或運算 -> 1110^1010 = 0100,對應十進制就為4。
在二進制數異或過程中,我們可以得知異或運算是一種半加運算。什么意思呢?半加即不帶進位的加法運算。
如上面1110^1010 = 0100,如果在加法中,如下
我們從低位開始加起,首先第0位為0+0=0;第一位1+1=0,如果是加號運算符,則需要進位,但由於是異或運算(半加),故不用進位,第二位為1+0=1;最后一位為1+1=0。最終結果就是0100,這就是半加的過程。
特性
1.一個數與0進行異或運算,其運算結果是自身;
2.一個數與自身進行異或運算,其運算結果為0;
3.異或運算滿足分配律,即 3^4^3與3^3^4的結果一樣,都為4。
異或運算的一些應用
1.異或最常用的一種用法 -- 交換兩個數的值。
這里直接上代碼。
1 /************************************************************************* 2 > File Name: swap.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Fri 16 Oct 2020 04:48:54 PM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int main(void) { 11 int a = 10, b = 20, tmp; 12 13 #if 0 14 /*用中間值來實現值交換*/ 15 tmp = a; 16 a = b; 17 b = tmp; 18 #else 19 /*用異或操作來實現值交換*/ 20 a ^= b; 21 b ^= a; 22 a ^= b; 23 #endif 24 printf("a: %d, b: %d\n", a, b); 25 26 return 0; 27 }
ydqun@VM-0-9-ubuntu operator % gcc swap.c [0] ydqun@VM-0-9-ubuntu operator % ./a.out [0] a: 20, b: 10 ydqun@VM-0-9-ubuntu operator %
這里異或操作實現的值交換的好處是少使用了一個臨時變量,執行效率也比較高。
2,尋找只出現一次的數字。
給定一個非空整數數組,除了某個元素只出現一次以外,其余每個元素均出現兩次。找出那個只出現了一次的元素。
說明:
你的算法應該具有線性時間復雜度。 你可以不使用額外空間來實現嗎?
示例 1:
輸入: [2,2,1]
輸出: 1
示例 2:
輸入: [4,1,2,1,2]
輸出: 4
1 /************************************************************************* 2 > File Name: single_number.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Fri 16 Oct 2020 05:20:25 PM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int single_number(int nums[], int n) { 11 int res = 0; 12 for (int index = 0; index < n; index++) { 13 res ^= nums[index]; 14 } 15 return res; 16 } 17 18 int main(void) { 19 int nums[5] = {1, 2, 2, 1, 6}; 20 printf("single number is : %d\n", single_number(nums, 5)); 21 }
ydqun@VM-0-9-ubuntu operator % g++ single_number.c [0] ydqun@VM-0-9-ubuntu operator % ./a.out [0] single number is : 6 ydqun@VM-0-9-ubuntu operator %
這里是運用了異或的特性2與特性3,1^2^2^1^6 = 1^1^2^2^6 = 0^0^6 = 6。
4.按位取反運算
按位取反運算符是把一個數的二進制照着每個位取反,即值為0的位變為1,值1的位變為0,但是我們要注意的是,要結合二進制數在內存中是以補碼的形式存儲的情況一起分析(不知道補碼概念請看https://www.cnblogs.com/ydqblogs/p/13823206.html),接下來我們以10按位取反為例子。
假設我們有一個整型變量x = 10,在計算機中一個整型樹為4個字節,而一個字節為8位,所以數字10在計算機中存儲占32位,且為補碼存儲在內存中,即
原 碼:00000000 00000000 00000000 00001010
補 碼:00000000 00000000 00000000 00001010
按位取反:11111111 11111111 11111111 11110101
這時候,“~10”的二進制數的最高位是1表示它是一個負數,它是在內存的存儲時的值(補碼),我們需要求回原碼。由補碼求原碼的操作跟由原碼求補碼的操作是一樣的。
接着上述:11111111 11111111 11111111 11110101
反 碼:10000000 00000000 00000000 00001010
補 碼:10000000 00000000 00000000 00001011
二進制10000000 00000000 00000000 00001011對應十進制是-11,這時候我們推導出~10=-11,我們可以寫一個程序測試一下我們的推導結果。
1 /************************************************************************* 2 > File Name: bit_reverse.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Tue 20 Oct 2020 09:25:50 AM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int main(void) { 11 printf("%d\n", ~10); 12 return 0; 13 }
ydqun@VM-0-9-ubuntu 20201017 % g++ bit_reverse.c [0] ydqun@VM-0-9-ubuntu 20201017 % ./a.out [0] ~10 = -11
由測試程序得出的結果,我們可以驗證我們的推導是正確的,所以我們可以看出一個規律:正整數x,~x = -(x+1)。接下來我們來推導一下負整數按位取反的結果。
假設我們有一個負數-10,我們來推導一下按位取反后的結果。
原 碼:10000000 00000000 00000000 1010
反 碼:11111111 11111111 11111111 0101
補 碼:11111111 11111111 11111111 0110
按位取反:00000000 00000000 00000000 1001
我們發現負數在內存中存儲的值在按位取反,符號位為0,即變為了正數,我們知道正數的補碼和原碼是一樣的,在內存中的補碼就是在程序中實際的原碼,由此得到:對於負數-x,~(-x) = x-1。同樣我們可以寫一個測試程序。
ydqun@VM-0-9-ubuntu 20201017 % g++ bit_reverse.c [1] ydqun@VM-0-9-ubuntu 20201017 % ./a.out [0] ~(-10) = 9
至此我們可以得出一個結論。
對於正整數x,~x = -(x + 1);
對於負整數-x, ~(-x) = (x - 1)。
5.左移運算符 -- <<
左移運算符是一個計算機用語,用來將一個數的各二進制位全部左移若干位,移動的位數由右操作數指定,右操作數必須是非負值,其右邊空出的位用0填補,高位左移一處則舍棄該高位。
接下來,我們來各自分析正數和負數的左移過程。
假設我們有一個正整型數x,值為2,我們要分析它左移2位后的值為多少,即求 2 << 2。
原 碼:00000000 00000000 00000000 00000010
補 碼:00000000 00000000 00000000 00000010
左移兩位:00000000 00000000 00000000 00001000
二進制數00000000 00000000 00000000 00001000對應的十進制值為8,由此我們得知2 << 2 = 8,由此,我們能得到對於一個正整數x,每左移一位,x的值就變為原來的2倍。那么負整數呢?左移運算結果又是多少?其實跟正整型數是一樣的結果,只不過我們在左移運算的時候要關心符號位,即左移不影響符號位,移動的都是數據域。
假設我們有一個十六進制的負整型數0xc0000001,對應十進制值為-2147483646,我們要求左移一位后該數值為多少?
原 碼:11000000 00000000 00000000 00000001
反 碼:10111111 11111111 11111111 11111110
補 碼:10111111 11111111 11111111 11111111
左移一位:11111111 11111111 11111111 11111110
反 碼:10000000 00000000 00000000 00000001
原 碼:10000000 00000000 00000000 00000010
由此我們得到0xc0000001左移一位后值為0x80000002。我們可以寫一個程序測試一下。
1 /************************************************************************* 2 > File Name: left_shift.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Tue 20 Oct 2020 01:29:00 PM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int main(void) { 11 int num = 0xc0000001; 12 printf("0x%x << 1 = 0x%x\n", num, num << 1); 13 return 0; 14 }
ydqun@VM-0-9-ubuntu 20201017 % ./a.out [0] 0xc0000001 << 2 = 0x80000002 ydqun@VM-0-9-ubuntu 20201017 %
在這里,關於一個數左移一位為什么是原來數的兩倍,我這里有個人的理解,首先,有一個名詞叫位的權值,對於二進制數,每一位的權值為2n ,其中n表示第幾位,所以我們得知第i位的權值是2i ,而第i+1為的權值為2i+1,所以往左移動一位,權值變為原來的2倍,則數的值也為原來的兩倍。十進制數也是同樣的道理。
5.右移運算符 -- >>
右移運算符和左移運算符大致一樣,唯一區別就是方向不同,另外對於有符號整型數右移,左邊是補符號位;而對於無符號整型數右移,左邊補0。在這里我們只分析有符號的負整型數。
假設我們有一個x值為-10,我們把其右移兩位。
原 碼:10000000 00000000 00000000 00001010
反 碼:11111111 11111111 11111111 11110101
補 碼:11111111 11111111 11111111 11110110
右移兩位:11111111 11111111 11111111 11111101 -- 因為是有符號且為負數,所以補1。
反 碼:10000000 00000000 00000000 00000010
原 碼:10000000 00000000 00000000 00000011
由上述分析結果得(-10) >> 2 = -3。我們可以寫測試程序測試一下結果。
1 /************************************************************************* 2 > File Name: right_shift.c 3 > Author: yudongqun 4 > Mail: qq2841015@163.com 5 > Created Time: Tue 20 Oct 2020 04:33:18 PM CST 6 ************************************************************************/ 7 8 #include <stdio.h> 9 10 int main(void) { 11 int num = -10; 12 printf("%d >> 2 = %d\n", num, num >> 2); 13 return 0; 14 }
ydqun@VM-0-9-ubuntu 20201017 % g++ right_shift.c [0] ydqun@VM-0-9-ubuntu 20201017 % ./a.out [0] -10 >> 2 = -3
到了這里,我們已經學習完C語言的所有位運算符,下面我們來給出一些位運算的綜合運用。
6、位運算的綜合運用