为啥 C 代码不返回结构?

Posted

技术标签:

【中文标题】为啥 C 代码不返回结构?【英文标题】:Why doesn't C code return a struct?为什么 C 代码不返回结构? 【发布时间】:2012-02-02 11:13:57 【问题描述】:

虽然它非常方便,但我很少遇到在 C 中返回 structs(或 unions)的函数,无论它们是动态链接函数还是静态定义函数。 他们改为通过指针参数返回数据。

(Windows 中的一个动态示例是GetSystemInfo。)

这背后的原因是什么? 是因为性能问题、ABI 兼容性问题还是其他原因?

【问题讨论】:

请澄清一下,您只是问为什么函数通常不返回结构(或者您是在问为什么程序员选择不返回指向结构的指针,而是使用带有结构指针的参数)。 我看到它广泛使用(并使用它)的一个情况是,当我们有一个专有系统,它有自己的编程语言,支持函数的多个返回值,例如,而不是 ptr_buf = malloc(size_t_required)它有allocate(int_size_required:ptr_buf,int_size_actually_allocated)。系统 API 也有 C 绑定,返回多个值的函数的 C 绑定总是返回结构。除了手动为每个函数定义(和命名)不同的结构非常繁琐之外,这并没有造成特别的问题。 【参考方案1】:

我会说“性能”,再加上它有时甚至可能让 C 程序员感到惊讶。对许多人来说,在 C 的一般“风格”中,将诸如结构之类的大东西当作仅仅是值来扔掉并不是...。根据语言,它们确实是。

同样,当需要复制结构时,许多 C 程序员似乎会自动求助于memcpy(),而不仅仅是使用赋值。

至少在 C++ 中,有一种叫做“返回值优化”的东西,它能够像这样默默地转换代码:

struct Point  int x, y; ;

struct Point point_new(int x, int y)

  struct Point p;
  p.x = x;
  p.y = y;
  return p;

进入:

void point_new(struct Point *return_value, int x, int y)

  struct Point p;
  p.x = x;
  p.y = y;
  *return_value = p;

它消除了结构值的(可能是堆栈饥渴的)“真实”返回。我想更好的是这个,不确定他们是否那么聪明:

void point_new(struct Point *return_value, int x, int y)

  return_value->x = x;
  return_value->y = y;

我不确定 C 编译器是否可以做到这一点,如果他们不能,那么我猜这可能是反对结构返回的真正论据,对于性能非常关键的程序。

【讨论】:

是的,我意识到示例代码不是完美的 C++,我想将其编写为 C 以便在 C 问题的上下文中更好地说明问题。 我有这样的记忆,如果函数将结构返回到堆栈,则函数的调用约定已经要求调用者为对象分配空间,然后被调用者将填充该结构。通常返回值进入登记册,但它不适合。其他方法可能需要堆栈上的返回值和参数重叠,这可能会更复杂。【参考方案2】:

原因大多是历史原因。 Rob Pike 在他的论文"The Text Editor sam" 中写道

编程风格的相关问题:sam 经常按值传递结构,这简化了代码。传统上,C 程序通过引用传递结构,但堆栈上的隐式分配更易于使用。结构传递是 C 的一个相对较新的特性(它不在 C14 的标准参考手册中),并且在大多数商业 C 编译器中都没有得到很好的支持。不过,它既方便又富有表现力,并且通过完全避免分配器和消除指针别名来简化内存管理。

话虽如此,但该技术存在缺陷;返回非常大的结构可能会导致堆栈溢出。

【讨论】:

【参考方案3】:

C 中的返回是通过将返回值存储在stack 中来实现的。 返回结构体或联合体可能会导致将非常大的数据放入堆栈,这可能会导致stack overflow。

只返回一个指向结构/联合的指针要安全得多,因为您只在堆栈中放入少量数据(通常为 4 个字节)。

【讨论】:

这也使得需要对整个返回数据进行冗余复制。 我认为这不可能是全部原因。以任何方式在堆栈上创建非常大的数据都有堆栈溢出的风险,但 C 程序员似乎避免将结构 作为返回值 在他们不会避免创建与局部变量相同的结构的情况下。例如,对于struct tm,您经常将一个放在堆栈上并将指向它的指针传递给localtime_r。如果程序员真的害怕堆栈溢出,他们就不会这样做。我想这可能是localtime_r 接口被巧妙地设计为支持 9 个单词很多并且你会 malloc 的平台。 ...但我认为这不太合理。它可能更多地与感知性能有关:巧妙地支持 ABI 和/或优化器无法执行 RVO 的平台以及值得避免不必要的副本。 我不会对你投反对票,但这远非正确。大量的架构和 ABI 尽可能使用寄存器来返回值。例如,在 ARM 上,返回 32 位 int 将返回寄存器 R0 中的值。 不是一个令人满意的答案。假设结构在堆栈上返回,则需要相同或更大的堆栈大小来声明结构(作为局部变量),然后将其作为指针传递。它可能与临时性有关(即旧编译器中缺少 RVO),但应该对此进行解释。【参考方案4】:

C 函数可以返回一个结构(C++ 函数也是如此,它很常见)。也许在 C 的最初几年,它不能。

用于 Linux 和相关系统的x86-64 ABI specification(第 21 页)甚至说适合两个 -64 位字的结构通常可以在两个寄存器中返回,而无需通过内存(甚至堆栈)。这很可能比通过堆栈更快。

正如 unwind 在his answer 中回复的那样,ABI 通常要求将结构结果静默转换为不可见的指针。

甚至可以定义另一个调用约定,在更多寄存器中返回更多数据。但是这样的新约定会破坏所有目标代码并需要重新编译所有内容(甚至包括在 Linux 上的系统库,如 libc.so.6),当然还需要更改编译器。

当然,ABI 约定与处理器、系统和编译器相关。

我不了解 Windows,也不知道 Windows 将什么定义为其 ABI。

【讨论】:

我认为 32 位 ABI(可能是 64 位)有点问题,因为调用者负责从堆栈中弹出结构。因此,如果调用者没有声明(在 C 中太常见),而他只是忽略了返回的结构,那么堆栈就会损坏。这真的很难调试。【参考方案5】:

在 ANSI C 之前,您不能返回结构类型的对象,也不能传递结构类型的参数。

来自 Chris Torek 在 comp.lang.c 中的引用:

请注意,V6 C 也不支持结构值参数和 结构值返回值。

现在它们不太常用的原因是人们更喜欢返回指向结构的指针,它只涉及指针副本而不是整个结构对象的副本。

【讨论】:

请读者注意:Chris Torek 的引用来自 1998 年,指的是 16 位 MS C 版本 6(从 1989 年开始),而不是 Visual C++ 6(从 1998 年开始)在编译 C 代码(或 C++)时按值传递或返回结构没有问题。 @MichaelBurr 我不认为它指的是 MS C 6。结构值参数和返回值在 MS C 5.1 中工作(可以从 winworldpc 下载)。 @raymai97 正确,V6 指的是Version 6 Unix【参考方案6】:

除了可能会影响性能或按值返回结构在标准之前的日子可能不普遍支持的想法之外,C 函数不对结构使用按值返回的另一个原因是,如果你返回一个你不能轻易返回成功/失败指示器的结构。我知道我偶尔会开始设计一个函数来返回一个在函数中初始化的结构,但后来我遇到了如何指示函数是否成功的问题。您几乎有以下选择:

保证成功(有时这是可能的) 传递一个指向错误代码位置的指针 在结构中具有指示成功/失败的字段或标记值

只有选项 1 才能防止界面变得杂乱无章。第二种选择违背了按值返回结构的目的,实际上使函数更难用于处理故障。第三种选择显然在几乎所有情况下都不是一个好的设计。

【讨论】:

【参考方案7】:

通常,Windows 函数要么不返回任何内容,要么返回错误代码,尤其是在返回结构或类时。

效率可能是个问题,尽管 RVO 应该消除开销。

我认为主要原因是为了使方法与之前使用的编码风格保持一致。

【讨论】:

以上是关于为啥 C 代码不返回结构?的主要内容,如果未能解决你的问题,请参考以下文章

为啥要在 C 中声明一个只包含数组的结构?

用vs2019编写c语言程序,明显语法错误为啥不回报错,没有加return 0;

为啥 main() 函数不返回浮点值?

为啥以下代码从 NSData 返回不正确的值?

为啥我的代码停止并且不返回异常?

为啥在 dart/flutter 中不等待 await 返回就执行代码?