前言
面向对象的程序设计最重要的三个特点是:多态、继承、封装。在C++中,为了实现多态,使用了一种动态绑定的方法。这个方法的核心表现形式就是C++的虚函数表,本文主要是验证虚函数表的一些特点(PS:就是有个臭毛病,书上说的不太信,非要自己验证一番)。
类的虚函数表
所谓虚函数,就是在某基类中声明为virtual并在一个或多个派生类中被重新定义的成员函数。
用法格式:
virtual 函数返回类型 函数名(参数列表)
{函数体};
(PS:最明显的标志就是virtual,被virtual修饰的就是虚函数)。
虚函数表是什么呢?
所谓虚函数表,其本质是一个指针数组(PS:指针数组,就是一个数组,其每一个元素都是一个指针)。这个数组的每一个元素都是一个指针,指向基类或者派生类的虚函数。程序员可以通过这个指针来访问对应的虚函数。
每一个包含虚函数的类都有自己的虚函数表,无论这个类是基类还是派生类。虚函数表与类是一对一绑定关系,一个类对应一个虚函数表,一个虚函数表对应一个类。
虚函数表有什么好处呢?
实现面向对象的程序设计,无论是继承还是重写,让派生类的开发和管理更加方便、形象。
个别代码的含义解析
&a //变量a的内存地址
(int64_t*)&a //将a的地址转化为int64_t*类型的指针,该指针指向变量a
*(int64_t*)&a //对变量a内存的第一块空间解引用(读取内存地址里的值),得到的还是一个内存地址。该内存地址是变量a类型的虚函数表的地址(也是虚函数表第一个元素的地址)。
(int64_t*)*(int64_t*)&a //将虚函数表的地址转化为int64_t*类型的指针,该指针指向虚函数表的第一个元素。
*(int64_t*)*(int64_t*)&a//对虚函数表的第一个元素的内存地址解引用(读取该内存里的值),得到的依然是一个内存地址,该内存地址是变量a类型的虚函数的函数体的地址。
typedef void(*VTable)();
(VTable)*(int64_t*)*(int64_t*)&a
//将函数体的地址转化为VTable类型的函数指针,然后通过该指针执行函数体。
一、类的虚函数表
class Base{
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
定义Base基类,该类有三个虚函数f()、g()、h()。执行这三个函数会分别在控制台输出Base::f、Base::g、Base::h。表示类名::函数名。
验证:每一个类都只有一个虚函数表。
int main(){
Base a;
Base b;
cout << &a <<endl;
cout << (int64_t*)&a << endl;
cout << *(int64_t*)&a << endl;
cout << (int64_t*)*(int64_t*)&a << endl;
cout << *(int64_t*)*(int64_t*)&a << endl;
//访问变量a函数体f
VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
va_f();
//访问变量a函数体g
VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
va_g();
//访问变量a函数体h
VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
va_h();
cout << &b <<endl;
cout << (int64_t*)&b << endl;
cout << *(int64_t*)&b << endl;
cout << (int64_t*)*(int64_t*)&b << endl;
cout << *(int64_t*)*(int64_t*)&b << endl;
//访问变量b函数体f
VTable vb_f = (VTable)*(int64_t*)*(int64_t *)&b;
vb_f();
//访问变量b函数体g
VTable vb_g = (VTable)*((int64_t*)*(int64_t *)&b+1);
vb_g();
//访问变量b函数体h
VTable vb_h = (VTable)*((int64_t*)*(int64_t *)&b+2);
vb_h();
return 0;
}
程序执行结果如图所示:
从上述结果可以看出,尽管变量a、b自身所在的内存地址是不一样的,但是其保存的第一个元素都是指向Base类的虚函数表的指针,无论是虚函数表的地址,还是虚函数表中指向f函数体的地址都是一样的。
二、派生类有自己的虚函数表(无重写单继承)
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 Drive :public Base {
};
验证:派生类的虚函数表和基类的虚函数表不是一个。
int main(){
Base a;
Drive c;
cout << &a <<endl;
cout << (int64_t*)&a << endl;
cout << *(int64_t*)&a << endl;
cout << (int64_t*)*(int64_t*)&a << endl;
cout << *(int64_t*)*(int64_t*)&a << endl;
//访问变量a函数体f
VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
va_f();
//访问变量a函数体g
VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
va_g();
//访问变量a函数体h
VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
va_h();
cout << &c <<endl;
cout << (int64_t*)&c << endl;
cout << *(int64_t*)&c << endl;
cout << (int64_t*)*(int64_t*)&c << endl;
cout << *(int64_t*)*(int64_t*)&c << endl;
//访问变量c函数体f
VTable vc_f = (VTable)*(int64_t*)*(int64_t *)&c;
vc_f();
//访问变量c函数体g
VTable vc_g = (VTable)*((int64_t*)*(int64_t *)&c+1);
vc_g();
//访问变量b函数体h
VTable vc_h = (VTable)*((int64_t*)*(int64_t *)&c+2);
vc_h();
return 0;
}
程序执行结果:
从上述结果可以看出,变量Base::a、Drive::c自身所在的内存地址是不一样,其指向的虚函数表的地址也不一样,但是虚函数表中指向f函数体的地址都是一样的。
三、派生类重写虚函数
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Drive1 :public Base {
virtual void h() {cout << "Drive :: h" << endl;}
};
验证:重写的虚函数其地址应该是新的,不应该是在基类虚函数的地址上覆盖。
int main(){
Base a;
Drive1 d;
cout << &a <<endl;
cout << (int64_t*)&a << endl;
cout << *(int64_t*)&a << endl;
cout << (int64_t*)*(int64_t*)&a << endl;
cout << *(int64_t*)*(int64_t*)&a << endl;
cout << *((int64_t*)*(int64_t *)&a +2)<<endl;
//访问变量a函数体f
VTable va_f = (VTable)*(int64_t*)*(int64_t *)&a;
va_f();
//访问变量a函数体g
VTable va_g = (VTable)*((int64_t*)*(int64_t *)&a+1);
va_g();
//访问变量a函数体h
VTable va_h = (VTable)*((int64_t*)*(int64_t *)&a+2);
va_h();
cout << &d <<endl;
cout << (int64_t*)&d << endl;
cout << *(int64_t*)&d << endl;
cout << (int64_t*)*(int64_t*)&d << endl;
cout << *(int64_t*)*(int64_t*)&d << endl;
cout << *((int64_t*)*(int64_t *)&d +2)<<endl;
//访问变量d函数体f
VTable vd_f = (VTable)*(int64_t*)*(int64_t *)&d;
vd_f();
//访问变量c函数体g
VTable vd_g = (VTable)*((int64_t*)*(int64_t *)&d+1);
vd_g();
//访问变量b函数体h
VTable vd_h = (VTable)*((int64_t*)*(int64_t *)&d+2);
vd_h();
return 0;
}
程序执行结果:
从上述结果可以看出,变量Base::a、Drive1::d自身所在的内存地址是不一样,其指向的虚函数表的地址也不一样,虚函数表中指向没有重写的虚函数的地址是一样的。但是重写后的虚函数的地址是不一样的,是一个新的地址,并不是在基类该虚函数的地址上覆盖。
参考博客:
[1]https://blog.csdn.net/lihao21/article/details/50688337
[2]https://blog.csdn.net/haoel/article/details/1948051
[3]https://blog.csdn.net/u012630961/article/details/81226351?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-3.control
[4]https://blog.twofei.com/496/
[5]https://www.cnblogs.com/rollenholt/articles/2023364.html
[6]https://blog.csdn.net/a13602955218/article/details/104743492