C++ 继承完全指南:从 is-a 关系到虚继承的底层真相
继承是面向对象编程的三大支柱之一。它让我们可以在已有类的基础上创建新类,实现代码复用和多态。但继承远不止 class Dog : public Animal 这么简单------public、protected、private 继承有什么区别?虚继承解决了什么问题?多继承有什么坑?底层又是怎么实现的?
今天,我们把这些一网打尽。
1. 什么是继承?为什么需要它?
继承的核心思想:基于已有的类创建新类,复用接口和实现。
cpp
// 没有继承:重复代码
class Dog {
std::string name;
int age;
public:
void eat() { /* ... */ }
void sleep() { /* ... */ }
void bark() { std::cout << "Woof!\n"; }
};
class Cat {
std::string name;
int age;
public:
void eat() { /* ... */ }
void sleep() { /* ... */ }
void meow() { std::cout << "Meow!\n"; }
};
cpp
// 有继承:公共部分提取到基类
class Animal {
protected:
std::string name;
int age;
public:
void eat() { std::cout << name << " is eating\n"; }
void sleep() { std::cout << name << " is sleeping\n"; }
};
class Dog : public Animal {
public:
void bark() { std::cout << name << " says Woof!\n"; }
};
class Cat : public Animal {
public:
void meow() { std::cout << name << " says Meow!\n"; }
};
关键原则 :public 继承表达 "is-a" 关系。Dog 是一个 Animal。如果不符合这个关系,就不应该用 public 继承。
2. 三种继承方式:public、protected、private
这是最容易混淆的点之一。基类成员的访问权限在经过不同继承方式后会发生改变:
cpp
class Base {
public:
int pub;
protected:
int prot;
private:
int priv; // 派生类永远无法直接访问
};
// public 继承:保持原有的访问级别
class DerivedPublic : public Base {
// pub 仍是 public
// prot 仍是 protected
// priv 不可访问
};
// protected 继承:public 降级为 protected
class DerivedProtected : protected Base {
// pub 变成 protected
// prot 仍是 protected
// priv 不可访问
};
// private 继承:全部变成 private
class DerivedPrivate : private Base {
// pub 变成 private
// prot 变成 private
// priv 不可访问
};
速查表:
| 基类成员 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
实践中:
- public 继承占 99%:表达 is-a 关系
- private 继承:相当于"用基类实现"(has-a 也可以用组合替代)
- protected 继承:极为罕见
3. 派生类的构造与析构
3.1 构造顺序
cpp
class Base {
public:
Base() { std::cout << "Base()\n"; }
Base(int x) { std::cout << "Base(" << x << ")\n"; }
};
class Member {
public:
Member() { std::cout << "Member()\n"; }
};
class Derived : public Base {
Member m;
public:
Derived() { std::cout << "Derived()\n"; }
Derived(int x) : Base(x) { std::cout << "Derived(" << x << ")\n"; }
};
Derived d;
// 输出:Base() → Member() → Derived()
// 构造顺序:基类 → 成员(按声明顺序)→ 派生类自身
Derived d2(10);
// 输出:Base(10) → Member() → Derived(10)
构造规则:
- 先构造基类部分,再构造成员,最后构造派生类自身
- 如果不显式调用基类构造,编译器调用基类的默认构造
- 如果基类没有默认构造,必须显式调用 :
Derived(int x) : Base(x) {},如果基类中没有默认无参构造,就直接不允许派生类对象的创建;
有这样一种说法:创建派生类对象时,先调用基类构造函数,再调用派生类构造函数,对吗?
错误 ,创建派生类对象,一定会先调用 派生类的构造函数,在此过程中会先去调用基类的构造
3.2 析构顺序
cpp
// 析构顺序严格与构造相反
// ~Derived() → ~Member() → ~Base()
当派生类析构函数执行完毕之后,会自动调用基类析构函数,完成基类部分的销毁。
记忆:创建一个对象,一定会马上调用自己的构造函数;一个对象被销毁,也一定会马上调用自己的析构函数。
重要 :基类的析构函数必须是虚的(后面详述)。
3.3 继承构造函数(C++11)
cpp
class Base {
public:
Base(int x) {}
Base(int x, double y) {}
Base(const std::string& s) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承基类的所有构造函数
};
Derived d1(42);
Derived d2(42, 3.14);
Derived d3("hello");
继承关系的局限性
- 创建、销毁的方式不能被继承 ------ 构造、析构
- 复制控制的方式不能被继承 ------ 拷贝构造、赋值运算符函数
- 空间分配的方式不能被继承 ------ operator new 、 operator delete友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)
4. 派生类中的名字隐藏
cpp
class Base {
public:
void func(int x) { std::cout << "Base::func(int)\n"; }
void func(double x) { std::cout << "Base::func(double)\n"; }
};
class Derived : public Base {
public:
void func() { std::cout << "Derived::func()\n"; }
// 这个 func 隐藏了基类的所有 func 重载!
};
Derived d;
d.func(); // OK:调用 Derived::func()
// d.func(10); // 错误!Derived::func() 隐藏了 Base::func(int)
// d.func(3.14); // 错误!Derived::func() 隐藏了 Base::func(double)
解决方法 :用 using 声明把基类重载引入派生类作用域:
cpp
class Derived : public Base {
public:
using Base::func; // 把 Base 的所有 func 重载引入
void func() { std::cout << "Derived::func()\n"; }
};
d.func(); // Derived::func()
d.func(10); // Base::func(int)
d.func(3.14); // Base::func(double)
5. 多态:虚函数与动态绑定
5.1 静态绑定 vs 动态绑定
cpp
class Base {
public:
void nonVirtual() { std::cout << "Base::nonVirtual\n"; }
virtual void virtFunc() { std::cout << "Base::virtFunc\n"; }
};
class Derived : public Base {
public:
void nonVirtual() { std::cout << "Derived::nonVirtual\n"; }
void virtFunc() override { std::cout << "Derived::virtFunc\n"; }
};
Derived d;
Base* p = &d;
p->nonVirtual(); // "Base::nonVirtual" ------ 静态绑定(看指针类型)
p->virtFunc(); // "Derived::virtFunc" ------ 动态绑定(看实际对象类型)
虚函数让基类指针/引用可以调用派生类的版本,这是多态的核心。
5.2 虚函数表原理
每个有虚函数的类都有一张虚函数表 ,对象通过虚指针指向它:
cpp
class Base {
public:
virtual void f() {}
virtual void g() {}
};
class Derived : public Base {
public:
void f() override {} // 重写 f
virtual void h() {} // 新增虚函数
};
内存布局大致如下:
Base 对象: Derived 对象:
┌──────────┐ ┌──────────┐
│ vptr │──→ vtable │ vptr │──→ vtable
├──────────┤ ┌──────┐ ├──────────┤ ┌──────────┐
│ 成员变量 │ │ &f │ │ 成员变量 │ │ &Derived::f │
└──────────┘ │ &g │ └──────────┘ │ &Base::g │
└──────┘ │ &Derived::h │
└──────────┘
调用虚函数的过程:
- 通过对象找到 vptr
- 通过 vptr 找到 vtable
- 在 vtable 中查找对应的函数地址
- 调用该函数
5.3 不要忘记虚析构函数
cpp
class Base {
public:
~Base() { std::cout << "~Base\n"; } // 非虚析构!
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() { delete[] data; std::cout << "~Derived\n"; }
};
Base* p = new Derived();
delete p; // 输出:~Base(Derived 的析构没调用,内存泄漏!)
任何可能被继承的类,析构函数都应该是虚的:
cpp
class Base {
public:
virtual ~Base() = default;
};
6. 抽象基类与纯虚函数
cpp
class Shape { // 抽象类:包含纯虚函数
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() = default; // 虚析构
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
void draw() const override { std::cout << "Drawing circle\n"; }
};
// Shape s; // 错误!不能实例化抽象类
Shape* s = new Circle(5.0); // 正确
纯虚函数可以有实现(很少用到):
cpp
class Base {
public:
virtual void interface() = 0;
};
void Base::interface() { // 提供默认实现
std::cout << "Default\n";
}
class Derived : public Base {
public:
void interface() override {
Base::interface(); // 调用基类的默认实现
std::cout << "Extended\n";
}
};
C++ 除了支持单继承外,还支持多重继承。那为什么要引入多重继承呢?其实是因为在客观现实世界中,我们经常碰到一个人身兼数职的情况,如在学校里,一个同学可能既是一个班的班长,又是学生中某个部门的部长;在创业公司中,某人既是软件研发部的 CTO ,又是财务部的 CFO ;一个人既是程序员,又是段子手。诸如此类的情况出现时,单一继承解决不了问题,就可以采用多基继承了。继承关系本质上是一个IS A的关系。

7. 多继承与菱形问题
7.1 多继承
cpp
class Flyable {
public:
virtual void fly() { std::cout << "Flying\n"; }
};
class Swimmable {
public:
virtual void swim() { std::cout << "Swimming\n"; }
};
class Duck : public Flyable, public Swimmable {
public:
void fly() override { std::cout << "Duck flying\n"; }
void swim() override { std::cout << "Duck swimming\n"; }
};
Duck d;
d.fly();
d.swim();
此结构下创建Duck类对象时,这三个类的构造函数调用顺序如何?
马上调用Duck类的构造函数,在此过程中会根据继承的声明顺序 ,依次调用Flyable和Swimmable的构造函数,创建出这三个类的基类子对象
Duck类对象销毁时,这四个类的析构函数调用顺序如何?
马上调用Duck类的析构函数,析构函数执行完后,按照继承的声明顺序的逆序,依次调用Swimmable和Flyable的析构函数

成员名访问冲突的二义性

解决成员名访问冲突的方法:加类作用域(不推荐)------ 应该尽量避免。
同时,如果D类中定义了同名的成员,可以对基类的这些成员造成隐藏效果,那么就可以直接通过成员名进行访问。
cpp
D d;
d.A::print();
d.B::print();
d.C::print();
d.print(); //ok
7.2 菱形继承问题
Animal
/ \
Mammal Bird
\ /
Bat(蝙蝠,既是哺乳动物又是鸟)
cpp
class Animal {
public:
int age;
virtual void eat() {}
};
class Mammal : public Animal {
public:
void feedMilk() {}
};
class Bird : public Animal {
public:
void layEggs() {}
};
class Bat : public Mammal, public Bird {
public:
// Bat 对象中有两份 Animal 的副本!
};
Bat bat;
// bat.age = 5; // 错误!歧义:哪个 Animal 的 age?
bat.Mammal::age = 5; // 必须这样指定
bat.Bird::age = 3;
菱形继承的问题:
- 数据冗余:两份
Animal副本 - 访问歧义:需要通过
Mammal::或Bird::指定
7.3 虚继承:解决菱形问题
cpp
class Animal {
public:
int age;
};
class Mammal : virtual public Animal { // 虚继承
public:
void feedMilk() {}
};
class Bird : virtual public Animal { // 虚继承
public:
void layEggs() {}
};
class Bat : public Mammal, public Bird {
public:
// 现在只有一份 Animal 副本
};
Bat bat;
bat.age = 5; // 没有歧义了!
虚继承的代价:
- 对象布局更复杂(通过虚基类指针访问共享的基类子对象)
- 访问虚基类成员有轻微性能开销
- 派生类必须负责初始化虚基类(不管有多少层)
采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。Mammal类和Bird类对象的内存布局中多出一个虚基类指针,位于所占内存空间的起始位置,同时继承自Animal类的内容被放在了这片空间的最后位置。Bat类对象中只会有一份Animal类的基类子对象。


cpp
// 虚继承下,最底层的 Bat 负责构造虚基类 Animal
class Bat : public Mammal, public Bird {
public:
Bat() : Animal(), Mammal(), Bird() {} // Bat 调用 Animal 的构造
// Mammal 和 Bird 对 Animal 的初始化被忽略
};
8. 阻止继承:final 关键字
cpp
class NoInherit final { // 这个类不能被继承
};
// class Try : public NoInherit {}; // 错误!
也可以阻止某个虚函数被进一步重写:
cpp
class Base {
public:
virtual void f() final; // 派生类不能重写 f
};
C++ 基类与派生类之间的向上转型、向下转型
示例代码:
cpp
#include <iostream>
using namespace std;
// 基类:动物
class Animal {
public:
virtual void speak() { // 虚函数,为多态和dynamic_cast做准备
cout << "动物叫" << endl;
}
virtual ~Animal() {} // 虚析构函数,防止内存泄漏
};
// 派生类:狗(继承自动物)
class Dog : public Animal {
public:
void speak() override { // 重写基类方法
cout << "汪汪汪" << endl;
}
// 派生类独有方法
void wagTail() {
cout << "狗摇尾巴" << endl;
}
};
向上转型(Upcasting):最安全、最常用
将派生类对象 转换为 基类类型(子类 → 父类)。
- 天然安全:派生类包含基类的所有成员,转型后不会访问到不存在的成员
- 隐式完成:不需要手动写转型语法,编译器自动帮你转
- 支持多态:调用虚函数时,会执行派生类的重写方法
cpp
int main() {
Dog dog; // 派生类对象
// 1. 隐式向上转型(最常用)
Animal* animalPtr = &dog; // 子类指针 → 父类指针
Animal& animalRef = dog; // 子类引用 → 父类引用
// 2. 多态生效:调用的是Dog的speak()
animalPtr->speak(); // 输出:汪汪汪
animalRef.speak(); // 输出:汪汪汪
return 0;
}
适用场景
- 函数参数接收基类指针/引用,传入派生类对象
- 统一管理继承体系的对象(如容器存储基类指针)
- 实现多态

向下转型(Downcasting):有风险、需谨慎
将基类类型 转换为 派生类类型(父类 → 子类)。
- 不安全:基类不包含派生类的独有成员,强行转型可能访问非法内存
- 必须显式转型:编译器不会自动转,需要手动用转型关键字
- 只有一种情况安全 :基类指针/引用原本就指向派生类对象
两种转型方式:static_cast vs dynamic_cast
static_cast 向下转型
特点
- 编译期检查:只检查语法,不检查实际指向的对象类型
- 无运行时安全校验:转型失败不会报错,直接触发未定义行为(崩溃、乱码)
- 无开销 :速度快
适用于你100%确定基类指针/引用原本就指向派生类对象。
cpp
int main() {
Dog dog;
Animal* animalPtr = &dog; // 向上转型(安全)
// static_cast 向下转型
Dog* dogPtr = static_cast<Dog*>(animalPtr);
if (dogPtr) { // 能转型成功
dogPtr->wagTail(); // 输出:狗摇尾巴(调用派生类独有方法)
}
// 危险用法:基类对象 转 派生类(编译不报错,运行崩溃!)
Animal animal;
Dog* badPtr = static_cast<Dog*>(&animal);
// badPtr->wagTail(); // 非法访问!程序崩溃
return 0;
}
dynamic_cast 向下转型
- 运行期检查:会检查实际指向的对象类型
- 安全 :转型失败返回
nullptr(指针)/ 抛异常(引用) - 有开销:需要遍历继承表,速度稍慢
- 强制要求 :基类必须有虚函数(否则编译报错)
适用于你不确定基类指针/引用到底指向哪个派生类对象(安全第一)。
cpp
int main() {
Animal* animalPtr;
Dog dog;
animalPtr = &dog;
// dynamic_cast 向下转型(安全)
Dog* dogPtr = dynamic_cast<Dog*>(animalPtr);
if (dogPtr) {
cout << "转型成功!" << endl;
dogPtr->wagTail(); // 输出:狗摇尾巴
} else {
cout << "转型失败!" << endl;
}
// 测试转型失败
Animal animal;
animalPtr = &animal;
Dog* badPtr = dynamic_cast<Dog*>(animalPtr);
if (badPtr) {
badPtr->wagTail();
} else {
cout << "转型失败!不是Dog对象" << endl; // 执行这里
}
return 0;
}
| 特性 | 向上转型 (Upcasting) | 向下转型 (Downcasting) |
|---|---|---|
| 转换方向 | 派生类 → 基类 | 基类 → 派生类 |
| 安全性 | 绝对安全 | 有风险,需校验 |
| 编译器处理 | 隐式自动转换 | 必须显式手动转换 |
| 常用关键字 | 无需关键字 | static_cast/dynamic_cast |
| 多态支持 | 天然支持 | 支持(需虚函数) |
| 核心风险 | 无 | 访问派生类独有成员崩溃 |
-
向下转型永远不要用 C 风格强转
比如
(Dog*)animalPtr,比static_cast更危险,完全无检查。 -
不确定对象类型 → 必用 dynamic_cast
这是工业级代码的标准写法,宁可多一点开销,也要避免崩溃。
-
dynamic_cast 必须搭配虚函数
基类没有虚函数,
dynamic_cast直接编译失败。 -
向上转型会丢失派生类独有成员
基类指针无法调用派生类的独有方法,必须向下转型后才能调用。
派生类之间的复制
我们有一个继承结构:
cpp
// 基类
class Base {
int baseData;
};
// 派生类
class Derived : public Base {
int deriveData; // 派生类自己的成员
};
派生类之间的复制,就是下面这两种操作:
- 复制构造 :
Derived d2 = d1; - 赋值重载 :
d2 = d1;
这两个操作,新手最容易犯的错误:只拷贝派生类成员,忘记拷贝基类成员!
如果你不给派生类写复制构造函数 和赋值运算符 ,编译器会自动生成,但它的行为是不完整的!
自动生成的复制构造函数
cpp
Derived(const Derived& other) {
// 编译器只会做一件事:拷贝派生类自己的成员
deriveData = other.deriveData;
// ❌ 坑点:基类成员 baseData 根本没被正确复制!
// 它只会调用基类的**默认构造函数**,而不是复制构造!
}
自动生成的赋值运算符
cpp
Derived& operator=(const Derived& other) {
deriveData = other.deriveData;
// ❌ 坑点:基类成员 baseData 完全没被复制!
return *this;
}
自动生成的复制行为 = 残缺复制
- 派生类成员:正常拷贝
- 基类成员:要么默认构造(丢数据),要么完全不复制
这就是为什么很多人继承后,对象复制总是少一半数据!
正确的派生类复制:必须显式调用基类复制
派生类复制的黄金法则:
先复制基类部分,再复制派生类部分。
1. 正确的复制构造函数
必须在初始化列表 中调用基类的复制构造函数!
cpp
// 正确写法
Derived(const Derived& other)
: Base(other) // ✅ 关键:先调用基类复制构造!
, deriveData(other.deriveData) // 再拷贝自己
{ }
Base(other):把派生类对象other传给基类复制构造,自动切割出基类部分完成复制- 顺序不能乱:基类永远先初始化
2. 正确的赋值运算符重载
必须在函数内部显式调用基类的赋值运算符!
cpp
// 正确写法
Derived& operator=(const Derived& other) {
if (this == &other) return *this; // 防止自赋值
Base::operator=(other); // ✅ 关键:调用基类赋值!
deriveData = other.deriveData; // 再赋值自己
return *this;
}
Base::operator=(other):手动调用父类的赋值函数,完成基类成员复制- 必须加
Base::,否则会递归调用自己
我们写一个完整代码,对比错误写法 和正确写法:
cpp
#include <iostream>
using namespace std;
// 基类
class Base {
protected:
int baseVal;
public:
Base(int val = 0) : baseVal(val) {
cout << "Base 构造" << endl;
}
// 基类复制构造
Base(const Base& other) : baseVal(other.baseVal) {
cout << "Base 复制构造" << endl;
}
// 基类赋值
Base& operator=(const Base& other) {
baseVal = other.baseVal;
cout << "Base 赋值" << endl;
return *this;
}
void showBase() { cout << "baseVal: " << baseVal << endl; }
};
// 派生类
class Derived : public Base {
private:
int derVal;
public:
Derived(int b, int d) : Base(b), derVal(d) {}
// ====================
// ✅ 正确的复制构造
// ====================
Derived(const Derived& other)
: Base(other) // 必须写!
, derVal(other.derVal)
{
cout << "Derived 复制构造" << endl;
}
// ====================
// ✅ 正确的赋值重载
// ====================
Derived& operator=(const Derived& other) {
if (this == &other) return *this;
Base::operator=(other); // 必须写!
derVal = other.derVal;
cout << "Derived 赋值" << endl;
return *this;
}
void showAll() {
showBase();
cout << "derVal: " << derVal << endl;
}
};
int main() {
Derived d1(10, 20);
cout << "--- d1 数据 ---" << endl;
d1.showAll();
cout << "\n--- 复制构造 d2 = d1 ---" << endl;
Derived d2 = d1;
d2.showAll();
cout << "\n--- 赋值 d3 = d1 ---" << endl;
Derived d3(0, 0);
d3 = d1;
d3.showAll();
return 0;
}
你可以清晰看到:基类先复制/赋值,派生类后执行。
- 派生类复制 = 基类复制 + 自身复制,缺一不可
- 复制构造:在初始化列表 调用
Base(other) - 赋值重载:在函数内调用
Base::operator=(other)
只要遵循这个规则,派生类复制永远不会出错!
9. 面试常考清单
9.1 public、protected、private 继承的区别?
答案要点 :改变基类成员在派生类中的访问级别。public 继承保持原级别,protected 继承将 public 变为 protected,private 继承将 public 和 protected 都变为 private。public 继承占绝大多数,表达 is-a 关系。
9.2 派生类的构造和析构顺序?
答案要点:
- 构造:基类 → 成员(声明顺序)→ 派生类自身
- 析构:严格相反:派生类自身 → 成员(逆声明顺序)→ 基类
9.3 为什么基类析构函数需要是虚的?
答案要点 :如果基类析构不是虚的,通过基类指针 delete 派生类对象时,只会调用基类析构函数,派生类部分不会被正确清理,导致资源泄漏。
9.4 什么是虚函数表?它是如何实现多态的?
答案要点:每个有虚函数的类有一张虚函数表,存储虚函数地址。对象通过虚指针指向虚表。调用虚函数时,运行时通过虚指针找到虚表中正确的函数地址并调用,实现动态绑定。
9.5 什么是菱形继承?如何解决?
答案要点 :派生类通过两条路径继承同一个基类,导致基类子对象重复。用虚继承 解决,中间类用 virtual public 继承基类,保证最底层的派生类中只有一份基类子对象。
9.6 重载(Overload)、重写(Override)、隐藏(Hide)的区别?
答案要点:
- 重载:同一作用域,同名函数,参数列表不同
- 重写:派生类覆盖基类虚函数,签名相同
- 隐藏:派生类名字隐藏基类名字(即使签名不同),用
using引入
9.7 纯虚函数是什么?抽象类有什么用?
答案要点 :virtual void f() = 0 是纯虚函数。包含纯虚函数的类是抽象类,不能实例化。抽象类定义接口契约,派生类必须实现。
9.8 继承和组合如何选择?
答案要点 :继承表达 is-a 关系,组合表达 has-a 关系。优先使用组合,因为耦合度更低、更灵活。需要多态和统一操作时才用继承。
9.9 final 关键字有什么用?
答案要点 :final 修饰类阻止被继承,修饰虚函数阻止被进一步重写。提高代码安全性,帮助编译器优化(去虚拟化)。
10. 最佳实践总结
- public 继承表达 is-a:不符合就别用
- 析构函数虚到底:只要类可能被继承,析构函数就是虚的
- 用 override 关键字:编译器帮你检查是否真的重写了
- 优先组合而非继承:组合更灵活、耦合更低
- 谨慎使用多继承:只在确实需要多接口时使用
- 菱形继承用虚继承:但尽量避免菱形继承本身
- 抽象基类定义接口:纯虚函数 + 虚析构 = 完美的接口
- using 解决名字隐藏:把基类的重载版本引入派生类作用域
继承是强大的工具,但强大的工具需要谨慎使用。用继承表达清晰的层次关系,用虚函数支持多态扩展,用组合保持灵活松耦------这就是 C++ 继承之道的精髓。