我最近遇到了一个看起来像这样的同事代码:
typedef struct A {
int x;
}A;
typedef struct B {
A a;
int d;
}B;
void fn(){
B *b;
((A*)b)->x = 10;
}
他的解释是,由于struct A
是struct B
的第一个成员,因此b->x
与b->a.x
相同,并提供更好的可读性。
这是有道理的,但这被认为是好的做法吗?这会跨平台工作吗?目前,这在GCC上运行良好。
答案 0 :(得分:64)
是的,它可以跨平台工作,但必然使它成为一个好主意。
根据ISO C标准(以下所有引用均来自C11)6.7.2.1 Structure and union specifiers /15
,不允许在结构的第一个元素之前填充
此外,6.2.7 Compatible type and composite type
声明:
如果类型相同,则两种类型具有兼容类型
无可争议的是,A
和A-within-B
类型完全相同。
这意味着对A
和A
类型的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)
也许您可以考虑使用宏来实现此功能,需要将函数或字段重用到宏中。