在C中按值传递结构是否有任何缺点,而不是传递指针?

时间:2008-10-02 11:21:13

标签: c struct parameter-passing abi

在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的情况下,不可能以我之前描述的方式搞乱,因为长度没有输入值,并且必须有返回的长度。

10 个答案:

答案 0 :(得分:188)

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

许多C编程适用于内存非常重要的嵌入式系统,堆栈大小可以用KB或甚至字节来衡量......如果你按值传递或返回结构,那些结构的副本将会放在堆栈上,可能导致this site以...命名的情况

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

答案 1 :(得分:61)

没有提到这一点的一个原因是,这可能会导致二进制兼容性问题。

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

请参阅:http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html

  

-fpcc-结构回

     

-freg-结构回

如果两个编译器不同意,事情就会爆发。毋庸置疑,不做这个的主要原因是堆栈消耗和性能原因。

答案 2 :(得分:19)

真的回答这个问题,需要深入了解装配地:

(以下示例在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现在大部分时间都是C的比赛场地)。我不是一个装配向导,所以欢迎任何评论!

答案 3 :(得分:15)

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

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

重播评论:

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

答案 4 :(得分:9)

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

更新:对不起,我的C ++思维上限了。我记得在C中从函数返回结构不合法的时候,但从那以后这可能已经发生了变化。只要您期望使用的所有编译器都支持这种做法,我仍然会说它是有效的。

答案 5 :(得分:9)

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

按值传递结构的另一个好处是内存所有权是显式的。没有想知道结构是否来自堆,谁有责任释放它。

答案 6 :(得分:9)

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

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

每个字符为1个字节,每个短字节为2个字节。结构有多大?不,这不是6个字节。至少不在任何更常用的系统上。在大多数系统中,它将是8.问题是,对齐不是恒定的,它取决于系统,因此相同的结构将在不同的系统上具有不同的对齐和不同的大小。

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

答案 7 :(得分:8)

这里没有人提到过:

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是意图的文档,并且任何违反此规范的人都在编写错误的代码(他们是这样),但这对某些人来说还不够好(特别是那些只花了4个小时跟踪的人)导致崩溃的原因。)

替代方案可能是制作struct const_blob { const char *c; size_t l }并使用它,但这相当混乱 - 它与typedef指针有相同的命名方案问题。因此,大多数人坚持只有两个参数(或者,更有可能的是,使用字符串库)。

答案 8 :(得分:5)

关于http://www.drpaulcarter.com/pcasm/的PC程序集教程的第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                 |
        +---------------------------+

答案 9 :(得分:0)

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