将 char 指针分配给运行时生成的字符串文字 - 这是动态分配吗?

Assigning a char pointer to string literal generated at runtime - Is this dynamic allocation?

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

有一个函数设计用于导出到 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 的指针,然后将其分配给一个在运行时生成的字符串,如原始代码所示?似乎内存是在运行时动态分配的,可能是由运行时引擎分配的。这是这段代码发生的事情吗?在这种情况下我的静态数组方法是首选吗?

getErrorString 必须记录其调用 malloc。通常认为编写 API 函数 return 指向已分配内存的指针然后期望其他人清理它是非常糟糕的做法。我们从 40 年的 C 语言历史中了解到,像这样设计不当的 API 正是造成内存泄漏的原因。

更好的 API 会提供显式 init/create 函数和显式 cleanup/delete 函数。这些函数在内部所做的是 none 调用者的业务 - 他们只需要确保在其他任何事情之前调用 init 并清理他们所做的最后一件事。

关于s****y API设计的话题,我希望一个函数getErrorString到return一个const char*,因为为什么会出错消息需要调用者修改?这应该是一个不可变的字符串。

How is this allowed without explicitly calling something like 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;
}

If I were writing equivalent code, my first instinct would be to declare some static array like msg[256] and then do something like: char msg[256] = {""}; sprintf(msg, "%s", getErrorString(error));

这没有多大意义。它表示不必要的额外内存(msg[] 中的 256 字节)和不必要的额外复制(sprintf)。

So my main question is, how can you declare a pointer to char, then assign it to a string which is generated at runtime, as shown in the original code?

您可以始终声明一个指向char的指针,然后为其分配一个在运行时生成的字符串。

不过,这可能是一个令人困惑的问题。您可能听说过字符串在 C 中表示为 char 的数组 — 这是真的 — 您可能还听说过您不能在 C 中分配数组 — 这也是真的。您可能听说过,您总是必须调用 strcpy 而不是直接分配字符串。但这 不一定 一定是真的——您也可以通过简单地分配指针来“分配”字符串,这就是您说 msg = getErrorString(error) 时发生的事情。换句话说,在 C 中有两种完全不同的字符串赋值方式:复制数组,或赋值指针。有关这一点的更多信息,请参阅 this other answer

It seems that memory is being dynamically allocated at runtime

是的,看起来就是这样。

Is my static array method preferred in this case?

正如其他评论所建议的,动态内存分配在这种情况下可能是也可能不是一个好主意。不过,作为一般规则,动态内存分配是一种非常好的——或多或少是重要的——技术。另一方面,静态内存分配本身可能有很多问题。

根据您所描述的代码,getErrorString 显然正在调用 malloccalloc(或某些等效项)本身并 returning 该指针。这实际上是相当普遍的做法 - 请参阅 POSIX strdup 函数作为另一个示例。

要注意的是,负责在您使用完内存后释放该内存。如果 getErrorString 动态分配内存,那么需要记录下来,这样任何使用它的人都会知道 free 当他们用完内存时。

If I were writing equivalent code, my first instinct would be to declare some static array like msg[256] and then do something like:

char msg[256] = {""}; sprintf(msg, "%s", getErrorString(error));

坏主意,因为您丢弃了由 getErrorString 编辑的指针值 return,这意味着您永远无法 free 它分配的内存。

getErrorString 正在为您分配所有必要的内存来存储字符串;您不需要留出自己的缓冲区来存储字符串本身。您只需要存储 returned 指针值,以便稍后 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 ) );
  ...
}

在这种情况下,错误信息被写入msggetErrorString returns msg 的地址,所以它可以作为 fprintf 的一部分被调用;因为它是一个 auto 变量,msg 的内存将在函数退出时自动释放。

或者,如果我愿意,我可以动态分配该内存:

char *msg = calloc( 81, sizeof *msg );
...
fprintf( stderr, "%s\n", 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 不再是 re-entrant 或 thread-safe - 你在整个程序中只有一个缓冲区,所以如果 getErrorString被本身调用 getErrorString 的其他代码中断,或者如果两个线程同时调用 getErrorString,则该缓冲区将被破坏。


作为最后的选择,如果所有的字符串都是常量,那么根本不需要预留任何内存 - 直接return字符串文字:

/**
 * 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, "%s\n", getErrorString( error ) );

如果您出于任何原因仍想留出内存来存储错误字符串,您可以:

const char *str = getErrorString( error );
char *buf = malloc( strlen( str ) + 1 );
if ( buf )
  strcpy( buf, str );

char *buf = strdup( getErrorString( error ) );