在 C 中通过值传递结构而不是传递指针有什么缺点吗?

在 C 中通过值传递结构而不是传递指针有什么缺点吗?

如果结构很大,显然存在复制大量数据的性能方面,但是对于较小的结构,它基本上应该与向函数传递多个值相同。

当用作返回值时,它可能更有趣。C 只有函数的单个返回值,但通常需要多个返回值。所以一个简单的解决方案就是把它们放在一个结构中,然后返回。

有什么理由支持或反对这样做吗?

由于我在这里谈论的内容可能不是每个人都清楚,我将给出一个简单的例子。

如果你用 C 语言编程,你迟早会开始编写这样的函数:

void examine_data(const char *ptr, size_t len)
{
...
}


char *p = ...;
size_t l = ...;
examine_data(p, l);

这不是问题。唯一的问题是,您必须同意您的同事的参数顺序,因此在所有函数中使用相同的约定。

但是当你想返回相同类型的信息时会发生什么呢? 你通常会得到这样的东西:

char *get_data(size_t *len);
{
...
*len = ...datalen...;
return ...data...;
}
size_t len;
char *p = get_data(&len);

这种方法工作得很好,但问题更多。返回值是一个返回值,除非在这个实现中它不是。从上面没有办法告诉函数 get_data不允许查看 len指向的内容。没有什么能让编译器检查一个值是否真的通过该指针返回。因此,下个月,当其他人修改代码而没有正确理解代码时(因为他没有阅读文档?)它在没人注意的情况下坏了,或者开始随机崩溃。

因此,我提出的解决方案是简单的结构

struct blob { char *ptr; size_t len; }

例子可以这样重写:

void examine_data(const struct blob data)
{
... use data.tr and data.len ...
}


struct blob = { .ptr = ..., .len = ... };
examine_data(blob);


struct blob get_data(void);
{
...
return (struct blob){ .ptr = ...data..., .len = ...len... };
}
struct blob data = get_data();

出于某种原因,我认为大多数人会本能地让 examine_data接受一个指向 struct blob 的指针,但我不明白为什么。它仍然得到一个指针和一个整数,只是更清楚的是,它们一起去。在 get_data的例子中,不可能像我之前描述的那样搞砸,因为长度没有输入值,而且必须有一个返回的长度。

97590 次浏览

我想说,通过值传递(不太大的)结构,无论是作为参数还是作为返回值,都是一种完全合法的技术。当然,我们必须注意,这个结构要么是 POD 类型,要么是指定了复制语义。

更新: 对不起,我开启了 C + + 的思维上限。我记得有一段时间,在 C 语言中从函数返回 struct 是不合法的,但是从那时起,这种情况可能已经改变了。我仍然会说,只要您希望使用的所有编译器都支持这种实践,它就是有效的。

我觉得你的问题总结得很好。

按值传递结构的另一个优点是内存所有权是显式的。不用担心 struct 是否来自堆,以及谁负责释放它。

简单的解决方案是返回一个错误代码作为返回值,其他所有内容作为函数中的一个参数,
这个参数当然可以是一个结构,但是没有看到任何通过值传递的特殊优势,只是发送了一个指针。
通过值传递结构是危险的,你需要非常小心你传递的是什么,记住在 C 中没有复制建构子,如果结构参数之一是一个指针,指针值将被复制,这可能会非常混乱和难以维护。

只是为了完成答案(完全归功于 罗迪)的堆栈使用是另一个原因,不通过结构的价值,相信我调试堆栈溢出是真正的 PITA。

回复评论:

通过指针传递 struct 意味着某个实体拥有这个对象的所有权,并且完全知道应该释放什么以及什么时候释放。通过值传递 struct 创建对 struct 内部数据的隐藏引用(指向其他结构的指针等)。. )这很难维持(可能,但是为什么?).

对于小结构(例如 point,rect) ,通过值传递是完全可以接受的。但是,除了速度之外,还有另外一个原因可以解释为什么要小心地按值传递/返回大型结构体: 堆栈空间。

很多 C 语言编程都是针对嵌入式系统的,因为嵌入式系统的内存非常昂贵,堆栈的大小可以用 KB 甚至字节来衡量... ... 如果你通过值传递或返回结构体,那么这些结构体的副本将被放置在堆栈上,这可能导致 这个网站以... ... 命名。

如果我看到一个应用程序似乎有过多的堆栈使用,那么通过值传递的结构是我首先要查找的内容之一。

到目前为止,这里的人们忘记提到(或者我忽略了)一件事,那就是结构通常有一个填充!

struct {
short a;
char b;
short c;
char d;
}

每个 char 是1字节,每个 short 是2字节。这个结构有多大?不,不是6字节。至少在任何更常用的系统上都没有。在大多数系统上是8。问题是,对齐不是恒定的,它是系统相关的,所以相同的结构在不同的系统上会有不同的对齐和不同的大小。

填充不仅会进一步消耗你的堆栈,还会增加无法提前预测填充的不确定性,除非你知道你的系统是如何填充的,然后查看应用程序中的每一个结构并计算它的大小。传递指针需要可预测的空间量——没有不确定性。指针的大小对于系统来说是已知的,它总是相等的,不管结构是什么样的,指针的大小总是以对齐且不需要填充的方式选择。

不这样做的一个原因是,这可能会导致二进制兼容性问题。

根据所使用的编译器,结构可以通过堆栈或寄存器传递,这取决于编译器选项/实现

见: http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

- fpcc-struct-return

- freg-struct-return

如果两个编译器意见不一致,事情就会搞砸。不用说,不这样做的主要原因是堆栈消耗和性能原因。

为了回答这个问题,我们需要深入挖掘集合地:

(下面的示例在 x86 _ 64上使用 gcc,欢迎任何人添加其他架构,如 MSVC、 ARM 等。)

让我们来看看我们的示例程序:

// foo.c


typedef struct
{
double x, y;
} point;


void give_two_doubles(double * x, double * y)
{
*x = 1.0;
*y = 2.0;
}


point give_point()
{
point a = {1.0, 2.0};
return a;
}


int main()
{
return 0;
}

使用完全优化编译它

gcc -Wall -O3 foo.c -o foo

看看大会:

objdump -d foo | vim -

这就是我们得到的:

0000000000400480 <give_two_doubles>:
400480: 48 ba 00 00 00 00 00    mov    $0x3ff0000000000000,%rdx
400487: 00 f0 3f
40048a: 48 b8 00 00 00 00 00    mov    $0x4000000000000000,%rax
400491: 00 00 40
400494: 48 89 17                mov    %rdx,(%rdi)
400497: 48 89 06                mov    %rax,(%rsi)
40049a: c3                      retq
40049b: 0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)


00000000004004a0 <give_point>:
4004a0: 66 0f 28 05 28 01 00    movapd 0x128(%rip),%xmm0
4004a7: 00
4004a8: 66 0f 29 44 24 e8       movapd %xmm0,-0x18(%rsp)
4004ae: f2 0f 10 05 12 01 00    movsd  0x112(%rip),%xmm0
4004b5: 00
4004b6: f2 0f 10 4c 24 f0       movsd  -0x10(%rsp),%xmm1
4004bc: c3                      retq
4004bd: 0f 1f 00                nopl   (%rax)

不包括 nopl垫,give_two_doubles()有27个字节,而 give_point()有29个字节。另一方面,give_point()产生的指令比 give_two_doubles()少一条

有趣的是,我们注意到编译器已经能够将 mov优化为更快的 SSE2变体 movapdmovsd。此外,give_two_doubles()实际上从内存中移入和移出数据,这使得速度变慢。

很显然,这些技术在嵌入式环境中可能并不适用(如今 C 语言的竞争环境大部分时间都是在嵌入式环境中)。我不是汇编向导,所以欢迎任何评论!

关于 http://www.drpaulcarter.com/pcasm/的 PC 组装教程第150页有一个关于 C 如何允许函数返回结构的清晰解释:

C 还允许结构类型为 用作 func-的返回值 显然,一个结构不能是 在「税务易」登记册内申报。 不同的编译器处理这个问题 情况不同。一个普通 编译器使用的解决方案是 在内部将函数重写为一个 接受结构指针作为 指针用于放置 结构中的返回值 定义在例程之外的。

我使用以下 C 代码来验证上述语句:

struct person {
int no;
int age;
};


struct person create() {
struct person jingguo = { .no = 1, .age = 2};
return jingguo;
}


int main(int argc, const char *argv[]) {
struct person result;
result = create();
return 0;
}

使用“ gcc-S”为这段 C 代码生成汇编:

    .file   "foo.c"
.text
.globl create
.type   create, @function
create:
pushl   %ebp
movl    %esp, %ebp
subl    $16, %esp
movl    8(%ebp), %ecx
movl    $1, -8(%ebp)
movl    $2, -4(%ebp)
movl    -8(%ebp), %eax
movl    -4(%ebp), %edx
movl    %eax, (%ecx)
movl    %edx, 4(%ecx)
movl    %ecx, %eax
leave
ret $4
.size   create, .-create
.globl main
.type   main, @function
main:
pushl   %ebp
movl    %esp, %ebp
subl    $20, %esp
leal    -8(%ebp), %eax
movl    %eax, (%esp)
call    create
subl    $4, %esp
movl    $0, %eax
leave
ret
.size   main, .-main
.ident  "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
.section    .note.GNU-stack,"",@progbits

调用前的堆栈创建:

        +---------------------------+
ebp     | saved ebp                 |
+---------------------------+
ebp-4   | age part of struct person |
+---------------------------+
ebp-8   | no part of struct person  |
+---------------------------+
ebp-12  |                           |
+---------------------------+
ebp-16  |                           |
+---------------------------+
ebp-20  | ebp-8 (address)           |
+---------------------------+

调用 create 之后的堆栈:

        +---------------------------+
| ebp-8 (address)           |
+---------------------------+
| return address            |
+---------------------------+
ebp,esp | saved ebp                 |
+---------------------------+

有件事没人提过:

void examine_data(const char *c, size_t l)
{
c[0] = 'l'; // compiler error
}


void examine_data(const struct blob blob)
{
blob.ptr[0] = 'l'; // perfectly legal, quite likely to blow up at runtime
}

const struct的成员是 const,但是如果该成员是一个指针(如 char *) ,它就变成了 char *const,而不是我们真正想要的 const char *。当然,我们可以假设 const是意图的文档,任何违反这一点的人都是在编写糟糕的代码(他们确实是) ,但这对于一些人来说还不够好(特别是那些花了四个小时追踪崩溃原因的人)。

另一种选择可能是创建一个 struct const_blob { const char *c; size_t l }并使用它,但是这相当混乱——它陷入了与我在使用 typedefing 指针时遇到的命名方案相同的问题。因此,大多数人坚持只有两个参数(或者,在这种情况下更可能使用字符串库)。

我只是想指出通过值传递结构的一个好处是,编译器最佳化可以更好地优化代码。

考虑到人们所说的一切..。

  1. 在 C 语言中,并不总是允许返回结构体,但现在允许了。
  2. 返回结构有三种方法..。 在一个寄存器中返回每个成员(可能是最佳的,但不太可能是实际的...) 返回堆栈中的结构(比寄存器慢,但仍然比冷访问堆内存好... ... 好棒的缓存!) 在指向堆的指针中返回结构(只有在读取或写入堆时才会受到伤害?一个好的编译器将只传递一次它读取的指针并尝试访问它,指令重新排序和访问它比需要的时间要早得多,所以当你准备好的时候它就准备好了?让生活更美好?(颤抖)
  3. 因此,不同的编译器设置在代码接口时可能会导致不同的问题。(不同大小的寄存器,不同数量的填充,不同的优化打开)
  4. 常量或易失性不会渗透到结构中,可能会导致一些糟糕的低效率或可能导致代码中断(例如,const struct foo 不会导致 foo-> bar 为 const)

读完这篇文章后,我将采取一些简单的措施..。

  1. 让函数接受参数而不是结构。它允许对常量和易失性等进行细粒度控制,它还确保所有传递的变量都与使用它们的函数相关。如果参数都是相同的类型,则使用其他方法执行排序。(使用 type defs 使函数调用更强类型化,这是操作系统的常规做法。)
  2. 不允许最终的基函数返回指向堆中生成的结构的指针,而是提供一个指向结果的结构的指针。Struct 仍然可能在堆中,但是它可能实际上在堆栈中——并且将获得更好的运行时性能。它还意味着您不需要依赖提供结构返回类型的编译器。
  3. 通过将参数作为片段传递,并清楚地了解常量、易失性或限制性,您可以更好地向编译器传达您的意图,从而使其能够进行更好的优化。

我不知道“太大”和“太小”在哪里,但我想答案是在2和注册计数 + 1成员之间。 如果我创建了一个包含一个 int 成员的 struct,那么显然我们不应该传递 struct。(它不仅效率低下,而且使意图非常模糊... ... 我想它在某些地方有用,但并不常见)

如果我创建一个包含两个条目的结构,它可能具有清晰度方面的价值,而编译器可能会将其优化为两个成对传输的变量。(risc-v 指定带有两个成员的 struct 返回寄存器中的两个成员,假设它们是 int 或者更小...)

如果我创建一个结构,其中包含的 int 和 double 数量与处理器中的寄存器中的一样多,那么从技术上来说,这是一种可能的优化。 但是,如果我超过了寄存器的数量,那么将 result 结构保存在指针中并只传递相关的参数可能是值得的。(这可能会使结构更小,函数的功能更少,因为现在系统上有很多寄存器,甚至在嵌入式世界中也是如此... ...)