在C语言中,如何选择是返回结构还是返回指向结构的指针?

In C, how would I choose whether to return a struct or a pointer to a struct?

最近在我的C肌肉上工作,并浏览了我与之合作过的许多库,这无疑让我对什么是好的实践有了一个很好的了解。我还没有看到一个函数返回一个结构:

1
something_t make_something() { ... }

从我所吸收的内容来看,这是一种"正确"的方式:

1
2
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }

代码片段2中的体系结构远比代码片段1流行。所以现在我问,为什么我会直接返回一个结构,就像在代码片段1中那样?当我在这两个选项之间进行选择时,我应该考虑哪些差异?

此外,这个选项是如何比较的?

1
void make_something(something_t *object)


something_t很小(读:复制它和复制指针一样便宜)并且您希望在默认情况下对其进行堆栈分配时:

1
2
3
4
5
6
something_t make_something(void);

something_t stack_thing = make_something();

something_t *heap_thing = malloc(sizeof *heap_thing);
*heap_thing = make_something();

something_t较大或希望对其进行堆分配时:

1
2
3
something_t *make_something(void);

something_t *heap_thing = make_something();

不管something_t的大小,如果您不关心它的分配位置:

1
2
3
4
5
6
7
void make_something(something_t *);

something_t stack_thing;
make_something(&stack_thing);

something_t *heap_thing = malloc(sizeof *heap_thing);
make_something(heap_thing);


这几乎总是关于ABI的稳定性。库版本之间的二进制稳定性。在不存在的情况下,有时需要动态调整结构的大小。很少涉及到非常大的struct或性能。

在堆上分配struct并返回它的速度几乎与按值返回一样快,这是非常罕见的。struct必须是巨大的。

实际上,速度不是技术2背后的原因,而是指针返回,而不是值返回。

技术2用于ABI稳定性。如果您有一个struct,而您的下一个版本的库又添加了20个字段,那么您以前版本的库的使用者如果是手工构建的指针,那么它们是二进制兼容的。他们所知道的struct结束后的额外数据是他们不必知道的。

如果您在堆栈上返回它,调用方将为它分配内存,并且它们必须同意您对它的大小的看法。如果您的库自上次重建以来进行了更新,那么您将丢弃堆栈。

技术2还允许您隐藏返回指针之前和之后的额外数据(将数据附加到结构结尾的版本是的变体)。您可以用一个可变大小的数组结束结构,或者用一些额外的数据预先结束指针,或者两者兼而有之。

如果希望在稳定的ABI中堆栈分配struct,那么几乎所有与struct对话的函数都需要传递版本信息。

所以

1
something_t make_something(unsigned library_version) { ... }

其中,库使用library_version来确定预期返回的something_t的版本,并改变它操作的堆栈的大小。使用标准C是不可能的,但是

1
void make_something(something_t* here) { ... }

是。在这种情况下,something_t可能有一个version字段作为其第一个元素(或大小字段),您需要在调用make_something之前填充它。

其他采用something_t的库代码会查询version字段,以确定它们使用的something_t的版本。


根据经验,您不应该按值传递struct对象。实际上,只要它们小于或等于CPU在一条指令中可以处理的最大大小,就可以这样做。但在风格上,即使是在那时,人们通常也会避开它。如果从不按值传递结构,则可以稍后向结构添加成员,而这不会影响性能。

我认为void make_something(something_t *object)是使用C中结构的最常见方法。您将分配留给调用者。它效率高,但不漂亮。

然而,面向对象的C程序使用something_t *make_something(),因为它们是用不透明类型的概念构建的,这迫使您使用指针。返回的指针是否指向动态内存或其他内容取决于实现。不透明类型的OO通常是设计更复杂的C程序最优雅和最好的方法之一,但遗憾的是,很少有C程序员知道/关心它。


第一种方法的一些优点:

  • 少写代码。
  • 更习惯于返回多个值的用例。
  • 在没有动态分配的系统上工作。
  • 对于小物体或小物体来说可能更快。
  • 无内存泄漏,因为忘记了free

一些缺点:

  • 如果对象很大(例如,兆字节),可能会导致堆栈溢出,或者如果编译器不能很好地优化它,可能会很慢。
  • 可能会让那些在70年代学过C的人大吃一惊,当时这是不可能的,而且还没有跟上时代。
  • 不适用于包含指向自身一部分的指针的对象。


我有点惊讶。

不同之处在于,示例1在堆栈上创建一个结构,示例2在堆上创建它。在C或C++代码中,C是有效的,在堆上创建大多数对象是习惯和方便的。在C++中,它不是,主要是在堆栈上运行。原因是,如果您在堆栈上创建一个对象,析构函数会自动调用,如果您在堆栈上创建它,则必须显式调用它。因此,确保没有内存泄漏和处理异常会更容易,因为堆栈上的所有内容都在进行。在C中,无论如何都必须明确地调用析构函数,并且没有特殊析构函数的概念(当然,您有析构函数,但它们只是具有destroy_MyObject()等名称的普通函数)。

现在C++中的异常是用于低级容器对象,例如向量、树、散列映射等等。它们确实保留堆成员,并且它们有析构函数。现在,大多数重内存对象都由一些直接的数据成员组成,这些成员给出大小、ID、标记等,然后是STL结构中的其余信息,可能是像素数据的矢量,也可能是英文单词/值对的映射。所以大多数数据实际上是堆,甚至在C++中。

而现代C++是这样设计的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class big
{
    std::vector<double> observations; // thousands of observations
    int station_x;                    // a bit of data associated with them
    int station_y;
    std::string station_name;
}  

big retrieveobservations(int a, int b, int c)
{
    big answer;
    //  lots of code to fill in the structure here

    return answer;
}

void high_level()
{
   big myobservations = retriveobservations(1, 2, 3);
}

将编译成相当有效的代码。大型观察成员不会生成不必要的临时副本。


与其他一些语言(如Python)不同,C没有元组的概念。例如,以下内容在python中是合法的:

1
2
3
4
5
def foo():
    return 1,2

x,y = foo()
print x, y

函数foo返回两个作为元组的值,分配给xy

因为C没有元组的概念,所以从一个函数返回多个值是不方便的。解决此问题的一种方法是定义一个结构来保存值,然后返回该结构,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct { int x, y; } stPoint;

stPoint foo( void )
{
    stPoint point = { 1, 2 };
    return point;
}

int main( void )
{
    stPoint point = foo();
    printf("%d %d
"
, point.x, point.y );
}

这只是一个示例,您可能会看到一个函数返回一个结构。