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类型)

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

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

相关文章

CSP-202305-2-矩阵运算

CSP-202305-2-矩阵运算&#xff1a;题目链接 知识点一&#xff1a;申请矩阵 1.动态分配 // 申请 int** dynamicArray new int*[rows]; for (int i 0; i < rows; i) {dynamicArray[i] new int[cols]; }// 释放 for (int i 0; i < rows; i) {delete[] dynamicArray[…

网络协议梳理

1 引言 在计算机网络中要做到有条不紊地交换数据&#xff0c;就必须遵守一些事先约定好的规则。这些规则明确规定了所交换的数据的格式以及有关的同步问题。这里所说的同步不是狭义的&#xff08;即同频或同频同相&#xff09;而是广义的&#xff0c;即在一定的条件下应当发生什…

gorm day1

gorm day1 gorm简介gorm声明模型 代码样例基本来自官方文档 Gorm简介 什么是ORM&#xff1f; 对象关系映射(Objection Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库(如mysql数据库&#xff09;存在的互不匹配现象的计数。简单来说&#xff0c;ORM是通…

面试经典150题——文本左右对齐(困难)

​"It always seems impossible until it’s done." - Nelson Mandela 1. 题目描述&#xff1a; 这个题目标为困难题目&#xff0c;但是如果我们静下心来把题目读懂了&#xff0c;其实无非就是不同情况下不同考虑而已&#xff0c;也没什么思维上的复杂&#xff0c;还…

深入理解Linux内核之IO多路复用上

目录 Linux代码结构看网络通信 Linux下的IO复用编程 文件描述符FD select poll epoll select、poll、epoll的比较 1、支持一个进程所能打开的最大连接数 2、FD剧增后带来的IO效率问题 3、消息传递方式 总结 Linux代码结构看网络通信 Linux内核的源码包含的东西很多&…

NAS系统折腾记 – Emby搭建家庭多媒体服务器

Emby简介 Emby是一款优秀的媒体服务器软件&#xff0c;致力于为用户提供丰富的多媒体体验。通过Emby&#xff0c;您可以方便地在家庭内的各种设备上观看您喜爱的电影、电视剧和其他视频内容。而且&#xff0c;Emby还具备强大的媒体管理功能&#xff0c;让您的影视资源井然有序…

小米平板6获取root权限教程

1. 绑定账号 1> 打开"设置-我的设备-全部参数-连续点击MIUI版本按钮"&#xff0c;直到提示已打开开发者模式( p s : 这里需要重点关注红框平板型号和 M I U I 版本&#xff0c;例如我这里平板型号是 X i a o m i P a d 6 &#xff0c; M I U I 版本是 14.0.10 &am…

C/C++ 回调函数 callback 异步编程

一、C语言的回调函数 1.小试牛刀 #include <iostream> using namespace std; #include <memory> #include <stdlib.h>int add(int a, int b) {return a b; }void test01() {// 函数指针可以指向任何类型的函数&#xff0c;只要函数的参数列表和返回值类型…

政安晨:示例演绎Python的列表

列表和你可以用它们做的事&#xff1a;包括索引、切片和对象变动 (变异-Mutation) 。 列表 在Python中&#xff0c;列表表示有序的值序列。以下是如何创建列表的示例&#xff1a; primes [2, 3, 5, 7] 我们可以将其他类型的元素放在列表中&#xff1a; planets [Mercury…

股票交易

这里尝试利用单调队列优化&#xff0c;这里不好直接用单调队列的原因是因为(以买为例)\(-ap[i]*k_1\)不是只与下标有关的 所以解决方案一&#xff1a;我们将下标变成一个整体&#xff0c;再把后面的代价换掉然后将与下标无关的直接提出去 解决方案二&#xff1a;利用“蚯蚓”那…

深入了解 Ansible:全面掌握自动化 IT 环境的利器

本文以详尽的篇幅介绍了 Ansible 的方方面面&#xff0c;旨在帮助读者从入门到精通。无论您是初学者还是有一定经验的 Ansible 用户&#xff0c;都可以在本文中找到对应的内容&#xff0c;加深对 Ansible 的理解和应用。愿本文能成为您在 Ansible 自动化旅程中的良师益友&#…

自学网安-IIS服务器

部署环境&#xff1a;win2003 配置环境&#xff1a;winxp ip&#xff1a;10.1.1.2 win2003 ip&#xff1a;10.1.1.1 开始安装 双击“应用程序服务器” 双击“Internet 信息服务&#xff08;IIS&#xff09;” 勾选万维网服务&#xff0c;确定然后下一步 查看端口号;netstat …