C++ 类的内存分布

2022/09/14

关于C++中成员函数,成员变量,虚函数是如何在内存中分布的

1. 一个空的类

class Test{
    
};

一个空的类里面什么都没有,按道理应该不占有内存空间,但是由于C++标准规定,每一个对象都具有一个唯一的地址,所以它的空间为 1 Byte

2. 函数与虚函数

如果一个类含有几个成员函数,那么它的内存占用会是什么样的呢?比如下面这个例子

class Test{
public:
    void print(){}
    void func(){}
};

通过 GBD 调试发现内存占用还是 1 Byte。所以类中的函数并不存储在类中,而是其他的地方。是的,类中的函数与普通函数一样,都存储在 Code(.text) 段,也就是中文中的代码区。

内存分布

在调用类函数的时候就像调用普通函数一样,不过可以假象类函数都有一个 this 指针参数,比如下面这个类函数

class Test{
    int a = 0;
    int b = 0;
public:
    int sum(){
        return a + b;
    }
};

这里的 sum 函数可以写作

int sum(Test* this){
    return this->a + this->b;
}

因为类的函数是存储在代码区的,而不是类里面,所以这部分是可以公用的,也就是说当一个类继承另外一个类,因为它具有另外一个的数据所以这部分代码是可以复用的,不必占用多余的内存空间。之前做过一个非常有意思的题目,判断下面代码是否可以运行

#include <iostream>

using namespace std;

class Test{
public:
    Test() = default;
    ~Test(){
        cout << "~test" << endl;
    }
    void print1(){
        cout << 1 << endl;
    }
    void print2(){
        cout << 2 << endl;
    }
};

int main(){
    {
        Test *t = nullptr;
        t->print1();
        t->print2();

    }
    cout << "ok" << endl;

    return 0;
}

这里直接把t设置为空指针,然后用空指针去调用类函数,第一感觉是肯定会出错,但是实际上是可以编译并运行的。这就就相当于把 this 设置为空指针,但是在类函数中并没有用到空指针,所以程序可以运行,但是如果在类函数中用到了其他的类变量则程序将会报错。

2.1 虚函数与虚函数表

问题又来了,如果说基类说继承的函数代码是共有的,在进行动态绑定的时候通过指针调用父类的成员函数,就一定存在一个唯一的结果,如何来实现多态呢?为此 C++ 标准引入了虚函数与虚函数表,如果类中定义了虚函数那么每个类就会在代码区创建一个属于自己的虚函数表,并且在类中具有一个虚函数指针用来指向所对应的虚函数表,这个指针的大小与普通的指针大小是一样的,比如下面这段代码

class Test{
public:
    virtual void func(){}
};

它的大小不再是 1 Byte 了,而是 8 Bytes。在进行实例化的时候实例化对象就有这样一个虚函数指针,在调用虚函数的时候就用这个虚函数指针来寻找对应的虚函数,从而实现多态。

3. 成员变量

如果一个类中存在几个成员变量呢,它的大小会发生变化吗?比如下面这个例子

class Test{
    int a = 0;
public:
    void print(){}
    void func(){}
};

现在在里面添加了一个 int 类型的成员变量,通过调试发现它的大小变成了 4 Byte,也就是一个 int 类型的大小,所以成员变量是存储在类中的。那如果这个变量是静态变量呢?这个不用试都可以猜出来,因为静态变量也不会存储在类中,而是存储在 Uninitialized data(.bss) 区,同样不会占用类的内存空间。

4. 继承

首先看一段代码

#include <iostream>

using namespace std;

class Base{
    int a = 3;
public:
    void print(){
        cout << a << endl;
    }
};

class Derive:public Base{

};

int main(){
    Derive d = Derive();


    return 0;
}

这里 DeriveBase 采用的是公开继承,那 Base 的私有成员变量 a 是否成功继承过来了呢?要回答这个问题可以直接用 GDB 来调试一下,看看 Derive 的大小是多少,调试结果为 sizeof(Derive) = 4。所以在继承的时候无论是采用的什么形式的继承,都是全部继承,也就是私有类型与共有类型都继承过来了。

5. 总结

  1. 空类的大小为 1 Byte
  2. 类中的成员函数与普通函数一样都存放在代码段(.code)
  3. 类中的成员变量存放在类中
  4. C++ 是通过虚函数指针与虚函数表来实现多态的,虚函数指针存放在类中
  5. 无论是私有继承还是公开继承都是继承全部数据