1. 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++编译器生成的C++类实例,虚函数与虚基类地址偏移值共用一个虚表(vtable)。类实例的开始处即为指向所属类的虚指针(vptr)。实际上,一个类与它的若干祖先类(父类、祖父类、...)组成部分共用一个虚表,但各自使用的虚表部分依次相接、不相重叠。
g++编译下,一个类实例的虚指针指向该类虚表中的第一个虚函数的地址。如果该类没有虚函数(或者虚函数都写入了祖先类的虚表,覆盖了祖先类的对应虚函数),因而该类自身虚表中没有虚函数需要填入,但该类有虚继承的祖先类,则仍然必须要访问虚表中的虚基类地址偏移值。这种情况下,该类仍然需要有虚表,该类实例的虚指针指向类虚表中一个值为0的条目。
该类其它的虚函数的地址依次填在虚表中第一个虚函数条目之后(内存地址自低向高方向)。虚表中第一个虚函数条目之前(内存地址自高向低方向),依次填入了typeinfo(用于RTTI)、虚指针到整个对象开始处的偏移值、虚基类地址偏移值。因此,如果一个类虚继承了两个类,那么对于32位程序,虚继承的左父类地址偏移值位于vptr-0x0c,虚继承的右父类地址偏移值位于vptr-0x10.
一个类的祖先类有复杂的虚继承关系,则该类的各个虚基类偏移值在虚表中的存储顺序尊重自该类到祖先的深度优先遍历次序。
Microsoft Visual C++与虚继承
一个类的虚基类指针指向的虚基类表的首个条目,该条目的值是虚基类指针到整个类实例内存首地址的偏移值。即obj.vbtbl - &obj。虚基类第2、第3、... 个条目依次为该类的最左虚继承父类、次左虚继承父类、...的内存地址相对于虚基类表指针自身地址(即 &vbtbl)的偏移值。
如果一个类同时有虚继承的父类与祖父类,则虚祖父类放在虚父类前面。
case 2:class A 含有一个 char 时,还是上述种类:
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 { };
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
输出结果分析:注意内存对齐就行了。
class A { char ch; };
class B : virtual public A { char ch; };
std::cout<<sizeof(A)<<std::endl; // 1
std::cout<<sizeof(B)<<std::endl; // 8
输出结果分析:8 这个结果或许有点惊讶,并没有直接 char + char = 2,然后再对齐至 4 字节。因为实际上这是错误的。
case 4:(重点)关于虚基类有2个例子,这是第一个:
class A { };
class B : virtual public A { };
class C : virtual public A { };
class X : public B, public C { };
std::cout<<sizeof(A)<<std::endl; // 1
std::cout<<sizeof(B)<<std::endl; // 4
std::cout<<sizeof(C)<<std::endl; // 4
std::cout<<sizeof(X)<<std::endl; // 8
case 5:同样关于虚基类
class A { char ch; };
class B : virtual public A { };
class C : virtual public A { };
class X : public B, public C { };
std::cout<<sizeof(A)<<std::endl;
std::cout<<sizeof(B)<<std::endl;
std::cout<<sizeof(C)<<std::endl;
std::cout<<sizeof(X)<<std::endl;这次就能更好体现虚基类只有一份的说法了。B 和 C 大小都是各自 vptr 大小 + 针对 char 的内存对齐 = 8。但是针对类 X,它的大小是仅有一份的A的大小,以及 B 和 C 的vptr的大小,所以是 4 + 4 + 4 = 12。
case 6:注意这种全为空
class A { };
class B : public A {};
class C : public A {};
class D : public B, public C {};
std::cout<<sizeof(D)<<std::endl; // 2这个大小为2哦,编译器至少要确定是从两路继承来的,所以至少需要两个字节。
2. Data member 的绑定
extern float x;
class Point3d
{
public:
point3d();
//问题:被传回和被设定的x是哪一个x呢?
float X() const
{
return x;
}
private:
float x, y, z;//Point3d::X()将返回内部的x。
};
- 把所有的数据成员放在 class 声明的开头,而不是下面,以保证正确的绑定。这就是所谓的以数据为中心。
- 把所有的内联函数,不管大小都放在 class 的声明之外。
typedef int length;
class Point3d
{
public:
//length 将被决议为global
//_val将被决议为Point3d::_val
void mumbel(length val)
{
_val = val;
}
length mumble()
{
return _val;
}
private:
//导致 _val = val; return _val;不合法
typedef float length;
length _val;
};
3. Data Member 的存取
Point3d origin *pt = &origion;
origin.x = 0.0;
pt->x = 0.0;
这两种方式差异?
- px->x = 0.0,事实上是经由一个”implicit class onject“(由 this 指针表达式)完成,即 this->x = pt.x = 0.0
- origin.x = 0.0,事实上欲对一个 nonstatic data member 进行存取操作,编译器需要把 class object 的起始地址加上 data member 的偏移量。那么此表达式实际上等于;&origin + (&Point3d::_y - 1)
4. 继承与 Data Member
- 重复设计一些相同的操作函数
- 把一个 class 分解为两层或者更多层,有可能会为了“表现 class 体系之抽象画” 而膨胀所需空间。因为内存对齐。
- 导入一个 vtbl 和 一个 vptr 的负担
- constructor 和 destructor 要负责初始化和抹消 vtbl 和 vptr 的负担。
- 放置在尾部保留 C struct 的对象布局,C++初期,这种方式被许多人采用。
- 放在最前端。对于 “在多重继承之下,通过指向 class members 的指针调用 virtual function" 会带来一些帮助。当然代价就是丧失 C 语言的兼容性。
Point3d p3d;
Point2d *p = &p3d; 多重继承的问题主要发生于derived class object 和其第二或后续的base class object之间的转换:
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
pv = &v3d;
// 需要这样的内部转换:
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
// 下列转换只需要简单地拷贝地址就行了
p2d = &v3d;
p3d = &v3d;
多重继承的一个语意上的副作用就是,它必须支持某种形式的“shared subobject继承”。
5.指向 Data Members 的指针
class Point
{
public:
virtual ~Point();
static Point origion;
float x,y,z;
};如果vptr放在对象的尾端,则三个坐标值在对象布局中的offset分别是0,4,8。如果vptr放在对象的起头,则三个坐标值在对象布局中的offset分别是4,8,12.然而你若去取data members的地址,
传回的值总是多1, 就是1,5,9,或5,9,13。
std::cout<<&point::x<<std::endl;
float point::*p1 = 0;
float point::*p2 = &point::x;
if(p1 == p2){
std::cout<<"same"<<std::endl;
相等即可,只不过不可能相等,除非减掉 1。
2361



被折叠的 条评论
为什么被折叠?



