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

时间:2016-10-21 02:45:53

标签: c pointers struct malloc

最近在我的C肌肉上工作并浏览了我一直在使用它的许多图书馆,这当然让我很清楚什么是好的做法。我没见过的一件事是一个返回结构的函数:

something_t make_something() { ... }

从我吸收的内容来看,这是"权利"这样做的方式:

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

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

此外,该选项如何比较?

void make_something(something_t *object)

6 个答案:

答案 0 :(得分:57)

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

something_t *make_something(void); something_t *heap_thing = make_something(); 很大或者您希望它被堆分配时:

something_t

无论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); 的大小如何,如果您不关心它的分配位置:

{{1}}

答案 1 :(得分:36)

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

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

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

技术2存在ABI稳定性。如果你有一个struct并且你的下一个版本的库又添加了20个字段,那么你以前版本的库的使用者是二进制兼容的,如果他们是预先构建的指针。超出他们所知道的struct末尾的额外数据是他们不必了解的。

如果你在堆栈上返回它,调用者正在为它分配内存,他们必须同意你的大小。如果您的库自上次重建后更新,您将要删除堆栈。

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

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

所以

something_t make_something(unsigned library_version) { ... }

库使用library_version来确定预期返回的something_t的版本,并且更改它操作的堆栈的数量。这不可能使用标准C,但

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

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

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

答案 2 :(得分:13)

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

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

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

答案 3 :(得分:9)

第一种方法的一些优点:

  • 少写代码。
  • 更多惯用于返回多个值的用例。
  • 适用于没有动态分配的系统。
  • 对于小型或小型物体来说可能更快。
  • 由于忘记free而无内存泄漏。

有些缺点:

  • 如果对象很大(例如,兆字节),可能会导致堆栈溢出,或者如果编译器没有很好地优化它,则可能会很慢。
  • 可能会让那些在20世纪70年代学会C的人感到惊讶,因为这是不可能的,并且没有及时更新。
  • 不适用于包含指向其自身一部分的指针的对象。

答案 4 :(得分:4)

我有点惊讶。

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

现在C ++中的异常是针对低级容器对象,例如矢量,树木,哈希地图等。这些确实保留堆成员,并且它们具有析构函数。现在,大多数内存繁重的对象由一些立即数据成员组成,这些成员给出大小,ID,标签等,然后是STL结构中的其余信息,可能是像素数据的向量或英语单词/值对的映射。因此,即使在C ++中,大多数数据实际上都在堆上。

现代C ++的设计使这种模式

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);
}

将编译为非常高效的代码。大型观察成员不会产生不必要的作品复制品。

答案 5 :(得分:3)

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

def foo():
    return 1,2

x,y = foo()
print x, y

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

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

typedef struct { int x, y; } stPoint;

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

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

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