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。但区别在哪?

核心区别:内存重叠时的行为不同。

什么叫内存重叠?就是 destsrc 指向的内存区域有交集。你想想看,如果源和目标有重叠,拷贝过程中就可能把还没读的数据给覆盖了。

举个例子:

char str[] = "HelloWorld";
// 想把 str+2 开始的5个字节拷贝到 str+4
memcpy(str+4, str+2, 5);  // 危险!

为什么会这样?因为 memcpy 不处理重叠,它只管从前往后逐字节拷贝。一旦源地址在目标地址之前,拷贝到后面时源数据已经被覆盖了。

memmove 则聪明得多——它会判断重叠方向,必要时从后往前拷贝,保证数据完整。

特性 memcpy memmove
处理内存重叠 ❌ 不保证 ✅ 保证正确
性能 通常更快 略慢(有判断开销)
适用场景 确定无重叠 不确定或可能有重叠
我的建议: 除非你100%确定没有重叠,否则一律用 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 内存重叠问题图解

为了让你更直观地理解内存重叠,我画了一张图:

内存重叠场景示意图 情况1:无重叠(安全使用memcpy) 源区域 src 目标区域 dest 情况2:重叠且dest在src之后(危险!) 原始数据区域 重叠区 memcpy从前往后拷贝,会覆盖未读取的源数据 情况3:重叠且dest在src之前(memmove安全处理) 原始数据区域 重叠区 memmove从后往前拷贝,保证数据完整

6.6 避坑指南与最佳实践

最后,我总结几条实战经验:

  1. 能用 memmove 就别用 memcpy——除非你明确知道没有重叠,且对性能有极致要求。
  2. 注意 memset 的第二个参数——它只取低8位,别拿它初始化非字节类型。
  3. memcmp 不会在 '\0' 停下——比较结构体或二进制数据时这是优点,但别跟 strcmp 搞混。
  4. 检查返回值——memchr 可能返回 NULLmemcmp 的返回值不要直接用 == 1 判断,用 == 0!= 0 更安全。
  5. 长度参数别搞错——我见过有人写 memcpy(dest, src, strlen(src)),结果忘了加 +1 拷贝终止符,导致字符串拼接后乱七八糟。
💡 小技巧: 如果你在调试内存问题,可以用 memset 把缓冲区填成特定模式(比如0xAA),这样在调试器里一眼就能看出哪些内存被写过。

好了,内存操作这块就聊到这儿。这些函数看着简单,但用不好就是各种隐蔽的bug。记住一句话:内存操作无小事,重叠问题用move


专注资料整理