C 語言中的 typedef、struct、與 union

前言

C 語言寫久了發現每次宣告個變數都要指定其型態相當麻煩,除了要打很多英文字母之外、也很常忘記到底一個int是佔幾個 byte 之類。還有就是相關的變數想要擺在一起的狀況,尤其要傳給某 function 時要寫一長串參數真的頗麻煩。

本篇將介紹 typedef、struct、與 union,妥善使用可大幅提升寫程式效率及易讀性,以下介紹一下各自的功能。

typedef

這個絕對是節省打字數的一大幫手。例如定義一個unsigned charU8型態:

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;
};
如果 struct 後面的 SCSI_CDB 不寫,則為一匿名 Struct,通常需搭配 typedef 或 union 使用。

C Struct.png

請留意位元組順序(Byte Order 或 Endianness),這會影響 2 Bytes 以上的格子分布情形。

Bit Field

如圖所示,使用 Struct 甚至可以切割出以 bit 為單位大小的格子,只要在變數名稱後面加上:以及所需bit數即可(注意:bit 數不能超過所宣告的型態,例如宣告一個 U8,最多就只能用 8 bits)。

注意:這裡寫的都是參考值,如果情況允許編譯器會照做,但有時他會按自己喜歡的方式來決定每個欄位的大小(通常整體 Struct 大小會增加到 2N Bytes),想要強制對齊可以使用以下兩種方法:

  1. struct FOO {/*your struct fields*/}__attribute__((packed));
  2. #pragma pack(push1)
    // 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
};

C Union.png

此例中 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 >> 32) & 0xFF; // MSB of starting_lba
cdb[3] = (2048 >> 24) & 0xFF;
cdb[4] = (2048 >> 16) & 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 等前置處理的指示詞對程式的可讀性其實也都很有幫助,有空再記錄一下~