虚方法(virsual method)挺起来玄乎其玄,向从未听说过这个概念的人解释清楚是一件相当困难的事情。 因为这是一个很不容易理解的概念,但它在比较抽象的代码里边是不可少的。 那么既然用枯燥的文字来描述虚方法不可行,我们毅然选择走另一条路:通过一个简单的例子引发的问题来探究虚方法的作用以及完整的解决方案。

  以非常熟悉的阿猫阿狗例子程序是我们这次探索的出发点。我们将使用指针代替局部变量来容纳 Pet 对象。 需要我们认识两个新的C++保留字:new和delete 前边我们已经讲解过一些关于指针的知识,说白了就是一种专门用来保存内存地址的数据类型。 以前我们常用的做法是:创建一个变量,再把这个变量的地址赋值给一个指针。然后,我们就可以没羞没臊地用指针去访问这个变量的值了。

引发问题:使用指向对象的指针

  事实上在C和C++中,我们完全可以在没有创建变量的情况下为有关数据分配内存。也就是直接创建一个指针并让它指向新分配的内存块:

  1. int *pointer = new int;//定义一个指向整型的指针pointer,用new创建一个整型的内存,即声明一个指向整型地址空间的指针pointer
  2. *pointer = 110;//赋值给new出来的内存为110
  3. std::cout << *pointer;
  4. delete pointer;//删除指针,释放内存

  最后一步非常必要和关键,这是因为程序不会自动释放内存,程序中的每一个 new 操作都必须有一个与之对应的 delete 操作!

  那么我们把阿猫阿狗程序做一下改造:pet.cpp

  1. #include <iostream>
  2. #include <string>
  3.  
  4. class Pet
  5. {
  6. public:
  7. Pet(std::string theName);
  8.  
  9. void eat();
  10. void sleep();
  11. void play();
  12.  
  13. protected:
  14. std::string name;
  15. };
  16.  
  17. class Cat : public Pet
  18. {
  19. public:
  20. Cat(std::string theName);
  21.  
  22. void climb();
  23. void play();
  24. };
  25.  
  26. class Dog : public Pet
  27. {
  28. public:
  29. Dog(std::string theName);
  30.  
  31. void bark();
  32. void play();
  33. };
  34.  
  35. Pet::Pet(std::string theName)
  36. {
  37. name = theName;
  38. }
  39.  
  40. void Pet::eat()
  41. {
  42. std::cout << name << "正在吃东西!\n";
  43. }
  44.  
  45. void Pet::sleep()
  46. {
  47. std::cout << name << "正在睡大觉!\n";
  48. }
  49.  
  50. void Pet::play()
  51. {
  52. std::cout << name << "正在玩儿!\n";
  53. }
  54.  
  55. Cat::Cat(std::string theName) : Pet(theName)
  56. {
  57. }
  58.  
  59. void Cat::climb()
  60. {
  61. std::cout << name << "正在爬树!\n";
  62. }
  63.  
  64. void Cat::play()
  65. {
  66. Pet::play();
  67. std::cout << name << "玩毛线球!\n";
  68. }
  69.  
  70. Dog::Dog(std::string theName) : Pet(theName)
  71. {
  72. }
  73.  
  74. void Dog::bark()
  75. {
  76. std::cout << name << "旺~旺~\n";
  77. }
  78.  
  79. void Dog::play()
  80. {
  81. Pet::play();
  82. std::cout << name << "正在追赶那只该死的猫!\n";
  83. }
  84.  
  85. int main()
  86. {
  87. Pet *cat = new Cat("加菲");
  88. Pet *dog = new Dog("欧迪");
  89.  
  90. cat -> sleep();
  91. cat -> eat();
  92. cat -> play();
  93.  
  94. dog -> sleep();
  95. dog -> eat();
  96. dog -> play();
  97.  
  98. delete cat;
  99. delete dog;
  100.  
  101. return 0;
  102. }

  结果:

  1. 加菲正在睡大觉!
  2. 加菲正在吃东西!
  3. 加菲正在玩儿!
  4. 欧迪正在睡大觉!
  5. 欧迪正在吃东西!
  6. 欧迪正在玩儿!
  7. 请按任意键继续. . .

  仔细一瞧,程序与我们的预期不符:我们在 Cat 和 Dog 类里对 play() 方法进行了覆盖,但实际上调用的是 Pet::play() 方法而不是那两个覆盖的版本。 WHY??

使用虚方法

  程序之所以会有这样奇怪的行为,是因为C++的创始者希望用C++生成的代码至少和它的老前辈C一样快。

  所以程序在编译的时候,编译器将检查所有的代码,在如何对某个数据进行处理和可以对该类型的数据进行何种处理之间寻找一个最佳点。

  正是这一项编译时的检查影响了刚才的程序结果:cat 和 dog 在编译时都是 Pet 类型指针,编译器就认为两个指针调用的 play() 方法是 Pet::play() 方法,因为这是执行起来最快的解决方案。

  而引发问题的源头就是我们使用了 new 在程序运行的时候才为 dog 和 cat 分配 Dog 类型和 Cat 类型的指针。 这些是它们在运行时才分配的类型,和它们在编译时的类型是不一样的!

  为了让编译器知道它应该根据这两个指针在运行时的类型而有选择地调用正确的方法(Dog::play() 和 Cat::play()),我们必须把这些方法声明为虚方法。

  声明一个虚方法的语法非常简单,只要在其原型前边加上 virtual 保留字即刻。

    virtual void play();

  另外,虚方法是继承的,一旦在基类里把某个方法声明为虚方法,在子类里就不可能再把它声明为一个非虚方法了。 这对于设计程序来说是一件好事,因为这可以让程序员无需顾虑一个虚方法会在某个子类里编程一个非虚方法。

  使用虚方法使得程序如预期完成:pet2.cpp

  1. #include <iostream>
  2. #include <string>
  3.  
  4. class Pet
  5. {
  6. public:
  7. Pet(std::string theName);
  8.  
  9. void eat();
  10. void sleep();
  11. virtual void play();//只有这里和上述程序不一样
  12.  
  13. protected:
  14. std::string name;
  15. };
  16.  
  17. class Cat : public Pet
  18. {
  19. public:
  20. Cat(std::string theName);
  21.  
  22. void climb();
  23. void play();
  24. };
  25.  
  26. class Dog : public Pet
  27. {
  28. public:
  29. Dog(std::string theName);
  30.  
  31. void bark();
  32. void play();
  33. };
  34.  
  35. Pet::Pet(std::string theName)
  36. {
  37. name = theName;
  38. }
  39.  
  40. void Pet::eat()
  41. {
  42. std::cout << name << "正在吃东西!\n";
  43. }
  44.  
  45. void Pet::sleep()
  46. {
  47. std::cout << name << "正在睡大觉!\n";
  48. }
  49.  
  50. void Pet::play()
  51. {
  52. std::cout << name << "正在玩儿!\n";
  53. }
  54.  
  55. Cat::Cat(std::string theName) : Pet(theName)
  56. {
  57. }
  58.  
  59. void Cat::climb()
  60. {
  61. std::cout << name << "正在爬树!\n";
  62. }
  63.  
  64. void Cat::play()
  65. {
  66. Pet::play();
  67. std::cout << name << "玩毛线球!\n";
  68. }
  69.  
  70. Dog::Dog(std::string theName) : Pet(theName)
  71. {
  72. }
  73.  
  74. void Dog::bark()
  75. {
  76. std::cout << name << "旺~旺~\n";
  77. }
  78.  
  79. void Dog::play()
  80. {
  81. Pet::play();
  82. std::cout << name << "正在追赶那只该死的猫!\n";
  83. }
  84.  
  85. int main()
  86. {
  87. Pet *cat = new Cat("加菲");
  88. Pet *dog = new Dog("欧迪");
  89.  
  90. cat -> sleep();
  91. cat -> eat();
  92. cat -> play();
  93.  
  94. dog -> sleep();
  95. dog -> eat();
  96. dog -> play();
  97.  
  98. delete cat;
  99. delete dog;
  100.  
  101. return 0;
  102. }

  结果:

  1. 加菲正在睡大觉!
  2. 加菲正在吃东西!
  3. 加菲正在玩儿!
  4. 加菲玩毛线球!
  5. 欧迪正在睡大觉!
  6. 欧迪正在吃东西!
  7. 欧迪正在玩儿!
  8. 欧迪正在追赶那只该死的猫!
  9. 请按任意键继续. . .

TIPS

  • 如果拿不准要不要把某个方法声明为虚方法,那麽就把它声明为虚方法好了。
  • 在基类里把所有的方法都声明为虚方法会让最终生成的可执行代码的速度变得稍微慢一些,但好处是可以一劳永逸地确保程序的行为符合你的预期!
  • 在实现一个多层次的类继承关系的时候,最顶级的基类应该只有虚方法。
  • 有件事现在可以告诉大家了:析构器都是虚方法!从编译的角度看,它们只是普通的方法。如果它们不是虚方法,编译器就会根据它们在编译时的类型而调用那个在基类里定义的版本(构造器),那样往往会导致内存呢泄露!

虚方法(virsual method)的更多相关文章

  1. 抽象方法(abstract method) 和 虚方法 (virtual method), 重载(overload) 和 重写(override)的区别于联系

    1. 抽象方法 (abstract method) 在抽象类中,可以存在没有实现的方法,只是该方法必须声明为abstract抽象方法. 在继承此抽象类的类中,通过给方法加上override关键字来实现 ...

  2. [翻译] Virtual method interception 虚方法拦截

    原文地址:http://blog.barrkel.com/2010/09/virtual-method-interception.html 注:基于本人英文水平,以下翻译只是我自己的理解,如对读者造成 ...

  3. 为何JAVA虚函数(虚方法)会造成父类可以"访问"子类的假象?

      首先,来看一个简单的JAVA类,Base. 1 public class Base { 2 String str = "Base string"; 3 protected vo ...

  4. 译:C#面向对象的基本概念 (Basic C# OOP Concept) 第三部分(多态,抽象类,虚方法,密封类,静态类,接口)

    9.多态 Ploy的意思就是多于一种形式.在文章开始,方法那一章节就已经接触到了多态.多个方法名称相同,而参数不同,这就是多态的一种. 方法重载和方法覆盖就是用在了多态.多态有2中类型,一种是编译时多 ...

  5. 访问祖先类的虚方法(直接访问祖先类的VMT,但是这种方法在新版本中未必可靠)

    访问祖先类的虚方法 问题提出 在子类覆盖的虚方法中,可以用inherited调用父类的实现,但有时候我们并不需要父类的实现,而是想跃过父类直接调用祖先类的方法. 举个例子,假设有三个类,实现如下: t ...

  6. 浅谈 虚方法(virtual)

    虚方法 理解:从字面意思来讲,"虚",可有可无,子类对父类的某种方法的重写,可以重写,也可以不重写. 虚方法,顾名思义(装个13),就是某种方法. 用法:public virtua ...

  7. C#中的抽象类、抽象方法和虚方法

    [抽象类]abstract 修饰符可与类和方法一起使用定义抽象类的目的是提供可由其子类共享的一般形式.子类可以根据自身需要扩展抽象类.抽象类不能实例化.抽象方法没有函数体.抽象方法必须在子类中给出具体 ...

  8. 类型,对象,线程栈,托管堆在运行时的关系,以及clr如何调用静态方法,实例方法,和虚方法(第二次修改)

    1.线程栈 window的一个进程加载clr.该进程可能含有多个线程,线程创建的时候会分配1MB的栈空间. 如图: void Method() { string name="zhangsan ...

  9. C#语法-虚方法详解 Virtual 虚函数

    虚方法 / Virtual 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享. ...

随机推荐

  1. shell中的命令与特殊符号

    1.记录命令:! !!:连续的“!”表示执行上一次的指令 !n:表示执行命令历史中第二条指令 注:!2居然给我关机了 !字符串(字符串大于1)表示执行命令历史中最近的一次 2.通配符 " * ...

  2. git 的搭建与使用

    公司之前用的是vpn,然后老大说让我搞一个git.于是,我开始了git的研究之路.... 概念:(说实话,看了还是有些不太理解) git         是一种版本控制系统,是一个命令,是一种工具 g ...

  3. centos7基本环境搭建

    1. 准备权限:让普通用户具备sudo执行权限 切换到root用户,su # vi /etc/sudoers 添加  koushengrui    ALL=(ALL)       ALL 这里很容易忘 ...

  4. robot framework 的AutoItLibrary常用关键字

    1.run 的用法,以及激活当前窗口

  5. PHP文件上传error的错误类型 - $_FILES['file']['error']

    假设文件上传字段的名称img,则: $_FILES['img']['error']有以下几种类型 1.UPLOAD_ERR_OK 其值为 0,没有错误发生,文件上传成功. 2.UPLOAD_ERR_I ...

  6. 3d Max 2017安装失败怎样卸载3dsmax?错误提示某些产品无法安装

    AUTODESK系列软件着实令人头疼,安装失败之后不能完全卸载!!!(比如maya,cad,3dsmax等).有时手动删除注册表重装之后还是会出现各种问题,每个版本的C++Runtime和.NET f ...

  7. maya 安装失败/出错/卸载 2018/2017/2016/2015/2013/2012

    AUTO Uninstaller 更新下载地址 1.选择软件 2.选择版本 3.点击[开始卸载]

  8. 牛客网Java刷题知识点之字符流缓冲区、BufferedWriter、BufferedReader、BufferedReader-readLine方法原理、自定义MyBufferedReader-read方法、自定义MyBufferedReader-readLine方法

    不多说,直接上干货! 把提高效率的动作,封装成一个对象.即把缓冲区封装成一个对象. 就是在一个类里封装一个数组,能对流锁操作数据进行缓存. 什么是字符流缓冲区? 善于使用字符流缓冲区,减轻负担,提高下 ...

  9. Jquery获取父元素

    jquery获取父元素 方法:parent(),parents(),closest() 栗子: <ul class="parent1"> <li><a ...

  10. Model对象嵌套list赋值方式(备忘)

    首先定义Model对象:var deliveryInfoModel = new DeliveryInfo(); 第二步定义嵌套的list对象:var list = new List<Delive ...