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

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已经调用了构造函数进行赋值操作,所以可以安全上路









  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Tkinter是Python的一个标准GUI库,可以用于创建图形用户界面。贪吃虫程序是一个经典的小游戏,玩家控制一个虫子在屏幕上移动并吃掉食物,每吃到一个食物,虫子的身体会变长一节。下面是一个简单的Tkinter贪吃虫程序的编写示例: ```python import tkinter as tk import random class SnakeGame(tk.Canvas): def __init__(self, master): super().__init__(master, width=400, height=400, background="black") self.snake_body = [(100, 100), (90, 100), (80, 100)] self.food = self.create_food() self.direction = "Right" self.bind_all("<Key>", self.on_key_press) self.score = 0 self.speed = 150 self.game_loop() def create_food(self): while True: x = random.randint(1, 39) * 10 y = random.randint(1, 39) * 10 if (x, y) not in self.snake_body: return self.create_oval(x, y, x+10, y+10, fill="red") def move(self): head_x, head_y = self.snake_body[0] if self.direction == "Up": new_head = (head_x, head_y - 10) elif self.direction == "Down": new_head = (head_x, head_y + 10) elif self.direction == "Left": new_head = (head_x - 10, head_y) else: new_head = (head_x + 10, head_y) self.snake_body.insert(0, new_head) if self.food in self.snake_body: self.score += 1 self.delete(self.food) self.food = self.create_food() if self.score % 5 == 0: self.speed -= 10 else: self.delete(self.snake_body.pop()) if (new_head[0] < 0 or new_head[0] >= 400 or new_head[1] < 0 or new_head[1] >= 400 or new_head in self.snake_body[1:]): self.game_over() else: self.create_rectangle(new_head[0], new_head[1], new_head[0]+10, new_head[1]+10, fill="green") def game_loop(self): self.move() if not self.game_over_flag: self.after(self.speed, self.game_loop) def on_key_press(self, event): keysyms = event.keysym.lower() if keysyms in ["up", "down", "left", "right"]: opposite_directions = {"Up": "Down", "Down": "Up", "Left": "Right", "Right": "Left"} if keysyms != opposite_directions[self.direction]: self.direction = keysyms def game_over(self): self.delete(tk.ALL) self.create_text(self.winfo_width() / 2, self.winfo_height() / 2, text=f"Game Over! Score: {self.score}", fill="white", font=("Courier", 20), justify=tk.CENTER) self.game_over_flag = True root = tk.Tk() root.title("Snake Game") root.resizable(False, False) game = SnakeGame(root) game.pack() root.mainloop() ``` 这个示例代码使用了Tkinter的Canvas组件来创建游戏界面,通过继承Canvas类并重写相关方法来实现贪吃虫游戏的逻辑。玩家可以通过方向键控制虫子的移动方向,当虫子吃到食物时,得分增加并且速度加快。当虫子碰到边界或者自己的身体时,游戏结束。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值