系列
c++内存分布之虚函数(单一继承) 【本文】
先声明、先存储、先父类、再子类
的顺序存放类的成员变量其实,覆盖继承不够准确。
需要考虑是否有虚函数的情况
父类和派生类出现了同名虚函数函数((普通虚函数、纯虚函数),派生类的虚函数表中将子类的同名虚函数的地址替换为自身的同名虚函数的地址-------多态出现
父类和派生类同时出现同名成员函数,这与成员变量覆盖继承的情况是一样的,派生类屏蔽父类的同名函数
本文代码演示环境: VS2017,演示项目为32位项目
要知道,影响c++内存分布的因素有很多,本文主要概述虚函数对内存分布的影响。
文章篇幅略长,其中夹杂了代码和图片,占用篇幅。但是这样却能更清楚的说明问题。
由于之前没有很细致的了解这方面知识,所以,文章会很啰嗦。
本文主要围绕基类和派生类展开下面的几种情况的内存分布
代码写的不够规范: 因为多态中,任何带虚函数的基类类的析构函数都应该是虚析构函数。但是我这里没有写出来,目的是缩短文章篇幅。
序号
情况(单继承)
1
基类是否含有虚函数,无派生类
2
基类和派生类均不含有成员函数(含虚函数),仅仅包含成员变量的继承
3
基类含有虚函数,派生类没有虚函数
4
基类和派生类均含有虚函数(无覆盖)
5
基类不含虚函数,仅派生类含有虚函数
6
派生类定义了与基类同名同类型的成员变量
7
派生类定义了与基类同名同形式的成员函数(非虚函数)
8
派生类定义了与基类同名同姓是的虚函数
序号
情况(多继承,基类个数:2)
1
基类均无虚函数(A,派生类有虚函数,B,派生类不含有虚函数)
2
两个基类中只有一个类有虚函数(A,派生类有虚函数,B,派生类不含有虚函数)
3
两个基类中都含有虚函数(A,派生类有虚函数,B,派生类不含有虚函数)
函数
说明
虚函数
晚绑定/运行时绑定。编译期间无法确定其地址(多态?)
非虚函数
编译器编译时就已经确定了地址
影响因素:基类、派生类(继承)、静态成员变量、常成员变量。初始化规则如下:
1.基类的静态变量或全局变量
2.派生类的静态变量或全局变量
3.基类的成员变量
4.派生类的成员变量
1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
3.类中const成员常量必须在构造函数初始化列表中初始化。
4.类中static成员变量,必须在类外初始化。
一个例子,
class base
{
private:
int _a;
int _b;
public:
base():_b(0), _a(_b+3) {}
void show()
{
std::cout << "a = " << _a << ", b = " << _b << std::endl;
}
};
base ba;
ba.show(); //输出: 0, @¥%……&*
知道了上面的规则,就知道这里输出的是: 0, $%##@(未知)。因为采用初始化列表的方式,成员变量的值只与成员变量声明定义的顺序有关,与初始化列表中的顺序无关。如果不采用初始化列表的方式初始化,将其放入到构造函数中,那么,成员变量的值与赋值的顺序有关。
note 下面先展开 无覆盖继承下的内分布情况
定义了一个基类,基类没有虚函数
class baseA
{
private:
int _ma = 1;
int _mb = 2;
};
baseA ba;
通过VS查看其内存布局情况
1>class baseA size(8):
1> +---
1> 0 | _ma
1> 4 | _mb
1> +---
使用sizeof的结果(预测:两个整形,sizeof(baseA) = 8字节。)
果然,同上面的内存分布输出统一。
依照上面的基础,再增加一个成员函数试试。看看结果。
class baseA
{
public:
void show()
{
std::cout << "&_ma=" << &_ma << std::endl;
std::cout << "&_mb=" << &_mb << std::endl;
}
private:
int _ma = 1;
int _mb = 2;
};
1>class baseA size(8):
1> +---
1> 0 | _ma
1> 4 | _mb
1> +---
sizeof结果:
分析:与只有成员变量的情况一致。函数show是普通成员函数,不是虚函数,故编译器在编译期间就已经确定了其地址(偏移)。
同时也可以得知,成员变量存储是按照先声明先存储的顺序。
根据只有成员变量的基础,我们再增加一个虚函数(不是纯虚函数)。 观察其内存分布
class baseA
{
public:
virtual void show() { std::cout << "baseA::show()\n\n"; }
private:
int _ma = 1;
int _mb = 2;
};
1>class baseA size(12):
1> +---
1> 0 | {vfptr}
1> 4 | _ma
1> 8 | _mb
1> +---
1>
1>baseA::$vftable@:
1> | &baseA_meta
1> | 0
1> 0 | &baseA::show
1>
有点意思了,增加一个虚函数后,内存分布中出现传说中的虚函数表和虚函数表指针。
通过监视,可以更加清楚的看见,对象ba中出现了虚函数指针和虚表,此时,虚函数表指针指向的就是虚表的第一个元素:baseA::show()函数。
此时,计算sizeof的结果是:
对比可知:
情形
分析
只有成员变量
大小由_ma和_mb组成:8字节
只有一个虚函数
大小由_ma,_mb 和 vfptr组成: 4 + 4 + 4 = 12字节
根据2.1的基础,再增加一个虚函数:
class baseA
{
public:
virtual void show() { std::cout << "baseA::show()\n\n"; }
virtual void play() { std::cout << "baseA::play()\n\n"; }
private:
int _ma = 1;
int _mb = 2;
};
1>class baseA size(12):
1> +---
1> 0 | {vfptr}
1> 4 | _ma
1> 8 | _mb
1> +---
1>
1>baseA::$vftable@:
1> | &baseA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &baseA::play
1>
1>baseA::show this adjustor: 0
1>baseA::play this adjustor: 0
此时结果
基类虚函数的情况已经分析结束,下面将开始分析 单一继承情况下含有虚函数的内存分布。
基类只有成员变量_ma和_mb,它是派生类deriveA的基类,派生类无成员变量和成员函数。观察派生类的内存分布情况
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
;
};
1>class deriveA size(8):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | _ma
1> 4 | | _mb
1> | +---
1> +---
基类不变,派生类增加成员变量,观察派生类的内存分布情况
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(16):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | _ma
1> 4 | | _mb
1> | +---
1> 8 | _derive_c
1>12 | _derive_d
1> +---
此时,增加派生类的特有成员变量后,再计算sizeof的结果
Note: 成员函数不是虚函数,故内存分布图中不会出现虚函数表指针和虚函数表。
基类含有虚函数,派生类(不含有虚函数)继承后,观察派生类内存布局
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
内存分布已经给出了答案: sizeof(deriveA) = 20. 根据上面的经验,基类含有一个虚函数,故sizeof(baseA) = 4 + 4 + 4 = 12.
派生类的内存分布
派生类的虚函数表指针仍然只有一个。看看监视结果:
监视可知,最先是虚函数表指针,然后是基类的成员变量,最后是派生类的成员变量。其与内存分布图是一致的。
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
virtual void play() { std::cout << "virtual baseA::play() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &baseA::play
预测输出: sizeof(派生类) = 20, sizeof(基类) = 12.
监视输出
虚函数表
标题有点绕: A、基类和派生类都有虚函数,B、派生类继承一个基类。
此时,基类和派生类同时出现了虚函数,派生类的内存分布情况是?可以先预测下。
// 基类
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::action
sizeof()
单位(字节)
基类
12
派生类
20
监视
虚函数表
上面观察到基类和派生类各自都只有一个虚函数的情况,那么,基类的虚函数增加到2个后,派生类的内存分布情况呢?
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
virtual void play() { std::cout << "virtual baseA::play() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &baseA::play
1> 2 | &deriveA::action
sizeof()
单位(字节)
基类
12
派生类
20
内存分布与5.1中的相同,唯一不同的是虚函数表额外增加了一个函数:play。同时可以知道,虚函数表中存储的顺序与上面5.1中总结的顺序时一致的: 先基类,再派生类,先定义,先存储的顺序。
监视结果
监视结果与5.1中显示的结果时一致的。说明我们之前的结论时正确的: 含有多个虚函数的类对应的虚函数表指针有且只有一个,不会随着虚函数个数增加而增加,且继承关系中也不会发生变化。
接着开始查看:基类只有单个虚函数和派生类有2个虚函数的情况。观察派生类的内存分布
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::action
1> 2 | &deriveA::dangcing
sizeof()
单位(字节)
基类
12
派生类
20
监视结果
成员变量内存分布
虚函数表
这种情况,更像是上面5.2和5.3的汇总。但还是要本着实事求是的精神,看看派生类的内存分布结果。
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
virtual void play() { std::cout << "virtual baseA::play() \n\n"; }
public:
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &baseA::play
1> 2 | &deriveA::action
1> 3 | &deriveA::dangcing
sizeof()
单位(字节)
基类
12
派生类
20
监视结果
成员变量内存分布
虚函数表
note: 注意到没有,前面漏掉了一种情况:基类无虚函数且派生类含有虚函数。 下面紧接着的一个章节将概述。
基类不存在虚函数,但是派生类存在一个虚函数,观察派生类的内存分布情况
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1> +---
1> 0 | {vfptr}
1> 4 | +--- (base class baseA)
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::action
Note: 注意到没有?{vfptr}出现的位置与上面的几中情况都不一样,{vfptr}放在了最前面,且放在了基类成员变量之前。此时,虚函数是声明在派生类,基类没有虚函数。
sizeof()
单位(字节)
基类
8(4 + 4)
派生类
20(4(_ma)+4(_mb) +4(虚函数表指针)+4(_derive_c) +4(_derive_d))
监视结果
虚函数表指针{vfptr} 现在属于派生类拥有,而非基类拥有。因为基类没有虚函数,派生类才有虚函数。
虚函数表,与上面的虚函数表存储方式一致。
既然如此,继续增加派生类的虚函数到2个,基类仍然没有虚函数,观察派生类的内存分布情况
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
class deriveA : public baseA
{
public:
virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
int _derive_c = 3;
int _derive_d = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | {vfptr}
1> 4 | +--- (base class baseA)
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _derive_d
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::action
1> 1 | &deriveA::dangcing
1>
1>deriveA::action this adjustor: 0
1>deriveA::dangcing this adjustor: 0
sizeof()
单位(字节)
基类
8(4 + 4)
派生类
20(4(_ma)+4(_mb) +4(虚函数表指针)+4(_derive_c) +4(_derive_d))
监视结果
虚函数表
内存布局
虚函数表指针
基类定义了两个成员变量,派生类也定义了两个成员变量,不同的的是,派生类与基类定义了一个同名成员变量_mb。
// 基类
class baseA
{
public:
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
int _derive_c = 3;
int _mb = 4;
};
deriveA da;
std::cout << da._mb; // 将输出4
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | _ma
1> 4 | | _mb
1> | +---
1> 8 | _derive_c
1>12 | _mb
1> +---
sizeof()
单位(字节)
基类
8
派生类
16
监视结果
下面展开基类与派生类具有相同形式成员函数(不是虚函数)的情况
// 基类
class baseA
{
public:
void show() { std::cout << "baseA::show() \n\n"; }
void run() { std::cout << "baseA::run() \n\n"; }
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
// 与基类中的声明一致,仅仅是函数体不同而已。
void run() { std::cout << "deriveA::run() \n\n"; }
int _derive_c = 3;
int _mb = 4;
};
deriveA da;
da.run();
1>class deriveA size(16):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | _ma
1> 4 | | _mb
1> | +---
1> 8 | _derive_c
1>12 | _mb
1> +---
监视结果
单一继承中,覆盖总结
派生类覆盖类型
派生类调用结果
成员变量
隐藏基类成员变量,按照就近原则调用派生类的成员变量
成员函数
隐藏基类成员函数,按照就近原则调用派生类的成员函数
下面展开相同形式相同名字的虚函数覆盖情况
此时,发生了成员函数的覆盖,预测下派生类的内存分布情况和虚函数表?
下面的代码展示了,基类和派生类共有一个同形式的函数run,仅函数体不同而已。
// 基类
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
virtual void run() { std::cout << "virtual baseA::run() \n\n"; }
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
// 与基类中的声明一致,仅仅是函数体不同而已。
virtual void run() { std::cout << "virtual deriveA::run() \n\n"; }
int _derive_c = 3;
int _mb = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _mb
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::run
内分布
虚函数表
虚函数表指针
下面的代码创建了一个派生类对象,并执行run函数。
deriveA da;
da.run();// 输出:virtual deriveA::run()
查看输出
派生类中实现了基类中的某个虚函数。此时,派生类的内存分布和虚函数表是?
下面的代码中,派生类实现了基类的虚函数run。
// 基类
class baseA
{
public:
virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
virtual void run() { std::cout << "virtual baseA::run() \n\n"; }
int _ma = 1;
int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
void run() { std::cout << "deriveA::run() \n\n"; }
int _derive_c = 3;
int _mb = 4;
};
1>class deriveA size(20):
1> +---
1> 0 | +--- (base class baseA)
1> 0 | | {vfptr}
1> 4 | | _ma
1> 8 | | _mb
1> | +---
1>12 | _derive_c
1>16 | _mb
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &baseA::show
1> 1 | &deriveA::run
虚函数表
虚函数表指针
注意图中高亮的这一行,这一行已经替换为派生类的run函数,而非基类的run函数
手机扫一扫
移动阅读更方便
你可能感兴趣的文章