C++(虚函数表原理和菱形继承)
目录
虚函数表原理:
示例:
内存:
内存对齐:
虚函数表原理:
通过虚函数指针偏移来对虚函数表进行访问虚函数:
多重继承的虚函数表:
菱形继承:
虚继承:
虚函数表原理:
虚函数表是实现多态的核心机制。一个类包含虚函数时,编译器会在编译时为该类自动生成一个虚函数指针数组,并按照该类中的虚函数生命顺序将其函数地址存储在虚函数表中。一个实例化对象通过虚函数指针访问其对应的虚函数表,该指针在对象构造时编译器对其初始化通常是指向该对象内存的起始位置,通过在虚函数表中进行查找虚函数从而进行调用。虚函数表属于类,而虚函数表指针是属于实例化对象,因此该类的所有实例化对象都共享同一张虚函数表。虚函数表指针在32位系统中,占用4字节,而在64位系统中,占用8字节。
C++中类对象的内存仅包含:非静态成员变量、虚函数表指针、基类的子对象。在实际内存大小可能会因为内存对齐填充区域而有所不同。
示例:
内存:
#include <iostream>
#include <string>
#include <thread>class A {
public:A(){}A(int target):target(target){}void GetTarget() {std::cout << target << std::endl;}static void func(){std::cout<<number<<std::endl;}
private:int target;static int number;
};
int A::number=10;class B :public A {
public:};int main() {std::cout << sizeof(A) << std::endl;return 0;
}
运行结果:
可以看出类的构造函数、析构函数、成员函数、静态成员变量和静态成员函数都不属于类对象,其中析构函数、析构函数、成员函数和静态成员函数是存储在代码段,静态成员变量存储在全局变量区域,是所有该类实例化对象共享的数据资源。其中非静态成员变量target是int类型,而int类型是4字节,没有虚函数,所以该类的内存大小为4字节。而此时类B继承类A,但是由于类B是一个空类,相对于类A来说没有新增加非静态成员变量和虚函数,所以内存大小和类A是相同的。
内存对齐:
那么对类B增加一个非静态成员变量和虚函数,再看一下类B的内存大小是否发生变化。
#include <iostream>
#include <string>
#include <thread>class A {
public:A(){}A(int target):target(target){}void GetTarget() {std::cout << target << std::endl;}static void func() {std::cout << number << std::endl;}
private:int target;static int number;
};
int A::number = 10;class B :public A {
public:B(){}B(int B_target,char ch):B_target(B_target),ch(ch){}void GetB_target() {std::cout << B_target << std::endl;}static void B_fun() {std::cout << B_number << std::endl;}virtual void VritualFun() {std::cout << "类B的虚函数VirtualFunc" << std::endl;}
private:int B_target;char ch;static int B_number;
};
int B::B_number = 10;int main() {std::cout << "类B:" << sizeof(B) << std::endl;return 0;
}
运行结果:
从运行结果可以知道内存大小为24字节,因为类B继承于类A,所以会继承类A中的非静态成员变量target,而target是int类型,所以占用4字节。又因为类B有两个非静态成员变量一个是int类型的B_target占用4字节,一个char类型的ch占用1字节,并且类B还有虚函数,所以会生成一个虚函数表指针,在64为系统下占用8字节。理论上类B应该是17字节,但是因为内存对齐机制,按照字节数最大类型进行一个对齐填充,最后的结果应该是最大字节数的整数倍,所以最后计算出来为24字节。
虚函数表原理:
#include <iostream>
#include <string>
#include <thread>class A {
public:A() = default;virtual void A_func1(){std::cout << "A_func1()" << std::endl;}virtual void A_func2(){std::cout << "A_func2()" << std::endl;}virtual void A_func3(){std::cout << "A_func3()" << std::endl;}
};class B :public A {
public:B() = default;virtual void A_func1(){std::cout << "B类重写的A_func1()" << std::endl;}virtual void B_func(){std::cout << "B_func()" << std::endl;}virtual void B_func1(){std::cout << "B_func1()" << std::endl;}
};int main() {std::cout << "类A" << std::endl;A a;a.A_func1();a.A_func2();a.A_func3();std::cout << "类B" << std::endl;B b;b.A_func1();b.A_func2();b.A_func3();b.B_func();b.B_func1();std::cout << "基类指针指向派生类对象" << std::endl;A* p = &b;p->A_func1();p->A_func2();p->A_func3();return 0;
}
运行结果:
从运行结果可以知道,类A可以进行调用虚函数时,就算其派生类对A_func1()进行重写也不会影响其本身的虚函数实现。类B继承于类A,因为对其基类的A_func1()函数进行了重写,所以通过可以调用重写的虚函数和基类中未被重写的虚函数以及自身的虚函数。定义一个基类的指针指向派生类,然后通过该指针来进行调用时,就可以调用派生类中对基类的虚函数重写的虚函数。但是需要注意的是,虚函数的多态性仅针对于基类中生命的虚函数,而派生类进行重写之后,基类指针调用该函数会自动绑定到该派生类的实现,派生类自身的虚函数不属于基类,基类指针无法访问这些虚函数,因为编译器在编译时会基于基类的定义进行类型检查,从而限制对未在基类中生命的虚函数的调用。
每一个类包含虚函数时,该类对象都有一个虚函数表指针,该指针指向了该类的虚函数表,虚函数表是一个函数指针数组,按照继承基类的虚函数顺序排列,如果派生类中对基类的虚函数进行了重写,那么就会替换掉基类的该虚函数。派生类新增的虚函数按照生命顺序排列在之后。然后后面就是该类对象的非静态成员变量。
通过虚函数指针偏移来对虚函数表进行访问虚函数:
先定义一个指针指向对象地址,因为虚函数表中存储的函数的地址,所以需要将对象指针转换为void**类型,然后解引用获取虚函数表指针,通过对该指针进行偏移来访问虚函数表中的虚函数。
示例:
#include <iostream>
#include <string>
#include <thread>class A {
public:A() = default;virtual void A_func1(){std::cout << "A_func1()" << std::endl;}virtual void A_func2(){std::cout << "A_func2()" << std::endl;}virtual void A_func3(){std::cout << "A_func3()" << std::endl;}
private:int target;
};class B :public A {
public:B() = default;virtual void A_func1(){std::cout << "B类重写的A_func1()" << std::endl;}virtual void B_func(){std::cout << "B_func()" << std::endl;}virtual void B_func1(){std::cout << "B_func1()" << std::endl;}
private:int B_target;
};int main() {std::cout << "类A " << std::endl;A a;a.A_func1();a.A_func2();a.A_func3();std::cout << "类B " << std::endl;B b;b.A_func1();b.A_func2();b.A_func3();b.B_func();b.B_func1();std::cout << "通过虚函数表指针偏移来调用其虚函数表中的虚函数 " << std::endl;void** p = *(void***)(&b);((void(*)())p[0])();((void(*)())p[1])();((void(*)())p[2])();((void(*)())p[3])();((void(*)())p[4])();return 0;
}
运行结果:
多重继承的虚函数表:
#include <iostream>
#include <string>
#include <thread>class A {
public:A(){}virtual void a(){}virtual void b(){}virtual void c(){}
};class B {
public:virtual void d(){}virtual void e(){}virtual void f(){}virtual void g(){}virtual void h(){}
};class C :public A, public B {virtual void a(){}virtual void b(){}virtual void d(){}virtual void i(){}virtual void j(){}
};int main() {return 0;
}
在多重继承下,派生类会继承所有基类的虚函数表,每个含有虚函数的基类都会在派生类对应一个独立的虚函数表指针。虚函数表指针数量等于其继承的含有虚函数的基类个数。虚函数表指针按照继承顺序依次排列。派生类新增的虚函数追加到第一个基类的虚函数表中。
菱形继承:
菱形继承是C++中多重继承中的一种特殊场景,两个类继承了同一个基类,之后一个类同时继承了这两个类,就会导致该派生类会包含多个相同的基类的子对象,从而引起数据冗余和访问二义性问题。
B继承了A,而C也继承了A,同时D又继承B和C,那么D会继承包含两份基类A的成员变量。
当通过D对A的成员进行访问时,编译器无法确定是应该通过B还是C来进行访问,导致编译错误。
虚继承:
可以通过虚继承来避免菱形继承。通过虚继承来确保派生类只包含一个共享的基类子对象。编译器会为虚继承的类添加虚基类指针,用于定位共享的基类子对象。虚继承会影响虚函数表的结构,增加额外的偏移信息。派生类的对象内存布局:虚函数表指针、虚基类指针、非静态成员变量、虚基类子对象。虚基类子对象通常位于对象末尾。通过对想找到虚基类指针然后通过虚类指针和偏移量来定位虚类子对象。虚继承的访问会比普通继承的访问慢,因为需要额外的指针解引用操作,无法在编译期间确定偏移量,可能破坏局部性原理,导致缓存不命中。因此尽量避免深度进行虚继承。
示例:
#include <iostream>class A {
public:};class B:virtual public A {
public:};class C:virtual public A {};class D:public B,public C{};int main() {return 0;
}