如果使用继承/多重继承,数据成员如何对齐/排序?这个编译器是否具体?
有没有办法在派生类中指定如何对成员(包括基类中的成员)进行排序/对齐?
谢谢!
答案 0 :(得分:61)
你真的在这里问了很多不同的问题,所以我会尽力回答每一个问题。
首先,您想了解数据成员的对齐方式。成员对齐是编译器定义的,但由于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;
};
base的内存布局是:
int i // base
派生的内存布局是:
int i // base
int k // derived
derived2的内存布局为:
int i // base
int k // derived
int l // derived2
derived3的内存布局为:
int i // base
int k // derived
int i // base
int k // derived
int l // derived2
int m // derived3
你可能会注意到base和derived在这里出现了两次。这是多重继承的奇迹。
为了解决这个问题,我们有了虚拟继承。
derived4的内存布局为:
base* base_ptr // ptr to base object
int n // derived4
int i // base
derived5的内存布局为:
base* base_ptr // ptr to base object
int o // derived5
int i // base
derived6的内存布局为:
base* base_ptr // ptr to base object
int n // derived4
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
人们对vftables的工作方式有很多不了解的事情。要理解的第一件事是类只存储指向vftables的指针,而不是整个vftables。
这意味着无论一个类有多少虚函数,它都只有一个vftable,除非它通过多重继承从其他地方继承vftable。几乎所有编译器都将vftable指针放在类的其余成员之前。这意味着你可能在vftable指针和类的成员之间有一些填充。
我还可以告诉您,几乎所有编译器都实现了pragma pack功能,允许您手动强制结构对齐。一般来说,除非你真的知道自己在做什么,否则你不想这样做,但它确实在那里,而且有时它是坏的。
您问的最后一件事是您是否可以控制订购。你总是控制订购。编译器将始终按照您编写的顺序对事物进行排序。我希望这个冗长的解释能够满足您需要知道的所有内容。
答案 1 :(得分:3)
这不仅仅是编译器特定的 - 它可能会受到编译器选项的影响。我不知道有任何编译器能够对成员和库的打包方式以及多重继承进行精细控制。
如果您正在做依赖于订单和打包的事情,请尝试在您的班级中存储POD结构并使用它。
答案 2 :(得分:1)
这是特定于编译器的。
编辑:基本上它归结为虚拟表的放置位置,根据使用的编译器可能会有所不同。
答案 3 :(得分:1)
一旦您的课程不是POD(普通旧数据),所有投注都会被取消。您可以使用特定于编译器的指令来打包/对齐数据。
答案 4 :(得分:1)
编译器通常将结构中的数据成员对齐以便于访问。这意味着数据元素通常从字边界开始,它的间隙通常留在结构中,以确保字边界不会跨越。
所以
struct foo { char a; int b; char c; }
32位机器通常会占用超过6个字节
基类通常首先布局,并在基类之后布置派生类。这允许基类的地址等于派生类的地址。
在多重继承中,类的地址与第二个基类的地址之间存在偏移量。 >static_cast
和dynamic_cast
将计算偏移量。 reinterpret_cast
没有。如果可能的话,C样式转换会进行静态转换,否则会重新解释。
正如其他人所提到的,所有这些都是编译器特定的,但上面的内容应该为您提供正常情况的粗略指导。
答案 5 :(得分:1)
多重继承中对象的顺序并不总是您指定的顺序。根据我的经验,编译器将使用指定的顺序,除非它不能。当第一个基类没有虚函数而另一个基类具有虚函数时,它不能使用指定的顺序。在这种情况下,类的第一个字节必须是虚函数表指针,但第一个基类没有。编译器将重新排列基类,以便第一个具有虚函数表指针。
我用msdev和g ++对它进行了测试,并且它们都重新安排了类。令人讨厌的是,他们似乎对他们如何做到这一点有不同的规则。如果你有3个或更多基类,而第一个没有虚函数,那么这些编译器会提供不同的布局。
为了安全起见,选择两个并避开另一个。
使用多重继承时,不要依赖基类的排序。
使用多重继承时,在没有虚函数的任何基类之前放置所有带有虚函数的基类。
使用2个或更少的基类(因为编译器在这种情况下都以相同的方式重新排列)
答案 6 :(得分:0)
我知道的所有编译器都将基类对象放在派生类对象中的数据成员之前。数据成员按类声明中的顺序排列。由于对齐可能存在间隙。我并不是说它必须是这样的。
答案 7 :(得分:0)
我可以回答其中一个问题。
如果使用继承/多重继承,数据成员如何对齐/排序?
我已经创建了一个工具来可视化类的内存布局,堆栈函数框架和其他ABI信息(Linux,GCC)。您可以从MySQL ++库here查看mysqlpp :: Connection类(继承OptionalExceptions)的结果。
答案 8 :(得分:0)
成员在内存中的顺序等于在程序中指定成员的顺序。非虚拟基类的元素位于派生类的元素之前。在多重继承的情况下,第一个(最左侧)类的元素排在第一位(依此类推)。虚拟基类排在最后。
从虚拟基类派生的每个类/结构都为其元素添加一个指针类型(理论上取决于实现)。
类/结构的对齐方式等于其成员的最大对齐方式(理论上取决于实现)。
当内存中的下一个元素需要它时(出于对齐目的)(理论上取决于实现),就会发生填充。
添加了跟踪填充,以使对象的大小为其对齐方式的倍数。
复杂的例子
struct base1 {
char m_tag;
int m_base1;
base1() : m_tag(0x11), m_base1(0x1b1b1b1b) { }
};
struct derived1 : public base1 {
char m_tag;
alignas(16) int m_derived1;
derived1() : m_tag(0x21), m_derived1(0x1d1d1d1d) { }
};
struct derived2 : virtual public derived1 {
char m_tag;
int m_derived2_a;
int m_derived2_b;
derived2() : m_tag(0x31), m_derived2_a(0x2d2daa2d), m_derived2_b(0x2d2dbb2d) { }
};
struct derived3 : virtual public derived1 {
char m_tag;
int m_derived3;
virtual ~derived3() { }
derived3() : m_tag(0x41), m_derived3(0x3d3d3d3d) { }
};
struct base2 {
char m_tag;
int m_base2;
virtual ~base2() { }
base2() : m_tag(0x51), m_base2(0x2b2b2b2b) { }
};
struct derived4 : public derived2, public base2, public derived3 {
char m_tag;
int m_derived4;
derived4() : m_tag(0x61), m_derived4(0x4d4d4d4d) { }
};
具有以下内存布局:
derived4 = derived2 -> ....P....O....I....N....T....E....R....
subobject derived2 -> 0x31 padd padd padd 0x2d 0xaa 0x2d 0x2d
0x2d 0xbb 0x2d 0x2d padd padd padd padd
virual table = base2 -> ....P....O....I....N....T....E....R....
subobject base2 -> 0x51 padd padd padd 0x2b 0x2b 0x2b 0x2b
derived3 -> ....P....O....I....N....T....E....R....
subobject derived3 -> 0x41 padd padd padd 0x3d 0x3d 0x3d 0x3d
subobject derived4 -> 0x61 padd padd padd 0x4d 0x4d 0x4d 0x4d
derived1 = base1 -> 0x11 padd padd padd 0x1b 0x1b 0x1b 0x1b
subobject derived1 -> 0x21 padd padd padd padd padd padd padd
0x1d 0x1d 0x1d 0x1d padd padd padd padd
padd padd padd padd padd padd padd padd
请注意,将派生4对象转换为派生2或派生3之后,新对象从指向虚拟基类的指针开始,该指针位于派生4图像下方的某个位置,就像真实的派生2或派生3对象一样。
将此派生的4投射到base2可以使我们得到一个具有虚拟表指针的对象,它应该(base2具有虚拟的析构函数)。
元素的顺序为:首先是derived2的(虚拟基类指针和)元素,然后是base的(虚拟表指针和)元素,之后是derived3的(虚拟基类指针和)元素,最后是元素的(子对象)的-的全部后跟虚拟基类(派生的1)。
还请注意,尽管真正的“ derived3”对象必须对齐为16个字节,因为它“包含”(最后)对齐为16的虚拟基类derived1,因为它具有与之对齐的成员16;但此处在多重继承中使用的'derived3'未对齐16个字节。可以,因为没有虚拟基类的派生3具有最大值。仅对齐8(其虚拟基类指针;这是在64位计算机上)。