当前位置: 首页 > news >正文

【C语言内存操作函数与数据存储详解】

Hello,大家好! 这里是小J,本篇文章是我在学习C语言内存操作函数和数据存储时的笔记整理,包含 memcpy / memmove / memset / memcmp 的模拟实现、大端小端字节序、整型提升、浮点型存储以及多道经典例题,希望能帮助大家彻底搞懂内存中的那些事。

文章目录

  • 一、内存操作函数
    • 1. memcpy – 内存拷贝
    • 2. memmove – 内存移动(支持重叠)
    • 3. memset – 内存填充
    • 4. memcmp – 内存比较
  • 二、数据在内存中的存储
    • 1. 字节序(大端/小端)
    • 2. 整型在内存中的存储(补码)
    • 3. char 类型的深入探讨
      • 3.1 经典例题1:char a = -1 打印
      • 3.2 经典例题2:char a = -128 打印 %u
      • 3.3 经典例题3:char a = 128 打印 %u
      • 3.4 经典例题4:char 数组循环
    • 4. 浮点型在内存中的存储(IEEE 754)
  • 三、综合例题解析(指针与整数转换)
  • 四、总结与记忆技巧

一、内存操作函数

<string.h> 头文件中提供了一组直接操作内存字节的函数,它们不关心数据类型,只以字节为单位进行读写。

1. memcpy – 内存拷贝

void*memcpy(void*dest,constvoid*src,size_tnum);

·功能:将 src 指向的内存块的前 num 个字节拷贝到 dest 指向的内存块。
·返回值:返回 dest。
·注意事项
· dest 和 src 不能有重叠(标准规定重叠时行为未定义)。
· 不关心数据类型,逐字节拷贝。
· 常用于拷贝数组、结构体等任意类型的数据。

模拟实现

#include<assert.h>void*my_memcpy(void*dest,constvoid*src,size_tnum){assert(dest&&src);void*ret=dest;while(num--){*(char*)dest=*(char*)src;dest=(char*)dest+1;src=(char*)src+1;}returnret;}

为什么用 char* ?
char 类型占1个字节,强制转换为 char* 后可以逐字节操作,实现“泛型”拷贝。

2. memmove – 内存移动(支持重叠)

void*memmove(void*dest,constvoid*src,size_tnum);

·功能:与 memcpy 类似,但允许源和目标内存区域重叠,能正确处理重叠情况。
·返回值:返回 dest。

模拟实现

核心思路:根据 dest 和 src 的相对位置,决定从前向后还是从后向前拷贝,避免覆盖。

void*my_memmove(void*dest,constvoid*src,size_tnum){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);}}returnret;}

图解重叠拷贝:

重叠拷贝的两种情况与拷贝方向

memmove的核心在于处理srcdest内存区域重叠时,如何避免数据在拷贝完成前被覆盖。关键在于根据destsrc的起始地址关系,决定拷贝方向。

情况一:dest 在 src 之前 (dest < src) -> 从前向后拷贝

  • 场景:目标区域在源区域的前面(低地址侧)。
  • 安全方向从前向后(front to back)。
  • 原因:从低地址开始拷贝,即使有重叠,也是先拷贝源区域的前部(非重叠部分)到目标区域。当拷贝到重叠部分时,源数据已经被复制到了更前面的目标位置,所以不会被后续操作破坏。

示例:将数组arr[3..6]拷贝到arr[0..3]

#include<stdio.h>#include<string.h>intmain(){intarr[]={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(inti=0;i<8;i++){printf("%d ",arr[i]);}// 输出:40 50 60 70 50 60 70 80return0;}

拷贝过程图解(从前向后)

初始数组索引: [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]

可以看到,即使srcdest有重叠(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>intmain(){intarr[]={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(inti=0;i<8;i++){printf("%d ",arr[i]);}// 输出:10 20 30 10 20 30 40 80return0;}

拷贝过程图解(从后向前)

初始数组索引: [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时,必须从后向前拷贝。


情况三:destsrc地址相同或区域不重叠

dest == src:源和目标为同一块内存,无需进行任何拷贝操作。从前或从后拷贝结果都一样。-*区域不重叠:无论destsrc之前还是之后,只要两个内存块没有交集,从前或从后拷贝都是安全的。但为了代码统一,通常按dest < srcdest >= src来分支。-

总结与模拟实现逻辑

void*my_memmove(void*dest,constvoid*src,size_tnum){// ... 参数检查if(dest<src){// 情况一:dest 在 src 之前 -> 从前向后拷贝// 逐字节从低地址向高地址拷贝}else{// 情况二:dest 在 src 之后或相等 -> 从后向前拷贝// 逐字节从高地址向低地址拷贝}// ... 返回 dest}

记忆口诀“前向后,后向前”。即目标(dest)在源(src)前面向前拷,目标在源后面向后拷。
`dest```

3. memset – 内存填充

void*memset(void*ptr,intvalue,size_tnum);

·功能:将 ptr 指向的内存块的前 num 个字节全部设置为 value(value 转换为 unsigned char)。
·返回值:返回 ptr。
·注意:以字节为单位设置,不能用来初始化 int 数组为 1(因为1会变成 0x01010101)。

示例

intarr[10];memset(arr,0,sizeof(arr));// 正确,全部置0memset(arr,1,sizeof(arr));// 错误,每个字节变成1,int值变成0x01010101

4. memcmp – 内存比较

intmemcmp(constvoid*ptr1,constvoid*ptr2,size_tnum);

·功能:比较两个内存块的前 num 个字节。
·返回值
· 0:相等
· >0:ptr1 大于 ptr2
· <0:ptr1 小于 ptr2

与 strcmp 的区别:memcmp 不关心 \0,直接比较字节;strcmp 遇到 \0 就停止。


二、数据在内存中的存储

1. 字节序(大端/小端)

大端:数据的高位字节存储在低地址。
小端:数据的低位字节存储在低地址。

例如整数 0x12345678 在内存中的存储:

· 小端:78 56 34 12
· 大端:12 34 56 78

判断当前机器的字节序

intcheck_sys(){intn=1;return*(char*)&n;// 取第一个字节,若为1则是小端,0则是大端}intmain(){if(check_sys()==1)printf("小端\n");elseprintf("大端\n");return0;}

2. 整型在内存中的存储(补码)

·正数:原码、反码、补码相同。
·负数:补码 = 原码取反 + 1。

计算机中统一使用补码存储。

3. char 类型的深入探讨

char 到底是有符号还是无符号取决于编译器。
在 VS 上,char 等价于 signed char,取值范围 -128 ~ 127。
unsigned char 取值范围 0 ~ 255。

3.1 经典例题1:char a = -1 打印

intmain(){chara=-1;signedcharb=-1;unsignedcharc=-1;printf("a=%d, b=%d, c=%d\n",a,b,c);// 输出:a=-1, b=-1, c=255return0;}

解析

· -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

intmain(){chara=-128;// -128 原码: 10000000 00000000 00000000 10000000// 补码: 11111111 11111111 11111111 10000000// 截断到 char: 10000000printf("%u\n",a);// 整型提升: 有符号 char 10000000 -> 11111111 11111111 11111111 10000000// 作为无符号整数输出: 4294967168return0;}

输出:4294967168(即 0xFFFFFF80)

3.3 经典例题3:char a = 128 打印 %u

intmain(){chara=128;// 128 原码: 00000000 00000000 00000000 10000000// 截断: 10000000(与 -128 的补码相同)printf("%u\n",a);// 结果同 -128,输出 4294967168return0;}

注意:128 超出了 signed char 的范围,会发生截断,实际存储为 10000000,即 -128 的补码。

3.4 经典例题4:char 数组循环

intmain(){chara[1000];inti;for(i=0;i<1000;i++){a[i]=-1-i;}printf("%zd\n",strlen(a));// 求字符串长度,遇到 \0 停止return0;}

解析

· -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>intmain(){inta[4]={1,2,3,4};int*ptr1=(int*)(&a+1);int*ptr2=(int*)((int)a+1);printf("%x,%x\n",ptr1[-1],*ptr2);return0;}

分析(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语言内存操作和数据存储的知识点。如果有任何疑问,欢迎在评论区交流讨论!

http://www.cnnetsun.cn/news/2553835.html

相关文章:

  • 如何快速入门prepare_detection_dataset:5分钟掌握数据集格式转换终极指南
  • 避坑指南:STM32多重ADC采集时,DMA缓冲区定义与数据提取的常见错误
  • 3步解锁加密音频:ncmdump实现NCM转MP3的高效方案
  • Qwen-Agent实战:5步构建本地化智能助手,告别云端API依赖
  • 从RC电路到C代码:一阶低通滤波器的前世今生,及其在STM32电机FOC控制中的落地
  • S32DS调试S32K344报错?手把手教你更新J-Link驱动搞定‘Device not recognised’
  • 海尔智能家居接入HomeAssistant完整指南:3步实现全屋设备统一管理
  • ESP32嵌入式GUI开发终极指南:使用lv_port_esp32构建专业级单色屏应用
  • EasyDoc安全部署指南:API密钥管理与文档隐私保护策略
  • 终极指南:如何在macOS上免费实现专业级PDF虚拟打印
  • 元学习与物理信息神经网络:破解数据稀缺下的宏观交通流估计难题
  • CTF实战:手把手教你用phar伪协议绕过NSS靶场文件上传限制
  • skill-sample-nodejs-fact部署指南:AWS Lambda vs Alexa托管服务终极对比
  • Forge中的多语言支持:实现跨语言LLM工具调用的终极指南 [特殊字符]
  • 输入题目,百考通AI自动生成结构完整、逻辑严谨的任务书
  • 百考通AI:专科毕业论文的智能通关密钥,彻底解决各环节的创作难题
  • 当视频文件戛然而止:用Untrunc解码数字记忆的修复密码
  • 我的Logseq移动办公流水线:安卓手机Termux搭配快捷指令,5分钟完成笔记收集与同步
  • OBS多平台推流终极指南:一键同步直播到多个平台的完整教程
  • 用百考通,写出一份有底气、能落地的任务书 ✍️
  • 别再只搭环境了!用LangChain+ChromaDB在Mac上快速构建你的第一个私有知识库问答机器人
  • 利用AI工具生成画图板工具
  • 3分钟快速掌握:macOS微信防撤回插件WeChatIntercept完全指南
  • 基于MLP误差预测的自适应多尺度模拟耦合技术
  • FeHelper:一站式前端开发工具箱的完整指南
  • 推理服务为什么一上批量采样就开始输出不可复现:从 RNG State 到 Per-Request Stream 的工程实战
  • 源代码论文分享|基于Java的医院急诊系统!
  • MAPED技术:电子衍射材料分析新突破
  • 5分钟学会OpenSpeedy:免费开源游戏加速工具终极指南
  • 多模态融合在死因推断中的应用:特征级与决策级融合策略对比