38、内存操作:memcpy/memmove/memset/memcmp 的实现与区别、内存重叠问题
内存操作,说白了就是 C 语言里最底层的“搬砖”活。你写再花哨的算法,最后数据还是在内存里挪来挪去。我早年刚入行时,觉得这些函数太简单了,不就是复制、填充、比较嘛?直到有一次线上服务莫名其妙崩溃,查了两天,最后定位到是 memcpy 踩了内存重叠的坑……嗯,从那以后我再也不敢小看这几个函数了。
今天咱们就把 memcpy、memmove、memset、memcmp 这四兄弟彻底聊透。不光讲用法,还要手撕源码,把内存重叠这个“隐形杀手”揪出来。
1. memset:内存“刷墙”工
memset 的作用很简单:把一段内存的每个字节都设成同一个值。我习惯用它来清零结构体或数组,尤其是定义局部变量时,不清零的话里面可能是随机值,容易出 bug。
void *memset(void *s, int c, size_t n);
注意第二个参数是 int,但实际只取低 8 位(一个字节)。所以 memset(buf, 0x1234, 100) 等价于 memset(buf, 0x34, 100)。
我自己实现过一个简化版,核心就是逐字节赋值:
void *my_memset(void *s, int c, size_t n) {
unsigned char *p = (unsigned char *)s;
unsigned char ch = (unsigned char)c;
for (size_t i = 0; i < n; i++) {
p[i] = ch;
}
return s;
}
当然,标准库会用更高效的算法(比如按 4 字节或 8 字节对齐写入),但原理一样。
memset(arr, 1, sizeof(arr)) 会把每个字节设成 1,结果每个 int 是 0x01010101,不是 1。这个坑我踩过。
2. memcpy 与 memmove:复制双胞胎,性格大不同
这两个函数都用于内存复制,原型几乎一样:
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
区别就一句话:memcpy 不保证处理内存重叠,memmove 保证。
什么叫内存重叠?就是源地址和目标地址指向的内存区域有交集。比如你想把数组 [1,2,3,4,5] 从位置 0 复制到位置 2,结果可能是 [1,2,1,2,1] 而不是 [1,2,1,2,3]——因为复制过程中把还没读的源数据覆盖了。
手撕 memcpy 实现
void *my_memcpy(void *dest, const void *src, size_t n) {
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
for (size_t i = 0; i < n; i++) {
d[i] = s[i];
}
return dest;
}
这个版本从前往后复制。如果 dest 在 src 后面且重叠,就会出问题。
手撕 memmove 实现
void *my_memmove(void *dest, const void *src, size_t n) {
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;
if (d < s) {
// 目标在前,从前往后复制(安全)
for (size_t i = 0; i < n; i++) {
d[i] = s[i];
}
} else if (d > s) {
// 目标在后,从后往前复制(避免覆盖未读数据)
for (size_t i = n; i > 0; i--) {
d[i-1] = s[i-1];
}
}
// 如果 d == s,什么都不用做
return dest;
}
核心逻辑就是判断地址高低,决定复制方向。你想想看,如果目标地址比源地址大,从前往后复制就会把源数据尾部覆盖掉;反过来从后往前复制,先复制尾部,再复制头部,就安全了。
3. memcmp:内存“照妖镜”
memcmp 用来比较两块内存是否相等,按字节比较:
int memcmp(const void *s1, const void *s2, size_t n);
返回值:相等返回 0;s1 < s2 返回负数;s1 > s2 返回正数。注意,它比较的是无符号字节,所以 0x80 比 0x7F 大。
我在项目中常用它来校验两个结构体是否相同,或者检查加密后的数据是否被篡改。但要注意:如果结构体里有指针成员,memcmp 比较的是指针值,不是指针指向的内容。这个区别很重要。
// 简单实现
int my_memcmp(const void *s1, const void *s2, size_t n) {
const unsigned char *p1 = (const unsigned char *)s1;
const unsigned char *p2 = (const unsigned char *)s2;
for (size_t i = 0; i < n; i++) {
if (p1[i] != p2[i]) {
return p1[i] - p2[i];
}
}
return 0;
}
4. 内存重叠问题深度剖析
咱们再深入聊聊内存重叠。很多初学者觉得“重叠就重叠呗,反正数据一样”。其实不然。看这个例子:
char buf[10] = "HelloWorld";
memcpy(buf + 2, buf, 6); // 把 "Hello" 复制到 "lloWo" 的位置
// 期望结果:HeHello
// 实际结果:HeHeHeWo (因为复制过程中源数据被覆盖)
为什么会这样?因为 buf 的地址是 0x100,buf+2 是 0x102。复制时,先复制 'H' 到 0x102,再复制 'e' 到 0x103,然后复制 'l' 到 0x104……但注意,当复制到第 3 个字节时,源地址 0x102 已经被改成了 'H',所以复制的是新值,不是原来的 'l'。这就是重叠导致的“脏读”。
而 memmove 会检测到目标地址大于源地址,于是从后往前复制:先复制 'o'(源地址 0x104)到 0x106,再复制 'l'(0x103)到 0x105……这样源数据始终是原始值,结果正确。
5. 总结对比表
| 函数 | 功能 | 处理重叠 | 性能 | 典型场景 |
|---|---|---|---|---|
| memset | 按字节填充 | 不涉及 | 高 | 清零、初始化 |
| memcpy | 内存复制 | 不保证 | 最高 | 无重叠复制 |
| memmove | 内存复制 | 安全处理 | 略低(方向判断) | 不确定是否重叠 |
| memcmp | 按字节比较 | 不涉及 | 高 | 校验、判等 |
好了,这四兄弟的底裤基本被扒干净了。我个人建议:日常开发中,memset 放心用,memmove 优先用,memcpy 谨慎用,memcmp 注意字节序。记住这些,面试官再问内存操作,你就能从原理到实现,再到避坑,一条龙给他讲明白。
n 都是字节数,不是元素个数。很多人写 memcpy(arr1, arr2, sizeof(arr1)) 时,如果 arr1 是指针,sizeof 就是指针大小,不是数组大小。这个错误我见过不下十次。
公众号:蓝海资料掘金营,微信 deep3321