【整型数据和浮点型数据】在内存中是如何存储的?原码,反码,补码的理解


前言

计算机中有很多数据,那么他们在内存中是如何存储的呢?计算机中的所有数据都是由一连串010101…这样的二进制序列存储的,本文以整型数据和浮点型数据为例,探讨这两种数据在内存中是怎样存储的?


一、数据类型介绍

char 			//字符数据类型
short			//短整型short[int]---“[]”表示[]里面的内容可以省略
int 			//整型
long			//长整型long [int]
long long		//长长整型 long long[int]
float 			//单精度浮点型
double			//双精度浮点型

类型的意义:使用这个类型开辟内存空间的大小占几个字节。

  • 我们把char, short, int, long, long long 类型归类为整型数据
  • 把float,double类型归类为浮点型数据

二、整型在内存中的存储

int a = 10;
int b = -10;

那么a和b在内存中是如何存储的呢?
我们先来了解一些概念:

2.1 原码, 反码, 补码

计算机中整数有三种2进制表示方法,及原码,反码和补码。
三种表示方法均有符号位数值位两部分,在二进制序列中,最高位符号位, 用0表示“正数”用1表示“负数”。

其中,正数的原码,反码和补码都相同。负数的三种表示方法各不相同:

  1. 原码:直接将数值按照正负数的形式翻译成二进制序列,得到原码。
  2. 反码:将原码的符号位(最高位)不变。数值位依次按位取反,得到反码。
  3. 补码:将反码+1,得到补码。

在内存中,整型数据是以补码的形式存储在内存中的。

#include <stdio.h>

int main()
{
	int a = 10;
	//a是正数,原码,反码,补码都相同
	//a的原码:00000000 00000000 00000000 00001010
	//a的反码:00000000 00000000 00000000 00001010
	//a的补码:00000000 00000000 00000000 00001010
	//16进制表示:0x00 00 00 0a
	int b = -10;
	//b是负数,原码,反码,补码各不相同
	//b的原码:10000000 00000000 00000000 00001010
	//b的反码:11111111 11111111 11111111 11110101
	//b的补码:11111111 11111111 11111111 11110110
	//16进制表示:0xff ff ff f6;

	return 0;
}

【注意】vs中的调试技巧在这里将不再详细描述, 如果下面有不理解的地方请查看详细调试技巧【Visual Stdio2022调试技巧】
Visual Stdio2022调试技巧

我们按F10进行调试,依次选择调试-窗口-内存-内存窗口1,可以查看内存中的数据。
在这里插入图片描述

我们按下F10,调试运行(在内存中创建a,b变量)。在内存窗口,地址栏:输入&a,查看变量a在内存中的数据。

在这里插入图片描述

我们可以看到:在内存中a变量存储的是0a 00 00 00, 但是我们自己推算的a在内存中应该存0x00 00 00 0a,为什么存储的顺序不一样呢?

  • 因为在在计算机中,编译器将数据以小端存储方式存储在内存中。
  • 为什么存在小端存储呢?什么又是大端存储?

2.2 小端存储和大端存储

产生的原因:在计算机系统中,我们是以字节为单位的。每一个地址对应一个字节,一个字节为8bit。但是在C语言中,除了8bit的char类型之外,还有16bit的short类型,32bit的int类型…等。对于位数大于8位的处理器,例如16位/32位的处理器,由于寄存器的宽度大于一个字节,那么必然存在一个如何将多个字节安排的问题。因此就导致了大端存储和小端存储模式。

  • 【大端存储模式】:指数据的高位保存在内存的低地址中,数据的低位保存在内存的高地址中。

  • 【小端存储模式】:指数据的高位保存在内存的高地址中, 数据的低位保存在内存的低地址中。

  • 例如: 16bit 的short类型x, 在内存中的地址为: 0x1234,那么0x12为高字节,0x34为低字节

  • 对于大端存储模式,就将0x12存放在低地址中,将0x34存放在高地址中。在内存中的表示为:12 34。

  • 对于小端存储模式,就将0x12存放在高地址中,将0x34存放在低地址中。在内存中的表示为:34 12。

所以变量a = 10; 在内存中以小端存储模式存储,a的二进制补码:0x 00 00 00 0a, 在内存中表示为:0a 00 00 00。
b = 10; b的二进制补码:0x ff ff ff f5, 在内存中表示为:f5 ff ff ff。

在这里插入图片描述

2.3 有符号数据和无符号数据

在C语言中使用 unsigned 标识的数据为无符号数据,用signed 标识的数据为有符号数据。
有符号数据前面的signed可省略,所有定义出来的变量默认都是有符号的。

  • 有符号和无符号有什么不同的地方?
  • 平时定义的数据类型,默认都是signed(有符号)的。
  • signed char 和 char, signed int 和 int … …所定义的数据是一个类型。
  • 无符号数据,二进制序列的最高位不是符号位,二进制序列的每一位都是数据位。
  • 无符号数据由于最高位也是数据位,所以表示的数据范围更大。
  • 无符号数据只能表示正数。

例如:

int a = -10;
有符号变量a,在内存中表示为:11111111 11111111 11111111 11110110
以有符号数据读取: 数字 -10。

unsigned int b = -10;
无符号变量b, 在内存中表示为:11111111 11111111 11111111 11110110
以无符号数据读取: 数字 4294967286 。

这是因为最高位不是符号位而是一个有效数值位。

#include <stdio.h>

void printBinary(unsigned int num) {
    int i;
    for (i = 31; i >= 0; i--) {
        if ((num >> i) & 1) {
            printf("1");
        }
        else {
            printf("0");
        }
    }
}

int main()
{
	int a = -10;
	unsigned int b = -10;

	printf("a = %d\n", a);
	printf("b = %u\n", b);
    printBinary(b);

	return 0;
}

在这里插入图片描述

2.4 练习题:判断当前机器的存储模式是以大端存储还是小端存储?

百度2015年系统工程师笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)

#include <stdio.h>

int check_sys()
{
	int i = 1;
	//00000000 00000000 00000000 00000001
	//0x00 00 00 01
	//将i的地址取出,强转成char* 类型,
	//解引用得到第一个字节序列
	//如果是大端存储,在内存中存放:00 00 00 01 -- 取第一个字节 数值为: 0
	//如果是小端存储,在内存中存放:01 00 00 00 -- 取第一个字节 数值为: 1
	return (*(char*)&i);
}

int main()
{
	int ret = check_sys();
	if (ret)
	{
		printf("小端");
	}
	else
	{
		printf("大端");
	}

	return 0;
}

在这里插入图片描述

三、整型数据的读取

对于整型数据的读取,我们以练习题的形式展示
(1)例题1:

//输出什么?
#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;
}

代码解释:

#include <stdio.h>

int main()
{
	char a = -1;
	//原码:10000000 00000000 00000000 00000001
	//反码:11111111 11111111 11111111 11111110
	//补码:11111111 11111111 11111111 11111111
	//a是char类型的所以在内存中只能存下1个字节,8个bit位,即数据的低8位
	//a在内存中的存储:11111111


	signed char b = -1;
	//signed char 和 char 等价
	//b在内存中的存储:11111111


	unsigned char c = -1;
	//无符号 char 每一位都是有效位(数值位)
	//c在内存中的存储:1111 1111


	printf("a=%d,b=%d,c=%d", a, b, c);
	//数据a以%d类型的形式打印, 发生整型提升
	//a由char类型整型提升到int 类型
	//a是有符号的char类型, 最高位补符号位
	//a在内存中的存储:11111111 ,符号位是:1
	//11111111 11111111 11111111 11111111 --  补码
	//数据a以%d类型打印,编译器把(11111111 11111111 11111111 11111111 --  补码)当作有符号整型数据打印
	//10000000 00000000 00000000 00000001 -- 取反后再+1得到原码
	//a的值为:-1

	//数据b以%d类型打印,整型提升,同a,
	//b的值为:-1

	//数据c以%d类型打印,整型提升
	//由char类型转换成int类型,
	//a是无符号char,所以最高位补0
	//00000000 00000000 00000000 11111111 -- 补码
	数据a以%d类型打印,编译器把(00000000 00000000 00000000 11111111 -- 补码)当作有符号整型数据打印
	//最高为是0,正数,原码反码补码相同
	//c的值为:2^8 -1  = 255

	return 0;
}

结果:
在这里插入图片描述

(2)例题2:

#include <stdio.h>
int main()
{
	char a = -128;
	printf("%u\n", a);

	return 0;
}

代码解释:

#include <stdio.h>
int main()
{
	char a = -128;
	//原码:10000000 00000000 00000000 10000000
	//反码:11111111 11111111 11111111 01111111
	//补码:11111111 11111111 11111111 10000000
	//a在内存中的存储:10000000


	printf("%u\n", a);
	//a以%u, 无符号整型类型打印,发生整型提升
	//数据a的类型是char类型,有符号char, 最高位补符号位:1
	//11111111 11111111 11111111 10000000 -- 补码
	//以%u无符号类型打印,编译器就把(11111111 11111111 11111111 10000000 -- 补码)当作无符号数打印
	//结果为:(2^32 -1) - (2^8 - 1) = 4294967168

	return 0;
}

结果:
在这里插入图片描述

(3)例题3:

#include <stdio.h>

int main()
{
	char a = 128;
	printf("%u\n", a);

	return 0;
}

代码解释:

#include <stdio.h>

int main()
{
	char a = 128;
	//00000000 00000000 00000000 10000000 -- 原码, 反码, 补码 
	//a在内存中的存储:10000000


	printf("%u\n", a);
	//a以%u 无符号整型打印,发生整型提升
	//a是char类型, 有符号char, 最高位补符号位,
	//a在内存中的存储:10000000, 符号位是 1
	//11111111 11111111 11111111 10000000 -- 补码
	//以无符号类型打印,编译器就把(11111111 11111111 11111111 10000000 -- 补码)当作无符号数
	//结果为:(2^32 -1) - (2^8 - 1) = 4294967168

	return 0;
}

结果是:
在这里插入图片描述

(4)例题4:

#include <stdio.h>

int main()
{
	int i = -20;
	unsigned int j = 10;

	printf("%d\n", i + j);
	
	return 0;
}

代码解释:

#include <stdio.h>

int main()
{
	int i = -20;
	//原码: 10000000 00000000 00000000 00010100
	//反码: 11111111 11111111 11111111 11101011
	//补码: 11111111 11111111 11111111 11101100

	unsigned int j = 10;
	//原码,反码,补码: 00000000 00000000 00000000 00001010


	printf("%d\n", i + j);
	//按照补码的形式进行运算,最后格式化成为有符号整数
	//i和j的数据类型不一致, i发生算术转换
	//补码: 11111111 11111111 11111111 11101100  -- 无符号的i
	//
	//i: 11111111 11111111 11111111 11101100 -- 补码
	//j: 00000000 00000000 00000000 00001010 -- 补码
  //i+j: 11111111 11111111 11111111 11110110 -- 补码
	//结果以%d类型打印,编译器就把(i+j: 11111111 11111111 11111111 11110110 -- 补码)当作有符号整型数据
	//i+j是负数:
	//补码按位取反:10000000 00000000 00000000 00001001
	//补码按位取反+1:10000000 00000000 00000000 00001010
	//i+j的数值:-10
	
	return 0;
}

结果为:
在这里插入图片描述

(5)例题5:

#include <stdio.h>

int main()
{
	unsigned int i;

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

	return 0;
}

代码解释:

#include <stdio.h>

int main()
{
	unsigned int i;
	//unsigned int,无符号整型,最大表示:32个全1
	//11111111 11111111 11111111 11111111
	//即2^32 -1
	//范围:[0, (2^32-1)]
	// 
	//i>=0 恒为真,死循环
	for (i = 9; i >= 0; i--)
	{
		printf("%u\n", i);
	}

	return 0;
}

同理:

#include <stdio.h>
unsigned char i = 0;
int main()
{
	//i是无符号字符型,最大表示(8个全1):11111111 -- 数值:255
	//i的取值范围:[0, 255]
	//i<=255,恒为真,死循环
    for(i = 0;i<=255;i++)
   {
        printf("hello world\n");
   }
    return 0;
}

(6)例题6

#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;
}

代码解释:

#include <stdio.h>

int main()
{
	char a[1000];
	int i;
	
	for (i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
		//将-1-i 的值存放在char类型的数组中
		//数组中每一个数都是char类型的,
		//char类型表示范围:
		//00000000  -- 0
		//00000001  -- 1
		//00000010  -- 2
		//00000011  -- 3
		//...
		//01111111  -- 127
		//10000000  -- (C语言规定这个数是)-128
		//10000001  -补码取反-> 11111110 -取反后+1-> 11111111 (原码)-127
		//10000010  -补码取反-> 11111101 -取反后+1-> 11111110 (原码)-126
		//10000011  -补码取反-> 11111100 -取反后+1-> 11111101 (原码)-125
		//...
		//11111110  -补码取反-> 10000001 -取反后+1-> 10000010 (原码)-2
		//11111111  -补码取反-> 10000000 -取反后+1-> 10000001 (原码)-1

		//char类型的数据取值范围是:[-128, 127]
		//a[i] = -1 - i;
		//a[i] 的取值有:-1, -2, -3... ...-127, -128, 127, 126... ... 3, 2, 1, 0, -1, -2, ... ...
		//一直循环下去
	}

	printf("%d", strlen(a));
	//strlen()求字符串的长度,是指从起始位置开始,直到遇见'\0', 求这之间的字符数。('\0'的ASCII码值为:0)
	//换种意思说,在数组a中,从a[0]开始,直到遇见数字0结束,这之间有多少个元素。
	//a[i] 的取值有: - 1, -2, -3... ... - 127, -128, 127, 126... ... 3, 2, 1, 0,
	//一共有: 127*2 + 1 = 255
	return 0;
}

结果:
在这里插入图片描述

四、 浮点型数据在内存中的存储

4.1小松鼠:举一个栗子

下面代码运行结果是多少?

#include <stdio.h>

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

	return 0;
}

小松鼠:运行结果见上述注释内容O.o!
运行结果:在这里插入图片描述

小松鼠:默默撤回了一个栗子QvQ。

为什么会产生这样的结果呢?
因为:浮点数在内存中的存储和正数在内存中的存储方式不一样,所得到的结果也是不一样的。

4.2 浮点数的存储规则IEEE754

在计算机中,浮点数的存储方式是根据国际标准IEEE(电气电子工程协会)754标准来存储的,标准规定:任何一个二进制浮点数V可以表示成下面的这种形式:

  • (-1)^S * (1.M) * 2^e, E=e+127
  • S 表示符号位,占1位,当S=0,V为正数,当S=1, V为负数
  • M表示有效数字,占23位,取值范围:1 <= M <2。
  • e 表示指数。E是阶码,表示指数位,是一个无符号数。

IEEE754标准规定:
对于32位的浮点数,最高位的1位是符号位S,紧接着的8位是指数E, 后23位是有效位M。

【32位浮点数】
在这里插入图片描述

对于64位的浮点数,最高位的1位是符号位S,紧接着的11位是指数E, 后52位是有效位M。

【64位浮点数】

在这里插入图片描述
例如:

  • 10进制的5.5,二进制表示:101.1, 相当于:1.011*2^2
  • 那么,按照上述标准可以写成,(-1)^0 * 1.011 * 2^2
  • 所以, S=0, 1.M=1.011, E=2+127=129

那么10进制的5.5在内存中的二进制存储为:

  • 0 10000001 01100000000000000000000
  • 即01000000 10110000 00000000 00000000
  • 其中,S=0,E=10000001=129,M=01100000000000000000000

IEEE754对有效数字M和指数位E,还有一些特殊规定:

  1. 为什么有效数字只保存M不保存1.M?

IEEE754标准规定, 在计算机内部保存1.M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的M部分。比如保存101.100时,只保存后面的100,等到读取的时候,再把第一位1和小数点加上去。这样做的目的是,节省了1位有效数字位,保存的精度会提升。例如在32位系统中,留给浮点数M的只有23位,将第一位舍去后,等于可以保存24位有效数字。

  1. 对指数e的表示

对于指数e的情况就比较复杂,首先E是一个无符号整型(usigned int), 这意味着,如果E为8位, 他的取值范围为:0 ~ 255;如果E时11位,他的取值范围时:0 ~ 2047。但是在科学计数法中e时可以为负数的,所以IEEE754规定,存入内存时,指数e必须加上一个固定数字,即存入E = e+127; 对于8位的E, 这个固定数字是127, 对于11位的E, 这个固定数字是1023。
比如:2^2的e是2,存入内存中的是E = e+127 = 129, 即10000001

小练习:举个例子
将10进制:20.59375转换成IEEE标准的32位浮点数的二进制存储格式。

  • 首先将10进制数转换成二进制数
  • 转换后为:10100.10011
  • 规格化表示:(-1)^0 * 1.010010011* 2^4
  • S=0; M=010010011; e=4, E=e+127=131
  • 131的二进制数:10000011
  • 所以最后得到的32位浮点数的二进制存储格式为:
  • 0 10000011 01001001100000000000000
    -即 01000001 10100100 11000000 00000000
    -即 0x41 A4 C0 00

五、 浮点型数据的读取

了解到了浮点数是如何在内存中存储的,接下来我们看如何从内存中将浮点数读取出来?

浮点型数据的读取就是从内存中依次取出S, M, E, 在”拼装“成 V=(-a)^S * (1.M) * 2^e, e=E-127;得到的V即为所取出的数据。

  • 例如:在本文前面的例子中,10进制5.5, 在内存中二进制的存储为:
    01000000 10110000 00000000 00000000
  • S占1位,S=0
  • E占8位, E=10000001=129, e=E-127 = 129-127 = 2
  • M占23位, 01100000000000000000000
  • V=(-1)^0* 1.011* 2 ^ 2 = 101.1,转换成10进制为:5.5

注意:E的值需要计算一下,M的值直接按二进制序列读取。在M前面加上1和小数点,1.M。

5.1 指数位E的读取

指数位E从内存中取出还可以分为3中情况:

  1. E不全为0或不全为1

浮点数的指数e为: 指数位E的计算值减去127(64位浮点数减去固定值1023),得到真实值e,再将有效数字M的前面加上第一位的1和小数点。

  • 比如:0.5的二进制形式为:0.1,由于IEEE754规定,正数部分必须为1, 所以要将小数点向有移动1位,则为:1.0 * 2^(-1),阶码E=e+127= -1+127 = 126,E表示为: 01111110, 而尾数1.0取出整数部分,留下小数部分0,补齐为23位,00000000000000000000000, 则二进制存储格式为: 0 01111110 00000000000000000000000
  1. E全为0

这是浮点数的指数e = E-127=0-127=-127(64位浮点数减去固定值1023)。有效数字M不在加上第一位的1,而是还原成0.xxxxxx的小数。这样做是为了表示+0和-0,以及接近于0的很小的数字。

  1. E全为1

这时,e=E-127 = 255-127=128。V=(-1)^S * (1.M) * 2^128,如果有效数字M全为0,表示正负无穷大(正负取决于符号位S)。

5.2 小松鼠:吃一个栗子

那么现在,我们就来分析下面这个例子:

#include <stdio.h>

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

	return 0;
}

代码分析:

#include <stdio.h>

int main()
{
	int n = 9;
	//n在内存中的存储: 
	//00000000 00000000 00000000 00001001 -- 原码,反码,补码

	float* pFloat = (float*)&n;
	//指针pFloat 指向变量 n 这块地址

	printf("n的值为:%d\n", n);//9
	//以 %d 的形式打印, 编译器将(00000000 00000000 00000000 00001001 -- 原码,反码,补码)认为有符号的整型
	//打印结果是:9

	printf("*pFloat的值为:%f\n", *pFloat);//0.000000
	//*pFloat 解引用,得到pFloat指针指向的内容。(00000000 00000000 00000000 00001001 -- 原码,反码,补码)
	//以%f的形式打印,编译器将(00000000 00000000 00000000 00001001 -- 原码,反码,补码)认为是一个单精度浮点数
	//以浮点数的形式解析这串二进制序列。
	//S=0
	//E=00000000 = 0
	//M=00000000000000000001001
	//此时我们发现,阶码E(指数位)为0,意味着这个浮点数的指数e= E-127 = -127, 这将是一个非常小非常小,无线趋近于正负0 的值
	//IEEE754标准规定,当E为全0时, 这个浮点数的真值就是0
	//因此输出:0.000000

	*pFloat = 9.0;
	//将指针pFloat指向的内容赋值为:9.0
	//即将9.0以单精度浮点数存储
	//9.0的二进制形式为: 1001.0 = 1.001*2^3
	//规格化浮点数:(-1)^0 * 1.001 * 2^3,  指数: e=3
	//S=0
	//E=e+127=3+127=130,  二进制表示为:10000010
	//M=001  补齐23位,00100000000000000000000
	//9.0的二进制存储格式为:0 10000010 00100000000000000000000
	//即:01000001 00010000 00000000 00000000
	

	printf("n的值为:%d\n", n);//1,091,567,616
	//*pFloat 将n的值以浮点数的形式改为9.0
	//n在内存中表示为:01000001 00010000 00000000 00000000
	//以%d的形式打印, 编译器将(01000001 00010000 00000000 00000000)认为是有符号整型数据
	//16进制表示为:0x41 10 00 00 -- 数值为:1,091,567,616
	//小端存储为: 00 00 10 41
	//所以打印:1,091,567,616
	
	printf("*pFloat的值为:%f\n", *pFloat);//9.0
	//以浮点型数据存,再以浮点型数据取,所以取出的值不变,还是9.0
	//下面是详细过程:
	//*pFloat 解引用,得到pFloat指向的内存的数据:01000001 00010000 00000000 00000000
	//以%f的形式打印,编译器将(01000001 00010000 00000000 00000000)认为是单精度浮点数
	//S=0
	//E=100000010=, e=E-127 = 130-127 = 3
	//M=001
	//规格化浮点数:(-1)^0 * 1.001 * 2^3 = 1001.0
	//转换成10进制为:9.0
	//打印:9.0

	return 0;
}

调试运行,查看内存的数据,发现和我们计算的数据一致。
在这里插入图片描述
结果:
在这里插入图片描述

总结

本文主要介绍了

  • 整型数据在内存中是如何存储的,掌握整型数据在内存中的原码,反码,补码,以及什么是大端存储模式?什么是小端存储模式?
  • 浮点型数据在内存中是如何存储的,掌握IEEE754规格化浮点数,知道32位浮点数,S占1位,E占8位,M占23位。64位浮点数,S占1位,E占11位,M占52位。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值