当前位置:   article > 正文

C++之 “虚函数” 详解_类成员、函数能被声明为虚函数吗

类成员、函数能被声明为虚函数吗

C++之 “虚函数” 详解

虚函数在C++中有着十分重要的作用,通过虚函数可以实现多态(polymorphism)机制

在看《C++ primer plus》时,发现作者将虚函数放在类继承的一章之中,和动态/静态联编一起进行了讲解。我也就顺着复习了继承,便再剖析一下虚函数。

虚函数--类的成员函数前面加virtual关键字,则这个成员函数称为虚函数。

在之前的博客《多态及其对象模型》中我剖析虚函数在其中的作用。

底层原理:

当存在虚函数时,编译器会给每个对象添加一个隐藏成员,此隐藏成员保存的是指向虚函数地址数组的指针。而这个数组就叫虚函数表(virtual function table)。

虚函数表中存放的正是为类对象进行声明的虚函数的地址。

需要注意的是,在继承之中,如果基类存在虚表,那么基类对象将存在一个指向其虚表的指针;而派生类对象中将包含的是一个指向独立虚表的指针。这个独立虚表中不仅包含从父类继承的虚函数地址,还包括派生类定义的新的虚函数的地址。

当调用虚函数时,程序将查看存储在对象中的虚表的地址,进而去虚表中查找相应的虚函数。

代码如下:

  1. #include<iostream>
  2. using namespace std;
  3. //单继承模型
  4. class Father
  5. {
  6. public:
  7. virtual void fun1()
  8. {
  9. cout << "Father::fun1()" << endl;
  10. }
  11. virtual void fun2()
  12. {
  13. cout << "Father::fun2()" << endl;
  14. }
  15. protected:
  16. int _a = 10;
  17. };
  18. class Son : public Father
  19. {
  20. public:
  21. virtual void fun2()
  22. {
  23. cout << "Son::fun2()" << endl;
  24. }
  25. virtual void fun3()
  26. {
  27. cout << "Son::fun3()" << endl;
  28. }
  29. protected:
  30. int _b = 20;
  31. };
  32. void Fun(Father& f)
  33. {
  34. f.fun2();
  35. }
  36. void test()
  37. {
  38. Father a;
  39. Son b;
  40. Fun(a);
  41. Fun(b);
  42. }
  43. int main()
  44. {
  45. test();
  46. system("pause");
  47. return 0;
  48. }

在监视窗口查看:

image.png

我们可以看到在基类内部存在着一个地址,而这个地址指向了一张表,既虚表,虚表内部存储的正是虚函数。但是我们会发现派生类虚函数fun3却不在此表内部。实际上,fun3也在此表内部,只不过编译器做了优化,没有在监视窗口显示出来而已。

我们可以在内存中观察,也可以通过书写函数将虚表内的地址打印出来。

内存中观察:

image.png


通过函数来将虚表及其内存放的地址打印出来,如下:

  1. #include<iostream>
  2. using namespace std;
  3. class Father
  4. {
  5. public:
  6. virtual void fun1()
  7. {
  8. cout << "Father::fun1()" << endl;
  9. }
  10. virtual void fun2()
  11. {
  12. cout << "Father::fun2()" << endl;
  13. }
  14. private:
  15. int _a = 10;
  16. };
  17. class Son : public Father
  18. {
  19. public:
  20. virtual void fun2()
  21. {
  22. cout << "Son::fun2()" << endl;
  23. }
  24. virtual void fun3()
  25. {
  26. cout << "Son::fun3()" << endl;
  27. }
  28. protected:
  29. int _b = 20;
  30. };
  31. typedef void(*FUNC) ();
  32. void PrintVTable(int* VTable)
  33. {
  34. cout << " 虚表地址>" << VTable << endl;
  35. for (int i = 0; VTable[i] != 0; ++i)
  36. {
  37. printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
  38. FUNC f = (FUNC)VTable[i];
  39. f();
  40. }
  41. cout << endl;
  42. }
  43. void test()
  44. {
  45. Father a;
  46. Son b;
  47. int* VTable1 = (int*)(*(int*)&a);
  48. int* VTable2 = (int*)(*(int*)&b);
  49. PrintVTable(VTable1);
  50. PrintVTable(VTable2);
  51. }
  52. int main()
  53. {
  54. test();
  55. system("pause");
  56. return 0;
  57. }

TIM截图20171203105545.png

image.png

可以看到在创建的两个对象中都存在着虚表。这也不可避免的出现了一些问题:

  1. 每个对象占据内存都变大了,增加内存来存放虚表的地址。

  2. 针对每个类,编译器都要创建一个虚函数表。

  3. 每次的函数调用,都要额外执行操作,要去虚表中查找虚函数的地址。

虽然非虚函数比虚函数的效率稍高一点,但是起不具备动态联编,不能构成多态。

虚函数应用:

  • 构造函数

构造函数不能为虚构函数。虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引 起混淆。

  • 析构函数

     

析构函数定义为虚函数,除非类不用做基类。

  1. class A
  2. {
  3. public:
  4.     A() 
  5.     {     
  6. _ptra = new char[10];
  7.     }
  8.     
  9.     ~A() 
  10.     { 
  11.         delete[] _ptra;
  12.     }       
  13. private:
  14.     char* _ptra;
  15. };
  16. class B: public A
  17. {
  18. public:
  19.     B()
  20. _ptrb = new char[20];
  21. }
  22.     ~B() 
  23. delete[] _ptrb;
  24. }
  25. private:
  26.     char * _ptrb;
  27. };
  28. void test()
  29. {
  30.     A * a = new B;
  31.     delete a;
  32. }

上面的程序存在内存泄漏,因为其是静态联编,在释放对象a时仅仅调用了A的·析构函数调用了,B的析构函数并未调用,这就造成了一个很危险的漏洞。

但如果将A类的析构函数定义为虚析构函数,那么执行的将是动态联编,则会将内存全部成功的释放掉。

  • 纯虚函数

纯虚函数只进行声明,而不定义。如下:

  1. class Test
  2. {
  3. public:
  4.     virtual void fun()=0;   // =0 标志一个虚函数为纯虚函数
  5. };

包含有纯虚函数的类是抽象类,而抽象类不能进行实例化。只能被其他派生类继承,而纯虚函数就是一个公共的接口,所有继承了抽象类的派生类内部都包含纯虚函数。

纯虚函数不定义,其定义交给派生类来完成,继承了抽象类的派生类必须对纯虚函数进行定义

  • 友元函数

友元函数不能定义为虚函数。因为友元不是类的成员,只有类的成员才能定义为虚函数。

总结:

        1. 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外) 

        2. 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。 

        3. 只有类的成员函数才能定义为虚函数。 

        4. 静态成员函数不能定义为虚函数。 

        5. 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。 

        6. 构造函数不能为虚函数,最好也不要将operator=定义为虚函数,因为容易使用时容易引起混淆。

        7. 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。 

        8. 最好把基类的析构函数声明为虚函数。

 

参考资料:

    《C++ primer plus》Stephen Prata,张海龙,袁国忠译

    《深度探索C++对象模型》 Stanley B.Lippman,侯捷译

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/731484
推荐阅读
相关标签
  

闽ICP备14008679号