virtual实现多态
类的多态特性是支持面向对象的语言最主要的特性,但是支持类并不能说明就是支持面向对象,能够解决多态问题的语言,才是真正支持面向对象的开发的语言。
C++多态举例:
#include <iostream>
using namespace std; class Vehicle {
public: Vehicle(float speed, int total) : speed(speed), total(total) {} virtual void ShowMember() { // 声明为虚函数 cout << "Vehicle function ShowMember." << endl; }
protected: float speed; int total;
}; class Car : public Vehicle {
public: Car(int aird, float speed, int total) : Vehicle(speed, total), aird(aird) {} void ShowMember() override { // 使用override关键字,明确表示这是重写基类的虚函数
//虚函数在派生类中,由于继承的关系,这里的virtual也可以不加cout << "Car function ShowMember." << endl; }
protected: int aird;
}; void test(Vehicle &temp) { temp.ShowMember(); // 这里会根据temp的实际类型调用对应版本的ShowMember函数
} int main() { Vehicle a(120, 4); Car b(180, 110, 4); test(a); // 输出: Vehicle function ShowMember. test(b); // 输出: Car function ShowMember. return 0;
}
代码展示C++中多态性的一个基本用法:
通过基类指针或引用调用派生类对象的虚函数时,将执行派生类中覆盖该函数的版本。
程序在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性.
多态性依赖虚函数的定义,在需要解决多态问题的重载成员函数前,加上virtual关键字,成员函数就变成了虚函数。但是虚函数增加了一些数据存储和执行指令的开销。
用基类指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。通过基类指针或基类引用做形参,当实参传入不同的派生类(或基类)的指针或引用,在函数内部触发动态绑定,从而来运行时 实现多态的。
这种技术可以让父类的指针有“多种形态”,本质上一种泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
虚函数的定义遵循的规则:
1.如果虚函数在基类与派生类中出现,仅仅是名字相同,而形式参数不同,或者是返回类型不同,那么即使加上了virtual关键字,也是不会进行滞后联编的。
2.只有类的成员函数才能说明为虚函数,因为虚函数仅适合用与有继承关系的类对象,所以普通函数不能说明为虚函数。
3.静态成员函数不能是虚函数,因为静态成员函数的特点是不受限制于某个对象。
4.内联(inline)函数不能是虚函数,因为内联函数不能在运行中动态确定位置。即使虚函数在类的内部定义定义,但是在编译的时候系统仍然将它看做是非内联的。
5.构造函数不能是虚函数,因为构造的时候,对象还是一片位定型的空间,只有构造完成后,对象才是具体类的实例。
6.析构函数可以是虚函数,而且通常声明为虚函数。 虽然使用虚函数会降低效率,但是在处理器速度越来越快时,将一个类中的所有成员函数都定义成为virtual总是有好处的,它除了会增加一些额外的开销是没有其它坏处的,对于保证类的封装特性是有好处的。
虚函数表
虚函数是通过一张虚函数表实现。表中是类的虚函数的地址表,可以解决继承、覆盖的问题。
有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以当用父类的指针来操作一个子类的时候,它像一个地图一样,指明了实际所应该调用的函数。C++的编译器保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。所以可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
假设有这样的一个类:
class Base {public:virtual void f() { cout << "Base::f" << endl; }virtual void g() { cout << "Base::g" << endl; }virtual void h() { cout << "Base::h" << endl; }
}
按照上面的说法,可以通过Base的实例来得到虚函数表。
typedef void(*Fun)(void); Base b;Fun pFun = NULL;cout << "虚函数表地址:" << (int*)(&b) << endl;cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;// Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b));pFun();
通过这个示例,我们可以看到,我们可以通过强行把&b转成int ,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f() (Fun)*((int*)*(int*)(&b)+1); // Base::g() (Fun)*((int*)*(int*)(&b)+2); // Base::h()
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“/0”一样,其标志了虚函数表的结束。
下面将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。
一般继承(无虚函数覆盖)
假设有如下所示的一个继承关系:
在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示。对于实例Derive d 的虚函数表如下:
可以看到
1)虚函数按照其声明顺序放于表中。 2)父类的虚函数在子类的虚函数前面。
一般继承(有虚函数覆盖)
如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
从表中可以看到下面几点,
(1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。 (2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
可以看到:
1) 每个父类都有自己的虚表。 2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)1
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;Base1 *b1 = &d;Base2 *b2 = &d;Base3 *b3 = &d;b1->f(); //Derive::f()b2->f(); //Derive::f()b3->f(); //Derive::f()b1->g(); //Base1::g()b2->g(); //Base2::g()b3->g(); //Base3::g()
为什么具备多态特性的类的析构函数,有必要声明为virtual?
将基类的析构函数声明为virtual,那么当通过基类指针删除对象时,会首先调用派生类的析构函数,然后再调用基类的析构函数。这样就可以确保所有的清理工作都被正确执行,避免了资源泄漏。因此,如果类被设计为基类,并且希望它的派生类能够通过基类指针被正确地删除,那么应该将基类的析构函数声明为virtual。
纯虚函数
纯虚函数是一种特殊的虚函数,它在基类中声明为= 0
,表明在基类中不提供具体的实现,并且要求任何派生类都必须提供该函数的实现。
纯虚函数为派生类提供了一种接口规范,确保派生类必须实现特定的功能。
如果一个类包含至少一个纯虚函数,那么这个类就是一个抽象基类。抽象基类不能被实例化,因为它包含不完整的功能。
派生类必须重写纯虚函数才能被实例化、如果一个类从抽象基类派生,并且它没有重写所有的纯虚函数,那么这个派生类也仍然是抽象的,不能实例化。派生类必须完全遵循抽象基类定义的接口规范。
#include <iostream>
using namespace std; // 抽象基类
class CVirtual {
public: CVirtual() {} virtual ~CVirtual() { cout << " CVirtual destruction " << endl; } // 纯虚函数 virtual void fun() = 0;
}; // 派生类
class CDerived : public CVirtual {
public: CDerived() {} ~CDerived() {} // 覆盖纯虚函数 void fun() override { cout << " CDerived function call " << endl; }
}; int main() { // CVirtual v; // 错误!抽象基类不能被实例化 CDerived d; // 正确!派生类提供了纯虚函数的实现 d.fun(); // 调用派生类中的fun()实现 return 0;
}