一文带你深度解剖数据在内存中的存储(和bug郭一起学C系列)
:scissors: 写在前面
经过上篇博客的学习,你已经知道了数据的运算, 那数据在内存中又是如何存储的呢?
今天bug郭就带你一起学习数据在内存中的储存!
@TOC
:100: 本章重点
- 数据类型详细介绍
- 整形在内存中的存储:原码、反码、补码
- 大小端字节序介绍及判断
- 浮点型在内存中的存储解析
:book: 数据类型介绍
那些我们学过的C语言数据类型,你还记得多少,我们一起来整理一一下吧:book:
:eye:内置类型
char //字符型 1byte
int //整型 4byte
short//短整型2byte
long //长整型4/8byte
long long //更长的整型8byte
float //单精度浮点型 4byte
double//双精度浮点型8byte
//C语言中无字符串类型
类型的意义
之前的博客中已经介绍过了
- 类型可以决定该类型的变量在内存中创建内存空间的大小
- 类型可以决定指针访问的权限,加减指针的位移
- …
我们可以根据我们变量的大小合理选择类型,创建空间大小。
不同的数据类型根据它们的字节大小,需要占用不同空间大小的内存空间
类型的基本归类
整型家族
char
signed char
unsigned char
short
signed short [int]
unsigned short [int]
int
signed int
unsigned int
long
signed long [int]
unsigned long [int]
long long
signed long long [int]
unsigned long long [int]
注意:字符型也归类为整型家族,每个类型都有有符号类型和无符号类型。
浮点数家族
float
double
构造类型
//结构体类型
struct
//枚举类型
enum
//联合类型
union
指针类型
char*
int*
float*
void*
空类型
void
void
空类型
通常使用在函数的参数,返回值,指针。
:tm:整形在内存中的存储
我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
那接下来我们来看看整型是如何存储的。
例如:
int a=1;
int b=-3;
我们已经知道整型占用内存空间为4
个字节。那么是如何分配储存的!
我们先来了解一下计算机中有符号数的三种表示方法:
原码,反码,补码
- 计算机中有符号数有三种表示方法,原反补。
- 这三种表示方法,都是由符号位和数值位组成,符号位
1
表示负数,0
表示正数,数值位各不相同!
原码
直接将数据通过二进制正负的形式翻译过来的的二进制位
反码
由原码,符号位不变,数值位按位取反。
补码
反码
+1
得到补码!
正数的原反补相同
==数据是以补码的形式在内存中存储==
为啥是补码呢?
学过计算机原理的同学肯定了解,因为计算机的CPU
中运算器(ALU)只能进行加法!所以负数要转化成加法运算,而补码很好的解决了这个问题!
:heavy_check_mark: 大小端
根据我们之前博客的学习:eye:,避免bug,调试技巧我们已经知道了,调试窗口,可以查看变量的地址和内存,我们&x
可以查看到x
在计算机中内存的储存。
int x=1;
//x为整型有32二级制位
//而每4个二进制位是一个16进制位,
//x=1的16进制表示方法:00 00 00 01
而我们看到vs
下x
的内存,低位01
却存在最左边。
为啥会存到最左边呢?
我们可以看到x
占用4个字节空间,地址从左往右依次递增!低地址存低位字节数据,高地址存高位字节数据。
这就是我们所介绍的小端存储。
而大端存储,不言而喻就是,高地址存低位,低地址存高位!
总结:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,,保存在内存的高地址中。
为啥会有大小端
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为
8bit
。但是在C语言中除了8bit
的char
之外,还有16bit
的short
型,32bit
的long
型(要看具体的编译器),另外,对于位数大于8
位的处理器,例如16
位或者32
位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如一个16bit
的short
型x
,在内存中的地址为0x0010
,x
的值为0x1122
,那么0x11
为高字节,0x22
为低字节。对于大端模式,就将0x11
放在低地址中,即0x0010
中,0x22
放在高地址中,即0x0011
中。小端模式,刚好相反。我们常用的X86
结构是小端模式,而KEIL C51
则为大端模式。很多的ARM
,DSP
都为小端模式。有些ARM
处理器还可以由硬件来选择是大端模式还是小端模式。
总结:
计算机寄存器宽度大于 一个字节,那么就多个字节类型数据的存储就产生了不一样的大小端存储模式。
:old_key:判断大小端
我们已经知道有大小端两种存储模式,而我们要如何判断一台机器是小端存储,还是大端储存呢?也就是判断当前机器的字节序?
我们可以设计几个程序,来验证该不同机器的字节序。
设计思路
我们可以想办法将某一地址处存的字节数据拿出即可判断,如果高地址低字节位,说明是小端存储,否者就是大端存储模式。
//代码一
//利用char*指针得到低地址的字节数据
#include<stdio.h>
int main()
{
int a=1;
int *pa=&a;
//利用char*存储a第一个字节的低地址
char*pc=(char*)pa;
printf("%d",*pc);//访问这个字节的地址,打印数据
return 0;
}
低地址打印了低字节位,说明bug郭的机器是采用小端存储模式!
我们刚刚是说写个程序,判断字节序,所以我们需要封装一下!
//代码1
#include <stdio.h>
int check_sys()
{
int i = 1;
return (*(char *)&i);
}
int main()
{
int ret = check_sys();
if(ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
我们之前还了解到了一个C语言自定义类型联合体,我们后期还会详细介绍!
联合体就是一块空间,多个变量联合使用,共同占用一块空间!当我们访问其中一个变量,该空间就存储着该变量!
我们可以利用联合体这一特性来判断字节序
//代码2
int check_sys()
{
union
{
int i;
char c;
}un;
un.i=1;
return un.c;
}
学会了吗,这就是大小端的判断!
:punch: 小试牛刀
到这里我们已经学习了整型在内存中如何存储,我们来写几个练习巩固一下吧!
练习题目
下面一共7
道题目
大家可以试着练习一下,我会给大家一一讲解
//练习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;
}
//练习2
#include <stdio.h>
int main()
{
char a = -128;
printf("%u\n",a);
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);
//按照补码的形式进行运算,最后格式化成为有符号整数
}
//练习5
#include <stdio.h>
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n",i);
}
return 0;
}
//练习6
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
//练习7
#include <stdio.h>
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
:pushpin: 练习讲解
//练习1
//输出什么?
#include <stdio.h>
int main()
{
char a = -1;
//-1 : 补码 11111111 11111111 11111111 11111111
//截断放入 a: 11111111
signed char b = -1;
//-1截断放入b中 b: 11111111
unsigned char c = -1;
// 同理 c: 11111111
printf("a=%d,b=%d,c=%d", a, b, c);
//a和b是有符号字符型,%d打印整型提升补充符号位后
//补码 11111111 11111111 11111111 11111111
//得到原码:10000000 00000000 00000000 00000001
//所以a和b打印结果是-1
//而c是无符号字符型,所以整型提升,补充0
//00000000 00000000 00000000 11111111
//转换原码 00000000 00000000 00000000 11111111
//所以c的打印结果是 225
return 0;
}
运行结果
//练习2
#include <stdio.h>
int main()
{
char a = -128;
//-128 原码:00000000 00000000 00000000 10000000
//补码: 11111111 11111111 11111111 10000000
//截断存入char a 中 10000000
printf("%u\n", a);
//%u无符号的形式打印
//a是有符号char 整型提升补充符号位
// 11111111 11111111 11111111 10000000
//而%u默认该数据为无符号数据,所以认为a的原码补码相同
//打印结果 4294967168
return 0;
}
运行结果
//练习3
#include <stdio.h>
int main()
{
char a = 128;
//128 00000000 00000000 00000000 10000000
//截断存入char a :10000000
printf("%u\n", a);
//整型提升char a有符号,补符号位
//11111111 11111111 11111111 10000000
//%u无符号的形式打印,认为该数据原码补码相同
//打印 4294967168
return 0;
}
看到练习3
的结果和练习2
的结果一样,一个是-128
,一个是128
但以%u
打印了一样的结果!
因为无论是128
还是-128
截断后存储到a
都是相同的二进制位!
//练习4
#include <stdio.h>
int main()
{
int i = -20;
//-20 原码:10000000 00000000 00000000 00010100
//补码 : 11111111 11111111 11111111 11101100
unsigned int j = 10;
//10 : 00000000 00000000 00000000 00001010
printf("%d\n", i + j);
//i+j补码:11111111 11111111 11111111 11110110
//原码: 10000000 00000000 00000000 00001010
// 打印 -10
//按照补码的形式进行运算,最后格式化成为有符号整数
}
运行结果
//练习5
#include <stdio.h>
int main()
{
unsigned int i;
//无符号int i 所以始终大于等于0
for (i = 9; i >= 0; i--)
{
printf("%u\n", i);
//无法退出循环
}
return 0;
}
unsigned int
范围:0~2^32
代码会发生死循环!
运行结果
//练习6
#include<stdio.h>
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
//a[i]是字符型,范围为-128~127
//超过会进行截断存入a[i]中
//当-129存入a[128]中截断
//-129 :原 10000000 00000000 00000000 10000001
// 补码 11111111 11111111 11111111 01111111
//截断 后存入a[128]中 01111111
//此时a[128]符号位为0 故存入的为 127
//后面数据同理
//当a[255]=-256
// -256 原 10000000 00000000 00000001 00000000
//补码 : 11111111 11111111 11111111 00000000
// 故此时a[255]存入的是 0
}
printf("%d", strlen(a));
//strlen 遇到'\0'停止计数,也就arr[255],所以返回长度为255
return 0;
}
char
中的范围就是这样的,所以但一个数据小于-128
时下一个数据就是127
大于127
下一个数据就是-128
运行结果
//练习7
#include <stdio.h>
unsigned char i = 0;
//unsigned char 范围为0~255
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
//当i=255时,i++后,i循环回到i=0
//所以该代码会发生死循环
}
return 0;
}
运行结果
这就是所以练习的答案了,是不是还有点意犹未尽!如果还没学会可以多看几遍!
:star: 重点归纳总结
- 计算机中数据的存储和计算都是以补码的形式进行的!
- 整型提升还有截断的对象也是针对补码。
- 无符号整型提升,二级制位补充
0
,有符号整型提升,二进制位补充符号位。 %u
(无符号打印)自动认为打印的数据是无符号数据,所以存储的补码也就是原码,%d
(有符号打印)认为打印的数据是有符号类型的,要将数据转换成原码打印输出!
:sweat_drops: 浮点型在内存中的存储
我们已经学会了整型在内存中的存储,你肯定会好奇,浮点型数据该怎样存储在内存中呢?
常见的浮点数:
3.14159 1E10 2.7
浮点数家族包括:
float、double、long double 类型。
浮点数表示的范围:
vs
中float.h
有详细介绍浮点数的表示范围,有兴趣的伙伴可以期查阅一下,bug郭截取了一段供大家参考:
// float.h
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Implementation-defined values commonly used by sophisticated numerical
// (floating point) programs.
//
#pragma once
#ifndef _INC_FLOAT // include guard for 3rd party interop
#define _INC_FLOAT
#include <corecrt.h>
#pragma warning(push)
#pragma warning(disable: _UCRT_DISABLED_WARNINGS)
_UCRT_DISABLE_CLANG_WARNINGS
_CRT_BEGIN_C_HEADER
#ifndef _CRT_MANAGED_FP_DEPRECATE
#ifdef _CRT_MANAGED_FP_NO_DEPRECATE
#define _CRT_MANAGED_FP_DEPRECATE
#else
#ifdef _M_CEE
#define _CRT_MANAGED_FP_DEPRECATE _CRT_DEPRECATE_TEXT("Direct floating point control is not supported or reliable from within managed code. ")
#else
#define _CRT_MANAGED_FP_DEPRECATE
#endif
#endif
#endif
大家肯定会疑问,这是个啥,看不懂啊,其实bug郭也看不懂,哈哈哈,不过问题不大!
浮点型的其类型说明符有
float
单精度说明符,double
双精度说明符。在Turbo C
中单精度型占4
个字节(32位)内存空间,其数值范围为3.4E-38~3.4E+38
,只能提供七
位有效数字。双精度型占8
个字节(64位)内存空间,其数值范围
1.7E-308~1.7E+308
,可提供16
位有效数字。
兄弟们,我们写个代码看看,你就了解了浮点型!
#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;
}
输出结果会是怎么样的?会是像我们整形数据那样分析吗?
结果肯定是否定的!
运行结果:
可以看到打印结果完全出乎我们意料,num
和*pFloat
在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。所以我们可以知道,浮点型数据和整型数据在计算机中有着不一样的存储方式!
:thumbsup: 浮点数存储方式介绍
根据国际标准
IEEE
(电气和电子工程协会)754
,任意一个二进制浮点数V
可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^s
表示符号位,当s=0
,V
为正数;当s=1
,V
为负数。M
表示有效数字,大于等于1
,小于2
。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
。
对于
double
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
位有效数字。
以32
位浮点数为例,留给M
只有23
位,将第一位的1
舍去以后,等于可以保存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从内存中取出还可以再分成三种情况:
- E不全为
0
或不全为1
这时,浮点数就采用下面的规则表示,即指数E
的计算值减去127
(或1023
),得到真实值,再将有效数字M
前加上第一位的1
。 比如:0.5(1/2)
的二进制形式为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
);
好了,关于浮点数的表示规则,就说到这里。
学到这我们了解了浮点数据的存储方式,就可以把刚刚的运行结果解释清楚了!
解析
#include<stdio.h>
int main()
{
int n = 9;
//n 9 00000000 00000000 00000000 00001001
float* pFloat = (float*)&n;
//*pFloat :00000000 00000000 00000000 00001001
//利用存储公式 (-1)^s * M *2^E
//所以 s 为 0为正数
//E 为 00000000 全为0 E无需减去127,M无需加上1.
//M 000000 00000000 00001001
//所以*pFloat 是一个无限接近0的数
printf("n的值为:%d\n", n);
//n的打印结果肯定是9
printf("*pFloat的值为:%f\n", *pFloat);
//所以打印结果为0.000000
*pFloat = 9.0;
//9.0 存入 float中
//9.0: 00001001.0 向左移动3位 00001.001*2^3
//s为0 E为3 3+127=130
// s 0
// E 10000010
// M 001后面添20个0补齐 00100000000000000000000
//所以存入 *pFloat 数据为
// 0 10000010 00100000000000000000000
printf("num的值为:%d\n", n);
//*pFloat解引用操作,将n的值也改变了
//补码 01000001 00010000 00000000 00000000
//转换成10进制 1091567616
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
让我们回到一开始的问题:
为什么0x00000009
还原成浮点数,就成了0.000000
?
首先,将0x00000009
拆分,得到第一位符号位s=0
,后面8
位的指数E=00000000
,最后23
位的有效数字M=000 0000 0000 0000 0000 1001
。
9
->0000 0000 0000 0000 0000 0000 0000 1001
由于指数E全为
0
,所以符合上一节的第二种情况。因此,浮点数V就写成:V=(-1)^0×0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V
是一个很小的接近于0
的正数,所以用十进制小
数表示就是0.000000
。
再看例题的第二部分。
请问浮点数9.0
,如何用二进制表示?还原成十进制又是多少?
首先,浮点数9.0
等于二进制的1001.0
,即1.001×2^3
。
那么,第一位的符号位s=0
,有效数字M
等于001
后面再加20
个0
,凑满23
位,指数E
等于3+127=130
,即10000010
。 所以,写成二进制形式,应该是s+E+M
,即
01000001 00010000 00000000 00000000
这个
32
位的二进制数,还原成十进制,正是1091567616
。
:trophy: 总结
- 浮点数存入时,由于指数有可能是负数,所以统一单精度指数加上
127
,双精度加上1023
存入E
中 - 当
M
有二进制位不足时采用右边补位补0
补齐M
E
全为0
和1
时为特殊情况!
兄弟们看到这里那就收藏一下吧!
- 点赞
- 收藏
- 关注作者
评论(0)