虚方法(virsual method)
虚方法(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)的更多相关文章
- 抽象方法(abstract method) 和 虚方法 (virtual method), 重载(overload) 和 重写(override)的区别于联系
1. 抽象方法 (abstract method) 在抽象类中,可以存在没有实现的方法,只是该方法必须声明为abstract抽象方法. 在继承此抽象类的类中,通过给方法加上override关键字来实现 ...
- [翻译] Virtual method interception 虚方法拦截
原文地址:http://blog.barrkel.com/2010/09/virtual-method-interception.html 注:基于本人英文水平,以下翻译只是我自己的理解,如对读者造成 ...
- 为何JAVA虚函数(虚方法)会造成父类可以"访问"子类的假象?
首先,来看一个简单的JAVA类,Base. 1 public class Base { 2 String str = "Base string"; 3 protected vo ...
- 译:C#面向对象的基本概念 (Basic C# OOP Concept) 第三部分(多态,抽象类,虚方法,密封类,静态类,接口)
9.多态 Ploy的意思就是多于一种形式.在文章开始,方法那一章节就已经接触到了多态.多个方法名称相同,而参数不同,这就是多态的一种. 方法重载和方法覆盖就是用在了多态.多态有2中类型,一种是编译时多 ...
- 访问祖先类的虚方法(直接访问祖先类的VMT,但是这种方法在新版本中未必可靠)
访问祖先类的虚方法 问题提出 在子类覆盖的虚方法中,可以用inherited调用父类的实现,但有时候我们并不需要父类的实现,而是想跃过父类直接调用祖先类的方法. 举个例子,假设有三个类,实现如下: t ...
- 浅谈 虚方法(virtual)
虚方法 理解:从字面意思来讲,"虚",可有可无,子类对父类的某种方法的重写,可以重写,也可以不重写. 虚方法,顾名思义(装个13),就是某种方法. 用法:public virtua ...
- C#中的抽象类、抽象方法和虚方法
[抽象类]abstract 修饰符可与类和方法一起使用定义抽象类的目的是提供可由其子类共享的一般形式.子类可以根据自身需要扩展抽象类.抽象类不能实例化.抽象方法没有函数体.抽象方法必须在子类中给出具体 ...
- 类型,对象,线程栈,托管堆在运行时的关系,以及clr如何调用静态方法,实例方法,和虚方法(第二次修改)
1.线程栈 window的一个进程加载clr.该进程可能含有多个线程,线程创建的时候会分配1MB的栈空间. 如图: void Method() { string name="zhangsan ...
- C#语法-虚方法详解 Virtual 虚函数
虚方法 / Virtual 本文提供全流程,中文翻译. Chinar 坚持将简单的生活方式,带给世人!(拥有更好的阅读体验 -- 高分辨率用户请根据需求调整网页缩放比例) Chinar -- 心分享. ...
随机推荐
- 9-----BBS论坛
BBS论坛(九) 9.1.权限和角色模型定义 (1)cms/models class CMSPermission(object): ALL_PERMISSION = 0b11111111 # 1.访问 ...
- java——设计一个支持push,pop,top、在恒定时间内检索最小元素的栈。
普通方法: 需要另外一个栈 用来存放每一时刻的min值 巧妙版: 只需要一个stack,stack中存的是与min的差值 但由于min是两个整数之间的差值,有可能会出现差值超过整数边界值的情况,因此要 ...
- 普通用户不能使用sudo命令的解决办法
普通用户不能使用sudo命令的解决办法 https://www.cnblogs.com/fasthorse/p/5949946.html 1. 切换到root用户下:su – root 2. 给/et ...
- io基础(字节流、字符流、转换流、缓冲字符流)
首先需要明确的一点是输入流输出流的输入输出是站在内存的角度看的,读取文件,把文件内容写到内存中,是输入流:写文件,把内存中的数据写到文件中,是输出流. IO操作主要有4个抽象类: 字节输入输出流:In ...
- 所有节点配置NTP服务
主节点: 打开vim /etc/ntp.conf文件 For more information about this file, see the man pages # ntp.conf(), ntp ...
- 吞吐率(Requests per second),缩写RPS
计算公式: 吞吐率 = 总请求数 / 处理这些请求的总完成时间 Requests per second = Complete requests / Time taken for tests 吞 ...
- HTML5中video标签与canvas绘图的使用
video标签的使用 video标签定义视频, 它是html5中的新标签, 它的属性如下(参考自文档): domo01 <!DOCTYPE html> <html lang=&quo ...
- 使用 Charles 获取 https 的数据
1. 配置 Charles 根证书 首先打开 Charles: 然后如下图操作: 之后会弹出钥匙串,如果不弹出,请自行打开钥匙串,如下图: 系统默认是不信任 Charles 的证书的,此时对证书右键, ...
- ThinkPHP 统计数据(数字字段)更新 setInc 与 setDec 方法
ThinkPHP 统计数据更新 ThinkPHP 内置了对统计数据(数字字段)的更新方法: setInc():将数字字段值增加 setDec():将数字字段值减少 setInc() ThinkPHP ...
- Linux大文件快速处理小方法
背景 工作中使用MapReduce任务导出一批含有路径的文件,共计行数300W+,需要检测文件是否在对应的服务器中存在,而文件所在的服务器并非hadoop集群的服务器,因此打算采用bash脚本进行.具 ...