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

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

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

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

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

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

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

#include <iostream>
#include <string> class Pet
{
public:
Pet(std::string theName); void eat();
void sleep();
void play(); protected:
std::string name;
}; class Cat : public Pet
{
public:
Cat(std::string theName); void climb();
void play();
}; class Dog : public Pet
{
public:
Dog(std::string theName); void bark();
void play();
}; Pet::Pet(std::string theName)
{
name = theName;
} void Pet::eat()
{
std::cout << name << "正在吃东西!\n";
} void Pet::sleep()
{
std::cout << name << "正在睡大觉!\n";
} void Pet::play()
{
std::cout << name << "正在玩儿!\n";
} Cat::Cat(std::string theName) : Pet(theName)
{
} void Cat::climb()
{
std::cout << name << "正在爬树!\n";
} void Cat::play()
{
Pet::play();
std::cout << name << "玩毛线球!\n";
} Dog::Dog(std::string theName) : Pet(theName)
{
} void Dog::bark()
{
std::cout << name << "旺~旺~\n";
} void Dog::play()
{
Pet::play();
std::cout << name << "正在追赶那只该死的猫!\n";
} int main()
{
Pet *cat = new Cat("加菲");
Pet *dog = new Dog("欧迪"); cat -> sleep();
cat -> eat();
cat -> play(); dog -> sleep();
dog -> eat();
dog -> play(); delete cat;
delete dog; return 0;
}

  结果:

加菲正在睡大觉!
加菲正在吃东西!
加菲正在玩儿!
欧迪正在睡大觉!
欧迪正在吃东西!
欧迪正在玩儿!
请按任意键继续. . .

  仔细一瞧,程序与我们的预期不符:我们在 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

#include <iostream>
#include <string> class Pet
{
public:
Pet(std::string theName); void eat();
void sleep();
virtual void play();//只有这里和上述程序不一样 protected:
std::string name;
}; class Cat : public Pet
{
public:
Cat(std::string theName); void climb();
void play();
}; class Dog : public Pet
{
public:
Dog(std::string theName); void bark();
void play();
}; Pet::Pet(std::string theName)
{
name = theName;
} void Pet::eat()
{
std::cout << name << "正在吃东西!\n";
} void Pet::sleep()
{
std::cout << name << "正在睡大觉!\n";
} void Pet::play()
{
std::cout << name << "正在玩儿!\n";
} Cat::Cat(std::string theName) : Pet(theName)
{
} void Cat::climb()
{
std::cout << name << "正在爬树!\n";
} void Cat::play()
{
Pet::play();
std::cout << name << "玩毛线球!\n";
} Dog::Dog(std::string theName) : Pet(theName)
{
} void Dog::bark()
{
std::cout << name << "旺~旺~\n";
} void Dog::play()
{
Pet::play();
std::cout << name << "正在追赶那只该死的猫!\n";
} int main()
{
Pet *cat = new Cat("加菲");
Pet *dog = new Dog("欧迪"); cat -> sleep();
cat -> eat();
cat -> play(); dog -> sleep();
dog -> eat();
dog -> play(); delete cat;
delete dog; return 0;
}

  结果:

加菲正在睡大觉!
加菲正在吃东西!
加菲正在玩儿!
加菲玩毛线球!
欧迪正在睡大觉!
欧迪正在吃东西!
欧迪正在玩儿!
欧迪正在追赶那只该死的猫!
请按任意键继续. . .

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. NIOGoodDemo

    Java NIO是在jdk1.4开始使用的,它既可以说成“新I/O”,也可以说成非阻塞式I/O.下面是java NIO的工作原理: 1. 由一个专门的线程来处理所有的 IO 事件,并负责分发. 2. ...

  2. wait、notify和notifyAll

    生产者消费者模型是我们学习多线程知识的一个经典案例,一个典型的生产者消费者模型如下: public void produce() { synchronized (this) { while (mBuf ...

  3. KEIL的多工程多目标

    https://blog.csdn.net/ybhuangfugui/article/details/51655502 https://mp.weixin.qq.com/s/CSUa4zegzz8JW ...

  4. jenkins对测试脚本的构建步骤

    使用Jenkins定时执行脚本 Jenkins是基于Java开发的一种持续集成工具,用于监控持续重复的工作,所以可用于定时执行python脚本. 环境准备:jdk1.7及以上+Jenkins[+tom ...

  5. 【Linux】Debian 8 设置命令行界面的文本颜色

    平时我们操作的系统命令行界面文本默认黑底白字,有时候会看不惯这种全篇都是白色字符,这个时候可以通过改变PS1环境变量来改变文本颜色.我个人喜欢黑底绿字的搭配,以下是我个人的命令行界面样式: 注意:以下 ...

  6. 网页设计和制作,数学,access 2010

    网页设计和制作 插入特殊字符:插入---字符---其他字符---选择字符---完成. 插入水平线:插入---字符---水平线---右键---选择第二个框---改变颜色---完成. 插入项目类表:选择要 ...

  7. CheckBox全选、取消全选

    关于CheckBox全选取消全选 //全选 $("#SysAllSelectedID").click(function () { $("[name=SysCheckbox ...

  8. .net core 共享 .Net Forms Authentication cookie

    Asp.net 项目迁移到 asp.net core 项目后需要 兼容以前老的项目的登录方式. Forms Authentication cookie 登录. 从网上搜集到关于这个问题的解决思路都没有 ...

  9. Bootstrap知识点梳理

  10. XHTML教会我的一些东西-1

    第一次写博客,虽然以前写作文是我的强项,我也很能说,但是似乎现在这种能力正在退化.不知为什么,到了大学之后我就变得跟以前不一样,似乎是回到了小学时的我.我在大学开始变得内向.沉默.不去主动和别人交谈. ...