1. memcpy 函数:内存块拷贝(不重叠)
memcpy 是 C 语言中用于内存拷贝的标准库函数,它不关心内存中存放的是什么类型的数据,只按字节进行复制。
c
void * memcpy ( void * destination, const void * source, size_t num );
功能
- 从
source指向的位置开始,向后复制num个字节的数据到destination指向的内存位置。 - 重要限制 :如果
source和destination指向的内存区域有任何重叠,复制结果是未定义的。 memcpy只负责拷贝不重叠 的内存;若存在重叠,应使用memmove。- 使用前需要包含头文件
<string.h>。
参数说明
| 参数 | 含义 |
|---|---|
destination |
目标空间指针,拷贝的数据存放于此 |
source |
源空间指针,要拷贝的数据来源于此 |
num |
要拷贝的字节数 |
返回值
拷贝完成后,返回目标空间的起始地址(即 destination 的值)。
1.1 代码演示
c
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[20] = { 0 };
int sz2 = sizeof(arr2) / sizeof(arr2[0]);
memcpy(arr2, arr1+2, 20);
for (int i = 0; i < sz2; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
代码逐行解释:
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };:定义一个整型数组arr1,包含 10 个元素。int arr2[20] = { 0 };:定义一个 20 个元素的整型数组arr2,全部初始化为 0。int sz2 = sizeof(arr2) / sizeof(arr2[0]);:计算arr2的元素个数(20)。memcpy(arr2, arr1+2, 20);:从arr1的第 3 个元素(arr1+2,即3)开始,拷贝 20 个字节(即 5 个int,因为sizeof(int)=4)到arr2的起始位置。拷贝的内容为{3,4,5,6,7}。- 循环打印
arr2的前 20 个元素,结果为3 4 5 6 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0。
1.2 模拟实现
c
#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
void* ret = dest; // 保存目标起始地址,用于返回
assert(dest && src); // 断言:dest 和 src 都不能为空指针
while (num--) // 逐字节拷贝,拷贝 num 次
{
*(char*)dest = *(char*)src; // 将 src 当前字节拷贝到 dest
dest = (char*)dest + 1; // dest 指针后移 1 字节
src = (char*)src + 1; // src 指针后移 1 字节
}
return ret; // 返回目标起始地址
}
代码逐行解释:
void* my_memcpy(void* dest, const void* src, size_t num):使用void*接收任意类型的指针,const修饰src表示不修改源数据。void* ret = dest;:保存dest的原始地址,因为后面dest会移动。assert(dest && src);:断言检查,确保两个指针都不为NULL,若为NULL则程序终止。while (num--):循环num次,每次拷贝一个字节。num--先判断num是否为非 0,再自减。*(char*)dest = *(char*)src;:将dest和src强制转换为char*(因为char占 1 字节),然后解引用赋值,完成一个字节的拷贝。dest = (char*)dest + 1;和src = (char*)src + 1;:将指针向后移动 1 个字节,指向下一个待拷贝的位置。return ret;:返回最初保存的目标起始地址,支持链式调用。
测试代码:
c
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[20] = { 0 };
int sz2 = sizeof(arr2) / sizeof(arr2[0]);
my_memcpy(arr2, arr1+2, 20);
for (int i = 0; i < sz2; i++)
{
printf("%d ", arr2[i]);
}
return 0;
}
输出结果与标准 memcpy 一致:3 4 5 6 7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0。
2. memmove 函数:内存块拷贝(支持重叠)
c
void * memmove ( void * destination, const void * source, size_t num );
功能
memmove也用于内存块拷贝,与memcpy的核心区别 在于:memmove可以正确处理源内存块和目标内存块重叠的情况。- 使用前需要包含头文件
<string.h>。
参数说明
| 参数 | 含义 |
|---|---|
destination |
目标空间指针 |
source |
源空间指针 |
num |
要拷贝的字节数 |
返回值
拷贝完成后,返回目标空间的起始地址。
2.1 代码演示
c
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
memmove(arr1 + 2, arr1, 20);
for (int i = 0; i < sz1; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
代码逐行解释:
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };:定义数组arr1。int sz1 = sizeof(arr1) / sizeof(arr1[0]);:计算元素个数(10)。memmove(arr1 + 2, arr1, 20);:将arr1的前 20 个字节(即前 5 个int:1,2,3,4,5)拷贝到arr1+2开始的位置(即第 3 个元素开始)。这里源和目标有重叠(源是arr1[0]~arr1[4],目标是arr1[2]~arr1[6]),memmove能正确处理。- 循环打印结果:
1 2 1 2 3 4 5 8 9 10。可以看到,arr1[2]和arr1[3]被正确复制为原来的1和2,而不是被覆盖后的错误值。
2.2 模拟实现
c
#include <assert.h>
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
assert(dest && src);
if (dest < src) // 目标地址在源地址之前,从前往后拷贝
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else // 目标地址在源地址之后(或相等),从后往前拷贝
{
while (num--)
{
*((char*)dest + num) = *((char*)src + num);
}
}
return ret;
}
代码逐行解释:
if (dest < src):判断目标地址是否小于源地址。如果是,说明目标在源的前面,从前往后拷贝不会覆盖未拷贝的源数据。- 从前往后拷贝的逻辑与
my_memcpy相同。
- 从前往后拷贝的逻辑与
else:目标地址在源地址之后(或相等),此时如果从前往后拷贝,会先覆盖源中尚未拷贝的数据。因此需要从后往前 拷贝。while (num--):循环num次。*((char*)dest + num) = *((char*)src + num);:每次拷贝最后一个字节。(char*)dest + num指向目标区域的最后一个字节,(char*)src + num指向源区域的最后一个字节。随着num递减,依次向前拷贝,确保不会覆盖未拷贝的数据。
测试代码:
c
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz1 = sizeof(arr1) / sizeof(arr1[0]);
my_memmove(arr1 + 2, arr1, 20);
for (int i = 0; i < sz1; i++)
{
printf("%d ", arr1[i]);
}
return 0;
}
输出结果:1 2 1 2 3 4 5 8 9 10,与标准 memmove 一致。
3. memset 函数:内存设置
c
void * memset ( void * ptr, int value, size_t num );
功能
将 ptr 指向的内存块的前 num 个字节设置为 value(以 unsigned char 方式转换),常用于初始化数组或结构体。
- 头文件 :
<string.h> - 参数 :
ptr:指向要设置的内存空间起始地址value:要设置的值(以unsigned char为单位转换)num:要设置的字节数
- 返回值 :返回
ptr的起始地址
使用示例
c
#include <stdio.h>
#include <string.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
memset(arr, 0, sizeof(arr)); // 将 arr 所有字节设为 0
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]); // 输出:0 0 0 0 0
}
return 0;
}
注意 :memset 按字节设置,对于非 char 类型的数组,设置非 0 值时要小心。例如 memset(arr, 1, sizeof(arr)) 会将每个字节设为 0x01,对于 int 数组,每个元素会变成 0x01010101(即 16843009),而不是 1。
3.1 常见陷阱与正确用法
陷阱:对 int 数组设置非 0 值
memset 是按字节 进行设置的,而 int 类型通常占 4 个字节。当使用 memset 对 int 数组设置非 0 值时,会将每个字节都设置为该值,导致结果与预期不符。
c
#include <stdio.h>
#include <string.h>
int main()
{
int arr[5];
memset(arr, 1, sizeof(arr)); // 将每个字节设为 0x01
for (int i = 0; i < 5; i++)
{
printf("arr[%d] = %d (0x%08x)\n", i, arr[i], arr[i]);
}
return 0;
}
输出结果:
arr[0] = 16843009 (0x01010101)
arr[1] = 16843009 (0x01010101)
arr[2] = 16843009 (0x01010101)
arr[3] = 16843009 (0x01010101)
arr[4] = 16843009 (0x01010101)
原理分析:
memset(arr, 1, sizeof(arr))将arr所占的 20 个字节(5 个int× 4 字节)每个字节都写为0x01。- 对于小端字节序的机器,每个
int在内存中的 4 个字节为01 01 01 01,对应的整数值为0x01010101,即十进制的 16843009。 - 因此,我们期望的
arr[i] = 1并没有实现,而是得到了一个很大的数。
正确用法:使用循环初始化
如果需要将 int 数组初始化为特定的非 0 值(如 1、-1 等),应使用循环逐个元素赋值。
c
#include <stdio.h>
int main()
{
int arr[5];
for (int i = 0; i < 5; i++)
{
arr[i] = 1; // 正确:每个元素赋值为 1
}
for (int i = 0; i < 5; i++)
{
printf("arr[%d] = %d\n", i, arr[i]);
}
return 0;
}
输出结果:
arr[0] = 1
arr[1] = 1
arr[2] = 1
arr[3] = 1
arr[4] = 1
特殊情况:初始化为 0 或 -1
虽然 memset 对非 0 值有陷阱,但对于 0 和 -1 这两个特殊值,memset 可以正确工作:
- 初始化为 0 :每个字节设为
0x00,int值为0x00000000,即 0。 - 初始化为 -1 :每个字节设为
0xFF,int值为0xFFFFFFFF,即 -1(补码表示)。
c
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[5];
int arr2[5];
memset(arr1, 0, sizeof(arr1)); // 每个字节 0x00 → int 值为 0
memset(arr2, 0xFF, sizeof(arr2)); // 每个字节 0xFF → int 值为 -1
for (int i = 0; i < 5; i++)
{
printf("arr1[%d] = %d, arr2[%d] = %d\n", i, arr1[i], i, arr2[i]);
}
return 0;
}
输出结果:
arr1[0] = 0, arr2[0] = -1
arr1[1] = 0, arr2[1] = -1
arr1[2] = 0, arr2[2] = -1
arr1[3] = 0, arr2[3] = -1
arr1[4] = 0, arr2[4] = -1
正确用法总结表
| 目标值 | 推荐方式 | 说明 |
|---|---|---|
| 0 | memset(arr, 0, sizeof(arr)) |
每个字节为 0x00,结果为 0 |
| -1 | memset(arr, 0xFF, sizeof(arr)) |
每个字节为 0xFF,结果为 -1 |
| 其他值(如 1、100) | 循环赋值 | for (int i = 0; i < n; i++) arr[i] = value; |
3.2 模拟实现
c
#include <assert.h>
void* my_memset(void* ptr, int value, size_t num)
{
void* ret = ptr; // 保存起始地址,用于返回
assert(ptr != NULL); // 断言:ptr 不能为空指针
unsigned char val = (unsigned char)value; // 将 value 转换为 unsigned char
unsigned char* p = (unsigned char*)ptr; // 将 void* 转换为 unsigned char*
for (size_t i = 0; i < num; i++)
{
p[i] = val; // 逐字节设置
}
return ret; // 返回起始地址
}
代码逐行解释:
void* my_memset(void* ptr, int value, size_t num):使用void*接收任意类型的指针,int value与标准memset一致,size_t num表示要设置的字节数。void* ret = ptr;:保存ptr的原始地址,用于最后返回。assert(ptr != NULL);:断言检查,确保指针不为NULL,若为NULL则程序终止。unsigned char val = (unsigned char)value;:将value强制转换为unsigned char。这是关键步骤,因为memset实际上是以unsigned char为单位来设置内存的,value的高位字节会被截断,只保留低 8 位。unsigned char* p = (unsigned char*)ptr;:将void*转换为unsigned char*,因为unsigned char占 1 字节,可以逐字节操作内存。for (size_t i = 0; i < num; i++):循环num次,逐字节设置。p[i] = val;:将当前字节设置为val。
return ret;:返回最初保存的起始地址,支持链式调用。
测试代码:
c
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
// 测试1:将数组清零
my_memset(arr, 0, sizeof(arr));
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]); // 输出:0 0 0 0 0
}
printf("\n");
// 测试2:将数组每个字节设为 0xFF(即 -1)
my_memset(arr, 0xFF, sizeof(arr));
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]); // 输出:-1 -1 -1 -1 -1
}
printf("\n");
// 测试3:将字符数组设置为 'A'
char str[10];
my_memset(str, 'A', 9);
str[9] = '\0';
printf("%s\n", str); // 输出:AAAAAAAAA
return 0;
}
输出结果:
0 0 0 0 0
-1 -1 -1 -1 -1
AAAAAAAAA
关键点总结:
my_memset按字节逐位设置,不关心数据类型。- 使用
unsigned char*进行逐字节操作,确保行为与标准memset一致。 value会被截断为低 8 位(unsigned char范围),因此设置非 0 值时要理解其字节级效果。- 返回原始指针地址,支持链式调用(如
memset(memset(arr, 0, 4), 1, 4))。
4. memcmp 函数:内存比较
c
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
功能
比较 ptr1 和 ptr2 指向的内存块的前 num 个字节。
- 头文件 :
<string.h> - 参数 :
ptr1:指向第一块待比较的内存ptr2:指向第二块待比较的内存num:要比较的字节数
- 返回值 :
< 0:ptr1小于ptr2= 0:相等> 0:ptr1大于ptr2
使用示例
c
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3, 4, 6};
int result = memcmp(arr1, arr2, sizeof(arr1));
if (result == 0)
printf("两个数组相等\n");
else if (result < 0)
printf("arr1 小于 arr2\n");
else
printf("arr1 大于 arr2\n");
return 0;
}
输出 :arr1 小于 arr2(因为最后一个元素 5 < 6)。
4.1 模拟实现
c
#include <assert.h>
int my_memcmp(const void* ptr1, const void* ptr2, size_t num)
{
assert(ptr1 && ptr2); // 断言:两个指针都不能为空
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 p1[i] - p2[i]; // 返回差值(正数或负数)
}
}
return 0; // 所有字节都相等
}
代码逐行解释:
int my_memcmp(const void* ptr1, const void* ptr2, size_t num):使用const void*接收任意类型的指针,const保证不修改指针指向的数据。返回值类型为int,与标准memcmp一致。assert(ptr1 && ptr2);:断言检查,确保两个指针都不为NULL,若为NULL则程序终止。const unsigned char* p1 = (const unsigned char*)ptr1;和const unsigned char* p2 = (const unsigned char*)ptr2;:将void*强制转换为unsigned char*,因为unsigned char占 1 字节,且是无符号类型,可以正确比较每个字节的数值(0~255)。使用const保持只读语义。for (size_t i = 0; i < num; i++):循环num次,逐字节比较。if (p1[i] != p2[i]):如果当前字节不相等,说明找到了第一个差异位置。return p1[i] - p2[i];:返回两个字节的差值。如果p1[i] > p2[i],返回正数;如果p1[i] < p2[i],返回负数。这个差值的符号与标准memcmp一致。
return 0;:如果循环结束都没有找到不相等的字节,说明两个内存块的前num个字节完全相同,返回 0。
测试代码:
c
#include <stdio.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3, 4, 6};
int arr3[] = {1, 2, 3, 4, 5};
int result1 = my_memcmp(arr1, arr2, sizeof(arr1));
int result2 = my_memcmp(arr1, arr3, sizeof(arr1));
printf("arr1 vs arr2: %d\n", result1); // 输出负数(arr1 < arr2)
printf("arr1 vs arr3: %d\n", result2); // 输出 0(相等)
return 0;
}
输出结果:
arr1 vs arr2: -1
arr1 vs arr3: 0
关键点总结:
my_memcmp按字节逐位比较,不关心数据类型。- 使用
unsigned char*而非char*,因为char在某些平台上可能是有符号的,会导致负数比较结果异常。 - 找到第一个不相等的字节后立即返回差值,不需要继续比较后续字节,效率较高。
- 所有字节都相等时返回 0。
总结对比表
| 函数 | 功能 | 支持重叠 | 头文件 |
|---|---|---|---|
memcpy |
内存块拷贝 | ❌ 不支持 | <string.h> |
memmove |
内存块拷贝 | ✅ 支持 | <string.h> |
memset |
内存块设置 | --- | <string.h> |
memcmp |
内存块比较 | --- | <string.h> |