學習目標

讀完本章後,你應該能夠:

  1. 理解 struct 的定義方式、用途與背後的設計動機
  2. 掌握結構體的多種初始化方式(聚合初始化、C++20 指定初始化器)
  3. 了解結構體的記憶體佈局、對齊(alignment)與填充(padding)
  4. 學會巢狀結構體、結構體陣列與成員函式
  5. 認識 POD / trivial type 的概念
  6. 精確分辨 structclass 的差異(語法 vs 慣例)
  7. 深入理解傳統列舉 enum 與限域列舉 enum class 的差異(作用域、隱式轉換、底層型別)
  8. 能在程式中正確選擇並使用列舉型別,並搭配 switch
  9. 認識 union 與位元欄位(bit-field),並理解為何 std::variant 更安全

一、為什麼需要結構體?(WHY)

想像你要表示一位學生,需要姓名、年齡、GPA 三筆資料。如果不用結構體,你可能會這樣寫:

std::string name1; int age1; double gpa1;
std::string name2; int age2; double gpa2;
// ... 100 位學生?

這種寫法有三大問題:

  • 資料散落:三筆相關資料沒有「綁在一起」,傳遞時要帶三個參數。
  • 難以擴充:要新增「電話」欄位時,得修改數十個地方。
  • 無法表達語意:編譯器不知道 name1age1 屬於同一個人。

結構體(struct) 解決了這個問題:它把多筆「邏輯上相關」的資料聚合成一個使用者自定義型別,讓資料有了名字、有了整體,可以像內建型別一樣被宣告、複製、傳遞與回傳。

這就是物件導向的第一步:讓資料有結構。後續章節的 class 會在此基礎上,再加入「行為」與「封裝」。


二、結構體(struct)

2.1 什麼是結構體?

結構體是一種使用者自定義型別,可以將多個不同型別的資料組合在一起,形成一個有意義的資料單元。其中的每個變數稱為成員(member)

struct Student {
    std::string name;   // 成員 1
    int age;            // 成員 2
    double gpa;         // 成員 3
};   // ← 別忘了這個分號!

2.2 建立實例與成員存取

使用 .(點運算子)存取結構體成員;若手上是指標,則用 ->

Student s;
s.name = "小明";
s.age = 20;
s.gpa = 3.85;

std::cout << s.name << " 的 GPA 是 " << s.gpa << std::endl;

Student* ptr = &s;
std::cout << ptr->name << std::endl;   // 等同 (*ptr).name

預期輸出:

小明 的 GPA 是 3.85
小明

2.3 初始化方式

(a) 聚合初始化(Aggregate Initialization)

結構體若沒有自訂建構子、沒有 private/protected 成員、沒有虛擬函式,就是一個聚合(aggregate),可以用大括號逐欄位初始化:

Student s1 = {"小華", 19, 3.72};   // 帶等號
Student s2{"小美", 21, 3.90};       // 不帶等號(推薦,可防窄化)
Student s3{};                       // 全部值初始化:name="", age=0, gpa=0.0
Student s4{"小強"};                 // 只給前面,其餘值初始化:age=0, gpa=0.0

重點:使用 {} 的列表初始化會阻止窄化轉換(narrowing)。例如 Student bad{"x", 3.9, 1};3.9(double)要塞進 int age 會編譯錯誤,避免無聲的精度遺失。

(b) C++20 指定初始化器(Designated Initializers)

C++20 起可以「指名」要初始化哪個欄位,可讀性更高:

Student s5{.name = "小強", .age = 22, .gpa = 3.50};
Student s6{.name = "小芳", .gpa = 3.95};   // 跳過 age,age 被值初始化為 0

注意:C++ 的指定初始化器要求欄位順序必須與宣告順序一致(與 C 語言不同),且不能重複指定。{.age = 22, .name = "x"} 會編譯錯誤。

2.4 記憶體佈局、對齊與填充(Padding)

結構體成員在記憶體中是依宣告順序連續排列的,但編譯器為了 CPU 存取效率,會插入填充位元組(padding),使每個成員落在其「對齊(alignment)」邊界上。

考慮以下兩個成員相同、但順序不同的結構體:

struct Bad {      // 浪費空間的排列
    char  a;      // 1 byte
    double b;     // 8 bytes(需對齊到 8 的倍數)
    char  c;      // 1 byte
};                // sizeof 通常是 24!

struct Good {     // 由大到小排列,較省空間
    double b;     // 8 bytes
    char  a;      // 1 byte
    char  c;      // 1 byte
};                // sizeof 通常是 16

Bad 的記憶體佈局(在常見的 64 位元平台):

位移:  0      1                8        9              16
      +---+-------------------+--------+---+-----------------+
      | a |   padding (7)     |   b    | c |  padding (7)     |
      +---+-------------------+--------+---+-----------------+
        1B        7B            8B      1B       7B          = 24 bytes

實務建議:在意記憶體(如大量物件、網路封包)時,把較大的成員放在前面可減少填充。可用 sizeof(T)alignof(T) 觀察。

2.5 巢狀結構體

結構體可以包含其他結構體作為成員,用以建構更複雜的資料模型:

struct Address {
    std::string city;
    std::string street;
    int zipcode;
};

struct Employee {
    std::string name;
    Address addr;       // 巢狀結構體
    double salary;
};

Employee emp = {"王大明", {"台北市", "忠孝東路", 10001}, 55000.0};
std::cout << emp.addr.city << std::endl;  // 台北市

2.6 結構體中的成員函式(方法)

C++ 的結構體可以擁有成員函式(這與 C 語言不同),使資料與行為綁定:

struct Rectangle {
    double width;
    double height;

    double area() const {          // const:承諾不修改成員
        return width * height;
    }
    double perimeter() const {
        return 2.0 * (width + height);
    }
};

Rectangle r{5.0, 3.0};
std::cout << r.area() << std::endl;   // 15

何時用 struct 加方法、何時用 class?見 2.8 的慣例討論。

2.7 傳遞結構體給函式

// 傳值 — 會「複製」整個結構體(大型結構體成本高)
void printStudent(Student s);

// 傳 const 參考 — 不複製、不可修改(讀取時的首選)
void printStudent(const Student& s);

// 傳參考 — 可在函式內修改原始資料
void updateGPA(Student& s, double newGPA);

指引(C++ Core Guidelines F.16):對於「便宜複製」的小型型別(約 ≤ 2 個指標大小)可傳值;較大的、只讀的參數應以 const T& 傳遞。

2.8 struct 與 class 的差異

項目 struct class
預設成員存取權限 public private
預設繼承方式 public private
慣例用途 簡單資料聚合(無不變式) 封裝行為與不變式的物件
語言功能 完全相同 完全相同

在 C++ 中,structclass唯一語法差異就是預設存取權限。其他功能(建構子、成員函式、繼承、虛擬函式…)兩者皆可使用。

慣例上如何選擇?(C++ Core Guidelines C.2 / C.8)

  • 若所有成員都能各自獨立變化、沒有互相約束的「不變式(invariant)」→ 用 struct
  • 若成員之間有約束(例如「分子/分母不能為 0」「餘額不可為負」)需要靠 private + 函式保護 → 用 class

2.9 POD 與 trivial type(簡介)

  • Trivial type:建構、複製、解構都「無特殊行為」(如同 C 的結構體),可安全用 memcpy 複製。
  • Standard-layout type:記憶體佈局與 C 相容,可跨語言互通。
  • POD(Plain Old Data):同時是 trivial 且 standard-layout(C++20 後此詞已被前兩者取代)。
#include <type_traits>
struct P { int x; int y; };
static_assert(std::is_trivial_v<P>);            // 通過
static_assert(std::is_standard_layout_v<P>);    // 通過

為什麼重要?因為 trivial / standard-layout 型別可以做高效的位元複製、序列化、與 C API 互通。一旦你加了使用者自訂建構子、虛擬函式或 private 成員,就不再是 POD。


三、列舉(Enum)

3.1 為什麼需要列舉?(WHY)

當一個變數只能是「有限個具名選項」之一(顏色、星期、狀態碼…),用 int 搭配「魔術數字」既不安全也難讀:

int light = 0;   // 0 是紅燈?還是綠燈?沒人看得懂
if (light == 2) { ... }   // 2 又是什麼?

列舉讓這些選項變成具名常數,程式自我說明,且編譯器能幫忙檢查。

3.2 傳統列舉(Unscoped Enum)

enum Color { Red, Green, Blue };   // 預設 Red=0, Green=1, Blue=2

Color c = Red;
int value = c;   // 隱式轉換為 int,value == 0

數值預設從 0 開始遞增,也可手動指定(未指定者沿用前一個 +1):

enum Status { Ok = 200, Created = 201, NotFound = 404 };

兩大問題

  1. 名稱洩漏到外圍作用域,容易撞名:
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green };  // 編譯錯誤!Red、Green 重複定義
  1. 隱式轉換為整數,造成型別不安全:
Color c = Red;
if (c == 1) { /* 竟然可以拿顏色跟數字比 */ }   // 危險但合法
int x = c * 3;                                  // 毫無意義卻能編譯

3.3 限域列舉(enum class)— C++11,推薦

enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green };  // 不衝突!

Color c = Color::Red;       // 必須加上 Color:: 前綴
// int value = c;           // 編譯錯誤!不會隱式轉換
int value = static_cast<int>(c);   // 必須顯式轉換

優點

  • 名稱限定在列舉內(Color::Red),不會洩漏到外圍作用域。
  • 不會隱式轉換為整數,杜絕「拿列舉做算術」這類錯誤。
  • 不同列舉型別之間不能互相比較,型別更安全。

enum vs enum class 完整比較:

特性 傳統 enum enum class(限域)
名稱作用域 洩漏到外圍 限定在列舉內(需 E::Name
隱式轉成 int 不會(需 static_cast
與其他型別比較 允許(不安全) 禁止(安全)
可指定底層型別 C++11 起可以 可以
前向宣告 需指定底層型別才行 可以
適用場景 與 C 相容、舊程式碼 新程式碼的首選

3.4 指定底層型別(Underlying Type)

列舉底層其實是某種整數型別。預設情況下 enum class 的底層型別是 int,但可以明確指定以節省空間或控制範圍:

enum class Weekday : unsigned char {     // 只佔 1 byte
    Mon, Tue, Wed, Thu, Fri, Sat, Sun
};

enum class ErrorCode : int {
    Success = 0,
    NotFound = 404,
    ServerError = 500
};

std::cout << sizeof(Weekday) << std::endl;   // 1

3.5 列舉搭配 switch

switch 是處理列舉的最佳拍檔。若 switch 的條件是 enum class沒有 default,當你漏掉某個列舉值時,多數編譯器(開 -Wall)會發出警告,幫你抓漏:

enum class Season { Spring, Summer, Autumn, Winter };

std::string describe(Season s) {
    switch (s) {
        case Season::Spring: return "春天";
        case Season::Summer: return "夏天";
        case Season::Autumn: return "秋天";
        case Season::Winter: return "冬天";
    }
    return "未知";   // 防禦性回傳(理論上不會到這)
}

技巧:刻意不寫 default,讓編譯器在你新增 Season 列舉值卻忘了更新 switch 時提出警告。

3.6 列舉的常見輔助:轉成底層整數(C++23 預習)

C++23 提供 std::to_underlying,在此之前可自行包裝:

template <typename E>
constexpr auto to_underlying(E e) noexcept {
    return static_cast<std::underlying_type_t<E>>(e);
}
// C++23: std::to_underlying(Color::Red)

四、union 與位元欄位(進階補充)

4.1 union(聯合)

union 讓所有成員共用同一塊記憶體,同一時間只有一個成員是「有效」的,常用於極度節省記憶體的場景:

union Value {
    int i;
    float f;
    char bytes[4];
};

Value v;
v.i = 65;
// 此時讀 v.f 是未定義行為!你只能讀「最後寫入」的成員

union 的危險:它不會記住目前是哪個成員有效,讀錯成員是未定義行為(UB)

4.2 為什麼 std::variant 更安全?

C++17 的 std::variant 是「型別安全的 union」:它會記住目前裝的是哪個型別,讀錯時會丟例外或編譯錯誤。

#include <variant>
std::variant<int, float, std::string> data;
data = 42;
std::cout << std::get<int>(data);          // OK
// std::get<float>(data);                   // 丟出 std::bad_variant_access
比較 union std::variant
記住目前型別 否(需自己管理 tag)
讀錯成員 未定義行為 丟例外 / 編譯錯誤
支援非 trivial 型別(如 string) 困難(需手動管理) 自動
建議 僅限低階/相容需求 一般情況首選

4.3 位元欄位(Bit-field,簡介)

當你需要把多個小範圍的旗標塞進少少幾個位元時,可用位元欄位:

struct Flags {
    unsigned int isVisible : 1;   // 只佔 1 個位元
    unsigned int isActive  : 1;
    unsigned int priority  : 3;   // 0~7
};

位元欄位常見於硬體暫存器對應、通訊協定。但其記憶體佈局由實作定義,不保證可移植,一般應用建議改用 std::bitset 或明確的位元遮罩。


五、最佳實踐(Best Practices)

以下建議參考 C++ Core Guidelines 與 cppreference:

  1. 優先使用 enum class 而非傳統 enum(Core Guidelines Enum.3)。除非需與 C 程式碼相容,否則一律用限域列舉。
  2. 為列舉指定底層型別(Enum.7,當大小重要時)。可控制記憶體與跨平台行為。
  3. struct 表示「無不變式」的純資料聚合(C.2);當成員間有約束需保護時改用 class
  4. 大型結構體以 const& 傳遞(F.16),避免不必要的複製。
  5. 善用聚合初始化與指定初始化器讓資料建立清晰、可防窄化。
  6. 避免裸用 union;偏好 std::variant(型別安全)。
  7. switch 列舉時刻意省略 default,借助編譯器警告抓出遺漏的分支。
  8. 記憶體敏感時注意成員排列順序以減少 padding。

六、常見錯誤與陷阱

  1. 忘記結構體定義結尾的分號
struct Point { int x; int y; }   // 錯誤!缺少分號
struct Point { int x; int y; };  // 正確
  1. 傳統 enum 名稱衝突:多個列舉定義相同列舉值名稱會導致編譯錯誤,應改用 enum class

  2. 將 enum class 值當 int 使用enum class 不會隱式轉換,必須用 static_cast<int>()

  3. 結構體傳值造成效能浪費:大型結構體應以 const& 傳遞,避免不必要的複製。

  4. 指定初始化器順序錯誤:欄位順序必須與宣告順序一致,否則編譯失敗(C++ 與 C 在此規則不同)。

  5. 讀取 union 中「非最後寫入」的成員:這是未定義行為,務必自行追蹤目前有效成員,或改用 std::variant

  6. 誤以為 sizeof(struct) 等於各成員 sizeof 的總和:因為有 padding,實際大小可能更大。

  7. 在標頭檔重複定義結構體:應使用 #pragma once 或 include guard。


重點整理

概念 說明
struct 將多個不同型別的資料組合成一個型別
聚合初始化 Student s{"name", 20, 3.5};{} 可防窄化
指定初始化器 C++20:Student s{.name="x", .age=20};(順序須一致)
記憶體對齊 / padding 成員順序影響 sizeof,大成員放前面較省空間
巢狀結構體 結構體成員可以是另一個結構體
成員函式 C++ 的 struct 可以有方法(與 C 不同)
POD / trivial 可安全位元複製、與 C 互通的型別
struct vs class 僅預設存取權限不同;慣例上有無不變式為選擇關鍵
傳統 enum 值洩漏到外圍作用域,可隱式轉 int(不安全)
enum class 限域、型別安全、需顯式轉換(推薦
底層型別指定 enum class E : uint8_t { ... };
union 成員共用記憶體;讀錯成員是 UB
std::variant 型別安全的 union,一般首選

練習題

以下練習由淺入深。建議先自己動手,再對照 structs.cpp / enums.cpp 的寫法。

練習 1(基礎):圖書資料

定義一個 Book 結構體,包含書名、作者、價格、ISBN,並以聚合初始化建立 3 本書,逐一印出資訊。 提示:價格用 double、ISBN 可用 std::string;印出時用 const Book& 傳遞給輔助函式。

練習 2(基礎):巢狀結構體

定義 AddressPerson 結構體,Person 內含一個 Address 成員。撰寫一個 void printPerson(const Person&) 函式印出完整個人資訊(含地址)。 提示:存取巢狀成員用兩層點運算子,如 p.addr.city

練習 3(基礎):enum class 與 switch

定義 enum class Priority { Low, Medium, High, Critical },撰寫 int responseHours(Priority) 回傳對應的處理時數(如 Critical=1、High=4…)。用 switch 實作,並刻意不寫 default 觀察編譯器警告。 提示:每個 casePriority::Low 這種完整寫法。

練習 4(中級):結構體陣列與搜尋

建立一個 Student 結構體陣列(至少 5 筆),撰寫函式 const Student& findTopGPA(const Student arr[], int n) 找出 GPA 最高的學生並回傳其參考。 提示:用一個指標(或索引)記住目前最高者,逐一比較 gpa

練習 5(中級):記憶體佈局觀察

定義兩個成員相同但順序不同的結構體(一個刻意製造 padding、一個盡量緊湊),用 sizeofalignof 印出兩者大小,並用註解解釋差異。 提示:放一個 char、一個 double、一個 char,再調換順序比較。

練習 6(挑戰):通訊錄系統

設計一個簡易通訊錄:用 struct Contact 儲存姓名、電話、email,用 enum class Category { Family, Friend, Colleague } 分類。實作: - void addContact(...):新增聯絡人到一個 std::vector<Contact> - void listByCategory(const std::vector<Contact>&, Category):列出某分類的所有聯絡人 - 一個把 Category 轉成中文字串的輔助函式(用 switch

提示Contact 內可放一個 Category 成員;過濾時用範圍 for 迴圈搭配 if

練習 7(挑戰):型別安全的數值容器

std::variant<int, double, std::string> 設計一個函式 void describe(const std::variant<...>& v),依目前裝載的型別印出不同訊息。比較若改用 union 你需要額外維護什麼? 提示:用 std::holds_alternative<T>(v) 判斷型別,或用 std::visit 搭配 lambda。


對應程式碼檔案

  • structs.cpp — 結構體完整範例(基本、方法、巢狀、陣列、結構化綁定、比較)
  • enums.cpp — 列舉完整範例(傳統 enum、enum class、底層型別、switch、型別安全比較)