【C语言内存操作函数与数据存储详解】
Hello,大家好! 这里是小J,本篇文章是我在学习C语言内存操作函数和数据存储时的笔记整理包含 memcpy / memmove / memset / memcmp 的模拟实现、大端小端字节序、整型提升、浮点型存储以及多道经典例题希望能帮助大家彻底搞懂内存中的那些事。文章目录一、内存操作函数1. memcpy – 内存拷贝2. memmove – 内存移动支持重叠3. memset – 内存填充4. memcmp – 内存比较二、数据在内存中的存储1. 字节序大端/小端2. 整型在内存中的存储补码3. char 类型的深入探讨3.1 经典例题1char a -1 打印3.2 经典例题2char a -128 打印 %u3.3 经典例题3char a 128 打印 %u3.4 经典例题4char 数组循环4. 浮点型在内存中的存储IEEE 754三、综合例题解析指针与整数转换四、总结与记忆技巧一、内存操作函数string.h 头文件中提供了一组直接操作内存字节的函数它们不关心数据类型只以字节为单位进行读写。1. memcpy – 内存拷贝void*memcpy(void*dest,constvoid*src,size_tnum);·功能将 src 指向的内存块的前 num 个字节拷贝到 dest 指向的内存块。·返回值返回 dest。·注意事项· dest 和 src 不能有重叠标准规定重叠时行为未定义。· 不关心数据类型逐字节拷贝。· 常用于拷贝数组、结构体等任意类型的数据。模拟实现#includeassert.hvoid*my_memcpy(void*dest,constvoid*src,size_tnum){assert(destsrc);void*retdest;while(num--){*(char*)dest*(char*)src;dest(char*)dest1;src(char*)src1;}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(destsrc);void*retdest;if(destsrc){// 从前向后拷贝while(num--){*(char*)dest*(char*)src;dest(char*)dest1;src(char*)src1;}}else{// 从后向前拷贝while(num--){*((char*)destnum)*((char*)srcnum);}}returnret;}图解重叠拷贝重叠拷贝的两种情况与拷贝方向memmove的核心在于处理src和dest内存区域重叠时如何避免数据在拷贝完成前被覆盖。关键在于根据dest和src的起始地址关系决定拷贝方向。情况一dest 在 src 之前 (dest src) - 从前向后拷贝场景目标区域在源区域的前面低地址侧。安全方向从前向后(front to back)。原因从低地址开始拷贝即使有重叠也是先拷贝源区域的前部非重叠部分到目标区域。当拷贝到重叠部分时源数据已经被复制到了更前面的目标位置所以不会被后续操作破坏。示例将数组arr[3..6]拷贝到arr[0..3]#includestdio.h#includestring.hintmain(){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,arr3,4*sizeof(int));// 打印结果for(inti0;i8;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]可以看到即使src和dest有重叠arr[3]既是源也是目标因为是从前向后拷贝源数据40, 50, 60, 70在被覆盖前就已经被正确读取并复制到了前面。情况二dest 在 src 之后 (dest src) - 从后向前拷贝场景目标区域在源区域的后面高地址侧。安全方向从后向前(back to front)。原因如果从前向后拷贝会先覆盖源区域的前部数据导致后续拷贝读取到错误的值。从后向前拷贝可以确保重叠部分的源数据在被覆盖前已被读取。示例将数组arr[0..3]拷贝到arr[3..6]#includestdio.h#includestring.hintmain(){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(arr3,arr,4*sizeof(int));// 打印结果for(inti0;i8;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时必须从后向前拷贝。情况三dest与src地址相同或区域不重叠dest src源和目标为同一块内存无需进行任何拷贝操作。从前或从后拷贝结果都一样。-*区域不重叠无论dest在src之前还是之后只要两个内存块没有交集从前或从后拷贝都是安全的。但为了代码统一通常按dest src和dest src来分支。-总结与模拟实现逻辑void*my_memmove(void*dest,constvoid*src,size_tnum){// ... 参数检查if(destsrc){// 情况一dest 在 src 之前 - 从前向后拷贝// 逐字节从低地址向高地址拷贝}else{// 情况二dest 在 src 之后或相等 - 从后向前拷贝// 逐字节从高地址向低地址拷贝}// ... 返回 dest}记忆口诀“前向后后向前”。即目标(dest)在源(src)前面就向前拷目标在源后面就向后拷。dest3. memset – 内存填充void*memset(void*ptr,intvalue,size_tnum);·功能将 ptr 指向的内存块的前 num 个字节全部设置为 valuevalue 转换为 unsigned char。·返回值返回 ptr。·注意以字节为单位设置不能用来初始化 int 数组为 1因为1会变成 0x01010101。示例intarr[10];memset(arr,0,sizeof(arr));// 正确全部置0memset(arr,1,sizeof(arr));// 错误每个字节变成1int值变成0x010101014. memcmp – 内存比较intmemcmp(constvoid*ptr1,constvoid*ptr2,size_tnum);·功能比较两个内存块的前 num 个字节。·返回值· 0相等· 0ptr1 大于 ptr2· 0ptr1 小于 ptr2与 strcmp 的区别memcmp 不关心 \0直接比较字节strcmp 遇到 \0 就停止。二、数据在内存中的存储1. 字节序大端/小端大端数据的高位字节存储在低地址。小端数据的低位字节存储在低地址。例如整数 0x12345678 在内存中的存储· 小端78 56 34 12· 大端12 34 56 78判断当前机器的字节序intcheck_sys(){intn1;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 经典例题1char a -1 打印intmain(){chara-1;signedcharb-1;unsignedcharc-1;printf(a%d, b%d, c%d\n,a,b,c);// 输出a-1, b-1, c255return0;}解析· -1 的补码全是 1111111118位。· a 和 b 作为 signed char高位为符号位%d 打印时会整型提升高位补符号位1得到 0xFFFFFFFF即 -1。· c 作为 unsigned char整型提升时高位补0得到 0x000000FF即 255。3.2 经典例题2char a -128 打印 %uintmain(){chara-128;// -128 原码: 10000000 00000000 00000000 10000000// 补码: 11111111 11111111 11111111 10000000// 截断到 char: 10000000printf(%u\n,a);// 整型提升: 有符号 char 10000000 - 11111111 11111111 11111111 10000000// 作为无符号整数输出: 4294967168return0;}输出4294967168即 0xFFFFFF803.3 经典例题3char a 128 打印 %uintmain(){chara128;// 128 原码: 00000000 00000000 00000000 10000000// 截断: 10000000与 -128 的补码相同printf(%u\n,a);// 结果同 -128输出 4294967168return0;}注意128 超出了 signed char 的范围会发生截断实际存储为 10000000即 -128 的补码。3.4 经典例题4char 数组循环intmain(){chara[1000];inti;for(i0;i1000;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 码 \0strlen 停止。· 从 -1 到 -128 共128个数再从 127 到 1 共127个数之后遇到 0。· 总长度 128 127 255。4. 浮点型在内存中的存储IEEE 754以 float32位为例· 最高位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· S0, E212712910000001, M011·存储0 10000001 01100000000000000000000·十六进制0x40B00000三、综合例题解析指针与整数转换#includestdio.hintmain(){inta[4]{1,2,3,4};int*ptr1(int*)(a1);int*ptr2(int*)((int)a1);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 754SEM“符号指数加尾数偏移127要记住”希望这篇博客能帮你彻底理清C语言内存操作和数据存储的知识点。如果有任何疑问欢迎在评论区交流讨论