前言:
博客主页: 干脆面la的主页
弄清楚数据再内存中的存储,这篇就够了!
本章节我们将以以下几个重点来深入刨析数据在内存中的存储:
- 数据类型详细介绍
- 整型在内存中的存储:原码、反码、补码
- 大小端字节序介绍与判断
- 浮点数在内存中的存储
目录
1. 数据类型介绍
char 字符整型类型 1字节
short 短整型 2字节
int 整型 4字节
long 长整型 4字节
long long 更长的整型 8字节
float 单精度浮点数 4字节
double 双精度浮点数 8字节
1.1 类型的基本归类
整型家族:
char:
unsigned char
signed char char != signed char
short:
unsigned short [int] ([int]可省略)
signed short [int] short == signed short
int:
unsigned int 无符号整型
signed int 有符号整型(通常省略signed) --> int == signed int
long:
unsigned long [int]
signed long [int] long == signed long
- 每一个char型字符都对应这一个Ascll码值,而Ascll码值是整数,因此归类为整型。
- char到底时signed char还是unsigned char取决于编译器时如何实现的,常见情况下char就是signed char。
- 我们知道无符号整型不能存放负数,如果输入负数也会转化为对应的正数(但不是单纯地去掉符号),为什么会出现这种情况呢?——后面我们介绍整型的原、反、补码时就能解决。
ps:观察下面两张图,心中是否有个疑问:为什么有时负数能被正常打印,有时却不可以呢?同样是整型数据在内存中存储的问题,接下来我们将会了解。
浮点数家族:
float 4字节
double 8字节
构造类型(自定义类型):
>数组类型
>结构体类型 struct
>enum类型 enum
>联合类型 union
顾名思义:构造类型是可以自己创造的类型,如数组类型:我们可以通过改变元素个数和元素类型来改变数组类型:int a[10],实际上int [10]是数组类型,而a是数组名;因此int a[5]的数组类型和int a[10]的数组类型其实是不一样的。
ps:剩下的三种构造类型本文暂时不做展开介绍 struct类型和enum类型在初识C语言 基础篇有简单介绍,如有兴趣欢迎了解~
指针类型:
int* pa
char* pb
float* pc
double* pd
空类型:
void表示空类型(无类型)
通常用于函数的返回类型、函数的参数、指针类型
void/*函数类型*/ test(void/*表示不传参*/) { void* p;//无具体类型指针 } int main() { return 0; }
2. 整型在内存中的存储
问:假设我们定义一个变量int a = 10,这个整型变量是如何放到内存中去的呢?
我们可以用vs2019的调试窗口中的内存来观察。内存中存放的实际上是二进制,但在窗口中用十六进制表示,一个十六进制数可以表示四位二进制数,因此用十六进制展示更加简洁:
2个十六进制数--->8个二进制数--->1个字节
我们知道a分配四个字节的空间,那么该如何存储?
—— 接下来的概念非常重要
2.1 原码、反码、补码
计算机的整数的二进制有三种表示方式,即原码、反码、补码
而内存中存储的是二进制的补码,具体上补码是怎么得到的呢?
下图是一个正数和负数的原码:
正整数: 原码、反码、补码相同
原码 :按照一个数直接写出来的二进制就是原码
负整数:反码 :符号位不变,其他位按位取反(0-->1,1-->0)
补码 :反码的二进制序列+1,就是补码
我们再回到先前提到的10的存放:>
观察下图: 我们发现内存中实际上是按每个字节倒着存放的,这个地方其实就是我们后面要讲到的一个点:内存中存放的补码其实不是直接将补码形式放进去的,而是有一个顺序的问题。
接下来我们再来看 -10的存放:>
我们发现两种都是倒着放的,为什么要这样放呢?
是一会儿我们将提到的“大小端字节序”的问题。
问:为什么要计算机要用补码来存放呢?
原因是计算机中只有加法器:1-1其实就是1+(-1)
下面我们用补码进行计算 我们可以发现结果是完全正确的,并且符号位和数值位可以统一进行处理,这就是补码存储的意义
2.2 大小端介绍
如下图定义一个变量 int a = 0x11223344 从11到44是从高字节数据到低字节数据:
大端字节序存储:当一个数据的低字节数据存放在高地址处,高字节数据存放在低地址处,这种存储方式就叫大端字节序存储
小端字节序存储:当一个数据的低字节数据存放在低地址处,高字节数据存放在高地址处,这种存储方式就叫小端字节序存储
如下图编译器中的存储方式是小端字节序存储,也就是低对低,高对高。
百度2015年系统工程师面试题
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。(10分)
其实思路很简单:定义一个变量int a = 1,那么它转化为16进制的话就是0x00 00 00 01设法拿到四个字节的第一个字节的内容就可以判断,如果第一个字节是0那么就是大端,第一个字节是1那么就是大端。可以用char* p = (char*)&a;对p解引用就可以访问第一个字节。
int main() { int a = 1; char* p = (char*)&a; if(*p == 1) { printf("小端\n"); } else { printf("大端\n"); } return 0; }
想一想:如果用以下代码是否可行?char ch = (char) a;
答:这段代码是不可行的,因为无论大端存储还是小端存储它只会将最低字节数据赋值给ch
上面的代码虽说可以实现目的但不是一种很好的方式:我们应该用一个函数分装起来
int check_sis() { int a = 1; return *(char*)&a;//1就是小端,0就是大端 } int main() { if(1 == check_sis()) { printf("小端"); } else { printf("大端"); } return 0; }
2.3 练习
1.下面代码输出什么?
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; }
解析:上题中a b c均为char型,存储-1时都会从最低字节数据进行截断,在打印的时候又以%d形式输出,会得到整型提升 而在vs中char == signed char,也就是有符号整型,因此a和b进行截断的时候最高位是符号位,整型提升时补符号位1;而unsigned char是无符号整型,因此c对-1进行截断时最高位不是符号位,因此整型提升时补0。
2.int main() { char a = -128; printf("%u\n", a); }
解析:截断和整型提升与上题道理一样,然后此次以%u输出,也就是无符号位,因此11111111 11111111 11111111 10000000就是实际值。
通过计算机换算得出结果为4294967168
运行起来的结果也相同,所以推理正确。
3.
int main() { int i = -10; unsigned int j = 10; printf("%d\n", i + j); return 0; }
4.
int main() { unsigned int i; for (i = 9; i >= 0; i--) { printf("%u\n", i); Sleep(1000); } return 0; }
解析:由于unsigned int为无符号数,里面不会存放负数,当因此i从9开始打印到0之后,再i--就会把-1放到一个无符号的整型里面去,对应的补码也就是11111111 11111111 11111111 11111111而每一位都是有效位,对应的正数4294967295,然后一直死循环下去...
5.
int main() { char a[1000]; int i; for (i = 1; i < 1000; i++) { a[i] = -1 - i; } printf("%d", strlen(a)); return 0; }
解析:首先我们先弄明白一个char型变量的取值范围(如下图)
无符号char:每一位都是真实数值,取值范围就是0~255;
有符号char:第一位(红色框内)为是符号位,从最底下的11111111对应的原码为-1依次向上递减到-127都可以计算出其对应的原码,也就是-1 ~ -127,直到10000000的时候是不需要计算的:只要二进制序列中是符号位为1,后面的都为0的时候(无法先-1后取反得到原码)会直接解析,此时10000000直接被解析为-128,因此取值范围是-128~127。
其实我们可以把char型变量不断+1这个过程看成一个循环(其他类型的变量也是一个道理,大家可以自己推推short类型的取值范围),如下图:
为大家了解完这么一个道理,我们便可以愉快地开始解题啦:
由题可知a[i] = -1 - i;也就是从a[0] = -1,a[1] = -2,a[3] = -3......直到a[127]=-128之后再-1,根据上面的思想a[128] = 127,然后再依次向后-1,直到a[255] = 0,在a[255]前面有255个元素,而strlen(a)返回的数值就是第一次遇到\0之前的元素个数。因此答案就是255。
6.
unsigned char i = 0; int main() { for (i = 0; i <= 255; i++) { printf("hello world\n"); } return 0; }
解析:unsigned char的取值范围是0~255,然而在i++到255的时候,仍然满足i<=255的条件,再++一次,i就循环变成了0,然后陷入死循环。
3. 浮点型在内存中的存储
常见的浮点数:
3.14(字面浮点型)
1E10(科学计数法的形式)
浮点数家族包括:float、double、long double类型
在浮点数后+f:如3.14f为float类型,否则默认为double型
浮点数表示的范围:float.h中定义
3.1 一个浮点数存储的例子
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; }
在不是很了解这块知识的时候,绝大多数人给出的结果是:
(1) 9 (2) 9.000000 (3) 9 (4) 9.000000
ps:用%f或者%lf打印的时候,默认小数点后面有六位
实际上运行的结果是这样的:num和*pFloat在内存中存储的明明是同一个数,为什么解读的差别这么大呢?
我们可以肯定浮点型和整型在内存中的存储一定是有差别的。
接下来我们就要了解:浮点型在内存中到底是怎么存储的呢?
3.2 浮点数存储规则
根据国际标准IEEE(电气和电子工程协会) 754,任意一个浮点数V可以表示成以下形式:
- (-1)^S * M * 2^E
- (-1)^S表示符号位:当S=0,V为正数;当S=1,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表示指数位
1、首先我们要了解一个十进制数该怎么转化为一个二进制数
任何一个十进制数都可以转化为下面的形式,如下图:
2、而101.1又可以把小数点向左移动两位写成科学计数法的形式
解析:这与十进制的数转化为科学计数法形式的道理是一样的,如下图:
3、这时候我们又可以把上述形式写成IEEE 754规定的形式
解析:当我们能够把一个浮点数V转化为以下这种形式,我们只要把S M E三个值存储起来就可以了,这个时候思路似乎就通了:
4、IEEE 754规定:
对于32位(float型)的浮点数最高的1位是符号位S,接着的8位是指数E,剩下的23位是有效数字M
对于64位(double型)的浮点数最高的1位是符号位S,接着的11位是指数E,剩下的52位是有效数字M
- 对于浮点数:没有原码、反码、补码的概念
- 对于符号位S:1表示负数,0表示正数
- 对于有效数字M的修正:前面说过1<=M<2,也就是说可以写成1.xxxxxxxx的形式,其中xxxxxxxx是小数部分,在计算机中保留M的时候1可以被舍去,只保留小数部分(xxxxxxxx)就可以了,意义是:精度更高一位。
- 对于指数E的修正:(1)首先E为一个无符号整型数,如果E是8位,它的取值范围是0~255;如果E是11位,它的取值范围是0~2047;(2)但是,科学计数法中其实是可以出现负数的,所以IEEE 754规定,存入内存是E的真实值必须再加一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023;(3)比如:2^10的E是10,所以保存位32位浮点数时必须为10+127=137,即10001001。
上图:0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移一位变成1.0*2^-1的形式,其E为-1+127=126,表示为01111110;其M为1.0去掉整数部分即为0,补齐0到23位000000000000000000000000;因此其二进制表示形式为:
0 01111110 00000000000000000000000
浮点数就是以这样的方式存入的......
5、E的取出
E从内存中取出又可以分为三种情况:
- E不为全0又不为全1:E的值直接减去127(或1023)
- E为全0:浮点数的指数E=1-127(或1023),并且M不还原隐藏位'1'。
- E为全1:如果M为全0,那么其表示的真值为无限大
接下来我们便可以对前面这道题进行相应的解释了~
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的结果毋庸置疑
第二个输出:以整型存放,以浮点型拿出,拿出数据的时候就会以浮点型的形式解析内存中的数据,因此实际数值非常小,打印出来就是0.000000
第三个输出:由于pFloat时float*定义的指针,因此通过对pFloat解引用来赋值可以以浮点型的数据存入n,再以整型取出,以整形的形式解析内存中的数据也就是1091567616
第四个输出:以浮点型存放,以浮点型拿出,答案是9.000000也毋庸置疑
再来看看结果,是不是很神奇呢?
4. 小结
弄明白了数据在内存中的存储,目的是为了修炼内功,比如当我们在未来写代码的时候,要是遇到自己输出的值并不是自己想要的,我们便可以联想到是不是类型选错了等等...
如果觉得本篇博客对你有所帮助,欢迎三连加关注,我将持续更新相关知识~~~
如有讲解不准确的地方,请轰炸博主
本章完......