34

从我系统上的手册页:

void *memmove(void *dst, const void *src, size_t len);

描述
memmove() 函数将 len 个字节从字符串 src 复制到字符串 dst。
两个字符串可能重叠;复制始终以非破坏性
方式完成。

来自 C99 标准:

6.5.8.5 比较两个指针时,结果取决于所指向对象在地址空间中的相对位置。如果两个指向对象或不完整类型的指针都指向同一个对象,或者都指向同一个数组对象的最后一个元素之后,它们比较相等。如果指向的对象是同一个聚合对象的成员,则指向稍后声明的结构成员的指针比较大于指向结构中较早声明的成员的指针,并且指向具有较大下标值的数组元素的指针比较大于指向同一数组的元素的指针具有较低的下标值。所有指向同一个联合对象成员的指针比较相等。如果表达式P 指向数组对象的一个​​元素,而表达式 Q 指向同一个数组对象的最后一个元素,则指针表达式Q+1 比较大于 P。在所有其他情况下,行为是 undefined

重点是我的。

参数dstsrc可以转换为指针以char减轻严格的别名问题,但是是否可以比较两个可能指向不同块内的指针,以便在它们指向同一块内时以正确的顺序进行复制?

显而易见的解决方案是if (src < dst),但如果srcdst指向不同的块,那是未定义的。“未定义”意味着您甚至不应该假设条件返回 0 或 1(这在标准词汇中被称为“未指定”)。

另一种选择是if ((uintptr_t)src < (uintptr_t)dst),至少未指定,但我不确定标准是否保证src < dst定义时,它等同于(uintptr_t)src < (uintptr_t)dst)。指针比较是从指针算术定义的。例如,当我阅读关于加法的第 6.5.6 节时,在我看来,指针算术可能会朝着与uintptr_t算术相反的方向发展,也就是说,兼容的编译器可能有, whenp是 type char*

((uintptr_t)p)+1==((uintptr_t)(p-1)

这只是一个例子。一般来说,将指针转换为整数时似乎很少保证。

这是一个纯粹的学术问题,因为memmove是与编译器一起提供的。在实践中,编译器作者可以简单地将未定义的指针比较提升为未指定的行为,或者使用相关的 pragma 来强制他们的编译器memmove正确编译它们。例如,这个实现有这个片段:

if ((uintptr_t)dst < (uintptr_t)src) {
            /*
             * As author/maintainer of libc, take advantage of the
             * fact that we know memcpy copies forwards.
             */
            return memcpy(dst, src, len);
    }

我仍然想用这个例子来证明标准在未定义的行为方面走得太远,如果确实memmove不能在标准 C 中有效地实现。例如,在回答这个 SO question时没有人打勾。

4

5 回答 5

23

我认为您是对的,不可能memmove在标准 C 中有效地实现。

我认为,测试区域是否重叠的唯一真正可移植的方法是这样的:

for (size_t l = 0; l < len; ++l) {
    if (src + l == dst) || (src + l == dst + len - 1) {
      // they overlap, so now we can use comparison,
      // and copy forwards or backwards as appropriate.
      ...
      return dst;
    }
}
// No overlap, doesn't matter which direction we copy
return memcpy(dst, src, len);

您不能在可移植代码memcpy有效地实现任何一个或memmove全部,因为无论您做什么,特定于平台的实现都可能会让您大吃一惊。但是便携式至少看起来是合理的。memcpy

C++ 引入了 的指针特化std::less,它被定义为适用于任何两个相同类型的指针。理论上它可能比 慢<,但显然在非分段架构上它不是。

C 没有这样的东西,所以从某种意义上说,C++ 标准同意你的观点,即 C 没有足够的定义行为。但是,C++ 需要它std::map等等。在不了解实现的情况下想要实现std::map(或类似的东西)的可能性比在不知道实现的情况下想要实现(或类似的东西)的可能性要大得多memmove

于 2010-10-26T12:18:52.867 回答
7

为了使两个内存区域有效且重叠,我相信您需要处于 6.5.8.5 定义的情况之一。也就是数组的两个区域,union,struct等。

其他情况未定义的原因是因为两个不同的对象甚至可能不在同一种内存中,具有相同类型的指针。在 PC 架构上,地址通常只是虚拟内存中的 32 位地址,但 C 支持各种奇怪的架构,而内存完全不是这样。

C 未定义事物的原因是在不需要定义情况时为编译器编写者留出余地。阅读 6.5.8.5 的方法是仔细描述 C 想要支持的体系结构,其中指针比较没有意义,除非它在同一个对象内。

此外,编译器提供 memmove 和 memcpy 的原因是,它们有时是使用专门的指令以针对目标 CPU 的优化汇编编写的。它们并不意味着能够以相同的效率在 C 中实现。

于 2010-10-26T11:59:35.200 回答
2

对于初学者来说,C 标准因在此类细节上存在问题而臭名昭著。部分问题是因为 C 在多个平台上使用,并且标准试图足够抽象以涵盖所有当前和未来的平台(可能使用一些我们从未见过的复杂的内存布局)。为了让编译器编写者为目标平台“做正确的事”,有许多未定义或特定于实现的行为。包括每个平台的详细信息是不切实际的(并且经常过时);相反,C 标准让编译器编写者记录在这些情况下会发生什么。“未指定”的行为仅意味着 C 标准没有指定会发生什么,不一定意味着无法预测结果。

由于确定两个指针是否指向同一个块、内存段或地址空间取决于该平台的内存是如何布局的,因此该规范没有定义一种方法来进行确定。它假设编译器知道如何做出这个决定。您引用的规范部分说指针比较的结果取决于指针的“地址空间中的相对位置”。请注意,“地址空间”在这里是单数的。本节仅指同一地址空间中的指针;即直接可比较的指针。如果指针位于不同的地址空间,则结果不是 C 标准定义的,而是由目标平台的要求定义的。

在 的情况下memmove,实现者通常首先确定地址是否可直接比较。如果不是,则该功能的其余部分是特定于平台的。大多数时候,在不同的内存空间中足以确保区域不重叠并且函数变成memcpy. 如果地址可以直接比较,那么它只是一个简单的字节复制过程,从第一个字节开始向前或从最后一个字节向后向后(无论哪个可以安全地复制数据而不会破坏任何内容)。

总而言之,C 标准在无法编写适用于任何目标平台的简单规则的地方故意未指定很多内容。然而,标准作者本可以更好地解释为什么有些事情没有定义并使用更具描述性的术语,如“架构相关”。

于 2010-10-26T13:13:49.593 回答
1

这是另一个想法,但我不知道它是否正确。为了避免O(len)史蒂夫回答中的循环,可以将它放在带有强制转换为实现#else的 an 子句中。假设只要偏移量对指针有效,to 的转换就会通过添加整数偏移量来实现,这使得指针比较明确定义。#ifdef UINTPTR_MAXuintptr_tunsigned char *uintptr_t

我不确定这种交换性是否由标准定义,但它是有道理的,因为即使只有指针的低位是实际数字地址并且高位是某种黑匣子,它也可以工作。

于 2010-10-26T15:45:53.280 回答
0

如果 memmove 确实不能在标准 C 中有效地实现,我仍然想用这个例子来证明标准在未定义的行为方面走得太远了

但这不是证据。绝对没有办法保证您可以在任意机器架构上比较两个任意指针。C 标准甚至编译器都无法规定这种指针比较的行为。我可以想象一台具有分段架构的机器可能会根据段在 RAM 中的组织方式产生不同的结果,或者甚至可能在比较指向不同段的指针时选择抛出异常。这就是行为“未定义”的原因。在完全相同的机器上完全相同的程序可能会在每次运行时给出不同的结果。

memmove() 经常给出的“解决方案”使用两个指针的关系来选择是从头到尾复制还是从尾到头复制只有在所有内存块都从相同地址空间分配的情况下才有效。幸运的是,尽管在 16 位 x86 代码时代并非如此,但通常都是这种情况。

于 2010-10-26T13:54:28.553 回答