在C中通过结构传递结构有什么缺点,而不是传递一个指针?
在C中通过结构传递结构有什么缺点,而不是传递一个指针?
如果结构很大,显然有复制大量数据的性能方面,但是对于一个较小的结构,它应该基本上是将多个值传递给一个函数。
用作返回值可能更有趣。 C只有函数的单个返回值,但是你经常需要几个。 所以一个简单的解决scheme是把它们放在一个结构中并返回。
这有什么理由吗?
由于这里可能不太清楚我在说什么,我将举一个简单的例子。
如果你用C编程,你迟早会开始编写如下的函数:
void examine_data(const char *ptr, size_t len) { ... } char *p = ...; size_t l = ...; examine_data(p, l);
这不是问题。 唯一的问题是,你必须同意你的同事参数的顺序,所以你在所有的function使用相同的约定。
但是当你想要返回相同types的信息时会发生什么? 你通常得到这样的东西:
char *get_data(size_t *len); { ... *len = ...datalen...; return ...data...; } size_t len; char *p = get_data(&len);
这工作正常,但更多的问题。 返回值是一个返回值,除了在这个实现中它不是。 上面没有办法告诉我们函数get_data不允许查看len指向的内容。 没有什么能够使编译器检查一个值是否通过该指针实际返回。 那么下个月,当别人修改代码而不正确地理解代码(因为他没有阅读文档?)它会被打破,没有任何人注意到,或者随机开始崩溃。
所以,我提出的解决scheme是简单的结构
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采取一个指针结构blob,但我不明白为什么。 它仍然得到一个指针和一个整数,它们更加清晰,他们一起去。 而在get_data的情况下,不可能像我之前描述的那样混乱,因为长度没有input值,并且必须有返回的长度。
对于小型结构(如点,矩形)来说,通过价值是完全可以接受的。 但是,除了速度之外,还有一个原因是为什么你应该小心地按照价值传递/返回大型结构:堆栈空间。
很多C语言编程都是针对embedded式系统的,其中内存很重要,堆栈大小可能以KB或者甚至Bytes为单位。如果按值传递或返回结构,这些结构的副本将被放置在该堆栈,可能导致这个网站的名字命名的情况…
如果我看到一个似乎具有过多堆栈使用的应用程序,那么按值传递的结构就是我首先要查找的东西之一。
没有提到的一个原因是没有提到,这可能会导致二进制兼容性问题。
根据所使用的编译器,结构可以通过堆栈或寄存器传递,具体取决于编译器选项/实现
请参阅: 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 <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()
更less的指令
有趣的是,我们注意到编译器已经能够将mov
优化成速度更快的SSE2变种movapd
和movsd
。 而且, give_two_doubles()
实际上是将数据从内存中移出,这使得事情变得缓慢。
显然这很可能不适用于embedded式环境(现在大部分时间都是C领域)。 我不是一个汇编向导,所以任何意见将是值得欢迎的!
简单的解决scheme将返回一个错误代码作为返回值和其他一切作为参数在函数中,
这个参数当然可以是一个结构体,但是没有看到任何通过值传递的特殊优点,只是发送了一个指针。
按值传递结构是危险的,你需要非常小心你传递的是什么,记住C中没有拷贝构造函数,如果结构参数之一是一个指针,指针值将被复制,这可能是非常混乱和难以保持。
只是为了完成答案(充满功劳罗迪 )栈的使用是另一个原因不传递结构的价值,相信我debugging栈溢出是真正的PITA。
重播评论:
通过指针传递结构意味着某个实体拥有这个对象的所有权,并且完全知道什么和什么时候应该被释放。 通过值传递结构创build一个隐藏的引用的内部数据的结构(指向另一个结构等..)在这是很难维护(可能,但为什么?)。
我认为按价值传递(不是太大)的结构,既是参数又是返回值,是一种完全合法的技术。 当然,必须小心,该结构是一个PODtypes,或复制语义是明确指定的。
更新:对不起,我有我的C + +的思想上限。 我记得在C中从一个函数返回一个结构不合法的时候,但是从那以后可能已经改变了。 只要所有希望使用的编译器都支持这个练习,我仍然会说这是有效的。
我认为你的问题总结得很好。
通过价值传递结构的另一个好处是内存所有权是明确的。 有没有想知道如果结构是从堆,谁有责任释放它。
这里没有人提到:
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
是意向的文档,任何违反这个规则的人都会编写错误的代码(他们是这样),但是对于一些人来说这还不够好(特别是那些花了四个小时追查一个崩溃)。
另一种可能是build立一个struct const_blob { const char *c; size_t l }
struct const_blob { const char *c; size_t l }
并使用它,但是这相当混乱 – 它进入了与typedef
指针相同的命名scheme问题。 因此,大多数人坚持只有两个参数(或者更可能是使用string库)。
有一件事现在忘记提到的人(或者我忽略了)是结构通常有填充!
struct { short a; char b; short c; char d; }
每个字符是1个字节,每个字符是2个字节。 结构有多大? 不,不是6字节。 至less不在任何更常用的系统上。 在大多数系统上,它将是8.问题是,alignment不是恒定的,它是依赖于系统的,所以相同的结构在不同的系统上将具有不同的alignment和不同的尺寸。
不仅填充会进一步消耗你的堆栈,它还增加了无法预知填充的不确定性,除非你知道你的系统如何填充,然后看看你的应用程序中的每一个结构体,并计算大小为了它。 传递指针需要可预测的空间 – 没有不确定性。 指针的大小对于系统来说是已知的,它总是相等的,无论结构是什么样的,并且指针大小总是以它们alignment的方式select,并且不需要填充。
关于C如何允许函数返回一个结构的清晰解释:http://www.drpaulcarter.com/pcasm/上的PC Assembly Tutorial的第150页:
C还允许将一个结构types用作函数的返回值。 EAX寄存器显然不能返回结构。 不同的编译器处理这种情况的方式不同 编译器使用的一个常见的解决scheme是将内部函数重写为一个以结构指针作为参数的函数。 指针用于将返回值放入定义在例程外的结构中。
我使用下面的C代码来validation上面的语句:
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
调用创build之前的堆栈:
+---------------------------+ 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 | +---------------------------+
我只想指出按价值传递结构的一个好处是优化编译器可以更好地优化你的代码。