深度搜索C++对象模型: Data Semantics
//*********************************************************************************************//
---> "虚拟继承的 sizeof 问题":
class X { };
class A : virtual public X { };
class B : virtual public X { };
class Y : public A, public B { };
对他们分别取sizeof 输出的结果为 : 1 4 4 8;
对于class X来说, 他里面没有任何东西 但是编译器会安插进去一个char, 使得class X的两个object得以在内存中配置独一无二的地址
对于class A和class B来说, 他们内部是虚拟继承的 所以编译器会安插一根虚拟继承指针, 这根指针为4字节, 这根指针指向了virtual base class;
对于class Y来说, 他继承了A和B的两根虚拟继承指针 所以大小为8;
对于类的大小 有三个影响因素:
--> 语言本身造成的额外负担: 语言本身支持了 virtual base class, 就会造成一些负担( 一根虚拟继承指针 );
--> 编译器对于特殊情况提供的优化处理: 照理说class A和class B也该继承class X的一字节char, 但是已经没有必要了 所以编译器优化掉了;
--> 对齐限制: 内存需要对齐, 内存大小必须为最大存储长度的整数倍;
class X { int m_num; };
class A : virtual public X { };
class B : virtual public X { };
class Y : public A, public B { };
对他们分别取sizeof 输出的结果为 : 4 8 8 12;
对于class X来说, 他里面有一个int类型数据 大小自然为4字节;
对于class A和class B来说 他是虚拟继承 所以编译器安插了一根虚拟继承指针( 4字节 ), 而他又继承了class X的一个int数据( 4字节 ), 所以一共8字节;
对于class Y来说, 如果"抛开虚拟继承" 那么他的大小应该是"父类大小和 8 + 8 = 16"; 但是Y的一个实体里面包含了两份X的拷贝;
所以提出了虚拟继承,虚拟继承之后Y的实体里只包含一份X的拷贝 所以是 4 + 4 + 4 = 12;
//-----------------------------------------------------------------------------------------------------------------------//
---> "Data Member 的布局":
class A
{
int a;
bool b;
bool c;
};
class X
{
int a;
};
class Y : public X
{
bool b;
};
class Z : public Y
{
bool c;
};
对他A和Z分别取sizeof 输出的结果为 : 8 12;
先分析class A, class A中的三个变量 必须符合"较晚出现的必须有较高的地址" 通俗点就说 顺序存储. 又由于内存对其 所以 4 + 1 + 1 + 2 = 8;
而对于class Z, 他继承了2个变量a b,和一个自己的变量c, 但是他的存储方式为 先构造X 在构造Y 在最后构造Z 结果为 4 + 1 + 3 + 1 + 3 = 12; ( 后文分析 )
对于非静态成员变量 他直接被放到了 class object 中, 除非经过显示或者隐式的 class object 否则无法存取, 事实上就是( this );
//-----------------------------------------------------------------------------------------------------------------------//
---> "继承与 Data Member":
在C++继承模型中, 一个派生类所表现出来的东西, 是其自己的成员加上他基类成员的综合, 至于他们的排列顺序 并无规定
( VS2012 基类在上头, 但是属于 virtual base class 除外, 因为任何一个规则碰上他 都完蛋 );
--> "只要继承不要多态":
对于只继承来说 单纯只是把基类成员放在上头, 派生类成员放在下头就结束了,
回头看上面的问题 为什么是 8 12 ? 为什么编译器不把 bool c 填充到前面空下的3字节空间里 ?
X px_1, px_2;
Y py_1;
其中px_1, px_2 都可以指向X Y Z三种class object;
*px_2 = *px_1;
此时 发生的是一个默认的"位逐次拷贝"复制操作, "复制对象 object 的 X 那部分";
( 如果 px_1 指向Y或Z的class object, 则上述操作应该将复制内容指定给其X的子对象 )
然而 如果如果C++把 bool c 填充到前面的3字节空间里:
px_1 = py_1;
*px_2 = *px_1;
让px_1指向了py_1之后, 如果发生了 某个 object 的 X subobject 的复制操作的时候, 就会使得结果有一个出乎意料的结果. 多出来了 Y subobject 的内容;
--> "加上多态":
class A
{
public:
int m_num1;
virtual void Func1() { };
};
class B : public A
{
public:
int m_num2;
virtual void Func1() { };
virtual void Func2() { };
};
对于类A, 他内含有一个虚函数 而继承了他的类B 也自然包含了继承而来的虚函数, 而且类B还有了一个自己的虚函数,
那么类A和类B在编译时就会自动产生一个 virtual table, 然后在 class object 中内插一根 vptr;
( 对于class A, 这个virtual table中只有func1(), 对于class B, 这个virtual table有func1() 和 func2();
那么class A的vptr指向只有func1()的vtbl, class B的vptr指向有func1()和func2()的vtbl );
对于C++来说 这根class object内的vptr放在那 ? 结论是可以是任何地方 并没有规定 但是一般放在头尾
( 经过测试 VS2013 把 vptr 放在了起始 )
对于上述类B的内存分布:
vptr -> m_num1 -> m_num2
如果上述类A和类B中没有Func1(); 即基类无虚函数,而派生类有虚函数
m_num1 -> vptr -> m_num2
--> "多重继承":
单一继承提供了一种"自然多态"形式, 主要说的是 base type 和 derived type 之间的转换:
A *a;
B *b = new B;
a = b;
这个转换是很自然的 因为 base type 和 derived type 是从同一地址开始的, 唯一的差异是 derived type 比较大;
此时把 derived class object 指定给 base class 的指针 是很自然的 不违反 base class 的解释规则 所以是"自然多态";
但是当派生类型为多重继承或者虚拟继承时 , "自然多态就会被打破":
class A { };
class B : public A { };
class C { };
class Z : public B, public C { };
( 注:所有类都有virtual接口 )
例如 将一个 Z class object 转换为 C class object 为"不自然的"
对一个多重派生对象, 其地址指定为"最左端" base class 的指针 ( class Z 的地址为 class B 的地址 )
第二个或后继的 base class 的地址指定操作, 则需要将地址修改 加上介于中间的 base class subobject大小
例如:
Z z;
A *a = &z; // 0x00000000
B *b = &z; // 0x00000000
C *c = &z; // 0x00000004 ( 因为介于之间的 base class 为 class B, 而sizeof( B ) = 4 )
在来说一下: " &z == c; "
结论是 为真, 为什么为真?
首先我们先判断一下上面发生了什么; 当我们进行 C *c = &z; 的时候 编译器会进行一次强制类型转换
然后编译器发现是多重继承 则按照我们所说的 "地址修改规则" 修改过地址之后 赋值给c 结果c的地址和z的地址不一样了;
那么" &z == c; " 发生了什么? 当我们对 &z 和 c 进行 == 判断的时候; 编译器会对其进行一个 "类型转换",
他会"把 ( Z * ) 的指针 强制转换为 ( C * ) 类型的指针", 说简单点就是 &z 被强转成了 C * 类型
那么由于 == 所带来的副作用 两个不同类型的数比较 会转换为两个类型中较大的类型
那么 " &z == c; " 操作最后实际上还是会把 &z 经过 上面多重继承的 "地址修改规则" 最后 &z == 0x00000004; 而 c == 0x00000004; 所以equl
最后 以上多重继承的内存结构如下: ( 基于VS2013 ) sizeof( Z ) == 8;
vptr_A -> vptr_C
//-----------------------------------------------------------------------------------------------------------------------//
---> "虚拟继承":
class A { };
class X : virtual public A { };
class Y : virtual public A { } ;
class Z : public X, public Y { };
( 所有类都有virtual接口 )
对于方式的原版 无虚拟继承时, Z中会维护两份A的拷贝,
实现虚拟继承, 我们需要找到一个足够有效的办法, 将X和Y格子维护的一个A object 折叠成为一个由Z维护的单一的A object;
而且还需要保存base class 和 derived class的指针以维持多态操作;
一般的实现方法为: 如果一个 class 内含一个或多个 virtual base class subobject( 类似Z ), 那么他将会被分为两部分,
一个不变区和一个共享区:
不变区不管类如何继承, 他都有固定的offset, 可以直接寻址,
而共享区的数据( 其实就是 virtual base class 部分的数据 ), 他们只能被间接寻址,
具体来说( 基于VS2013 )
每一个 class object 如果有一个或多个 virtual base class, 那么编译器会生成一个 virtual base class table, 里面放这 virtual base class 的指针;
然后在 class object 内安插一根指向 virtual base class table 的指针 ( 这就是为什么 virtual 继承会带来4字节的额外负担 )
( 注: 如果一个类 即是虚拟继承 内部又包含虚函数, 那么他应该有两根指针 virtual base class ptr, virtual table ptr )
//*********************************************************************************************//
---> "对象成员的效率":
对于数据封装性来说:
-> 单纯的数据;
-> 数组;
-> 结构;
-> 类内联;
-> 类成员;
如果我们对以上类型进行分析, 则会发现: "在优化之后 他们的效率相同";
结论:"封装不会带来额外的负担";
对于数据继承性来说:
-> 单一继承;
-> 虚拟继承( 单层 );
-> 虚拟继承( 多层 );
如果我们对以上继承进行分析, 则会发现: "在优化之后, 他们效率 开始下降";
结论:"单一继承( 不管层数 ) 不会带来额外负担, 虚拟继承 会带来额外的负担, 而且随着继承深度加深 效率越来越低";
//*********************************************************************************************//