Skip to content

自定義類別

在 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\column1234
1charxxx
2intintintint

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;
};