文章目录
前言
计算机中有很多数据,那么他们在内存中是如何存储的呢?计算机中的所有数据都是由一连串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,得到补码。
在内存中,整型数据是以补码的形式存储在内存中的。
#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,还有一些特殊规定:
- 为什么有效数字只保存M不保存1.M?
IEEE754标准规定, 在计算机内部保存1.M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的M部分。比如保存101.100时,只保存后面的100,等到读取的时候,再把第一位1和小数点加上去。这样做的目的是,节省了1位有效数字位,保存的精度会提升。例如在32位系统中,留给浮点数M的只有23位,将第一位舍去后,等于可以保存24位有效数字。
- 对指数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中情况:
- 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
- E全为0
这是浮点数的指数e = E-127=0-127=-127(64位浮点数减去固定值1023)。有效数字M不在加上第一位的1,而是还原成0.xxxxxx的小数。这样做是为了表示+0和-0,以及接近于0的很小的数字。
- 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位。