C语言是现在最重要、最流行的编程语言之一,相对于其余程序设计语言,C语言更偏向底层结构化设计,提供对硬件更精准的控制,但也要求使用者更关注指针、位等字节级编程,本文以C语言为载体,研究字节级信息的存储和相关特性,讨论字符串、整数、浮点数的编码方式和相关隐患。不正之处,敬请指教html
众所周知,现代计算机存储和处理的信息以二进制信号表示,即0和1。计算机内都是以0、1bit串存储信息,区分不一样信息的惟一方法是根据解析0、1bit串的上下文。编程
首先,咱们来思考这么个问题:给定一个串0110 0001
,表明什么含义呢?数组
在没有任何上下文的时候,咱们直观的猜想这是个数字,将二进制转换为十六进制为0x61
,转换为十进制则为97
。 若进一步假定计算机在读取串0110 0001
的上下文是解析ASCII码,则串0110 0001
被计算机解释为ASCII码中的字符a
。但若设定计算机在读取串0110 0001
的上下文是unsigned char
掩码,则0110 0001
不表明任何含义,假如计算机在读取该串的上下文是引用一个字长为8bit
的指针,则0110 0001
就表明着地址97
。markdown
单独的bit串并无意义,信息是经过位+上下文承载的,相同的位串在不一样上下文被解释成不一样的信息。app
C语言中的字符串被编码为以null
结尾的字符数组,字符串的每一个字符一般以ASCII字符码进行编码。编程语言
//"123"存储为:0x31 0x32 0x33 0x00
//二进制表示:0011 0001 0011 0010 0011 0011 0000 0000
char *str1 = "123";
//"abc"存储为:0x61 0x62 0x63 0x00
//二进制表示:0110 0001 0110 0010 0110 0011 0000 0000
char *str2 = "abc";
复制代码
字符串的存储方式是不受字节顺序和字长影响的,所以字符串的可移植性是最好的。oop
C语言数字编码体系是真实世界数字体系的近似表示,在某系特性上存在必定的差别。字体
打个比方,C语言用char
类型变量存储[-128,127]
共256个整数,在使用以下表达式时:编码
/*例1*/
char x = -128;
char y = -x;//真实世界y应该是128,可是在C语言中,y仍是-128!
/*例2*/
char x = 100,y=30;
char sum = x+y;//真实世界y应该是130,可是在C语言中,y
是-126!
复制代码
若对C语言数字编码体系不了解,很容易致使程序出现极其隐秘的bug。下面咱们着重讨论C语言是如何表示整数、浮点数。加密
1. 概述
C语言针对整数提供两种编码方式:无符号编码(unsigned)、有符号编码(signed)。
首先,咱们来看一下十进制的正整数表示方式,思考咱们如何解释123
数字。应该为
。
类比到无符号8bit
的二进制0110 0001
,有:
思考若用
16bit
空间存储97
,bit串应该是?
考虑 位的bit串,公式化为(公式1):
其中, 为第 位的bit取值(注意是位,非字节)
基本C数据类型中,一般以1字节、2字节、4字节、8字节做为存储大小,则对应 取值为八、1六、3二、64
2. 取值范围
一个颇有趣的现象,假设用8bit存储无符号,最大取值为 ,思考为啥不是 而是还要减1呢?
其实在给定的8bit中,二进制表示范围为0000 0000
~1111 1111
,那最大的就是全部存储空间的bit取值为1,即1111 1111
。该值为1 0000 0000-1
,按公式1很容易知道1 0000 0000
为2^8
。
聪明的你应该猜得出,给定长度为 的bit串存储无符号整数,最大值都是奇数,为 。
无符号的最小值即所有bit取值为0,所以不管bit串多少位,
C数据类型 | 最小值 | 最大值 |
---|---|---|
unsigned char(8bit) | 0 | 255 |
unsigned short(16bit) | 0 | 65535 |
unsigned int(32bit) | 0 | 4294967295 |
其余无符号的数据类型均可以经过存储空间的bit数肯定取值范围。
1. 概述
相比于无符号数的直观,有符号数显得更加晦涩。由于有符号数引入了补码、减法、非对称性。
1.1 原码
首先,仍是咱们看一下十进制的负整数,好比-123
,咱们就在123
前面加个-
号即表示负整数,咱们是否也可让计算机用-0110 0001
二进制形式表示-97
?
别忘了,咱们计算机只能存储0、1,它不能存储-
号,所以简单粗暴的作法是不行的,必须找一种编码方式,让二进制能够表示负整数。
应该能够很容易知道,那我就用0,1bit串的最高位做为符号位(这种编码称为原码),若最高位为0表明正数,最高位为1表明负数。来看看这种编码方式:
这种编码形式很直观,但人们很快发现原码并不适合计算机编码。
为何呢?由于计算机CPU没有人那般智能,为简化硬件设计的复杂度,CPU只有加法器,没有减法器!这意味着CPU将全部的减法都转化为加法进行运算。咱们尝试在CPU角度利用原码编码方式上计算2-1
:\
0000 0010
;2-1
视为2+(-1)
,则-1
编码为 1000 0001
;2+(-1)
结果为:1000 0011
,即-3!很明显错误了。所以,采用原码做为有符号数的编码不利于CPU简化运算和硬件设计。
1.2 反码
第二种编码方式反码被提出了,这种编码方式将无符号编码中最高位的权重从
转换为
,在char类型中,
取值为8,则
。 好比:
这种编码方式有个特色:正负数之间在位级上正好取反
同时,反码正好能弥补原码在CPU中运算的不足,仍旧以2-1
举例:
0000 0010
;2-1
视为2+(-1)
,将1编码为0000 0001
,则取反后-1
编码为 1111 1110
;2+(-1)
结果为:0000 0010+ 1111 1110 =1 0000 0000
;0000 0001
,即为1
。经过反码的运算能够得出,CPU在只有加法器的条件下,经过配合简单的位级取反和移位操做,就能快速的计算出最终减法结果。这种编码规则能适应底层硬件设计,但你应该很快发现反码取值有一个很奇怪的特性,对数字0有两种编码方式,即:
也就是说反码将编码00000000
解释为+0
,将编码11111111
解释为-0
。这种解释方式和真实事件存在差别,毕竟咱们真实世界不会将0
区分正负。
为了改进该缺点,现代计算机都采用一种称为补码的编码形式,经过将负数空间再减1
,使得编码1111 1111~1000 0000
从 -0~-127
降低到-1~-128
。
现代计算机基本都是使用补码做为有符号整数的编码方式
1.2 补码
补码编码方式将最高位的权重视为 ,注意:反码的最高位权重为 。
借鉴无符号数的公式1,容易推出 位有符号数的编码定位(公式2):
注意:补码编码方式也有个特色,正负数之间在位级上正好取反加1,好比1
和-1
的编码以下:
再来看看2-1
的运算:
0000 0010
;2-1
视为2+(-1)
,将1编码为0000 0001
,则取反加1后-1
编码为 1111 1111
;2+(-1)
结果为:0000 0010+ 1111 1111 =1 0000 0001
;0000 0001
,即为1
。2. 无符号和有符号数的转换
本文有讲述过信息=位+上下文,那对于C语言而言,有符号和无符号数在存储上并没有区别,有的仅仅是对同一位串的不用解析。
公式1对比公式2:
能够从公式中看出,若无符号数转有符号数:
若无符号数转有符号数,则:
好比在强制类型转换中:
/*无符号转有符号*/
unsigned char x = 255;
/*最高位权重从128转变为-128(变化了-256),即255-256=-1*/
char y = (char)x; //y=-1;
复制代码
/*有符号转无符号*/
char x = -128;
/*最高位权重从-128转变为128(变化了+256),即-128+256=128*/
unsigned char y = (unsigned char)x;//x=128
复制代码
有兴趣的同窗能够将8bit
无符号的100
转换为有符号数,将8bit
有符号的100
转换为无符号。
3. 取值范围
一个有趣的事实,使用补码表示有符号数,则会出现不对称性。到底是什么不对称呢?
咱们对比有符号数的正数和负数二进制串表示范围:
正数:0000 0001
~ 0111 1111
负数:1000 0000
~ 1111 1111
发现了吗?不看最高位的状况下,正数表示范围从000 0001
开始,而负数从000 0000
开始。表明负数的范围要比正数表示的范围大1,这就是正负数取值范围的不对称性。
因此8bit有符号的二进制取值范围为:
正数:0000 0001
~ 0111 1111
=> 1
~ 127
负数:1000 0000
~ 1111 1111
=> -128
~ -1
0:0000 0000
综上,8bit有符号取值范围为-128
~ 127
,在此就不罗列二、四、8字节有符号数的取值范围了,原理都同样,请小伙伴自行推导。
本节的最后,提个小小的思考题:
char x = -128;
//y的取值是?
char y = -x;
复制代码
相比于整数的编码方式,浮点数的编码更加精细,计算机对浮点数的编码一般使用IEEE754
标准。
考虑十进制小数的定点表示法,10的负指数次幂表示小数位,例如表示
:
对应的,能够对二进制使用相同的定点表示法,例如用二进制表示 :
这种表示方法很直观,可是在表示很是大的数或很是靠近0的数须要至关多的存储空间,好比表示数字
或
,须要使用至少101个bit的内存空间。
所以,相似十进制表示法,引入科学计数法,经过
形式表示一个数。在这种表示法中,只须要精确的存储
和
就能表示一个数,哪怕这个数很是大或者很是小。好比表示
,只需在存储空间中存储
,
便可。
正式的,IEEE754
标准经过形如
表示一个数,其中:
s
:符号位,表示数值的正负数,参考上述整数的原码表示法M
:尾数位,表示一个二进制小数,即上述中的
,经过frac
字段计算得出E
:阶码位,表示对二进制小数加权,即上述中的
,经过exp
字段计算得出32位浮点数表示:符号位1位,exp
字段8位,frac
字段23位 64位浮点数表示:符号位1位,exp
字段11位,frac
字段52位
IEEE754
标准经过形如
表示一个数,所以想要解析或者编码浮点数,必须计算出s
,M
,E
三个值。根据存储中的exp
不一样取值,对浮点数区分不一样的编码状况,每种编码状况对应不一样的计算方式:
只要肯定下
exp
字段和frac
字段总位数,则肯定浮点数能够表示多少个不一样数字。但能够经过调节exp
字段和frac
字段占用的位数,肯定浮点数的表示范围和密集度。当exp
字段拥有更多位数时,浮点数的表示范围更宽,当frac
字段拥有更多位数时,浮点数表示更加密集
当exp的值不为全0或全1状况下,浮点数为规范化值。对于k
位exp
,n
位frac
,计算方式以下:
E
:B
:32位浮点数
B
值为127,64位浮点数B
值为1023
exp
减去偏置值B
表示有符号的E
:好比在32位浮点数中,内存中存储的exp
为128,则
注意此处用了偏置形式表示有符号
E
,而不用补码形式表示,主要缘由在于偏置形式表示能够直接经过二进制位比较不一样浮点数的大小,而不用作任何补码计算。
经过计算,能够得出32位浮点数
E
取值范围为-126~127
,64位浮点数E
取值范围为-1022~1023
M
:frac
字段视为小数位,例如frac
取值为1101
,则视为小数位后为0.1101
frac
字段加1计算M
:好比frac
取值为1101
,则M
取值为1.1101
该方式能够得到一个额外精度位,由于不管
frac
取值为什么值,均将第一位视为1
当exp的值为全0状况下,浮点数为非规范化值,这种编码表示0或很是接近0的数值。对于k
位exp
,n
位frac
,计算方式以下:
E
:B
:32位浮点数
B
值为-126,64位浮点数B
值为-1022
1
减去偏置值B
表示有符号的E
:好比在32位浮点数中,内存中存储的exp
为0,则
注意此处经过
B
计算E
的方法为: ,目的是和规格化表示相衔接,由于规格化表示最靠近0的取值时,也是
M
:frac
字段视为小数位,例如frac
取值为1101
,则视为小数位后为0.1101
frac
字段得出M
:好比frac
取值为1101
,则M
取值为0.1101
在规格化表示中,
,这种表示方式不管frac
取值为什么值,均将第一位视为1,也就表明没法使M
为0(注意frac
取为无符号表示,不能取值为负数),所以,为了使得M
取值为0,在非规格化中,直接经过小数位frac
字段得出M
:
因为精巧的设计非规格化的
E
与M
计算方式,使得非规格化和规格化表示能够平滑过渡,即最大的非规格化数仅比规格化数小一点
最后是当exp
取值为全1,则当frac
取值为全0时,表示无穷大,当frac
取值不为全0时,表示NaN
,表明不是个数。
浮点数经过
形式表示一个数值,但不是任何数值均可以在有限空间中经过肯定的
形式表示,所以经过IEEE 754
编码方式表示浮点数,避免不了存在溢出状况。分两种状况讨论:
M
没法精确表示数值:好比十进制的0.1
,转化为二进制为0.00011001100[1100]
,括号内的值为无限循环数值,没法经过有限存储空间表示,发生溢出,致使浮点数只能近似表示。
- 当
,即上述讨论的E
没法表示过大或太小的数值:
好比十进制的
,没法经过32
位浮点数表示出来。
本文主要围绕计算机信息的表示,讨论了字符串、无符号整数、有符号整数以及浮点数在字节级别的编码形式。
+0
和-0
问题,同时也知足了硬件加法器的设计要求IEEE 754
标准进行编码,即采用
形式表示一个数字,只须要存储
和
便可表示浮点数,下降极大数或靠近0的数值对存储空间的要求,但也使得存储形式更加复杂。参考文章:
1.深刻理解计算机系统
2.计算机为何要用补码?