在C中,我将如何select是否返回一个结构或指针结构?

最近在我的C肌肉工作,并通过我一直在努力的许多图书馆看,当然给了我一个很好的主意,什么是好的做法。 有一件事,我没有看到是一个函数,返回一个结构:

something_t make_something() { ... } 

从我所吸收的是这样做的“正确的”方式:

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

代码片段2中的架构比FAR更受欢迎。所以现在我问,为什么我会直接返回一个结构,就像在代码片段1中那样? 当我在两种select之间进行select时,我应该考虑哪些差异?

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

 void make_something(something_t *object) 

something_t很小时(阅读:复制它就像复制一个指针一样便宜),并且你希望它默认被堆栈分配:

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

不pipesomething_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); 

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


在堆上分配一个struct并返回它几乎和按值返回一样快。 struct必须是巨大的。

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

技术2存在ABI稳定性。 如果你有一个struct并且你的下一个版本的库增加了另外20个字段,那么你以前版本的库的消费者是二进制兼容的,如果他们是交给预构造指针的话。 他们知道的struct之外的额外数据是他们不必知道的。

如果你把它放回堆栈,调用者为它分配内存,他们必须同意你的大小。 如果自上次重build以来更新了库,则将垃圾堆栈。

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

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

所以

 something_t make_something(unsigned library_version) { ... } 

其中库使用library_version来确定预期返回什么版本的something_t ,并且它改变了它操纵的堆栈的多less 。 这是不可能使用标准的C,但是

 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()因为它们是用opaquetypes的概念构build的,这会迫使你使用指针。 返回的指针是指向dynamic内存还是其他内容取决于实现。 带有不透明types的OO往往是devise更复杂的C程序最优雅和最好的方法之一,但遗憾的是,很less有C程序员知道/关心它。

第一种方法的一些优点:

  • 编写的代码较less。
  • 更多的习惯用于返回多个值的用例。
  • 适用于没有dynamic分配的系统。
  • 小或小物体的速度可能更快。
  • 由于忘记free没有内存泄漏。

一些缺点:

  • 如果对象很大(比如说一个兆字节),可能会导致堆栈溢出,或者如果编译器不能很好地优化,可能会很慢。
  • 可能会让那些在上世纪70年代学习C的人感到惊讶,但这是不可能的,并没有跟上date。
  • 不适用于包含指向自己部分的指针的对象。

我有点惊讶。

不同的是,示例1在堆栈上创build了一个结构,示例2在堆上创build了一个结构。 在C语言或C ++代码中,C是有效的,在堆上创build大多数对象是习惯和方便的。 在C ++中,它并不是,大多数情况下它们都在栈中。 原因是如果你在堆栈上创build一个对象,析构函数会被自动调用,如果你在堆上创build它,它必须被明确地调用。所以确保没有内存泄漏并且处理exception是很容易的一切都在堆栈上。 在C语言中,析构函数必须被明确地调用,而且没有特殊析构函数的概念(当然你有析构函数,但是它们只是像destroy_myobject()这样名字的普通函数)。

现在C ++中的exception是低级别的容器对象,例如向量,树,散列图等等。 这些确实保留了堆成员,并且具有析构函数。 现在大多数内存较大的对象由几个直接的数据成员给出大小,标识符,标签等等,然后STL结构中的其余信息,可能是一个像素数据vector或英文单词/值对映射。 所以大部分数据实际上都在堆上,即使在C ++中也是如此。

而现代C ++的devise就是这样的模式

 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中是合法的:

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

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