c语言 - 将 char 指针分配给运行时生成的字符串文本 - 这是动态分配



我正在审查别人写的一些代码。我遇到了一个有趣的案例,涉及此代码中的字符串,我需要帮助来理解它是如何工作的。

有一个旨在导出到 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显然调用malloccalloc(或等效物)本身并返回该指针。 这实际上是相当普遍的做法 - 参见 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*,因为为什么调用方需要修改错误消息?这应该是一个不可变的字符串。

最新更新