C++ 多态
前言
最近跟着b站的黑马C++教程视频链接学了下关于多态的知识点,其核心点是虚函数和纯虚函数,以下是正文
一、多态的基本概念
首先,多态是什么?
多态按字面意思就是多种形态,当我们写代码时,有多个类,且类之间存在层次结构,且类之间是通过继承关联时,就会用到多态。
多态可以分为静态多态和动态多态,我们一般指多态时都是指动态多态
二者的区分可以看如下例子
#include<iostream>
using namespace std;
// 多头的基本概念
class Animal
{
public:
// 虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
// 猫类
class Cat : public Animal
{
public:
//重写 函数返回值类型 函数名 参数列表 完全相同
void speak()
{
cout << "小猫在说话" << endl;
}
};
// 执行说话的函数
// 地址早绑定 在编译阶段确定函数地址
// 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
//动态多态满足条件
//1、有继承关系
//2、子类重写父类的虚函数
// 动态多态使用
//父类的指针或者引用, 执行子类对象
void doSpeak(Animal& animal) // Animal & animal = cat;
{
// c++之间允许类的类型转换
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
}
int main()
{
test01();
system("pause");
return 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
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
上面是动态多态的例子,当我们把Animal类的speak函数的关键字virtual删去时,得到的Cat和Animal就是静态多态。
从这个代码中,我们也能看到,多态的其实就是我们使用父类的指针或者引用,去指向子类对象,如代码中的
void doSpeak(Animal& animal) // Animal & animal = cat;
{
// c++之间允许类的类型转换
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
doSpeak函数的形参类型是Animal类的引用,而实际调用时,却传入子类Cat的对象,而这就是多态的体现。多态在我的理解就是一个变量实际上可以有多种可能对象,而静态多态与动态多态的区别就在于关键字virtual,有了virtual,在传入参数时,就会根据对象的类去选择函数地址,而这种对函数地址的确定方式叫地址晚绑定,而静态多态则是地址早绑定,早绑定是指在编译阶段就确定函数的地址,而晚绑定则是在运行时才确定函数地址。这样在执行程序时,就会根据传入实际参数的类去选择具体的函数。
动态多态满足条件
两点:
1、有继承关系。
2、子类重写父类的虚函数。
纯虚函数与抽象类
什么是纯虚函数
区别于虚函数,从上面我们可以知道,写了虚函数的基类,实际上,如果我们在上述的dospeak()函数中,传入的参数是一个基类对象的话,那么会执行基类中虚函数。但是,纯虚函数是不行的,甚至含有纯虚函数的类是无法实例化对象的。
纯虚函数语法
virtual 返回值类型 函数名() = 0;
纯虚函数,区别于虚函数,是没有函数的定义(即具体实现的),因为是不会执行纯虚函数的。
当一个类中如果有纯虚函数,我们就称它为抽象类。
抽象类
抽象类的条件:有纯虚函数。
抽象类的特点:
1、无法实例化对象。
2、抽象类的子类,必须要重写父类中的纯虚函数,否则也属于抽象类。
相关代码如下
#include<iostream>
using namespace std;
#include<string>
// 纯虚函数和抽象类
class Base
{
public:
// 纯虚函数
// 只要有一个纯虚函数,这个类就称为抽象类
// 抽象类特点:
// 1、 无法实例化对象
// 2、抽象类的子类,必须要重写父类中的纯虚函数,否则也属于抽象类
//
virtual void func() = 0;
};
class Son : public Base
{
public:
virtual void func()
{
cout << "Son下func函数调用" << endl;
}
};
void test01()
{
//Base b; // 抽象类是无法实例化对象
//new Base; // 栈区、堆区都不行
Base* base = new Son;
base->func();
}
void test02()
{
for (int i = 0; i < 5; i++)
{
cout << "i" << endl;
}
}
int main()
{
//test01();
test02();
system("pause");
return 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
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
虚析构和纯虚析构
我们在之前学过类的构造与析构,我们在日常使用他们时,一般是在构造中new(即动态申请内存),然后在析构中delete释放内存。但是在我们使用多态时,按照如上使用
下述代码为例子
#include<iostream>
using namespace std;
#include<string>
// 虚析构和纯虚析构
class Animal
{
public:
// 纯虚函数
virtual void speak() = 0;
Animal()
{
cout << "Animal构造函数调用" << endl;
}
// 利用虚析构,可以解决 父类指针释放子类对象时不干净的问题
/*virtual ~Animal()
{
cout << "Animal虚析构函数调用" << endl;
}*/
//纯虚析构 需要声明 也需要定义实现
// 有了纯虚析构之后,这个类也属于抽象类, 无法实例化对象
~Animal();
};
Animal:: ~Animal()
{
cout << "Animal析构函数调用" << endl;
}
class Cat : public Animal
{
private:
string* m_name;
public:
Cat(string name)
{
m_name = new string(name);
cout << "Cat构造函数调用" << endl;
}
// 未调用Cat析构函数,因为走了Animal的析构,所以要把Animal的析构置虚
~Cat()
{
if (m_name != NULL)
{
delete m_name;
m_name = NULL;
}
cout << "Cat析构函数调用" << endl;
}
// 重写纯虚函数
virtual void speak()
{
cout << *m_name << "小猫在说话" << endl;
}
};
void test01()
{
Animal* animal = new Cat("Tom");
animal->speak();
// 父类指针在析构时候, 不会调用子类中析构函数,导致子类中如果有堆区属性, 出现内存泄露
delete animal;
}
int main(int argc, char** argv)
{
test01();
system("pause");
return 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
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
可以看见,我们传入子类对象,并且会走子类构造函数,new一下然后再子类析构中delete一下,但是运行结果确实这样子的
并没有走子类的析构函数,这样会造成内存泄漏的问题。而之所以不会走子类中的析构函数是我们没在父类中使用虚析构或者纯虚析构,虚析构基本类似于之前提到的虚函数
纯虚析构
接下来将重点介绍,纯虚析构
先来使用纯虚析构修改过的上诉代码
#include<iostream>
using namespace std;
#include<string>
// 虚析构和纯虚析构
class animal
{
public:
// 纯虚函数
virtual void speak() = 0;
animal()
{
cout << "animal构造函数调用" << endl;
}
// 利用虚析构,可以解决 父类指针释放子类对象时不干净的问题
/*virtual ~animal()
{
cout << "animal虚析构函数调用" << endl;
}*/
//纯虚析构 需要声明 也需要定义实现
// 有了纯虚析构之后,这个类也属于抽象类, 无法实例化对象
virtual ~animal() = 0;
};
animal:: ~animal()
{
cout << "animal纯虚析构函数调用" << endl;
}
class cat : public animal
{
private:
string* m_name;
public:
cat(string name)
{
m_name = new string(name);
cout << "cat构造函数调用" << endl;
}
// 未调用cat析构函数,因为走了animal的析构,所以要把animal的析构置虚
~cat()
{
if (m_name != null)
{
delete m_name;
m_name = null;
}
cout << "cat析构函数调用" << endl;
}
// 重写纯虚函数
virtual void speak()
{
cout << *m_name << "小猫在说话" << endl;
}
};
void test01()
{
animal* animal = new cat("tom");
animal->speak();
// 父类指针在析构时候, 不会调用子类中析构函数,导致子类中如果有堆区属性, 出现内存泄露
delete animal;
}
int main(int argc, char** argv)
{
test01();
system("pause");
return 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
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
可以看到,使用了纯虚析构后,程序也会执行子类对象的析构函数,避免了内存泄漏的问题。
使用虚析构或者纯虚析构的目的:
防止使用父类指针或者引用,释放子类对象不干净,造成内存泄漏。
纯虚析构的特征:
1、使用纯虚析构,区别于纯虚函数,即需要声明,也需要定义(具体实现)。
2、有了纯虚析构后,该类也属于抽象类,无法实例化对象,子类继承不需要重写纯虚析构函数。
3、使用父类指针指向子类对象时,也会走父类的析构函数。
子类不需要重写好理解,因为子类与父类的析构函数名一定不一致。
总结
之前只是听老师讲多态,这次去敲了下,果然体验不一样。
对于多态,我个人理解就和之前学过的类、结构体嵌套,但是又有所不同。总的来说,多态在使用父类指针指向子类对象时,就像夹心糖一样,会最先走父类的构造,最后走父类的析构,父类就像是子类对象的皮,子类是心。