文章目录
0. 概要
在C语言中,memmove
和memcpy
都用于在内存中复制数据。然而,它们有一个关键的区别:memmove
能够安全地处理源地址(src
)和目标地址(dest
)之间的内存重叠问题,而memcpy
不能。在这篇博客中,我们将深入分析memmove
的实现,探讨它如何处理内存重叠,并展示详细的内存操作过程。
1. memmove
的实现代码
首先,来看一下memmove
的实现代码:
void *my_memmove(void *dest, const void *src, size_t n) {
uint8_t *d = (uint8_t *)dest;
const uint8_t *s = (const uint8_t *)src;
if (d == s) {
return dest;
}
if (d < s) {
// 如果目标地址在源地址之前,直接从前往后复制
for (size_t i = 0; i < n; i++) {
d[i] = s[i];
}
} else {
// 如果目标地址在源地址之后,从后往前复制,避免重叠问题
for (size_t i = n; i != 0; i--) {
d[i - 1] = s[i - 1];
}
}
return dest;
}
2. 前往后复制的安全性
2.1 情景一:目标地址在源地址之前
假设目标地址在源地址的前面,内存布局如下图所示:
内存地址(从低到高):
低地址 -> | dest | src | ... | <- 高地址
在这种情况下,从前往后复制是安全的。每次从src
中读取数据并写入dest
时,dest
的地址总是在src
之前,因此不会覆盖尚未复制的数据。
2.2 图示解释
假设源地址src
的起始地址是0x100
,目标地址dest
的起始地址是0x080
,数据长度n = 5
。
原始内存布局:
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> | | | | | | | src[0] | src[1] | src[2] | src[3] | src[4] |
第一步:复制src[0]
到dest[0]
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> src[0] | | | | | | src[0] | src[1] | src[2] | src[3] | src[4] |
第二步:复制src[1]
到dest[1]
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> src[0] | src[1] | | | | | src[0] | src[1] | src[2] | src[3] | src[4] |
第三步:复制src[2]
到dest[2]
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> src[0] | src[1] | src[2] | | | | src[0] | src[1] | src[2] | src[3] | src[4] |
第四步:复制src[3]
到dest[3]
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> src[0] | src[1] | src[2] | src[3] | | | src[0] | src[1] | src[2] | src[3] | src[4] |
第五步:复制src[4]
到dest[4]
地址 -> 0x080 0x081 0x082 0x083 0x084 0x085 0x086 0x087 0x088 0x089 0x08A 0x08B
内容 -> src[0] | src[1] | src[2] | src[3] | src[4] | | src[0] | src[1] | src[2] | src[3] | src[4] |
在上述步骤中,由于目标地址总是在源地址之前,所以从前往后复制是安全的,不会出现覆盖问题。
3 从前往后复制可能导致的覆盖问题
3.1 情景二:目标地址在源地址之后
假设目标地址在源地址的后面,内存布局如下图所示:
内存地址(从低到高):
低地址 -> | src | dest | ... | <- 高地址
在这种情况下,如果仍然从前往后复制,可能会导致源数据被覆盖。
3.2 图示解释
假设源地址src
的起始地址是0x100
,目标地址dest
的起始地址是0x102
,数据长度n = 5
。
原始内存布局:
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[2] src[3] src[4] ...
第一步:复制src[0]
到dest[0]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[0] src[3] src[4] ...
第二步:复制src[1]
到dest[1]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[0] src[1] src[4] ...
第三步:复制src[2]
到dest[2]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[0] src[1] src[0] ...
在这个过程中,src[2]
的内容已经在第二步时被覆盖,因此在第三步时无法正确复制。这就导致了数据的损失或混乱。
4. 从后往前复制的安全性
4.1 图示解释
为了避免上述覆盖问题,我们可以从后往前复制数据。
假设源地址src
的起始地址是0x100
,目标地址dest
的起始地址是0x102
,数据长度n = 5
。
原始内存布局:
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0
] src[1] src[2] src[3] src[4] ...
第一步:复制src[4]
到dest[4]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[2] src[3] src[4] src[4]
第二步:复制src[3]
到dest[3]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[2] src[3] src[3] src[4]
第三步:复制src[2]
到dest[2]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[2] src[2] src[3] src[4]
第四步:复制src[1]
到dest[1]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[1] src[1] src[2] src[3] src[4]
第五步:复制src[0]
到dest[0]
地址 -> 0x100 0x101 0x102 0x103 0x104 0x105 0x106
内容 -> src[0] src[0] src[1] src[2] src[3] src[4]
通过从后往前复制,我们避免了对尚未复制的源数据的覆盖,从而保证了数据的完整性。
5. 结论
在实现memmove
时,关键是要正确处理源地址和目标地址之间的内存重叠问题。如果目标地址在源地址之前,可以安全地从前往后复制;而如果目标地址在源地址之后,则需要从后往前复制,以避免数据覆盖问题。通过这种方式,我们可以确保memmove
函数的安全性和正确性。