【C语言内存函数完全指南】:memcpy、memmove、memset、memcmp 的用法、区别与模拟实现(含代码示例)
✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松get !
🚀 个人主页 :0xCode小新 · CSDN
🌱 代码仓库 :0xCode小新· Gitee
📌 专栏系列
- 📖 《c语言》
📖 《鸿蒙应用开发项目教程》💬 座右铭 : “ 积跬步,以致千里。”
在C语言编程中,对内存的直接操作是每个开发者必须掌握的核心技能。无论是数据复制、移动、初始化还是比较,都离不开高效且安全的内存函数。本篇内容将以清晰易懂的方式,带你深入理解C语言中四大内存函数——
memcpy
、memmove
、memset
和memcmp
的使用方法、适用场景,并手把手教你如何模拟实现它们。这篇指南将为你提供实用的知识与代码示例,助你在内存管理中游刃有余。
文章目录
- 1. [memcpy](https://legacy.cplusplus.com/reference/cstring/memcpy/?kw=memcpy)使用和模拟实现
- 1. 函数是什么?
- 2. 核心特性与使用场景
- 3. 实战示例
- 4. 动手模拟实现
- 5. 重要提醒:什么时候不能用 memcpy?
- 2. [memmove](https://legacy.cplusplus.com/reference/cstring/memmove/)使用和模拟实现
- 1. 函数是什么?为什么需要它?
- 2. 核心问题:什么是内存重叠?为什么 memcpy 处理不了?
- 3. memmove 的智能解决方案
- 4. 动手模拟实现
- 5. 验证实现
- 6. 使用建议
- 3. [memset](https://legacy.cplusplus.com/reference/cstring/memset/)函数的使用和模拟实现
- 1. 函数是什么?
- 2. 核心特性与工作原理
- 3. 实战示例
- 4. 重要注意事项
- 5. 动手模拟实现
- 6. 验证实现
- 4. [memcmp](https://legacy.cplusplus.com/reference/cstring/memcmp/)函数的使用和模拟实现
- 1. 函数是什么?
- 2. 核心特性与工作原理
- 3. 返回值规则详解
- 4. 实战示例
- 5. 与 strcmp 的区别
- 6. 动手模拟实现
- 7. 验证实现
- 结语
1. memcpy使用和模拟实现
1. 函数是什么?
memcpy
是 C 语言标准库中一个用于内存拷贝的函数。它的核心任务是将一块内存中的数据,原封不动地复制到另一块内存中。
函数原型:
void *memcpy(void *destination, const void *source, size_t num);
destination
: 指向目标内存区域的指针,复制的内容将存放于此。source
: 指向源内存区域的指针,复制的内容来源于此。num
: 要复制的字节数。- 返回值: 返回指向
destination
的指针。
2. 核心特性与使用场景
- 按字节复制:
memcpy
不关心内存中存储的是什么数据类型(int
,char
,struct
等),它只负责忠实地、一个字节一个字节地进行复制。- 不关心终止符:与
strcpy
不同,memcpy
遇到'\0'
并不会停止。它只认准你传入的num
参数,复制完指定字节数后才会停下。这使得它可以用于复制任何二进制数据,如图片、结构体等。- 不处理重叠:这是
memcpy
最关键的一个限制。如果source
和destination
所指向的内存区域有重叠,使用memcpy
的结果是“未定义的”。这意味着可能会复制出错,程序崩溃,或者出现各种意想不到的情况。处理重叠内存是memmove
的任务。
3. 实战示例
让我们通过一个代码示例来看看它的实际效果:
#include <stdio.h>
#include <string.h> // 包含 memcpy 的头文件int main() {int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};int arr2[10] = {0}; // 目标数组,初始化为0// 将 arr1 的前 20 个字节(即前5个int元素)复制到 arr2memcpy(arr2, arr1, 20);// 打印 arr2 的结果for (int i = 0; i < 10; i++) {printf("%d ", arr2[i]);}// 输出:1 2 3 4 5 0 0 0 0 0return 0;
}
代码解读:
memcpy(arr2, arr1, 20);
这行代码的意思是:从arr1
的首地址开始,拷贝 20 个字节的数据到arr2
。- 在我们的系统中,一个
int
类型通常占 4 个字节。因此,20 个字节正好对应 5 个int
元素。 - 结果就是
arr2
的前 5 个元素变成了1, 2, 3, 4, 5
,后面的元素保持为 0。
4. 动手模拟实现
理解一个函数最好的方式就是亲手实现它。下面我们来看看如何模拟实现一个自己的 memcpy
:
#include <assert.h>void* my_memcpy(void* dst, const void* src, size_t count) {// 1. 保存目标指针的起始位置,用于最后返回void* ret = dst;// 2. 安全检查:确保源指针和目标指针不是空指针assert(dst && src);// 3. 逐字节拷贝while (count--) {// 将 void* 强制转换为 char*,因为char类型占1个字节,便于逐字节操作*(char*)dst = *(char*)src;// 移动指针到下一个字节dst = (char*)dst + 1;src = (char*)src + 1;}// 4. 返回目标内存的起始地址return ret;
}
实现要点解析:
void*
指针的妙用:void*
是“无类型指针”,可以接受任何类型的地址。这赋予了memcpy
处理任意数据类型的能力。- 类型转换:在函数内部,我们将
void*
转换为char*
。因为char
类型的大小是 1 字节,这样(char*)dst + 1
就正好移动一个字节,实现了逐字节拷贝。 - 断言
assert
:这是一种防御性编程技巧,确保传入的指针是有效的,避免对空指针进行操作导致程序崩溃。 - 返回值:返回最初的
dst
指针,是为了支持函数的链式调用,例如 printf(“%s”, (char*)memcpy(dest, src, n))。
5. 重要提醒:什么时候不能用 memcpy?
当源内存和目标内存发生重叠时!
请看这个反面例子:
int arr[] = {1, 2, 3, 4, 5};
// 将前3个元素复制到从第2个元素开始的位置
my_memcpy(arr+1, arr, 12); // 12字节 = 3个int
期望的结果可能是 {1, 1, 2, 3, 5}
,但由于我们的 my_memcpy
是从低地址向高地址拷贝,在复制过程中,源数据在被读取之前就被覆盖了,导致结果出错。这种情况下,必须使用 memmove
函数。
2. memmove使用和模拟实现
1. 函数是什么?为什么需要它?
memmove
是 C 语言标准库中另一个用于内存拷贝的函数,它与 memcpy
功能相似,但有一个关键的区别:memmove
能够正确处理源内存和目标内存重叠的情况。
函数原型:
void *memmove(void *destination, const void *source, size_t num);
- 参数与返回值:与
memcpy
完全相同 - 核心优势:处理内存重叠时的安全性
2. 核心问题:什么是内存重叠?为什么 memcpy 处理不了?
让我们通过一个例子来理解这个问题:
#include <stdio.h>
#include <string.h>int main() {int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 场景:将数组前5个元素复制到从第3个元素开始的位置// 这会导致源区域 [0-4] 与目标区域 [2-6] 发生重叠memmove(arr + 2, arr, 20); // 20字节 = 5个int元素for (int i = 0; i < 10; i++) {printf("%d ", arr[i]);}// 输出:1 2 1 2 3 4 5 8 9 10return 0;
}
如果用 memcpy 会发生什么?
如果我们错误地使用 memcpy(arr + 2, arr, 20)
,由于 memcpy
只是简单地从低地址向高地址逐字节拷贝,会发生这样的悲剧:
- 先把
arr[0]
(1) 拷贝到arr[2]
→ 数组变成[1, 2, 1, 4, 5, ...]
- 再把
arr[1]
(2) 拷贝到arr[3]
→ 数组变成[1, 2, 1, 2, 5, ...]
- 接着把
arr[2]
(现在已经是1了!) 拷贝到arr[4]
→ 数组变成[1, 2, 1, 2, 1, ...]
看到问题了吗?源数据在被读取之前就被覆盖了,导致复制结果完全错误!
3. memmove 的智能解决方案
memmove
通过判断内存重叠情况,智能地选择拷贝方向来解决这个问题:
- 当目标地址在源地址之前,或者两者没有重叠时:从低地址向高地址拷贝(与
memcpy
相同) - 当目标地址在源地址之后,且存在重叠时:从高地址向低地址拷贝(反向拷贝)
4. 动手模拟实现
下面是 memmove
的模拟实现代码,体现了这种智能的拷贝策略:
#include <assert.h>void* my_memmove(void* dst, const void* src, size_t count) {void* ret = dst;// 安全检查assert(dst && src);// 情况1:目标地址在源地址之前,或者没有重叠// 从低地址向高地址拷贝(正向拷贝)if (dst <= src || (char*)dst >= (char*)src + count) {while (count--) {*(char*)dst = *(char*)src;dst = (char*)dst + 1;src = (char*)src + 1;}}// 情况2:目标地址在源地址之后,且存在重叠// 从高地址向低地址拷贝(反向拷贝)else {// 将指针移动到内存块的末尾dst = (char*)dst + count - 1;src = (char*)src + count - 1;while (count--) {*(char*)dst = *(char*)src;dst = (char*)dst - 1;src = (char*)src - 1;}}return ret;
}
实现要点解析:
- 重叠判断逻辑:
dst <= src
:目标在源之前,安全正向拷贝(char*)dst >= (char*)src + count
:目标在源结束之后,没有重叠,安全正向拷贝- 其他情况:目标在源之后且存在重叠,需要反向拷贝
- 反向拷贝技巧:
- 先将指针移动到内存块的最后一个字节:
dst = (char*)dst + count - 1
- 然后从后往前逐个字节拷贝
- 这样确保重叠部分中尚未被读取的数据不会被覆盖
- 先将指针移动到内存块的最后一个字节:
5. 验证实现
让我们用之前的例子来测试:
int main() {int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};printf("原始数组: ");for (int i = 0; i < 10; i++) printf("%d ", arr[i]);printf("\n");// 测试重叠拷贝my_memmove(arr + 2, arr, 20);printf("拷贝后数组: ");for (int i = 0; i < 10; i++) printf("%d ", arr[i]);printf("\n");return 0;
}
输出结果:
perfect!数据被正确地复制了,没有因为内存重叠而出错。
6. 使用建议
- 安全第一:当你不确定源内存和目标内存是否重叠时,优先使用
memmove
。 - 性能考量:如果确定两者不重叠,
memcpy
可能稍微快一点(因为不需要做重叠判断)。 - 适用场景:
- 数组元素的移动
- 缓冲区内部数据的调整
- 任何可能涉及内存重叠的拷贝操作
3. memset函数的使用和模拟实现
1. 函数是什么?
memset
是 C 语言标准库中用于内存初始化和批量设置的函数。它能够将指定内存区域的每个字节都设置为特定的值。
函数原型:
void *memset(void *ptr, int value, size_t num);
ptr
: 指向要设置的内存区域的起始地址value
: 要设置的值(以int
形式传递,但实际使用时会被转换为unsigned char
)num
: 要设置的字节数- 返回值: 返回指向
ptr
的指针
2. 核心特性与工作原理
- 按字节设置:这是
memset
最重要的特性。它不关心内存中存储的是什么数据类型,只是简单地将每个字节都设置为指定的值。- 高效批量操作:相比于使用循环逐个赋值,
memset
通常经过高度优化,执行效率更高。- 用途广泛:常用于内存初始化、数组清零、字符串填充等场景。
3. 实战示例
让我们通过几个例子来理解它的用法:
示例1:字符串填充
#include <stdio.h>
#include <string.h>int main() {char str[] = "hello world";// 将前6个字符设置为'x'memset(str, 'x', 6);printf("%s\n", str);return 0;
}
输出:
示例2:数组清零
#include <stdio.h>
#include <string.h>int main() {int arr[5] = {1, 2, 3, 4, 5};printf("清零前: ");for (int i = 0; i < 5; i++) {printf("%d ", arr[i]);}printf("\n");// 将整个数组清零memset(arr, 0, sizeof(arr));printf("清零后: ");for (int i = 0; i < 5; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}
输出:
4. 重要注意事项
陷阱:不要误解 memset 对整型数组的设置
这是一个常见的错误用法:
int arr[5];
// 错误:试图将每个元素设置为1
memset(arr, 1, sizeof(arr));
你以为的结果:{1, 1, 1, 1, 1}
实际的结果:每个 int
元素的每个字节都被设置为1
假设 int
占4字节,那么每个 int
元素的值将是:
00000001 00000001 00000001 00000001
转换为十进制就是:16843009,而不是1!
正确用法总结:
- ✅ 清零:
memset(arr, 0, sizeof(arr))
- ✅ 设置为 -1:
memset(arr, -1, sizeof(arr))
(因为-1的二进制表示是所有位都是1) - ✅ 字符数组/字符串操作
- ❌ 不要用于将整型数组设置为非0非-1的值
5. 动手模拟实现
让我们自己实现一个 memset
函数来加深理解:
void* my_memset(void* ptr, int value, size_t num) {// 保存原始指针,用于返回void* start = ptr;// 将value转换为unsigned char,确保只取低8位unsigned char byte_value = (unsigned char)value;// 将void*转换为unsigned char*以便逐字节操作unsigned char* p = (unsigned char*)ptr;// 逐字节设置内存for (size_t i = 0; i < num; i++) {p[i] = byte_value;}return start;
}
更优化的版本(使用指针运算):
void* my_memset_optimized(void* ptr, int value, size_t num) {unsigned char* p = (unsigned char*)ptr;unsigned char byte_value = (unsigned char)value;// 使用指针运算代替数组索引while (num--) {*p++ = byte_value;}return ptr;
}
实现要点解析:
- 类型转换:将
void*
转换为unsigned char*
是关键,因为unsigned char
正好是1字节,便于逐字节操作。 - 值处理:将传入的
int
值转换为unsigned char
,确保只使用低8位,避免意外行为。 - 循环方式:可以使用数组索引
p[i]
或指针运算*p++
,指针通常更加高效。 - 返回值:返回原始指针,支持链式调用。
6. 验证实现
#include <stdio.h>int main() {// 测试1:字符串填充char str[] = "hello world";my_memset(str, 'A', 5);printf("测试1: %s\n", str); // 输出: AAAAA world// 测试2:数组清零int arr[3] = {10, 20, 30};my_memset(arr, 0, sizeof(arr));printf("测试2: %d %d %d\n", arr[0], arr[1], arr[2]); // 输出: 0 0 0// 测试3:验证按字节设置的特点int test = 0;my_memset(&test, 1, sizeof(test));printf("测试3: %d (验证按字节设置)\n", test); // 输出: 16843009return 0;
}
4. memcmp函数的使用和模拟实现
1. 函数是什么?
memcmp
是 C 语言标准库中用于比较两块内存区域内容的函数。它能够逐字节地比较两个内存块,判断它们是否相等或者哪个更大。
函数原型:
int memcmp(const void *ptr1, const void *ptr2, size_t num);
ptr1
: 指向第一个内存块的指针ptr2
: 指向第二个内存块的指针num
: 要比较的字节数- 返回值: 比较结果,遵循特定的规则
2. 核心特性与工作原理
- 按字节比较:
memcmp
逐个字节地比较两个内存区域,直到发现不同的字节或比较完所有指定字节 - 不关心内容类型:与
memcpy
类似,它不关心内存中存储的是什么数据类型 - 不依赖终止符:与
strcmp
不同,memcmp
遇到'\0'
不会停止,它会比较完所有指定字节 - 精确比较:能够比较任何二进制数据,包括结构体、数组等
3. 返回值规则详解
memcmp
的返回值规则很明确:
返回值 | 含义 |
---|---|
< 0 | 第一个不匹配的字节在 ptr1 中的值 < 在 ptr2 中的值(按无符号字符解释) |
= 0 | 两个内存块的内容完全相等 |
> 0 | 第一个不匹配的字节在 ptr1 中的值 > 在 ptr2 中的值(按无符号字符解释) |
重要提示:比较时是按照 unsigned char
类型来解释每个字节的!
4. 实战示例
让我们通过几个例子来深入理解:
示例1:基本字符串比较
#include <stdio.h>
#include <string.h>int main() {char str1[] = "Hello";char str2[] = "Hello";char str3[] = "Hell0"; // 最后是数字0int result1 = memcmp(str1, str2, 5);int result2 = memcmp(str1, str3, 5);printf("比较 'Hello' 和 'Hello': %d\n", result1); // 输出: 0printf("比较 'Hello' 和 'Hell0': %d\n", result2); // 输出: >0的数字return 0;
}
示例2:区分大小写的比较
#include <stdio.h>
#include <string.h>int main() {char buffer1[] = "DWga0tP12df0";char buffer2[] = "DWGA0TP12DF0"; int n = memcmp(buffer1, buffer2, sizeof(buffer1));if (n > 0)printf("'%s' 大于 '%s'\n", buffer1, buffer2);else if (n < 0)printf("'%s' 小于 '%s'\n", buffer1, buffer2);elseprintf("'%s' 等于 '%s'\n", buffer1, buffer2);return 0;
}
输出:
这是因为小写字母的 ASCII 值大于对应的大写字母。
示例3:比较结构体
#include <stdio.h>
#include <string.h>typedef struct {int id;char name[20];float score;
} Student;int main() {Student s1 = {1, "Alice", 95.5};Student s2 = {1, "Alice", 95.5};Student s3 = {2, "Bob", 88.0};// 比较两个相同的学生int result1 = memcmp(&s1, &s2, sizeof(Student));printf("相同学生比较: %d\n", result1); // 输出: 0// 比较两个不同的学生int result2 = memcmp(&s1, &s3, sizeof(Student));printf("不同学生比较: %d\n", result2); // 输出: 非0值return 0;
}
5. 与 strcmp 的区别
理解 memcmp
和 strcmp
的区别很重要:
特性 | memcmp | strcmp |
---|---|---|
停止条件 | 比较完指定字节数 | 遇到 '\0' 终止符 |
参数 | 需要指定比较的字节数 | 自动根据字符串长度比较 |
适用范围 | 任何内存数据(结构体、数组等) | 仅适用于以 '\0' 结尾的字符串 |
性能 | 通常更快(不需要检查终止符) | 需要检查终止符 |
6. 动手模拟实现
让我们自己实现一个 memcmp
函数:
int my_memcmp(const void* ptr1, const void* ptr2, size_t num) {// 转换为 unsigned char* 以便逐字节比较const unsigned char* p1 = (const unsigned char*)ptr1;const unsigned char* p2 = (const unsigned char*)ptr2;// 逐字节比较for (size_t i = 0; i < num; i++) {if (p1[i] != p2[i]) {return (int)(p1[i]) - (int)(p2[i]);}}return 0;
}
更优化的指针版本:
int my_memcmp_optimized(const void* ptr1, const void* ptr2, size_t num) {const unsigned char* p1 = ptr1;const unsigned char* p2 = ptr2;while (num-- > 0) {if (*p1 != *p2) {return (*p1 > *p2) ? 1 : -1;}p1++;p2++;}return 0;
}
实现要点解析:
- 类型转换:转换为
unsigned char*
确保按字节比较,并且正确处理符号问题 - 比较逻辑:发现不同的字节立即返回,否则继续比较
- 返回值计算:返回第一个不同字节的差值,确保符合标准规定的正负号
- 边界处理:正确处理
num
为 0 的情况
7. 验证实现
#include <stdio.h>void test_comparison(const char* desc, const void* p1, const void* p2, size_t n) {int result1 = memcmp(p1, p2, n);int result2 = my_memcmp(p1, p2, n);printf("%s:\n", desc);printf(" 标准 memcmp: %d\n", result1);printf(" 我们的实现: %d\n", result2);printf(" 结果一致: %s\n\n", (result1 == result2) ? "是" : "否");
}int main() {// 测试1:相同字符串char str1[] = "Hello";char str2[] = "Hello";test_comparison("相同字符串", str1, str2, 5);// 测试2:不同字符串char str3[] = "Hello";char str4[] = "Hell0";test_comparison("不同字符串", str3, str4, 5);// 测试3:数组比较int arr1[] = {1, 2, 3};int arr2[] = {1, 2, 4};test_comparison("数组比较", arr1, arr2, sizeof(arr1));// 测试4:部分比较test_comparison("部分比较", "ABCDE", "ABCDF", 4); // 只比较前4个字节return 0;
}
结语
通过本篇文章的学习,我们学习了C语言中四大内存函数:
memcpy
、memmove
、memset
和memcmp
。大家不仅要会使用这些函数,还要理解每个函数的工作原理,自己也可以通过模拟实现来加深理解,并且要时刻注意内存边界和重叠问题,在不确定时优先选择更安全的函数!
那么本篇内容到这里就结束了,希望大家能将所学知识灵活运用到实际编程中,在C语言的道路上稳步前行!
最后分享一段我非常喜欢的话送给大家:
每个优秀的人,都有一段沉默的时光。那段时光,是付出了很多努力,却得不到结果的日子,我们把他叫做扎根。