在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的情况下,不可能以我之前描述的方式陷入困境,因为长度没有输入值,并且必须有返回的长度.
对于小结构(例如点,矩),通过值是完全可以接受的.但是,除了速度之外,还有另外一个原因,你应该小心通过值传递/返回大型结构:堆栈空间.
很多C编程都适用于内存非常重要的嵌入式系统,堆栈大小可以用KB或甚至字节来衡量......如果按值传递或返回结构,那些结构的副本将放在堆栈,可能导致此站点以...命名的情况
如果我看到一个似乎有过多堆栈使用的应用程序,那么通过值传递的结构是我首先要寻找的东西之一.
没有提到这一点的一个原因是,这可能导致二进制兼容性问题的问题.
根据所使用的编译器,结构可以通过堆栈或寄存器传递,具体取决于编译器选项/实现
请参阅:http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
-fpcc-结构回报
-freg-结构回报
如果两个编译器不同意,事情就会爆发.不用说,没有这样做的主要原因是堆栈消耗和性能原因.
要真正回答这个问题,需要深入挖掘装配地:
(以下示例在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: 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 : 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变体movapd
和movsd
.give_two_doubles()
实际上,实际上是将数据移入和移出内存,这会使事情变得缓慢.
显然,大部分内容可能不适用于嵌入式环境(现在C大多数时候都是C的竞争场所).我不是一个装配向导,所以欢迎任何评论!
简单的解决方案将返回一个错误代码作为返回值,其他所有内容作为函数中的参数,
这个参数当然可以是一个结构,但是没有看到任何特定的优势通过值传递,只是发送了一个指针.
按值传递结构是危险的,你需要非常小心你传递的是什么,记住C中没有复制构造函数,如果其中一个结构参数是一个指针,指针值将被复制它可能会非常混乱和难以保持.
只是为了完成答案(完全归功于Roddy)堆栈使用是另一个不按值传递结构的原因,相信我调试堆栈溢出是真正的PITA.
重播评论:
通过指针传递struct意味着某个实体对此对象具有所有权,并且完全了解应该释放什么和何时.按值传递struct会创建对struct的内部数据的隐藏引用(指向其他结构的指针等),这很难维护(可能但为什么?).
我会说按值传递(不太大)结构,作为参数和返回值,是一种完全合法的技术.当然,必须注意结构是POD类型,或者复制语义是明确指定的.
更新:对不起,我的C++思想上限了.我记得在C中从函数返回结构不合法的时候,但从那以后这可能已经发生了变化.只要您期望使用的所有编译器都支持这种做法,我仍然会说它是有效的.
我认为你的问题总结得很好.
通过值传递结构的另一个好处是内存所有权是显式的.没有想知道结构是否来自堆,谁有责任释放它.
到目前为止,人们忘记提到的一件事(或者我忽略了)是结构通常有填充物!
struct { short a; char b; short c; char d; }
每个字符都是1个字节,每个短字节是2个字节.结构有多大?不,这不是6个字节.至少不在任何更常用的系统上.在大多数系统上,它将是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 }
a的成员const struct
是const
,但如果该成员是指针(如char *
),它变成char *const
而不是const char *
我们真正想要的.当然,我们可以假设这const
是意图的记录,并且任何违反此规范的人都在编写错误的代码(他们是这样),但这对某些人来说还不够好(特别是那些花了四个小时追踪其原因的人).崩溃).
替代方案可能是制作struct const_blob { const char *c; size_t l }
并使用它,但这相当混乱 - 它会遇到与typedef
指针相同的命名方案问题.因此,大多数人坚持只有两个参数(或者,更有可能的是,使用字符串库).
http://www.drpaulcarter.com/pcasm/上的PC Assembly教程的第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 | +---------------------------+