您的位置:首页 > 编程语言 > C语言/C++

c++好玩的东西

2017-04-23 17:10 267 查看
Class 的大小

一个空 class 的大小为 1 字节,因为编译器需要安插进去一个 char,使得这个 class 对象得以在内存中被配置独一无二的地址。虽然空 class 大小为 1 字节,但是假如某个类 A 继承该空 class,计算类 A 的大小时会优化父类空 class 的大小,如类 A 为空,sizeof(A) = 1,不空,则为类A真实数据大小。

我们通常说某个 class 内部有 virtual function 尽管没有数据成员,它的大小仍为为 4 字节,因为它有一个 vptr,指向一个 vtbl ,所以按指针算大小就是 4 字节。实际上,就算没有 virtual function,如果某个类虚继承别的类,编译器仍生成有 vtbl ,因为它的 vtbl 还要用来保存 virtual base class object(虚基类子对象)的 offset 。

对于 virtual 继承,所有的 virtual base class subobject 在派生类中只保留一份,所以计算派生类对象时,按一份计算即可。

计算 class 的大小时,还要考虑内存对齐,标准就是一个 bus 的长度,32 位为 4 字节。不过要注意,派生类数据成员并不会填补基类花在内存对齐上的部分。

下面举出无数例子来验证结论 :)

case 1 :class A 为空时的普通继承、普通继承有成员、虚继承、有虚函数

class A { };
class B : public A { };
class C : public A { char ch; };
class X : virtual public A { };
class Y { virtual void fun(); };

std::cout<<sizeof(A)<<std::endl;    // 1
std::cout<<sizeof(B)<<std::endl;    // 1
std::cout<<sizeof(C)<<std::endl;    // 1
std::cout<<sizeof(X)<<std::endl;    // 4
std::cout<<sizeof(Y)<<std::endl;    // 4


A为空,除了它自己,别人都会优化掉它。虚继承和含有虚函数这两种情况都有 vptr,所指东西不同而已。当然 vptr 在既有虚继承,又有虚函数的类中正负分别指向 virtual functions 和 virtual base class subobject 的 offset,只有一个 vptr。(Mircosoft 编译器有两个虚函数表 vtbl ,有两个 vptr;G++实际上只有一个 vtbl,一个vptr,并且 virtual funcions 和 virtual base class subobject 的 offset 按顺序存放)。

g++与虚继承

g++编译器生成的C++类实例,虚函数与虚基类地址偏移值共用一个虚表(vtable)。类实例的开始处即为指向所属类的虚指针(vptr)。实际上,一个类与它的若干祖先类(父类、祖父类、…)组成部分共用一个虚表,但各自使用的虚表部分依次相接、不相重叠。

g++编译下,一个类实例的虚指针指向该类虚表中的第一个虚函数的地址。如果该类没有虚函数(或者虚函数都写入了祖先类的虚表,覆盖了祖先类的对应虚函数),因而该类自身虚表中没有虚函数需要填入,但该类有虚继承的祖先类,则仍然必须要访问虚表中的虚基类地址偏移值。这种情况下,该类仍然需要有虚表,该类实例的虚指针指向类虚表中一个值为0的条目。

该类其它的虚函数的地址依次填在虚表中第一个虚函数条目之后(内存地址自低向高方向)。虚表中第一个虚函数条目之前(内存地址自高向低方向),依次填入了typeinfo(用于RTTI)、虚指针到整个对象开始处的偏移值、虚基类地址偏移值。因此,如果一个类虚继承了两个类,那么对于32位程序,虚继承的左父类地址偏移值位于vptr-0x0c,虚继承的右父类地址偏移值位于vptr-0x10.

一个类的祖先类有复杂的虚继承关系,则该类的各个虚基类偏移值在虚表中的存储顺序尊重自该类到祖先的深度优先遍历次序。

Microsoft Visual C++与虚继承

Microsoft Visual C++与g++不同,把类的虚函数与虚基类地址偏移值分别放入了两个虚表中,前者称为虚函数表vftbl,后者称虚基类表vbtbl。因此一个类实例可能有两个虚指针分别指向类的虚函数表与虚基类表,这两个虚指针分别称为虚函数表指针vftbl与虚基类表指针vbtbl。当然,类实例也可以只有一个虚指针,或者没有虚指针。虚指针总是放在类实例的数据成员之前,且虚函数表指针总是在虚基类表指针之前。因而,对于某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在类实例的0字节偏移处,也可能在类实例的4字节偏移处(对于32位程序来说),这给类成员函数指针的实现带来了很大麻烦。

一个类的虚基类指针指向的虚基类表的首个条目,该条目的值是虚基类指针到整个类实例内存首地址的偏移值。即obj.vbtbl - &obj。虚基类第2、第3、… 个条目依次为该类的最左虚继承父类、次左虚继承父类、…的内存地址相对于虚基类表指针自身地址(即 &vbtbl)的偏移值。

如果一个类同时有虚继承的父类与祖父类,则虚祖父类放在虚父类前面。

case 2:class A 含有一个 char 时,还是上述种类:

#include <iostream>
using namespace std;
class A { char ch; };
class B : public A { };

class C : public A { char ch; };
class C_ : public A { char ch; int val; };   // C_

class X : virtual public A { };
int main() {
std::cout << sizeof(A) << std::endl;    // 1
std::cout << sizeof(B) << std::endl;    // 1
std::cout << sizeof(C) << std::endl;    // 2
std::cout << sizeof(C_) << std::endl;   // 8   // 1 + 3(对齐数值)+ sizeof(int) = 8
std::cout << sizeof(X) << std::endl;    // 8   // 1 + 3(对齐数值)+ sizeof(vptr) = 8
}


对齐就行了。

case 3:特殊一点的

#include <iostream>
using namespace std;
class A { char ch; };
class B : virtual public A { char ch; };
int main() {
std::cout << sizeof(A) << std::endl;    // 1
std::cout << sizeof(B) << std::endl;   //8
}


输出结果分析:8 这个结果或许有点惊讶,并没有直接 char + char = 2,然后再对齐至 4 字节。因为实际上这是错误的。

派生类不会填补基类花在内存对齐上的无用空间!

看一下《深度探索C++对象模型》原话,其中 Concrete1 为基类,Concreae2 为派生类 :

也就是说发生 Concrete1 subobject 的复制操作时,就会破坏 Concrete2 members。即基类子对象复制到派生类,会覆盖掉派生类后来增加的成员。

case 4:(重点)关于虚基类有2个例子,这是第一个:

[cpp] view plain copy

print?

class A
a168
{ };

class B : virtual public A { };

class C : virtual public A { };

class X : public B, public C { };

std::cout<
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: