C API函数的风格

10

我正在开发一个支持多种编程环境(如VB6和FoxPro)的库。由于C是最低公共分母,所以我必须遵循C语言的约定。现在我有一个有关样式的问题。

假设函数处理输入并返回一个字符串。在处理过程中,可能会发生错误。当前提议的样式如下:

int func(input params... char* buffer, unsigned int* buffer_size);

这种风格的好处在于,所有内容都包含在原型中,包括错误代码。并且可以避免内存分配。问题在于函数相当冗长。由于缓冲区大小可以是任意的,因此需要编写更多的代码来实现。

另一个选择是返回char*,并返回NULL以指示错误:

char* func(input params...);

这种风格需要调用者删除缓冲区。需要内存分配,因此服务器程序可能会面临内存碎片化问题。

第二种选项的变体是使用线程本地变量来保存返回的指针char*,使得用户不需要删除缓冲区。

你喜欢哪种风格?原因是什么?


缓冲区的类型不应该是char**吗?此外,为什么选项一需要一个buffer_size而选项二不需要呢? - mweerden
他将一个预先分配的缓冲区作为输入参数传递,并期望被调用的函数用错误文本填充它。 - sharptooth
好的,那么 buffer_size 不需要是指针,对吗? - mweerden
是的,输入时的缓冲区大小是缓冲区的大小,输出时它是函数使用的缓冲区量。 - Michael Kohne
9个回答

10

在这个主题上,我有点“受损”。以前我设计和维护嵌入式电信的相当大的API。这种情况下,您不能将任何东西视为理所当然。甚至像全局变量或TLS这样的东西也不行。有时甚至会出现堆缓冲区,实际上是寻址ROM内存。

因此,如果您正在寻找“最低公共分母”,您可能还需要考虑目标环境中可用的语言结构(编译器可能接受标准C中的任何内容,但如果某些内容不受支持,则链接器会说不)。

话虽如此,我总是会选择替代方案1。部分原因是(正如其他人指出的那样),您永远不应该直接为用户分配内存(间接方法将在后面进一步解释)。即使用户保证使用纯粹和简单的C,他们仍然可能使用自己定制的内存管理API来跟踪泄漏、诊断记录等。通常欣赏支持这样的策略。

错误通信是处理API时最重要的事情之一。由于用户可能具有处理代码中错误的不同方式,因此您应该尽可能一致地在整个API中进行此通信。用户应能够以一致的方式且最少的代码包装向您的API传达错误处理。我通常始终建议使用清晰的枚举代码或定义/typedef。我个人更喜欢typedef:了的枚举:

typedef enum {

  RESULT_ONE,
  RESULT_TWO

} RESULT;

因为它提供了类型/赋值安全性。

拥有一个 get-last-error 函数也很好(需要中央存储),我个人仅用它来提供关于已识别错误的额外信息。

通过像这样创建简单的复合词,可以限制替代方案1的冗长:

struct Buffer
{
  unsigned long size;
  char* data;
};

那么你的API可能会更好看:

ERROR_CODE func( params... , Buffer* outBuffer );

这种策略还可以打开更复杂的机制。例如,如果您需要调整缓冲区大小,则必须能够为用户分配内存,那么可以提供一种间接方法:

struct Buffer
{
  unsigned long size;
  char* data;
  void* (*allocator_callback)( unsigned long size );
  void  (*free_callback)( void* p );
};

当然,这种结构的风格总是存在严重的争议。

祝你好运!


你不能把那个缓冲区结构体在栈上复制传递吗?使用动态内存有点愚蠢(我假设你正在使用它)。 - toto
2
@toto:请注意,缓冲区不一定是动态内存,只因为函数接受指针。它完全可以是通过地址传递的堆栈实例。然而,我通常会倡导使用像这样的缓冲区结构作为一般类型,而不是每次需要将其传递给函数时都要包装大小和数据的堆栈结构。我假设这就是你想要谈论堆栈声明结构的内容。 - sharkin
是的,抱歉,那只是我留下的一些Java残留问题。对于堆栈上的对象,指针是可以的。 - toto

5

我更倾向于使用第一种定义方式,即传递缓冲区及其大小。虽然有例外,但通常情况下您不希望在调用函数后需要进行清理工作。如果我分配内存并将其传递到函数中,则知道我必须自己进行清理。

处理不同大小的缓冲区不应该是什么大问题。


这在很大程度上就是Windows API本身的工作方式,因此模拟它是合乎逻辑的。 - Rob K

3

第二种风格的另一个问题是分配内存的上下文可能不同。例如:

// your library in C
char * foo() {
   return malloc( 100 );
}

// my client code C++
char * p =  foo();      // call your code
delete p;               // natural for me to do, but ... aaargh!

这只是问题的一小部分。你可以说双方都应该使用malloc和free,但如果他们使用不同的编译器实现呢?最好所有的分配和释放都发生在同一个地方。无论是库还是客户端代码,这都取决于你。


2
如果我必须在这两种风格之间进行选择,我会每次都选择第一种。第二种风格会让你的库的用户考虑到另外一件事,即内存分配,而有人很可能会忘记释放内存。

1

两种方法都可以使用吗?我同意答案中支持样式1而不是样式2的共识。如果您所有的API都遵循一致的命名习惯,那么我认为样式2也可以使用,例如:


// Style 1 functions
int fooBuff(char* buffer, unsigned int buffer_size, input params... );

// Style 2 functions
char* fooBuffAlloc(input params...);
bool fooBuffFree(char* foo);

/D


1

第二种变体更加简洁。

COM IErrorInfo 是第二种方法的实现。服务器调用 SetErrorInfo 来设置出错的详细信息并返回错误代码。调用者检查代码并可以调用 GetErrorInfo 来获取详细信息。调用者负责释放 IErrorInfo,但在第一种变体中传递每个调用的参数也不美观。

服务器可以在启动时预分配足够的内存,以确保它有足够的内存来返回错误详细信息。


1

当其他程序员使用第一版时,它将更少出现错误。

如果程序员必须自己分配内存,他们更有可能记得释放它。如果库为他们分配内存,这又是另一个抽象层,可能会导致复杂性。


1

需要考虑的几个问题:

  • 分配和释放应该在同一作用域内完成(理想情况下)。最好由调用者传入预先分配的缓冲区。调用者可以安全地在以后释放它。这引出了一个问题——缓冲区应该有多大?在Win32中广泛使用的方法是将NULL作为输入缓冲区,并且size参数将告诉您需要多少。

  • 您会忽略多少种可能的错误条件?返回char*可能会限制错误报告的范围。

  • 您希望实现哪些前置和后置条件?您的原型反映了这一点吗?

  • 您是在调用方还是被调用方进行错误检查?

我无法告诉您哪个更好,因为我没有全局视图。但我相信这些问题可以让您开始思考,以及其他帖子。


0

我会以类似于snprintf和类似函数的模式进行类似于第一种方式的操作,但只是微妙地不同:

int func(char* buffer, size_t buffer_size, input params...);

如果你有很多这样的东西,它们可能看起来很相似,你可以在有用的地方使用可变数量的参数。

我非常同意使用版本1而不是版本2的已经提出的原因 - 版本2更容易出现内存问题。


我认为他使用“...”时并不是在指省略号。如果没有其他原因,省略号必须始终出现在参数列表的最后。 - sharkin
我相信他没有提到这个,但是这给了我重新排列参数的想法。=) - Kim Reece

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接