學習目標
- 理解命名空間(namespace)的用途,知道它如何避免名稱衝突(name collision)
- 掌握命名空間的定義、巢狀命名空間(含 C++17
A::B::C語法)與命名空間別名 - 區分
using指令(directive)與using宣告(declaration),並理解為何不可在標頭檔使用using namespace - 認識匿名(unnamed)命名空間與內部連結(internal linkage)
- 初步認識 inline 命名空間與引數依賴查找(ADL)
- 區分宣告(declaration)與定義(definition)
- 了解標頭檔(
.h/.hpp)與實作檔(.cpp)的分離設計 - 掌握 include guard(
#ifndef/#define/#endif)與#pragma once - 理解前向宣告(forward declaration)如何降低編譯相依
- 理解編譯單元(translation unit)、單一定義規則(ODR)與連結(linkage)
- 完整掌握建置流程:前處理 → 編譯 → 組譯 → 連結
- 讀懂並撰寫 Makefile(變數、規則、目標、相依、
.PHONY、自動變數$@ $^ $<) - 認識 CMake 作為現代建置工具的替代方案
1. 命名空間(namespace)
1.1 為什麼需要命名空間?WHY
想像一個沒有命名空間的世界。你的專案用了兩個第三方函式庫:一個繪圖庫提供 Color、draw(),一個遊戲引擎也提供 Color、draw()。當你同時 #include 兩者,連結器(甚至編譯器)會抱怨「重複定義」(multiple definition / redefinition)——這就是名稱衝突(name collision)。
在 C 語言時代,大家只能靠「加前綴」來閃避,例如 gfx_Color、game_Color、SDL_Init、gl_DrawArrays。這讓名稱又臭又長,而且前綴本身也可能撞車。
命名空間是 C++ 對這個問題的正式解法:它建立一個具名的作用域(named scope),把相關的型別、函式、變數歸類在一起。不同命名空間裡可以存在「同名但不同實體」的東西,彼此井水不犯河水。
namespace graphics { struct Color { int r, g, b; }; }
namespace game { struct Color { float h, s, v; }; }
graphics::Color c1{255, 0, 0}; // 兩個 Color 完全不衝突
game::Color c2{0.0f, 1.0f, 1.0f};
核心觀念:命名空間解決的是「名稱管理」問題,不是「存取控制」問題(那是 class 的
private/public)。命名空間裡的東西預設都是公開可見的,只要寫出完整限定名稱就能用。
1.2 定義命名空間
namespace Math {
constexpr double PI = 3.14159265358979; // 常數
double add(double a, double b) { // 函式
return a + b;
}
struct Vector2 { double x, y; }; // 型別
}
// 使用時加上「限定符」(qualifier) Math::
double result = Math::add(3.0, 4.0);
Math::Vector2 v{1.0, 2.0};
double pi = Math::PI;
命名空間有一個重要特性:可以重新打開、分次擴充。下面兩段程式碼最終讓 Math 同時擁有 add 與 sub:
namespace Math { double add(double a, double b) { return a + b; } }
// ... 中間可以有其他程式碼,甚至跨檔案 ...
namespace Math { double sub(double a, double b) { return a - b; } }
這個特性正是為什麼標準函式庫可以把 std 拆散到幾十個標頭檔(<vector>、<string>、<iostream> ...),每個檔案都往 std 裡「補充」一些內容。
1.3 巢狀命名空間(Nested Namespaces)
當模組層次很深時,可以巢狀命名空間:
傳統寫法(C++17 之前):
namespace Company {
namespace Graphics {
namespace Rendering {
void draw() { /* ... */ }
}
}
}
C++17 簡化語法:
namespace Company::Graphics::Rendering {
void draw() { /* ... */ }
}
// 兩種寫法等價,使用方式完全相同:
Company::Graphics::Rendering::draw();
C++20 更進一步允許
namespace A::B::inline C { ... }在巢狀語法中標記 inline 命名空間,本章不深入。
1.4 using 指令 vs using 宣告
這兩者很容易搞混,差別在「引入的範圍」:
| 寫法 | 名稱 | 引入內容 | 風險 |
|---|---|---|---|
using namespace std; |
using 指令 (directive) | 引入整個命名空間的所有名稱 | 高:容易造成名稱衝突、模糊呼叫 |
using std::cout; |
using 宣告 (declaration) | 只引入單一名稱 | 低:精準、可控 |
// using 指令 — 把整個 std 倒進當前作用域(不推薦)
using namespace std;
cout << "Hello" << endl;
// using 宣告 — 只引入需要的名稱(推薦)
using std::cout;
using std::endl;
cout << "Hello" << endl;
為什麼不要在標頭檔用 using namespace std;?
標頭檔會被許多 .cpp #include。一旦標頭檔裡寫了 using namespace std;,所有引入它的檔案都會被「污染」——即使那些檔案根本不想要。這會引爆難以追查的衝突:
// my_lib.h(錯誤示範)
#include <algorithm>
using namespace std; // ❌ 災難的開始
// user.cpp
#include "my_lib.h"
int count = 0; // 與 std::count 演算法產生模糊
// distance, swap, find ... 全都可能與你的名稱撞車
最佳實踐(C++ Core Guidelines SF.7): Don't write
using namespaceat global scope in a header file. 在標頭檔的全域範圍絕對不要寫using namespace。在.cpp中也優先使用 using 宣告,或把using namespace限制在函式或區塊內。
1.5 匿名(unnamed)命名空間與內部連結
匿名命名空間沒有名字,裡面的內容只在當前編譯單元(這個 .cpp)可見,擁有內部連結(internal linkage)——其他檔案看不到、連結器也不會把它們拿去和別人比對。
namespace {
int g_internalCounter = 0; // 只在本 .cpp 可見
void helper() { /* ... */ } // 等同於 static 函式
}
這是現代 C++ 取代「檔案層級 static」的推薦做法。為什麼比 static 好?因為匿名命名空間裡可以放型別(static 不能修飾型別),而且語意一致。
// 舊寫法(C 風格)
static int counter = 0;
static void helper() {}
// 現代寫法(更通用)
namespace {
int counter = 0;
void helper() {}
struct LocalOnly {}; // 型別也能限制在本檔案
}
1.6 命名空間別名(Namespace Alias)
命名空間名稱太長時,建立短別名:
namespace fs = std::filesystem;
namespace chrono = std::chrono;
namespace CEP = Company::Engine::Physics;
fs::path p = fs::current_path();
auto now = chrono::system_clock::now();
別名只在當前作用域有效,不會改變原命名空間。
1.7 inline 命名空間(簡介)
inline 命名空間裡的名稱會「自動提升」到外層命名空間,常用於函式庫版本管理:
namespace lib {
inline namespace v2 { // v2 是預設版本
void api() { /* 新版 */ }
}
namespace v1 {
void api() { /* 舊版 */ }
}
}
lib::api(); // 實際呼叫 lib::v2::api()(因為 v2 是 inline)
lib::v1::api(); // 仍可明確指定舊版
初學階段了解概念即可,知道它存在、用於 ABI/版本演進就好。
1.8 引數依賴查找(ADL,簡介)
ADL(Argument-Dependent Lookup,又稱 Koenig lookup)是指:呼叫函式時,編譯器除了當前作用域,還會去「引數型別所在的命名空間」尋找函式。
#include <iostream>
int main() {
std::cout << "hi\n"; // operator<< 定義在 std,卻不必寫 std::operator<<
}
上面 operator<< 之所以能找到,正是因為 std::cout 屬於 std,ADL 自動把 std 納入查找範圍。ADL 讓運算子多載與泛型程式碼用起來自然,但偶爾也會造成「找到非預期函式」的驚喜。本章只需建立直覺:函式查找不只看名稱,也看引數型別的命名空間。
2. 多檔案專案
2.1 宣告 vs 定義(先弄清這組詞)
這是理解多檔案的基石:
- 宣告(declaration):告訴編譯器「有這個東西,它長這樣」。不配置實體。
- 定義(definition):真正產生實體(函式的程式碼、變數的儲存空間)。
int add(int, int); // 宣告:只有原型(prototype),沒有實作
extern int g_count; // 宣告:g_count 存在於別處
class Widget; // 宣告(前向宣告):Widget 是個 class
int add(int a, int b) { // 定義:有函式本體
return a + b;
}
int g_count = 0; // 定義:配置儲存空間並初始化
口訣:宣告可以很多次,定義只能一次(這正是 ODR,見 §2.8)。標頭檔放「宣告」,實作檔放「定義」。
2.2 為什麼要分離標頭檔與實作檔?WHY
| 優點 | 說明 |
|---|---|
| 介面與實作分離 | 使用者只需讀 .h 就知道怎麼用,不必看實作細節 |
| 加速增量編譯 | 改 .cpp 只重編該檔;介面不變則依賴它的檔案不用重編 |
| 隱藏實作 | 可只發佈 .h + 編好的 .o/.a/.so,不公開原始碼 |
| 避免重複定義 | 函式定義集中在一個 .cpp,符合 ODR |
| 團隊協作 | 不同人開發不同模組,透過標頭檔約定介面 |
2.3 標頭檔(Header Files)
標頭檔(.h 或 .hpp)通常放宣告與「可以重複出現」的東西:
- 函式宣告(原型)
- 類別 / 結構定義
- 樣板(template)定義
inline函式constexpr/inline常數- 型別別名(
using/typedef) - 列舉(enum)
.h 與 .hpp 沒有語意差別,只是慣例:.hpp 常暗示「這是 C++(非純 C)標頭」。同一專案請保持一致。
2.4 Include Guard(標頭防護)
標頭檔常被間接 #include 多次(A 引入 B,B 又引入 C,C 又被 A 直接引入…)。若沒有防護,同一份內容被展開兩次,類別會「重複定義」而編譯失敗。
方法一:#ifndef / #define / #endif(傳統、最可攜)
#ifndef MATH_UTILS_H // 如果還沒定義這個巨集
#define MATH_UTILS_H // 就定義它
class MathUtils { /* ... */ };
#endif // MATH_UTILS_H // 第二次引入時,巨集已存在,整段被跳過
巨集名稱要夠獨特(建議含專案名),否則兩個檔案用同名 guard 會互相遮蔽。
方法二:#pragma once(現代、簡潔)
#pragma once
class MathUtils { /* ... */ };
| 比較 | #ifndef guard |
#pragma once |
|---|---|---|
| 標準保證 | ✅ 標準 C++ | ⚠️ 非標準,但幾乎所有主流編譯器支援 |
| 取名負擔 | 需手動取唯一巨集名 | 不需取名 |
| 同檔多路徑/硬連結邊界情形 | 較穩健 | 少數檔案系統可能誤判 |
| 簡潔度 | 三行 | 一行 |
實務上 #pragma once 已是主流;要求極致可攜性的函式庫仍會用 #ifndef。兩者擇一即可,不要兩個都用。
2.5 實作檔(Implementation Files)
.cpp 放函式 / 方法的定義:
// math_utils.cpp
#include "math_utils.h" // 先引入自己的標頭,確保宣告與定義一致
double MathUtils::add(double a, double b) {
return a + b;
}
慣例:每個
.cpp第一個#include就是它自己的標頭,這能及早抓出「標頭缺少某個#include才能獨立編譯」的問題(自給自足,self-contained header)。
2.6 前向宣告(Forward Declaration)
當你只需要知道某型別存在(用指標或參考),而不需要它的完整定義時,可用前向宣告取代 #include,藉此切斷編譯相依、加速編譯:
// game.h
class Player; // 前向宣告:不必 #include "player.h"
class Game {
Player* player; // ✅ 指標只需知道型別存在(大小固定 = 指標大小)
Player& ref; // ✅ 參考同理
// Player obj; // ❌ 建立物件需要完整定義(編譯器要知道大小與成員)
void update();
};
什麼時候必須有完整定義(即必須 #include)?
- 建立物件(需要知道大小)
- 存取成員 / 呼叫方法
- 繼承
- 用作
sizeof/ 值傳遞 / 值回傳
前向宣告也是解決循環相依(circular dependency)的關鍵工具:當 A 與 B 互相引用時,至少其中一方改用前向宣告 + 指標即可打破環。
2.7 編譯單元(Translation Unit)
一個 .cpp 經前處理器展開所有 #include、處理所有 #define/#ifdef 後,形成的完整原始碼就是一個編譯單元(translation unit, TU)。編譯器一次處理一個 TU,產出一個目的檔(.o / .obj)。
關鍵推論:每個 TU 是獨立編譯的,它看不到別的 .cpp 內容,只能透過標頭檔得知別處的宣告。這就是為什麼「定義在 A.cpp、宣告在標頭、B.cpp 引入標頭就能呼叫」這套機制能運作——編譯時各自留下「未解符號」,等連結時再接起來。
2.8 單一定義規則(ODR, One Definition Rule)
ODR 是 C++ 最重要的規則之一:
- 在單一 TU 內,任何變數、函式、類別、樣板,定義至多一次。
- 在整個程式中,非
inline的函式與(具外部連結的)變數,定義恰好一次。 - 類別、
inline函式/變數、樣板,可在多個 TU 各定義一次,但每份定義必須逐字相同(token-for-token、語意一致)。
違反 ODR 的典型後果:
- 在標頭定義非 inline 函式 → 多個
.cpp引入 → 連結期 multiple definition 錯誤。 - 完全沒定義就使用 → 連結期 undefined reference 錯誤。
// utils.h(錯誤示範)
int square(int x) { return x * x; } // ❌ 非 inline 定義放標頭
// 兩個 .cpp 引入 → 重複定義
// 修正方式 1:加 inline
inline int square(int x) { return x * x; } // ✅ 允許多 TU 各一份
// 修正方式 2:標頭只宣告,定義移到 utils.cpp
int square(int x); // ✅ utils.h
2.9 連結(Linkage):internal vs external
連結性決定「一個名稱能否被其他 TU 看見」:
| 連結性 | 意義 | 如何產生 |
|---|---|---|
| external linkage | 跨 TU 可見,連結器看得到 | 一般全域函式、全域變數、class 成員 |
| internal linkage | 只在本 TU 可見 | static 全域、匿名命名空間、const/constexpr 全域(預設) |
| no linkage | 完全不對外 | 區域變數、函式參數 |
int g_external = 1; // external:別的 .cpp 可用 extern 取用
static int g_internal = 2; // internal:只有本檔案看得到
namespace { int g_also_internal = 3; } // internal(推薦寫法)
void f() { int local = 4; } // no linkage
要在 B.cpp 使用 A.cpp 的 g_external,需在標頭(或 B.cpp)寫 extern int g_external; 宣告。
3. 建置流程(Build Process)
從原始碼到可執行檔,C++ 工具鏈分四個階段:
前處理 編譯 組譯 連結
math_utils.cpp ──▶ [展開 #include] ──▶ [產生組語] ──▶ math_utils.o ─┐
+ │
math_utils.h ├─▶ [Linker] ──▶ program
│ (解析符號、
main.cpp ───────▶ [展開 #include] ──▶ [產生組語] ──▶ main.o ────────┘ 合併目的檔)
+
math_utils.h
階段說明:
1. 前處理 (Preprocess, g++ -E):處理 #include / #define / #ifdef,產生純文字 TU
2. 編譯 (Compile, g++ -S):語法/語意分析、最佳化,產生組合語言
3. 組譯 (Assemble, g++ -c):組語 → 機器碼目的檔 .o(含未解析的外部符號)
4. 連結 (Link, g++ ):把所有 .o 與函式庫接起來,解析符號 → 可執行檔
對照指令:
g++ -E main.cpp -o main.i # 只做前處理(看展開後的樣子)
g++ -S main.cpp -o main.s # 編譯到組語
g++ -c main.cpp -o main.o # 編譯+組譯,產生目的檔(不連結)
g++ main.o math_utils.o -o program # 連結成執行檔
一次到位(最常用):
g++ -std=c++17 -Wall main.cpp math_utils.cpp -o program
理解這四階段,就能看懂錯誤訊息屬於哪一層:「undefined reference」是連結錯誤(少了某個
.o或函式庫);「expected ';'」是編譯錯誤;「No such filexxx.h」是前處理錯誤。
4. Makefile 詳解
當檔案變多,手動敲 g++ 既冗長又容易漏編。make 讀取 Makefile,依「相依關係」只重建有變動的部分(增量建置)。
4.1 規則的基本結構
目標(target): 相依項(prerequisites)
<TAB>指令(recipe)
- 目標:要產生的檔案(或抽象動作如
clean)。 - 相依項:產生目標所需的輸入;任一相依比目標新,就重跑指令。
- 指令:必須以 Tab 字元開頭(不是空格!這是最常見的新手陷阱)。
4.2 變數
CXX = g++ # 編譯器
CXXFLAGS = -std=c++17 -Wall -Wextra -g # 編譯旗標
TARGET = program # 產物名稱
SRCS = main.cpp math_utils.cpp # 原始碼清單
OBJS = $(SRCS:.cpp=.o) # 字串替換:main.o math_utils.o
$(SRCS:.cpp=.o) 是「替換參照」,把清單中每個 .cpp 後綴換成 .o。
4.3 自動變數(Automatic Variables)
在指令中,make 提供方便的縮寫:
| 變數 | 意義 | 範例(規則 foo.o: foo.cpp bar.h) |
|---|---|---|
$@ |
目標名稱 | foo.o |
$^ |
所有相依項(去重) | foo.cpp bar.h |
$< |
第一個相依項 | foo.cpp |
$? |
比目標新的相依項 | 有變動的那些 |
4.4 樣式規則(Pattern Rule)與 .PHONY
% 是萬用字元,%.o: %.cpp 表示「任何 .o 都可由同名 .cpp 編出」。
.PHONY 宣告「假目標」——這些目標不對應真實檔案(如 clean、all、run)。若不標記,且剛好存在一個叫 clean 的檔案,make clean 會誤判「clean 已是最新,不必執行」。
4.5 完整範例(含逐行註解)
# === 變數設定 ===
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -g
TARGET = program
SRCS = main.cpp math_utils.cpp
OBJS = $(SRCS:.cpp=.o)
# === 預設目標(第一個目標即預設)===
all: $(TARGET)
# === 連結:所有 .o → 執行檔 ===
# $@ = program ; $^ = 所有 .o
$(TARGET): $(OBJS)
$(CXX) $(CXXFLAGS) -o $@ $^
# === 樣式規則:每個 .cpp → .o ===
# $< = 對應的 .cpp ; $@ = 對應的 .o ; -c 表示只編譯不連結
%.o: %.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@
# === 標頭相依:math_utils.h 改了,相關 .o 要重編 ===
main.o: main.cpp math_utils.h
math_utils.o: math_utils.cpp math_utils.h
# === 假目標 ===
.PHONY: all clean run
run: $(TARGET)
./$(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
使用:
make # 建置(只重編有變動者)
make run # 建置後執行
make clean # 清除產物
4.6 為什麼要列「標頭相依」?
make 預設只看「目標 vs 相依項」的時間戳。若你只寫 %.o: %.cpp,那麼改了 math_utils.h 時,make 不知道 .o 也該重編,會用到過時的舊物件碼。所以要額外補上 main.o: main.cpp math_utils.h。大型專案會用 g++ -MMD 自動產生這些相依,免於手寫。
4.7 現代替代方案:CMake(簡介)
手寫 Makefile 在跨平台、大型專案時會變得難以維護。CMake 是目前 C++ 界的事實標準:你寫平台無關的 CMakeLists.txt,CMake 再產生對應平台的建置檔(Unix Makefile、Ninja、Visual Studio 專案…)。
cmake_minimum_required(VERSION 3.15)
project(MathDemo CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(program main.cpp math_utils.cpp)
建置流程:
cmake -S . -B build # 設定(產生 build/ 內的建置檔)
cmake --build build # 真正編譯
本章仍以 Makefile 為主以理解底層原理;實務專案推薦 CMake。
重點整理
| 概念 | 說明 |
|---|---|
namespace |
建立具名作用域,避免名稱衝突 |
namespace A::B::C |
C++17 巢狀命名空間簡化語法 |
| 命名空間可重新打開 | 同一命名空間可分次、跨檔案擴充 |
using namespace X |
using 指令,引入整個命名空間(勿用於標頭) |
using X::name |
using 宣告,只引入單一名稱(推薦) |
| 匿名命名空間 | 內部連結,取代檔案層級 static,可放型別 |
| 命名空間別名 | namespace fs = std::filesystem; |
| inline 命名空間 | 名稱提升到外層,用於版本管理 |
| ADL | 函式查找會納入引數型別所在的命名空間 |
| 宣告 vs 定義 | 宣告可多次,定義僅一次 |
| Include Guard | #ifndef/#define/#endif 防止重複引入 |
#pragma once |
現代簡潔的 include guard 替代方案 |
| 前向宣告 | 用指標/參考時免 #include,降低編譯相依、解循環引用 |
| 編譯單元(TU) | 一個 .cpp 展開後的完整原始碼,獨立編譯 |
| ODR | 每個實體全程式僅一個定義(inline/樣板例外) |
| 連結性 | external(跨 TU)/ internal(本 TU)/ no linkage |
| 建置四階段 | 前處理 → 編譯 → 組譯 → 連結 |
| Makefile | 自動化增量建置;$@ $^ $<、.PHONY、樣式規則 |
| CMake | 跨平台的現代建置工具 |
常見錯誤與陷阱
- 在標頭檔的全域範圍寫
using namespace std;:污染所有引入者的命名空間,引爆難查的衝突。違反 Core Guidelines SF.7。 - 在標頭檔定義非
inline函式或全域變數:多個.cpp引入後造成 ODR 違反、連結期 "multiple definition"。改用inline,或把定義移到.cpp。 - 忘記 include guard /
#pragma once:同一標頭被間接引入兩次 → 重複定義錯誤。 - 同時用
#pragma once和#ifndefguard:多餘,擇一即可。 - 前向宣告後就想建立物件或存取成員:前向宣告只夠用於指標/參考;建立物件、呼叫方法、繼承、
sizeof都需要完整定義(必須#include)。 - Makefile 指令行用空格代替 Tab:
make會報 "missing separator"。指令行必須以 Tab 開頭。 - Makefile 漏寫標頭相依:改了
.h卻沒重編相關.o,用到過時物件碼,出現詭異行為。 undefined reference看成編譯錯誤:它其實是連結錯誤——通常是少編了某個.cpp,或函式只宣告未定義。- include guard 巨集名撞名:兩個不同標頭用了相同的 guard 巨集,導致其中一個內容被整段跳過。巨集名要夠獨特。
- 濫用全域
static而非匿名命名空間:static不能修飾型別;現代 C++ 偏好匿名命名空間取得內部連結。
練習題
練習 1:基本命名空間(基礎)
建立兩個命名空間 Physics 與 Chemistry,各自定義一個 calculate() 函式(內容自訂,例如分別計算動能與莫耳數)。在 main() 中分別以完整限定名稱呼叫,並印出結果,觀察兩個同名函式如何和平共存。
- 提示:呼叫時寫成
Physics::calculate(...)與Chemistry::calculate(...),思考若把兩個函式都放到全域會發生什麼事。
練習 2:巢狀命名空間與別名(基礎)
使用 C++17 語法建立 MyApp::Database::Query 命名空間,於其中定義 execute(const std::string& sql)。在 main() 中先以完整名稱呼叫一次,再用命名空間別名 namespace q = MyApp::Database::Query; 簡化後呼叫一次。
- 提示:別名只在當前作用域有效;比較加別名前後程式碼的可讀性。
練習 3:using 宣告 vs using 指令(中級)
在同一個 .cpp 內示範:(a) 用 using std::cout; using std::endl; 宣告引入;(b) 在某個函式內用 using namespace std; 指令。寫一段註解說明為什麼 (b) 不該出現在標頭檔,以及它在函式內為何相對安全。
- 提示:思考「作用域範圍」——指令放在函式內,污染只限於該函式。
練習 4:多檔案專案 + Makefile(中級)
把一個 Rectangle 類別(含 area() 面積與 perimeter() 周長)拆成 rectangle.h(宣告,含 #pragma once)與 rectangle.cpp(定義),並在 main.cpp 使用它。撰寫 Makefile,包含 all、clean、run 三個目標,並正確列出標頭相依。以 make 編譯成功。
- 提示:每個
.cpp第一行#include自己的標頭;Makefile 指令行記得用 Tab。
練習 5:匿名命名空間驗證內部連結(中級)
建立兩個 .cpp(a.cpp、b.cpp)與一個共用 main.cpp。在 a.cpp 與 b.cpp 各自的匿名命名空間中定義同名函式 helper()(回傳不同字串),並各自提供一個有外部連結的 callHelperA() / callHelperB() 來呼叫自己的 helper()。在 main.cpp 呼叫兩者,驗證它們不衝突且各自呼叫到本檔案的 helper()。
- 提示:若把
helper()改成非匿名、外部連結的全域函式,連結器會回報什麼錯誤?試著重現它。
練習 6:前向宣告解循環相依(挑戰)
設計 Teacher 與 Student 兩個類別,Teacher 持有其學生清單(std::vector<Student*>),Student 持有指向其導師的指標(Teacher*)。用前向宣告打破 teacher.h 與 student.h 的循環 #include,把需要完整定義的方法實作移到對應 .cpp。以多檔案 + Makefile 編譯通過。
- 提示:在標頭只放前向宣告與指標成員;需要呼叫對方方法(需完整定義)的程式碼放
.cpp,並在.cpp內#include對方的標頭。
練習 7:ODR 與 inline 實驗(挑戰)
故意在 bad.h 裡定義一個非 inline 的全域函式 int twice(int x){ return x*2; },讓 main.cpp 與 extra.cpp 都 #include "bad.h" 並一起連結,重現 "multiple definition" 連結錯誤。接著用兩種方式各別修好它:(a) 加 inline;(b) 標頭只留宣告、定義移到 bad.cpp。記錄你看到的錯誤訊息與修正後的差異。
- 提示:先用
g++ main.cpp extra.cpp -o app觀察錯誤屬於哪個階段;再思考為什麼inline能讓多 TU 各持一份定義而不違反 ODR。
對應程式碼檔案
| 檔案 | 內容 |
|---|---|
| namespaces.cpp | 命名空間完整範例(基本/巢狀/匿名/別名/using) |
| header_example/math_utils.h | 標頭檔範例(含 #pragma once、類別宣告) |
| header_example/math_utils.cpp | 實作檔範例(方法定義) |
| header_example/main.cpp | 主程式範例(使用多檔案類別) |
| Makefile | 多檔案專案 Makefile(變數/規則/自動變數/.PHONY) |