.cpp.i .s / .oa.out
PREREQ P1

C++ 基礎編譯流程

先備知識 · 語法與互動
g++ -std=c++17|int main()|cout <<|7 / 2 == 3|static_cast
向下捲動開始互動
📌 本頁使用方式(先備知識 · 語法與互動)

先讀語法與例子,再操作互動圖解並完成四選一自測。忘記寫法時回看速查表,最後用中英詞彙卡複習。

CONTENTS · 內容目錄
PROLOGUE · 開場

你按下執行之後,C++ 會先經過四個建置階段

把本頁的 hello world 丟給 g++,它會先展開標頭、再翻成組合語言與目的檔,最後產生執行檔。中間檔案的行數與大小會隨編譯器、標準函式庫、平台與選項改變,但四個階段的順序不變。

常見的 C++ 建置方式是在執行前完成編譯與連結。Python 的常見實作會先把原始碼編成位元碼,再交給虛擬機執行;語法錯誤也可能在執行前被發現。兩者的重要差別是型別檢查與執行方式,不能簡化成「一個逐行讀、一個一次讀完」。

PythonC++
怎麼跑起來常見實作先產生位元碼,再由虛擬機執行先整份編譯成執行檔,再執行那個檔
什麼時候發現打錯字語法錯誤可能先被發現;名稱或型別使用錯誤常在執行相關運算時發現編譯時就發現,程式根本跑不起來
型別執行時才決定,同一個變數可以換型別宣告時就定死,之後不能換
要不要自己管記憶體不用,有垃圾回收區域物件及標準容器會管理自身生命週期;直接使用 new 時需安排對應釋放(P4)
執行效能取決於實作、工作內容與所用函式庫取決於程式、編譯選項與所用函式庫
執行需要什麼目標機器上要有直譯器執行檔本身,但換平台要重編

C++ 讓型別、物件大小、記憶體配置與複製成本清楚出現在程式中,因此適合用來觀察每項操作實際付出的成本。

📌 本頁定位

這一頁的目標很具體:讓你看懂一支獨立 C++ 程式,以及 g++ -std=c++17 -Wall main.cpp -o main 各部分的作用。

PART 01 · 從原始碼到執行檔

g++ -std=c++17 -Wall main.cpp -o main 拆開來看

先看一行完整編譯指令,再逐個認識選項:

零件意思不寫會怎樣
g++GNU 的 C++ 編譯器驅動程式
-std=c++17用 C++17 這版語言標準吃到編譯器的預設版本,某些寫法會突然不能用
-Wall把所有常見警告都打開少了這組常用警告;編譯器仍可能發出其他診斷
main.cpp要編的原始碼沒東西可編
-o main產出的執行檔叫 main預設叫 a.out

跑起來就是 ./main(Windows 上是 main.exe)。沒有錯誤與警告時,g++ 通常不印訊息;若只有警告,仍可能產生執行檔,但應先讀懂警告再執行。

可在本機獨立編譯的第一支程式
#include <iostream> #include <cmath> using namespace std; int main() { cout << "Hello, C++!" << endl; cout << sqrt(16) << endl; return 0; }
預期輸出
Hello, C++!
4

三個地方值得停一下:① #include <cmath> 沒寫的話 sqrt 就是未宣告識別字,編譯直接失敗。② sqrt(16) 印出 4 而不是 4.000000,因為 cout 預設只印有意義的位數。③ return 0 是回報給作業系統的結束碼,0 代表正常結束。

按「開始」,看這 9 行原始碼怎麼一路變成執行檔。
速度
四階段的旗標 CLI
g++ -E main.cpp -o main.i # 只做前處理 g++ -S main.cpp -o main.s # 停在組合語言 g++ -c main.cpp -o main.o # 停在目的檔 g++ main.o -o main # 只做連結

平常那行 g++ main.cpp -o main 是把四步一次做完,中間檔案跑完就丟掉。

結果依環境而異 NOTE

前處理通常會讓檔案大幅增長,因為 <iostream><cmath> 的宣告會被展開。實際行數與檔案大小不應視為固定規格。

9
main.cpp 行數
依環境
前處理後 main.i 行數
依選項
組合語言 main.s 行數
依平台
最後的執行檔大小
💡 為什麼要知道有四個階段

編譯診斷通常包含檔名、行與欄;連結診斷常包含 undefined reference,代表某個符號找不到定義。連結訊息也可能附來源位置,不能只靠有無行號判斷。PART 07 會逐一比較。

PART 02 · 程式骨架

從最小程式認識四個常見零件

一般 C++ 執行程式需要 main 作為進入點。下面的標頭、using 宣告與 return 依需求使用:若只寫 int main() {} 也合法,到達 main 結尾等同回傳 0。

#include <iostream> // ① 把要用的東西宣告搬進來 using namespace std; // ② 省掉 std:: 前綴 int main() { // ③ 程式從這裡開始 cout << "hi" << endl; return 0; // ④ 回報結束碼給作業系統 }
零件它在做什麼Python 對應
#include <iostream><iostream> 這個標頭檔整份貼進來,於是編譯器才認得 coutimport
using namespace std;std 這個命名空間攤平,讓你可以寫 cout 而不是 std::coutfrom x import *
int main()一般 C++ 程式的進入點,由執行環境呼叫if __name__ == "__main__"
return 0;回報結束碼,0 表示正常。非 0 代表出錯,shell 腳本會看這個值sys.exit(0)

#include前處理階段做的,字面上就是文字替換。前處理後常見的大幅膨脹,主要來自標頭檔內容被展開。

尖括號與雙引號的差別

寫法去哪裡找什麼時候用
#include <iostream>先找系統的標準路徑標準函式庫:<iostream> <string> <vector> <cmath>
#include "greeting.hpp"先依實作規則搜尋來源檔附近等位置;找不到時通常再採尖括號形式的搜尋規則專案自己提供的標頭檔

專案自己的標頭檔用雙引號引進來。若標頭檔不在目前資料夾,可以用 -I 告訴 g++ 額外的搜尋位置:

引用同一資料夾內的自訂標頭

先將以下內容另存為 greeting.hpp。這裡定義一個回傳固定文字的小函式;目前只需觀察自訂標頭如何被引入,函式語法會在 P3 展開。

#pragma once #include <string> inline std::string greeting() { return "Hello from a local header!"; }
#include <iostream> #include "greeting.hpp" using namespace std; int main() { cout << greeting() << endl; return 0; }
預期輸出
Hello from a local header!

greeting.hppmain.cpp 放在同一資料夾時,可直接用 g++ -std=c++17 main.cpp -o main 編譯;若放在 include/,則使用 -I include 加入搜尋路徑。找不到時會看到 fatal error: greeting.hpp: No such file or directory

✅ 現代 C++ 對照

using namespace std; 會把整個標準函式庫的名字帶入目前作用域,容易跟自己的名字相撞。較穩健的寫法是逐個 using std::cout;,或直接保留 std:: 前綴。

備註:不要在標頭檔寫 using namespace std;

標頭檔會被原地貼進每個 include 它的來源檔;若在標頭中攤平命名空間,所有引用者都會被迫接受這項選擇。自訂 .hpp 應保留 std:: 前綴。

PART 03 · 變數與型別

宣告的時候就定死,之後不能換

這是 Python 轉 C++ 第一個要改的習慣。Python 裡 x 前一行是整數、下一行變字串完全沒問題;C++ 的 int x 從出生到離開範圍都是 int

Python(動態型別)C++(靜態型別)
宣告x = 5int x = 5;
換型別x = "hi" 合法x = "hi"; 編譯錯誤
型別寫在哪不寫,執行時看值決定寫在變數名前面,編譯時決定
沒給初值不存在這種事,一定要指派區域 int 未初始化時不能直接讀值
字元沒有單獨的字元型別,長度 1 的字串char c = 'A'; 單引號
字串s = "hi"string s = "hi"; 要 #include <string>
五個基本型別各佔多少空間
#include <iostream> #include <string> using namespace std; int main() { int n = 42; double d = 3.14159; char c = 'A'; bool b = true; string s = "David"; cout << "int n = " << n << " sizeof = " << sizeof(n) << " bytes" << endl; cout << "double d = " << d << " sizeof = " << sizeof(d) << " bytes" << endl; cout << "char c = " << c << " sizeof = " << sizeof(c) << " bytes" << endl; cout << "bool b = " << b << " sizeof = " << sizeof(b) << " bytes" << endl; cout << "string s = " << s << " sizeof = " << sizeof(s) << " bytes (物件本身大小)" << endl; cout << boolalpha << "boolalpha 之後 b = " << b << endl; return 0; }
預期輸出
int    n = 42   sizeof = 4 bytes
double d = 3.14159   sizeof = 8 bytes
char   c = A     sizeof = 1 bytes
bool   b = 1     sizeof = 1 bytes
string s = David sizeof = 32 bytes (物件本身大小)
boolalpha 之後 b = true

sizeof(s) 計算 string 物件本身占用的位元組數,s.size() 計算字串中的字元數。物件大小與字元儲存方式由標準函式庫實作決定;短字串可直接存在物件內,較長的字串可另行配置空間。

點一個型別,看它佔幾個 byte、裝得下多大的數,以及裝不下時會怎樣。
怎麼選 RULE

一般整數可先考慮 int,先確認資料與運算中間結果是否在其範圍內,必要時改用 long long。浮點數常用 double;float 適用於記憶體或介面限制且精度足夠的情況。範圍與精度應查 <limits>,不靠型別名稱猜測。

數字都是真的 REAL

面板上的大小與範圍是在這台 64 位元 Linux 上用 sizeof<climits> 實際印出來的。C++ 標準只規定下限,換平台可能不一樣,所以要問大小就用 sizeof,不要背。

區域變數使用前必須先有確定的值

未初始化的區域基本型別沒有可依賴的值;在這裡讀取它會產生未定義行為。程式可能看似輸出某個數字,也可能在最佳化後出現完全不同的結果,因此不能把一次試跑當成證據。

同一支程式,兩個沒初始化的 int
#include <iostream> using namespace std; void dirty() { // 先在堆疊上留下一些值 int a = 123456, b = -987654; (void)a; (void)b; } int main() { dirty(); int x; // 沒有初始化:錯誤示範:使用前未初始化 int y; int z = 0; // 有初始化 cout << "x = " << x << endl; cout << "y = " << y << endl; cout << "z = " << z << endl; return 0; }
預期輸出
錯誤示範:讀取未初始化的 x、y,行為未定義,沒有可依賴的輸出。

在 C++17 中讀取這裡未初始化的 x 或 y 是未定義行為,不能推論為某個殘值,也沒有固定輸出。這是錯誤示範;正確做法是在使用前初始化或賦值,例如 int x = 0;。

⚠️ 最常見的錯誤寫法

例如 int sum; 後直接 sum += 3; 會先讀到未初始化的 sum。應先寫 int sum = 0;,計數器同理寫 int count = 0;。這個問題與數字看起來是否合理無關,關鍵是使用前有沒有確定的值。

💡 一行宣告多個變數

int low, high; 一次宣告兩個 int。若寫 int a, b = 5;,只有 b 被初始化,a 仍不能直接讀取;要各自給初值,例如 int a = 0, b = 5;。

PART 04 · 輸入輸出

cout <<cin >>:箭頭指著資料流的方向

記法很直觀:cout << x 是把 x輸出串流,cin >> x 是把輸入串流的東西倒出來放進 x。箭頭指哪邊,資料就往哪邊跑。

cout 的四種常見用法
#include <iostream> #include <string> using namespace std; int main() { string name = "David"; int age = 20; cout << "姓名:" << name << ",年齡:" << age << endl; cout << "一行" << '\n' << "兩行" << '\n'; cout << 1 << 2 << 3 << endl; // << 可以一直串下去 cout << "1 + 2 = " << 1 + 2 << endl; // 運算先做完才丟給 << return 0; }
預期輸出
姓名:David,年齡:20
一行
兩行
123
1 + 2 = 3

第三行印出 123 而不是 1 2 3 —— << 不會自動加空格,要空格自己寫。第四行的 1 + 2 先算完才丟給 <<,因為 + 的優先順序比 << 高。

你想做的事PythonC++
印一個值並換行print(x)cout << x << endl;
印了不換行print(x, end="")cout << x;
印兩個值中間有空格print(a, b)cout << a << " " << b;
讀一整行s = input()getline(cin, s);
讀一個整數n = int(input())cin >> n;
印 true / falseprint(True)cout << boolalpha << b;

endl'\n' 差在哪

兩個都換行,但 endl 多做一件事:強迫把緩衝區的東西立刻寫出去。輸出量大的時候(例如印一萬筆排序結果)每行都 endl 會明顯變慢。

endl'\n'
換行
清空緩衝區不會
速度
什麼時候用要立刻看到輸出時,例如印 debug 訊息、程式可能會當掉其他所有情況

順帶一提:'\n'單引號包的一個 char"\n" 是雙引號包的字串。C++ 的單引號與雙引號不能互換,char c = "A"; 會編譯錯誤。

讀進來:>> 讀一個詞,getline 讀一整行

#include <iostream> #include <string> using namespace std; int main() { int n; cout << "請輸入一個整數:"; cin >> n; // 以空白或換行為界,讀進一個 int cin.ignore(); // 丟掉 >> 沒吃掉的那個換行 string line; cout << "請輸入一整行:"; getline(cin, line); // 連空白一起讀,直到換行 cout << "n = " << n << ",line = " << line << endl; return 0; }

上面這支需要互動輸入。下面改用固定字串作為輸入來源;對本例的 >> 格式化讀取而言,空白切分與型別轉換規則相同,因此能穩定展示解析結果:

同一組資料,>> 與 getline 各讀到什麼
#include <iostream> #include <sstream> #include <string> using namespace std; int main() { // 用 istringstream 提供固定資料,沿用 >> 的解析規則 istringstream input("David Malan 20\nHello data structures\n"); string first, last; int age; input >> first >> last >> age; // >> 以空白為分隔,一次讀一個 token cout << "first=[" << first << "] last=[" << last << "] age=" << age << endl; string line; getline(input, line); // >> 沒吃掉的換行還留著,先讀到空字串 cout << "第一次 getline=[" << line << "]" << endl; getline(input, line); // 這次才讀到整行 cout << "第二次 getline=[" << line << "]" << endl; return 0; }
預期輸出
first=[David] last=[Malan] age=20
第一次 getline=[]
第二次 getline=[Hello data structures]

看第二行輸出:第一次 getline 讀到的是空字串。因為 >> 讀完 20 就停了,那個換行還留在串流裡,getline 一讀就碰到它、當場結束。這是初學者最常卡住的地方 —— 解法是在 >> 之後、getline 之前補一行 cin.ignore();,把那個換行丟掉。

⚠️ >> 以空白分隔,讀不到有空格的名字

cin >> name; 碰到 David Malan 只會拿到 DavidMalan 留在串流裡等下一次讀。要整行就用 getline(cin, name);

printf:用格式字串控制輸出

printf 以格式字串指定欄寬、精度與引數型別,需引入 <cstdio>。cout 依型別選擇輸出方式,也能搭配 <iomanip> 排版。以下保留兩種方式的比較,重點是 printf 的格式必須與引數型別一致。

轉換字元對應型別格式示例
%dintprintf("%d 頁", pages);
%s字串,要先 .c_str()printf("%s costs %d cents\n", item.c_str(), price);
%f %10.7fdouble10 是欄寬、7 是小數位數,數字自動靠右對齊
%lldlong longprintf("Sum is %lld required %10.7f seconds\n", s, secs);
#include <iostream> #include <cstdio> #include <string> using namespace std; int main() { string item = "eraser"; int price = 35; printf("The %s costs %d cents\n", item.c_str(), price); // string 要先 .c_str() long long s = 500000500000LL; double secs = 0.0012345; printf("Sum is %lld required %10.7f seconds\n", s, secs); bool found = true; cout << "found(沒加 boolalpha)= " << found << endl; cout << boolalpha; cout << "found(加了 boolalpha)= " << found << endl; return 0; }
預期輸出
The eraser costs 35 cents
Sum is 500000500000 required  0.0012345 seconds
found(沒加 boolalpha)= 1
found(加了 boolalpha)= true

%s 需要 C 字串,std::string 可用 .c_str() 取得對應指標。不能直接把 string 物件交給 %s。cout 預設將 bool 印成 1 或 0,加入 boolalpha 後會印 true 或 false,這個設定持續生效,直到改用 noboolalpha。

PART 05 · 運算子

整數除法:這一節最重要的一件事

在 Python 3 裡 7 / 23.5,要整數除法得寫 7 // 2。C++ 沒有 // 這個運算子(那是註解),它的規則只有一句:兩邊都是整數,除法就是整數除法,小數直接丟掉

整數除法與取餘數
#include <iostream> using namespace std; int main() { cout << "7 / 2 = " << 7 / 2 << " <- 兩邊都是 int,小數被截掉" << endl; cout << "7 % 2 = " << 7 % 2 << " <- 餘數" << endl; cout << "7.0 / 2 = " << 7.0 / 2 << " <- 有一邊是 double 就變浮點除法" << endl; cout << "-7 / 2 = " << -7 / 2 << " <- 往零的方向截,不是往下取整" << endl; cout << "-7 % 2 = " << -7 % 2 << " <- 餘數跟被除數同號" << endl; cout << "1453 / 16 = " << 1453 / 16 << endl; cout << "1453 % 16 = " << 1453 % 16 << " <- 進位制轉換就靠這兩個" << endl; return 0; }
預期輸出
7 / 2     = 3   <- 兩邊都是 int,小數被截掉
7 % 2     = 1   <- 餘數
7.0 / 2   = 3.5 <- 有一邊是 double 就變浮點除法
-7 / 2    = -3  <- 往零的方向截,不是往下取整
-7 % 2    = -1  <- 餘數跟被除數同號
1453 / 16 = 90
1453 % 16 = 13   <- 進位制轉換就靠這兩個

四個要記住的點:① 只要有一邊是 double,整條式子就變浮點除法。② -7 / 2-3 不是 -4,C++ 往的方向截,跟 Python 的 // 往下取整不一樣。③ 因此 -7 % 2-1,餘數跟被除數同號,Python 的 % 則跟除數同號。④ 最後兩行就是進位制轉換的全部:/ 拿到下一輪要處理的數,% 拿到這一位的數字。

式子Python 3C++為什麼
7 / 23.53C++ 兩邊都是 int,做整數除法
7.0 / 23.53.5有一邊是浮點數就做浮點除法
7 // 23語法錯誤//
-7 / 2-3.5-3往零截斷
-7 // 2-4Python 往下取整
-7 % 21-1C++ 的餘數跟被除數同號

遞增與複合指定

i++ 與 ++i 差在什麼時候加
#include <iostream> using namespace std; int main() { int i = 5; cout << "i++ 印出 " << i++; // 先用後加 cout << ",之後 i = " << i << endl; int j = 5; cout << "++j 印出 " << ++j; // 先加後用 cout << ",之後 j = " << j << endl; int total = 10; total += 5; cout << "total += 5 -> " << total << endl; total -= 3; cout << "total -= 3 -> " << total << endl; total *= 2; cout << "total *= 2 -> " << total << endl; total /= 4; cout << "total /= 4 -> " << total << " <- 整數除法還是會截" << endl; return 0; }
預期輸出
i++ 印出 5,之後 i = 6
++j 印出 6,之後 j = 6
total += 5  -> 15
total -= 3  -> 12
total *= 2  -> 24
total /= 4  -> 6  <- 整數除法還是會截

i++先把舊值交出去,再加一++i先加一,再交出新值。單獨寫一行當作敘述時兩者沒差(for 迴圈的 i++ 換成 ++i 結果一樣),差別只在把它塞進別的式子裡的時候。最後一行提醒你:/= 一樣會截斷。

優先順序,還有一個 Python 習慣帶不過來

連續比較在 C++ 是另一個意思
#include <iostream> using namespace std; int main() { cout << boolalpha; cout << "2 + 3 * 4 = " << 2 + 3 * 4 << endl; cout << "(2 + 3) * 4 = " << (2 + 3) * 4 << endl; cout << "10 < 5 = " << (10 < 5) << endl; cout << "10 < 5 < 3 = " << (10 < 5 < 3) << " <- 先算 10<5 得到 0,再算 0<3" << endl; return 0; }
預期輸出
2 + 3 * 4   = 14
(2 + 3) * 4 = 20
10 < 5      = false
10 < 5 < 3  = true   <- 先算 10<5 得到 0,再算 0<3

Python 的 10 < 5 < 3 是「同時滿足兩個條件」,答案是假。C++ 沒有這種語法糖,它從左邊算起:10 < 5 得到 false(也就是 0),再算 0 < 3,於是變成。要表達範圍檢查,一定要寫成 (5 <= x) && (x <= 10)

優先順序(高到低)運算子
1() [] -> .
2++ -- ! *(解參考) &(取址)
3* / %
4+ -
5<< >>
6< <= > >=
7== !=
8&&
9||
10= += -= *= /= %=

不用背整張表,記兩件事就夠:<< 的優先順序比比較運算高,所以 cout << a < b 會編譯錯誤,要寫 cout << (a < b)&&||,混用時加括號比較安全。

隨堂 1 · 平均分數

三個人的分數是 90、85、79,總分 254。int avg = 254 / 3; 之後 avg 是多少?

(A) 84.67
(B) 84
(C) 85
(D) 編譯錯誤
💡 rand()srand():靠 % 把亂數壓進範圍

rand() 產生 0 到 RAND_MAX 的偽亂數。rand() % 6 + 1 可示範如何將結果映射到 1 到 6:先取餘數,再平移。不過這種映射通常有偏差,不能用來保證公平骰子;本例只教 % 的範圍運算。

#include <iostream> #include <cstdlib> using namespace std; int main() { srand(42); for (int i = 0; i < 3; ++i) { int face = rand() % 6 + 1; cout << "" << i + 1 << " 次點數 " << face << endl; } }
預期輸出
每行點數都介於 1 到 6;確切序列依 rand 的實作而異。

不呼叫 srand 時,rand 的行為如同用種子 1 初始化。同一實作使用相同種子可重現相同序列,但不同實作不保證相同。srand(time(0)) 可使用時間作種子,需 <ctime>;在相同秒數啟動仍可能得到相同序列。

PART 06 · 型別轉換

編譯器會自己轉,問題是它轉的時機不是你要的

C++ 會依運算規則做隱式型別轉換,不是所有情況都只把小型別轉成大型別。此處先掌握一個重點:int / int 先算整數商,即使之後存入 double,已被截掉的小數也不會恢復。

式子編譯器怎麼做結果
7 / 2.0把 7 轉成 7.0,做浮點除法3.5
7 / 2兩邊都是 int,不轉3
double d = 7 / 2;先算完 3,才轉成 3.03
int n = 3.99;截斷小數,不是四捨五入3
int n = true;boolint1
int n = 'A';charint,取 ASCII 碼65

第三列最不容易察覺。double avg = sum / n; 看起來完全合理,但等號右邊是兩個 int,除法在轉成 double 之前就做完了。

同一組數字,轉型位置不同,答案不同
#include <iostream> using namespace std; int main() { int a = 7, b = 2; cout << "a / b = " << a / b << endl; cout << "(double)(a / b) = " << (double)(a / b) << " <- 先做整數除法才轉,太晚了" << endl; cout << "static_cast<double>(a) / b = " << static_cast<double>(a) / b << " <- 先把一邊變成 double,正確" << endl; double d = 3.99; cout << "static_cast<int>(3.99) = " << static_cast<int>(d) << " <- 轉 int 是截斷,不是四捨五入" << endl; int scores[3] = {90, 85, 78}; int sum = 0; for (int i = 0; i < 3; i++) sum += scores[i]; cout << "平均(錯)= " << sum / 3 << ",平均(對)= " << static_cast<double>(sum) / 3 << endl; return 0; }
預期輸出
a / b                       = 3
(double)(a / b)             = 3  <- 先做整數除法才轉,太晚了
static_cast<double>(a) / b  = 3.5 <- 先把一邊變成 double,正確
static_cast<int>(3.99)      = 3   <- 轉 int 是截斷,不是四捨五入
平均(錯)= 84,平均(對)= 84.3333

第二行的 (double)(a / b) 是典型的錯法:括號包住整個除法,等於「先算出 3,再把 3 變成 3.0」。第三行才對 —— 把其中一邊先變成 double,除法才會用浮點的規則做。只轉一邊就夠,另一邊會被隱式轉換帶上去。

💡 一個口訣

要浮點的答案,轉型要放在除號的左邊、括號的裡面static_cast<double>(a) / b。放錯位置的 static_cast<double>(a / b) 編得過、跑得動、答案錯,而且錯得很安靜。

💡 其他常見轉型寫法

對本節的算術型別 x,static_cast<double>(x)、(double)x 與 double(x) 都能轉成 double。這不表示各種轉型語法在所有型別上等價;先使用用途清楚、方便搜尋的 static_cast,閱讀舊程式時再辨認其他形式。

整數溢位:分清有號與無號規則

INT_MAX 表示目前實作的 int 上限。有號運算超出範圍是未定義行為,不保證變成負數;無號數的模數繞回則有明確規則。以下把危險運算留在註解中,執行的只有定義良好的部分。

int 撞到天花板
#include <iostream> #include <climits> using namespace std; int main() { int big = INT_MAX; cout << "int 範圍:" << INT_MIN << " ~ " << INT_MAX << endl; // big + 1 是有號溢位,不可執行。 cout << "unsigned 0 - 1 = " << 0u - 1 << endl; if (LLONG_MAX > INT_MAX) { long long safe = static_cast<long long>(big) + 1; cout << "先轉 long long = " << safe << endl; } }
預期輸出
若 int 為 32 位元、long long 為 64 位元:
int 範圍:-2147483648 ~ 2147483647
unsigned 0 - 1 = 4294967295
先轉 long long = 2147483648

不要執行 int 的 INT_MAX + 1;應先確認較寬型別能容納結果,再在運算之前轉型。此例使用 long long 前會檢查 LLONG_MAX 是否大於 INT_MAX。0u - 1 則是無號運算,標準保證結果為 UINT_MAX。

💡 INT_MAX 也拿來當「無限大」哨兵

INT_MAX 是有限整數,不是真正的無限大。它可以當「尚未找到較小值」的初始標記,例如 int best = INT_MAX;,之後只以較小且有效的觀測值更新。若 INT_MAX 本身也可能是合法答案,還需額外的 bool found 區分;也不可對這個標記隨意加一。

⚠️ 中間運算也可能溢位

例如計算 n 個項目兩兩配對的數量 n * (n - 1) / 2,中間乘積可能先超出 int。可先轉型:long long total = static_cast<long long>(n) * (n - 1) / 2;,仍須確認 n 的有效範圍與 long long 的容量。賦值給較大型別本身不會補救先前的溢位。

✅ 現代 C++ 對照

對本例的算術型別,C 式轉型與 static_cast 結果相同。static_cast 將轉型意圖明確寫在程式中,也限制可接受的轉換種類;不要把它當作能修復先前運算溢位的工具。

PART 07 · 讀懂編譯錯誤

g++ 的診斷訊息有常見格式,先看懂開頭幾行

g++ 的訊息通常會列出檔名、行號、欄號與診斷內容。先修正第一個 error:,接著重新編譯,再檢查其餘訊息;後面的診斷有些會隨第一個錯誤一起消失,也可能仍有需要處理的獨立問題。

main.cpp: In function 'int main()': main.cpp:6:5: error: expected ',' or ';' before 'cout'
片段意思
main.cpp哪一個檔出事
6第幾行
5那一行的第幾個字元(欄)
error:等級。error 一定要修,warning 編得過但通常也該修
expected ',' or ';' before 'cout'錯在哪。這一段才是要讀的東西
In function 'int main()'出事的位置在哪個函式裡。函式一多這行就很有用

下面保留四類常見錯誤與對應程式,診斷的詳細措辭可能依 g++ 版本而不同。

① 漏了分號
#include <iostream> using namespace std; int main() { int x = 10 cout << x << endl; return 0; }
g++ 錯誤訊息
[編譯失敗]
cell.cpp: In function ‘int main()’:
cell.cpp:6:5: error: expected ‘,’ or ‘;’ before ‘cout’
    6 |     cout << x << endl;

注意它指向第 6 行,但本例漏掉的分號在第 5 行。編譯器到讀見下一個不合語法的 token 才能確定問題,因此診斷位置可能晚於真正原因。看到 expected ';' 時,可先檢查該位置及前一個敘述的結尾。

② 型別不合
#include <iostream> #include <string> using namespace std; int main() { string name = "David"; int n = name; cout << n << endl; return 0; }
g++ 錯誤訊息
[編譯失敗]
cell.cpp: In function ‘int main()’:
cell.cpp:7:13: error: cannot convert ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’} to ‘int’ in initialization
    7 |     int n = name;

cannot convert A to B 是最好讀的一類:它明講了「有一個 A,但這裡要的是 B」。中間那串 std::__cxx11::basic_string<char>string 的完整名字,看到 aka 後面才是人話。這一類錯誤在你開始用 template <typename T> 之後會變得又臭又長,訣竅一樣:只讀 cannot convert 那一句

③ 未宣告的識別字
#include <iostream> using namespace std; int main() { int total = 0; total = total + conut; cout << total << endl; return 0; }
g++ 錯誤訊息
[編譯失敗]
cell.cpp: In function ‘int main()’:
cell.cpp:6:21: error: ‘conut’ was not declared in this scope; did you mean ‘const’?
    6 |     total = total + conut;

was not declared in this scope 只有三種可能:打錯字(這裡是 conut)、忘了宣告忘了 #include。g++ 甚至會猜你想打什麼(did you mean),雖然這次猜錯了。如果訊息裡的名字是 coutsqrt 這種標準函式庫的東西,那就是第三種。

還有一類長得完全不一樣,因為它發生在連結階段而不是編譯階段:

④ 連結錯誤:函式只宣告沒定義
#include <iostream> using namespace std; int addUp(int a, int b); // 只宣告,沒有定義 int main() { cout << addUp(3, 4) << endl; return 0; }
g++ 錯誤訊息
[編譯失敗]
/usr/bin/ld: main.o: in function `main':
main.cpp:(.text+0x13): undefined reference to `addUp(int, int)'
collect2: error: ld returned 1 exit status

認得出來的兩個特徵:開頭是 /usr/bin/ld:(ld 是連結器),而且沒有指到你的哪一行undefined reference to 的意思是「我知道有這個函式,但找不到它的身體」。常見原因是宣告了忘記實作、函式簽章打錯字,或多檔案專案只編了一個 .cpp

看到這句八成是先去哪裡看
expected ';' before …漏分號它指的那行的上一行
was not declared in this scope打錯字/忘了 include那個名字的拼字,或檔案開頭
cannot convert A to B型別不合等號兩邊、或函式的參數
no matching function for call to …參數個數或型別對不上函式的宣告與呼叫處比對
undefined reference to …連結階段:只宣告沒定義該函式的實作,或編譯指令少了檔案
No such file or directoryinclude 的路徑不對-I 有沒有加對(見 PART 02)
Segmentation fault這不是編譯錯誤,是執行時當掉通常是指標,見 P4
🎯 三個習慣,可以少掉一半的 debug 時間

從第一個 error 開始修,修完馬上重編 —— 一個錯常常引發後面二十個。
編譯時加 -Wall -Wextra。它能發現許多可疑寫法,例如部分未初始化使用與有號/無號比較;警告能力仍取決於控制流程、編譯器與選項,不能取代測試。
每寫十行就編譯一次。寫完兩百行才第一次按編譯,錯誤會互相糾纏到看不出源頭。

備註:assert,另一種比編譯錯誤更晚一步的檢查

上面講的都是編譯期的錯誤;assert(條件) 抓的是執行期的錯誤。條件不成立時,程式立刻中止並印出發生問題的那一行,比讓錯誤值繼續傳遞、到後面才失敗容易追查。

#include <iostream> #include <cassert> using namespace std; int half(int n) { assert(n % 2 == 0); // 條件不成立就中止,並印出這一行 return n / 2; } int main() { cout << half(10) << endl; cout << half(7) << endl; // 這裡會觸發 assert return 0; }
預期輸出
5
[執行結束碼 -6]
main: main.cpp:6: int half(int): Assertion `n % 2 == 0' failed.

未定義 NDEBUG 時,half(10) 通過斷言;half(7) 的條件為假,assert 會輸出診斷並呼叫 abort 終止程式,結束碼表示依平台及執行工具而異。若編譯時定義 NDEBUG,assert 會被停用,因此不可用它取代必要的使用者輸入檢查。

EX · 隨堂練習

兩題確認你抓到重點

EXERCISE · 這段程式錯在哪

目標是印出 7 除以 2 的小數答案,但它印出 3

int a = 7, b = 2; double result = a / b; cout << result << endl;
(A) result 要宣告成 float
(B) a / b 是整數除法,轉型要在除法之前做
(C) cout 把小數點吃掉了,要設精度
(D) 這段根本編譯不過
EXERCISE · 錯誤訊息指到第 6 行

g++ 印出 main.cpp:6:5: error: expected ',' or ';' before 'cout',第 6 行是 cout << x << endl;。你應該先做什麼?

(A) 把第 6 行整行重打一次
(B) 去看第 5 行結尾是不是漏了分號
(C) 檢查有沒有忘記 include iostream
(D) 先看最後一個錯誤訊息,那才是真正的原因
REFERENCE · 速查表

P1 速查表

程式骨架

#include <iostream> // 標準函式庫用尖括號 #include <string> #include "greeting.hpp" // 自訂標頭用雙引號 using namespace std; int main() { int n = 0; // 宣告的同時就給值 cout << "請輸入:"; cin >> n; cout << "答案是 " << n * 2 << endl; return 0; // 0 代表正常結束 }

編譯指令

指令做什麼
g++ -std=c++17 -Wall main.cpp -o main四階段一次做完,產出執行檔 main
./main執行
g++ -std=c++17 -I include main.cpp -o main自訂標頭放在 include/ 時加 -I
g++ -E main.cpp | wc -l查看目前環境中前處理後的實際行數
g++ -g main.cpp -o main帶除錯資訊,之後可以用 gdb ./main

型別大小與範圍(64 位元 Linux 實測)

型別sizeof範圍或精度什麼時候用
bool1true / false條件、旗標
char1-128 ~ 127單一字元,單引號 'A'
short2-32,768 ~ 32,767需要較小整數表示範圍的資料
int4−2,147,483,648 ~ 2,147,483,647整數的預設選擇
unsigned int40 ~ 4,294,967,295只會是非負數時;小心它減到負會繞回
long long8±9.22 × 1018會超過二十一億時
float4約 6 位有效數字精度足夠且需節省空間時
double8約 15 位有效數字小數的預設選擇
int *8一個位址此表的環境為 8 bytes;其他實作可能不同,見 P4
string32物件本身的大小可能含物件內短字串儲存;長度用

Python ↔ C++ 對照表

這張表是這一頁最該存下來的東西。左邊是你已經會的,右邊是同一件事在 C++ 怎麼寫。

要做的事PythonC++差在哪
註解# 一行// 一行  /* 多行 */C++ 的 // 不是除法
宣告變數x = 5int x = 5;要寫型別,要分號
常數PI = 3.14const double PI = 3.14;C++ 的 const 是編譯器強制的
印出並換行print(x)cout << x << endl;不會自動加空格
讀一行輸入s = input()getline(cin, s);cin >> s 只讀到第一個空白
定義函式def f(a, b):int f(int a, int b) { }回傳型別與參數型別都要寫
整數除法7 // 27 / 2C++ 的 / 兩邊都是 int 就是整數除法
浮點除法7 / 2static_cast<double>(7) / 2轉型要放在除法之前
取餘數7 % 27 % 2負數時符號不同
次方2 ** 10pow(2, 10)#include <cmath>,而且回傳 double
強制轉整數int(3.99)static_cast<int>(3.99)都是截斷
字串長度len(s)s.length()C++ 的 string#include <string>
取子字串s[2:5]s.substr(2, 3)第二個參數是長度不是結束位置
可變長度的序列a = [1, 2, 3]vector<int> a = {1, 2, 3};P5
加到尾巴a.append(x)a.push_back(x);名字不同,行為一樣
真假值True Falsetrue falseC++ 全小寫;印出來預設是 1 / 0
程式進入點if __name__ == "__main__":int main() { }C++ 一定要有,而且只能有一個

流程控制(條件、邏輯運算、迴圈)的對照見 P2 的速查表

延伸閱讀

資源看什麼
C++ Tutor把本頁的程式貼進去單步執行,看每個變數的值怎麼變。整數除法那一段特別值得跑一次。
Compiler Explorer貼入程式並檢視編譯器產生的組合語言,調整選項可比較結果。
cppreference · 運算子優先順序完整的優先順序表,寫括號猶豫時查這個。
cppreference · Translation phases前處理、編譯與字面值等翻譯階段的正式參考。
P0b · 環境設定還沒把 g++ 裝起來的話先看這頁。
本站第 01 章 · 導論把這一頁的東西實際用起來。
QUIZ · 自我檢測

自我檢測:C++ 基礎與編譯流程 隨堂自測 · 6 題

每個選項都有解說:選錯也點開看看為什麼錯。全對之後再往下翻詞彙卡。

Q1.以下程式印出什麼?
int a = 7, b = 2;
cout << a / b << " " << a % b << endl;
Q2.以下程式印出什麼?
int x = 10;
double y = x / 4;
cout << y << endl;
Q3.以下合法的 C++ 程式印出什麼?
cout << boolalpha << (10 < 5 < 3) << endl;
Q4.以下程式印出什麼?
int i = 5;
cout << i++ << " ";
cout << i << endl;
Q5.假設本次執行環境的 sizeof(int) 為 4,以下程式印出什麼?
int a[5] = {1, 2, 3, 4, 5};
cout << sizeof(int) << " " << sizeof(a) << " " << sizeof(a) / sizeof(int) << endl;
Q6.編譯時出現 undefined reference to addUp(int, int),這個錯誤發生在哪一個階段?該往哪裡找?
CARDS · 關鍵詞彙卡

關鍵詞彙卡:點卡片翻面

先看正面術語,心中默想定義再翻面對答案;洗牌後再過一輪,直到每張都能不假思索說出來。