虚方法(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. Python中的数据类型和数据结构

    一.数据类型 Python中有六个标准数据类型: Number(数字) String(字符串) List(列表) Tuple(元组) Sets(集合) Dictionary(字典) 其中,除列表Lis ...

  2. Nmap工具使用

    Nmap是一款网络扫描和主机检测的非常有用的工具. Nmap是不局限于仅仅收集信息和枚举,同时可以用来作为一个漏洞探测器或安全扫描器.它可以适用于winodws,linux,mac等操作系统.Nmap ...

  3. sf03_杨辉三角go实现

    package main import "fmt" /* 变量规范 全局变量以v_为前缀 函数形参以p_为前缀 函数内部变量,字母数字下划线等普通组合,其中函数返回值以out_为前 ...

  4. 转 RMAN-20005: target database name is ambiguous

    发生的这个错误的由于: 在RMAN CATALOG中,register了一个name叫test的数据库,后来这个库被我搞坏了.就重建了一个test的数据库,名称没有更改,又重新register到RMA ...

  5. 配置WAMP完美攻略

    软件介绍 Wamp Server 是一款功能强大的PHP 集成安装环境. 为了节约时间,本次使用 Wamp Server 来进行配置. wamp 的全部含义就是本篇文章的标题. 使用版本和操作系统 W ...

  6. Selenium + Python操作IE 速度很慢的解决办法

    IEDriverServer 64位换成32位 https://docs.seleniumhq.org/download/

  7. 利用划分树求解整数区间内第K大的值

    如何快速求出(在log2n的时间复杂度内)整数区间[x,y]中第k大的值(x<=k<=y)? 其实我刚开始想的是用快排来查找,但是其实这样是不行的,因为会破坏原序列,就算另外一个数组来存储 ...

  8. vs2012配置使用entity framework 6

    项目中使用mysql作为数据库,想快速地实现一些数据服务,为了节省开发时间,提升开发效率,性能不是考虑的重点,所以选择了使用ORM框架:Entity Framework.指定了DB的table des ...

  9. 虚拟机扩容(/dev/mapper/centos-root 空间不足)

    1:.首先查看我们的根分区大小是多少 df -h 文件系统                类型      容量  已用  可用 已用% 挂载点 /dev/mapper/centos-root xfs  ...

  10. C# 将外部exe程序 嵌入到自己的窗体界面

    将别人开发的exe程序,放到自己的窗体里面来运行. 1.基本功能实现 首先,在自己的窗体后面加上代码: [DllImport("User32.dll", EntryPoint = ...