c ++数据对齐/成员顺序和继承

gen*_*sys 26 c++ inheritance alignment

如果使用继承/多重继承,数据成员如何对齐/排序?这个编译器是否具体?

有没有办法在派生类中指定如何对成员(包括基类中的成员)进行排序/对齐?

谢谢!

Bea*_*anz 64

你真的在这里问了很多不同的问题,所以我会尽力按顺序回答每个问题.

首先,您想知道数据成员的对齐方式.成员对齐是编译器定义的,但由于CPU处理错位数据的方式,它们都倾向于遵循相同的方式

结构应该基于最严格的成员(通常但不总是最大的内在类型)对齐,并且结构总是对齐,使得数组的元素全部对齐.

例如:

struct some_object
{
    char c;
    double d;
    int i;
};
Run Code Online (Sandbox Code Playgroud)

这个结构将是24个字节.因为该类包含一个double,所以它将是8字节对齐,这意味着char将被填充7个字节,并且int将被填充4以确保在some_object的数组中,所有元素将是8字节对齐的.一般来说,这是依赖于编译器的,尽管您会发现对于给定的处理器体系结构,大多数编译器将数据对齐相同.

你提到的第二件事是派生类成员.派生类的排序和对齐有点痛苦.类分别遵循我上面描述的结构规则,但是当你开始谈论继承时,你会陷入混乱的草皮.鉴于以下课程:

class base
{
    int i;
};

class derived : public base // same for private inheritance
{
    int k;
};

class derived2 : public derived
{
    int l;
};

class derived3 : public derived, public derived2
{
    int m;
};

class derived4 : public virtual base
{
    int n;
};

class derived5 : public virtual base
{
    int o;
};

class derived6 : public derived4, public derived5
{
    int p;
};
Run Code Online (Sandbox Code Playgroud)

base的内存布局是:

int i // base
Run Code Online (Sandbox Code Playgroud)

派生的内存布局是:

int i // base
int k // derived
Run Code Online (Sandbox Code Playgroud)

derived2的内存布局是:

int i // base
int k // derived
int l // derived2
Run Code Online (Sandbox Code Playgroud)

derived3的内存布局是:

int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3
Run Code Online (Sandbox Code Playgroud)

你可能会注意到base和derived在这里出现了两次.这是多重继承的奇迹.

为了解决这个问题,我们有了虚拟继承.

derived4的内存布局是:

base* base_ptr // ptr to base object
int n // derived4
int i // base
Run Code Online (Sandbox Code Playgroud)

derived5的内存布局是:

base* base_ptr // ptr to base object
int o // derived5
int i // base
Run Code Online (Sandbox Code Playgroud)

derived6的内存布局是:

base* base_ptr // ptr to base object
int n // derived4
int o // derived5
int i // base
Run Code Online (Sandbox Code Playgroud)

您将注意到派生的4,5和6都有一个指向基础对象的指针.这是必要的,因此当调用任何base函数时,它有一个传递给这些函数的对象.此结构依赖于编译器,因为它未在语言规范中指定,但几乎所有编译器都实现相同的结构.

当你开始讨论虚函数时,事情会变得更复杂,但同样,大多数编译器也会同样实现它们.参加以下课程:

class vbase
{
    virtual void foo() {};
};

class vbase2
{
    virtual void bar() {};
};

class vderived : public vbase
{
    virtual void bar() {};
    virtual void bar2() {};
};

class vderived2 : public vbase, public vbase2
{
};
Run Code Online (Sandbox Code Playgroud)

这些类中的每一个都包含至少一个虚函数.

vbase的内存布局是:

void* vfptr // vbase
Run Code Online (Sandbox Code Playgroud)

vbase2的内存布局是:

void* vfptr // vbase2
Run Code Online (Sandbox Code Playgroud)

vderived的内存布局是:

void* vfptr // vderived
Run Code Online (Sandbox Code Playgroud)

vderived2的内存布局是:

void* vfptr // vbase
void* vfptr // vbase2
Run Code Online (Sandbox Code Playgroud)

人们对vftables的工作方式有很多不了解的事情.要理解的第一件事是类只存储指向vftables的指针,而不是整个vftables.

这意味着无论一个类有多少虚函数,它都只有一个vftable,除非它通过多重继承从其他地方继承vftable.几乎所有编译器都将vftable指针放在类的其余成员之前.这意味着你可能在vftable指针和类的成员之间有一些填充.

我还可以告诉你,几乎所有编译器都实现了pragma pack功能,允许你手动强制结构对齐.一般来说,除非你真的知道自己在做什么,否则你不想这样做,但它确实在那里,有时它是坏的.

你问的最后一件事是你是否可以控制订购.你总是控制订购.编译器将始终按照您编写的顺序对事物进行排序.我希望这个冗长的解释能够满足您需要知道的所有内容.