通过对内存这一个部分的讲解,对编程会有一个相对深入的认识。数据结构是整个内存的一个重要内容,那么关于数据结构这方面的问题还需要对进制、位运算、编码这三个方面再进行阐述一下。前面说将的数据结构是从逻辑上进行这方面的讲解,现在从原理和过程角度进行阐述。
我们编程其实就是在跟数据进行打交道,计算机每时每刻都在运算(计算这些数据),那么计算机是通过什么方式把大千世界的数据转换成计算机能看得懂的“语言”?通过下面这三方面会对这个过程有更深入的认识。
第一部分:进制
1.1 进制这个概念是从哪里来的?
进制来源于数学中《数论》的一个基本概念,进制也就是进位制度,是人们规定的一种进位方法,表示方法为:
X进制,或者:逢X进一 (X值的是几进制度)。
进制是一种人类社会生产生活的一种基本的、约定俗成的认识。比如早期的英国货币采取十二进制(一英镑等于而是先令,一先令等于十二便士),再比如三尺等于一米,一英尺等于十二英寸等等。为什么历史上会出现这么多不同的进制,这是因为世界各地的生活习惯、文化差异造成的。但是现在我们见到的几乎都是十进制,比如十个一块钱等于十块钱,五百克等于一斤等等。也就是说十进制基本上战胜了各种进制方式,这有可能是人们有十个手指头的原因(不得而知)。
但是,其他的进制形式并不是消亡了,而是在特定领域中广泛的运行,且起到决定性的作用。
在计算机的世界里二进制是组成数据运算的最小单位(0和1),因为这也符合计算机硬件的简单工作原理。元器件的电脉冲信号的开和闭状态就可以清晰的表明这种二进制运算规则,以至于在此基础上实现了广泛的数据运算。正如道家所说“一生二、二生三、三生万物”。计算机根基的运算规则是二进制,但是常用的还有八进制和十六进制等去表示一些常用的位数表述方式。
1.2 十进制(decimal system)
十进制是由10个不同的符号组合表示的:0、1、2、3、4、5、6、7、8、9
2886.32 = 2 × 103 + 8 × 102 + 8 × 101 + 6 × 100 + 3 × 10-1 + 3 × 10-2
其中:10为基底(基数、X进制),多少次方就是权值(位值,理解为当前这个符号在那个位上),权重的个位从0开始,权重的小数位从-1开始
1.3 二进制(binary system)
二进制是由2个不同的符号组合表示的:0、1
110.11 = 1 × 22 + 1 × 21 + 0 × 20 + 1 × 2-1 + 1 × 1-2
其中:2为基底(基数、X进制),多少次方就是权值(位值,理解为当前这个符号在那个位上),权重的个位从0开始,权重的小数位从-1开始
1.4 八进制(octal system)
八进制是由8个不同的符号组合表示的:0、1、2、3、4、5、6、7
365.2 = 3 × 82 + 6 × 81 + 5 × 80 + 2 × 8-1
其中:8为基底(基数、X进制),多少次方就是权值(位值,理解为当前这个符号在那个位上),权重的个位从0开始,权重的小数位从-1开始
1.5 十六进制(hexadecimal system)
十六进制是由16个不同的符号组合表示的:0、1、2、3、4、5、6、7、8、9、A、B、C、D、E、F(其中:A=10 B=11 C=12 D=13 E=14 F=15)
F5.4 = 15 × 161 + 5 × 160 + 4 × 16-1
其中:16为基底(基数、X进制),多少次方就是权值(位值,理解为当前这个符号在那个位上),权重的个位从0开始,权重的小数位从-1开始
1.6 进制的转换
上面分别说明了十进制、二进制、八进制、十六进制的展开式(也就是说是任何进制和十进制之间的换算关系)。那么进制之间是可以相互转换的,这样说把任何的数字通过转化成二进制,然后用元器件脉冲开闭信号不就可以实现计算机的运算了。
再比如:我想从十进制去转换成任何的进制(反求)如何实现,以十进制和二进制进行举例,其他同理。
例1:十进制转二进制(整数除二倒序)
一个十进制数:10
10 ÷ 2 = 5 余数为:0
5 ÷ 2 = 4 余数为:1
4 ÷ 2 = 2 余数为:0
2 ÷ 2 = 1
所以:十进制数字:10 ====>> 二进制数字:1010
(同理把二进制展开就可以求出原先十进制数:1 × 23 + 0 × 22 + 1 × 21 + 0 × 20 = 8 + 0 + 2 + 0 = 10)
例2:十进制转二进制(小数乘二正序)
一个十进制数:10.10
(先求整数部分)
10 ÷ 2 = 5 余数为:0
5 ÷ 2 = 4 余数为:1
4 ÷ 2 = 2 余数为:0
2 ÷ 2 = 1
(再求小数部分)
0.10 × 2 = 0.20 取整为:0
0.20 × 2 = 0.40 取整为:0
0.40 × 2 = 0.80 取整为:0
0.80 × 2 = 1.60 取整为:1
0.60 × 2 = 1.20 取整为:1
0.20 × 2 = 0.40 取整为:0
0.40 × 2 = 0.80 取整为:0
… … ……循环计算下去,如果取尽小数就停止。
所以:十进制数字:10.10 ====>> 二进制数字:1010.0001100
同理把二进制展开就可以求出原先十进制数:
1 × 23 + 0 × 22 + 1 × 21 + 0 × 20 + 0 × 2-1 + 0 × 2-2 + 0 × 2-3 + 1 × 2-4 + 1 × 2-5 + 0 × 2-6 + 0 × 2-7= 8 + 0 + 2 + 0 . 0 + 0 + 0 + 0.06 + 0.03 + 0 + 0= 10.09
为什么10.10有小数的话和原先数值有差距呢?
这就是计算机在计算小数时候的精度问题。随着取整的数值越来越多,精度会越来越高。
例3:用Python去进行进制转换
第一步:打开我们的IDE软件Pycharm
第二步:根据代码进行换算(整数进制转换)
1. bin 二进制转换函数
2. oct 八进制转换函数
3. hex 十六进制转换函数
1 # @author: "Thomas.Shih" 2 # @date: 2018/2/3 0003 3 # -*- coding:utf-8 -*- 4 a = 482666 # 先定义一个变量并赋值一个整数(十进制) 5 6 # 十进制转换成二进制 7 B = bin(a) 8 print(B) 9 # 结果:0b1110101110101101010 (其中0b,b表示二进制,0是无符号) 10 # a = -482666 # 先定义一个变量并赋值一个整数(十进制) 11 # # 十进制转换成二进制 12 # D = bin(a) 13 # print(D) 14 # 结果:-0b1110101110101101010 (其中0b,b表示热进制,0是无符号位,现在就是有符号位-) 15 16 # 十进制转换成八进制 17 O = oct(a) 18 print(O) 19 # 结果:0o1656552(其中0表示八进制) 20 21 # 十进制转换成十六进制 22 H = hex(a) 23 print(H) 24 # 结果:0x75d6a(其中x表示十六进制)
第二部分:位运算
2.1 位操作符
2.1.1 位与
(首先用Python得出a和b的二进制表示来理解位操作符)
1 # -*- coding:utf-8 -*- 2 a = 60 3 b = 13 4 c = 0 5 print(bin(a)) # 对应的二进制数为:0b0011_1100 6 print(bin(b)) # 对应的二进制数为:0b0000_1101 7 print(bin(c)) # 对应的二进制数为:0b0000_0000
(1) “&” ===>> 解释:位与运算 (and 这个表示的逻辑与,概念不一样)
(2) 真值表:1 & 0 = 0、1 & 1 = 1、0 & 0 = 0、0 & 1 =1
(3) 从真值表可以看出:位与操作的特点是,只有1和1位与结果为1,其余全是0
(4) 位与和逻辑与的区别:位与时两个操作数是按照二进制位批次对应位相与的。逻辑与是两个操作数作为整体来相与的。
式子:
0b0011_1100
0b0000_1101
………………….
0b0000_1100
1 c = a & b 2 print(c) 3 print(bin(c)) 4 # 结果0b1100(0b00000_1100)不足位补零
2.1.2 位或
(1) “|” ===>> 解释:位或运算 (or 这个表示的逻辑或,概念不一样)
(2) 真值表:1 | 0 = 1、1 | 1 = 1、0 | 0 = 0、0 | 1 =1
(3) 从真值表可以看出:位或操作的特点是:只有2个0相位或才能够得到0,只要有1个1结果就是1。
(4) 位与和逻辑与的区别:位或时两个操作数是按照二进制彼此对应位相或的,逻辑或是两个操作数作为整体来相或的。
式子:
0b0011_1100
0b0000_1101
………………….
0b0011_1101
1 # 进行位或运算 2 # 0b0011_1100 3 # 0b0000_1101 4 # ........ 5 # 0b0011_1101 6 c = a | b 7 print(c) 8 print(bin(c)) 9 # 结果0b111101(0b0011_1101)不足位补零
2.1.3 位反(位异)
(1) “~” ===>> 解释:位反(异)运算 (not这个表示的逻辑反,概念不一样)
(2) 真值表:~1 = 0、~0 = 1
(3) 从真值表可以看出:位反操作的特点是:将操作数的二进制位逐个按位取反,取反的意思就是1编程0,0变成1。
(4) 位与和逻辑与的区别:位或时两个操作数是按照二进制彼此对应位取反的,逻辑反是假是真,真是假。任何非0的数被按位取反再取反就会得到他自己。
式子:
0b0011_1100
………………….
0b1100_0011
1 # 进行异运算 2 c = ~ a 3 print(c) 4 print(bin(c))
# 结果-0b111101不足位补零
思考:这里为什么按照位取反结果不一样呢?结果还是负数?
2.1.4 位异或
(1) “^” ===>> 解释:异或运算
(2) 真值表:1 ^ 0 = 1、1 ^ 1 = 0、0 ^ 0 = 0、0 ^ 1 =1
(3) 从真值表可以看出:位反操作的特点是:两个数如果相等结果为0,不相等结果为1。
式子:
0b0011_1100
0b0000_1101
………………….
0b0011_0001
1 # 进行异或运算 2 # 0b0011_1100 3 # 0b0000_1101 4 # ........ 5 # 0b0011_0001 6 c = a ^ b 7 print(c) 8 print(bin(c)) 9 # 结果:0b110001不足位补零
2.1.5 左移位和右移位
(1) “<<“、”>>” ===>> 解释:左移动、右移动
(2) 从真值表可以看出:
对于无符号数,左移时右侧补0。
对于无符号数,右移时左侧补0。
对于有符号数,左移时右侧补0(叫做算术移位,相当于逻辑移位)
对于有符号数,右移时左侧补符号位(如果正数就补0,负数就补1,叫算术移位)
(这里不讨论有符号数移位)
1 # 进行左移运算 2 # 0b0011_1100 3 # ........ 4 # 0b1111_0000 5 c = a << 2 6 print(c) 7 print(bin(c)) 8 # 结果:0b11110000不足位补零 9 10 # 进行右移运算 11 # 0b0011_1100 12 # ........ 13 # 0b0000_1111 14 c = a >> 2 15 print(c) 16 print(bin(c)) 17 # 结果 :0b1111不足位补零
第三部分:编码
3.1 什么是编码?
要了解编码首先要了解一下数据的分类,如下图:
数值数据包含:无符号数据和有符号数据;非数值数据包含的非常宽泛。其中对于数值数据中,像内存地址等都不需要符号,但数学运算的时候都需要有符号。那编码又是什么呢?编码就是组织数值数据和非数字数据的规则,通过对不同数据类型按照编码规则最终编译成让计算机识别的二进制代码的过程,这就叫编码。
3.2 有符号数据的表示(signed)
(1) 符号如何处理:
用0、1表示正、负号,放在数值的最高位
比如有这么一个32位的二进制数:
0000_0000_0000_0000_0000_0000_0011_1100 最高位为0表示正
1000_0000_0000_0000_0000_0000_0011_1100 最高位为1表示负
(2) 小数点如何处理
第一种:定点数:约定小数点隐含在某一位置上
第二种:浮点数(float):小数点可以任意浮动,注意:小数点均不占位数。
3.3 原码、反码、补码
在计算机中:有符号数可以表示为原码、反码、补码。
(1) 原码
解释:最高位表示数的符号,其它位表示数值。
例如:以一个8位二进制表示一个正负数
[+7]原码 = 00000111B [-7]原码 = 10000111B
(2) 反码
例如:以一个8位二进制表示一个正负数
正数的反码和原码相同
[+7]反码 = 00000111B = [+7]原码
负数的反码是由其原码的符号位不变,其余位按位取反。
[-7]反码 = 11111000B
(3) 补码
例如:以一个8位二进制表示一个正负数
正数的补码和原码相同
[+7]补码 = 00000111B = [+7]原码
负数的反码是由其原码的符号位不变,其余位按位取反,然后再在最低位加1。
[-7]补码 = 11111001B
(注意!:关于补码最低位加1这个问题容易出现谬误,所谓最低位加1准确的说是逢十进一)
举例1:如果一个数的反码为1111_0010,它的补码为1111_0010
举例2:如果一个数的反码为1111_1001,它的补码为1111_1010
举例3:如果一个数的反码为1111_0111,它的补码为1111_1000
通过上面的例子可以说明,所谓补码就是把末位的1变为0,0变为1,如果遇到0的情况,直接加1,如果遇到末位为1的情况提一位直到遇到0的情况变为1为止。
比如像:反码为:1111_1111,它的补码为:0001_0000_0000
3.4 补码转化原码
已知:一个数的补码,求原码的操作就是对该补码再求补码。
两种情况:
(1) 如果补码的符号位为”0″,表示是一个正数,其原码就是补码。
(2) 如果补码的符号位为”1″,表示时一个负数,其给定的这个补码的补码就是原码。
3.5 之前关于位取反问题的解释
如果有一个数字9,进行位取反操作:~9
计算步骤如下(为了方便最高位采用4位显示方式):
(1) 把9转换成二进制得:0 1001
(2) 计算位取反得:1 0110
(3) 返回显示的十进制数(因为首位为负数的二进制形式都是用补码方式保存的,所以补码的补码变换回原码形式,再显示十进制数值):
*求反得到的二进制数是1 0110(这个数是按照补码方式来存储的,要显示原码首先找到这个数的补码形式)
*1 0110 的补码形式为 : 1 0111
*1 0111作为原码看到,求还原原码 第一,求反:1 1000 ,再求补:1 1001
*最终结果为-9
*然后再减去一个1
*得-10
再举一个例子:如果有一个数字18,进行位取反操作:~18
计算步骤如下:
(1) 把18转换成二进制得:0 0001_0010
(2) 计算位取反得:1 1110_1101
(3) 返回显示的十进制数(因为首位为负数的二进制形式都是用补码方式保存的,所以补码的补码变换回原码形式,再显示十进制数值):
*求反得到的二进制数是1 1110_1101(这个数是按照补码方式来存储的,要显示原码首先找到这个数的补码形式)
*1 1110_1101 的补码形式为 : 1 1110_1110
*1 1110_1110作为原码看到,求还原原码 第一,求反:1 0001_0001 ,再求补:1 0001_0010
*最终结果为-18
*然后再减去一个1
*得-19
为了理解再举一个负数求反的例子:如果有一个数字-18,进行位取反操作:~-18
计算步骤如下:
(1) 把-18转换成二进制得:1 0001_0010
(2) 计算位取反得:0 1110_1101
(3) 返回显示的十进制数(因为首位为负数的二进制形式都是用补码方式保存的,所以补码的补码变换回原码形式,再显示十进制数值):
*求反得到的二进制数是1 1110_1101(这个数是按照补码方式来存储的,要显示原码首先找到这个数的补码形式)
*1 1110_1101 的补码形式为 : 1 1110_1110
*1 1110_1110作为原码看到,求还原原码 第一,求反:0 0001_0001 ,再求补:0 0001_0010
*最终结果为18
*然后再减去一个1
*得17
3.6 前面的几点总结
所谓为什么用补码方式计算负数要在最后减去一个1,是因为用补码方式计算四则运算的时候,像时钟一样多算一个数,所以时针要往回拨一个数字,才是准确的数字。另外用原码补码方式进行四则运算可以参照其他的教材,其原理同上面说的是一样的。
从位取反可以看出,位取反首先是对正负号的转换,正数取反为正数递进一个数取反,负数取反为负数递减一个数取正。
3.7 理解ASCII、Unicode、UTF-8/16
这个部分不需要太深入的了解,如果有这方面需要的知识可以查询相关编码对应的表格
(1) 这些东西为字符编码。最早发明字符的时候,26个英文字母加上其他标点符号只有128个2*8=256.然后加入中文后,把1个字节变成两个字节2*16 = 65536。当然其他语言加入之后,把组成语言的基本要素组织下来,需要更加多编码规则。
(2) ASCII 编码:
一个字母对应一个ASCII码,这就是2*8这种最简单的方式,1个字节来存储。
通过Python的ord函数可以找到数字、字母、符号对应的ASCII码代码
函:ord
用:查看ASCII对应编码
1 ord("A") 2 # 返回值:65 3 ord("a") 4 # 返回值:97 5 ord("+") 6 # 返回值:43 7 ord("1") 8 # 返回值:49
(3) Unicode码:
这种编码也叫万国码,把世界上的各国的语言通过这个编码对应起来,很多国外的游戏跨国家发售的时候,都通过这个万国码进行转码到本国的语言。当然每个国家的编码也有自己的规则,中国的叫”gbk”等。
(4) UTF-8码:
这种编码是现在最广泛使用的一种字符编码。如果程序里面都是英文,容量会翻一倍,所以这种代码叫可变长度的编码方式。比如:字母用UTF编码可以根据长度进行伸缩,中文也可以根据长度进行伸缩(一般字母在UTF8中按ASCII方式进行存储,汉字一般用3位,特别生僻的字可能会用到4位)。
(5) 其他方面:
在90年代的时候还没有Unicode编码,当时只有ASCII码,用一张图,每个字符对应这张图上面的编号位。
用Python去操作查看一下这些编码:
函:encode
用:对数据进行编码,查看编码方式
函:decode
用:对数据进行解码
# encode函数:编码函数 # decode函数:解码函数 name = "范特西" name.encode("utf-8") # 范特西对应的utf-8的编码地址为: # b'\xe8\x8c\x83\xe7\x89\xb9\xe8\xa5\xbf' s = "特斯拉" # s_to_unicode = s.decode("utf-8") unicode_to_gbk = s.encode("gbk") print(s) # print("unicode: ", s_to_unicode) # print("gbk", s_to_gbk) gbk_to_unicode = unicode_to_gbk.decode("gbk") unicode_to_utf8 = gbk_to_unicode.encode("utf-8") print(unicode_to_utf8) print(gbk_to_unicode)