最近在我的C肌肉上工作,并查看了我一直在使用的许多库,这当然给了我一个很好的想法,什么是好的实践。我还没有见过返回结构体的函数:
something_t make_something() { ... }
从我所吸收的,这是做这件事的"正确"方式:
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }
代码段2中的体系结构比代码段1中的更受欢迎。那么现在我要问,为什么要像代码片段1那样直接返回一个结构体呢?当我在这两个选项之间做出选择时,我应该考虑哪些差异?
此外,这个选项比较起来如何?
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();
不管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);
这几乎总是与ABI稳定性有关。库版本之间的二进制稳定性。在不是这样的情况下,有时是关于动态调整结构的大小。很少是关于非常大的struct
s或性能。
在堆上分配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
版本。
根据经验,您永远不应该按值传递struct
对象。在实践中,只要它们小于或等于CPU在单个指令中可以处理的最大大小,就可以这样做。但从风格上讲,即便如此,人们通常也会避免使用它。如果你从不按值传递结构,你可以稍后向结构中添加成员,这不会影响性能。
我认为void make_something(something_t *object)
是c中最常见的使用结构的方式。你把分配留给调用者。这是有效的,但不漂亮。
something_t *make_something()
,因为它们是用不透明类型的概念构建的,这迫使您使用指针。返回的指针是否指向动态内存或其他取决于实现。带有不透明类型的OO通常是设计更复杂的C程序的最优雅和最好的方法之一,但遗憾的是,很少有C程序员知道/关心它。
第一种方法的一些优点:
- 对于返回多个值的用例来说更习惯。
- 适用于没有动态分配的系统。
- 对于小物体可能更快。 没有由于忘记
free
而导致的内存泄漏。- 如果对象很大(比如,兆字节),可能会导致堆栈溢出,或者如果编译器没有很好地优化它,可能会很慢。
- 可能会让那些在20世纪70年代学习C语言的人感到惊讶,因为这是不可能的,而且没有跟上时代。
- 不适用于包含指向自身部分指针的对象。
我有点惊讶。
区别在于示例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);
}
将编译成相当高效的代码。大的观察成员不会产生不必要的复制副本。
与其他一些语言(如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 %dn", point.x, point.y );
}
这只是一个例子,你可能看到一个函数返回一个结构