您的位置:首页 > 编程语言 > C语言/C++

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

2017-12-14 20:40 337 查看

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 baoyan
Name:Harry Potter
ID:112124
Age:19
Gender:M
Department:Computer Science
sizeof(string)=4
sizeof(CStudent)=16
sizeof(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已经调用了构造函数进行赋值操作,所以可以安全上路

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++ 继承 派生