位元運算是 C 貼近硬體的招牌能力:驅動程式的暫存器操作、網路協定的欄位打包、 權限旗標、雜湊函式、壓縮與加密,處處都是位元運算。本單元對應 K&R §2.9, 從六個運算子出發,系統整理遮罩、位移、欄位擷取,以及一批面試與實務都常見的位元技巧。
學習目標
- 熟練六個位元運算子:
&、|、^、~、<<、>> - 會用遮罩(mask)設定、清除、切換、測試特定位元
- 理解位移與 2 的冪次乘除的關係,會用位移擷取欄位(K&R
getbits) - 掌握常見位元技巧:popcount、判斷 2 的冪、XOR 交換、位元旗標模式
- 能寫程式檢測機器的位元組序(endianness)
- 知道位元運算的未定義行為地雷(負數位移、過量位移)
對照章節 / 參考來源
- K&R §2.9 Bitwise Operators(
getbits範例) - C11 標準 §6.5.7(位移運算子)、§6.5.10–6.5.12(AND/XOR/OR)
- 《Hacker's Delight》(Henry S. Warren):位元技巧的百科全書
- Bit Twiddling Hacks(Sean Eron Anderson)
觀念講解
1. 六個運算子複習
| 運算子 | 名稱 | 例(8 位元) | 用途 |
|---|---|---|---|
& |
AND | 0b1100 & 0b1010 = 0b1000 |
取交集、清除位元、取遮罩內欄位 |
\| |
OR | 0b1100 \| 0b1010 = 0b1110 |
取聯集、設定位元 |
^ |
XOR | 0b1100 ^ 0b1010 = 0b0110 |
切換位元、比較差異、同值歸零 |
~ |
NOT(一補數) | ~0b0001 = 0b1110 |
反轉全部位元、製作反遮罩 |
<< |
左移 | 0b0011 << 2 = 0b1100 |
乘 2 的冪、製作遮罩 |
>> |
右移 | 0b1100 >> 2 = 0b0011 |
除 2 的冪、對齊欄位 |
注意 &&/|| 是邏輯運算子,&/| 才是位元運算子;^ 沒有邏輯版本。
位元運算的優先權低於比較運算子:x & 1 == 0 其實是 x & (1 == 0),務必加括號。
2. 遮罩四式:設定 / 清除 / 切換 / 測試
以「第 n 位」(bit 0 為最低位)為例:
x |= (1u << n); /* set :把第 n 位設為 1 */
x &= ~(1u << n); /* clear :把第 n 位清為 0 */
x ^= (1u << n); /* toggle :把第 n 位反轉 */
bit = (x >> n) & 1u; /* test :讀出第 n 位 */
ASCII 圖(set bit 3):
x = 0 1 0 0 0 1 0 1
1u << 3 = 0 0 0 0 1 0 0 0
x | mask = 0 1 0 0 1 1 0 1
^ 只有這一位被影響
用 1u 而非 1:1 << 31 在 32 位 int 上是 signed overflow(UB),無號常量才安全。
3. 位移技巧與欄位擷取
x << k等於x * 2^k;x >> k對無號數等於x / 2^k(向下取整)。- 負的有號數右移是「實作定義」行為(多數平台是算術右移,但別依賴它做可攜程式)。
- 製作「低 n 位全 1」的遮罩:
(1u << n) - 1,如 n=4 得0b1111。
K&R §2.9 的 getbits:取出 x 中「從位置 p 開始、往低位數 n 位」的欄位:
/* getbits(x, 4, 3) -> 取出 bit4 bit3 bit2 */
unsigned getbits(unsigned x, int p, int n)
{
return (x >> (p + 1 - n)) & ~(~0u << n);
}
拆解:x >> (p+1-n) 把欄位平移到最低位;~(~0u << n) 做出低 n 位為 1 的遮罩。
這正是硬體暫存器讀取、網路封包解析的基本功。
4. 實用技巧精選
/* popcount:數 1 的個數(Kernighan 法,每輪消掉最低位的 1) */
int count = 0;
while (x) { x &= x - 1; count++; }
/* 判斷 2 的冪:2 的冪只有一個 1,x & (x-1) 必為 0 */
is_pow2 = x != 0 && (x & (x - 1)) == 0;
/* 取出最低位的 1(isolate lowest set bit) */
lowest = x & -x; /* 對無號數等價於 x & (~x + 1) */
/* XOR 交換(不用暫存變數;現代編譯器下無效能優勢,屬教學/面試題) */
a ^= b; b ^= a; a ^= b;
/* 位元旗標(flags)模式:一個整數存多個布林開關 */
enum { F_READ = 1u << 0, F_WRITE = 1u << 1, F_EXEC = 1u << 2 };
perm |= F_READ | F_WRITE; /* 開兩個旗標 */
if (perm & F_EXEC) { /* ... */ } /* 測試 */
實務對照:open(path, O_RDWR | O_CREAT | O_TRUNC, 0644) 的第二個參數、
fcntl、mmap、pthread 的屬性旗標,全部都是這個模式。
5. 位元組序(Endianness)檢測
多位元組整數在記憶體中的排列方式:
uint32_t v = 0x01020304;
little-endian (x86, Apple Silicon): 04 03 02 01 (低位元組在低位址)
big-endian (網路序, SPARC): 01 02 03 04 (高位元組在低位址)
檢測法:用 unsigned char* 檢視整數的第一個位元組(合法:char 指標可以別名任何物件):
uint32_t v = 1;
int little = *(unsigned char *)&v == 1;
網路程式必須用 htonl/ntohl 系列在主機序與網路序(big-endian)之間轉換。
範例程式
| 檔案 | 內容 | 編譯 / 執行 |
|---|---|---|
01_masks_getbits.c |
六運算子總覽、遮罩四式、K&R getbits/setbits、二進位印出工具 | cc -std=c11 -Wall -Wextra -o out 01_masks_getbits.c && ./out |
02_bit_tricks.c |
popcount 三種寫法、2 的冪、XOR 交換、最低位 1、位元反轉 | cc -std=c11 -Wall -Wextra -o out 02_bit_tricks.c && ./out |
03_flags_endian.c |
位元旗標權限系統實作、endianness 檢測與位元組傾印 | cc -std=c11 -Wall -Wextra -o out 03_flags_endian.c && ./out |
常見錯誤與陷阱
- 優先權:
x & 1 == 0恆為x & 0即 0——==比&先算。 位元運算混搭比較時一律加括號:(x & 1) == 0。 - 有號數位移的 UB:
1 << 31(int 溢位)、負數左移、位移量 ≥ 型別寬度 (如x << 32對 32 位 x)都是未定義行為。遮罩運算一律用無號型別。 ~與!混用:!x是邏輯非(結果 0/1),~x是位元反轉;if (~x)幾乎永遠為真(只有 x 全 1 時例外),通常是寫錯。- 右移補位假設:有號負數
>>是補 0 還是補符號位由實作決定; 要可攜就先轉成無號再移。 - XOR 交換自己:
swap(&a, &a)用 XOR 版本會把值清成 0, 泛用函式請用暫存變數版本。
練習題
- 【易】寫
print_binary(unsigned x),用位移與遮罩從最高位印到最低位,不用任何函式庫。 - 【易】實作 K&R 習題 2-6
setbits(x, p, n, y):把 x 中位置 p 起的 n 位換成 y 的低 n 位。 - 【中】實作 K&R 習題 2-7
invert(x, p, n):反轉指定欄位;及 2-8rightrot(x, n):循環右移。 - 【中】寫一個 RGB565 顏色打包/解包器:把 8 位元 r、g、b 壓進 16 位元 (r 佔 5 位、g 佔 6 位、b 佔 5 位),再解回近似值。
- 【難】不用迴圈實作 32 位元 popcount(分治法:先兩兩相加、再四四相加……)。
- 【難】實作
next_pow2(uint32_t x):回傳 ≥ x 的最小 2 的冪 (提示:連續右移 OR:x |= x>>1; x |= x>>2; ...)。
補充:實務場景速寫
硬體暫存器操作(嵌入式的日常):
#define UART_ENABLE (1u << 0)
#define UART_PARITY (1u << 3)
volatile uint32_t *ctrl = (uint32_t *)0x4000C000; /* memory-mapped 暫存器 */
*ctrl |= UART_ENABLE; /* 開啟裝置,不動其他位元 */
*ctrl &= ~UART_PARITY; /* 關掉 parity */
volatile 告訴編譯器每次都真的讀寫記憶體(暫存器可能被硬體改變),
與位元遮罩是嵌入式開發的固定搭檔。
網路封包欄位打包:IPv4 表頭的 version 與 IHL 擠在同一個位元組,
(byte >> 4) & 0xF 取版本、byte & 0xF 取表頭長度——就是 getbits。
權限與旗標:Unix 的 st_mode(檔案型別 + 9 個權限位)、
open 的 O_*、mmap 的 PROT_*,全是本單元的旗標模式。
編譯器內建函式:手寫 popcount 是為了理解原理;
生產程式直接用 __builtin_popcount(x)、__builtin_clz(x)(前導零)、
__builtin_ctz(x)(尾隨零),編譯器會產生單一機器指令(POPCNT/CLZ)。
延伸閱讀
- K&R §2.9 與習題 2-6 ~ 2-9
- 《Hacker's Delight》第 2、5 章(基本技巧與 popcount)
- Bit Twiddling Hacks
- GCC/Clang 內建函式:
__builtin_popcount、__builtin_clz、__builtin_ctz - RFC 1700 /
man 3 byteorder(網路位元組序)