c++虚函数、静态绑定与动态绑定

首先说明,所谓绑定,就是指函数的调用

接下来,我们直接看一段代码来说明问题

class Base
{
public:
    Base(int data=10):m_a(data){}
    void show(){cout<<"Base::show()"<<endl;}
    void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

上述代码中,定义了一个Base类和一个Derive类,并且Derive类继承了Base类,其中

Base类中有一组互为重载关系的成员函数show

Derive类中有一个与Base类中同名的成员函数,因此,Derive::show()与Base::show()、Base::show(int)构成了隐藏关系。

静态绑定

接下来,我们写一段测试代码来说明问题,这段测试代码包括

  • 定义一个基类指针,并指向其子类对象
  • 并使用基类指针调用show成员函数,观察运行结果
  • 查看基类指针的类型和基类指针所指对象的类型
void test()
{
    Derive d(50);
    Base* pb=&d;
    pb->show();
    pb->show(11);

    cout<<"Base size:"<<sizeof(Base)<<endl;
    cout<<"Derive size:"<<sizeof(Derive)<<endl;

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;
}

 

通过实验结果可以看到,尽管基类指针(Base* pb)指向的是基类对象,但是通过pb所调用的函数仍旧是基类作用域下的成员函数。

静态绑定就是指在编译期间就确定好了函数的具体实现版本,由于pb的类型是Base*(也被称为静态类型),因此通过pb所调用的成员函数show在编译期间就被确定在Base作用域下的show成员函数

  • 静态绑定适用于非虚函数和静态函数
  • 静态绑定中,函数调用的实现版本在编译期间就已经确定,无法在运行期间改变

为加深理解,我们再写一例

class Base {
public:
    void func() {
        cout << "Base::func()" << endl;
    }
};

class Derived : public Base {
public:
    void func() {
        cout << "Derived::func()" << endl;
    }
};

int main() {
    Base b;
    Derived d;

    b.func(); // 静态绑定,调用 Base::func()
    d.func(); // 静态绑定,调用 Derived::func()

    Base* p = &d;
    p->func(); // 静态绑定,调用 Base::func(),因为 p 的静态类型是 Base*
    return 0;
}

 虚函数

接下来,我们对Base类的代码做一点小小的改动

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

在Base类的成员函数前加一个virtual关键字,此时Base的成员函数就被称之为虚函数

一个类里如果定义了虚函数,那么

  • 编译期间,编译器就会为该类产生一个唯一的vftable虚函数表
  • 该vftable虚函数表中主要存储的内容就是RTTI指针和虚函数的地址
  • 当程序运行时,每一张虚函数表都被加载到rodata区.(只读)

注:RTTI(run-time type infomation),即运行时的类型信息

以上Base类的虚函数表(vftable)为

除此以外,如果一个类里定义了虚函数,那么

  • 该类所定义的对象,其运行时,内存中的开始部分,会多存储一个vfptr虚函数指针,指向该类的虚函数表(存储该虚函数表的首地址)
  • 该类所定义的每个对象,都会有一个vfptr指针,但虚函数表只有一张

 

因此,一个类里虚函数的个数,不影响内存的大小(对象内存中只有一个虚函数指针vfptr),影响的是虚函数表的大小 

此外,如果派生类中的某个成员函数和基类中某个成员函数完全相同(包括函数名、函数类型),只有函数体的实现不同,那么该成员函数也将自动被处理为虚函数。

class Base
{
public:
    Base(int data=10):m_a(data){}
    virtual void show(){cout<<"Base::show()"<<endl;}
    virtual void show(int){cout<<"Base::show(int)"<<endl;}
protected:
    int m_a;
};

class Derive:public Base
{
public:
    Derive(int data=20):Base(data),m_b(data){}
    void show(){cout<<"Derive:show()"<<endl;}
private:
    int m_b;
};

void test()
{
    Derive d(50);
    Base* pb=&d;
    pb->show();
    pb->show(11);

    cout<<"Base size:"<<sizeof(Base)<<endl;
    cout<<"Derive size:"<<sizeof(Derive)<<endl;

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;
}

 再次观察上述修改后的代码,在测试函数中查看运行结果

可以看到,无论是Base还Derive,其大小都是16B,而不再是4B,其原因就在于,Base类中除了有一个int类型的成员变量外还有一个占8B的vfptr,又根据内存对齐原则,故而Base类的大小就是8(vfptr)+4(int)+4(内存对齐)=16B 

覆盖

接下来,我们将目光转向上述代码的Derive类。

上边说到,由于Derive子类中出现了与Base父类完全相同的成员函数(void show()),因此编译器自动将其声明为虚函数,因此我们知道,编译器也将在编译阶段为Derive生成一张唯一的虚函数表vftable,因此在测试代码中定义子类对象d时,d也将拥有一个虚函数指针vfptr

 动态绑定

接下来,我们再来看测试代码中这行代码的运行结果的差异

    pb->show();

可以看到,在成员函数show没有被声明为virtual之前,该行代码执行的是Base::show(),而当其被声明为虚函数后,执行结果就成为了Derive::show()

这是因为,代码在执行到pb->show()时,如果发现show不是虚函数,就进行静态绑定,如果发现show是虚函数,就进行动态绑定。

所谓动态绑定,实质上是因为其汇编过程为

mov eax dword ptr[pb]
mov ecx dword ptr[eax]
call ecx(虚函数的地址)

第一行汇编代码执行:将指针pb指向的地址(虚函数表的首地址)放到寄存器eax中

第二行汇编代码执行:将eax中的前四个字节的地址(也就是对应show()函数的地址)放到ecx寄存器中

第三行汇编代码执行:执行ecx寄存器中的代码

从上述汇编过程可以看到,由于我们执行的是ecx寄存器中的代码,但是ecx中保存的地址需要等到运行时期才能确定。

这种在程序运行时需要根据对象的实际类型来确定调用哪个方法或函数的机制就叫动态绑定

接下来,我们再来看指针pb和*pb的类型变化

    cout<<typeid(pb).name()<<endl;
    cout<<typeid(*pb).name()<<endl;

 

可以看到,

  • pb的类型:无论是否有虚函数,基类指针pb的类型永远都是Base
  • *pb的类型:
    • 如果Base有虚函数,*pb识别的就是运行时期的类型(RTTI类型)
    • 如果Base没有虚函数,*pb识别的就是编译时期的类型(Base类型)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[python]基于LSTR车道线实时检测onnx部署

【框架地址】 https://github.com/liuruijin17/LSTR 【LSTR算法介绍】 LSTR车道线检测算法是一种用于识别和定位车道线的计算机视觉算法。它基于图像处理和机器学习的技术&#xff0c;通过对道路图像进行分析和处理&#xff0c;提取出车道线的位置和方向等信息。 LSTR车道线…

docker镜像结构

# 基础镜像 FROM openjdk:11.0-jre-buster # 设定时区 ENV TZAsia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # 拷贝jar包 COPY docker-demo.jar /app.jar # 入口 ENTRYPOINT ["java", "-jar"…

[SWPUCTF 2021 新生赛]babyrce

我们打开发现他让我们输入cookie值为admin1 出现一个目录 我们跳转进去 我们可以发现这个有个preg_match不能等于空格不让会跳转到nonono我们可以通过${IFS}跳过

桌面显示器应用Type-C接口有什么好处

随着科技的不断发展&#xff0c;桌面显示器作为我们日常工作中不可或缺的设备之一&#xff0c;也在不断更新换代。其中&#xff0c;Type-C接口的应用成为了桌面显示器发展的一个重要趋势。那么&#xff0c;桌面显示器应用Type-C接口究竟有什么好处呢&#xff1f; 首先&#xff…

一文教你地平线旭日派X3部署yolov5从训练-->转模型-->部署

一文教你地平线旭日派X3部署yolov5从训练&#xff0c;转模型&#xff0c;到部署 近日拿到了地平线的旭日派X3&#xff0c;官方说是支持等效5tops的AI算力&#xff0c;迫不及待的想在上面跑一个yolov5的模型&#xff0c;可谓是遇到了不少坑&#xff0c;好在皇天不负有心人&…

【Leetcode】1690. 石子游戏 VII

文章目录 题目思路代码结果 题目 题目链接 石子游戏中&#xff0c;爱丽丝和鲍勃轮流进行自己的回合&#xff0c;爱丽丝先开始 。 有 n 块石子排成一排。每个玩家的回合中&#xff0c;可以从行中 移除 最左边的石头或最右边的石头&#xff0c;并获得与该行中剩余石头值之 和 相…

H5 加密(MD5 Base64 sha1)

1. 说明 很多的时候是避免不了注册登录这一关的&#xff0c;但是一般的注册是没有任何的难度的&#xff0c;无非就是一些简单的获取用户输入的数据&#xff0c;然后进行简单的校验以后调用接口&#xff0c;将数据发送到后端&#xff0c;完成一个简单的注册的流程&#xff0c;那…

SVDiff: Compact Parameter Space for Diffusion Fine-Tuning——【论文笔记】

本文发表于ICCV 2023 论文地址&#xff1a;ICCV 2023 Open Access Repository (thecvf.com) 官方代码&#xff1a;mkshing/svdiff-pytorch: Implementation of "SVDiff: Compact Parameter Space for Diffusion Fine-Tuning" (github.com) 一、Introduction 最近几…

Multiuser Communication Aided by Movable Antenna

文章目录 II. SYSTEM MODEL AND PROBLEM FORMULATIONA. 通道模型B. Problem Formulation III. PROPOSED SOLUTION II. SYSTEM MODEL AND PROBLEM FORMULATION 如图1所示&#xff0c;BS配置了尺寸为 N N 1 N 2 NN_{1} \times N_{2} NN1​N2​ 的均匀平面阵列&#xff08;uni…

再谈Redis三种集群模式:主从模式、哨兵模式和Cluster模式

总结经验 redis主从:可实现高并发(读),典型部署方案:一主二从 redis哨兵:可实现高可用,典型部署方案:一主二从三哨兵 redis集群:可同时支持高可用(读与写)、高并发,典型部署方案:三主三从 一、概述 Redis 支持三种集群模式,分别为主从模式、哨兵模式和Cluster模式。…

logback日志配置

springboot默认使用logback 无需额外添加pom依赖 1.指定日志文件路径 当前项目路径 testlog文件夹下 linux会在项目jar包同级目录 <property name"log.path" value"./testlog" /> 如果是下面这样配置的话 window会保存在当前项目所在盘的home文件夹…

yo!这里是单例模式相关介绍

目录 前言 特殊类设计 只能在堆上创建对象的类 1.方法一&#xff08;构造函数下手&#xff09; 2.方法二&#xff08;析构函数下手&#xff09; 只能在栈上创建对象的类 单例模式 饿汉模式实现 懒汉模式实现 后记 前言 在面向找工作学习c的过程中&#xff0c;除了基本…

查看自己电脑是arm还是x64(x86);linux操作系统识别

1、查看自己电脑是arm还是x64&#xff08;x86&#xff09; linux 参考&#xff1a; https://liuweiqing.blog.csdn.net/article/details/131783851 uname -a如果输出是 x86_64&#xff0c;那么你的系统是 64 位的 x86 架构&#xff08;通常我们称之为 x64&#xff09;。如果…

【annie/lux 快速下载哔哩哔哩视频】全网最简单,只需要5步!!!

1.首先 现在annie更名为lux 官网地址&#xff1a;https://github.com/iawia002/lux/releases 2.进入官网之后如图所示 3.下载lux软件 4.下载lux 这里需要说明一下 如果不下载这个的话也可以下载视频 但是视频和音频是分开的&#xff0c;你的视频没有声音 5.下载视频

Hive 主要内容一览

Hive架构 用户接口&#xff1a;Client CLI&#xff08;command-line interface&#xff09;、JDBC/ODBC(jdbc访问hive) 元数据&#xff1a;Metastore 元数据包括&#xff1a;表名、表所属的数据库&#xff08;默认是default&#xff09;、表的拥有者、列/分区字段、表的类型&am…

面试150 位1的个数 位运算

Problem: 191. 位1的个数 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 参考 复杂度 Code public class Solution {// you need to treat n as an unsigned valuepublic int hammingWeight(int n){int res 0;while (n ! 0){res 1;n & n - 1;// 把最后…

Python(SQLite)executescript用法

SQLite 数据库模块的游标对象还包含了一个 executescript() 方法&#xff0c;这不是一个标准的 API 方法&#xff0c;这意味着在其他数据库 API 模块中可能没有这个方法。但是这个方法却很实用&#xff0c;它可以执行一段 SQL 脚本。 例如&#xff0c;如下程序使用 executescr…

Spring Cloud + Vue前后端分离-第16章 项目功能升级

源代码在GitHub - 629y/course: Spring Cloud Vue前后端分离-在线课程 Spring Cloud Vue前后端分离-第16章 项目功能升级 BUG修复与功能优化 16-1 已提交的代码讲解 1.将gateway中的路由配置改为IP&#xff0c;用lb://时&#xff0c;有时候会有延时&#xff0c;需要等一会…

【Qt5小项目】接金币小游戏

代码量在250行左右&#xff0c; 需要源码的可以私信我。

ElementUI Data:Table 表格

ElementUI安装与使用指南 Table 表格 点击下载learnelementuispringboot项目源码 效果图 el-table.vue&#xff08;Table表格&#xff09;页面效果图 项目里el-table.vue代码 <script> export default {name: el_table,data() {return {tableData: [{dat…