C++之虚函数与虚继承详解

准备工作

1、VS2012使用命令行选项查看对象的内存布局

微软的Visual Studio提供给用户显示C++对象在内存中的布局的选项:/d1reportSingleClassLayout。使用方法很简单,直接在**[项目P]**选项下找到“visual属性”后点击即可。切换到cpp文件所在目录下输入如下的命令即可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是我们想要查看的class所在的cpp文件,[className]指我们想要查看的class的类名。(下面举例说明...)

虚继承和虚函数是完全无相关的两个概念。

虚继承是解决C++多重继承问题的一种手段,从不同途径继承来的同一基类,会在子类中存在多份拷贝。这将存在两个问题:

其一,浪费存储空间;

第二,存在二义性问题,通常可以将派生类对象的地址赋值给基类对象,实现的具体方式是,将基类指针指向继承类(继承类有基类的拷贝)中的基类对象的地址,但是多重继承可能存在一个基类的多份拷贝,这就出现了二义性。

虚继承可以解决多种继承前面提到的两个问题:

虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。

虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。

虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。

补充:

1、D继承了B,C也就继承了两个虚基类指针

2、虚基类表存储的是,虚基类相对直接继承类的偏移(D并非是虚基类的直接继承类,B,C才是)

1#if 0 2//测试虚表的存在 3 4#include <iostream> 5using namespace std; 6class A 7{ 8 int i = 10; 9 int ia = 100; 10 void func() {} 11 virtual void run() { cout << "A::run()" << endl; } 12 virtual void run1() { cout << "A::run1()" << endl; } 13 virtual void run2() { cout << "A::run2()" << endl; } 14}; 15class B : public A 16{ 17 virtual void run() { cout << "B::run()" << endl; } 18 virtual void run1() { cout << "B::run1()" << endl; } 19}; 20class C :public A 21{ 22 virtual void run() { cout << "C::run()" << endl; } 23 virtual void run1() { cout << "C::run1()" << endl; } 24 virtual void run3() { cout << "C::run3()" << endl; } 25}; 26class D :/*virtual*/ public A 27{ 28 virtual void run() { cout << "D::run()" << endl; } 29 virtual void run1() { cout << "D::run1()" << endl; } 30 virtual void run2() { cout << "D::run2()" << endl; } 31 virtual void run3() { cout << "D::run3()" << endl; } 32}; 33 34int test() 35{ 36 cout << sizeof(A) << endl 37 << sizeof(B) << endl 38 << sizeof(C) << endl 39 << sizeof(D) << endl; 40 cout << sizeof(long long) << endl; 41 //A * pA = new D; 42 D d; 43 //d.run(); 44 45 typedef void(*Function)(void); 46 47 int ** pVtable = (int **)&d; 48 49#if 0 50 int * pVtable = (int*)&d; 51 int vtaleAdress = *pVtable; 52 53 int * ppVtable = (int*)vtaleAdress; 54 int func1 = *ppVtable; 55 56 Function f1 = (Function)func1; 57 f1() 58#endif 59 //pVtable[0][0] 60 61 for (int idx = 0; pVtable[0][idx] != NULL; ++idx) 62 { 63 Function f = (Function)pVtable[0][idx]; 64 f(); 65 } 66 67 //cout << (int)pVtable[1] << endl; 68 //cout << (int)pVtable[2] << endl; 69 70 getchar(); 71 return 0; 72} 73 74int main(void) 75{ 76 test(); 77 return 0; 78} 79 80#endif

测试一、二:单个继承的不同情况

1#if 0 2// 测试一:单个虚继承,不带虚函数 3// 虚继承与继承的区别 4// 1. 多了一个虚基指针 5// 2. 虚基类位于派生类存储空间的最末尾 6 7// 测试二:单个虚继承,带虚函数 8// 1.如果派生类没有自己的虚函数,此时派生类对象不会产生 9//  虚函数指针 10// 2.如果派生类拥有自己的虚函数,此时派生类对象就会产生自己本身的虚函数指针, 11// 并且该虚函数指针位于派生类对象存储空间的开始位置 12// 13 14#pragma vtordisp(off) 15#include <iostream> 16using std::cout; 17using std::endl; 18 19class A 20{ 21public: 22 A() : _ia(10) {} 23 24 //virtual 25 void f() 26 { 27 cout << "A::f()" << endl; 28 } 29private: 30 int _ia; 31}; 32 33class B 34 : virtual public A 35{ 36public: 37 B() : _ib(20) {} 38 39 void fb() 40 { 41 cout << "A::fb()" << endl; 42 } 43 44 virtual void f() 45 { 46 cout << "B::f()" << endl; 47 } 48 49#if 1 50 virtual void fb2() 51 { 52 cout << "B::fb2()" << endl; 53 } 54#endif 55 56 private: 57 int _ib; 58}; 59 60int main(void) 61{ 62 cout << sizeof(A) << endl; 63 cout << sizeof(B) << endl; 64 B b; 65 getchar(); 66 return 0; 67} 68 69 70#endif

测试三:多重继承 

1// 测试三:多重继承(带虚函数) 2// 1. 每个基类都有自己的虚函数表 3// 2. 派生类如果有自己的虚函数,会被加入到第一个虚函数表之中 4// 3. 内存布局中, 其基类的布局按照基类被声明时的顺序进行排列 5// 4. 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是 6// 真实的被覆盖的函数的地址;其它的虚函数表中存放的并不是真实的 7// 对应的虚函数的地址,而只是一条跳转指令 8#if 1 9#pragma vtordisp(off) 10#include <iostream> 11 12using std::cout; 13using std::endl; 14 15class Base1 16{ 17public: 18 Base1() : _iBase1(10) {} 19 /*virtual*/ void f() 20 { 21 cout << "Base1::f()" << endl; 22 } 23 24 /*virtual*/ void g() 25 { 26 cout << "Base1::g()" << endl; 27 } 28 29 /*virtual*/ void h() 30 { 31 cout << "Base1::h()" << endl; 32 } 33private: 34 int _iBase1; 35}; 36 37class Base2 38{ 39public: 40 Base2() : _iBase2(100) {} 41 virtual void f() 42 { 43 cout << "Base2::f()" << endl; 44 } 45 46 /*virtual*/ void g() 47 { 48 cout << "Base2::g()" << endl; 49 } 50 51 /*virtual*/ void h() 52 { 53 cout << "Base2::h()" << endl; 54 } 55private: 56 int _iBase2; 57}; 58 59class Base3 60{ 61public: 62 Base3() : _iBase3(1000) {} 63 virtual void f() 64 { 65 cout << "Base3::f()" << endl; 66 } 67 68 /*virtual*/ void g() 69 { 70 cout << "Base3::g()" << endl; 71 } 72 73 /*virtual*/ void h() 74 { 75 cout << "Base3::h()" << endl; 76 } 77private: 78 int _iBase3; 79}; 80 81 82class Derived 83 : virtual public Base1 84 //, virtual public Base2 85 //, public Base3 86{ 87public: 88 Derived() : _iDerived(10000) {} 89 void f() 90 { 91 cout << "Derived::f()" << endl; 92 } 93 94 /*virtual*/ void g1() 95 { 96 cout << "Derived::g1()" << endl; 97 } 98 99private: 100 int _iDerived; 101}; 102 103int main(void) 104{ 105 Derived d; 106 Base1 b1; 107 //Base1 *pBase1 = &b1; 108 //Base2 * pBase2 = &d; 109 //Base3 * pBase3 = &d; 110 Derived * pDerived = &d; 111 112 //pBase2->f(); 113 cout << "sizeof(d) = " << sizeof(d) << endl; 114 115 cout << "&Derived = " << &d << endl; // 这三个地址值是不一样的 116 //cout << "pBase1 = " << pBase1 << endl; 117 //cout << "pBase2 = " << pBase2 << endl; // 118 //cout << "pBase3 = " << pBase3 << endl; // 119 120 getchar(); 121 122 return 0; 123} 124 125#endif

 测试四:钻石型继承

1// 测试四:钻石型虚继承(菱形继承) 2 3//虚基指针所指向的虚基表的内容: 4// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移 5// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移 6#if 0 7 8#pragma vtordisp(off) 9#include <iostream> 10using std::cout; 11using std::endl; 12 13class B 14{ 15public: 16 B() : _ib(10), _cb('B') {} 17 18 virtual void f() 19 { 20 cout << "B::f()" << endl; 21 } 22 23 virtual void Bf() 24 { 25 cout << "B::Bf()" << endl; 26 } 27 28private: 29 int _ib; 30 char _cb; 31}; 32 33class B1 : virtual public B 34{ 35public: 36 B1() : _ib1(100), _cb1('1') {} 37 38 virtual void f() 39 { 40 cout << "B1::f()" << endl; 41 } 42 43#if 1 44 virtual void f1() 45 { 46 cout << "B1::f1()" << endl; 47 } 48 virtual void Bf1() 49 { 50 cout << "B1::Bf1()" << endl; 51 } 52#endif 53 54private: 55 int _ib1; 56 char _cb1; 57}; 58 59 60 61class B2 : virtual public B 62{ 63public: 64 B2() : _ib2(1000), _cb2('2') {} 65 66 virtual void f() 67 { 68 cout << "B2::f()" << endl; 69 } 70#if 1 71 virtual void f2() 72 { 73 cout << "B2::f2()" << endl; 74 } 75 virtual void Bf2() 76 { 77 cout << "B2::Bf2()" << endl; 78 } 79#endif 80private: 81 int _ib2; 82 char _cb2; 83}; 84 85class D : public B1, public B2 86{ 87public: 88 D() : _id(10000), _cd('3') {} 89 90 91 virtual void f() 92 { 93 cout << "D::f()" << endl; 94 } 95 96#if 1 97 virtual void f1() 98 { 99 cout << "D::f1()" << endl; 100 } 101 virtual void f2() 102 { 103 cout << "D::f2()" << endl; 104 } 105 106 virtual void Df() 107 { 108 cout << "D::Df()" << endl; 109 } 110#endif 111private: 112 int _id; 113 char _cd; 114}; 115 116int main(void) 117{ 118 D d; 119 cout << sizeof(d) << endl; 120 getchar(); 121 return 0; 122} 123 124#endif

 道友可以自己将尝试每种情况下程序内存分布的情况,以便更清晰的认识,虚函数与虚继承。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

KVM调整cpu和内存

一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid