一文详解 C++ 继承体系
继承是 C++ 面向对象的核心机制之一,用来复用代码、表达“is-a”关系并支持运行时多态(virtual functions)。但 C++ 的继承体系很强大也很复杂:有访问控制(public/protected/private)、单继承/多继承、虚继承、虚函数表(vptr/vtable)、对象切片(slicing)、构造/析构顺序、name hiding、以及模板技巧(CRTP)等。弄清这些细节能避免常见的 BUG 和运行时错误。
优惠:https://secret-thanks.com/bi3/VX0.PC3Bp_vyb/mSVLJjZBDI0z2mMWDbQlwINQDLAd3LL/TTYdwhN/DbA-0IM_DJgi
基本语法与访问控制
struct Base {
public:int x;
protected:int p;
private:int q;virtual void vf() {}
};struct Derived : public Base {// public 继承:Base 的 public -> public, protected -> protected
};struct Derived2 : protected Base {// protected 继承:Base 的 public/protected -> protected 在 Derived2 中
};struct Derived3 : private Base {// private 继承:Base 的 public/protected -> private 在 Derived3 中
};
- public 继承:表示
Derived is-a Base
,允许外部把Derived*
隐式转换为Base*
,常用的“接口继承”方式。 - protected/private 继承:更像是“实现继承 / 用于实现”的手段(“implemented-in-terms-of”),外部不能隐式把 Derived 转为 Base(或受限制)。极少用于 API 设计,常用于库内部实现细节。
struct
默认继承/成员访问是public
,class
默认是private
。
成员继承与名字隐藏(name hiding)
- 派生类继承基类的非 private 成员(数据、函数、类型别名等),但访问控制仍然受继承类型影响。
- 名字隐藏:如果派生类定义了与基类同名函数(即使签名不同),基类中同名的所有重载都会被隐藏。要引入基类的重载,使用
using
:
struct Base { void f(int); void f(double); };
struct Derived : Base {using Base::f; // 引入基类的 f 重载集合void f(char); // 新增一个重载
};
构造 / 析构 顺序(重要)
构造顺序(most-derived -> bases? careful!):
- 虚基类(virtual bases)先被构造(只构造一次),顺序按照最派生类继承声明的某个规则(通常是从左到右和声明顺序,细节由标准定义)。
- 然后按派生类 base-specifier-list 中从左到右顺序构造非虚基类。
- 再构造派生类的成员(按在类中声明的顺序)。
- 最后进入派生类构造函数体(body)。
析构顺序与之相反(派生析构体先运行,然后成员,然后 base,最后虚 base)。
注意:对于虚继承,最派生类负责调用虚基类的构造函数并传参(即虚基由 most-derived 初始化)。
对象布局、vptr/vtable、对象切片
- 为了实现动态绑定(virtual functions),编译器通常在每个多态(含虚函数)的对象中放一个指向该类型 vtable 的指针(称为
vptr
)。vtable
存放函数指针用于动态调用。 - 多重/虚继承时,可能存在多个
vptr
或额外的指针(如vbptr
)用于基类偏移调整,具体实现依编译器而异。 - 对象切片(slicing):把派生类对象按值赋给基类对象会丢失派生部分:
struct Base { virtual ~Base() = default; int b; };
struct Derived : Base { int d; };Derived dd; Base b = dd; // slicing:b 不包含 d
因此要传递多态对象应使用指针或引用(Base*
/Base&
或 智能指针)。
虚函数与运行时多态
struct Base {virtual void foo() { std::cout << "Base\n"; }virtual ~Base() = default; // 若要通过 Base* 删除派生对象,必须 virtual
};struct Derived : Base {void foo() override { std::cout << "Derived\n"; }
};
- virtual 声明使得调用
ptr->foo()
在运行时按对象的动态类型选择实现(动态绑定)。 override
(C++11)用于标注:编译器会检查该函数确实覆盖了基类的虚函数(避免写错签名后非覆盖导致的问题)。final
可以阻止进一步重写:void foo() final;
。- 虚析构:如基类打算作为多态基类,必须有虚析构函数,否则通过
Base*
删除Derived*
会导致未定义行为(派生析构未被调用)。
纯虚函数与抽象类(interface)
struct Interface {virtual void f() = 0; // 纯虚函数virtual ~Interface() = default;
};
- 有纯虚函数的类是抽象类,不能实例化。
- 抽象类常用作接口(纯虚 + 无数据成员)。
- 纯虚函数也可以有实现(少见),但仍使类抽象:
virtual void f() = 0;
在类外定义void Interface::f(){...}
。
协变返回(Covariant return types)
派生类重写虚函数时允许返回协变类型(返回指针或引用时):
struct Base { virtual Base* clone() const; };
struct Derived : Base { Derived* clone() const override; } // 合法:Derived* 是 Base* 的协变类型
多重继承(multiple inheritance)
C++ 允许从多个基类派生:
struct A { int a; virtual ~A()=default; };
struct B : A {};
struct C : A {};
struct D : B, C {}; // D 中存在两个 A 子对象(如果不是虚继承)
- 多重继承带来二义性:
D d; d.a;
会编译错误(不确定访问哪个 A::a),必须显式d.B::a
或d.C::a
。 - 如果基类都是多态(含 virtual),并想要共享同一个基类子对象(避免重复),则用 虚继承(virtual base)。
虚继承(diamond / 菱形问题)
经典菱形:
A/ \B C\ /D
如果 B、C 都继承自 A,而 D 又从 B、C 继承,默认会有两个 A 子对象。若想在 D 中只保留一个 A 子对象,B 和 C 应该虚继承 A:
struct A { int a; };
struct B : virtual A {};
struct C : virtual A {};
struct D : B, C {}; // 只有一个 A 子对象,由 D 初始化
- 初始化:最派生类
D
负责初始化虚基A
(传递构造参数)。B、C 不再单独构造 A。 - 代价:虚继承会引入额外的实现复杂度(编译器可能增加偏移表或指针来在运行时找到虚基),导致对象体积或访问成本上升。具体代价依实现而异(但通常是存在的)。
超低价esim流量卡:https://www.wanmoon.mom/redteago-esim/
指针/引用转换、dynamic_cast 与 RTTI
static_cast<T*>
:在继承链上做编译时转换(不做运行时检查)。dynamic_cast<T*>
:在多态基类上做安全向下转换(需要包含虚函数以启用 RTTI)。如果失败,返回nullptr
(指针情况);引用失败抛std::bad_cast
。
Base* b = ...;
Derived* d = dynamic_cast<Derived*>(b);
if (d) { /* 成功 */ }
typeid
:检查对象的动态类型(多态类型时返回动态类型),需注意typeid(*ptr)
要保证 ptr 非空。
名称查找 / overload resolution 的细微点
- 名称查找先在派生类进行,若找到对应名称则基类中同名会被隐藏(无论签名),接着在该名字对应的可用集合中做重载解析。
- 解决隐藏常用方法:
using Base::f;
或者显式Base::f()
调用。
空基优化(EBO:Empty Base Optimization)
- 如果基类是空类(没有非静态数据成员),编译器通常会把它作为空子对象并不增加派生对象的大小(EBO),这在如
std::tuple
等模板库里很常见,用以节省空间。 - 但如果派生类有多个同类型的空基,标准允许但具体细节和符号链接(type identity)有关;一般情况下 EBO 会被使用。
模板技巧:CRTP(静态多态)
CRTP(Curiously Recurring Template Pattern)是一种静态多态替代运行时虚函数的技巧:
template<typename Derived>
struct BaseCRTP {void interface() { static_cast<Derived*>(this)->implementation(); }
};struct Impl : BaseCRTP<Impl> {void implementation() { /* ... */ }
};
优点:无虚拟表开销(编译期分派),适合性能敏感场景;缺点:不能在运行时基类指针上统一操作(不是运行时多态)。
常见坑与问答(FAQ)
- Q:覆写没有被调用?
A:可能没有把基类函数声明为virtual
,或发生了切片(by-value),或签名不同(const/ref/参数/返回类型不一致导致并非覆盖)。用override
可快速发现签名问题。 - Q:为什么 delete base_ptr 导致内存泄漏/未定义行为?
A:基类析构函数不是 virtual,导致派生析构未被调用。多态基类应有虚析构。 - Q:什么时候用虚继承?
A:只有在确实需要解决菱形重复基类子对象时使用;它有运行时成本,通常尽量避免复杂的多重继承设计。 - Q:继承还是组合?
A:如果是“is-a” 用继承;如果只是“has-a” 或复用实现首选组合(成员持有)—组合更灵活、耦合更低。 - Q:如何安全下转(downcast)?
A:使用dynamic_cast
(带多态基类)或设计更清晰的 API(避免运行时检查),谨慎使用static_cast
。
实战示例(菱形 + 构造参数)
#include <iostream>struct A {A(int v) : x(v) { std::cout<<"A("<<x<<")\n"; }int x;
};struct B : virtual A {B(int v) : A(v) {} // 无效?B 不能单独决定虚基的初始化(取决于 most-derived)
};struct C : virtual A {C(int v) : A(v) {}
};struct D : B, C {D(int v) : A(v), B(v), C(v) { // most-derived D 必须显式初始化虚基 Astd::cout<<"D\n";}
};int main(){D d(42);std::cout<<d.x<<"\n";
}
输出会显示 A(42)
只被构造一次(由 D
初始化),然后 D
的构造继续。