为什么许多在C中返回结构的函数实际上返回结构的指针?


49

return与在函数的语句中返回整个结构相比,返回指向结构的指针有什么好处?

我说的是像fopen其他底层函数一样的函数,但是可能还有一些高层函数也返回指向结构的指针。

我相信这更多是一种设计选择,而不仅仅是编程问题,而且我很想知道更多关于这两种方法的优缺点。

我认为返回指向结构的指针的一个好处的原因之一是能够通过返回NULL指针更容易地判断函数是否失败。

我想返回一个完整的结构NULL会比较困难,或者效率较低。这是正当的理由吗?


9
@ JohnR.Strohm我试过了,它确实有效。一个函数可以返回一个结构。...那是为什么呢?
yoyo_fun 17-10-19

27
标准化前C不允许复制结构或按值传递结构。C标准库中有许多那个时代的遗留物,今天还不能这样写,例如直到C11 gets()才删除了完全错误设计的功能。一些程序员仍然讨厌复制结构,旧习惯很难改掉。
阿蒙(Amon)

26
FILE*实际上是一个不透明的句柄。用户代码不应该关心其内部结构是什么。
CodesInChaos

3
当您进行垃圾回收时,按引用返回只是一个合理的默认值。
伊丹·阿里

6
@ JohnR.Strohm您的个人资料中的“非常高级”似乎可以追溯到1989年之前;-)-当ANSI C允许K&R C不允许的时候:复制赋值,参数传递和返回值中的结构。K&R的原始书确实明确地指出了这一点(我的意思是:),您可以使用结构恰好完成两件事,使用结构获取地址,& 并使用来访问成员.。”
彼得-恢复莫妮卡的

Answers:


61

有很多实际的原因,为什么函数要fopen返回指针而不是struct类型实例:

  1. 您想向用户隐藏该struct类型的表示形式。
  2. 您正在动态分配对象;
  3. 您通过多个引用引用一个对象的单个实例;

对于类似的类型FILE *,这是因为您不想向用户公开该类型表示的详细信息-一个FILE *对象充当不透明的句柄,您只需将该句柄传递给各种I / O例程(FILE有时为实现struct类型,它不具有如此)。

因此,您可以在某处的标头中显示不完整的 struct类型:

typedef struct __some_internal_stream_implementation FILE;

虽然不能声明不完整类型的实例,但是可以声明指向它的指针。因此,我可以创建一个,FILE *并通过fopenfreopen等等分配给它,但是我不能直接操纵它指向的对象。

fopen函数也可能FILE使用malloc或类似方法动态分配对象。在这种情况下,返回指针是有意义的。

最后,有可能在struct对象中存储某种状态,并且需要在多个不同的位置使该状态可用。如果返回该struct类型的实例,则这些实例将是内存中彼此独立的对象,最终将失去同步。通过返回指向单个对象的指针,每个人都引用同一对象。


31
将指针用作不透明类型的一个特殊优点是,结构本身可以在库版本之间进行更改,而您无需重新编译调用程序。
Barmar

6
@Barmar:的确,ABI稳定性是C 巨大卖点,如果没有不透明的指针,它将不会那么稳定。
Matthieu M.

37

有两种“返回结构”的方法。您可以返回数据的副本,也可以返回对其的引用(指针)。通常出于两个原因,最好返回一个指针(并通常在其周围传递)。

首先,复制结构要比复制指针花费更多的CPU时间。如果这是您的代码经常执行的操作,则可能会导致明显的性能差异。

其次,无论您将指针复制多少次,它仍然指向内存中的同一结构。对它的所有修改将反映在相同的结构上。但是,如果您复制结构本身,然后进行修改,则更改只会显示在该副本上。持有不同副本的任何代码都不会看到更改。有时,很少,这就是您想要的,但是在大多数情况下,这不是您想要的,如果弄错了,它可能会导致错误。


54
通过指针返回的缺点:现在您必须跟踪该对象的所有权并可能释放它。同样,指针间接寻址可能比快速复制的开销更大。这里有很多变量,因此使用指针并不是普遍更好。
阿蒙(Amon)

17
另外,在大多数台式机和服务器平台上,这些天的指针是64位。在我的职业生涯中,我已经看到了不止64种结构的结构。因此,您不能总是说复制指针的成本要比复制结构的成本低。
所罗门慢速片,

37
这通常是一个很好的答案,但是我有时对这部分内容持不同意见,这很少,这是您想要的,但是在大多数情况下并不是这样 -相反。返回指针会导致多种不必要的副作用,以及几种错误的获取指针所有权的讨厌方法。在CPU时间不是很重要的情况下,我更喜欢复制版本,如果这是一个选项,则错误发生的可能性小得多。
布朗

6
应该注意的是,这实际上仅适用于外部API。对于内部函数,过去几十年中甚至每个有能力的编译器都将重写一个函数,该函数返回一个大结构以将指针作为附加参数,并直接在其中构造对象。不可变与可变的论点已经足够多了,但是我认为我们都可以同意,关于不变数据结构几乎永远不会是您想要的东西的说法是不正确的。
Voo

6
您也可以将编译防火墙作为指针的专业版。在具有广泛共享标头的大型程序中,带有函数的不完整类型会阻止每次实现详细信息更改时都需要重新编译。更好的编译行为实际上是封装的副作用,这是在接口和实现分开时实现的。通过值返回(以及传递,分配)需要实现信息。
彼得-恢复莫妮卡的时间

12

除了其他答案外,有时返回少量 struct值也是值得的。例如,一个人可能返回一对数据,以及一些与此有关的错误(或成功)代码。

举一个例子,fopen仅返回一个数据(opened FILE*),如果发生错误,则通过errno伪全局变量给出错误代码。但是最好返回struct两个成员之一:FILE*句柄和错误代码(如果文件句柄为,则将设置错误代码NULL)。由于历史原因,情况并非如此(错误是通过errno全局报告的,今天已成为一个宏)。

注意,Go语言有一个很好的表示法来返回两个(或几个)值。

还要注意,在Linux / x86-64上,ABI和调用约定(请参阅x86-psABI页)指定struct通过两个寄存器返回两个标量成员(例如,一个指针和一个整数,或两个指针或两个整数)中的一个(这非常有效,并且不会通过内存)。

因此,在新的C代码中,返回较小的C struct更具可读性,线程友好性和效率。


实际上,小型结构被打包rdx:rax。因此struct foo { int a,b; };,将其打包打包返回rax(例如,使用shift / or),并且必须使用shift / mov将其打包。这是Godbolt的一个例子。但是x86可以将64位寄存器的低32位用于32位操作而不必关心高位,因此它总是太糟糕了,但是绝对比大多数时候使用2个寄存器处理2成员结构更糟糕。
彼得·科德斯

相关信息:bugs.llvm.org/show_bug.cgi?id=34840 std::optional<int>返回的上半部分的布尔值rax,因此您需要一个64位掩码常量才能对其进行测试test。或者您可以使用bt。但是,与using相比dl,它给调用者和被调用者带来了麻烦。还相关:libstdc ++的std::optional<T>值即使T也不是简单可复制的,因此它总是通过隐藏的指针返回:stackoverflow.com/questions/46544019/…。(可轻松复制libc ++)
Peter Cordes

@PeterCordes:您的相关内容是C ++,而不是C
Basile Starynkevitch

糟糕,对。那么同样的事情也适用完全相同struct { int a; _Bool b; };在C,如果主叫方想考布尔,因为平凡,可复制C ++结构使用相同的ABI为C.
彼得·科德斯

1
经典示例div_t div()
chux-恢复莫妮卡

6

您走在正确的轨道上

您提到的两个原因均成立:

我认为返回指向结构的指针的一个好处的原因之一是,通过返回NULL指针,可以更轻松地判断函数是否失败。

我想,返回一个为NULL的FULL结构会更困难,或者效率更低。这是正当的理由吗?

如果您的内存中有纹理(例如),并且您想在程序中的多个位置引用该纹理;每次您要引用副本时,复制副本都是不明智的。相反,如果仅传递一个指针来引用纹理,则程序将运行得更快。

不过,最大的原因是动态内存分配。通常,在编译程序时,您不确定确切地需要某些数据结构使用多少内存。发生这种情况时,将在运行时确定需要使用的内存量。您可以使用'malloc'请求内存,然后在使用'free'完成后释放内存。

一个很好的例子是从用户指定的文件中读取。在这种情况下,您不知道编译程序时文件的大小。您只能算出程序实际运行时需要多少内存。

malloc和free返回指针都指向内存中的位置。因此,利用动态内存分配的函数将返回指向它们在内存中创建其结构的位置的指针。

另外,在注释中,我看到是否可以从函数返回结构存在疑问。您确实可以做到这一点。以下应该工作:

struct s1 {
   int integer;
};

struct s1 f(struct s1 input){
   struct s1 returnValue = xinput
   return returnValue;
}

int main(void){
   struct s1 a = { 42 };
   struct s1 b= f(a);

   return 0;
}

如果已经定义了结构类型,怎么可能不知道某个变量将需要多少内存?
yoyo_fun

9
@JenniferAnderson C具有不完整类型的概念:类型名称可以声明但尚未定义,因此其大小不可用。我无法声明该类型的变量,但是可以声明指向该类型的指针,例如struct incomplete* foo(void)。这样,我可以在标头中声明函数,但只能在C文件中定义结构,因此可以进行封装。
阿蒙(Amon)

@amon这就是在C中实际上声明函数头(原型/签名)之前如何声明它们的方式?并且有可能对C中的结构和联合做同样的事情
yoyo_fun 17-10-19

@JenniferAnderson可以在头文件中声明函数原型(没有主体的函数),然后可以在不知道函数主体的情况下在其他代码中调用这些函数,因为编译器只需要知道如何排列参数以及如何接受返回值。在链接程序时,实际上您必须知道函数定义(即,带有主体),但是只需要处理一次即可。如果使用非简单类型,则还需要知道该类型的结构,但是指针的大小通常相同,并且与原型的使用无关紧要。
simpleuser

6

FILE*就客户端代码而言,像a的东西实际上并不是指向结构的指针,而是与某些其他实体(如文件)关联的不透明标识符的形式。当程序调用时fopen,它通常不关心返回的结构的任何内容-它所关心的只是其他函数fread将执行它们需要做的任何事情。

如果标准库保留FILE*有关该文件中当前读取位置的信息,则对的调用fread必须能够更新该信息。其fread接收的指针FILE品牌那么容易。如果fread接收到FILE,它将无法更新FILE调用方持有的对象。


3

信息隐藏

与在函数的return语句中返回整个结构相比,返回指向结构的指针有什么好处?

最常见的一种是信息隐藏。举例来说,C没有能力使字段成为struct私有字段,更不用说提供访问它们的方法了。

因此,如果您要强制阻止开发人员查看和篡改pointee的内容(例如)FILE,则唯一的方法是通过将指针视为不透明的指针(指向pointer的大小和大小)来防止其暴露于其定义。定义是外界所不知道的。这样,的定义FILE将仅对那些实现需要其定义的操作(例如)的用户fopen可见,而只有结构声明对公共头来说才可见。

二进制兼容性

隐藏结构定义还可以帮助提供喘息的空间,以保持dylib API中的二进制兼容性。它允许库实现者更改不透明结构中的字段,而不会破坏与使用库的人的二进制兼容性,因为其代码的本质只需要知道他们可以对结构进行什么操作,而无需知道结构的大小或字段的大小它有。

例如,我实际上可以运行一些在Windows 95时代构建的古老程序(并不总是完美的,但令人惊讶的是,许多程序仍然可以运行)。可能是那些古老的二进制文件的某些代码使用了不透明的指针指向其大小和内容从Windows 95时代开始发生变化的结构。但是,由于这些程序没有暴露于这些结构的内容中,因此它们仍可以在新版本的Windows中继续工作。当在二进制兼容性很重要的库上工作时,通常允许更改客户端不暴露的内容而不会破坏向后兼容性。

效率

我想返回一个完整的NULL结构会更困难,或者效率更低。这是正当的理由吗?

假设类型实际上可以适应并在堆栈上分配,通常效率较低,除非幕后使用的通用内存分配器通常malloc比已分配的固定大小而不是可变大小的分配器池少得多。在这种情况下,最有可能的安全折衷方案是允许库开发人员维护与相关的不变式(概念保证)FILE

至少从性能的角度来看,fopen返回指针并不是一个合理的理由,因为它返回的唯一原因NULL是无法打开文件。这将优化一个例外情况,以换取放慢所有常见情况的执行路径。在某些情况下,可能存在有效的生产力原因,可以使设计更直接,使它们返回指针,以允许NULL在某些后置条件下返回。

对于文件操作,与文件操作本身相比,开销相对来说是微不足道的,并且fclose无论如何都无法避免手动操作。因此,这不像我们可以通过公开定义FILE并按值返回资源来节省客户端释放(关闭)资源的麻烦,fopen或者考虑到文件操作本身的相对成本以避免堆分配,期望性能会大大提高。

热点和修复

但是,对于其他情况,我在旧代码库中配置了很多浪费的C代码,这些代码中有热点,malloc并且不必要的强制性高速缓存未命中,这是由于这种做法经常与不透明的指针一起使用,并且在堆上不必要地分配了太多东西,有时在大循环。

我使用的另一种做法是,通过使用命名约定标准来传达结构定义,即使客户端不打算篡改它们,也不要暴露任何其他字段:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;
};

struct Foo foo_create(void);
void foo_destroy(struct Foo* foo);
void foo_something(struct Foo* foo);

如果将来存在二进制兼容性问题,那么我发现它足以为将来的目的多余地保留一些额外的空间,例如:

struct Foo
{
   /* priv_* indicates that you shouldn't tamper with these fields! */
   int priv_internal_field;
   int priv_other_one;

   /* reserved for possible future uses (emergency backup plan).
     currently just set to null. */
   void* priv_reserved;
};

保留的空间有点浪费,但是如果将来我们发现需要添加更多数据Foo而不破坏使用我们库的二进制文件,那么可以节省生命。

在我看来,信息隐藏和二进制兼容性通常是仅允许除可变长度结构之外的其他堆结构分配的唯一正当理由(这将总是需要它,或者如果客户端不得不分配,至少会有点尴尬,否则使用它以VLA方式在堆栈上分配内存以分配VLS)。即使大型结构通常也更便宜以值返回,如果这意味着该软件可以更有效地使用堆栈中的热内存。即使以创造价值获得回报并不便宜,人们也可以这样做:

int foo_create(struct Foo* foo);
...
/* In the client code: */
struct Foo foo;
if (foo_create(&foo))
{
    foo_something(&foo);
    foo_destroy(&foo);
}

... Foo从堆栈进行初始化,而不会产生多余的副本。或者,Foo如果出于某种原因,客户端甚至可以自由地在堆上进行分配。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.