C++ 标准内存布局

C++ 标准布局 (Standard-Layout) 对象,简单来说就是指可以使用 C 函数 memcpy 把一个T 类型的对象 a 拷贝为对象 b, 而 b 仍然是一个合法的 T 类型。
标准布局主要是为了 C++ 对象和普通的 C 语言的内存布局是兼容的,这样 C++ 和 C 直接就可以进行交互。很明显,如何这个对象使用了 C++ 独有的一些特性,比如虚函数、虚基类等,这个布局 C 语言就无法识别,这就不是一个标准布局。
下面我们通过几个例子来理解什么是与 C 语言的内存布局兼容, 从 C++11 开始,可以使用 std::is_standard_layout 来判断是否是标准布局。


实例一:

class Layout1
{
    int x;
    char y;
};

虽然这是 C++里面的 class, C 语言里面没有 class,但是它和如下的 struct 定义具有相同的内存布局:

struct Layout1
{
    int x;
    char y;
};

所以它是 C 兼容的,class Layout1 是一个标准布局。同理,当所有的 class 成员变量具有相同的 public/private/protected 访问权限时,也是一个标准布局:

class Layout2
{
public:
    int x;
    char y;
};

class Layout3
{
public:
    int x;

public:
    char y;
};

class Layout4
{
protected:
    int x;

protected:
    char y;
};

虽然有多个 public: / protected: / private: 区域定义,但是他们等价于只有一个 public: / protected: / private: 定义,并按照声明出现的顺序排序。所以,上述 Layout2、Layout3、Layout4 也都是标准布局。


实例二:

如果 class 中的成员变量的访问权限不一致,如下所示,有 public: ,有 protected: ,那么这个布局将不能与 C 语言兼容,不是一个标准布局。

class Layout5
{
public:
    int x;

protected:
    char y;
};

因为有不同的访问权限,C++在对成员变量进行内存布局时可能先把所有的 public:变量进行布局,再把所有的 protected:变量进行布局;也有可能先把所有的 protected: 进行布局,再把所有的 public: 变量进行布局。
具体按照那种顺序进行布局,这是C++特有的性质,更有可能是C++编译器决定的,不同的C++编译器的内存布局可能不一致。所以,C语言自然无法与这种内存布局兼容,故 Layout5 不是一个标准布局。
类似的情况,还有下面这种布局:

class Layout6
{
public:
    int x;

protected:
    char y;

public:
    int z;
};

所有的 public: 变量有 x 和 z, 所以他们会合并在一起进行布局。虽然 Layout6 的变量声明顺序是 x, y, z;但是其内存布局有可能为 xz,y 或者 y,xz。这些特性都是C++特有的,C语言无法兼容。


实例三:

下面我们看看在 class 继承时的布局:

class Layout2
{
public:
    int x;
    char y;
};

class Layout7 : public Layout2
{
public:
    int z;
};

我们知道 Layout2 是一个标准布局,但是 Layout7 不是一个标准布局。这里我们要根据内存对齐的概念理解为什么这不是一个标准布局。
这里我们假设按照4字节进行内存对齐,在基类Layout2中的最后需要填充3字节的Padding,这样才能做到4字节内存对齐。然后,才能继续继续子类的布局。也就是说在 y 和 z 之间填充了3字节的 padding,这对于C语言来说是无法兼容的,因为C语言根本不知道 class 的继承结构。
在 C 语言中也需要进行内存对齐,比如:

struct Layout8
{
    int x;
    char y;
    int z;
};

Layout8 和 Layout7 的区别在于内存 padding 的位置不同,Layout8 是在 z 后面填充,而 Layout7 是在先在基类后面进行填充,再判读是否需要在整类的后面进行填充。
可能你会问,是不是只要基类不需要内存padding了,它就是一个标准内存布局呢?比如下面的 Layout9 :

class Layout2
{
public:
    int x;
    int y;
};

class Layout9 : public Layout2
{
public:
    int z;
};

不是这样的。实际上内存对齐是可以通过编译命令设置的,甚至可以不同的对象按照不同的内存对齐方式。所以,只要子类中有成员变量的定义,那么这就不是一个标准布局。但是,下面的 Layout10 是一个标准布局,因为子类没有任何成员变量。

class Layout10 : public Layout2
{
};

实例四:

只要类中有虚函数、虚基类等“虚”的东西,那么它都不是一个标准布局,因为虚函数、虚基类在C++类布局中会添加一个虚指针,这个虚指针的内存布局位置不同的C++编译器都有可能不同。自然,C 语言无法兼容这种带虚指针的内存布局。

class Layout11
{
public:
    int x;
    char y;
    virtual void foo() {

    }
};

实例五:

由于所有的 static 成员变量是布局在 class 全局的内存中的,不在对象内存布局中,所以下面的 Layout12 是一个标准布局。

class Layout12
{
public:
    static int x;
    char y;
private:
    static int z;
};

总结

如果只是看C++标准布局的定义,你会觉着很难理解,也很难记忆。我们从与C语言是否兼容的角度来分析其内存布局,就可以比较容易的判定是否是一个标准布局。标准布局的主要目的在于同C语言交互和共享。