目录
1.唠唠啥是多态(概念篇)
2.多态咋实现?(定义与实现篇)
3.抽象类是个啥?(抽象类篇)
4.多态背后的小秘密(原理篇)
5.单继承 / 多继承的虚表那些事(虚表实战篇)
6.面试常考!继承多态高频题(面试保命篇)
注意:
本篇笔记基于 VS2013 x86 环境整理,该环境下指针占 4 个字节;如果是 x64 环境,指针占 8 个字节,部分代码需要对应修改,学习时注意环境差异哦~
1. 唠唠啥是多态(概念篇)
1.1 通俗理解多态
多态,简单说就是同一个行为,不同对象去做,结果不一样,也就是 “多种形态”。
举几个新手能秒懂的例子:
- 买票行为:普通人全价买,学生半价买,军人优先买,同样是 “买票”,不同人执行结果不同;
- 支付宝扫红包:新用户能扫到大红包,老用户只能扫到小红包,同样是 “扫码”,不同用户结果不同。
1.2 编程中的多态
在 C++ 继承关系中,不同继承类的对象,调用同一个函数,产生不同的执行效果,这就是编程中的多态。比如 Student 类继承 Person 类,Person 对象调用 BuyTicket () 是全价,Student 对象调用 BuyTicket () 是半价。
2. 多态咋实现?(定义与实现篇)
想要实现多态,可不是随便写就行,得满足继承关系+两个核心条件,还得搞懂虚函数、重写这些基础概念,新手一步一步来~
2.1 多态的 3 个构成条件
1.存在继承关系(派生类继承基类);
2.必须通过基类的指针或者引用调用虚函数(记死:对象直接调用不行,必须指针 / 引用!);
3.被调用的函数是虚函数,且派生类重写了基类的这个虚函数。
2.2 虚函数:多态的 “开关”
**被 virtual 关键字修饰的类成员函数,就是虚函数,**这是实现多态的基础,没有虚函数就没有多态。
#include <iostream>
using namespace std;
// 基类Person
class Person {
public:
// 虚函数:买票全价
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
注意:virtual 只需要在函数声明时加,类外定义时不用加~
2.3 虚函数的重写:派生类的 “定制化”
虚函数的重写(也叫覆盖),简单说就是派生类写一个和基类虚函数 “一模一样” 的函数,把基类的虚函数功能替换掉。
3 个 “一模一样” 的要求
派生类虚函数和基类虚函数的返回值类型、函数名字、参数列表必须完全相同,少一个都不行!
// 派生类Student继承Person
class Student : public Person {
public:
// 重写基类的虚函数BuyTicket
virtual void BuyTicket() {
cout << "买票-半价" << endl;
}
};
// 测试多态:用基类引用调用虚函数
void Func(Person& p) {
p.BuyTicket(); // 谁调用,执行谁的重写函数
}
int main() {
Person mike; // 基类对象
Student johnson; // 派生类对象
Func(mike); // 执行Person::BuyTicket,输出:买票-全价
Func(johnson); // 执行Student::BuyTicket,输出:买票-半价
return 0;
}
新手注意点
派生类重写基类虚函数时,就算不加 virtual 关键字,也能构成重写(因为继承后基类虚函数的属性被保留了),但非常不建议这么写,代码可读性差,新手一定要养成加 virtual 的习惯!
重写的两个 “例外情况”(了解即可,面试偶尔考)
不是所有重写都要求 “一模一样”,这两个例外新手记个印象,不用死磕:
1.协变:基类虚函数返回基类指针 / 引用,派生类虚函数返回派生类指针 / 引用,也算重写;
示例:
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;} // 协变,构成重写
};
**2.析构函数的重写:**基类和派生类析构函数名字不同,但只要基类析构函数是虚函数,派生类析构函数无论加不加 virtual,都算重写。
原因:编译器会把所有析构函数的名字统一处理成destructor,本质还是 “名字相同”。
新手必知:基类析构函数一定要写成虚函数,否则用基类指针释放派生类对象时,会析构不完整,造成内存泄漏!
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() {cout << "~Student()" << endl;}
};
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; // 析构Person
delete p2; // 先析构Student,再析构Person,析构完整
return 0;
}
2.4 C++11 新关键字:防止重写写错(新手强烈推荐用)
新手写代码很容易把函数名、参数写错,导致无法构成重写,编译还不报错,运行结果不对才发现问题,巨麻烦!C++11 的override和final能帮我们在编译阶段就检查出错误,一定要用起来~
final:虚函数 “锁死”,不让派生类重写
修饰虚函数,表示这个虚函数不能再被任何派生类重写,写了就编译报错。
class Car {
public:
virtual void Drive() final {} // 锁死Drive函数
};
class Benz : public Car {
public:
// 错误:Drive被final修饰,不能重写,编译报错
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
override:检查是否真的重写了基类虚函数
修饰派生类的虚函数,**表示这个函数是用来重写基类虚函数的,**如果基类没有对应的虚函数,编译直接报错。
class Car {
public:
virtual void Drive(){}
};
class Benz : public Car {
public:
// override检查:基类有Drive虚函数,重写成功
virtual void Drive() override {
cout << "Benz-舒适" << endl;
}
// 错误:基类没有Run虚函数,override检查失败,编译报错
virtual void Run() override {
cout << "Benz-跑" << endl;
}
};
2.5 新手易混:重载、重写、隐藏的区别(必背)
这三个概念是新手重灾区,面试必考,用一张表讲清楚,新手对着记就行:

3. 抽象类是个啥?(抽象类篇)
抽象类也叫接口类,是 C++ 中强制派生类重写虚函数的手段,新手可以理解为 “一个只有‘规则’,没有‘实现’的类”。
3.1 抽象类的定义
在虚函数的后面加上 = 0,这个函数就变成了纯虚函数,包含纯虚函数的类,就是抽象类。
class Car {
public:
// 纯虚函数:只有声明,没有实现
virtual void Drive() = 0;
};
3.2 抽象类的核心特点(记死)
1.**抽象类不能实例化对象:**直接定义抽象类对象会编译报错,比如Car c;是错误的;
2.派生类继承抽象类后,也不能实例化对象,除非重写了所有的纯虚函数;
3.只要有一个纯虚函数没重写,派生类还是抽象类。
class Car { // 抽象类
public:
virtual void Drive() = 0;
};
// 派生类Benz重写纯虚函数Drive,不是抽象类,可以实例化
class Benz : public Car {
public:
virtual void Drive() {
cout << "Benz-舒适" << endl;
}
};
// 派生类BMW重写纯虚函数Drive,不是抽象类,可以实例化
class BMW : public Car {
public:
virtual void Drive() {
cout << "BMW-操控" << endl;
}
};
int main() {
// Car c; // 错误:抽象类不能实例化
Car* pBenz = new Benz; // 基类指针指向派生类对象,多态
pBenz->Drive(); // 输出:Benz-舒适
Car* pBMW = new BMW;
pBMW->Drive(); // 输出:BMW-操控
delete pBenz;
delete pBMW;
return 0;
}
3.3 接口继承和实现继承(理解即可)
C++ 中的继承分两种,不用死抠概念,知道区别就行:
1.实现继承:继承普通函数,派生类直接用基类函数的实现,比如继承void func() {cout << “hello”;},派生类直接调用就能输出 hello;
2.接口继承:继承虚函数 / 纯虚函数,派生类继承的是 “函数的接口(规则)”,不是实现,必须自己重写才能用,这也是抽象类的核心作用。
小建议:如果不需要实现多态,就不要把函数定义成虚函数,浪费资源~
4. 多态背后的小秘密(原理篇)
新手学到这里可能会问:为啥基类指针 / 引用调用虚函数,就能自动识别对象类型,执行对应的函数?答案就是虚函数表(简称虚表),这是多态的底层核心,新手不用搞懂底层编译原理,但必须理解虚表的基本概念!
4.1 虚函数表:存虚函数地址的 “数组”
核心概念
只要一个类有虚函数,编译器就会为这个类生成一个虚函数表,这个表是一个存储虚函数指针的数组,数组最后面通常放一个nullptr(空指针)作为结束标志。
同时,这个类的每个对象中,都会多一个虚表指针(__vfptr),这个指针占 4 字节(x86)/8 字节(x64),指向类的虚表。
简单说:类有虚表,对象有虚表指针,指针指向虚表。
新手必看示例:计算有虚函数的类对象大小
class Base {
public:
virtual void Func1() {
cout << "Func1()" << endl;
}
private:
int _b = 1; // int占4字节
};
int main() {
Base b;
cout << sizeof(b) << endl; // 输出8(x86环境)
return 0;
}
**解释:**对象 b 的大小 = 虚表指针(4 字节)+ 成员变量_b(4 字节)= 8 字节,新手终于明白为啥有虚函数的对象会 “变大” 了吧~
4.2 派生类虚表的生成规则
派生类继承基类后,编译器会为派生类生成自己的虚表,规则超简单,分 3 步:
**1.拷贝:**先把基类虚表的内容,完整拷贝到派生类虚表中;
2.覆盖:如果派生类重写了基类的某个虚函数,用派生类自己的虚函数指针,替换掉派生类虚表中对应的基类虚函数指针(这就是 “重写 = 覆盖” 的底层原因);
**3.追加:**派生类自己新增的虚函数,按声明的顺序,依次追加到派生类虚表的最后面。
// 基类Base:有2个虚函数,1个普通函数
class Base {
public:
virtual void Func1() {cout << "Base::Func1()" << endl;}
virtual void Func2() {cout << "Base::Func2()" << endl;}
void Func3() {cout << "Base::Func3()" << endl;} // 普通函数,不进虚表
private:
int _b = 1;
};
// 派生类Derive:重写Func1,无新增虚函数
class Derive : public Base {
public:
virtual void Func1() {cout << "Derive::Func1()" << endl;} // 重写Func1
private:
int _d = 2;
};
- 基类 Base 的虚表:Func1()指针 → Func2()指针 → nullptr;
- 派生类 Derive 的虚表:Derive::Func1()指针(覆盖) → Base::Func2()指针(拷贝) → nullptr;
- 普通函数 Func3 () 不进虚表,因为只有虚函数才会存到虚表中。
4.3 多态的底层实现(通俗理解)
回到最开始的买票例子,多态的底层就是基类指针 / 引用通过虚表指针,找到对应对象的虚表,再从虚表中找到对应的虚函数执行。
通俗点说:
1.基类指针 / 引用指向基类对象时,通过对象的虚表指针,找到基类的虚表,执行基类的虚函数;
2.基类指针 / 引用指向派生类对象时,通过对象的虚表指针,找到派生类的虚表,执行派生类重写后的虚函数。
这也是为什么必须用基类指针 / 引用:如果直接用对象调用,编译器在编译阶段就确定了要执行的函数(静态绑定),不会去查虚表,自然无法实现多态。
4.4 静态绑定和动态绑定(面试必考,秒懂)
绑定就是确定函数调用地址的过程,分两种,对应 C++ 的两种多态:
1.静态绑定(前期绑定 / 早绑定):编译阶段就确定函数的调用地址,直接调用,速度快。
例子:函数重载、普通对象调用函数(包括虚函数);
2.动态绑定(后期绑定 / 晚绑定):运行阶段才确定函数的调用地址,通过虚表查找,速度稍慢,但能实现多态。
例子:基类指针 / 引用调用虚函数。
void Func(Person* p) {
p->BuyTicket(); // 动态绑定:运行时查虚表,实现多态
}
int main() {
Person mike;
mike.BuyTicket(); // 静态绑定:编译阶段确定调用Person::BuyTicket
Func(&mike); // 动态绑定
return 0;
}
4.5 新手必知的虚表小知识点(避坑)
1.虚函数不是存在虚表中,虚表中存的是虚函数的指针,虚函数和普通函数一样,都存在代码段;
2.虚表不是存在对象中,对象中存的是虚表指针,虚表本身存在代码段(常量区);
3.一个类的所有对象,共享同一个虚表(节省内存),每个对象只有自己的虚表指针。
5. 单继承 / 多继承的虚表那些事(虚表实战篇)
新手掌握了基础虚表概念后,再看看单继承和多继承中的虚表情况,这部分是面试重点,也是新手进阶的关键,跟着代码敲一遍就懂了~
5.1 单继承中的虚表(简单,新手易掌握)
单继承就是派生类只继承一个基类,虚表生成完全遵循之前的 “拷贝 - 覆盖 - 追加” 规则,需要注意的是:VS 编译器的监视窗口会隐藏派生类新增的虚函数,看起来虚表不完整,但实际是存在的,我们可以用代码打印出完整虚表。
#include <iostream>
using namespace std;
// 定义虚函数指针类型
typedef void(*VFPTR)();
// 打印虚表函数
void PrintVTable(VFPTR vTable[]) {
cout << "虚表地址:" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) {
printf("第%d个虚函数地址:0X%x -> ", i, vTable[i]);
VFPTR f = vTable[i];
f(); // 调用虚函数,看执行结果
}
cout << endl;
}
// 基类Base
class Base {
public:
virtual void func1() {cout << "Base::func1" << endl;}
virtual void func2() {cout << "Base::func2" << endl;}
private:
int a;
};
// 派生类Derive:单继承Base,重写func1,新增func3、func4
class Derive : public Base {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
virtual void func4() {cout << "Derive::func4" << endl;}
private:
int b;
};
int main() {
Base b;
Derive d;
// 取出对象的虚表指针(x86环境,取前4字节)
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
// 打印基类和派生类的虚表
cout << "Base的虚表:" << endl;
PrintVTable(vTableb);
cout << "Derive的虚表:" << endl;
PrintVTable(vTabled);
return 0;
}
**解释:**派生类虚表先覆盖 func1,再拷贝 func2,最后追加新增的 func3、func4,和我们的规则完全一致~
5.2 多继承中的虚表(稍复杂,理解核心即可)
多继承就是派生类继承多个基类,核心特点:派生类会有多个虚表指针,对应多个基类的虚表,派生类重写的虚函数,会覆盖所有基类虚表中对应的虚函数,派生类新增的虚函数,放在第一个继承的基类的虚表末尾。
#include <iostream>
using namespace std;
typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[]) {
cout << "虚表地址:" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i) {
printf("第%d个虚函数地址:0X%x -> ", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
// 基类1:Base1
class Base1 {
public:
virtual void func1() {cout << "Base1::func1" << endl;}
virtual void func2() {cout << "Base1::func2" << endl;}
private:
int b1;
};
// 基类2:Base2
class Base2 {
public:
virtual void func1() {cout << "Base2::func1" << endl;}
virtual void func2() {cout << "Base2::func2" << endl;}
private:
int b2;
};
// 派生类Derive:多继承Base1、Base2,重写func1,新增func3
class Derive : public Base1, public Base2 {
public:
virtual void func1() {cout << "Derive::func1" << endl;}
virtual void func3() {cout << "Derive::func3" << endl;}
private:
int d1;
};
int main() {
Derive d;
// 取出第一个基类Base1的虚表指针
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
// 取出第二个基类Base2的虚表指针(偏移Base1的大小)
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
// 打印两个虚表
cout << "Base1的虚表(Derive):" << endl;
PrintVTable(vTableb1);
cout << "Base2的虚表(Derive):" << endl;
PrintVTable(vTableb2);
return 0;
}
运行结果核心结论
1.Derive 有两个虚表指针,分别对应 Base1 和 Base2 的虚表;
2.Derive 重写的 func1,覆盖了 Base1 和 Base2 虚表中的 func1;
3.Derive 新增的 func3,放在第一个基类 Base1 的虚表末尾。
5.3 菱形继承 / 菱形虚拟继承(了解即可)
实际开发中不建议设计菱形继承 / 菱形虚拟继承,因为逻辑太复杂,容易出问题,还会有性能损耗,虚表的结构也会变得非常复杂,新手不用深入研究,知道 “实际开发不用,面试一般不考” 就行~
6. 面试常考!继承多态高频题(面试保命篇)
这部分是 C++ 面试的高频考点,新手一定要背会答案,理解解题思路,遇到类似题就能秒答~
6.1 选择题
- 下面哪种面向对象的方法可以让你变得富有 ( )
A: 继承 B: 封装 C: 多态 D: 抽象
答案:A(解析:继承家产,新手趣味记忆~) - () 是面向对象程序设计语言中的一种机制,实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象 ()
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
答案:D(解析:动态绑定就是运行时根据对象类型调用函数,对应多态) - 面向对象设计中的继承和组合,说法错误的是 ( )
A: 继承是静态复用,白盒复用;B: 组合是动态复用,黑盒复用;C: 优先使用继承,而不是组合;D: 继承破坏父类封装性
答案:C(解析:设计原则:优先使用组合,而不是继承) - 以下关于纯虚函数的说法,正确的是 ( )
A: 声明纯虚函数的类不能实例化对象;B: 声明纯虚函数的类是虚基类;C: 子类必须实现基类的纯虚函数;D: 纯虚函数必须是空函数
答案:A(解析:B 虚基类是虚拟继承的概念;C 子类不实现就是抽象类;D 纯虚函数没有实现,不是空函数) - 关于虚函数的描述正确的是 ( )
A: 派生类虚函数与基类参数个数 / 类型不同;B: 内联函数不能是虚函数;C: 派生类必须重新定义基类虚函数;D: 虚函数可以是 static 型
答案:B(解析:A 重写要求参数完全相同;C 派生类可以不重写,继承基类的;Dstatic 函数没有 this 指针,不能进虚表) - 关于虚表说法正确的是 ( )
A: 一个类只能有一张虚表;B: 子类无重写则与基类共用虚表;C: 虚表运行时动态生成;D: 一个类的不同对象共享该类的虚表
答案:D(解析:A 多继承有多个虚表;B 子类有自己的虚表,只是内容拷贝基类;C 虚表编译阶段生成) - 假设 A 类有虚函数,B 继承 A 并重新 A 的虚函数,无自定义虚函数,则 ( )
A:A 对象前 4 字节是虚表地址,B 不是;B: 都是虚基表地址;C: 二者虚表地址相同;D: 虚函数个数相同,虚表不同
答案:D(解析:B 重写了 A 的虚函数,虚表内容不同,所以是不同的虚表) - 菱形虚拟继承的构造函数执行顺序题,答案:A(解析:虚拟继承中,基类构造函数只执行一次,先执行最顶层基类,再执行派生类)
- 多继承指针偏移问题,答案:C(解析:派生类对象中,第一个基类指针和派生类指针地址相同,第二个基类指针有偏移)
- 虚函数默认参数题,答案:B(解析:虚函数的默认参数由基类决定,派生类重写不改变默认参数,本题执行 B::func,参数用基类的 1)
6.2 问答题
- 什么是多态?
答:同一行为,不同对象执行产生不同的结果;在继承中,基类指针 / 引用调用虚函数,不同派生类对象执行不同的重写函数。 - 重载、重写、隐藏的区别?
答:重载是同一作用域,函数名相同参数不同;重写是基类 + 派生类,虚函数三要素相同;隐藏是基类 + 派生类,函数名相同但不构成重写。 - 多态的实现原理?
答:基于虚函数表和动态绑定;类有虚函数则生成虚表,对象有虚表指针指向虚表;基类指针 / 引用通过虚表指针找到对应对象的虚表,执行虚函数,实现动态绑定。 - inline 函数可以是虚函数吗?
答:可以,但编译器会忽略 inline 属性,因为虚函数需要放进虚表,无法内联。 - 静态成员可以是虚函数吗?
答:不能,静态成员函数没有 this 指针,无法访问虚表指针,不能放进虚表。 - 构造函数可以是虚函数吗?
答:不能,对象的虚表指针是在构造函数的初始化列表阶段初始化的,构造函数执行前,虚表指针还不存在。 - 析构函数可以是虚函数吗?什么场景下用?
答:可以,且基类析构函数必须定义为虚函数;当用基类指针 / 引用释放派生类对象时,能保证派生类和基类的析构函数都执行,避免内存泄漏。 - 对象访问普通函数快还是虚函数更快?
答:普通对象调用,速度一样;基类指针 / 引用调用,普通函数更快,因为虚函数需要运行时查虚表(动态绑定)。 - 虚函数表是在什么阶段生成的,存在哪?
答:编译阶段生成,存在代码段(常量区)。 - 什么是抽象类?抽象类的作用?
答:包含纯虚函数的类是抽象类,不能实例化;作用是强制派生类重写虚函数,体现接口继承关系。
238



被折叠的 条评论
为什么被折叠?



