菱形继承和虚函数

此篇博客主要介绍菱形虚拟继承和虚函数的混合情况下的模型分析。

一.首先介绍一下虚函数的实质内容

1.没有虚继承的虚函数

class A
{
public:
	int a;
	virtual void fun1()
	{
		cout << "A::fun1 " << endl;
	}
};
class B :public A
{
public:
	int b;
	virtual void fun1()//函数重写  
	{
		cout << "B::fun1 " << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2" << endl;
	}

};
int main()
{
	B b;//创建一个B的对象  
	A a;
        b.b = 20;
	a.a = 10;
	b.a = 30;
	system("pause");
	return 0;
}
首先我们来看对象a的虚函数机制


接下里我们对B对象虚表里面的 函数进行验证,对象自己验证

typedef void(*fun)();//这里定义fun为函数指针类型
	fun *p = (fun *)*(int*)(&b);
	(*p)();//打印B::fun1
         p++;//指向B::fun2
        (*p)();//打印B::fun2


2.有虚继承的虚函数

首先看这个类的大小:

class A
{
public:
	int a;
	virtual void fun1()
	{
		cout << "A::fun1 "<< endl;
	}
};
class B :virtual public A
{
public:
	int b;
	virtual void fun1()//函数重写
	{
		cout << "B::fun1 " << endl;
	}
	virtual void fun2()
	{
		cout << "B::fun2" << endl;
	}
};
int main()
{
    B b;
    A a;
    b.b=20;
    a.a=10;
    sizeof(B);
    system("pause");
    return 0;
}

刚才无虚继承的类大小是12怎么加了个vurtual虚继承以后就变成了20呢?一下多了八个字节,接下来我们详细分析。


接下来打印验证:

	typedef void(*fun)();//这里定义fun为函数指针类型
	fun *p = (fun *)*(int*)(&b);
	(*p)();
	p += 4;;
	(*p)();

3.菱形继承➕虚函数
首先给出基本代码:

class A
{
public:
	int _a;
	virtual void fun1()
	{
		cout << "A::fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "A::fun2" << endl;
	}
};
class B1 : virtual public A
{
public:
	int _b1;
	virtual void fun1()
	{
		cout << "B1::fun1" << endl;
	}
	virtual void fun3()
	{
		cout << "B1::fun3" << endl;
	}
	
};
class B2 :virtual public A
{
public:
	int _b2;
	virtual void fun1()
	{
		cout << "B2::fun1" << endl;
	}
	virtual void fun4()
	{
		cout << "B2::fun4" << endl;
	}
};
class C :public B1,public B2
{
public:
	int _c;
	virtual void fun1()
	{
		cout << "C::fun1" << endl;
	}
	virtual void fun3()
	{
		cout << "C::fun3" << endl;
	}
	virtual void fun4()
	{
		cout << "C::fun4" << endl;
	}

	virtual void fun5()
	{
		cout << "C::fun5" << endl;
	}
};
int main()
{
	
	cout <<"A:"<< sizeof(A) << endl;
	cout << "B1:"<<sizeof(B1) << endl;
	cout << "B2:"<<sizeof(B2) << endl;
	cout << "C:"<<sizeof(C) << endl;
	A a;
	B1 b1;
	B2 b2;
	C c;
	a._a = 10;
	b1._a = 10;
	b1._b1 = 20;
	b2._b2 = 30;
	b2._a = 10;
	c._c = 40;
	c._a = 10;
	c._b1 = 20;
	c._b2 = 30;
	system("pause");
	return 0;
}

先来看看每个对象的大小然后我在逐个对象分析其模型



注意这里的B是B1,图上写的是B


接下来打印验证

typedef void(*fun)();//这里定义fun为函数指针类型
fun *p = (fun *)*(int*)(&c);
(*p)();//C::fun3
(*(p + 1))();//C::fun5
(*(p + 3))();//C::fun4
(*(p + 5))();//C::fun1
(*(p + 6))();//A::fun2


总结:1.无虚继承时在虚函数中,派生类中的虚函数非基类重写,则将其放置在继承来的虚函数表后面,不重新开辟虚函数表,并且将派生类中重写的虚函数替换基类中的虚函数

             2.有虚继承虚函数时,派生类中的虚函数如果不是基类的重写的话,新开辟一个虚函数表将未重写虚函数放进去,将继承来的虚函数表放在其后面,并且将重写的替换

             3.菱形继承+虚函数时,最重要的一点就是在,最低层的派生类中(上述例子就是C),只有一份基类(A)的虚表,并且在其模型(C模型)的最下面。并且将其没有重写的虚函数放在第一个继承模型(B1)的虚函数表下面,而且将其重写的也要替换为自己的。

<think>好的,我现在需要详细分析用户关于C++菱形继承中构造函数调用顺序的问题。用户提供了多个引用,我需要结合这些引用自己的知识来构建一个准确的回答。 首先,用户的问题集中在菱形继承的构造函数调用顺序,特别是虚继承对调用顺序的影响。根据引用[2]引用[5],菱形继承中使用虚继承时,虚基类的构造函数只会被调用一次,并且在所有非虚基类之前调用。例如,在类D继承自BC,而BC都虚继承自A的情况下,A的构造函数会先被调用,然后是BC的构造函数,最后是D的构造函数。 接下来,我需要确认不同情况下的调用顺序。根据引用[3],当存在虚继承时,虚基类的构造函数会优先于非虚基类调用。如果同一层次中有多个虚基类,它们的构造函数按照声明的顺序执行。例如,如果类D继承自B(虚继承A)C(虚继承A),则A的构造函数先被调用,然后是BC,顺序取决于D中继承声明的顺序。 用户提供的引用[5]中的代码示例显示,类C继承自A1A2,两者都是虚继承自base。在C的构造函数中,虽然显式调用了A1A2的构造函数,但由于虚继承的存在,base的构造函数只会被调用一次。但根据代码示例,base的构造函数没有默认参数,导致编译错误,因为C的构造函数没有显式调用base的构造函数。这提示在虚继承中,最派生类必须直接调用虚基类的构造函数。 需要进一步理清虚继承下构造函数调用的规则。根据C++标准,虚基类的构造函数由最派生类负责调用,中间类的构造函数中对虚基类的调用会被忽略。因此,在菱形继承中,最派生类(如D)必须显式调用虚基类(如A)的构造函数,否则如果虚基类没有默认构造函数,会导致编译错误。 此外,引用[2]指出,虚基类的构造函数在非虚基类之前调用,而非虚基类的构造函数按继承声明顺序调用。例如,如果D继承BC,BC都虚继承A,那么构造顺序是A→B→C→D。 总结用户提供的引用C++标准,菱形继承中的构造函数调用顺序可以归纳为: 1. 虚基类的构造函数按声明顺序优先调用。 2. 非虚基类的构造函数按继承声明的顺序调用。 3. 派生类自身的构造函数最后调用。 用户示例代码中的问题在于,虚基类base没有默认构造函数,而类C的构造函数没有显式调用base的构造函数,导致编译错误。正确的做法是在C的构造函数中直接调用base的构造函数,例如:C(int i): base(i), A1(i+1), A2(i+2){}。 需要进一步验证这一点。根据引用[5]的描述,虚基类的构造函数必须在最派生类中显式调用,否则如果基类没有默认构造函数,会报错。例如,在代码中,C必须调用base的构造函数,即使A1A2已经调用了base的构造函数,但虚继承的情况下,这些调用会被忽略,由C直接调用。 因此,正确的构造顺序应该是:base→A1→A2→C。但由于示例代码中C没有调用base的构造函数,导致编译错误。修正后的代码应显式调用base的构造函数。 最后,需要将分析整理成清晰的步骤,结合示例代码说明调用顺序,并指出常见错误解决方法。同时,确保回答覆盖用户提供的所有相关引用内容,特别是关于虚基类构造函数调用一次调用顺序的要点。</think>### C++菱形继承中构造函数的调用顺序详解 在菱形继承结构中,**虚继承**会显著改变构造函数的调用顺序,其核心规则是**虚基类优先初始化且仅初始化一次**。以下是详细分析: --- ### **一、菱形继承的典型结构** ``` A(虚基类) / \ B C(虚继承自A) \ / D(继承自BC) ``` --- ### **二、构造函数调用顺序规则** 根据C++标准引用[1-5],构造顺序遵循以下优先级: 1. **虚基类构造优先** 所有虚基类的构造函数按**继承声明顺序**优先执行,且仅执行一次。 2. **非虚基类构造次之** 非虚基类的构造函数按**继承声明顺序**执行。 3. **派生类自身构造最后执行** --- ### **三、具体调用顺序分析** #### **1. 虚继承场景下的构造顺序** 以引用[5]的代码为例: ```cpp class base{ /*...*/ }; class A1 : virtual public base { /*...*/ }; class A2 : virtual public base { /*...*/ }; class C : public A1, public A2 { public: C(int i) : A1(i+1), A2(i+2) {} // 错误!未显式调用base的构造函数 }; ``` **错误原因**: 虚基类`base`的构造函数**必须由最派生类`C`直接调用**,即使中间类(如`A1`、`A2`)已显式调用`base`,这些调用菱形继承中会被忽略[^5]。 **修正后的正确调用方式**: ```cpp class C : public A1, public A2 { public: C(int i) : base(i), A1(i+1), A2(i+2) {} // 显式调用虚基类构造 }; ``` **构造顺序**: 1. `base`的构造函数(虚基类优先) 2. `A1`的构造函数(按继承声明顺序`A1`先于`A2`) 3. `A2`的构造函数 4. `C`的构造函数 --- #### **2. 非虚继承对比** 若`A1`、`A2`未使用虚继承: ```cpp class A1 : public base { /*...*/ }; class A2 : public base { /*...*/ }; class C : public A1, public A2 { /*...*/ }; ``` **构造顺序**: 1. `A1`的构造函数 → 调用`base`构造 2. `A2`的构造函数 → 再次调用`base`构造 3. `C`的构造函数 此时`base`会被构造两次,导致数据冗余(菱形继承问题)。 --- ### **四、关键规则总结** | 场景 | 调用顺序规则 | 代码示例 | |--------------------|----------------------------------------------------------------------------|--------------------------------------------------------------------------| | 虚基类构造 | 由最派生类直接调用,且仅调用一次 | `D() : A(), B(), C() { ... }` | | 非虚基类构造 | 按继承声明顺序执行 | `class D : B, C { ... }` → 先`B`后`C` | | 虚基类与非虚基类共存 | 虚基类优先于所有非虚基类构造 | `D() : A(), B(), C() { ... }`(`A`为虚基类) | | 多重虚基类 | 虚基类按声明顺序构造 | `class D : virtual A1, virtual A2 { ... }` → `A1`先于`A2` | --- ### **五、常见问题与解决方案** #### **问题1:编译错误“no matching function for call to 'base::base()'”** **原因**:虚基类`base`没有默认构造函数,且最派生类未显式调用其有参构造函数。 **解决**:在派生类构造函数中显式调用虚基类构造: ```cpp C(int i) : base(i), A1(i+1), A2(i+2) {} ``` #### **问题2:虚基类构造函数被多次调用** **原因**:未使用虚继承,导致菱形继承中基类被重复构造。 **解决**:将中间类(如`B`、`C`)声明为虚继承: ```cpp class B : virtual public A { ... }; class C : virtual public A { ... }; ``` #### **问题3:成员初始化顺序错误** **原因**:C++中成员变量的初始化顺序仅由**声明顺序**决定,而非初始化列表中的书写顺序[^2]。 **错误示例**: ```cpp class Child { int a, b; public: Child() : b(1), a(b) {} // a先于b初始化,导致a使用未初始化的b }; ``` --- ### **六、总结** - **虚继承的核心作用**:解决菱形继承中的数据冗余问题,确保虚基类仅构造一次。 - **构造顺序口诀**: **虚基优先,声明顺序,派生最后** (虚基类 → 非虚基类按声明顺序 → 派生类自身) - **关键代码实践**: ```cpp // 菱形继承的正确写法 class A { /*...*/ }; class B : virtual public A { /*...*/ }; class C : virtual public A { /*...*/ }; class D : public B, public C { public: D(int x) : A(x), B(x), C(x) {} // 必须显式调用虚基类构造 }; ``` --- ### 相关问题 1. 如何避免虚继承中的构造函数重复调用? 2. 虚基类与非虚基类混用时构造顺序如何确定? 3. 析构函数在菱形继承中的调用顺序是怎样的?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值