您问到了最关键的一点!这正是策略模式的精妙之处------它通过组合(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 = 100 return 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
是一个强大的"函数适配器",它通过"部分应用"参数(固定一些参数,预留一些占位符),能够将任何可调用对象(尤其是成员函数)转换成我们需要的格式,从而极大地增强了策略模式的灵活性。
Effective c++ 35条款详解
老赵的博客2025-08-29 14:24