C语言第十一章内存在数据中的存储

一.整数在内存中的存储

在计算机内存中,所有的数字都是以二进制来存储的。整数也不例外,在计算机内存中,整数往往以补码的形式来存储数据。这是为什么呢?

在早期计算机表示整数时,最高位为符号位。但是0却有两种表示形式:00000000和10000000分别表示正零和负零。这样两种零的表示形式,无疑是极其不方便的。但是出现了整数补码的表示形式。这种形式完美解决了此类问题,因为在计算机内存中,整数以补码的形式存储,所以正负零的补码均为00000000。

在计算机内存中表示整数,无非就是为了下来的加、减计算。但是计算机硬件的核心运算单位是加法器,直接实现减法运算会增加电路的复杂度。这时候出现的补码表示形式,让让两数减法变成了正数和负数的加法,此时的负数用补码的形式表示,进行两数相加就可以将二进制的减法运算转换成加法运算,降低了电路的复杂度。(负数补码形式加上正数的值正好等于两正数相减,因为补码就是这样巧妙设计的。)

补码的运算规则简单,就是原码进行取反后+1,容易实现。并且补码的符号位无需单独处理,两数可以用二进制的补码形式直接相加,得到的结果符号位自然正确。

补码和原码表示的范围相同,因为二进制的位数相同,并且最高位均为符号位,所以范围相同,这样就可以更好的参与运算和管理。

整数的二进制表示形式有3种,分别是:原码、反码、补码。有符号的整数,三种表示方法均有符号位和数值位两部分,符号位都是用0表示"正",用1表示"负",最高位的一位是被当做符号位,剩余的都是数值位;无符号的整数,最高位不是符号位,而是表示数值。正整数的原码、反码、补码均相同。负整数的反码=原码符号位不变,其余数值位取反;补码=反码+1。

原码:直接将数值按照正负数的形式翻译成二进制得到的就是原码。 反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。 补码:反码+1就得到补码。

二.大小端字节序和字节序判断

1.大小端内容的引入

上面我们了解了整数在内存中以补码的形式存储,下面我们调试内存块查看一下细节:

cpp 复制代码
#include <stdio.h>
int main()
{
 int a = 0x11223344;    //创建整型变量,用于调试观察内存
 return 0;
}

上述代码,我们创建了一个整型变量a,用于存放11223344这个16进制的数字。因为该数字为正数,所以该数的原码、反码、补码相同。计算机数据的存储是以二进制的形式存储的,但是为了方便查看调试数据,编译器会以16进制形式显示。

根据上面调试的图片,可以得出:创建的变量a存储数据时,竟然是倒着存储的。这是为什么呢?

2.大小端是什么呢?

首先在上述的例子中,要把数据存在内存中无非就是下面几种方法:

上述图片中,第一个和第二个存储顺序分别是正序、逆序。这样存储数据方便数据接下来的访问,而剩余的存储数据都是乱序,不方便后续数据的读取。这就是大小端字节序的由来。

超过一个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,下面是具体的概念:大端存储模式: 是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。(图片第一种存储顺序)小端存储模式: 是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处(图片第二种存储顺序)。

3.为什么有大小端?

这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit 位,但是在C语言中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型,另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。

因此就导致了大端存储模式和小端存储模式。 例如:一个 16bit 的short型x ,在内存中的地址为0x0010 , x的值为0x1122 ,那么0x11为高字节, 0x22为低字节。对于大端模式,就将0x11 放在低地址中,即0x0010中, 0x22放在高地址中,即0x0011中。小端模式刚好相反。我们常用的X86 结构是小端模式,而KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

4.练习

(1)练习一

题目表述:设计一个程序来判断当前机器的字节序。

cpp 复制代码
//代码1 
#include <stdio.h>
int check_sys()
{
 int i = 1;    //创建变量,用于得到首字节地址的内容
 return (*(char *)&i);        //返回首字节地址的内容
}

int main()
{
 int ret = check_sys();    
 if(ret == 1)        //当返回值为1,说明为小端字节存储
     printf("⼩端\n");
 else                //当返回值为0,说明为大端字节存储
     printf("⼤端\n");
 return 0;
}

上述代码的具体解释:首先创建一个整型变量,通过在内存中第一个字节内容的判断,从而得出该机器的字节序。函数直接返回强制类型转化为1字节的变量地址的解引用值。利用该变量首字节的内容灵活写出的长须函数返回值。

cs 复制代码
//代码2 
int check_sys()
{
 union        //联合体类型的关键字
 {
     int i;
     char c;
 }   un;        //创建联合体变量un
 un.i = 1;        
 return un.c;
}

上述代码的具体解释:创建了自定义类型的联合体,联合体的特点是:所有的成员共享同一块内存空间,大小为最大成员的大小。这里创建的联合体成员变量i=1。并且返回该内存的首字节的地址。所以该代码2和上述代码1的效果相同。

(2)练习二

cpp 复制代码
#include <stdio.h>
int main()
{
 char a= -1;
 signed char b=-1;
 unsigned char c=-1;
 printf("a=%d,b=%d,c=%d",a,b,c);
 return 0;
}

在计算机中,整数在C语言中默认为32位,所以-1的原码、反码、补码均为32位的二进制的数字;赋值操作将32位的整数赋值给char类型的数字,这时的操作将存在隐式转换,将获取32位的后8位进行变量的赋值;在打印时,因为printf函数的占位符为%d,所以打印的是有符号的整数,但是各个变量为不同的char类型,所以在打印之前存在整型提升。当变量为有符号类型时,整型提升的前24位补的是符号位上的数字。如果是无符号的类型,整型提升时前24位补的是0。整型提升后就是需要打印的数据,将其转换为原码,最后变为10进制数字,该十进制数字就是最后打印的结果。

(3)练习三

cpp 复制代码
#include <stdio.h>
int main()
{
 char a = -128;
 printf("%u\n",a);
 return 0;
}

因为整数在C语言中默认为32位,所以赋值之前就可以得出-128的原码、反码和补码。赋值操作时,因为-128是32位整数,而变量是只能存储8位的char类型,所以这里存在隐式转换。转换之后,因为占位符是无符号整型,而数字却是8位数字,所以这里存在整型提升(整型提升的规则由变量的数据类型决定,占位符仅仅决定如何解释内存中的数据)。当整型提升之后,根据占位符可知,该数是一个无符号整型,所以原码、反码、补码相同,化成10进制的数字为:4294967168。

(4)练习四

cpp 复制代码
#include <stdio.h>
int main()
{
 char a = 128;
 printf("%u\n",a);
 return 0;
}

在C语言中,整数128赋值前默认为32位,其反码、补码、原码均为32位。赋值时,需要将32位的整数赋值给只能存储8位的char类型的变量,所以这里存在隐式转换,只取32位的后8位赋值给变量a。隐式转换之后,因为打印时,占位符和变量数据类型不匹配,所以这里存在整型提升。将变量的数据类型进行整型提升,得到的数值。根据占位符可知为无符号整型,所以最高位为数值位,不存在符号位,将其转化为10进制就是打印出来的数据:4294967168。

(5)练习五

cpp 复制代码
#include <stdio.h>
int main()
{
 char a[1000];
 int i;
 for(i=0; i<1000; i++)
 {
 a[i] = -1-i;
 }
 printf("%d",strlen(a));
 return 0;
}

strlen函数会从给的地址开始向后寻找 \0,找到后返回字符的个数。a位数组名,也就是数组首元素的地址,所以strlen函数会从数组的首元素开始,向后查找,直至找到内存中 \0的数字,后返回字符的个数。这里需要注意的是数组是char[1000]类型的,所以数组元素类型均为char类型,char类型的存储范围是:-128~127。当i=255时,char类型的值补码为:00000000。所以打印的结果为256(因为i是从0开始的)。

(6)练习六

cpp 复制代码
#include <stdio.h>
unsigned char i = 0;
int main()
{
 for(i = 0;i<=255;i++)
 {
 printf("hello world\n");
 }
 return 0;
}

由于char类型的内存范围是-128~127,所以上述代码的循环条件恒成立,故该循环会无线循环次数的打印hello world。

(7)练习七

cpp 复制代码
#include <stdio.h>
int main()
{
 unsigned int i;
 for(i = 9; i >= 0; i--)
 {
 printf("%u\n",i);
 }
 return 0;
}

该代码首先创建了无符号整型变量变量i,那么变量i恒大于等于0。循环的终止条件永远达不到,所以循环会无限次的打印数据。当i变为0时,下一次循环会变成非常大的数字:4294967295,然后继续-1,无限循环。

(8)练习八

cpp 复制代码
#include <stdio.h>
//X86环境 ⼩端字节序 
int main()
{
 int a[4] = { 1, 2, 3, 4 };
 int *ptr1 = (int *)(&a + 1);
 int *ptr2 = (int *)((int)a + 1);
 printf("%x,%x", ptr1[-1], *ptr2);
 return 0;
}

上述代码的具体解释:首先创建了一个整型数组,ptr1指针指向的位置是该数组末尾,ptr2将数组首元素的地址强制类型转化为int类型后+1,指向的是数组首元素地址内部的第二部分(详细见第二张图片)。ptr2最后为int *类型,打印时就需要得到4个字节的地址。

%x是16进制的数字打印时所需的占位符。因为该题目已知是小端字节序存储,所以ptr2得到的数字就是:02 00 00 00。

三.浮点数在内存中的存储

常见的浮点数有:3.14159、1E10等,浮点数的数据类型包括: float、double、long double 类型。 浮点数表示的范围:在文件float.h中定义,下面是相关文件:

1.引例

cpp 复制代码
#include <stdio.h>
int main()
{
 int n = 9;
 float *pFloat = (float *)&n;
 printf("n的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 *pFloat = 9.0;
 printf("num的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 return 0;
}

上述代码的具体分析:创建了整型变量n,初始化为9。分别用整型和浮点型的形式进行打印,经过预测答案应该是:9 9.0 9 9.0(错误答案),但是经过运行发现并不是这样的结果。这种现象说明浮点数和正数在内存中的存储方式并不相同。

2.浮点数的存储

上面的代码中,num 和*pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。 根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V,可以表示成下面的形式:

举例子来说:十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2 。那么,按照上面V的格式,可以得出S=0,M=1.01,E=2。十进制的 -5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2。

IEEE 754规定:对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M;对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M。
float类型浮点数内存分配 double类型浮点数内存分配

3.浮点数存的过程

IEEE 754对有效数字M和指数E,还有一些特别的规定。前面说过, M>=1,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的 xxxxxx小数部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。

至于指数E,情况就比较复杂,首先,E为一个无符号整数。这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,保证E最后成为一个无符号整数。对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,二进制形式即10001001。

4.浮点数取的过程

指数E从内存中取出还可以再分成三种情况:

E不全为0或不全为1时,

浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将有效 数字M前加上第一位的1(将存的过程倒着来一遍)。

比如:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则为1.0*2^ (-1),其阶码为-1+127(中间值)=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位 00000000000000000000000,则其二进制表示形式为:

0 01111110 00000000000000000000000

E全为0时

浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。

0 00000000 00100000000000000000000

E全为1时

如果有效数字M全为0,表示±无穷大(正负取决于符号位S);

0 11111111 00010000000000000000000

5.题目解析

经过上面的学习,我们接下来看引例的题目。

cpp 复制代码
#include <stdio.h>
int main()
{
 int n = 9;
 float *pFloat = (float *)&n;
 printf("n的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 *pFloat = 9.0;
 printf("num的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 return 0;
}

上述题目的具体解释:代码首先创建了整型变量n,用于存储整型9。接下来以%d形式打印有符号整型,结果就是9;但是浮点数在内存中会将9的原码以folat类型来分类存储S M E。如图一所示:前一位表示S,接下来8位表示E,后面的23位表示M。根据E为全0的定义,打印的精度不足,最后就会打印出:0.000000;下来在内存中存储小数9.0,但是却以整数方式读取数据,内存就会认为存储的浮点数是整数的补码,得到原码后,打印结果就是对应的10进制数字;因为是浮点数的存储,所以如图二:S=0 M=1.001 E=3。最终以%f 形式打印,结果就是:9.000000。

相关推荐
m0_7487080518 分钟前
C++中的观察者模式实战
开发语言·c++·算法
qq_5375626730 分钟前
跨语言调用C++接口
开发语言·c++·算法
wjs202440 分钟前
DOM CDATA
开发语言
Tingjct42 分钟前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
猷咪1 小时前
C++基础
开发语言·c++
IT·小灰灰1 小时前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧1 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q1 小时前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳01 小时前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾1 小时前
php 对接deepseek
android·开发语言·php