在C语言中扩展结构体

62

最近我看到了一位同事的代码,长成这个样子:

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个回答

67

是的,它可以在不同平台上工作(a),但这并不一定意味着这是一个好主意。

根据ISO C标准(以下引用均来自C11),6.7.2.1 结构和联合说明符/15,结构体的第一个元素之前不允许有填充。

此外,6.2.7 兼容类型和复合类型指出:

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

而且 AB 类型是完全相同的事实是无可争议的。

这意味着对于 AB 类型访问 A 字段的内存访问将是相同的,更明智的做法可能是使用 b->a.x,如果您担心将来的可维护性,这可能是您应该使用的。

虽然您通常需要担心严格的类型别名,但我认为在这里不适用。虽然引用指针被认为是非法的,但标准有特定的例外。

6.5 表达式/7 列出了其中一些例外情况,并附注:

此列表的目的是指定对象可能或不可能别名的那些情况。

列出的例外情况包括:

  • 与对象的有效类型兼容的类型;
  • 其他一些例外情况,这里不需要关心;
  • 包含前述类型之一的聚合体或联合体类型(包括子聚合体或包含联合体的成员)

结合上面提到的结构填充规则,包括该短语:

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

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

这是我对标准相关部分的理解,我以前也犯过错误(b),但在这种情况下我不太可能会犯错。

因此,如果您真正需要这样做,它将可以正常工作,但我会在代码中非常接近结构文档中记录任何约束,以免在将来受到伤害。


(a) 在一般意义上。当然,代码片段:

B *b;
((A*)b)->x = 10;

由于b未初始化为合理值,这将是未定义行为。但我假设这只是示例代码,旨在说明你的问题。如果有人对此表示担忧,请考虑将其改为:

B b, *pb = &b;
((A*)pb)->x = 10;

(b)正如我的妻子经常且很少需要提示地告诉你的那样 :-)


35

我会冒险反对@paxdiablo:我认为这是一个很好的想法,在大型、生产质量的代码中非常普遍。

它基本上是在C语言中实现基于继承的面向对象数据结构的最明显和最好的方式。以struct A的一个实例开始声明struct B意味着“B是A的子类”。第一个结构成员从结构的起始位置保证为0字节,这使得它能够安全地工作,并且在我看来边缘美。

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

当然,它要求您“知道自己在做什么”,但通常实现复杂类型关系时,在C语言中都是这样。:)

在GObject和GTK+的情况下,有很多支持基础设施和文档来帮助处理这个问题:很难忘记它。这可能意味着创建新类不像在C++中那样快速,但这也许是可以预期的,因为C语言中没有本地支持类。


1
它不仅仅是边缘美丽,更多的是边缘不可预测的行为。 "第一个结构成员从结构的开始保证为0字节..." 可能会因编译器实现而改变。这是C++标准吗?即使它是标准的一部分,对于不熟悉代码库的人来说,他们需要先弄清楚类型,这可能会导致维护上的麻烦! - Atmaram Shetye

12

绕过类型检查的任何操作通常应该避免。这个技巧依赖于声明的顺序,而不是强制转换或者编译器本身的顺序。

这应该可以跨平台使用,但我认为这不是一个好的做法。

如果你确实有深层嵌套的结构(尽管你可能要思考为什么),那么你应该使用一个临时的本地变量来访问这些字段:

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)类似于“动态转换”)。


指针别名在C语言中是完全合法的,但它会阻止一些优化。你认为restrict(http://gcc.gnu.org/onlinedocs/gcc/Restricted-Pointers.html)关键字的作用是什么? - Patrick Collins
好的,那么我误解了标准的这一部分。为了保持真实性,我已经从我的答案中移除了此部分。不说比说错更好。 - dureuill
1
这不是黑客行为;它明确被标准允许,因为这是实现各种东西的好方法(传统的C ADT就是一个例子)。结构体中转换的行为和“声明顺序”在标准中都有明确定义。 - al45tair

12

那是个可怕的想法。一旦有人在 struct B 的前面插入另一个字段,你的程序就会崩溃。而 b.a.x 有什么问题呢?


7
抱歉,我不同意其他答案,但这个系统不符合标准C。同时指向相同位置的两个具有不同类型的指针是不可接受的,这被称为别名问题,并且在C99和许多其他标准中都不允许。一种更好的方法是使用内联getter函数,然后它们不必以那种方式看起来不整洁。或者也许这是union的工作?它特别允许持有多种类型之一,但在那里还有无数其他缺点。

简而言之,这种脏强制转换以创建多态性在大多数C标准中是不允许的,仅因为它在您的编译器上似乎有效并不意味着它是可以接受的。请参见此处的说明,了解为什么不允许它以及为什么高优化级别的编译器可能会破坏不遵循这些规则的代码http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization


1
实际上,在所描述的用法中,这并不是别名,并且它明确符合C99 6.7.2.1p13规定:“一个适当转换后的指向结构体对象的指针指向其初始成员,反之亦然”。 - Greg A. Woods

5

3

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

Thanks to these simple conditions, it is possible to detect the type of every object instance by doing:

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

or, more quickly:

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

个人认为,联合体往往会导致庞大的switch语句,这是编写面向对象代码时要避免的重要部分,因此它们看起来不太美观。我自己也以这种风格编写了大量代码 --- 通常情况下,结构体的第一个成员包含函数指针,可以像虚表一样工作,用于处理相关类型。


2

我能看出这个方法是可行的,但我不认为这是一个好的做法。这取决于每个数据结构在内存中的字节顺序。任何时候,当你将一个复杂的数据结构(如结构体)强制转换成另一个数据结构时,这并不是一个很好的想法,特别是当这两个结构的大小不相同时。


1

我认为楼主和许多评论者都抓住了这个代码扩展结构体的想法。

但实际上并不是这样。

这是组合的一个例子。非常有用。(去掉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 sockets库(http://beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html)都采用了这种方法。在内核和sockets的情况下,你有一个结构体被运行通过通用和不同的代码段。这与继承的使用场景并没有太大区别。

我不建议只为了可读性而编写像那样的结构体。


“扩展结构体”实际上是一种继承方式(在 C 语言的语法范围内)。在你的例子中,“order” 类型的对象也可以作为“person”类型的对象来处理,以访问其成员字段,并且你的fn()函数应该(或者说应当)重新编写为取一个struct person * - Greg A. Woods

0

我认为Postgres在他们的一些代码中也这样做。虽然这并不意味着这是一个好主意,但它确实说明了它似乎被广泛接受。


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