ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

数据在内存中的存储与内存函数

数据在内存中的存储与内存函数 深入理解 C 语言数据在内存中的存储与内存函数解析及底层模拟实现本篇博客我们将围绕 C 语言中数据在内存中的存储方式整数、浮点数、大小端以及内存操作函数memcpy、memmove、memset、memcmp从函数的使用、底层原理到模拟实现带你彻底吃透内存处理逻辑。所有代码均保证可以直接编译运行。目录一、整数在内存中的存储二、大小端字节序和字节序判断三、浮点数在内存中的存储四、memcpy 使用和模拟实现五、memmove 使用和模拟实现六、memset 函数的使用七、memcmp 函数的使用八、总结一、整数在内存中的存储整数在内存中以补码存储。原码最高位符号位0正1负其余为绝对值。反码正数同原码负数符号位不变数值位取反。补码正数同原码负数 反码 1。示例10 和 -10 的二进制表示32 位10 的原码00000000 00000000 00000000 00001010反码00000000 00000000 00000000 00001010补码00000000 00000000 00000000 00001010-10 的原码10000000 00000000 00000000 00001010反码11111111 11111111 11111111 11110101补码11111111 11111111 11111111 11110110为什么用补码零的表示唯一全0。减法转加法10 (-10)补码相加溢出丢弃结果为0。多表示一个负数如8位范围[-128, 127]。避坑正数三码合一。强制类型转换直接截断补码低位非转换原码。对INT_MIN取负会溢出属未定义行为。我们可以通过代码查看整数在内存中的实际字节排列#includestdio.hintmain(void){inta10;intb-10;unsignedchar*p(unsignedchar*)a;printf(10 的内存字节小端);for(inti0;isizeof(int);i){printf(%02x ,p[i]);}printf(\n);p(unsignedchar*)b;printf(-10 的内存字节小端);for(inti0;isizeof(int);i){printf(%02x ,p[i]);}printf(\n);return0;}在常见的小端机器上输出为10 的内存字节小端0a 00 00 00 -10 的内存字节小端f6 ff ff ff值得注意的地方补码存储的意义使用补码可以将符号位与数值统一处理减法可以转换为加法简化硬件设计。有符号与无符号%d打印有符号整数%u打印无符号整数。同一个补码解释方式不同结果不同。整型提升在表达式计算中小于int的整型会先提升为int需要注意符号扩展。二、大小端字节序和字节序判断大端模式Big-endian低位地址存放高位字节。小端模式Little-endian低位地址存放低位字节。为什么会有大小端根本原因多字节数据如int、float在内存中需要确定字节的排列顺序而不同 CPU 架构的设计选择不同。硬件设计差异小端Little-endian低位字节存低地址。x86 架构采用因为加法运算从低位向高位进位处理效率更高。大端Big-endian高位字节存低地址。PowerPC、SPARC 等采用更符合人类阅读习惯且符号位在低地址便于快速判断正负。历史与生态早期计算机厂商各自定义形成两大阵营没有统一标准导致至今并存。网络标准统一为跨平台通信TCP/IP 协议规定网络字节序为大端主机需用htons、htonl等函数转换。总结大小端是硬件设计的权衡结果无绝对优劣。单机编程通常无感但跨平台数据交换、指针强转、位运算时必须注意。判断当前机器字节序的方法方法 1联合体Union#includestdio.hintcheck_endian(void){union{inti;charc;}u;u.i1;returnu.c;// 返回 1 表示小端0 表示大端}intmain(void){if(check_endian()){printf(当前机器是小端字节序\n);}else{printf(当前机器是大端字节序\n);}return0;}值得注意的核心点联合体共用内存union所有成员起始地址相同char c与int i共享同一块内存因此u.c取到的是u.i的最低地址字节。原理u.i 1若最低地址字节为1说明低位字节存低地址即小端若为0说明低位字节存高地址即大端。返回值含义返回u.c1表示小端0表示大端。char是否有符号不影响因为只比较0和1。可移植性C 语言允许通过联合体读取非活跃成员类型双关但 C 中属于未定义行为。跨语言时建议改用unsigned char*指针法。更严谨的指针法inti1;unsignedchar*p(unsignedchar*)i;return*p;// 1 小端0 大端不依赖联合体可移植性更好。依赖int至少 2 字节标准未规定int字节数但所有常见平台均满足判断的是多字节整数的字节序与具体字节数无关。不要与字符1比较u.c是整数值1或0不是字符1比较时直接用 1或if (check_endian())。方法 2指针#includestdio.hintcheck_endian2(void){inti1;char*p(char*)i;return*p;// 取第一个字节若为 1 则是小端}intmain(void){if(check_endian2()){printf(当前机器是小端字节序\n);}else{printf(当前机器是大端字节序\n);}return0;}值得注意的地方指针法判断大小端 · 绝对要注意的核心点取最低地址字节(char*)i使p指向i的第一个字节最低地址。小端该字节为1大端为0。返回值含义返回*p1表示小端0表示大端。判断时直接用if (check_endian2())或 1。合法且可移植C 标准允许用char*访问任意对象的字节表示比联合体类型双关更通用。char符号性无关*p仅为0或1无论char是否有符号都不影响判断不要与字符1混淆。依赖int至少 2 字节标准未规定int字节数但常见平台均满足判断的是多字节整数的字节序与具体字节数无关。三、浮点数在内存中的存储根据 IEEE 754 标准浮点数在内存中的存储分为三个部分符号位 S0 表示正数1 表示负数。指数位 E实际指数加上偏移量。float偏移量为 127double偏移量为 1023。尾数位 M省略整数部分的 1只存储小数部分。float32 位1 位符号 8 位指数 23 位尾数double64 位1 位符号 11 位指数 52 位尾数示例浮点数 5.5 的存储5.5 的二进制为101.1即1.011 * 2^2。符号位 S 0指数 E 2 127 129即10000001尾数 M 01100000000000000000000。组合起来0 10000001 01100000000000000000000即0x40B00000小端存储为00 00 B0 40。#includestdio.hintmain(void){floatf5.5f;unsignedchar*p(unsignedchar*)f;printf(5.5f 的内存字节);for(inti0;isizeof(float);i){printf(%02x ,p[i]);}printf(\n);doubled5.5;p(unsignedchar*)d;printf(5.5 的内存字节);for(inti0;isizeof(double);i){printf(%02x ,p[i]);}printf(\n);return0;}值得注意的地方精度问题很多十进制小数无法用二进制精确表示例如0.1。因此不要直接用比较两个浮点数应使用误差范围。特殊值指数位全 0 或全 1 表示特殊值0、无穷大、NaN。类型转换float与double之间转换可能丢失精度。内存对齐结构体中浮点数成员也遵循对齐规则。四、memcpy 使用和模拟实现函数原型void*memcpy(void*dest,constvoid*src,size_tnum);功能从src指向的内存起始位置拷贝num个字节到dest指向的内存起始位置。不处理内存重叠。使用示例#includestdio.h#includestring.hintmain(void){intarr1[]{1,2,3,4,5};intarr2[5]{0};memcpy(arr2,arr1,sizeof(arr1));for(inti0;i5;i){printf(%d ,arr2[i]);}printf(\n);charstr1[]hello;charstr2[20]{0};memcpy(str2,str1,strlen(str1)1);// 包含 \0printf(%s\n,str2);return0;}模拟实现#includestdio.h#includeassert.hvoid*my_memcpy(void*dest,constvoid*src,size_tnum){void*retdest;assert(dest!NULL);assert(src!NULL);while(num--){*(char*)dest*(char*)src;dest(char*)dest1;src(constchar*)src1;}returnret;}intmain(void){intarr1[]{1,2,3,4,5};intarr2[5]{0};my_memcpy(arr2,arr1,sizeof(arr1));for(inti0;i5;i){printf(%d ,arr2[i]);}printf(\n);return0;}值得注意的地方按字节拷贝memcpy不关心数据类型按字节复制。因此对于结构体、数组等非常方便。不处理重叠如果dest和src有重叠区域结果未定义应使用memmove。返回值返回dest的起始地址支持链式调用。空指针断言模拟实现中应使用assert防止空指针。五、memmove 使用和模拟实现函数原型void*memmove(void*dest,constvoid*src,size_tnum);功能与memcpy类似但处理内存重叠的情况。使用示例#includestdio.h#includestring.hintmain(void){intarr[]{1,2,3,4,5,6,7,8,9,10};// 将前 5 个元素移动到从第 3 个元素开始的位置重叠memmove(arr2,arr,5*sizeof(int));for(inti0;i10;i){printf(%d ,arr[i]);}printf(\n);return0;}模拟实现#includestdio.h#includeassert.hvoid*my_memmove(void*dest,constvoid*src,size_tnum){void*retdest;assert(dest!NULL);assert(src!NULL);if(destsrc){// 从前向后拷贝while(num--){*(char*)dest*(char*)src;dest(char*)dest1;src(constchar*)src1;}}else{// 从后向前拷贝while(num--){*((char*)destnum)*((char*)srcnum);}}returnret;}intmain(void){intarr[]{1,2,3,4,5,6,7,8,9,10};my_memmove(arr2,arr,5*sizeof(int));for(inti0;i10;i){printf(%d ,arr[i]);}printf(\n);return0;}值得注意的地方重叠方向决定生死dest src必须从前向后否则必须从后向前。写反直接导致数据被覆盖逻辑全错。从后向前偏移陷阱*((char*)dest num)中因循环条件num--先判断后递减首次实际拷贝偏移是num-1最后一次是0。必须强转char*且传字节数不能对void*直接加减必须强转逐字节操作num一定是字节数如5 * sizeof(int)绝不能是元素个数。size_t无符号下溢while (num--)在num 0时下溢为SIZE_MAX循环体虽不执行但后续绝对禁止再使用num。断言不是万能盾assert仅调试期有效发布版定义NDEBUG会被移除生产代码必须自行判空不能只依赖断言。返回原始dest必须用ret保存起始地址并返回支持链式调用不能用移动后的指针返回。六、memset 函数的使用函数原型void*memset(void*ptr,intvalue,size_tnum);功能将ptr指向的内存的前num个字节设置为value。使用示例#includestdio.h#includestring.hintmain(void){charstr[20];// 将前 10 个字节设置为 Amemset(str,A,10);str[10]\0;printf(%s\n,str);// 输出 AAAAAAAAAA// 将整型数组清零intarr[5];memset(arr,0,sizeof(arr));for(inti0;i5;i){printf(%d ,arr[i]);}printf(\n);return0;}值得注意的地方按字节设置不是按元素memset以字节为单位value只取低 8 位。对int数组设非零值如memset(arr, 1, sizeof(arr))会得到0x01010101不是 1。第三参数必须是字节数必须用sizeof(arr)或n * sizeof(元素类型)绝不能传元素个数。字符数组必须手动补\0memset不会自动添加结束符如代码中str[10] \0必不可少否则打印越界。清零最安全memset(arr, 0, sizeof(arr))是初始化整型/结构体数组的常用正确写法。非零初始化用循环需要给多字节类型赋非零值时必须用循环赋值不能依赖memset。返回值返回dest起始地址支持链式调用。七、memcmp 函数的使用函数原型intmemcmp(constvoid*ptr1,constvoid*ptr2,size_tnum);功能比较ptr1和ptr2指向的内存区域的前num个字节。返回值0表示ptr1小于ptr20表示相等0表示ptr1大于ptr2。使用示例#includestdio.h#includestring.hintmain(void){intarr1[]{1,2,3};intarr2[]{1,2,4};intretmemcmp(arr1,arr2,sizeof(arr1));if(ret0){printf(arr1 arr2\n);}elseif(ret0){printf(arr1 arr2\n);}else{printf(arr1 arr2\n);}charstr1[]hello;charstr2[]hello world;retmemcmp(str1,str2,5);printf(前5个字符比较结果%d\n,ret);// 0return0;}值得注意的地方按字节比较视为unsigned char不关心数据类型也不关心\0只逐字节比较。字符串比较时若长度小于实际长度只比较前缀。返回值只保证正负零标准仅保证0、0、0绝不能写 1或 -1不同平台返回值可能不同。第三参数是字节数不是元素个数必须用sizeof(arr)或n * sizeof(类型)长度绝不能超过实际可读内存否则越界未定义行为。多字节类型受字节序影响比较int、float等时按内存字节顺序比较。小端下低字节先比较结果可能与数值大小直觉相反如0x0100与0x0001。不检查\0不自动停止与strcmp不同memcmp不遇\0停止完全按指定字节数比较。n0时直接返回 0。空指针不检查传入NULL会直接崩溃调用者必须自行保证指针有效。与strcmp的本质区别strcmp比较字符串直到\0memcmp比较指定字节数可用于任意内存块。八、总结结合之前的指针进阶知识我们可以提炼出以下数据存储与内存处理的通关心法整数补码存储正数原码、反码、补码相同负数补码 反码 1。补码零表示唯一化减法为加法强制类型转换是截断补码低字节非转换原码注意INT_MIN取负会溢出属未定义行为。大小端字节序小端低字节存低地址x86 主流大端高字节存低地址网络字节序。判断方法联合体利用成员共用内存指针取首字节判断。跨平台数据交换需用htons、htonl等转换。浮点数 IEEE 754float为 1 符号 8 指数 23 尾数double为 1 11 52指数偏移分别为 127 和 1023尾数省略整数 1。存在精度限制比较需引入误差范围如fabs(a-b) 1e-9切忌直接用。memcpy 与 memmove均按字节拷贝参数num必须是字节数。memcpy不处理重叠性能优先memmove处理重叠dest src从前向后否则从后向前。二者均返回dest起始地址用ret保存支持链式调用assert仅调试有效发布版需自行判空。memset按字节设置value只取低 8 位第三参数为字节数。适合清零或字符填充对整型赋非零值会得到0x01010101等错误结果。字符数组填充后需手动补\0否则打印越界。memcmp按字节比较视为unsigned char返回正负零遇\0不停止严格比较指定字节数。多字节类型受字节序影响n为 0 时直接返回 0不检查NULL调用者需保证指针有效。核心心法指针类型决定步长字节操作揭示底层掌握内存的字节级视角字符串与内存函数便再无秘密可言。
返回列表