教程:https://www.imooc.com/learn/474
静态多态(早绑定)
举例说明:
两个函数互为重载
第一个只有一个参数,调用的是第一个函数;第二个有两个参数,调用的是第二个函数。可见,程序在运行之前,在编译阶段,就已经确定使用哪个函数了,这样子叫做“早绑定”,也就是“静态多态”
动态多态(晚绑定)
举例说明:
都是要计算面积,但圆形算面积的方法跟矩形的是不同的,像这样,对不同的对象下达相同的指令,但却做不同的操作,叫做“动态多态”
使用动态多态要有封装和继承为基础。在封装篇,把数据封装到类中;在继承篇,将封装了的各个类形成了继承关系。
动态多态至少有两个类,只有使用三个类及以上,动态多态才表现得更为明显
举例讲解:
定义一个形状类:
定义一个圆形类(是形状类的子类):
圆面积的计算(是圆形类中的public方法):
定义一个矩形类(是形状类的子类):
矩形面积的计算(是矩形类中的public方法):
主函数:
可以使用一个父类的指针,指向其中一个子类的对象Circle,使用另外一个父类的指针,指向另一个子类的对象Rect,如此,两个子类的对象都被父类的指针所指向,在进行操作时,是通过父类的两个指针分别调用两个子类的计算面积方法(calcArea())。
像这样写,结果只是调用了两次父类的calcArea(),即下图定义时写的方法:
所以要实现动态多态,用以上方法不可实现
方法:用“virtual”关键字修饰成员函数,使其成为虚函数
以上例举例:
定义父类时:
定义子类时:
此时这个virtual不是必须的,如果没写,系统会自动加,但建议加上,方便阅读区分
主函数:
编码示例:
Shape.h
#ifndef SHAPE_H //用宏定义避免重复包含
#define SHAPE_H
#include <iostream>
using namespace std;
class Shape
{
public:
Shape();
~Shape();
double calcArea();
};
#endif // !SHAPE_H
Shape.cpp
#include "Shape.h"
#include <iostream>
using namespace std;
Shape::Shape()
{
cout << "Shape()" << endl;
}
Shape ::~Shape()
{
cout << "~Shape()" << endl;
}
double Shape::calcArea()
{
cout << "Shape->calcArea()" << endl;
return 0;
}
Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
class Circle : public Shape
{
public :
Circle(double r);
~Circle();
double calcArea();
protected:
double m_dR;
};
#endif // !CIRCLE_H
Circle.cpp
#include "Circle.h"
#include <iostream>
using namespace std;
Circle::Circle(double r)
{
cout << "Circle()" << endl;
m_dR = r;
}
Circle :: ~Circle()
{
cout << "~Circle()" << endl;
}
double Circle::calcArea()
{
cout << "Circle->calcArea()" << endl;
return 3.14*m_dR*m_dR;
}
Rect.h
#ifndef RECT_H
#define RECT_H
#include "Shape.h"
class Rect : public Shape
{
public :
Rect(double width, double height);
~Rect();
double calcArea();
protected:
double m_dWidth;
double m_dHeight;
};
#endif // !RECT_H
Rect.cpp
#include "Rect.h"
#include <iostream>
using namespace std;
Rect::Rect(double width, double height)
{
cout << "Rect()" << endl;
m_dHeight = height;
m_dWidth = width;
}
Rect :: ~Rect()
{
cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
cout << "Rect->calcArea()" << endl;
return m_dHeight * m_dWidth;
}
demo.cpp
#include "Circle.h"
#include "Rect.h"
#include <iostream>
using namespace std;
int main()
{
Shape *shape1 = new Rect(3, 6);
Shape *shape2 = new Circle(5);
shape1->calcArea();
shape2->calcArea();
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
return 0;
}
由输出结果知,这样子编码,通过shape1、shape2只能调用到Shape类的calcArea(),且析构函数只执行了父类的
修改:
Shape.h
改为:
virtual double calcArea();
这样子实现了动态多态,但还是建议在子类的函数前也加上virtual
虚析构函数
动态多态中可能会存在内存泄漏问题
举例说明:
定义一个Shape类:
定义一个Circle类:
多定义了一个数据成员(Coordinate的指针),代表圆心坐标。
会在构造函数中实例化一个坐标对象:
在析构函数执行时释放对象
主函数:
如果delete后跟着的是父类的指针,只会执行父类的析构函数,如果跟着的是子类的指针,则会执行父类和子类的析构函数。
所以这样写,没有执行子类的析构函数,造成内存泄漏(因为在实例化Circle对象的时候,从对中申请了内存作为原点——m_pCenter = new Coordinate(x,y);
)
用虚析构函数解决
虚析构函数: virtual->析构函数
如上图,用virtual修饰(父类的)析构函数
此时子类的析构函数可加可不加,最好加(以便还有类继承)
virtual在函数中的使用限制
1、普通函数不能是虚函数:
只有类里面的成员函数才能用virtual修饰
2、静态成员函数不能是虚函数
静态成员函数不属于任何一个对象,是和类“同生共死”的。
3、内联函数不能是虚函数
如果用virtual修饰,会让系统忽视“inline”关键字,视为纯粹的虚函数
4、构造函数不能为虚函数
编码示例:
Coordinate.h
#ifndef COORDINATE_H
#define COORDINATE_H
class Coordinate
{
public:
Coordinate(int x, int y);
~Coordinate();
private:
int m_iX;
int m_iY;
};
#endif // !COORDINATE_H
Coordinate.cpp
#include "Coordinate.h"
#include <iostream>
using namespace std;
Coordinate::Coordinate(int x, int y)
{
cout << "Coordinate()" << endl;
m_iX = x;
m_iY = y;
}
Coordinate ::~Coordinate()
{
cout << "~Coordinate()" << endl;
}
Shape.h
#ifndef SHAPE_H //用宏定义避免重复包含
#define SHAPE_H
#include <iostream>
using namespace std;
class Shape
{
public:
Shape();
~Shape();
virtual double calcArea();
};
#endif // !SHAPE_H
Shape.cpp
#include "Shape.h"
#include <iostream>
using namespace std;
Shape::Shape()
{
cout << "Shape()" << endl;
}
Shape ::~Shape()
{
cout << "~Shape()" << endl;
}
double Shape::calcArea()
{
cout << "Shape->calcArea()" << endl;
return 0;
}
Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
#include "Coordinate.h"
class Circle : public Shape
{
public :
Circle(double r);
~Circle();
double calcArea();
protected:
double m_dR;
Coordinate *m_pCenter;
};
#endif // !CIRCLE_H
Circle.cpp
#include "Circle.h"
#include <iostream>
using namespace std;
Circle::Circle(double r)
{
cout << "Circle()" << endl;
m_dR = r;
m_pCenter = new Coordinate(3, 5); //从堆中申请内存
}
Circle :: ~Circle()
{
cout << "~Circle()" << endl;
delete m_pCenter;
m_pCenter = NULL;
}
double Circle::calcArea()
{
cout << "Circle->calcArea()" << endl;
return 3.14*m_dR*m_dR;
}
Rect.h
#ifndef RECT_H
#define RECT_H
#include "Shape.h"
class Rect : public Shape
{
public :
Rect(double width, double height);
~Rect();
double calcArea();
protected:
double m_dWidth;
double m_dHeight;
};
#endif // !RECT_H
Rect.cpp
#include "Rect.h"
#include <iostream>
using namespace std;
Rect::Rect(double width, double height)
{
cout << "Rect()" << endl;
m_dHeight = height;
m_dWidth = width;
}
Rect :: ~Rect()
{
cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
cout << "Rect->calcArea()" << endl;
return m_dHeight * m_dWidth;
}
demo.cpp
#include "Circle.h"
#include "Rect.h"
#include <iostream>
using namespace std;
int main()
{
Shape *shape1 = new Rect(3, 6);
Shape *shape2 = new Circle(5);
shape1->calcArea();
shape2->calcArea();
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
return 0;
}
执行Coordinate的析构函数是因为Circle.cpp Circle的析构函数实例化了一个Coordinate的对象,该对象的销毁在~Circle()中,但这里并没有执行Circle的析构函数,造成内存泄漏
解决:
Shape.h
virtual ~Shape();
如果Circle类中没有Coordinate *m_pCenter;
那么没有虚析构函数也是可以的
虚函数与虚析构函数原理
指针指向对象——对象指针
指针指向函数:
中间时5个函数的函数地址,当指针指向函数入口,命令计算机开始执行时,计算机使函数体得到执行,直到执行完毕
函数指针与普通指针本质一样,也是由四个基本内存单元组成,存储一个内存的地址,这个内存的地址就是函数的首地址
举例说明:
当实例化一个Shape对象的时候,在这个Shape对象中,除了m_iEdge这个数据成员外,还有一个数据成员,称为虚函数表指针,虚函数表指针也是一个指针,占有四个基本内存单元。虚函数表指针指向一个虚函数表,这个虚函数表会与Shapele的定义一起出现。虚函数表也会占用空间,这里假设虚函数表的起始位置为0xCCFF,那么这个虚函数表指针的值就是0xCCFF。父类的虚函数表只有一个,通过父类实例化出来的所有对象,其虚函数表指针的值都是0xCCFF,确保该类的每一个对象的虚函数表指针都指向自己的虚函数表。在父类Shape的虚函数表中,肯定定义了一个函数指针,这个函数指针指向calcArea()这个函数的地址,这里假设calcArea()函数的入口地址是0x3355,那么虚函数表中的calaArea_ptr当前的位置就是0x3355。
调用时,先找到虚函数表指针,通过虚函数表指针找到虚函数表,通过位置的偏移找到相应的虚函数的入口地址,从而找到当前定义的虚函数calcArea()
例2:
实例化一个Circle对象,Circle类中没有定义虚函数,但从父类中继承了虚函数,所以在实例化一个Circle对象时,也会产生一个虚函数表,这个虚函数表是Circle的虚函数表,起始地址是0x6688,与Shape类的不一样,但在虚函数表中,calcArea()
的偏移地址与Shape类的是一样的,这样保证在Circle中访问calcArea()函数时,能通过虚函数指针找到自己的虚函数表,在自己的虚函数表中找到calcArea()函数是指向父类的calcArea()函数的入口的
如果在Circle中也定义了calcArea()函数:
对Shape类无变化
对Circle类:
虚函数表无变化,但此时已在勒种定义了自己的calcArea()函数,所以其虚函数表中关于calcArea()的指针已经覆盖了原有父类指针的值,已经变为0x4B2C
所以,按上图写法,用Shape的指针指向Circle的对象,会通过Circle对象中的虚函数表指针找到Circle的虚函数表,通过Circle的虚函数表,以和父类相同的偏移量,找到Circle的虚函数的入口地址,从而执行子类中的虚函数
函数的覆盖与隐藏
隐藏:父类和子类出现同名
覆盖:如上例,如果没在Circle中定义有同名的虚函数,虚函数表中虚函数的地址会继续沿用父类的,但在Circle中定义了,虚函数表中虚函数的地址会覆盖父类的,改为子类的虚函数的地址
虚析构函数的实现原理
通过在父类中用virtual修饰析构函数后,用父类的指针指向子类的对象,使用delete 父类指针就可以释放子类对象
理论前提:
执行完子类的析构函数就会执行父类的析构函数
思路:用父类的指针指向子类的析构函数就可以了
上图,在父类中定义了虚析构函数,那么在父类的虚函数表中,就会有一个父类析构函数的函数指针~Shape_ptr
子类虚函数表中,也会产生一个子类的析构函数的函数指针,指向子类的析构函数。
这时如果使用父类的指针指向子类的对象(这里是用Shape的指针指向Circle的对象),通过delete Shape的指针,可以通过Shape找到子类的虚函数表指针,通过子类的虚函数表指针找到虚函数表,通过虚函数表找到子类的析构函数,从而使得子类的析构函数可以执行,子类的析构函数执行完毕后系统会自动执行父类的析构函数
证明虚函数表的存在
概念说明:
1、对象的大小:在类实例化对象时,其数据成员所占内存大小(注意是数据成员,不包括成员函数)(例如,Shape类没有数据成员,理论上不占内存,但为了标记他的存在,会用一个内存单元来标记,用sizeof()输出会显示是1,但一旦后面又增加了数据成员,例如加一个int类型的(占4个内存单元),用sizeof()会显示4而不是5;Circle类有一个int数据成员, 占4个内存单元)
2、对象的地址:通过一个类实例化一个对象,该对象会在内存中占一定的内存单元,内存单元的第一个地址就是该对象的地址
3、对象成员的地址:当用一个类去实例化一个对象之后,这个对象中可能有一个或多个数据成员,每个数据成员所占的地址就是对象成员的地址。对象的每个数据成员,因为类型不同,占据的内存大小也有不同,所以地址也是不同的
4、虚函数表指针:在具有虚函数的情况下,实例化一个对象的时候,该对象的第一块内存中存储的是一个指针,这个指针是虚函数表的指针,因为是指针,所以占据内存大小也是4。据此特点,可以通过计算对象的大小来证明虚函数表指针的存在
注意:
Shape shape;
int *p = (int*)&shape; //要记得强制转换为int
每个类只有一份虚函数表,所有该类的对象共用同一张虚函数表(√)