C语言内存函数:memcpy / memmove / memset / memcmp
本章目标:理解四大内存函数的底层原理 + 搞懂 memcpy 和 memmove 的本质区别 + 掌握 memset 的所有坑 + 手写模拟实现
引入:字符串函数管字符,内存函数管一切
前面我们学了很多字符串函数——strcpy、strcat、strcmp、strlen——它们都有一个共同特点:以 \0 为结束标志,只管字符数据。
但现实编程中,我们经常需要拷贝、设置、比较任意类型的数据:
拷贝一个
int数组 → 字符串函数管不了
拷贝一个double数组 → 字符串函数管不了
拷贝一个自定义结构体 → 字符串函数管不了
把某块内存全部设成 0xAA → 字符串函数管不了
内存函数就是为了这些场景诞生的。
核心思维:内存函数不看数据类型,只看字节。不管你传进来的是 int、float、double、还是自定义结构体,在内存里统统都是一串字节。内存函数就是在字节层面做拷贝、设置、比较的工作。
第一部分:memcpy——内存块拷贝
1.1 函数原型逐字拆解
void *memcpy(void *destination, const void *source, size_t num);
| 参数 | 含义 | 注意事项 |
|---|---|---|
destination |
目标地址(拷贝到这里) | 必须有足够的空间,否则越界 |
source |
源地址(拷贝从这里) | 不能为 NULL |
num |
要拷贝的字节数(字节!不是元素个数!) | 单位是字节 |
返回值是 void*(目标起始地址),便于链式调用。
1.2 拷贝 int 数组
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int arr2[10] = {0};
// 拷贝前 5 个 int → 5 × 4 字节 = 20 字节
memcpy(arr2, arr1, 20);
for (int i = 0; i < 10; i++)
printf("%d ", arr2[i]);
// 输出:1 2 3 4 5 0 0 0 0 0
return 0;
}
⚠️ 提醒:注意这里传的是
20(字节数),不是5(元素个数)!很多初学者写成memcpy(arr2, arr1, 5),结果只拷贝了 5 个字节(相当于只拷贝了arr1[0]的前 5 个 bit,数据完全乱掉)。正确写法是5 * sizeof(int)或直接写20(假设 int 是 4 字节)。
1.3 拷贝 double 数组
memcpy 的优势就体现在这里——字符串函数对 double 无能为力,但 memcpy 可以:
#include <stdio.h>
#include <string.h>
int main()
{
double source[] = {1.5, 2.5, 3.5, 4.5, 5.5};
double dest[5];
// 拷贝整个 double 数组(5 × 8 = 40 字节)
memcpy(dest, source, sizeof(source));
for (int i = 0; i < 5; i++)
printf("%.1f ", dest[i]); // 输出:1.5 2.5 3.5 4.5 5.5
return 0;
}
1.4 拷贝结构体
#include <stdio.h>
#include <string.h>
struct Stu {
char name[20];
int age;
char sex[5];
};
int main()
{
struct Stu s1 = {"张三", 20, "男"};
struct Stu s2;
// 一次性拷贝整个结构体(20 + 4 + 5 = 29 字节,实际按对齐后的大小)
memcpy(&s2, &s1, sizeof(struct Stu));
printf("%s %d %s\n", s2.name, s2.age, s2.sex); // 输出:张三 20 男
return 0;
}
💡 实战用法:结构体整体拷贝用 memcpy 非常方便,不用逐个成员赋值。但要确保目标结构体的空间足够。
1.5 内存重叠——memcpy 最危险的坑
什么是内存重叠? 简单说就是:源和目标在内存中是同一块区域的不同位置。
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 把 arr[0]~arr[4](5个int = 20字节)拷贝到 arr[2]~arr[6]
memcpy(&arr[2], &arr[0], 5 * sizeof(int));
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] arr[7] arr[8] arr[9]
1 2 3 4 5 6 7 8 9 10
↑───────────────────────────↑
从这里拷贝到这里(源和目标重叠!)
memcpy 在重叠场景下会发生什么?
memcpy 的实现通常是从源地址逐字节向后拷贝。在重叠情况下,还没被拷贝的源数据,可能已经被刚才拷贝过去的"脏数据"覆盖了。
假设 memcpy 从 &arr[2] 开始向前拷贝(实际取决于具体实现):
Step 1: arr[2] = arr[0] = 1 → arr[2] 从 3 变成 1 ✅(暂时正确)
Step 2: arr[3] = arr[1] = 2 → arr[3] 从 4 变成 2 ✅(暂时正确)
Step 3: arr[4] = arr[2] = 1 → ⚠️ arr[2] 已经被 Step 1 改成 1,所以这里拷的是脏数据!
Step 4: arr[5] = arr[3] = 2 → ⚠️ arr[3] 已经被 Step 2 改成 2,所以这里拷的是脏数据!
...
课件上的例子:{1,2,3,4,5,6,7,8,9,10} 经过 memcpy(&arr[2], &arr[0], 20) 后预期得到 {1,2,1,2,1,2,1,8,9,10}——运行起来结果确实是对的。
但这不代表 memcpy 是安全的。 C 标准明确说:
If the objects overlap, the behavior is undefined.
undefined = 编译器可以任意处理,可能这次对,下次优化后就不对了,在不同平台、不同编译器、不同优化级别下结果都可能不同。
🔑 安全结论:如果源和目标在同一个数组(或同一块内存区域),绝对不要用 memcpy,用 memmove。
第二部分:memmove——安全版内存拷贝
2.1 函数原型
void *memmove(void *destination, const void *source, size_t num);
参数和 memcpy 完全一样,唯一的区别:memmove 保证在重叠情况下结果一定正确。
2.2 代码演示
#include <stdio.h>
#include <string.h>
int main()
{
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 用 memmove 处理重叠拷贝
memmove(&arr[2], &arr[0], 5 * sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]);
// 输出:1 2 1 2 3 4 5 8 9 10
return 0;
}
2.3 memmove 的实现原理:方向判断
memmove 能处理重叠的核心在于:它根据目标地址和源地址的相对位置,决定从哪个方向开始拷贝。
情况一:目标地址 > 源地址(向下覆盖)
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] ...
1 2 3 4 5 6 7 ...
↑─────────────────────────────↑
目标(arr[2]) 源(arr[0])
目标地址 > 源地址 → 从后往前拷贝
理由:后面先拷,不会覆盖前面还没拷的源数据
Step 1: 拷贝 arr[6] → arr[8](最后面开始,不影响前面)
Step 2: 拷贝 arr[5] → arr[7]
Step 3: 拷贝 arr[4] → arr[6]
Step 4: 拷贝 arr[3] → arr[5]
Step 5: 拷贝 arr[2] → arr[4]
Step 6: 拷贝 arr[1] → arr[3]
Step 7: 拷贝 arr[0] → arr[2] ← 源数据全程没有被覆盖过 ✅
情况二:目标地址 < 源地址(向上覆盖)
arr[0] arr[1] arr[2] arr[3] arr[4] arr[5] arr[6] arr[7] ...
1 2 3 4 5 6 7 8 ...
↑───────────────────────────────────────────↑
目标(arr[1]) 源(arr[3])
目标地址 < 源地址 → 从前往后拷贝
理由:前面先拷,不会覆盖后面还没拷的源数据
2.4 memcpy vs memmove 一览表
memcpy |
memmove |
|
|---|---|---|
| 函数原型 | 完全相同 | 完全相同 |
| 重叠安全性 | 未定义行为 | 保证正确 |
| 性能 | 略快(无方向判断) | 略慢(有方向判断) |
| 何时用 | 100% 确定不重叠 | 不确定是否重叠 / 同一数组内拷贝 |
💡 实操原则:如果有任何怀疑内存重叠的可能,直接用 memmove。现代编译器对 memmove 优化得很好(甚至和 memcpy 一样快),不要为了省那点性能赌未定义行为。
第三部分:memset——按字节设置内存
3.1 函数原型
void *memset(void *ptr, int value, size_t num);
将 ptr 指向的内存区域的前 num 个字节设置为 value。
3.2 基础演示
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "hello world";
memset(str, 'x', 6); // 前 6 个字节全部设成 'x'(ASCII 120)
printf("%s\n", str); // 输出:xxxxxxworld
return 0;
}
3.3 memset 的最核心原则:按字节设置
⚠️ 提醒:memset 是逐字节设置的,不是按数据类型设置的。这个原则搞不清楚,会产生各种离谱的 bug。
为什么设 int 数组为 0 是对的?
int arr[5];
memset(arr, 0, sizeof(arr)); // 前 20 字节全设为 0x00
每个 int 变成 0x00000000 = 0,完全正确 ✅
为什么设 int 数组为 -1 也是对的?
int arr[5];
memset(arr, -1, sizeof(arr)); // 前 20 字节全设为 0xFF
-1 的二进制补码是 0xFFFFFFFF(全 1)。每个 int 变成 0xFFFFFFFF = -1(二进制补码),完全正确 ✅
为什么设 int 数组为 1 是错的?
int arr[5];
memset(arr, 1, sizeof(arr)); // 前 20 字节全设为 0x01
每个 int 变成 0x01010101 = 16843009(十进制),完全不是 1 ❌
3.4 memset 完整示例(含各种设值)
#include <stdio.h>
#include <string.h>
int main()
{
int arr[5];
// 正确:设 0
memset(arr, 0, sizeof(arr));
printf("%d ", arr[0]); // 0 ✅
// 正确:设 -1
memset(arr, -1, sizeof(arr));
printf("%d ", arr[0]); // -1 ✅
// 错误:设 1(陷阱!)
memset(arr, 1, sizeof(arr));
printf("%d ", arr[0]); // 16843009 ❌(不是1!)
return 0;
}
3.5 为什么 memset 只适合设 0 和 -1?
这涉及到 int 在内存中的存储方式(二进制补码):
| 设的值 | 每个字节 | int(4字节)的值 |
|---|---|---|
memset(..., 0, ...) |
0x00 × 4 |
0x00000000 = 0 |
memset(..., -1, ...) |
0xFF × 4 |
0xFFFFFFFF = -1 |
memset(..., 1, ...) |
0x01 × 4 |
0x01010101 = 16843009 |
memset(..., 127, ...) |
0x7F × 4 |
0x7F7F7F7F = 2139062143 |
💡 记忆口诀:
memset设 int 数组,只有 0 和 -1 是安全的,其他值都会产生奇怪的大数字。
3.6 实战:memset 的正确用法
#include <stdio.h>
#include <string.h>
int main()
{
// 初始化 char 数组(随便设什么值都可以,因为 char 是单字节)
char buf[100];
memset(buf, 0, sizeof(buf)); // 全设为 \0 ✅
memset(buf, 'A', 50); // 前50个字节全设为 'A' ✅
// 初始化 int 数组
int nums[100];
memset(nums, 0, sizeof(nums)); // 全设为 0 ✅
memset(nums, -1, sizeof(nums)); // 全设为 -1 ✅
// 错误示范:想设成 5,实际上每个 int 是 0x05050505
int wrong[5];
memset(wrong, 5, sizeof(wrong));
printf("%d\n", wrong[0]); // 输出 84215045,不是 5!❌
return 0;
}
第四部分:memcmp——内存块比较
4.1 函数原型
int memcmp(const void *ptr1, const void *ptr2, size_t num);
从 ptr1 和 ptr2 的起始地址开始,逐字节比较,最多比较 num 个字节。
4.2 返回值规则
| 比较结果 | 返回值 |
|---|---|
第一个不同的字节:ptr1 的字节 > ptr2 的字节 |
正数(> 0) |
第一个不同的字节:ptr1 的字节 < ptr2 的字节 |
负数(< 0) |
所有 num 个字节都相等 |
0 |
4.3 典型示例:比较字符串
#include <stdio.h>
#include <string.h>
int main()
{
char buf1[] = "DWgaOtP12df0";
char buf2[] = "DWGAOTP12DF0";
int n = memcmp(buf1, buf2, sizeof(buf1));
if (n > 0)
printf("'%s' > '%s'\n", buf1, buf2);
else if (n < 0)
printf("'%s' < '%s'\n", buf1, buf2);
else
printf("'%s' == '%s'\n", buf1, buf2);
return 0;
}
// 输出:'DWgaOtP12df0' > 'DWGAOTP12DF0'
// 原因:'g' (ASCII 103) > 'G' (ASCII 71)
4.4 memcmp vs strcmp:核心区别
char str1[] = "hello";
char str2[] = "hello\x00world"; // 中间有 \0
// strcmp:遇到 \0 就停止比较
strcmp(str1, str2) == 0; // 相等(strcmp 只看到 "hello" 就停了)
// memcmp:严格按照 num 字节数比较,忽略 \0 的特殊含义
memcmp(str1, str2, 10) == 0; // 不相等(\0 只是普通字节 0x00)
🔑 记忆口诀:
strcmp看到\0就停,memcmp 眼里\0只是字节 0x00。
4.5 精确比较:比较前 N 个字节
memcmp 的精髓在于可以只比较一部分字节:
#include <stdio.h>
#include <string.h>
int main()
{
char version1[] = "V1.2.3";
char version2[] = "V1.2.4";
// 只比较前 5 个字节("V1.2.")
int n = memcmp(version1, version2, 5);
printf("%s\n", n == 0 ? "版本号前缀相同" : "版本号前缀不同");
// 输出:版本号前缀相同
// 比较全部
n = memcmp(version1, version2, sizeof(version1));
printf("%s\n", n == 0 ? "完全相同" : "不同");
// 输出:不同(因为 '3' < '4')
return 0;
}
第五部分:手写模拟实现
5.1 手写 memcpy
#include <assert.h>
void *my_memcpy(void *dest, const void *src, size_t num)
{
assert(dest != NULL && src != NULL); // 断言:地址不能为 NULL
void *ret = dest; // 记住目标起始地址(返回值用)
// 逐字节拷贝:从源逐个字节复制到目标
while (num--) {
*(char*)dest = *(char*)src; // 强转为 char*,一次拷1字节
dest = (char*)dest + 1; // 目标地址后移1字节
src = (char*)src + 1; // 源地址后移1字节
}
return ret;
}
💡 为什么强转成
char*? 因为char永远是 1 字节,用char*可以确保每次精确拷贝 1 个字节,不管原类型是什么。
5.2 手写 memmove
#include <assert.h>
void *my_memmove(void *dest, const void *src, size_t num)
{
assert(dest != NULL && src != NULL);
void *ret = dest;
if (dest < src) {
// 情况一:目标地址 < 源地址(向上覆盖)
// 从前往后拷贝,前面先拷,不影响后面的源数据
while (num--) {
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
} else {
// 情况二:目标地址 >= 源地址(向下覆盖)
// 从后往前拷贝,后面先拷,不影响前面的源数据
dest = (char*)dest + num; // 先跳到最后
src = (char*)src + num;
while (num--) {
dest = (char*)dest - 1;
src = (char*)src - 1;
*(char*)dest = *(char*)src;
}
}
return ret;
}
第六部分:经典面试题精讲
面试题一:memcpy 和 memmove 的核心区别是什么?
参考答案:
功能上:两者都可以拷贝任意类型的内存块,函数原型几乎相同。
核心区别:memcpy 不处理重叠,memmove 处理重叠。
memcpy:源和目标内存重叠时,行为未定义(可能对可能错)memmove:通过判断目标地址和源地址的相对位置,选择从前往后或从后往前拷贝,保证在任何重叠情况下结果都正确实操建议:不确定是否重叠时一律用 memmove。
面试题二:下面代码的输出是什么?
#include <stdio.h>
#include <string.h>
int main()
{
int arr[5];
memset(arr, 0, sizeof(arr));
int i = 0;
for (i = 0; i < 5; i++)
printf("%d ", arr[i]);
return 0;
}
推演:
sizeof(arr)= 5 × 4 = 20 字节memset(arr, 0, 20)→ 前 20 字节全设为 0x00- 每个 int(4 字节)=
0x00000000= 0
答案:0 0 0 0 0
面试题二·变体:改成 memset(arr, 1, sizeof(arr)) 呢?
int arr[5];
memset(arr, 1, sizeof(arr));
printf("%d\n", arr[0]);
推演:
memset(arr, 1, 20)→ 前 20 字节全设为 0x01arr[0]的 4 个字节 =0x01 0x01 0x01 0x010x01010101= 1×16777216 + 1×65536 + 1×256 + 1 = 16843009
答案:16843009(不是 1!)
⚠️ 提醒:这是 memset 最经典的面试题陷阱,90% 的初学者会答成 1,正确答案是 16843009。
面试题三:memcmp 比较的是什么单位的数据?
参考答案:
memcmp按字节逐个比较,不是按数据类型(int、double 等)比较。int a = 0x12345678; // 内存:78 56 34 12(小端) int b = 0x12340000; int ret = memcmp(&a, &b, 4); // 比较 4 个字节 // 从低地址字节开始比: // 字节1: 0x78 vs 0x00 → 78 > 00 → 正数 printf("%d\n", ret); // 正数常见混淆:初学者以为 memcmp 是"按 int 比较",实际上是"按字节比较"。
面试题四:实现一个函数,把 int 数组前 n 个元素全部设为 0,要求不能用 memset
void set_int_array_zero(int *arr, int n)
{
// 方法一:循环赋值
for (int i = 0; i < n; i++)
arr[i] = 0;
// 方法二:指针方式
int *p = arr;
while (n--)
*p++ = 0;
}
💡 引申问题:为什么不用 memset 设 int 数组为 0 呢?因为 memset 按字节设,int 是 4 字节,设 0 恰好是对的。但如果我们想设其他值(比如每个 int 设成 0xAAAAAAAA),就得手动循环了。
本章总结
四大内存函数速查表:
| 函数 | 原型 | 功能 | 返回值 | 特别注意 |
|---|---|---|---|---|
memcpy |
memcpy(d,s,n) |
拷贝 n 字节 | 目标地址 | 重叠时未定义 |
memmove |
memmove(d,s,n) |
拷贝 n 字节 | 目标地址 | 重叠时也正确 |
memset |
memset(p,v,n) |
前 n 字节设成 v | 目标地址 | v 会被截成单字节 |
memcmp |
memcmp(p1,p2,n) |
比较 n 字节 | 正/0/负 | 按字节比,忽略 \0 |
memcpy vs memmove 核心区别:
| 场景 | memcpy |
memmove |
|---|---|---|
| 源和目标完全不重叠 | 正确 ✅ | 正确 ✅ |
| 目标地址 > 源地址(向下覆盖) | 未定义 ❌ | 正确 ✅ |
| 目标地址 < 源地址(向上覆盖) | 未定义 ❌ | 正确 ✅ |
| 性能 | 略快 | 略慢(可忽略) |
memset 安全性速查:
| 设的值 | int 数组结果 | 是否安全 |
|---|---|---|
0 |
0x00000000 = 0 |
✅ 安全 |
-1 |
0xFFFFFFFF = -1 |
✅ 安全 |
1 |
0x01010101 = 16843009 |
❌ 危险 |
127 |
0x7F7F7F7F = 2139062143 |
❌ 危险 |
255 |
0xFFFFFFFF = -1(巧合安全) |
⚠️ 碰巧 |
字节视角操作内存,memmove 是 memcpy 的安全升级版,memset 设 int 只有 0 和 -1 是保险的
memset(arr, 1, sizeof(arr)) 输出的不是 1,是 16843009——这条考过无数人
更多推荐

所有评论(0)