本章次要钻研了计算机中无符号数,补码,浮点数的编码方式,通过钻研数字的理论编码方式,咱们可能理解计算机中不同类型的数据可示意的值的范畴,不同算术运算的属性,能够晓得计算机是如何解决数据溢出的。理解计算机的编码方式,对于咱们写出能够逾越不同机器,不同操作系统和编译器组合的代码具备重要的帮忙。

@[TOC]

信息存储

为什么会有二进制?二进制有什么含意和劣势?

  对于有10个手指的人类来说,应用十进制表示法是很天然的事件,然而当结构存储和解决信息的机器时,二进制值工作得更好。二值信号可能很容易地被示意、存储和传输。例如,能够示意为穿孔卡片上有洞或无洞、导线上的高电压或低电压,或者顺时针或逆时针的磁场。对二值信号进行存储和执行计算的电子电路非常简单和牢靠,制造商可能在一个独自的硅片上集成数百万甚至数十亿个这样的电路。孤立地讲,单个的位不是十分有用。然而,当把位组合在一起,再加上某种解释,即赋予不同的可能位模式以含意,咱们就可能示意任何无限汇合的元素。比方,应用一个二进制数字零碎,咱们可能用位组来编码非正数。通过应用规范的字符码咱们可能对文档中的字母和符号进行编码

计算机的三种编码方式

  无符号:无符号(unsigned)编码基于传统的二进制表示法,示意大于或者等于零的数字。

  补码:补码(two' s-complement)编码是示意有符号整数的最常见的形式,有符号整数就是能够为正或者为负的数字。

  浮点数:浮点数( floating-point)编码是示意实数的迷信记数法的以2为基数的版本。

整数&浮点数

  在计算机中,整数的运算合乎运算符的交换律和结合律,溢出的后果会示意为正数。整数的编码范畴比拟小,然而其后果示意是准确的。

  浮点数的运算是不可联合的,并且其溢出会产生非凡的值——正无穷。浮点数的编码范畴大,然而其后果示意是近似的。

  造成上述不同的起因次要是因为计算机对于整数和浮点数的编码格局不同。

虚拟内存&虚拟地址空间

  大多数计算机应用8位的块,或者字节(byte),作为最小的可寻址的内存单位,而不是拜访内存中独自的位。机器级程序将内存视为一个十分大的字节数组,称为虚拟内存( virtual memory)。内存的每个字节都由一个惟一的数字来标识,称为它的地址(address),所有可能地址的汇合就称为虚拟地址空间( virtual address space)。

  指针是由数据类型和指针值形成的,它的值示意某个对象的地位,而它的类型示意那个地位上所存储对象的类型(比方整数或者浮点数)。C语言中任何一个类型的指针值对应的都是一个虚拟地址。C语言编译器能够依据不同类型的指针值生成不同的机器码来拜访存储在指针所指向地位处的值。然而它生成的理论机器级程序并不蕴含对于数据类型的信息

二进制&十进制&十六进制

二进制转十六进制(分组转换)

  四位二进制能够示意一位十六进制。二进制和十六进制的相互转换方法如下表所示。这里就不开展解说了。

十六进制173A4C
二进制000101110011101001001100
十进制转十六进制

Gamma公式展现 $\Gamma(n) = (n-1)!\quad\forall
n\in\mathbb N$ 是通过 Euler integral

  设x为2的非负整数n次幂时,也就是$x = {2^n}$。咱们能够很容易地将x写成十六进制模式,只有记住x的二进制示意就是1前面跟n个0(比方
$1024 = {2^{10}}$,二进制为10000000000)。十六进制数字0代表4个二进制0。所以,当n示意成i+4j的模式,其中0≤i≤3,咱们能够把x写成结尾的十六进制数字为1(i=0)、2(i=1)、4(i=2)或者8(i=3),前面跟随着j个十六进制的0。比方,$2048 = {2^{11}}$,咱们有n=11=3+4*2,从而失去十六进制示意为0x800。上面再看几个例子。

n${2^{n}}$(十进制)${2^{n}}$(十六进制)
95120x200
19(3+4*4)5242880x80000
14(2+4*2)163840x4000
16(0+4*4)655360x10000
17(1+4*4)1310720x20000
5(1+4*1)320x20
7(3+4*1)1280x80

  十进制转十六进制还能够应用另一种办法:辗转相除法。反过来,十六进制转十进制能够用相应的16的幂乘以每个十六进制数字。

虚拟地址的范畴

  每台计算机都有一个字长( word size),指明指针数据的标称大小( nominal size)。因为虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的零碎参数就是虚拟地址空间的最大大小。也就是说,对于一个字长为w位的机器而言,虚拟地址的范畴为0~${2^{w}}$-1 。程序最多拜访${2^{w}}$个字节。

16位字长机器的地址范畴:0~65535(FFFF)

32位字长机器的地址范畴:0~4294967296(FFFFFFFF,4GB)

64位字长机器的地址范畴:0~18446744073709551616(1999999999999998,16EB)

32位编译指令:gcc -m32 main.c

64位编译指令:gcc -m64 main.c

C语言根本数据类型的典型大小(字节为单位)

有符号无符号32位64位
[signed] charunsigned char11
shortunsigned short22
intunsigned int44
longunsigned long48
int32_tuint32_t44
int64_tuint64_t88
char*48
float44
double88

  留神:根本C数据类型的典型大小调配的字节数是由编译器如何编译所决定的,并不是由机器位数而决定的。本表给出的是32位和64位程序的典型值。

  为了防止因为依赖“典型”大小和不同编译器设置带来的奇怪行为,ISOC99引入了类数据类型,其数据大小是固定的,不随编译器和机器设置而变动。其中就有数据类型int32_t和int64_t,它们别离为4个字节和8个字节。应用确定大小的整数类型是程序员精确控制数据示意的最佳路径。

  对关键字的程序以及包含还是省略可选关键字来说,C语言容许存在多种形式。比方,上面所有的申明都是一个意思:

unsigned long unsigned long int long unsigned long unsigned int

大端&小端

  大端:是指数据的高字节保留在内存的低地址中,而数据的低字节保留在内存的高地址中,这样的存储模式有点儿相似于把数据当作字符串程序解决:地址由小向大减少,而数据从高位往低位放。

  小端:是指数据的高字节保留在内存的高地址中,而数据的低字节保留在内存的低地址中,这种存储模式将地址的高下和数据位权无效地联合起来,高地址局部权值高,低地址局部权值低,和咱们的逻辑办法统一。

  举个例子,假如变量x的类型为int,位于地址0x100处,它的十六进制值为0x01234567。地址范畴0x100~0x103的字节程序依赖于机器的类型。

大端法

地址0x1000x1010x1020x103
数据01234567

小端法

地址0x1000x1010x1020x103
数据67452301
留神,在字0x01234567中,高位字节的十六进制值为0x01,而低位字节值为0x67。

记忆形式:

大端==高尾端,即尾端(67)放在高地址(0x103)。

小端==低尾端,即尾端(67)放在低地址(0x100)。

扩大:大小端有什么意义?

1.不同设施的数据传输

  A设施为小端模式,B设施为大端模式。当通过网络将A设施的数据传输到B设施时,就会呈现问题。(B设施如何转换A设施的数据将在前面章节解说)

2.浏览反汇编代码

  假如Intel x86-64(x86都属于小端)生成某段程序的反汇编码如下:

4004d3:01 05 43 0b 20 00              add   %eax,0x200b43(%rip)

  这条指令是把一个字长的数据加到一个值上,该值的存储地址由0x200b43加上以后程序计数器的值得到,以后程序计数器的值即为下一条将要执行指令的地址。

  咱们习惯的浏览程序为最低位在右边,最高位在左边,0x00200b43。而小端模式生成的反汇编码最低位在左边,最高位在右边,01 05 43 0b 20 00.和咱们的浏览程序正好相同。

3.编写合乎各种零碎的通用程序

/*打印程序对象的字节示意。这段代码应用强制类型转换来躲避类型零碎。很容易定义针对其余数据类型的相似函数*/#include <stdio.h>typedef unsigned char* byte_pointer;/*传递给 show_bytes一个指向它们参数x的指针&x,且这个指针被强制类型转换为“unsigned char*”。这种强制类型转换通知编译器,程序应该把这个指针看成指向一个字节序列,而不是指向一个原始数据类型的对象。*/void show_bytes(byte_pointer start, size_t len){    size-t i;    for (i=0;i<len;i++)        printf("%.2x",start [i]);    printf("\n");}void show_int (int x){show_bytes ((byte_pointer)&x,sizeof(int));}void show_float (float x){    show_bytes ((byte_pointer)&x,sizeof(float));}void show_pointer (void* x){    show_bytes ((byte_pointer)&x,sizeof(void* x));}void test_show_bytes (int val){    int ival = val;    float fval =(float)ival;    int *pval = &ival;    show_int(ival);    show_float(fval);    show_pointer(pval);}

  以上代码打印示例数据对象的字节示意如下表:

机器类型字节(十六进制)
Linux3212345int39 30 00 00
Windows12345int39 30 00 00
Linux6412345int39 30 00 00
Linux3212345.0float00 e4 40 46
Windows12345.0float00 e4 40 46
Linux6412345.0float00 e4 40 46
Linux32&ivalint *e4 f9 ff bf
Windows&ivalint *b4 cc 22 00
Linux64&ivalint *b8 11 e5 ff ff 7f 00 00

  注:Linux64为x86-64处理器。

  除了字节程序以外,int和 float的后果是一样的。指针值与机器类型相干。参数12345的十六进制示意为0x00003039。对于int类型的数据,除了字节程序以外,咱们在所有机器上都失去雷同的后果。此外,指针值却是齐全不同的。不同的机器/操作系统配置应用不同的存储调配规定。( Linux32、 Windows机器应用4字节地址,而 Linux64应用8字节地址)

  能够察看到,只管浮点型和整型数据都是对数值12345编码,然而它们有截然不同的字节模式:整型为0x00003039,而浮点数为0x4640E400。一般而言,这两种格局应用不同的编码方法。

位运算符&逻辑运算符

  位运算符:& | ~ ^。逻辑运算符:&& || !。特地要 ~ 和!的区别,看上面的例子。

表达式后果
!0x410x00
!!0x410x01
!0x000x01
~0x410x3E
~0x000xff

  “!”逻辑非运算符,逻辑操作符个别将其操作数视为条件表达式,返回后果为Bool类型:“!true”示意条件为真(true)。“!false ”示意条件为假(false)。

  "~"位运算符,代表位的取反,对于整形变量,对每一个二进制位进行取反,0变1,1变0。

^为异或运算符,有一个重要的性质:a ^ a = 0,a ^ 0= a。即任何数和其本身异或后果为0,和0异或后果仍为原来的数。利用这个性质,咱们能够找出数组中只呈现一次/两次/三次等的数字。如何找呢?

例1:假如给定一个数组 arr,除了某个元素只呈现一次以外,其余每个元素均呈现两次。找出那个只呈现了一次的元素。

思路:其余元素呈现了都是两次,因而,将数组内的所有元素顺次异或,最初的后果即为只呈现一次的元素。比方,arr = [0,0,1,1,8,8,12],0 ^ 0 ^ 1 ^ 1^ 8^ 8^ 12 = 12。 感兴趣的能够本人编程试下。

例2:给定一个整数数组 arr,其中恰好有两个元素只呈现一次,其余所有元素均呈现两次。 找出只呈现一次的那两个元素。

思路:首先能够通过异或取得两个呈现一次的数字的异或值,该异或值中的为1的bit位必定是来自这两个数字之中的一个。而后能够轻易选一个为1的bit位,依照这个bit位,将所有该位为1的数字分为一组,所有该位为0的数字分为一组,这样就成了查找两个子数组中只呈现了一次的数字。

例3:假如给定一个数组 arr,除了某个元素只呈现一次以外,其余每个元素均呈现了三次。找出那个只呈现了一次的元素。

思路:能够本人思考下。

  逻辑运算符&& 和||还有一个短路求值的性质。具体如下。

  如果对第一个参数求值就能确定表达式的后果,那么逻辑运算符就不会对第二个参数求值。罕用的例子如下

int main(){    int a=3,b=3;    (a=0)&&(b=5);    printf("a=%d,b=%d\n",a,b); // a = 0 ,b = 3    (a=1)||(b=5);    printf("a=%d,b=%d",a,b); // a = 1 ,b = 3} 

  a=0为假所以没有对B进行操作

  a=1为真,所以没有对b进行操作

逻辑左移和算术左移

  逻辑左移(SHL)和算数左移(SAL),规定雷同,左边对立添0

  逻辑右移(SHR),右边对立添0

  算数右移(SAR),右边增加的数和符号无关 (负数补0,正数补1)

  比方一个有符号位的8位二进制数11001101,逻辑右移不论符号位,如果移一位就变成01100110。算术右移要管符号位,右移一位变成11100110。

  e.g:1010101010,其中[]位是增加的数字

  逻辑左移一位:010101010[0]

  算数左移一位:010101010[0]

  逻辑右移一位:[0]101010101

  算数右移一位:[1]101010101

移位符号(<<, >>, >>>)

  <<,有符号左移位,将运算数的二进制整体左移指定位数,低位用0补齐。

  >>,有符号右移位,将运算数的二进制整体右移指定位数,负数高位用0补齐,正数高位用1补齐(放弃正数符号不变)。

扩大:当挪动位数大于理论位数时该怎么办?

  对于一个由w位组成的数据类型,如果要挪动k≥w位会失去什么后果呢?例如,计算上面的表达式会失去什么后果,假如数据类型int为w=32。

int lval=OxFEDCBA98 << 32;int aval=0xFEDCBA98 >> 36;unsigned uval = OxFEDCBA98u >>40;

   C语言规范很小心地躲避了阐明在这种状况下该如何做。在许多机器上,当挪动一个w位的值时,移位指令只思考位移量的低$[{\log _2}w]$位,因而实际上位移量就是通过计算k mod w失去的。例如,当w=32时,下面三个移位运算别离是挪动0、4和8位,失去后果:

lval OxFEDCBA98aval OxFFEDCBA9uvaL OXOOFEDCBA

  不过这种行为对于C程序来说是没有保障的,所以应该放弃位移量小于待移位值的位数。

整数示意

  约定一些术语如下所示

符号类型含意
$[B2{T_w}]$函数二进制转补码
$[B2{U_w}]$函数二进制转无符号数
$[U2{B_w}]$函数无符号数转二进制
$[U2{T_w}]$函数无符号转补码
$[T2{B_w}]$函数补码转二进制
$[T2{U_w}]$函数补码转无符号数
$T{Min_w}$常数最小补码值
$T{Max_w}$常数最大补码值
$U{Max_w}$常数最大无符号数
$+ _w^t$操作补码加法
$+ _w^u$操作无符号数加法
$* _w^t$操作补码乘法
$* _w^u$操作无符号数乘法
$- _w^t$操作补码取反
$- _w^u$操作无符号数取反

无符号数的编码

  无符号数编码的定义:

  对向量$\vec x = [{x_{w - 1}},{x_{w - 2}}, \cdots ,{x_0}]$:$B2{U_w}(\vec x) = \sum\limits_{i = 0}^{w - 1} {{x_i}{2^i}}$。

  其中,$\vec x$看作一个二进制示意的数,每个位${x_i}$取值为0或1。举个例子如下所示。

$B2{U_4}([0001]) = 0*{2^3} + 0*{2^2} + 0*{2^1} + 1*{2^0} = 0 + 0 + 0 + 1 = 1$

$B2{U_4}([1111]) = 1*{2^3} + 1*{2^2} + 1*{2^1} + 1*{2^0} = 8 + 4 + 2 + 1 = 15$

  无符号数能示意的最大值为:$UMa{x_w} = \sum\limits_{i = 0}^{w - 1} {{2^w} - 1}$

补码的编码

  补码编码的定义:

  对向量$\vec x = [{x_{w - 1}},{x_{w - 2}}, \cdots ,{x_0}]$:$B2{T_w}(\vec x) = - {x_{w - 1}}{2^{w - 1}} + \sum\limits_{i = 0}^{w - 2} {{x_i}{2^i}}$。

  最高无效位${x_{w - 1}}$也称为符号位,它的“权重”为$ - {2^{w - 1}}$,是无符号示意中权重的正数。符号位被设置为1时,示意值为负,而当设置为0时,值为非负。举个例子

$B2{T_4}([0001]) = - 0*{2^3} + 0*{2^2} + 0*{2^1} + 1*{2^0} = 0 + 0 + 0 + 1 = 1$

$B2{T_4}([1111]) = - 1*{2^3} + 1*{2^2} + 1*{2^1} + 1*{2^0} = - 8 + 4 + 2 + 1 = - 1$

  w位补码所能示意的范畴:$TMi{n_w} = - {2^{w - 1}}$,$TMa{x_w} = {2^{w - 1}} - 1$。

对于补码须要留神的中央

  第一,补码的范畴是不对称的:$\left| {TMin} \right| = \left| {TMax} \right| + 1$,也就是说,$TMin$没有与之对应的负数。之所以会有这样的不对称性,是因为一半的位模式(符号位设置为1的数)示意正数,而另一半(符号位设置为0的数)示意非正数。因为0是非正数,也就意味着能示意的整数比正数少一个。

  第二,最大的无符号数值刚好比补码的最大值的两倍大一点:$UMa{x_w} = 2TMa{x_w} + 1$。补码示意中所有示意正数的位模式在无符号示意中都变成了负数

  注:补码并不是计算机示意正数的惟一形式,只是大家都采纳了这种形式。计算机也能够用其余形式示意正数,比方原码和反码。有趣味能够持续深刻理解。

确定数据类型的大小

  在不同位长的零碎中,int,double,long等占据的位数不同,其可示意的范畴的大小也不一样,如何编写具备通用性的程序呢?ISO C99规范在文件stdint.h中引入了整数类型类。这个文件定义了一组数据类型,他们的申明模式位:intN_t,uintN_t(N取值个别为8,16,32,64)。比方,uint16_t在任何操作系统中都能够表述一个16位的无符号变量。int32_t示意32位有符号变量。

  同样的,这些数据类型的最大值和最小值由一组宏定义示意,比方INTN_MIN,INTN_MAX和UINTN_MAX。

  打印确定类型的内容时,须要应用宏。

  比方,打印int32_t,uint64_t,能够用如下形式:

printf("x=%"PRId32",y=%"PRIu64"\n",x,y);

  编译为64位程序时,宏PRId32开展成字符串“d”,宏PRIu64则开展成两个字符串“l”“u”。当C预处理器遇到仅用空格(或其余空白字符)分隔的一个字符串常量序列时,就把它们串联起来。因而,下面的 printf调用就变成了:printf("x=%d.y=%lu\n",x,y);

  应用宏能保障:不管代码是如何被编译的,都能生成正确的格局字符串。

无符号数和补码的互相转化

  补码转换为无符号数:

对于满足

举例如下:

x$T2{U_4}(x)$解释
-88-8<0,-8+${2^4}$=16,$T2{U_4}(-8)$=16
-313-3<0,-3+${2^4}$=13,$T2{U_4}(-3)$=16
-214-2<0,-2+${2^4}$=14,$T2{U_4}(-2)$=16
-115-1<0,-1+${2^4}$=15,$T2{U_4}(-1)$=16
00$T2{U_4}(0)$=0
55$T2{U_4}(5)$=5

  无符号数转补码:

  对于满足$0 \le u \le UMa{x_w}$的u有:

  联合上面两张图了解下:

  从补码到无符号数的转换。函数T2U将正数转换为大的负数

  从无符号数到补码的转换。函数U2T把大于${2^{w - 1}} - 1$的数字转换为负值

有符号数与无符号数的转换

  后面提到过,补码并不是计算机示意正数的惟一形式,然而简直所有的计算机都是应用补码来示意正数。因而无符号数转有符号数就是应用函数$U2{T_w}$,而从有符号数转无符号数就是利用函数$T2{U_w}$。

  留神:当执行一个运算时,如果它的一个运算数是有符号的而另一个是无符号的,那么C语言会隐式地将有符号参数强制类型转换为无符号数,并假如这两个数都是非负的,来执行这个运算。

  比方,假如数据类型int示意为32位补码,求表达式-1<0U的值。因为第二个运算数是无符号的,第一个运算数就会被隐式地转换为无符号数,因而表达式就等价于4294967295U<0U,所以表达式的值为0。

扩大数字

  要将一个无符号数转换为一个更大的数据类型,咱们只有简略地在示意的结尾增加0。这种运算被称为零扩大( zero extension)。

  比方,将16位的无符号数12(0xC),扩大为32位为0x0000000C。

  要将一个补码数字转换为一个更大的数据类型,能够执行一个符号扩大( sign exten sion),即扩大符号位。

  比方,将16位的有符号数-25(0x8019,1000000000011001),扩大为32位为0xffff8019。

截断数字

  无符号数截断公式为:$B2{U_k}(x) = B2{U_w}(X)mod{2^k}$等价于$x' = x\bmod {2^k}$,$x'$为其截断k位的后果。

  比方,将9从int转换为short,即须要截断16位,k=16。$9\bmod {2^{16}} = 9$。因而,9从int转换为short的后果为9。

  有符号数的截断公式为:$B2{T_k}(x) = U2{T_k}(B2{U_w}(X)mod{2^k})$等价于$x' = U2{T_k}(x{\kern 1pt} {\kern 1pt} {\kern 1pt} mod{2^k})$,$x'$为其截断k位的后果。

  比方,将53791从int转换为short,即须要截断16位,k=16。$53791\bmod {2^{16}} = 53791$,$U2{T_{16}}(53791) = 53791 - 65536 = - 12345$。因而,53791从int转换为short的后果为-12345。

  无符号数截断的几个例子(将4位数值截断为3位)

原始值截断值
00
22
91
113
157

  有符号数截断的几个例子(将4位数值截断为3位)

原始值截断值
00
22
-71
-53
-1-1

小结

  对于有符号数和无符号数的转换,数字的扩大与截断,常常产生于不同类型不同位长数字的转换,这些操作个别都是由计算机主动实现的,然而咱们最好要晓得计算机是如何实现转换的,这对于咱们查看BUG是特地有用的。这些内容咱们不肯定要都记住,然而当产生谬误时,咱们是要晓得从哪里查看。

整数运算

无符号数加法

  对满足
,失常状况下,x+y的值放弃不变,而溢出状况则是该和减去${2^{\rm{w}}}$的后果。

比方,思考一个4位数字示意(最大值为15),x=9,y=12,和为21,超出了范畴。那么x+y的后果为9+12-15=6。

补码加法

对满足

  当${{2^{w - 1}} \le x + y}$,产生正溢出,当${w + y < - {2^{w - 1}}}$,产生负溢出。当${ - {2^{w - 1}} \le x + y < {2^{w - 1}}}$,失常。具体参考下图。

  举例如下表所示(以4位补码加法为例)

xyx+y$x + _4^ty$状况
-8[1000]-5[1011]-13[10011]3[0011]1
-8[1000]-8[1000]-16[10000]0[0000]1
-8[1000]5[0101]-3[11101]-3[1101]2
2[0010]5[0101]7[00111]7[0111]3
5[0101]5[0101]10[01010]-6[1010]4

补码的非

  对满足$TMi{n_w} \le x \le TMa{x_w}$的x,其补码的非$- _w^tx$由下式给出

(吐槽下CSDN,应用typora写好latex公式,粘贴过去报错,原来CSDN的Markdown是用Katex渲染的。这不是减少工作量吗?)

  也就是说,对w位的补码加法来说,${TMi{n_w}}$是本人的加法的逆,而对其余任何数值x都有-x作为其加法的逆。

无符号数的乘法

  对满足$0 \le x,y \le UMa{x_w}$的x和y有:$x*_w^uy = (x*y)mod{2^w}$。

补码的乘法

  对满足$TMi{n_w} \le {\rm{x}}{\rm{y}} \le TM{\rm{a}}{{\rm{x}}_{\rm{w}}}$的x和y有:$x*_w^ty = U2{T_w}((x*y)mod{2^w})$。

举例,3位数字乘法的后果

模式xyx * y截断的x * y
无符号4 [100]5 [101]20 [010100]4 [100]
补码-4 [100]-3 [101]12 [001100]-4 [100]
无符号2 [010]7 [111]14 [001110]6 [110]
补码2 [010]-1 [111]-2 [111110]-2 [110]
无符号6 [110]6 [110]36 [100100]4 [100]
补码-2 [110]-2 [110]4 [000100]-4 [100]

常数与符号数的乘法

  在大多数机器上,整数乘法指令相当慢,须要10个或者更多的时钟周期,然而其余整数运算(例如加法、减法、位级运算和移位)只须要1个时钟周期。因而,编译器应用了一项重要的优化,试着用移位和加法运算的组合来代替乘以常数因子的乘法。

  因为整数乘法比移位和加法的代价要大得多,许多C语言编译器试图以移位、加法和减法的组合来打消很多整数乘以常数的状况。例如,假如一个程序蕴含表达式x*14。利用$14 = {2^3} + {2^2} + {2^1}$,编译器会将乘法重写为(x<<3)+(x<<2)+(x<1),将一个乘法替换为三个移位和两个加法。无论x是无符号的还是补码,甚至当乘法会导致溢出时,两个计算都会失去一样的后果。(依据整数运算的属性能够证实这一点。)更好的是,编译器还能够利用属性$14 = {2^4} - 1$,将乘法重写为(x<<4)-(x<<1),这时只须要两个移位和一个减法。

  演绎以下,对于某个常数的K的表达式x * K生成代码。咱们能够用上面两种不同模式中的一种来计算这些位对乘积的影响:

  模式A:$(x < < n) + (x < < (n - 1)) + \cdots + (x < < m)$

  模式B:$(x < < (n + 1)) - (x < < m)$

  对于嵌入式开发中,咱们常常应用这种形式来操作寄存器了。在编程中,咱们要习惯应用移位运算来代替乘法运算,能够大大提高代码的效率。

常数与符号数的除法

  在大多数机器上,整数除法要比整数乘法更慢—须要30个或者更多的时钟周期。除以2的幂也能够用移位运算来实现,只不过咱们用的是右移,而不是左移。无符号和补码数别离应用逻辑移位算术移位来达到目标。

无符号数的除法

  对无符号运算应用移位是非常简单的,局部起因是因为无符号数的右移肯定是逻辑右移。同时留神,移位总是舍入到零

  举例如下,以12340的16位示意逻辑右移k位的后果。左端移入的零以粗体示意。

k>>k(二进制)十进制$12340/{2^k}$
000110000001101001234012340.0
1000110000001101061706170.0
40000001100000011771771.25
800000000001100004848.203125
补码的除法(向下舍入)

  对于除以2的幂的补码运算来说,状况要略微简单一些。首先,为了保障正数依然为负,移位要执行的是算术右移

  对于x≥0,变量x的最高无效位为0,所以成果与逻辑右移是一样的。因而,对于非正数来说,算术右移k位与除以${2^k}$是一样的。

  举例如下所示,对-12340的16位示意进行算术右移k位。对于不须要舍入的状况(k=1),后果是$x/{2^k}$。当须要进行舍入时,移位导致后果向下舍入。例如,右移4位将会把-771.25向下舍入为-772。咱们须要调整策略来解决正数x的除法。

k>>k(二进制)十进制$-12340/{2^k}$
01100111111001100-12340-12340.0
11110011111100110-6170-6170.0
41111110011111100-772-771.25
81111111111001111-49-48.203125
补码的除法(向上舍入)

  咱们能够通过在移位之前“偏置( biasing)”这个值,来修改这种不适合的舍入。

  下表阐明在执行算术右移之前加上一个适当的偏置量是如何导致后果正确舍入的。在第3列,咱们给出了-12340加上偏量值之后的后果,低k位(那些会向右移出的位)以斜体示意。咱们能够看到,低k位右边的位可能会加1,也可能不会加1。对于不须要舍入的状况(k=1),加上偏量只影响那些被移掉的位。对于须要舍入的状况,加上偏量导致较高的位加1,所以后果会向零舍入

k偏量-12340+偏量>>k(二进制)十进制$-12340/{2^k}$
0011001111110011001100111111001100-12340-12340.0
1111001111110011011110011111100110-6170-6170.0
41511001111110110111111110011111100-771-771.25
825511010000110010111111111111001111-48-48.203125

总结

  当初咱们看到,除以2的幂能够通过逻辑或者算术右移来实现。这也正是为什么大多数机器上提供这两种类型的右移。可怜的是,这种办法不能推广到除以任意常数。同乘法不同,咱们不能用除以2的幂的除法来示意除以任意常数K的除法。

浮点数

二进制小数

  一种对于二进制的小数编码:$b = \sum\limits_{i = - n}^m {{2^i} \times {b_i}}$。

  二进制小数点向左挪动一位相当于这个数被2除,二进制小数点向右挪动一位相当于将数乘2。

IEEE浮点示意

IEEE浮点规范用$V = {( - 1)^s} \times M \times {2^E}$的模式来示意一个数

  • 符号(sign)s决定这数是正数(s=1)还是负数(s=0),而对于数值0的符号位解释作为非凡状况解决。
  • 尾数( significand)M是一个二进制小数,它的范畴是$1 \sim 2 - \varepsilon $,或者是$0 \sim 1 - \varepsilon $。
  • 阶码( exponent)E的作用是对浮点数加权,这个权重是2的E次幂(可能是正数)。将浮点数的位示意划分为三个字段,别离对这些值进行编码:
  • 一个独自的符号位s间接编码符号s
  • k位的阶码字段$\exp = {e_{k - 1}} \cdots {e_1}{e_0}$编码阶码E。
  • n位小数字段$frac = {f_{n - 1}} \cdots {f_1}{f_0}$编码尾数M,然而编码进去的值也依赖于阶码字段的值是否等于0。

  C语言中的编码方式:

  单精度浮点格局(float) —— s、exp和frac字段别离为1位、k = 8位和n = 23位,失去一个32位示意。

   双精度浮点格局(double) —— s、exp和frac字段别离为1位、k = 11位和n = 52位,失去一个64位示意。

  依据exp的值,被编码的值能够分成三种不同的状况:

   状况1:规格化的值 —— exp的位模式:既不全为0(数值0),也不全为1(单精度数值为255,双精度数值为2047)。

   阶码的值:E = e - Bias(偏置编码法)

  e是无符号数,其位示意为 ${e_{k - 1}} \cdots {e_1}{e_0}$,单精度下取值范畴为1~254.双精度下取值范畴为1 ~ 2047。

   偏置值$Bias = {2^{k - 1}} - 1$,单精度下是127,双精度下是1023。

  因而阶段码E的取值范畴:单精度下是-126 ~ +127。双精度下是-1022 ~ 1024。

e的范畴:1~254

Bias的值:127

E的范畴:-126~127

   尾数的值:M=1+f(隐式编码法,因为有个隐含的1,所以无奈示意0)

   其中$0 \le f \le 1.0$,f的二进制示意为$0.{f_{n - 1}} \cdots {f_1}{f_0}$,也就是二进制小数点在最高无效位的右边。

  因而增加了一个隐含的1,M的取值范畴为$1.0 \le M \le 2.0$。

为什么不在exp域中应用补码编码?为什么采纳偏置编码的模式?

exp域如果为补码编码,比拟两个浮点数既要比拟补码的符号位,又要比拟编码位。

而在exp域中采纳偏置编码,咱们只须要比拟一次无符号数e的值就能够了。

举例:float f = 15213.0

$\begin{array}{l}
{15213_{10}} = {11101101101101_2}\
\quad {\kern 1pt} \;\;\, = {1.1101101101101_2} \times {2^{13}}
\end{array}$

$\begin{array}{l}
M = {1.1101101101101_2}\
frac = {11011011011010000000000_2}
\end{array}$

则:

$\begin{array}{l}
E = 13\
Bias = 127\
\exp = 140 = {10001100_2}
\end{array}$

  状况2:非规格化的值 —— exp的位模式为全0。

  阶码的值:E = 1 - Bias。

  尾数的值:M = f(没有隐含的1,能够示意0)

  非规格化数有两个用处:

  示意数值0 —— 只有尾数M = 0。

   示意十分靠近于0.0的数

  状况3:非凡值 —— exp的位模式为全1。

  当小数域全为0时,失去的值示意无穷,当s = 0时是$+ \infty$,当s=1时是$-\infty$。当小数域为非零0,失去NaN(Not a Number)。

浮点数的运算规定

整数和浮点数相乘

  规定:$x{ + _f}y = Round(x + y)$,$x{ \times _f}y = Round(x \times y)$,其中$Round(x \times y)$要遵循下表的舍入规定。

1.41.61.52.5-1.5
向0舍入1112-1
向负无穷舍入1112-2
向正无穷舍入2223-1
偶数舍入(四舍五入)1222-2
两个浮点数相乘

  两个浮点数相乘规定:$({( - 1)^{s1}} \times M1 \times {2^{E1}}) \times ({( - 1)^{s2}} \times M2 \times {2^{E2}}) = {( - 1)^S} \times M \times {2^E}$

  S:s1^s2

  M:M1+M2

  E:E1+E2

两个浮点数相加

  浮点数相加规定:${( - 1)^{s1}} \times M1 \times {2^{E1}} + {( - 1)^{s2}} \times M2 \times {2^{E2}} = {( - 1)^S} \times M \times {2^E}$

  S和M的值为两个浮点数小数点对齐后相加的后果。

  E:E1 (假如E1>E2)

浮点数的偶数舍入

  例如有效数字超出规定数位的多余数字是1001,它大于超出规定最低位的一半(即0.5),故最低位进1。如果多余数字是0111,它小于最低位的一半,则舍掉多余数字(截断尾数、截尾)即可。对于多余数字是1000、正好是最低位一半的非凡状况,最低位为0则舍掉多余位,最低位为1则进位1、使得最低位仍为0(偶数)。

  留神这里阐明的数位都是指二进制数。

举例:要求保留小数点后3位。

对于1.0011001,舍入解决后为1.010(去掉多余的4位,加0.001)
对于1.0010111,舍入解决后为1.001(去掉多余的4位)
对于1.0011000,舍入解决后为1.010(去掉多余的4位,加0.001,使得最低位为0)

对于1.1001001,舍入解决后为1.101(去掉多余的4位,加0.001)
对于1.1000111,舍入解决后为1.100(去掉多余的4位)
对于1.1001000,舍入解决后为1.100(去掉多余的4位,不加,因为最低位曾经为0)

对于1.01011,舍入解决后为1.011(去掉多余的2位,加0.001)
对于1.01001,舍入解决后为1.010(去掉多余的2位)
对于1.01010,舍入解决后为1.010(去掉多余的2位,不加)

留神

  浮点数的运算不反对结合律。

举例:(1e10+3.14)-1e10=0,3.14+(1e10-1e10)=3.14。因为舍入的起因,第一个表达式会失落3.14。

举例:(1e20 1e20)1e-20 求值为正无穷,而1e20 (1e201e-20) = 1e20。

C语言中的浮点数

在C语言中,当在int、float和 double格局之间进行强制类型转换时,程序扭转数值和位模式的准则如下(假如int是32位的)

  • 从int转换成 float,数字不会溢出,然而可能被舍入。
  • 从int或float转换成 double,因为double有更大的范畴(也就是可示意值的范畴),也有更高的精度(也就是有效位数),所以可能保留准确的数值。
  • 从 double转换成float,因为范畴要小一些,所以值可能溢出成$+ \infty$或$- \infty$。另外,因为精确度较小,它还可能被舍入从float或者 double转换成int,值将会向零舍入。例如,1.999将被转换成1,而-1.999将被转换成-1。进一步来说,值可能会溢出。C语言规范没有对这种状况指定固定的后果。一个从浮点数到整数的转换,如果不能为该浮点数找到一个正当的整数近似值,就会产生这样一个值。因而,表达式(int)+1e10会失去-21483648,即从一个正值变成了一个负值。

举例:int x = ...; float f = ....;double d =... ;

表达式对/错备注
x == (int)(float)xfloat 没有足够的位示意int,转换会造成精度失落
x == (int)(double)x
f ==(float)(double)f
d == (double)(float)dfloat->double精度不够
f == -(-f)
2/3 == 2/3.0
d<0.0 ==> ((d*2)<0.0)
d>f ==> -f >-d
d*d >=0.0
(d+f)-d == f没有结合律

总结

  本章中须要把握的内容次要有:无符号数,补码,有符号数的编码方式,可示意的范畴大小,互相转换的规定,运算规定。浮点数的编码方式理解即可,这部分有点难以了解,如果前面有用到的话再回来细看,然而对于C语言中其余数据类型到浮点数的转换规则是要把握的。

  养成习惯,先赞后看!如果感觉写的不错,欢送关注,点赞,转发,一键三连谢谢!

版权申明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协定,转载请附上原文出处链接和本申明。
本文链接:https://blog.csdn.net/qq_1693...
如遇到排版错乱的问题,能够通过以下链接拜访我的CSDN。

CSDN:CSDN搜寻“嵌入式与Linux那些事”

欢送欢送关注我的公众号:嵌入式与Linux那些事,支付秋招口试面试大礼包(华为小米等大厂面经,嵌入式知识点总结,口试题目,简历模版等)和2000G学习材料。