C语言C语言&嵌入式IT狗工作室

第10篇:C++继承中虚表的内存布局

2019-11-16  本文已影响0人  铁甲万能狗

我们已经表明,非虚类的对象实例不包含虚指针,编译器在编译阶段也没有为非虚类没有构建虚表.而本篇我们会从简单的单继承链分析虚类中虚表构造过程和内存布局。这一切假定你有如下基础

本文是从编译器的角度结合GDB调试器来理解虚表的创建过程,而不是像绝大部分份文章逼格高一般抛一大堆和虚成员函数相关的理论,而是从更务实从内存分析的角度讨论为什么在使用虚函数过程中需要虚指针和虚表。

明确虚函数的目的

我们要明确在类继承中使用虚函数的目的。在开发需求中,我们旨在让调用层代码保留相同的公共接口,因为调用层代码不需要关心被调用层的功能实现细节。那么虚函数就是**让不同的派生类将继承自父类的同一个虚成员函数(接口)的根据派生类的功能需求进行不同行为的实现,以此达到不同的派生类提供调用层的决策代码同一个函数接口的不同实现版本,从而保持对调用层代码逻辑无需变动,而且隐藏了同一个函数接口的不同版本的实现细节。

示例导入

#include <iostream>
class Employee{
public:
    bool iService=true;
    virtual ~Employee(){};
    virtual void add_salary(){
       std::cout<<"add_salary method in Employee"<<std::endl;
    }
};

class Teamer:public Employee{
public:
    int idNo=1000;
    virtual ~Teamer(){}
    void add_salary(){
         std::cout<<"add_salary method in Teamer"<<std::endl;
    }

    virtual void info(){
       std::cout<<"Teamer info for Teamer"<<std::endl;
    }

    void show(){
       std::cout<<"show method in Teamer"<<std::endl;
    }
};
int main(void){
    Employee *tm1=new Teamer();
    Employee *tm2=new Teamer();
    Employee *pp1=new Employee();
    Employee *pp2=new Employee();
    delete tm1,tm2,pp1,pp2;
}

在这里我们可以尝试打印*tm1,*tm2,*pp1和 *pp2,如下图所示

图1

从上图的输出中,我们要引入一个虚指针(_vptr)的概念

另外我们还打印出所有Teamer对象和Employee对象,他们获得内存分配都为16个字节。因此我们不妨在查看我们刚才实例化的所有对象。

查看对象的内存数据

现在我们不妨看看刚才实例化的各个对象的内存布局,使用x命令,因为每个对象的堆内存块尺寸都为16个字节,因此我们使用x/16xb将他们的内存数据转存到屏幕中,如下图所示。

备注:这里我们回顾了内存对齐的相关知识。

探究虚表的内存布局

我们从前文打印的第一个Teamer对象 tm1的信息中,可以知道其_vptr指针指向0x400cf0,你是否发现“<虚表 for Teamer+16>”的字样。这个其实表明0x400cf0是已经+16字节偏移后的地址值

我们已经在前文提到在首个的新的虚类对象且初始化时,编译器会该类动态创建一个虚表,但为什么每个不同虚类的虚表都要额外偏移16个字节呢? 在本示例中,我们不妨减去这个偏移量,也即得到0x400ce0这个地址,然后使用x命令,该命令将300字节的内存数据转储到屏幕。

(gdb) x/300xb 0x400ce0

上面的命令以十六进制格式打印300字节,从0x400d00开始。 为什么要这个地址? 因为在上面我们看到类Teamer的虚表指针指向0x400d10,该地址已经偏移0x10个字节,即减去0x10就能得到原本虚表的地址。

下图中_ZTV是虚表的前缀,_ZTS是type-string(名称)的前缀,_ZTI是type-info的前缀。

我们从下图可以得到很多虚表的内存细节。

我们可以从上图中绿色部分的内存数据中即每行冒号之后的8字节空间提取有用的数据,例如

我们这两个内存区域的数据分别整理成如下表,注意写本文时使用的是CentOS 7的x64小端机器,因此读取图中的内存数据时,是从右向左读取,因此整理下表每个内存位置对应的值,并且分别是有info symbol命令 再次查看每个内存位置的值对应的具体含义。

结合整理如下表可知:虚表中的地址值分别代表虚拟类中对应虚函数的地址

虚表内存布局

更简单获取虚类的虚表条目的另外一条命令就是info vtbl,这里就不展示了,我们看到上图的虚表中的虚解构函数都成对地出现,我们先暂不讨论为什么会这样,因为我日后会令起一文再阐述该问题。

虚表构建细节

我们仍然使用上文的调用示例代码

int main(void){
    //
    Employee *tm1=new Teamer();
    Employee *tm2=new Teamer();
    Employee *pp1=new Employee();
    Employee *pp2=new Employee();
    delete tm1,tm2,pp1,pp2;
}

从上面的示例代码中我们已经知道


多态:

理解完虚表的内存布局和构建细节之后,这个时候才合适抛出一些理论性的东西,多态是面相对象语言一个重要的特性,多态即让同一个用户自定义类型的对象在不同的决策时机呈现不同的行为实现
C++中的多态就分为

小结

我们在本篇的最后引入了C++多态的概念,我们会在后续的文章会详细阐述运行时多态的实现技术,而虚函数是C++实现运行时多态的基础。而实现运行时动态调度函数的驱动载体是虚指针虚表,因此本篇着重介绍包含虚成员函数的类创建虚表的细节和内存布局。

上一篇 下一篇

猜你喜欢

热点阅读