继承与派生
C++的属性
C++ 有三大属性
- 封装
- 继承
- 多态
封装的内容在前几节课已经讲过了,现在就来讲讲继承的概念
程序代码
每个文章对于知识点的讲解都是围绕着一个程序的展开,所以先给出程序代码
1 | //继承与派生 |
继承
什么是继承:继承允许我们依据另一个类来定义一个类。当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
例如:在程序中,类CStudent继承了Cperson的类,我们称Cperson类为基类
,CStudent类为派生类
。但是经常称为子类
和父类
的关系
我们下面将从多个角度来分析子类与父类
继承的命名规则
在程序中,子类继承父类类的命名规则是class CStudent :public CPerson
为什么这个地方采用public呢,其实命名的形式是
class derived-class: access-specifier base-class
,对于access-specifier,被称为访问修饰符
,可以是 private,public,protected,的任意一种,如果不显示的表示出来,则默认的是private,在日常的使用和开发中,我们大部分情况下使用public,关于这三者的区别
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
需要注意的是:
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
protected
在CPerson
类中,我们发现,对于变量的定义不是在private
下,而是换成了protected
的范围内,所以,什么是protected呢,为什么要进行privated的限制呢?
protected:protected的正是为了继承而诞生了,假如父类的私有成员的访问权限是private,当子类继承了父类以后,子类的成员函数如果像对父类进行成员的访问,此时是访问不了的,因为private的封装性,保证了父类的私有函数只能由父类的成员函数来访问,此时我们把private换成protected,就可以使子类来访问父类中的私有函数。
一个类, 如果希望, 它的成员, 可以被自己的子类(派生类)直接访问,但是, 又不想被外部访问那么就可以把这些成员, 定义为 protected访问权限
子类的构造函数
由于子类继承了父类,子类可以直接使用父类的成员函数,父类的私有变量等等,所以在构造函数的时候,不仅要为子类本身的私有变量赋值,并且要为父类的私有变量赋值,需要注意的是
子类不存在默认的构造函数,必须显示的构造。并且父类必须带参构造,构造要先于子类
我们看一下程序中是如何进行子类的构造函数
1 | CStudent(int nId=1000000, const char* sClass="未初始化", |
具体分析一下这种构造形式:
1.对于
1 | (int nId=1000000, const char* sClass="未初始化",const char* sName="未初始化", const char* sSex = "男", int y = 1900, int m = 1, int d = 1) |
这一部分,是当在主函数调用构造函数时,如果没有参数,则默认参数如上。 其中nId和sClass都是CSstudent中的变量,sName,sSex,以及y,m,d都是CPerson中的变量。需要注意的是,CPerson中有对象成员
CMyDate birthday,所以y,m,d其实是CMyDate对象的私有变量。上文中强调,对于父类的构造,需要先于子类的构造,这个与上文讲过的对象成员的构造一模一样,由于子类的构造是在构造函数的函数体内,所以如果我们想要父类的构造先于子类,那么只能在函数体外构造,所以程序中用了这样的形式
CPerson(sName,sSex,y,m,d)
其实,这是调用了父类的构造函数,我们看父类的构造函数
1 | CPerson(const char* sName, const char* sSex, int y, int m, int d):birthday(y, m, d){} |
由于birthday是对象成员,所以必须在CPerson 对象的构造之前完成。与上一篇文章不同的地方是,在上一篇文章中,在形参列表中没有y,m,d这些变量,而是直接采用了birthday(2003,06,14)这种形式,只不过用参数来表示的形式更加的灵活,可以自定义birthday中的值,而不是采取默认值
我们发现,在子类函数的构造函数中,实际上是通过(调用父类构造函数+自身私有变量的赋值)这种形式来进行构造的,通过这个例子,我们应该明确一种思想,就是子类继承了父类,但是子类的构造只需要把只属于子类的变量进行初始化即可,继承自父类的变量仍然由父类自身初始化。这种思想在后面的setData函数和inputData函数中仍有体现
函数接口
从上面的的析构函数中容易看出子类在对其成员初始化的时候,是非常麻烦的,所以我们应当为每个类都设置一个函数接口就是inputData
和setData
这两个函数,其中input 函数是输入数据,用于初始化的时候使用,而setData函数则是设置数据,用于设置的时候使用,这样就方便了我们对类的成员的设定。但是这样也会出现很多问题需要解决
问题
CPerson
类中由对象成员CMyDate birthday,由于birthday既是成员又是一个类,所以在CPerson的setData和inputData函数里面,我们可以直接调用CMyDate中的函数来使用,
正如程序中使用的那样直接使用**birthday.setData(y, m, d)和birthday.inputData()**即可,但是在CStudent中没有CPerson的对象成员,所以无法像birthday那样调用。由于CSstudent是CPerson的子类,所以可以直接调用CPerson中的inputData和setData函数来使用。
BUT 此时出现了一个问题,就是在CSstudent中有自己的setData函数,那么(当父类函数与子类函数重名的时候,父类函数会被覆盖),从而无法调用父类函数来对父类的私有成员进行赋值,所以我们需要使用域限定!
域限定
为了解决父类函数和子类函数重名的问题,引出了域限定这个概念
什么是域限定:
关键字:::
使用方式:正如程序中所示CPerson::setData(sName, sSex, y, m, d)
在子函数中使用那样,域限定的使用可以显示的说明一个函数到底是子类的函数还是父类的函数
域限定并不少见,无论是上一篇文章的静态成员的类外初始化,还是 成员函数在类外的定义,都是需要域限定来显示标明范围
思想
通过上面的函数接口,我们就很容易的想到,子类继承父类,但是在子类的成员设置或者函数的初始化,都要求子类只对专属于自己的私有成员进行初始化,属于父类的成员需要调用父类的函数来初始化。