Lua 位和字节

一、位运算

从 Lua 5.3 版本开始,提供了针对数值类型的一组标准位运算符,与算数运算符不同的是,运算符只能用于整型数。

运算符描述
&按位与
|按位或
按位异或
>>逻辑右移
<<逻辑左移
~(一元运算)按位取反

位的使用,可以参考小盆友的另一篇文章 《android位运算简单讲解》 blog.csdn.net/weixin_3762…

print(string.format("%x", 0xff & 0xabcd))       --> cd
print(string.format("%x", 0xff | 0xabcd))       --> abff
print(string.format("%x", 0xff ~ 0xabcd))       --> ab32
print(string.format("%x", ~0))                  --> ffffffffffffffff (16 个 6,每一个十六进制 4 位,刚好是 64 位)
print(string.format("%x", 0xff << 12))          --> ff000
print(string.format("%x", 0xff >> -12))         --> ff000
-- 移位数等于或大于整型表示的位数,由于所有的位都被移出,则结果为 0
print(string.format("%x", -1 << 80))            --> 0

1-1、注意小点

所有的位运算针对一个整数型的所有位。

Lua 的两个移位操作都会用 0 填充空出的位,这种行为称为逻辑移位。 Lua 中没有提供算术右移(即使用符号位填充空出的位),但是可以通过向下取整除法( floor 除法)达到算数右移

local data = -0x100
print("逻辑右移:", string.format("%x >> 1 --> %x", data, data >> 1))    --> 逻辑右移:	ffffffffffffff00 >> 1 --> 7fffffffffffff80
-- 达到算数右移一位,2^n ( n 即为右移位数)
print("算数右移:", string.format("%x >> 1 --> %x", data, data // 2))    --> 算数右移:	ffffffffffffff00 >> 1 --> ffffffffffffff80

移位数是负数则表示向相反方向移位。a>>na<<-n 相等。

二、无符号整型数

在有符号整型数中使用一个比特位来存储符号位。所以 64 的整型数中最大可以表示为 2^63-1 ,而不是 2^64-1 。

值得注意的是 Lua 语言不显示支持无符号整型数, 但这并不妨碍我们在 Lua 中使用无符号整型这一特性,只是在使用过程中需要注意一些细节。

2-1、细节一:打印无符号整数

对于一个数最直观的就是展示出来,所以我们可以使用 string.format 进行格式化数值, 使用 %u(无符号整数)或 %X (十六进制)进行展示,这样就能很直观的感知无符号整数每一位的数值是多少。如果直接将无符号整数打印,会被认为是有符号整数,就不利于阅读。

local x = 3 << 62
print("有符号整数显示:", x)        --> 有符号整数显示:	-4611686018427387904
print("使用十进制无符号显示", string.format("%u", x))       --> 使用十进制无符号显示	13835058055282163712
print("使用十六进制无符号显示", string.format("0x%X", x))   --> 使用十六进制无符号显示	0xC000000000000000

2-2、细节二:无符号整数,加减乘运算

对于无符号整数的加减乘运算,和有符号是一样的,只是要注意溢出

local x = 3 << 62
print("使用十进制无符号显示", string.format("%u", x))                   --> 使用十进制无符号显示	13835058055282163712
print("使用十进制无符号显示 +1", string.format("%u", x + 1))            --> 使用十进制无符号显示 +1	13835058055282163713
print("使用十进制无符号显示 -1", string.format("%u", x - 1))            --> 使用十进制无符号显示 -1	13835058055282163711
x = 1 << 62
print("使用十六进制无符号显示", string.format("0x%X", x))               --> 使用十六进制无符号显示	    0x4000000000000000
print("使用十进制无符号显示 * 2", string.format("%X", x * 2))           --> 使用十进制无符号显示 * 2	0x8000000000000000

2-3、细节三:无符号整数,除法运算

对于无符号整数的除法会有些不一样,需要注意其符号位的影响,可以通过下面函数进行无符号整数的除法(细节也在每一行的注释中)

function udiv(n, d)
    -- d<0 实质比较除数是否大于 2^63
    if d < 0 then
        -- 如果除数大于被除数(n<d),则商为 0 ;否则为 1
        if math.ult(n, d) then
            return 0
        else
            return 1
        end
    end

    -- n >> 1 等价于将无符号整数 n / 2 , 这样的做法是先去除符号位置的影响
    -- 最后 << 1 等价于 * 2 ,这样是为了纠正一开始 >> 1
    -- // d 进行有符号整数的正常向下取整除法
    local q = ((n >> 1) // d) << 1
    -- 计算因为移位和向下取整导致的丢失数值,计算出两个结果后,如果偏差大于除数,说明需要加一
    local r = n - q * d
    if not math.ult(r, d) then
        q = q + 1
    end
    return q
end

local u1 = 1 << 1
-- 正常数字除法
local div1 = 2
print(string.format("%X/%X = %X", u1, div1, udiv(u1, div1)))        --> 2/2 = 1
-- (符号位为 1 )很大的数进行无符号除法
local u2 = 1 << 63
print(string.format("%X/%X = %X", u2, div1, udiv(u2, div1)))        --> 8000000000000000/2 = 4000000000000000 
-- (符号位为 0 )正常的数进行无符号除法
local u3 = 1 << 62
print(string.format("%X/%X = %X", u3, div1, udiv(u3, div1)))        --> 4000000000000000/2 = 2000000000000000
-- 除数很大,符号为为 1 ,被除数 < 除数
local div2 = 1 << 63
print(string.format("%X/%X = %X", u3, div2, udiv(u3, div2)))        --> 4000000000000000/8000000000000000 = 0
-- 被除数 > 除数
local u4 = (1 << 63) + 1
print(string.format("%X/%X = %X", u4, div2, udiv(u4, div2)))        --> 8000000000000001/8000000000000000 = 1
local u5 = (1 << 63) + 3
print(string.format("%X/%X = %X", u5, div1, udiv(u5, div1)))        --> 8000000000000003/2 = 4000000000000001

2-4、细节四:无符号整数比较

如果直接将无符号整数进行比较,则会导致一种情况:因为第 64 位在有符号整数是符号位,而无符号整数则用于正常表示数值,所以当第 64 位为 1 时,则会导致在直接比较时(此时被当作是有符号整数)无符号越大的值反而越小。

解决此类方法有两种方式:

第一种,使用 math.ult 进行比较无符号整数

local n1 = 0x7fffffffffffffff
local n2 = 0x8000000000000000
print(n1, n2, n1 < n2)              --> 9223372036854775807	-9223372036854775808	false
print(n1, n2, math.ult(n1, n2))     --> 9223372036854775807	-9223372036854775808	true

第二种,在进行有符号比较前先用掩码去两个操作数的符号位

local n3 = -10
local n4 = 10
local mask = 0x8000000000000000
print("有符号:", n3, "<", n4, n3 < n4)                     --> 有符号:	-10<10	true
print("无符号:", n4, "<", n3, math.ult(n4, n3))            --> 无符号:	10<-10	true
print("无符号:", n4, "<", n3, (n4 ~ mask) < (n3 ~ mask))   --> 无符号:	10<-10	true

2-5、细节五:整型数和浮点数互转

整型转浮点数

local u = 0xC000000000000000
print(math.type(u), string.format("%X", u))         --> integer	C000000000000000
-- + 0.0 是为将 u 转为 float , % 取余的规则符合通用规则,只要其中有一个为浮点数,结果则为浮点数
-- %(2 ^ 64) 是为将结果约束在这其中,否则显示时会被认为是有符号
local f = (u + 0.0) % 2 ^ 64
print(math.type(f), string.format("%.0f", f))       --> float	13835058055282163712
local f1 = (u + 0.0)
print(math.type(f1), string.format("%.0f", f1))     --> float	-4611686018427387904

浮点数转整型

function utointerger(value)
    if math.type(value) == "integer" then
        return value
    end
    -- f + 2 ^ 63 是为了让数转为一个大于 2 ^ 64 的数
    -- % (2 ^ 64) 是为了让数值限制在 [0, 2 ^ 64) 范围
    -- - 2 ^ 63 是为了把结果改为一个"负值"(最高位为 1 )
    -- 对于小于 2 ^ 63 的数:
    -- 其实没有什么特殊的,加完一个数值(2 ^ 63)之后有减掉了,所以没有什么特殊
    local result = math.tointeger(((value + 2 ^ 63) % (2 ^ 64)) - 2 ^ 63)
    return result + (math.tointeger(value - result) or 0)
end

local f = 0xF000000000000000.0
local u = utointerger(f)
print(math.type(u), string.format("%X", u))     --> integer	F000000000000000

值得注意:

因为这里是一个浮点数的计算,在我们之前的分享浮点数的文章中,有讲到 在 [-2^53, 2^53] 范围内,只需要将 整型数值 + 0.0 则可以进行转换为 浮点数,如果超出范围,会导致精度丢失,取近似值。

所以上述代码的 f 值如果超出这一范围,低位数可能会有问题

local f1 = 0x8000000000000001.0
local u1 = utointerger(f1)
print(math.type(u1), string.format("%X", u1))       --> integer	8000000000000000

三、二进制数据

Lua 从 5.3 开始提供了对二进制数和基本类型值之间进行转换的函数。

string.pack 会把值 “打包” 为二进制字符串

string.unpack 则从字符串中提取二进制

3-1、string.pack(fmt, v1, v2, ...)

按照 fmt 格式将 v1、v2 进行打包为二进制字符串

参数:

  • fmt:打包格式
  • v1、v2:需要打包的数据

返回值:

字符串类型,即打包后的二进制数据

local format = "iii"
local s = string.pack(format, 3, -27, 450)
print(s, #s)        --> �����	12

3-2、string.unpack(fmt, s, pos)

按照 fmt 格式对字符串 s 偏移了 pos 个位置后,进行解析

参数:

  • fmt:解析格式
  • s:需要解析的字符串
  • pos:可选,从字符串 s 的第 pos 个位置开始解析,默认为 1

返回值:

会有两个返回值,第一个为解析后的内容,第二个为解析的字符串 s 中最后一个读取的元素在字符串中的位置。

local format = "iii"
local s = string.pack(format, 3, -27, 450)  --> �����	12
-- 最后的 13 即,最后一个读取的元素位置
print(string.unpack(format, s))             --> 3	-27	450	13

3-3、整型数格式可选参数

每种选项对应一种类型大小

格式描述
bchar
hshort
iint(可以跟一个数字,表示多少字节的整型数)
llong
jLua 语言中的整型数大小

3-3-1、固定字节

在使用 i 的时候,大小会与机器有关,如果想要达到固定大小,可以考虑在后面加上数字(1~16),类似 i8,则会产生 8 个字节的整型数。

local n1 = 1 << 54
print(string.format("%X", n1))          --> 40000000000000
local x1 = string.pack("i8", n1)
print(#x1, string.format("%X", string.unpack("i8", x1)))    --> 8	40000000000000

3-3-2、打包和解包都会检测溢出

打包的时候,如果发现字节不够装数值,则会抛出异常 bad argument #2 to 'pack' (integer overflow)

local x2 = string.pack("i8", 1 << 63)
print(string.format("%X", string.unpack("i8", x2)))

-- 打包会进行检查是否溢出
-- bad argument #2 to 'pack' (integer overflow)
print(string.pack("i7", 1 << 63))

解包也是一样的,会抛出异常 12-byte integer does not fit into Lua Integer ,因为对于 Lua 的整型数是 8 字节。

local x = string.pack("i12", 2 ^ 61)
print(string.unpack("i12", x))
x = "aaaa" .. "aaaa" .. "aaaa"
-- 解包也会检查是否能装得下
-- 12-byte integer does not fit into Lua Integer
--string.unpack("i12", x)

3-3-3、格式化无符号

每一个格式化选项都有一个大写的版本,对应无符号整型数:

local s = "\xFF"
print(string.unpack("b", s))        --> -1	2
print(string.unpack("B", s))        --> 255	2

3-4、字符串格式可选参数

可以使用三种形式打包字符串

格式描述
z\0 结尾的字符串
cn定长字符串,n 是被打包字符串的字节数
sn显式长度字符串,会在存储字符串前加上该字符串的长度,n 是用于保存字符串长度的无符号整型数的大小

举些例子

s1 表示将字符串长度保存在一个字节中

print("----- sn -----")
local s1 = string.pack("s1", "hello")
print("s1 长度", #s1)
for i = 1, #s1 do
    print(string.unpack("B", s1, i))
end

--> s1 长度	6
--> 5	2   (length)
--> 104	3   ('h')
--> 101	4   ('e')
--> 108	5   ('l')
--> 108	6   ('l')
--> 111	7   ('o')

s2 则表示用两个字节存储

s1 = string.pack("s2", "hello")
print("s2 长度", #s1)
for i = 1, #s1 do
    print(string.unpack("B", s1, i))
end

--> s2 长度	7
--> 5	2   (length)
--> 0	3   (length)
--> 104	4   ('h')
--> 101	5   ('e')
--> 108	6   ('l')
--> 108	7   ('l')
--> 111	8   ('o')

值得注意,如果保存长度的字节容纳不下字符串的长度,则会有抛出异常

当然也可以直接使用 s 进行打包字符串,会使用 size_t 类型进行保存,在 64 位的机子上,一般使用 8 个字节保存,大多数情况这会有些浪费。

s1 = string.pack("s", "hello")
print("s 长度", #s1)
print(string.unpack("s", s1, i))
for i = 1, #s1 do
    print(string.unpack("B", s1, i))
end

--> s 长度	13
--> hello	14
--> 5	2   (length)
--> 0	3   (length)
--> 0	4   (length)
--> 0	5   (length)
--> 0	6   (length)
--> 0	7   (length)
--> 0	8   (length)
--> 0	9   (length)
--> 104	10  ('h')
--> 101	11  ('e')
--> 108	12  ('l')
--> 108	13  ('l')
--> 111	14  ('o')

cn 打包固定长度自负串

-- 因为这里每个中文占 3 个字节,所以是 c9 ,后面打印也就有九行
local name = "江澎涌"
local fmt = string.format("c%d", #name)
local s3 = string.pack(fmt, name)
for i = 1, #s3 do
    print(string.unpack("B", s3, i))
end
print(string.unpack(fmt, s3))

--> 230	2
--> 177	3
--> 159	4
--> 230	5
--> 190	6
--> 142	7
--> 230	8
--> 182	9
--> 140	10
--> 江澎涌	10

3-5、浮点数格式可选参数

格式描述
f单精度浮点数
d双精度浮点数
nLua 语言浮点数
local p = string.pack("fdn", 3.14, 1.70, 10.89)
print(string.unpack("fdn", p))                  --> 3.1400001049042	1.7	10.89	21

3-6、大小端模式

默认情况下,格式使用的是机器原生的大小端模式。可以在格式中使用,一旦使用,后续的都会作用

格式描述
大端模式、网络字节序
<小端模式
=改回机器默认模式

大端模式

local s1 = string.pack(">i2 i2", 500, 24)
for i = 1, #s1 do
    print(string.unpack("B", s1, i))
end

--> 1	2
--> 244	3
--> 0	4
--> 24	5

小端模式

local s2 = string.pack("<i2 i2", 500, 24)
for i = 1, #s2 do
    print(string.unpack("B", s2, i))
end

--> 244	2
--> 1	3
--> 24	4
--> 0	5

一图胜千言

改回默认

local s3 = string.pack(">i2 =i2", 500, 24)
for i = 1, #s3 do
    print(string.unpack("B", s3, i))
end

--> 1	2
--> 244	3
--> 24	4
--> 0	5

3-7、对齐数据

使用 !n 进行强制数据对齐到 n 为倍数的索引上。如果只是使用 ! 则会使用机器默认对其方式

如果数据比 n 小,则对其到其自身大小,否则对其到 n 上。

例如 !4 ,则 1 字节整型数会被写入以 1 位倍数的所以位置上,2 字节的整型数会被写入到以 2 为倍数的索引位置上,而 4 字节或更大的整型数会被写入到以 4 为倍数的索引位置上。

local s = string.pack("!4 i1 i2 i4", 10, 10, 10)
print("#s", #s)
for i = 1, #s do
    print(string.unpack("i1", s, i))
end

--> #s	8
--> 10	2
--> 0	3
--> 10	4
--> 0	5
--> 10	6
--> 0	7
--> 0	8
--> 0	9

string.pack 通过补 0 的形式实现对齐。string.unpack 则会在读取字符串时简单的跳过这些补位。

对齐只对 2 的整数次幂有效,如果不是则会报错 format asks for alignment not power of 2

在所有的格式化字符串默认的都会带有前缀 =!1,即表示使用默认的大小端模式且不对齐。

3-8、手工添加补位

可以通过 x 进行 1 字节的补位,string.pack 会在结果字符串中增加一个 0 字节,string.unpack 则会从目标字符串中跳过这一字节

local s = string.pack("i1i1xi1", 10, 10, 10)
print("#s", #s)
for i = 1, #s do
    print(string.unpack("i1", s, i))
end

--> #s	4
--> 10	2
--> 10	3
--> 0	4
--> 10	5

四、拓展一下

可以使用这一节的内容实现一些类似二进制文件查看器的功能,如下图所示

具体代码 github.com/zincPower/l…

还可以对图片进行操作,具体可以看 github.com/zincPower/l…

五、写在最后

Lua 项目地址:Github传送门 (如果对你有所帮助或喜欢的话,赏个star吧,码字不易,请多多支持)

如果觉得本篇博文对你有所启发或是解决了困惑,点个赞或关注我呀

公众号搜索 “江澎涌”,更多优质文章会第一时间分享与你。

image.png

全部评论

相关推荐

02-08 15:53
门头沟学院 Java
CoderEcho:让公司知道便宜没好货
点赞 评论 收藏
分享
02-10 12:23
已编辑
新余学院 C++
采集想要offer:专业技能那里要一条一条的列出来吧,感觉你项目很厉害了,但是如果你不写技术栈面试官对你项目不太懂的话都没办法问你八股😂C++都是基架岗,都是一群9✌🏻在卷,我觉得你要是有时间学个go把MySQL和redis写上去找个开发岗吧
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务