虚方法(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. silverlight vs2010 需要缺少的web组件才能加载

    在打开一个开源的Silverlight项目是遇到如图所示的问题,点击是后没有反应. 查了资料,需要安装微软的webpi(Microsoft Web Platform Installer) webpi的 ...

  2. python_案例综合:教材记录管理

    class Book(): def __init__(self,ISBN,Ftile,Author,Publisher): self.ISBN = ISBN self.Ftile = Ftile se ...

  3. Zookeeper如何正确设置和获取watcher

    Watcher 设置是开发中最常见的,需要搞清楚watcher的一些基本特征,对于exists.getdata.getchild对于节点的不同操作会收到不同的 watcher信息   state=-1 ...

  4. maya2014安装失败如何卸载重装

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

  5. 性能测试工具LoadRunner13-LR之Virtual User Generator 创建java脚本以及小结

    Java vuser是自定义的java虚拟脚本,脚本中可以使用标准的java语言. 环境配置 1.安装jdk(注意:lr11最高支持1.6) 2.配置环境变量 3.在lr选择java Vuser协议 ...

  6. ubuntu14.04通过 gvm 安装 go语言开发环境

    最近用回了ubuntu ,所以打算安装golang学习当下比较火热的这个语言 本来打算使用 sudo apt-get install golang的 安装后发现 是1.2.1不是最新版 所以上网上搜了 ...

  7. VCL

    vcl常用配置 不缓存摸一个资源 在vcl_recv中 if (req.url ~ "private") { return (pass); } 动静分离 先定一个多个backend ...

  8. .NET面试题4

    常见面试题目: 1.字符串是引用类型类型还是值类型? 2.在字符串连接处理中,最好采用什么方式,理由是什么? 3.使用 StringBuilder时,需要注意些什么问题? 4.以下代码执行后内存中会存 ...

  9. GDI+图形图像处理技术——GDIPlus绘图基础

    GDI+概述 GDI在windows中定义为Graphis Device interface,及图形设备接口,是Windows API(application Programming Interfac ...

  10. mybatis SqlSession事务

    mybatis版本:3.4.6. mybatis默认的SqlSessionFactory是DefaultSqlSessionFactory,它openSession()的源码是: public Sql ...