資訊儲存與位元運算 (Information Storage & Bit Operations)

Overview Table

小節 主題 核心結論
2.1 資訊儲存 (p.70) 記憶體最小可定址單位是 byte(8 bits),程式看到的是 virtual memory 位元組陣列
2.1.1 十六進位表示法 (p.72) 1 hex digit = 4 bits;C 語言以 0x 前綴表示
2.1.2 資料大小 (p.75) word size w 決定 virtual address space 上限 2w bytes
2.1.3 定址與位元組順序 (p.78) little endian(低位在前)vs big endian(高位在前)
2.1.4 字串表示 (p.85) ASCII + null 結尾;文字資料比二進位資料跨平台
2.1.5 程式碼表示 (p.85) 機器碼是位元組序列,不可跨平台移植
2.1.6 布林代數 (p.86) ~ & | ^ 對 bit vector 逐位運算;可編碼有限集合
2.1.7 C 位元運算 (p.90) 適用所有整數型別;常用於 masking
2.1.8 C 邏輯運算 (p.92) && || ! 只回傳 0/1,且有 short-circuit 短路求值
2.1.9 C 移位運算 (p.93) 左移補 0;右移分 logical(補 0)與 arithmetic(補符號位)

2.1 資訊儲存:記憶體即位元組陣列 (p.70)

多數電腦不以個別 bit 為單位存取記憶體,而以 8-bit 的 byte 作為最小可定址單位 (smallest addressable unit)。機器層級程式將記憶體視為一個巨大的位元組陣列,稱為 virtual memory;每個 byte 由唯一編號 address 識別,所有可能位址的集合構成 virtual address space

Virtual Memory(概念圖:巨大位元組陣列)
address:  0x000  0x001  0x002  0x003  ...   0x100  0x101  ...  2^w - 1
         +------+------+------+------+     +------+------+
         | byte | byte | byte | byte | ... | byte | byte | ...
         +------+------+------+------+     +------+------+
              程式資料 / 指令 / 控制資訊 全部存於此陣列中
C 語言演進 (p.71)

Bell Labs C → ANSI C (1989)ISO C90ISO C99(引入固定大小型別如 int32_t)→ ISO C11。gcc 以 -std=c99-std=c11-ansi(= -std=c89)等選項切換標準;不加選項時預設為 GNU 89 混合版。

2.1.1 Hexadecimal Notation (p.72)

一個 byte 為 8 bits,二進位範圍 000000002111111112,十進位 0255,十六進位 0016FF16。二進位太冗長、十進位轉換麻煩,故以 hexadecimal(基底 16,數字 0-9 + A-F)描述 bit pattern。C 中以 0x0X 開頭,大小寫皆可(0xFA1D37B = 0xfa1d37b)。

Hex 0 1 2 3 4 5 6 7 8 9 A B C D E F
Dec 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Bin 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

轉換方法(常考手算):

心算技巧 (p.72)

只需背 A=10、C=12、F=15,其餘 B、D、E 可由相鄰值推得(B=A+1、D=C+1、E=F-1)。

2.1.2 Data Sizes (p.75)

每台電腦有一個 word size,表示 pointer 資料的標稱大小。因 virtual address 以一個 word 編碼,word size 決定最重要的系統參數——virtual address space 的最大值:w-bit 機器的位址範圍為 02w1,程式最多可存取 2w bytes。

C 宣告 (signed) unsigned 版本 32-bit 程式 64-bit 程式
[signed] char unsigned char 1 1
short unsigned short 2 2
int unsigned 4 4
long unsigned long 4 8
int32_t uint32_t 4 4
int64_t uint64_t 8 8
char * 4 8
float 4 4
double 8 8
char 的正負號未定 (p.77)

C 標準不保證 char 是 signed(雖多數編譯器如此)。需要 1-byte signed 值時必須明寫 signed char。另外 unsigned longunsigned long intlong unsignedlong unsigned int 為同義宣告。

移植陷阱 (p.78)

許多舊程式假設 int 可存放 pointer——32-bit 程式沒問題,但 64-bit 程式中 pointer 為 8 bytes、int 只有 4 bytes,遷移時產生隱藏 bug。跨型別大小的可攜寫法是使用 sizeof(T) 與固定大小型別。

2.1.3 Addressing and Byte Ordering (p.78)

跨多 byte 的物件需要兩個慣例:(1) 物件的位址 = 所占 bytes 中最小的位址,且多 byte 物件幾乎都存為連續位元組;(2) bytes 在記憶體中的排列順序。設 w-bit 整數 bit 表示為 [xw1,xw2,,x1,x0],most significant byte 為 [xw1,,xw8]、least significant byte 為 [x7,,x0]:

int x = 0x01234567,位址 &x = 0x100(int 為 4 bytes)

Big endian:     0x100  0x101  0x102  0x103
             ... | 01 | | 23 | | 45 | | 67 | ...   高位 byte 在前

Little endian:  0x100  0x101  0x102  0x103
             ... | 67 | | 45 | | 23 | | 01 | ...   低位 byte 在前

位元組順序通常對程式設計師不可見,但三種情境會浮現 (p.79-80):

  1. 網路傳輸:little-endian 與 big-endian 機器互傳二進位資料會反序,故網路程式必須依循 network standard 進行 byte order 轉換(見 11-Network-Programming/01-Client-Server-Model-and-Networks 對應章節)。
  2. 閱讀 disassembler 輸出:如 x86-64 指令 bytes 01 05 43 0b 20 00 中,後 4 bytes 43 0b 20 00 反序讀為 0x200b43——little-endian 機器上機器碼中的數值需反序解讀。
  3. 繞過型別系統:用 castunion 以不同型別檢視同一物件(系統程式設計常用,一般應用不建議)。

檢視位元組表示的工具程式 (p.80-82):typedef unsigned char *byte_pointer;,show_bytes(byte_pointer start, size_t len)printf(" %.2x", start[i]) 逐 byte 印出十六進位;size_t 是表達資料結構大小的首選型別。show_int / show_float / show_pointer 均以 (byte_pointer) &x cast 後傳入,長度用 sizeof(T) 取得——cast 不改變 pointer 的值,只改變編譯器解讀資料的方式;sizeof(T) 回傳型別 T 所需 bytes 數,是可攜性的關鍵。

實測 12345(hex 0x00003039)的結果 (p.82):

機器 int 12345 float 12345.0 int * &ival
Linux 32 (IA32) 39 30 00 00 00 e4 40 46 4 bytes,值機器相依
Windows (IA32) 39 30 00 00 00 e4 40 46 4 bytes,值機器相依
Sun (SPARC) 00 00 30 39 46 40 e4 00 4 bytes,值機器相依
Linux 64 (x86-64) 39 30 00 00 00 e4 40 46 8 bytes,值機器相依
New to C 重點 (p.77, 83-84)

  • T *p; 宣告指向型別 T 的 pointer;&x 取址建立 pointer,cast 只改變解讀方式;void * 是不帶型別資訊的特殊 pointer。
  • typedef 為型別命名,語法同變數宣告(名稱位置放型別名)。
  • pointer 與 array 可互換使用:start[i] 即讀取 start 所指位置後第 i 個 byte。
  • printf 格式:%d 十進位、%f 浮點、%c 字元、%.2x 至少 2 位十六進位。

2.1.4 Representing Strings (p.85)

C 字串 = 以 null 字元(值 0)結尾的字元陣列,每個字元以標準編碼表示,最常見為 ASCII

Unicode 與 UTF-8 (p.86)

ASCII 只夠編碼英文;Unicode 基底編碼(Universal Character Set)用 32-bit 表示字元,而 UTF-8 以變長位元組序列編碼,常見字元僅需 1-2 bytes,且 ASCII 字元在 UTF-8 中保持相同的單 byte 編碼——所有 ASCII 位元組序列在 UTF-8 下意義不變。Java 字串原生使用 Unicode。

2.1.5 Representing Code (p.85)

同一個 C 函式 int sum(int x, int y) { return x + y; } 在不同平台編譯出完全不同的機器碼位元組序列(Linux 32 / Windows / Sun / Linux 64 各異)。

2.1.6 Introduction to Boolean Algebra (p.86)

Boolean algebra(George Boole, ~1850)將邏輯值 true/false 編碼為 1/0;Claude Shannon 1937 年碩士論文首次將布林代數連結到數位邏輯(電機繼電器網路)的設計與分析。最簡單的布林代數定義於二元素集合 {0,1}:

運算 邏輯意義 真值表重點
~p(not, ¬) 反相 ~0=1、~1=0
p & q(and, ∧) 兩者皆 1 才是 1 1&1=1,其餘 0
p | q(or, ∨) 任一為 1 即 1 0|0=0,其餘 1
p ^ q(xor, ⊕) 恰一個為 1 才是 1 0^1=1^0=1;0^0=1^1=0

Bit vector 擴充:對長度 w 的位元向量 a=[aw1,,a0]b=[bw1,,b0],定義 (a&b)i=ai&bi(0i<w),|^~ 同理逐位擴充。例(w=4):0110 & 1100 = 01000110 | 1100 = 11100110 ^ 1100 = 1010~1100 = 0011

重要代數性質(p.88 Web Aside DATA:BOOL):

Bit vector 編碼有限集合 (p.88):子集 A{0,1,,w1} 編碼為 bit vector,ai=1iA。此時 | = 聯集 ∪、& = 交集 ∩、~ = 補集。例:[01101001] 編碼 A={0,3,5,6}。實際應用如 signal mask(以 bit i 表示 signal i 是否 enabled,見 08-Exceptional-Control-Flow/05-Signals 對應章節)、RGB 三 bits 表示 8 色 (Practice 2.9)。

2.1.7 Bit-Level Operations in C (p.90)

C 直接支援位元運算 | & ~ ^(符號與布林代數一致),可套用於任何整數型別("integral" data type)。求值訣竅:hex 展開成 binary → 逐位運算 → 轉回 hex

C 運算式 二進位運算 結果
~0x41 ~[0100 0001] [1011 1110] = 0xBE
~0x00 ~[0000 0000] [1111 1111] = 0xFF
0x69 & 0x55 [0110 1001] & [0101 0101] [0100 0001] = 0x41
0x69 | 0x55 [0110 1001] | [0101 0101] [0111 1101] = 0x7D

Masking(最常見用途,p.91):mask 是標示「選取哪些 bits」的 bit pattern。

XOR swap(inplace_swap, p.90;利用 a^a = 0、(a^b)^a = b):
          *x           *y
init:      a            b
step1:     a           a^b     (*y = *x ^ *y)
step2: a^(a^b) = b     a^b     (*x = *x ^ *y)
step3:     b       (a^b)^b = a (*y = *x ^ *y)
XOR swap 的陷阱 (p.91, Practice 2.11)

inplace_swap 反轉奇數長度陣列時,最後一輪 first == last,xy 指向同一位置:step 1 就把它變成 a ^ a = 0,中間元素被清成 0。修正法:迴圈條件改為 first < last。此技巧亦無效能優勢,純屬趣味。

VAX 的 bis / bic (p.92, Practice 2.13)

bis(x, m)(bit set)= x | m;bic(x, m)(bit clear)= x & ~m。由此可組合出 x | y = bis(x, y)x ^ y = bis(bic(x, y), bic(y, x))(即 (x&y)(x&y))。

2.1.8 Logical Operations in C (p.92)

C 的邏輯運算子 && || ! 對應邏輯上的 or、and、not,與位元運算行為截然不同,極易混淆:

面向 位元運算 & | ~ 邏輯運算 && || !
運算對象 逐 bit 整個值:非 0 = true,0 = false
回傳值 任意 bit pattern 只有 0x01(true)或 0x00(false)
求值方式 兩邊都求值 short-circuit:第一個引數已可決定結果時,不求值第二個

範例 (p.93):!0x41 = 0x00!0x00 = 0x01!!0x41 = 0x010x69 && 0x55 = 0x010x69 || 0x55 = 0x01

2.1.9 Shift Operations in C (p.93)

設 x 的 bit 表示為 [xw1,xw2,,x0]:

8-bit 範例 (p.94):
x = [01100011]                  x = [10010101]
x << 4            = [00110000]        [01010000]   右端補 0
x >> 4 (logical)  = [00000110]        [00001001]   左端補 0
x >> 4 (arith.)   = [00000110]        [11111001]   左端補 MSB
                    (MSB=0,兩種相同)   (MSB=1,結果不同!)
C 標準對 signed 右移未定義形式 (p.94)

C 標準沒有規定 signed 數該用算術或邏輯右移——依賴其一即有移植性風險。實務上幾乎所有 compiler/machine 組合對 signed 資料用算術右移;對 unsigned 資料則必須用邏輯右移。Java 則明確定義:x >> k 為算術、x >>> k 為邏輯右移。

移位量 k ≥ w (p.95)

C 標準未定義 kw 的行為。許多機器只取移位量的低 log2w bits,即實際移位量為 kmodw(例:w=32 時,<< 32>> 36>> 40 實為移 0、4、8 位)——但不保證,移位量務必小於 word size。Java 則明定採 kmodw

運算子優先權 (p.95)

1 << 2 + 3 << 4 會被解析為 (1 << (2+3)) << 4 = 512,而非直覺的 (1<<2) + (3<<4) = 52,因為加減法優先權高於移位。不確定就加括號!

Exam/Test Patterns

情境 / 關鍵字 答案
binary ↔ hex 轉換 每 4 bits 對 1 hex digit;bits 非 4 倍數時最左組補前導 0
2n 的 hex 表示 n=i+4j → 前導 digit 2i(1/2/4/8)+ j 個 0;211 = 0x800
decimal → hex 反覆 x=q16+r,餘數由低位往高位讀
w-bit word size 的最大 VA 空間 2w bytes(位址 02w1);32-bit → 4 GB、64-bit → 16 EB
64-bit 程式中 long / pointer 大小 各 8 bytes;int 仍為 4 bytes
int x = 0x01234567 在 little endian 位址 0x100 起的排列 67 45 23 01(低位 byte 在低位址)
給 show_bytes 輸出判斷 endianness 最低有效 byte 先印出 → little endian;最後印出 → big endian
disassembly 中讀出 little-endian 常數 位元組反序重組(43 0b 20 000x200b43)
字串 "12345" 的 bytes 31 32 33 34 35 00(ASCII 數字 x = 0x3x,null 結尾);與 endianness、word size 無關
取 x 最低 byte / 全 1 mask x & 0xFF;~0(可攜,與 word size 無關)
a ^ a 的值;(a ^ b) ^ a 0;b(Boolean ring:元素為自身加法反元素)
XOR swap 對同一位址呼叫 元素被清成 0(奇數長度陣列反轉 bug;改用 first < last)
!x vs ~x !0x41=0x00(邏輯);~0x41=0xBE(位元逐位反相)
a && 5/a 為何安全 && short-circuit:a 為 0 時不求值 5/a
x == y 只用位元/邏輯運算 !(x ^ y)
unsigned 的 >> 必為 logical shift(補 0)
signed 的 >> C 未定義;實務上幾乎都是 arithmetic shift(補 MSB)
shift 量 ≥ w 未定義行為;許多機器算 kmodw,但不可依賴
1 << 2 + 3 << 4 的值 512(加減法優先權高於移位;移位左結合)
bit vector 表示集合時 | & ~ 聯集、交集、補集
bis / bic 對應 C 運算 bis(x,m) = x | m;bic(x,m) = x & ~m