C/C++结构体中字段的顺序

3

我有一个类似于这种情况的问题

struct Child
{
  u16 x, y;
  // other fields
};

struct Father
{
  struct Child child1;
  struct Child child2;
  // other fields
};

Father tilemap[WIDTH][HEIGHT];

现在我意识到我想为x,y省下四个字节,因为它们总是为同一个父级的两个孩子设置相同的值。
在我的代码中,我传递了许多Father*和Child*,同时使用father->child1->x或child1->x来恢复坐标。我想在Father级别上安全地移动坐标,但我对某些事情不确定。
声明字段的顺序是否会受到gcc/g++的任何优化或可能实现的影响而得到尊重?我能否确信&father == &father.child1?
真正的问题在于,我传递了Child*,却不知道它是child1还是child2字段,因此我无法直接知道恢复父级地址(及其坐标)的偏移量。我想在Child级别上使用一个位来区分它们,但我能轻松地恢复父级的地址吗?
如果有任何建议,将不胜感激,谢谢。
编辑:作为进一步的信息,我使用C++作为主要语言,但这些结构不包含任何奇怪的方法,仅包含字段和空构造函数。

请参考以下链接:https://dev59.com/2XVC5IYBdhLWcg3weBA- - Ian Clelland
6个回答

8
C语言中有关字段布局的一般规则如下:
  1. 第一个成员的地址与结构体本身的地址相同。即,该成员字段的offsetof为0。
  2. 成员的地址始终按声明顺序递增。也就是说,第n个字段的offsetof低于(n + 1)-th成员的偏移量。
当然,在C++中,只有当它是标准布局类型时才成立,即大致上没有public/private/protected混合成员、没有虚函数和没有从其他类继承的成员的类或结构体。

我想提一下标准所保证的内容和值得依赖的内容之间的区别。在这种情况下,我认为依赖于&father==&father.child是不好的做法。 - ugoren
@ugoren - 不良实践是一个相对的术语。仅仅为了好玩而这样做是个坏主意。但在 C 中,经常这样做来实现一种类型继承。在低级编程中,正如 OP 所做的那样,为了速度或简单起见,依赖于这些事实也很普遍,甚至更加不可移植。 - rodrigo
1
你说得对,不良实践是相对的,更具体地说,是替代方案的问题。在大多数情况下,用&father->child1替换(Child *)father会得到更好的代码。反之亦然,如果你知道它是0,即使你知道它是0,也应该使用offsetof。但如果有情况,尽管你尝试了很多次,你仍然不得不依赖于这个,那就没关系。 - ugoren

2

免责声明:部分回答。仅限于C++

在gcc/g++的任何优化或可能的实现中,已声明字段的顺序是否会被保留?

编译器不会篡改内存布局中成员的顺序。它们的顺序与您声明它们的顺序相同。

我可以相信 &father == &father.child1 吗?

在这种特定情况下,是的。但是,仅仅因为child1是father的第一个成员并不意味着 &father == &father.child1?。只有当father是POD时才成立,在这种情况下,它是POD。


拥有一些联合位域总是在整个数据字段之后,会改变这个事实吗? - Jack
@Jack:不会的,Seth 在下面解释了为什么 :) vvv - Armen Tsirunyan
只要Father有一个平凡的构造函数和析构函数并且仅包含POD类型,那么它就是一个POD类型,因此包含一个联合体不会改变它。@Jack - Seth Carnegie
实际上,Father 只需要是一个标准布局类。此外,&father == &father.child1 甚至无法编译,因此需要对一个或多个操作数进行 reinterpret_cast - Mankarse

1
C标准的相关部分如下所述(重点在于我):
在结构对象中,非位域成员和位域所在的单元的地址按照它们声明的顺序递增。指向结构对象的指针,经过适当转换后,指向其初始成员(如果该成员是位域,则指向其所在的单元),反之亦然。结构对象内可能有未命名的填充,但不会出现在其开头。
C++标准也做出了同样的承诺:
指向标准布局结构体对象的指针,经过适当的reinterpret_cast转换,指向其初始成员(如果该成员是位域,则指向其所在的单元),反之亦然。[注意:因此,在标准布局结构体对象内可能存在未命名的填充,但不会出现在其开头,以实现适当的对齐。-注]

所以当你问:

我可以相信 &father == &father.child1 吗?

答案是肯定的。


对于C++,相关的文本位于§9.2/20(联合使用§9.2/19)。 - Mankarse

0

请尝试以下操作

struct Child
{
  int isChild1;
  u16 x, y;
  // other fields
};
...
Father *father_p;
if (*child_p).isChild1
   father_p = child_p;
else
   father_p = child_p - sizeof(struct Child);
(*father_p).x = ... // whatever you want to do with coordinates

你应该小心,不要传递一个没有包含在相应的父对象中的子对象,否则你将在father_p中得到一个虚假地址,可能会破坏你的内存。


0

你有没有考虑过使用类似于 享元模式,而不是依赖于内存布局呢?

在Father中不要存储两个Childs,而是存储两个精简的BasicChilds(不包含x、y数据),并根据需要动态生成完整的Childs:

struct BasicChild
{
    float foo;
    float bar;
    void printPosition(int x, int y) {std::cout << x << "," << y << "\n";}
};

struct Child
{
    Child(BasicChild basicChild, int x, int y)
        : basicChild_(basicChild), x_(x), y_(y){}
    float foo() {return basicChild_.foo;}
    float bar() {return basicChild_.bar;}
    void printPosition() {basicChild_.printPosition(x_, y_);}

private:
    int x_, y_;
    BasicChild basicChild_;
};

struct Father
{
    Child child1() {return Child(child1_, x_, y_);}
    Child child2() {return Child(child2_, x_, y_);}

private:
    int x_, y_;
    BasicChild child1_;
    BasicChild child2_;
};

0

如果您决定依赖于任何编译器特定的行为,那么最好的做法是为您所依赖的任何假设添加静态断言。这样,如果由于编译器升级、编译选项或代码中其他地方的编译指示而导致布局发生任何变化,您将立即在编译时得到通知,并且可以准确地了解问题所在。这将成为移植到其他平台的基础,如果有这个要求的话。

本文中的示例:static_assert是什么,你会用它做什么?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接