—深度剖析数据在内存中的存储
本节重点
- 数据类型详细介绍
- 整形在内存中的存储:原码、反码、补码
- 大小端字节序介绍及判断
- 浮点型在内存中的存储解析
1.数据类型介绍
1.1 基本内置类型(C语言本身库有函数)
char | 字符数据类型 |
short | 短整型 |
int | 整形 |
long | 长整型 |
long long | 更长的整形 |
float | 单精度浮点数 |
double | 双精度浮点数 |
1.2 构造类型(自定义函数)
自定义函数是系统不自带的,通过自己的编写后可以使用的函数。一般的编程语言、工作表等都可以编写自定义函数使用。
编写自定义函数可以简化主程序,让程序的检查调试更方便
1.3 类型的意义
a.使用这个类型开辟内存空间的大小(大小决定了使用范围)
比如说使用char类型创建的变量,开辟的内存空间是1个字节,使用int类型创建的变量,开辟的内存空间是4个字节。
b.如何看待内存空间的视角
int main()
{
int a = 10;
int b = 10;
float c = 10.0;
return 0;
}
虽然 a, b都是占用4个字节的空间,但是我们在看待a的时候,因为其类型是int,所以我们会把a当做整型来看待,在看待b的时候,因为其类型是float,所以我们会把b当做小数(而非整型)来看待。
2.数据类型细分类
2.1 整形家族(基本内置类型)
char(char在存储时存储的是ASCLL码值) | unsigned char |
signed char | |
short | unsigned short |
signed short | |
int | unsigned int |
signed int | |
long | unsigned long |
signed long |
unsigned:无符号,signed:有符号
2.2 浮点型家族(基本内置类型)
float
double
2.3 构造类型(自定义函数类型)
数组类型 | 去掉数组名称,剩下的就是数组类 |
如: int arr[10]的类型是int [10] | |
结构体类型 | struct |
枚举类型 | enum |
联合类型 | union |
2.4 指针类型
int* | pi |
char* | pc |
float* | pf |
void* | pv |
2.5 空类型
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
3.整型在内存中的存储
之前讲过 一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们谈谈数据在所开辟内存中到底是如何存储的?
3.1 如何储存
看下面这个例子:
#include<stdio.h>
int main()
{
int a = 3;
int b = -1;
return 0;
}
数据在内存中存储时是按二进制的补码存储的
展示内存的时候,为了方便展示,显示的是16进制数据。
3.2 原码、反码、补码
下面先来了解几个概念︰原码、反码、补码
计算机中的有符号数(整型)有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示′正”,用1表示"负”,而数值位三种表示方法各不相同。
(注:无符号数也有三种表示方法,即原码、反码和补码;但三者相同)
有符号数中:
正数 :原码反码补码 三码合一;
负数的原反补 按照下面的规则进行转换:
##blue##
原码:直接将二进制按照正负数的形式翻译成二进制就可以。反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码 + 1就得到补码
整数有两种,有符号数和无符号数
有符号数:符号位 + 数值位,即:
正数 : 0 + 数值位
负数 : 1 + 数值位
int a = 3;//4个字节---32比特位
//十进制显示形式
//00000000 00000000 00000000 00000011 - 原码、反码、补码
//0000 0000 0000 0000 0000 0000 0000 0000 0000 0011
//十六进制显示形式
//0x00 00 00 03
int b = -1;
//十进制显示形式
//10000000 00000000 00000000 00000001 - 原码
//11111111 11111111 11111111 11111110 - 反码
//11111111 11111111 11111111 11111111 - 补码
//十六进制显示形式
//0xff ff ff ff (补码)
3.3 为什么内存中要存储补码?
我们首先来看一下 1 - 1 这个例子:
①先按照原码的方式去计算:
②接下来用补码来进行计算:
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理; 同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
怎么理解补码与原码相互转换,其运算过程是相同的?(以下运算,符号位均不变)
原码->取反 + 1->补码
补码->取反 + 1->原码
当然补码到原码也可以是:补码 -> - 1 取反->原码
4.大小端介绍
4.1 什么是大小端
##blue##
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中。小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
4.2 为什么有大小端
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。
但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器)。
另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。
因此就导致了大端存储模式和小端存储模式。
例如一个16bit的short型x,在内存中的地址为ox0010),x的值为0×1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反
我们常用的x86结构是小端模式,而KEIL c51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
4.3 一道笔试题
请简述大端字节序和小端字节序的概念,并设计一个小程序来判断当前机器的字节序
int a = 20; //大端0x 00 00 00 14
//小端0x 14 00 00 00
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
int check_sys()
{
int a=1;
return char* p=(char*)&a;;
}
int main()
{
int ret = check_sys();
if(ret ==1)
{
printf("小端\n");
}
else
{
printf("大端\n")
}
return 0;
}
4.4 练习与理解
练习1
下面这段代码的结果是什么?
#include<stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("a = %d,b = %d,c = %d\n", 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 补码
//真正存储的补码: 11111111(左边)
//整型提升(根据真补码左边第一位补充):11111111 11111111 11111111 111111111
//转换成原码输出打印
signed char b = -1;
//真正存储的补码: 11111111(过程与a相同)
unsigned char c = -1;
//真正存储的补码: 11111111
//无符号数整型提升时,将符号位当0处理
//整型提升结果(无符号数 原反补相同) : 00000000 00000000 00000000 11111111
printf("a = %d,b = %d,c = %d\n", a, b, c); // %d表示打印整型
return 0;
结果 : -1, -1, 255
}
练习2
下面这段代码的结果是什么?
#include<stdio.h>
int main()
{
char a = -128; //128二进制: 10000000
printf("%u\n", a);
// %u打印十进制的无符号数字
// %d打印十进制有符号
return 0;
}
以下是解析:
注:%d与%u的整型提升是一样的,但随后
%d :若符号位为负,则进行原反补
%u :直接打印
#include<stdio.h>
int main()
{
char a = -128;
printf("%u\n", a);
// %u打印十进制的无符号数字
// %d打印十进制有符号
return 0;
}
练习3
下面这段代码的结果是什么?
#include<stdio.h>
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
过程与结果 同上…
练习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
//结果
//补码:11111111 11111111 11111111 11110110
//原码:
//即 -10
printf("%d\n", i + j);
return 0;
}
练习5
下面这段代码的结果是什么?
#include<stdio.h>
int main()
{
unsigned int i = 0;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
以下是解析:
#include<stdio.h>
#include<Windows.h> //用于引用Sleep函数
int main()
{
unsigned int i = 0;
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
Sleep(1000);
}
return 0;
}
\\结果:9,8,7,6,5,4,3,2,1,0,4294967295,......
\\00000000 00000000 00000000 00000000 减1
\\得: 11111111 11111111 11111111 11111111
\\即4294967295
练习6
下面这段代码的结果是什么?
#include<stdio.h>
#include<string.h>
int main()
{
char arr[1000];
int i;
for (i = 0; i < 1000; i++)
{
arr[i] = -1 - i;
}
printf("%d", strlen(arr));
return 0;
}
以下是解析:
#include<stdio.h>
#include<string.h>
int main()
{
char arr[1000]; //0-999
int i;
for (i = 0; i < 1000; i++)
{
arr[i] = -1 - i; //char arr数组内储存的值 : -1-2......-128,127(-129),......0 原因如上图
}
printf("%d", strlen(arr));
return 0;
}
char arr数组内储存的值:-1
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf(“hello world\n”);
}
return 0;
}
练习7
下面这段代码的结果是什么?
#include<stdio.h>
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
以下是解析:
#include<stdio.h>
unsigned char i = 0; //unsigned char的范围: 0-255
int main()
{
for (i = 0; i <= 255; i++) //当i=255是,i+1=0,陷入死循环
{
printf("hello world\n");
}
return 0;
}
结果:死循环打印hello world
5.浮点型在内存中的存储
5.1浮点型数据基础知识
①常见的浮点数∶3.14159 ,1E10(即 1*10^10)
②浮点数家族包括:float、double、long double类型。
③浮点数表示的范围 : float.h 中定义,整型家族表示的范围:limits.h 中定义
5.2下面这段代码的结果是什么?
#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和 * pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
详细解读∶
根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数V可以表示成下面的形式:
5.3储存方式:(−1)𝑆∗𝑀∗2E
其中
- (-1) s表示符号位,当s = 0,V为正数﹔当s = 1,V为负数.
即(-1)s =1时,V为正数 ; 即(-1)s =0时,V为负数- M表示有效数字,大于等于1, 小于2(科学计数法)
- 2 E 表示指数位
举例来说:
9.0 —> 1001.0 —>1.002*2^3 —> (-1)^0 * 1.002 * 23(注:此处2x代表小数点位置,此处1,002不是逢十进一而是逢二进一)
(-1)0 * 1.002 * 23
(-1) 𝑆 ∗ 𝑀 ∗2 E
s = 0 ; M = 1.001 ; E = 3
IEEE754规定 : 对于32位的浮点数(float),最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。下面是图解:
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
5.4存储时的特别规定
IEEE754对有效数字M和指数E,还有一些特别规定:
5.4.1对M
前面说过,1 < M < 2,也就是说,M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
因此
IEEE754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
5.4.1对E
至于指数E,情况就比较复杂。
在介绍之前,举几个例子以巩固浮点数计算 : 5.0 , 3.0 , 0.5 , 3.5写出这几个数的二进制储存方式(公式)
过程及结果:101.0 , 11.0 , 0.1(0.5即1/2,即2-1) , 11.1 ----->>> 1.01* 22 , 1.1* 22…
首先,E为一个无符号整数(unsigned int)(为了高效,兼容与准确)这意味着,如果E为8位,它的取值范围为0 - 255;
如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;
对于11位的E,这个中间数是1023。
比如,2 ^ 10的E是10,所以保存成32位浮点数时,必须保存成10 + 127 = 137(类似开氏度与摄氏度的转换),即10001001。
尝试将float f = 5.5;
完全转化为二进制
结果 : 0 10000001 01100000000000000000000(注意,M储存时不足部分向右补0)
然后,指数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的很小的数字。(规定的)
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);