在 C 语言字符串中,'\0' 后面的内存会发生什么?

66

这个问题看起来很简单/愚蠢/基础,但我不知道:假设我想向我的函数用户返回一个C字符串,但我不知道函数开始时它的长度。我只能在一开始就给定一个上限,并且根据处理情况,大小可能会缩小。

问题是,是否有问题在处理期间分配足够的堆空间(上限),然后在字符串远未占满分配的内存的情况下终止呢?也就是说,如果我在分配的内存中间插入 '\0',那么 (a.) free() 仍然可以正常工作吗? (b.) '\0' 后面的空间是否变得无关紧要?加了 '\0' 后,内存只是被释放还是一直占用着空间直到调用 free()?为了节省一些前期计算 malloc 所需空间的时间,留下这个悬挂的空间通常是不良的编程风格吗?

为了更好地理解,假设我想删除连续的重复项,就像这样:

输入 "Hello oOOOo !!" --> 输出 "Helo oOo !"

...以下代码显示了如何预先计算我的操作结果的大小,以有效地进行两次处理以正确获取堆大小。

char* RemoveChains(const char* str)
{
    if (str == NULL) {
        return NULL;
    }
    if (strlen(str) == 0) {
        char* outstr = (char*)malloc(1);
        *outstr = '\0';
        return outstr;
    }
    const char* original = str; // for reuse
    char prev = *str++;       // [prev][str][str+1]...
    unsigned int outlen = 1;  // first char auto-counted

    // Determine length necessary by mimicking processing
    while (*str) {
        if (*str != prev) { // new char encountered
            ++outlen;
            prev = *str; // restart chain
        }
        ++str; // step pointer along input
    }

    // Declare new string to be perfect size
    char* outstr = (char*)malloc(outlen + 1);
    outstr[outlen] = '\0';
    outstr[0] = original[0];
    outlen = 1;

    // Construct output
    prev = *original++;
    while (*original) {
        if (*original != prev) {
            outstr[outlen++] = *original;
            prev = *original;
        }
        ++original;
    }
    return outstr;
}

14
请注意,要求调用者使用 free() 来释放从函数返回的对象是不好的风格,因为调用者可能链接到不同的 C 库,并且这也会阻止您在未来使用不同的分配器。您应该提供一个小的包装函数来释放从库中返回的字符串。 - Simon Richter
1
你的包装函数只需在指针上调用free(),但这现在是一个实现细节。如果你更改RemoveChains()以使用不同的分配函数,你也可以适应包装器,并且现有程序将继续运行。 - Simon Richter
1
你可以通过最初使用malloc分配一个足够大但不要太大(例如256字节)的缓冲区来管理未知大小。然后,你可以向该缓冲区写入数据,并跟踪剩余空间的大小。如果空间不足,则使用两倍大小(例如512)进行realloc,并继续写入数据。反复执行此操作。重新分配所花费的总时间最坏情况下为O(n),其中n是最终长度,在许多情况下,它将是O(log n),因为如果缓冲区后面有足够的未分配空间,realloc不必复制数据。你可以在结束时使用正确的大小进行realloc - Nicu Stiurca
@SimonRichter:如果调用者无法释放库中malloc的结果,你永远无法让你的代码工作。解决方案不是绕过问题,而是改变导致问题的工具。 - gnasher729
@gnasher729,如果你规定了库函数调用的结果应该使用free()进行解除分配,那么这与告诉人们要使用FreeStringFromRemoveChains()一样是惯例--也就是说,你没有赢得任何东西,但你失去了以后引入更高效的分配器的灵活性(因为分配器成为API的一部分),并且在库链接到不同的C库的调用者时引入了一个微妙的错误。 - Simon Richter
显示剩余6条评论
11个回答

1

你可以预先分配到一个上限,使用全部或者更少的空间。

只要确保你实际上使用了全部或者更少的空间即可。

进行两次遍历也是可以的。

你对权衡方面提出了正确的问题。

如何做出决策?

最初使用两次遍历,因为:

1. you'll know you aren't wasting memory.
2. you're going to profile to find out where
   you need to optimize for speed anyway.
3. upperbounds are hard to get right before
   you've written and tested and modified and
   used and updated the code in response to new
   requirements for a while.
4. simplest thing that could possibly work.

你可以稍微优化一下代码。 通常来说,代码越短越好。而且,如果代码能够充分利用已知的真相,我就更有信心它能够达到预期的效果。

char* copyWithoutDuplicateChains(const char* str)
    {
    if (str == NULL) return NULL;

    const char* s = str;
    char prev = *s;               // [prev][s+1]...
    unsigned int outlen = 1;      // first character counted

    // Determine length necessary by mimicking processing

    while (*s)
        { while (*++s == prev);  // skip duplicates
          ++outlen;              // new character encountered
          prev = *s;             // restart chain
        }

    // Construct output

    char* outstr = (char*)malloc(outlen);
    s = str;
    *outstr++ = *s;               // first character copied
    while (*s)
        { while (*++s == prev);   // skip duplicates
          *outstr++ = *s;         // copy new character
        }

    // done

    return outstr;
    }

1
在C语言中,不需要将malloc的返回值转换。 - Johann Gerell

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