學習目標

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

  • 了解 C++ 語言的歷史、設計哲學與特色
  • 清楚說明程式的編譯流程(前置處理 → 編譯 → 組譯 → 連結)
  • 安裝並設定 C++ 開發環境(g++ / clang++ / IDE)
  • 熟悉常用的編譯器旗標(-std-Wall-Wextra-g-O2
  • 理解 Debug 與 Release 兩種建置模式的差異
  • 撰寫、編譯並執行你的第一個 C++ 程式
  • 看得懂編譯器錯誤訊息並加以修正
  • 認識 C++ 程式的基本結構、註解與命名空間
  • 建立一套適合自己的 C++ 學習方法

1. 什麼是 C++?

C++ 是一門通用型程式語言,由 Bjarne Stroustrup 於 1979 年在貝爾實驗室開始開發,最初稱為「C with Classes」。C++ 在 C 語言的基礎上加入了物件導向、泛型程式設計、例外處理等特性,同時保留了 C 語言高效能、低階操作的能力。

C++ 的核心設計哲學有幾個著名的原則:

  • 零成本抽象(Zero-overhead abstraction):你不會為了用不到的功能付出效能代價;而你用到的抽象,手寫也不會更快。
  • 信任程式設計師:語言給你完整的控制權(包含直接操作記憶體),但相對地也要你自己負責。
  • 與 C 高度相容:方便既有的 C 程式碼與生態系沿用。
  • 多範式(multi-paradigm):同一個語言可以寫出不同風格的程式。

「C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do, it blows your whole leg off.」這句經常被引用的玩笑話,說明了 C++ 強大但也需要謹慎使用。本課程的目標,就是讓你學會「安全地」使用它的強大。

C++ 的主要特色

特色 說明
高效能 直接編譯成機器碼,執行速度極快
多範式 支援程序式、物件導向、泛型、函數式程式設計
跨平台 可在 Windows、macOS、Linux 等平台編譯執行
豐富的標準函式庫 STL 提供容器、演算法、迭代器等工具
向下相容 C 大部分 C 程式碼可直接在 C++ 中使用
手動記憶體控制 可精確掌握資源生命週期(搭配 RAII 更安全)

C++ 的應用領域

  • 作業系統(Windows、Linux 核心模組)
  • 遊戲引擎(Unreal Engine、Unity 底層)
  • 嵌入式系統與物聯網(IoT)
  • 高頻交易系統(對延遲極度敏感的場景)
  • 瀏覽器引擎(Chrome V8、Firefox)
  • 資料庫(MySQL、MongoDB)
  • 機器學習框架底層(TensorFlow、PyTorch 的 C++ 核心)

2. C++ 標準演進

C++ 持續演進,每隔幾年就會由 ISO 委員會發布新標準:

標準 年份 重要新增特性
C++98 1998 第一個 ISO 標準,STL、模板、例外處理
C++03 2003 小幅修正 C++98 的問題
C++11 2011 autonullptr、lambda、智慧指標、範圍 for
C++14 2014 泛型 lambda、decltype(auto)、二進位字面值
C++17 2017 結構化綁定、std::optionalif constexprstd::filesystem
C++20 2020 Concepts、Ranges、Coroutines、Modules
C++23 2023 std::expectedstd::print、更多 Ranges

本課程以 C++17 為基準,這是目前業界最廣泛支援、工具鏈最成熟的現代標準。學會 C++17 後,再學習 C++20/23 的新特性會非常自然。

「現代 C++(Modern C++)」泛指 C++11 之後的寫法風格:多用 auto、智慧指標、範圍 for、{} 初始化,少用裸指標與手動 new/delete。本課程會一路引導你養成現代寫法的習慣。


3. 編譯流程

C++ 是編譯式語言,原始碼必須先轉成機器碼才能執行。這跟 Python、JavaScript 這類「直譯式 / 即時編譯」語言不同——C++ 在程式執行前就已經把所有東西翻譯成 CPU 看得懂的指令,這也是它執行速度快的原因之一。

完整流程其實有四個階段:

        原始碼 (.cpp)
            │
            ▼
   ┌─────────────────┐
   │ ① 前置處理器     │  Preprocessing
   │   (Preprocessor) │  展開 #include、#define、條件編譯
   └─────────────────┘
            │  產出:純文字(翻譯單元 translation unit)
            ▼
   ┌─────────────────┐
   │ ② 編譯器         │  Compilation
   │   (Compiler)     │  語法/型別檢查 → 組合語言 (.s)
   └─────────────────┘
            │  產出:組合語言 (.s)
            ▼
   ┌─────────────────┐
   │ ③ 組譯器         │  Assembly
   │   (Assembler)    │  組合語言 → 機器碼目的檔 (.o)
   └─────────────────┘
            │  產出:目的檔 (.o / .obj)
            ▼
   ┌─────────────────┐
   │ ④ 連結器         │  Linking
   │   (Linker)       │  合併多個 .o + 函式庫 → 可執行檔
   └─────────────────┘
            │
            ▼
     可執行檔 (a.out / program.exe)

各階段詳細說明

  1. 前置處理(Preprocessing) - 處理所有以 # 開頭的指令。 - #include <iostream> 會被「整份貼上」標頭檔的內容。 - #define MAX 100 會把後面出現的 MAX 全部替換成 100。 - 條件編譯(#ifdef#ifndef)也在此處理。 - 想親眼看看前置處理的結果,可以執行 g++ -E hello_world.cpp(會印出展開後的純文字,通常非常長)。

  2. 編譯(Compilation) - 檢查語法是否正確、型別是否相容。 - 大部分的錯誤訊息(syntax error、type error)都來自這個階段。 - 將每個翻譯單元轉成組合語言。用 g++ -S hello_world.cpp 可以產生 .s 檔看組合語言。

  3. 組譯(Assembly) - 把組合語言轉成 CPU 真正執行的機器碼,存成目的檔(object file)。 - 用 g++ -c hello_world.cpp 可以只編譯到 .o 而不連結。

  4. 連結(Linking) - 把多個 .o 檔以及用到的函式庫(例如標準函式庫)合併成一個可執行檔。 - 「找不到符號」這類錯誤(undefined reference to ...)就是連結階段的問題,例如你呼叫了一個只宣告卻沒有定義的函式。

一句指令做完全部

平常我們直接用一行指令讓 g++ 把四個階段一次跑完:

g++ -std=c++17 -Wall -o hello_world hello_world.cpp

但理解中間有四個階段非常重要——當你看到錯誤訊息時,能立刻判斷它是編譯期錯誤(語法/型別)還是連結期錯誤(找不到定義),就能更快定位問題。


4. 安裝開發環境

macOS

# 安裝 Xcode Command Line Tools(包含 clang++)
xcode-select --install

# 驗證安裝
clang++ --version

# 或安裝 g++(透過 Homebrew)
brew install gcc
g++ --version

macOS 上的 g++ 預設其實是 clang 的別名。若要用「真正的 GCC」,Homebrew 安裝後通常是 g++-14 之類的名稱。對初學者而言,clang++ 與 g++ 行為幾乎一致,任選其一即可。

Linux(Ubuntu / Debian)

sudo apt update
sudo apt install g++ build-essential
g++ --version

Windows

  • 安裝 MSYS2 並透過 pacman -S mingw-w64-ucrt-x86_64-gcc 安裝 g++
  • 或安裝 Visual Studio 社群版(含 MSVC 編譯器)
  • 也可使用 WSL(Windows Subsystem for Linux),在 Windows 中執行完整的 Linux 環境

IDE / 編輯器設定

你不一定需要笨重的 IDE,下面由輕到重列出常見選擇:

工具 特性 適合對象
VS Code 輕量、跨平台、擴充豐富(C/C++ extension) 大多數初學者的首選
CLion JetBrains 出品,功能完整(需付費 / 學生免費) 想要強大重構與除錯
Visual Studio Windows 上功能最完整 Windows 開發者
Vim / Neovim 終端機內,搭配 LSP 喜歡鍵盤操作的人
純文字 + 終端機 最能理解「編譯到底做了什麼」 學習階段強烈建議先試試

建議:剛學 C++ 時,先用「純文字編輯器 + 終端機手動 g++ 編譯」幾天,徹底理解編譯流程後,再轉用 IDE 享受自動化的便利。這樣你才不會在 IDE 出問題時手足無措。

VS Code 的最小設定:安裝官方「C/C++」擴充套件後,按 F5 即可編譯與除錯。背後其實還是呼叫 g++/clang++,只是 IDE 幫你產生了指令。


5. 編譯器旗標(Compiler Flags)

旗標(flag)是給編譯器的選項,控制它的行為。以下是初學階段最該認識的幾個:

旗標 用途 說明
-std=c++17 指定語言標準 確保使用 C++17 的語法與函式庫
-Wall 開啟常見警告 「all」其實只是「大部分常見的」警告
-Wextra 開啟額外警告 -Wall 更嚴格,建議搭配使用
-Werror 把警告當成錯誤 強迫你處理每一個警告(培養好習慣)
-g 加入除錯資訊 讓 gdb/lldb 能對應到原始碼行號
-O0 不最佳化(預設) 編譯快、好除錯
-O2 較強最佳化 Release 常用,執行更快
-o 檔名 指定輸出檔名 不指定時預設為 a.out

學習階段強烈建議養成這個習慣:

g++ -std=c++17 -Wall -Wextra -g -o program program.cpp

為什麼要開警告? 警告(warning)代表「程式可以編譯,但編譯器覺得這裡很可疑」。很多難以察覺的 bug(例如忘記回傳值、比較 signed/unsigned、變數未使用)都會以警告形式提前被抓出來。把警告當成免費的程式碼審查!

Debug 建置 vs Release 建置

實務上,同一份程式碼通常有兩種建置模式:

模式 典型旗標 特性 用途
Debug -O0 -g 不最佳化、含除錯資訊、編譯快 開發、除錯階段
Release -O2(或 -O3-DNDEBUG 最佳化、執行快、移除 assert 正式發佈給使用者
# Debug:方便用除錯器逐行追蹤
g++ -std=c++17 -Wall -Wextra -g -O0 -o app_debug main.cpp

# Release:給最終使用者,速度最快
g++ -std=c++17 -O2 -DNDEBUG -o app_release main.cpp

-DNDEBUG 會定義巨集 NDEBUG,使標準函式庫的 assert() 失效。Release 版執行更快,但因為做了最佳化,逐行除錯時行號可能會「跳來跳去」,所以除錯請用 Debug 版。


6. 第一個程式:Hello World

// hello_world.cpp
#include <iostream>    // 引入輸入輸出函式庫

int main() {           // 程式進入點
    std::cout << "Hello, World!" << std::endl;
    return 0;          // 回傳 0 表示程式正常結束
}

編譯與執行

# 編譯
g++ -std=c++17 -Wall -o hello_world hello_world.cpp

# 執行(macOS / Linux)
./hello_world

# 執行(Windows)
hello_world.exe

預期輸出:

Hello, World!

7. 程式基本結構

每個 C++ 程式都包含以下基本元素:

#include <iostream>       // (1) 前置處理指令:引入標頭檔

int main() {              // (2) main 函式:程式進入點
    // (3) 程式敘述
    std::cout << "Hello!" << std::endl;

    return 0;             // (4) 回傳值:0 表示成功
}

逐行解析

元素 說明
#include <iostream> 引入標準輸入輸出函式庫,提供 std::coutstd::cin
int main() 程式的進入點,作業系統從這裡開始執行
{} 大括號定義程式碼區塊(scope)
std::cout 標準輸出物件,std 是命名空間,:: 是作用域運算子
<< 輸出運算子,將資料送到輸出串流
std::endl 換行並清空輸出緩衝區
return 0; 回傳整數 0 給作業系統,表示程式正常結束
; 每個敘述結尾必須加分號

關於 main 的回傳值

main() 的回傳值會交給作業系統,慣例上:

  • return 0; → 程式成功(也可用 return EXIT_SUCCESS;
  • 非零值 → 發生錯誤(例如 return 1;return EXIT_FAILURE;

在終端機可以用以下方式查看上一個程式的回傳值:

./hello_world
echo $?     # macOS / Linux,印出剛剛程式的回傳值

小知識:main() 是唯一可以「不寫 return 也合法」的函式。若沒寫,編譯器會自動補上 return 0;。不過明確寫出來是好習慣。


8. 註解(Comments)

註解是給「人」看的,編譯器會完全忽略。寫好註解能讓程式更易維護。

// 這是單行註解,從 // 到行尾都會被忽略

/*
   這是多行註解
   可以跨越很多行
   常用於檔案開頭說明
*/

int x = 5;  // 也可以放在程式碼後面

/* 也可以 */ int y = 10; /* 夾在中間(不建議) */

注意:多行註解 /* */ 不能巢狀

/* 外層 /* 內層 */ 這裡會出錯 */

好的註解 vs 壞的註解

// ❌ 壞:只是把程式碼翻譯一遍,沒有提供新資訊
i = i + 1;  // 把 i 加 1

// ✅ 好:解釋「為什麼」,而非「做什麼」
i = i + 1;  // 跳過開頭的標頭位元組

C++ Core Guidelines 建議(NL.1、NL.2):註解應該說明意圖(intent)程式碼無法表達的事情,而不是重複程式碼本身。最好的「註解」其實是清楚的命名與結構。


9. 命名空間(Namespaces)入門

命名空間用來避免名稱衝突。想像兩個函式庫都有一個叫 print 的函式,命名空間就像「姓氏」,讓編譯器分得清誰是誰。

標準函式庫的所有東西都放在 std 命名空間裡,所以我們要寫 std::coutstd::string。其中 :: 稱為作用域解析運算子(scope resolution operator)

#include <iostream>

namespace game {
    int score = 0;
    void reset() { score = 0; }
}

int main() {
    game::score = 100;        // 用 命名空間::名稱 存取
    std::cout << game::score; // 100
    return 0;
}

using 的取捨

你可能在很多教學看到 using namespace std;,它讓你可以省略 std::

#include <iostream>
using namespace std;   // 之後可直接寫 cout、endl

int main() {
    cout << "Hello" << endl;
    return 0;
}
寫法 優點 缺點
std::cout(加前綴) 清楚、不會衝突 字較多
using namespace std; 簡短 可能造成命名衝突,不建議用在標頭檔或大型專案
using std::cout;(只引入特定名稱) 折衷,較安全 需逐一列出

C++ Core Guidelines(SF.6、SF.7):不要在標頭檔的全域範圍寫 using namespace。本課程一律使用 std:: 前綴,讓你從一開始就養成清楚的好習慣。


10. 輸入與輸出(iostream 深入)

<iostream> 提供幾個重要的全域串流物件:

物件 用途
std::cout 標準輸出(console output)
std::cin 標準輸入(console input)
std::cerr 標準錯誤輸出(不緩衝,立即顯示)
std::clog 標準錯誤輸出(有緩衝,適合記錄日誌)

輸出:std::cout<<

std::cout << "整數:" << 42 << ",浮點數:" << 3.14 << '\n';

<< 可以串接,由左到右依序把資料送進串流。它能自動辨識型別(整數、浮點數、字串、字元都可以直接輸出)。

換行:std::endl vs '\n'

std::cout << "A" << std::endl;  // 換行 + 清空緩衝區(flush)
std::cout << "B" << '\n';       // 只換行,不清空(效能較好)

大量輸出時,頻繁使用 std::endl 會因為一直清空緩衝區而變慢。一般情況用 '\n' 即可,只有在需要「立刻看到輸出」時(例如除錯)才用 std::endl

輸入:std::cin>>

#include <iostream>

int main() {
    int age;
    std::cout << "請輸入你的年齡:";
    std::cin >> age;            // 從鍵盤讀取一個整數存入 age
    std::cout << "明年你 " << age + 1 << " 歲\n";
    return 0;
}

>> 稱為擷取運算子(extraction operator),會跳過開頭的空白,讀取一個值。可以連續讀多個:

int a, b;
std::cin >> a >> b;            // 輸入「3 5」會讓 a=3, b=5

進階提醒:std::cin >> x 在輸入格式錯誤時(例如要 int 卻輸入字母)會進入錯誤狀態。讀取一整行文字則建議用 std::getline(std::cin, str)。這些細節會在後面章節深入。


11. 如何看懂編譯器錯誤訊息

初學者最大的挫折來源,就是看不懂編譯器「噴」出來的一大串英文。其實有訣竅。

以這段有錯的程式為例:

#include <iostream>

int main() {
    std::cout << "Hello"      // 忘記分號
    return 0;
}

g++ 可能輸出:

main.cpp:5:5: error: expected ';' before 'return'
    5 |     return 0;
      |     ^~~~~~

拆解這個訊息:

  • main.cpp:5:5檔名 : 行號 : 欄號,問題在第 5 行第 5 欄。
  • error: → 這是錯誤(會中止編譯);若是 warning: 則只是提醒。
  • expected ';' before 'return' → 在 return 之前少了分號。
  • 下面的 ^~~~~~ 用箭頭指出可疑的位置。

看錯誤訊息的三個原則

  1. 永遠從「第一個」錯誤開始修。一個小錯誤常會引發後面一連串連鎖錯誤,修好第一個,後面可能就消失了。
  2. 看清楚行號,但真正的錯誤有時在「前一行」(例如漏掉分號,編譯器是到下一行才發現不對勁)。
  3. 複製錯誤訊息去搜尋undefined referenceexpected ';'was not declared in this scope 都是常見且好查的訊息。

常見錯誤訊息對照表

錯誤訊息片段 通常代表
expected ';' before ... 漏了分號
'cout' was not declared in this scope 忘記 #include <iostream> 或漏了 std::
undefined reference to 'foo' 函式有宣告但沒有定義(連結階段)
expected '}' at end of input 大括號沒有成對
invalid conversion from ... 型別不相容

重點整理

  1. C++ 是一門高效能、多範式的編譯式語言,設計哲學強調「零成本抽象」。
  2. 程式碼經過前置處理 → 編譯 → 組譯 → 連結四個階段產生可執行檔。
  3. 學會分辨編譯期錯誤(語法/型別)與連結期錯誤(找不到定義)。
  4. 養成使用 -std=c++17 -Wall -Wextra 的習慣,把警告當作免費的程式碼審查。
  5. Debug 建置(-g -O0)用於開發,Release 建置(-O2 -DNDEBUG)用於發佈。
  6. 每個程式都需要 main() 函式作為進入點,回傳 0 表示成功。
  7. 註解說明「為什麼」,而非重複「做什麼」。
  8. 命名空間避免名稱衝突;本課程一律使用 std:: 前綴。
  9. std::cout/<< 輸出、std::cin/>> 輸入;'\n' 通常優於 std::endl
  10. 看編譯錯誤要從第一個開始修,注意檔名、行號與箭頭。

常見錯誤與陷阱

錯誤 說明
忘記加 ; 每個敘述結尾都要加分號,否則編譯錯誤
main 拼錯 函式名稱必須是 main,大小寫敏感
忘記 #include 使用 cout 前必須引入 <iostream>
混淆 <<>> << 用於輸出,>> 用於輸入
在標頭檔用 using namespace std; 大型專案中可能造成命名衝突,建議使用 std:: 前綴
大括號不成對 {} 必須一一對應,建議寫完 { 立刻補上 }
編譯沒開警告 不加 -Wall 會錯過很多潛在 bug
把 Release 版拿來除錯 最佳化後行號會錯亂,除錯請用 -g -O0
中文全形符號 程式碼中誤打全形分號 或括號會編譯錯誤

練習題

練習 1(基礎):客製化 Hello World

修改 hello_world.cpp,讓它輸出你的名字、今天的日期,以及一句你最喜歡的話(每項各一行)。 - 提示:用多個 std::cout,或用一個 std::cout 串接多個 <<'\n'

練習 2(基礎):多行格式化輸出

參考 basic_output.cpp,寫一個程式輸出以下內容(使用跳脫字元 \n\t\"):

C++ 程式設計
================
作者:(你的名字)
日期:2026/06/15
"學習 C++ 是值得的!"
  • 提示:\" 可在字串中輸出雙引號;================ 直接打即可。

練習 3(基礎):編譯錯誤排除

以下程式碼有數個錯誤,請找出並修正,使其能用 g++ -std=c++17 -Wall 通過編譯:

#include <iostream>

int Main() {
    std:cout << "Hello" << endl
    return 0;
}
  • 提示:檢查函式名稱大小寫、std:: 的冒號數量、endl 前綴、行尾分號。

練習 4(中級):互動式問候

寫一個程式,先用 std::cin 讀取使用者的名字(字串)與年齡(整數),再輸出:

你好,<名字>!明年你就 <年齡+1> 歲了。
  • 提示:宣告 std::string name;int age;(需 #include <string>),用 std::cin >> name >> age; 讀取。

練習 5(中級):觀察編譯的四個階段

針對你的 hello_world.cpp,依序執行下列指令並觀察各自的產物,寫下你的觀察: 1. g++ -E hello_world.cpp -o hello.i(前置處理後的純文字) 2. g++ -S hello_world.cpp(產生組合語言 hello_world.s) 3. g++ -c hello_world.cpp(產生目的檔 hello_world.o) 4. g++ hello_world.o -o hello_world(連結成可執行檔) - 提示:用編輯器打開 hello.ihello_world.s 看看內容差異有多大。

練習 6(挑戰):比較 Debug 與 Release

用兩種模式各編譯一次同一支程式,並比較產生的可執行檔大小:

g++ -std=c++17 -g -O0 -o app_debug hello_world.cpp
g++ -std=c++17 -O2 -DNDEBUG -o app_release hello_world.cpp
ls -l app_debug app_release
  • 思考題:為什麼兩個檔案大小不同?哪一個適合拿去用除錯器逐行追蹤?
  • 提示:Debug 版包含 -g 的除錯資訊,通常較大。

對應程式碼檔案