虚函数详解[通俗易懂]

虚函数详解[通俗易懂]文章目录一、虚函数实例二、虚函数的实现(内存布局)1、无继承情况2、单继承情况(无虚函数覆盖)3、单继承情况(有虚函数覆盖)4、多重继承情况(无虚函数覆盖)5、多重继承情况(有虚函数覆盖)三、虚函数的相关问题1、构造函数为什么不能定义为虚函数2、析构函数为什么要定义为虚函数?3、如何去验证虚函数表的存在  面向对象的语言有三大特性:继承、封装、多态。虚函数作为多态的实现方式,重要性毋庸置疑。 …

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

一、多态与重载

1、多态的概念

  面向对象的语言有三大特性:继承、封装、多态。虚函数作为多态的实现方式,重要性毋庸置疑。

  多态意指相同的消息给予不同的对象会引发不同的动作(一个接口,多种方法)。其实更简单地来说,就是“在用父类指针调用函数时,实际调用的是指针指向的实际类型(子类)的成员函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的

2、重载—编译期多态的体现

  重载,是指在一个类中的同名不同参数的函数调用,这样的方法调用是在编译期间确定的

3、虚函数—运行期多态的体现

  运行期多态发生的三个条件:继承关系、虚函数覆盖、父类指针或引用指向子类对象

二、虚函数实例

在这里插入图片描述
  在上述例子中,我们首先定义了一个基类base,基类有一个名为vir_func的虚函数,和一个名为func的普通成员函数。而类A,B都是由类base派生的子类,并且都对成员函数进行了重载。然后我们定义三个base类型的指针Base、a、b分别指向类base、A、B。可以看到,当使用这三个指针调用func函数时,调用的都是基类base的函数而使用这三个指针调用虚函数vir_func时,调用的是指针指向的实际类型的函数。最后,我们将指针b做强制类型转换,转换为A类型指针,然后分别调用func和vir_func函数,发现普通函数调用的是类A的函数,而虚函数调用的是类B的函数。

  以上,我们可以得出结论当使用类的指针调用成员函数时,普通函数由指针类型决定,而虚函数由指针指向的实际类型决定

  虚函数的实现过程:通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

三、虚函数的实现(内存布局)

  虚函数表中只存有一个虚函数的指针地址,不存放普通函数或是构造函数的指针地址。只要有虚函数,C++类都会存在这样的一张虚函数表,不管是普通虚函数亦或是纯虚函数,亦或是派生类中隐式声明的这些虚函数都会生成这张虚函数表。

  虚函数表创建的时间:在一个类构造的时候,创建这张虚函数表,而这个虚函数表是供整个类所共有的。虚函数表存储在对象最开始的位置。虚函数表其实就是函数指针的地址。函数调用的时候,通过函数指针所指向的函数来调用函数。

1、无继承情况

#include <iostream>
using namespace std;
 
class Base
{ 
   
public:
    Base(){ 
   cout<<"Base construct"<<endl;}
    virtual void f() { 
   cout<<"Base::f()"<<endl;}
    virtual void g() { 
   cout<<"Base::g()"<<endl;}
    virtual void h() { 
   cout<<"Base::h()"<<endl;}
    virtual ~Base(){ 
   }
};
 
int main()
{ 
   
    typedef void (*Fun)();  //定义一个函数指针类型变量类型 Fun
    Base *b = new Base();
    //虚函数表存储在对象最开始的位置
    //将对象的首地址输出
    cout<<"首地址:"<<*(int*)(&b)<<endl;
 
    Fun funf = (Fun)(*(int*)*(int*)b);
    Fun fung = (Fun)(*((int*)*(int*)b+1));//地址内的值 即为函数指针的地址,将函数指针的地址存储在了虚函数表中了
    Fun funh = (Fun)(*((int *)*(int *)b+2));
 
    funf();
    fung();
    funh();
 
    cout<<(Fun)(*((int*)*(int*)b+4))<<endl; //最后一个位置为0 表明虚函数表结束 +4是因为定义了一个 虚析构函数
 
    delete b;
    return 0;
}

在这里插入图片描述

2、单继承情况(无虚函数覆盖)

  假设有如下所示的一个继承关系:
在这里插入图片描述
  请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
在这里插入图片描述
【Note】:

  • 虚函数按照其声明顺序放于表中

  • 父类的虚函数在子类的虚函数前面

3、单继承情况(有虚函数覆盖)

  覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
在这里插入图片描述
  为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
在这里插入图片描述
【Note】:

  • 覆盖的f()函数被放到了虚表中原来父类虚函数的位置

  • 没有被覆盖的函数依旧在原来的位置

这样,我们就可以看到对于下面这样的程序,

Base *b = new Derive();
b->f();

由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。

4、多重继承情况(无虚函数覆盖)

  下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
在这里插入图片描述
对于子类实例中的虚函数表,是下面这个样子:

在这里插入图片描述
【Note】:

  • 每个父类都有自己的虚表(有几个基类就有几个虚函数表)

  • 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)。

5、多重继承情况(有虚函数覆盖)

  下面我们再来看看,如果发生虚函数覆盖的情况。下图中,我们在子类中覆盖了父类的f()函数。
在这里插入图片描述
下面是对于子类实例中的虚函数表的图:
在这里插入图片描述
  我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()

四、虚函数的相关问题

1、构造函数为什么不能定义为虚函数

  构造函数不能是虚函数。

  首先,我们已经知道虚函数的实现则是通过对象内存中的vptr来实现的。而构造函数是用来实例化一个对象的,通俗来讲就是为对象内存中的值做初始化操作。那么在构造函数完成之前,vptr是没有值的,也就无法通过vptr找到作为虚函数的构造函数所在的代码区。

2、析构函数为什么要定义为虚函数?

  析构函数可以是虚函数且推荐最好设置为虚函数。

class B
{ 
   
public:
    B() { 
    printf("B()\n"); }
    virtual ~B() { 
    printf("~B()\n"); }
private:
    int m_b;
};
 
class D : public B
{ 
   
public:
    D() { 
    printf("D()\n"); }
    ~D() { 
    printf("~D()\n"); }
private:
    int m_d;
};
 
int main()
{ 
   
    B* pB = new D();
    delete pB;
    return 0;
}

在这里插入图片描述
  C++中有这样的约束:执行子类构造函数之前一定会执行父类的构造函数;同理,执行子类的析构函数后,一定会执行父类的析构函数,这也是为什么我们一直建议类的析构函数写成虚函数的原因。

3、如何去验证虚函数表的存在

typedef void(*Fun)(void);
// 取类的一个实例
Base b;
Fun pFun = NULL;
// 把&b转成int ,取得虚函数表的地址
cout << "虚函数表地址:" << (int*)(&b) << endl;
// 再次取址就可以得到第一个虚函数的地址了
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
pFun = (Fun)*((int*)*(int*)(&b));
pFun();

参考:https://mp.weixin.qq.com/s?__biz=MzIzNjk2NjUxOQ==&mid=2247483655&idx=1&sn=5b29918a121006d14a09e75d2dcb0a8b&chksm=e8ce861fdfb90f09aaa9a5f3c3bbf38b342f73fdfbe37c111c39937e7baa931fc0ac73cf8074#rd

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/158739.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • java webservice 实例_Java WebService 简单实例(附实例代码)

    java webservice 实例_Java WebService 简单实例(附实例代码)前言:朋友们开始以下教程前,请先看第五大点的注意事项,以避免不必要的重复操作。一、准备工作(以下为本实例使用工具)1、MyEclipse10.7.12、JDK1.6.0_22二、创建服务端1、创建【WebServiceProject】,命名为【TheService】。2、创建【Class】类,命名为【ServiceHello】,位于【com.hyan.service】包下。3、编写供客户端调…

  • ABP vNext入门/Docker入门/.NET Core微服务入门——阅读目录

    ABP vNext入门/Docker入门/.NET Core微服务入门——阅读目录

    2020年11月20日
  • 网络安全与渗透测试工具导航下载_渗透师导航

    网络安全与渗透测试工具导航下载_渗透师导航一些网络安全与渗透测试工具导航,值得收藏,看看有没有你熟悉的,也许有一天你会用得到! 入门指南 在线靶场 文件上传漏洞靶场 导航 payload 子域名枚举 自动爬虫实现的子域名收集工具 waf开源及规则 web应用扫描工具 webshell检测以及病毒分析 DDos防护 Android系列工具 XSS扫描 代码审计 端口扫描、指

  • Linux renice_free linux

    Linux renice_free linuxLinuxnice和renice命令教程(7个示例)Linux命令行的威力可以从以下事实来衡量:您甚至可以使用命令行工具轻松调整进程的调度优先级。是的,这是可能的,在本教程中,我们将讨论如何使用nice和renice实用程序来实现这一点。但在这之前,值得一提的是本文中的所有示例都已在Ubuntu16.04LTS系统上进行了测试。Linuxnice和renice命令尽管nice命令允许您以修…

  • Python字符串转换为日期时间– strptime()「建议收藏」

    Python字符串转换为日期时间– strptime()「建议收藏」Wecanconvertastringtodatetimeusingstrptime()function.Thisfunctionisavailableindatetimeandtimemodulestoparseastringtodatetimeandtimeobjectsrespectively.我们可以使用strptime()函数将字…

  • yum源本地配置和网络源配置——超级完整详细「建议收藏」

    yum源本地配置和网络源配置——超级完整详细「建议收藏」本地yum源主和客户端服务器配置:红帽系统是可以免费使用的,但是服务是收费的,RHEL7提供了很好的yum源,但是收费,所以如果你的系统是图形化安装,你使用图形化的时候,会有一个窗口弹出,这是红帽的广告插件,但是你不想付费又不想看到这个广告的时候,可以直接关闭的:查看广告插件:yumlistsubscript*,删除插件:yumremovesubscript*-y。首先关…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号