C++ 導論:型別、指標與物件

cppds Chapter 1 — Introduction(對應講義 01)
ADT|原子型別|指標|五種集合|I/O 格式化|控制結構|例外|Fraction|邏輯閘
向下捲動開始互動
📌 本頁使用方式(cppds Ch.1|講義 01)

照節次讀:每一節先讀說明、動手玩互動元件,預測結果再按按鈕驗證。 ② 對照講義:頁上 §徽章對應 cppds 章節;細節與完整程式請回講義 01 與 cppds 原文。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳。 ④ 最後翻 關鍵詞彙卡(45 張)自測術語(中文為主、術語附英文),並用 REF 總覽區當速查表。標「講義補充」的節是課堂沒細講的延伸,第一輪可略過。

CONTENTS · 內容目錄
PROLOGUE · 開場

演算法、抽象化,還有這門課要帶你去哪 cppds §1.1–1.6

面對一個問題,資訊科學家的目標是寫出演算法:一份一步一步的指令清單, 能解決這個問題的任何一個實例。講義給了正式定義,四個條件缺一不可:

① 定義良好
指令是有序的集合,每一步都明確。
② 步驟不含糊
說「把兩個整數相加」,就得先講清楚什麼是整數、什麼是加。
③ 產生結果
回傳資料,或造成某種效果(例如印出東西)。
④ 有限時間內停止
跑不完的程序不算演算法。存在演算法可解的問題,我們說它是「可計算的」(computable)。

資訊科學同時也是研究抽象化的學問。開車不需要懂引擎, 你只用方向盤、油門、煞車這組「介面」。寫程式也一樣:#include <cmath> 之後喊一聲 sqrt(16) 就拿到 4,不必知道底層是牛頓法還是查表。這叫程序抽象化

把同一招用在資料上,就是抽象資料型別(ADT):只描述資料有哪些操作、行為是什麼, 完全不管怎麼實作。使用者摸到的是外殼,實作藏在殼裡面一層,這層包裝叫封裝。 而 ADT 的具體實作,就是我們整學期要磨的東西:資料結構

為什麼值得花一學期? 同一個 ADT 通常有好幾種實作,介面一樣、效能天差地遠。第 2 章開始你會看到: 同一個問題,一種寫法跑一秒,另一種要跑到天荒地老。學會「評估解法」跟學會「寫出解法」一樣重要。
為什麼用 C++? C++ 讓你貼著記憶體學:指標、new/delete、值語意,這些被高階語言藏起來的細節全部攤開。 課程用 cppds 這本書,程式碼跟著書的風格走(NULL、手刻結構)。
PART 01 · 原子型別

原子型別與變數:每個變數都是一格固定大小的記憶體 cppds §1.8

C++ 要求每個變數先宣告型別才能用。數值型別主要是 intdouble(後者精度是 float 的兩倍);算術運算子 + - * / % 都在,次方則要用 <cmath>pow()。 運算子與整數除法的完整示範程式及輸出,見本節下方的講義卡「算術運算子與整數除法」。

五種常見型別的大小、範圍與地雷,整學期都會回頭查:
型別大小範圍/說明地雷提醒
int4 byte−2,147,483,648 到 2,147,483,647溢位不報錯,直接繞回負數。
long long8 byte約 ±9.2×10¹⁸int 不夠用時的第一選擇。
double8 byte約 ±1.7×10³⁰⁸,15~16 位有效數字0.1 + 0.2 != 0.3,浮點誤差是常態。
char1 byte'a' 其實是整數 97(ASCII)ch − 'a' 可算出字母序,第 2 章 anagram 會用到。
bool1 bytetrue / false非零即 true;cout 預設印 1/0,boolalpha 之後印 true/false。
int 是固定大小的
C++ 的 int 通常 32 位元,上限 2,147,483,647。所以 pow(2, 100) 只能給你 double 近似值。超過上限不會報錯,直接繞回負數,這種溢位是經典 bug 來源。
整數除法
7 / 3 是 2,不是 2.33。 兩個 int 相除結果就是 int,往零截斷。想要小數,至少讓一邊是 double

布林、比較與邏輯運算子

bool 只有 truefalse 兩個值,搭配 &&(且)、||(或)、!(非)。 比較運算的結果也是 bool。整理成一張表:

運算寫法說明
大小比較< > <= >=結果是 bool
相等/不等==  !=注意 = 是賦值,== 才是比較
邏輯且/或/非&&  ||  !短路求值:左邊定案就不看右邊

變數是一個「有名字的盒子」

宣告 int the_sum = 0; 時,編譯器保留一塊剛好放得下 int 的記憶體, 把 0 直接放進去。之後的賦值都是換掉盒子裡的值。三件事值得慢慢咀嚼:

QUIZ · 型別與除法

int n = 7; double d = n / 2; 之後 d 的值是?

(A) 3.0
(B) 3.5
(C) 編譯錯誤
QUIZ · 布林運算

cout << (10 < 5 < 3); 印出什麼?

(A) 1
(B) 0
(C) 編譯錯誤

講義完整範例:把運算子親手跑一遍

下面三段是講義 01 的原始示範,值得抄進編譯器跑一次。第一段的重點是整數除法會截斷: 兩個 int 相除結果還是 int,小數部分直接丟掉;只要有一邊是 double,就變成浮點除法。

講義 01 · 算術運算子與整數除法
using namespace std; cout << 2 + 3 * 4 << endl; cout << (2 + 3) * 4 << endl; cout << pow(2, 10) << endl; cout << 6 / 3 << endl; cout << 7 / 3 << endl; // 整數除法:截斷! cout << 7.0 / 3 << endl; // 有一個 double,就是浮點除法 cout << 7 % 3 << endl; cout << pow(2, 100) << endl; // double 的近似值,不是精確整數
預期輸出
14
20
1024
2
2
2.33333
1
1.26765e+30

pow() 回傳 double:2100 印出來是 1.26765e+30 這種科學記號近似值,不是精確整數。要大整數得另找函式庫。

講義 01 · 比較運算子與一個大陷阱
using namespace std; cout << boolalpha; cout << (5 == 10) << endl; cout << (10 > 5) << endl; cout << ((5 >= 1) && (5 <= 10)) << endl; cout << ((1 < 5) || (10 < 1)) << endl; // 注意:連鎖比較不是你想的那樣! cout << (10 < 5 < 3) << endl; // (10 < 5) 先算出 0,然後 0 < 3 是 true!
預期輸出
false
true
true
true
true
⚠️ 連鎖比較陷阱:數學課寫的 10 < 5 < 3 在 C++ 是合法程式, 但意思是 (10 < 5) < 3:先算出 false(也就是 0),再算 0 < 3 得到 true。 範圍判斷一律拆成兩段用 && 接:(3 < x) && (x < 5)
講義 01 · bool 悄悄變 int
using namespace std; int the_sum = 0; cout << the_sum << endl; the_sum = the_sum + 1; cout << the_sum << endl; the_sum = true; // 合法,但 bool 被轉成 int 的 1 cout << the_sum << endl;
預期輸出
0
1
1

C++ 允許 bool 和數值互轉:true 是 1、false 是 0。方便,但也是 bug 溫床:if (x = 1)(少個等號)永遠為真而且編譯得過。

PART 02 · 指標

指標:存位址的變數 cppds §1.8(Pointers)

C++ 把兩種世界都交到你手上:一般變數直接放值,指標是一種放「別的變數的位址」的變數。 兩個運算子要熟到變反射動作:& 取位址、* 解參考(沿位址取值)。

照順序按四顆按鈕,看變數格與指標格怎麼變。
程式對照 CODE
int varN = 100; // 一般變數:住著值 int *ptrN = &varN; // 指標:住著位址 cout << ptrN; // 印出位址(如 0x7ffe...) cout << *ptrN; // 解參考:沿位址取值,得到 100 *ptrN = 50; // 透過指標改 varN 本人 ptrN = NULL; // 指向「無」;解參考 NULL 會當機
為什麼現在就要學指標?
第 4 章的鏈結串列、第 8 章的樹、第 9 章的圖, 全部用「節點+指標」蓋出來。NULL 扮演「這裡沒有東西」的哨兵: 解參考 NULL 的下場是 segfault,程式當場倒地。
QUIZ · 讀指標

接上面第 ③ 步之後執行 cout << varN;,印出什麼?

(A) 50
(B) 100
(C) 位址(0x…)
QUIZ · 危險的指標

int *p; cout << *p; 這兩行的問題是?

(A) 解參考未初始化的指標,行為未定義
(B) 語法錯誤
(C) 一定印出 0

講義完整範例:位址親眼看一次

講義 01 · 取址、存址、解參考
using namespace std; int var_n = 100; int *ptr_n = &var_n; // ptr_n 存的是 var_n 的位址 cout << "value of var_n: " << var_n << endl; cout << "address of var_n: " << &var_n << endl; cout << "ptr_n stores: " << ptr_n << endl; cout << "*ptr_n gives: " << *ptr_n << endl;
預期輸出
value of var_n:   100
address of var_n: 0x7ffd4c2a5b44
ptr_n stores:     0x7ffd4c2a5b44
*ptr_n gives:     100

位址每次執行都不一樣(0x 開頭的十六進位數),但第二、三行一定相同:ptr_n 存的就是 var_n 的位址。

講義 01 · 透過指標改值
using namespace std; int var_n = 100; int *ptr_n = &var_n; *ptr_n = 50; // 從「另一扇門」走進同一格記憶體 cout << var_n << endl; // var_n 變成 50
預期輸出
50

這一行是整章的關鍵畫面:*ptr_n = 50 改的不是 ptr_n,是它指向的那格記憶體。之後的鏈結串列、樹、圖全靠這個動作把結構串起來。

PART 03 · 集合型別

集合型別:vector、string、array、set、map cppds §1.9

有序集合(sequence)有 arrayvectorstring; 無序集合有 setmap 家族。這裡的「無序」指沒有位置索引,不是沒有排序; C++ 的 setmap 內部仍照 key 排序,雜湊版才是 unordered_*。 這一節的幾張表整學期都會回頭查,值得現在花時間讀熟。

vector:會自己長大的序列

vector 是同型別元素的有序集合,可以動態長大。兩個要點: 元素必須同型別;切片這類「取一段」的需求用迭代器範圍表達 vector<double>(v.begin()+1, v.begin()+3) 做出來(取索引 1 到 2,不含 3)。

方法用法說明
[ ] / .at(i)v[i]、v.at(i)索引從 0 起算;at() 有界限檢查、越界丟例外,[] 沒有
push_backv.push_back(x)加到尾端
pop_backv.pop_back()移除最後一項,不回傳值!要值先讀 v.back()
insert / erasev.insert(v.begin()+i, x)在第 i 格插入/刪除(要搬移後面所有元素)
size / front / backv.size()長度、第一項、最後一項
sort / reverse / count / findsort(v.begin(), v.end())來自 <algorithm>,吃迭代器範圍
五格快照,從左到右依序執行:
① push_back(17)
17
② push_back(26)
1726
③ push_back(31)
172631
④ v[1] → 26
172631
⑤ pop_back()
1726
① ② ③:連續三次 push_back,v 從空的長到 [17, 26, 31]。④ v[1] 直達索引 1 的 26,O(1)。 ⑤ v.pop_back():31 被丟掉,它不回傳被移除的值——要值得先 v.back() 讀出來再刪。

string:可變的字元序列

雙引號是 string、單引號是 char,兩者不能混用。 串接用 +,長度是 length()。重點: C++ 的字串是可變的my_name[0] = 'X' 完全合法。

方法用法說明
substrs.substr(pos, len)從 pos 取 len 個字元的子字串
finds.find("v")第一次出現的索引
append / +=s.append(" Ranum")接在尾端
insert / erases.insert(pos, t)插入/刪除一段
c_strs.c_str()轉成 C 風格字元陣列(printf 會用到)

沒有內建 split():之後解析輸入時會用 <sstream> 的 stringstream 達到同樣效果。

array:固定大小、零防護

C 語言傳下來的原生陣列 int my_arr[] = {2, 1, 4}; 大小定了就不能改, 它完全沒有界限檢查my_arr[3] 會編譯、會執行、然後靜靜讀出一格垃圾記憶體。附錄 A 的自學頁會深挖它的底層。

set:不重複、自動排序

set<int> s = {3, 6, 4, 6, 3}; 建出來只剩 3、4、6:重複的直接丟掉。 成員檢查用 s.count(x)(回 1 或 0),加入 insert、移除 erase。 聯集、交集、差集、子集判斷由 <algorithm>set_unionset_intersectionset_differenceincludes 提供, 搭配 inserter 把結果倒進新容器。

map/unordered_map:key 對應 value

key 對應 value 的查找表。map 內部照 key 排序,unordered_map 用雜湊表(第 2、6 章會分析它們的效能)。 走訪時拿到的是 pair,key 是 .first、value 是 .second

操作用法說明
[ ]m["Iowa"]取值;key 不存在時會自動插入一個預設值,這是地雷
atm.at(k)取值,key 不存在改丟例外
countm.count(k)1 表示存在、0 不存在,安全的成員檢查
findm.find(k) == m.end()找不到回 end();這是 C++ 版的「查不到給預設值」慣用法
erase / size / clearm.erase(k)刪除、數量、清空
QUIZ · map 的 [] 陷阱

map<string,int> m; cout << m["kent"]; cout << m.size(); 印出什麼?

(A) 01
(B) 00
(C) 丟出例外
QUIZ · 字串可變性

string s = "David"; s[0] = 'X'; 在 C++ 的結果是?

(A) 合法,s 變成 "Xavid"
(B) 執行期錯誤
(C) 編譯錯誤

講義完整範例:五種容器逐一跑過

表格看熟之後,把講義的示範程式親手跑一遍,輸出先用腦袋預測再對答案。

講義 01 · vector:增刪改
using namespace std; vector<double> my_list = {1024, 3, 1, 6.5}; my_list.push_back(0); // 加到尾端 for (double x : my_list) cout << x << " "; cout << endl; my_list.insert(my_list.begin() + 2, 4.5); // 插進索引 2 for (double x : my_list) cout << x << " "; cout << endl; my_list.pop_back(); // 移除最後一項 my_list.erase(my_list.begin() + 1); // 移除索引 1 for (double x : my_list) cout << x << " "; cout << endl;
預期輸出
1024 3 1 6.5 0 
1024 3 4.5 1 6.5 0 
1024 4.5 1 6.5 
講義 01 · vector 配 :sort、reverse、count、find
using namespace std; vector<double> my_list = {1024, 4.5, 6.5, 1}; sort(my_list.begin(), my_list.end()); for (double x : my_list) cout << x << " "; cout << endl; reverse(my_list.begin(), my_list.end()); for (double x : my_list) cout << x << " "; cout << endl; cout << count(my_list.begin(), my_list.end(), 6.5) << endl; my_list.erase(find(my_list.begin(), my_list.end(), 6.5)); // 按值移除 for (double x : my_list) cout << x << " "; cout << endl;
預期輸出
1 4.5 6.5 1024 
1024 6.5 4.5 1 
1
1024 4.5 1 

C++ 把「容器」和「演算法」拆開:sort、reverse、count、find 都吃一對疊代器 (begin, end),所以同一套演算法能用在不同容器上。

講義 01 · string 的常用操作
using namespace std; string my_name = "David"; char initial = 'D'; // 單引號:單一字元 cout << my_name << endl; cout << my_name[3] << endl; cout << my_name + my_name << endl; // 串接 cout << my_name.length() << endl; cout << my_name.substr(2, 3) << endl; // 從索引 2 取 3 個字元 cout << my_name.find("v") << endl; my_name.append(" Ranum"); cout << my_name << endl;
預期輸出
David
i
DavidDavid
5
vid
2
David Ranum
講義 01 · 可變性與原生陣列的危險
using namespace std; vector<int> my_list = {1, 3, 6}; my_list[0] = 1024; // 合法:vector 元素可變 for (int x : my_list) cout << x << " "; cout << endl; string my_name = "David"; my_name[0] = 'X'; // string 的字元也可以直接改 cout << my_name << endl; int my_arr[] = {2, 1, 4}; // 原生陣列:大小永遠固定 3 my_arr[1] = 99; cout << my_arr[1] << endl; cout << my_arr[3] << endl; // 危險!沒有界限檢查,讀到垃圾值
預期輸出
1024 3 6 
Xavid
99
32764   ← 未定義行為:每次執行都可能不同

最後一行是未定義行為(undefined behavior):編譯過、跑得動、答案是垃圾。原生陣列不做界限檢查,這就是課程偏好 vector(配 .at())的原因。

講義 01 · set:去重、查成員、集合運算
#include <iostream> #include <set> #include <algorithm> #include <iterator> using namespace std; int main() { set<int> my_set = {3, 6, 4, 6, 3}; // 重複的直接被丟掉 for (int x : my_set) cout << x << " "; cout << endl; cout << my_set.count(3) << endl; // 1 = 在裡面 cout << my_set.count(99) << endl; // 0 = 不在 my_set.insert(99); my_set.erase(4); for (int x : my_set) cout << x << " "; cout << endl; set<int> your_set = {99, 3, 100}; set<int> result; set_intersection(my_set.begin(), my_set.end(), your_set.begin(), your_set.end(), inserter(result, result.begin())); for (int x : result) cout << x << " "; // 交集 cout << endl; return 0; }
預期輸出
3 4 6 
1
0
3 6 99 
3 99 

set 自動排序又自動去重;交集、聯集(set_union)、子集判斷(includes)都在 <algorithm> 裡,一樣吃疊代器範圍。

講義 01 · map:鍵值對、走訪、安全查詢
#include <iostream> #include <map> using namespace std; int main() { map<string, int> phone_ext = {{"david", 1410}, {"brad", 1137}, {"roman", 1171}}; phone_ext["kent"] = 2001; // 加新配對 for (auto& p : phone_ext) cout << p.first << " "; // 鍵(自動按序) cout << endl; for (auto& p : phone_ext) cout << p.second << " "; // 值 cout << endl; cout << phone_ext.count("alice") << endl; // 0:不存在 if (phone_ext.find("alice") == phone_ext.end()) { cout << "NO ENTRY" << endl; // 查無此鍵時給預設行為 } return 0; }
預期輸出
brad david kent roman 
1137 1410 2001 1171 
0
NO ENTRY

小心 map 的 []:查一個不存在的鍵會默默把它插進去(值為 0)。純查詢用 .count() 或 .find(),別用 []。

PART 04 · 講義補充

輸入與輸出:cin、cout 與格式化 講義補充(cppds §1.7 範疇)

cout 寫到標準輸出、cin 從標準輸入讀,都住在 <iostream>cin >> radius 有個好用的特性:目標變數的型別決定文字怎麼轉換。 讀進 double 就直接轉成數字,不用自己再轉一次型。 代價是輸入不合法時 cin 不丟錯誤,而是進入 fail 狀態,之後的讀取全部沉默失敗,除錯時要特別留意。

cout 也跟 print() 不同:沒有自動空格、沒有自動換行,分隔符自己用 << 串、行尾自己加 endl。要控制欄寬和小數位數,用 <iomanip>串流操縱子(stream manipulator):

操縱子效果持續性
endl換行並沖刷緩衝
setw(n)下一個值放進寬度 n 的欄位只影響下一個值
fixed + setprecision(n)定點表示、小數點後 n 位持續有效
left / right欄位內靠左/靠右持續有效
setfill(c)欄位空白改用字元 c 填持續有效
boolalphabool 印成 true/false持續有效
cout << "The " << setw(10) << item << " costs " ... << price

        
調整參數,馬上看排版結果(item = "banana"、price = 24)。
printf 也還活著
printf("The %10s costs %5.2f\n",
  item.c_str(), double(price));
C 傳下來的格式字串,後來很多語言的 % 格式化都是從它抄的。注意 string 要先 c_str()
QUIZ · setw 的持續性

cout << setw(10) << "a" << "b"; 的輸出是?

(A) 9 個空白、a、緊接著 b
(B) a、b 各佔 10 格
(C) ab 一起靠右在 10 格內

講義完整範例:輸入與格式化輸出

講義 01 · cin:型別決定轉換
using namespace std; double radius; cout << "Please enter the radius of the circle "; cin >> radius; double diameter = 2 * radius; cout << diameter << endl;
示範執行(輸入 4.5)
Please enter the radius of the circle 4.5
9
講義 01 · 完整的格式化組合技
using namespace std; int price = 24; string item = "banana"; cout << "The " << setw(10) << item << " costs " << setw(10) << fixed << setprecision(2) << double(price) << " cents" << endl; cout << "The " << left << setw(10) << item << " costs " << setw(10) << double(price) << " cents" << right << endl; cout << "The " << setw(10) << item << " costs " << setfill('0') << setw(10) << double(price) << " cents" << setfill(' ') << endl; cout << "Item:" << setfill('.') << setw(10) << item << endl; cout << "Price:" << setfill('.') << setw(4) << "$" << setfill(' ') << setw(5) << double(price) << endl;
預期輸出
The     banana costs      24.00 cents
The banana     costs 24.00      cents
The     banana costs 0000024.00 cents
Item:....banana
Price:...$24.00

setw 只影響下一個值;fixed、setprecision、left、setfill 則持續有效,直到你改掉它。左邊第三行的 0000024.00 就是 setfill('0') 的效果。

講義 01 · printf 也還在
using namespace std; int price = 24; string item = "banana"; printf("The %s costs %d cents\n", item.c_str(), price); printf("The %10s costs %5.2f cents\n", item.c_str(), double(price));
預期輸出
The banana costs 24 cents
The     banana costs 24.00 cents

C 家族的 printf 在 C++ 一樣能用:%10s 是寬度 10 的字串、%5.2f 是寬度 5、小數 2 位。注意 string 要先 .c_str() 轉成 C 字串。

PART 05 · 講義補充

控制結構:迴圈與選擇 講義補充(cppds §1.7 範疇)

演算法需要兩種控制結構:迭代與選擇。C++ 的身體是大括號圍出來的, 縮排只是給人看的,編譯器完全不管。初學最容易踩的坑: 縮排看起來對、大括號漏了,程式的意思就變了。

三種迴圈

寫法用途
while (counter <= 5) { ... }條件為真就重複
for (int item : my_list)range-based for:走訪容器成員
for (int i = 0; i < 10; i++)計數迴圈,起點、終點、步幅全自訂

計數 for 的三段式讓起點、終點、步幅全部自訂:

for (int i = 5; i < 10; i++)   // i = 5,6,…,9 for (int i = 5; i < 10; i += 2) // i = 5,7,9 for (int i = 10; i > 1; i--)   // i = 10,9,…,2

選擇:if、else if、else

巢狀 if 疊四層看得眼花,慣用寫法是把 else 跟下一個 if 接成 else if。最後的 else 是保底,漏了它,所有條件都不成立時就什麼都不做:

if (score >= 90)      cout << "A"; else if (score >= 80) cout << "B"; else if (score >= 70) cout << "C"; else if (score >= 60) cout << "D"; else                    cout << "F";

list comprehension 的等價寫法

「取 1 到 10 的奇數、各自平方、收進 vector」這類篩選+轉換需求,C++ 用迴圈與條件組合:

vector<int> sqList; for (int x = 1; x <= 10; x++) { if (x % 2 != 0) sqList.push_back(x * x); }

之後學到 <algorithm>transformcopy_if,會有更接近宣告式的寫法。

QUIZ · 講義練習改編:average 函式

average(a_list):平均 ≥ 60 印 pass、否則 fail,平均取到小數一位。對 {99,100,74,63,100,100} 應印出?

(A) pass (Average: 89.3)
(B) pass (Average: 89.0)
(C) fail (Average: 89.3)

講義完整範例:巢狀迴圈與一個練習

講義 01 · 巢狀 range-based for:攤平字母
using namespace std; vector<string> word_list = {"cat", "dog", "rabbit"}; vector<char> letter_list; for (string a_word : word_list) { for (char a_letter : a_word) { letter_list.push_back(a_letter); } } for (char c : letter_list) cout << c << " "; cout << endl;
預期輸出
c a t d o g r a b b i t 

外層走訪每個單字、內層走訪單字裡的每個字母。這種「攤平」寫法之後在建圖(word ladder buckets)會再出現。

講義 01 · 練習:把 average() 寫完
#include <iostream> #include <vector> #include <iomanip> using namespace std; void average(vector<int> a_list) { if (a_list.empty()) { cout << "Vector is empty" << endl; return; } // 你的程式碼:算出 avg,及格與否放進 status("pass"/"fail"),然後: // cout << status << " (Average: " << fixed << setprecision(1) << avg << ")" << endl; } int main() { average({99, 100, 74, 63, 100, 100}); average({22, 19, 74, 63, 100, 44}); return 0; }
完成後的預期輸出
pass (Average: 89.3)
fail (Average: 53.7)

提示:總和用 int 累加,平均前記得轉 double,不然 536/6 會整數除法變 89。這正是本頁 P01 的陷阱重出江湖。

PART 06 · 講義補充

例外處理:錯誤發生時給程式一條活路 講義補充

錯誤分兩種。語法錯誤是句子寫壞了,編譯器直接拒收,程式根本生不出來, 這其實是 C++ 對你最溫柔的時刻。邏輯錯誤是程式會跑但答案不對; 其中最糟的一類會直接把程式弄死,像除以零、用越界的索引,這種執行期錯誤叫例外

vector<int> v = {1, 2, 3}; try { cout << v.at(10); // at() 越界會 throw out_of_range } catch (const out_of_range& e) { cout << "Bad index for the vector"; cout << v.back(); // 改用最後一個元素頂著 }
try 圈住可能爆炸的程式碼,catch 接住爆炸後決定怎麼辦。沒接住的例外會讓程式當場終止。
自己丟例外
if (aNumber < 0) throw runtime_error( "You can't use a negative number"); else cout << sqrt(aNumber);
與其把負數餵給 sqrt 拿到 nan,不如自己檢查、自己 throw,錯誤訊息由你定義。
[] 與 at() 的分工
v[10] 越界是未定義行為,安靜地读垃圾; v.at(10) 越界丟例外,吵但誠實。開發期用 at() 抓 bug,效能關鍵處確定安全後再用 []。
QUIZ · 例外基本功

下列哪個「不會」被 try/catch 接住?

(A) v[10] 的越界存取
(B) v.at(10) 的越界存取
(C) 自己 throw runtime_error(...)

講義補充:沒接住會怎樣?

講義 01 · 沒有 try/catch 的下場
using namespace std; vector<int> v = {1, 2, 3}; // 索引 10 不存在:.at() 丟出 out_of_range 例外, // 沒被接住的例外會讓程式當場終止 cout << v.at(10) << endl;
執行期錯誤訊息
terminate called after throwing an instance of 'std::out_of_range'
  what():  vector::_M_range_check: __n (which is 10) >= this->size() (which is 3)

對照上面接住的版本:同一行程式,有 try/catch 就能優雅收場,沒有就整支程式陣亡。另外注意 v[10](不用 .at())連例外都不丟,直接未定義行為。

PART 07 · 函式

定義函式與三種傳參方式 cppds §1.10

C++ 函式的定義需要四樣東西:回傳型別、名字、帶型別的參數列、本體, 用 return 交回結果。int square(int n) { return n * n; } 就是完整的一課:連回傳值的型別都要先說好。

參數怎麼傳進去,決定函式能不能動到呼叫端的變數。C++ 預設傳值(整份複製), 加 &傳參考(操作原變數),傳指標則介於中間:複製的是位址。

比較項目byValue(a)byRef(a)byPtr(&a)
函式參數int xint &xint *x
函式拿到什麼影本本人(別名)位址
函式內怎麼改x = 99x = 99*x = 99
呼叫後 a 的值1(不變)9999
byValue 改的是自己那份影本,動不到呼叫端;byRef 和 byPtr 都碰得到 a 本人, 只是 byPtr 要多一道「解參考」的手續。
三種傳法 CODE
int square(int n) { return n * n; } void byValue(int x)  { x = 99; } // 拿到影本 void byRef(int &x) { x = 99; } // 拿到本人 void byPtr(int *x) { *x = 99; } // 拿到位址 int a = 1; byValue(a); // a 還是 1 byRef(a); // a 變 99 byPtr(&a); // a 變 99
兩個實務慣例
① 大容器用 const vector<int>& v:借看不借改,也省下整包複製。
② C 風格陣列當參數會退化成指標,函式裡改 a[i] 就是改原陣列,而且長度資訊會遺失。vector 沒有這些怪癖。
QUIZ · swap 的正確簽名

要讓 swap2(x, y) 真的交換呼叫端的兩個變數,參數該怎麼宣告?

(A) int &a, int &b
(B) int a, int b
(C) const int &a, const int &b
講義 01 · 函式可以疊著呼叫
#include <iostream> using namespace std; int square(int n) { return n * n; } int main() { cout << square(3) << endl; cout << square(square(3)) << endl; // 先算內層 9,再平方 return 0; }
預期輸出
9
81

square(square(3)) 由內往外算:內層回傳 9,外層拿 9 再平方。這種「函式結果餵給函式」的組合思維是遞迴章的前菜。

PART 08 · 類別

自訂型別:把 Fraction 一步一步蓋起來 cppds §1.11

浮點數存 1/3 只能存個近似值。想要「精確的分數」,就得自己造型別。 講義用五步把 Fraction 從空殼蓋成能加、能比、能印的完整類別,每一步都有理由。

f1 = Fraction(1, 2) f2 = Fraction(2, 4) num den num den 1 2 2 4 1 × 4 = 4 2 × 2 = 4 &f1 = 0x7ffd…a0 &f2 = 0x7ffd…c8 1 × 4 == 2 × 2 → f1 == f2 為 true 位址不同,值卻相等:深相等比的是內容,不是記憶體位址
圖說:operator== 不看兩個物件擺在哪裡,而是把 f1.num × f2.denf2.num × f1.den 交叉相乘後比大小。1/2 與 2/4 位址完全不同,卻是同一個值,所以是「深相等」;若只比位址(淺相等),答案會是 false。

第 1 步:建構子與 private 成員

class Fraction { public: Fraction(int top, int bottom) { num = top; den = bottom; } private: int num, den; };

建構子跟類別同名、沒有回傳型別。物件自己用隱含指標 this 存取, 成員直接叫名字就好(要明講也可以寫 this->num)。 重點在 private::C++ 的 private 是編譯器強制執行的, 外面寫 a.num 直接編譯錯誤:這不是命名慣例的君子協定,是法律。 順帶一提,class 不寫存取修飾詞時,預設全部 private。

第 2 步:建構子多載(多型的第一課)

Fraction(int top, int bottom) { num = top; den = bottom; } // 3/5 Fraction(int top) { num = top; den = 1; } // 整數 5 → 5/1 Fraction() { num = 1; den = 1; } // 沒給就是 1/1 // 或者一行解決:Fraction(int top = 0, int bottom = 1)

三個建構子同名不同參數列,編譯器在編譯期依引數的數量與型別挑一個。 用預設參數也能做到同一件事,使用者根本分不出來你選了哪種寫法,這正是封裝的意思。

第 3 步:operator<<,讓 cout 認得你

cout << my_fraction 一開始直接編譯失敗。 C++ 不會偷偷退回某種預設表示法幫你混過去,它拒絕猜。 解法是多載串流插入運算子,並宣告成 friend,讓它能讀 private 的 num 和 den:

friend ostream& operator<<(ostream& stream, const Fraction& f) { stream << f.num << "/" << f.den; return stream; // 回傳 stream 才能繼續串接 }

第 4 步:operator+,加法要順便約分

通分相加:a/b + c/d = (ad + cb)/bd。直接這樣存會愈加愈肥(1/4 + 1/2 = 6/8), 所以建構子或加法要用最大公因數約分,6/8 進門就變 3/4。這維護了一條不變量: Fraction 永遠是最簡分數,其他方法都可以放心依賴這件事。

Fraction operator+(Fraction other) { int newNum = num * other.den + other.num * den; int newDen = den * other.den; return Fraction(newNum / gcd(newNum, newDen), newDen / gcd(newNum, newDen)); }

第 5 步:operator==,定義「相等」是什麼意思

沒定義 operator== 之前,f1 == f2 也不能編譯。 用交叉相乘 num * other.den == other.num * den 比的是值、不是物件身分, 也就是本節開頭圖說的深相等。 另外記住 C++ 的值語意f3 = f1 是把整個物件複製一份, 不是多一個名字指向同一個物件。

運算式結果怎麼算出來的
Fraction(1,4) + Fraction(1,2)3/4通分得 6/8,gcd(6,8)=2,約分成 3/4
Fraction(1,2) == Fraction(2,4)true交叉相乘 1×4 與 2×2 都是 4,深相等不必先通分
QUIZ · 值語意

Fraction f1(1,2); Fraction f3 = f1; 之後改動 f3,f1 會變嗎?

(A) 不會,f3 是完整的複製品
(B) 會,f3 和 f1 指向同一物件
(C) 編譯錯誤
QUIZ · 講義練習:負分母

講義要求 Fraction(9, -10) 也要能跟其他分數正確比大小。建構子該做什麼?

(A) 分母為負時,分子分母同乘 −1
(B) 丟出例外拒絕負分母
(C) 在 operator> 裡特判分母正負

收工驗收:完整 Fraction 的使用畫面

講義 01 · 五步蓋完之後(pythonds3/cppds/fraction.hpp)
#include <iostream> #include "pythonds3/cppds/fraction.hpp" // 本節蓋好的完整類別 using namespace std; int main() { Fraction x(1, 2); Fraction y(2, 3); cout << y << endl; cout << x + y << endl; // operator+:通分後約分 cout << boolalpha << (x == y) << endl; Fraction z(2, 4); cout << (x == z) << endl; // 深相等:1/2 == 2/4 return 0; }
預期輸出
2/3
7/6
false
true
講義 01 · 課後練習:負分母與大小比較
class Fraction { // 你的程式碼: // - 建構子把負分母正規化(-9/-10 → 9/10、9/-10 → -9/10) // - bool operator>(Fraction& other) // - bool operator<(Fraction& other) }; int main() { Fraction a(3, 5); Fraction b(9, -10); cout << boolalpha << (a > b) << endl; return 0; }
完成後的預期輸出
true

提示:比大小跟 operator== 一樣用交叉相乘就好,不用真的除;但要先把「負號都搬到分子」,交叉相乘的不等號方向才不會被負分母翻轉。

PART 09 · 繼承

繼承與多型:用邏輯閘蓋一座電路 cppds §1.12

vector 是一種循序集合,我們說 vector「是一個」(IS-A)sequence:這就是繼承要表達的關係。 子類拿到父類的全部家當,再加上自己的特色。講義用數位電路模擬把這套想法跑一遍, 類別階層長這樣:LogicGate →(BinaryGateUnaryGate)→ (AndGateOrGateNotGate)。

A B C D
AND AND OR NOT g1 g2 g3 g4 A B C D 1 1 0 0 1 0 1 0 輸出
講義的示範電路:NOT((A AND B) OR (C AND D))。改動上方任一個下拉選單,電路立刻重算。
對應程式:AndGate g1, g2; OrGate g3; NotGate g4;
Connector c1(&g1,&g3), c2(&g2,&g3), c3(&g3,&g4); 然後 g4.getOutput()。
這段設計裡的四個關鍵字 virtual ... = 0:純虛擬函式。LogicGate 自己不知道怎麼算邏輯,所以宣告了卻不實作, 這讓它成為抽象類別,沒有人能直接 new 一個 LogicGate。
protected:介於 public 與 private 之間,子類碰得到、外人碰不到。label、pin 都放這層。
: LogicGate(n):初始化列,呼叫父類建構子把 n 交上去。
HAS-A:Connector 不繼承 LogicGate,它「擁有」兩個閘(fromGate、toGate)。 IS-A 用繼承、HAS-A 用成員,分清楚這兩種關係是物件導向設計的第一堂課。
基底與 BinaryGate CODE
class LogicGate { public: LogicGate(string n) { label = n; } int getOutput() { output = performGateLogic(); return output; } virtual int performGateLogic() = 0; // 純虛擬函式 protected: // 子類看得到、外界看不到 string label; int output; }; class BinaryGate : public LogicGate { public: BinaryGate(string n) : LogicGate(n) { // 初始化列呼叫父類建構子 pinA = NULL; pinB = NULL; } void setNextPin(Connector* source) { if (pinA == NULL) pinA = source; else if (pinB == NULL) pinB = source; else cout << "Cannot Connect: NO EMPTY PINS"; } protected: Connector *pinA, *pinB; // NULL = 這隻腳還空著 };
具體的閘 CODE
class AndGate : public BinaryGate { public: AndGate(string n) : BinaryGate(n) {} int performGateLogic() { return (getPinA() == 1 && getPinB() == 1) ? 1 : 0; } }; // OrGate、NotGate 同款,只換這一個函式
Connector CODE
class Connector { // 不在閘的階層裡:HAS-A public: Connector(LogicGate* fgate, LogicGate* tgate) { fromGate = fgate; toGate = tgate; tgate->setNextPin(this); // 把自己插上目的閘的腳位 } private: LogicGate *fromGate, *toGate; };
QUIZ · 多型的機關

getOutput() 定義在 LogicGate,卻能執行到 AndGate 的邏輯。靠的是?

(A) virtual:執行期依實際型別挑選 performGateLogic
(B) 編譯器看變數名稱猜的
(C) AndGate 重新定義了 getOutput
QUIZ · 抽象類別

LogicGate g("G0"); 這行會怎樣?

(A) 編譯錯誤:抽象類別不能實例化
(B) 可以,但 getOutput 會回傳 0
(C) 執行期錯誤

想逐步看物件、指標與 virtual 派發的過程, 可以把 gates 的程式碼貼進 C++ Tutor 跑一遍。

講義完整範例:整座電路的程式版

講義 01 · 用類別把電路接起來(pythonds3/cppds/gates.hpp)
#include <iostream> #include "pythonds3/cppds/gates.hpp" // 本節的完整閘類別階層 using namespace std; int main() { AndGate g1("gand1"); AndGate g2("gand2"); OrGate g3("gor3"); NotGate g4("gnot4"); Connector c1(&g1, &g3); // g1 輸出 → g3 輸入 Connector c2(&g2, &g3); Connector c3(&g3, &g4); cout << g4.getOutput() << endl; return 0; }
示範執行(講義實跑:A=1、B=0、C=1、D=0)
Enter pin A input for gate gand1: 1
Enter pin B input for gate gand1: 0
Enter pin A input for gate gand2: 1
Enter pin B input for gate gand2: 0
1

呼叫 g4.getOutput() 會沿著 Connector 一路「往上游要值」:NOT 問 OR、OR 問兩個 AND、AND 才向使用者要輸入。所以 NOT((1 AND 0) OR (1 AND 0)) = NOT(0) = 1。拿上面的互動電路對照:同樣輸入應該得到同樣的 1。

EXERCISES · 練習

綜合練習 cppds §1 綜合

EXERCISE 1 · 溢位

int big = 2147483647; big = big + 1; 之後 big 是?

(A) -2147483648
(B) 2147483648
(C) 執行期錯誤
EXERCISE 2 · pop_back 的回傳值

想取出 vector 最後一個元素並移除它,正確的寫法是?

(A) int x = v.back(); v.pop_back();
(B) int x = v.pop_back();
(C) int x = v.erase(v.end());
EXERCISE 3 · 深相等

Fraction x(1,2); Fraction z(2,4); 定義了交叉相乘的 operator== 之後,x == z 是?

(A) true:比的是值,不是物件身分
(B) false:成員不同就不等
(C) 編譯錯誤
EXERCISE 4 · 型別選擇

要記錄「每位學生的成績清單」,讓你能用學號查到該生所有成績,最直接的容器組合是?

(A) map<string, vector<int>>
(B) vector<map<string,int>>
(C) set<vector<int>>
REFERENCE · 總覽

本章地圖與詞彙 cppds §1 總覽

主題cppds本頁之後在哪裡用到
演算法、抽象化、ADT§1.1–1.6P00每一章的開場白
原子型別、變數、運算子§1.8P01附錄 A 的位址計算
指標§1.8P02串列(ch4)、樹(ch8)、圖(ch9)
集合型別五張表§1.9P03vector/map 全書主力
I/O 與格式化講義補充P04之後所有輸出排版
控制結構講義補充P05所有演算法的骨架
例外處理講義補充P06at()、自訂錯誤
函式與傳參§1.10P07所有演算法的簽名
類別、多載、值語意§1.11P08自訂 Stack、Queue、BST
繼承、virtual、HAS-A§1.12P09物件導向設計

關鍵詞彙(講義 Key terms 精選)

抽象與封裝
ADT:只講操作與行為的資料模型。
資料結構:ADT 的具體實作。
封裝:把狀態藏起來,只留公開介面。
介面:元件之間交換資訊的邊界。
語言機制
編譯器:把整份原始碼翻成機器碼,型別錯誤在這關被抓。
指標& 取址/* 解參考。
namespace:標準函式庫都住在 std 裡。
標頭檔:#include 拉進來的宣告。
錯誤三兄弟
語法錯誤:編譯期被擋。
邏輯錯誤:會跑但答案錯。
例外:執行期被打斷,可 try/catch 接住。
物件導向
建構子:與類別同名、無回傳型別。
多載:同名不同參數列。
覆寫:子類換掉父類的實作。
多型:同一介面、各自表述(virtual)。
深/淺複製:複製內容 vs 複製參考。
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面 題庫 ch1.json · 45 張

詞彙卡取自本章課程題庫,已譯為繁體中文,正面附英文原名。先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。