C++利器:可以动态联编的虚拟函数

纹枰老妖

发布日期: 2021-02-20 19:49:18 浏览量: 458
评分:
star star star star star star star star star star_border
*转载请注明来自write-bug.com

一、虚函数的介绍与使用

对于C++语言来说,程序在执行类成员函数时(跳转到相应的函数地址),有两种方式,一种是静态联编(static binding),意思就是程序在编译时就知道该函数的地址;另一种是动态联编(dynamic binding),意思是就算代码在编译后,程序还是不知道相应的函数地址,得到程序执行到这个地方,程序才能得到正确的函数地址。那么为什么需要动态联编呢?考虑这样的情况:比方说有一种类叫做宠物,狗、猫、兔子、金鱼、乌龟等都是它的子类,那么当我们定义宠物类的时候,应该定义一个吃饭的功能,因为宠物都应该吃饭。这样的话,我们把程序当成小明,小明要领养什么宠物,只有他才知道(程序执行时才能确定),宠物或许是狗,或许是兔子。可是我们不能给狗喂萝卜,兔子也不吃骨头。所以我们在定义宠物类的时候,就应该把吃饭这个类成员函数,声明称一个虚函数。

先用代码来做一个简单的例子:

  1. #include <iostream>
  2. using namespace std;
  3. class Pet//这个是我们的基类:宠物类
  4. {
  5. public:
  6. char * name;
  7. int weight;
  8. int age;
  9. Pet() {};
  10. //下面这个就是我们的虚函数
  11. virtual void eat()
  12. {
  13. cout << "错误:我们还不能确定宠物的品种,无法喂食!" << endl;
  14. };
  15. ~Pet() {};
  16. };
  17. class Dog : public Pet
  18. {
  19. public:
  20. Dog(char * s, int w, int a)
  21. {
  22. name = s; weight = w; age = a;
  23. }
  24. virtual void eat() //重新定义这个虚函数
  25. {
  26. cout << "到点了,请准备好狗粮," << name << "该吃饭了。" << endl;
  27. }
  28. ~Dog() {};
  29. };
  30. class Rabbit : public Pet
  31. {
  32. public:
  33. Rabbit(char * s, int w, int a)
  34. {
  35. name = s; weight = w; age = a;
  36. }
  37. void eat()
  38. {
  39. cout << "到点了,请准备好萝卜," << name << "该吃饭了。" << endl;
  40. }
  41. ~Rabbit() {};
  42. };
  43. int main()
  44. {
  45. int a, b, c;
  46. char s[20];
  47. cout << "请输入1或2来创建一个生物(1是狗2是兔);然后分别输"
  48. << "入该生物的姓名、重量和年龄,用回车键分割" << endl;
  49. cin >> a; cin >> s; cin >> b; cin >> c;
  50. Pet * p;
  51. if (1 == a)
  52. {
  53. p = new Dog(s, b, c);
  54. cout << "您创建的生物是狗狗,它的名字叫做" << s
  55. << ",它今年" << c << "岁了,重大" << b << "公斤!"
  56. << "它现在被您收养了。"
  57. << endl;
  58. }
  59. else
  60. {
  61. p = new Rabbit(s, b, c);
  62. cout << "您创建的生物是兔子,它的名字叫做" << s
  63. << ",它今年" << c << "岁了,重大" << b << "公斤!"
  64. << "它现在被您收养了。"
  65. << endl;
  66. }
  67. cout << "接下来是您与宠物的日常:" << endl;
  68. for (int i = 0; i < 10; i++)
  69. {
  70. if (i < 9) cout << "您与宠物愉快的玩耍" << endl;
  71. else p->eat();//到了这里就会调用相应的虚函数,如果子类是狗类,那么调用狗类的吃饭函数,否则是兔类吃饭函数
  72. }
  73. return 0;
  74. }

二、虚函数的使用实境

虚函数一般用在这种情况下:在菱形或更为复杂的继承体系下,由于同层子类不能互相引用或指针指向,所以一般都是使用基类的指针或引用指向派生类对象,而无需做任何强制类型转换。就像我们的上个梨子一样:由于我们不知道“小明”要养的是什么动物,所以无法确定应该调用怎样的“喂食”函数;然而我们可以创建一个基类的“宠物”指针,然后用它来指向实际中的实例对象,这样“喂食”时,就会调用对应的函数了。

三、虚函数的工作原理

虚函数的原理并不复杂,通常都是这样的:编译器在编译每一种类之后,会搜集出该类的所有函数地址,然后在某块内存(或许是代码区,或许是数据区)中,开辟出一个数组,数组中的每个元素,保存的都是该类的一个函数地址,而这个数组的头结点,就是该类的所谓虚函数表指针。然后在程序实例化该类的一个实际对象时,会偷偷的给这个对象增加一个数据成员,而这个数据成员,是一个指针,保存的就是前面说的数组地址的头结点。还是以我们上面的程序为例:编译器首先为基类宠物类创建一个地址表(虚函数地址数组),然后增添一个隐藏成员“虚函数表指针”,该成员指向这个地址表;然后对派生类比如“狗”类而言,将做同样的事:创建一个新的地址表,里面保存的是“狗类”的所有虚函数地址,然后用一个数据成员指向这个“狗”类的函数表地址;这样当程序使用这个类对象的虚函数时,会根据该虚函数在地址表中的偏移量来调用对应的函数。(但如果我们的派生类没有定义那个喂食函数,那么派生类数组地址中,对应的保存的是基类中的那个喂食函数,读者朋友如果有心,可以自己试一下)

虚函数的使用注意事项

  • 构造函数不能声明为虚函数。这是因为实例一个派生类对象时,先调用该类的构造函数,执行完毕后,再调用基类的某个构造函数。所以派生类不应该重新定义基类的构造函数

  • 友元函数不能声明为虚函数,这是友元函数不是类成员,而虚函数必须是类成员。如果这种规则会导致您编程时的困惑,可以试着在友元函数中调用虚函数来解决这个问题

  • 对于一个在继承体系下是基类或中间类的类而言,应该将它的析构函数定位为虚函数。考虑这样一种情况:A是基类,B是继承类,其中B比A多出了一个在堆中new出的字符串内容,假如有以下代码【A * p = b;】,其中那个b是B类的一个实例对象。当我们delete这个p指向的对象时,如果A不是虚析构函数,那么这将会导致内存泄漏,因为在B中的析构函数才会释放字符串占用的内存,所以说应该将A这种的类的析构函数定位为虚函数。【另外还有一种新手常见的错误,考虑这样一种继承体系,B继承自A,B内有虚函数,A内没有,新手犯得错误就是,先实例化一个B类对象,然后再做出一个A类指针指向这个B实例对象,最后delete这个指针时,程序崩溃了。。。其实原因很简单,因为B中有一个虚函数表指针,而A没有,所以当把B的对象“变成”A类型时,不应该包含这个虚函数表指针!如果我们的编译器是把虚函数表指针放在开头部分的话,那么当A指针指向B对象时,会漫过去这个虚表指针,最后当delete这个指针时,不崩溃才怪,因为malloc的返回值和free的传入值不一样嘛。所以如果子类中有虚函数时,我们应该在父类中也弄虚函数,如果确实没啥虚函数好弄的,我们可以把析构函数做成虚函数啊,总结归纳就是:把父类的析构函数声明为虚函数,是一种良好的编程习惯!】

  • 就像我们前面提过的一样:如果一个基类或中间类定义了某个虚函数,而在下一链中的派生类没有重新定义虚函数,那么对于这个派生类而言,它的实例对象中使用这个虚函数时,将会使用上一链的那个最新版本的虚函数

  • 所有虚函数的参数表应该是相同的,假如不这样,如下所示:

    1. class A
    2. {
    3. ...
    4. virtual void fun(int i){...};
    5. }
    6. class B : public A
    7. {
    8. ...
    9. virtual void fun(){...};

    那么下面的代码就会是:

    1. A * p = new B();
    2. p->fun(); //合法的代码
    3. p->fun(9);//非法的代码

    这是因为在B中,fun被定义为一个不接受任何参数的函数,而且隐藏了A中那个接受int参数的fun版本,而不是将fun生成具有两个函数特征标的重载版本。
    但是,如果返回值是基类的引用或指针,那么我们可以在重新定义下一链的派生类函数时,将返回值改为该派生类的引用或指针,这称为“返回类型协变”,英文叫做“covariance of return type”,她允许返回类型随类类型的变化而变化。

  • 如果某个基类或中间类的虚函数有很多重载版本,那么在下一链中的类虚函数定义中,应该要重新定义所有的重载版本。如果只定义一个或很少的虚函数,那么其它那些没有被定义的虚函数都会被隐藏掉,该派生类对象无法使用这些被隐藏掉的版本

上传的附件

发送私信

5
文章数
4
评论数
最近文章
eject