學習目標
讀完本章後,你應該能夠:
- 理解
struct的定義方式、用途與背後的設計動機 - 掌握結構體的多種初始化方式(聚合初始化、C++20 指定初始化器)
- 了解結構體的記憶體佈局、對齊(alignment)與填充(padding)
- 學會巢狀結構體、結構體陣列與成員函式
- 認識 POD / trivial type 的概念
- 精確分辨
struct與class的差異(語法 vs 慣例) - 深入理解傳統列舉
enum與限域列舉enum class的差異(作用域、隱式轉換、底層型別) - 能在程式中正確選擇並使用列舉型別,並搭配
switch - 認識
union與位元欄位(bit-field),並理解為何std::variant更安全
一、為什麼需要結構體?(WHY)
想像你要表示一位學生,需要姓名、年齡、GPA 三筆資料。如果不用結構體,你可能會這樣寫:
std::string name1; int age1; double gpa1;
std::string name2; int age2; double gpa2;
// ... 100 位學生?
這種寫法有三大問題:
- 資料散落:三筆相關資料沒有「綁在一起」,傳遞時要帶三個參數。
- 難以擴充:要新增「電話」欄位時,得修改數十個地方。
- 無法表達語意:編譯器不知道
name1、age1屬於同一個人。
結構體(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++ 中,
struct和class的唯一語法差異就是預設存取權限。其他功能(建構子、成員函式、繼承、虛擬函式…)兩者皆可使用。
慣例上如何選擇?(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 };
兩大問題:
- 名稱洩漏到外圍作用域,容易撞名:
enum Color { Red, Green, Blue };
enum TrafficLight { Red, Yellow, Green }; // 編譯錯誤!Red、Green 重複定義
- 隱式轉換為整數,造成型別不安全:
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:
- 優先使用
enum class而非傳統enum(Core Guidelines Enum.3)。除非需與 C 程式碼相容,否則一律用限域列舉。 - 為列舉指定底層型別(Enum.7,當大小重要時)。可控制記憶體與跨平台行為。
- 用
struct表示「無不變式」的純資料聚合(C.2);當成員間有約束需保護時改用class。 - 大型結構體以
const&傳遞(F.16),避免不必要的複製。 - 善用聚合初始化與指定初始化器讓資料建立清晰、可防窄化。
- 避免裸用
union;偏好std::variant(型別安全)。 switch列舉時刻意省略default,借助編譯器警告抓出遺漏的分支。- 記憶體敏感時注意成員排列順序以減少 padding。
六、常見錯誤與陷阱
- 忘記結構體定義結尾的分號
struct Point { int x; int y; } // 錯誤!缺少分號
struct Point { int x; int y; }; // 正確
-
傳統 enum 名稱衝突:多個列舉定義相同列舉值名稱會導致編譯錯誤,應改用
enum class。 -
將 enum class 值當 int 使用:
enum class不會隱式轉換,必須用static_cast<int>()。 -
結構體傳值造成效能浪費:大型結構體應以
const&傳遞,避免不必要的複製。 -
指定初始化器順序錯誤:欄位順序必須與宣告順序一致,否則編譯失敗(C++ 與 C 在此規則不同)。
-
讀取 union 中「非最後寫入」的成員:這是未定義行為,務必自行追蹤目前有效成員,或改用
std::variant。 -
誤以為
sizeof(struct)等於各成員sizeof的總和:因為有 padding,實際大小可能更大。 -
在標頭檔重複定義結構體:應使用
#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(基礎):巢狀結構體
定義 Address 和 Person 結構體,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 觀察編譯器警告。
提示:每個 case 用 Priority::Low 這種完整寫法。
練習 4(中級):結構體陣列與搜尋
建立一個 Student 結構體陣列(至少 5 筆),撰寫函式 const Student& findTopGPA(const Student arr[], int n) 找出 GPA 最高的學生並回傳其參考。
提示:用一個指標(或索引)記住目前最高者,逐一比較 gpa。
練習 5(中級):記憶體佈局觀察
定義兩個成員相同但順序不同的結構體(一個刻意製造 padding、一個盡量緊湊),用 sizeof 與 alignof 印出兩者大小,並用註解解釋差異。
提示:放一個 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、型別安全比較)