【问题标题】:C API design: Who should allocate? [closed]C API 设计:谁应该分配? [关闭]
【发布时间】:2011-03-18 19:12:28
【问题描述】:

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

起初我可以看到两个选项:

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 中隐藏mallocs,在_destroy 中隐藏frees。

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

缺点:不能传递以不同方式分配的结构。

myStruct *s = myStruct_init();

myStruct_foo(s);

myStruct_destroy(foo);

我目前倾向于第一种情况;再说一次,我不知道 C API 设计。

【问题讨论】:

标签: c api memory-management malloc


【解决方案1】:

#2 的另一个缺点是调用者无法控制事物的分配方式。这可以通过为客户端提供一个 API 来注册他自己的分配/解除分配功能(就像 SDL 所做的那样)来解决,但即使这样也可能不够细粒度。

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

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

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

【讨论】:

  • +1 表示关于抽象和不透明指针的观点 - 这是一个很大的优势,因为它将您的实现与调用代码完全分离
  • 对于何时使用每种方法有一个实际的挑剔建议的好答案。
【解决方案2】:

方法号 2 每次。

为什么?因为对于方法号 1,您必须将实现细节泄露给调用者。调用者必须至少知道结构有多大。如果不重新编译任何使用它的代码,就无法更改对象的内部实现。

【讨论】:

  • 这意味着 #2 可以实现为二进制兼容接口,在 .so 或 .dll 中提供次要版本的 API 添加、增强等不会破坏客户端代码这个答案需要更多的支持
  • 调用者确实必须知道对象的大小(也许还有对齐方式?),但这并不意味着它必须静态地知道它:你可以有myStruct_size(void)myStruct_alignment(void)。见this question
  • @Kalrish 为什么调用者必须知道大小?我同意如果调用者在任何时候都需要知道大小,您可以添加您建议的方法,但是设计合理的 API 不需要调用者了解对象内部的任何信息- 包括大小和对齐方式。
  • @JeremyP 这样的设计使得无法使用例如静态内存或重用相同的内存 - 内存分配是静态隐藏实现的问题之一。尽管如此,我同意它使用起来并不愉快。也许,一个中间解决方案是同时实现*_alloc(...) 方法作为 API 的一部分。这样,“懒惰”的用户可以使用动态分配,而包装器(例如 C++)可以进行自己的内存管理。
  • @Kalrish 是的,但那又怎样?如果您坚持能够从堆栈分配内存(例如),您将无法进行适当的封装。对象应该总是作为引用来实现,每一种理智的 OO 语言都以这种方式实现它们。 C++ 不是一个健全的 OO 语言,幸运的是这个问题不是 C++ 问题,所以我们可以忽略它。
【解决方案3】:

为什么不同时提供两者,以获得两全其美?

使用 _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);
}

如果你希望它是不透明的,那么让_init和_terminatestatic并且不要在API中暴露它们,只提供_create和_destroy。如果您需要其他分配,例如使用给定的回调,为此提供另一组函数,例如_createcall,_destroycall。

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

【讨论】:

  • 有没有采用这种方法的知名 C 库?
  • @cubuspl2 是否有任何知名的 C 库或作者记录了他们为什么不采用这种方法?
【解决方案4】:

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

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

当然,这只是我的看法:)

【讨论】:

  • 现在,这是一个有趣的评论。我听到很多人说正好相反,GTK+ 是一个糟糕的 API。不幸的是,我只使用了一点,我通常在 C++ 的云中,并使用 Gtkmm。我的经验记得 ref-counted 指针,以及 _new 和 _free 函数,然而,这似乎更符合第 3 个选项。我很好奇你的理由。
  • GLib/Gtk 的一般设计理念似乎是“我们原则上不会使用 C++,所以我们将手动编码所有相同的东西”。这种方法在某种意义上有一些优势,它仍然是一个纯 C API,这使得它更容易与各种纯 C 的 FFI 一起使用......但从纯 C/C++ 的角度来看,它似乎相当不切实际。
【解决方案5】:

两者在功能上是等效的。但是,在我看来,方法#2 更容易使用。选择 2 而不是 1 的几个原因是:

  1. 更直观。在我(显然)使用myStruct_Destroy 销毁对象之后,为什么我必须在对象上调用free

  2. 对用户隐藏myStruct 的详细信息。他不必担心它的大小等。

  3. 在方法 #2 中,myStruct_init 不必担心对象的初始状态。

  4. 您不必担心由于用户忘记调用free而导致的内存泄漏。

但是,如果您的 API 实现是作为单独的共享库发布的,则方法 #2 是必须的。要将您的模块与跨编译器版本的malloc/newfree/delete 实现中的任何不匹配隔离开来,您应该自己保留内存分配和取消分配。请注意,C++ 比 C 更是如此。

【讨论】:

  • 两者等价,因为后者需要动态分配,而前者不需要。
  • 嗯...是的。应该说功能等效。已更新。
【解决方案6】:

我对第一种方法的问题不是调用者的时间更长,而是api现在被束缚在能够扩展它正在使用的内存量上,因为它不知道如何它收到的内存已分配。调用者并不总是提前知道它需要多少内存(想象一下,如果你试图实现一个向量)。

另一个您没有提到的选项(大多数情况下会有点过分)是传入一个函数指针,该 api 将其用作分配器。这不允许您使用堆栈,但允许您执行一些操作,例如用内存池替换 malloc 的使用,这仍然让 api 控制它何时想要分配。

至于哪种方法是正确的api设计,在C标准库中是双向的。 strdup() 和 stdio 使用第二种方法,而 sprintf 和 strcat 使用第一种方法。我个人更喜欢第二种方法(或第三种方法),除非 1)我知道我永远不需要重新分配 2)我希望我的对象的生命周期很短,因此使用堆栈非常方便

编辑: 实际上还有另外一种选择,这是一个糟糕的选择,有一个突出的先例。你可以像 strtok() 用静态方法那样做。不好,只是为了完整起见而提及。

【讨论】:

    【解决方案7】:

    两种方式都可以,我倾向于使用第一种方式,因为我所做的很多 C 语言都是针对嵌入式系统的,所有内存要么是堆栈上的微小变量,要么是静态分配的。这样就不会出现内存不足的情况,要么一开始就有足够的内存,要么一开始就搞砸了。很高兴知道你什么时候有 2K 的 Ram :-) 所以我所有的库都像 #1 一样,假设内存是分配的。

    但这是 C 语言开发的一个边缘案例。

    话虽如此,我可能还是会选择#1。也许对名称使用 init 和 finalize/dispose(而不是销毁)。

    【讨论】:

      【解决方案8】:

      这可能会引起一些反思:

      案例 #1 模仿 C++ 的内存分配方案,具有或多或少相同的好处:

      • 在堆栈上轻松分配临时对象(或在静态数组等中,以编写您自己的结构分配器替换 malloc)。
      • 如果 init 出现任何问题,可以轻松释放内存

      case #2 隐藏了更多关于使用的结构的信息,也可以用于不透明的结构,通常是当用户看到的结构与 lib 内部使用的结构不完全相同时(比如可能有更多的字段隐藏在结构结束)。

      case#1 和 case #2 之间的混合 API 也很常见:有一个字段用于传递指向某个已初始化结构的指针,如果它为 null,则分配它(并且总是返回指针)。使用这样的 API,即使 init 执行了分配,free 通常也是调用者的责任。

      在大多数情况下,我可能会选择案例 #1。

      【讨论】:

        【解决方案9】:

        两者都可以接受 - 正如您所指出的,它们之间存在权衡。

        在现实世界中有大量的例子——正如Dean Harding 所说,GTK+ 使用第二种方法; OpenSSL 是一个使用第一个的示例。

        【讨论】:

          【解决方案10】:

          我会选择 (1) 一个简单的扩展,即让您的 _init 函数始终返回指向对象的指针。然后你的指针初始化可能只是这样:

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

          如你所见,右手边只有类型的引用,不再有变量的引用。一个简单的宏然后至少部分地给你(2)

          #define NEW(T) (T ## _init(malloc(sizeof(T))))
          

          你的指针初始化读取

          myStruct *s = NEW(myStruct);
          

          【讨论】:

          • 如何处理 malloc 失败?
          • @Secure:好点。我认为_init 函数应该对传入NULL 指针具有鲁棒性,并在返回时通过它。与往常一样,该检查留给指针的用户。
          • 在这方面的另一个设计理念是大多数函数应该期待有效的指针(释放器明显例外)并且 assert() 它们不为 NULL。这将使您的方法有效地将断言用于程序逻辑,这是一个很大的禁忌。当然,这取决于程序的整体设计,但我个人更喜欢明确地处理错误。 IE。 malloc 单独使用,并在使用指针完成任何其他操作之前测试其有效性。
          • @Secure:我倾向于扩展约定以检查宏 NEW 返回的指针。这只是这种约定的轻微扩展,因为您必须已经检查几个函数,不仅是 malloc,而且还有 realloccalloc(也许还有我忘记的其他函数)。
          【解决方案11】:

          看你的方法 #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);
          

          【讨论】:

            猜你喜欢
            • 2011-04-13
            • 2011-06-14
            • 1970-01-01
            • 2010-10-31
            • 2017-02-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-08-29
            相关资源
            最近更新 更多