关于C语言中1元素数组的typedef

12

有时候,在C语言中,你会这样做:

typedef struct foo {
   unsigned int some_data;
} foo; /* btw, foo_t is discouraged */

为了 OO 式地使用这个新类型,您可以像这样拥有分配/释放对:

foo *foo_alloc(/* various "constructor" params */);
void foo_free(foo *bar);

或者,另外一种方式是使用init/clear对(可能会返回错误代码):

int foo_init(foo *bar, /* and various "constructor" params */);
int foo_clear(foo *bar);

我曾经看到过以下惯用语,特别是在MPFR库中使用:

struct foo {
   unsigned int some_data;
};
typedef struct foo foo[1]; /* <- notice, 1-element array */
typedef struct foo *foo_ptr; /* let's create a ptr-type */

现在,alloc/free和init/clear对应的代码如下:

foo_ptr foo_alloc(/* various "constructor" params */);
void foo_free(foo_ptr bar);
int foo_init(foo_ptr bar, /* and various "constructor" params */);
int foo_clear(foo_ptr bar);

现在你可以像这样使用它 (例如,init/clear 对):

int main()
{  
   foo bar; /* constructed but NOT initialized yet */
   foo_init(bar); /* initialize bar object, alloc stuff on heap, etc. */
   /* use bar */
   foo_clear(bar); /* clear bar object, free stuff on heap, etc. */
}

备注: init/clear对似乎可以提供一种更通用的初始化和清除对象的方式。与alloc/free对相比,init/clear对要求“浅层”对象已经被构建。使用init进行“深度”构建。

问题: 1元素数组“类型惯用语”的任何非明显缺陷吗?

2个回答

15

这非常聪明(但见下文)。

它鼓励了一个误导性的想法,即C函数参数可以通过引用传递。

如果我在C程序中看到这个:

foo bar;
foo_init(bar);

我知道调用 foo_init 不会改变 bar 的值。我也知道当代码将其未初始化的值传递给函数时,很可能会导致未定义的行为。

除非 我碰巧知道 foo 是数组类型的 typedef。然后我突然意识到 foo_init(bar) 不是传递 bar,而是其第一个元素的地址。现在每次我看到引用类型 foo foo 类型的对象的代码时,都必须考虑如何定义 foo 作为单元素数组的 typedef 才能理解代码。

这是让 C 看起来像它不是的一种尝试,有点像:

#define BEGIN {
#define END }

等等类似的语法并不会使代码更易于理解,因为它使用了C并不直接支持的特性。它会导致代码变得更难理解(特别是熟悉C的读者),因为你必须理解定制的声明 支撑整个过程的C底层语义。

如果你想传递指针,请直接传递指针,并显式地这样做。例如,看一下在<stdio.h>中定义的各种标准函数中对FILE*的使用。没有试图在宏或typedef后面隐藏指针,C程序员已经使用该接口数十年了。

如果你想编写看起来像是通过引用传递参数的代码,请定义一些类似函数的宏,并给它们全大写的名称,这样知识渊博的读者就会知道发生了一些奇怪的事情。

我上面说过这是“聪明的”。这让我想起我学习C语言时所做的一些事情:

#define EVER ;;

这使我可以写出一个无限循环,如下所示:

for (EVER) {
    /* ... */
}

当时,我认为那很聪明。

我仍然认为那很聪明。只是我不再认为这是好事了。


1
有趣的是,这个问题源于你提到的同样的挫败感。也就是说,作为 Foo 对象类型的用户,你可能需要理解底层的 typedef 定义。我曾经尝试使用 MPFR 库,因为这个原因而感到困惑。但我忘记了这一点,只是因为我花了很长时间才明白它 :-) 在那个库中,这可能更有意义,因为你需要在堆栈上轻松创建和初始化小对象(数字)。但更花哨(聪明)的东西会变得棘手。 - Ole Thomsen Buus
2
不错,Keith!阅读你的回答真是一种愉悦。SO需要更多像这样有风格、有品味、并且包含有趣个人回忆等额外内容的答案,这些内容能够给读者带来超出简单回答的体验。 - Joseph Myers

3
这种方法的唯一优点是代码更加美观,输入也更容易。它允许用户在堆栈上创建结构体,而无需进行动态分配,如下所示:
foo bar;

然而,即使不需要用户每次都将其转换为指针类型,该结构仍然可以传递给需要指针类型的函数。
foo_init(bar);

没有1元素的数组,要么需要像您提到的那样使用alloc函数或常量&用法。
foo_init(&bar);

我能想到的唯一缺陷是与直接堆栈分配相关的常规问题。如果这是一个被其他代码使用的库,那么将来对结构体的更新可能会破坏客户端代码,而使用alloc free pair则不会出现这种情况。


1
没错 - 这也是我的看法。它使得符号表示更简单了。 - Ole Thomsen Buus
1
而且这样做会降低可读性,破坏我们内部的解析器(大脑)。请不要这样做 - user529758
@H2CO3 我同意,即使需要多打一些字,坚持使用标准的语言语法和用法通常是最好的选择。 - Justin Meiners
1
但是 typedef struct Foo Foo; 似乎相当被接受?我们可以将其简化为从类型名称中删除 "struct" 这个词。其他使用 typedef 隐藏特定语言功能(例如使用数组)的情况可能应该被避免。 - Ole Thomsen Buus

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