C++多态
封装、继承、多态是面向对象语言的三大特征。
简介
**封装:**把客观事物进行抽象建模,这个建模出来的逻辑结构就是抽象的类。类中包含了变量和方法,这些访问权限不同的变量和方法 ,可以被不同授信的其他类或者对象进行访问,而将实现细节及不对外展示的功能及信息进行屏蔽及隐藏。
**继承:**将现有内的现有功能在不改变现有内逻辑的情况下进行扩展时,新类传承现有类并重写现有类的方法或者新增新的功能。这个过程就是继承,新生成的类称为“子类”或“派生类”;当前的类称为“基类”、“父类”或“超类”。继承的过程是一个从通用、一般到异化、特殊的过程。
多态: 相同的对象收到不通的消息或者不同的对象收到相同的消息而产生不同的动作即为动态。实现多态有两种方式,包括重载和覆盖。重载是通过定义不同的输入参数来区分这些方法,在其后的调用中,目标对象爱就会根据参数样式的差去选择合适的方法;而覆盖则是子类继承父类虚方法的时候对父类方法进行重写,在将子类类型的指针赋值给父类类型的指针时,调用父类的虚方法时就会根据其实际调用的子类而调用子类中该方法的实现。
分类
**静态多态:**通过重载实现的多态即为静态多态。
#pragma once
#include<string>
#include<iostream>
using namespace std;
class CPlayer
{
public:
CPlayer(){};
~CPlayer(){};
public:
void Play(std::string path)
{
cout << "Play local files with the path = " << path << endl;
}
void Play(std::string url, int type)
{
cout << "Play network files with the path = " << url;
cout << "and the type = " << type;
}
};
int main()
{
CPlayer pPlayer;
pPlayer.Play("D://MyMisic/hello.mp3");
pPlayer.Play("http://192.168.170.154:8014/player?file=hello", 1);
return 0;
}
**动态多态:**子类继承父类并重写其虚方法去覆盖父类方法而实现的多态即为动态多态。
#pragma once
#include<string>
#include<iostream>
using namespace std;
class BasePlayer
{
public:
BasePlayer() {
cout << "Create BasePlayer!" << endl;
}
virtual ~BasePlayer() {
cout << "Distory BasePlayer!" << endl;
}
public:
virtual void Play()
{
cout << "This player is base palyer!" << endl;
}
};
class LocalPlayer :
public BasePlayer
{
public:
LocalPlayer(std::string path) {
m_stringFilePath = path;
cout << "Create LocalPlayer!" << endl;
};
virtual ~LocalPlayer() {
if (m_pData)
{
delete m_pData;
m_pData = nullptr;
}
cout << "Distory LocalPlayer!" << endl;
};
public:
virtual void Play()
{
cout << "This player is LocalPlayer! ";
cout << "Play local files with the path = " << m_stringFilePath << endl;
}
private:
std::string m_stringFilePath;
char* m_pData;
};
class NetworkPlayer :
public BasePlayer
{
public:
NetworkPlayer(std::string url,int type) {
m_nType = type;
m_stringUrl = url;
cout << "Create NetworkPlayer!" << endl;
};
virtual ~NetworkPlayer() {
if (m_pData1)
{
delete m_pData1;
m_pData1 = nullptr;
}
cout << "Distory NetworkPlayer!" << endl;
};
public:
virtual void Play()
{
cout << "This player is NetworkPlayer! ";
cout << "Play url file with the path = " << m_stringUrl;
cout << ", type = " << m_nType << endl;
}
private:
std::string m_stringUrl;
int m_nType;
char* m_pData1;
};
int main()
{
BasePlayer* pLocPlayer = new LocalPlayer("D://Music/hello.mp3");
BasePlayer* pNetPlayer = new NetworkPlayer("http://192.168.170.154:8014/player?file=hello", 1);
if (pLocPlayer)
{
pLocPlayer->Play();
}
if (pNetPlayer)
{
pNetPlayer->Play();
}
if (pLocPlayer)
{
delete pLocPlayer;
pLocPlayer = nullptr;
}
if (pNetPlayer)
{
delete pNetPlayer;
pNetPlayer = nullptr;
}
system("pause");
}
动态多态的原理
- 动态多态是子类继承父类,并重写父类的方法,父类的方法需要定义为虚函数,C++通过修饰符virtual 来使方法变成虚函数。
执行以上动态多态的代码,运行的结果如下:
- 而我们将父类的析构函数改成非虚函数时,运行的结果如下:
从上面的运行结果我们可以看出以下信息:
- 通过派生类生成父类对象时,对象的构造过程是:
- 父类对象的销毁过程如下:
可以看出如果不将父类的析构函数设置成虚函数,子类的析构函数将得不到执行,这样当子类存在自己开辟的资源或者内存时,子类资源或者内存就会泄漏。 - 通过将父类成员函数设置成虚函数,子类继承方法后重写方法的实现,在将子类类型的指针赋值给父类类型的指针后,通过父类指针去调用对应的虚函数方法时,就会根据其实际对应的子类调用子类中重写的方法。
虚函数工作原理
整体上来概括就是:虚函数表+虚表指针。
编译器在处理含有虚方法的抽象类时,会给其添加一个隐藏的成员用来保存一个指针,这个指针指向一个保存着内对象所有函数地址的数组。这个指针称为虚表指针,保存内对象函数地址的数组称为虚函数表。给予这个原理每个类都会有一个虚函数表,类的对象都会有一个虚表指针。看如下示例:
#pragma once
#include<string>
#include<iostream>
using namespace std;
class BasePlayer
{
public:
BasePlayer() {};
virtual ~BasePlayer() {};
public:
//设置用户信息
virtual void SetUser(SUserInfo info) {};
//播放音频文件
virtual void Play() {};
//转换音频格式
virtual void Conversion() {};
};
class LocalPlayer :
public BasePlayer
{
public:
LocalPlayer(std::string path) {};
virtual ~LocalPlayer() {};
public:
virtual void Play() {};
virtual void Conversion() {};
private:
...
};
class NetworkPlayer :
public BasePlayer
{
public:
NetworkPlayer(std::string url, std::string path, int type) {};
virtual ~NetworkPlayer() {};
public:
virtual void Play() {};
virtual void Cache(string path) {};
private:
...
};
下图是上面代码底层的实现原理:
从这着原理图我们可以看出:
- 每个类都会有一个其对应的虚函数表,父类有父类的虚函数表,子类有子类的虚函数表,而类的对象通过虚表指针指向该类的虚函数表。
- 如果子类未重写父类的方法A,则子类方法A在虚函数表中对应的地址将同父类中方法A的地址保持一样。
- 如果子类重写了父类的方法A,则子类方法A在虚函数表中对应的地址将覆盖其重父类继承来的地址而变成重写后子类方法A的新地址。
- 如果子类中增加了新的虚方法B,则子类的虚函数表中将会把新增的虚方法B的地址加入进去。
故而我们在将子类对象的指针赋值给父类指针,通过父类指针去访问虚方法的时候,这个父类指针对象中虚表指针指向的并不是父类虚函数表,而是其赋值对象子类的虚函数表。在通过这个指针调用虚方法时,才会找到其真正需要执行的方法动作。
对应以上解释,示例如下:
#include "Player.h"
int main()
{
BasePlayer* pLocPlayer = new LocalPlayer("D://Music/hello.mp3");
BasePlayer* pNetPlayer = new NetworkPlayer("http://192.168.170.154:8014/player?file=hello","D://Music/" 1);
if (pLocPlayer)
{
pLocPlayer->Play();
}
if (pNetPlayer)
{
pNetPlayer->Play();
}
if (pLocPlayer)
{
delete pLocPlayer;
pLocPlayer = nullptr;
}
if (pNetPlayer)
{
delete pNetPlayer;
pNetPlayer = nullptr;
}
system("pause");
}