0引言:在学习C++时,碰到过以下四个以“虚”命名的概念,在系统理解这些高大上的术语后,才发现它们果真“名不虚传”。
为了方便捋清楚这些概念和之间的相互关系,本人对其进行了系统的总结,欢迎讨论。
1.虚基类
(1)作用:间接派生类只保存共同基类的一份成员(数据成员/函数成员),优化存储空间。
(2)虚基类初始化方法:
在基类的直接派生类中声明为虚函数(virtual public B / virtual public C),在最后的派生类中初始化直接基类和虚基类(这一点要特别注意,虚基类也是由最后的派生类执行,会屏蔽直接派生类对虚基类的初始化【避免虚基类多次初始化】,即D必须对A、B、C进行初始化,B和C对A的初始化不起作用。)
(3)栗子
1 #include <iostream> 2 using namespace std; 3 4 //虚基类 5 class A 6 { 7 public: 8 A(int da) 9 { 10 data_a = da; 11 cout << "A" << endl; 12 } 13 ~A() 14 { 15 cout << "~A" << endl; 16 } 17 protected: 18 int data_a; 19 }; 20 //直接派生类 21 class B :virtual public A 22 { 23 public: 24 B(int da, int db) :A(da) 25 { 26 data_b = db; 27 cout << "B" << endl; 28 } 29 ~B() 30 { 31 cout << "~B" << endl; 32 } 33 protected: 34 int data_b; 35 }; 36 37 //直接派生类 38 class C :virtual public A 39 { 40 public: 41 C(int da, int dc) :A(da) 42 { 43 data_c = dc; 44 cout << "C" << endl; 45 } 46 ~C() 47 { 48 cout << "~C" << endl; 49 } 50 protected: 51 int data_c; 52 }; 53 //间接派生类 54 class D : public B, public C 55 { 56 public: 57 D(int da, int db, int dc, int dd) :A(da), B(da, db), C(da, dc) 58 { 59 data_d = dd; 60 cout << "D" << endl; 61 } 62 ~D() 63 { 64 cout << "~D" << endl; 65 } 66 void display() 67 { 68 cout << "data_a=" << data_a << " " << "data_b=" << data_b << " " << "data_c=" << data_c << " " << "data_d=" << data_d << endl; 69 } 70 protected: 71 int data_d; 72 }; 73 74 int main() 75 { 76 D test_d(10, 20, 30, 40); 77 test_d.display(); 78 79 return 0; 80 }
输出:
2.虚函数
(1)思考:在基类和派生类中存在两个函数不仅名字相同,参数个数也相同,但是功能不同即函数体不同【不是函数重载!】,如何实现两个函数的调用?
一般思路是采取同名覆盖原则,即派生类中同名函数覆盖掉基类同名函数,如果想调用基类的同名函数,必须用类作用域符::来进行区分。这种做法在派生结构复杂时使用不太方便,能否采用一种调用形式,既可以调用派生类也可以调用基类同名函数?
这就是虚函数大展手脚的时候了,虚函数允许在派生类中重新定义与基类同名的函数,并且允许通过基类指针或引用来访问基类和派生类的同名函数。
(2)栗子
//main.cpp #include <iostream> #include "virtual.h" using namespace std; int main() { Shape *pShape = new Shape();//定义基类指针,指向基类对象所在内存空间 pShape->PrintArea(); Retangle ret(10, 20); pShape = &ret;//将基类指针指向派生类类型对象内存 pShape->PrintArea(); Circle cir(10); pShape = ○//将基类指针指向派生类类型对象内存 pShape->PrintArea(); return 0; } //virtual.h #ifndef virtual_h #define virtual_h class Shape { public: virtual void PrintArea(); virtual double CalculateArea(); protected: double area; }; class Retangle: public Shape { public: Retangle(double len, double wid); virtual void PrintArea(); virtual double CalculateArea(); private: double length; double width; }; class Circle : public Shape { public: Circle(double r); virtual void PrintArea(); virtual double CalculateArea(); private: double radius; }; #endif //virtual.cpp #include "virtual.h" #include <iostream> using namespace std; void Shape::PrintArea() { cout << "当前没有形状设置!" << endl; } double Shape::CalculateArea() { return area; } Retangle::Retangle(double len, double wid) { length = len; width = wid; } double Retangle::CalculateArea() { area = length * width; return Shape::CalculateArea(); } void Retangle::PrintArea() { CalculateArea(); cout << "矩阵面积为:" << area << endl; } Circle::Circle(double r) { this->radius = r; } double Circle::CalculateArea() { area = 3.14*radius*radius; return Shape::CalculateArea(); } void Circle::PrintArea() { CalculateArea(); cout << "圆的面积为:" << area << endl; }
输出:
(3)关于使用虚函数的好处:
1) 基类里声明为虚函数函数体可以为空,它的作用就是为了能让这个函数在它的子类里面可以被同名使用,这样编译器就可以使用后期绑定来达到多态了;
2) 通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到基类里面的这个函数不在派生类里面不去修改它的实现。
3.纯虚函数
纯虚函数就很好理解了,有些函数虽然并不是基类所需要的,但是派生类可能会用到,所以会在基类中为派生类保留一个函数名,以便以后派生类定义使用。纯虚函数不具备函数功能,是不能被调用的。
以上述代码为例,可以将Shape类中的 PrintArea()定义为纯虚函数, virtual void PrintArea() = 0;
4.虚析构函数
在学习派生类的析构函数时,留了一个坑,就是析构函数的调用次序问题,在不使用虚函数的前提下,析构函数的调用次序是:先调用派生类构造函数清理新增的成员,再调用子对象析构函数(基类构造函数)清理子对象,最后再调用基类构造函数清理基类成员,过程正好与构造函数的调用过程相反。
在使用虚函数时,析构函数的调用会出现哪些情况?
同样是虚函数例子中的代码(加上构造和析构语句),进行以下测试:
1 int main() 2 { 3 Shape *pShape = new Circle(10);//基类指针指向派生类对象内存空间 4 delete pShape; 5 pShape = NULL; 6 7 return 0; 8 }
输出:
测试结果表明:程序调用了两次构造函数,但是只调用了一次析构函数,造成了内存泄漏。
这是因为派生类析构函数无法从基类继承,在没有声明基类析构函数为虚函数时,基类指针释放时无法找到派生类析构函数地址,也就不能释放派生类对象所在内存空间。而将基类析构函数也声明为虚函数时,该基类所有派生类也将自动成为虚函数,所有虚析构函数的入口地址都会存放在一个虚函数表(指针数组)中,查找方便,这样就避免了无法调用派生类析构函数所造成的内存泄漏问题了。
5.总结:
(1).虚析构函数是建立在虚函数的基础之上的,即在想使用基类指针访问派生类对象时必须要声明基类虚析构函数,不管基类是否需要析构函数;
(2).因为虚函数表会占据一定的空间开销,在不存在上述1中情况时没有必要使用虚函数;
(3).多态性:因为编译器只做静态的语法检查,无法确定调用对象,运行时才确定关联关系,所以多态性又分为静态多态性和动态多态性。
静态多态性(编译时的多态性,静态关联)是指在程序编译时就能够确定调用的是哪个函数,函数重载/运算符重载/通过对象名调用的虚函数都属于静态关联。
动态多态性(运行时多态性,动态关联,滞后关联)是指只有在程序运行时才能够确定操作的对象,通过虚函数实现。
6.参考: