1.虚函数的实现原理:
实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚函数表。该表是编译器在编译时设置的静态数组,一般我们称为 vtable。虚函数表包含可由该类调用的虚函数,此表中的每个条目是一个函数指针,指向该类可访问的虚函数。
每个对象在创建时,编译器会为对象生成一个指向该类的虚函数表的指针,我们称之为 vptr。vptr 在创建类实例时自动设置,以便指向该类的虚拟表。如果对象(或者父类)中含有虚函数,则编译器一定会为其分配一个 vptr;如果对象不包含(父类也不含有),此时编译器则不会为其分配 vptr。与 this 指针不同,this 指针实际上是编译器用来解析自引用的函数参数,vptr 是一个真正的指针。
虚函数表相关知识点:
1.虚函数表存放的内容:类的虚函数的地址。
2.虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
3.虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
4.虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针 vptr,来指向类的虚函数表 vtable。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};
class Derive : public Base
{
public:
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
Base *p = new Derive();
p->B_fun1(); // Base::B_fun1()
return 0;
}
2.虚拟函数表指针 vptr
带有虚函数的类,通过该类所隐含的虚函数表来实现多态机制,该类的每个对象均具有一个指向本类虚函数表的指针,这一点并非 C++ 标准所要求的,而是编译器所采用的内部处理方式。实际应用场景下,不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。如果涉及多重继承和虚继承,情况还将更加复杂。因此永远不要使用 C 语言的方式调用 memcpy() 之类的函数复制对象,而应该使用初始化(构造和拷构)或赋值的方式来复制对象。
程序示例,我们通过对象内存的开头处取出 vptr,并遍历对象虚函数表。
#include <iostream>
#include <memory>
using namespace std;
typedef void (*func)(void);
class A {
public:
void f() { cout << "A::f" << endl; }
void g() { cout << "A::g" << endl; }
void h() { cout << "A::h" << endl; }
};
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derive: public Base {
public:
void f() { cout << "Derive::f" << endl; }
void g() { cout << "Derive::g" << endl; }
void h() { cout << "Derive::h" << endl; }
};
int main()
{
Base base;
Derive derive;
//获取vptr的地址,运行在gcc x64环境下,所以将指针按unsigned long *大小处理
//另外基于C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置
unsigned long* vPtr = (unsigned long*)(&base);
//获取vTable 首个函数的地址
func vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
func vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
func vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
vPtr = (unsigned long*)(&derive);
//获取vTable 首个函数的地址
vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
cout<<sizeof(A)<<endl;
cout<<sizeof(base)<<endl;
cout<<sizeof(derive)<<endl;
return 0;
}
/*
Base::f
Base::g
Base::h
Derive::f
Derive::g
Derive::h
1
8
8
*/
我们可以看到同样的函数实现,对象在分配空间时,编译器会为对象多分配一个 vptr 指针的空间。