Cstring中'\ 0'之后的内存会发生什么?

令人惊讶的简单/愚蠢/基本的问题,但我不知道:假设我想返回我的函数的用户一个Cstring,其长度我不知道在函数的开始。 我可以在一开始只放置长度的上限,并且根据处理,尺寸可能缩小。

问题是,分配足够的堆空间(上限)是否有问题,然后在处理过程中终止string? (a。) free()仍然正常工作,(b。)在'\ 0'之后的空格变得无关紧要了吗? 一旦添加了“\ 0”,内存是否返回,还是坐在那里占用空间,直到free()被调用? 为了节省一些前期编程时间,在调用malloc之前计算必要的空间,通常是不好的编程风格吗?

为了给这个上下文,我们假设我想删除连续的重复,像这样:

input“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; } 

如果我在分配的内存中间插入一个'\ 0',

(a。)免费()仍然正常工作,并

是。

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

依靠。 通常,当你分配大量的堆空间时,系统首先分配虚拟地址空间 – 当你写入页面的时候,一些实际的物理内存被分配给它(当你的操作系统具有虚拟内存时,它可能会被换出到磁盘支持)。 有趣的是,虚拟地址空间的浪费分配和实际的物理/交换内存之间的区别允许稀疏arrays在这些操作系统上具有合理的内存效率。

现在,这个虚拟寻址和分页的粒度是在内存页面大小 – 可能是4k,8k,16k …? 大多数操作系统都有一个函数可以调用来查找页面大小。 所以,如果你做了很多小的分配,那么四舍五入到页面大小是浪费的,如果你有一个有限的地址空间相对于你真正需要使用的内存量,那么取决于虚拟寻址将不会缩放(例如,32位寻址的4GB RAM)。 另一方面,如果你有一个64位的进程运行32GB的内存,并且这样做的string分配相对较less,那么你就有了大量的虚拟地址空间可供使用,吨金额很多。

但是 – 注意写入整个缓冲区之间的区别,然后在某个更早的时间点终止它(在这种情况下,一次写入的内存将有后备内存,并可能以交换结束),而不是有一个大的缓冲区,你只写到第一个位然后终止(在这种情况下,后备存储器仅分配给用完的页面大小的空间)。

还有一点值得指出的是,在许多操作系统上堆内存可能不会返回到操作系统,直到进程终止:相反,malloc / free库通知操作系统何时需要增长堆(例如,在UNIX上使用sbrk()或Windows上的VirtualAlloc() )。 从这个意义上说, free()内存对于你的进程是可以重用的,但是对于其他进程来说是免费的。 某些操作系统会优化这一点 – 例如,对于非常大的分配,使用不同且独立可释放的内存区域。

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

再一次,这取决于你处理多less这样的分配。 如果相对于你的虚拟地址空间/ RAM有很多相关信息 – 你想明确地让内存库知道并不是所有最初请求的内存实际上都需要使用realloc() ,或者你甚至可以使用strdup()来分配一个新的根据实际的需要(然后free()原来的)更紧密地阻塞 – 取决于你的malloc /免费库的实现可能会更好或更糟,但是很less有应用程序会受到任何差异的显着影响。

有时你的代码可能在一个库中,你无法猜测调用应用程序将要pipe理多less个string实例 – 在这种情况下,最好提供一些速度较慢的行为,这样做永远不会太坏…所以,我们倾向于缩小内存块(在一个病态的情况下 – 在任意大的分配之后使用零个或一个字符),而不是在原始string缓冲区中有一个未知比例的string数据(一组额外的操作,因此不会影响大的O效率)。 作为一个性能优化,如果不使用的空间大于或等于已使用的空间,则可能只会打扰返回的内存 – 调整味道,或使调用者可configuration。

您对另一个答案发表评论:

那么判断realloc是否需要更长的时间,或者是预处理的大小的确定呢?

如果performance是你的首要任务,那么是的 – 你想要个人资料。 如果你不是CPU绑定的话,那么一般来说就是采用“预处理”命令并做一个合适的分配 – 这样就不会出现碎片和混乱。 反驳,如果你必须为一些function编写一个特殊的预处理模式 – 这是一个额外的“表面”的错误和代码来维护。 (当从snprintf()实现你自己的asprintf()时,通常需要这种权衡决定,但是至less你可以信任snprintf()按照logging的方式行事,而不必个人维护)。

一旦添加了“\ 0”,内存是否返回,还是坐在那里占用空间,直到free()被调用?

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

如果我在分配的内存中间插入一个'\ 0',(a。)free()仍然正常工作

无论你在那个 free 内存里做什么如果你通过malloc返回完全相同的指针,它总能正常工作。 当然,如果你在外面写下所有的赌注,

\0只是mallocfree一个字符,他们不关心你把什么数据放在内存中。 因此,如果您在中间添加\0或者根本不添加\0 ,那么free仍然可以正常工作。 分配的额外空间将仍然存在,只要将\0添加到内存中,它就不会返回到进程。 我个人宁愿只分配所需的内存量,而不是分配一些上限,因为这只会浪费资源。

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

\0是将字符数组解读为一个纯粹的约定 – 它独立于内存pipe理。 也就是说,如果你想把钱还给你,你应该叫realloc 。 该string不关心内存(什么是许多安全问题的来源)。

malloc只是分配一块内存..它的upto你使用,但是你想和从初始指针位置释放…在中间插入'\ 0'没有任何结果…

具体malloc不知道你想要什么types的内存(它返回一个void指针)。

让我们假设你想分配10个字节的内存从0x10开始到0x19 ..

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

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

然而,一个免费的0x10释放了10个字节的整个块。

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

  2. 该空间将保持浪费,直到free()被调用,或除非您随后缩小分配

一般来说,内存是内存就是内存。 它并不在乎你写的东西。 但它有一个种族,或者如果你喜欢的味道(malloc,新的,VirtualAlloc,HeapAlloc等)。 这意味着分配一块内存的一方也必须提供解除分配的手段。 如果你的API来自一个DLL,那么它应该提供一些免费的function。 这当然会给调用者带来负担吗? 那么为什么不把呼叫者的整个负担? 处理dynamic分配内存的最佳方法是不要自己分配。 让调用者分配它并传递给你。 他知道他分配了什么味道,每当他使用它,他有责任释放它。

来电者如何知道要分配多less? 像许多Windows API一样,你的函数在被调用的时候返回所需要的字节数,例如用一个NULL指针,然后在提供一个非NULL指针时使用IsBadWritePtr(如果它适合你的情况来仔细检查可访问性)。

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

为了详细说明在C中使用NULL终止符:你不能分配一个“Cstring”,你可以分配一个字符数组并存储一个string,但malloc和free只是将它视为一个所需长度的数组。

ACstring不是数据types,而是使用空字符“\ 0”作为string终止符的char数组的约定。 这是传递string的一种方式,而不必将长度值作为单独的parameter passing。 其他一些编程语言具有明确的stringtypes,它们与字符数据一起存储长度以允许在单个参数中传递string。

将它们的参数logging为“Cstring”的函数被传递给char数组,但是没有办法知道没有null终止符的数组有多大,所以如果不是,那么事情就会变得非常糟糕。

你会注意到函数,期望字符数组,不一定被视为string将始终需要缓冲区长度parameter passing。 例如,如果要处理零字节为有效值的char数据,则不能使用'\ 0'作为终止符。

你可以做一些MS Windows API,你(调用者)传递一个指针和你分配的内存的大小。 如果大小不够,则会告知您要分配多less个字节。 如果足够,则使用内存,结果是使用的字节数。

因此,关于如何有效使用内存的决定留给了调用者。 他们可以分配固定的255字节(在Windows中使用path时通用),并使用函数调用的结果来知道是否需要更多的字节(而不是由于MAX_PATH为255而不绕过Win32 API的path)或者大多数的字节可以被忽略…调用者也可以传递零作为内存大小,并且被准确地告知需要分配多less – 不是有效的处理方式,而是更有效的空间方式。

你当然可以预分配给一个上限,并使用全部或更less的东西。 只要确保你实际上使用全部或less些东西。

两次传球也很好。

你问了关于权衡的正确问题。

你怎么决定的?

最初使用两遍,因为:

 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; }