0. 背景知识
虚函数表不必多说,是每位C++学习者都要面对的问题。简单来说,这是C++面向对象的基础,保证子类指针转换为父类指针类型后,调用成员函数时仍然是子类的成员函数。听上去有点绕,但如果这一部分理解不了,还请在继续阅读前,先去学习一下虚函数相关内容。
虚函数表大家都知道,是存放每个对象所有虚函数的指针的一块内存区域,指针指向真正的虚函数代码。《深入理解C++对象模型》一书中讲到:
- 同一个Class下的所有对象共享同一个虚函数表;
- 子类会继承父类的虚函数表(并不是直接使用,而是复制一份),并在此基础上做拓展;
- 带有虚函数表的对象大小最小(空对象)为
32 or 64 bit (4 or 8 Byte)
,也就是虚函数表指针的大小;与之相反的,无虚函数的空对象最小为1 Byte
作为占位; - 调用虚函数时,相比直接调用成员函数多了一步,首先是通过对象头的虚函数表地址找到对应虚函数地址,然后才是调用对应的虚函数。
有必要备注一下:第3条虽然书中说是编译器相关的,但经过测试发现,Clang/GCC/MSVC都是这么做的,因此可以视这为现行标准之一。
说了这么多,虚函数表有什么可以玩的东西呢?先别急,我们再看一下override
与final
关键词。
在成员函数声明中,override
关键词表示覆写父类虚函数,且可以继续被后续子类覆写;而final
关键词表示覆写父类虚函数,且禁止被后续子类覆写。这两个关键词存在时,均暗含了“此函数为virtual”的意思,因此额外声明一个“virtual”时IDE会提示冗余。
那么我们开始和虚函数表玩耍吧,玩什么呢?交换对象的虚函数表。
1. 示例代码
示例代码可以在64位机型上直接运行,且尽可能简化以突出主题,希望读者如果有不清楚的地方,最好还是自己亲自尝试一下。
#include <cstdio>
#include <cstdint> // 引入uint64_t
// 我们从定义三个类开始,分别为A,B,C,且B继承A,C继承B
class A
{
public:
virtual void Print() { printf("p - A, "); }
virtual void PrintWithOverride() { printf("po - A, "); }
virtual void PrintWithFinal() { printf("pf - A\r\n"); }
};
class B : public A
{
public:
virtual void Print() { printf("p - B, "); }
virtual void PrintWithOverride() override { printf("po - B, "); }
void PrintWithFinal() override { printf("pf - B\r\n"); }
};
class C : public B
{
public:
void Print() { printf("p - C, "); }
void PrintWithOverride() override { printf("po - C, "); }
void PrintWithFinal() final { printf("pf - C\r\n"); }
};
int main()
{
A *a = new A();
B *b = new B();
C *c = new C();
// 获取虚函数表指针
uint64_t avpt = *(uint64_t *) a;
uint64_t bvpt = *(uint64_t *) b;
uint64_t cvpt = *(uint64_t *) c;
// 打印每个类里,虚函数表和三个虚函数的指针
printf("VPT & VFuncs: \r\n");
printf("A: %p - %p, %p, %p\r\n", avpt, *(uint64_t *)avpt,*((uint64_t *)avpt+1),*((uint64_t *)avpt+2));
printf("B: %p - %p, %p, %p\r\n", bvpt, *(uint64_t *)bvpt,*((uint64_t *)bvpt+1),*((uint64_t *)bvpt+2));
printf("C: %p - %p, %p, %p\r\n", cvpt, *(uint64_t *)cvpt,*((uint64_t *)cvpt+1),*((uint64_t *)cvpt+2));
// 打印正常情况下的输出
printf("\r\nBefore swap:\r\n");
printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal();
printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal();
printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal();
printf("(A)B: "); ((A*)b)->Print(); ((A*)b)->PrintWithOverride(); ((A*)b)->PrintWithFinal();
printf("(A)C: "); ((A*)c)->Print(); ((A*)c)->PrintWithOverride(); ((A*)c)->PrintWithFinal();
/*
* 输出:
Before swap:
A: p - A, po - A, pf - A
B: p - B, po - B, pf - B
C: p - C, po - C, pf - C
(A)B: p - B, po - B, pf - B
(A)C: p - C, po - C, pf - C
*/
/* Magic: 我们在这里交换三个对象的虚函数表
* C -> A
* A -> B
* B -> C */
*(uint64_t *) a = cvpt;
*(uint64_t *) b = avpt;
*(uint64_t *) c = bvpt;
printf("Swap:\r\nA <- C\r\nB <- A\r\nC <- B\r\n");
// 打印并测试交换完成后的结果
printf("After swap:\r\n");
printf("A: "); a->Print(); a->PrintWithOverride(); a->PrintWithFinal();
printf("B: "); b->Print(); b->PrintWithOverride(); b->PrintWithFinal();
printf("C: "); c->Print(); c->PrintWithOverride(); c->PrintWithFinal();
printf("(C)A: "); ((C*)a)->Print(); ((C*)a)->PrintWithOverride(); ((C*)a)->PrintWithFinal();
printf("(B)C: "); ((B*)c)->Print(); ((B*)c)->PrintWithOverride(); ((B*)c)->PrintWithFinal();
/*
* 输出:
After swap:
A: p - C, po - C, pf - C
B: p - A, po - A, pf - A
C: p - B, po - B, pf - C
(C)A: p - C, po - C, pf - C
(B)C: p - B, po - B, pf - C
* 这里会发现:B->C之后,C打印出来的pf仍然为C
* 尽管交换虚表这一操作完全是未定义的行为,但我们可以注意到,
* 编译器在处理final时使用了特殊的方式。
*/
/*
* 那我们使用直接从虚表中调用PF会怎么样呢?
* 值得注意的是,这里如果打印虚函数的取地址,取到的实际是虚函数在表中的索引值
*/
typedef void (C::*VFP)();
VFP cpf=(VFP)&C::PrintWithFinal;
printf("\r\n%d",&C::PrintWithFinal);
printf("\r\nCall C::PF via VT reference: \r\n");
(c->*cpf)();
/*
* 输出:
17
Call C::PF via VT reference:
pf - B
*/
return 0;
}
2 条评论
看的我热血沸腾啊https://www.jiwenlaw.com/
cstopd28463DE-娱乐娱乐http://jshxinuo.cn//