c++虚函数内存模型(面试入坑之后的通透)

Posted 头号理想

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++虚函数内存模型(面试入坑之后的通透)相关的知识,希望对你有一定的参考价值。

昨天腾讯复试面试官对我一顿爆锤 其中就有c++对象模型
然后我决定研究一下这方面的知识 同时也记录下来 分享给大家

开始

在开始之前我先介绍一些常量和函数的知识

对于一个只包含非静态成员变量和普通成员变量的函数的类

class T 
    void fun() ;
    void fun_1() ;
    int var;
;

就像上边这样的


成员函数放在代码区,为该类的所有实例化出来的 对象所共有
每次新建一个对象都会新建一块内存区来存放var的值,调用成员函数的时候,
程序根据类型找到相应的代码区中所对应的函数进行调用

那么虚函数是怎么实现的呢?


如果一个类中含有虚函数,并且他创建了一个对象,这个对象所占内存一定比没有虚函数之前实例化出来的时候大4
(在后边的例子会测试)
多出来的4就是实现虚函数的关键,虚函数表指针
它指向虚函数表,而虚函数表中存放的都是指针,指向虚函数fun_b()的地址 然后我们就可以实现调用

注意:

普通函数,虚函数,虚函数表都是同一个类中所有对象所共有的,只有成员变量和虚函数表指针是每个对象所私有的
当一个类中有多个虚函数的时候,虚函数表仍然只有一个,虚函数表指针也只有一个,
不同的是虚函数表中会多一个指针变量,指向虚函数实现的区域的地址

通过对象内存中的虚函数表指针找到虚函数表,通过虚函数表中找到对应虚函数的实现区域进行调用

构造函数和析构函数可以为虚函数吗?

构造函数不能为虚函数,析构函数最好设置成为虚函数!
原因:
我们知道虚函数的实现就是通过对象内存中的虚函数表指针实现的
但是构造函数是用来实例化一个对象的,所以在这个时候,虚函数表指针是没有值的 所以构造函数不能成为虚函数

析构函数如果不是虚函数,那我们只会调用基类的析构函数,造成内存泄漏的问题

当我们继承之后 虚表会怎么变化呢?

我们要知道每个类都会对应一个虚函数表 如果在派生类中我们重写虚函数
那么这个虚函数表就会发生改变,而基类的虚函数表不会收到影响

在一个对象中 虚函数指针一般处于对象的开头

为了在查找虚函数表的时候更加迅速

最最简单的类 只有成员变量
首先我们简单写一个类 然后使用c++内置方法 查看内存中的相对位置
除了要在运行时确定虚函数地址外,还需要提供运行时的类型信息RTTI
为了避免虚函数表长度对RTTI的影响 所以RTTI位于虚函数表的开始位置

class Base1

public:
    int base1_1;
    int base1_2;
;

int main() 
    cout <<"sizeof(Base1) :"<< sizeof(Base1) << endl;
    cout <<"offsetof(Base1, base1_1) :"<< offsetof(Base1, base1_1) << endl;
    cout << "offsetof(Base1, base1_2) :"<<offsetof(Base1, base1_2) << endl;

![在这里插入图片描述](https://img-blog.csdnimg.cn/20210326094406508.png

可以看到 成员对象是按顺序来保存的,最先声明的在上边 然后一次保存
类的大小就是成员变量大小之和 8=4+4

类中添加简单成员函数


class Base1

public:
    int base1_1;
    int base1_2;

    void foo() ;
;

int main() 
    cout <<"sizeof(Base1) :"<< sizeof(Base1) << endl;
    cout <<"offsetof(Base1, base1_1) :"<< offsetof(Base1, base1_1) << endl;
    cout << "offsetof(Base1, base1_2) :"<<offsetof(Base1, base1_2) << endl;


为啥这次结果和上次是一样的呢?
因为如果一个函数知识普通的成员函数,那么它就不会发生动态绑定,也不会对布局产生任何影响
当我们调用一个非虚函数的时候,调用的一定是当前指针类型拥有的成员函数,在编译时期就确定下来

插入一个虚函数


class Base1

public:
    int base1_1;
    int base1_2;

    void foo() ;

    virtual void base1_fun1() ;
;

int main() 
    cout <<"sizeof(Base1) :"<< sizeof(Base1) << endl;
    cout <<"offsetof(Base1, base1_1) :"<< offsetof(Base1, base1_1) << endl;
    cout << "offsetof(Base1, base1_2) :"<<offsetof(Base1, base1_2) << endl;


可以看到上边莫名其妙少了4个字节的内存
这就是我们常说的虚函数表指针,类型为void** 说明他指向一个函数指针数组(之后会说明)

在上边的基础上再加一个虚函数


class Base1

public:
    int base1_1;
    int base1_2;

    void foo() ;

    virtual void base1_fun1() ;
    virtual void base1_fun2() ;
;

int main() 
    cout <<"sizeof(Base1) :"<< sizeof(Base1) << endl;
    cout <<"offsetof(Base1, base1_1) :"<< offsetof(Base1, base1_1) << endl;
    cout << "offsetof(Base1, base1_2) :"<<offsetof(Base1, base1_2) << endl;


为什么没发生改变呢?
这就更肯定了刚刚我们的结论,如果再加一个虚函数,仅仅是给虚函数表中加一项
并不会影响到类中对象的大小的布局情况

假如说我们实例化出来两个对象 他俩的地址一定是不同的 但是他俩的虚函数指针指向的地方一定是相同的(同一个虚函数表)

通过上边的测试 我们可以得出一个结论
同一个类的不同实例公用一个虚函数表,他们都通过虚函数表指针指向虚函数表

那么问题来了 虚函数表保存在哪里呢?
他是在编译时期为我们创建好的 只存在一份
定义对象的时候,编译器自动将类对象的指针指向这个虚函数表

继承中的内存布局


class Base1

public:
    int base1_1;
    int base1_2;

    void foo() ;

    virtual void base1_fun1() ;
    virtual void base1_fun2() ;
    
;

class Derive1 : public Base1

public:
    int derive1_1;
    int derive1_2;
;

int main() 
    cout << "sizeof(Base1) :" << sizeof(Base1) << endl;
    cout << "offsetof(Base1, base1_1) :" << offsetof(Base1, base1_1) << endl;
    cout << "offsetof(Base1, base1_2) :" << offsetof(Base1, base1_2) << endl;

    cout << "sizeof(Derive1) :" << sizeof(Derive1) << endl;
    cout << "offsetof(Derive1, derive1_1) :" << offsetof(Derive1, derive1_1) << endl;
    cout << "offsetof(Derive1, derive1_2) :" << offsetof(Derive1, derive1_2) << endl;


我们首先可以看到基类在上边,派生类在下边
然后我们细看内存分布
其实虚函数指针只有基类的一个

这篇文章暂时就到这里 经过这次学习我对虚函数模型有了新的认识
所以大家还是学知识的时候如果可以就往深学习一些
希望我所写的对大家有帮助~

以上是关于c++虚函数内存模型(面试入坑之后的通透)的主要内容,如果未能解决你的问题,请参考以下文章

lldb 入坑指北 - 打印 c++ 实例的虚函数表

C++对象内存模型2 (虚函数,虚指针,虚函数表)

c++ 之 内存模型:虚函数篇

一文读懂C++虚函数的内存模型

C++面试必备

C++ 无虚函数的单继承内存模型