您问到了最关键的一点!这正是策略模式的精妙之处——它通过组合(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新…