在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
返回两个值作为元组,分配给x
和y
。
由于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 ); }
这只是一个例子,你可能会看到一个函数返回一个结构。