C/C++编程:多个基类(虚基类)

1059 篇文章 285 订阅

一个类可以从多个基类派生。在多重继承模型中(其中的类派生自多个基类),则使用“基类列表”语法元素指定基类。比如:

class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

指定基类的顺序并不重要,只不过在某些情况下,将调用析构函数和构造函数。这些情况下,指定基类的顺序将影响:

  • 构造函数进行初始化的顺序。如果你的代码依赖要在 Book 部分之前初始化的 CollectionOfBook 的 Collection 部分,则规范的顺序很重要。 初始化按在 基础列表 中指定类的顺序进行。
  • 调用析构函数以进行清理的顺序。同样,如果在销毁另一部分时必须呈现类的特定“部分”,则顺序非常重要。 析构函数的调用顺序与在 基础列表 中指定的类的顺序相反。

注意:

  • 基类的规范顺序将会影响类的内存布局,不要基于内存中基成员的顺序做出任何编程决策
  • 指定基础列表时,不能多次指定类的内存布局。但是,可以将类多次作为派生类的间接基

名称多义性

  • 多重继承使得沿着多个路径继承名称称为可能。沿着这些路径的类成员名称不一定是唯一的。这种名称冲突叫做多义性

  • 任何引用类成员的表达式必须采用明确的引用。

以下示例说明如何产生多义性:

class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // Note that class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

对于前面的类声明,如下所示的代码是不明确的,因为 b 所指的 b 是在 A 中还是在 B 中并不清楚:

C *pc = new C;

pc->b();

请看前面的示例。 由于名称 a 是类 A 和类 B 的成员,因此编译器无法辩明哪个 a 指定将调用函数。 如果成员可以引用多个函数、对象、类型或枚举数,则对该成员的访问是不明确的。
编译器通过按此顺序执行测试来检测多义性:

  • 如果对名称的访问是不明确的(如上所述),则会生成错误消息。
  • 如果重载函数是明确的,则将解析它们。
  • 如果对名称的访问违背了成员访问权限,则会生成错误消息。

在表达式通过继承产生多义性时,您可以通过限定考虑中的名称及其类名来手动消除该多义性。 若要适当编译上面的示例而不产生多义性,请使用如下代码:

C *pc = new C;

pc->B::a();

虚拟基类

  • 由于一个类可能多次成为派生类的间接基类,因此C++提供了一种优化这种基类的工作方式的方法。虚拟基类提供了一种节省空间和避免多次使用多重继承的类层次结构出现多义性的方法。

  • 每个非虚拟对象包含在基类中定义的数据成员的一个副本。这种重复浪费了空间,并要求您在每次访问基类成员时必须指定所需基类成员的副本。

  • 当将某个类指定为虚拟基类时,该基类可以多次作为间接基类而无需复制其成员。基类的数据成员的单个副本由将其作为虚拟基类的所有基类共享

  • 声明虚拟基类时,vritual关键字显示在派生类的基类基列表中。

请考虑下图中的类层次结构,它演示了模拟的午餐排队。
在这里插入图片描述
在该图中,Queue 是 CashierQueue 和 LunchQueue 的基类。 但是,当将这两个类组合成 LunchCashierQueue 时,会出现以下问题:新类包含类型 Queue 的两个子对象,一个来自 CashierQueue,另一个来自 LunchQueue。 下图显示了概念上的内存布局(实际物理内存布局可能会进行优化)。

在这里插入图片描述
请注意,Queue 对象中有两个 LunchCashierQueue 子对象。 以下代码将 Queue 声明为虚拟基类:

class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

virtual 关键字可确保仅包含子对象的一个副本 Queue (参阅下图) 。
在这里插入图片描述

  • 一个类可能有一个特定类型的虚拟组件和非虚拟组件
    在这里插入图片描述
    在图中,CashierQueue 和 LunchQueue 将 Queue 用作虚拟基类。 但是,TakeoutQueue 将 Queue 指定为基类而不是虚拟基类。 因此,LunchTakeoutCashierQueue 具有类型 Queue 的两个子对象:一个来自包含 LunchCashierQueue 的继承路径,另一个来自包含 TakeoutQueue 的路径。 下图对此进行了演示。
    在这里插入图片描述
    与非虚拟继承相比较,虚拟继承提供了显著的大小优势。但是,它可能会引入额外的处理开销:
  • 使用virtual继承而来的类所产生的对象往往比使用non-virtual继承的兄弟体积大,访问虚基类的成员函数时,也比如non-virtual base classs更慢
  • 支配“virtual bases初始化”的规则比non-virtual base的情况更复杂。virtual bases的初始化责任是由继承体系中的最底层类负责,这暗示:
    • 类如果派生自virtual bases而需要初始化,必须认知其virtual bases—不管那些bases有多远
    • 当一个新的派生类加入继承体系中,它必须承担virtual bases(不论直接或者间接)的初始化责任

建议

  • 非必要不使用virtual base。平时请使用non-virtual继承
  • 如果你必须使用virtual base classes,尽可能避免在其中放置数据。

示例

下面是一个用来塑模“人”的C++ Interface class:

class IPerson{     // 这个类需要指出实现接口
public:
	virtual ~IPerson();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
};


IPerson的客户必须以IPerson的指针或者引用来编写程序,因此抽象类无法被实体化创建对象。为了创建一些可以被当作IPerson来使用的对象,IPerson的客户使用factoryfunction将“派生自IPerson的具现类”实体化:

std::shared_ptr<IPerson> makePerson(DatebaseID perIdentifier);

DatebaseID askUserForDatabaseID();

DatebaseID id(askUserForDatabaseID());
std::shared_ptr<IPerson> pp(makePerson(id));

但是makePerson如何创建对象并返回一个指针指向它呢?五一一定有些派生自IPerson的局限class,在makePerson中可以创建。

假设这个类叫做CPerson。就像具象class一样,CPerson必须提供“继承自IPerson”的纯虚函数的实现代码。我们可以从无到有写出这些东西,但是更好的是利用既有组件。后者做了大部分必要的事情。比如,假设有个既有的数据库类,名为PersonInfo,提供CPerson所需要的实质东西:

class PersonInfo{
public:
	explicit PersonInfo(DatebaseID  pid);
	virtual ~PersonInfo();
	virtual const char * theName() const;
	virtual const char * theBirthDate() const;
private:
	virtual const char *valueDelimOpen()const;
	virtual const char *valueDelimClose()const;
}

const char * PersonInfo::valueDelimOpen()const{
	return "[";
}

const char *PersonInfo::valueDelimClose()const{
	return "]";
}

const char *PersonInfo::theName()const{
	// 保留缓冲区给返回值只用:因为缓冲区是static,因此会被自动初始化为“全部是0”;
	static char value[MAX_LNGTH];  // 不好的设计:会带来超限问题和线程问题

	std::strcpy(value, valueDelimOpen());
	// ... 
 	std::strcat(value, valueDelimClose());
 
	return value;
}

因为CPerson的name和birthDate必须返回类型“xxx”而不是”[xxx]“, 所以必须重新实现valueDelimClose和valueDelimOpen

CPerson和PersonInfo的关系是, PersonInfo刚好由若干函数可以帮助CPerson比较容易实现出来,它们的关系是根据某物实现出。而这种关系可以使用复合或者private继承实现。一般来讲复合更好,但如果需要重新定义虚函数,就必须继承。因此这里必须private继承才能满足要求(CPerson也可以结合复合+继承技术以有效定义PersonInfo的虚函数)。

但CPerson也必须实现IPerson,业就是说必须public继承才能完成。因此这里需要多重继承:

class IPerson{     // 这个类需要指出实现接口
public:
	virtual ~IPerson();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
};

class PersonInfo{
public:
	explicit PersonInfo(DatebaseID  pid);
	virtual ~PersonInfo();
	virtual const char * theName() const;
	virtual const char * theBirthDate() const;
private:
	virtual const char *valueDelimOpen()const {return "["; };
	virtual const char *valueDelimClose()const {return "]"; };
}


class CPerson : public IPerson, private PersonInfo{
public:
	explicit CPerson(DatebaseID  pid) : PersonInfo(pid) {}
	virtual const char * theName() const {return PersonInfo::theName(); }
	virtual const char * theBirthDate() const {return PersonInfo::theBirthDate(); }

private:
	virtual const char *valueDelimOpen()const {return ""; };
	virtual const char *valueDelimClose()const {return ""; };
	
}

在这里插入图片描述

注意

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对vritual继承的需要
  • virtual继承会增加大小、速度、初始化、赋值等成本。如果virtual base classes不带任何数据,将是最实用价值的情况
  • 多重继承的确由正当用途。其中一个情节涉及“public继承某个interface class”和“private继承某个协助实现的类”的两相结合
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值