class 大小计算

笔试面试时常碰到的问题,话不多说,如下:

class大小计算(visual studio下):

1、non-static data member的大小

2、virtual函数的vptr

3、virtual base class 的 virtual base class pointer

4、齐位造成的补足空间(可能补在成员间可能补在对象边界)

一、non-static data member

非静态成员的大小,各种内置类型的大小是多少就不列出了,略过。

个人觉得要注意的一种情况是:空对象,即无数据成员的对象;这种空对象作为独立个体使用的时候是强制占位1个char的

不过老实说,不是口味独特得想要故意问你这样一个问题,平时应该基本不会纠结一个没有数据成员的对象的实例大小的,更多是作为一个基类来继承。那么此时出现第二个要注意的问题:继承空的基类不会引起对象变大

没错,单独使用它时,系统需要一个“此对象存在”的依据,这个对象要在内存留下一定痕迹,系统才能够了解到“此对象存在”。所以强制放入一个char(啊,vs是这么做的)来占位 1,也就是sizeof()操作符取得的大小为1(对,sizeof()是操作符,不是库函数)。

但是当这个对象不是独立的个体的时候,当它成为了其他类的subobject部分(嵌套基类部分)的时候,它又不需要占有位置了,因为它的派生类可以证明这个类存在,所以没有理由为派生类产生一个额外的负担。

这个时候出现一个很有意思,但我真的觉得没什么实际作用的问题:一个空的派生类继承一个空的基类会怎么样?啊,是啊,值得思考,不过你到底是为什么要用两个空的类相互继承?好吧,抛开这个,我们考虑一下实际的。

class empty1 {};
class empty2 :public empty1 {};
class ch :public empty1 { char a; };
int main()
{
	cout << sizeof(empty1) << endl;              //1
	cout << sizeof(empty2) << endl;              //?
	cout << sizeof(ch) << endl;                  //1
	system("pause");
}

好吧,empty2多大?剧透一下,是1 :)。所以很明显了,只需要一个char证明整个类存在就ok了,不要每个subobject都放一个,没意义。

接着,第二个有意思没用途的问题,有虚指针但是没有数据成员的类继承空的基类要加上一个char的位置吗?啊,先不吐槽这个有点奇怪的设计。

class empty1 {};
class empty2 :public empty1 {};
class vptr :public empty2 { virtual void fun() {} };//看这里
class ch :public empty1 { char a; };
int main()
{
	cout << sizeof(empty1) << endl;
	cout << sizeof(empty2) << endl;
	cout << sizeof(ch) << endl;
	cout << sizeof(vptr) << endl;//还有这里
	system("pause");
}

好的,vptr多大?嘛,和你想的一样,4,并没有加上一个char啦(或许有人会觉得是两个?那请回去看上一段)。也就是说,不是显示定义的数据成员才能证明一个类存在的,vptr(虚指针)一样可以。另外,这里把virtual函数改成virtual继承一样是4,它们带来的vptr大小实在没区别。

二、vitual函数的vptr

嘛有人会说:这一类没什么好说的,有virtual那就加一个vptr,没有就没有咯。

啊,大概还有补充版本:不过不是说只有一个vptr,计算vptr的时候要把每个subobject也独立看待,所以每个含有vptr的subobject都会增加一个vptr。

这个问题看起来应该篇幅很短,很简单就说的明白,不难理解。

首先,不知道vptr得先知道vptr:用来实现virtual函数的工作而加入到类当中的指针->虚指针(啊,虚继承的指针之后再谈)。

使用virtual函数才能够实现到动态绑定所需的效果:在使用基类  指针/引用  去  指向/绑定  派生类对象的时候调用派生类版本的virtual函数。啊,具体怎么实现,那大概会有另一篇博文吧(看我心情喽)。总之你使用了virtual,那就要加一个vptr到类里(啊,你用几个virtual就不影响有几个vptr了)。这么说来的话,每个使用了virtual的嵌套基类为了符合“subobject应当能够发挥完整实体的同样功能”这一点,也需要自己的vptr来访问自己的virtual函数,也就像上面一样,每个subobject都要增加vptr。

理论上,是这样的啊,很好理解,有virtual就要vptr,每个有virtual的基类都增加一个vptr,完全ok。

所以下面这个测试的结果应该是 4 ,4,12,12喽(vs下运行)

class vptr1 { virtual void fun() {} };
class vptr2 { virtual void fun() {} };
class non_vptr { int elem; };
class vptr3 :public non_vptr,public vptr2 { virtual void fun() {} };
class vptr4 :public vptr1, public vptr2 { virtual void fun() {} };
int main()
{
	cout << sizeof(vptr1) << endl;
	cout << sizeof(vptr2) << endl;
	cout << sizeof(vptr3) << endl;
	cout << sizeof(vptr4) << endl;
	system("pause");
}

vptr1,2,3,4本身都有虚函数,所以他们自己至少要一个vptr,vptr3,4继承了vptr1,2,所以他们分别得到1个、2个虚指针。

vptr3 = 自己的虚指针4 + non_vptr数据成员4 + vptr2的虚指针4   =  12;

vptr3 = 自己的虚指针4 + vptr1,2的两个虚指针4 * 2 = 12;

对吧,不过结果是4,4,8,8.

emmmmmmm?

所以出现了偏差,啊,大概不是偏差,是错误。

错误的出现来自于一个优化,以及编译器的实现方式。细说的话需要另一整篇啦,所以只是给出一,个结论:visual studio中,编译器把添加的虚指针放在类的首部(或subobject的首部),并且最底层的派生类derived_bottom与最顶层的基类base_top共用一个虚指针,若按照继承顺序,拥有虚指针的首个基类将会被提前到顶部。那么来看看这几点如何得到我们之前的4,4,8,8的结果,但首先我得说,这两点都不是强制的,只是说vs一般这么做。

1、将虚指针放在首部

在vptr4中,vptr1在声明顺序中为最顶层的基类,vptr1部分的subobject将处于vptr4类的开始,vptr1的虚指针就是放在vptr4的开头位置;那么此时,vptr1与vptr2以subobject先后放置在vptr4开头,vptr4本身是一个类(而不是subobject),其虚指针应该放在自己的开头,也就意味着vptr4的虚指针和vptr1虚指针其实放在了同一个位置,当然,你也可以说vptr4的虚指针不是应该放在vptr4自己独有部分的开头吗?没错,但这时便需要提到为了优化而实现的第二点。

2、最底层的派生类(其实也就是当前类,只不过派生类独有的部分放在最底部)和最顶层的基类共享一个虚指针

由上一点,vptr4的虚指针和vptr1的虚指针放在同一位置,所以加上这一点,vptr4的虚指针和vptr1变成了同一个,这是一项优化,用以减少一个虚指针的负载,本来vptr1与vptr4都应该有独立的虚指针,但通过调整到同一个位置我们减少了一个虚指针。为啥要让他们共享嘞,前面我有提到调用虚拟函数时是通过移动对象的指针来指向不同部分的虚指针以调用不同的虚函数,那么把虚指针放在首部,便省去了一次调整指针的操作,因为对象指针指向对象开始,对象的虚指针又放在对象开头,那么调用自己的版本时也就省去了一次移动;若要调用的是最顶层类的版本,那么最顶层的类的虚指针就放在对象的开头,也就是说,让最顶层类与最底层部分共享一个虚指针就可以减少两种调用情况的指针调整,还可以省去一个虚指针的负载,在规则上也符合“把虚指针放在开头”的规则,那就这么做吧!

编译器当然也可以不这么做,也可以像之前说的吧vptr4部分的虚指针放到vptr4部分的首部,而不是整个类的首部,不过多了一个虚指针,然后在调用虚函数的时候需要进行额外的指针偏移罢了(啊,说起来果然还是共享吧)。

总而言之,虚指针应该是n-1个,n为有虚指针的类。

class vptr1 { virtual void fun() {} };
class vptr2 { virtual void fun() {} };
class vptr3 { virtual void fun() {} };
class vptr4 :public vptr1, public vptr2, public vptr3 { virtual void fun() {} };
int main()
{
	cout << sizeof(vptr4) << endl;
	system("pause");
}

结果为12,(4-1) * 4

3、virtual base class带来的虚指针

略,见下综合virtual。

4、齐位造成的补足空间

计算机为了凑够足够大的内存碎片而为类强制加入的空间。规则是每个成员只能放在自己所占字节数的倍数的地址,并且类的总大小必须是最大内置类型成员的倍数,如果有其他对象存在,则把那个对象的成员分离成内置类型计算。文字描述的话很模糊,举一些例子。

class add {
	char c;
	int i;
	char c2;
};
int main()
{
	cout << sizeof(add) << endl;
	system("pause");
}

add的大小是多少呢?1+4+1 = 6?

实际是12 -> 3*4

add有三个成员,对于c来说,c本身占1字节,c可以在任何地址存放;和他相邻的i为4字节,故只能在4的倍数开始的地址存放(当然这里指的不是实际的地址,而是在类开始的一个偏移量),于是在c与i之间补上3字节使得i存放在位置“4”。那么仅按照这一点的话add大小应该为1+3+4+1 = 9;但是add中最大的内置成员为int i;所以add的大小需要是i的倍数,故在最后再补上3个字节,得到sizeof(add) = 1+3+4+1+3 = 12

一个例子是不够的,还需要其他的例子

class add {
	char c;
	double d;
	char i;
	char c2;
};
int main()
{
	cout << sizeof(add) << endl;
	system("pause");
}

修改add如上,结果变为24。

根据之前的方式得到 1+7+8+1+1+6 = 24;这便说明了最后是将add补足到最大数位double型的倍数。

那么如果有其他的对象出现,并且对象的大小大于double呢?是否也补足到对象的倍数?

class test {
	int i1, i2, i3, i4;
};
class add {
	double d1, d2;
	test t1;
	char c1;
};
int main()
{
	cout << sizeof(add) << endl;
	system("pause");
}

做出如上的修改,得到40。此时类中最大的成员是test,16字节。但是40并不是16的倍数,所以并不是补足到最大成员的倍数,而是最大内置类型的倍数,也就是说可以把类对象成员拆解成内置类型来比较。此处的add大小 = 8+8+4+4+4+4+1+7 = 40。

5、综合virtual

那么,我先略过了virtual base带来的虚指针,其实分开也是可以的啦,只不过我坚持认为应该在齐位之后在来理解两个虚拟混合的情况,而且需要虚拟继承(说真的如果有虚拟继承你应该考虑修改你的设计,特别是你的虚基类有数据成员的时候)的时候通常你的基类中都会有虚函数指针。

首先,由于 齐位 和 空对象补充char 这两点会对对象大小造成影响,故先除去这两个变量;办法是为每个用来测试的对象增加一个大小为4(int)的成员,也就是大小等于虚拟指针的成员,来保证编译器不需要为了保证对象存在插入char,也不必为了齐位而添加无意义的字节,因为你所有的成员都一样大。

先从简单的开始,单一继承

class vb1 { 
public:
	virtual void fun() {}
	int a;
};

class d1 :virtual vb1{
	virtual void fun() {}
	int b;
};

int main()
{
	cout << sizeof(d1) << endl;
	system("pause");
}

求d1大小,啊,不要说20,是16,说20的请回顾之前。

d1 = vb1的int成员大小4 + 自己的int成员大小4 + 共用的虚函数指针大小4 + 虚继承产生的指针4 = 16

没有任何问题,相当于复习。

接着是两个虚继承

class vb1 { 
public:
	virtual void fun() {}
	int a;
};
class vb2 { 
public:
	virtual void fun() {}
	int b;
};

class d1 :virtual vb1,virtual vb2{ //虚继承两个基类
	virtual void fun() {}
	int c;
};
int main()
{
	cout << sizeof(d1) << endl;
	system("pause");
}

那么增加了什么呢?看起来是vb2的int成员大小4 + vb2的虚函数指针4 + 指向vb2的虚继承产生的指针大小4 = 12

故结果应该是16 + 12 = 28。

啊,结果是24,我们先不解释,先看三个虚继承

class vb1 { 
public:
	virtual void fun() {}
	int a;
};
class vb2 { 
public:
	virtual void fun() {}
	int b;
};
class vb3 {
public:
	virtual void fun() {}
	int c;
};

class d1 :virtual vb1,virtual vb2,virtual vb3{//继承三个虚拟基类
	virtual void fun() {}
	int d;
};
int main()
{
	cout << sizeof(d1) << endl;
	system("pause");
}

啊,按照刚才的算法d1的大小应该是16 + 12 + 12 = 40

不过结果是32,剩下的8呢?

那么这里出现了一个问题,“是不是每一个虚基类都需要在派生类中增加一个虚指针”。

显然,一般成员带来的负载是4,虚函数指针带来的负载也是4,故每个虚基类的增加至少的负载是4,实际上也是这样,每增加一个虚基类,派生类的大小增加8.vs如何做到把虚基类指针的负载“消除”呢?

这里需要提到微软的virtual base table,啊是不是有点眼熟。对,和virtual function table很像,事实上也真的很像,只是把存储虚函数地址换成了存储虚基类的地址。

也就是说,出现虚拟继承后,编译器为这个对象增加一个指向virtual base table的vptr,每增加一个虚继承基类就在这个table中增加一个slot(增加一格)用来存储新的虚拟基类,这样就解决了虚拟继承带来的大小膨胀,同时也拥有固定的存取速度(增加由vptr指向vbase class再取得成员的一层间接)。带来的代价是多一层间接、对所有构造函数、析构函数、其他拷贝构造成员进行增强来支持这个机制。

所以结论是,虚拟继承只会带来一个vptr的负载,不管有多少个虚继承的基类。

验证:

//虚拟指针大小测试
class vb1 { 
public:
	virtual void fun() {}
	int a;
};
class vb2 { 
public:
	virtual void fun() {}
	int a;
};
class vb3 { int a; };
class vb4 { int a; };
class vb5 { int a; };
class d1 :virtual vb1,virtual vb2,virtual vb3,virtual vb4,virtual vb5{
	virtual void fun() {}
	int b;
};//5个虚拟继承基类
int main()
{
	cout << sizeof(d1) << endl;
	system("pause");
}

结果是两个虚拟基类结果的24加上vb3、vb4、vb5的整型成员大小4 * 3 即 24 +12 = 36(对,我真的没骗你,没有虚指针)

另一种情况:

啊,这破文章真的有点长了(并不),不过或许值得。

大概有人注意到了,我之前说了那么多,结果把virtual base calss自己最主要的用处给省略了;为啥要使用virtual base class,为了省去不同直接基类继承来的间接基类的负载。

举栗:

class vb1 { 
public:
	//virtual void fun() {}
	int n1;
};
class vb2 :public virtual vb1 { int n2; };
class vb3 :public virtual vb1 { int n3; };//虚继承vb1
class vb4 :vb1 { int n4; };
class vb5 :vb1 { int n5; };//一般继承vb1

class d1 : vb2, vb3 {
	int n6;
};//虚拟继承d1

class d2 : vb4, vb5 {
	int n6;
};//一般继承d2
int main()
{
	cout << sizeof(vb2) << endl;
	cout << sizeof(d1) << endl;
	cout << sizeof(d2) << endl;
	system("pause");
}

为了消除齐位balabala的影响我只在对象里放了一个int,也除去了virtual function。

vb2 = 12,d1 = 24,d2 = 20。

啊,结果虚拟继承的d1反而增加了自己的体积,这也说明你真的应该慎重使用虚拟继承。

那么来看看为什么它们各自是那么大。

首先d1,vb2、vb3中放置着各自的virtual base table pointer,对,这俩家伙都有一个(从vb2你可以得知这一点),但是d1自己这一部分是没有的,d1是通过vb2,vb3来调用虚拟基类的。仔细想想这完全没问题,可能你会想,vb2里又没有重复的vb1,为什么要给它增加额外的负担,应该把vptr放到d1中。啊,不错的想法,那么怎么实现呢?若把vptr增加到d1中,如何区分要增加哪个vptr呢?d1继承自vb2与vb3,vb2、vb3拥有共同的基类vb1,那么是在继承vb2时增加vptr还是vb3时增加vptr,如何区分vb2与vb3,甚至更多7、8、9、10的继承呢?再者,如果d1虚拟继承自vb2,vb3呢?你当然也应该为d1增加虚指针,可是d1和vb2、vb3一样是虚拟继承自某个类,你如何区分哪些虚拟继承要增加虚指针,而哪些又不要?

追根究底你会找到某些方法,但是实际上厂商显然没有打算为了这样一个优化付出相当复杂的逻辑判断作为代价,它们决定简单得给每个使用了virtual继承的类增加一个vptr

vb2 = vb2自己的整型成员大小4 + 自己的vptr大小4 + vb1的数据成员大小4 = 12

d1 = vb2、vb3的int成员大小4*2 + vb2、vb3的vptr4*2 + d1自己的int成员大小4 + 不重复的vb1的int成员大小4= 24

d2 = vb2、vb3的int成员大小4*2 + d2自己的int成员大小4 + 加上重复的vb1的int成员4*2 = 20

啊,你肯定注意到我注释了一行虚拟函数,那么去掉注释再运行一次,发现结果变成

vb2 = 16,d1 = 28、d2 = 28;

是不是在你意料之中呢?

解释是对于d1:虚拟继承消除了重复的vptr

对于d2:它增加了两个vptr的负载

所以请在真的可以带来很大内存影响的时候再使用虚拟继承,因为它会打乱全部的内存布置,真的很讨厌。

(个人理解,请绝对不要完全相信我,有错请不吝赐教)

猜你喜欢

转载自blog.csdn.net/qq_37051430/article/details/83575567