之前只是看过C++虚函数表相关介绍,今天有空就来写代码研究一下。
面向对象的编程语言有3大特性:封装、继承和多态。C++是面向对象的语言(与C语言主要区别),所以C++也拥有多态的特性。
C++中多态分为两种:静态多态和动态多态。
静态多态为编译器在编译期间就可以根据函数名和参数等信息确定调用某个函数。静态多态主要体现为函数重载和运算符重载。
函数重载即类中定义多个同名成员函数,函数参数类型、参数个数和返回值不完全相同,编译器编译后这些同名函数的函数名会不一样,也就是说编译期间就确定了调用某个函数。C语言函数编译后函数名就是原函数名,C++函数名为原函数名拼接函数参数等信息。
动态多态即运行时多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。动态多态由虚函数来实现。 比如
class Base{};
class A: public Base{};
class A: public Base{};
Base *base = new A;
base = new B;
探索虚函数表结构
之前的文件提到过,一个类占用的空间,如果有虚函数就会占用8字节的空间来存放虚函数表的地址。 虚函数表内存空间 中依次存放着各个虚函数的指针,通过这个指针可以调用相关的虚函数。
下面通过代码来验证一下上面这个内存结构,定义一个Base类,中间有3个方法,f1/f2/f3。
class Base {
public:
virtual void f1(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
virtual void f2(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
virtual void f3(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
实例化这个类后的内存模型如下图所示:
下面通过代码来验证这个内存模型。
int main() {
typedef void(*Fun)();
std::cout << sizeof(Base)<< std::endl;
Base b;
printf("b ptr = %p\n", &b);
long v_table_addr_value = *(long*)&b;
printf("vtable ptr = 0x%lx\n", v_table_addr_value);
void *v_table_addr = (void*)v_table_addr_value;
printf("vtable ptr = %p\n", v_table_addr);
long f1_addr_value = *(long*)v_table_addr;
printf("f1() ptr = 0x%lx\n", f1_addr_value);
Fun f1 = (Fun)f1_addr_value;
f1();
long f2_addr_value = *(long*)((char*)v_table_addr + 8);
printf("f2() ptr = 0x%lx\n", f2_addr_value);
Fun f2 = (Fun)f2_addr_value;
f2();
long f3_addr_value = *(long*)((char*)v_table_addr + 16);
printf("f3() ptr = 0x%lx\n", f3_addr_value);
Fun f3 = (Fun)f3_addr_value;
f3();
return 0;
}
通过上述代码的输出结果可以验证上图的内存模型。
继承基类重写虚函数
现在定义一个继承类Derived ,重写了f1() 函数,也就是覆盖掉了Base 类中的函数f1() 。同时又新增了虚拟函数f4() 。
class Base {
public:
virtual void f1(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
virtual void f2(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
virtual void f3(){
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
class Derived : public Base
{
public:
virtual void f1() override {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
virtual void f4() {
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
通过上一节类似的代码可以验证new Derived() 其内存模型为 由此可以得出以下结论:
- 虚函数按照其声明顺序放于表中。
- 父类的虚函数在子类的虚函数前面。
- 覆盖的函数放到了虚函数表中原来父类虚函数的位置。
- 没有被覆盖的虚函数函数位置不变。
寻找被覆盖的虚函数
未完待续
Derived *d = new Derived();
printf("b ptr = %p\n", d);
d->f1();
d->Base::f1();
多基类继承 虚函数表
未完待续 有3个基类Base1,Base2, Base3,都有两个虚函数f1()、f2()。
|