C++学习笔记----9、发现继承的技巧(六)---- 有趣且令人迷惑的继承问题(7)
6、非公共继承
在前面所有的例子中,父类总是用public关键字列出。你可能会想是否父类也可以是private或protected。实际上,是可以的,但都不像public一样常见。如果不对父类进行访问指示符的指定,对于类来讲就是private继承,对于struct是public继承。
将与父类的关系声明为protected意味着从基类来的public成员函数与数据成员变为了继承类中的protected。类似地,指定为private继承意味着基类的所有public与protected成员函数与数据成员在继承类中会变成private。
可能想要统一地对父类的访问层次用这种方式进行降级有几个原因,但大部分原因是层次结构设计的瑕疵。有些程序员滥用这个语言特性,常见于多重继承的组合,应用了类的“部件”。不是使Airplane类包含一个引擎数据成员与一个机身数据成员,反而是使Airplane类是一个protected引擎与一个protected机身。用这种方式,对客户端代码来讲,Airplane既不像引擎,也不像机身(因为所有的东东都是protected),但是可以内部使用所有这些功能。
注意:如果没有其他原因,只是不熟悉的话,非公共继承很少见,推荐谨慎使用。
7、虚基类
在本章的前面部分,学到了不明确的基类,当多重父类每个都有一个通用属性的父类时会加剧这种不明确,如下图所示:
早期的推荐方案是确保共享的父类没有自身的任何功能。那样的话,其成员函数永远不会被调用,就不会有不明确的问题。
C++有另外的方法,叫做虚基类,来解决这种问题,如果你确实想让共享父类有自身的功能的话。如果共享父类被标记为虑基类,就不会有不明确性。下面的代码添加了一个sleep()成员函数,包含了一个实现,对于Animal基类来说,修改了Dog与Bird类,作为虚基类继承了Animal。在不用虚基类的情况下,在DogBird对象上调用sleep()会是不明确的,因为DogBird有两个Animal的子对象,一个来自于Dog,一个来自于Bird,所以会产生编译器错误。然而,当Animal是虚继承时,DogBird只有一个Animal的子对象,所以调用sleep()时不会有不明确性。
import std;
using namespace std;
class Animal
{
public:
virtual void eat() = 0;
virtual void sleep() { println("zzzzz...."); }
};
class Dog : public virtual Animal
{
public:
virtual void bark() { println("Woof!"); }
void eat() override { println("The dog ate."); }
};
class Bird : public virtual Animal
{
public:
virtual void chirp() { println("Chirp!"); }
void eat() override { println("The bird ate."); }
};
class DogBird : public Dog, public Bird
{
public:
void eat() override { Dog::eat(); }
};
int main()
{
DogBird myConfusedAnimal;
myConfusedAnimal.sleep(); // Not ambiguous because of virtual base class
}
在这种类层次结构中要注意构造函数。例如,下面的代码添加了一些不同类的数据成员,添加了构造函数来初始化这些数据成员,添加了Animal的缺省构造函数,原因会在代码之后进行解释。
class Animal
{
public:
explicit Animal(double weight) : m_weight{ weight } {}
virtual double getWeight() const { return m_weight; }
protected:
Animal() = default;
private:
double m_weight{ 0.0 };
};
class Dog : public virtual Animal
{
public:
explicit Dog(double weight, string name) : Animal{ weight }, m_name{ move(name) } {}
private:
string m_name;
};
class Bird : public virtual Animal
{
public:
explicit Bird(double weight, bool canFly) : Animal{ weight }, m_canFly{ canFly } {}
private:
bool m_canFly{ false };
};
class DogBird : public Dog, public Bird
{
public:
explicit DogBird(double weight, string name, bool canFly)
: Dog{ weight, move(name) }, Bird{ weight, canFly } {}
};
int main()
{
DogBird dogBird{ 22.33, "Bella", true };
println("Weight: {}", dogBird.getWeight());
}
当运行这段代码时,结果并不是预想的那样:
Weight: 0
看起来给定的22.33的重量在main()函数中的DogBird构造时丢失掉了。怎么回事?这段代码使用了虚Animal基类;因此,DogBird实例只有一个Animal子对象。DogBird构造函数调用了Dog与Bird的构造函数,都指向了Animal基类的构造函数。这就意味着Animal被构造了两次。这是不允许的。在这种情况下,当从继承类的构造函数中调用时,编译器使对Dog与Bird构造函数中的对Animal构造函数的调用失效,取而代之的是调用了Animal基类的缺省构造函数,这样就需要Animal的protected缺省构造函数。所有这些意味着大部分继承类自身要对共享类的构造函数调用负责。正确的实现如下:
class Animal
{
public:
explicit Animal(double weight) : m_weight{ weight } {}
virtual double getWeight() const { return m_weight; }
protected:
Animal() = default;
private:
double m_weight{ 0.0 };
};
class Dog : public virtual Animal
{
public:
explicit Dog(double weight, string name) : Animal{ weight }, m_name{ move(name) } {}
protected:
explicit Dog(string name) : m_name{ move(name) } {}
private:
string m_name;
};
class Bird : public virtual Animal
{
public:
explicit Bird(double weight, bool canFly) : Animal{ weight }, m_canFly{ canFly } {}
protected:
explicit Bird(bool canFly) : m_canFly{ canFly } {}
private:
bool m_canFly{ false };
};
class DogBird : public Dog, public Bird
{
public:
explicit DogBird(double weight, string name, bool canFly)
: Animal { weight }, Dog{ move(name) }, Bird{ canFly } {}
};
int main()
{
DogBird dogBird{ 22.33, "Bella", true };
println("Weight: {}", dogBird.getWeight());
}
在这个实现中,给Dog与Bird添加了protected单参数构造函数。由于只用于继承类所以是protected。客户端代码只用两个参数的构造函数来构造Dog与Bird。
在做了这些修改后,输出正确:
Weight: 22.33
注意:在类层次结构中虚基类是避免不明确性的一个伟大的方式。唯一的缺点是许多c++程序员对这个概念还不是很熟悉。