继承的核心语义
首先要明白,什么是继承?继承的核心语义如下:
Child
=Base
+Diff(Child)
即继承就是基类与 子类相对基类不同 的内容进行连接合并。
这里有一个问题:对于 覆盖 的情况是怎么样的呢?
答案是:覆盖实际上会保留 基类和子类两份样本。
验证如下:
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : public Base {
public:
int val;
Child1(int v) : Base(v), val(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : public Base {
public:
Child2(int v) : Base(v) {}
void print() {
cout << "Child2" << endl;
}
};
int main()
{
Child1* ptr1 = new Child1(1);
ptr1->print();
Child2* ptr2 = new Child2(2);
ptr2->print();
cout << endl;
cout << reinterpret_cast<Base*>(ptr1)->val << endl; // 输出 1
cout << ptr1->val << endl; // 输出 2
}
由上述测试代码中输出的 1
和 2
可以证明保留了两份 样本,另外对于 非虚继承,基类对象的内存是位于 子类对象内存头部的。
菱形继承的问题
在 C++ 中,允许继承多个基类,且基类的构造顺序与初始化列表相同(见 [[基于文法分析关键字]] 可理解);而析构顺序与构造顺序相反。
多继承可能产生菱形继承问题,如下继承结构:
根据 [[虚拟内存]] 可知,对象的继承的本质是子对象是父对象的超集,父对象的内存数据占据子对象的头部,而对于多继承,很容易可以猜测到: 多继承就是多个父类按初始化列表顺序占据子对象的头部。(内存布局如右图所示)。
所以 菱形继承 内存布局有什么问题吗?实际上是没问题的!因为实际上完全可以根据内存布局区分是哪个变量。如下代码可以正常运行:
cpp
#include <iostream>
class Base {
public:
int val;
};
class Child1 : public Base {
};
class Child2 : public Base {
};
class SubChild : public Child1, public Child2 {
};
int main()
{
SubChild sc;
sc.Child1::val = 1;
sc.Child2::val = 2;
std::cout << sc.Child1::val << std::endl;
std::cout << sc.Child2::val << std::endl;
// std::cout << sc.val << std::endl; // 此行编译不通过:val 不明确
std::cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(&sc) + sizeof(Child1)) << std::endl; // 输出 sc.Child2::val = 2,此行证明了内存布局
return 0;
/**
输出: 1 2
*/
}
从上面代码可以看出,只要显式指示偏移(如指定某个基类) ,甚至基于指针偏移进行暴力推导完全是可以获得真实的变量的,真正的问题是 不优雅 & 无法实现基类属性的重用。
虚继承的核心原理
虚继承的核心语义
如果上述的代码无法满足需求,那么代表我们实际希望的是: Base::val 在整个继承体系中保留唯一的一份。
结合 [[基于文法分析关键字]] 可以知道,语法分析需要建立 AST 抽象语法树 ,这样的继承关系就决定了 Child 1
和 Child 2
都需要获得完整语义的 AST 节点 ,也就表明 val
在 SubChild
必然存在两份。
要想解决这个问题,实际上要表达的语义是:
SubChild = Base + Diff(Child1) + Diff(Child2)
,其中Diff
表示子类与基类的差异。
如果不使用 虚继承 ,上述计算就会出现问题: Diff(Child1)
和 Diff(Child2)
在编译 Child1
和 Child2
就已经被确定了,这没有问题。但是 Base
在编译时也被确定了,这样将导致直接拷贝Base
到子类,导致SubChild
中保留了两份样本,解决方案就是 延后确定 Base
。
那么有什么办法可以实现 延后确定 呢?而且此情况还要考虑到多态,所以需要一种运行时方案。
一个合理的方案就是
Base
保存在运行时信息中,也就是说,存储Child
类型 和Base
类型的内存偏址以便在运行时获取。
虚继承的内存布局
既然涉及 virtual
关键字,那么有理由猜测,虚继承的内存本质就是 编译时计算子类类型与基类类型的偏移并保存在 运行时信息中,也就是说
Child
=Base
+Diff(Child)
中的 Base
实际保存的是 Base
在子类的偏址。即
Child
=Offset(Base)
+Diff(Child)
运行时再根据 Offset(Base)
访问基类内存。
测试代码:
测试代码 1: 无虚继承
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : public Base {
public:
int val;
Child1(int v) : Base(v), val(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : public Base {
public:
Child2(int v) : Base(v) {}
void print() {
cout << "Child2" << endl;
}
};
int main()
{
Child1* ptr1 = new Child1(1);
ptr1->print(); // Child 1
Child2* ptr2 = new Child2(2);
ptr2->print(); // Child 2
cout << endl;
cout << ptr1->val << endl; // 2
cout << reinterpret_cast<Base*>(ptr1)->val << endl; // 1
cout << ptr1->val << endl; // 2
cout << endl;
cout << ptr2->val << endl; // 2
cout << endl;
cout << *reinterpret_cast<int*>(ptr1) << endl; // 1
cout << *reinterpret_cast<int*>(dynamic_cast<Base*>(ptr1)) << endl; // 1
cout << *reinterpret_cast<int*>(reinterpret_cast<Base*>(ptr1)) << endl; // 1
cout << endl;
cout << *reinterpret_cast<int*>(ptr2) << endl; // 2
}
由 [[C++ 类型转换]] 知 reinterpret_cast
不涉及运行时信息,而 dynamic_cast
依赖运行时信息进行转换,这两者的差异可以被用于测试。
有上述代码可知:当不存在虚继承时,基类内存布局是直接在子类内存布局头部的。代码中也体现了覆盖的特点。
测试代码 2: 存在虚继承
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : virtual public Base {
public:
int val;
Child1(int v) : Base(v), val(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : public Base {
public:
Child2(int v) : Base(v) {}
void print() {
cout << "Child2" << endl;
}
};
int main()
{
Child1* ptr1 = new Child1(1);
ptr1->print(); // Child 1
Child2* ptr2 = new Child2(2);
ptr2->print(); // Child 2
cout << endl;
cout << ptr1->val << endl; // 2
cout << reinterpret_cast<Base*>(ptr1)->val << endl; // 470137916
cout << endl;
cout << ptr2->val << endl; // 2
cout << endl;
cout << *reinterpret_cast<int*>(ptr1) << endl; // 470137916
cout << *reinterpret_cast<int*>(dynamic_cast<Base*>(ptr1)) << endl; // 1
cout << *reinterpret_cast<int*>(reinterpret_cast<Base*>(ptr1)) << endl; // 470137916
cout << endl;
// 访问
char* tempPtr = reinterpret_cast<char*>(ptr1);
auto offset = reinterpret_cast<char*>(dynamic_cast<Base*>(ptr1)) - tempPtr;
cout << offset << endl; // 16
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr1) + offset) << endl; // 1
cout << endl;
cout << *reinterpret_cast<int*>(ptr2) << endl; // 2
}
由上述代码可知,Base
与 Child 1
的 offset
是保存在运行时的,而且 Base
内存布局在 Child 1
内存布局之后 (offset > 0
),这也就表明: 虚继承不是直接拷贝内存布局,而是编译保留 offset
参数(对象所有)。并在实例化时根据 offset
填充子类完整结构。
值得注意的是:
offset
参数被继承后(连接操作)仍为offset
,这也就表明如果Child1
虚继承Base
,而SubChild1
继承/虚继承Child1
都需要为Base
进行初始化,因为只有这样offset
指向的内存才能被初始化。
测试代码 3: 虚继承 + 非虚继承
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : virtual public Base {
public:
int val;
Child1(int v) : Base(v), val(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : public Base {
public:
Child2(int v) : Base(v) {}
void print() {
cout << "Child2" << endl;
}
};
class SubChild1 : public Child1 {
public:
SubChild1(int v) : Base(v), Child1(v) {}
};
int main()
{
Child1* ptr1 = new Child1(1);
ptr1->print(); // Child 1
Child2* ptr2 = new Child2(2);
ptr2->print(); // Child 2
cout << endl;
cout << ptr1->val << endl; // 2
cout << reinterpret_cast<Base*>(ptr1)->val << endl; // 826326076
cout << ptr1->val << endl; // 2
cout << endl;
cout << ptr2->val << endl; // 2
cout << endl;
cout << *reinterpret_cast<int*>(ptr1) << endl; // 826326076
cout << *reinterpret_cast<int*>(dynamic_cast<Base*>(ptr1)) << endl; // 1
cout << *reinterpret_cast<int*>(reinterpret_cast<Base*>(ptr1)) << endl; // 826326076
cout << endl;
char* tempPtr = reinterpret_cast<char*>(ptr1);
auto offset = reinterpret_cast<char*>(dynamic_cast<Base*>(ptr1)) - tempPtr;
cout << offset << endl; // 16
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr1) + offset) << endl; // 1
cout << endl;
cout << *reinterpret_cast<int*>(ptr2) << endl; // 2
cout << endl;
SubChild1* ptr3 = new SubChild1(4);
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr3) + offset) << endl; // 4
cout << *reinterpret_cast<int*>(ptr3) << endl; // 826326076
}
从上述代码可知,Child1
虚继承Base
,连接了 Base
的 offset
参数,而 SubChild1
连接了 Child1
,故 SubChild1
头部为 Child1
对象内存布局,且 offset
参数相同。故可推测内存布局为:
测试代码4 : 虚继承解决菱形继承
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : virtual public Base {
public:
Child1(int v) : Base(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : virtual public Base {
public:
Child2(int v) : Base(v + 2) {}
void print() {
cout << "Child2" << endl;
}
};
class SubChild1 : public Child1, public Child2 {
public:
SubChild1(int v) : Base(v), Child1(v), Child2(v) {}
};
int main()
{
SubChild1* ptr = new SubChild1(1);
ptr->Child1::print(); // Child 1
ptr->Child2::print(); // Child 2
cout << endl;
cout << ptr->val << endl; // 由于存储的是 offset, Base 最后初始化,所以 val 为 1
cout << endl;
Child1* child1 = new Child1(-1);
Child2* child2 = new Child2(0);
auto offset1 = reinterpret_cast<char*>(dynamic_cast<Base*>(child1)) - reinterpret_cast<char*>(child1);
auto offset2 = reinterpret_cast<char*>(dynamic_cast<Base*>(child2)) - reinterpret_cast<char*>(child2);
cout << offset1 << endl; // 8
cout << offset2 << endl; // 8
cout << endl;
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset1) << endl; // -1109083096
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset2) << endl; // -1109083096
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset2 + sizeof(Child1)) << endl; // -33686019
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset1 + offset2) << endl; // 1
return 0;
}
测试代码5:收官之战
通过上述分析,基本可以得到下列多继承内存布局:
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : virtual public Base {
public:
Child1(int v) : Base(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : virtual public Base {
public:
int head_val1, head_val2;
Child2(int v) : Base(v + 2), head_val1(10), head_val2(11) {}
void print() {
cout << "Child2" << endl;
}
};
class Child3 : virtual public Base {
public:
int head_val3, head_val31;
Child3(int v) : Base(v + 3), head_val3(12), head_val31(13) {}
};
class SubChild1 : public Child1, public Child2, public Child3 {
public:
SubChild1(int v) : Base(v), Child1(v), Child2(v), Child3(v) {}
};
int main()
{
SubChild1* ptr = new SubChild1(1);
ptr->Child1::print(); // Child 1
ptr->Child2::print(); // Child 2
cout << endl;
cout << ptr->val << endl; // 由于存储的是 offset, Base 最后初始化,所以 val 为 1
cout << endl;
Child1* child1 = new Child1(-1);
Child2* child2 = new Child2(-1);
Child3* child3 = new Child3(-1);
auto offset1 = reinterpret_cast<char*>(dynamic_cast<Base*>(child1)) - reinterpret_cast<char*>(child1);
auto offset2 = reinterpret_cast<char*>(dynamic_cast<Base*>(child2)) - reinterpret_cast<char*>(child2);
auto offset3 = reinterpret_cast<char*>(dynamic_cast<Base*>(child3)) - reinterpret_cast<char*>(child3);
cout << offset1 << endl; // 8
cout << offset2 << endl; // 16
cout << offset3 << endl; // 16
cout << endl;
/**
* 考虑到 Child1 无多余成员 (child1 other 为空),所以 offset1 可视为 offset 指针自身大小
*/
char* head_ptr = reinterpret_cast<char*>(ptr) + offset1 + offset1; // 此处为 child2 other
cout << *reinterpret_cast<int*>(head_ptr) << endl; // 10
cout << *reinterpret_cast<int*>(head_ptr + sizeof(int)) << endl; // 11;
char* head_ptr_2 = reinterpret_cast<char*>(ptr) + offset1 + offset2 + offset1; // 此处为 child3 other
cout << *reinterpret_cast<int*>(head_ptr_2) << endl; // 12
cout << *reinterpret_cast<int*>(head_ptr_2 + sizeof(int)) << endl; // 13
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset1 + offset2 + offset3) << endl; // 1
return 0;
}
从代码测试结果分析可猜测:
offset
参数是嵌入对象的,而不属于类,否则不应该占据大小。同时,offset
实际上表达的是 对象头部到末尾的偏移量(因为offset
参数位于头部)。- 虚继承只会连接
offset
参数和Diff
,而非虚继承会连接整个基对象。 - 只要基类被虚继承,后续继承链无论是否虚继承,
offset
参数都会创建于对象中。故后续继承链都需要为offset
指向的基类调用构造函数(特别是非默认构造函数)。
测试代码6:再次佐证
下列代码综合了虚继承和非虚继承/多继承和多重继承,再次佐证上述猜想:
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child1 : virtual public Base {
public:
Child1(int v) : Base(v + 1) {}
void print() {
cout << "Child1" << endl;
}
};
class Child2 : virtual public Base {
public:
int head_val1, head_val2;
Child2(int v) : Base(v + 2), head_val1(10), head_val2(11) {}
void print() {
cout << "Child2" << endl;
}
};
class Child3 : virtual public Base {
public:
int head_val3, head_val31;
Child3(int v) : Base(v + 3), head_val3(12), head_val31(13) {}
};
class SubChild1 : public Child1, public Child2, public Child3 {
public:
SubChild1(int v) : Base(v), Child1(v), Child2(v), Child3(v) {}
};
class SubChild2 : public SubChild1 {
public:
int head_val4;
SubChild2(int v) : SubChild1(v), Base(v + 4),head_val4(14) {};
};
int main()
{
SubChild1* ptr = new SubChild2(1);
ptr->Child1::print(); // Child 1
ptr->Child2::print(); // Child 2
cout << endl;
cout << ptr->val << endl; // 由于存储的是 offset, Base 最后初始化,所以 val 为 1
cout << endl;
Child1* child1 = new Child1(-1);
Child2* child2 = new Child2(-1);
Child3* child3 = new Child3(-1);
SubChild1* child4 = new SubChild1(-1);
SubChild2* child5 = new SubChild2(-1);
auto offset1 = reinterpret_cast<char*>(dynamic_cast<Base*>(child1)) - reinterpret_cast<char*>(child1);
auto offset2 = reinterpret_cast<char*>(dynamic_cast<Base*>(child2)) - reinterpret_cast<char*>(child2);
auto offset3 = reinterpret_cast<char*>(dynamic_cast<Base*>(child3)) - reinterpret_cast<char*>(child3);
auto offset4 = reinterpret_cast<char*>(dynamic_cast<Base*>(child4)) - reinterpret_cast<char*>(child4);
auto offset5 = reinterpret_cast<char*>(dynamic_cast<Base*>(child5)) - reinterpret_cast<char*>(child5);
cout << offset1 << endl; // 8
cout << offset2 << endl; // 16
cout << offset3 << endl; // 16
cout << offset4 << endl; // 40 = 8 + 16 + 16
cout << offset5 << endl; // 48 = 40 + 8 ,offset 参数自身大小 + 总偏移
cout << endl;
/**
* 考虑到 Child1 无多余成员 (child1 other 为空),所以 offset1 可视为 offset 指针自身大小
*/
char* head_ptr = reinterpret_cast<char*>(ptr) + offset1 + offset1; // 此处为 child2 other
cout << *reinterpret_cast<int*>(head_ptr) << endl; // 10
cout << *reinterpret_cast<int*>(head_ptr + sizeof(int)) << endl; // 11;
char* head_ptr_2 = reinterpret_cast<char*>(ptr) + offset1 + offset2 + offset1; // 此处为 child3 other
cout << *reinterpret_cast<int*>(head_ptr_2) << endl; // 12
cout << *reinterpret_cast<int*>(head_ptr_2 + sizeof(int)) << endl; // 13
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset1 + offset2 + offset3) << endl; // 14
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset4 + offset1) << endl; // 5
cout << *reinterpret_cast<int*>(reinterpret_cast<char*>(ptr) + offset5) << endl; // 5
return 0;
}
测试代码7:扫描 offset
优先于初始化列表
下列代码展示了扫描 offset
参数可以导致构造函数的插队调用:
cpp
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base" << endl;
}
~Base() {
cout << "~Base" << endl;
}
};
class Child : public Base {
public:
Child() {
cout << "Child" << endl;
}
~Child() {
cout << "~Child" << endl;
}
};
class SubChild : virtual public Child {
public:
SubChild() {
cout << "SubChild" << endl;
}
~SubChild() {
cout << "~SubChild" << endl;
}
};
class SubChild2 : public Child {
public:
SubChild2() {
cout << "SubChild2" << endl;
}
~SubChild2() {
cout << "~SubChild2" << endl;
}
};
class SubChild3 : public Child {
public:
SubChild3() {
cout << "SubChild3" << endl;
}
~SubChild3() {
cout << "~SubChild3" << endl;
}
};
class Final : public SubChild2, public SubChild, public SubChild3 {
public:
Final() : SubChild2(), SubChild3(), SubChild() {
cout << "Final" << endl;
}
~Final() {
cout << "~Final" << endl;
}
};
int main()
{
Final fObj;
cout << endl;
/* Base
* Child
* Base
* Child
* SubChild2
* SubChild
* Base
* Child
* SubChild3
* Final
*
* ~Final
* ~SubChild3
* ~Child
* ~Base
* ~SubChild
* ~SubChild2
* ~Child
* ~Base
* ~Child
* ~Base
*/
return 0;
}
从上述代码可知,offset
的扫描顺序优先于 继承顺序 (注意 继承顺序 优先于初始化列表顺序 ),由于 Final
对象存在 offset
参数,故优先扫描 offset
参数,故初始化 Child
,所以输出 Base -> Child
。
接着才是按 继承顺序 : SubChild2
、SubChild
、SubChild3
完成初始化。析构顺序始终与构造顺序相反(本质是堆栈)。
总结
多继承 & 多重继承
- 继承的本质就是对象按初始化列表顺序或按继承依赖树先根序连接。但特殊的是,如果是虚继承,只会先连接
offset
参数;否则将完整拷贝基类,如果前向继承链中任何节点存在虚继承,则还会补充创建一个offset
参数汇总 对象头部到尾部的偏移。- 对于每一个类,只要初始化时扫描到
offset
参数,就需要为offset
参数指向的基类调用构造函数(否则将调用默认构造函数,即offset
指向的基类需要在后续继承链中都指定初始化)。offset
参数的扫描 优先于 继承顺序 ,继承顺序 优先于 初始化列表顺序 。
多继承可能导致工程级问题
从上述 核心原理 可知:虚继承将影响整条调用链,虚继承后的继承链的对象都会属于本类的 offset
参数,而被虚继承的 offset
指向的基类的构造函数是扫描当前类对象的 offset
参数时调用的,这就表明:
被虚继承的基类的构造函数最好是手动指定,否则将调用默认构造函数。
问题代码:含参虚基类构造耦合
下列代码是一段可能产生 bug 的代码:
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base() : val(-1) {}
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child : virtual public Base {
public:
Child() : Base(2) {};
void print() {
cout << val << endl;
}
};
class SubChild : public Child {
public:
SubChild() : Child(), Base(3) {};
};
class Final : public SubChild {
};
int main()
{
Final fObj;
cout << fObj.val << endl; // -1
fObj.print(); // -1
return 0;
}
在上述代码中,fObj.val
输出 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1 而不是 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 2 </math>2 或 <math xmlns="http://www.w3.org/1998/Math/MathML"> 3 3 </math>3。这表明Child
类 和 SubChild
的构造函数都没有隔离 Base
的含参构造函数。这很可怕,因为SubChild
是非虚继承 Child
的,这在复杂的工程中可能不容易被发现。
如果这是一条非常复杂的继承链,那么我们将难以得知 Child
是 虚继承 Base
的。这时候Base
的构造函数是在扫描Final
类对象的offset
时调用的,那么可能中间依赖于Base
含参构造函数 的方法可能出现重大问题(如上述代码中的 print
函数),从而导致整条继承链出现重大问题。
也就是说:只要出现虚继承,可能整条继承链的对被虚继承类的构造函数调用都要重新审查,这将是一个工程级的问题。
替代方案:组合
可以考虑采用 组合 (即基类对象 作为 子类对象的成员 )来进行隔离。事实上,继承的本质是连接,与组合 在内存布局上表示是相似的,不同的是:编译器会为 继承 加入 虚函数、虚继承 等多态性支持,而 组合 需要程序员自行实现与维护多态(如果不需要可以不实现)。
采用 组合 可以隔离 虚继承 造成了 offset
在整条继承链上的传递。如下代码:
cpp
#include <iostream>
using namespace std;
class Base {
public:
int val;
Base() : val(-1) {}
Base(int v) : val(v) {}
void print() {
cout << "Base" << endl;
}
};
class Child : virtual public Base {
public:
Child() : Base(2) {};
void print() {
cout << val << endl;
}
};
class SubChild : public Child {
public:
SubChild() : Child(), Base(3) {};
};
class Final {
public:
SubChild sc;
};
int main()
{
Final fObj;
cout << fObj.sc.val << endl; // 3
fObj.sc.print(); // 3
return 0;
}
从上述代码可知,使用 组合 模式,可以将 offset
的传递终止于 SubChild
,继而不会影响 Final
。