一、位運算
從 Lua 5.3 版本開始,提供了針對數值類型的一組標準位運算符,與算數運算符不同的是,運算符只能用于整型數。
運算符 | 描述 |
---|---|
& | 按位與 |
| | 按位或 |
~ | 按位異或 |
>> | 邏輯右移 |
<< | 邏輯左移 |
~(一元運算) | 按位取反 |
位的使用,可以參考小盆友的另一篇文章 《android位運算簡單講解》
https://blog.csdn.net/weixin_37625173/article/details/83796580
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>>n
和 a<<-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^63if d < 0 then-- 如果除數大于被除數(n<d),則商為 0 ;否則為 1if math.ult(n, d) thenreturn 0elsereturn 1endend-- n >> 1 等價于將無符號整數 n / 2 , 這樣的做法是先去除符號位置的影響-- 最后 << 1 等價于 * 2 ,這樣是為了糾正一開始 >> 1-- // d 進行有符號整數的正常向下取整除法local q = ((n >> 1) // d) << 1-- 計算因為移位和向下取整導致的丟失數值,計算出兩個結果后,如果偏差大于除數,說明需要加一local r = n - q * dif not math.ult(r, d) thenq = q + 1endreturn q
endlocal 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" thenreturn valueend-- 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)
endlocal 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、整型數格式可選參數
每種選項對應一種類型大小
格式 | 描述 |
---|---|
b | char |
h | short |
i | int(可以跟一個數字,表示多少字節的整型數) |
l | long |
j | Lua 語言中的整型數大小 |
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 doprint(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 doprint(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 doprint(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 doprint(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 | 雙精度浮點數 |
n | Lua 語言浮點數 |
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 doprint(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 doprint(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 doprint(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 doprint(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 doprint(string.unpack("i1", s, i))
end--> #s 4
--> 10 2
--> 10 3
--> 0 4
--> 10 5
四、拓展一下
可以使用這一節的內容實現一些類似二進制文件查看器的功能,如下圖所示
具體代碼 https://github.com/zincPower/lua_study_2022/blob/master/7%20%E4%BD%8D%E5%92%8C%E5%AD%97%E8%8A%82/dump.lua
還可以對圖片進行操作,具體可以看 https://github.com/zincPower/lua_study_2022/blob/master/7%20%E4%BD%8D%E5%92%8C%E5%AD%97%E8%8A%82/writeBinary.lua
五、寫在最后
Lua 項目地址:Github傳送門 (如果對你有所幫助或喜歡的話,賞個star吧,碼字不易,請多多支持)
如果覺得本篇博文對你有所啟發或是解決了困惑,點個贊或關注我呀。
公眾號搜索 “江澎涌”,更多優質文章會第一時間分享與你。