Effective c++ 35條款詳解

您問到了最關鍵的一點!這正是策略模式的精妙之處——它通過組合(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); // 敵人開始回血,再次改變

? 總結

策略模式通過組合和委托實現了另一種形式的多態,它與傳統的繼承多態相比:

  1. 更靈活:可以在運行時動態改變對象的行為

  2. 更解耦:算法與使用算法的對象完全分離

  3. 更強大:可以突破繼承體系的限制,使用任何來源的算法

在您看到的例子中,GameCharacter?父類并不需要知道具體使用哪種健康計算策略,它只是提供一個框架來調用被注入的策略。真正的多態效果來自于向不同對象注入不同的策略函數,而不是來自于繼承 hierarchy。

1. 條款35的核心目標是什么?

條款35的核心目標是:為你提供“實現多態行為”的替代方案,而不是簡單地使用虛函數

為什么?因為虛函數并非完美:

  1. ** tightly coupled(緊耦合)**:派生類的實現與基類的函數簽名 tightly coupled。一旦基類虛函數簽名改變,所有派生類都必須修改。

  2. 缺乏靈活性:虛函數的行為在編譯時(通過繼承)就基本固定了,難以在運行時動態改變。

  3. 難以添加公共邏輯:如果想在所有虛函數調用前后都添加一些通用邏輯(如日志、鎖、驗證),需要在每個重寫函數里重復編寫。

條款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/模板方法模式的優點:
  1. 強大的控制力:基類牢牢控制了接口的調用時機、上下文(如加鎖、日志、驗證),這些都是不可被派生類改變的。

  2. “好萊塢原則”:派生類(子類)只負責提供實現細節,但什么時候調用、怎么調用,由基類(父類)決定。

  3. 代碼復用和增強:所有“增強性”的代碼(日志、鎖)只在基類寫一次。

所以,NVI就是Template Method模式在C++中實現多態的一種經典用法。


3. 第二種方案:函數指針 -> Strategy模式

這就是我們之前詳細討論的策略模式。通過組合一個函數指針(或任何可調用對象)來實現多態。

cpp

復制

下載

class GameCharacter {int healthValue() const {return healthCalcFunc(*this); // 策略模式:調用外部策略}// ... 其他成員HealthCalcFunc healthCalcFunc; // 組合了一個策略對象
};
🔑 策略模式的優點:
  1. 極高的靈活性同一個類的不同對象可以有不同的計算策略,并且可以在運行時動態切換

  2. 解耦GameCharacter類和健康計算算法完全分離。算法可以獨立變化和復用。

  3. 突破繼承體系:計算策略可以來自任何地方(普通函數、另一個完全不相關的類的成員函數等)。


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給了你一個“武器庫”,讓你在實現多態時不再只有“虛函數”這一把錘子。

方案核心思想優點缺點
傳統虛函數繼承 + 重寫簡單直觀緊耦合,缺乏靈活性,難以添加公共代碼
NVITemplate 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?是一個強大的“函數適配器”,它通過“部分應用”參數(固定一些參數,預留一些占位符),能夠將任何可調用對象(尤其是成員函數)轉換成我們需要的格式,從而極大地增強了策略模式的靈活性。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/920932.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/920932.shtml
英文地址,請注明出處:http://en.pswp.cn/news/920932.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

51c自動駕駛~合集19

自己的原文哦~ https://blog.51cto.com/whaosoft/11793894 #DRAMA 首個基于Mamba的端到端運動規劃器 運動規劃是一項具有挑戰性的任務&#xff0c;在高度動態和復雜的環境中生成安全可行的軌跡&#xff0c;形成自動駕駛汽車的核心能力。在本文中&#xff0c;我…

大數據新視界 -- Hive 數據倉庫:架構深度剖析與核心組件詳解(上)(1 / 30)

&#x1f496;&#x1f496;&#x1f496;親愛的朋友們&#xff0c;熱烈歡迎你們來到 青云交的博客&#xff01;能與你們在此邂逅&#xff0c;我滿心歡喜&#xff0c;深感無比榮幸。在這個瞬息萬變的時代&#xff0c;我們每個人都在苦苦追尋一處能讓心靈安然棲息的港灣。而 我的…

軟考 系統架構設計師系列知識點之雜項集萃(137)

接前一篇文章:軟考 系統架構設計師系列知識點之雜項集萃(136) 第253題 在面向對象設計中,用于描述目標軟件與外部環境之間交互的類被稱為( ),它可以( )。 第1空 A. 實體類 B. 邊界類 C. 模型類 D. 控制類 正確答案:B。 第2空 A. 表示目標軟件系統中具有持久…

(附源碼)基于Spring Boot公務員考試信息管理系統設計與實現

摘 要 隨著公務員考試日益受到社會的廣泛關注&#xff0c;一個高效、便捷的公務員考試信息管理系統顯得尤為重要。本文設計并實現了一個基于前端Vue框架&#xff0c;后端采用Java與Spring Boot技術&#xff0c;數據庫選用MySQL&#xff0c;并部署在Tomcat服務器上的信息管理系統…

學習JavaScript的第一個簡單程序:Hello World

在JavaScript中&#xff0c;最簡單的程序是打印"Hello World"。可以通過以下方式實現&#xff1a; console.log("Hello World");將上述代碼保存為hello.js文件&#xff0c;通過Node.js運行或在瀏覽器控制臺中執行。 瀏覽器環境實現 在HTML文件中嵌入Jav…

【Big Data】Alluxio 首個基于云的數據分析和開源AI數據編排技術

目錄 1. 什么是 Alluxio&#xff1f;? 2. Alluxio 的誕生背景&#xff1a;為什么需要數據編排層&#xff1f;? 痛點 1&#xff1a;計算與存儲強耦合&#xff0c;適配成本高? 痛點 2&#xff1a;跨集群 / 跨云數據移動效率低? 痛點 3&#xff1a;數據訪問延遲高&#x…

uniApp App 嵌入 H5 全流程:通信與跳轉細節拆解

在 uniApp App 開發中&#xff0c;通過 WebView 嵌入 H5 頁面是常見需求&#xff08;如活動頁、第三方頁面&#xff09;&#xff0c;核心需解決「H5 與 App 通信」「H5 操作后返回/跳轉 App」兩大問題。本文基于 DCloud 官方方案&#xff08;原文鏈接&#xff09;&#xff0c;對…

技能提升必備:鴻蒙HarmonyOS應用開發者認證

技能提升必備&#xff1a;鴻蒙HarmonyOS應用開發者認證&#xff0c;HarmonyOS 認證是華為為開發者打造的能力衡量體系。隨著 HarmonyOS 系統影響力不斷擴大&#xff0c;市場對相關開發人才需求激增。該認證分為基礎與高級等不同級別&#xff0c;覆蓋應用開發、設備開發等方向。…

Chromium 架構中的 ContentClient / ContentBrowserClient 設計原理全解析

一、前言在閱讀 Chromium 源碼時&#xff0c;很多人會對這樣一段調用產生疑惑&#xff1a;bool BrowserMainLoop::AudioServiceOutOfProcess() const { return base::FeatureList::IsEnabled(features::kAudioServiceOutOfProcess) && !GetContentClient()->browser…

window和liunx提權學習筆記

liunx提權 反彈shell升級交互式反彈sehell 反彈的服務器 接受的服務器 連接上之后的shell&#xff0c;沒有tab鍵補全&#xff0c;不可以上下鍵顯示歷史命令 你會發現并不能如愿所償&#xff0c;「上下方向鍵」被強制轉換為了 ^[[A、^[[B 等字符。 正是由于「簡單 shell」的各種…

畢業項目推薦:47-基于yolov8/yolov5/yolo11的焊縫質量檢測識別系統(Python+卷積神經網絡)

文章目錄 項目介紹大全&#xff08;可點擊查看&#xff0c;不定時更新中&#xff09;概要一、整體資源介紹技術要點功能展示&#xff1a;功能1 支持單張圖片識別功能2 支持遍歷文件夾識別功能3 支持識別視頻文件功能4 支持攝像頭識別功能5 支持結果文件導出&#xff08;xls格式…

Java實現圖像像素化

使用Java實現圖像像素化藝術效果&#xff1a;從方案到實踐的完整指南引言&#xff1a;像素藝術的復興與編程實現 在當今高清、視網膜屏幕的時代&#xff0c;像素藝術&#xff08;Pixel Art&#xff09;作為一種復古的數字藝術形式&#xff0c;反而煥發出了新的生命力。從獨立游…

SpringBoot 自研運行時 SQL 調用樹,3 分鐘定位慢 SQL!

在復雜的業務系統中&#xff0c;一個接口往往會執行多條SQL&#xff0c;如何直觀地看到這些SQL的調用關系和執行情況&#xff1f; 本文將使用SpringBoot MyBatis攔截器構建一個SQL調用樹可視化系統。 項目背景 在日常開發中&#xff0c;我們經常遇到這樣的場景&#xff1a; …

部署 Go 項目的 N 種方法

Go 語言&#xff08;Golang&#xff09;以其簡單、高效和易于部署的特點&#xff0c;成為了很多企業開發和部署服務的首選語言。無論是微服務架構&#xff0c;還是命令行工具&#xff0c;Go 的編譯方式和標準庫使得部署變得更加輕松。本文將介紹部署 Go 語言項目的幾種常見方法…

【ARM】MDK工程切換高版本的編譯器后出現error: A1167E\A1159E\A1137E\A1517E\A1150E報錯

1、 文檔目標解決工程從Compiler 5切換到Compiler 6進行編譯時出現一些非語法問題上的報錯。2、 問題場景對于一些使用Compiler 5進行編譯的工程&#xff0c;要切換到Compiler 6進行編譯的時候&#xff0c;原本無任何報錯警告信息的工程在使用Compiler 6進行編譯后出現了一些非…

AtCoder Beginner Contest 421

文章目錄A MisdeliveryB Fibonacci ReversedC AlternatedD RLE MovingE YachtF Erase between X and YG Increase to make it IncreasingAtCoder Beginner Contest 421A Misdelivery Mansion AtCoder has N rooms numbered from room 1 to room N. Each room i is inhabited b…

數據結構:冒泡排序 (Bubble Sort)

目錄 從最簡單的操作開始 如何利用這個原子操作實現一個具體的小目標&#xff1f; 我們來手動模擬一下&#xff1a; 如何從一個小目標擴展到最終目標&#xff1f; 代碼的逐步完善 第一階段&#xff1a;定義函數框架和我們需要的“原子操作” 第二階段&#xff1a;實現“…

教育項目管理工具新趨勢:可視化與自動化如何提升效率?

課程項目不同于普通商業項目&#xff0c;它涉及 “教研設計→內容開發→師資準備→市場推廣→學員服務” 全鏈路&#xff0c;環節多、角色雜、周期跨度大。傳統的 Excel 表格、口頭溝通不僅難以追蹤進度&#xff0c;更易造成信息斷層。而看板工具憑借 “可視化流程、輕量化協作…

計算兩個二值圖像的交集計算交點數量的基礎上,進一步使用 DBSCAN 算法對交點進行聚

好的&#xff0c;如果你需要在計算交點數量的基礎上&#xff0c;進一步使用 DBSCAN 算法對交點進行聚類&#xff0c;以合并距離較近的點&#xff0c;可以按照以下步驟實現&#xff1a; 計算交點&#xff1a;使用 cv2.bitwise_and 計算兩個二值圖像的交集&#xff0c;并提取交點…

Linux中的IP命令詳解

華子目錄 1.ip命令是什么1.1ip命令的由來1.2ip命令的安裝包1.2ip選項&#xff08;基本不用&#xff09; 2.查看網絡信息2.1顯示全部網絡接口信息2.2顯示單個網絡接口信息2.3顯示單個接口狀態2.4查看路由表2.5查看arp緩存 3.設置網卡ip地址3.1啟用或停用網卡3.2設置默認網關3.3新…