虚函数,多态,虚函数表
1.多态
父类的一个指针,可以有多种执行状态(父类的指针调用子类的函数),即多态
#include <iostream> using namespace std; class Base { public: virtual void func() { cout << "Base func() called." << endl; } }; class Dervied : public Base { public: void func() { cout << "Dervied func() called." << endl; } }; int main() { Base *b = new Dervied();//父类调用了子类的构造函数 b->func();//父类是虚函数,调用子类的同名函数 return 0; }
结果:Dervied func() called.
2.什么是虚函数?
虚函数是一种由virtual关键字修饰的一种类内函数,可分为虚函数和纯虚函数。我们还是直接先上代码看看吧(代码1.1):
#include <iostream> using namespace std; class A { public: virtual void func() { cout << "A func() called." << endl; } }; class B : public A { public: void func() { cout << "B func() called." << endl; } }; int main() { A a; a.func(); B b; b.func(); return 0; }
1.析构函数可以写成虚的,但是构造函数不行。
为什么呢?其中的原因比较复杂,简单地来说就是虚函数是通过一种特殊的功能来实现的,它储存在类所在的内存空间中,构造函数一般用于申请内存,那连内存都没有,怎么能找到这种特殊的功能呢?所以构造函数不能是虚的。
首先我们把基类的析构函数写成虚的,有代码1.2:
#include <iostream> using namespace std; class A { public: A() { cout << "A() called." << endl; } virtual ~A() { cout << "~A() called." << endl; } }; class B : public A { public: B() { cout << "B() called." << endl; } ~B() { cout << "~B() called." << endl; } }; int main() { B b; return 0; }
纯虚函数呢?还是代码1.1那个例子,把:
virtual void func() { cout << "A func() called." << endl; }
修改为:
virtual void func()=0;
纯虚函数是不能被调用的,因为它根本就没有实现,只有声明。
所以a.func();这样的代码是会报错的。
- 在单一 class 中实现虚函数意义并不大,虚函数主要是为了实现子类函数重写父类函数的作用
- 要实现多态,通常父类中的虚函数与子类中的函数的返回值类型、函数名和参数列表必须都相同的,但是在协变的情况下返回值类型可以不一样,协变即虚函数的返回值类型为所在类的指针或引用
- 子类重写的函数默认是虚函数,也可以显式的加上 virtual,也可以不加
- 虚函数不能是内联函数,加上 inline 是没有效果的
- 构造函数不能是虚函数
- 析构函数可以是虚函数(在多态中应写虚析构)
3.虚表
这个void *的数组,就是我们的虚函数表。这个void *其实就存放着所有被virtual关键字修饰的函数的实际存放地址。
父类调用子类时,指针指向的是子类的虚函数表。
- 若对象有虚函数,对象空间最开始 4Byte(32Bit目标平台)或 8Byte(64bit目标平台)内容是虚表(虚函数列表)的首地址,叫虚指针
- 在实例化对象时,编译器检测到虚函数(virtual修饰的成员函数)时,会将虚函数的地址放到虚表(类似于一个存放函数指针的数组)中
- 当实例化子类时,检测到有虚函数的重写,编译器会用子类重写的虚函数地址覆盖掉之前父类的虚函数地址,当调用虚函数时,检测到函数是虚函数就会从虚表中找对应的位置调用,若子类没有重写,虚表中的虚函数地址就还是父类的,若子类中有重写,虚表记录的就是子类重写的虚函数地址,即实现了父类的指针调用子类的函数
- 虚表中先记录父类中的虚函数地址,接着记录子类中虚函数地址(若子类重写父类的虚函数则是覆盖)
- 最后虚表还有一个尾值是 0
4.虚析构
- 在多态中,如果释放父类指针(指向子类的父类指针),只会调用父类的析构函数,将父类的析构函数声明为虚函数(虚析构,加 virtual 修饰的析构函数),就会先调用子类的析构函数再调用父类的析构函数,所以在多态中,要用虚析构
- 父类的析构函数加了 virtual 修饰,delete 会调用子类和父类的析构函数,子类可以显式的加 virtual ,也可以不加, 默认是有的 virtual
- 还有一点需要注意的,delete 谁的指针就会调用谁的析构函数
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了