在前面的章节中,你学到了继承关系是一种真实世界对象以层次存在的模式。在编程世界中,当需要写一个类基于其构建,或进行细微的修改的另一个类时,那种模式就有了关系。完成这个目标的一个方式是拷贝一个类的代码粘贴到另一个类中。修改相关部分或者改变代码,就能达到生成一个新类,与原来的类有一点不同的目标。这个方法,真的不怎么样,让面向对象编程的程序员感到憋屈与冒犯,有以下的原因:
- 对原有类的问题修复不会反映在新的类中,因为这两个类包含完全独立的代码。
- 编译器不知道这两个类之间的关系,所以它们不是多态--它们也不是同一件事的不同的变形。
- 这个方法并没有建立一个真正的继承关系。新类与原来的类相似只是因为有相似的代码,而不是因为是同类型的对象。
- 原来的代码有可能获取不到。它可能会只存在于预编译的二进制格式,所以拷贝粘贴代码可能变得不可行。
一点儿也不奇怪,c++提供内置的对于真正的继承关系定义的支持。c++继承关系的特点会成为我们接下来讨论的内容。
1、扩展类
当你写一个在c++中定义的类时,可以告诉编译器你的类是继承自,源于,或扩展自一个既有类。这样做的话,你的类就会自动包含原有类的数据成员与成员函数,原有类叫父类,基类,或超类。扩展一个既有类给你的类(它现在叫做子类,派生类,扩展类)唯一的描述它与父类不同的能力。
在c++中扩展一个类,在写类定义时指定要扩展的类。要展示继承的语法,会用到两个类,基类与派生类。别担心--更有趣的例子后面马上来。一开始,先考虑下面Base类的定义:
class Base
{
public:
void someFunction() {}
protected:
int m_protectedInt{ 0 };
private:
int m_privateInt{ 0 };
};
如果你想要构建一个叫做Derived的新类,它继承自Base,使用下面的语法:
class Derived : public Base
{
public:
void someOtherFunction()
{
println("I can access base class data member m_protectedInt.");
println("Its value is {}", m_protectedInt);
//println("The value of m_privateInt is {}", m_privateInt); // Error!
}
};
Derived是一个成熟的类,只是碰巧共享了Base类的属性。现在不要担心public关键字--我们后面会解释。下图展示了Derived与Base之间的简单关系。
可以像其它对象一样声明Derived类型的对象。甚至可以定义第三个类继承自Derived,弄成类的链条,如下图所示:
Derived不必是Base的唯一的继承类。另外的类也可以继承自Base,可以高效地变成Derived的兄弟姐妹,如下图所示:
在系统内部,继承类包含了基类的实例,叫做子对象。可以图示如下:
1.1、继承的客户视角
对于客户来说,或者代码的另外部分来说,Derived类型的对象也是一个Base类型的对象,因为Devived继承自Base。这意味着Base的所有的公共的成员函数与数据成员以及Derived的所有公共成员函数与数据成员都是可用的。
为了调用它,使用继承类的代码不需要知道在继承链条中哪个类定义了一个成员函数。例如,下面的代码调用了Derived对象的两个成员函数,即使其中一个成员函数定义在Base类中:
Derived myDerived;
myDerived.someFunction();
myDerived.someOtherFunction();
理解继承类是沿着一个方向工作的是很重要的。Derived类对Based类有一个清晰定义的关系,但是Base类,如字面意思,不会知道Derived类的任何信息。这意味着Base类型的对象无法访问Derived的成员函数与数据成员,因为Base不是Derived。
下面的代码编译不会成功,因为Base类不包含一个公共的叫做someOtherFunction()的成员函数:
Base myBase;
myBase.someOtherFunction(); // Error! Base doesn't have a someOtherFunction().
注意:从使用对象的代码的角度,对象属于它定义的类,也属于任何基类。
对类类型的指针或引用可以指向声明的类类型或它的任何继承类。我们后面专门讨论这个令人迷惑的话题。现在要理解的概念是指向Base的指针实际上也可以指向一个Derived对象。对于引用也一样。客户仍然可以访问存在于Base中的成员函数与数据成员,但是通过这种技术,在Base上可以操作的代码也可以在Derived上操作。
例如,下面的代码编译与运行都没有问题,即使一开始会出现一个类型不匹配:
Base* base{ new Derived{} }; // Create Derived, store in Base pointer.
然而,不能通过Base指针调用Derived类的成员函数。下面的代码是不灵的:
base->someOtherFunction();
这会被编译器标记为错误,因为虽然对象是Derived类型,也确实定义了someOtherFunction(),但是编译器只会认为它的类型是Base,而Base是没有定义someOtherFunction()的。