在C中扩展结构

时间:2014-03-06 08:10:09

标签: c struct

我最近遇到了一个看起来像这样的同事代码:

typedef struct A {
  int x;
}A;

typedef struct B {
  A a;
  int d;
}B;

void fn(){
  B *b;
  ((A*)b)->x = 10;
}

他的解释是,由于struct Astruct B的第一个成员,因此b->xb->a.x相同,并提供更好的可读性。
这是有道理的,但这被认为是好的做法吗?这会跨平台工作吗?目前,这在GCC上运行良好。

11 个答案:

答案 0 :(得分:64)

是的,它可以跨平台工作,但必然使它成为一个好主意。

根据ISO C标准(以下所有引用均来自C11)6.7.2.1 Structure and union specifiers /15,不允许在结构的第一个元素之前填充

此外,6.2.7 Compatible type and composite type声明:

  

如果类型相同,则两种类型具有兼容类型

无可争议的是,AA-within-B类型完全相同。

这意味着对AA类型的B字段的内存访问将是相同的,因为更明智的b->a.x可能是什么如果您对将来的可维护性有任何疑虑,使用。

而且,虽然您通常不得不担心严格的类型别名,但我不认为这适用于此。 非法指定别名,但标准有特定的例外。

6.5 Expressions /7通过脚注说明了一些例外情况:

  

此列表的目的是指定对象可能或可能没有别名的情况。

列出的例外情况是:

  • a type compatible with the effective type of the object;
  • 这里不需要关注的其他一些例外情况;和
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)

结合上面提到的struct padding规则,包括短语:

  

指向适当转换的结构对象的指针指向其初始成员

似乎表明这个例子是特别允许的。我们必须记住的核心要点是表达式((A*)b)的类型是A*,而不是B*。这使得变量兼容以实现无限制别名。

这是我对标准相关部分的解读,我在(a)之前就错了,但在这种情况下我对此表示怀疑。

所以,如果你有正版需要,它可以正常工作,但我会记录代码 very 中的任何约束,以便接近结构不要在将来被咬伤。


(a)正如我的妻子会经常并且没有太多提示那样告诉你: - )

答案 1 :(得分:34)

我会站出来反对@paxdiablo这个问题:我认为这是个好主意,而且在大型的,生产质量的代码中非常普遍。

它基本上是在C中实现基于继承的面向对象数据结构的最明显和最好的方法。使用struct B实例启动struct A的声明意味着“B是子类一个”。事实上,第一个结构成员保证从结构的开始是0字节是使它安全工作的原因,并且在我看来它的边界很漂亮。

它在基于GObject库的代码中广泛使用和部署,例如GTK +用户界面工具包和GNOME桌面环境。

当然,它要求你“知道你在做什么”,但在C中实现复杂的类型关系通常总是如此:)

对于GObject和GTK +,有很多支持基础设施和文档来帮助解决这个问题:很难忘记它。这可能意味着创建一个新类并不像C ++那样快,但这可能是预期的,因为C语言中没有本机支持。

答案 2 :(得分:12)

通常应避免任何绕过类型检查的事情。 这个hack依赖于声明的顺序,编译器也不能执行强制转换或此命令。

它应该跨平台工作,但我认为这不是一个好习惯。

如果你真的有深层嵌套结构(你可能不得不想知道为什么),那么你应该使用一个临时局部变量来访问这些字段:

A deep_a = e->d.c.b.a;
deep_a.x = 10;
deep_a.y = deep_a.x + 72;
e->d.c.b.a = deep_a;

或者,如果您不想复制a

A* deep_a = &(e->d.c.b.a);
deep_a->x = 10;
deep_a->y = deep_a->x + 72;

这显示a来自哪里,并且不需要演员。

Java和C#也经常暴露像“c.b.a”这样的结构,我不知道问题是什么。如果你想要模拟的是面向对象的行为,那么你应该考虑使用面向对象的语言(比如C ++),因为以你提出的方式“扩展结构”不提供封装或运行时多态性(尽管有人可能会争辩) ((A *)b)类似于“动态演员”)。

答案 3 :(得分:11)

这是一个可怕的想法。一旦有人出现并在结构B的前面插入另一个字段,你的程序就会爆炸。 b.a.x会出现什么问题?

答案 4 :(得分:7)

我很抱歉不同意这里的所有其他答案,但是这个系统不符合标准C.不同的是,有两个不同类型的指针同时指向同一个位置,这被称为在C99和许多其他标准中的严格别名规则不允许使用别名。这样做的一个不那么难看的就是使用内嵌的getter函数,这样就不必那么整洁。或者这可能是工会的工作?特别允许持有几种类型中的一种,但是也存在无数其他缺点。

简而言之,大多数C标准都不允许这种创建多态的脏转换,只是因为它似乎对你的编译器起作用并不意味着它是可以接受的。请参阅此处,了解不允许的原因,以及为什么高优化级别的编译器可能会破坏不符合这些规则的代码http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization

答案 5 :(得分:5)

是的,它会起作用。它是Object Oriented using C的核心原则之一。有关扩展(即继承)的更多示例,请参阅此答案“Object-orientation in C”。

答案 6 :(得分:3)

这是完全合法的,而且在我看来,非常优雅。有关生产代码中此示例,请参阅GObject docs

  

由于这些简单的条件,可以检测类型   通过执行每个对象实例:

B *b;
b->parent.parent.g_class->g_type
     

或者,更快:

B *b;
((GTypeInstance*)b)->g_class->g_type

就个人而言,我认为工会是丑陋的,往往导致巨大的switch陈述,这是你通过编写OO代码避免工作的重要部分。我自己用这种风格编写了大量的代码 - 通常,struct的第一个成员包含的函数指针可以像所讨论的类型的vtable一样工作。

答案 7 :(得分:2)

我可以看到它是如何工作的,但我不会称之为好习惯。这取决于每个数据结构的字节如何放在内存中。无论何时将一个复杂的数据结构转换为另一个(即结构),这都不是一个好主意,特别是当两个结构的大小不同时。

答案 8 :(得分:1)

我认为OP和许多评论者已经认识到代码正在扩展结构。

不是。

这是作文的例子。很有用。 (摆脱typedef,这是一个更具描述性的例子):

struct person {
  char name[MAX_STRING + 1];
  char address[MAX_STRING + 1];
}

struct item {
  int x;
};

struct accessory {
  int y;
};

/* fixed size memory buffer.
   The Linux kernel is full of embedded structs like this
*/
struct order {
  struct person customer;
  struct item items[MAX_ITEMS];
  struct accessory accessories[MAX_ACCESSORIES];
};

void fn(struct order *the_order){
  memcpy(the_order->customer.name, DEFAULT_NAME, sizeof(DEFAULT_NAME));
}

你有一个固定大小的缓冲区,很好地划分区域。它肯定胜过一个巨大的单层结构。

struct double_order {
  struct order order;
  struct item extra_items[MAX_ITEMS];
  struct accessory extra_accessories[MAX_ACCESSORIES];

};

所以现在你有了第二个可以被处理的结构(一个继承),就像第一个带有显式转换的结构一样。

struct double_order d;
fn((order *)&d);

这保留了与使用较小结构编写的代码的兼容性。 Linux内核(http://lxr.free-electrons.com/source/include/linux/spi/spi.h(查看struct spi_device))和bsd套接字库(http://beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html)都使用此方法。在内核和套接字的情况下,您有一个结构,通过通用和差异化的代码段运行。与继承的用例并不完全不同。

我不建议为了可读性而写这样的结构。

答案 9 :(得分:0)

我认为Postgres也会在他们的一些代码中做到这一点。并不是说它是一个好主意,但它确实说明了它似乎被广泛接受的东西。

答案 10 :(得分:-1)

也许您可以考虑使用宏来实现此功能,需要将函数或字段重用到宏中。