Visual C++中的虚函数和纯虚函数(以外观设计模式为例)

我是荔园微风,作为一名在IT界整整25年的老兵,今天来说说Visual C++中的虚函数和纯虚函数。该系列帖子全部使用我本人自创的对比学习法。也就是当C++学不下去的时候,就用JAVA实现同样的代码,然后再用对比的方法把C++学会。

直接说虚函数和纯虚函数有很多人会直接晕,但是来看这篇帖子的很多人是有JAVA或其他面象对象编程基础的,我要不就先作个类比,究竟什么是虚函数和纯虚函数,其实很简单,初学者可以直接把C++中的虚函数和纯虚函数理解成JAVA中的抽象函数。这样是不是瞬间就明白了?

我也是在实践中发现,如果你先理解了JAVA的抽象类和抽象函数,你再理解C++的虚函数和纯虚函数就会很简单。哈哈,这个方法好吧。

那么理解了我上面说的,我们只需要再区别一下C++中的虚函数和纯虚函数理解成JAVA中的抽象函数,找寻一些他们之间的不同点就可以了。

好了,下面代码部分是本文的精华,请仔细比对下面两段代码,一段是C++,一段是JAVA,他们在实现同一个事情,输出结果也一样,但表达不同。

Facade外观模式是一种通过为多个复杂子系统提供一个一致的接口,从而使这些子系统更加容易被访问的模式。

以医院为例,就医时患者需要与医院不同的职能部门交互,完成挂号、门诊、取药等操作。为简化就医流程,设置了一个用户界面友好的接待机器的角色,患者只要在这台机器上操作,机器就能代患者完成上述就医步骤,患者则只需与接待机器交互即可。

我们先用JAVA代码来实现:

import java.util.*;interface Patient{String getName();
}interface Disposer{void dispose(Patient patient);
}class Registry implements Disposer{//挂号public void dispose(Patient patient){System.out.println("registering..."+ patient.getName());}
}class Doctor implements Disposer{//门诊public void dispose(Patient patient){System.out.println("diagnosing..."+ patient.getName());}
}class Pharmacy implements Disposer { //取药public void dispose(Patient patient){System.out.println("giving medicine..."+ patient.getName());}
}class Facade{private Patient patient;public Facade(Patient patient){this.patient=patient;}public void dispose(){Registry registry=new Registry();Doctor doctor=new Doctor();Pharmacy ph=new Pharmacy();registry.dispose(patient);doctor.dispose(patient);ph.dispose(patient);}
}class ConcretePatient implements Patient{private String name;public ConcretePatient(String name){this.name=name;}public String getName(){ return name;}
}class FacadeTest{public static void main(String[] args){Patient patient=new ConcretePatient("name");Facade f=new Facade(patient);f.dispose();
}

然后我们再用比较难的C++语言写一遍上面这个意思。

#include <iostream>
#include <string>
using namespace std:class Patient{
public:virtual string getName()=0;
};class Disposer {
public:virtual void dispose(Patient *patient)=0;
};class Registry:public Disposer{
//挂号
public:void dispose(Patient *patient){cout<<"registering..."<< patient->getName()<< endl;}
};class Doctor:public Disposer{
//门诊
public:void dispose(Patient *patient){cout <<"diagnosing..."<< patient->getName()<< endl;}
};class Pharmacy: public Disposer{
//取药
public:void dispose(Patient *patient){cout <<"giving medicine."<< patient->getName()<< endl;}
};class Facade{
private:Patient *patient;
public:Facade(Patient *patient){ this->patient=patient;}void dispose(){Registry *registry=new Registry();Doctor *doctor=new Doctor();Pharmacy *ph=new Pharmacy();registry->dispose(patient);doctor->dispose(patient);ph->dispose(patient);}
};class ConcretePatient:public Patient{
private:string name;
public:ConcretePatient(string name){this->name=name;}string getName(){return name;}
};int main(){Patient *patient=new ConcretePatient("name");Facada *f=new Facade(patient);f->dispose();return 0;
} 

Java抽象类和C++虚基类的不同点

java和C++都是面向对象编程语言,遵循面向对象的特性,继承,封装,多态。由于java的抽象类和C++虚基类很像,本文对二者在这两个概念上进行一些比较。从名称上来讲,标准的概念:

C++:虚函数,虚基类;

java:抽象方法,抽象类,接口。

C++中,虚函数的存在是为了实现多态。C++中用virtual关键字来标识虚函数,即普通成员函数加上virtual就成为虚函数。Java中没有虚函数的概念,它的普通函数就相当于C++的虚函数,动态绑定是Java的默认行为。java中,如果某个方法不想被子类实现,就用final关键字使其变成非虚函数。java抽象函数/C++纯虚函数,其实就是没有方法体的方法,即一个方法只有声明,没有定义(实现)。抽象函数或者说是纯虚函数的存在是为了定义接口。

 C++中纯虚函数形式为:virtual void print() = 0;

 Java中纯虚函数形式为:abstract void print();

Java抽象类的存在是因为父类中既包括子类共性函数的具体定义,也包括需要子类各自实现的函数接口。抽象类中可以有数据成员和非抽象方法。抽象类中可以没有抽象方法,但具有抽象方法的类必须定义为抽象类,抽象类不能实例化。C++中抽象类只需要包括纯虚函数,就是一个抽象类。如果仅仅包括虚函数,不能定义为抽象类,因为类中其实没有抽象的概念。Java抽象类是用abstract修饰声明的类,而C++中,需要用virtual void print() = 0;的形式来标识。

Java接口用interface来定义。接口中的变量自动具有public static final属性,接口中的方法自动具有public abstract属性,接口允许多继承。接口中不能有普通成员变量,也不能具有非纯虚函数。

C++中没有接口这个概念,如果所有的方法都是纯虚函数,即全虚基类,可以将其视为和java中的接口是同等概念。这些纯虚函数必须要由子类重写,就像java中的接口中的方法必须被实现一样。
虚基类同样不能实例化。纯虚函数不能有自己的函数体,但是纯虚析构函数除外。

JAVA中的抽象函数与C++中的虚函数比较

java中没有虚函数的概念,但是有抽象函数的概念,用abstract关键字表示,java中抽象函数必须在抽象类中,而且抽象函数不能有函数体,抽象类不能被实例化,只能由其子类实现抽象函数,如果某个抽象类的子类仍是抽象类,那么该子类不需要实现其父类的抽象函数。

C++中的有虚函数的概念,用virtual 关键字来表示,每个类都会有一个虚函数表,该虚函数表首先会从父类中继承得到父类的虚函数表, 如果子类中重写了父类的虚函数(不管重写后的函数是否为虚函数),要调用哪个虚函数,是根据当前实际的对象来判断的(不管指针所属类型是否为当前类,有可能是父类型),指针当前指向的是哪种类型的对象,就调用哪个类型中类定义的虚函数。每个类只有一张虚拟函数表,所有的对象共用这张表。C++的函数多态就是通过虚函数来实现的。

C++中,如果函数不是虚函数,则调用某个函数,是根据当前指针类型来判断的,并不是根据指针所指向对象的类型。Java中,如果函数不是抽象函数,而是一个普通函数,它是默认实现类似C++中虚函数功能的,也就是说,调用某个函数,是根据当前指针所指向对象的类型来判断的,而不是根据指针类型判断。正好与C++中的普通函数相反。即JAVA里自动实现了虚函数。

纯虚函数: 主要特征是不能被用来声明对象,是抽象类,是用来确保程序结构与应用域的结构据具有直接映射关系的设计工具。带有纯虚函数的类称为抽象类,抽象类能被子类 继承使用,在子类中必须给出纯虚函数的实现,如果子类未给出该纯虚函数的实现,那么该子类也是抽象类,只有在子类不存在纯虚函数时,子类才可以用来声明对 象!抽象类也能用于声明指针或引用,或用于函数声明中。具有抽象类特性的类还有构造函数和析构函数,全部是保护的类。如果没有给出纯虚函数的实现,则在它所在的类的构造函数或析构函数中不能直接或间接的调用它。纯虚函数的实现可以在类声明外进行定义。

C++中一般都是把析构函数声明为虚函数。因为虚函数可以实现动态绑定,也就是到底调用哪个函数是根据指针当前指向哪个对象来确定的,不是根据指针的类型来确定。如果C++中不把析构函数声明为虚函数,那么其有个子类,重写了虚函数,那么当父类指针指向一个子类对象时,当调用析构函数时,只调用父类的析构函数,而无法调用子类的析构函数,所以一般情况是把析构函数声明为虚函数,实现动态绑定。当然如果一个类不包含虚函数,这经常预示不打算将它作为基类使用。当一个类不打算作为基类时,将析构函数声明为虚拟通常是个坏主意。比如:标准 string 类型不包含虚函数,如果把String作为基类继承得到子类会出问题。

总之:多态基类应该声明虚析构函数。如果一个类有任何虚函数,它就应该有一个虚析构函数;如果不是设计用于做基类或不是设计用于多态,这样的类就不应该声明虚析构函数。

关于接口与抽象类:C++中没有接口的概念,与之对应的是纯虚类,即只含有纯虚函数的类,C++抽象类的概念是含有纯虚函数成员的类。这是因为c++提供多继承,而像java、c#这些只提供单继承(避免多继承的复杂性和低效性)的语言为了模拟多继承功能就提供了接口概念,接口可以继承多个。abstract class是抽象类,至少包含一个纯虚函数的类就叫做抽象类。但是如果一个类,所有的成员都是纯虚函数,那么它和一般的抽象类在用法上是有区别的。至少microsoft给的com接口定义全部都是仅由纯虚函数构成的类。因此把这样的类定义叫做纯虚类也不算错。 纯虚函数和虚函数的区别在于前者不包含定义,而后者包含函数体。 那么纯虚类就是不包含任何实现(包括成员函数定义和成员变量定义。前者代表算法,后者代表结构)。不包含任何算法和结构的类叫做纯虚类,应该没有问题。

在java里面的确没有纯虚类的概念,因为java里没有纯虚函数这个概念。java管虚函数叫做abstract function,管抽象类叫做abstract class,直接说来,java根本没有virtual这个关键字,都用abstract代替,因此java里面根本就没有pure这个概念。有那就是interface。在interface里面定义的函数都不能有函数体,这个在java里面叫做接口。那么c++里面与interface等同的概念就是纯虚类了,c++用纯虚类来模拟interface这个抽象概念,因此这里说的“纯虚类”与java的abstract class不同,与c++的一般抽象类也不同。“纯虚类”与c++一般抽象类的区别就好比java里面interface 和 abstract class的区别。

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。

总结

 C++中的虚函数就是JAVA中的普通函数, C++ 中的纯虚函数就是JAVA中的抽象函数, C++ 中的抽象类就是JAVA中的抽象类, C++ 中的虚基类就是JAVA中的接口。


作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/13531.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【SpringCloud Alibaba】(四)使用 Feign 实现服务调用的负载均衡

在上一文中&#xff0c;我们实现了服务的自动注册与发现功能。但是还存在一个很明显的问题&#xff1a;如果用户微服务和商品微服务在服务器上部署多份的话&#xff0c;之前的程序无法实现服务调用的负载均衡功能。 本文就带着大家一起实现服务调用的负载均衡功能 1. 负载均衡…

LayUi 树形组件tree 实现懒加载模式,展开父节点时异步加载子节点数据

如题。 效果图&#xff1a; //lazy属性为true&#xff0c;点开时才加载 引用代码&#xff1a; <link href"~/Content/layui-new/css/layui.css" rel"stylesheet" /><form id"form" class"layui-form" style"margin-to…

AC695-按键处理-带UI

AC695-按键修改 消息发出 对应界面处理

【算法和数据结构】257、LeetCode二叉树的所有路径

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;首先看这道题的输出结果&#xff0c;是前序遍历。然后需要找到从根节点到叶子节点的所有路径&#xff…

【图论】树上差分(点差分)

一.题目 输入样例&#xff1a; 5 10 3 4 1 5 4 2 5 4 5 4 5 4 3 5 4 3 4 3 1 3 3 5 5 4 1 5 3 4 输出样例&#xff1a;9 二 .分析 我们可以先建一棵树 但我们发现&#xff0c;这样会超时。 所以&#xff0c;我们想到树上差分 三.代码 /* 5 10 3 4 1 5 4 2 5 4 5 4 5 4 3 5 …

IOS UICollectionView 设置cell大小不生效问题

代码设置flowLayout.itemSize 单元格并没有改变布局大小&#xff0c; 解决办法如下图&#xff1a;把View flow layout 的estimate size 设置为None&#xff0c;上面设置的itemSize 生效了。

物联网阀控水表计量准确度如何?

物联网阀控水表是一种新型的智能水表&#xff0c;它采用了先进的物联网技术&#xff0c;可以通过远程控制和监测水表的运行情况&#xff0c;实现更加精准的水量计量和费用结算。那么&#xff0c;物联网阀控水表的计量准确度如何呢&#xff1f;下面我们将从以下几个方面进行详细…

PHP 3des加解密新旧方法可对接加密

一、旧3des加解密方法 <?php class Encrypt_3DES {//加密秘钥&#xff0c;private $_key;private $_iv;public function __construct($key, $iv){$this->_key $key;$this->_iv $iv;}/*** 对字符串进行3DES加密* param string 要加密的字符串* return mixed 加密成…

【SpringⅢ】Spring 的生命周期

目录 &#x1f96a;1 Bean 的作用域 &#x1f969;1.1 singleton&#xff1a;单例模式 &#x1f359;1.2 prototype&#xff1a;原型模式 &#x1f371;1.3 Bean 的其他作用域 &#x1f35c;2 Spring 生命周期(执行流程) &#x1f958;2.1 启动容器 &#x1f372; 2.2 读…

[小尘送书-第二期]《从零开始读懂量子力学》由浅入深,解释科学原理;从手机到超导,量子无处不在;从微观到宏观,遐想人生的意义!

大家好&#xff0c;我是小尘&#xff0c;欢迎关注&#xff0c;一起交流学习&#xff01;欢迎大家在CSDN后台私信我&#xff01;一起讨论学习&#xff0c;讨论如何找到满意的工作&#xff01; 本文目录 一、前言二、作者简介三、内容简介四、抽奖方式五、名家推介写在最后 一、前…

[containerd] 在Windows上使用IDEA远程调试containerd, ctr, containerd-shim

文章目录 1. containerd安装2. 源码编译3. 验证编译的二进制文件是否含有调试需要的信息3.1. objdump工具验证3.2. file工具验证3.3. dlv工具验证 4. debug 1. containerd安装 [Ubuntu 22.04] 安装containerd 2. 源码编译 主要步骤如下&#xff1a; 1、从github下载containe…

[Java] 单例设计模式详解

模式定义&#xff1a;保证一个类只有一个实例&#xff0c;并且提供一个全局访问点&#xff0c;时一种创建型模式 使用场景&#xff1a;重量级的对象&#xff0c;不需要多个实例&#xff0c;如线程池&#xff0c;数据库连接池 单例设计模式的实现 1.懒汉模式&#xff1a;延迟…

第一次后端复习整理(JVM、Redis、反射)

1. JVM 文章仅为自身笔记 详情查看一篇文章掌握整个JVM&#xff0c;JVM超详细解析&#xff01;&#xff01;&#xff01; 1.1 什么是JVM jvm是Java虚拟机 1.2 Java文件的编译过程 程序员编写代码形成.java文件经过javac编译成.class文件再通过JVM的类加载器进入运行时数据…

【141. 环形链表】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#x…

JVM | 基于类加载的一次完全实践

引言 我在上篇文章&#xff1a;JVM | 类加载是怎么工作的 中为你介绍了Java的类加载器及其工作原理。我们简单回顾下&#xff1a;我用一个易于理解的类比带你逐步理解了类加载的流程和主要角色&#xff1a;引导类加载器&#xff0c;扩展类加载器和应用类加载器。并带你深入了解…

剑指offer12 矩阵中的路径 13 机器人的运动范围 34.二叉树中和为某一值得路径

class Solution { public:bool exist(vector<vector<char>>& board, string word) {int rowboard.size(),colboard[0].size();int index0,i0,j0;if(word.size()>row*col) return 0;//vector<vector<int>> visit[row][col];//标记当前位置有没有…

算法之归并排序算法

归并&#xff08;Merge&#xff09;排序法是将两个&#xff08;或两个以上&#xff09;有序表合并成一个新的有序表&#xff0c;即把待排序序列 分为若干个子序列&#xff0c;每个子序列是有序的。然后再把有序子序列合并为整体有序序列。 public class MergeSortTest {public …

到底什么是前后端分离

目录 Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 总结 Web 应用的开发主要有两种模式&#xff1a; 前后端不分离 前后端分离 理解它们的区别有助于我们进行对应产品的测试工作。 前后端不分离 在早期&#xff0c;Web 应用开发主要采用前后端不…

linux系统下(centos7.9)安装Jenkins全流程

一、卸载历史版本 # rpm卸载 rpm -e jenkins# 检查是否卸载成功 rpm -ql jenkins# 彻底删除残留文件 find / -iname jenkins | xargs -n 1000 rm -rf二、环境依赖安装 yum -y install epel-releaseyum -y install daemonize三、安装Jenkins Jenkins官网传送带&#xff1a; …

《零基础入门学习Python》第071讲:GUI的终极选择:Tkinter8

虽然我们能用 tkinter 设计不少东西了&#xff0c;但是不少同学还是感觉对这个界面编程掌控得还不够多&#xff0c;说白了&#xff0c;就是我们现在还没办法随心所欲的去绘制我们想要的界面&#xff0c;但是不瞒你说&#xff0c;今天的这一节课将会给你的人生乃至人生观带来翻天…