我正在审查别人写的一些代码。我遇到了一个有趣的案例,涉及此代码中的字符串,我需要帮助来理解它是如何工作的。
有一个旨在导出到 DLL 的函数。在函数的顶部,我们有这个声明
char *msg; // pointer to char
int error = 0; //my error code
然后,在代码的后面,我们为我们使用的 IDE 调用一个特殊的库函数:
if(error < 0)
msg = getErrorString(error);
此内置库函数 (getErrorString) 希望您提供指向 char 的指针,它可以在运行时存储生成的错误字符串。
最后,代码作者调用以下内容:
free(msg); // freeing dynamically allocated memory??
所以,我想在运行时动态分配的内存足够大,可以存储生成的错误字符串?如果不明确调用像malloc这样的东西,怎么允许这样做?如果我正在编写等效的代码,我的第一直觉是声明一些静态数组,如 msg[256],然后执行以下操作:
char msg[256] = {""};
sprintf(msg, "%s", getErrorString(error));
所以我的主要问题是,如何声明指向 char 的指针,然后将其分配给在运行时生成的字符串,如原始代码所示?内存似乎是在运行时动态分配的,可能是由运行时引擎分配的。这是这段代码发生了什么吗?在这种情况下,我的静态数组方法是否更可取?
如果不明确调用 malloc 之类的东西,怎么会允许这样做?
好吧,几乎可以肯定的情况是,在getErrorString
的实现中的某个地方,有一个对malloc
或等效的调用。
getErrorString
的实现可能如下所示:
char *getErrorString(int error)
{
char *ret = malloc(25);
if(ret == NULL) abort();
switch(error) {
case EMUCHMEM:
strcpy(ret, "too much memory");
break;
case EIUNDERFLOW:
strcpy(ret, "integer underflow");
break;
case EDIVBY1:
strcpy(ret, "divide by 1");
break;
default:
sprintf(ret, "error %d", error);
break;
}
return ret;
}
如果我正在编写等效的代码,我的第一直觉是声明一些静态数组,如 msg[256],然后执行以下操作:
char msg[256] = {""};
sprintf(msg, "%s", getErrorString(error));
这没有多大意义。 它表示不必要的额外内存(msg[]
256 字节)和不必要的额外复制(按sprintf
)。
所以我的主要问题是,如何声明指向 char 的指针,然后将其分配给在运行时生成的字符串,如原始代码所示?
你总是可以声明一个指向char
的指针,然后给它分配一个在运行时生成的字符串。
不过,这可能是一个令人困惑的问题。 你可能听说过字符串在 C 中表示为char
数组——这是真的——你可能也听说过你不能用 C 赋值数组——这也是真的。 您可能听说过,您始终必须调用strcpy
而不是直接分配字符串。 但这不一定是真的——你也可以通过简单地分配指针来"分配"字符串,这就是你说msg = getErrorString(error)
时发生的事情。 换句话说,在 C 中分配字符串有两种完全不同的方法:复制数组或分配指针。 有关这一点的更多信息,请参阅其他答案。
内存似乎在运行时动态分配
是的,看起来就是这样。
在这种情况下,我的静态数组方法是否更可取?
正如其他评论所建议的那样,在这种情况下,动态内存分配可能是一个好主意,也可能不是一个好主意。 不过,作为一般规则,动态内存分配是一种完全可以的——或多或少至关重要的——技术。 另一方面,静态内存分配本身可能有很多问题。
根据您描述的代码,getErrorString
显然调用malloc
或calloc
(或等效物)本身并返回该指针。 这实际上是相当普遍的做法 - 参见 POSIXstrdup
函数作为另一个示例。
问题是,当您完成该内存时,您有责任解除分配该内存。 如果getErrorString
动态分配内存,则需要记录下来,以便使用它的任何人都知道在完成该内存时free
该内存。
如果我正在编写等效的代码,我的第一直觉是声明一些静态数组,如 msg[256],然后执行以下操作:
char msg[256] = {""}; sprintf(msg, "%s", getErrorString(error));
坏主意,因为您丢弃了getErrorString
返回的指针值,这意味着您永远无法free
它分配的内存。
getErrorString
分配所有必要的内存来为你存储字符串;你不需要留出自己的缓冲区来存储字符串本身。 只需存储返回的指针值,以便以后可以free
该内存。
如何处理动态分配的内存一直是 API 的一个棘手问题。通常,理想的设计是负责内存分配的实体也负责释放;就个人而言,我会设计getErrorString
来接收错误代码和指向目标缓冲区及其大小的指针:
char *getErrorString( int errorCode, char *buf, size_t bufSize )
{
switch( errorCode )
{
case SOME_ERROR:
strncpy( buf, "Error message for SOME_ERROR", bufSize );
break;
case SOME_OTHER_ERROR:
strncpy( buf, "Error message for SOME_OTHER_ERROR", bufSize );
break;
...
}
/**
* Make sure buf is properly 0-terminated, since strncpy won't
* zero-terminate if the target buffer is shorter than the
* source string
*/
buf[bufSize-1] = 0;
return buf;
}
这样,我就负责缓冲区的分配和释放。 我可以使用auto
数组,而不必担心内存管理:
void foo( void )
{
char msg[81];
...
fprintf( stderr, "%s", getErrorString( error, msg, sizeof msg ) );
...
}
在这种情况下,错误消息正在写入msg
;getErrorString
返回msg
的地址,因此可以将其作为fprintf
的一部分调用;由于它是一个auto
变量,因此msg
的内存将在函数退出时自动释放。
或者,如果我愿意,我可以动态分配该内存:
char *msg = calloc( 81, sizeof *msg );
...
fprintf( stderr, "%sn", getErrorString( error, msg, 81 );
...
free( msg );
但无论哪种方式,分配和解分配内存的责任都在同一个实体中(调用getErrorString
的代码);它不会在两个不同的实体之间拆分。
另一个选项是函数维护静态内部缓冲区:
char *getErrorString( int error )
{
static char buf[SOME_SIZE+1]; // where SOME_SIZE is the length of the longest
// error string
switch( error )
{
case SOME_ERROR:
strcpy( buf, "Error string for SOME_ERROR" );
break;
case SOME_OTHER_ERROR:
strcpy( buf, "Error string for SOME_OTHER_ERROR" );
break;
...
}
return buf;
}
由于buf
声明static
其生存期是整个程序的生存期,因此当getErrorString
退出时它不会消失。 这样,没有人不必担心管理缓冲区的内存。
这种方法的问题在于getErrorString
不再是可重入的或线程安全的 - 你在整个程序中只有一个缓冲区,所以如果getErrorString
被本身调用getErrorString
的其他代码打断,或者如果两个线程同时调用getErrorString
,那么该缓冲区将被损坏。
作为最后一种选择,如果所有字符串都是常量,那么根本不需要留出任何内存 - 只需直接返回字符串文字:
/**
* Attempting to modify a string literal invokes undefined behavior,
* so we don't want this pointer to be used as the target of
* a strcpy or sprintf call. We change the return value to const char *
* so the compiler will yell at us if we try to modify the pointed-to
* string.
*/
const char *getErrorString( int error )
{
switch( error )
{
case SOME_ERROR:
return "Error string for SOME_ERROR";
break;
case SOME_OTHER_ERROR:
return "Error string for SOME_OTHER_ERROR";
break;
...
}
return "Unknown error code";
}
现在我们可以直接调用函数,不用担心它:
fprintf( stderr, "%sn", getErrorString( error ) );
如果出于任何原因仍要留出内存来存储错误字符串,则可以:
const char *str = getErrorString( error );
char *buf = malloc( strlen( str ) + 1 );
if ( buf )
strcpy( buf, str );
或
char *buf = strdup( getErrorString( error ) );
>getErrorString
必须记录它称之为malloc
的文档。通常认为编写一个 API 是非常糟糕的做法,其中函数返回指向分配内存的指针,然后期望其他人清理它。我们从 40 年的 C 历史中知道,像这样设计糟糕的 API 正是你到处制造内存泄漏的方式。
更好的 API 将提供一个显式的初始化/创建函数和一个显式的清理/删除函数。这些函数在内部做什么与调用者无关 - 它们只需要确保在任何其他事情之前调用 init 并清理它们所做的最后一件事。
关于 s****y API 设计的主题,我希望函数getErrorString
返回const char*
,因为为什么调用方需要修改错误消息?这应该是一个不可变的字符串。