C API设计:谁应该分配?

Tor*_*dek 52 c api malloc memory-management

在C API中分配内存的正确/首选方法是什么?

我首先看到两个选项:

1)让调用者完成所有(外部)内存处理:

myStruct *s = malloc(sizeof(s));
myStruct_init(s);

myStruct_foo(s);

myStruct_destroy(s);
free(s);
Run Code Online (Sandbox Code Playgroud)

_init_destroy功能是必需的,因为一些更多的内存可分配里面,而且必须在某个地方处理.

这样做的缺点是更长,但在某些情况下也可以消除malloc(例如,它可以传递给堆栈分配的结构:

int bar() {
    myStruct s;
    myStruct_init(&s);

    myStruct_foo(&s);

    myStruct_destroy(&s);
}
Run Code Online (Sandbox Code Playgroud)

此外,调用者必须知道结构的大小.

2)隐藏mallocs in _initfreein in _destroy.

优点:代码更短,因为无论如何都会调用函数.完全不透明的结构.

缺点:无法传递以不同方式分配的结构.

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(foo);
Run Code Online (Sandbox Code Playgroud)

我目前正在倾向于第一个案例; 然后,我不知道C API设计.

Pav*_*aev 15

#2的另一个缺点是调用者无法控制事物的分配方式.这可以通过为客户端提供API来注册他自己的分配/释放函数(如SDL)来解决,但即使这样也可能不够精细.

#1的缺点是当输出缓冲区不是固定大小(例如字符串)时它不能很好地工作.最好,您需要提供另一个函数来首先获取缓冲区的长度,以便调用者可以分配它.在最坏的情况下,根本不可能有效地这样做(即,在一个单独的路径上计算长度比一次计算和复制过于昂贵).

#2的优点是它允许您严格地将数据类型公开为不透明指针(即声明结构但不定义它,并且一致地使用指针).然后,您可以在库的未来版本中更改结构的定义,而客户端在二进制级别上保持兼容.使用#1,您必须通过要求客户端以某种方式在结构中指定版本(例如cbSize,Win32 API中的所有这些字段),然后手动编写可以处理结构的较旧版本和较新版本的代码来执行此操作.随着您的库的发展,保持二进制兼容.

一般来说,如果你的结构是透明的数据,随着未来对图书馆的小修改不会改变,我会选择#1.如果它是一个或多或少复杂的数据对象,并且您希望完全封装以防万一,以便将来开发,请使用#2.


Jer*_*myP 15

每次方法编号为2.

为什么?因为使用方法编号1,您必须将实现详细信息泄漏给调用者.调用者必须至少知道结构的大小.如果不重新编译使用它的任何代码,则无法更改对象的内部实现.

  • 调用者确实必须知道对象的大小(也许还有对齐方式?),但这并不意味着它必须_静态_知道它:您可以拥有 `myStruct_size(void)` 和 `myStruct_alignment(void)`。见[这个问题](http://stackoverflow.com/questions/26471718/dynamically-allocate-properly-aligned-memory-is-the-new-expression-on-char-arra)。 (3认同)
  • 这意味着#2可以实现为二进制兼容接口,在.so或.dll中提供时不会破坏客户端代码的次要版本API添加,增强功能等这个答案需要更多的支持 (2认同)

Sec*_*ure 12

为什么不提供两者,以充分利用这两个世界?

使用_init和_terminate函数来使用方法#1(或任何你认为合适的命名).

使用其他_create和_destroy函数进行动态分配.由于_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);
}
Run Code Online (Sandbox Code Playgroud)

如果你想让它不透明,那么make _init和_terminate static并且不要在API中公开它们,只提供_create和_destroy.如果您需要其他分配,例如使用给定的回调,请为此提供另一组函数,例如_createcalled,_destroycalled.

重要的是跟踪分配,但无论如何你必须这样做.您必须始终使用已使用的分配器的对应部分进行解除分配.

  • 是否有任何着名的C库采用这种方法? (2认同)

Dea*_*ing 9

我最喜欢的设计良好的C API示例是GTK +,它使用您描述的方法#2.

虽然方法#1的另一个优点不仅仅是您可以在堆栈上分配对象,而且还可以多次重用同一个实例.如果这不是一个常见的用例,那么#2的简单性可能是一个优势.

当然,这只是我的意见:)

  • GLib/Gtk的一般设计理念似乎是"我们原则上不会使用C++,因此我们将手动编写所有相同的东西".这种方法在一定程度上具有一些优势,它仍然是一个纯粹的C API,这使得它更容易与各种C-only FFI一起使用......但从纯C/C++的角度来看,它似乎相当不切实际. (2认同)