【C语言内存操作函数与数据存储详解】
Hello,大家好! 这里是小J,本篇文章是我在学习C语言内存操作函数和数据存储时的笔记整理,包含 memcpy / memmove / memset / memcmp 的模拟实现、大端小端字节序、整型提升、浮点型存储以及多道经典例题,希望能帮助大家彻底搞懂内存中的那些事。
文章目录
一、内存操作函数
<string.h> 头文件中提供了一组直接操作内存字节的函数,它们不关心数据类型,只以字节为单位进行读写。
1. memcpy – 内存拷贝
void *memcpy(void *dest, const void *src, size_t num);
· 功能:将 src 指向的内存块的前 num 个字节拷贝到 dest 指向的内存块。
· 返回值:返回 dest。
· 注意事项:
· dest 和 src 不能有重叠(标准规定重叠时行为未定义)。
· 不关心数据类型,逐字节拷贝。
· 常用于拷贝数组、结构体等任意类型的数据。
模拟实现
#include <assert.h>
void* my_memcpy(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
为什么用 char* ?
char 类型占1个字节,强制转换为 char* 后可以逐字节操作,实现“泛型”拷贝。
2. memmove – 内存移动(支持重叠)
void *memmove(void *dest, const void *src, size_t num);
· 功能:与 memcpy 类似,但允许源和目标内存区域重叠,能正确处理重叠情况。
· 返回值:返回 dest。
模拟实现
核心思路:根据 dest 和 src 的相对位置,决定从前向后还是从后向前拷贝,避免覆盖。
void* my_memmove(void* dest, const void* src, size_t num)
{
assert(dest && src);
void* ret = dest;
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;
}
图解重叠拷贝:
重叠拷贝的两种情况与拷贝方向
memmove 的核心在于处理 src 和 dest 内存区域重叠时,如何避免数据在拷贝完成前被覆盖。关键在于根据 dest 和 src 的起始地址关系,决定拷贝方向。
情况一:dest 在 src 之前 (dest < src) -> 从前向后拷贝
- 场景:目标区域在源区域的前面(低地址侧)。
- 安全方向:从前向后 (
front to back)。 - 原因:从低地址开始拷贝,即使有重叠,也是先拷贝源区域的前部(非重叠部分)到目标区域。当拷贝到重叠部分时,源数据已经被复制到了更前面的目标位置,所以不会被后续操作破坏。
示例:将数组 arr[3..6] 拷贝到 arr[0..3]
#include <stdio.h>
#include <string.h>
int main() {
int arr[] = {10, 20, 30, 40, 50, 60, 70, 80};
// 目标:将 arr[3], arr[4], arr[5], arr[6] (40,50,60,70) 移动到 arr[0], arr[1], arr[2], arr[3]
// src = &arr[3], dest = &arr[0], num = 4 * sizeof(int)
memmove(arr, arr + 3, 4 * sizeof(int));
// 打印结果
for (int i = 0; i < 8; i++) {
printf("%d ", arr[i]);
}
// 输出:40 50 60 70 50 60 70 80
return 0;
}
拷贝过程图解(从前向后):
初始数组索引: [0] [1] [2] [3] [4] [5] [6] [7]
初始值: 10 20 30 40 50 60 70 80
dest src
| |
V V
步骤1: 拷贝 arr[3] (40) -> arr[0], 数组变为: [40, 20, 30, 40, 50, 60, 70, 80]
步骤2: 拷贝 arr[4] (50) -> arr[1], 数组变为: [40, 50, 30, 40, 50, 60, 70, 80]
步骤3: 拷贝 arr[5] (60) -> arr[2], 数组变为: [40, 50, 60, 40, 50, 60, 70, 80]
步骤4: 拷贝 arr[6] (70) -> arr[3], 数组变为: [40, 50, 60, 70, 50, 60, 70, 80]
可以看到,即使 src 和 dest 有重叠(arr[3] 既是源也是目标),因为是从前向后拷贝,源数据 40, 50, 60, 70 在被覆盖前就已经被正确读取并复制到了前面。
情况二:dest 在 src 之后 (dest > src) -> 从后向前拷贝
- 场景:目标区域在源区域的后面(高地址侧)。
- 安全方向:从后向前 (
back to front)。 - 原因:如果从前向后拷贝,会先覆盖源区域的前部数据,导致后续拷贝读取到错误的值。从后向前拷贝可以确保重叠部分的源数据在被覆盖前已被读取。
示例:将数组 arr[0..3] 拷贝到 arr[3..6]
#include <stdio.h>
#include <string.h>
int main() {
int arr[] = {10, 20, 30, 40, 50, 60, 70, 80};
// 目标:将 arr[0], arr[1], arr[2], arr[3] (10,20,30,40) 移动到 arr[3], arr[4], arr[5], arr[6]
// src = &arr[0], dest = &arr[3], num = 4 * sizeof(int)
memmove(arr + 3, arr, 4 * sizeof(int));
// 打印结果
for (int i = 0; i < 8; i++) {
printf("%d ", arr[i]);
}
// 输出:10 20 30 10 20 30 40 80
return 0;
}
拷贝过程图解(从后向前):
初始数组索引: [0] [1] [2] [3] [4] [5] [6] [7]
初始值: 10 20 30 40 50 60 70 80
src dest
| |
V V
步骤4: 拷贝 arr[3] (40) -> arr[6], 数组变为: [10, 20, 30, 40, 50, 60, 40, 80]
步骤3: 拷贝 arr[2] (30) -> arr[5], 数组变为: [10, 20, 30, 40, 50, 30, 40, 80]
步骤2: 拷贝 arr[1] (20) -> arr[4], 数组变为: [10, 20, 30, 40, 20, 30, 40, 80]
步骤1: 拷贝 arr[0] (10) -> arr[3], 数组变为: [10, 20, 30, 10, 20, 30, 40, 80]
如果错误地使用从前向后拷贝:
步骤1: 拷贝 arr[0] (10) -> arr[3], 数组变为: [10, 20, 30, 10, 50, 60, 70, 80] (arr[3]的40被覆盖)
步骤2: 拷贝 arr[1] (20) -> arr[4], 但此时 arr[1] 的值?(实际上还是20,但假设继续...)
... 最终结果错误。
因此,当 dest > src 时,必须从后向前拷贝。
情况三:dest 与 src 地址相同或区域不重叠
dest == src:源和目标为同一块内存,无需进行任何拷贝操作。从前或从后拷贝结果都一样。-* 区域不重叠:无论 dest 在 src 之前还是之后,只要两个内存块没有交集,从前或从后拷贝都是安全的。但为了代码统一,通常按 dest < src 和 dest >= src 来分支。-
总结与模拟实现逻辑:
void* my_memmove(void* dest, const void* src, size_t num) {
// ... 参数检查
if (dest < src) {
// 情况一:dest 在 src 之前 -> 从前向后拷贝
// 逐字节从低地址向高地址拷贝
} else {
// 情况二:dest 在 src 之后或相等 -> 从后向前拷贝
// 逐字节从高地址向低地址拷贝
}
// ... 返回 dest
}
记忆口诀:“前向后,后向前”。即目标(dest)在源(src) 前面就向前拷,目标在源 后面就向后拷。
`dest```
3. memset – 内存填充
void *memset(void *ptr, int value, size_t num);
· 功能:将 ptr 指向的内存块的前 num 个字节全部设置为 value(value 转换为 unsigned char)。
· 返回值:返回 ptr。
· 注意:以字节为单位设置,不能用来初始化 int 数组为 1(因为1会变成 0x01010101)。
示例:
int arr[10];
memset(arr, 0, sizeof(arr)); // 正确,全部置0
memset(arr, 1, sizeof(arr)); // 错误,每个字节变成1,int值变成0x01010101
4. memcmp – 内存比较
int memcmp(const void *ptr1, const void *ptr2, size_t num);
· 功能:比较两个内存块的前 num 个字节。
· 返回值:
· 0:相等
· >0:ptr1 大于 ptr2
· <0:ptr1 小于 ptr2
与 strcmp 的区别:memcmp 不关心 \0,直接比较字节;strcmp 遇到 \0 就停止。
二、数据在内存中的存储
1. 字节序(大端/小端)
大端:数据的高位字节存储在低地址。
小端:数据的低位字节存储在低地址。
例如整数 0x12345678 在内存中的存储:
· 小端:78 56 34 12
· 大端:12 34 56 78
判断当前机器的字节序
int check_sys()
{
int n = 1;
return *(char*)&n; // 取第一个字节,若为1则是小端,0则是大端
}
int main()
{
if (check_sys() == 1)
printf("小端\n");
else
printf("大端\n");
return 0;
}
2. 整型在内存中的存储(补码)
· 正数:原码、反码、补码相同。
· 负数:补码 = 原码取反 + 1。
计算机中统一使用补码存储。
3. char 类型的深入探讨
char 到底是有符号还是无符号取决于编译器。
在 VS 上,char 等价于 signed char,取值范围 -128 ~ 127。
unsigned char 取值范围 0 ~ 255。
3.1 经典例题1:char a = -1 打印
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a=%d, b=%d, c=%d\n", a, b, c);
// 输出:a=-1, b=-1, c=255
return 0;
}
解析:
· -1 的补码全是 1:11111111(8位)。
· a 和 b 作为 signed char,高位为符号位,%d 打印时会整型提升,高位补符号位1,得到 0xFFFFFFFF,即 -1。
· c 作为 unsigned char,整型提升时高位补0,得到 0x000000FF,即 255。
3.2 经典例题2:char a = -128 打印 %u
int main()
{
char a = -128;
// -128 原码: 10000000 00000000 00000000 10000000
// 补码: 11111111 11111111 11111111 10000000
// 截断到 char: 10000000
printf("%u\n", a);
// 整型提升: 有符号 char 10000000 -> 11111111 11111111 11111111 10000000
// 作为无符号整数输出: 4294967168
return 0;
}
输出:4294967168(即 0xFFFFFF80)
3.3 经典例题3:char a = 128 打印 %u
int main()
{
char a = 128;
// 128 原码: 00000000 00000000 00000000 10000000
// 截断: 10000000(与 -128 的补码相同)
printf("%u\n", a);
// 结果同 -128,输出 4294967168
return 0;
}
注意:128 超出了 signed char 的范围,会发生截断,实际存储为 10000000,即 -128 的补码。
3.4 经典例题4:char 数组循环
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%zd\n", strlen(a)); // 求字符串长度,遇到 \0 停止
return 0;
}
解析:
· -1 - i 的值:-1, -2, -3, …, -128, 127, 126, …, 1, 0, -1, -2 …
· 当值为 0 时,对应 ASCII 码 \0,strlen 停止。
· 从 -1 到 -128 共128个数,再从 127 到 1 共127个数,之后遇到 0。
· 总长度 = 128 + 127 = 255。
4. 浮点型在内存中的存储(IEEE 754)
以 float(32位)为例:
· 最高位1位:符号位 S
· 之后8位:指数位 E(移码表示,偏移127)
· 最后23位:尾数位 M(隐含整数部分1)
公式:(-1)^S × 1.M × 2^(E-127)
示例:float f = 5.5;
· 二进制:101.1 → 1.011 × 2^2
· S=0, E=2+127=129(10000001), M=011
· 存储:0 10000001 01100000000000000000000
· 十六进制:0x40B00000
三、综合例题解析(指针与整数转换)
#include <stdio.h>
int main()
{
int a[4] = {1, 2, 3, 4};
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x\n", ptr1[-1], *ptr2);
return 0;
}
分析(X86 小端环境):
· &a 是整个数组的地址,类型 int()[4]。
· &a + 1 跳过整个数组,指向数组末尾后面一个位置。
· ptr1 = (int)(&a + 1) → ptr1 指向 a[4] 之后的地址。
· ptr1[-1] 等价于 *(ptr1 - 1),指向前一个 int,即 a[3] = 4。
· 输出 4(十六进制 4)。
· (int)a 将数组首地址强制转换为整数,然后 +1 表示地址值加1字节。
· ptr2 指向 a[0] 的第二个字节(小端环境下 a[0] 存储为 01 00 00 00,偏移1字节后指向 00 00 00 02 的开头)。
· *ptr2 读取4个字节:00 00 00 02 → 值为 0x2000000(即 33554432)。
· 输出 2000000(十六进制)。
最终输出:4,2000000
四、总结与记忆技巧
| 函数/概念 | 核心要点 | 记忆口诀 |
|---|---|---|
| memcpy | 不重叠拷贝,逐字节 | “拷贝不重叠,字节逐个搬” |
| memmove | 处理重叠,分向前后 | “重叠不用慌,前后分开搬” |
| memset | 按字节填充 | “字节填充,注意整型陷阱” |
| memcmp | 字节比较,无视 \0 | “比较不看零,按字节判断” |
| 大小端 | 高低地址存高位/低位 | “小端低低,大端高低” |
| 整型提升 | 有符号补符号位,无符号补0 | “提升看符号,无符号补零” |
| 浮点存储 | IEEE 754:S+E+M | “符号指数加尾数,偏移127要记住” |
希望这篇博客能帮你彻底理清C语言内存操作和数据存储的知识点。如果有任何疑问,欢迎在评论区交流讨论!
更多推荐

所有评论(0)