一句话核心结论 :C++ 继承有三种关系——is-a(public 继承)、has-a / is-implemented-in-terms-of(复合)、is-implemented-in-terms-of(private 继承) 。本章 9 个条款讲透:什么时候用 public 继承、什么时候复合更好、private 继承的边界、虚函数的 7 个工程点、模板方法模式、多重继承的虚继承、避免遮蔽名字、virtual 函数替代方案。
系列导航 前言:为什么”继承”是 C++ 最难的部分? Java / C# 程序员:继承就是 extends —— 一个类继承另一个类。
C++ 程序员:继承分 5 种 (public / protected / private)、关系分 3 种 (is-a / has-a / is-implemented-in-terms-of)、多态分 2 种 (编译期 / 运行期)、虚继承处理菱形 ——
C++ 的”继承”是一整套哲学体系 ,用错一种就是坑。
本章 9 个条款的核心问题:
什么时候用 public 继承? (is-a 关系)什么时候用复合? (has-a / is-implemented-in-terms-of)什么时候用 private 继承? (实现继承)什么时候用多重继承? (接口 + 实现)虚函数怎么设计才不出错? 一、条款 32:确定你的 public 继承塑模出 is-a 关系 1.1 什么是 is-a? 1 2 3 class Person { };class Student : public Person { };
is-a 的严格定义:
任何 使用基类对象的地方,都能 用派生类对象替换——而不破坏逻辑。
Liskov 替换原则 (LSP):
1 2 3 4 5 void process (const Person& p) { }Person p; Student s; process (p); process (s);
1.2 反例:违反 is-a 的灾难 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Rectangle {public : virtual void setWidth (double w) { width_ = w; } virtual void setHeight (double h) { height_ = h; } double area () const { return width_ * height_; } private : double width_, height_; }; class Square : public Rectangle {public : void setWidth (double w) override { width_ = height_ = w; } void setHeight (double h) override { width_ = height_ = h; } }; void test (Rectangle& r) { r.setWidth (5 ); r.setHeight (4 ); assert (r.area () == 20 ); } Square s; test (s);
为什么违反?
“Rectangle” 的不变量:width 和 height 独立 “Square” 的不变量:width == height Square 不能完全 做 Rectangle 能做的事 正确做法 :
1 2 3 4 5 6 7 class Shape {public : virtual double area () const = 0 ; }; class Rectangle : public Shape { };class Square : public Shape { };
1.3 真实世界:鸟的”is-a”问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Bird {public : virtual void fly () { } }; class Penguin : public Bird { }; void Penguin::fly () override { throw std::runtime_error ("Penguins can't fly" ); } class Bird { };class FlyingBird : public Bird {public : virtual void fly () ; }; class Penguin : public Bird { };class Sparrow : public FlyingBird { };
启示 :is-a 必须是真的”每一个属性都成立” ——不能有”特例”。
1.4 关键启示 public 继承 = is-a ——不可妥协Liskov 替换原则 ——派生类必须能完全替代基类违反 is-a 的常见征兆 :基类有”派生类做不到”的方法遇到违反时 :重新设计继承关系(用 abstract 中间层)二、条款 33:避免遮蔽继承而来的名字 2.1 问题:派生类的名字”遮蔽”基类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Base {public : virtual void mf1 () const ; virtual void mf1 (int ) const ; void mf3 () const ; void mf4 () const ; }; class Derived : public Base {public : void mf1 () const override ; void mf5 () const ; }; Derived d; d.mf1 (); d.mf1 (42 ); d.mf3 (); d.mf4 (); d.mf5 ();
问题 :Derived::mf1 遮蔽了 Base::mf1 的所有重载 ——即使参数不同。
2.2 解决方案 1:using 声明 1 2 3 4 5 6 7 8 9 class Derived : public Base {public : using Base::mf1; void mf1 () const override ; }; Derived d; d.mf1 (); d.mf1 (42 );
2.3 解决方案 2:转发函数 1 2 3 4 5 6 class Derived : public Base {public : void mf1 () const override ; void mf1 (int x) const { Base::mf1 (x); } };
适用 :只想”重载”部分基类方法时。
2.4 关键启示 派生类的同名函数会遮蔽基类 ——即使参数不同using 声明 是首选——简单转发函数 是精确控制——复杂但灵活不要用 using 让”所有继承名字”暴露 ——按需三、条款 34:区分接口继承和实现继承 3.1 虚函数的 4 种语义 1 2 3 4 5 6 7 8 9 10 11 class Shape {public : virtual void draw () const = 0 ; virtual void error (const std::string& msg) ; int objectID () const ; };
类型 接口 实现 纯虚函数 ✅ 必须继承 ❌ 无默认实现 普通虚函数 ✅ 继承 ✅ 提供默认实现(可重写) 非虚函数 ✅ 继承 ✅ 固定(不允许改)
3.2 案例:3 种虚函数的”用错” 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Shape {public : virtual void draw () const = 0 ; }; void Shape::draw () const { } class Circle : public Shape { }; Circle c;
反直觉 :纯虚函数可以 有实现——但派生类必须 重写才能实例化。
3.3 案例:普通虚函数 vs 纯虚函数 + 默认实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Shape {public : virtual void draw () const { } }; class Derived : public Shape { }; class Shape {public : virtual void draw () const = 0 ; }; void Shape::draw () const { } class Derived : public Shape {public : void draw () const override { Shape::draw (); } };
3.4 案例:非虚函数的”is-a 不可改” 1 2 3 4 5 6 7 8 class Shape {public : int objectID () const ; }; class Circle : public Circle { };
非虚函数表达 :这个行为对所有派生类都一样 ——不可定制。
3.5 关键启示 纯虚函数 = 接口继承(”必须重写”)普通虚函数 = 接口 + 默认实现(”可重写”)非虚函数 = 接口 + 固定实现(”不能改”)默认实现用纯虚 + 实现体 ——避免”忘记重写”四、条款 35:考虑 virtual 函数以外的其他选择 4.1 NVI 模式(Non-Virtual Interface) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class GameCharacter {public : int healthValue () const { int ret = doHealthValue (); return ret; } private : virtual int doHealthValue () const { } }; class Hero : public GameCharacter {private : int doHealthValue () const override { } };
NVI 的优势 :
基类可以加”模板代码” ——前置/后置处理虚函数是 private ——明确”派生类只能重写实现,不能改变接口”public 函数是 non-virtual ——派生类不能改”外部调用”的形式4.2 策略模式(Strategy Pattern) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class HealthCalcFunc {public : virtual int calc (const GameCharacter& gc) const = 0 ; virtual ~HealthCalcFunc () = default ; }; class SlowHealthLoser : public HealthCalcFunc { };class FastHealthLoser : public HealthCalcFunc { };class GameCharacter { std::shared_ptr<HealthCalcFunc> healthFunc_; public : explicit GameCharacter (std::shared_ptr<HealthCalcFunc> hcf = defaultHealthFunc()) : healthFunc_(std::move(hcf)) { } int healthValue () const { return healthFunc_->calc (*this ); } };
优势 :
运行时可换 ——切换策略多个对象共享 ——同一策略可测试 ——传不同策略4.3 传统虚函数 vs NVI vs 策略 维度 虚函数 NVI 策略 运行时替换 ❌ ❌ ✅ 模板代码 ❌ ✅ ✅ 编译时绑定 ✅ ✅ ❌(间接) 复杂度 低 中 高
4.4 关键启示 NVI 模式 = public non-virtual + private virtual ——加模板代码策略模式 = “对象组合” ——运行时可换函数对象(functor)替代虚函数 ——编译期多态std::function 也行 ——更灵活五、条款 36:绝不重新定义继承而来的 non-virtual 函数 5.1 反例 1 2 3 4 5 6 7 8 9 10 11 12 13 class Base {public : void mf () const { std::cout << "Base::mf\n" ; } }; class Derived : public Base {public : void mf () const { std::cout << "Derived::mf\n" ; } }; Base* pb = new Derived (); pb->mf (); delete pb;
问题 :
静态类型是 Base*——调 Base::mf 期望派生类重写?no——非虚函数不能”动态分派”! 5.2 为什么”绝不”重新定义? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Base {public : void mf () const { } }; class Derived : public Base {public : void mf () const { } };
is-a 的矛盾 :
如果 Derived::mf 必须”和 Base::mf 一样”——重新定义就是冗余 如果 Derived::mf 必须”和 Base::mf 不一样”——mf 不应该是 non-virtual 所以:任何场景都不该重新定义 5.3 关键启示 non-virtual 函数 = “对所有派生类都一样” ——不要重写重写 non-virtual = 编译期遮蔽 ——调用静态类型版本想让派生类定制?改成 virtual 六、条款 37:绝不重新定义继承而来的缺省参数值 6.1 经典陷阱 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Shape {public : enum class Color { Red, Green, Blue }; virtual void draw (Color c = Color::Red) const = 0 ; }; class Circle : public Circle {public : void draw (Color c = Color::Green) const override { } }; Shape* ps = new Circle (); ps->draw (); delete ps;
为什么? 缺省参数是静态绑定 ——按静态类型 决定。
6.2 解决方案 1:NVI 模式 1 2 3 4 5 6 7 8 9 class Shape {public : void draw (Color c = Color::Red) const { doDraw (c); } private : virtual void doDraw (Color c) const = 0 ; };
优势 :
Shape* / Circle* 调 draw()——都是 Red派生类不重写缺省——避免陷阱 6.3 关键启示 缺省参数是静态绑定 ——按静态类型虚函数 + 缺省 = 易踩坑 ——避免NVI 模式 ——public 函数带缺省,private virtual 不带七、条款 38:通过复合塑模出 has-a 或 “is-implemented-in-terms-of” 7.1 复合的两种语义 has-a (有一个):
1 2 3 4 5 class Address { };class Person {private : Address address_; };
is-implemented-in-terms-of (用…实现):
1 2 3 4 5 6 7 8 template <typename T>class Set {private : std::list<T> rep_; public : bool add (const T& item) { } };
7.2 复合 vs 继承:决策表 关系 用什么? is-a public 继承 has-a 复合(值成员) is-implemented-in-terms-of 复合(私有成员)或 private 继承
7.3 实战:复合优于继承的案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 template <typename T>class Set : public std::list<T> {public : void add (const T& item) { push_back (item); } bool contains (const T& item) const { return std::find (begin (), end (), item) != end (); } }; Set<int > s; s.add (1 ); s.contains (1 ); s.push_back (2 );
问题 :
派生类暴露 了基类的所有 public 接口 Set 不应该有”顺序”语义,但 list 有正确做法 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <typename T>class Set {private : std::list<T> rep_; public : void add (const T& item) { rep_.push_back (item); } bool contains (const T& item) const { return std::find (rep_.begin (), rep_.end (), item) != rep_.end (); } }; Set<int > s; s.add (1 ); s.contains (1 ); s.push_back (2 );
7.4 关键启示 复合比继承更”克制” ——只暴露你要的接口has-a / is-implemented-in-terms-of = 复合 is-a = public 继承 (不可替代)优先复合 ——继承是”最后手段”八、条款 39:明智而审慎地使用 private 继承 8.1 private 继承 vs 复合 1 2 3 4 5 6 7 8 9 10 class Widget : private Timer { }; class Widget {private : Timer t_; };
两者都能表达 “Widget is implemented in terms of Timer”。
8.2 private 继承的语义 维度 public 继承 private 继承 派生类”是”基类? ✅ ❌(只是”用基类实现”) 隐式 upcast? ✅ ❌(基类成员变 private) 默认继承 N/A private(class) / public(struct)
8.3 什么时候用 private 继承? 场景 1:需要访问基类的 protected 成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Timer {public : virtual void onTick () const ; protected : int currentTick () const ; }; class Widget : private Timer { void f () { int t = currentTick (); } };
场景 2:需要重写基类的虚函数 1 2 3 4 class Widget : private Timer {private : void onTick () const override ; };
场景 3:空基类优化(EBO) 1 2 3 4 5 6 7 8 9 class Empty {}; class WithMember { Empty e_; }; class WithPrivateInherit : private Empty { };
C++ 规则 :复合不能 EBO,private 继承可以。
8.4 默认用复合 1 2 3 4 5 6 7 8 9 10 11 class Widget {private : Timer timer_; };
8.5 关键启示 private 继承 = “用基类实现” ——比 public 继承弱默认用复合 ——更灵活3 个例外 ——protected 访问 / 虚函数重写 / EBO复合 + friend 也能达到 private 继承的效果九、条款 40:明智而审慎地使用多重继承 9.1 多重继承的”正常”用法:多接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class ISerializable {public : virtual void serialize (std::ostream&) const = 0 ; virtual void deserialize (std::istream&) = 0 ; }; class IPrintable {public : virtual void print () const = 0 ; }; class Document : public ISerializable, public IPrintable { void serialize (std::ostream&) const override ; void deserialize (std::istream&) override ; void print () const override ; };
这是 MI 的”清洁”用法 ——多个纯接口。
9.2 菱形继承的灾难 1 2 3 4 5 6 7 class File { };class InputFile : public File { };class OutputFile : public File { };class IOFile : public InputFile, public OutputFile { };
问题 :
IOFile 包含两份 File(从 InputFile 和 OutputFile)内存浪费 + 转换歧义 9.3 虚继承解决菱形 1 2 3 4 5 6 7 class File { };class InputFile : virtual public File { }; class OutputFile : virtual public File { };class IOFile : public InputFile, public OutputFile { };
虚继承的代价 :
访问基类成员变慢 (间接寻址) 构造/析构复杂 设计变难 9.4 多重继承的”反模式” 1 2 3 4 5 6 7 8 9 10 11 class DebugMsg {public : void printDebug () { } }; class MyClass : public DebugMsg { };
9.5 关键启示 多个接口 = 多重继承 ——清洁用法菱形继承 = 虚继承 ——但有性能代价避免”实现继承”的 MI ——用复合MI 的设计要谨慎 ——能用单继承 + 接口代替就代替十、9 个条款的”继承与 OOP”全景 graph TB
A["继承关系"] --> B["is-a\npublic 继承"]
A --> C["has-a / is-impl-in-terms-of\n复合"]
A --> D["is-impl-in-terms-of\nprivate 继承"]
A --> E["多接口\n多重继承"]
B -.->|条款 32| B1["Liskov 替换"]
C -.->|条款 38| C1["克制暴露"]
D -.->|条款 39| D1["protected / 虚 / EBO"]
E -.->|条款 40| E1["多接口优先\n菱形要虚继承"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C1 fill:#FFF9C4,stroke:#F9A825,color:#333
style D1 fill:#FFF9C4,stroke:#F9A825,color:#333
style E1 fill:#FFF9C4,stroke:#F9A825,color:#333虚函数的设计 :
graph TB
A["虚函数的选择"] --> B["纯虚\n= 接口"]
A --> C["普通虚\n= 接口 + 默认"]
A --> D["非虚\n= 接口 + 固定"]
B -.->|条款 33| B1["using 解除遮蔽"]
C -.->|条款 35| C2["NVI / 策略 / 模板"]
D -.->|条款 36| D3["绝不重定义"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style B1 fill:#FFF9C4,stroke:#F9A825,color:#333
style C2 fill:#FFF9C4,stroke:#F9A825,color:#333
style D3 fill:#FFF9C4,stroke:#F9A825,color:#333十一、常见误区与陷阱 11.1 误区 1:Square 继承 Rectangle 1 2 class Square : public Rectangle { };
11.2 误区 2:重写 non-virtual 函数 1 2 3 4 5 6 7 class Base {public : void mf () { } }; class Derived : public Base { void mf () { } };
11.3 误区 3:虚函数 + 缺省参数 1 virtual void draw (Color c = Red) const ;
11.4 误区 4:用继承做”实现”(用复合更好) 1 2 3 4 5 6 class Set : public std::list<T> { };class Set { std::list<T> rep_; };
11.5 误区 5:菱形继承忘了 virtual 1 2 3 4 class IOFile : public InputFile, public OutputFile { };class InputFile : virtual public File { };
十二、C++11/14/17 的演进 主题 C++98 时代 C++11/14/17 时代 虚函数 virtual + override(手动) override 关键字 + final 关键字多重继承 接口 + 实现混合 多接口 + 抽象基类 缺省参数 静态绑定 同 C++98(仍静态绑定) 模板方法 虚函数实现 虚函数 / 模板 / std::function EBO 仅 private 继承 同 C++98 虚继承 慢 + 复杂 同 C++98
C++11 的 final 关键字 :
1 2 3 4 class Base {public : virtual void f () final ; };
C++11 的 override 关键字 :
1 2 3 4 class Derived : public Base {public : void f () override ; };
十三、面试高频考点 13.1 必背题 题目 答案要点 什么是 is-a? public 继承表达的关系:派生类完全 能做基类的事 复合 vs 继承怎么选? is-a 用继承;has-a / is-impl 用复合 private 继承什么时候用? protected 访问 / 虚函数重写 / EBO 多重继承的菱形问题? 虚继承解决,但有性能代价 虚函数的缺省参数能重写吗? 不能(静态绑定)——用 NVI 模式 纯虚函数可以有实现吗? 可以——派生类必须重写才能实例化 NVI 模式是什么? public non-virtual + private virtual Square 继承 Rectangle 对吗? 不对——违反 Liskov
13.2 高频追问 追问 关键点 为什么 NVI 优于普通虚函数? public 不可重写,virtual 是 private 细节 策略模式 vs 虚函数? 策略 = 运行时可换;虚函数 = 编译期绑定 什么是 EBO? Empty Base Optimization——空基类不占空间 模板方法 vs 虚函数? 模板方法用继承 + 虚函数;模板用编译期多态 如何判断是否 is-a? 代入:基类的所有不变量,派生类必须满足
十四、配套实验 14.1 实验 1:NVI 模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <iostream> class GameCharacter {public : int healthValue () const { std::cout << "Computing health...\n" ; int ret = doHealthValue (); return ret; } private : virtual int doHealthValue () const = 0 ; }; class Hero : public GameCharacter {private : int doHealthValue () const override { return 100 ; } }; class Monster : public GameCharacter {private : int doHealthValue () const override { return 50 ; } }; int main () { Hero h; Monster m; std::cout << "Hero HP: " << h.healthValue () << "\n" ; std::cout << "Monster HP: " << m.healthValue () << "\n" ; return 0 ; }
14.2 实验 2:复合 vs 继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <list> #include <algorithm> class BadSet : public std::list<int > {public : void add (int x) { push_back (x); } bool contains (int x) const { return std::find (begin (), end (), x) != end (); } }; class GoodSet { std::list<int > rep_; public : void add (int x) { rep_.push_back (x); } bool contains (int x) const { return std::find (rep_.begin (), rep_.end (), x) != rep_.end (); } }; int main () { BadSet bs; bs.add (1 ); bs.push_back (2 ); std::cout << "BadSet contains 2: " << bs.contains (2 ) << "\n" ; GoodSet gs; gs.add (1 ); std::cout << "GoodSet contains 1: " << gs.contains (1 ) << "\n" ; return 0 ; }
14.3 实验 3:多重继承 + 虚继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <iostream> class File { public : int data_ = 42 ; };class InputFile : public File { };class OutputFile : public File { };class BadIOFile : public InputFile, public OutputFile { }; class GoodInputFile : virtual public File { };class GoodOutputFile : virtual public File { };class GoodIOFile : public GoodInputFile, public GoodOutputFile { }; int main () { GoodIOFile g; std::cout << "data_ = " << g.data_ << "\n" ; return 0 ; }
十五、回到 9 条黄金法则 条款 黄金法则 32 public 继承 = is-a(Liskov 替换) 33 派生类同名函数会遮蔽基类——using 声明解除 34 区分纯虚 / 普通虚 / 非虚——三种语义 35 NVI / 策略 / 函数对象——替代虚函数 36 绝不重定义 non-virtual 37 绝不重定义缺省参数——用 NVI 模式 38 复合 > 继承——除非真的 is-a 39 private 继承仅 3 个场景:protected / 虚 / EBO 40 MI 用于多接口——菱形用虚继承
十六、结尾思考题 思考题 1 :Square 能不能继承 Rectangle?为什么?
思考题 2 :实现一个 Shape 继承体系:用 NVI 模式 + 模板方法。
思考题 3 :什么时候应该用 private 继承?写出一个实际场景。
思考题 4 :虚函数的缺省参数为什么是”静态绑定”?这有什么实际影响?
思考题 5 :NVI 模式、策略模式、std::function 三种方式实现”健康值计算”——比较优劣。
十七、本篇速查表 主题 关键 API / 模式 适用场景 is-a public 继承 “完全” 替换 has-a / is-impl 复合 成员 is-impl private 继承 protected / 虚 / EBO 多接口 MI 多个纯接口 菱形 virtual public 共享基类 遮蔽解决 using 声明 解除遮蔽 虚函数语义 纯 / 普通 / 非虚 三种语义 NVI public non-virtual + private virtual 模板代码 策略模式 std::function / 抽象策略类运行时切换 模板方法 继承 + 虚函数 框架设计
十八、系列导航 下一篇 :第 7 篇《模板与泛型:编译期多态的 8 大设计》——条款 41-48 一起讲透 C++ 模板:隐式接口与编译期多态、typename 的双重含义、模板参数推导、显式指定模板参数、智能指针的 helper 函数、模板元编程、SFINAE、traits classes。
行动建议 :
今天 :检查你的类继承关系——真的有 is-a 关系吗?今天 :把虚函数 + 缺省参数改成 NVI 模式本周 :识别你项目里的”复合好于继承”场景本周 :用 using 声明解决你项目里”派生类遮蔽基类”的问题思考 :你的多态是用虚函数还是策略模式?