C++----验证派生类虚函数表的组成
1.先将基类中的虚表内容拷贝一份到派生类虚表中 2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
上述前两点在上一篇博客已经写得非常清楚了,就是第三点,我们来验证一下:
class Person {
public:virtual void Buyticket(){cout << "全价" << endl;}virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual void Buyticket(){cout << "半价" << endl;}virtual ~Student() { cout << "~Student()" << endl; }virtual void fun(){cout << "fun()" << endl;}
};void System(Person* p)
{p->Buyticket();
}int main()
{Student s;}
在监视窗口看一下s的构成:并没有发现fun的存在,那在内存窗口看一下
可以看到在nullptr前有一个四字节的数据,我们暂时猜测他是fun函数
接下来写段代码验证一下:查阅资料发现,vtable的类型为void*,也就是说虚函数表中存的函数指针的类型是void*,为什么呢?
1. vptr 本质是什么?
vptr(虚函数表指针):对象中隐藏的一个指针成员,指向 虚函数表(vtable)。
vtable(虚函数表):一个 函数指针数组,数组里存放着所有虚函数的入口地址。
2. vtable 的存储形式
假设有类:
class Base {
public:virtual void f();virtual void g();
};
编译器会生成一个 vtable,大概长这样:
vtable (一个数组,存函数指针)
+----------------+
| &Base::f | // 函数地址
+----------------+
| &Base::g |
+----------------+
| 0 (可能的结尾) |
+----------------+
注意:函数指针的类型取决于函数签名,可能是
void(*)(Base*)
int(*)(Base*, int)
等等,并不是统一的。
3. 为什么用 void*
来抽象?
C++ 里不同函数指针类型之间不能互相赋值。
但 vtable 里存放的是一堆不同签名的函数指针(返回值不同、参数不同)。
为了在编译器内部统一表示,vtable 通常被实现为一个
void*
数组,即void*[]
。
4. 那么 vptr 呢?
vptr 就是 指向 vtable 数组开头的指针。
如果 vtable 是
void*[]
,那么 vptr 的类型就是void**
。
5. 对应关系
vtable:
void* vtable[]
(存放函数地址,编译器内部知道怎么解释)vptr:
void** vptr
(存放在对象里,指向 vtable 的首元素)
6. 举个例子(伪代码)
对象内存布局大概是这样:
+--------------------+
| vptr (void**) ---> +--- vtable[0] (void*,函数指针)
| member1 | vtable[1] (void*)
| member2 | ...
+--------------------+
所以:
Student s;
void** vtable = *(void***)&s; // 取出 vptr
其实就是:
&s
→ 得到对象地址强转为
(void***)
→ 把对象开头当成存了一个void**
的地方解引用 → 得到
vptr
(指向 vtable)
如何更好地理解void** vtable = *(void***)&s; 呢?举一个例子:
struct X {int a; // 前4字节double b;
};X obj{42, 3.14};
int _a = *(int*)&obj;
cout<<_a;
//打印42
和这个例子是一样的,强制转换指针类型不是目的,利用指针类型解引用访问相应大小字节内容才是真!
typedef void(*Fun)(void*); // 函数指针类型
//在C++11中可以使用 using Fun = void(*)(void*);int main()
{Student s;void** vtable = *(void***)&s; // 拿到 vtable 指针Fun f = (Fun)vtable[2]; // 第 2 个函数指针(从 0 开始)(*f)(&s);//也可以写f(&s),编译器会自动解引用函数指针并跳转执行
}