多态

多态是C++面向对象三大特效之一

多态的基本概念

多态分为两类:

​ 静态多态:如函数重载,运算符重载

​ 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态的区别:

​ 静态多态的函数地址早绑定,编译阶段确定函数地址

​ 动态多态的函数地址晚绑定,运行阶段确定函数地址

补充:子类可以成为父类的引用,例如当一个函数传入父类的引用时,在调用函数时可以传入子类。

举个例子讲讲什么是动态多态,什么又是地址晚绑定。例如,当我们创建一个动物类(父类)时,在类内写了一个void函数void what( ),输出I am animal,这时我们再写一个子类猫,在猫类内也写一个同名的函数输出I am cat。这时写一个传入父类引用的函数,调用函数时传入子类猫。正常来说,此时函数会输出I am animal,这个属于静态多态,函数what 的地址早在编译阶段就绑定animal.what( )里的I am animal了。

我们自然是想要输出I am cat 的,这时候就要用动态多态,让地址晚绑定,在运行阶段时绑定地址,根据具体的子类执行对应的函数。

要满足动态多态的条件有:

有继承关系

子类重写父类的虚函数(函数返回值,函数名,参数列表都完全相同)

父类的指针或者引用执行子类对象,例如例子里的cat子类对象,由父类的引用对象animal执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<iostream>
class Animal{
public:
//加上virtual关键字
virtual void what(){
printf("I am animal");
}
};
class Cat: public Animal{
public:
//子类的重写函数的virtual可写可不写
void what(){
printf("I am cat");
}
};
void ShowWhat(Animal &animal){
animal.what();
}
int main(){
Cat cat;
ShowWhat(cat);
return 0;
}

I am cat

多态的原理剖析

image-20240207234523854

如图,当类内定义虚函数时,会产生一个虚函数指针和虚函数表,指针指向表,子类会继承父类的虚函数指针,但是当子类重写父类虚函数时,子类的虚函数地址会覆盖继承下来的虚函数表里面的父类的虚函数地址,这时当父类的指针或引用指向子类对象时,会发生多态。若对象是cat(子类),vfptr(从父类继承下来的)就指向Cat类(子类)的vftable。

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类的重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:

1
virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类称为抽象类,有以下特点:

​ 无法实例化对象

​ 子类必须重写抽象类中的纯虚函数,否则子类也属于抽象类

虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式 : 将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

​ 可以解决父类指针释放子类对象

​ 都需要有具体的函数实现

虚析构和纯虚析构区别:

​ 如果是纯虚析构,该类属于抽象类,无法实例化对象

纯虚析构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
#include<string.h>
using namespace std;
class Animal{
public:
virtual void Speak() = 0;
virtual ~Animal() = 0;
};
Animal::~Animal(){
//纯虚析构函数调用
}
class Cat: public Animal{
public:
Cat(string name){
//Cat构造函数
Name = new string(name);
}
void Speak(){
printf("Cat is saying!");
}
~Cat(){
//Cat的析构函数
if(this->Name != NULL){
delete Name;
Name = NULL;
}
}
string *Name;
};

虚析构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
#include<string.h>
using namespace std;
class Animal{
public:
virtual void Speak() = 0;
virtual ~Animal(){
//虚析构调用
}
};
class Cat: public Animal{
public:
Cat(string name){
//Cat构造函数
Name = new string(name);
}
void Speak(){
printf("Cat is saying!");
}
~Cat(){
//Cat的析构函数
if(this->Name != NULL){
delete Name;
Name = NULL;
}
}
string *Name;
};

基本一致

1
2
3
4
5
int main(){
Animal *animal = new Cat("Tom");
animal->Speak();
delete animal;
}

多态的案例

多态的优点:

​ 代码组织结构清晰

​ 可读性强

​ 利于前期和后期的扩展以及维护

案例1 计算机类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//先实现一个计算机抽象类
class Calculator{
public:
virtual int result(){
printf("Error!!");
return 0;
}
int num1,num2;
};
//加法计算器
class Add: public Calculator{
public:
int result(){
return num1 + num2;
}
};
//减法计算器
class Sub: public Calculator{
public:
int result(){
return num1 - num2;
}
};

以此类推,能一直扩展计算器的运算功能,以此创建一个对象试着计算一下

1
2
3
4
5
6
7
//实现加法运算
Calculator *ptr = new Add;
ptr->num1 = 100;
ptr->num2 = 100;
printf("%d", ptr->result());
delete ptr;
//输出200

文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件

文件类型分为两种!:

​ 文本文件:文件以文本的ASCII码形式存储在计算机中

​ 二进制文件:文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们

操作文件的三大类:
ofstream:写操作

​ ifstream:读操作

​ fstream:读写操作

文本文件

写文件

1
2
3
4
5
6
7
8
9
10
11
#include<fstream>
int main(){
//创建流对象
ofstream ofs;
//打开文件
ofs.open("文件路径",打开方式);
//写数据
ofs<<"写入的数据";
//关闭文件
ofs.close();
}

文件打开方式

image-20240208014845434

注意:文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件

1
ios::binary|ios::out

读文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<fstream>
int main(){
//创建流对象
ifstream ifs;
//打开文件并判断文件是否打开成功
ifs.open("文件路径",打开方式);
if(!ifs.is_open()){
printf("打开失败");
}
//读数据(四种方式)
......
//关闭文件
ifs.close();
}

读数据的四种方式(介绍一下常见的)

1
2
3
4
string buf;
while(getline(ifs,buf)){
cout<<buf<<endl;
}
1
2
3
4
char buf[1024] = {0};
while(ifs.getline(buf, sizeof(buf))){
cout<<buf<<endl;
}
1
2
3
4
char buf[1024] = {0};
while(ifs>>buf){
cout<<buf<<endl;
}
1
2
3
4
5
//一个一个读
char c;
while((c = ifs.get()) != EOF){
cout << c;
}

二进制文件

打开方式要指定ios: :binary

写文件

二进制方式写文件主要是利用流对象调用成员函数write

1
ostream& write(const char *buffer, int len);

buffer指向内存中的一段存储空间,len是读取的字节数

读文件

二进制方式读文件主要利用流对象调用成员函数read

1
istream& read(char *buffer,int len);

字符指针buffer指向内存中一段存储空间。len是读写的字节数