Android C++系列:C++最佳实践4多重继承与虚继承

416 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情

1. 背景

Java和C++在语法层面比较的时候就不得不提到C++的多继承,我们知道Android是单继承,C++是多继承。在大型项目中不可避免的会用到多继承,本文分析C++多继承的一些特征。

2. 如何实现多继承?

C++中,我们可以在派生列表中包含多个基类:

class Sub : public Base{
	...
}
class SubA : public Base1, public Base2{
	...
}

关于多继承的几点说明:

  1. 每个基类均包含一个可选的访问说明符;
  2. 派生类列表只能包含已经被定义过的类;并且这些类不能是final的;
  3. C++对于派生类能继承的积累个数没有特殊规定,但是派生类列表中同一个基类只能出现一次。比如:可以Sub:public Base1,public Base2,public Base3 ....,但是不能Sub:public Base1,Base1.

3. 多继承中从每个基类中继承的状态

在多继承中,子类的对象包含每个基类的子对象,比如Sub继承Base1,Base2,Base1又继承自Base,那么Sub对象的结构如下图:

image-20220416220303128.png

构造一个派生类的对象将同时构造并初始化它的所有基类子对象,并且多重继承的派生类的构造函数值也只能初始化它的直接子类。

子类的构造方法初始值列表将实参分别传递给每个直接父类。父类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。怎么理解这句话呢?就是构造顺序与class Sub : public Base1, public Base2这个顺序有关,而与Sub::Sub(std::string name):Base1(name),Base2(){}没有关系。

在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数,但是如果从多个基类中集成了相同的构造函数,程序就会出错。

比如Base1,Base2 都有以const st::string&参数的构造函数,那么Sub同时继承Base1,Base2就会出错,这个时候我们必须专门为Sub定义自己的构造函数Sub:Sub(const std::string &s):Base1(s),Base2(s)

4. 多重继承中的类型转换

子类继承多个父类的情况下,我们可以令某个可访问基类的指针或引用直接指向一个子类对象。比如:

Base1 *base1 = new Sub();
Base2 *base2 = new Sub();
Base base = new Sub();

编译器不会在子类像基类的几种转换中进行比较和选择,因为它认为转换成哪个父类都行。但是这样会带来二义性,比如重载方法时:

void action(const Base1&);
void action(const Base2&);

当我们给action传Sub对象时就会出问题,因为编译器不知道怎么转换了。

所以我们在重载方法时要注意这种类型转换可能引起的问题。

5. 多重继承中的资源查找

在Java中我们查找属性时先从子类找,找不到再找父类,C++也是类似,但是在多重继承的结构中可能会有些复杂。

因为在查找过程中,会在所有直接基类中同时进行,如果名字在多个基类中都被找到,则这个属性的名字就产生了二义性。

我们思考一个问题,在Java中,如果我们定义了两个接口A,B,它们都有void test()这么一个方法,那么我们的Test类如果同时实现了A,B接口,具体的类该怎么实现呢?

在Java中确实比较简单,只需要实现一个test()方法就可以,但是在C++的多重继承中,父类不仅有方法还有属性,这种二义性该怎么解决呢?在C++中,对于一个派生类来说,从它的几个基类中分别继承名字相同的成员是完全合法的,只是需要在我们使用这些名字是加上前缀限定符明确指定它属于哪个基类(不调用不会出错,如果调用了还没有加前缀就会出错)。

比如上面说的test方法,我们的子类可以使用sub->Base1::text()方法来调用。

还有一种更复杂的情况,就是派生类继承的两个基类有函数名相同,但是参数列表不同的方法,这样查找是更容易出错。

**最佳实践:**为了避免二义性,除了我们在调用时加前缀,最好的办法是在派生类中为这个函数定义一个自己的版本,在函数内部来屏蔽这些二义性。比如:

int Sub::getMax() const
{
	return std::max(Base1::getMax(), Base2::getMax());
}

6. 虚继承

我们在派生类列表中见过这么一种形式:

class Base1:public virtual Base{
...
}

virtual代表虚继承,那么虚继承是做什么,要解决什么问题呢?

比如这样的一种结构:

class Base{
protected:
	int num;
}
class Base1:public Base{

}
class Base2:public Base{

}
class Sub:public Base1, Base2{

}

这种情况,Base其实被继承了两次,而默认情况派生类含有继承链上每个类对应的子部分。如果某个类在派生过程中出现多次,则派生类中将包含这个基类的多个子对象。 带来的问题呢?一句话,资源浪费。

而虚继承就是解决这个问题的,它的目的是令某个类做出声明,承诺愿意共享它的基类。我们把共享的基类子对象称为虚基类。这样,无论虚基类在继承体系出现多少次,在派生类中都只包含唯一一个共享的虚基类子对象。

**最佳实践:**在实际场景中,位于中间层次的基类将其继承声明为虚继承一般不会带来负面的问题。这样我们可以为后续使用者或者扩展着提供便捷。

7. 总结

文本介绍了C++多继承和Java实现多个接口的区别,并具体介绍了多重继承以及多重继承中的类型转换、资源查找,以及虚继承。