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

问题描述 投票:26回答:9

如果使用继承/多重继承,数据成员如何对齐/排序?这个编译器是特定的吗?

是否可以在派生类中指定如何对成员(包括基类中的成员)进行排序/对齐?

谢谢!

c++ inheritance alignment
9个回答
64
投票

您真的在这里问了很多不同的问题,所以我将尽我所能依次回答每个问题。

首先,您想知道数据成员如何对齐。成员对齐方式是由编译器定义的,但是由于CPU处理未对齐数据的方式,它们都趋于遵循相同的

结构应基于限制性最强的成员(通常但并非总是最大的内在类型)进行对齐的准则,并且结构始终对齐以使数组的元素都对齐相同。

例如:

struct some_object
{
    char c;
    double d;
    int i;
};

此结构为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;
};

基本的内存布局为:

int i // base

派生的内存布局为:

int i // base
int k // derived

派生2的内存布局为:

int i // base
int k // derived
int l // derived2

派生3的内存布局为:

int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3

您可能会注意到,base和衍生物在这里都出现两次。这就是多重​​继承的奇迹。

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

派生4的内存布局为:

void* base_ptr // implementation defined ptr that allows to find base
int n // derived4
int i // base

派生5的内存布局为:

void* base_ptr // implementation defined ptr that allows to find base
int o // derived5
int i // base

派生6的内存布局为:

void* base_ptr // implementation defined ptr that allows to find base
int n // derived4
void* base_ptr2 // implementation defined ptr that allows to find base
int o // derived5
int i // base

您将注意到派生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
{
};

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

vbase的内存布局为:

void* vfptr // vbase

vbase2的内存布局为:

void* vfptr // vbase2

vderived的内存布局为:

void* vfptr // vderived

vderived2的内存布局为:

void* vfptr // vbase
void* vfptr // vbase2

人们对vftable的工作原理有很多不了解的地方。首先要了解的是,类仅存储指向vftable的指针,而不存储整个vftable的指针。

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

我还可以告诉您,几乎所有编译器都实现了编译指示包功能,这些功能使您可以手动强制进行结构对齐。通常,除非您真的知道自己在做什么,否则您就不想这样做,但它确实存在,有时是必要的。

您问的最后一件事是您是否可以控制订购。您始终可以控制订购。编译器将始终按照您编写它们的顺序对它们进行排序。我希望这个冗长的说明能触及您需要知道的所有内容。


3
投票

这不仅是特定于编译器的-可能会受到编译器选项的影响。我不知道有任何编译器可以使您对如何通过多重继承打包和排序成员和库进行精细控制。

如果您要执行依赖顺序和包装的操作,请尝试在类中存储POD结构并使用它。


1
投票

是特定于编译器的。

编辑:从根本上讲,它取决于虚拟表的放置位置,根据使用的编译器,它可以有所不同。


1
投票

只要您的课程不是POD(普通旧数据),所有投注将关闭。您可能会使用特定于编译器的指令来打包/对齐数据。


1
投票

编译器通常按结构对齐数据成员,以方便访问。这意味着数据元素通常将从单词边界开始,并且其间隙通常会留在结构中,以确保不跨越单词边界。

so

结构foo{字符int b;字符c;}

对于32位计算机,通常将占用6个字节以上

通常首先布局基类,然后在基类之后布局派生类。这允许基类的地址等于派生类的地址。

在多重继承中,类的地址与第二个基类的地址之间存在偏移量。 >static_castdynamic_cast将计算偏移量。 reinterpret_cast否。如果可能,C样式强制转换会执行静态强制转换,否则将进行重新解释强制转换。

正如其他人所提到的,所有这些都是编译器特定的,但以上内容应为您提供通常发生情况的粗略指南。


1
投票

多重继承中对象的顺序并不总是您指定的。根据我的经验,除非可以,否则编译器将使用指定的顺序。当第一个基类没有虚函数而另一个基类具有虚函数时,它不能使用指定的顺序。在这种情况下,该类的第一个字节必须是虚拟函数表指针,但第一个基类没有一个。编译器将重新排列基类,以便第一个具有虚拟函数表指针。

我已经使用msdev和g ++进行了测试,并且它们都重新排列了类。令人讨厌的是,他们似乎对如何执行有不同的规则。如果您有3个或更多的基类,而第一个没有虚函数,则这些编译器将提供不同的布局。

为了安全起见,请选择两个并避免另一个。

  1. [使用多重继承时不依赖基类的顺序。

  2. 使用多重继承时,将所有具有虚拟功能的基类放在任何没有虚拟功能的基类之前。

  3. 使用2个或更少的基类(因为在这种情况下,编译器都以相同的方式重新排列)


0
投票

我知道的所有编译器都将基类对象放在派生类对象中的数据成员之前。数据成员按类声明中给出的顺序排列。由于对齐可能存在间隙。我并不是说必须要这样。


0
投票

我可以回答其中一个问题。

如果使用继承/多重继承,数据成员如何对齐/排序?

我创建了一个工具来可视化类的内存布局,函数的堆栈框架和其他ABI信息(Linux,GCC)。您可以查看MySQL ++库here中mysqlpp :: Connection类的结果(继承自OptionalExceptions。)>

enter image description here


0
投票

[成员在内存中的顺序等于在程序中指定它们的顺序。非虚拟基类的元素在派生类的元素之前。在多重继承的情况下,第一个(最左侧)类的元素排在第一位(依此类推)。虚拟基类排在最后。

© www.soinside.com 2019 - 2024. All rights reserved.