在C++中,虛函數是實現多態的傳統方式,但并非唯一選擇。過度依賴虛函數可能導致派生類與基類的強耦合,或難以在運行時靈活切換行為。《Effective C++》Item35指出:應根據場景選擇更合適的替代方案,包括NVI模式、函數指針、策略模式等。本文解析這些方案的原理、適用場景及實踐方式。
一、虛函數的局限性
虛函數的核心是“基類定義接口,派生類提供實現”,但存在以下短板:
- 行為擴展受限:派生類只能通過重寫虛函數修改行為,難以在調用前后添加統一邏輯(如日志、權限檢查)。
- 運行時切換困難:虛函數的行為綁定在派生類類型上,無法動態替換單個對象的行為(需創建新對象)。
- 耦合性高:派生類必須繼承基類,無法復用非繼承關系的實現。
示例:虛函數難以添加統一前置邏輯
class GameCharacter {
public:// 虛函數:派生類各自實現攻擊邏輯virtual void attack() const = 0;
};class Warrior : public GameCharacter {
public:void attack() const override {std::cout << "Warrior swings sword" << std::endl;}
};class Mage : public GameCharacter {
public:void attack() const override {std::cout << "Mage casts fireball" << std::endl;}
};
若需為所有attack
添加“消耗體力”的前置邏輯,需修改每個派生類的實現,違反開閉原則。
二、替代方案及實踐
1. NVI(Non-Virtual Interface)模式:用非虛函數包裹虛函數
原理:基類提供public非虛函數作為接口,在其中調用protected虛函數(派生類僅需重寫虛函數)。非虛函數可添加統一邏輯(如日志、前置檢查)。
class GameCharacter {
public:// 非虛接口:固定流程,不可重寫void attack() const {// 統一前置邏輯:消耗體力consumeStamina(