一、运行时多态的核心原理:虚函数与虚函数表
运行时多态是面向对象的核心特性,允许基类指针 / 引用在运行时根据实际指向的对象类型,调用对应派生类的成员函数 。其底层实现依赖虚函数表 (vtable) 和虚函数表指针 (__vfptr)。
1.1 虚函数的存储机制
(1)虚函数表(vtable)
- 本质:一个存储虚函数地址的静态数组 ,每个有虚函数的类会在编译期生成唯一的虚函数表。
- 结构:数组元素是对应虚函数的入口地址,末尾通常包含一个 nullptr 作为结束标记,因此数组长度 = 类中虚函数个数 + 1。
- 继承特性:派生类会完整继承基类的虚函数表;若派生类重写了某个虚函数,会覆盖表中对应位置的地址;新增的虚函数会追加到表的末尾。
(2)虚函数表指针(__vfptr)
- 每个包含虚函数的类的对象 ,会在内存布局的最开头 隐藏一个
__vfptr指针,指向所属类的虚函数表。 - 关键特性:同一类的所有对象共享同一个虚函数表,因此每个对象仅需额外存储一个指针的空间(32 位系统 4 字节,64 位系统 8 字节)。
1.2 运行时多态的执行过程
多态调用的核心是编译期只做语法检查,运行期才绑定具体函数实现:
- 编译阶段 :编译器仅检查基类是否存在该虚函数,若存在则生成 "通过
__vfptr调用虚函数表中对应位置" 的代码,不绑定具体函数地址。 - 运行阶段 :
- 通过基类指针 / 引用获取实际对象的
__vfptr; - 通过
__vfptr找到对象实际所属类的虚函数表; - 调用表中对应位置的函数,实现动态绑定。
- 通过基类指针 / 引用获取实际对象的
1.3 代码实例:形状类的多态实现
cpp
#include <iostream>
using namespace std;
// 基类:形状
class Shape {
public:
// 虚函数:绘制形状
virtual void draw() const { cout << "绘制形状" << endl; }
// 虚析构函数:必须声明为虚,防止内存泄漏
virtual ~Shape() = default;
};
// 派生类:圆形
class Circle : public Shape {
public:
// 重写虚函数(C++11推荐加override关键字)
void draw() const override { cout << "绘制圆形" << endl; }
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override { cout << "绘制矩形" << endl; }
};
int main() {
// 基类指针指向不同派生类对象
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
// 运行时多态:调用对应派生类的draw()
shape1->draw(); // 输出:绘制圆形
shape2->draw(); // 输出:绘制矩形
delete shape1;
delete shape2;
return 0;
}
1.4 面试高频考点
- 虚函数表的存储位置 :编译期生成,存储在程序的只读数据段 (.rodata),运行时不可修改。
- 空类与有虚函数类的大小 :
- 空类大小为 1 字节(用于区分不同对象的地址);
- 有一个虚函数的类大小为
sizeof(void*)(32 位 4 字节,64 位 8 字节),即__vfptr的大小。
- 构造函数 / 析构函数能否为虚函数?
- 构造函数不能:构造时对象尚未完全创建,
__vfptr还未初始化,无法进行动态绑定; - 析构函数建议声明为虚:当用基类指针删除派生类对象时,会先调用派生类析构函数,再调用基类析构函数,防止内存泄漏。
- 构造函数不能:构造时对象尚未完全创建,
- 静态成员函数能否为虚函数? 不能:静态成员函数属于类而非对象,没有
this指针,无法访问__vfptr。
二、纯虚函数与抽象类
纯虚函数用于定义接口规范,强制派生类必须实现指定功能,是实现 "接口与实现分离" 的核心手段。
2.1 纯虚函数的语法
virtual 返回类型 函数名(参数列表) = 0;
= 0是语法标记,不是赋值为 0,表示该函数在当前类中没有默认实现。- 注意:纯虚函数可以在类外提供实现(用于给派生类提供默认逻辑),但派生类仍需重写该函数才能实例化。
2.2 抽象类
(1)定义
包含至少一个纯虚函数的类称为抽象类。
(2)核心特性
- 不能创建抽象类的实例(但可以声明抽象类的指针 / 引用,用于指向派生类对象);
- 只能作为基类被继承;
- 若派生类未实现基类的所有纯虚函数,则派生类仍是抽象类,无法实例化。
2.3 接口类
(1)定义
所有成员函数都是纯虚函数,且没有非静态成员变量的类称为接口类。
(2)作用
定义统一的接口规范,支持 C++ 的多继承(接口类的多继承不会产生二义性,因为没有成员变量和普通成员函数)。
2.4 代码实例:计算器抽象类
cpp
#include <iostream>
using namespace std;
// 抽象类:计算器接口
class AbsCompute {
public:
// 纯虚函数:计算功能,强制派生类实现
virtual int calculate(int a, int b) const = 0;
virtual ~AbsCompute() = default;
};
// 派生类:加法计算器
class Add : public AbsCompute {
public:
int calculate(int a, int b) const override { return a + b; }
};
// 派生类:减法计算器
class Sub : public AbsCompute {
public:
int calculate(int a, int b) const override { return a - b; }
};
// 通用计算函数:依赖抽象接口,不依赖具体实现
void compute(const AbsCompute& calc, int a, int b) {
cout << "计算结果:" << calc.calculate(a, b) << endl;
}
int main() {
Add add;
Sub sub;
compute(add, 10, 5); // 输出:计算结果:15
compute(sub, 10, 5); // 输出:计算结果:5
return 0;
}
2.5 面试高频考点
-
纯虚函数与普通虚函数的区别 :
表格
特性 普通虚函数 纯虚函数 有无默认实现 有 无(可在类外提供,但不强制) 所在类能否实例化 能 不能(所在类为抽象类) 派生类是否必须重写 否 是(否则派生类仍是抽象类) -
抽象类与接口类的区别 :
- 抽象类可以包含普通成员函数、成员变量和纯虚函数;
- 接口类只能包含纯虚函数,不能有成员变量。
-
纯虚函数的实现场景:当多个派生类需要共享部分默认逻辑时,可以在基类中给纯虚函数提供实现,派生类重写时可以调用基类的实现。
三、C++ 四种类型转换
C++ 提供了四种显式类型转换运算符,替代 C 语言的隐式 / 强制转换,目的是提高类型安全性和代码可读性。四种转换的适用场景和安全性各不相同,是面试必考点。
3.1 静态转换 static_cast
(1)特点
- 编译期进行类型检查,无运行时开销;
- 不能转换掉
const/volatile属性; - 是 C++ 中最常用的转换,用于 "合理且安全" 的类型转换。
(2)适用场景
-
基本数据类型之间的转换 (如
int↔double、char↔int):int a = 10; double b = static_cast<double>(a); // int转double char c = static_cast<char>(a); // int转char -
void*与任意类型指针的互转 :int num = 20; void* p = # int* q = static_cast<int*>(p); // void*转int* -
基类与派生类之间的指针 / 引用转换 :
-
上行转换(派生类→基类):安全,编译器自动支持;
-
下行转换(基类→派生类):不安全,因为没有运行时类型检查,若基类指针实际指向基类对象,转换后访问派生类成员会导致未定义行为。
Derived d;
Base* b = static_cast<Base*>(&d); // 上行转换,安全
Derived* d2 = static_cast<Derived*>(b); // 下行转换,不安全(需程序员保证b实际指向Derived)
-
-
左值转右值引用 (
std::move的底层实现):int x = 10; int&& r = static_cast<int&&>(x); // 将左值x转为右值引用
3.2 动态转换 dynamic_cast
(1)特点
- 运行期进行类型检查(基于RTTI 运行时类型识别机制);
- 仅适用于有虚函数的类的指针 / 引用转换;
- 是唯一安全的下行转换方式。
(2)转换结果
- 指针转换:成功返回目标类型指针,失败返回
nullptr; - 引用转换:成功返回目标类型引用,失败抛出
std::bad_cast异常。
(3)RTTI 机制与typeid运算符
RTTI 允许在运行时获取对象的实际类型,核心是typeid运算符:
typeid(表达式)返回std::type_info对象的引用;type_info::name()返回类型名称(不同编译器输出格式不同);typeid(*p)获取指针p指向对象的实际类型,typeid(p)获取指针本身的类型。
(4)代码实例
cpp
#include <iostream>
#include <typeinfo>
using namespace std;
class Base {
public:
virtual void func() {} // 必须有虚函数才能使用dynamic_cast
virtual ~Base() = default;
};
class Derived : public Base {
public:
void func() override {}
void derivedFunc() { cout << "派生类特有函数" << endl; }
};
int main() {
Base* b1 = new Derived();
Base* b2 = new Base();
// 下行转换:成功
Derived* d1 = dynamic_cast<Derived*>(b1);
if (d1) {
d1->derivedFunc(); // 输出:派生类特有函数
}
// 下行转换:失败,返回nullptr
Derived* d2 = dynamic_cast<Derived*>(b2);
if (!d2) {
cout << "转换失败,b2实际指向Base对象" << endl;
}
// typeid获取实际类型
cout << "b1实际类型:" << typeid(*b1).name() << endl; // 输出Derived的类型名
cout << "b2实际类型:" << typeid(*b2).name() << endl; // 输出Base的类型名
delete b1;
delete b2;
return 0;
}
3.3 常转换 const_cast
(1)特点
- 唯一能转换掉
const/volatile属性的转换运算符; - 仅作用于指针或引用,不能转换基本数据类型。
(2)合法使用场景
原对象本身不是const类型 ,但被临时赋予了const属性(如函数参数为const指针,需要修改其指向的内容)。
(3)注意事项
若原对象是const类型,通过const_cast修改其内容是未定义行为 (编译器可能将const对象存储在只读内存段,修改会导致程序崩溃)。
(4)代码实例
cpp
#include <iostream>
using namespace std;
// 函数参数为const指针,但原对象非const时可修改
void modify(int* p) { *p = 100; }
void func(const int* p) {
// modify(p); // 错误:不能将const int*传给int*
int* q = const_cast<int*>(p); // 转换掉const属性
modify(q);
}
int main() {
int a = 10; // 原对象非const
const int* p = &a;
func(p);
cout << a << endl; // 输出:100,合法
const int b = 20; // 原对象是const
int* q = const_cast<int*>(&b);
*q = 200; // 未定义行为!可能导致程序崩溃
return 0;
}
3.4 重解释转换 reinterpret_cast
(1)特点
- 最不安全的转换,不做任何类型检查,仅重新解释内存的二进制内容;
- 可转换任意指针 / 引用类型,也可将指针与整数互转。
(2)适用场景
仅用于底层编程,需要直接操作内存二进制内容的场景:
- 指针与整数的互转(如将指针保存为整数);
- 不同类型指针的互转(如
char*转int*,按字节访问内存); - 底层硬件编程(如操作寄存器地址)。
(3)代码实例:查看整数的字节序
cpp
#include <iostream>
using namespace std;
int main() {
int num = 0x12345678;
// 将int*转为char*,按字节访问内存
char* p = reinterpret_cast<char*>(&num);
// 小端模式输出:78 56 34 12;大端模式输出:12 34 56 78
for (int i = 0; i < 4; i++) {
cout << hex << (int)p[i] << " ";
}
cout << endl;
return 0;
}
3.5 四种转换对比与面试考点
| 转换运算符 | 检查时机 | 能否转换 const | 适用场景 | 安全性 |
|---|---|---|---|---|
static_cast |
编译期 | 不能 | 基本类型转换、void * 互转、安全的上下行转换 | 较高 |
dynamic_cast |
运行期 | 不能 | 有虚函数类的安全下行转换 | 最高 |
const_cast |
编译期 | 能 | 临时去除 const/volatile 属性 | 中 |
reinterpret_cast |
编译期 | 不能 | 底层内存操作、指针与整数互转 | 最低 |