面向对象三大特征有:封装、继承和多态,本篇为多态篇。
多态:相同对象收到不同消息或者不同对象收到相同消息有不同的操作。多态可分为静态多态和动态多态。
静态多态:同一个方法接受不同的参数会有不同的操作(输出)。
动态多态:基于继承和封装,不同类的对象对于相同的输入有不同的操作(输出)。
1、虚函数与虚函数表
先给出一个例子抛个砖引个玉,我们想实现一个基本的面向对象问题:定义一个形状基类,和一个圆形类和矩形类,能否使得生成的圆形对象和矩形对象正确的计算出各自的面积:
// 定义一个图形基类
class Shape
{
public:
double calcArea()
{
cout << "calcArea" << endl;
return 0;
}
};
// 定义圆形类,继承自图形类
class Circle : public Shape
{
private:
float m_fRadius;
public:
Circle(float r):m_fRadius(r){}
double calcArea()
{
return 3.14 * m_fRadius * m_fRadius;
}
};
// 定义矩形类,继承自图形类
class Rect : public Shape
{
private:
int m_iHeight;
int m_iWidth;
public:
Rect(int height, int width):m_iHeight(height), m_iWidth(width){}
double calcArea()
{
return m_iHeight * m_iWidth;
}
};
// main函数测试上述代码能否实现计算不同图形的面积功能
int main()
{
Shape *shape1 = new Circle(4.0);
Shape *shape2 = new Rect(3, 5);
cout << shape1 -> calcArea();
cout << shape2 -> calcArea();
delete shape1;
delete shape2;
shape1 = NULL;
shape2 = NULL;
return 0;
}
使用上面的代码并不能实现我们想要的功能,只会打印出两行父类中calcArea中的“calcArea”字符。那么到底应该怎么做才能实现我们想要的功能呢?这时候就需要使用到虚函数了,在基类Shape中的calcArea函数前面加上virtual关键字可以解决这个问题。(为了程序的严谨,请在Circle类和Rect类中的calcArea函数前面也加上virtual关键字!)
1.1 多态中存在的问题:内存泄漏
同样的,看下面代码:
#include <iostream>
using namespace std;
class Shape
{
public:
virtual double calcArea()
{
cout << "calcArea()" << endl;
return 0;
}
};
class Coordinate
{
public:
Coordinate(double x, double y) : m_dX(x), m_dY(y){}
private:
double m_dX;
double m_dY;
};
class Circle : public Shape
{
public:
Circle(int x, int y, double radius)
{
m_pCenter = new Coordinate(x,y);
m_dRadius = radius;
}
~Circle()
{
// 释放内存
delete m_pCenter;
m_pCenter = NULL;
}
virtual double calcArea()
{
return 3.14 * m_dRadius * m_dRadius;
}
private:
double m_dRadius;
Coordinate* m_pCenter;
};
int main()
{
Shape* shape = new Circle(3, 5, 4.2);
shape -> calcArea();
// 在执行delete shape时会出现问题
delete shape;
shape = NULL;
return 0;
}
在上述代码的main函数中delete shape时会出现内存泄漏。因为delete的是父类的指针,因此只会执行父类的析构函数,子类Circle中的析构函数则不会执行,因此Circle析构函数中的delete m_pCenter语句不会被执行,也就是说在这里会造成内存泄漏。(同样的如果delete的是子类指针,则父类和子类的析构函数都会被执行,先执行子类的析构函数再执行父类的析构函数)
因此需要引入虚析构函数,即将父类的析构函数改为虚析构函数:
同时子类中的析构函数前面也加上virtual关键字:
*为什么使用虚析构函数就能实现先访问子类的析构函数之后再访问父类的析构函数呢?
因为有虚函数表指针的存在。如果在类中存在虚函数,编译后就会产生一个虚函数表,并且在对象所在的内存中多一项虚函数表指针(该指针在对象所在内存中的起始处)指向该虚函数表,对象通过该虚函数表指针访问其对应的虚函数。
1.2 virtual在使用中的限制
- 普通函数不能是虚函数
- 静态成员函数不能是虚函数
- 内联函数不能是虚函数
- 构造函数不能是虚函数
2、纯虚函数和抽象类
2.1 纯虚函数
纯虚函数没有函数体并且在函数体的后面会有 =0的标记,如下:
virtual double calcArea() = 0;
2.2 抽象类
含有纯虚函数的类叫作抽象类。抽象类不能实例化对象!
抽象类的继承类如果没有实现纯虚函数也是抽象类,只有抽象类的继承类把所有的纯虚函数都实现了,子类才能实例化对象。
3 接口类
如果抽象类中没有数据成员仅有函数且所有函数均为纯虚函数,这样的类叫作接口类。接口类更多的是用来表达一种能力或协议。