在 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个回答

51
如果我在分配的内存中间插入 '\0',那么 (a.) free() 仍然能正常工作吗? (b.) '\0' 后面的空间是否变得无关紧要?一旦添加了 '\0',内存是否被返回,还是一直占用空间直到调用 free()?
(a.) 是的。
(b.) 取决于情况。通常,当你分配大量堆空间时,系统会首先分配虚拟地址空间 - 当你写入页面时,一些实际的物理内存被分配来支持它(当你的操作系统具有虚拟内存支持时,这些内存可能稍后被交换到磁盘)。众所周知,虚拟地址空间和实际物理/交换内存之间的区别允许在此类操作系统上稀疏数组具有合理的内存效率。
现在,这种虚拟寻址和页面大小的粒度在内存页大小上进行 - 这可能是4k、8k、16k等?大多数操作系统都有一个函数,你可以调用它来找出页面大小。因此,如果你正在进行大量的小型分配,那么将其舍入到页面大小就是浪费的,而且如果相对于你真正需要使用的内存量,你有限制的地址空间,那么依赖于上面描述的虚拟寻址的方式将不能扩展(例如,32位寻址的4GB RAM)。另一方面,如果你有一个运行在64位进程中的32GB RAM,并且只做相对较少的字符串分配,则有巨大的虚拟地址空间可供使用,将其舍入到页面大小几乎没有影响。
但请注意,在整个缓冲区中写入然后在某个早期点终止它(在这种情况下,曾经写入的内存将具有支持内存,并可能最终进入交换)与有一个大缓冲区,在其中你只写入第一位然后终止(在这种情况下,仅为用于四舍五入到页面大小的已用空间分配支持内存)。值得一提的是,在许多操作系统上,堆内存可能直到进程终止时才会被返回给操作系统:相反,malloc/free库在需要增加堆(例如在UNIX上使用sbrk()或在Windows上使用VirtualAlloc())时通知操作系统。从这个意义上说,free()内存对于您的进程可以重新使用,但其他进程不能使用。某些操作系统会优化此功能-例如,为非常大的分配使用不同且独立可释放的内存区域。
“一般来说,为了节省预先计算必要空间的时间而留下这些挂起空间是否是一个不好的编程风格呢?”这取决于您处理的此类分配数量。如果有很多,相对于您的虚拟地址空间/ RAM,您需要明确地让内存库知道实际上并不需要所有最初请求的内存,可以使用realloc(),甚至可以使用strdup()根据实际需要更紧密地分配新块(然后释放原始块)-这取决于您的malloc/free库实现,可能会更好或更差,但很少有应用程序会受到任何差异的显着影响。
有时您的代码可能在库中,您无法猜测调用应用程序将管理多少字符串实例-在这种情况下,最好提供永远不会太差的较慢行为...因此倾向于缩小内存块以适应字符串数据(固定数量的其他操作,因此不会影响大O效率),而不是在原始字符串缓冲区中浪费未知比例的空间(在病态情况下-零或一个字符用于任意大的分配之后)。作为性能优化,如果未使用空间> =使用空间,则您可能只需要返回内存-根据口味进行调整,或使其成为调用者可配置的。
你对另一个答案发表评论:“因此,问题在于判断realloc会花费更长时间还是预处理大小确定会花费更长时间?”

如果性能是您的首要考虑因素,那么是的-您需要进行性能分析。如果您不受 CPU 限制,那么通常情况下,请接受“预处理”开销并进行合适大小的内存分配 - 这样就会减少碎片化和混乱。然而,如果您必须为某些函数编写特殊的预处理模式-那么这就是额外的错误和代码维护“表面”(extra "surface")。(在实现自己的 asprintf()snprintf() 时,这种权衡决策通常是必要的,但至少您可以相信 snprintf() 的行为符合文档说明,并且不需要个人维护它)。


小小的澄清:尽管如果您的程序尚未实际访问额外的内存,操作系统可能不认为它是“已使用”的,但是,直到您使用free()释放该块或使用较小的大小进行realloc(),它将被malloc()和相关函数视为“已使用”。 - Wyzard
如果整个块已经存储了数据,例如长字符串随后缩小的情况,那么这些页面将被操作系统视为正在使用中,因此如果内存紧张,它们必须被分页出去(而不是仅仅被丢弃),即使您的程序实际上并不关心内容。 - Wyzard

35

如果在分配的内存中加入了'\0',那么该内存会被释放吗?还是会一直占用内存直到调用free()函数?

在这里没有什么关于\0的神奇之处。如果你想要"缩小"已经分配好的内存,你必须调用realloc函数。否则,内存将一直占用直到你调用free函数。

如果我在已分配的内存中间插入一个'\0',那么(a.) free()函数是否仍然能正常工作?

无论你在内存中做了什么,只要你传递给free函数与malloc函数返回的指针完全相同,free函数就始终可以正常工作。当然,如果你在它之外写入,则一切皆有可能。


谢谢,我明白了。那么问题就在于判断realloc是否会花更长时间,还是预处理大小确定会花更长时间? - Erika Electra

11

\0mallocfree 的角度来看只是一个普通字符,它们不关心您将什么数据放入内存中。因此,无论您在中间添加 \0 还是根本不添加 \0free 都会正常工作。额外分配的空间仍然存在,只要您在内存中添加了 \0,这些空间就不会立即返回给进程。我个人更喜欢只分配所需的内存量,而不是在某个上限处分配内存,这样只会浪费资源。


7

\0是将字符数组解释为字符串的纯约定,与内存管理无关。也就是说,如果你想要回收你的内存,应该调用realloc函数。字符串不关心内存(这是许多安全问题的根源)。


7

当您调用malloc()从堆中获取内存时,该内存就归您使用。插入\0就像插入任何其他字符一样。这块内存将一直属于您,直到您释放它或操作系统回收它。


5

malloc函数只是分配一块内存,你可以随意使用并从初始指针位置调用free函数进行释放。在中间插入'\0'没有任何影响。

具体来说,malloc函数不知道你想要哪种类型的内存(它仅返回一个void指针)。

假设你希望分配10个字节的内存,起始地址为0x10到0x19。

char * ptr = (char *)malloc(sizeof(char) * 10);

在第5个位置(0x14)插入null并不会释放0x15及其后面的内存...

然而,从0x10处释放将释放整个10字节的块。


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

4
  1. free()函数在内存中仍然可以使用NUL字节。

  2. 空间将会一直浪费,直到调用free()函数或者你随后缩小了分配的空间。


3
通常来说,内存就是内存,不管你往里面写什么。但是它有一个“种族”,或者如果你喜欢的话是一种“口味”(如malloc、new、VirtualAlloc、HeapAlloc等)。这意味着分配一块内存的一方必须提供处理内存释放的手段。如果您的API在DLL中,那么它应该提供某种形式的free函数。
当然,这会给调用者带来负担,对吧?
那么为什么不把全部负担都放在调用者身上呢?
处理动态分配内存的最佳方法是不要自己分配内存。让调用者分配并将其传递给您。他知道他分配了哪种口味,并且他负责在使用完后释放它。
调用者如何知道要分配多少内存?
与许多Windows API一样,当使用NULL指针调用函数时,让您的函数返回所需的字节数,然后在提供非NULL指针时执行任务(如果适用于您的情况,则使用IsBadWritePtr来双重检查可访问性)。
这也可以更加高效。内存分配成本很高。过多的内存分配会导致堆碎片化,然后分配成本会更高。这就是为什么在内核模式下我们使用所谓的“look-aside列表”。为了最小化已经分配和“释放”的块的数量,我们重用它们,使用NT内核为驱动程序编写者提供的服务。
如果您将内存分配的责任交给调用者,那么他可能会将廉价的内存从堆栈(_alloca)传递给您,或者反复传递相同的内存而不进行任何其他分配。您当然不在意,但是您确实允许您的调用者负责优化内存处理。

1
关于在C语言中使用NULL终止符的解释: 你不能分配一个“C字符串”,但可以分配一个char数组并将字符串存储在其中,但是malloc和free只会将其视为请求长度的数组。
C字符串不是一种数据类型,而是一种使用char数组的约定,其中空字符'\0'被视为字符串终止符。这是一种传递字符串的方式,无需传递长度值作为单独的参数。其他一些编程语言具有显式的字符串类型,可以存储与字符数据一起的长度,以允许在单个参数中传递字符串。
将其参数记录为“C字符串”的函数传递char数组,但没有办法知道数组的大小,因此如果没有终止符,事情将变得非常糟糕。
您将注意到,期望不一定被视为字符串的char数组的函数始终需要传递缓冲区长度参数。例如,如果要处理零字节是有效值的char数据,则无法使用'\0'作为终止字符。

1

你可以像一些MS Windows API那样做,其中你(调用者)传递一个指针和你分配的内存大小。如果大小不够,会告诉你需要分配多少字节。如果足够,就使用内存,结果是使用的字节数。

因此,如何有效地使用内存的决定留给了调用者。他们可以分配固定的255字节(在Windows中处理路径时很常见),并使用函数调用的结果来知道是否需要更多字节(由于MAX_PATH为255而不绕过Win32 API,这种情况并不适用于路径)或者是否可以忽略大部分字节...调用者还可以将内存大小设置为零,并被告知需要分配多少内存-虽然在处理方面不太高效,但在空间方面可能更高效。


我应该说,在回答你的问题之前,这里的其他人也是正确的——malloc/free等不关心字符数组中是否有\0。许多字符串函数则会关心。此外,如果你要返回一个建议的分配大小,请确保在合同中清楚地说明返回的大小是否包括或排除尾随的\0字节 :) (或者它是字符数还是字节数——通常是字节,但最好具体说明!)在被文档不完善的API调用烧伤后,我养成了额外分配一个字节的习惯。 - Ian Yates

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