C++ 的一个主要目标是促进代码重用。公有继承是一种机制,此外还有私有继承、保护继承,还有包含(containment)、组合(composition)或层次化(layering),还有类模板(class template)。
has-a 关系,新的类包含另一个类的对象,通常“包含”、“私有继承”和“保护继承”用于实现 has-a 关系。
1、包含
【接口和实现】 使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是 is-a 关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是 has-a 关系的组成部分。
包含其他类实例对象的类:
2、私有继承
class Student : private std::string, private std::vector<double> {};
私有继承,不会影响基类,会导致派生类继承到的所有基类成员全部变为 private
,即基类成员只能在派生类中使用。
2.1 私有继承特性
(1)构造函数使用成员初始化列表语法,它使用类名而不是成员名来标识构造函数。
Student(const char* str, const double* pd, int n)
: std::string(str), std::vector<double>(pd, n) {}
(2)使用包含时,类成员对象是有名称的;而使用私有继承时,因为是继承,类成员对象没有名称。
(3)因为第(2)点,使用包含时将使用对象名 来调用方法,使用私有继承时将使用类名和作用域解析运算符 来调用方法。
(4)在(3)中,私有继承时,如何访问基类对象呢?
答:因为是继承,那便可以通过强制类型转换,将派生类对象转换为基类对象。e.g. Student 继承于 std::string,可以强转为 string 对象。
const std::string& Student::Name() const {
return (const std::string&) *this;
}
(5)通过显式地转换为基类来调用正确的函数。 引用 stu 不会自动转换为 string 引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
ostream& operator<<(ostream& os, const Student& stu) {
os << "Scores for "<< (const String&) stu << ":\n";
}
注:即使这个例子使用的是公有继承,也必须使用显式类型转换。原因之一是,如果不使用类型转换,
os << stu
将会与友元函数原型匹配,形成无线循环地递归调用。原因之二,Student 例子中,使用的是多重继承,如果两个基类都提供了operator<<()
,编译器将无法确定应转换成哪一个基类。
2.2 在派生类外部访问基类方法,该如何呢?
(1)可以通过在派生类中定义一个方法,然后在该方法中访问基类方法。这样派生类可以控制程序对基类方法的访问。
(2)还有另一个方法:使用 using 重新定义访问权限。
将函数调用包装在另一个函数调用中,即使用一个 using 声明(就像名称空间那样)来指出派生类可以使用特定的基类成员,即使采用的是私有继承。
using 声明只使用成员名,没有函数括号、参数、返回类型。
using 声明只适用于继承,不适用于“包含”。
e.g. 下面 using 声明,将使得 std::vector<double>::size()
的访问权限为公有的。
class Student : private std::string, private std::vector<double> {
public:
using std::vector<double>::size;
};
2.3 使用“包含”还是“私有继承”?
(1)一般倾向于使用 包含 来建立 has-a 关系
- 易于理解;
- 继承会引入很多问题,特别是多重继承时;
- 包含可以包括多个同类的子对象,而继承只能使用一个这样的对象(当对象都没有名称时,将难以区分)。
(2)啥时候应使用私有继承?
- 新类需要访问原有类的保护成员时,包含无法访问保护成员,而继承关系中,派生类可以访问;
- 需要重新定义虚函数,则应使用私有继承。
3、保护继承
class Student : protected std::string, protected std::vector<double> {...};
保护继承,基类的公有成员和保护成员都将成为派生类的保护成员。和私有继承相同,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。
保护继承和私有继承的区别在哪里?
当从派生类派生出下一代派生类时,区别可见。
(1)私有继承:三代类将不能再使用基类的接口,这是因为基类的公有方法在二代派生类中已经变为私有方法了。
(2)保护继承:基类的公有方法在第二代中变成受保护的,第三代派生类可以使用。
【总结】
隐式向上转换,意味着无需进行显式类型转换,就可以将基类指针或引用指向派生类对象。私有继承不能隐式向上转换,因为派生类通过私有继承,声明了派生类的私密性,基类无权访问。(但又能通过显式向上转换,只能说这样破坏了类的安全性)
4、多重继承(multiple inheritance,MI)
多重继承使得能够使用 2 个或更多的基类派生出新的类,将基类的功能组合在一起。
(1)公有多重继承表示 is-a 关系;
(2)私有、保护多重继承表示 has-a 关系。\
声明:class DerivedClass :public BaseClass1,public BaseClass2 {}
。public
只对一个基类生效,为保证都是公有继承,必须显式声明为 public
,否则编译器默认为私有继承。
1、虚基类和虚函数,两者的虚,virtual,有关联吗?
没有关系。只是为了减少引入的关键字。重载了 virtual 定义。
2、为什么不抛弃将基类声明为虚的方式,使虚行为成为多重继承的准则?
(1)存在个别情况,需要基类的多个拷贝(世界之大,需求无奇不有)。
(2)将基类作为 虚的要求,程序需要完成额外的计算。这对于不需要虚的场景而言付出额外的代价是不应当的。
3、是否存在其它麻烦?
当然。虚基类,可能需要修改已有的代码,比如需要在中间派生类中加上 virtual 关键字,使其虚继承于基类。
多重继承的主要问题是:如何解决“菱形继承”带来的多个基类对象问题
4.1 公有多重继承
多重继承的引入了 2 个主要问题:
(1)从 2 个不同的 base class 继承同名方法,怎么处理?
(2)从 2 个或更多个 base class 继承同一个类的多个实例,怎么处理?
虽然这两个问题有相应的解决方案,但仍建议是“谨慎、适度地使用 MI”。
1、解决方案 - 虚基类
使用 虚基类,virtual base class,其使得从多个类(它们派生于同一个基类)派生出的对象只继承一个基类对象。在类声明中使用关键字 virtual,virtual 和 public 没有顺序要求。
class BaseClass1 :virtual public AbstractBaseClass {...};
class BaseClass2 :public virtual AbstractBaseClass {...};
class DerivedClass : public BaseClass1, public BaseClass2 {...};
对于非虚基类
唯一可以出现在初始化列表中的构造函数是其相邻基类的构造函数。
DerviedClass(const BaseClass& bc, int a = 1):BaseClass(bc), mA(a) {}
对于虚基类
要显式地调用 所有所需的 基类构造函数。如果类有间接虚基类,则除非只需要使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。
DerivedClass(const AbstractBaseClass& abc, int p = 0, int v = 1)
: AbstractBaseClass(abc), BaseClass1(abc, p), BaseClass2(abc, v) {}
// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
若使用这种调用“相邻基类”构造函数逐层传递信息的方法是有缺陷的!
DerivedClass(const AbstractBaseClass & abc, int p = 0, int v = 1)
: BaseClass1(abc, p), BaseClass2(abc, v) {} // 有缺陷的
存在的问题是:自动传递信息时,将通过 2 条不同的途径(BaseClass1 和 BaseClass2)将
abc
传递给 AbstractBaseClass 对象。为避免这种冲突, C++在基类是 virtual 时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将使用 p 和 v 初始化 BaseClass1 和 BaseClass2 的成员变量,但 adb 并不会传递给BaseClass1 和 BaseClass2。因此,编译器将会调用 AbstractBaseClass 的默认构造函数——这个就是“缺陷”。因此,如果不希望使用默认构造函数来构造虚基类对象,需要显式地调用所需的基类构造函数👇👇👇这种用法对于 虚基类实际上是必须的(因为一般情况下都不会调用默认构造函数);对于非虚基类,则是非法的。
3、使用作用解析运算符 ::
调用指定祖先的方法
(1)对于单继承,若派生类没有重新定义基类方法,将直接调用最近的祖先类中的定义。
(2)对于多重继承,每个直接祖先都有这样的方法(继承链上的广度方向),若直接调用将产生“二义性”问题!必须使用作用域解析运算符解决二义性问题。
DerivedClass dc = DerivedClass(...);
dc.BaseClass1::Show();
dc.BaseClass2::Show();
但是会有一个问题:如果 Show 方法会逐层递归调用祖先基类的方法,那么上述分别调用 BaseClass1::Show()
和 BaseClass2::Show()
,将使得虚基类 AbstractBaseClass::Show()
方法会被多次调用。这里只是打印一些信息,或许无所谓,但换成实际场景,多次操作可能就无法接受!
解决方法:将相关逻辑进行模块化处理,在本例中可以将要打印的信息提炼为 Data() 方法,每个类中重写 Data 方法,并只负责打印自己类中的信息。在 DerivedClass 中重写 Show 方法,调用各个祖先类的 Data 方法。
void DerivedClass::Show() {
AbstractBaseClass::Data();
BaseClass1::Data();
BaseClass2::Data();
DerivedClass::Data();
}
DerivedClass dc = DerivedClass(...);
dc.Show();
(3)若是间接祖先和直接祖先之间(继承链中深度方向上)有同名方法,不使用 ::
不一定会导致二义性,优先规则:派生类中的名称优先于直接或间接祖先类中的相同名称。(不建议这样,还是用 ::
更好!)
4、混合使用虚基类和非虚基类
当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含:
(1)一个表示所有的虚途径的基类对象;
(2)分别表示各条非虚途径的多个基类子对象。
在下图的例子中,类 M 的对象将包含 3(= 1 + 2)个基类 B 对象。\