前言
C 語言寫久了發現每次宣告個變數都要指定其型態相當麻煩,除了要打很多英文字母之外、也很常忘記到底一個int
是佔幾個 byte 之類。還有就是相關的變數想要擺在一起的狀況,尤其要傳給某 function 時要寫一長串參數真的頗麻煩。
本篇將介紹 typedef、struct、與 union,妥善使用可大幅提升寫程式效率及易讀性,以下介紹一下各自的功能。
typedef
這個絕對是節省打字數的一大幫手。例如定義一個unsigned char
為U8
型態:
typedef unsigned char U8;
現在想要宣告一個以下變數:
unsigned char number;
以後只要寫成這樣就 OK,簡單又明瞭。
U8 number;
不能再更厲害。你說這沒什麼,不過就少打那幾個字。
欸這尤其對指標變數、或者 Callback 函式宣告特別好用,而且特別不容易出錯,例如:
typedef unsigned int U8, *U8_PTR; //這樣寫U8_PTR就會是「U8指標」型態
typedef U8* MyFunc_PTR(U8, U8); //此函式return一個「U8的指標(就是U8_PTR)」
typedef U8 (*MyFunc_PTR)(U8, U8); //此函式return一個「U8」數值
如此一來就不會犯下這種錯誤。
U8 * num_ptr1, num_ptr2; //此時num_ptr2是普通的U8而非指標
U8_PTR num_ptr1, num_ptr2; //確保num_ptr1、num_ptr2皆為U8_PTR
struct
C 語言跟某一區段記憶體區塊的處理息息相關,例如開一個陣列 Array 將一連續記憶體空間切割成相同大小;而 Struct 則是用來將一連續記憶體空間切割成大大小小的命名區塊。
舉例定義一個新形態命名為SCSI_CDB
:
struct SCSI_CDB
{
BYTE opc;
BYTE evpd : 1; // occupy 1 bit
BYTE cmddt : 1; // occupy 1 bit
BYTE rsv : 6; // occupy 6 bits
BYTE page_code;
WORD alloc_len;
BYTE control;
};
請留意位元組順序(Byte Order 或 Endianness),這會影響 2 Bytes 以上的格子分布情形。
Bit Field
如圖所示,使用 Struct 甚至可以切割出以 bit 為單位大小的格子,只要在變數名稱後面加上:
以及所需bit數
即可(注意:bit 數不能超過所宣告的型態,例如宣告一個 U8,最多就只能用 8 bits)。
注意:這裡寫的都是參考值,如果情況允許編譯器會照做,但有時他會按自己喜歡的方式來決定每個欄位的大小(通常整體 Struct 大小會增加到 2N Bytes),想要強制對齊可以使用以下兩種方法:
- struct FOO {/*your struct fields*/}__attribute__((packed));
- #pragma pack(push, 1)
// your structs
#pragma pack(pop)
搭配 typedef
改寫同樣 Struct。
typedef struct _SCSI_CDB
{ /* your struct fields */
// ...
} SCSI_CDB, *SCSI_CDB_PTR;
此處_SCSI_CDB
可寫可不寫,僅供辨識用。
union
在同一連續記憶體空間,想要在不同時候採用不同切法(一般型態、Array 或 Struct 皆可)時就可以使用 union,其所佔的實際大小(即 sizeof 的值)將由最肥的那一組來決定。
舉例定義一個新形態命名為SCSI_CDB
:
union SCSI_CDB
{
struct {
BYTE opc;
BYTE evpd : 1;
BYTE cmddt : 1;
BYTE rsv : 6;
BYTE page_code;
WORD alloc_len;
BYTE control;
} inquiry; // 6 bytes
struct {
BYTE opc;
BYTE obs : 2;
BYTE rarc : 1;
BYTE fua : 1;
BYTE dpo : 1;
BYTE rd_protect : 3;
DWORD slba;
BYTE grp : 5;
BYTE rsv : 3;
WORD lba_len;
BYTE control;
} read10; // 10 bytes
};
此例中 SCSI_CDB 這個 Union 結構的大小就是 10 Bytes,其中前 6 個 Byte 會被兩個匿名 Struct 所共用。覺得一個 Union 寫起來太長可以把各 Struct 拆出來寫變成這樣
struct SCSI_INQUIRY {/* ... */};
struct SCSI_READ10 {/* ... */};
union SCSI_CDB
{
SCSI_INQUIRY inquiry; // 6 bytes
SCSI_READ10 read10; // 10 bytes
};
當然,也可以混用其他型態隨你玩。
union SCSI_CDB
{
long long dummy;
U8 bCDB[16];
struct {
BYTE opc;
BYTE rsv;
BYTE page_code;
WORD alloc_len;
BYTE control;
} inquiry; // 6 bytes
};
搭配 typedef
改寫同樣 Union。
typedef struct _SCSI_INQUIRY {/* ... */} SCSI_INQUIRY, *SCSI_INQUIRY_PTR;
typedef struct _SCSI_READ10 {/* ... */} SCSI_READ10, *SCSI_READ10_PTR;
typedef union _SCSI_CDB
{
SCSI_INQUIRY inquiry; // 6 bytes
SCSI_READ10 read10; // 10 bytes
} SCSI_CDB, *SCSI_CDB_PTR;
此處_SCSI_INQUIRY
、_SCSI_READ10
、及_SCSI_CDB
皆可寫可不寫,僅供辨識用。
改善範例:以填 SCSI CDB 為例
在傳送一些 SCSI Command 的時候,原本的 Code 都是直接填 CDB 格子,但總覺得不夠直觀,於是紀錄一下可行的改善法。
Before
從 SCSI Command 的 Sample Code 中可以簡單這樣使用,例如一個 Inquiry、及一個 Read(10)。
// Inquiry
unsigned char cdb[6] = {0};
cdb[0] = 0x12; // opcode
cdb[2] = 0; // page_code
cdb[3] = (512 >> 8) & 0xFF; // Upper bytes of allocate_length
cdb[4] = (512) & 0xFF; // Lower bytes of allocate_length
// Read
unsigned char cdb[10] = {0};
cdb[0] = 0x28; // opcode
cdb[2] = (2048 >> 24) & 0xFF; // MSB of starting_lba
cdb[3] = (2048 >> 16) & 0xFF;
cdb[4] = (2048 >> 8) & 0xFF;
cdb[5] = (2048 >> 0) & 0xFF; // LSB of starting_lba
cdb[7] = (1 >> 8) & 0xFF; // Upper bytes of read_length
cdb[8] = (1) & 0xFF; // Lower bytes of read_length
根據 Spec 可以知道每個 Command 所對應到的同一個位子有不同的意思,甚至長度也不同。這樣一來就很麻煩,變成每次要填格子都要看一次 Spec,而且寫出來的 Code 也難以理解。
After
於是改造一下可以寫成這個樣子,應該是容易理解許多。一樣一個 Inquiry、及一個 Read(10)。
// Inquiry
SCSI_CDB cdb = {0};
cdb.inquiry.opc = 0x12;
cdb.inquiry.page_code = 0;
cdb.inquiry.alloc_len = SWAP_U16(512); // Allocate 512 bytes for inquiry data
// Read10
SCSI_CDB cdb = {0};
cdb.read10.opc = 0x28;
cdb.read10.slba = SWAP_U32(2048); // Starting LBA = 2048
cdb.read10.lba_len = SWAP_U16(1); // Read Length = 1 LBA
註:SWAP_16 及 SWAP_32 用到 #define 以更改 Byte Order 來符合 Spec 要求。
欸你說這樣還不是要記得有哪些 Command、還有該 Command 對應哪些欄位?等等,其實只要先進一點的文字編輯器帶有 Intillisense 自動完成功能,這個問題就不是問題,總比在那邊翻 Spec 來的容易。
後記
其他像是 #define 等前置處理的指示詞對程式的可讀性其實也都很有幫助,有空再記錄一下~