Appearance
自定義類別
在 C 中,有三種類別可以自訂,struct、enum、union
Struct
在之前的篇章中,我們使用的都是 C 語言內建的型別(如 int, char)。但在現實開發中,我們面對的是複雜的「物件」。
為何需要 Struct
想像你要管理一個班級的資料,如果不使用 struct,你可能需要這樣寫:
c
char *names[50];
int ids[50];
int scores[50];這樣做最大的問題是:資料是散開的。你必須小心翼翼地確保 names[0]、ids[0] 和 scores[0] 永遠指向同一個人。
使用 struct 後:
我們可以把相關的資料「打包」在一起,形成一個全新的型別。這就像是把零散的零件組裝成一台完整的機器。
c
struct Student { // 自定義的名稱
// 想要的屬性
char *name;
int id;
int score;
};宣告與使用
struct 讓你的程式碼更具備「語意化」,一眼就能看出這段資料代表什麼。
c
int main() {
// 1. 直接初始化(必須照順序)
struct Student a = {"Ben", 10, 95};
// 2. 直接初始化:標明內容 (沒有標的就是 0)
struct Student b = {.name = "Alice"};
// 3. 點運算子 (.):存取成員
struct Student c;
c.name = "Sam";
printf("%s, %d\n", a.name, a.id);
return 0;
}Struct 與指標
當 struct 遇到指標時,會出現一個 C 語言中非常帥氣的運算子:-> (箭頭)。
c
void update_score(struct Student *s) {
s->score = 100; // 等同於 (*s).score = 100
}-> 出現的原因
想想前面章節如何使用一個變數,用 *ptr
但是這時候需要取用的是 struct 的內容,也就是 (*ptr).attr,這邊的運算先後是 . > *,因此必須使用 () 準確表示,但這樣太複雜了,於是就有了 ->。
記憶體對齊
預設的類別有很多種大小,通常是 2 的指數 (1, 2, 4, 8),因為這對於電腦儲存比較友善,struct 存放不同大小的變數十,裡面的內容在記憶體中並一定不是連續排放的,這就導致了 sizeof(struct) 有時比想像中大。
範例: int 4 bytes 讓 int 對齊四的倍數位址,而 char 後面空出 3 bytes
| row\column | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 1 | char | x | x | x |
| 2 | int | int | int | int |
typedef 簡稱
每次都要寫 struct Student 有點累贅,我們可以使用 typedef 來幫這個結構起個綽號。
c
typedef struct {
char *name;
int id;
} Student;
// 之後宣告就不需要寫 struct 了
Student c = {"Alice", 12};進階 Struct 技巧
有時如果需要在 struct 裡面放動態的陣列會十分的麻煩,因為這代表在釋放之前要再額外釋放裡面的指標,這很容易造成 memory leak,因此就有了 「彈性陣列成員」(Flexible Array Member)寫法。 可以把這種寫法想像成一個火車頭 (struct) 連接著一串貨箱 (str[]),在實際使用時再決定要多長的貨箱。
宣告:
c
struct s {
int n;
char str[];
};使用:
c
int len = 20; // 長度為 20 的字串
// 總大小 = 結構體固定部分的大小 + 想要申請的陣列大小
struct s *ptr = malloc(sizeof(struct s) + len * sizeof(char));此結構常見於網路封包
Enum
如果有一連串的數據需要 #define,這會有點惱人,但如果使用 enum 事情會變簡單不少。
範例:
c
enum Color {
White = 0xffffff,
Black = 0x000000
};
// 也可以讓它自行排列
enum Weekdays {
Sunday, // 從 0 開始
Monday, // 1
Tuesday
// ...
};
// 可以宣告一個 enum 變數
enum Color color = White;
// 可以用 int 轉型
color = (enum Color) 0x000000;
// 也可以直接在其他地方使用
if (White == color)enum 的變數都算是 int 型別,因此可以使用數字比較與賦值 (需要轉型)。
Union
這個東西的作用有點難以解釋,作用非常稀少
union 的語法和 struct 幾乎一模一樣,但記憶體運作方式卻截然不同。
什麼是 Union
在 struct 中,每個成員都有自己獨立的空間。但在 union 中,所有成員共用同一塊記憶體位址。這塊位址的大小,取決於成員中「最大」的那一個。
c
union Data {
int i;
float f;
char str[20];
}; // 這個 union 的大小會是 20 bytes (陣列最大)特性
同一時間,你只能存取其中一個成員。 如果你寫入了一個新成員,舊成員的值就會被覆蓋掉(或是變得毫無意義)。
c
union Data data;
data.i = 10;
data.f = 220.5; // 此時 data.i 的值已經被破壞了,因為它們住在同一個房間既然會互相破壞,為什麼還要用它
極致的節省空間:
在嵌入式系統(例如微控制器、洗碗機控制板)中,記憶體非常珍貴。如果你有一個物件,在 A 狀態下需要存int,在 B 狀態下需要存double,但這兩個狀態不會同時發生,用union就能省下一半空間。特殊的硬體操作(型別轉換技巧):
工程師有時會利用union來查看同一個數據在不同型別下的「樣子」。例如,我想知道一個float在記憶體裡的二進位位元(bits)長怎樣,可以用union把它看成一個int。
搭配 Struct
單獨使用 union 很危險,因為你不知道現在裡面存的是誰。實務上我們常把它塞進 struct 裡,並加上一個「標籤 (Tag)」。
c
struct SensorData {
int type; // 0 代表溫度, 1 代表濕度
union {
float temperature;
int humidity;
} value;
};