C++ 虚基类

使用多继承,容易产生二义性。

1. 当派生类的多个直接基类中某个成员名称相同,派生类访问这个成员时,如果不指定基类名,编译器无法判断该成员来自哪一个基类。

class Base1{
	protected:
		int a;
	public:
		void set(int x):a(x){}
};
class Base2{
	protected:
		int a; //与Base1的成员a同名 
	public:
		void set(int x):a(x){}//与Base1的成员set(int x)同名
};

class MultiDeri : public Base1, public Base2{
	public:
		int get(){
			return a;// 二义性错误: Base1的a还是 Base2的a
		}
}; 

void main()
{
	MultiDeri md;
	md.set(30); // 二义性错误: Base1的set()还是 Base2的set()
}

解决办法:在被引用的基类成员前加上基类名和作用域限定符:: 

return Base1::a;

md.Base2.set(30);


2. 即使派生类的所有积累的成员都不同名,也可能出现一种很深的二义性错误。

根据类的继承原理,一个派生类包含了积累的所有成员,这些成员构成了所谓的基类对象。在多继承中,多层次、多路径的交叉派生关系可能导致派生类对象包含了某个基类子对象的多个同名副本。

普通基类

A            A    

↑            ↑

B           C

↖      ↗

      D


在派生类 D的对象中存在基类A的成员(基类子对象)的两个副本:当试图访问基类A的成员时会发生二义性错误。

class A{
	protected:
		int a;
};
class B : public A{
	protected:
		int b;
};
class C : public A{
	protected:
		int c;
};
class D : public B, public C{
	public:
		int d;
}; 

void main()
{
	D md;//隐式调用基类构造函数 
	md.a = 100; // 二义性错误
}

d1含有基类A的成员a的两个副本(所谓的基类子对象),分别从路径D->B->A和D->C->A继承而来。因此编译器无法确定成员a是哪一个副本。

解决办法: md.B::a = 100; 


因此可以看出保存基类的多个成员的副本既浪费存储空间有可能造成二义性甚至多义性错误。派生类在访问间接基类的成员时,指明派生路径又很麻烦。所以C++提供了虚基类机制来解决。


虚基类

一种派生方式,在创建派生类的对象时,类层次结构中某个虚基类的子对象只保存一份,即一个派生类对象只有虚基类成员的一个副本:

解决办法:在被引用的基类成员前加上基类名和作用域限定符:: 

       A   

↗       ↖

B           C

↖      ↗

      D



...
class B : public virtual A{
	protected:
		int b;
};
class C : public virtual A{
	protected:
		int c;
};
...
最后md.a = 100; 合法。


虚基类和一般基类的重要区别:

我们知道,一般基类的构造函数是由它的直接派生类的构造函数负责调用的,直接派生类创建对象时调用直接基类的构造函数。但虚基类则不同。

原因是虚基类子对象在内存中只有一份,说明构造函数只能执行一次。为了保证虚基类的构造函数只被调用一次,C++规定徐基类的构造函数是由最派生类(创建对象的类为最派生类)的构造函数调用的。而该最派生类的非虚基类对虚基类构造函数的调用将被忽略。

同样的,虚基类有默认构造函数的话,隐式调用就是虚基类和它的派生类的构造函数自动调用默认构造函数。如果虚基类没有默认构造函数,则需要显示方式调用徐基类的构造函数。最派生类显式调用徐基类的构造函数时,该派生类的非虚基类对虚基类的构造函数的调用都被C++编译器忽略,这样保证对虚基类的子对象只初始化一次。也就是,显式调用也只调用最派生类对虚基类的调用。

其他的,如调用顺序问题是按照定义派生类时基类声明顺序。

### C++虚基类的使用方法及注意事项 #### 一、虚基类的作用 虚基类的主要目的是为了防止多重继承时产生的二义性和重复数据成员问题。当一个派生类通过多个路径继承同一个基类时,如果不使用虚继承,则会创建该基类的多个实例副本[^2]。 #### 二、虚基类的定义方式 要实现虚继承,在声明派生类时需在 `class` 关键字后面加上关键字 `virtual` 和 `public` 或其他访问修饰符。例如: ```cpp class Base {}; class Derived1 : virtual public Base {}; // 虚继承 class Derived2 : virtual public Base {}; // 虚继承 class FinalDerived : public Derived1, public Derived2 {}; ``` 在此例子中,由于 `Derived1` 和 `Derived2` 都是从 `Base` 类虚继承而来的,所以最终的 `FinalDerived` 只有一个 `Base` 的子对象[^2]。 #### 三、初始化顺序 对于虚基类,其构造函数会在任何非虚基类之前被调用。即使某个中间派生类未显式调用虚基类的构造函数,编译器也会自动为其提供默认参数来完成初始化过程。以下是具体示例代码及其解释: ```cpp #include <iostream> using namespace std; class A { protected: int value; public: A(int v = 0) : value(v) { cout << "A Constructor Called with Value: " << value << endl; } }; class B : virtual public A { public: B() : A(1) { cout << "B Constructor Called" << endl; } }; class C : virtual public A { public: C() : A(2) { cout << "C Constructor Called" << endl; } }; class D : public B, public C { public: D() : A(3), B(), C() { cout << "D Constructor Called" << endl; } }; int main(){ D obj; } ``` 在这个程序里,尽管 `B` 和 `C` 各自尝试设置不同的初始值给它们共同的虚拟父级 `A` ,但由于存在更具体的指令——即来自最底层衍生类别 `D` 所指定之数值 (此处为3),故此优先采用后者作为实际传递至 `A` 构造函式的参数[^3]。 #### 四、内存布局特点 因为引入了额外的信息用于管理共享的基础结构体实例位置关系等原因,通常情况下运用到虚基础类型的实体相较于单纯线性单一层次体系架构下的同等情况会有更大的存储需求量以及稍微复杂一点的操作逻辑[^2]。 #### 五、动态类型转换中的表现形式 如果涉及到从含有虚根节点的对象模型内部提取特定种类别的指针或者引用操作的话,那么就可能需要用到诸如 `dynamic_cast<>` 运算符来进行安全可靠的转型处理动作[^4]。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值