继承和多态是 C++ 的灵魂,也是很多初学者的噩梦。你可能背过"父类指针指向子类对象",但你真的理解编译器背后做了什么吗? 这篇文章不仅讲怎么用,更讲为什么。 我们将从最基础的定义开始,一层层剥开 C++ 的外衣,直抵内存深处。
目录
[2.1 定义格式](#2.1 定义格式)
[2.2 继承方式与访问属性变化](#2.2 继承方式与访问属性变化)
[5.1 隐藏规则](#5.1 隐藏规则)
[5.2 继承作用域相关的典型考点](#5.2 继承作用域相关的典型考点)
[6.1 4个常见默认成员函数](#6.1 4个常见默认成员函数)
[7.1 C++98 写法:构造函数设为 private](#7.1 C++98 写法:构造函数设为 private)
[7.2 C++11 写法:final 关键字](#7.2 C++11 写法:final 关键字)
[10.1 多继承与菱形继承](#10.1 多继承与菱形继承)
[10.2 虚继承](#10.2 虚继承)
[10.3 多继承中的指针偏移问题](#10.3 多继承中的指针偏移问题)
[10.4 IO 库中的菱形虚继承](#10.4 IO 库中的菱形虚继承)
[11.1 继承和组合](#11.1 继承和组合)
[11.2 继承与组合的例子](#11.2 继承与组合的例子)
[14.1 虚函数](#14.1 虚函数)
[14.2 虚函数的重写](#14.2 虚函数的重写)
[十八、override 和 final 关键字](#十八、override 和 final 关键字)
[21.1 虚函数表指针](#21.1 虚函数表指针)
[21.2 虚函数表与动态绑定](#21.2 虚函数表与动态绑定)
[21.3 虚函数表](#21.3 虚函数表)
一、继承的概念
继承(inheritance) 是 C++ 面向对象中实现"类级别代码复用"的关键机制。它允许你在一个已有类(基类)的基础上,扩展出一个新类(派生类),复用原有成员,再加上自己的成员。
先看一个没用继承的写法:同时表示学生和老师。
cpp
class Student
{
public:
void identity()
{
//身份认证逻辑
}
void study()
{
//学习
}
protected:
std::string _name; //姓名
std::string _address; //地址
std::string _tel; //电话
int _age; //年龄
int _stuid; //学号
};
class Teacher
{
public:
void identity()
{
//身份认证逻辑
}
void teaching()
{
//授课
}
protected:
std::string _name; //姓名
int _age; //年龄
std::string _address; //地址
std::string _tel; //电话
std::string _title; //职称
};
问题:
-
identity重复; -
姓名/地址/电话/年龄等字段也重复;
-
一旦认证规则改了,要改两处。
下面我们公共的成员都放到Person类中,Student和teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦:
cpp
class Person
{
public:
void identity()
{
std::cout << "void identity() " << _name << std::endl;
}
protected:
std::string _name = "张三"; //姓名
std::string _address; //地址
std::string _tel; //电话
int _age = 18; //年龄
};
class Student : public Person
{
public:
void study()
{
//学习
}
protected:
int _stuid;//学号
};
class Teacher : public Person
{
public:
void teaching()
{
//授课
}
protected:
std::string _title;//职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
-
学生是人;
-
老师是人;
-
公共属性放在
Person里,派生类只扩展自己的部分。
cpp
classDiagram
class Person{
string _name
string _address
string _tel
int _age
identity()
}
class Student{
int _stuid
study()
}
class Teacher{
string _title
teaching()
}
Person <-- Student
Person <-- Teacher
二、继承的定义
2.1 定义格式
下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)


继承的基本语法:
cpp
class 派生类名 : 继承方式 基类名
{
//新增成员
};
例如:
cpp
class Student : public Person
{
public:
int _stuid;//学号
int _major;//专业
};
名词对应:
-
基类/父类:
Person; -
派生类/子类:
Student; -
继承方式:
public、protected、private。
2.2 继承方式与访问属性变化
三种继承方式,会改变"基类成员在派生类中的访问级别"(注意:只影响访问级别,不影响对象里是否有那块内存)。
| 基类成员原属性 | public 继承后 | protected 继承后 | private 继承后 |
|---|---|---|---|
| 基类的 public 成员 | 变成 public | 变成 protected | 变成 private |
| 基类的 protected 成员 | 变成 protected | 变成 protected | 变成 private |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
总结:
派生类中一个成员的最终访问级别 = Min(该成员在基类中的访问限定符, 继承方式)
排序规则:
public > protected > private。
要点:
-
基类的
private成员在派生类中语法不可访问,但在对象里仍然有那块内存; -
需要"类外不能访问,但派生类能访问"的成员,就应该放到
protected里; -
class默认继承方式是private,struct默认是public; -
实际编码中大多 采用public 继承。
三、继承类模板
继承同样可以作用在类模板上,比如基于std::vector<T>封装一个简单的stack:
cpp
namespace bit
{
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
std::vector<T>::push_back(x);
}
void pop()
{
std::vector<T>::pop_back();
}
const T& top()
{
return std::vector<T>::back();
}
bool empty()
{
return std::vector<T>::empty();
}
};
}
int main()
{
bit::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
std::cout << st.top() << " ";
st.pop();
}
return 0;
}
模板细节:
-
模板是按需实例化的;
-
基类是类模板时,在派生类模板中访问基类成员,编译器不一定能推断;
-
所以在模板中调用基类模板成员时,最好写成
std::vector<T>::push_back(x)这种带类名限定的形式。
四、基类和派生类间的转换
cpp
class Person
{
protected:
std::string _name;//姓名
std::string _sex;//性别
int _age;//年龄
};
class Student : public Person
{
public:
int _No;//学号
};
int main()
{
Student sobj;
//1.派生类对象可以转换为基类
Person* pp = &sobj;//向上转型
Person& rp = sobj;//向上转型
Person pobj = sobj;//对象切片,只拷贝Person那部分
//2.基类对象不能隐式转换为派生类对象
//sobj = pobj;//错误,派生类多出来的信息没法填
return 0;
}
规则:
-
public继承下,派生类对象可以隐式转换为基类对象/指针/引用(向上转型); -
反过来不行,因为基类不包含派生类扩展的那部分信息;
-
基类指针如果实际上指向派生类对象,可以用强转转回来:
-
C 风格
(Student*)pp很危险; -
多态场景下应优先使用
dynamic_cast<Student*>(pp)做运行时检查。
-

五、继承中的作用域
5.1 隐藏规则
继承体系中有两个独立的作用域:基类作用域和派生类作用域。同名成员会发生隐藏。
规则:
-
基类和派生类的作用域彼此独立;
-
在派生类中定义了与基类同名的成员(变量或者函数),则派生类的同名成员会隐藏基类成员;
-
对函数来说,只要名字相同就隐藏,不看参数列表;
-
想在派生类中访问被隐藏的基类成员,需要写成基类名::成员名;
-
实际编码中尽量避免在继承体系里大量使用同名成员,容易搞混。
示例:
cpp
class Person
{
protected:
std::string _name = "好评"; //姓名
int _num = 111; //身份证号
};
class Student : public Person
{
public:
void Print()
{
std::cout << "姓名:" << _name << std::endl;
std::cout << "身份证号:" << Person::_num << std::endl;
std::cout << "学号:" << _num << std::endl;
}
protected:
int _num = 999;//学号
};
int main()
{
Student s1;
s1.Print();
return 0;
}
这里:
-
Person::_num表示身份证号; -
Student::_num表示学号; -
在
Student作用域中直接写_num访问的是学号,身份证那一份被隐藏了。
5.2 继承作用域相关的典型考点
例1:两个fun是什么关系?
cpp
class A
{
public:
void fun()
{
std::cout << "func()" << std::endl;
}
};
class B : public A
{
public:
void fun(int i)
{
std::cout << "func(int i) " << i << std::endl;
}
};
A::fun()和B::fun(int):
-
不在同一作用域;
-
名字相同;
-
参数列表不同;
它们之间的关系是隐藏,不是重载。
例 2:下面程序结果?
cpp
int main()
{
B b;
b.fun(10);
b.fun();
return 0;
}
答案:编译错误。
原因:
-
在
B的作用域中,A::fun被B::fun(int)隐藏了; -
名字查找只看到
B::fun(int); -
调用
b.fun()时,没有无参版本匹配,编译失败。
如果既想保留A::fun()又要在B中增加fun(int),可以:
cpp
class B : public A
{
public:
using A::fun;//把A中的fun引入当前作用域
void fun(int i)
{
std::cout << "func(int i) " << i << std::endl;
}
};
六、派生类的默认成员函数
派生类也有那几大默认函数:构造、拷贝构造、赋值、析构等。在继承体系中它们有统一的调用顺序。
6.1 4个常见默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成---个,那么在派生类中,这几个成员函数是如何生成的呢?
- 派生类的构造函数 必须调用基类的构造函数初始化基类的那一部分成员。如果基类 没有默认的构造****函数 ,则必须在 派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数 必须调用基类 的拷贝构造 完成基类 的拷贝初始化。
- 派生类的operator=必须要调用基类的 operator=完成基类 的复制。需要注意的是派生类的operator=隐藏了基类 的operator=,所以显示调用基类 的operator=,需要指定基类作用域
- 派生类的析构函数 会在被调用完成后用自动调用基类 的析构函数清理基类 成员。因为这样才能保证派生****类 对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造 再调派生类构造。
- 派生类对象析构清理 先调用派生类 析构再调基类 的析构。
- 因为多态中一些场景析构函数 需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数 名进行特殊处理,处理成destructor(),所以基类析构函数 不加virtual的情况下,派生类析构函 数和基类析构函数 构成隐藏关系。


cpp
class Person
{
public:
Person(const char* name = "peter")
: _name(name)
{
std::cout << "Person()" << std::endl;
}
Person(const Person& p)
: _name(p._name)
{
std::cout << "Person(const Person& p)" << std::endl;
}
Person& operator=(const Person& p)
{
std::cout << "Person operator=(const Person& p)" << std::endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
protected:
std::string _name;//姓名
};
class Student : public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{
std::cout << "Student()" << std::endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
std::cout << "Student(const Student& s)" << std::endl;
}
Student& operator=(const Student& s)
{
std::cout << "Student& operator=(const Student& s)" << std::endl;
if (this != &s)
{
Person::operator=(s);//显式调用基类的=
_num = s._num;
}
return *this;
}
~Student()
{
std::cout << "~Student()" << std::endl;
}
protected:
int _num;//学号
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
总结:
-
构造顺序:先构造基类子对象,再构造派生类自己的成员;
-
拷贝构造:先调用基类拷贝构造,再拷贝派生类成员;
-
赋值运算符 :先调用基类
operator=,再赋值派生类成员; -
析构顺序:先析构派生类,再析构基类(栈式反向);
-
派生类的
operator=会隐藏基类的operator=,所以要显式写Person::operator=(s)。
七、实现一个不能被继承的类
有些类希望"只能被使用,不能被继承",例如工具类、单例类等。常见做法有两种。
7.1 C++98 写法:构造函数设为 private
cpp
class Base
{
private:
Base()
{}
};
-
派生类构造函数必须先调用基类构造;
-
但基类构造是
private,派生类调用不到; -
语法上可以写
class Derive : public Base {};,但无法构造派生类对象。
7.2 C++11 写法:final 关键字
C++11 提供了final关键字,直接在类定义后标记:
cpp
class Base final
{
public:
void func5()
{
std::cout << "Base::func5" << std::endl;
}
protected:
int a = 1;
};
class Derive : public Base
{
public:
void func4()
{
std::cout << "Derive::func4" << std::endl;
}
protected:
int b = 2;
};
从Base继承会直接编译失败,因为Base被标记为final,无法被继承。
八、继承与友元
**友元关系不会自动继承。**基类的友元不是派生类的友元。
cpp
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
std::string _name;//姓名
};
class Student : public Person
{
protected:
int _stuNum;//学号
};
void Display(const Person& p, const Student& s)
{
std::cout << p._name << std::endl;
std::cout << s._stuNum << std::endl;
}
想让Display既能访问Person的内部成员,又能访问Student的内部成员,规范的做法是:
-
在
Person中声明friend void Display(const Person&, const Student&);; -
在
Student中也声明同样的friend。
九、继承与静态成员
静态成员在整个继承体系中只有一份,是全类共享的。
cpp
class Person
{
public:
std::string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
int main()
{
Person p;
Student s;
//访问静态成员的两种写法,本质是同一块内存
Person::_count = 10;
Student::_count = 20;
std::cout << Person::_count << std::endl;//20
std::cout << Student::_count << std::endl;//20
return 0;
}
要点:
-
静态成员属于类,而不是具体哪个对象;
-
在 public 继承下,可以用
Person::_count和Student::_count两种形式访问同一份静态成员。
十、多继承及其菱形继承问题
10.1 多继承与菱形继承
多继承 :一个类同时继承多个直接基类。对象布局通常是"基类1子对象 + 基类2子对象 + ... + 派生类成员"。
典型菱形结构:




cpp
class Person
{
public:
std::string _name;//姓名
};
class Student : public Person
{
protected:
int _num;//学号
};
class Teacher : public Person
{
protected:
int _id;//职工编号
};
class Assistant : public Student, public Teacher
{
protected:
std::string _majorCourse;//主修课程
};
Assistant对象中会有两份Person子对象:一份来自Student,一份来自Teacher。
cpp
int main()
{
Assistant a;
//错误:_name不明确,既可以来自Student,也可以来自Teacher
//a._name = "peter";
a.Student::_name = "peter";
a.Teacher::_name = "jack";
return 0;
}
问题:
-
数据冗余:
_name有两份; -
使用有二义性:
a._name不明确,必须带上类域。
这就是绝大部分人不推荐菱形继承的原因。
10.2 虚继承
为了解决菱形继承中"同一基类多份"的问题,C++ 提供了虚继承(virtual inheritance)。
cpp
class Person
{
public:
std::string _name;//姓名
};
//虚继承Person
class Student : virtual public Person
{
protected:
int _num;//学号
};
//虚继承Person
class Teacher : virtual public Person
{
protected:
int _id;//职工编号
};
class Assistant : public Student, public Teacher
{
protected:
std::string _majorCourse;//主修课程
};
int main()
{
Assistant a;
a._name = "peter";//现在只有一份Person子对象,不再二义
return 0;
}
效果:
-
对每条虚继承链,最终派生类对象中只有一份虚基类子对象;
-
避免数据冗余和访问二义性。
代价:
-
对象布局更复杂;
-
构造函数初始化顺序更绕;
-
一般只在库级基础设施里使用,业务代码中尽量避免。
示例:
cpp
class Person
{
public:
Person(const char* name)
: _name(name)
{}
std::string _name;//姓名
};
class Student : virtual public Person
{
public:
Student(const char* name, int num)
: Person(name)
, _num(num)
{}
protected:
int _num;//学号
};
class Teacher : virtual public Person
{
public:
Teacher(const char* name, int id)
: Person(name)
, _id(id)
{}
protected:
int _id;//职工编号
};
class Assistant : public Student, public Teacher
{
public:
Assistant(const char* name1, const char* name2, const char* name3)
: Person(name3)
, Student(name1, 1)
, Teacher(name2, 2)
{}
protected:
std::string _majorCourse;//主修课程
};
int main()
{
Assistant a("张三", "李四", "王五");
//最终Person那一份名字会被构造成"王五"
return 0;
}
10.3 多继承中的指针偏移问题
多继承下,不同基类子对象在派生类对象中位置不同,因此把派生类对象地址转成不同基类指针时,会产生偏移。
cpp
class Base1
{
public:
int _b1;
};
class Base2
{
public:
int _b2;
};
class Derive : public Base1, public Base2
{
public:
int _d;
};
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
//通常:p1 == p3,p2 == p3 + sizeof(Base1)
return 0;
}
-
Base1子对象一般位于Derive对象开头,所以p1和p3相等; -
Base2子对象在Base1之后,因此p2在地址上相对p3有偏移。
10.4 IO 库中的菱形虚继承

-
顶层有一个
ios_base; -
basic_ios从ios_base继承; -
basic_istream和basic_ostream都虚继承自basic_ios; -
basic_iostream多继承自basic_istream和basic_ostream; -
因为是虚继承,
basic_iostream对象里只会有一份basic_ios子对象。
十一、继承和组合
11.1 继承和组合
两种最常见的代码复用关系:
-
继承(inheritance) :is-a 关系
-
Student是一种Person; -
Benz是一种Car; -
常和多态一起出现。
-
-
组合(composition) :has-a 关系
-
Car里有 4 个Tire; -
stack内部有一个vector做底层存储。
-
从封装和耦合角度看:
-
继承属于白箱复用:
-
派生类能看到基类大部分实现细节;
-
基类实现变动会把派生类一起拖下水;
-
耦合高,封装性相对差一些。
-
-
组合属于黑箱复用:
-
只通过接口和被组合对象交互;
-
内部实现对外隐藏;
-
耦合更低,更利于维护。
-
总结:
能用组合解决的,就尽量用组合;只有在天然 is-a 且需要多态时,再考虑继承。
11.2 继承与组合的例子
轮胎和车:很明显是 has-a 关系。
cpp
class Tire
{
protected:
std::string _brand = "Michelin";//品牌
size_t _size = 17;//尺寸
};
class Car
{
protected:
std::string _colour = "白色";//颜色
std::string _num = "陕ABIT00";//车牌号
Tire _t1;
Tire _t2;
Tire _t3;
Tire _t4;
};
具体车型和车:很自然是 is-a 关系。
cpp
class BMW : public Car
{
public:
void Drive()
{
std::cout << "好开-操控" << std::endl;
}
};
class Benz : public Car
{
public:
void Drive()
{
std::cout << "好坐-舒适" << std::endl;
}
};
vector与stack:
cpp
template<class T>
class vector
{};
//is-a写法:stack继承vector
template<class T>
class stack_is_a : public vector<T>
{};
//has-a写法:stack里组合一个vector
template<class T>
class stack_has_a
{
public:
vector<T> _v;
};
从语义上讲,stack 更像"内部用某种顺序容器实现"的抽象,不是"某个特殊的 vector",所以组合更合适。
十二、多态的概念
多态(polymorphism) 字面意思就是多种形态。
从发生阶段看:
-
编译时多态(静态多态):函数重载、函数模板;
- 根据参数类型/个数不同,在编译期决定调用哪个函数;
-
运行时多态(动态多态):虚函数+基类指针/引用;
- 调用哪个版本在运行时决定。
这里重点讲运行时多态。
例子:买票。
-
每个人都执行
BuyTicket(); -
普通人:全价;
-
学生:打折;
-
军人:优先窗口;
-
函数名一样,行为随对象类型改变,这就是多态。
例子:动物叫:
-
Animal定义接口talk(); -
猫重写后输出"喵",狗重写后输出"汪",接口统一,行为不同。

十三、多态的构成条件
要形成运行时多态,必须满足三个条件:
-
存在继承关系 (例如
Student继承Person); -
基类中有虚函数,派生类对其重写;
-
通过基类指针或引用调用虚函数。
少任何一条都不是动态多态。

示例:买票。
cpp
class Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-全价" << std::endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-打折" << std::endl;
}
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);//买票-全价
Func(&st);//买票-打折
return 0;
}
十四、虚函数与重写
14.1 虚函数
在成员函数前加virtual,这个成员函数就变成虚函数:
cpp
class Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-全价" << std::endl;
}
};
注意:
-
只有成员函数可以是虚函数;
-
虚属性会被派生类继承,即使派生类省略
virtual关键字,该函数仍然是虚函数。
14.2 虚函数的重写
重写(override) 的条件:
基类中有虚函数
virtual R f(Args...),派生类中也有R f(Args...),参数列表完全相同,即重写。
示例:动物叫。
cpp
class Animal
{
public:
virtual void talk() const
{}
};
class Dog : public Animal
{
public:
virtual void talk() const
{
std::cout << "汪汪" << std::endl;
}
};
class Cat : public Animal
{
public:
virtual void talk() const
{
std::cout << "(>^ω^<)喵" << std::endl;
}
};
void letsHear(const Animal& animal)
{
animal.talk();
}
int main()
{
Cat cat;
Dog dog;
letsHear(cat);
letsHear(dog);
return 0;
}
-
letsHear只知道拿到一个Animal&; -
调用
talk()时,通过虚函数机制,根据实际对象类型选择对应版本。
十五、默认参数与虚函数
多态场景的选择题:
以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
cpp
class A
{
public:
virtual void func(int val = 1)
{
std::cout << "A->" << val << std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout << "B->" << val << std::endl;
}
};
int main()
{
B* p = new B;
p->test();
delete p;
return 0;
}
输出是:B: B -> 1。
原因:
-
虚函数调用的动态绑定发生在运行时;
-
默认参数的选择发生在编译时,依据的是"静态类型"。
想象你去吃汉堡:
-
优惠券(编译时的类型 A) :你手里拿着一张由A公司 印发的优惠券(代码里
test()函数是在A类里写的)。这张优惠券上写着一行小字:"默认赠送 1 包番茄酱"。 -
厨师(运行时的对象 B) :真正给你做汉堡的厨师是B师傅 (代码里
new B)。B师傅平时的习惯是"默认赠送 0 包番茄酱"。
冲突发生了: 当你拿着 A公司的优惠券给 B师傅看时:
-
谁做汉堡? 当然是 B师傅 做(因为是
virtual虚函数,看实际对象)。 -
给几包酱? 必须按 优惠券 上写的来(默认参数是编译时决定的,看纸上写了啥)。
结果 :B师傅做了汉堡(打印 B->),但是被迫按优惠券给了 1 包酱(打印 1)。
十六、协变
协变(covariant) 返回类型指的是:基类虚函数返回"基类指针/引用",派生类重写时返回"派生类指针/引用"。
示例:
cpp
class A {};
class B : public A {};
class Person
{
public:
virtual A* BuyTicket()
{
std::cout << "买票-全价" << std::endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
std::cout << "买票-打折" << std::endl;
return nullptr;
}
};
这种写法在语义上仍然算重写,C++ 允许这种"协变"返回。
实际业务中不常用,了解就行。
十七、虚析构函数与多态销毁
多态最容易翻车的地方:用基类指针delete派生类对象。
cpp
class A
{
public:
virtual ~A()
{
std::cout << "~A()" << std::endl;
}
};
class B : public A
{
public:
~B()
{
std::cout << "~B()->delete:" << _p << std::endl;
delete[] _p;
}
protected:
int* _p = new int[10];
};
int main()
{
A* p1 = new A;
A* p2 = new B;
delete p1;//调用A::~A
delete p2;//先调用B::~B,再调用A::~A
return 0;
}
如果把A的析构函数上的virtual删掉,那么:
cpp
A* p2 = new B;
delete p2;//只会调用A::~A,不会调用B::~B,数组泄漏
因此:
-
凡是"打算当多态基类使用"的类,都应该把析构函数声明为
virtual; -
确保delete 基类指针时能正确析构派生类对象。
十八、override 和 final 关键字
默认情况下,如果派生类函数"看起来像是重写",实则签名不一致,编译器不会报错,只会当作隐藏。这很容易埋坑。
C++11 提供了两个关键字:
-
override:表示"我就是要重写基类虚函数",如果没成功重写,则编译报错; -
final:表示"这个虚函数到我这里为止",派生类不能再重写。
示例一:用override查拼写错误。
cpp
class Car
{
public:
virtual void Dirve()//故意拼错
{}
};
class Benz : public Car
{
public:
virtual void Drive() override
{
std::cout << "Benz-舒适" << std::endl;
}
};
编译器会报类似"带 override 却没有重写任何基类方法"的错误,暴露出Dirve的拼写问题。
示例二:用final禁止重写。
cpp
class Car
{
public:
virtual void Drive() final {}
};
class Benz : public Car
{
public:
virtual void Drive()
{
std::cout << "Benz-舒适" << std::endl;
}
};
这会直接编译失败,因为试图重写一个被final修饰的虚函数。
推荐习惯:
-
只要是"故意要重写"的虚函数,派生类都加上
override; -
某个虚函数不希望再被重写时,加上
final。
十九、重载、重写与隐藏对比

二十、纯虚函数与抽象类
在虚函数声明后写**= 0** ,就变成纯虚函数(pure virtual)。
cpp
class Car
{
public:
virtual void Drive() = 0;
};
特性:
-
纯虚函数可以只声明不实现;
-
包含纯虚函数的类称为抽象类(abstract class);
-
抽象类不能实例化对象;
-
派生类如果没有重写完所有纯虚函数,它自己也是抽象类。
例子:
cpp
class Car
{
public:
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
std::cout << "Benz-舒适" << std::endl;
}
};
class BMW : public Car
{
public:
virtual void Drive()
{
std::cout << "BMW-操控" << std::endl;
}
};
int main()
{
//Car car;//错误,抽象类不能实例化
Car* pBenz = new Benz;
pBenz->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
delete pBenz;
delete pBMW;
return 0;
}
抽象类常用来做"接口类":
-
提供一组纯虚函数,规定"要做什么";
-
具体"怎么做"由派生类去实现。
二十一、多态的原理
21.1 虚函数表指针
下⾯编译为32位程序的运行结果是什么()
A. 编译报错 B. 运行报错 C. 8 D. 12
cpp
class Base
{
public:
virtual void Func1()
{
std::cout << "Func1()" << std::endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
std::cout << sizeof(b) << std::endl;
return 0;
}
答案是D.12:
-
一个
int4 字节; -
一个
char加上对齐填充; -
再加上一个隐藏的虚函数表指针 vptr 4 字节。
这个 vptr 就是多态的关键。

21.2 虚函数表与动态绑定
买票例子:
cpp
class Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-全价" << std::endl;
}
private:
std::string _name;
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-打折" << std::endl;
}
private:
std::string _id;
};
class Soldier : public Person
{
public:
virtual void BuyTicket()
{
std::cout << "买票-优先" << std::endl;
}
private:
std::string _codename;
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}
底层大致这样:
-
每个含虚函数的类对应一张虚函数表(vtable) ,里面存放该类所有虚函数的地址;
-
每个对象中有一个 vptr 指向对应类型的虚表;
-
调用
ptr->BuyTicket()时:-
从对象中取出 vptr;
-
在虚表中按固定偏移找到
BuyTicket对应的函数指针; -
跳转到该函数地址执行。
-
因此:
-
ptr指向Person对象→调用Person::BuyTicket; -
指向
Student对象→调用Student::BuyTicket; -
指向
Soldier对象→调用Soldier::BuyTicket。
这就是动态绑定(dynamic binding)。
21.3 虚函数表
cpp
class Base
{
public:
virtual void func1()
{
std::cout << "Base::func1" << std::endl;
}
virtual void func2()
{
std::cout << "Base::func2" << std::endl;
}
void func5()
{
std::cout << "Base::func5" << std::endl;
}
protected:
int a = 1;
};
class Derive : public Base
{
public:
virtual void func1()
{
std::cout << "Derive::func1" << std::endl;
}
virtual void func3()
{
std::cout << "Derive::func3" << std::endl;
}
void func4()
{
std::cout << "Derive::func4" << std::endl;
}
protected:
int b = 2;
};
通常:
-
Base的虚表类似[&Base::func1, &Base::func2]; -
Derive的虚表类似[&Derive::func1, &Base::func2, &Derive::func3]; -
func4和func5因为不是虚函数,不在虚表中。


cpp
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Base虚表地址:%p\n", *(void**)p3);
printf("Derive虚表地址:%p\n", *(void**)p4);
printf("虚函数地址:%p\n", (void*)&Base::func1);
printf("普通函数地址:%p\n", (void*)&Base::func5);
delete p1;
return 0;
}
可以观察到:
-
栈、堆、静态区、常量区的地址大致位于不同区间;
-
虚表指针
*(void**)p3和*(void**)p4指向的区域,通常与代码/常量区比较接近; -
虚函数和普通函数的地址都落在代码段;
-
虚表本质上就是一个"函数指针数组"。
完