① 照節次讀:每一節先讀說明、動手玩互動元件,預測結果再按按鈕驗證。 ② 對照講義:頁上 §徽章對應 cppds 章節;細節與完整程式請回講義 01 與 cppds 原文。 ③ 每節做 quiz:答錯就回到該節重讀,不要往下跳。 ④ 最後翻 關鍵詞彙卡(45 張)自測術語(中文為主、術語附英文),並用 REF 總覽區當速查表。標「講義補充」的節是課堂沒細講的延伸,第一輪可略過。
面對一個問題,資訊科學家的目標是寫出演算法:一份一步一步的指令清單, 能解決這個問題的任何一個實例。講義給了正式定義,四個條件缺一不可:
資訊科學同時也是研究抽象化的學問。開車不需要懂引擎,
你只用方向盤、油門、煞車這組「介面」。寫程式也一樣:#include <cmath> 之後喊一聲
sqrt(16) 就拿到 4,不必知道底層是牛頓法還是查表。這叫程序抽象化。
把同一招用在資料上,就是抽象資料型別(ADT):只描述資料有哪些操作、行為是什麼, 完全不管怎麼實作。使用者摸到的是外殼,實作藏在殼裡面一層,這層包裝叫封裝。 而 ADT 的具體實作,就是我們整學期要磨的東西:資料結構。
C++ 要求每個變數先宣告型別才能用。數值型別主要是 int 與
double(後者精度是 float 的兩倍);算術運算子 + - * / %
都在,次方則要用 <cmath> 的 pow()。
運算子與整數除法的完整示範程式及輸出,見本節下方的講義卡「算術運算子與整數除法」。
| 型別 | 大小 | 範圍/說明 | 地雷提醒 |
|---|---|---|---|
| int | 4 byte | −2,147,483,648 到 2,147,483,647 | 溢位不報錯,直接繞回負數。 |
| long long | 8 byte | 約 ±9.2×10¹⁸ | int 不夠用時的第一選擇。 |
| double | 8 byte | 約 ±1.7×10³⁰⁸,15~16 位有效數字 | 0.1 + 0.2 != 0.3,浮點誤差是常態。 |
| char | 1 byte | 'a' 其實是整數 97(ASCII) | ch − 'a' 可算出字母序,第 2 章 anagram 會用到。 |
| bool | 1 byte | true / false | 非零即 true;cout 預設印 1/0,boolalpha 之後印 true/false。 |
int 通常 32 位元,上限 2,147,483,647。所以 pow(2, 100)
只能給你 double 近似值。超過上限不會報錯,直接繞回負數,這種溢位是經典 bug 來源。7 / 3 是 2,不是 2.33。
兩個 int 相除結果就是 int,往零截斷。想要小數,至少讓一邊是 double。bool 只有 true 和 false 兩個值,搭配
&&(且)、||(或)、!(非)。
比較運算的結果也是 bool。整理成一張表:
| 運算 | 寫法 | 說明 |
|---|---|---|
| 大小比較 | < > <= >= | 結果是 bool |
| 相等/不等 | == != | 注意 = 是賦值,== 才是比較 |
| 邏輯且/或/非 | && || ! | 短路求值:左邊定案就不看右邊 |
宣告 int the_sum = 0; 時,編譯器保留一塊剛好放得下 int 的記憶體,
把 0 直接放進去。之後的賦值都是換掉盒子裡的值。三件事值得慢慢咀嚼:
true 塞給 int 變數是合法的,但值會被轉成 1,變數還是 int。int、class、while)不能拿來當名字。int n = 7; double d = n / 2; 之後 d 的值是?
cout << (10 < 5 < 3); 印出什麼?
下面三段是講義 01 的原始示範,值得抄進編譯器跑一次。第一段的重點是整數除法會截斷: 兩個 int 相除結果還是 int,小數部分直接丟掉;只要有一邊是 double,就變成浮點除法。
14 20 1024 2 2 2.33333 1 1.26765e+30
pow() 回傳 double:2100 印出來是 1.26765e+30 這種科學記號近似值,不是精確整數。要大整數得另找函式庫。
false true true true true
&& 接:(3 < x) && (x < 5)。0 1 1
C++ 允許 bool 和數值互轉:true 是 1、false 是 0。方便,但也是 bug 溫床:if (x = 1)(少個等號)永遠為真而且編譯得過。
C++ 把兩種世界都交到你手上:一般變數直接放值,指標是一種放「別的變數的位址」的變數。
兩個運算子要熟到變反射動作:& 取位址、* 解參考(沿位址取值)。
NULL 扮演「這裡沒有東西」的哨兵:
解參考 NULL 的下場是 segfault,程式當場倒地。接上面第 ③ 步之後執行 cout << varN;,印出什麼?
int *p; cout << *p; 這兩行的問題是?
value of var_n: 100 address of var_n: 0x7ffd4c2a5b44 ptr_n stores: 0x7ffd4c2a5b44 *ptr_n gives: 100
位址每次執行都不一樣(0x 開頭的十六進位數),但第二、三行一定相同:ptr_n 存的就是 var_n 的位址。
50
這一行是整章的關鍵畫面:*ptr_n = 50 改的不是 ptr_n,是它指向的那格記憶體。之後的鏈結串列、樹、圖全靠這個動作把結構串起來。
有序集合(sequence)有 array、vector、string;
無序集合有 set 和 map 家族。這裡的「無序」指沒有位置索引,不是沒有排序;
C++ 的 set/map 內部仍照 key 排序,雜湊版才是 unordered_*。
這一節的幾張表整學期都會回頭查,值得現在花時間讀熟。
vector 是同型別元素的有序集合,可以動態長大。兩個要點:
元素必須同型別;切片這類「取一段」的需求用迭代器範圍表達
vector<double>(v.begin()+1, v.begin()+3) 做出來(取索引 1 到 2,不含 3)。
| 方法 | 用法 | 說明 |
|---|---|---|
| [ ] / .at(i) | v[i]、v.at(i) | 索引從 0 起算;at() 有界限檢查、越界丟例外,[] 沒有 |
| push_back | v.push_back(x) | 加到尾端 |
| pop_back | v.pop_back() | 移除最後一項,不回傳值!要值先讀 v.back() |
| insert / erase | v.insert(v.begin()+i, x) | 在第 i 格插入/刪除(要搬移後面所有元素) |
| size / front / back | v.size() | 長度、第一項、最後一項 |
| sort / reverse / count / find | sort(v.begin(), v.end()) | 來自 <algorithm>,吃迭代器範圍 |
v[1] 直達索引 1 的 26,O(1)。
⑤ v.pop_back():31 被丟掉,它不回傳被移除的值——要值得先 v.back() 讀出來再刪。雙引號是 string、單引號是 char,兩者不能混用。
串接用 +,長度是 length()。重點:
C++ 的字串是可變的,my_name[0] = 'X' 完全合法。
| 方法 | 用法 | 說明 |
|---|---|---|
| substr | s.substr(pos, len) | 從 pos 取 len 個字元的子字串 |
| find | s.find("v") | 第一次出現的索引 |
| append / += | s.append(" Ranum") | 接在尾端 |
| insert / erase | s.insert(pos, t) | 插入/刪除一段 |
| c_str | s.c_str() | 轉成 C 風格字元陣列(printf 會用到) |
沒有內建 split():之後解析輸入時會用
<sstream> 的 stringstream 達到同樣效果。
C 語言傳下來的原生陣列 int my_arr[] = {2, 1, 4}; 大小定了就不能改,
它完全沒有界限檢查:
my_arr[3] 會編譯、會執行、然後靜靜讀出一格垃圾記憶體。附錄 A 的自學頁會深挖它的底層。
set<int> s = {3, 6, 4, 6, 3}; 建出來只剩 3、4、6:重複的直接丟掉。
成員檢查用 s.count(x)(回 1 或 0),加入 insert、移除 erase。
聯集、交集、差集、子集判斷由 <algorithm> 的
set_union、set_intersection、set_difference、includes 提供,
搭配 inserter 把結果倒進新容器。
key 對應 value 的查找表。map 內部照 key 排序,unordered_map 用雜湊表(第 2、6 章會分析它們的效能)。
走訪時拿到的是 pair,key 是 .first、value 是 .second。
| 操作 | 用法 | 說明 |
|---|---|---|
| [ ] | m["Iowa"] | 取值;key 不存在時會自動插入一個預設值,這是地雷 |
| at | m.at(k) | 取值,key 不存在改丟例外 |
| count | m.count(k) | 1 表示存在、0 不存在,安全的成員檢查 |
| find | m.find(k) == m.end() | 找不到回 end();這是 C++ 版的「查不到給預設值」慣用法 |
| erase / size / clear | m.erase(k) | 刪除、數量、清空 |
map<string,int> m; cout << m["kent"]; cout << m.size(); 印出什麼?
string s = "David"; s[0] = 'X'; 在 C++ 的結果是?
表格看熟之後,把講義的示範程式親手跑一遍,輸出先用腦袋預測再對答案。
1024 3 1 6.5 0 1024 3 4.5 1 6.5 0 1024 4.5 1 6.5
1 4.5 6.5 1024 1024 6.5 4.5 1 1 1024 4.5 1
C++ 把「容器」和「演算法」拆開:sort、reverse、count、find 都吃一對疊代器 (begin, end),所以同一套演算法能用在不同容器上。
David i DavidDavid 5 vid 2 David Ranum
1024 3 6 Xavid 99 32764 ← 未定義行為:每次執行都可能不同
最後一行是未定義行為(undefined behavior):編譯過、跑得動、答案是垃圾。原生陣列不做界限檢查,這就是課程偏好 vector(配 .at())的原因。
3 4 6 1 0 3 6 99 3 99
set 自動排序又自動去重;交集、聯集(set_union)、子集判斷(includes)都在 <algorithm> 裡,一樣吃疊代器範圍。
brad david kent roman 1137 1410 2001 1171 0 NO ENTRY
小心 map 的 []:查一個不存在的鍵會默默把它插進去(值為 0)。純查詢用 .count() 或 .find(),別用 []。
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 填 | 持續有效 |
| boolalpha | bool 印成 true/false | 持續有效 |
c_str()。cout << setw(10) << "a" << "b"; 的輸出是?
Please enter the radius of the circle 4.5 9
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') 的效果。
The banana costs 24 cents The banana costs 24.00 cents
C 家族的 printf 在 C++ 一樣能用:%10s 是寬度 10 的字串、%5.2f 是寬度 5、小數 2 位。注意 string 要先 .c_str() 轉成 C 字串。
演算法需要兩種控制結構:迭代與選擇。C++ 的身體是大括號圍出來的, 縮排只是給人看的,編譯器完全不管。初學最容易踩的坑: 縮排看起來對、大括號漏了,程式的意思就變了。
| 寫法 | 用途 |
|---|---|
| while (counter <= 5) { ... } | 條件為真就重複 |
| for (int item : my_list) | range-based for:走訪容器成員 |
| for (int i = 0; i < 10; i++) | 計數迴圈,起點、終點、步幅全自訂 |
計數 for 的三段式讓起點、終點、步幅全部自訂:
巢狀 if 疊四層看得眼花,慣用寫法是把 else 跟下一個 if
接成 else if。最後的 else
是保底,漏了它,所有條件都不成立時就什麼都不做:
「取 1 到 10 的奇數、各自平方、收進 vector」這類篩選+轉換需求,C++ 用迴圈與條件組合:
之後學到 <algorithm> 的
transform 和 copy_if,會有更接近宣告式的寫法。
寫 average(a_list):平均 ≥ 60 印 pass、否則 fail,平均取到小數一位。對 {99,100,74,63,100,100} 應印出?
c a t d o g r a b b i t
外層走訪每個單字、內層走訪單字裡的每個字母。這種「攤平」寫法之後在建圖(word ladder buckets)會再出現。
pass (Average: 89.3) fail (Average: 53.7)
提示:總和用 int 累加,平均前記得轉 double,不然 536/6 會整數除法變 89。這正是本頁 P01 的陷阱重出江湖。
錯誤分兩種。語法錯誤是句子寫壞了,編譯器直接拒收,程式根本生不出來, 這其實是 C++ 對你最溫柔的時刻。邏輯錯誤是程式會跑但答案不對; 其中最糟的一類會直接把程式弄死,像除以零、用越界的索引,這種執行期錯誤叫例外。
v[10] 越界是未定義行為,安靜地读垃圾;
v.at(10) 越界丟例外,吵但誠實。開發期用 at() 抓 bug,效能關鍵處確定安全後再用 []。下列哪個「不會」被 try/catch 接住?
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())連例外都不丟,直接未定義行為。
C++ 函式的定義需要四樣東西:回傳型別、名字、帶型別的參數列、本體,
用 return 交回結果。int square(int n) { return n * n; }
就是完整的一課:連回傳值的型別都要先說好。
參數怎麼傳進去,決定函式能不能動到呼叫端的變數。C++ 預設傳值(整份複製),
加 & 變傳參考(操作原變數),傳指標則介於中間:複製的是位址。
| 比較項目 | byValue(a) | byRef(a) | byPtr(&a) |
|---|---|---|---|
| 函式參數 | int x | int &x | int *x |
| 函式拿到什麼 | 影本 | 本人(別名) | 位址 |
| 函式內怎麼改 | x = 99 | x = 99 | *x = 99 |
| 呼叫後 a 的值 | 1(不變) | 99 | 99 |
const vector<int>& v:借看不借改,也省下整包複製。要讓 swap2(x, y) 真的交換呼叫端的兩個變數,參數該怎麼宣告?
9 81
square(square(3)) 由內往外算:內層回傳 9,外層拿 9 再平方。這種「函式結果餵給函式」的組合思維是遞迴章的前菜。
浮點數存 1/3 只能存個近似值。想要「精確的分數」,就得自己造型別。
講義用五步把 Fraction 從空殼蓋成能加、能比、能印的完整類別,每一步都有理由。
operator== 不看兩個物件擺在哪裡,而是把 f1.num × f2.den 與 f2.num × f1.den 交叉相乘後比大小。1/2 與 2/4 位址完全不同,卻是同一個值,所以是「深相等」;若只比位址(淺相等),答案會是 false。建構子跟類別同名、沒有回傳型別。物件自己用隱含指標 this 存取,
成員直接叫名字就好(要明講也可以寫 this->num)。
重點在 private::C++ 的 private 是編譯器強制執行的,
外面寫 a.num 直接編譯錯誤:這不是命名慣例的君子協定,是法律。
順帶一提,class 不寫存取修飾詞時,預設全部 private。
三個建構子同名不同參數列,編譯器在編譯期依引數的數量與型別挑一個。 用預設參數也能做到同一件事,使用者根本分不出來你選了哪種寫法,這正是封裝的意思。
cout << my_fraction 一開始直接編譯失敗。
C++ 不會偷偷退回某種預設表示法幫你混過去,它拒絕猜。
解法是多載串流插入運算子,並宣告成 friend,讓它能讀 private 的 num 和 den:
通分相加:a/b + c/d = (ad + cb)/bd。直接這樣存會愈加愈肥(1/4 + 1/2 = 6/8), 所以建構子或加法要用最大公因數約分,6/8 進門就變 3/4。這維護了一條不變量: Fraction 永遠是最簡分數,其他方法都可以放心依賴這件事。
沒定義 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,深相等不必先通分 |
Fraction f1(1,2); Fraction f3 = f1; 之後改動 f3,f1 會變嗎?
講義要求 Fraction(9, -10) 也要能跟其他分數正確比大小。建構子該做什麼?
2/3 7/6 false true
true
提示:比大小跟 operator== 一樣用交叉相乘就好,不用真的除;但要先把「負號都搬到分子」,交叉相乘的不等號方向才不會被負分母翻轉。
vector 是一種循序集合,我們說 vector「是一個」(IS-A)sequence:這就是繼承要表達的關係。
子類拿到父類的全部家當,再加上自己的特色。講義用數位電路模擬把這套想法跑一遍,
類別階層長這樣:LogicGate →(BinaryGate、UnaryGate)→
(AndGate、OrGate、NotGate)。
getOutput() 定義在 LogicGate,卻能執行到 AndGate 的邏輯。靠的是?
LogicGate g("G0"); 這行會怎樣?
想逐步看物件、指標與 virtual 派發的過程, 可以把 gates 的程式碼貼進 C++ Tutor 跑一遍。
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。
int big = 2147483647; big = big + 1; 之後 big 是?
想取出 vector 最後一個元素並移除它,正確的寫法是?
Fraction x(1,2); Fraction z(2,4); 定義了交叉相乘的 operator== 之後,x == z 是?
要記錄「每位學生的成績清單」,讓你能用學號查到該生所有成績,最直接的容器組合是?
| 主題 | cppds | 本頁 | 之後在哪裡用到 |
|---|---|---|---|
| 演算法、抽象化、ADT | §1.1–1.6 | P00 | 每一章的開場白 |
| 原子型別、變數、運算子 | §1.8 | P01 | 附錄 A 的位址計算 |
| 指標 | §1.8 | P02 | 串列(ch4)、樹(ch8)、圖(ch9) |
| 集合型別五張表 | §1.9 | P03 | vector/map 全書主力 |
| I/O 與格式化 | 講義補充 | P04 | 之後所有輸出排版 |
| 控制結構 | 講義補充 | P05 | 所有演算法的骨架 |
| 例外處理 | 講義補充 | P06 | at()、自訂錯誤 |
| 函式與傳參 | §1.10 | P07 | 所有演算法的簽名 |
| 類別、多載、值語意 | §1.11 | P08 | 自訂 Stack、Queue、BST |
| 繼承、virtual、HAS-A | §1.12 | P09 | 物件導向設計 |
詞彙卡取自本章課程題庫,已譯為繁體中文,正面附英文原名。先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。