6、内存操作(string.h):memset、memcpy、memmove、memcmp、memchr。内存重叠问题与memcpy/memmove的区别。
内存操作,说白了就是直接跟内存地址打交道。很多C语言新手一听到指针就头疼,但内存操作函数恰恰是绕不开的坎。我做了这么多年嵌入式开发,可以负责任地告诉你:搞懂了这五个函数,你就掌握了C语言内存管理的半壁江山。
今天咱们就一个一个捋清楚。先从最常用的说起。
6.1 memset:内存的“清道夫”
memset 的作用很简单——把一段内存区域全部设置成某个值。原型长这样:
void *memset(void *s, int c, size_t n);
参数说明:
s:内存起始地址c:要设置的值(注意是int类型,但实际只取低8位)n:字节数
我个人习惯在定义结构体或数组后,第一时间用 memset 清零。比如:
char buffer[1024];
memset(buffer, 0, sizeof(buffer)); // 全部清零
memset 是按字节赋值的。如果你想把 int 数组全部设为1,千万别写 memset(arr, 1, sizeof(arr))——每个字节变成0x01,每个 int 实际是0x01010101,根本不是1!
我在项目中遇到过有人用 memset 初始化结构体指针,结果传了指针本身而不是指针指向的内存地址,直接段错误。嗯,这种低级错误其实挺常见的。
6.2 memcpy vs memmove:内存拷贝的“双胞胎”
这两个函数长得几乎一模一样:
void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
功能都是把 src 开始的 n 个字节拷贝到 dest。但区别在哪?
核心区别:内存重叠时的行为不同。
什么叫内存重叠?就是 dest 和 src 指向的内存区域有交集。你想想看,如果源和目标有重叠,拷贝过程中就可能把还没读的数据给覆盖了。
举个例子:
char str[] = "HelloWorld";
// 想把 str+2 开始的5个字节拷贝到 str+4
memcpy(str+4, str+2, 5); // 危险!
为什么会这样?因为 memcpy 不处理重叠,它只管从前往后逐字节拷贝。一旦源地址在目标地址之前,拷贝到后面时源数据已经被覆盖了。
memmove 则聪明得多——它会判断重叠方向,必要时从后往前拷贝,保证数据完整。
| 特性 | memcpy | memmove |
|---|---|---|
| 处理内存重叠 | ❌ 不保证 | ✅ 保证正确 |
| 性能 | 通常更快 | 略慢(有判断开销) |
| 适用场景 | 确定无重叠 | 不确定或可能有重叠 |
memmove。性能差异微乎其微,但安全系数天差地别。
我曾经在一个网络协议栈里用 memcpy 处理数据包缓冲区,结果因为缓冲区滑动导致数据错乱,排查了整整两天。后来换成 memmove,问题瞬间消失。从那以后,我再也不敢在不确定的情况下用 memcpy 了。
6.3 memcmp:内存比较器
比较两块内存是否相等:
int memcmp(const void *s1, const void *s2, size_t n);
返回值:
- 等于0:两块内存完全一样
- 小于0:
s1的第一个不同字节小于s2对应字节 - 大于0:反之
注意,它比较的是原始字节,不是字符串。所以不会在 '\0' 处停下。这一点跟 strcmp 完全不同。
char a[] = "Hello\0World";
char b[] = "Hello\0World";
if (memcmp(a, b, 11) == 0) {
// 比较前11个字节,包括中间的'\0'
}
6.4 memchr:内存中的“探针”
在内存中查找某个字符第一次出现的位置:
void *memchr(const void *s, int c, size_t n);
返回指向该字符的指针,没找到返回 NULL。
这个函数在解析二进制协议时特别好用。比如你要在一段数据里找某个标记字节:
uint8_t packet[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE};
void *found = memchr(packet, 0xCC, sizeof(packet));
if (found) {
// 找到了!
}
6.5 内存重叠问题图解
为了让你更直观地理解内存重叠,我画了一张图:
6.6 避坑指南与最佳实践
最后,我总结几条实战经验:
- 能用
memmove就别用memcpy——除非你明确知道没有重叠,且对性能有极致要求。 - 注意
memset的第二个参数——它只取低8位,别拿它初始化非字节类型。 memcmp不会在'\0'停下——比较结构体或二进制数据时这是优点,但别跟strcmp搞混。- 检查返回值——
memchr可能返回NULL,memcmp的返回值不要直接用== 1判断,用== 0或!= 0更安全。 - 长度参数别搞错——我见过有人写
memcpy(dest, src, strlen(src)),结果忘了加+1拷贝终止符,导致字符串拼接后乱七八糟。
memset 把缓冲区填成特定模式(比如0xAA),这样在调试器里一眼就能看出哪些内存被写过。
好了,内存操作这块就聊到这儿。这些函数看着简单,但用不好就是各种隐蔽的bug。记住一句话:内存操作无小事,重叠问题用move。