golang中常量左移后的奇妙问题

lazyboy
  • 1.5k
package main

import (
    "fmt"
)

const (
    cimax uint8 = 255
)

func main() {
    var i uint8
    var imax uint8 = 255

    i = imax << 4
    fmt.Printf("%d\n", i)

    //i = cimax << 4 // 为什么这里的常量不能正常左移呢?
    //fmt.Printf("%d\n", i)

    fmt.Printf("%d\n", 0xf0)
}

是不是常量不能作为左移操作法的操作数?

回复
阅读 6.8k
4 个回答
✓ 已被采纳

以下是一些不够严谨的分析。

参照文档进行研究,Go必然有这么几个特点:

  1. 编译型语言,编译器的静态求值是自然的。
  2. 必须使用强制类型转换。甚至于对一个uint16变量赋uint8的值都是不允许的。
  3. 整数字面值不包含存储空间的大小,可以直接赋给各种uintX(必须不溢出)。
  4. <<运算的结果的类型,以左侧操作数为准。

而从表象上来看:

  1. 静态求值的规则是先不顾代码范围,强制求值,之后再塞回目标的存储空间中
  2. 对变量左移的时候,会直接考虑存储空间大小,将左移的数据丢弃

对于第1条,如下的代码都会出现同样的错误。对于这两个例子,因为<<左侧都是uint8,所以最终的结果也是uint8,则得出的结果装不下uint8就会报错,哪怕最后准备了uint16的容器也不行:

const c_i8 uint8 = 255
var i16 uint16
i16 = uint16(c_i8 << 4) // constant 4080 overflows uint8
i16 = uint16(uint8(255) << 4) // constant 4080 overflows uint8

对于第2条,如下的代码都可以正确执行:

var i8 uint8
var i8_2 uint8 = 255
i8 = i8_2 << 4
fmt.Printf("%x\n", i8) // f0
i8_2 <<= 4
fmt.Printf("%x\n", i8_2) // f0

所以并不是说常量不能作为操作数,而是常量左移之后,Go面对溢出的行为并不是截断而是报错。临时对付这个问题,你可能需要先用不定长度的uint类型绕开溢出的限制,再用掩码(& 0xFF)运算去约束结果的范围:

const c_i8 uint8 = 255
var i16 uint16
i16 = uint16(uint(c_i8) << 4 & 0xFF)
fmt.Printf("%x\n", i16)

更深入的分析期待其他回答者的补充。

依云
  • 24.9k
>>> go build a.go
# command-line-arguments
./a.go:18: constant 4080 overflows uint8

这错误很明显了啊,溢出了。你那个变量的类型是 uint8,能存储的最大值是 255。

为什么「变量」没有报溢出的错误呢?因为它是「变量」,编译器在编译时没能确定执行到那里的时候它一定会溢出(说不定你在什么地方改变了它的值所以不会溢出了)。

编译器会保证「常量」不会变化(你不能以正常的方式对它赋值),所以编译的时候它能够把这些地方先计算了,于是它发现你在给一个 uint8 类型的变量赋值一个超过它的最大值的数。

(大概是因为 go 太新了吧。像这种 C 程序,gcc 带优化编译完之后只会剩下输出语句,计算都会提前算了。不过对于变量也不会有警告。)

这里并不是常量不能左移,还是类型溢出的原因,例子中的常量值左移后变成uint32类型的了uint8类型已经表示不了。你尝试一下把常量cimax和变量i声明为uint32就没有问题,或者不改类型,把常量cimax的初始值修改为小于15的值(前4位为0,这样左移4位后不会产生进位)也没有问题。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏