最近在我的C肌肉上工作并浏览了我一直在使用它的许多图书馆,这当然让我很清楚什么是好的做法。我没见过的一件事是一个返回结构的函数:
something_t make_something() { ... }
从我吸收的内容来看,这是"权利"这样做的方式:
something_t *make_something() { ... }
void destroy_something(something_t *object) { ... }
代码片段2中的体系结构比代码片段1更受欢迎。所以现在我问,为什么我会直接返回一个结构,就像在代码片段1中一样?当我在两个选项之间做出选择时,我应该考虑哪些差异?
此外,该选项如何比较?
void make_something(something_t *object)
答案 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
而无内存泄漏。有些缺点:
答案 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
将两个值作为元组返回,分配给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 );
}
这只是一个例子,你可能会看到一个函数返回一个结构。