第十一章 C++ 封装/继承/多态

面向对象程序设计(OOP)是一种计算机编程思想,主要目标是为了实现代码的重用性、灵活性和扩展性。面向对象程序设计以对象为核心,程序由一系列对象组成。对象间通过消息传递(一个对象调用了另一个对象的函数)相互通信,来模拟现实世界中不同事物间的关系。

面向对象程序设计有三大特性:封装,继承,多态。

封装的基本体现就是对象,封装使得程序的实现“高内聚、低耦合”的目标。封装就是把数据和数据处理包围起来,对数据的处理只能通过已定义的函数来实现。封装可以隐藏实现细节,使得代码模块化。继承允许我们依据一个类来定义另一个类。当创建一个类时,不需要重新编写新的数据成员和函数成员,只需继承一个已有类即可。这个已有类称为基类(父类),新建的类称为派生类(子类)。派生类就自然拥有了基类的数据成员和函数成员。这样做可以重用代码功能和提高执行效率。多态性是指不同的对象接收到同一个的消息传递(函数调用)之后,所表现出来的行为是各不相同的。多态是构建在封装和继承的基础之上的。多态就是允许不同类的对象对同一函数名的调用后的结果是不一样的。多态虽然概念有些复杂,但是只要理解了继承,多态就非常简单了。

我们创建一个怪物的父类,创建Monster.h和Monster.cpp文件,用于该类的声明和实现。以下是Monster.h 内容:

#pragma once
#include <iostream>
#include <string>
using namespace std;

// 定义一个怪物类
class Monster {

protected:
	int id;		    // ID
	string name;	// 名称
	int attack;		// 攻击值

public:
	
	// 声明默认构造方法
	Monster();

	// 声明有参构造方法
	Monster(int _id, string _name, int _attack);

	// 声明战斗方法
	void battle();
};

以下是Monster.cpp内容:

#include "Monster.h"

// 默认构造方法
Monster::Monster() {
	this->id = 1;
	this->name = "monster";
	this->attack = 0;
}

// 有参构造方法
Monster::Monster(int _id, string _name, int _attack) {
	this->id = _id;
	this->name = _name;
	this->attack = _attack;
}

// 定义战斗方法
void Monster::battle() {
	cout << name << " attack " << attack << " !" << endl;
}

类的声明和定义,其实就是函数的声明和定义的区别,当然我们也可以在头文件中定义函数的实现。Monster类主要定义了怪物的攻击特征,因为游戏中的怪物都会具备这样的共性。然后我们再声明蜘蛛Spider类和蛇Snake类,分别继承这个怪物Monster父类,这样他们就拥有了父类的攻击特征。为了简单方便,我们直接在Monster.h中定义这两个类,代码如下:

// 定义个蜘蛛(继承怪物)类
class Spider : public Monster {};

// 定义个蛇(继承怪物)类
class Snake : public Monster {};

注意,C++默认继承是private,也就是说子类不能访问父类的数据和函数,因此我们这里改用public,这样我们就能访问父类的数据和函数了。在我们的主文件ConsoleApplication.cpp中,我们可以这样使用这两个新类:

// 实例化一个Spider对象
Spider spider;
spider.battle();

// 实例化一个Snake对象
Snake snake;
snake.battle();

我们一般使用类去声明一个变量(对象)的时候,称之为实例化,这个变量(对象)也称之为类的一个实例。继承既简化了我们的代码,又能实现对应功能。另外一点,构造方法是不能被继承的。因此,在创建子类对象时,为了初始化从父类继承来的数据成员,我们需要手动完成子类的构造方法,并在其中也可以调用父类的构造方法。调用方式就是在Monster类的构造函数后,加一个冒号(:),然后加上父类的带参数的构造函数。这样,在子类的构造函数被调用时,系统就会去调用父类的带参数的构造函数去完成数据的初始化。这里面比较特殊的是父类默认构造方法,它是自动被子类的默认构造方法调用的,当然这个影响在实际开发中影响不大。我们给子类添加构造方法:

// 定义个蜘蛛(继承怪物)类
class Spider : public Monster {

public:
	// 调用父类的构造函数
	Spider() : Monster() {}
	Spider(int _id, string _name, int _attack) : Monster(_id, _name,_attack){}
};

// 定义个蛇(继承怪物)类
class Snake : public Monster {

public:
	// 调用父类的构造函数
	Snake() : Monster() {}
	Snake(int _id, string _name, int _attack) : Monster(_id, _name, _attack) {}
};

在我们的主文件ConsoleApplication.cpp中,我们可以使用新类的构造函数了:

// 实例化一个Spider对象
Spider spider2(1, "Spider", 100);
spider2.battle();

// 实例化一个Snake对象
Snake snake2(2, "Snake", 200);
snake2.battle();

函数重写(override):在基类中定义了一个普通函数,然后在派生类中又定义了一个同名同参数同返回类型的函数,这就是重写了。在派生类对象上直接调用这个函数名,只会调用派生类中的那个。例如我们重写父类的战斗方法,先在Monster.h做声明,然后在Monster.cpp文件中完成即可:

// 子类重写战斗方法(Monster.h)
void battle();

// 子类重写Spider类的战斗方法(Monster.cpp)
void Spider::battle() {

	attack += 100;
	cout << "Spider attack " << attack << "!" << endl;
}

在我们的主文件ConsoleApplication.cpp中,我们可以使用重写函数了:

// 实例化一个Spider对象
Spider spider;
spider.battle();

函数重载(overload):在基类中定义了一个普通函数,然后在派生类中定义一个同名,但是具有不同的形参表的函数,这就是重载。在派生类对象上调用这几个函数时,用不同的参数会调用到不同的函数,如果没有则仍然去父类寻找。例如我们重载父类的战斗方法,先在Monster.h做声明,然后在Monster.cpp文件中完成即可:

// 子类重载Spider类的战斗方法(Monster.h)
void battle(int _attack);

// 子类重载Spider类的战斗方法(Monster.cpp)
void Spider::battle(int _attack) {

	cout << "Spider attack " << _attack << "!" << endl;
}

在我们的主文件ConsoleApplication.cpp中,我们可以使用重载函数了:

// 实例化一个Spider对象
Spider spider;
spider.battle(500);

备注:派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。多继承即一个子类可以有多个父类,它继承了多个父类的特性。C++中一个派生类中允许有两个及以上的基类,我们称这种情况为多继承。使用多继承可以描述事物之间的组合关系,但是如此一来也可能会增加命名冲突的可能性。因此,在其他高级语言中,C#和Java是不允许多继承的!

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。简单的说,重写是父类与子类之间多态性的体现,而重载是同一个类的行为的多态性的体现。C++支持两种多态性:编译时多态性和运行时多态性(也称为静态多态和动态多态)。一般情况下,我们所说的多态都是指的运行时多态,也就是动态多态。

C++编译器在编译的时候,要确定每个对象调用的函数的地址。这种绑定关系是根据对象的数据类型来决定的。如果想要系统在运行时再去确定对象的类型以及正确的调用函数,就要在基类中声明函数时使用virtual关键字,这样的函数我们称为虚函数。我们说多态是在程序进行动态绑定得以实现的,而不是编译时就确定对象的调用方法的静态绑定。程序运行到动态绑定时,通过基类的指针所指向的对象类型,然后调用其相应的方法,即可实现多态。

构成多态还有两个条件:

1. 调用函数的对象必须是指针或者引用。

2. 被调用的函数必须是虚函数,且完成了虚函数的重写。

多态一般的用法,就是用父类的指针指向子类,然后用父类的指针去调用子类中被重写的虚函数。通过父类指针调用子类的虚函数,可以让父类指针有多种形态。在我们的实例中,我们首先需要在父类Monster中使用 virtual 来修饰战斗函数,如下所示:

// 父类声明战斗方法
virtual void battle();

如果父类Monster中的battle方法是一个普通的方法,即没有使用virtual修饰的话,那么虽然spiderPointer和snakePointer里面存储的的确是Spider类和Snake类的指针,但是他们依然只会执行父类Monster的battle方法。只有我们使用virtual修饰父类battle方法后,才能得到我们想要的正确执行结果。在我们的主文件ConsoleApplication.cpp中,我们可以使用多态了:

// 实例化一个Spider对象
Spider spider3(1, "Spider", 100);
// 实例化一个Snake对象
Snake snake3(2, "Snake", 200);

// 定义怪物对象指针
Monster* spiderPointer = &spider3;
spiderPointer->battle();	// 执行子类函数
Monster* snakePointer = &snake3;
snakePointer->battle();		// 执行父类函数

因为Snake类并没有重写battle函数,因此它只能调用父类的battle函数了。但是Spider类重新了battle函数,在重写的函数中,我们在原来的攻击值上增加了100。

备注:封装性是基础 ,继承性是关键 ,多态性是补充 ,多态性又存在于继承的环境之中 ,所以这三大特征是相互关联的 ,相互补充的。

接下来要理解的抽象的特性。面向对象程序设计中一切都是对象,对象都是通过类来描述的,但并不是所有的类都可以来描述对象的。如果一个类没有足够的信息来描述一个具体的对象,而需要其他具体的类来实现它,那么这样的类我们称它为抽象类。比如游戏怪物类Monster,它没有一个具体肖像,只是一个概念,需要一个具体的实体,如一只蜘蛛,一条蛇来对它进行特定的描述,我们才知道它的具体呈现。抽象类就是实现多态的一种机制。它定义了一组抽象的方法,至于这组抽象方法的具体内容由派生类来实现。同时抽象类提供了继承的概念,它的出发点就是为了继承,否则它没有存在的任何意义。在 C#和Java 中,可以通过两种形式来体现面向对象编程的抽象:抽象类(abstract)和接口(interface)。C++语言并没有像C#和Java那样对抽象类和接口有显式的支持。C++中只能使用virtual关键字来声明虚函数。C++语言中也没有抽象类的概念,但是可以通过虚函数实现抽象类。如果类中至少有一个函数被声明为虚函数,则这个类就是抽象类。抽象类只能用作父类被继承,子类必须实现虚函数的具体功能。C++ 接口则是使用抽象类来实现的。该类中没有定义任何的成员变量,所有的成员函数都是虚函数。接口就是一种特殊的抽象类。

最后还有讲一个组合的概念。类是一种构造数据类型,在类中可以其他类定义数据成员,这种数据成员称为对象成员。类中包含对象成员的形式,称为组合(Composition)。类之间的组合关系称为has-a关系,是指一个类拥有另一个类的实例对象。含有对象成员的类在调用构造函数对其数据成员进行初始化,其中的对象成员也需要调用其构造函数赋初值,语法如下:

<类名> :: <构造函数名> ([<形参表>]) : [对象成员1](<实参表1>) , [对象成员2](<实参表2>){...}

单冒号之后用逗号分隔的是类中对象成员和传递的实参,称为成员初始化列表。普通的数据成员既可以在构造函数中对其赋值,也可以在成员初始化列表中完成。对象成员只能在初始化列表中初始化,并且对象成员的构造函数的调用先于主类的构造函数。

首先我们构造两个类Sword.h(武器)和Player.h(玩家),代码如下:

#pragma once
#include <iostream>
#include <string>
using namespace std;

// 定义一把武器
class Sword {

protected:
	int id;		    // 唯一标示
	string name;	// 名称
	int attack;		// 攻击值

public:
	// 定义默认构造函数
	Sword() {
		
		id = 1;
		name = "Sword";
		attack = 10;
	}

	// 定义有参构造方法
	Sword(int _id, string _name, int _attack) {
		this->id = _id;
		this->name = _name;
		this->attack = _attack;
	}

	// 定义攻击方法
	void battle() {
		cout << name << " Sword attack " << attack << " !" << endl;
	}
};

然后是Player.h(玩家):

#pragma once
#include <iostream>
#include <string>
#include "Sword.h"
using namespace std;

// 定义一个玩家
class Player {

protected:
	int id;			    // 唯一标示
	string name;		// 名称
	Sword weapon;		// 武器类

public:

	// 默认构造函数,调用武器类的构造函数
	Player() : id(1), name("Player"), weapon() {}

	// 定义有参构造函数,调用武器类的构造函数
	Player(int pid, string pname, int sid, string sname, int sattact) : weapon(sid, sname, sattact) {
		this->id = pid;
		this->name = pname;
	}

	// 战斗函数,调用Sword的战斗函数
	void battle() {
		weapon.battle();
	}
};

然后在我们的主文件ConsoleApplication.cpp中,我们可以使用类的组合了:

// 类的组合使用
Player player(1, "小菜鸟", 1, "木剑", 10);
player.battle();

类的组合在程序开发过程中经常使用。面向对象程序开发中,一切皆为对象。每一个对象都代表了一个封装好的数据和功能集合。一个复杂的对象可以通过继承,组合等多种方式来实现。在Unity中,场景中所有的物体都视为游戏对象(GameObject),一个复杂的游戏对象由不同的组件对象(component )构成。我们可以给一个游戏对象添加各种不同的组件来让该游戏对象具有不同的功能。这其实就是类组合的应用。

本课程的所有代码案例下载地址:

C++示例源代码(配合教学课程使用)-C/C++文档类资源-CSDN下载

备注:这是我们游戏开发系列教程的第一个课程,主要是编程语言的基础学习,优先学习C++编程语言,然后进行C#语言,最后才是Java语言,紧接着就是使用C++和DirectX来介绍游戏开发中的一些基础理论知识。我们游戏开发系列教程的第二个课程是Unity游戏引擎的学习。课程中如果有一些错误的地方,请大家留言指正,感激不尽!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咆哮的程序猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值