第8章:字符串内存操作:memset、memcpy、memmove、memcmp 的区别与实现
大家好,我是老刘。今天咱们聊聊C语言里那四个让人又爱又恨的内存操作函数——memset、memcpy、memmove、memcmp。说实话,我见过太多人在这些函数上栽跟头了。尤其是刚入行的朋友,总觉得它们差不多,用起来随便挑一个就行。嗯,这种想法很危险。
你想想看,如果只是简单的字符串拷贝,用strcpy就够了。但当你面对的是二进制数据、结构体、或者需要精确控制内存布局的场景时,这四个函数就是你的看家本领。今天我就把它们的区别、实现、以及我踩过的坑,一次性给你讲透。
8.1 为什么需要内存操作函数?
字符串函数(比如strcpy、strcat)遇到'\0'就停。但内存操作函数不一样——它们只认长度,不认结束符。这意味着你可以操作任意类型的数据:整数数组、结构体、甚至是一段音频流。
我在做嵌入式项目时,经常需要把传感器数据打包成字节流发送出去。这时候strcpy根本没用,因为数据里可能包含0x00。你只能用memcpy。说白了,字符串函数是给“人读的文本”用的,内存函数是给“机器读的二进制”用的。
8.2 memset:把内存块填成统一值
memset的原型很简单:
void *memset(void *s, int c, size_t n);
它的作用是把s指向的内存区域的前n个字节,全部设置成c(实际只取c的低8位)。
常见用法:
- 初始化数组为0
- 清空结构体
- 重置缓冲区
举个例子:
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
int main() {
Student stu;
// 清空结构体,确保所有字段初始为0
memset(&stu, 0, sizeof(Student));
printf("id = %d, name = '%s', score = %.2f\n", stu.id, stu.name, stu.score);
return 0;
}
重要提醒:memset的第二个参数是int,但实际只取低8位。所以memset(buf, 0x1234, 100)等价于memset(buf, 0x34, 100)。我曾经见过有人想初始化int数组为1,写了memset(arr, 1, sizeof(arr)),结果每个字节都是0x01,int值变成了0x01010101——完全不是他想要的效果。
我的习惯:初始化结构体时,我永远用memset(&obj, 0, sizeof(obj)),而不是逐个字段赋值。这样既安全又高效,还能避免漏掉某个字段。
8.3 memcpy:高效但危险的内存拷贝
memcpy的原型:
void *memcpy(void *dest, const void *src, size_t n);
它从src拷贝n个字节到dest。注意:src和dest不能重叠。如果重叠,行为是未定义的。
为什么不能重叠?因为memcpy的实现通常是从前往后拷贝。如果dest在src后面且两者重叠,拷贝过程中会覆盖src还没读到的数据,导致结果错误。
看一个典型实现:
void *my_memcpy(void *dest, const void *src, size_t n) {
if (dest == NULL || src == NULL) return NULL;
char *d = (char *)dest;
const char *s = (const char *)src;
for (size_t i = 0; i < n; i++) {
d[i] = s[i];
}
return dest;
}
这个实现很简单,但性能一般。很多库会用字(4字节或8字节)对齐拷贝来加速。不过那是优化层面的事,咱们先理解原理。
我曾经踩过的坑:有一次我需要把数组元素后移一位,写了memcpy(arr+1, arr, (n-1)*sizeof(int))。结果arr[0]被拷贝到arr[1]后,arr[1]又被拷贝到arr[2]……最后整个数组变成了arr[0]的重复。这就是典型的重叠问题。从那以后,只要不确定是否重叠,我就用memmove。
8.4 memmove:安全的拷贝,哪怕重叠
memmove的原型和memcpy一模一样:
void *memmove(void *dest, const void *src, size_t n);
区别在于:memmove保证即使dest和src重叠,结果也是正确的。它通过判断地址大小,决定从前往后还是从后往前拷贝。
实现思路:
- 如果dest < src,从前往后拷贝(和memcpy一样)
- 如果dest > src,从后往前拷贝,避免覆盖
看代码:
void *my_memmove(void *dest, const void *src, size_t n) {
if (dest == NULL || src == NULL) return NULL;
char *d = (char *)dest;
const char *s = (const char *)src;
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;
}
性能对比:memmove比memcpy多了一次判断和可能的分支,但现代CPU的分支预测很准,性能差异微乎其微。我个人建议:除非你100%确定不重叠,否则一律用memmove。安全第一,别为了那点性能埋雷。
8.5 memcmp:比较两块内存是否相等
memcmp的原型:
int memcmp(const void *s1, const void *s2, size_t n);
它逐字节比较s1和s2的前n个字节。返回值:
- 0:相等
- <0:s1小于s2
- >0:s1大于s2
注意:比较的是无符号字节。所以即使有符号char是负数,也会被当作正数比较。
实现:
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;
}
使用场景:比较两个结构体是否相等、校验数据完整性、判断密码哈希值。但注意:结构体可能有padding字节(对齐填充),这些字节的值是未定义的。直接memcmp两个结构体可能得到错误结果。我一般会先memset清零再赋值,或者逐个字段比较。
8.6 四者对比一览表
| 函数 | 功能 | 重叠安全 | 典型用途 | 注意事项 |
|---|---|---|---|---|
| memset | 填充内存块 | 不涉及 | 初始化、清空 | 只取低8位,不能初始化非0值 |
| memcpy | 拷贝内存 | 不安全 | 不重叠的拷贝 | 性能略高,但风险大 |
| memmove | 拷贝内存 | 安全 | 任何拷贝场景 | 推荐优先使用 |
| memcmp | 比较内存 | 不涉及 | 数据校验、相等判断 | 注意padding和字节序 |
8.7 知识体系图
下面这张图帮你理清这四个函数的关系和选择逻辑:
8.8 避坑指南与实战经验
我在项目里见过太多因为内存操作函数用错导致的bug。下面这几个坑,你一定要注意:
- memset初始化非0值:想初始化int数组为1?别用memset。它按字节填充,结果会是0x01010101。正确做法是用循环赋值。
- memcpy重叠拷贝:只要src和dest指向的内存区域有重叠,就别用memcpy。哪怕你觉得“应该没问题”,也请用memmove。我吃过这个亏,不想你再吃一次。
- memcmp比较结构体:结构体有padding字节,值不确定。直接memcmp可能误判。要么memset清零后再用,要么逐个字段比较。
- 长度参数错误:sizeof(指针)和sizeof(数组)不一样。传参时数组退化为指针,sizeof得到的是指针大小(4或8字节),不是数组大小。我建议用sizeof(数组名)或明确计算长度。
- 忽略返回值:memset、memcpy、memmove都返回目标地址。虽然不检查也能用,但链式调用时很有用。比如:char *p = (char *)memcpy(buf, src, n) + n;
我曾经犯过的错:有一次我写网络协议栈,需要把接收缓冲区里的数据往前挪。我用了memcpy(buf, buf+offset, len),结果数据全乱了。调试了一下午才发现是重叠问题。从那以后,我写了个宏:#define SAFE_COPY(d,s,n) memmove(d,s,n),强制自己用memmove。虽然有点极端,但再也没出过这类bug。
8.9 总结
好了,今天的内容就到这里。memset、memcpy、memmove、memcmp这四个函数,说白了就是C语言里操作内存的四大金刚。记住:
- memset用来填,memcpy/memmove用来搬,memcmp用来比
- 不确定是否重叠?用memmove
- 初始化结构体?先memset清零
- 比较结构体?小心padding
这些函数看着简单,但用错了后果很严重。希望今天的讲解能帮你避开我当年踩过的坑。下次见!
公众号:蓝海资料掘金营,微信deep3321