C APIdevise:谁应该分配?
什么是在C API中分配内存的正确/首选的方式?
起初我可以看到两个select:
1)让调用者做所有的(外部)内存处理:
myStruct *s = malloc(sizeof(s)); myStruct_init(s); myStruct_foo(s); myStruct_destroy(s); free(s);
_init
和_destroy
函数是必要的,因为可能会在内部分配更多的内存,并且必须在某处处理它。
这有一个更长的缺点,而且malloc可以在某些情况下被消除(例如,它可以通过一个堆栈分配的结构:
int bar() { myStruct s; myStruct_init(&s); myStruct_foo(&s); myStruct_destroy(&s); }
另外,调用者必须知道结构的大小。
2)在_init
隐藏malloc
,在_destroy
free
s。
优点:较短的代码,因为function将被称为无论如何。 完全不透明的结构。
缺点:不能通过以不同方式分配的结构体。
myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(foo);
我现在正在倾向于第一个案件。 那么我又不了解C API的devise。
我最喜欢的deviseC API的例子是GTK + ,它使用了你描述的方法#2。
虽然方法#1的另一个优点不仅仅是可以在堆栈上分配对象,还可以多次重复使用同一个实例。 如果这不是一个常见的用例,那么#2的简单可能是一个优势。
当然,这只是我的意见:)
#2的另一个缺点是调用者无法控制事物的分配方式。 这可以通过为客户端注册自己的分配/释放函数(如SDL)提供一个API来解决,但即使这样也可能不够细化。
#1的缺点是,当输出缓冲区不是固定大小(如string)时,它不能正常工作。 充其量,你将需要提供另一个函数来获得缓冲区的长度,以便调用者可以分配它。 在最糟糕的情况下,要做到这一点是不可能的(即在一条独立path上计算长度比一次性计算和复制要贵得多)。
#2的优点是,它允许你严格暴露你的数据types作为一个不透明的指针(即声明结构,但不定义它,并使用指针一致)。 然后,您可以更改您的库的将来版本中适合的结构的定义,而客户端在二进制级别上保持兼容。 使用#1,你必须通过要求客户端以某种方式(例如Win32 API中的所有cbSize
字段)指定结构内部的版本,然后手动编写能够处理结构的较旧版本和较新版本的代码保持二进制兼容,随着你的图书馆的发展。
一般来说,如果你的结构是透明的数据,不会随着将来对库的小修改而改变,那么我会去#1。 如果它是一个或多或less复杂的数据对象,你想完全封装,以防万一它未来的发展,去#2。
方法2每次。
为什么? 因为使用方法编号1,你必须泄漏执行细节给调用者。 调用者必须知道至less有多大的结构。 你不能改变对象的内部实现而不用重新编译任何使用它的代码。
为什么不提供两个,为了两全其美?
使用_init和_terminate函数来使用方法#1(或任何你认为合适的命名)。
使用附加的_create和_destroy函数进行dynamic分配。 由于_init和_terminate已经存在,它可以归结为:
myStruct *myStruct_create () { myStruct *s = malloc(sizeof(*s)); if (s) { myStruct_init(s); } return (s); } void myStruct_destroy (myStruct *s) { myStruct_terminate(s); free(s); }
如果你希望它是不透明的,那么使_init和_terminate是static
,不要在API中暴露它们,只提供_create和_destroy。 如果您需要其他分配,例如给定的callback,请为此提供另一组函数,例如_createcalled,_destroycalled。
重要的是跟踪分配,但是你必须这样做。 您必须始终使用所使用的分配器的对应部分来取消分配。
两者在function上是等同的。 但是,在我看来,方法#2更容易使用。 喜欢2超过1的几个原因是:
-
这更直观。 为什么我必须在使用
myStruct_Destroy
(显然)销毁对象后free
地调用对象。 -
从用户隐藏
myStruct
详细信息。 他不必担心它的大小等 -
在#2方法中,
myStruct_init
不必担心对象的初始状态。 -
您不必担心用户忘记拨打
free
电话的内存泄漏。
如果您的API实现是作为单独的共享库提供的,然而方法#2是必须的。 要隔离你的模块,在编译器版本中malloc
/ new
和free
/ delete
实现不匹配,你应该保持内存分配和解除分配给你自己。 请注意,C ++比C更真实。
我用第一种方法的问题不是那么多,调用者是更长的时间,现在的api现在被铐在能够扩大它正在使用的内存量正是因为它不知道如何记忆它收到了被隔离。 调用者并不总是提前知道需要多less内存(想象一下,如果你试图实现一个向量)。
另一个你没有提到的选项,大部分时间都是过度的,是传入一个函数指针,api用作分配器。 这不允许你使用堆栈,但是确实允许你做一些事情,比如用一个内存池replacemalloc的使用,这个内存池仍然保持api控制何时分配。
至于哪种方法是合适的apidevise,在C标准库中都是这样做的。 strdup()和stdio使用第二种方法,而sprintf和strcat使用第一种方法。 就我个人而言,我更喜欢第二种方法(或第三种方法),除非1)我知道我永远不需要重新分配,2)我期望我的对象的生命周期很短,因此使用这个堆栈非常方便
编辑:实际上有另外一种select,这是一个有突出先例的不好的select。 你可以像strtok()那样用静态方法来做到这一点。 不好,刚刚提到的完整性。
两种方式都可以,我倾向于做第一种方式,因为很多CI都是为embedded式系统做的,所有的内存都是堆栈上的微小variables,或者是静态分配的。 这样就不会有内存不足,要么你一开始就够了,要么从一开始就搞砸了。 当你有2K的RAM时,很高兴知道:-)所以我所有的库都像#1那样,假设内存被分配。
但这是C开发的一个边缘案例。
说了这么多,我可能还会和#1一起去。 也许使用init和finalize / dispose(而不是销毁)名称。
这可以给一些反思的元素:
案例#1模仿C ++的内存分配scheme,或多或less有相同的好处:
- 在堆栈上(或者在静态数组中或者这样写你自己的结构分配器来代替malloc),很容易的分配临时对象。
- 如果在初始化中出现任何错误,容易释放内存
情况2隐藏了更多关于使用结构的信息,也可以用于不透明结构,通常当用户看到的结构与lib内部使用的结构不完全相同时(比如说结构的末端可能会隐藏更多的字段)。
情况#1和情况#2之间的混合API也是常见的:有一个字段用于传递指向某个已经初始化的结构的指针,如果它是空的,则被分配(并且总是返回指针)。 使用这样的API,即使init执行分配,free也通常是调用者的责任。
在大多数情况下,我可能会去案件#1。
两者都是可以接受的 – 正如你所指出的那样,它们之间存在权衡。
两者都有很多现实世界的例子,正如Dean Harding所说,GTK +使用第二种方法; OpenSSL是一个使用第一个例子。
我会去(1)用一个简单的扩展名,那就是让你的_init
函数总是返回指向对象的指针。 您的指针初始化然后可能只是读取:
myStruct *s = myStruct_init(malloc(sizeof(myStruct)));
正如你可以看到右手边只有一个参考types,而不是variables了。 一个简单的macros然后给你(2)至less部分
#define NEW(T) (T ## _init(malloc(sizeof(T))))
和你的指针初始化读取
myStruct *s = NEW(myStruct);
看你的方法#2说
myStruct *s = myStruct_init(); myStruct_foo(s); myStruct_destroy(s);
现在看看myStruct_init()
是否因各种原因需要返回一些错误代码,然后放开这个方法。
myStruct *s; int ret = myStruct_init(&s); // int myStruct_init(myStruct **s); myStruct_foo(s); myStruct_destroy(s);