C++虚函数
C++中虚函数的作用主要是实现了多态的机制,核心理念就是通过基类访问派生类定义的函数。多态也就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,也就是试图使用不变的代码来实现可变的算法。比如:模板技术、RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行是决议。
静态多态与动态多态:
静态多态,也叫早绑定,也就是说程序在编译阶段根据参数个数确定调用那个函数,例如
class Rect //矩形类 { public: int calcArea(int width); int calcArea(int width,int height); }; int main() { Rect.rect; rect.calcArea(10); rect.calcArea(10,20); return 0; }
动态多态,也叫晚绑定,是以封装和继承为基础,在运行时才决定调用那个函数,例如:
class Shape { public: virtual double calcArea();//虚函数 }; class Circle:public Shape { public: Circle(double r); double calcArea(); }; class Rect:public Shape { Rect(double width,double height); double calcArea(); }; int main() { Shape *shape1 = new Circle(4.0); Shape *shape2 = new Rect(3.0,5.0); shape1->calcArea(); shape2->calcArea(); return 0; }
注意:
1.基类声明的虚函数,在派生类中也是虚函数,即使不再使用virtual关键字,虚函数也实现了动态多态的方式,如上述main函数中,shape1和shape2分别调用各自成员calcArea函数;
2.如果基类没有用virtual修饰,即为普通成员函数时,这时在main函数中,shape1和shape2调用的则为基类的calcArea函数;
重载与重写:
重载(overload),是指编写一个与已有函数同名但参数表不同的函数;
重写(override),也称为“覆盖”,是指派生类重写基类的虚函数,重写的函数必须有一致的参数表和返回值;
父类和子类出现同名函数称为隐藏。
父类对象.函数函数名(...); //调用父类的函数
子类对象.函数名(...); //调用子类的函数
子类对象.父类名::函数名(...); //子类调用从父类继承来的函数。
父类和子类出现同名虚函数称为覆盖
父类指针 = new 子类名(...); 父类指针->函数名(...);//调用子类的虚函数。
virtual关键字:
纯虚函数,标志本身为一个抽象类,不能直接实例化,是用于规范派生类的行为,实际上就是告诉派生类必须实现相同的函数接口
class Shape { public: virtual double calcArea()=0; // =0标志一个虚函数为纯虚函数 };
虚析构函数,析构函数可以是虚的,甚至是纯虚的,一个如果用作于其他类的基类时,它的析构函数必须是虚的,否则会导致内存泄漏,例如
class Shape { public: //virtual ~Shape(); ~Shape(); }; class Circle:public Shape { public: Circle(int x, int y, double r); ~Circle(); virtual double calcArea(); private: double mRadius; Coordinate *mpCenter; //坐标类指针 }; Circle::Circle(int x, int y, double r) { mpCenter = new Coordinate(x,y); mRadius = r ; } Circle::~Circle() { delete m_pCenter; mpCenter = nullptr; } int main() { Shape *shape = new Circle(3, 5, 4.0); shape->calcArea(); delete shape; shape = nullptr; return 0; }
上面的程序中,如果在圆形的类中定义一个圆心的坐标,并且坐标是在堆中申请的内存,则在mian函数中通过父类指针操作子类对象的成员函数的时候是没有问题的,可是在销毁对象内存的时候则只是执行了父类的析构函数,子类的析构函数却没有执行,这会导致内存泄漏。
如果delete后边跟父类的指针则只会执行父类的析构函数,如果delete后面跟的是子类的指针,那么它即会执行子类的析构函数,也会执行父类的析构函数。
virtual在函数中的使用限制:
普通函数不能是虚函数,也就是说这个函数必须是某一个类的成员函数,不可以是一个全局函数,否则会导致编译错误。
静态成员函数不能是虚函数 static成员函数是和类同生共处的,他不属于任何对象,使用virtual也将导致错误。
内联函数不能是虚函数 如果修饰内联函数 如果内联函数被virtual修饰,计算机会忽略inline使它变成存粹的虚函数。
构造函数不能是虚函数,否则会出现编译错误。
虚函数表:
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
1.一般情况下,在虚函数表中,虚函数按照其声明顺序存放的,父类的虚函数在子类的虚函数前面;
2.如果子类中有虚函数重载了父类的虚函数,覆盖的函数被放到了虚表中原来父类虚函数的位置,没有被覆盖的函数依旧;
3.如果是多重继承(无虚函数覆盖),每个父类都有自己的虚表,子类的成员函数被放到了第一个父类(所谓的第一个父类是按照声明顺序来判断的)的表中;
4.如果是多重继承(有虚函数覆盖),每个父类虚函数表中的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的函数;