网站首页 > 基础教程 正文
我们知道在C++中,通过虚函数,可以实现用父类指针指向其子类的实例,然后通过该指针可以调用实际子类的方法。这样让父类指针具有了“多种形态”, 而且这种可变性不是编译期确定的,而是在运行过程中确定的调用关系。这也是一种泛型技术,属于动态多态,它的实现机制离不开虚函数表的功能。
一、什么是虚函数表
如果一个类定义了虚函数,那么就会生成一个虚函数表(Virtual Table),包括继承它的子类, 也同样会具有该表。这样的类对象会添加一个隐藏成员,隐藏成员保存了一个指针,这个指针叫虚表指针(vptr),它指向一个虚函数表。
虚函数表就像一个数组,表中有许多的槽(slot),每个槽中存放的是一个虚函数的地址,可以理解为数组里存放着指向每个虚函数的指针。即:每个类使用一个虚函数表,每个类对象用一个虚表指针。
C++的编译器可以保证虚表指针存在于对象实例中最前面的位置,这是为了保证取到虚函数表的有最高的性能,即使在有多层继承或是多重继承的情况下。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同,子类独有的虚函数放在后面。
二、代码实践验证
假设我们有这样一个类, 代码如下:
#include <iostream>
using namespace std;
typedef void (*func_ptr)(void);
class DemoAA {
public:
DemoAA(int num) { m_num = num; cout << "Constructor AA" << endl; }
~DemoAA() { cout << "Destructor AA" << endl; }
void SayHello() { cout << "Hello, world. m_num=" << m_num << endl; }
virtual void TestOne() { cout << "Call TestOne" << endl; }
virtual void TestTwo() { cout << "Call TestTwo" << endl; }
virtual void TestThree() { cout << "Call TestThree" << endl; }
protected:
int m_num;
};
class DemoBB : public DemoAA{
public:
DemoBB(int num) : DemoAA(num) { cout << "Constructor BB" << endl; }
~DemoBB() { cout << "Destructor BB" << endl; }
virtual void TestOne() { cout << "DemoBB: Call TestOne" << endl; }
virtual void TestFour() { cout << "DemoBB: Call TestFour" << endl; }
};
void TestAddrCall() {
DemoAA obj(123);
obj.SayHello();
long* vptr = (long*)(&obj);
cout << "vptr addr: " << vptr << endl;
func_ptr pfunc = nullptr;
long* vbtl = (long*)(*vptr);
pfunc = (func_ptr) * (vbtl);
pfunc(); // TestOne
pfunc = (func_ptr) * (vbtl + 1);
pfunc(); // TestTwo
pfunc = (func_ptr) * (vbtl + 2);
pfunc(); // TestThree
}
void TestSubclass() {
DemoBB obj(456);
DemoAA* pbase = &obj;
pbase->SayHello();
pbase->TestOne();
pbase->TestTwo();
pbase->TestOne();
}
int main() {
TestAddrCall();
TestSubclass();
return 0;
}
其中(long*)(&obj)就是虚函数表地址(也是对象实例的地址),(long*)*(long*)(&obj)就是第一个函数地址, 在程序中取出对象obj的地址,根据对象的布局可以得出就是虚表的地址,根据这个地址可以把虚表的第一个内存单元的内容取出,然后强制转换成一个函数指针,利用这个函数指针来访问虚函数。又因为虚表是连续的,利用每次+1可以来访问下一个内存单元。运行结果如下:
三、总结
1、虚函数表记录了当前所属类的最新的所有虚函数,而每个类的对象实例的开始存储了虚函数表的指针,通过这个指针可以找到对应的虚函数表及其函数地址;
2、无论是基类的指针,还是子类的指针,最终指向的都是具体的实例对象。它们能够向上或向下转型,主要原因还是在于首地址存储了虚标指针,这个就有点像C里的union结构体,同一片内存可以有不同的解析方法。但是安全性(特别是向下转型)就需要自己来保证了。
3、如果调用的是虚函数,会有一层虚函数表查询的过程,多了一次寻址转换过程;但是普通函数就不在此列,是直接通过符号表映射到代码地址的,因此虚函数调用会有一丢丢(几乎可以忽略)的性能开销。
猜你喜欢
- 2024-11-12 金三银四不跳槽更待何时?安卓开发1年字节5面面经,已成功上岸
- 2024-11-12 C++要学到什么程度才能找到实习? c++学完学什么
- 2024-11-12 C++基础语法梳理:inline 内联函数!虚函数可以是内联函数吗?
- 2024-11-12 C++基类中虚析构函数 c++ 虚析构函数
- 2024-11-12 C和C++代码精粹:C语言和C++有什么区别么?
- 2024-11-12 3个面试C++开发岗位的高频笔试题 c++软件开发面试
- 2024-11-12 一文在手,"类间关系"不再困惑
- 2024-11-12 c++的面试总结 c++面试知识点
- 2024-11-12 C++ 虚函数 实例学习 简单易懂 c++虚函数的使用
- 2024-11-12 C++里面的虚析构函数 虚构造函数与虚析构函数
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)