在C字符串中'\ 0'之后内存会发生什么?

时间:2012-04-16 08:25:02

标签: c string pointers malloc c-strings

令人惊讶的简单/愚蠢/基本问题,但我不知道:假设我想让我的函数的用户返回一个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;
}

11 个答案:

答案 0 :(得分:49)

  

如果我将'\ 0'粘贴到已分配内存的中间,那么

     

(a。)free()仍能正常工作,

  

(b。)'\ 0'之后的空格变得无关紧要?一旦'\ 0'被添加,内存是刚刚返回,还是它坐在那里占用空间直到调用free()?

取决于。通常,当您分配大量堆空间时,系统首先分配虚拟地址空间 - 当您向页面写入时,会分配一些实际物理内存来备份它(当您的操作系统具有虚拟内存时,可能会在以后交换到磁盘)支持)。众所周知,虚拟地址空间的浪费分配与实际物理/交换内存之间的区别允许稀疏阵列在此类操作系统上具有合理的内存效率。

现在,这个虚拟寻址和分页的粒度是内存页面大小 - 可能是4k,8k,16k ......?大多数操作系统都有一个功能,您可以调用它来查找页面大小。所以,如果你做了很多小的分配,那么四舍五入到页面大小是浪费的,如果相对于你真正需要使用的内存量有一个有限的地址空间,那么取决于上述方式的虚拟寻址将无法扩展(例如,具有32位寻址的4GB RAM)。另一方面,如果你有一个64位的进程运行,比如说32GB的RAM,并且正在进行相对较少的这种字符串分配,那么你可以使用大量的虚拟地址空间,并且可以获得更大的页面大小。相当于。

但是 - 请注意在整个缓冲区中写入然后在某个早期点(在这种情况下,一次写入内存将具有后备内存并最终可能以交换结束)之间的区别与使用大缓冲区之间的区别只写入第一位然后终止(在这种情况下,后备存储器仅分配给四舍五入到页面大小的已用空间)。

还值得指出的是,在许多操作系统上,堆内存可能不会返回到操作系统,直到进程终止:相反,malloc / free库会在需要扩展堆时通知操作系统(例如使用{{ UNIX上的1}}或Windows上的sbrk()。从这个意义上讲,VirtualAlloc()内存可供您的进程重用,但不能免费供其他进程使用。某些操作系统会对此进行优化 - 例如,使用独特且可独立释放的内存区域进行非常大的分配。

  

将这个悬空空间留在那里通常是不好的编程风格,为了节省一些前期编程时间,在调用malloc之前计算必要的空间?

同样,这取决于你正在处理多少这样的分配。如果相对于您的虚拟地址空间/ RAM有很多相关内容 - 您希望明确地让内存库知道并非使用free()实际需要所有最初请求的内存,或者您甚至可以使用realloc()根据实际需要更紧密地分配新块(然后strdup()原始) - 取决于你的malloc / free库实现可能会更好或更差,但很少有应用程序会受到任何差异的显着影响。

有时您的代码可能位于库中,您无法猜测调用应用程序将管理多少字符串实例 - 在这种情况下,最好提供从不太糟糕的较慢行为...因此倾向于缩小内存块以适应字符串数据(一组额外的操作,因此不影响大O效率),而不是浪费原始字符串缓冲区的未知比例(在病态情况下 - 任意大后使用零或一个字符)分配)。作为性能优化,如果未使用的空间是> =已用空间 - 调整到味道,或者使其可以调用,则可能只会为返回内存而烦恼。

你评论另一个答案:

  

所以归结为判断realloc是否会花费更长时间,还是预处理大小确定?

如果性能是您的首要任务,那么是 - 您想要分析。如果你不受CPU约束,那么作为一般规则采取“预处理”命中并进行正确大小的分配 - 只有更少的碎片和混乱。如果你必须为某些函数编写一个特殊的预处理模式 - 这是一个额外的“表面”,用于存储错误和代码。 (从free()实施您自己的asprintf()时,通常需要做出这种权衡决定,但至少您可以信任snprintf()作为记录,并且不必亲自维护)。

答案 1 :(得分:33)

  

一旦添加'\ 0',内存是否会被返回,或者是它   坐在那里占用空间,直到free()被叫?

\0没有什么神奇之处。如果要“缩小”已分配的内存,则必须调用realloc。否则,记忆将只是坐在那里,直到你拨打free

  

如果我将“\ 0”粘贴到已分配内存的中间,那么(a。)   free()仍能正常工作

无论你做什么在该内存中 free如果你传递malloc返回的完全相同的指针,它将始终正常工作。当然,如果你在外面写字,所有的赌注都会被取消。

答案 2 :(得分:11)

\0只是mallocfree视角中的另外一个字符,它们不关心您在内存中放入的数据。因此,无论您是在中间添加free还是根本不添加\0\0仍然有效。分配的额外空间仍然存在,只要您将\0添加到内存中,它就不会返回到进程。我个人更愿意只分配所需的内存量而不是在某个上限分配,因为这只会浪费资源。

答案 3 :(得分:7)

\0是将字符数组解释为stings的纯惯例 - 它独立于内存管理。也就是说,如果你想收回你的钱,你应该致电realloc。字符串不关心内存(许多安全问题的根源是什么)。

答案 4 :(得分:7)

只要通过调用malloc()从堆获取内存,就可以使用内存。插入\ 0就像插入任何其他字符一样。这个记忆将一直由你掌握,直到你释放它或直到操作系统声称它为止。

答案 5 :(得分:5)

malloc只是分配了一块内存..它可以根据你的意愿使用,并从初始指针位置自由调用...在中间插入'\ 0'没有任何后果......

具体来说,malloc不知道你想要什么类型的内存(它返回一个void指针)..

让我们假设您希望从0x10到​​0x19分配10个字节的内存。

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

在第5个位置(0x14)插入空值不会释放内存0x15 ......

然而,从0x10释放释放整个10字节的块..

答案 6 :(得分:4)

  1. free()仍将使用内存中的NULL字节

  2. 在调用free()之前,空间将一直被浪费,或者除非您随后缩小分配

答案 7 :(得分:3)

通常,内存是内存是内存。它并不关心你写入它的内容。但它有一个种族,或者如果你喜欢一种味道(malloc,new,VirtualAlloc,HeapAlloc等)。这意味着分配一块内存的一方也必须提供解除分配的方法。如果您的API包含在DLL中,那么它应该提供某种免费功能。 这当然给呼叫者带来了负担吗? 那么为什么不给呼叫者带来 WHOLE 负担呢? 处理动态分配内存的最佳方法是 NOT 自行分配。让呼叫者分配它并将其传递给您。他知道他分配了什么样的味道,并且只要他完成使用它就有责任释放它。

来电者如何知道分配多少? 像许多Windows API一样,您的函数在调用时返回所需的字节数,例如使用NULL指针,然后在提供非NULL指针时执行该作业(如果适合您的情况,则使用IsBadWritePtr来仔细检查可访问性)。

这也可以更有效率。内存分配成本很高。内存分配太多会导致堆碎片,然后分配成本会更高。这就是为什么在内核模式中我们使用所谓的“后备列表”。为了最大限度地减少内存分配的数量,我们使用NT内核为驱动程序编写者提供的服务重用已经分配和“释放”的块。 如果你将内存分配的责任传递给你的调用者,那么他可能会从栈中传递廉价内存(_alloca),或者一遍又一遍地传递相同的内存而无需任何额外的分配。你当然不在乎,但是你允许你的来电者负责最佳的内存处理。

答案 8 :(得分:1)

详细说明在C中使用NULL终止符: 你不能分配一个“C字符串”你可以分配一个char数组并在其中存储一个字符串,但malloc和free只是将它看作一个请求长度的数组。

C字符串不是数据类型,而是使用char数组的约定,其中空字符'\ 0'被视为字符串终止符。 这是一种传递字符串的方法,而不必将长度值作为单独的参数传递。其他一些编程语言具有显式字符串类型,它们与字符数据一起存储长度,以允许在单个参数中传递字符串。

将其参数记录为“C字符串”的函数传递给char数组,但如果没有null终止符,则无法知道数组有多大,所以如果不存在则会出现可怕的错误。

您会注意到期望char数组不一定被视为字符串的函数总是需要传递缓冲区长度参数。 例如,如果要处理零字节为有效值的字符数据,则不能使用'\ 0'作为终止符。

答案 9 :(得分:1)

您可以执行某些MS Windows API所执行的操作(调用方)传递指针以及分配的内存大小。如果大小不够,则告诉您要分配多少字节。如果足够,则使用内存,结果是使用的字节数。

因此,关于如何有效使用内存的决定留给了调用者。它们可以分配固定的255个字节(在Windows中使用路径时很常见)并使用函数调用的结果来知道是否需要更多字节(不是因为MAX_PATH为255而没有绕过Win32 API而导致的路径)或者是否大多数的字节数可以忽略...... 调用者也可以传递零作为内存大小,并准确告知需要分配多少 - 不是有效的处理方式,而是在空间方面更有效。

答案 10 :(得分: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;
    }