操作无符号整数的注意事项

开发 后端
在很多强类型编程语言中都会有一种特殊的类型——无符号整数类型,该数据类型在使用过程中往往稍不留意就会引发出乎意料的bug。

[[408212]]

在很多强类型编程语言中都会有一种特殊的类型——无符号整数类型,该数据类型在使用过程中往往稍不留意就会引发出乎意料的bug。

至于,有什么注意事项以及需要了解的知识点,一起来看看吧。

1. go源码中的数据类型

  1. // go源码位置: src/math/const.go 
  2. // 
  3. // Integer limit values
  4. const ( 
  5.     MaxInt8   = 1<<7 - 1 
  6.     MinInt8   = -1 << 7 
  7.    
  8.     MaxInt16  = 1<<15 - 1 
  9.     MinInt16  = -1 << 15 
  10.    
  11.     MaxInt32  = 1<<31 - 1 
  12.     MinInt32  = -1 << 31 
  13.    
  14.     MaxInt64  = 1<<63 - 1 
  15.     MinInt64  = -1 << 63 
  16.    
  17.     MaxUint8  = 1<<8 - 1 
  18.     MaxUint16 = 1<<16 - 1 
  19.     MaxUint32 = 1<<32 - 1 
  20.     MaxUint64 = 1<<64 - 1 
  21.  
  22.  
  23. // go源码位置: src/builtin/builtin.go 
  24. // 
  25. // uint8 is the set of all unsigned 8-bit integers. 
  26. // Range: 0 through 255. 
  27. type uint8 uint8 
  28.  
  29. // uint16 is the set of all unsigned 16-bit integers. 
  30. // Range: 0 through 65535. 
  31. type uint16 uint16 
  32.  
  33. // uint32 is the set of all unsigned 32-bit integers. 
  34. // Range: 0 through 4294967295. 
  35. type uint32 uint32 
  36.  
  37. // uint64 is the set of all unsigned 64-bit integers. 
  38. // Range: 0 through 18446744073709551615. 
  39. type uint64 uint64 
  40.  
  41. // int8 is the set of all signed 8-bit integers. 
  42. // Range: -128 through 127. 
  43. type int8 int8 
  44.  
  45. // int16 is the set of all signed 16-bit integers. 
  46. // Range: -32768 through 32767. 
  47. type int16 int16 
  48.  
  49. // int32 is the set of all signed 32-bit integers. 
  50. // Range: -2147483648 through 2147483647. 
  51. type int32 int32 
  52.  
  53. // int64 is the set of all signed 64-bit integers. 
  54. // Range: -9223372036854775808 through 9223372036854775807. 
  55. type int64 int64 
  56.  
  57. // float32 is the set of all IEEE-754 32-bit floating-point numbers. 
  58. type float32 float32 
  59.  
  60. // float64 is the set of all IEEE-754 64-bit floating-point numbers. 
  61. type float64 float64 

 从源码中可以看出:

  1. 无符号类型只有正数值域(最小值为0),没有负数值域
  2. 有符号类型有正、负数值域
  3. 无符号类型正数值域数值个数是有符号类型正数值域数值个数的2倍

以 uint8 与 int8 为例,无符号类型 uint8,正数值域 0 ~ 255 共 256个数值。有符号类型 int8,正数值域 0 ~ 127 共 128个数值。

2. 无符号类型与有符号类型的值域

可能你会问:相同长度的无符号、有符号类型,值域为什么是这样的分布?

看过计算机微机原理的同学大概都会略知一二,明白其缘由:

  1. 计算机只认识0、1,每个0、1被称为1个bit (bit是计算机中最小的单位)
  2. 计算机系统中所有的数值以及数据都使用0、1串组合存储
  3. 不同的0、1组合,由于使用不同编码、解码方式才被赋予了不同的含义,进而拥有了各种不同数据类型

 

就拿长度都是8 bit的无符号类型uint8与有符号类型int8来说:

uint8无符号类型的8个bit位都用来表示数值

int8有符号类型的8个bit中,只有后7个bit位用来表示数值。剩余的一个bit用来表示符号位,为0表示正数值,为1表示负数值。

在二进制中,1个bit长度之差造成的表达值域就是2倍

3. 无符号类型与有符号类型的加减法

先看一段代码:

  1. func demo() { 
  2.     var a uint8 = 1 
  3.     var b uint8 = 2 
  4.     v1 := a - b 
  5.     fmt.Println("uint8 1-2=", v1) 
  6.  
  7.     var c int8 = 1 
  8.     var d int8 = 2 
  9.     v2 := c - d 
  10.     fmt.Println("int8 1-2=", v2) 
  11.  
  12.     fmt.Println("---------------"
  13.      
  14.     var e uint8 = math.MaxUint8 
  15.     var f uint8 = 1 
  16.     v3 := e + f 
  17.     fmt.Printf("uint8 255+1=%d  %T\n", v3, v3) 
  18.  
  19.     var g int8 = math.MaxInt8 
  20.     var h int8 = 1 
  21.     v4 := g + h 
  22.     fmt.Printf("int8 127+1=%d %T\n", v4, v4) 

聪明的你,猜下执行结果会是什么?

  1. uint8 - v1 1-2= 255 
  2. int8 - v2 1-2= -1 
  3. --------------- 
  4. uint8 - v3 255+1=0  uint8 
  5. int8 - v4 127+1=-128 int8 

结果分析:

  1. v3、v4在进行相加操作时,由于运算结果超出了对应的数值位长度而发生溢出,导致溢出的数据位无效
  2. v2结果正确
  3. v1不仅是本文重点之一,也会在很多场合中稍有不慎就导致严重bug

在网上看到的一个关于无符号整形减法产生的bug,如下图所示:

4. 关于无符号整形加减法的一些结论

先说一些关于无符号整形加减法的结论:

1.无符号整形进行加法操作时会像其他类型一样,在运算结果超出数值位时发生溢出

2.无符号整形进行减法操作时,运算结果有两种情况

2.1 减数>=被减数,则最终结果大于等于0

2.2 减数<被减数,最终结果也大于等于0

关于无符号整形进行减法比较特殊,减数小于被减数时结果也大于等于0,是不是很意外。

总结一句话概括:无符号类型数值无论加减操作,其结果从不会小于0。

5. 无符号类型数值相减问题

  1. [root@localhost workspace]# cat -n t.go 
  2.      1 package main 
  3.      2 
  4.      3 import"fmt" 
  5.      4 
  6.      5 func main (){ 
  7.      6 var a uint8 = 1 
  8.      7 var b uint8 = 2 
  9.      8      v1 := a - b 
  10.      9 
  11.     10      fmt.Println("uint8 - v1 1-2=", v1) 
  12.     11  } 

 第8行代码执行了两个uint8无符号类型减法操作,得到结果v1。

  1. [root@localhost workspace]# go build -gcflags="-N -l -S" t.go 
  2. # command-line-arguments 
  3. "".main STEXT size=222 args=0x0 locals=0x80 funcid=0x0 
  4.     0x000000000 (/root/workspace/t.go:5)    TEXT    "".main(SB), ABIInternal, $128-0 
  5.     ...... 
  6.     0x002b00043 (/root/workspace/t.go:8)    MOVBLZX "".a+54(SP), AX 
  7.     0x003000048 (/root/workspace/t.go:8)    ADDL    $-2, AX // !!! 加 -2 
  8.     0x003300051 (/root/workspace/t.go:8)    MOVB    AL, "".v1+52(SP) 
  9.     0x003700055 (/root/workspace/t.go:10)   MOVB    AL, ""..autotmp_3+55(SP) 

首先,要说明一点:在计算机中没有减法,只有加法操作(出乎你的意料)。

通过汇编代码可以看出 a-b 被转换成了a + (-b),即 1-2 = 1+(-2)。

按理说1-2应该等于-1才对,这其中又发生了什么呢?

6. 负数的表达形式——补码

前面说过,计算机只认识二进制的0、1,2属于10进制的数值,10进制则可以看做是一种计算机上的编解码规则。那么,其对应的二进制又是什么呢?

  1. func demo2() { 
  2.     var a, b, c uint8 
  3.     a = 1 
  4.     b = 2 
  5.     c = a + (-b) 
  6.  
  7.     fmt.Printf( 
  8.         "a的二进制为:%08b \n"
  9.         "b的二进制为:%08b \n"
  10.         "-b的二进制为:%08b \n"
  11.         "c的二进制为:%08b \n"
  12.         "c的十进制为:%d", a, b, -b, c, c, 
  13.     ) 

 执行结果为:

  1. a的二进制为:00000001 
  2. b的二进制为:00000010 
  3. -b的二进制为:11111110 
  4. c的二进制为:11111111 
  5. c的十进制为:255 

在计算机系统里面,数值有三种编码:原码、反码、补码。

  1. 反码、补码一般用于负数,反码=负数对应正数的原码取反,补码=反码+1
  2. 正数的原码、反码、补码一样
  3. 负数分两种情况:3.1 对于有符号类型:负数的数值位使用补码表示,同时设置符号位为13.2 对于无符号类型:负数的数值位使用补码表示,由于无符号位,故无需设置符号位

由上文若干规则可知:

  1. uint8 类型的 -b 对应的二进制为 11111110,uint8 类型 a 对应二进制为00000001
  2. c=a+(-b),则对应bit位相加为11111111
  3. 同类型相加结果还为同一类型,所以c仍然为uint8
  4. 由于无符号类型的值域不存在负数域,所以11111111转换为十进制为255

7. 一些疑惑

你是否跟我一样,存在一些疑惑?

无符号类型可以赋值为负数吗?

你可能会问:无符号类型既然永远不为负数,那么可以赋值为负数吗?

  1. func demo3() { 
  2.     var a uint8 
  3.     a = -2 
  4.  
  5.     fmt.Println(a) 

执行结果:

  1. # command-line-arguments 
  2. ./main.go:59:4: constant -2 overflows uint8 

通过报错信息可知,是无法给无符号类型赋值负数的。

无符号类型不可以赋值负数,为什么可以进行取负操作?

既然无符号类型不可以赋值为负数,为什么无符号类型可以取负操作?

  1. func demo3() { 
  2.     var a uint8 
  3.     a = 2 
  4.  
  5.     fmt.Println(-a) 

可能你又会问:-a需要跟a类型一致才对,-a不能表示为无符号类型,为什么没报错呢?

  1. [root@localhost workspace]# cat -n t.go 
  2.      1 package main 
  3.      2 
  4.      3 import"fmt" 
  5.      4 
  6.      5 func main (){ 
  7.      6 var a1 uint8 
  8.      7      a1 = 2 
  9.      8 
  10.      9      fmt.Printf("%b\n", -a1) 
  11.     10  } 
  12.  
  13.  
  14. [root@localhost workspace]# go build -gcflags="-N -l -S" t.go 
  15. # command-line-arguments 
  16. "".main STEXT size=197 args=0x0 locals=0x80 funcid=0x0 
  17.     0x000000000 (/root/workspace/t.go:5)    TEXT    "".main(SB), ABIInternal, $128-0 
  18.     ...... 
  19.     0x002b00043 (/root/workspace/t.go:9)    MOVB    $-2, ""..autotmp_1+71(SP) 
  20.     0x003000048 (/root/workspace/t.go:9)    XORPS   X0, X0 
  21.     0x003300051 (/root/workspace/t.go:9)    MOVUPS  X0, ""..autotmp_2+80(SP) 
  22.     0x003800056 (/root/workspace/t.go:9)    LEAQ    ""..autotmp_2+80(SP), AX 
  23.     0x003d00061 (/root/workspace/t.go:9)    MOVQ    AX, ""..autotmp_4+72(SP) 
  24.     0x004200066 (/root/workspace/t.go:9)    TESTB   AL, (AX) 
  25.     0x004400068 (/root/workspace/t.go:9)    MOVBLZX ""..autotmp_1+71(SP), CX 
  26.     0x004900073 (/root/workspace/t.go:9)    LEAQ    type.uint8(SB), DX //!!! type.uint8对-2进行类型转换 
  27.     0x005000080 (/root/workspace/t.go:9)    MOVQ    DX, ""..autotmp_2+80(SP) 
  28.     0x005500085 (/root/workspace/t.go:9)    LEAQ    runtime.staticuint64s(SB), DX 
  29.     0x005c00092 (/root/workspace/t.go:9)    LEAQ    (DX)(CX*8), CX 
  30.     0x006000096 (/root/workspace/t.go:9)    MOVQ    CX, ""..autotmp_2+88(SP) 
  31.     0x006500101 (/root/workspace/t.go:9)    TESTB   AL, (AX) 
  32.     0x006700103 (/root/workspace/t.go:9)    JMP 105 
  33.     0x006900105 (/root/workspace/t.go:9)    MOVQ    AX, ""..autotmp_3+96(SP) 
  34.     0x006e00110 (/root/workspace/t.go:9)    MOVQ    $1, ""..autotmp_3+104(SP) 
  35.     0x007700119 (/root/workspace/t.go:9)    MOVQ    $1, ""..autotmp_3+112(SP) 
  36.     0x008000128 (/root/workspace/t.go:9)    LEAQ    go.string."%b\n"(SB), CX 
  37.     0x008700135 (/root/workspace/t.go:9)    MOVQ    CX, (SP) 
  38.     0x008b00139 (/root/workspace/t.go:9)    MOVQ    $3, 8(SP) 
  39.     0x009400148 (/root/workspace/t.go:9)    MOVQ    AX, 16(SP) 
  40.     0x009900153 (/root/workspace/t.go:9)    MOVQ    $1, 24(SP) 
  41.     0x00a200162 (/root/workspace/t.go:9)    MOVQ    $1, 32(SP) 
  42.     0x00ab00171 (/root/workspace/t.go:9)    PCDATA  $1, $0 
  43.     0x00ab00171 (/root/workspace/t.go:9)    CALL    fmt.Printf(SB) 
  44.     ...... 

通过汇编可以看到,通过type.uint8(SB), DX对运算结果进行了类型转换。

因此,我们可以得出结论:-a是a与负号(-)的一种运算,运算结果的最终类型会被转换为与a一致。

总结

本文通过若干示例,展示了无符号类型与有符号类型的差别和注意事项。

那么,什么时候用无符号类型,什么时候用有符号类型呢?

  1. 运算结果期待包含负数,则不能用无符号类型,此时最好使用有符号类型
  2. 运算结果不需要包含负数,并且希望类型的正数值域足够大,此时最好使用无符号类型
  3. 能不用无符号类型就少用无符号类型,减少bug产生!!!

其他特殊场景,如:在go语言runtime中GPM的逻辑处理器P结构上,P存储goroutine的本地队列头尾位置使用了无符号类型。

  1. type p struct { 
  2.     ...... 
  3.     // Queue of runnable goroutines. Accessed without lock. 
  4.     runqhead uint32// 本地运行队列 头位置 
  5.     runqtail uint32// 本地运行队列 尾位置 
  6.     runq     [256]guintptr // 每个P可以有256个G 
  7.   ...... 
  8.  
  9. // runqput tries to put g on the local runnable queue. 
  10. // If next if false, runqput adds g to the tail of the runnable queue. 
  11. // If next is true, runqput puts g in the _p_.runnext slot. 
  12. // If the run queue is full, runnext puts g on the global queue. 
  13. // Executed only by the owner P. 
  14. // runqput把G放到p里。如果nexttrue,就放到下一个。否则就追加到队尾。如果队列满了,就放到全局队列。 
  15. func runqput(_p_ *p, gp *g, next bool) { 
  16.     ...... 
  17.     h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers 
  18.     t := _p_.runqtail 
  19.    
  20.     // 放本地队列 
  21.     ift-h < uint32(len(_p_.runq)) { 
  22.         _p_.runq[t%uint32(len(_p_.runq))].set(gp) 
  23.         atomic.Store(&_p_.runqtail, t+1) // store-release, makes the item available for consumption 
  24.         return 
  25.     } 
  26.     ...... 

由于t、h的数值是一直在进行+1操作,会超过uint32的最大表示范围。

思考当 t、h溢出之后会怎么样?会有问题吗?

 

责任编辑:姜华 来源: 今日头条
相关推荐

2011-05-03 16:58:55

喷墨打印机墨水

2009-10-30 10:05:48

双线接入

2011-05-26 11:22:04

SEO

2010-08-17 16:29:03

UPS旁路

2011-07-21 14:28:17

MySQL事务事务保存点

2009-07-28 10:26:30

ASP.NET操作Ex

2009-12-15 17:47:17

VSIP

2020-10-20 14:05:48

用户需求分析IT

2011-09-26 11:02:10

2010-11-26 16:27:01

MySQL使用变量

2023-01-14 09:49:11

2021-11-16 10:35:59

云计算云计算环境云应用

2010-04-15 11:32:54

Unix操作系统

2010-06-13 15:52:36

MySQL 复制设置

2010-02-05 14:13:17

Android平台

2010-07-23 10:09:41

SQL Server

2011-04-11 16:23:57

2010-05-07 10:19:48

Oracle 注意事项

2010-07-29 10:27:30

Flex键盘事件

2009-12-16 15:41:10

Ruby on Rai
点赞
收藏

51CTO技术栈公众号