c++头脑风暴-多态虚继承多重继承内存布局
Posted cpp加油站
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++头脑风暴-多态虚继承多重继承内存布局相关的知识,希望对你有一定的参考价值。
本篇文章深入分析多态、虚继承、多重继承的内存布局以及实现原理。
首先还是看一下思维导图:
下面根据这个大纲一步一步的进行深入解析。
一、没有虚函数时内存布局是怎样的
1. 没有虚函数时类的内存布局
一个类没有虚函数的时候,其实就是结构体,它的内存布局就是按照成员变量的顺序来的。
看如下代码:
#include <iostream>
using namespace std;
class CPeople
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
;
int main()
CPeople people;
return 0;
gdb怎么用这里就不展开了,默认你会使用gdb,使用gdb设置打印格式,然后看对象people的内存布局及大小,如下:
(gdb) set p pretty on
(gdb) p people
$6 =
height = 2.0731864055035386e-317,
age = 0,
sex = 0 '\\000'
(gdb) p sizeof(people)
$7 = 16
(gdb)
此时没有虚函数,类CPeople就是一个结构体,计算大小按照8个字节对齐。
2. 没有虚函数时派生类的内存布局
把上面代码修改一下,增加一个派生类CSon,如下:
#include <iostream>
using namespace std;
class CPeople
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
;
class CSon: CPeople
int sisters;
public:
CSon()
~CSon()
;
int main()
CSon son;
return 0;
此时再查看对象son的内存布局及大小,如下:
(gdb) p son
$1 =
<CPeople> =
height = 2.317785465194599e-310,
age = -228471872,
sex = 54 '6'
,
members of CSon:
sisters = 4196224
(gdb) p sizeof(son)
$2 = 24
说白了,就类似于下面这样的一个结构体:
struct a
struct b
double h;
int a;
char s;
bbb;
int s;
;
没有虚函数时不会涉及到虚函数表和虚表指针等问题,所以相对而言还比较简单。
二、有虚函数时内存布局是怎样的
1. 有虚函数的类的内存布局
还是先看一个包含虚函数的单类,代码如下:
#include <iostream>
using namespace std;
class CPeople
public:
double height;//这里设置为公共成员变量方便查看地址
int age;
char sex;
public:
CPeople()
~CPeople()
virtual void set()
;
int main()
CPeople people;
return 0;
还是使用gdb进行查看内存布局,如下:
(gdb) p people
$1 =
_vptr.CPeople = 0x4008e0 <vtable for CPeople+16>,
height = 1.1659688840009374e-312,
age = 4196320,
sex = 0 '\\000'
(gdb) p sizeof(people)
$2 = 24
(gdb) p &people
$3 = (CPeople *) 0x7fffffffe810
(gdb) p &people.height
$4 = (double *) 0x7fffffffe818
(gdb) p &people.age
$5 = (int *) 0x7fffffffe820
(gdb) p &people.sex
$6 = 0x7fffffffe824 ""
(gdb)
可以看到,有了虚函数以后,在之前基础上增加了_vptr.CPeople = 0x4008e0 <vtable for CPeople+16>
这一行,其中vptr其实就是虚表指针,vtable就表示虚表,所以有了虚函数,对象就会相应的增加一个虚指针。
凡事存在虚函数的类,生成的对象都会生成一个虚表指针,并且这个虚表指针存储于对象所占用内存的最开始,也就是首先生成了虚表指针,然后再给成员变量分配的空间,虚表指针占用大小与操作系统有关,我这里是64位系统,所以这个虚表指针在这里是占用了8个字节。
接下来使用CPeople生成一个派生类CSon,但不实现同样的虚函数,看看是什么样的:
#include <iostream>
using namespace std;
class CPeople
public:
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
virtual void set()
;
class CSon:public CPeople
public:
int sisters;
// void set()
;
int main()
CSon son;
return 0;
gdb查看内存布局,如下:
(gdb) p son
$1 =
<CPeople> =
_vptr.CPeople = 0x400990 <vtable for CSon+16>,
height = 1.1659688840009374e-312,
age = 4196496,
sex = 0 '\\000'
,
members of CSon:
sisters = 0
(gdb)
此时对于派生类对象而言,跟之前没有虚函数的时候没啥区别哈,一样的只是在基类基础上增加了派生类的成员变量而已,接下来我们在派生类中实现基类同样的虚函数看看会发生什么。
2. 多态的原理
派生类中实现基类同样的虚函数,其实就是多态的基本操作啦,先看一下直接使用派生类对象是怎么样的,如下:
#include <iostream>
using namespace std;
class CPeople
public:
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
virtual void set()
;
class CSon:public CPeople
public:
int sisters;
virtual void set()
;
int main()
CSon son;
return 0;
还是使用gdb查看,如下:
(gdb) p son
$2 = (CSon)
<CPeople> =
_vptr.CPeople = 0x4009a0 <vtable for CSon+16>,
height = 1.1659688840009374e-312,
age = 4196512,
sex = 0 '\\000'
,
members of CSon:
sisters = 0
(gdb) p /a *(void**)0x4009a0
$5 = 0x40082a <CSon::set()>
看起来内存布局其实跟之前没有区别哈,派生类并没有重新生成虚表指针,直接继承了基类的虚表指针,但从gdb的第二个打印我们可以看出,根据虚函数表指针找到虚函数表,此时我们看到虚函数表里面存放的是派生类的虚函数。
其实在普通继承(非虚继承)的时候派生类并不会重新生成虚表指针,只是会使用它自身的虚函数地址去覆盖基类的相同虚函数,如果是派生类独有的虚函数,则直接追加到虚函数表的最后面。
下面真正的实现一把多态,使用父类指针生成一个派生类对象,看看是怎样的,代码如下:
#include <iostream>
using namespace std;
class CPeople
public:
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
virtual void set()
;
class CSon:public CPeople
public:
int sisters;
virtual void set()
virtual void get()
;
int main()
CPeople *son = new CSon();
if ( son != nullptr )
delete son;
son = nullptr;
return 0;
使用gdb查看*son
的内存布局,如下:
(gdb) p *son
$2 = (CSon)
<CPeople> =
_vptr.CPeople = 0x400a90 <vtable for CSon+16>,
height = 0,
age = 0,
sex = 0 '\\000'
,
members of CSon:
sisters = 0
(gdb) p /a *(void**)0x400a90
$3 = 0x400938 <CSon::set()>
(gdb) p /a *(void**)0x400a90@2
$4 = 0x400938 <CSon::set()>, 0x400944 <CSon::get()>
这里可以看到哈,其实跟直接使用派生类对象时内存布局没有不同哈,是一样的,只不过直接使用派生类对象是在编译时就已经确定了是调用基类还是派生类的虚函数,而使用基类指针则是在运行时才能确定的。
总结一下:c++继承时的多态一般指的运行时多态,使用基类指针或者引用指向一个派生类对象,在非虚继承的情况下,派生类直接继承基类的虚表指针,然后使用派生类的虚函数去覆盖基类的虚函数,这样派生类对象通过虚表指针访问到的虚函数就是派生类的虚函数了。
接着我们看下对象中各成员变量内存分布是怎么样的,还是用gdb,如下:
(gdb) p son
$2 = (CSon *) 0x613c20
(gdb) p &son->height
$3 = (double *) 0x613c28
(gdb) p &son->sisters
$4 = (int *) 0x613c38
看的出来对象指针所指的一块内存,首地址是0x613c20,然后虚表指针占用8个字节,接着依次按照基类和派生类声明成员变量的顺序来存放,也就是说,非虚继承时内存是按照类继承顺序以及成员变量声明顺序来存储的,基类在前,派生类在后面。
三、虚继承
如果仔细看的话,可以发现我先前多次强调了非虚继承,这是因为在没有虚函数的时候是不是虚继承影响不大,但存在虚函数的时候虚继承和非虚继承是不一样的,如下:
#include <iostream>
using namespace std;
class CPeople
public:
double height;
int age;
char sex;
public:
CPeople()
~CPeople()
virtual void set()
;
class CSon:virtual public CPeople
public:
int sisters;
virtual void set()
virtual void get()
;
int main()
CPeople *son = new CSon();
if ( son != nullptr )
delete son;
son = nullptr;
return 0;
同样使用gdb调试,打印出内存布局,如下:
(gdb) p *son
$1 = (CSon)
<CPeople> =
_vptr.CPeople = 0x400b00 <vtable for CSon+64>,
height = 0,
age = 0,
sex = 0 '\\000'
,
members of CSon:
_vptr.CSon = 0x400ad8 <vtable for CSon+24>,
sisters = 0
(gdb) p /a *(void**)0x400ad8@2
$4 = 0x40095a <CSon::set()>, 0x40096e <CSon::get()>
看一下跟之前有啥区别呢,很明显,多了一个派生类自己的虚表指针,并且派生类的虚表指针和基类的虚表指针地址还不一样,这说明什么呢。
这说明虚继承不只是实现了派生类自己的虚表指针,还重新生成了属于它自己的虚函数表,但这样一来,等于虚继承就比非虚继承多了很多开销,所以大多数情况还是不要使用虚继承吧。
再说回内存布局,在非虚继承的时候,前面也说了是按照顺序存储,那么虚继承也是这样吗?看下面打印的数据:
(gdb) p son
$2 = (CSon *) 0x613c20
(gdb) p &son->height
$3 = (double *) 0x613c38
(gdb) p &son->sisters
$4 = (int *) 0x613c28
什么意思呢,很明显这里变了,派生类的虚表指针和成员变量在前面,基类的虚表指针和成员变量在后面,那么为什么基类的放在后面去了呢?
所以虚拟继承不只是资源开销多一些,内存布局也会发生变化,那为什么还要有虚继承这个东西呢,接着往下看。
四、多重继承和二义性问题
看下面这段使用了多重继承的代码:
#include <iostream>
using namespace std;
class A
public:
A()
cout << "A()" << endl;
virtual ~A()
cout << "~A()" << endl;
;
class B: public A
public:
B()
cout << "B()" << endl;
~B()
cout << "~B()" << endl;
;
class C: public A
public:
C()
cout << "C()" << endl;
~C()
cout << "~C()" << endl;
;
class D:public B, public C
;
int main()
D d;
return 0;
执行后输出结果如下:
A()
B()
A()
C()
~C()
~A()
~B()
~A()
看到没有类A的构造函数和析构函数都执行了两次,这很显然是不正确的,因为执行类B构造函数时要执行一次类A的构造函数,执行类C的时候也要执行一次类A的构造函数,析构函数同理,到这里问题还不大,毕竟可以编译和运行。
把代码改一下,如下:
#include <iostream>
using namespace std;
class A
public:
A()
cout << "A()" << endl;
void print()
cout << "print()" << endl;
virtual ~A()
cout << "~A()" << endl;
;
class B: public A
public:
B()
cout << "B()" << endl;
~B()
cout << "~B()" << endl;
;
class C: public A
public:
C()
cout << "C()" << endl;
~C()
cout << "~C()" << endl;
;
class D:public B, public C
;
int main()
D d;
d.print();
return 0;
编译直接就报错了:
test.cpp:54:4: 错误:对成员‘print’的请求有歧义
为什么会有歧义呢,我们注释掉d.print()
这一行,然后看下对象d的内存布局,如下:
(gdb) p d
$1 = (D)
<B> =
<A> =
_vptr.A = 0x400fb8 <vtable for D+16>
, <No data fields>,
<C> =
<A> =
_vptr.A = 0x400fd8 <vtable for D+48>
, <No data fields>, <No data fields>
(gdb)
对象d里面有两个A,类B继承一个,类C继承一个,相当于有两条路,编译器此时不知道该走哪条路了,这就发生了歧义。
而所谓有歧义,其实就是我们通常所说的二义性问题,而二义性问题要怎么解决呢?这就回答了我们上一章的问题,需要使用虚继承。
把代码修改一下:
#include <iostream>
using namespace std;
class A
public:
A()
cout << "A()" << endl;
void print()
cout << "print()" << endl;
virtual ~A()
cout << "~A()" << endl;
;
class B: virtual public A
public:
B()
cout << "B()" << endl;
~B()
cout << "~B()" << endl;
;
class C: virtual public A
public:
C()
cout << "C()" << endl;
~C()
cout << "~C()" << endl;
;
class D:public B, public C
;
int main()
D d;
d.print();
return 0;
使用virtual public XX
这样的形式就叫做虚继承,类A就是虚基类,此时再看对象d的内存布局,如下:
(gdb) p d
$1 = (D)
<B> c++头脑风暴-多态虚继承多重继承内存布局