抽象、封装、继承和多态
继承
继承是面向对象编程最重要的概念之一。
继承允许我们根据另一个类来定义一个类。 这有助于更轻松地创建和维护应用程序。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为 (base) 基类,新建的类称为(derived)派生类。
派生类继承了基类的所有特性,并且可以拥有自己的附加特性。
我做了一张图 更能清晰的了解基类和派生类的关系
为了演示继承关系,我们通过创建一个Father类和Daughter类来进行演示。
class Father
{
public:
Father() {};
void sayHellow() {
cout << "过年好!\n";
}
};
class Daughter
{
public:
Daughter() {};
};
Father类中有一个sayHellow()的公共方法.
创建派生类
实例中通过Father类派生出Daughter类。
class Daughter : public Father
{
public:
Daughter() {};
};
通过:(冒号)加上public(访问说明符)可以指定基类,public代表基类中的所有公共成员在派生类中同样也是公共的。
我们可以理解为,Father类中的所有公共成员都成为了Daughter类的公共成员。
调用基类的方法
由于Father类中的所有公共成员都被Daughter类继承了。我们可以创建一个Daughter类型的对象,并通过该对象调用Father类中的sayHellow()函数。
#include <iostream>
using namespace std;
class Father
{
public:
Father() {};
void sayHellow() {
cout << "过年好!\n";
}
};
class Daughter: public Father
{
public:
Daughter() {};
};
int main() {
Daughter d;
d.sayHellow();
}
//结果将会输出:过年好!
派生类继承了所有的基类方法,但有以下几个例外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的重载运算符。
- 基类的友元函数。
通过逗号进行分隔可以让派生类指定多个基类。例如 狗:public 哺乳动物,public 犬科动物
Protected 访问修饰符
到目前为止,我们只使用了public和private访问说明符。
Public成员可以从类外任何地方访问,而private成员的访问仅限于类和友元函数。
正如我们以前所见,使用公共方法访问私有类变量是一个好习惯。
除此之外还有一个访问说明符 - protected。
protected的成员变量或函数与私有成员非常相似,唯一的区别就是 - 可以在派生类中访问它。
class Father {
public:
void sayHellow() {
cout << var;
}
private:
int var=0;
protected:
int someVar;
};
someVar可以被Father类的所有派生类访问,而var则不行。
访问说明符也被用来指定继承的类型。
注意,我们使用public来继承Father类:
class Daughter: public Father
继承的类型也支持private和protected
- 公共继承(Public Inheritance):基类的public成员成为派生类的public成员,基类的protected成员成为派生类的protected成员。 基类的private成员永远不能直接从派生类访问,但是可以通过调用基类的public和protected成员来访问。
- 受保护继承(Protected Inheritance):基类的public和protected成员成为派生类的受保护成员。
- 私有继承(Private Inheritance):基类的public和protected成员成为派生类的私有成员。
Public是最常用的继承类型,继承类型默认为Private
派生类的构造和析构函数
- 构造函数 - 首先调用基类的构造函数,然后调用派生类的构造函数。
- 析构函数 - 首先调用派生类的析构函数,然后调用基类的析构函数。
当继承类时,基类的构造函数和析构函数不会被继承。
但是,当派生类的对象被创建或删除时,它们将被调用。
为了进一步解释这种行为,我们来创建一个包含构造函数和析构函数的示例类:
class Father {
public:
Father()
{
cout <<"Father构造函数"<<endl;
}
~Father()
{
cout <<"Father析构函数"<<endl;
}
};
在main中创建一个对象会产生一下的输出:
int main() {
Father f;
}
/* 输出
Father构造函数
Father析构函数
*/
接下来,让我们创建一个Daughter类,使用它自己的构造函数和析构函数,并使其成为Father的派生类:
class Daughter: public Father {
public:
Daughter()
{
cout <<"Daughter构造函数"<<endl;
}
~Daughter()
{
cout <<"Daughter析构函数"<<endl;
}
};
基于前面几题的基础上,当我们创建一个Daughter对象时会发生什么?(Daughter继承了Father,并且他们都声明了自己的构造函数与析构函数)
int main() {
Daughter f;
}
/*输出
Father 构造函数
Daughter 构造函数
Daughter 析构函数
Father 析构函数
*/
抽象
数据抽象是向外界提供唯一重要信息的概念。
这是一个表示基本特征而不包括实现细节的过程。
一个好的现实世界的例子是一本书:当你听到书,你不知道确切的细节,即页数,颜色,大小,但你明白一本书的想法 - 抽象这本书。
抽象的概念是我们关注基本本质,而不是一个特定例子的具体特征。
抽象函数
前几节的例子演示了派生类与基类指针的使用方法。接下来我们接着之前游戏的例子,我们的每一个角色都有一个attack()函数。
为了能够让Role指针为每一个派生类提供调用attack()函数,我们需要在基类将函数声明成抽象函数。
在基类中声明一个抽象函数,在派生类中使用相应的函数,多态允许使用Role指针来调用派生类的函数。
每个派生类将覆盖attack()函数并有一个单独的实现:
class Role{
public:
virtual void attack() {
}
};
class Warrior: public Role {
public:
void attack() {
cout << "剑刃风暴!"<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "冰暴!"<<endl;
}
};
通过关键字virtual可以将基类的函数声明成抽象函数。
现在,我们可以使用Role指针来调用attack()函数。
int main() {
Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;
r1->attack();
r2->attack();
}
/* 输出:
剑刃风暴!
冰暴!
*/
由于attack()函数被声明为抽象的,它就像一个模板,告诉派生类自己有一个attack()函数。
如果基类中的函数是抽象的,则派生类中的函数实现将根据所引用对象的实际类型进行调用,而不管原先声明的是那种类型。
声明或者继承了一个抽象函数的类被称为一个多态类。
纯虚函数
在某些情况下,你希望在一个基类中包含一个抽象函数,以便它可以在派生类中被重新定义以适应该类的对象,但是没有有意义的定义可以给基类中的函数类。
没有定义的抽象成员函数被称为纯虚函数。他们指定派生类自己定义该函数。
语法是用= 0(一个等号和一个零)替换它们的定义:
class Role {
public:
virtual void attack() = 0;
};
一个纯虚函数基本上定义了派生类将自己定义的那个函数。
从具有纯虚拟函数的类继承的每个派生类必须重写该函数。
如果纯虚函数没有在派生类中重写,那么当您尝试实例化派生类的对象时,代码将无法编译并导致错误。
抽象类
你不能对一个有纯虚函数的基类创建对象。
下列例子将会报错。
Role r; // Role拥有一个纯虚函数,这样创建对象将会报错
这些类被称为抽象类。他们只能被当作基类使用,因此被允许具有纯虚函数。
多态
多态按字面的意思就是多种形态。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
简单地说,多态意味着单个函数可以有多个不同的实现。
接下来我们将用例子来更清晰的了解多态。
假设我们现在要制作一个简单的游戏
游戏需要先创建一个角色
角色可以选择很多种不同的职业:法师,战士,射手等。
但是这些职业有一个共同的功能就是攻击。
不过由于职业性质的不同攻击的方式也会不一样,
在这种情况下,多态允许在不同的对象上调用相同的攻击函数,
但会导致不同的行为。
第一步是创建角色
class Role {
protected:
int attackPower;
public:
void setAttackPower(int a){
attackPower = a;
}
};
我们的Role类有一个名为setAttackPower的公共方法,它设置受保护的成员变量attackPower。
接下来我们为两个不同的职业,战士和法师创建类。这两个类都将继承Role类,所以他们都有attackPower(攻击力),但是他们又都有自己特定的攻击方式。
class Warrior: public Role{
public:
void attack() {
cout << "剑刃风暴! - "<<attackPower<<endl;
}
};
class Magician: public Role {
public:
void attack() {
cout << "冰暴! - "<<attackPower<<endl;
}
};
如上所示,他们的攻击方式各不相同。接下来我们准备创建我们的战士和法师的对象。
int main() {
Warrior w;
Magician m;
}
战士和法师都继承了Role类,所以战士和法师都是角色。所以我们可以实现以下的功能。
Role *r1 = &w;
Role *r2 = &m;
我们现在已经创建了两个Role类型的指针,指向战士和法师的对象。
现在,我们可以调用相应的功能:
int main() {
Warrior w;
Magician m;
Role *r1 = &w;
Role *r2 = &m;
r1->setAttackPower(50);
r2->setAttackPower(80);
w.attack();
m.attack();
}
/* 输出:
剑刃风暴! - 50
冰暴! - 80
*/
通过直接在对象上调用函数,我们可以达到相同的效果。 但是,使用指针效率更高。
封装
封装这个词的部分含义是 “围绕” 一个实体的想法,不仅仅是把内在的东西放在一起,而且还要保护它。
在面向对象方面,封装不仅仅是将一个类中的属性和行为组合在一起,这也意味着限制进入该类的内部工作。
这里的关键原则是一个对象只显示其他应用程序组件需要有效运行应用程序的内容。其他一切都被保留在视野之外(隐藏)。
这被称为数据隐藏。
class Animal {
public:
virtual void say() = 0;//纯虚函数
};
class Cat :public Animal{
public:
void say() { cout << "喵喵喵\n"; };
};
class Dog :public Animal {
public:
void say() { cout << "汪汪汪\n"; };
};
int main()
{
Cat cat;
Dog dog;
//都是动物对象 对 猫和狗进行了封装 封装成了动物
Animal *a1 = &cat;
Animal *a2 = &dog;
a1->say();
a2->say();
}
/*
喵喵喵
汪汪汪
*/
我们应该隐藏这个属性,控制对它的访问,所以它只能被对象本身访问。这样,balance 不能直接从物体外面改变,只能用其方法进行访问。
这也被称为“黑匣子”,是指关闭对象的内部工作区域,除了我们想要公开的部分。
这使我们可以在不改变整个程序的情况下改变方法的属性和实现。例如,我们可以稍后再回来,更改 balance 属性的数据类型。
总之封装的好处是:
-
控制数据访问或修改的方式。
-
代码更加灵活,易于根据新的要求进行更改。
-
更改代码的一部分而不影响其他代码部分。