多重继承
多重继承简单来说就是一个派生类继承了多个基类。其语法规则如下:(以下所有例子都使用C++ Primer中的)
class ZooAnimal {};
class Endangered {};
class Bear : public ZooAnimal {};
class Panda : public Bear, public Endangered {}; // 这里用了多重继承
上面的Panda类继承了Bear类和Endangered类,实现了多重继承。主要的知识到这里就结束了。但实际应用过程中,这样的继承会有许多的问题,下面列举几个:
多重继承的基类有相同的成员会怎么样?
构造多重继承的派生类对象的构造顺序是怎样?
析构顺序是怎样?
多重继承的基类又继承自相同的类,这会发生什么问题?
解决这些问题,需要我们对多重继承的机制进行详细了解。
派生类构造函数构造所有基类
构造派生类对象会同时构造并初始化它的所有基类子对象。多重继承的派生类的构造函数初始值也只能初始化它的直接基类。构造顺序如下:
派生类对象先构造基类;
多重继承则从左到右进行构造;
以上面的例子为例:
欲先构造
Panda,现需要构造Bear,再构造Endangerd;欲先构造
Bear,则需构造ZooAnimal。
则构造一个Panda类对象的构造顺序为:ZooAnimal-> Bear-> Endangerd-> Panda。
多重继承的析构
了解了多重继承的构造顺序,析构顺序就变得简单许多:就是构造顺序的反方向。
使用继承的构造函数
C++11 允许派生类使用基类的构造函数作为自己的构造函数,称为继承构造函数。多重继承中使用:
class A {
public:
A(int x) {};
};
class B {
public:
B(int x) {};
};
class C : public A, public B {
public:
using A::A;
using B::B; // 编译时报错,构造函数重复声明
};上面的代码中C的构造函数继承了A和B的,但问题在于A和B的构造函数的形参列表相同,导致了二义性,不能通过编译。
将上面某个基类的构造函数形参列表稍微修改一下,就可以通过编译。要不然就自己重新定义一个自己的版本构造函数。
多重继承与拷贝和移动
若派生类定义了自己的拷贝构造函数,则发生拷贝时直接调用,否则就会由编译器生成合成拷贝构造函数。该合成拷贝构造函数对每个基类依次进行拷贝构造。
拷贝构造的顺序和之前提到的构造函数的顺序一致,移动赋值运算符也与其一致。
多个基类的类型转换
在只有一个基类的情况下,派生类的指针可以作为实参放到基类形参之上。就像下面这样:
class ZooAnimal {};
class Bear : public ZooAnimal {};
void get_name(const ZooAnimal& animal) {}
int main()
{
Bear black_bear;
get_name(black_bear);
}多个基类也类似:
#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
ZooAnimal(std::string name) : name(name) {}
protected:
std::string name;
};
class Bear : public ZooAnimal {
public:
using ZooAnimal::ZooAnimal;
};
class Endangered {};
class Panda : public Bear, public Endangered {
public:
using Bear::Bear;
};
void print(const Bear&){}
void highlight(const Endangered&){}
std::ostream& operator<<(std::ostream& os, const ZooAnimal& val) {
os << "GOOD";
return os;
}
int main()
{
Panda ying_yang("ying_yang");
print(ying_yang);
highlight(ying_yang);
std::cout << ying_yang << std::endl;
}如果有函数的形参为派生类的两个基类类型,若调用了这个函数,则会产生二义性导致编译错误。
// 上面省略
void print(const Bear&){}
void print(const Endangered&){}
int main()
{
Panda ying_yang("ying_yang");
print(ying_yang); // 错误!
}基于指针或引用类型的查找,基于指针或引用的静态类型,类型中定义了什么就只能用什么,即使基类指针赋值为派生类的对象(虚函数):
#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
ZooAnimal(std::string name) : name(name) {}
~ZooAnimal() {}
virtual void print() {}
protected:
std::string name;
};
class Bear : public ZooAnimal {
public:
using ZooAnimal::ZooAnimal;
virtual void print() {}
virtual void toes() {}
};
class Endangered {
public:
virtual void print() {}
virtual void highlight() {}
~Endangered() {}
};
class Panda : public Bear, public Endangered {
public:
using Bear::Bear;
void print() { std::cout << "Call Panda Print!" << std::endl; }
void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
void toes() {}
void cuddle() {}
};
int main()
{
// 例1
Bear *pb = new Panda("ying_yang");
pb->print(); // 访问Panda::print()
//pb->cuddle(); // 错误,Bear没有cuddle成员
//pb->highlight();// 错误,Bear没有highlight成员
delete pb; // 访问Panda::~Panda()
// 例2
Endangered *pe = new Panda("yang_yang");
pe->print();
//pe->toes(); // 错误
//pe->cuddle(); // 错误
pe->highlight();
delete pe;
}若继承的多个类中有重名函数,通过派生类访问这个函数会导致二义性错误:
class Bear : public ZooAnimal {
public:
using ZooAnimal::ZooAnimal;
virtual void print() {}
virtual void toes() {}
void max_weight() {}
};
class Endangered {
public:
virtual void print() {}
virtual void highlight() {}
void max_weight() {}
~Endangered() {}
};
class Panda : public Bear, public Endangered {
public:
using Bear::Bear;
void print() { std::cout << "Call Panda Print!" << std::endl; }
void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
void toes() {}
void cuddle() {}
};
int main()
{
Panda pd("ying_yang");
pd.max_weight(); // 二义性错误
}一种可能的解决的方法,就是使用类作用域,例如:
Panda pd("ying_yang");
pd.Bear::max_weight();
pd.Endangered::max_weight();最好的解决方法就是在Panda类中重新写一个新的max_weight,通过类作用域来确定使用哪个类的函数。
虚继承
多重继承有一种特殊情况,就是基类可能继承自同一个类,专有名词为“菱形继承”。IO标准库就有一个菱形继承的情况:iostream多重继承为ostream和istream,同时这两个类又继承自base_ios。
如果出现了菱形继承,代表着我们的派生类会有两个原始基类的副本,着显然行不通。不同的语言有不同的方法解决这个问题。在C++中,我们通过虚继承来解决这个问题。
稍微修改一下我们之前写过的类,如果嫌麻烦,可以看UML图大致了解。
#include <iostream>
#include <ostream>
#include <string>
class ZooAnimal {
public:
ZooAnimal(std::string name) : name(name) {}
~ZooAnimal() {}
virtual void print() {}
protected:
std::string name;
};
class Raccoon : virtual public ZooAnimal {
public:
using ZooAnimal::ZooAnimal;
virtual void print() {}
virtual void toes() {}
void max_weight() {}
};
class Bear : virtual public ZooAnimal {
public:
using ZooAnimal::ZooAnimal;
virtual void print() {}
virtual void toes() {}
void max_weight() {}
};
class Endangered {
public:
virtual void print() {}
virtual void highlight() {}
void max_weight() {}
~Endangered() {}
};
class Panda : public Bear, public Raccoon, public Endangered {
public:
using Bear::Bear;
void print() { std::cout << "Call Panda Print!" << std::endl; }
void highlight() { std::cout << "Call Panda highlight!" << std::endl; }
void toes() {}
void cuddle() {}
};这段代码就使用了虚继承,其基本的语法格式如下:
class Raccoon : virtual public ZooAnimal;
class Raccoon : public virtual ZooAnimal; // 另一种写法继承虚基类表明我们允许以共享的方式分享这个类,不论虚基类在继承体系中出现了多少次,在派生类中只包含一个共享的虚基类子对象。
不论是基类和虚基类,派生类对象,都能被可访问基类的指针和引用操作。和多重继承的性质一样。
例如,假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性:
如果在D1和D2中都没有x的定义,则x将被解析为B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。
如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。
如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。
构造顺序和析构顺序
判断构造顺序,遵循一个准则:虚继承基类先构造,自底向上构造。以上面的虚继承构造为例:
首先使用Panda的构造函数初始值列表构造虚基类ZooAnimal;
再构造Bear;
再构造Raccoon;
再构造Endangered;
最后构造Panda;
析构函数和构造函数以相反的顺序执行。
评论区