C 字符串中"\0"之后的内存会发生什么变化?



令人惊讶的简单/愚蠢/基本问题,但我不知道:假设我想向函数的用户返回一个 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 = '';
        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] = '';
    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.) free() 仍然正常工作,并且

是的。

(b.) "\0"后面的空格是否变得无关紧要?添加"\0"后,内存是被返回,还是坐在那里占用空间直到调用 free()?

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

现在,这种虚拟寻址和分页的粒度以内存页面大小为单位 - 可能是 4k、8k、16k...? 大多数操作系统都有一个函数,您可以调用该函数来找出页面大小。 因此,如果您正在执行大量小分配,那么四舍五入到页面大小是浪费,并且如果您的地址空间相对于您真正需要使用的内存量有限,那么以上述方式依赖虚拟寻址将无法扩展(例如,具有 32 位寻址的 4GB RAM)。 另一方面,如果您有一个使用 32GB RAM 运行的 64 位进程,并且执行的此类字符串分配相对较少,那么您有大量的虚拟地址空间可供使用,并且四舍五入到页面大小不会太多。

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

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

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

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

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

你评论另一个答案:

因此,归根结底是判断 realloc 需要更长的时间,还是预处理尺寸的确定?

如果性能是您的首要任务,那么是的 - 您需要分析。 如果您不受 CPU 限制,那么作为一般规则,接受"预处理"命中并执行适当大小的分配 - 只是碎片和混乱更少。 与此相反,如果你必须为某些函数编写一个特殊的预处理模式 - 这是一个额外的"表面",用于维护错误和代码。 (从snprintf()实现自己的asprintf()时通常需要这种权衡决策,但至少您可以信任snprintf()按照文档进行操作,而不必亲自维护它)。

添加"\0"后,内存是刚刚返回,还是返回 坐在那里占用空间直到调用 free()?

没有什么神奇的.如果要"缩小"分配的内存,则必须调用realloc。否则,内存将一直坐在那里,直到您调用free.

如果我在分配的内存中间粘贴一个"\0",(a.) free() 仍然正常工作

无论您

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

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

一旦你通过调用 malloc() 从堆中获取内存,内存就是你可以使用的。插入 \0 就像插入任何其他字符一样。此内存将一直由您拥有,直到您释放它或直到操作系统将其收回。

字符数组解释为刺痛的纯约定 - 它独立于内存管理。 也就是说,如果你想拿回你的钱,你应该打电话给realloc。字符串不关心内存(这是许多安全问题的根源)。

malloc 只是分配了一大块内存..您可以随心所欲地使用并独立于初始指针位置进行调用......在中间插入"\0"没有任何后果...

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

假设您希望从0x10开始分配 10 字节的内存以0x19 ..

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

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

但是,从0x10释放整个10字节块。

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

  2. 在调用free()之前,空间将保持浪费状态,或者除非您随后缩小分配

一般来说,记忆就是记忆就是记忆。它不在乎你写进去什么。但是它有一个种族,或者如果你更喜欢一种口味(malloc,new,VirtualAlloc,HeapAlloc等)。这意味着分配一段内存的一方还必须提供释放它的方法。如果您的 API 以 DLL 形式出现,那么它应该提供某种免费函数。这当然会给来电者带来负担,对吧?那么为什么不把整个负担放在呼叫者身上呢?处理动态分配内存的最佳方法是不要自己分配它。让呼叫者分配它并将其传递给您。他知道自己分配了什么口味,并且每当他使用完它时,他都有责任释放它。

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

这也可以更有效率。内存分配成本很高。过多的内存分配会导致堆碎片,然后分配的成本更高。这就是为什么在内核模式下我们使用所谓的"旁路列表"。为了尽量减少完成的内存分配次数,我们使用 NT 内核提供给驱动程序编写器的服务重用已分配和"释放"的块。如果您将内存分配的责任传递给调用方,那么他可能会从堆栈(_alloca)中传递廉价内存,或者一遍又一遍地传递相同的内存而不进行任何额外的分配。您当然不在乎,但您确实允许调用方负责最佳内存处理。

详细说明 NULL 终止符在 C 中的用法:您不能分配"C 字符串",您可以分配一个 char 数组并在其中存储一个字符串,但 malloc 和 free 只是将其视为请求长度的数组。

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

将其

参数记录为"C 字符串"的函数被传递 char 数组,但如果没有空终止符,就无法知道数组有多大,所以如果没有它,事情就会出错。

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

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

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

您当然可以预先分配到上限,并使用全部或更少。只要确保你实际使用全部或更少的东西。

做两次也可以。

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

你怎么决定?

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

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

相关内容

  • 没有找到相关文章

最新更新