深度剖析数据在内存中的存储

深度剖析数据在内存中的存储,第1张

深度剖析数据在内存中的存储
  • 1.数据类型
    • (1)基本数据类型
    • (2)类型的基本归类
  • 2.整型在内存中的存储
    • (1)原码,反码,补码
    • (2)大端,小端
  • 3.练习
  • 4.浮点型在内存中的存储

1.数据类型 (1)基本数据类型

char 字符数据类型 1字节
short 短整型 2字节
int 整形 4字节
long 长整型 4字节
long long 更长的整形 8字节 (C99)
float 单精度浮点数 4字节
double 双精度浮点数 8字节
-Bool 布尔类型,专门用来表示真假的 (C99)

注:

  • sizeof(long)>=sizeof(int)
  • C语言没有字符串数据类型,字符串以字符数组的形式表示。
(2)类型的基本归类

整型:

  • char:
    unsigned char
    signed char
    注: char并不等价于signed char,char到底是signed char还是unsigned char,这是取决于编译器的,但常见的编译器下char就是signed char。
  • short:
    unsigned short [int]
    signed short [int]
    short ==> signed short
  • int
    unsigned int
    signed int
    int==>signed int
  • long
    unsigned long
    signed long
    long==>signed long

注: 有符号整型和无符号整型只是看待内存中数据的一种角度,并不是绝对的:

如上述代码:一开始无符号整型变量num = 10,输出结果为10,这很容易理解,可是后面将无符号整型变量num的值改变为有符号数-10(此时num属于无符号变量,故将num的符号位视为有效位,也就是-10的符号位视为有效位),然后以有符号整型的形式输出,因为是以有符号整型的形式输出,所以编译器将该无符号整型变量num视为有符号整型变量,即num的符号位是存在的,故输出-10。

这个例子告诉我们无符号数(num)可以视作有符号数。


我们接着再看这个代码,一开始依然是无符号整型变量num = 10,输出结果为10,这很容易理解,后面还是将无符号整型变量的值改为有符号数-10(此时num属于无符号变量,故将num的符号位视为有效位,也就是-10的符号位视为有效位),与前面代码不同的是,这里是以无符号整型的方式输出,也就是将无符号变量num依然视为一个无符号变量,即它的符号位视作有效位,故最终结果是一个很大的数。

这个了例子告诉我们有符号数(-10)可以视作无符号数。

由以上两个例子我们可以看出有符号整型和无符号整型只是看待内存中数据的一种角度,并不是绝对的,一个无符号数可以将其看作是有符号数,同样的一个有符号数我们也可以将其看作是一个无符号数。

浮点数家族:

  • float
  • double

构造类型(自定义类型):

  • 数组类型
  • 结构体类型 struct
  • 枚举类型 enum
  • 联合类型 union

指针类型:

  • int* pi
  • char* pc
  • float* pf
  • void* pv:void 表示空类型(无类型),通常应用于函数的返回类型、函数的参数、指针类型。
2.整型在内存中的存储

一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的,那么数据在所开辟内存中到底是如何存储的?

比如:

int a = 20;
int b = -10;

我们都知道为a,b分配了4个字节的空间,那它是如何存储的呢?这就涉及到原码,反码,补码的知识了。

(1)原码,反码,补码

计算机中的有符号数有三种表示方法,即原码、反码和补码。

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

三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。

对于正数: 正数的原码,反码,补码都相同,如正整数5:

原码:00000000000000000000000000000101
反码:00000000000000000000000000000101
补码:00000000000000000000000000000101

对于负数: 需要按照上面的规则求原码,反码和补码,如-5:

原码:10000000000000000000000000000101
反码:111111111111111111111111111111111010
补码:111111111111111111111111111111111011

注:

  • 原码的符号位不变,其它位按位取反+1得到补码,补码-1,符号位不变,其它位按位取反也可以得到原码。

  • 对于整型来说:数据以补码的形式在内存中存储,但使用的时候使用的是原码的值。

  • 无符号数的原码、反码、补码也都相同。

为什么呢?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,补码按位取反+1可以得到原码,不需要额外的硬件电路。

(2)大端,小端

我们先来看下面这段简单的代码:

int a = 20;
int b = -10;

对于变量a:
因为是正数,所以变量a的原码,反码,补码都相同,都是
00000000 00000000 00000000 00010100

补码的十六进制:00 00 00 14

对于变量b:
原码:10000000 00000000 00000000 00001010
反码:111111111 111111111 111111111 11110101
补码:111111111 111111111 111111111 11110110

补码的十六进制:FF FF FF F6

我们已经知道对于整型来说,数据以补码的形式在内存中存储,那么变量a和b在内存中存储的是00 00 00 14和FF FF FF F6吗?

我们在VS2019中来观察变量a和b的内存,显然,它们的存储顺序与上述是相反的,这是为什么呢,这其实就涉及到另一个知识点——大端,小端存储模式。

大端(存储)模式: 是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。

小端(存储)模式: 是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。

为什么有大端和小端:

为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址都对应着一个字节。但是在C语言中除了1字节(8位)的char之外,还有2字节(16位)的short型,4字节(32位)的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字
节,那么必然存在着一个按什么顺序存储多个字节的问题。因此就导致了大端存储模式和小端存储模式。

例如一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22为低字节。对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,刚好相反。

我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。

了解了大端模式和小端模式之后,我们就能很好地理解刚刚那张图了:

显然我们的处理器使用的是小端模式,对于变量a,字节14属于低位,故保存在低地址空间中,后面也是低位存储在低地址空间中。对于变量b,字节F6属于低位,故保存在低地址空间中,后面也是低位存储在低地址空间中。

注: 大小端是以字节为单位的,表示的是每个字节的存储顺序,一个char类型变量只有一个字节,存储顺序永远一样,所以char类型的变量是没有大小端之分的。

百度2015年系统工程师笔试题:

请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

方法一:利用指针类型的访问权限特性

#include

int judge()
{
	int i = 1;
	return *(char*)&i;
}
int main()
{
	judge() == 1 ? printf("小端") : printf("大端");

	return 0;
}

方法二:利用联合

#include

int judge()
{
	union
	{
		int i;
		char c;
	}un;

	un.i = 1;

	return un.c;
}
int main()
{
	judge() == 1?printf("小端") : printf("大端");

	return 0;
}
3.练习

下面这些代码输出啥呢?

(1)

#include 
int main()
{
  char a= -1;
  printf("a = %d\n",a);
  signed char b=-1;
  printf("b = %d\n",b);
  unsigned char c=-1;
  printf("c = %d\n",c);
  return 0;
}

首先我们来分析变量a,第一句代码char a= -1;这表示把32位的有符号整数-1存储在8位的char型变量a中,由于整型在内存中以补码的形式存储,所以先求出-1的补码为
11111111 111111111 111111111 111111111,显然这存储不下,所以会发生高位截断,也就是只存储低八位,所以a中存储的是11111111,printf("a = %d\n",a);表示以有符号整型的方式输出,所以先进行整型提升,将其提升为int类型,由于在VS2019环境下char ==> signed char,所以a是一个有符号变量,它的符号位1,整型提升时高位补1,经过整型提升后char a提升为int a,其中存储的二进制序列为11111111 11111111 11111111 11111111,又因为输出的是原码的值,所以需要先求出原码,前面已经说过以有符号整型的方式输出,所以这里的a是为有符号变量,符号位为1,是负数,那么按照规则求出它的原码为1000000 0000000 0000000 00000001,因为以有符号整型的方式输出,所以这里的符号位存在,故最终输出的结果为a = -1。

变量b和变量a一样都是signed char型,所以分析过程都是一样,结果为b = -1。

有了前面的分析作为基础,最后我们再来简单分析下变量c,c中存储的二进制序列是11111111,然后进行整型提升,因为c为unsigned char类型,所以高位补零,整型提升后,unsigned char c变为int c,c中存储的二进制序列是00000000 00000000 00000000 11111111,因为是以有符号整型的方式输出,所以我们认为它是一个有符号变量,c的符号位为0,为正数,故它的原码也为00000000 00000000 00000000 11111111,故输出结果为c = 255。

(2)

#include 
int main()
{
  char a = -128;
  printf("%u\n",a);
  return 0;
}

-128的补码为11111111 11111111 11111111 10000000,所以a中存储的是10000000,然后进行整型提升得到int a,其中存储的二进制序列为11111111 11111111 11111111 10000000,然后以无符号整型的方式输出,所以我们把a视为一个无符号数,所以其原码为11111111 11111111 11111111 10000000,故输出结果为4294967168。

(3)

int i= -20;
unsigned  int  j = 10;
printf("%d\n", i+j);

int iunsigned int j 都是32位整数,那么它们的加法运算可以直接拿它们的补码进行加法运算。

i的补码:11111111 11111111 11111111 11101100
j的补码:00000000 00000000 00000000 00001010
i+j: 11111111 11111111 11111111 11110110
i+j原码: 1000000 0000000 00000000 00001010

以有符号整型的方式输出,所以最终结果为-10。

注: 底层运算肯定是进行了算术转换的。

(4)

unsigned int i;

for(i = 9; i >= 0; i--)
{
  printf("%u\n",i);
}

一开始i = 9 8 7 6 5 4 3 2 1 0,输出9 8 7 6 5 4 3 2 1 0,然后再减一,理论上此时i= -1,-1的补码为11111111 111111111 111111111 111111111,所以此时i中存储的是11111111 111111111 111111111 111111111,因为i是一个无符号整型,所以i的原码也是11111111 111111111 111111111 111111111,所以此时的符号位也视为有效位,所以此时作为一个无符号整型,它的值是4294967295,即实际上此时i的值是4294967295,最后以无符号整型的方式输出i,输出结果为4294967295。

作为一个无符号整型变量,它的值永远不能可能小于0,所以改代码会无限循环下去。


(5)

int main()
{
  char a[1000];
  int i;
  for(i=0; i<1000; i++)
 {
    a[i] = -1-i;
 }
  printf("%d",strlen(a));
  return 0;
}

在做这一题之前,我们需要先了解一下char类型变量的取值范围,char类型变量占8位,那么它的内存空间中的二进制序列一共有256种,它们都是补码。

我们先来分析下unsigned char的取值范围,看下图:

对于unsigned char,补码即为原码,且它的符号位不存在,即它的符号位都视为有效位,因此取值范围为0-255。

问题来了,255再+1,此时unsigned char型变量的值是多少?
255+1=256,256在内存中的补码是:00000000 00000000 00000001 00000000,所以unsigned char型变量存储的是00000000,即255+1=256时,此时unsigned char变量的值为0,再+1,变量的值又为1了,由此可见,unsigned char型变量的值实际上是处于一个循环中。如下图所示:

再来分下signed char,看下图:

由于是signed char,因此符号位是存在的,正数的二进制取值为00000000-01111111,因为是正数,所以补码与原码相同,算出它的取值范围0 ~ 127,负数的二进制取值范围为10000000-11111111,因为是负数,所以先要按照规则求出相应的补码,最终求出负数的取值范围为-128 ~ -1。故singed char的取值范围为-128~127。

通过上图我们可以观察到对于signed char型变量实际上处于一个循环中,首先从0开始+1得到1,然后不断+1,最终得到127,127+1后,signed char型变量存储的是10000000,也就是-128,然后不断+1,最终得到-1,再+1后,又变成了0,由此可见signed char型变量的取值也是处于一个循环中,如下图:

**注:**有符号数10000000没办法直接计算,编译器默认解析为-128.

现在我们再回归到题目本身,看下面代码:

int main()
{
  char a[1000];
  int i;
  for(i=0; i<1000; i++)
 {
    a[i] = -1-i;
 }
  printf("%d",strlen(a));
  return 0;
}

前面说过signed char型变量的取值范围在-128~127,也就是说-128 ~ 127都可以存储在char型变量中,那么根据代码会先从-1开始存储,当存储到-128时,再减一,根据上面的循环图,-1之后存储的就是127,然后不断-1,存储到0,再-1,根据上面的循环图,又存储-1,然后依次往后进行存储,直到将数组1000个元素初始化完成。如下图:

初始化完成后,printf("%d",strlen(a));表示求字符串a的长度,那就从第一个元素开始一直到 0为止,即为字符串a的长度,也就是255。

(6)

#include 
int main()
{
	unsigned char i = 0;
	
  for(i = 0;i<=255;i++)
 {
    printf("hello world\n");
 }
 
	return 0;
  
}

前面已经说过unsigned char型变量处于一个循环中,所以unsigned char型变量i恒<=255,所以循环条件永远满足,故这个for循环是个死循环,会无限输出hello world。

4.浮点型在内存中的存储

我们先来看一下下面代码,大家觉得输出结果是啥呢?

int main()
{
	int n = 9;
	float *pFloat = (float *)&n;
	
	printf("n的值为:%d\n",n);
	printf("*pFloat的值为:%f\n",*pFloat);
	
	*pFloat = 9.0f;
	
	printf("n的值为:%d\n",n);
	printf("*pFloat的值为:%f\n",*pFloat);
	
	return 0;
}

上述代码的输出结果如下:

n 和 *pFloat 在内存中明明是同一个数,为什么以整数和浮点数的方式输出,最后的结果会差别这么大? 要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。

根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成这种形式:(-1)^S * M * 2^E

其中(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。M表示有效数字,大于等于1,小于2(也就是整数部分必须为1,如果不为1,就要把小数点向右移动若干位,使得整数部分为1)。2 ^E表示指数位。

举例来说: 十进制的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。
IEEE 754对有效数字M和指数E,还有一些特别规定。 前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。

IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。

比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字,小数部分多了1位,精度更高。

以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,小数位就有23位了,等于可以保存24位有效数字。

至于指数E,情况就比较复杂。

首先,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。

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

(1)E不全为0或不全为1

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

比如: 0.5(1/2)的二进制形式为0.1,由于规定整数部分必须为1,即将小数点右移1位,
则为1.0*2^(-1),显然S = 0,因为存入内存时E的真实值必须加上中间值,因此其阶码为-1+127=126,故E = 01111110,而尾数1.0去掉整数部分为0,补齐0到23位,所以M = 00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000

这时候指数E从内存中取出,直接将指数E的值-127(1023)得到E的真实值。

(2)E全为0

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

(3)E全为1

这时,E可能为128,这时候的浮点数表示±无穷大(正负取决于符号位s);

好了,关于浮点数的表示规则,就说到这里。

下面,让我们回到一开始的问题:

int main()
{
	int n = 9;
	float *pFloat = (float *)&n;
	
	printf("n的值为:%d\n",n);
	printf("*pFloat的值为:%f\n",*pFloat);
	
	*pFloat = 9.0f;
	
	printf("num的值为:%d\n",n);
	printf("*pFloat的值为:%f\n",*pFloat);
	
	return 0;
}

上述代码的输出结果如下:
首先第一个输出结果,int类型的变量n以整型的方式输出,直接输出9,这个无可争议,不做赘述。

然后来了第二个输出,为什么9以浮点数的形式输出就成了 0.000000 ?

此时n中存储的是整数的二进制序列,以浮点数的形式输出,那么我们就将整数的二进制序列按浮点数格式划分出S、E、M,然后写出它的浮点数表达式。

首先,将9(00000000 00000000 00000000 00001001)按照浮点数的格式拆分,得到第一位符号位s=0,后面8位的指数 E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 00001001。

按浮点数格式划分出S、E、 M后,写出它的浮点数表达式。 由于指数E全为0,所以符合上面的第二种情况,E的真实值为-126。因此,浮点数V就写成:
V=(-1)^ (0) ×0.00000000000000000001001× 2^(-126)= 1.001× 2^(-146) 。显然,V是一个很小的接近于0的正数,所以以浮点数的形式输出就是0.000000。

再看下面的另一行输出,把浮点数9.0通过float类型指针赋值给int类型的变量n后,再以整型的方式输出变量n,为什么输出的是1091567616?

这里是将n中存储的浮点数二进制序列以整型的方式输出,所以根据浮点数的二进制序列转化为整型的二进制序列。

首先,浮点数9.0的二进制序列是1001.0,即1.001×2^3,那么,第一位的符号位s=0,有效数字M等于001后面再加20个0,凑满23位,指数E等于3+127=130,即10000010。 所以,写成二进制形式,应该是s+E+M,即0 10000010 001 0000 0000 0000 0000 0000,转化为整型的二进制序列后,就可以以整型的方式输出,输出结果正是 1091567616 。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1330795.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-12
下一篇 2022-06-12

发表评论

登录后才能评论

评论列表(0条)

保存