在C语言中,通过值传递结构体与通过指针传递结构体相比,有什么缺点吗?

191
在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函数接受一个指向结构体blob的指针,但我不知道为什么会这样。它仍然接受一个指针和一个整数,只是更清晰地表明它们在一起。而在get_data的情况下,无法像之前描述的那样搞砸,因为没有长度的输入值,必须返回一个长度。


就此而言,“void examine data(const struct blob)”是不正确的。 - Chris Lutz
谢谢,我已经修改了它以包含变量名。 - dkagedal
2
从上面的代码中无法判断函数get_data是否被禁止查看len指向的内容。而且编译器也没有检查该指针是否确实返回了一个值,这使得这段代码毫无意义(可能是因为最后两行出现在函数外部导致代码无效)。请问您能否详细说明一下? - Adam Spiers
4
函数下面的两行是为了说明如何调用该函数。函数签名并没有暗示实现只会写入指针的事实。编译器也无法知道它应该验证一个值是否被写入指针,因此返回值机制只能在文档中描述。 - dkagedal
3
C语言中人们不经常这样做的主要原因是历史原因。在C89之前,你无法通过值传递或返回结构体,所以所有早于C89的系统接口在逻辑上应该这样做(比如 gettimeofday),但实际上使用指针代替,人们就把这个当成了例子。 - zwol
传递结构体时,以C结构体格式传递的值是否会被复制到内存中?我需要通过值传递类似于字符串类的东西,它有一个光标和缓冲区指针。我不能有任何额外的指令。假设在函数中有3个char,除非我们使用硬件辅助堆栈,否则它们将直接放入寄存器中而不保存到RAM中,在这种情况下,寄存器将保存到堆栈中而不是以C结构体格式。带有3个char的结构体是否与在堆栈上创建的3个char*相同?我认为在没有指示的情况下,结构体不会被写入RAM。 - user2356685
11个回答

0

考虑到人们所说的所有事情...

  1. C语言中并不总是允许返回结构体,但现在可以了。
  2. 有三种方法可以返回结构体... a. 在寄存器中返回每个成员(可能是最优的,但实际上不太可能...) b. 在堆栈中返回结构体(比堆内存访问慢,但仍然比冷访问堆内存好...缓存万岁!) c. 通过指向堆的指针返回结构体(只有在读写时才会受到影响?一个好的编译器将传递它只读一次并尝试访问的指针,进行指令重排,并在需要时更早地访问它,以使生活更美好?(发抖))
  3. 由于这个原因,不同的编译器设置可能会导致代码接口出现不同的问题。(不同大小的寄存器,不同数量的填充,不同的优化开启)
  4. const或volatile并不能渗透整个结构体,并且可能导致一些非常低效或可能导致错误的代码(例如,const struct foo并不意味着foo->bar是const。)

阅读后我将采取一些简单的措施...

  1. 让你的函数接受参数而不是结构体。这样可以对const-ness和volatile-ness等进行细粒度控制,还可以确保传递的所有变量与使用它们的函数相关。如果参数都是同一种类型,请使用其他方法来强制排序。(创建类型定义以使函数调用更具强类型,这是操作系统经常做的。)
  2. 不要让最终的基本函数返回指向堆中创建的结构体的指针,而是提供一个指向结构体的指针来存放结果。该结构体仍可能在堆中,但实际上该结构体可能在栈中 - 这将获得更好的运行时性能。这也意味着您不需要依赖编译器为您提供结构体返回类型。
  3. 通过将参数作为片段传递并明确const-ness、volatile-ness或restrict-ness,您可以更好地传达您的意图给编译器,从而使其能够进行更好的优化。

我不确定“太大”和“太小”在哪里,但我猜答案在2和寄存器计数+1个成员之间。

如果我创建一个只包含一个int成员的结构体,那么显然我们不应该传递这个结构体。(不仅效率低下,而且意图非常模糊...我想它在某些地方可能有用,但并不常见)

如果我创建一个包含两个项目的结构体,它可能具有清晰度的价值,编译器也可以将其优化为成对旅行的两个变量。(risc-v指定具有两个成员的结构体返回两个成员,假设它们是int或更小的类型...)

如果我创建一个结构体,其中包含与处理器中的寄存器数量相同的int和double,则从技术上讲,这是一种可能的优化方式。但一旦超过寄存器数量,最好将结果结构体保留在指针中,并仅传递相关的参数。(此外,还应该使结构体更小,函数执行更少,因为现在即使在嵌入式系统中,我们也有很多寄存器可用...)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接