静态绑定与动态绑定

讨论静态绑定与动态绑定,首先需要理解的是绑定,何为绑定?函数调用与函数本身的关联,以及成员访问与变量内存地址间的关系,称为绑定。 理解了绑定后再理解静态与动态。

  • 静态绑定:指在程序编译过程中,把函数调用与响应调用所需的代码结合的过程,称为静态绑定。发生在编译期。
  • 动态绑定:指在执行期间判断所引用对象的实际类型,根据实际的类型调用其相应的方法。程序运行过程中,把函数调用与响应调用所需的代码相结合的过程称为动态绑定。发生于运行期。

C++中动态绑定

在C++中动态绑定是通过虚函数实现的,是多态实现的具体形式。而虚函数是通过虚函数表实现的。这个表中记录了虚函数的地址,解决继承、覆盖的问题,保证动态绑定时能够根据对象的实际类型调用正确的函数。这个虚函数表在什么地方呢?C++标准规格说明书中说到,编译器必须要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。也就是说,我们可以通过对象实例的地址得到这张虚函数表,然后可以遍历其中的函数指针,并调用相应的函数。

虚函数的工作原理

要想弄明白动态绑定,就必须弄懂虚函数的工作原理。C++中虚函数的实现一般是通过虚函数表实现的(C++规范中没有规定具体用哪种方法,但大部分的编译器厂商都选择此方法)。类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。 类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。

虚函数(virtual)是通过虚函数表来实现的,在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反映实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存(位于对象实例的最前面),所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要,指明了实际所应调用的函数。它是如何指明的呢?后面会讲到。

JMP指令是汇编语言中的无条件跳转指令,无条件跳转指令可转到内存中任何程序段。转移地址可在指令中给出,也可以在寄存器中给出,或在储存器中指出。

首先我们定义一个带有虚函数的基类

class Base
{
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
} int a;
};

查看其内存布局



我们可以看到在Base类的内存布局上,第一个位置上存放虚函数表指针,接下来才是Base的成员变量。另外,存在着虚函数表,该表里存放着Base类的所有virtual函数。

既然虚函数表指针通常放在对象实例的最前面的位置,那么我们应该可以通过代码来访问虚函数表,通过下面这段代码加深对虚函数表的理解:

#include "stdafx.h"
#include<iostream>
using namespace std; class Base
{
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
} int a;
}; int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
cout<<"虚函数表指针地址:"<<(int*)(&b)<<endl; //对象最前面是指向虚函数表的指针,虚函数表中存放的是虚函数的地址
pFunc pfun;
pfun=(pFunc)*((int*)(*(int*)(&b))); //这里存放的都是地址,所以才一层又一层的指针
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+1);
pfun();
pfun=(pFunc)*((int*)(*(int*)(&b))+2);
pfun(); system("pause");
return 0;
}

运行结果:

通过这个例子,对虚函数表指针,虚函数表这些有了足够的理解。下面再深入一些。C++又是如何利用基类指针和虚函数来实现多态的呢?这里,我们就需要弄明白在继承环境下虚函数表是如何工作的。目前只理解单继承,至于虚继承,多重继承待以后再理解。

单继承代码如下:

class Base
{
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
} int a;
}; class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
};

内存布局对比:





通过对比,我们可以看到:

  • 在单继承中,Child类覆盖了Base类中的同名虚函数,在虚函数表中体现为对应位置被Child类中的新函数替换,而没有被覆盖的函数则没有发生变化。
  • 对于子类自己的虚函数,直接添加到虚函数表后面。

另外,我们注意到,类Child和类Base中都只有一个vfptr指针,前面我们说过,该指针指向虚函数表,我们分别输出类Child和类Base的vfptr:

int _tmain(int argc, _TCHAR* argv[])
{
typedef void(*pFunc)(void);
Base b;
Child c;
cout<<"Base类的虚函数表指针地址:"<<(int*)(&b)<<endl;
cout<<"Child类的虚函数表指针地址:"<<(int*)(&c)<<endl; system("pause");
return 0;
}

运行结果:

可以看到,类Child和类Base分别拥有自己的虚函数表指针vfptr和虚函数表vftable。

下面这段代码,说明了父类和基类拥有不同的虚函数表,同一个类拥有相同的虚函数表,同一个类的不同对象的地址(存放虚函数表指针的地址)不同。

int _tmain(int argc, _TCHAR* argv[])
{
Base b;
Child c1,c2;
cout<<"Base类的虚函数表的地址:"<<(int*)(*(int*)(&b))<<endl;
cout<<"Child类c1的虚函数表的地址:"<<(int*)(*(int*)(&c1))<<endl; //虚函数表指针指向的地址值
cout<<"Child类c2的虚函数表的地址:"<<(int*)(*(int*)(&c2))<<endl; system("pause");
return 0;
}

运行结果:

在定义该派生类对象时,先调用其基类的构造函数,然后再初始化vfptr,最后再调用派生类的构造函数( 从二进制的视野来看,所谓基类子类是一个大结构体,其中this指针开头的四个字节存放虚函数表头指针。执行子类的构造函数的时候,首先调用基类构造函数,this指针作为参数,在基类构造函数中填入基类的vfptr,然后回到子类的构造函数,填入子类的vfptr,覆盖基类填入的vfptr。如此以来完成vfptr的初始化)。也就是说,vfptr指向vftable发生在构造函数期间完成的。

动态绑定例子:

#include "stdafx.h"
#include<iostream>
using namespace std; class Base
{
public:
virtual void fun1(){
cout<<"base fun1!\n";
}
virtual void fun2(){
cout<<"base fun2!\n";
}
virtual void fun3(){
cout<<"base fun3!\n";
} int a;
}; class Child:public Base
{
public:
void fun1(){
cout<<"Child fun1\n";
}
void fun2(){
cout<<"Child fun2\n";
}
virtual void fun4(){
cout<<"Child fun4\n";
}
}; int _tmain(int argc, _TCHAR* argv[])
{
Base* p=new Child;
p->fun1();
p->fun2();
p->fun3(); system("pause");
return 0;
}

运行结果:



结合上面的内存布局:

其实,在new Child时构造了一个子类的对象,子类对象按上面所讲,在构造函数期间完成虚函数表指针vfptr指向Child类的虚函数表,将这个对象的地址赋值给了Base类型的指针p,当调用p->fun1()时,发现是虚函数,调用虚函数指针查找虚函数表中对应虚函数的地址,这里就是&Child::fun1。调用p->fun2()情况相同。调用p->fun3()时,子类并没有重写父类虚函数,但依旧通过调用虚函数指针查找虚函数表,发现对应函数地址是&Base::fun3。所以上面的运行结果如上图所示。

到这里,你是否已经明白为什么指向子类实例的基类指针可以调用子类(虚)函数?每一个实例对象中都存在一个vfptr指针,编译器会先取出vfptr的值,这个值就是虚函数表vftable的地址,再根据这个值来到vftable中调用目标函数。所以,只要vfptr不同,指向的虚函数表vftable就不同,而不同的虚函数表中存放着对应类的虚函数地址,这样就实现了多态的”效果“。

关注微信公众号,推送后端开发、区块链等技术分享!

C++虚函数的工作原理的更多相关文章

  1. C++中虚函数的作用和虚函数的工作原理

    1 C++中虚函数的作用和多态 虚函数: 实现类的多态性 关键字:虚函数:虚函数的作用:多态性:多态公有继承:动态联编 C++中的虚函数的作用主要是实现了多态的机制.基类定义虚函数,子类可以重写该函数 ...

  2. eval函数的工作原理

    如果您想详细了解eval和JSON请参考以下链接: eval  :https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Glob ...

  3. C++ 之虚函数的实现原理

    c++的多态使用虚函数实现,通过“晚绑定”,使程序在运行的时候,根据对象的类型去执行对应的虚函数. C++ 之虚函数的实现原理 带有虚函数的类,编译器会为其额外分配一个虚函数表,里面记录的使虚函数的地 ...

  4. 漫谈 C++ 虚函数 的 实现原理

    文中讲述的原理是推理和探讨 , 和现实中的实现不一定完全相同 . C++ 的 虚函数 , 编译器 会 生成一个 虚函数表 . 虚函数表, 实际上是 编译器 在 内存 中 划定 的 一块 区域, 用于存 ...

  5. C++虚函数实现多态原理(转载)

    一.前言 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态 ...

  6. c++ 继承类强制转换时的虚函数表工作原理

    本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解. #include<iostream> using namespace st ...

  7. C++多态性:虚函数的调用原理

    多态性给我们带来了好处:多态使得我们可以通过基类的引用或指针来指明一个对象(包含其派生类的对象),当调用函数时可以自动判断调用的是哪个对象的函数. 一个函数说明为虚函数,表明在继承的类中重载这个函数时 ...

  8. 《C++反汇编与逆向分析技术揭秘》——函数的工作原理

    各种调用方式的考察 示例: cdecl方式是调用者清空堆栈: 如果执行的是fastcall: 借助两个寄存器传递参数: 参数1和2借助局部变量来存储: 返回值 如果返回值是结构体: 返回值存放在eax ...

  9. 虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte

    #include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout < ...

随机推荐

  1. html、jsp页面标签的遍历

    应用场景:最近的项目中二级子页面遍历生成.操作表格比较多,记录一下一直用的遍历方法. 一般此类表格都是通过ajax请求数据,然后从callbackFunction中获取数据集合,遍历生成表: eg: ...

  2. 【设计模式】行为型01策略模式(strategy patten)

    学设计模式一段时间了,有些懂了,有些半知半解,通过写笔记博客的方式总结一下: 关于策略模式,我的个人理解就是将一些经常变动的算法独立抽取出来,可以是一个方法,也可以是一个策略类,这样,如果有需求变更, ...

  3. 透过字节码分析java基本类型数组的内存分配方式。

    我们知道java中new方式创建的对象都是在堆中创建的,而局部变量对应的值存放在栈上.那么java中的int [] arr={1,2,3}是存放在什么地方的呢,int []arr = new int[ ...

  4. 长春理工大学第十四届程序设计竞赛(重现赛)L

    L.Homework Stream 题目链接:https://ac.nowcoder.com/acm/contest/912/L 题目 作为大珩班尖子生,小r每天有很多作业要完成,例如工图.工图和工图 ...

  5. CSS中属性的详细运用(新手必看)

    =不同的浏览器有不同的默认字体大小font-size 这里以谷歌浏览器为准字体大小为10px   (其他浏览器是12px) 1.这里强调一个备注:属性继承 a 是特殊的,要改变a里面的颜色,必须在它后 ...

  6. Spring Bean 生命周期之“我从哪里来?” 懂得这个很重要

    Spring bean 的生命周期很容易理解.实例化 bean 时,可能需要执行一些初始化以使其进入可用 (Ready for Use)状态.类似地,当不再需要 bean 并将其从容器中移除时,可能需 ...

  7. 如何安装xenserver

    xenserver安装 选择键盘 是否同意协议 清理磁盘 选择本地磁盘安装 选择本地镜像文件 输入管理密码 配置IP地址 配置DNS服务器地址 选择地点 配置NTP服务器地址 开始安装 安装完成

  8. spring boot使用log4j2将日志写入mysql数据库

    log4j2官方例子在spring boot中报错而且还是用的是org.apache.commons.dbcp包 我给改了一下使用org.apache.commons.dbcp2包 1.log4j2. ...

  9. Tell Don’t Ask

    The Tell, Don’t Ask (TDA) principle suggests that it is better to issue an object a command do perfo ...

  10. Java项目案例之---开灯(面向对象复习)

    开灯(面向对象复习) 设计一个台灯类(Lamp)其中台灯有灯泡类(Buble)这个属性,还有开灯(on)这个方法 设计一个灯泡类(Buble),灯泡类有发亮的方法 其中有红灯泡类(RedBuble)和 ...