【C语言内存函数完全指南】:memcpy、memmove、memset、memcmp 的用法、区别与模拟实现(含代码示例)
本文深入解析C语言四大内存函数:memcpy、memmove、memset和memcmp。通过图解和代码示例,详细讲解各函数的功能特性和使用场景:memcpy实现高效内存拷贝但不处理重叠区域;memmove智能处理内存重叠;memset用于内存初始化;memcmp比较内存内容。文章还提供了模拟实现代码,帮助开发者深入理解底层原理。特别强调memcpy与memmove的关键区别,并给出内存重叠问题的
✨ 用 清晰易懂的图解 帮你建立直观认知 ,用通俗的 代码语言 帮你落地理解, 让每个知识点都能 轻松get !
🚀 个人主页 :0xCode小新 · CSDN
🌱 代码仓库 :0xCode小新· Gitee
📌 专栏系列
- 📖 《c语言》
📖 《鸿蒙应用开发项目教程》💬 座右铭 : “ 积跬步,以致千里。”
在C语言编程中,对内存的直接操作是每个开发者必须掌握的核心技能。无论是数据复制、移动、初始化还是比较,都离不开高效且安全的内存函数。本篇内容将以清晰易懂的方式,带你深入理解C语言中四大内存函数——
memcpy
、memmove
、memset
和memcmp
的使用方法、适用场景,并手把手教你如何模拟实现它们。这篇指南将为你提供实用的知识与代码示例,助你在内存管理中游刃有余。
文章目录
- 1. [memcpy](https://legacy.cplusplus.com/reference/cstring/memcpy/?kw=memcpy)使用和模拟实现
- 2. [memmove](https://legacy.cplusplus.com/reference/cstring/memmove/)使用和模拟实现
- 3. [memset](https://legacy.cplusplus.com/reference/cstring/memset/)函数的使用和模拟实现
- 4. [memcmp](https://legacy.cplusplus.com/reference/cstring/memcmp/)函数的使用和模拟实现
- 结语
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元素)复制到 arr2
memcpy(arr2, arr1, 20);
// 打印 arr2 的结果
for (int i = 0; i < 10; i++) {
printf("%d ", arr2[i]);
}
// 输出:1 2 3 4 5 0 0 0 0 0
return 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 10
return 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); // 输出: 16843009
return 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"; // 最后是数字0
int result1 = memcmp(str1, str2, 5);
int result2 = memcmp(str1, str3, 5);
printf("比较 'Hello' 和 'Hello': %d\n", result1); // 输出: 0
printf("比较 '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);
else
printf("'%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语言的道路上稳步前行!
最后分享一段我非常喜欢的话送给大家:
每个优秀的人,都有一段沉默的时光。那段时光,是付出了很多努力,却得不到结果的日子,我们把他叫做扎根。
更多推荐
所有评论(0)