您問到了最關鍵的一點!這正是策略模式的精妙之處——它通過組合(composition)而非繼承(inheritance)來實現多態效果。
讓我詳細解釋這是如何工作的,以及它與傳統繼承多態的區別。
🔄 策略模式如何實現多態效果
在策略模式中,多態不是通過子類重寫父類虛函數實現的,而是通過將不同的算法對象(策略)注入到相同類型的對象中來實現的。
代碼示例回顧
cpp
復制
下載
class GameCharacter { public:explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {}int healthValue() const {return healthFunc(*this); // 這里實現多態調用}private:HealthCalcFunc healthFunc; // 策略對象 };// 不同的策略 int loseHealthQuickly(const GameCharacter&); int loseHealthSlowly(const GameCharacter&);// 創建對象并注入不同策略 EvilBadGuy ebg1(loseHealthQuickly); // 使用快速掉血策略 EvilBadGuy ebg2(loseHealthSlowly); // 使用緩慢掉血策略🧠 多態機制解析
1. 運行時多態(傳統繼承方式)
cpp
復制
下載
// 傳統繼承多態 class GameCharacter { public:virtual int healthValue() const = 0; // 純虛函數 };class EvilBadGuy: public GameCharacter { public:int healthValue() const override { return calculateQuickHealth(); // 特定實現} };class GoodGuy: public GameCharacter { public:int healthValue() const override {return calculateSlowHealth(); // 不同實現} };多態機制:通過虛函數表,在運行時根據對象實際類型調用正確的函數。
2. 策略模式多態(組合方式)
cpp
復制
下載
// 策略模式多態 class GameCharacter { public:int healthValue() const {return healthFunc(*this); // 委托給策略對象}void setHealthFunc(HealthCalcFunc hcf) {healthFunc = hcf; // 可以動態改變策略}private:HealthCalcFunc healthFunc; // 策略對象 };多態機制:通過函數指針/函數對象,在運行時調用被注入的策略函數。
🎯 策略模式多態的獨特優勢
1. 運行時動態改變行為
cpp
復制
下載
EvilBadGuy ebg(loseHealthQuickly); // 初始使用快速掉血// 戰斗中動態改變行為 ebg.setHealthFunc(loseHealthSlowly); // 切換到緩慢掉血// 添加特殊效果 ebg.setHealthFunc(poisonedHealthCalc); // 中毒效果傳統繼承多態無法做到這一點——對象的行為在創建時就已經固定。
2. 同一類對象的不同行為
cpp
復制
下載
// 兩個同類型的對象,行為不同 EvilBadGuy quickEnemy(loseHealthQuickly); EvilBadGuy slowEnemy(loseHealthSlowly);// 它們都是EvilBadGuy,但健康計算方式不同 int health1 = quickEnemy.healthValue(); // 使用快速計算 int health2 = slowEnemy.healthValue(); // 使用緩慢計算傳統繼承多態需要創建不同的子類才能實現這種行為差異。
3. 突破繼承 hierarchy 的限制
cpp
復制
下載
// 完全無關的類提供的算法 class PhysicsEngine { public:int calculateBasedOnPhysics(const GameCharacter&) const; };class WeatherSystem { public:int calculateBasedOnWeather(const GameCharacter&, int temperature) const; };// 使用這些外部算法 PhysicsEngine physics; WeatherSystem weather; int currentTemp = 25;auto physicsCalc = std::bind(&PhysicsEngine::calculateBasedOnPhysics, &physics, _1); auto weatherCalc = std::bind(&WeatherSystem::calculateBasedOnWeather, &weather, _1, currentTemp);EvilBadGuy ebg1(physicsCalc); // 使用物理引擎計算 EvilBadGuy ebg2(weatherCalc); // 使用天氣系統計算傳統繼承多態無法這樣靈活地使用外部算法。
📊 兩種多態方式對比
特性 傳統繼承多態 策略模式多態 行為綁定時機 編譯時(通過類定義) 運行時(通過對象構造或方法調用) 行為可變性 創建后不可變 創建后可動態改變 代碼組織 算法與類綁定 算法與類分離 靈活性 相對較低 極高 適用場景 行為是類型的固有特性 行為需要動態變化或配置 💡 實際項目中的應用思考
在游戲開發中,這種區別非常實用:
cpp
復制
下載
// 游戲中的實際應用 class Enemy { public:Enemy(HealthCalcFunc hcf) : healthCalc(hcf) {}int calculateHealth() const {return healthCalc(*this);}void applyStatusEffect(StatusEffect effect) {// 根據狀態效果切換健康計算策略switch (effect) {case StatusEffect::POISON:healthCalc = poisonedHealthCalc;break;case StatusEffect::REGENERATION:healthCalc = regeneratingHealthCalc;break;case StatusEffect::INVULNERABILITY:healthCalc = invulnerableHealthCalc;break;}}private:HealthCalcFunc healthCalc; };// 使用 Enemy enemy(normalHealthCalc); // 普通敵人enemy.applyStatusEffect(StatusEffect::POISON); // 敵人中毒,健康計算方式改變 enemy.applyStatusEffect(StatusEffect::REGENERATION); // 敵人開始回血,再次改變? 總結
策略模式通過組合和委托實現了另一種形式的多態,它與傳統的繼承多態相比:
更靈活:可以在運行時動態改變對象的行為
更解耦:算法與使用算法的對象完全分離
更強大:可以突破繼承體系的限制,使用任何來源的算法
在您看到的例子中,
GameCharacter
?父類并不需要知道具體使用哪種健康計算策略,它只是提供一個框架來調用被注入的策略。真正的多態效果來自于向不同對象注入不同的策略函數,而不是來自于繼承 hierarchy。
1. 條款35的核心目標是什么?
條款35的核心目標是:為你提供“實現多態行為”的替代方案,而不是簡單地使用虛函數。
為什么?因為虛函數并非完美:
** tightly coupled(緊耦合)**:派生類的實現與基類的函數簽名 tightly coupled。一旦基類虛函數簽名改變,所有派生類都必須修改。
缺乏靈活性:虛函數的行為在編譯時(通過繼承)就基本固定了,難以在運行時動態改變。
難以添加公共邏輯:如果想在所有虛函數調用前后都添加一些通用邏輯(如日志、鎖、驗證),需要在每個重寫函數里重復編寫。
條款35給出了三種主要的替代方案,其核心思想都是從“繼承”轉向“組合”,提升靈活性和可維護性。
2. NVI (Non-Virtual Interface) - 首推方案
NVI手法就是Template Method模式的一種特定應用。它主張:
使用非虛公有函數作為接口
調用私有的虛函數來實現具體行為
健康計算的NVI實現
cpp
復制
下載
class GameCharacter { public:// 1. 這就是“非虛接口”(Non-Virtual Interface)// 它是公有的、非虛的int healthValue() const {// ... 可以在調用前后添加“公共代碼” <- 這是關鍵優勢!std::cout << "開始計算健康值..." << std::endl; // 例如:日志std::lock_guard<std::mutex> lock(healthMutex); // 例如:加鎖int retVal = doHealthValue(); // 2. 轉而調用一個虛函數// ... 也可以在調用后添加代碼std::cout << "健康值計算完成: " << retVal << std::endl;return retVal;}// ... 其他成員函數virtual ~GameCharacter() = default; // 虛析構函數必不可少private:// 3. 私有虛函數,真正完成工作的函數virtual int doHealthValue() const {// 提供一個默認實現return 100;}mutable std::mutex healthMutex; // 示例用的互斥量 };// 派生類 class EvilBadGuy : public GameCharacter { private:// 4. 重新定義私有虛函數int doHealthValue() const override {// 實現特定于派生類的行為return 50; // 壞蛋健康值更低} };🔑 NVI/模板方法模式的優點:
強大的控制力:基類牢牢控制了接口的調用時機、上下文(如加鎖、日志、驗證),這些都是不可被派生類改變的。
“好萊塢原則”:派生類(子類)只負責提供實現細節,但什么時候調用、怎么調用,由基類(父類)決定。
代碼復用和增強:所有“增強性”的代碼(日志、鎖)只在基類寫一次。
所以,NVI就是Template Method模式在C++中實現多態的一種經典用法。
3. 第二種方案:函數指針 -> Strategy模式
這就是我們之前詳細討論的策略模式。通過組合一個函數指針(或任何可調用對象)來實現多態。
cpp
復制
下載
class GameCharacter {int healthValue() const {return healthCalcFunc(*this); // 策略模式:調用外部策略}// ... 其他成員HealthCalcFunc healthCalcFunc; // 組合了一個策略對象 };🔑 策略模式的優點:
極高的靈活性:同一個類的不同對象可以有不同的計算策略,并且可以在運行時動態切換。
解耦:
GameCharacter
類和健康計算算法完全分離。算法可以獨立變化和復用。突破繼承體系:計算策略可以來自任何地方(普通函數、另一個完全不相關的類的成員函數等)。
4. 第三種方案:
std::function
?-> 更強大的Strategy模式這是第二種方案的現代化升級。
std::function
是一個通用的函數包裝器,可以包裝任何可調用對象(函數指針、函數對象、lambda表達式、std::bind
表達式等),比普通函數指針強大得多。cpp
復制
下載
#include <functional>class GameCharacter { public:// 使用std::function作為策略類型using HealthCalcFunc = std::function<int(const GameCharacter&)>;explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf) {}int healthValue() const {return healthFunc(*this);}// ... private:HealthCalcFunc healthFunc; };// 使用示例: int defaultHealthCalc(const GameCharacter&); // 普通函數struct HealthCalculator { // 函數對象int operator()(const GameCharacter&) const { /* ... */ } };GameCharacter::HealthCalcFunc funcObj = HealthCalculator(); // 函數對象 GameCharacter char1(funcObj);// 使用lambda表達式!極其靈活 GameCharacter char2([](const GameCharacter& gc) { return 75; });// 使用std::bind綁定類的成員函數 class GameLevel { public:float health(const GameCharacter&) const; // 成員函數 }; GameLevel currentLevel; // 將health成員函數和currentLevel對象綁定,創建一個符合策略接口的可調用對象 GameCharacter char3(std::bind(&GameLevel::health, currentLevel, std::placeholders::_1));🔑?
std::function
的優點:它擁有策略模式的所有優點,并且靈活性達到了極致。你可以將任何東西作為策略塞給它,只要簽名匹配。
總結:條款35的精髓與選擇
條款35給了你一個“武器庫”,讓你在實現多態時不再只有“虛函數”這一把錘子。
方案 核心思想 優點 缺點 傳統虛函數 繼承 + 重寫 簡單直觀 緊耦合,缺乏靈活性,難以添加公共代碼 NVI Template Method模式 基類擁有絕對控制權,便于添加公共邏輯 行為仍在編譯時通過繼承固定 函數指針 Strategy模式(組合) 運行時動態改變行為,解耦 函數指針功能有限 std::function
超級Strategy模式 極致靈活,可接納任何可調用對象 語法稍復雜,可能帶來運行時開銷 如何選擇?
如果你想嚴格控制接口的調用上下文(必須加鎖、必須打日志),用NVI。
如果你需要在運行時動態改變對象的行為,或者算法來自外部(如配置文件),用Strategy模式(
std::function
)。如果行為非常簡單且穩定,用傳統虛函數也無妨。
希望這次講解能幫你徹底攻克這個重要的條款!它的價值在于開闊你的設計思路,讓你看到面向對象設計的更多可能性。
問題二:
std::bind
?—— “綁定類對象和成員函數”您的理解完全正確!
std::bind
?的核心作用就是將一個對象實例和它的一個成員函數“粘合”在一起,創建一個新的可調用對象。1. 為什么需要這樣做?
普通函數指針無法直接指向一個非靜態的成員函數,因為非靜態成員函數必須通過一個特定的對象實例來調用(它需要?
this
?指針)。
std::bind
?解決了這個問題。2.?
std::bind
?如何工作?—— “部分應用”
std::bind
?是一個函數模板,它接受一個可調用對象及其部分參數,然后返回一個新的可調用對象。你可以把它想象成“預先填好一部分參數的函數”。它的基本格式是:
cpp
復制
下載
auto newCallable = std::bind(existingCallable, arg1, arg2, ..., argN);
existingCallable
:想要綁定的原始可調用對象(函數、函數對象、成員函數指針等)。
arg1, arg2, ..., argN
:傳遞給?existingCallable
?的參數。可以是具體值,也可以是占位符。3. 關鍵:占位符?
std::placeholders::_1
占位符?
_1
?表示:“等將來調用?newCallable
?時,你把第一個參數放在這個位置”。讓我們看一個具體的例子來理解這個過程:
cpp
復制
下載
#include <functional> #include <iostream>// 一個外部服務類 class DamageService { public:int calculateDamage(int baseDamage, int enemyLevel) const {return baseDamage + enemyLevel * 5;} };int main() {DamageService service; // 1. 創建一個服務對象實例// 2. 神奇的綁定!// 我們要把 service.calculateDamage 變成一個只需要一個參數的新函數using namespace std::placeholders; // 引入 _1, _2 等占位符auto boundFunction = std::bind(&DamageService::calculateDamage, // 要綁定的成員函數&service, // 綁定到哪個對象實例(this指針)_1, // 占位符:新函數的第一個參數將放在這里10 // 固定值:將 enemyLevel 固定為 10);// 3. 使用新創建的函數// boundFunction 現在只需要一個參數!它的簽名相當于 int(int)int result = boundFunction(50); // 相當于調用 service.calculateDamage(50, 10)std::cout << result; // 輸出: 50 + 10*5 = 100return 0; }4. 在策略模式中的應用
在條款35的上下文中,
std::bind
?的魔力在于:它能將一個不符合策略接口(比如需要多個參數的成員函數)的調用,適配成完全符合策略接口(int(const GameCharacter&)
)的調用。cpp
復制
下載
class ExternalService { public:int complexCalc(const GameCharacter&, int difficulty, const std::string& region) const; };ExternalService service; int currentDifficulty = 5; std::string currentRegion = "forest";// 使用 bind 進行“適配”: // 1. 固定了 service, currentDifficulty, currentRegion 這三個參數 // 2. 只留出一個“空位” _1 給 GameCharacter 對象 auto adaptedStrategy = std::bind(&ExternalService::complexCalc,&service,_1, // 為 GameCharacter 占位currentDifficulty, // 固定參數currentRegion); // 固定參數// 現在 adaptedStrategy 的簽名完美匹配 HealthCalcFunc (int(const GameCharacter&)) GameCharacter hero(adaptedStrategy);總結:
std::bind
?是一個強大的“函數適配器”,它通過“部分應用”參數(固定一些參數,預留一些占位符),能夠將任何可調用對象(尤其是成員函數)轉換成我們需要的格式,從而極大地增強了策略模式的靈活性。