C++的静态绑定和动态绑定、虚函数表的理解

news/2024/6/24 8:05:36/文章来源:https://blog.csdn.net/KK_2018/article/details/137271866

C++的静态绑定和动态绑定、虚函数表的理解

概念

在C++中,静态绑定(Static Binding)和动态绑定(Dynamic Binding)是两种不同的函数调用机制,主要涉及到如何根据对象的类型来选择执行哪个函数的决定过程。这两种机制在多态性的实现中扮演着关键角色。动态绑定是面向对象编程中实现多态性的关键机制,允许代码在更高的抽象层次上运行,提高了代码的复用性和扩展性。而静态绑定则因其高效性,在不需要多态性的情况下,依然是一个很好的选择。
下面我将通过代码来详细说明这个内容。

静态绑定
静态绑定,也称为早期绑定,指的是函数调用在编译时期就已经确定了。在这种情况下,调用哪个函数是根据指针或引用的 静态类型(即编码时指定的类型,而不是运行时的实际类型) 来决定的。静态绑定适用于非虚函数(non-virtual functions)。

  • 优点:效率高,因为函数调用的决策在编译时就已经完成了,不需要在运行时进行查找。
  • 缺点:缺乏灵活性,不能根据对象的实际类型来调用对应的函数,不利于实现多态性。

动态绑定

动态绑定,也称为晚期绑定,是指函数调用在运行时确定。这通常是通过虚函数(virtual functions)机制来实现的。当一个函数在基类中被声明为虚函数后,派生类可以重写(Override)这个函数,创建自己的实现。如果通过基类的指针或引用调用这个函数,那么实际调用的版本 将根据对象的实际类型(运行时的类型) 来确定。

  • 优点:提供了灵活性,允许多态性的实现。可以根据对象的实际类型调用相应的函数,即使是在基类的指针或引用上调用。
  • 缺点:效率相对较低,因为需要在运行时进行类型检查和函数查找。

注意点

什么是绑定

"绑定"指的是函数调用与函数实现之间的关联,在C++中,这种绑定可以是静态的(编译时决定)或动态的(运行时决定)。

  • 静态绑定发生在一个函数调用与一个函数实现之间。这里,函数的调用是基于调用对象的静态类型(即声明时的类型)来决定的。也就是说,编译器在编译时就决定了会调用哪个函数;
  • 动态绑定发生在一个虚函数调用与多个可能的函数实现之间。在这种情况下,函数的调用是基于调用对象的实际类型(即运行时对象的类型)来决定的。也就是说,运行时系统(而不是编译器)在运行时决定了会调用哪个函数。

静态绑定和动态绑定如何判断

不加virtual:静态绑定

当一个成员函数没有被声明为virtual时,其绑定方式是静态的。这意味着函数调用会在编译时解析,基于对象的静态类型(即代码中声明的类型)。这种方式适用于大多数普通函数调用,它能提供更高的执行效率,因为调用的函数在编译时就已经确定,无需在运行时进行额外的查找。

如果你调用一个非虚函数,C++编译器会根据调用该函数的对象的类型(更准确地说,是指针或引用的类型)来确定应该调用哪个函数。

静态绑定适用于当你不需要多态性,即在编译时就能确定调用哪个函数的场景。

加virtual:动态绑定

当在一个类的成员函数前加上virtual关键字时,你是在告诉编译器:“请在运行时决定调用哪个函数”。这允许所谓的“后期绑定”或“动态绑定”,意味着如果有任何派生类重写了该虚函数,那么通过基类指针或引用调用该函数时,将调用对象实际类型的函数实现,而不是其静态类型的函数实现。(下面会介绍其中的原理——虚函数表)
动态绑定是实现多态性的关键,特别是当你想通过基类的接口来调用派生类的实现时。

关键点

一旦在基类中声明了一个函数为virtual,所有派生类中的同名函数都自动成为虚函数,即使在派生类中没有显式地使用virtual关键字。

构造函数不能是虚函数。静态成员函数也不能是虚函数,因为静态成员函数不依赖于类的任何特定实例。

代码举例

#include <iostream>class A
{
public:A();A(int a):m_a(a){};void printValue(){std::cout<<"A: "<<m_a<<std::endl;}private:int m_a;
};class B:public A
{
public:B(int a,int b):A(a),m_b(b){};void printValue(){std::cout<<"B: "<<m_b<<std::endl;}
private:int m_b;
};int main()
{A* objA=nullptr;B* objB=new B(10,20);objA=objB;objA->printValue(); // 输出A: 10return 0;
}

上面这段代码中,objA是指向A类型的指针,也就是它的静态类型是A*,虽然在下面有领objA指向了objB,但是printValue函数在A类中没有被声明为virtual,因此调用该函数的时候使用的静态绑定,也就是编译器会根据调用该函数的对象的静态类型(即A*)来决定调用A的printValue函数,而不是运行时实际指向的对象类型(B)调用B的printValue函数。

因此,最终调用的是A的printValue函数。

如何解决呢?定义为虚函数即可

#include <iostream>
class A
{
public:A();A(int a):m_a(a){};virtual void printValue(){std::cout<<"A: "<<m_a<<std::endl;}private:int m_a;
};class B:public A
{
public:B(int a,int b):A(a),m_b(b){};void printValue() override{std::cout<<"B: "<<m_b<<std::endl;}
private:int m_b;
};int main()
{A* objA=nullptr;B* objB=new B(10,20);objA=objB;objA->printValue(); // 输出B: 20return 0;
}

这段代码中,我将printValue函数定义为了虚函数,因此编译器会在运行时决定调用哪个printValue。由于运行的时候objA实际指向的对象类型是B,因此便会调用B的printValue函数。

原理就是下面的虚函数表!

虚函数表

首先要明确 “虚函数表” 的概念:每一个具有虚函数的类都有一个自己的虚函数表,这个表是编译器生成的“数组”,里面保存的是指向类中虚函数的指针(地址);派生类的虚函数表和基类的虚函数表都是单独存在的,并且内容默认与基类一样,但是当发生如下两种情况的时候,派生类的虚函数表会变化:

  1. 当派生类重写了基类的虚函数时;
  2. 当派生类中新增了虚函数时。
    因此,上面的代码中,类A的虚函数表是这样的
    在这里插入图片描述

由于在类B中重写了虚函数,因此类B的虚函数表是这样的
在这里插入图片描述

不管是基类还是派生类,每个类的对象的成员都会有一个指向自家虚函数表的指针,也就是虚指针,虚指针通常是对象内存布局的第一个成员(为了确保不管类的成员如何变化,虚指针的位置不变),一个对象调用虚函数的时候,首先会访问它的虚指针,然后根据虚指针指向的位置找到虚函数表,然后在虚函数表中找到要调用的函数的入口地址,然后根据此地址去调用对应的函数实现。
所以,对于类A,加上虚指针的图就是这样的(类B的图把其中的A和a换成B、b即可)。
在这里插入图片描述
因此,上面的代码执行时,过程是这样的
发现有virtual关键字,为动态绑定——运行的时候发现objA指向的对象是objB——访问objB的虚指针找到虚函数表——在虚函数表中找到printValue函数的入口地址——跳转到入口地址执行printValue函数

总结

从原理上看,虚函数只是多了一次寻址,但是实际应用中使用虚函数会比普通函数开销大很多,原因主要是:

  • 虚函数的使用场景往往是运行时,即编译时无法确定,这意味着调用虚函数的路径中会有很多编译器无法优化的跳转和分支;
  • 虚表多了一层寻址,这种间接寻址更容易导致缓存不命中,需要频繁访问内存,大大降低了访问效率;
  • 虚函数往往无法进行内联优化,而普通成员函数可以以内联的方式提高程序运行效率。

根据以上描述,非必要的情况我们不使用虚函数,但是反过来,只要是有用到虚函数的场景,比如运行时确定、必要的多态,那就毫不犹豫地使用虚函数,这套机制远比大多数自己实现的运行时多态要好得多。

也就是,虚函数该用就用,没必要用就不用!

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

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

相关文章

电商技术揭秘五:电商平台的个性化营销与数据分析

文章目录 引言1. 个性化营销的概念与价值1.1 个性化营销的定义1.1.1 个性化营销的基本概念1.1.2 个性化营销在电商领域的重要性 1.2 个性化营销的核心价值1.2.1 提升用户体验1.2.2 增加转化率和客户忠诚度1.2.3 优化营销资源配置 2. 用户画像与行为分析2.1 用户画像的构建2.1.1…

本地项目上传到GitHub

本文档因使用实际项目提交做为案例&#xff0c;故使用xxx等字符进行脱敏&#xff0c;同时隐藏了部分输出&#xff0c;已实际项目和命令行输出为准 0、 Git 安装与GitHub注册 1&#xff09; 在下述地址下载Git&#xff0c;安装一路默认下一步即可。安装完成后&#xff0c;随便…

道本科技智慧合规助力企业转型升级

在当今这个快速变化的商业世界里&#xff0c;企业合规管理已经从一项基本的监管要求转变为推动企业持续发展的关键动力。合规不仅是避免法律麻烦的盾牌&#xff0c;它还充当着引领企业向更高效、更可靠和更可持续方向发展的催化剂。而在实现这一目标的过程中&#xff0c;智慧合…

Linux中如何修改界面为中文, 设置中文输入法

目录 修改界面为中文方法一方法二方法三(kali中)方法四方法五(kali中) 切换为中文/英文输入法方法一方法二(kali中) 待续、更新中 修改界面为中文 方法一 查看当前系统拥有的中文语言包 locale -a | grep CN zh_CN.utf8 : 简体中文语言包 打开文件locale.conf vi /etc/lo…

完整部署一套k8s-v.1.28.0版本的集群

一、系统情况 虚拟机版本&#xff1a;esxi 6.7 系统版本&#xff1a;centos7.9_2009_x86 配置&#xff1a;4核8G&#xff08;官网最低要求2核2G&#xff09; 192.168.0.137 master节点 192.168.0.139 node2节点 192.168.0.138 node1节点&#xff08;节点扩容练习&#xf…

5.C++:string常用函数及模拟实现

一、resize() void test7() {string s("hello world");s.resize(5); //改变长度s.resize(20,x); //改变空间大小 } int main() {test7();return 0; } 二、string常用函数接口 1.打印 2.、push_back、append 3.迭代器&#xff0c;读写&#xff1b;s.begin()、s.…

Linux虚拟机环境搭建spark

Linux环境搭建Spark分为两个版本&#xff0c;分别是Scala版本和Python版本。 一、 安装Pyspark 本环境以 Python 环境为例。 1、下载spark 下载网址&#xff1a;https://archive.apache.org/dist/spark 下载安装包&#xff1a;根据自己环境选择合适版本&#xff0c;本环境…

爱上数据结构:栈和队列的概念及使用

​ ​ &#x1f525;个人主页&#xff1a;guoguoqiang. &#x1f525;专栏&#xff1a;数据结构 ​ 一、栈 1.栈的基本概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶&#xff0c;…

数据结构——栈(C语言版)

前言&#xff1a; 在学习完数据结构顺序表和链表之后&#xff0c;其实我们就可以做很多事情了&#xff0c;后面的栈和队列&#xff0c;其实就是对前面的顺序表和链表的灵活运用&#xff0c;今天我们就来学习一下栈的原理和应用。 准备工作&#xff1a;本人习惯将文件放在test.c…

Python基础之pandas:文件读取与数据处理

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、文件读取1.以pd.read_csv()为例&#xff1a;2.数据查看 二、数据离散化、排序1.pd.cut()离散化&#xff0c;以按范围加标签为例2. pd.qcut()实现离散化3.排序4.…

AI复活:商业新风口还是情感禁区?

随着人工智能技术的飞速发展&#xff0c;AI已经渗透到我们生活的方方面面&#xff0c;其中&#xff0c;“AI复活”服务作为新兴的技术应用&#xff0c;正逐渐走进大众视野。然而&#xff0c;这一技术带来的不仅是商业机会&#xff0c;更伴随着伦理和情感的争议。 “AI复活”服务…

深度解析C语言——预处理详解

对C语言有一定了解的同学&#xff0c;相信对预处理一定不会陌生。今天我们就来聊一聊一些预处理的相关知识。预处理是在编译之前对源文件进行简单加工的过程&#xff0c;主要是处理以#开头的命令&#xff0c;例如#include <stdio.h>、#define等。预处理是C语言的一个重要…

Element Plus:图标

第一步&#xff1a;npm install element-plus/icons-vue 第二步&#xff1a;在main.js里 import * as ElementPlusIconsVue from element-plus/icons-vueconst app createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key,…

HarmonyOS 应用开发之通过键值型数据库实现数据持久化

场景介绍 键值型数据库存储键值对形式的数据&#xff0c;当需要存储的数据没有复杂的关系模型&#xff0c;比如存储商品名称及对应价格、员工工号及今日是否已出勤等&#xff0c;由于数据复杂度低&#xff0c;更容易兼容不同数据库版本和设备类型&#xff0c;因此推荐使用键值…

ubuntu22.04@Jetson Orin Nano安装配置VNC服务端

ubuntu22.04Jetson Orin Nano安装&配置VNC服务端 1. 源由2. 环境3. VNC安装Step 1: update and install xserver-xorg-video-dummyStep 2: Create config for dummy virtual displayStep3: Add the following contents in xorg.conf.dummyStep 4: Update /etc/X11/xorg.con…

【详解】运算放大器工作原理及其在信号处理中的核心作用

什么是运算放大器 运算放大器&#xff08;简称“运放”&#xff09;是一种放大倍数非常高的电路单元。在实际电路中&#xff0c;它常常与反馈网络一起组成一定的功能模块。它是一种带有特殊耦合电路和反馈的放大器。输出信号可以是输入信号的加法、减法、微分和积分等数学运算…

c++对象指针

对象指针在使用之前必须先进行初始化。可以让它指向一个已定义的对象&#xff0c;也可以用new运算符动态建立堆对象。 定义对象指针的格式为&#xff1a; 类名 *对象指针 &对象; //或者 类名 *对象指针 new 类名(参数); 用对象指针访问对象数据成员的格式为&#xff1a…

Django详细教程(二) - 部门用户管理案例

文章目录 前言一、新建项目二、新建app三、设计表结构四、新建数据库五、新建静态文件六、部门管理1.部门展示2.部门添加3.部门删除4.部门编辑 七、模板继承八、用户管理1.辨析三种方法方法一&#xff1a;原始方法方法二&#xff1a;Form组件(简便)方法三&#xff1a;ModelForm…

1.Netty介绍及NIO三大组件

Netty网络编程Netty的底层是NIO&#xff08;非阻塞IO&#xff09;&#xff0c;常用的多线程和线程池使用的是阻塞IO&#xff0c;其效率并不高。支持高并发&#xff0c;性能好高性能的服务端程序、客户端程序 NIO三大组件 一、Channel 读写数据的双向传输通道 常见的传输通道…

【C++第二阶段】继承多态电脑组装实例

你好你好&#xff01; 以下内容仅为当前认识&#xff0c;可能有不足之处&#xff0c;欢迎讨论&#xff01; 文章目录 继承继承语法继承方式继承中的对象模型继承中构造和析构顺序同名成员处理同名静态成员处理多继承语法菱形继承问题 多态多态基本概念重写&重载 多态原理多…