这是 C++ 面向对象中一个非常经典的问题。
很多初学者会想:
“既然虚函数这么厉害,可以实现多态,那为什么构造函数不用虚函数?析构函数反而经常需要虚函数?”
今天我们用小学生能理解的故事 + 内存原理来解释。
一、先回顾:什么是虚函数?
在讲构造函数之前,我们先回忆一下:
普通函数调用
比如:
class Animal { public: void speak() { cout<<"动物叫"<<endl; } }; class Dog:public Animal { public: void speak() { cout<<"小狗汪汪叫"<<endl; } }; int main() { Animal *p; Dog dog; p=&dog; p->speak(); }输出:
动物叫为什么?
因为:
p的类型是:
Animal*编译器看到:
你是Animal指针,那我调用Animal版本。
如果加上:
virtual void speak() { cout<<"动物叫"<<endl; }那么:
p->speak();输出:
小狗汪汪叫原因:
运行时通过虚函数表找到真正对象。
这叫:
动态绑定(运行时多态)
二、为什么构造函数不能是虚函数?
先说答案:
因为虚函数需要对象已经创建完成,而构造函数的任务就是创建对象。
这两个事情顺序冲突。
1. 虚函数依赖什么?
虚函数依赖:
虚函数表 vtable
例如:
class Animal { public: virtual void speak(); };编译器会偷偷给对象增加:
对象内存: +-------------+ | vptr | -----> 虚函数表 +-------------+ | 成员变量 | +-------------+其中:
vptr:
叫:
虚函数表指针
例如:
Dog dog;创建后:
Dog对象 +-------------+ | vptr | +-------------+ | dog数据 | +-------------+vptr里面保存:
speak() eat() run() ...的地址。
2. 但是构造函数什么时候执行?
例如:
Dog dog;过程:
实际上:
第一步:
分配内存
准备一块空间↓
第二步:
初始化虚函数表指针
设置vptr↓
第三步:
调用构造函数
Dog()注意:
构造函数执行之前:
对象还没有真正完成。
三、如果构造函数是虚函数,会发生什么?
假设:
class Animal { public: virtual Animal() { } };假设允许。
那么:
创建对象:
Animal a;问题来了:
编译器问:
我应该调用哪个构造函数?
如果:
Animal *p=new Dog();那么:
想调用:
Dog构造函数还是:
Animal构造函数?但是此时:
Dog对象还没有创建。
它甚至不知道:
我是Dog还是:
Animal就像:
你还没有出生。
别人问:
你长大以后喜欢什么职业?
没有意义。
所以:
构造函数不能依靠虚函数机制决定自己。
四、构造函数为什么必须确定?
因为构造函数负责:
1. 初始化成员变量
例如:
class Student { private: string name; public: Student() { name="小明"; } };构造函数负责:
出生 + 初始化2. 建立对象结构
比如:
对象 姓名 年龄 成绩 指针 资源必须先建立。
而虚函数需要:
对象已经存在以后才能使用。
所以:
一句话总结:
构造函数负责创造对象,而虚函数必须依赖已经存在的对象,所以构造函数不能是虚函数。
五、那为什么析构函数需要虚函数?
这是重点。
先看一个危险代码:
class Animal { public: ~Animal() { cout<<"Animal析构"<<endl; } }; class Dog:public Animal { public: ~Dog() { cout<<"Dog析构"<<endl; } }; int main() { Animal *p=new Dog(); delete p; }猜输出:
Animal析构还是:
Dog析构 Animal析构?
答案:
Animal析构问题来了:
Dog部分没有释放!
例如:
class Dog { int *data; public: Dog() { data=new int[100]; } ~Dog() { delete [] data; } };如果没有调用:
Dog::~Dog()那么:
data泄漏。
这叫:
内存泄漏
六、解决方法:析构函数加virtual
改成:
class Animal { public: virtual ~Animal() { cout<<"Animal析构"<<endl; } };然后:
Animal *p=new Dog(); delete p;输出:
Dog析构 Animal析构为什么?
因为:
delete也是一种函数调用。
有virtual以后:
delete会根据真实对象类型调用:
Dog::~Dog() ↓ Animal::~Animal()七、生活中的例子
假设:
父类:
动物
Animal子类:
Dog Cat Bird你有:
Animal *p指向:
Dog删除:
delete p;没有virtual:
相当于:
老板: 这是动物,删除动物即可。结果:
狗自己的东西没有清理。
有virtual:
老板: 先问一下: 你真实是什么? Dog。 好: 先清理Dog, 再清理Animal。八、为什么构造函数不能虚,析构函数能虚?
我们做一个对比:
| 构造函数 | 析构函数 | |
|---|---|---|
| 作用 | 创建对象 | 销毁对象 |
| 执行时间 | 对象刚出生 | 对象死亡 |
| 需要对象存在吗 | 不需要 | 需要 |
| 虚函数需要对象吗 | 需要 | 需要 |
| 是否可以virtual | ❌不能 | ✅可以 |
| 原因 | 对象还没建立 | 对象已经存在 |
九、一个重要规则:父类析构函数应该写virtual
以后写继承:
例如:
class Base { public: virtual ~Base() { } };这是非常好的习惯。
例如:
class Shape { public: virtual ~Shape() { } }; class Circle:public Shape { }; class Rectangle:public Shape { };以后:
Shape *p; p=new Circle(); delete p;安全。
十、竞赛记忆口诀
同学们记住:
构造函数:
出生之前没有对象,无法找到孩子是谁,所以不能虚。
析构函数:
死亡的时候对象已经存在,要根据真实身份清理,所以需要虚。
十一、再深入一点:为什么C++设计成这样?
因为C++追求:
高效率 + 程序员控制
如果构造函数也支持虚函数:
每创建一个对象:
都需要:
查虚函数表 动态决定创建谁会增加复杂度。
而实际上:
C++提供了更好的方式:
工厂模式
例如:
Animal* createAnimal() { return new Dog(); }通过函数决定创建什么对象。
最后总结(一句话)
虚函数解决的是“已经存在的对象,调用哪个函数”的问题;构造函数解决的是“对象如何出生”的问题。对象还没有出生,就没有办法使用虚函数。因此构造函数不能是虚函数;而析构函数是在对象已经存在后销毁,所以为了保证多态删除安全,通常必须声明为虚函数。