移位運算(<<, >>)看似簡單,實則暗藏符號位擴展、未定義行為、硬體相關性等細節。本文通過三組實例、記憶體檢視代碼、循環移位宏,系統梳理 C 語言移位的完整知識體系。
範例(8-bit):
+3 = 0000 0011
-3 = 1111 1101 (補碼)
255 = 1111 1111 (無符號) / -1 (有符號)
| 表達式 | 類型 | 二進制過程 | 結果 | 說明 |
|---|---|---|---|---|
x << 1 |
unsigned char x=3 |
00000011 → 00000110 | 6 | 左移補 0,無符號/有符號同規則 |
x >> 1 |
unsigned char x=3 |
00000011 → 00000001 | 1 | 邏輯右移:無符號必補 0 |
x << 1 |
char x=3 (有符號) |
00000011 → 00000110 | 6 | 左移同無符號 |
x >> 1 |
char x=3 |
00000011 → 00000001 | 1 | 多數機器用算術右移(補符號位 0),但標準未規定! |
x << 1 |
char x=-3 |
11111101 → 11111010 | -6 | 左移補 0,等價乘 2(無溢出時) |
x >> 1 |
char x=-3 |
11111101 → 11111110 | -2 | 算術右移:補符號位 1,等價除 2 向下取整 |
int x; x >> 32 在 32-bit int 上為 UB。void print_bits(unsigned char x) {
for (int i = 7; i >= 0; --i)
putchar((x & (1 << i)) ? '1' : '0');
putchar('\n');
}
// 測試
int main() {
char a = 195; // 1100 0011 (有符號 -61)
print_bits(a);
char b = a << 2; // 0000 1100 = 12
print_bits(b);
printf("%d\n", b); // 輸出 12
return 0;
}
原理:左循環 n 位 = (x << n) | (x >> (WIDTH - n)),右循環同理。WIDTH = 8 * sizeof(x)。
// 通用宏(支援任意整數類型)
#define ROTL(x, n) (((x) << (n)) | ((x) >> ((sizeof(x)*8) - (n))))
#define ROTR(x, n) (((x) >> (n)) | ((x) << ((sizeof(x)*8) - (n))))
// 手動展開版(教學用,以 8-bit 為例)
unsigned char rotl8(unsigned char x, unsigned n) {
n %= 8;
return (x << n) | (x >> (8 - n));
}
unsigned char rotr8(unsigned char x, unsigned n) {
n %= 8;
return (x >> n) | (x << (8 - n));
}
// 測試:0x45 (0100 0101) 左循環 2 位 → 0x15 (0001 0101)
int main() {
unsigned char a = 0x45;
unsigned char b = rotl8(a, 2);
printf("0x%02X\n", b); // 輸出 0x15
return 0;
}
| 陷阱 | 後果 | 規避 |
|---|---|---|
| 有符號右移假設為算術右移 | 跨平台/編譯器行為不一 | 需算術右移時,轉為無符號後手動實現:(int)((unsigned)x >> n) 不可行;正確寫法見下 |
| 移位量為負或過大 | UB,程式可能崩潰或產生任意值 | 斷言 0 <= n < WIDTH |
| 左移導致有符號溢出 | UB(如 1 << 31 在 32-bit int) | 用無符號字面值 1u << 31 或 uint32_t |
| 宏參數副作用 | ROTL(x++, 1) 導致 x 增加兩次 | 宏參數加括號,或改用 inline 函數 |
// 假設雙補碼、算術右移(幾乎所有現代平台)
// 若需嚴格標準合規,可檢測編譯器行為或用條件編譯
int arithmetic_right_shift(int x, int n) {
// 利用無符號右移 + 符號位擴展模擬
if (x >= 0) return (unsigned)x >> n;
// 負數:先無符號右移,再將高 n 位置 1
return -((~(unsigned)x) >> n) - 1; // 較複雜,實際多直接依賴編譯器
}
建議:在明確支援的平台(x86, ARM, RISC-V 等)直接使用 >>,並在文檔註明假設;編寫跨平台庫時,提供配置選項或編譯期檢測。
移位運算是系統級編程的基石。牢記:左移補 0,無符號右移補 0,有符號右移視實現而定(通常算術右移),移位寬度不可超位寬,有符號左移溢出為 UB。循環移位雖無硬體指令,但通過「左右移結合或運算」極易實現。掌握這些細節,能避免 90% 的移位相關 Bug。