如何在没有中间拷贝的情况下在标准C中实现memmove?
从我的系统上的手册页:
void * memmove(void * dst,const void * src,size_t len);
描述
memmove()函数将len字节从stringsrc复制到stringdst。
这两个string可能重叠 ; 副本总是在一个非破坏性的
方式。
从C99标准:
6.5.8.5比较两个指针时,结果取决于指向对象的地址空间中的相对位置。 如果两个指向对象或不完整types的指针指向同一个对象,或者两个指向同一个数组对象的最后一个元素,则它们相等。 如果指向的对象是同一个聚合对象的成员,则稍后声明的结构成员的指针比结构中较早声明的成员的指针要多,指向具有较大下标值的数组元素的指针比指向同一数组元素的指针具有较低的下标值。 所有指向同一联合对象的成员的指针相等。 如果expression式
P
指向数组对象的元素,并且expression式Q指向相同数组对象的最后一个元素,则指针expression式Q+1
将比P
。 在所有其他情况下,行为是不确定的 。
重点是我的。
参数dst
和src
可以转换为指向char
指针,以减轻严格的别名问题,但是可以比较指向不同块的两个指针,以便在指向内部时按照正确的顺序进行复制同一块?
显而易见的解决scheme是if (src < dst)
,但是如果src
和dst
指向不同的块,那么它是不确定的。 “未定义”意味着你甚至不应该假设条件返回0或1(这在标准的词汇中被称为“未指定”)。
另一种方法是if ((uintptr_t)src < (uintptr_t)dst)
至less没有被指定,但是我不确定标准是否保证当src < dst
被定义时,它相当于(uintptr_t)src < (uintptr_t)dst)
。 指针比较是从指针算术定义的。 例如,当我阅读第6.5.6节的内容时,在我看来,指针运算可能与uintptr_t
运算的方向相反,也就是说,如果p
是char*
types,
((uintptr_t)p)+1==((uintptr_t)(p-1)
这只是一个例子。 一般来说,将指针转换为整数时似乎很less有保证。
这是一个纯粹的学术问题,因为memmove
与编译器一起提供。 实际上,编译器作者可以简单地将未定义的指针比较为未指定的行为,或者使用相关的编译指令来强制编译器正确编译它们的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); }
我仍然希望用这个例子来certificate标准C中未能定义的标准,如果memmove
不能在标准C中有效地实现,那么这个标准就太过分了。例如,在回答这个问题时没有人勾选。
我认为你是对的,不可能在标准C中有效地实施memmove
。
我认为,唯一真正便携的方法来testing区域是否重叠,如下所示:
[编辑:稍后看,我认为它应该是第二行结束时的dst+len-1
。 但是我不能为了testing而烦恼,所以我现在就要离开了,可能我第一次知道我在说什么。]
for (size_t l = 0; l < len; ++l) { if (src + l == dst) || (src + l == dst + len) { // 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
至less貌似合理。
C ++引入了std::less
的指针专用化,它定义为适用于任何两个相同types的指针。 理论上可能比<
慢,但是显然在非分段的架构上它不是。
C没有这样的事情,所以从某种意义上说,C ++标准同意你C没有足够的定义行为。 但是,C ++需要它为std::map
等等。 如果没有实现知识的话,你想要实现std::map
(或类似的东西)的可能性大大高于你想实现memmove
(或类似的东西)的知识。
对于两个内存区域是有效的和重叠的,我相信你需要处于6.5.8.5定义的情况之一。 也就是说,数组的两个区域,联合,结构等
其他情况未定义的原因是因为两个不同的对象甚至可能不在相同types的存储器中,具有相同types的指针。 在PC体系结构中,地址通常只是虚拟内存中的32位地址,但C支持各种奇怪的体系结构,其中内存就是这样的。
C没有定义的原因是在不需要定义的情况下给编译器编写者留有余地。 阅读6.5.8.5的方法是仔细地描述C想要支持的地方,在指针比较没有意义的地方,除非它在同一个对象内。
此外,memmove和memcpy由编译器提供的原因是它们有时使用专用指令写入目标CPU的调谐程序集。 它们并不意味着能够以相同的效率在C中实现。
对于初学者来说,C标准在像这样的细节上出现问题是臭名昭着的。 部分问题是因为C在多个平台上使用,并且标准尝试抽象得足以覆盖当前和未来的所有平台(这可能会使用一些超出我们所见过的任何复杂的内存布局)。 编译器编写者为目标平台“做正确的事情”有很多未定义或特定于实现的行为。 包括每个平台的细节将是不切实际(并且不断过时); 相反,C标准留给了编译器编写者来logging在这些情况下会发生什么。 “未指定”的行为仅仅意味着C标准没有具体说明发生了什么,不一定是结果不能被预测。 如果您阅读目标平台和编译器的文档,结果通常仍然是可预测的。
由于确定两个指针是指向同一个块,内存段还是地址空间取决于该平台的内存如何布置,因此规范没有定义一个确定的方法。 它假设编译器知道如何做出这个决定。 你引用的那部分规范说,指针比较的结果取决于指针在地址空间中的相对位置。 请注意,“地址空间”在这里是单数。 这部分只是指在同一地址空间的指针; 也就是可以直接比较的指针。 如果这些指针位于不同的地址空间中,那么C标准将不确定结果,而是由目标平台的要求来定义。
在移动的情况下,实现者通常首先确定地址是否可直接比较。 如果没有,那么function的其余部分是特定于平台的。 大多数情况下,处于不同的内存空间足以确保区域不重叠,函数变成memcpy
。 如果地址是直接可比较的,那么它只是一个简单的字节复制过程,从第一个字节开始往前,或者从最后一个字节开始向后(无论哪一个将安全地复制数据而不会破坏任何东西)。
总而言之,C标准在有意无意的地方留下了许多不能写出一个简单的规则在任何目标平台上工作的情况。 然而,标准作者可以做得更好,解释为什么有些东西没有定义,并使用更多的描述性术语,如“依赖于架构”。
这是另一个想法,但我不知道是否正确。 为了避免Steve的回答中的O(len)
循环,可以使用cast-to- uintptr_t
实现将其放在#ifdef UINTPTR_MAX
的#else
子句中。 假如unsigned char *
到uintptr_t
通过添加整数偏移量,每当偏移量与指针有效时,这就使指针比较定义良好。
我不确定这种交换性是否由标准定义,但它是有意义的,因为它的工作原理即使只有指针的低位是实际的数字地址,高位是某种黑盒子。
我仍然希望用这个例子来certificate这个标准对于未定义的行为来说太过分了,如果memmove不能在标准的C
但这并不是certificate。 绝对没有办法保证你可以比较任意机器架构上的两个任意指针。 这种指针比较的行为不能由C标准甚至编译器来规定。 我可以想象一个具有分段体系结构的机器,可能会产生不同的结果,这取决于这些分段如何在RAM中组织,或者甚至在比较指向不同分段的指针时可能会select抛出exception。 这就是为什么行为是“未定义”的原因。 在完全相同的机器上完全相同的程序可能会给运行提供不同的结果。
memmove()使用两个指针的关系给出“解决scheme”的经典select是否从开始到结束或从结束到开始复制仅适用于所有内存块是从相同的地址空间分配的。 幸运的是,这通常是这种情况,尽pipe它不在16位x86代码的时代。