C语言——内存函数
内存操作是 C 语言底层编程的核心,memcpy、memmove、memset、memcmp 是 <string.h> 中的四大核心函数,用于数据拷贝、内存初始化和比较。本文梳理了它们的原型、功能、使用场景与注意事项,通过数组、结构体等多类型示例演示用法,并提供带注释的手写实现,解析逐字节操作、内存重叠处理等底层原理。同时对比函数差异,总结指针校验、避免越界等避坑指南,助力开发者理解底层逻辑,提升
在C语言的底层开发中,内存操作是绕不开的核心话题。memcpy、memmove、memset、memcmp这四大内存函数,不仅是面试高频考点,更是写出高效、健壮代码的必备工具。本文将从函数原型、底层原理到手写实现,带你系统性掌握它们的用法与设计思想,彻底搞懂内存操作的底层逻辑。
前置通用
1. 内存操作函数均定义在<string.h>头文件中,使用前必须包含;
2. 函数参数多为void*类型,保证通用性(可接收任意类型的指针),底层通过char*逐字节操作(因为char是1字节,是C语言最小内存单位);
3. 内存操作函数直接操作内存地址,不关心数据类型,比普通类型拷贝(如int/char赋值)更高效、更底层。
一、内存拷贝函数:memcpy
1. 函数原型
void *memcpy(void *dest, const void *src, size_t n);
参数说明:
• dest:目标内存地址(拷贝后的数据存放位置),void*类型;
• src:源内存地址(被拷贝的数据位置),const修饰表示不修改源数据,保证安全性;
• n:需要拷贝的字节数,size_t是C语言无符号整型(unsigned int),专门用于表示内存大小/长度;
返回值:返回目标内存的起始地址dest(方便链式调用)。
2. 核心功能
从src指向的内存地址开始,拷贝n个字节的数据到dest指向的内存地址,不处理内存重叠场景。
3. 关键使用笔记
✅ 可拷贝任意类型数据(int/char/数组/结构体等),突破普通拷贝的类型限制;
❌ 源地址和目标地址不能重叠,若重叠,拷贝结果未定义(编译器可能报错/数据乱码);
✅ dest和src的内存空间必须足够大,至少能容纳n个字节,否则会发生内存越界(程序崩溃);
✅ 底层是从低地址到高地址逐字节拷贝。
4. 基础使用示例(多类型拷贝,逐行注释)
#include <stdio.h>
#include <string.h> // 包含memcpy头文件
// 测试结构体拷贝(memcpy支持任意类型)
typedef struct {
char name[20];
int age;
float score;
} Student;
int main() {
// 示例1:拷贝字符数组
char str1[20] = {0}; // 目标数组,初始化为0
char str2[] = "hello memcpy"; // 源数组
// 拷贝str2的全部内容到str1,strlen(str2)获取有效字符数,+1包含'\0'结束符
memcpy(str1, str2, strlen(str2) + 1);
printf("字符数组拷贝结果:%s\n", str1); // 输出:hello memcpy
// 示例2:拷贝int数组(拷贝3个int元素,共3*4=12字节)
int arr1[10] = {0};
int arr2[] = {1,2,3,4,5};
memcpy(arr1, arr2, 3 * sizeof(int)); // sizeof(int)获取int类型字节数(通常4)
printf("int数组拷贝结果:");
for (int i = 0; i < 3; i++) {
printf("%d ", arr1[i]); // 输出:1 2 3
}
printf("\n");
// 示例3:拷贝结构体
Student s1 = {"张三", 18, 95.5};
Student s2 = {0};
memcpy(&s2, &s1, sizeof(Student)); // 拷贝整个结构体,sizeof获取结构体总字节数
printf("结构体拷贝结果:%s %d %.1f\n", s2.name, s2.age, s2.score); // 输出:张三 18 95.5
return 0;
}
运行结果:
字符数组拷贝结果:hello memcpy
int数组拷贝结果:1 2 3
结构体拷贝结果:张三 18 95.5
5. 手写模拟实现
#include <stdio.h>
#include <assert.h> // 包含断言头文件,用于校验指针有效性
// 模拟实现memcpy函数
void* my_memcpy(void* dest, const void* src, size_t n) {
// 断言:dest和src不能为NULL(空指针),否则直接终止程序,避免野指针访问
assert(dest != NULL && src != NULL);
// 保存目标地址起始位置,最后返回(因为后续指针会偏移)
void* ret = dest;
// 核心:逐字节拷贝,循环n次,每次拷贝1个字节
// 先将void*强转为char*,因为只有char*能保证每次+1偏移1个字节
while (n--) { // n是无符号整型,n--直到0结束
*(char*)dest = *(char*)src; // 解引用,拷贝当前字节
dest = (char*)dest + 1; // 目标指针后移1字节
src = (char*)src + 1; // 源指针后移1字节
}
return ret; // 返回目标地址起始位置
}
// 测试自定义my_memcpy
int main() {
int arr1[10] = {0};
int arr2[] = {10,20,30,40};
// 调用自定义my_memcpy,拷贝2个int元素(8字节)
my_memcpy(arr1, arr2, 2 * sizeof(int));
printf("自定义memcpy测试:");
for (int i = 0; i < 2; i++) {
printf("%d ", arr1[i]); // 输出:10 20
}
return 0;
}
核心实现要点:
1. 用assert校验指针非空,提升程序健壮性;
2. 先保存dest起始地址,因为后续dest指针会偏移,无法直接返回;
3. 必须将void*强转为char*后再操作,保证逐字节拷贝(void*不能直接解引用和偏移)。
二、可重叠内存拷贝函数:memmove
1. 函数原型
void *memmove(void *dest, const void *src, size_t n);
参数/返回值:和memcpy完全一致,仅功能上支持内存重叠。
2. 核心功能
和memcpy的拷贝功能一致,且支持源地址和目标地址重叠的场景(是memcpy的升级版),C语言标准规定:memmove必须处理内存重叠,memcpy可选。
3. 关键使用笔记
✅ 完全兼容memcpy的所有场景,推荐优先使用memmove,更安全;
✅ 处理内存重叠的核心是根据地址位置选择拷贝方向:
• 当dest < src(目标地址在源地址左侧,无重叠/不影响):从低地址到高地址拷贝(和memcpy一致);
• 当dest > src(目标地址在源地址右侧,重叠):从高地址到低地址拷贝(避免先拷贝的字节覆盖后续要拷贝的源数据);
❌ 同样要求dest和src内存空间足够,避免内存越界。
4. 经典使用示例(内存重叠场景,对比memcpy)
#include <stdio.h>
#include <string.h>
int main() {
// 内存重叠场景:同一个数组,将前4个元素拷贝到从第2个元素开始的位置
int arr[] = {1,2,3,4,5,6,7,8};
int n = sizeof(arr) / sizeof(arr[0]);
// 示例1:用memmove处理重叠(正确)
memmove(arr+1, arr, 4 * sizeof(int)); // arr+1是第2个元素的地址(偏移4字节)
printf("memmove重叠拷贝结果:");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]); // 输出:1 1 2 3 4 6 7 8(正确)
}
printf("\n");
// 重置数组
int arr2[] = {1,2,3,4,5,6,7,8};
// 示例2:用memcpy处理重叠(结果未定义,编译器可能乱码)
memcpy(arr2+1, arr2, 4 * sizeof(int));
printf("memcpy重叠拷贝结果:");
for (int i = 0; i < n; i++) {
printf("%d ", arr2[i]); // 不同编译器结果不同,大概率乱码
}
return 0;
}
运行结果(VS2022示例):
memmove重叠拷贝结果:1 1 2 3 4 6 7 8
memcpy重叠拷贝结果:1 1 1 1 1 6 7 8
5. 手写模拟实现(逐行注释,处理地址重叠)
#include <stdio.h>
#include <assert.h>
#include <string.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
// 保存目标地址的起始位置,用于最终返回
void* ret = dest;
// 断言确保传入的指针不为空
assert(dest && src);
// 情况1:目标地址在源地址左侧(无重叠风险)
if (dest < src)
{
// 从低地址到高地址拷贝
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
// 情况2:目标地址在源地址右侧或重叠
else
{
// 先将指针移动到内存块的末尾
char* dst_end = (char*)dest + num - 1;
char* src_end = (char*)src + num - 1;
// 从高地址到低地址拷贝
while (num--)
{
*dst_end = *src_end;
dst_end--;
src_end--;
}
}
return ret;
}
// 测试用例
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
// 测试重叠拷贝:从arr+2的位置拷贝20字节(5个int)到arr+1的位置
my_memmove(arr+1, arr+2, 20);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
核心实现要点:
1. 先将void*转为char*并赋值给临时变量,避免多次强转,简化代码;
2. 通过比较dest和src的地址大小,选择拷贝方向;
3. 从高地址拷贝时,先将指针偏移到最后一个字节,再倒序拷贝。
三、内存设置函数:memset
1. 函数原型
void *memset(void *ptr, int value, size_t n);
参数说明:
• ptr:需要设置的内存起始地址;
• value:需要设置的字节值(int类型,实际存储时取低8位,即0~255);
• n:需要设置的字节数;
返回值:返回设置后的内存起始地址ptr。
2. 核心功能
将ptr指向的内存地址开始,连续n个字节的内容全部设置为value,以字节为单位操作(最关键)。
3. 关键使用笔记
⚠️ 核心易错点:memset是按字节赋值,不是按变量类型赋值!如果给非字符类型(int/float等)赋值,只能设置每个字节都相同的值,否则结果不符合预期;
✅ 常用于内存初始化(如将数组/缓冲区置0、置为指定字符);
✅ value通常传字符(如'a')或整数0,因为字符在内存中占1字节,契合memset的操作方式;
❌ 不要用memset给int数组赋值为1(原因:int占4字节,会被设置为0x01010101,十进制为16843009,而非1)。
4. 基础使用示例(正确/错误示例对比)
#include <stdio.h>
#include <string.h>
int main() {
// 示例1:正确使用→给字符数组设置值(按字节,完美契合)
char str[20] = {0};
memset(str, 'a', 5); // 将前5个字节设置为'a'
printf("字符数组设置结果:%s\n", str); // 输出:aaaaa(正确)
// 示例2:正确使用→将int数组置0(每个字节都是0,整个int就是0)
int arr1[10] = {1,2,3,4,5};
memset(arr1, 0, sizeof(arr1)); // sizeof(arr1)获取数组总字节数
printf("int数组置0结果:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr1[i]); // 输出:0 0 0 0 0(正确)
}
printf("\n");
// 示例3:错误使用→用memset给int数组赋值为1(按字节赋值,结果非1)
int arr2[5] = {0};
memset(arr2, 1, sizeof(arr2));
printf("int数组赋值1的错误结果:");
for (int i = 0; i < 5; i++) {
printf("%d ", arr2[i]); // 输出:16843009 16843009 ...(错误)
}
return 0;
}
运行结果:
字符数组设置结果:aaaaa
int数组置0结果:0 0 0 0 0
int数组赋值1的错误结果:16843009 16843009 16843009 16843009 16843009
5. 手写模拟实现(逐行注释,按字节设置)
#include <stdio.h>
#include <assert.h>
// 模拟实现memset
void* my_memset(void* ptr, int value, size_t n) {
assert(ptr != NULL); // 校验指针非空
void* ret = ptr;
// 强转为char*,逐字节设置,value强转char取低8位
char* p = (char*)ptr;
while (n--) {
*p = (char)value;
p++;
}
return ret;
}
// 测试
int main() {
char buf[30] = {0};
// 调用自定义my_memset,前8个字节设置为'#'
my_memset(buf, '#', 8);
printf("自定义memset测试:%s\n", buf); // 输出:########
return 0;
}
总结:
当有一块内存空间需要设置内容的时候,就可以使用memset函数,值得注意的是memset函数对内存
单元的设置是以字节为单位的。
四、内存比较函数:memcmp
1. 函数原型
int memcmp(const void *ptr1, const void *ptr2, size_t n);
参数说明:
• ptr1/ptr2:需要比较的两块内存的起始地址;
• n:需要比较的字节数;
返回值:int类型,根据比较结果返回正负/0(按ASCII码值逐字节比较):
◦ 返回0:前n个字节的内容完全相同;
◦ 返回正数:第一次比较不同的字节,ptr1的字节值 > ptr2的字节值;
◦ 返回负数:第一次比较不同的字节,ptr1的字节值 < ptr2的字节值。
2. 核心功能
从ptr1和ptr2指向的内存地址开始,逐字节比较前n个字节的内容,不关心数据类型,找到第一个不同的字节即停止比较。
3. 关键使用笔记
✅ 可比较任意类型的内存块(int/char/结构体/数组),比strcmp(仅比较字符串)更通用;
✅ 逐字节比较,直到找到第一个不同的字节或比较完n个字节;
✅ 比较的是内存中的原始二进制值,而非变量的实际值(如浮点型1.0和整型1内存值不同,比较结果不同);
✅ 字符串比较可用memcmp,但strcmp会自动识别'\0'结束,memcmp需要手动指定字节数。
4. 基础使用示例(多场景比较)
#include <stdio.h>
#include <string.h>
int main() {
// 示例1:比较字符数组(前3个字节相同,第4个不同)
char str1[] = "hello";
char str2[] = "hella";
int res1 = memcmp(str1, str2, 5);
printf("字符数组比较结果:%d\n", res1); // 输出:1('l'=108 > 'a'=97)
// 示例2:比较int数组(前2个元素相同,第3个不同)
int arr1[] = {1,2,3,4};
int arr2[] = {1,2,4,3};
int res2 = memcmp(arr1, arr2, 3 * sizeof(int));
printf("int数组比较结果:%d\n", res2); // 输出:-1(3的二进制值 < 4)
// 示例3:比较前2个字节,内容相同
int arr3[] = {1,2,3};
int arr4[] = {1,2,5};
int res3 = memcmp(arr3, arr4, 2 * sizeof(int));
printf("前2个int比较结果:%d\n", res3); // 输出:0(完全相同)
return 0;
}
运行结果:
字符数组比较结果:1
int数组比较结果:-1
前2个int比较结果:0
5. 手写模拟实现(逐行注释,逐字节比较)
#include <stdio.h>
#include <assert.h>
// 模拟实现memcmp
int my_memcmp(const void* ptr1, const void* ptr2, size_t n) {
assert(ptr1 != NULL && ptr2 != NULL); // 校验指针非空
const char* p1 = (const char*)ptr1;
const char* p2 = (const char*)ptr2;
// 逐字节比较
while (n--) {
if (*p1 != *p2) {
// 返回差值:正数/负数,契合标准库返回规则
return *p1 - *p2;
}
p1++;
p2++;
}
// 循环结束,说明前n个字节完全相同
return 0;
}
// 测试
int main() {
char s1[] = "abcde";
char s2[] = "abxde";
int res = my_memcmp(s1, s2, 5);
if (res == 0) {
printf("内容相同\n");
} else if (res > 0) {
printf("s1 > s2,结果:%d\n", res); // 输出:s1 > s2,结果:-20('c'-'x'=-20,实际为负数,示例验证)
} else {
printf("s1 < s2,结果:%d\n", res);
}
return 0;
}
核心实现要点:
1. 用const char*接收强转后的指针,因为不修改比较的内存内容;
2. 找到第一个不同的字节,直接返回两字节的差值,保证返回值的正负性;
3. 循环结束后返回0,表示所有比较的字节均相同。
总结:
如果要比较2块内存单元的数据的大小,可以使用 memcmp 函数,这个函数的特点就是可以指定比较
长度。
memcmp 函数是通过返回值告知大小关系的。
五、四大内存函数核心对比表
| 函数名 | 核心功能 | 支持内存重叠 | 操作单位 | 核心返回值/结果 |
| memcpy | 内存拷贝 | 不支持 | 字节 | 返回目标地址 |
| memmove | 内存拷贝(升级版) | 支持 | 字节 | 返回目标地址 |
| memset | 内存设置 | 无此场景 | 字节 | 返回设置后地址 |
| memcmp | 内存比较 | 无此场景 | 字节 | 0/正数/负数 |
六、通用编程规范&避坑指南
1. 指针非空校验:使用/实现内存函数时,必须先校验指针是否为NULL,推荐用assert,快速定位野指针问题;
2. 字节数计算:用sizeof(类型/变量)计算字节数,避免硬编码(如4代替sizeof(int)),提升代码可移植性;
3. 内存越界:确保dest/ptr指向的内存空间足够容纳n个字节,否则会导致程序崩溃/内存污染;
4. memset按字节操作:非字符类型变量仅能置0,不要赋值为其他整数;
5. 优先使用memmove:替代memcpy,避免内存重叠导致的未定义行为;
6. memcmp比较字符串:需手动指定strlen(str)+1,包含'\0'结束符,否则可能出现错误比较。
到这里,我们已经把C语言中四大核心内存函数从原理、用法到手写实现都完整拆解了一遍。这些函数看似简单,却是C语言“贴近硬件、高效灵活”特性的最佳体现。掌握它们不仅能帮你写出更健壮的底层代码,更能加深你对内存模型的理解。希望这份内容能成为你学习路上的实用手册,也欢迎在评论区留下你的疑问和思考,一起交流进步。
更多推荐

所有评论(0)