【C++的探索路10】继承与派生之基本性质篇

来源:互联网 发布:抽拉式油烟机 知乎 编辑:程序博客网 时间:2024/06/16 04:58

Introduction

重载为C++多态的一个体现,继承与派生除了有多态的体现外,还有体现出了代码的复用性。继承与派生这部分内容将对涉及该方面的内容进行拓展学习。
首先看看书中章节部分的内容:
内容还是略多,现对这部分分割为三类,分类具体涉及如下:
这篇文章先由基础性质入手,后续的章节对继承与派生的剩下内容进行补足。

继承与派生的基本概念

继承与派生的作用

继承和派生这两个词我们分别在法律与英语中经常听到:甲继承了他爹的遗产、某某某词派生出某某某词。
继承与派生的作用当然与前面的类、运算符重载的目的一样,是为了实现更加便捷的编程,通过利用继承与派生可以显著的提升编程效率。
其实现方法可以简写为:求同存异(取共同属性,避免重复)

涉及概念

基类与派生类

C++中的继承没有法律上的继承那么复杂,只涉及最纯粹的父子关系。
这种父子关系中包含了两个类--父类与子类:子类继承于父类,父类衍生出子类。
父类的别名为基类,这里的基是基于的意思;子类又可称作派生类由父类派生而来。

子承父业之儿子中有老子

子类既然是继承于父类,因此可以认为父类是作为形参传值给了子类,假设父类为class F,子类为class Z
子类的定义方式为
class Z:public F{函数体}
学术点的描述就是:继承类有了基类作为其的形参。

别动老子东西之访问与内存

儿子在家里肯定是要守规矩的,有些老爷子的私有物品是不能动的;在C++中也是遵守这一规则:子类不能访问父类的私有(private)成员
但不能动不意味着他就没有这些家伙事
如上面程序,假设F类中含有int v1,v2两个变量。而在class Z的类中含有int v3成员变量,则
sizeof(F)=8,而sizeof(Z)=12;这就是因为他在骨子里掌握着他老子的内功心法。

程序实战

编程,最重要的事就是make your hands dirty
依照惯例,扔个main函数

int main(){CStudent s1;CUndergraduateStudent s2;s2.SetInfo("Harry Potter", "112124", 19, 'M', "Computer Science");cout << s2.GetName() << " ";s2.QualifiedForBaoyan();s2.PrintInfo();cout << "sizeof(string)= " << sizeof(string) << endl;cout<<"sizeof(CStudent)= "<< sizeof(CStudent) << endl;cout << "sizeof(CUndergraduateStudent)= " << sizeof(CUndergraduateStudent) << endl;/*输出结果Harry Potter Qualified for baoyanName:Harry PotterID:112124Age:19Gender:MDepartment:Computer Sciencesizeof(string)=4sizeof(CStudent)=16sizeof(CUndergraduateStudent)=20*/return 0;}

接下来就是对程序进行解析,以及内部功能实现

程序解析

主程序前三行定义了学生类s1,以及研究生类s2,对是否保研进行判断。我们知道研究生是学生种类中的一种,因此研究生类可以作为学生类的子类进行编程处理。
程序第四行为初始化行,依次输入:姓名、学号、年龄、性别、专业,这些东西都需要作为内部成员变量进行输入。
剩下几行依次调用成员函数:GetName、QualifiedForBaoyan、PrintInfo实现了研究生类的获取姓名、保研信息(Qualified for baoyan)显示以及学生的信息打印。
在最后调用一系列sizeof对类的大小进行输出,可以看到研究生类可能比学生类多出一个成员变量。

依次实现

第一步:定义学生类与研究生类

在主程序的SetInfo里面包含有五个参,但看最后的sizeof,可以看到研究生类只比学生类大4,因此应该是多了Major这个成员变量。
class CStudent {public:string name, stunum;int age; char gender;};class CUndergraduateStudent:public CStudent {string major;public:};
请注意:这次尝试中由于不具备一些编程基础,暂时将成员变量定义为public,这不是一个好的习惯。
定义为public的原因与继承的子类无法调用父类的私有成员有关系。

第二步:完善函数

依次对成员函数进行完善
第一个函数SetInfo,如下述
void CUndergraduateStudent::SetInfo(string nam, string num, int ag, char gen, string maj) {name = nam; stunum = num; age = ag; gender = gen; major = maj;}

第二个函数GetName,作用为打印名字,具体如下
string CStudent::GetName() {return name;}

第三个函数QualifiedForBaoyan作用应该为打印Qualified for baoyan
void QualifiedForBaoyan() {cout << "Qualified for baoyan" << endl;}

第四个函数PrintInfo()依次将姓名,学号,年龄、性别以及学院进行打印
void CUndergraduateStudent::PrintInfo() {cout << "Name: " << name << endl << "ID: " << stunum << endl << "Age: " << age << endl << "Gender: " << gender << endl;cout << "Department: " << major << endl;}

反馈补充

关于编程的良好习惯

编完以后,发现和书上的并不完全是一回事,主要是在成员的私有化方面出了问题。
子类不能直接访问父类的私有成员变量,但并不是意味着不能间接的访问。间接的访问就是使用父类的函数接口进行赋值,调用模式则为"父类::成员函数的形式",改好后,程序如下:
class CStudent {private:string name, stunum;int age; char gender;public:void SetInfo(string nam, string num, int ag, char gen);void PrintInfo();string GetName();};void CStudent::SetInfo(string nam, string num, int ag, char gen) {name = nam; stunum = num; age = ag; gender = gen; }void CStudent::PrintInfo() {cout << "Name: " << name << endl << "ID: " << stunum << endl;cout << "Age: " << age << endl << "Gender: " << gender << endl;}string CStudent::GetName() {return name;}class CUndergraduateStudent :public CStudent {string major;public:void SetInfo(string nam, string num, int ag, char gen, string maj);void QualifiedForBaoyan() {cout << "Qualified for baoyan" << endl;}void PrintInfo();};void CUndergraduateStudent::SetInfo(string nam, string num, int ag, char gen, string maj) {CStudent::SetInfo(nam, num, ag, gen);major = maj;}void CUndergraduateStudent::PrintInfo() {CStudent::PrintInfo();cout << "Department: " << major << endl;}

关于sizeof的大小问题

如果在VS2017上运行了上面两种程序,我们会发现,sizeof输出的结果并不是所谓的4,16,20,而是

这是由于string类在VS中有不同的实现方法
但即使是这样,CStudent的成员变量的sizeof数值应当是2*string+1*int+1*char=28*2+4+1=61而不是64,多出来的3是什么鬼?
这是由于计算机在CPU和内存之间传送数据都是以4字节或8字节为单位进行的,出于传输效率的考虑,编译器直接将gender补齐了3个字节。

Review

正确处理类的复合关系与继承关系

复合关系

类与类之间产生联系,除了继承以外,还可以通过复合这种关系进行联系;所谓复合关系就是数学上的包含。相比较而言,继承倾向为拥有。为说明白他们两的区别,举个例子:
假设我们定义一个点类,点类的基本形式需要定义它的横纵坐标,因此可以定义为
class CPoint {double x, y;};
如果我们还需要定义一个圆类,第一件事就是确定它的圆心,第二件事是确定半径,根据继承的思想,可以把圆类写成:
class CCircle :public CPoint {double radius;};
虽然表面上无伤大雅,并且实现了这一功能;但实际上对程序想要表达的意思进行了歪曲:因为圆根本就不是点;从而一定程度影响了程序的可读性。正确的写法为:
class CCircle {CPoint center;double radius;};
而CCircle类就是所谓的封闭类,CPoint center则为封闭类的成员。

其他状况:
如果我们已经编写了CWoman类,此时需要定义一个CMan类,如果我们采用
class CMan:public CWoman的形式,显然不妥:男人并不是女人,很多地方无法直接运用。正确的写法是概括出CMan与CWoman的共性,写出CHuman类,再由CHuman派生出这两个类。

正确的处理复合关系之主人养狗

假设一个小区中有户主养狗,每个户主最多养10条狗。如何编写程序?

法一:你中有我,我中有你

说实话,一开始我也是这么写的
class CDog;class CMaster {CDog dogs[10];int dogNum;};class CDog {CMaster m;};
但用编译器跑一遍,编译器会报错:
CMaster::dogs使用未定义的 class"CDog"
也就是所谓的循环定义的现象。

法二:指针指一下

避免循环定义的方法就是使用指针,因为指针是地址,大小固定为4个字节,不需要知道CDog类是什么样子。
class CDog;class CMaster {CDog *dogs[10];int dogNum;};class CDog {CMaster m;};
经运算,程序正常运行。
但还是不够好,因为每条狗里面都包含有主人的信息;那么这又有什么不好呢?
第一点:当多条狗属于一个主人的时候,也就是多个CDog对象都包含同一个CMaster对象,造成了重复的冗余。
第二点:如果主人的个人信息发生变化,比如我把狗转让给另外一个人,那么还需要一个个去查找,非常麻烦。

法三:为狗类设置一个主人类的指针

class CDog;class CMaster {CDog *dogs[10];int dogNum;};class CDog {CMaster *m;};
相互指引,又不浪费内存,这种方案最佳

protected访问范围说明符(传家宝问题)

由继承的基本概念可知:
1,派生类(子类)可以访问基类(父类)的公有成员。
2,但是儿子不能动老子的私有物品
但是这两种性质引发一个问题:传家宝如何安全的由孩子他爹传给他孩子?
既然是传家宝,就应该适度设一个访问权限:比如private;但设置为private的话,挨打挨惯了的孩子显然没胆去看一眼;设置为public的话,又怕被贼惦记。

他爹在这个时候就想出一个办法:放出神兽:private对传家宝进行看护。如何看呢?
1,可以允许他孩子访问。
2,不允许他的私生子访问:避免由于财产争夺,引发家庭问题。

第二点的意思是:只能访问成员函数所作用的那个对象的基类保护成员,而不能访问其他同类对象的基类保护成员。

举个例子:
class CBase {private:int nprivate;public:int npublic;protected:int nprotected;};class CDerived :public CBase {void AccessBase(){npublic = 1;nprivate = 1;nprotected = 1;CBase f;f.nprotected=1;}};int main() {CBase b;CDerived d;int n = b.nprotected;n = d.nprivate;int m = d.npublic;return 0;}
将上面一段代码输入到VS2017中,我们会发现
祖国山河一片红:
问题首先出现在AccessBase的
nprivate中,很好解释:私有成员无法被访问。

第二个问题出现在f.nprotected。
这是为什么呢?因为别人家的儿子怎么能来动你家儿子的传家宝?!!

第三个问题是一样的道理

第四个问题更简单:私有成员无法为外部访问。

正是基于protected的这么一个很好的性质,所以常用的做法是将需要隐藏的成员说明为保护,而不是私有。

总结


派生类的构造函数与析构函数

我们知道,类的使用需要进行初始化操作,初始化就要用构造函数;而构造完了还需要析构函数收拾烂摊子。
对于继承与派生而言,析构与构造函数同样是非常重要的。

基本概念

派生类的构造函数

由前面定义:继承类包含有基类的成分,因此:在派生类构造之前必须对基类对象进行构造。
必须交代清楚包含的基类对象是如何进行初始化的;在书写过程中在派生类构造函数后面添加初始化列表,初始化列表中知名调用基类构造函数的形式
具体写法如下:
构造函数名(形参表):基类名(基类构造函数实参表){
}


派生类的析构函数

在析构的时候则和构造相反:先析构派生类再析构基类,其实原理相同。


程序分析

原始程序

class CBug {int legNum, color;public:CBug(int lN, int c) :legNum(lN), color(c) {cout << "CBug constructor called" << endl;}~CBug() {cout << "CBug deconstructed" << endl;}void PrintInfo() {cout << legNum << "," << color << endl;}};class CFlyingBug :public CBug {int wingNum;public:CFlyingBug(int ln, int cl, int wn):CBug(ln,cl),wingNum(wn) {cout << "CFlyingBug Constructor called" << endl;}~CFlyingBug(){cout << "CFlyingBug deconstructor called" << endl;}};int main() {CFlyingBug fb(2, 3, 4);fb.PrintInfo();return 0;}
程序运行后,依次输出
CBug constructor called
CFlyingBug Constructor called
2,3
CFlyingBug deconstructor called
CBug deconstructed
打印顺序与前面的分析一样:先执行父类的构造,有了父亲才有儿子;析构时先析构派生类,最后析构基类。
注意书写参数表的形式,直接进行赋值操作。

加点料

1,如果在CFlyingBug里面加上CFlyingBug(){}构造函数会发生什么呢?
会报错,因为CBug没有初始化
2,如果CBug内部构造函数进行参数赋值,然后将CFlyingBug对象中的参数表去掉?
不会报错,这是因为CBug已经调用了构造函数进行赋值操作,所以可以安全上路









原创粉丝点击