在通过之前学习c语言相关的知识后我们知道c语言中有多种的数据类型,那么这其中在编写程序的时候用的整型和浮点型在内存空间中是按照什么样的规律存储的呢?整型和浮点型数据在的存储方法是相同的吗?在本篇中就将详细的讲解数据在内存当中存储的相关知识
1. 整数在内存中的存储
在通过之前的学习我们知道整数的2进制表示⽅法有三种,即 原码、反码和补码,整型数据在内存中存储的是补码, 其中在有符号整数中 ,在以上三种二进制表示形式中都存在符号位;由二进制位中的最高位来决定 ,若为1则表示该整数位负数 ,为0则表示该数为正数
我们还知道正数的原码,反码和补码都是相同的 ,负数的三种二进制表示形式有以下的转换关系:
原码:直接将数值按照正负数的形式翻译成⼆进制得到的就是原码。
反码:将原码的符号位不变,其他位依次按位取反就可以得到反码。
补码:反码+1就得到补码。
注:通过补码得到原码可是先减一得到反码再取反得到原码,也可以是补码直接取反再加一得到原码
1.2. 大小端字节序和字节序判断
再学习了整数是如何再内存当中存储的后,来看以下这段代码 在下x86环境下
cpp
int main()
{
int a = 0x11223344;
return 0;
}
因为在以上代码中由于a变量的类型为int,所以在内存当中的存储长度为4字节也就是32个比特位,又因为整型变量中存放的是一个16进制表示形式的数字,一个16进制的数可以用4个二进制数来表示,那么可以得出在变量a所在的内存空间4个字节分别存放着11 22 33 44
来通过调试中的查看内存&a来验证以上的讲解是否正确:
这时就会发现在内存当中变量a实际的存储时与以上讲解的相反的,在内存存储为44 33 22 11
那么造成以上结果的原因是什么呢?
大小端字节序的概念
其实在超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储,下面是具体的概念:
大端(存储)模式:
是指数据的低位字节内容 保存在内存的高地址处 ,而数据的高位字节内容 ,保存在内存的低地址处。
小端(存储)模式:
是指数据的低位字节内容 保存在内存的低地址处 ,而数据的高位字节内容 ,保存在内存的高地址处。
所以在以上代码中就存在两种存储:
其实这是因为在vs环境下数据是按照小端字节序存储 ,所以就会出现调试时,内存存储为44 33 22 11
那么在我们知道了什么是大小端字节序后就来了解为什么会有大小端字节序:
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在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处理器还可以由硬件来选择是大端模式还是小端模式。
字节序判断
在学习了大小端字节序的概念和作用后,那如果要写一个程序来判断我们使用的机器是什么的字节序,应该如何实现呢?
我们知道如果将应该整形变量初始化为1,那么如果机器是大端字节序的存储,那么在存储该数据的第一个字节处就全部为0,那么如果机器是小端字节序的存储,那么在存储该数据的第一个字节处为01,这时就可以想到通过取出第一个字节的内容来判断大小端
cpp
#include <stdio.h>
int check_sys()
{
int i = 1;
if(*(char*)&i==1)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("⼩端\n");
}
else
{
printf("⼤端\n");
}
return 0;
在以上代码中就先&a后强制类型转换为char*,这就使得&a在解引用时候访问的的权限为1个字节,这就将 存储该数据的第一个字节处的值进行了判断,*(char*)&i==1为小端,则为大端
以上代码还有什么可以优化的地方吗?
其实check_sys函数部分返回可以直接是***(char*)&i**,所以以上代码可以简化为:
cpp
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("⼩端\n");
}
else
{
printf("⼤端\n");
}
return 0;
}
练习
在了解以上知识后来写一些练习题
练习1
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;
}
以上代码输出结果是什么呢?
在之前整型提升与算数转换以上代码中学习了将整型提升是如何实现的,所以在以上代码中-1存储在a中时先要将-1的补码截断,后再存储进去
以此在a中存储的就是11111111
因为在变量b和c中都是初始化为-1,所以在变量b和c中存储的也是11111111
之后在打印a,b,c三个变量内的数据时,因为%d是打印有符号的整型,所以在打印前要对变量a,b,c进行整型提升,因为变量a类型为char,b变量类型为signed char所以提升时是高位是按照符号位来提升
最终补码都为11111111111111111111111111111111 ,因为打印为数据的原码,所以这补码转换为原码为10000000000000000000000000000001
而c变量类型为unsigned char,提升时为无符号整数提升,高位补0,
最终原码为:00000000000000000000000011111111
所以以上代码打印结果为a=-1,b=-1,c= 255
练习2
cpp
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
cpp
#include <stdio.h>
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
以上两个代码输出结果分别是什么呢?
因为**%u是打印无符号的整型** ,所以a整型提升后的值就会直接被当作原码
打印结果就为一个很大的值
因为**%u是打印无符号的整型** ,所以a整型提升后的值就会直接被当作原码
打印结果也为一个很大的值
练习3
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;
}
以上代码输出结果是什么呢?
我们知道char存储整数的范围是-128~127,超过范围会按照以下方式循环
所以在以上代码中因为strlen求的是字符串的长度,统计的是\0之前字符的个数,a[i]一开始为-1一直减到-128时,再减一就会使得a[i]变为127,后a[i]每减一个值就会加一,直到0为止,这之间的字符个数为255
所以输出结果为255
练习4
cpp
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
cpp
#include <stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
以上代码输出结果是什么呢?
首先我们要了解 unsigned char类型的取值范围是:-128~127
当值超过255时加一会轮回为0
所以在以上第一段代码中,i<255时会一直打印hello world,且当256时候会轮回为0,所以对应的代码会陷入死循环
所以在以上第二段代码中,在unsigned int中可存储的值为0~一个很大的值,所以当i减到0后会变为这个很大的数后再一直再减到0,最终程序陷入死循环
练习5
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;
}
以上代码输出结果是什么呢?
由于是以上代码是在小端字节序的环境下运行的,所以数组a在内存中存储会按照以下形式存放在内存当中
在&a时是取出整个数组的地址,再加一就是使地址为跳过这整个数组处,再将该地址强制类型转换为int*存放在指针变量ptr1内
(int)a + 1中a表示数组首元素的地址,先将地址a强制类型转换为int,这时再对地址+1就是使地址移动了一个字节单位,再将该地址强制类型转换为int*存放在指针变量ptr2内
最终ptr[-1]=*(ptr-1),因为ptr1类型为int*,所以减一时步长为4字节,解引用时权限也为4个字节,所以ptr[-1]=4,打印16进制形式为0x00000004
而*ptr2时候,因为ptr2的类型也为int*,所以解引用时权限也为4个字节,最终打印16进制形式为
0x02000000
最终结果如下所示:
2.浮点数在内存中的存储
在学习了整数在内存中的存储后,那是否浮点数的存储也是和整数的形式一样的吗?
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;
}
在以上代码你认为输出结果是什么?
在解答以上代码前,首先要了解浮点数是怎么存储在内存当中的
2.1 浮点数的存储
根据国际标准IEEE(电气和电子工程协会) 754,任意⼀个⼆进制浮点数V可以表示成下面的形式:
• (−1)S 表示符号位,当S=0,V为正数;当S=1,V为负数
• M 表示有效数字,M是⼤于等于1,小于2的
• 2E 表示指数位
举个例子:
5.5的十进制表示形式为5.5*10^1
5.5的二进制表示形式就为101.1 写成科学计数法的形式就为1.011*10^2
所以在此S就为0,M就为1.0101,E就为2
IEEE 754规定:
对于32位的浮点数,最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数,最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
float类型浮点数内存分配
double类型浮点数内存分配
所以浮点数的存储就是存储S,M,E的相关值
2.1.1 浮点数存的过程
IEEE 754 对有效数字M和指数E,还有**⼀些特别规定** 。
前⾯说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去 ,只保存后⾯的xxxxxx部分。⽐如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去后,等于可以保存24位有效数字
IEEE 754还规定存储中的E必须是无符号的整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数 的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数 ,对于8位的E,这个中间数是127 ;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001,存储时就将10001001存储到内存当中。
2.1.2 浮点数取的过程
指数E从内存中取出还可以再分成三种情况:
1.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
2.E全为0
这时就把表示指数E的初始值为-127(或-1023),得到真实值后,这时不会将有效数字M前加上第一位的1,这是为了让表示的数更接近0
0 00000000 01000000000000000000000
3.E全为1
这时就表示指数E的初始值为128(或1024),这时若有效数M为0,则表示的数为无穷大(正负号由S的值决定)
0 11111111 01000000000000000000000
代码解析
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;
}
在了解了浮点数在内存中存储的规则后我们就可以来试着分析以上代码了
首先是将9存放在整型变量n中,存放在内存的二进制表示形式为:
0000 0000 0000 0000 0000 0000 0000 1001
因此第一个printf就是将存储的数据按照有符号整数的形式打印出来,所以输出结果就为9
之后将指针变量pFloat的值初始化为n的地址强制类型转换成float*后的值,所以这时*pFloat就是得到n存储在内存当中的整个值,不过这时打印是%f是打印浮点型,所以存放在内存的值就被当作浮点数取出来0 00000000 00000000000000000001001
在此S=0,M=00000000000000000001001,E=0
所以V=(-1)^0*1.00000000000000000001001*2^(-126)因为%f只能打印出小数点后6位,所以输出结果就为0.000000
再之后将*pFoalt=9.0就是将该浮点数的S,M,E相关值存储在原来位置的内存中9.0转换为二进制形式后为:1001.0 用科学计数法表示为:1.0010*2^3
所以S=0,M=0010,E=3因此存放在内存的二进制表示形式为:
0 10000010 00100000000000000000000
%d打印就是将以上二进制认为是有符号整数打印出,结果就为一个很大的数
最后%f就是直接将存入的浮点数认为是浮点数打印出,且保留小数点后6位,所以输出结果就为9.000000
代码输出结果与以上分析相同