C++ 多继承底层原理、潜在问题&组合解决方案

继承的核心语义

首先要明白,什么是继承?继承的核心语义如下:

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
}

由上述测试代码中输出的 12 可以证明保留了两份 样本,另外对于 非虚继承,基类对象的内存是位于 子类对象内存头部的。

菱形继承的问题

在 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 1Child 2 都需要获得完整语义的 AST 节点 ,也就表明 valSubChild 必然存在两份。

要想解决这个问题,实际上要表达的语义是:SubChild = Base + Diff(Child1) + Diff(Child2),其中 Diff 表示子类与基类的差异。

如果不使用 虚继承 ,上述计算就会出现问题: Diff(Child1)Diff(Child2) 在编译 Child1Child2 就已经被确定了,这没有问题。但是 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
}

由上述代码可知,BaseChild 1offset 是保存在运行时的,而且 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,连接了 Baseoffset 参数,而 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

接着才是按 继承顺序 : SubChild2SubChildSubChild3 完成初始化。析构顺序始终与构造顺序相反(本质是堆栈)。

总结

多继承 & 多重继承

  • 继承的本质就是对象按初始化列表顺序或按继承依赖树先根序连接。但特殊的是,如果是虚继承,只会先连接 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

相关推荐
我是谁??5 分钟前
C/C++使用AddressSanitizer检测内存错误
c语言·c++
发霉的闲鱼39 分钟前
MFC 重写了listControl类(类名为A),并把双击事件的处理函数定义在A中,主窗口如何接收表格是否被双击
c++·mfc
小c君tt41 分钟前
MFC中Excel的导入以及使用步骤
c++·excel·mfc
xiaoxiao涛1 小时前
协程6 --- HOOK
c++·协程
羊小猪~~3 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德4 小时前
多项式加法——C语言
数据结构·c++·算法
legend_jz4 小时前
STL--哈希
c++·算法·哈希算法
CSUC4 小时前
【C++】父类参数有默认值时子类构造函数列表中可以省略该参数
c++
Vanranrr4 小时前
C++ QT
java·c++·qt
鸿儒5174 小时前
C++ lambda 匿名函数
开发语言·c++