173

在 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();

出于某种原因,我认为大多数人会本能地让 Exam_data 获取指向结构 blob 的指针,但我不明白为什么。它仍然得到一个指针和一个整数,它们在一起更清楚了。而在 get_data 的情况下,不可能像我之前描述的那样搞砸,因为长度没有输入值,并且必须有一个返回的长度。

4

10 回答 10

221

对于小型结构(例如点、矩形),按值传递是完全可以接受的。但是,除了速度之外,您应该小心按值传递/返回大型结构的另一个原因:堆栈空间。

许多 C 编程是针对嵌入式系统的,其中内存非常宝贵,堆栈大小可能以 KB 甚至字节为单位……如果您按值传递或返回结构,这些结构的副本将被放置在堆栈,可能导致该站点以...命名的情况

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

于 2008-10-02T11:39:50.777 回答
66

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

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

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

-fpcc-结构返回

-freg-struct-return

如果两个编译器不同意,事情可能会爆炸。不用说,不这样做的主要原因是堆栈消耗和性能原因。

于 2008-10-03T13:45:10.207 回答
20

真正回答这个问题,需要深入挖掘组装领域:

(以下示例在 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 的竞争环境)。我不是组装向导,所以欢迎任何意见!

于 2010-07-28T17:18:17.913 回答
16

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

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

每个 char 为 1 个字节,每个 short 为 2 个字节。结构有多大?不,它不是 6 个字节。至少在任何更常用的系统上都没有。在大多数系统上它将是 8。问题是,对齐方式不是恒定的,它取决于系统,因此相同的结构在不同的系统上会有不同的对齐方式和不同的大小。

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

于 2008-10-02T13:04:04.383 回答
15

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

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

重播评论:

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

于 2008-10-02T11:35:22.863 回答
10

这是没有人提到的一件事:

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
}

a 的成员const structconst,但如果该成员是指针(如char *),它就变成char *const了而不是const char *我们真正想要的。当然,我们可以假设这const是意图的文档,并且任何违反这一点的人都在编写错误的代码(他们确实如此),但这对某些人来说还不够好(尤其是那些只花了四个小时追踪原因的人)碰撞)。

另一种方法可能是制作struct const_blob { const char *c; size_t l }并使用它,但这相当混乱 - 它会遇到与typedefing 指针相同的命名方案问题。因此,大多数人坚持只使用两个参数(或者,对于这种情况,更可能的是使用字符串库)。

于 2011-09-26T04:47:00.483 回答
9

我想说按值传递(不太大)结构,作为参数和返回值,是一种完全合法的技术。当然,必须注意结构是 POD 类型,或者复制语义是明确指定的。

更新:抱歉,我的 C++ 思维能力已达上限。我记得有一段时间在 C 中从函数返回结构是不合法的,但从那时起这可能已经改变了。只要您希望使用的所有编译器都支持这种做法,我仍然会说它是有效的。

于 2008-10-02T11:24:36.837 回答
9

我认为你的问题总结得很好。

按值传递结构的另一个优点是内存所有权是明确的。毫无疑问,结构是否来自堆,以及谁有责任释放它。

于 2008-10-02T11:26:20.653 回答
5

http://www.drpaulcarter.com/pcasm/上的 PC Assembly Tutorial 第 150 页清楚地解释了 C 如何允许函数返回结构:

C 还允许将结构类型用作函数的返回值。显然不能在 EAX 寄存器中返回一个结构。不同的编译器以不同的方式处理这种情况。编译器使用的一种常见解决方案是在内部将函数重写为将结构指针作为参数的函数。指针用于将返回值放入在调用的例程之外定义的结构中。

我使用下面的 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                 |
        +---------------------------+
于 2011-04-21T15:19:06.450 回答
0

我只想指出按值传递结构的一个优点是优化编译器可以更好地优化您的代码。

于 2016-09-05T15:00:18.267 回答