11、指针与内存操作:memcpy/memmove/memset/memcmp 的实现与区别、内存对齐与指针、字节序(大端/小端)与指针

内存操作,说白了就是直接跟内存打交道。C语言里最常用的四个函数——memcpy、memmove、memset、memcmp,我几乎每个项目都会用到。但很多人只是会用,没想过它们背后是怎么工作的。今天咱们就把它讲透。

11.1 memcpy 与 memmove:拷贝的陷阱

先看 memcpy。它的原型很简单:

void *memcpy(void *dest, const void *src, size_t n);

功能是把 src 开始的 n 个字节拷贝到 dest。但有个坑——它不处理内存重叠。

注意:如果 dest 和 src 指向的内存区域有重叠,memcpy 的行为是未定义的。我曾经在一个通信协议栈里踩过这个坑,数据包解析时源和目标指向了同一个缓冲区的不同位置,结果数据被覆盖得乱七八糟。

那 memmove 呢?它专门解决这个问题:

void *memmove(void *dest, const void *src, size_t n);

它保证即使 dest 和 src 重叠,也能正确拷贝。怎么做到的?我告诉你,核心就是判断方向。

如果 dest 在 src 后面(即 dest > src),就从后往前拷贝;否则从前往后。这样就不会覆盖还没拷贝的数据。

我自己实现过这两个函数,代码其实不复杂:

// 简易 memcpy 实现
void *my_memcpy(void *dest, const void *src, size_t n) {
    unsigned char *d = dest;
    const unsigned char *s = src;
    for (size_t i = 0; i < n; i++) {
        d[i] = s[i];
    }
    return dest;
}

// 简易 memmove 实现
void *my_memmove(void *dest, const void *src, size_t n) {
    unsigned char *d = dest;
    const unsigned char *s = 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];
        }
    }
    return dest;
}
我的习惯:只要不确定会不会重叠,一律用 memmove。性能损失微乎其微,但安全多了。

11.2 memset 与 memcmp:清零与比较

memset 用来把一段内存设为固定值。最常用的场景就是清零:

void *memset(void *s, int c, size_t n);

注意第二个参数是 int,但实际只取低 8 位。所以 memset(buf, 0x1234, 100) 其实只填了 0x34。

memcmp 用来比较两段内存是否相等:

int memcmp(const void *s1, const void *s2, size_t n);

返回 0 表示相等,正数表示 s1 > s2,负数反之。我一般用它来校验数据完整性,比如检查接收到的数据包和预期是否一致。

避坑指南:memcmp 是按字节比较的,不是按字或双字。所以如果你比较的是结构体,要注意填充字节(padding)可能造成误判。我曾经用 memcmp 比较两个结构体,明明字段值一样,结果返回不相等——查了半天才发现是 padding 字节不同。

11.3 内存对齐与指针

内存对齐,说白了就是数据在内存里的摆放规则。CPU 读取对齐的数据效率高,不对齐的话可能报错或者性能暴跌。

比如一个 int 占 4 字节,它的地址最好是 4 的倍数。如果地址是 0x1001,那就是未对齐的。在 ARM 上直接访问会触发异常,在 x86 上虽然能访问,但速度慢很多。

指针跟对齐有什么关系?关系大了。你想想看,如果用一个 char* 指向一个 int,然后强制转换成 int* 去访问,很可能就未对齐了。

char buf[8] = {0};
int *p = (int *)&buf[1];  // 未对齐!
*p = 0x12345678;          // 危险!

我建议你养成一个习惯:指针转换时,确保目标类型的对齐要求能满足。怎么判断?看地址是否能被类型大小整除。

小技巧:用 (uintptr_t)ptr % sizeof(type) 检查对齐。如果结果为 0,就是对齐的。

11.4 字节序:大端与小端

字节序,就是多字节数据在内存里的存放顺序。大端(Big-Endian)把高位字节放在低地址,小端(Little-Endian)把低位字节放在低地址。

比如 0x12345678,在内存里:

地址 大端 小端
0x00 0x12 0x78
0x01 0x34 0x56
0x02 0x56 0x34
0x03 0x78 0x12

x86 和 ARM 默认都是小端,网络协议通常用大端。所以做网络编程时,要用 htonl、ntohl 这些函数转换。

指针怎么感知字节序?其实指针本身不关心字节序,它只是指向一个地址。但如果你用 char* 去遍历一个 int,就能看到字节序的影响:

int x = 0x12345678;
unsigned char *p = (unsigned char *)&x;
for (int i = 0; i < 4; i++) {
    printf("%02x ", p[i]);
}
// 小端输出:78 56 34 12
// 大端输出:12 34 56 78
我曾经踩过的坑:在嵌入式设备上解析网络数据包时,直接拿指针强转结构体。结果因为字节序不同,解析出来的字段全是反的。后来老老实实用了 ntohs/ntohl 转换。

11.5 知识体系总览

下面这张图把本章的核心逻辑串起来了:

指针与内存操作 memcpy 不处理重叠 memmove 处理重叠 memset 填充/清零 memcmp 比较 内存对齐与指针 对齐检查 | 指针转换 | 结构体填充 字节序:大端 vs 小端 网络字节序 | 主机字节序 | 指针遍历 理解底层原理,写出健壮的代码

这张图把四个函数、内存对齐、字节序串在了一起。你看,它们都围绕着一个核心——指针与内存的交互方式。理解了这些,你写代码时就能预判问题,而不是等 bug 找上门。

总结一下:
  • memcpy 快但不安全,memmove 慢一点但可靠
  • memset 注意只填低 8 位,memcmp 小心 padding
  • 指针转换前检查对齐,避免未定义行为
  • 字节序影响多字节数据的解释,网络编程尤其要注意

嗯,这些内容看起来多,其实核心就一句话:指针操作内存时,要清楚数据的布局和规则。你想想看,是不是这个理?

公众号:蓝海资料掘金营,微信deep3321