📘 本篇围绕 C++ 运行时多态机制(虚函数、虚表、类型识别等)展开。虚函数是类设计的灵魂,但也隐藏许多坑:对象切割、析构不虚、类型转换失败……本篇精选 10 个实际常见的问题,解释 + 示例 + 延伸练习,助你培基托底,深入内核。
🔁 Day 3 快速回顾
矩点 | 记忆要点 |
---|---|
构造函数类型 | 默认、有参、拷贝、移动 |
explicit | 禁止隐式类型转换 |
Rule of Five | 自编辑资源管理类必须遵守 |
静态成员变量 | 类共用,非对象的成员 |
对象大小 | 受成员顺序、对齐和虚表影响 |
🔹 今日目标:进入 C++ 运行时多态的核心机制
- 明确虚函数、vtable 和 vptr 的本质
- 理解不同条件下多态是否生效
- 识别对象切割、析构不虚等隐怪问题
- 熟悉二连符工具:
typeid
和dynamic_cast
🎓 十大核心问题 + 深层解读
【Q1】什么是虚函数?
📅 概念:
虚函数用 virtual
关键字声明,支持运行时多态
🔎 代码示例:
class Animal {
public:
virtual void speak() { std::cout << "Animal\n"; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Dog\n"; }
};
🔹 区别:
类型 | 绑定时机 | 是否支持多态 |
---|---|---|
普通函数 | 编译期 | 不支持 |
虚函数 | 运行期 | 支持 |
【Q2】虚函数的本质是什么?
📅 原理:
C++ 通过“虚函数表 vtable ” 和 “虚表指针 vptr ” 实现多态
🔎 代码示意:
- 每个有虚函数的类都有一个 vtable
- 每个对象内部隐藏一个 vptr 指向对应的 vtable
🔹 调用虚函数 = 通过 vptr 查表调用
【Q3】多态生效需要哪些条件?
🔹 有效多态的 3 大条件:
- 基类函数是 virtual
- 子类重写了此虚函数
- 通过“基类指针/引用” 调用
🔔 错误示例:
Dog d;
d.speak(); // 非多态,编译期绑定
🔔 正确示例:
Animal* a = new Dog;
a->speak(); // 多态,运行期确定调用
【Q4】什么是对象切割 (Object Slicing) ?
📅 定义: 用子类对象输入基类对象,就会被“切割成基类部分”
🔎 代码示例:
void print(Animal a) {
a.speak(); // 只有 Animal 部分,切割了
}
Cat c;
print(c); // 调用 Animal::speak()
📈 改进:
给入参改为引用/指针,防止切割
【Q5】析构函数为什么一定要声明为 virtual?
📅 问题: 如果某类有 virtual 函数,应该把析构函数也声明为 virtual
否则后果: 通过基类指针 delete 子类对象时,只调用基类析构,子类资源泄露
解決:
virtual ~Animal(); // 完全析构
【Q6】什么是 RTTI?
RTTI = Run-Time Type Information
在运行期获取对象实际类型,仅适用于含虚函数的类
两个工具操作符:
typeid(expr)
:获取类型对象dynamic_cast<T*>
:安全地转换指针
【Q7】怎么用 typeid 确认类型?
Base* p = new Derived;
if (typeid(*p) == typeid(Derived)) {
std::cout << "It's Derived\n";
}
前提: 类中有虚函数,否则 typeid(*p) 只看指针类型
【Q8】 dynamic_cast 是怎么做安全类型转换的?
Base* b = new Derived;
if (Derived* d = dynamic_cast<Derived*>(b)) {
d->draw(); // 转换成功
} else {
std::cout << "Conversion failed\n";
}
特性:
- 成功返回子类指针
- 失败返回 nullptr
- 仅适用于含 virtual 函数的类
【Q9】含虚函数类会增加对象大小吗?
代码比较:
class A { int x; };
class B { virtual void f(); int x; };
std::cout << sizeof(A) << " " << sizeof(B);
结果: B 比 A 大,因为多了 vptr
【Q10】如何避免对象切割并保留多态?
错误示例:
Base b = Derived(); // 切割只保留基类
b.virtualFunc(); // 调用的是 Base 版本
正确使用:
使用基类指针或引用,不用值传递
📙 结论手册:重点全记
矩点 | 记忆讲解 |
---|---|
虚函数 | 运行时绑 |