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。但有个坑——它不处理内存重叠。
那 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;
}
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,负数反之。我一般用它来校验数据完整性,比如检查接收到的数据包和预期是否一致。
11.3 内存对齐与指针
内存对齐,说白了就是数据在内存里的摆放规则。CPU 读取对齐的数据效率高,不对齐的话可能报错或者性能暴跌。
比如一个 int 占 4 字节,它的地址最好是 4 的倍数。如果地址是 0x1001,那就是未对齐的。在 ARM 上直接访问会触发异常,在 x86 上虽然能访问,但速度慢很多。
指针跟对齐有什么关系?关系大了。你想想看,如果用一个 char* 指向一个 int,然后强制转换成 int* 去访问,很可能就未对齐了。
char buf[8] = {0};
int *p = (int *)&buf[1]; // 未对齐!
*p = 0x12345678; // 危险!
我建议你养成一个习惯:指针转换时,确保目标类型的对齐要求能满足。怎么判断?看地址是否能被类型大小整除。
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
11.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
这张图把四个函数、内存对齐、字节序串在了一起。你看,它们都围绕着一个核心——指针与内存的交互方式。理解了这些,你写代码时就能预判问题,而不是等 bug 找上门。
- memcpy 快但不安全,memmove 慢一点但可靠
- memset 注意只填低 8 位,memcmp 小心 padding
- 指针转换前检查对齐,避免未定义行为
- 字节序影响多字节数据的解释,网络编程尤其要注意
嗯,这些内容看起来多,其实核心就一句话:指针操作内存时,要清楚数据的布局和规则。你想想看,是不是这个理?