學習目標

  • 理解命名空間(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

想像一個沒有命名空間的世界。你的專案用了兩個第三方函式庫:一個繪圖庫提供 Colordraw(),一個遊戲引擎也提供 Colordraw()。當你同時 #include 兩者,連結器(甚至編譯器)會抱怨「重複定義」(multiple definition / redefinition)——這就是名稱衝突(name collision)

在 C 語言時代,大家只能靠「加前綴」來閃避,例如 gfx_Colorgame_ColorSDL_Initgl_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 同時擁有 addsub

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 namespace at 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)的關鍵工具:當 AB 互相引用時,至少其中一方改用前向宣告 + 指標即可打破環。

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++ 最重要的規則之一:

  1. 單一 TU 內,任何變數、函式、類別、樣板,定義至多一次
  2. 整個程式中,非 inline 的函式與(具外部連結的)變數,定義恰好一次
  3. 類別、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 file xxx.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 宣告「假目標」——這些目標不對應真實檔案(如 cleanallrun)。若不標記,且剛好存在一個叫 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 跨平台的現代建置工具

常見錯誤與陷阱

  1. 在標頭檔的全域範圍寫 using namespace std;:污染所有引入者的命名空間,引爆難查的衝突。違反 Core Guidelines SF.7。
  2. 在標頭檔定義非 inline 函式或全域變數:多個 .cpp 引入後造成 ODR 違反、連結期 "multiple definition"。改用 inline,或把定義移到 .cpp
  3. 忘記 include guard / #pragma once:同一標頭被間接引入兩次 → 重複定義錯誤。
  4. 同時用 #pragma once#ifndef guard:多餘,擇一即可。
  5. 前向宣告後就想建立物件或存取成員:前向宣告只夠用於指標/參考;建立物件、呼叫方法、繼承、sizeof 都需要完整定義(必須 #include)。
  6. Makefile 指令行用空格代替 Tabmake 會報 "missing separator"。指令行必須以 Tab 開頭。
  7. Makefile 漏寫標頭相依:改了 .h 卻沒重編相關 .o,用到過時物件碼,出現詭異行為。
  8. undefined reference 看成編譯錯誤:它其實是連結錯誤——通常是少編了某個 .cpp,或函式只宣告未定義。
  9. include guard 巨集名撞名:兩個不同標頭用了相同的 guard 巨集,導致其中一個內容被整段跳過。巨集名要夠獨特。
  10. 濫用全域 static 而非匿名命名空間static 不能修飾型別;現代 C++ 偏好匿名命名空間取得內部連結。

練習題

練習 1:基本命名空間(基礎)

建立兩個命名空間 PhysicsChemistry,各自定義一個 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,包含 allcleanrun 三個目標,並正確列出標頭相依。以 make 編譯成功。

  • 提示:每個 .cpp 第一行 #include 自己的標頭;Makefile 指令行記得用 Tab。

練習 5:匿名命名空間驗證內部連結(中級)

建立兩個 .cppa.cppb.cpp)與一個共用 main.cpp。在 a.cppb.cpp 各自的匿名命名空間中定義同名函式 helper()(回傳不同字串),並各自提供一個有外部連結的 callHelperA() / callHelperB() 來呼叫自己的 helper()。在 main.cpp 呼叫兩者,驗證它們不衝突且各自呼叫到本檔案的 helper()

  • 提示:若把 helper() 改成非匿名、外部連結的全域函式,連結器會回報什麼錯誤?試著重現它。

練習 6:前向宣告解循環相依(挑戰)

設計 TeacherStudent 兩個類別,Teacher 持有其學生清單(std::vector<Student*>),Student 持有指向其導師的指標(Teacher*)。用前向宣告打破 teacher.hstudent.h 的循環 #include,把需要完整定義的方法實作移到對應 .cpp。以多檔案 + Makefile 編譯通過。

  • 提示:在標頭只放前向宣告與指標成員;需要呼叫對方方法(需完整定義)的程式碼放 .cpp,並在 .cpp#include 對方的標頭。

練習 7:ODR 與 inline 實驗(挑戰)

故意在 bad.h 裡定義一個非 inline 的全域函式 int twice(int x){ return x*2; },讓 main.cppextra.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)