c++学习记录六:继承
(本文来自c++简明教程)
类最突出的一个特性就是“子类化”(subclassing),也就是一个类从以前定义好的一个类继承它的成员。处于几方面的原因,这个特性显得非常重要。
第一个原因是子类化处理允许你定制现有的软件。你可以拿到别人创建的一个类,在上面添加自己独有的新功能。还可以覆盖类的任何或者全部功能。这样一来,类就具有了扩展性和重用性(我保证这是你在本章见到的最后两个专业术语)。
听起来似乎很不错……有时的确如此!但是,考虑到本章要讨论的一些技术原因,你的工作并非总是那么轻松。最起码,你必须修订所有构造函数。
使用一个继承层次结构和创建接口时,也需要用到“子类化”技术,详情将在下一章讨论。
子类化
子类是具有另一个类(称为基类)的所有成员,而且允许你添加新的成员。下面是声明一个子类时的语法:
1 | class 类名:public基类 { |
类名是指你要声明的一个新类的名称。它继承了基类的所有成员(构造函数除外——详情在后文讲述)。
声明是指数据成员、成员函数或两者的组合——这与标准的类声明是一样的。声明代表在现有成员(已经从基类继承)的基础上新增的成员。
在声明中,你也可以指定一个或多个已经在基类中声明的成员。这样一来,现有基类成员的声明就会被覆盖,所以会在子类中忽略。
通常,为避免混淆,你只应将“覆盖”技术用于现有的函数提供新的定义。“覆盖”是一项重要的变成技术,它是下一章要讨论的主题。
下例将在Fraction类的基础上创建一个新类。
1 | class FloatFraction : public Fraction { |
上述代码展示了一个简单的概念:它将FloatFraction类声明为现有的Fraction类的一个子类。结构就是,每个FloatFraction对象都拥有Fraction类中声明的所有成员。除此之外,每个FloatFraction对象都新增了一个名为get_float的成员函数。
声明好这个类之后,你可以像使用其他任何类那样,把它作为一个类型名称来使用:
1 | FloatFraction f1; |
假定get_float函数已经正确定义,上述代码会打印结果“0.25”。
下面是基于String类来创建一个新类的例子。这个例子稍微复杂一些。
1 | class ExtString : public String { |
上述代码将ExtString声明为String类的一个子类,所以,它包含了String类的成员。除此之外,它还包含 私有数据成员nullTerminated。由于这个成员是私有的,所以在类的外部定义的代码不能引用它,但外部的代码能够访问两个公共成员,即length(一个数据成员)和isNullTerminated(一个函数)。
下面是从Fraction类继承的另一个例子,但这一次是通过一个中间类(FloatFraction)来继承的:
1 | class ProperFraction : public FloatFraction { |
这里的基类是FloatFraction。换言之,ProperFraction(真分数)在这里相当于“孙辈”的一个类,它除了包含FloatFraction类的所有成员,还包含Fraction类的所有成员(因为FloatFraction本身又是Fraction的子类)。
这些声明创建了一个层次结构。在这个层次结构中,ProperFraction是Fraction的一个间接子类。
作为最后一个例子,以下是直接从Fraction类继承的另一个类:
1 | class FractionUnits : public Fraction { |
在FractionUnits的声明中虽然涉及到String类,但String类即不是一个基类,也不是一个子类。你可以说每个FractionUnits对象都有“具有”一个String对象,而且它是一种Fraction。换言之,它是一种更具体的Fraction对象。
你可以这样想:当A是B的一个子类时,你可以说A是一种更具体的B。“狗”是“哺乳动物”的一个子类,而“哺乳动物”是“动物”的一个子类,以此类推。与此同时,“狗”类包含了“牙齿”类、“尾巴”类,等等。
1 | class Dog : public Mammal { |
小插曲:为什么要声明public基类?
在前面给出的声明子类的语法中,你应该注意到其中使用了public关键字来限定基类。在这种情况下,该关键字规定了“基类访问级别”。
1 | class 类名 : public 基类 { |
从技术上说,你可以进行一个直接的声明,在其中省略public关键字:
1 | class FloatFraction : Fraction { |
和类的成员声明一样,这里的问题在于,默认基类访问级别是private。但是,private访问级别在这种情况下通常是不适宜的。尤其是,如果使用private基类访问级别,那么从基类继承的所有成员都会在新类中成为private成员。例如,get_num函数虽然会继承,但它会成为private成员函数——所以不能在类的代码外部访问。
1 | FloatFraction aFract; |
相反,如果使用public基类访问级别,那么所有基类成员将按照原来的样子进行继承。这几乎一点是你所希望的,所以最好养成习惯,在基类名称前添加一个public。
除了public之外,很少需要使用其他基类访问级别。这是c++语法设计得不好的一个例子。这种设计对于大多数人来说都是没有用处的,而且它的来源已经无从考证。但幸运的是,解决方案也非常简单,你只需记住在基类名称前加一个public。
示例1:FloatFraction类
下一个例子标志着本书的程序开发水平将迈上一个新的台阶。我们不再将全部内容都放在一个文件中。相反,我假定源代码存储在两个单独的文件中,本例假定是Fract.h和Fract.cpp。为了方便起见,后文将列出两个文件的内容。
首先展示的是实现文件的代码,它负责对FloatFraction类本身进行测试。注意,它用一个**#include预编译命令添加基类Fraction**的类型信息。
FloatFract1.cpp
1 | #include <iostream> |
这是一个很短的文件,因为它只包含了子类FloatFraction所需的代码。这个类其实涉及到大量c++代码,只是那些代码已经包含在基类Fraction中。换言之,这里只是在FloatFraction类中重用那些代码:
Fraction类的声明包含在文件Fract.h中。
Fract.h
1 | #include <iostream> |
用于实现Fraction类成员函数(内联的函数除外)的代码则放在文件Fract.cpp中。注意,这个文件必须添加到当前项目中。请查看你的开发环境的Project(项目)和File(文件)菜单,并查找一个“Add New Item”命令。
Fract.cpp
1 | #include "Fract.h" |
注意:如果你正在使用Microsoft Visual Studio,请记住每个.cpp文件都需要以#include “stdafx.h”开头。一定要在Fract.cpp以及其他每个.cpp文件的开头插入这个预编译指令。
幕后玄机
这个例子中,最引人注意的一点是FloatFraction1.cpp中的代码相当段!
在子类FloatFraction中,我们只添加了一个函数,即get_float。由于函数定义很短,所以这里对它进行了内联。在函数定义中,我们利用了一个小技巧:在执行除法运算之前,先对get_num表达式的数据类型进行强制转换。这样一来,程序最后执行的就是浮点除法,所以生成的是一个浮点结果:
1 | double get_float() { |
查看函数定义时,你或许会觉得奇怪:代码为什么用get_num和get_den函数来分别获得num(分子)和den(分母)的值?它们不是成员吗?既然是成员,为什么不能直接访问它们的值呢?
为Fraction类本身写代码时,上述说法是成立的。但是,这里并不是为Fraction类本身写代码,我们写的是FloatFraction这个子类的代码。由于num和den是私有成员,所以它们不能直接由其他类来访问——Fraction的子类也不例外!因此,FloatFraction的代码必须使用get_num和get_den来获取这些成员的值。
在main中,我们使用了FloatFraction子类新增的成员函数(get_float)和继承的成员函数(set):
1 | fract1.set(1, 2); |
练习
- 修改示例1,在FloatFraction类中新增一个set_float成员函数。函数应该获取一个double类型的参数,并用它设置num和den的值。一个办法是:
- 用100乘以参数值,取整;
- 将num设置成这个值;
- 将den设为100;
- 调用normalize。
注意:如果set函数来进行步骤2和步骤3,那么步骤4能够自动进行。提示:取整时,请使用到int的一次强制类型转换:
new_value = static_cast
2.为FloatFraction类写一个构造函数,它能接受单个double类型的参数。在完成了练习1之后,这个工作会非常简单。
FloatFraction类的问题
子类的创建几乎肯定能像上一节展示的那样轻松地完成。但遗憾的是,在你试验了几次FloatFraction对象之后,很快就会发现那个类存在的局限。
例如,以下代码完全能按你预想的那样工作(假定定义了一个完整的Fraction类):
1 | Fraction f1(1, 2); |
但是,如果你换用FloatFraction类(根据前面所说的,它应对支持Fraction类支持的一切),那么以下每一个语句都会造成错误:
1 | FloatFraction f1(1, 2); //错误 |
对于当前版本的FloatFraction类来说,存在两个需要解决的问题(幸好每个问题都很容易解决):
- 一个问题是子类没有继承构造函数。
- 另一个问题是Fraction类的许多函数都返回Fraction类型的对象,或者获取Fraction类型的参数,而不是FloatFraction类型。
现在让我们依次研究这两个问题。构造函数的问题最严重,所以值得为它列出一条基本规则。
基本规则:子类不会继承构造函数,所以不要依赖于基类的构造函数。
c++不允许继承构造函数,这看似不公平,但c++的这种设计是有道理的。假如子类在基类的继承上添加了一个或多个数据成员,那么会发生什么?例如:
1 | class FloatFraction2 : public Fraction { |
Fraction的构造函数在设置num和den的值是,会调用set函数,而set函数会同时调用normalize函数来相应地调整上述两个值。但在上面的例子中,新的数据成员float_amt和whole还能享受到这一切吗?
c++的一个设计理念在于,你应当总是写自己的构造函数——即使是在创建一个子类的时候。除此之外,你应该初始化每个成员。由于一个基类构造函数可能不够用(因为它可能无法初始化新的成员),所以c++认为你应该为每个子类写一套全新的构造函数。
不过,这个规则也有例外的时候。以前讲过,如果类的作者没有提供,那么编译器会自动提供3个特殊的成员函数。对于子类来说,情况将变得更加复杂,但类似的规则任然适用。其中每个函数最终都要使用基类。
子类的默认构造函数
如果类的作者没有为一个类写任何构造函数,编译器会自动提供一个默认构造函数。由于FloatFraction类的当前版本不包含它自己的构造函数声明,编译器会提供一个默认构造函数。
将子类考虑在内,自动提供的默认构造函数的一般规则是:它首先调用基类的默认构造函数,然后,它对子类中的每个新的数据成员进行清零处理。
注意:假如一个成员本身就是一个对象,就会调用那个对象自己的默认构造函数。
子类的拷贝构造函数
以前讲过,假如类代码本身没有声明一个拷贝构造函数,编译器就会自动提供一个默认拷贝构造函数。编译器提供的版本首先调用基类的拷贝构造函数。然后,它对每个新的数据成员执行直接的、逐个成员的拷贝操作。
子类的赋值函数
你或许已经猜到:自动赋值函数(如果你不自己写一个,编译器就会提供一个)首先调用基类赋值函数。然后,它对每个新的数据成员执行一次直接的、逐个成员的拷贝操作。
添加遗漏的构造函数
遗漏的FloatFraction类的构造函数很容易提供。它们所做的事情要比Fraction的构造函数做的多一些。
1 | FloatFraction() {set(0, 1);} |
为了生成上述代码,可以拷贝并粘贴Fraction的构造函数代码,将其中的每一个“Fraction”替换成“FloatFraction”——使用字处理软件或者文本编辑器,可以很容易地完成这个操作。
除了简单地进行替换,还对一个地方进行了修改。你或许想要保持拷贝构造函数的简单定义,使其变成下面这:
1 | FloatFraction(const FloatFraction &src) |
问题在于,num和den都是Fraction类的私有成员。这意味着不能从其他类访问这些成员,即使是从Fraction的子类中(本章稍后还会讲到这个问题)!所以,在FloatFraction的代码中,你需要使用公共函数get_num和get_den。
解决与基类的类型冲突
如果仔细测试一下FloatFraction类,会发现它的大多数运算都无法工作。set函数是一个重要的例外,它几乎是继承之后惟一能工作的函数。
但是,c++的这个设计并不是没有道理的。例如,我们来看Fraction::add函数是如何声明的:
1 | Fraction add(const Fraction &other); |
该函数在FloatFraction类中得到了完全的继承和支持。但看看该函数所做的事情:它获取Fraction类型(而不是FloatFraction类型)的一个参数,并返回Fraction类型的一个值(同样地,不是FloatFraction类型)。
结果就是,你可以在下面这样的语句中使用add函数:
1 | Fraction f1, f2; |
上述代码正确使用了所有类型,因为它不厌其烦地使用Fraction类来传递和接收值。但是,这样做的用处毕竟是有限的。
你真正想做的——但目前还不能——是创建混合了多个FloatFraction对象的表达式:
1 | FloatFraction f0, f1, f2; |
幸运的是,有一个简单的方案可以解决这种问题。只需添加一个新的构造函数,它能执行从Fraction类的转换:
1 | FloatFraction(Fraction fract) |
现在,一切问题都能迎刃而解。add函数返回Fraction类型的一个对象,但那个值能够轻松地转换成FloatFraction类型。这样做之所以有效,是因为FloatFraction不包含新的数据成员。所以,不需要额外做什么,就能使来自Fraction类的运算正常进行。但是,正如本章后面的示例2所示,如果涉及新的数据成员,你就必须覆盖某些Fraction成员函数。
示例2:最终的FloatFraction类
为了使FloatFraction类按照我们希望的那样工作,有必要提供所有必要的构造函数,同时提供FloatFraction(Fraction)类型的一个构造函数。下面是完整的代码。
FloatFract2.cpp
1 | #include <iostream> |
幕后玄机
本例应该很容易理解。在修订后的FloatFraction声明中,包含所有必须的构造函数。所以像下面这样的声明是完全支持的:
1 | FloatFraction f1(1, 2), f2(1, 3), f3; |
类还提供了从Fraction转换成FloatFraction的构造函数:
1 | FloatFraction(const Fraction &src) |
由于存在上述构造函数,所以Fraction类中声明的所有数学运算都能应用与FloatFraction类——例如,operator+函数返回一个Fraction对象不再成为一个问题,因为对象的类型会根据需要自动转换成FloatFraction。所以,以下语句能够正确执行:f3 = f1 + f2;
练习
- 修改示例2的main部分,证实测试相等性操作符(==),加法和乘法操作符能够正确工作。
- 回答下列问题:有了这个最终版本的FloatFraction,是否能在任何情况下自由地混合FloatFraction和Fraction对象?
示例3:ProperFraction类
本节展示了如何创建一个子类的子类,从而对继承进行扩展。并非一定要采取这种方式来编写类,你可以让所有子类都直接从同一个基类继承(换言之,创建一个“平板”式的类层次结构)。但是,你仍然有必要知道一个子类可以成为另一些子类的基类。
每次创建一个子类,并构成层次结构的另外一个级别时,都会添加更多的功能/容量来存储数据。正如前面讲述的那样(使用狗/哺乳动物/动物的例子),你最好将一个子类视为基类的一个更具体的版本。请注意以下基本规则。
基本规则:通常,你应该创建一个现有类的子类,以便为那个类添加更多具体的功能或者容量。
同样地,假如A是B的一个子类,A就应该被视为一种B,就像够应被视为一种哺乳动物一样。
但老实说,这只是面向对象编程的一个理论性框架。有的时候,你之所以要创建子类,完全是因为用它能最快地完成工作。假定你已经声明了FloatFraction类,现在希望它的子类ProperFraction(真分数)中添加更多的函数。本例展示了如何生成FloatFraction的子类(注意,FloatFraction本身又是Fraction的子类)。
PropFract1.cpp
1 | #include "Fract.h" |
幕后玄机
对于本例,你需要注意两个问题:
- get_num函数是Fraction类的最常用的函数之一,它在ProperFraction类中被覆盖。这使问题复杂化了,你必须指定要使用哪一个版本的get_num。
- 继承方案也有点赋值。为了确保所有类都能正常工作,你需要支持从Fraction类的转换,以及间接基类FloatFraction的转换。
get_num函数在这里进行了覆盖,因为该函数应该具有不同的行为。我们希望类像这样工作:假定一个对象存储了值5/2,对应的真分数应该是2 1/2。对于Fraction对象来说,以下代码应该打印”5/2”。
1 | Fraction fract(5, 2); |
但是,对于一个ProperFraction对象来说,相同的代码应该打印“1/2”,因为上述代码变成了以下语句的一部分。以下语句应该打印“2 1/2”:
1 | ProperFraction fract(5, 2); |
在一个真分数中,由于整数部分是隔离出来的,所以分子应该输出1,而不是5。
ProperFraction对象内部存储数据的方式和Fraction对象一样。假定你用相同的方式来设置它们:
1 | Fraction fract1(5, 2); |
无论ProperFraction对象,还是Fraction对象,都会为num存储值5,为den存储值2。这些对象的区别在于,当对象的用户获得一个值时,它们具有不同的行为。如果你调用get_num函数,Fraction对象会返回5,而ProperFraction对象会返回1。所以,get_num函数需要具有不同的行为。
为了实现get_num函数的ProperFraction版本,你需要一种方式来直接获取num值,这时情况变得更加复杂。但是,为了直接获取num的值,惟一的办法就是调用get_num的原始版本,而它已经被覆盖了。
幸运的是,c++提供了一个简单的解决方案:即使你覆盖了一个成员,也总是能使用作用域操作符(::)来引用基类的版本。使用这种语法,就能调用get_num的原始版本:
1 | int ProperFraction::get_num() { |
即使Fraction是ProperFraction的一个间接基类,上述语法也是适用的。
其余代码则相当容易理解。你需要为ProperFraction类提供构造函数,其中包括从间接基类FloatFraction及其基类Fraction转换的构造函数。
1 | ProperFraction() { set(0, 1); } |
同样地,需要使用作用域操作符(::)来指定get_num的基类版本。
练习
- 写一个operator<<函数,它能和ProperFraction类配合,允许你直接打印到cout(以及其他输出流)。如有必要,请复习前几章的编程技术。记住,这个函数必须是一个声明为friend的全局函数:
1
2
3ProperFraction pf;
// ...
cout << pf << endl; - 为ProperFraction类写一个构造函数,它获取3个int参数,即w,n和d(分别代表真分数的整数部分、分子和分母)。例如,你可以输入4,2和3,将一个对象初始化为4 2/3。
ProperFraction pf(4, 2 ,3);
private成员和protected成员
在本章中,访问num和den的问题使问题变得更加复杂。子类FloatFraction和ProperFraction确实继承了两个数据成员,即num和den。然而,由于这些成员是private的,所以不能在代码中访问它们(当然,除了从Fraction类自身的代码中访问)。即使子类的代码也不能引用private成员。那些成员虽然存在,但相当于“隐形”的。
所以,在子类函数的定义代码中,你必须借助get_num和get_den函数来获取num和den的值。如前所述,一旦其中的一个函数被重载(get_num),情况会变得更复杂。假如子类能直接引用num和den,事情就简单了。在这种情况下,以下函数:
1 | int ProperFraction::get_num() { |
可以改写成:
1 | int ProperFraction::get_num() { |
然后,它可以通过内联来进一步缩短:int get_num() {return num/den;}
看起来,确实有必要让子类也能引用num和den。最理想的结果是确保子类的代码能够访问私有数据,同时禁止除了Fraction类及其所有子类之外的其他代码访问私有数据。
确实有办法做到这一点。c++提供了第三种访问基本,即protected(受保护的)。该基本介于private和public之间。假如num和den被赋予了这个访问级别(通过使用protected关键字),那么所有子类都能直接引用它们。
1 | class Fraction { |
值得注意的是,使用protected声明的成员仍然要对访问进行限制。只有Fraction类本身和它的(直接或间接)子类才能引用受保护的成员。
那么,能不能像这样更改Fraction类的代码呢?答案是“也许”。但在某些情况下,这是不可能的。假如Fraction类已经编译,并由另一个程序员将编译后的版本交给你,就不能更改类。在这种情况下,你只能尽量克服私有数据的限制。
另一个更加深入的问题是:需要将类成员声明为protected(而不是private)吗?一方面,你也许很快就能决定是不是应该声明为public,但另一方面,一旦需要在protected和private之间做出选择,局面就不那么明朗了。如果声明为private,表面你不希望任何人在写一个子类时更改它或者注意它。例如,让lcd和gcf这两个函数成为private函数也许时有意义的,因为任何人都不应该改动它。它们只由normalize调用,后者确实是惟一应该使用它的函数。
其他成员则也许应该设置成protected成员。normalize函数本身就是一个很好的候选者,因为该函数具有常规性的用处。除此之外,就像前面展示的那样,数据成员num和den在设置成protected成员之后,子类的作者可以很方便地引用它们。
但是,假如一个子类的作者试图直接设置num和den,而不是依赖set函数。就可能出问题。所以,取决于你对子类作者的信任程度,也许有必要使num和den保持private状态。
下面总结了c++的3个访问级别:
示例4:包含的成员(FractionUnits)
在本章的最后,我准备再开发一个子类(从Fraction继承),它包含一个新的数据成员,即units。子类添加的新数据成员可以具有任何类型,这里使用的是一种对象类型,换言之,是另一个类。如前所述,这种额外的类型——String——虽然不是当前的类层次结构的一部分,但包含在使用它的类中。
就像本章前两个例子做的那样,我假定基类Fraction的所有代码都放在文件Fract.h和Fract.cpp中,你需要将这两个文件添加到项目中。在此,我假定String类才采取类似的方式来处理,它应该由文件StringClass.h和StringClass.cpp来支持。为了运行下一个例子,首先根据上一章的代码来创建这些文件。除此之外,你还可以使用c++内建的string类(参见代码之后的“注意”)。
FractionUnits1.cpp
1 | #include "Fract.h" |
注意:如果你的编译器支持新的string类,就可以运行这个例子,具体做法是:(1)将#include”StringClass.h”替换成#include
;(2)string类而不是String来表明units;(3)在operator<<函数中,通过使用fr.unit.size()(而不是strlen(fr.units))来计算字符串的长度。
幕后玄机
和本章的其他例子一样,FractionUnits类声明了它自己的构造函数(记住,构造函数不会继承):
1 | FractionUnits() { set(0, 1); } |
除此之外,这个类还覆盖了几个函数。FractionUnits类的作用是允许类的用户存储某种形式的计量单位的名称(比如“feet”,“inches”,“pounds”)。它声明了一个新的数据成员units,并使其成为public成员,使FractionUnits对象的用户可以直接设置这个成员。
这个类实现了特定的行为,目的是简化单位处理(这反映了面向对象编程的一个基本概念:数据结构和行为应该一起设计和构建)。
- 包含相同单位的两个对象相加时,单位应该予以保留。例如,1/2 inches 加 1/3 inches的结果应该是5/6 inches。所以,add和operator+函数需要覆盖。
- 比较两个FractionUnits对象时,只有在两者具有相同的单位时,才应被视为相等。例如,1/4 feet等于 1/4 feet,但不等于1/4 miles。所以,operator==函数需要重载。
- 将一个FractionUnits对象打印到控制台时,应该打印单位(如果有的话)。例如:总之,类需要覆盖add,operator==以及operator+函数,而且要支持一个新版本的operator<<函数。
1
2
3FractionUnits fr(1, 2);
fr.units = "miles";
cout << fr;这里的函数定义尽量使用了基类的代码,因为根据面向对象的设计原则,应该尽可能地“重用”。例如,覆盖的add函数调用基类版本(Fraction::add)来完成它的大部分工作。1
2
3
4FractionUnits add(const FractionUnits &other);
FractionUnits operator+(const FractionUnits &other)
{return add(other);}
int operator==(const FractionUnits &other);在这里,operator+函数是一个很有趣的例子。虽然内部定义所做的事情似乎与Fraction类的版本相同,但事实上不是这样的。两个版本都调用了add函数,但是,它们各自调用的都是在它自己的类中定义的add函数版本。假如这个函数没有覆盖,operator+就会调用Fraction::add,而不是FractionUnits::add。1
2
3
4
5
6FractionUnits FractionUnits::add(const FractionUnits &other) {
FractionUnits fr = Fraction::add(other);
if(units == other.units)
fr.units = units;
return fr;
}最后,虽然operator<<函数从技术上说没有被覆盖(因为它不是一个成员函数),但是代码提供了该函数的一个版本,它与FractionUnits类进行交互。由于函数重载,这样做不会与operator<<的Fraction类的版本发生冲突。1
2
3FractionUnits operator+(const FractionUnits &other) {
return add(other);
}friend ostream &operator<<(ostream &os, FractionUnits &fr);
练习
通过覆盖mult和operator函数,为Fraction实现乘法。单位的任意组合都应该是允许的。如果一个对象包含的单位具有字符串s,而另一个是空字符串,那么单位应该设置成s。如果一个包含字符串s1,另一个包含字符串s2,结果单位就应该是s1s2。例如,1/2 feet乘以3/4 sec的结果是3/8 feet*sec。
小结
下面总结了本章的要点:
- 为了声明一个现有的子类,要使用以下语法:
1
2
3class 类名 : public 基类 {
声明
}; - 这个上下文中的public关键字并不是不可缺少的,但强烈建议你添加它(尤其是在你刚开始学习c++时)。假如基类具有private访问级别,那么基类中的所有成员在由子类继承时,都会成为private。
- 基类的所有成员都由子类继承(构造函数除外)。
- 子类中的声明可以指定新成员,这些新成员和继承的成员一道,都会成为子类的成员。另外,还可以在子类中覆盖基类现有的成员。通常,只有在需要提供不同的行为的前提下,才需要覆盖成员函数。
- 默认情况下,在引用一个类成员名称时,引用的是那个特定的类中的版本。例如,假定foo是一个被覆盖的函数,在引用foo时,引用的就是新版本,而不是基类的版本。然而,你仍然可以使用作用域操作符(::)来引用基类版本。例如,在以下的定义中,ProperFraction类中定义的get_num函数调用的是Fraction(基类)中定义的版本:
1
2
3
4int ProperFraction::get_num() {
int n = Fraction::get_num();
return n % get_den();
} - 记住,构造函数不会由子类继承。每个子类必须声明它自己的构造函数。
- 和以前一样,编译器会自动提供一个默认构造函数、拷贝构造函数以及operator=函数。对于子类,编译器提供的上述每一个函数都会调用基类版本。例如,默认构造函数将调用基类的默认构造函数,然后将子类的每个新成员设置成零或null值(但要注意:和以前一样,只有在你没有写任何一个构造函数的前提下,编译器才会提供默认构造函数)。
- 你通常都需要写一个构造函数,将基类的一个对象转换成子类的一个对象。在继承了成员函数的时候,这有助于解决类型冲突:
1
2
3
4
5
6class FloatFraction : public Fraction {
public:
// ....
FloatFraction(const Fraction &src)
{set(ser.get_num(), src.get_den());}
}; - 基类的私有成员虽然由一个子类继承,但不能在子类代码中访问。为了声明能由子类访问,但不能由类继承层次结果外部的代码访问的成员,你需要将它们声明为protected(这些成员也能由继承层次结构下方任何级别的间接子类访问)。
1
2protected:
int num, den; - 类(子类和普通类)可以包含对象成员。换言之,一个类可以包含另一个类的示例。
c++学习记录六:继承