用float double作為中轉型別的「雷區」

2021-07-24 13:53:26 字數 4212 閱讀 9406

n由於lua用double作為number型別的底層資料中轉型別。而實際應用中多以int型別作為函式呼叫的引數(特別是c實現的api)。因而,double/int/unsigend int之間的數值轉換在接入lua的專案中應用十分廣泛。

實際專案發現,double/int/unsigend int之間的數值轉換存在乙個嚴重且極容易被忽視的」雷區」

根據ieee二進位制浮點數算術標準(ieee 754)定義,浮點數只是乙個近似值。

測試原由:

近日發現乙個奇葩的問題,在lua中,傳乙個大於int_max的整數給lua,或者在c++中用最高位為1的unsigned int u 採用如下方式返回值 lua_pushnumber(l, u);

lua將產生乙個「異常狀態的nunmber物件」,對該物件執行%x取值可以取到正確的十六進製制值,執行%d取值,將只能取到-2147483648(0x80000000)

更讓人糾結的是這個現象只發生在linux下,windows下是正常的,大於int_max的值%d提取將取到對應的負數值,在需要的地方傳值給對應的unsigned int,仍然是正確的值。

看到這個現象,第一反應是lua本身的bug,於是研究了lua的原始碼,發現lua除了採用double儲存和傳遞number物件,沒有其他不規矩操作。

而lua在%x和%d對number取值時執行的操作分別如下:

((int)lual_check_number(l, n)) //%d

((unsigned int)lual_check_number(l, n)) //%x

於是懷疑到c++double型別到int和unsigned int型別轉換出了問題,於是寫下了如下測試**:

以下是測試**和測試結果

func testfloat1(t *testing.t) 

for _, u := range tt

//x=0x7ffffffe u=2147483646 oki= 2147483646 f=2147483646.0

fi= 2147483646 err=false

//x=0x7fffffff u=2147483647 oki= 2147483647 f=2147483647.0

fi= 2147483647 err=false

//x=0x80000000 u=2147483648 oki=-2147483648 f=2147483648.0

fi=-2147483648 err=false

//x=0x80000001 u=2147483649 oki=-2147483647 f=2147483649.0

fi=-2147483648 err=true

//x=0xff000000 u=4278190080 oki= -16777216 f=4278190080.0

fi=-2147483648 err=true

}func testfloat2(t *testing.t)

for _, f := range tt

//x=0x7ffffffe f= 2147483646.0 u=2147483646

fi= 2147483646 oki= 2147483646 err=false

//x=0x7fffffff f= 2147483647.0 u=2147483647

fi= 2147483647 oki= 2147483647 err=false

//x=0xffffffff f= -1.0 u=4294967295

fi= -1 oki= -1 err=false

//x=0xfffffffe f= -2.0 u=4294967294

fi= -2 oki= -2 err=false

//x=0x80000000 f= 2147483648.0 u=2147483648

fi=-2147483648 oki=-2147483648 err=false

//x=0x80000001 f= 2147483649.0 u=2147483649

fi=-2147483648 oki=-2147483647 err=true

//x=0x80000002 f= 2147483650.0 u=2147483650

fi=-2147483648 oki=-2147483646 err=true

//x=0x80000002 f=36507222018.0 u=2147483650

fi=-2147483648 oki=-2147483646 err=true

//x=0xff000000 f= 4278190080.0 u=4278190080

fi=-2147483648 oki= -16777216 err=true

//x=0xfffffffe f= 4294967294.0 u=4294967294

fi=-2147483648 oki= -2 err=true

//x=0xffffffff f= 4294967295.0 u=4294967295

fi=-2147483648 oki= -1 err=true

}

結論如下:

1. 無論在linux還是在windows下,將乙個超出int值域範圍[-2147483648,2147483647]的doulbe值,轉換為int時,將只能取到-2147483648(0x80000000)

2. 將乙個超出超出unsigned int值域範圍[0, 4294967295]的double型別,轉換為unsigned int,將安全的取到對應16進製制值的低32位

3. windows優先將常量表示式計算為int,linux優先將常量表示式結果計算為unsigned int(不知為何,這個差異在這個測試用例中沒能體現出來)

4. (int)doublevalue操作在c++中是極度危險的「雷區」,應當在編碼規範層次嚴格禁止。

5. (unsigned int)doublevalue操作在c++中是安全的

6. 想從double得到int,必須使用(int)(unsigned int)doublevalue這樣的操作

經驗教訓:

由於lua採用double儲存和傳遞number物件,這個問題必須得到重視,並且需要在編碼規範的層次,嚴格禁止這種unsigned int->double, double->int的行為

在c++**中大量使用的如下操作將是危險的:

1. int nintvalue = (int)lua_valuetonumber(l, 1); //danger!!! 對不在int範圍內的number,只能取到-2147483648(0x80000000)

2. lua_pushnumber(l, unsignedintvalue); //danger!!!如果unsignedintvalue最高位為1,將產生乙個超出int範圍的異常number物件

以上兩種用法必須修改為

1. int nintvalue = (int)(unsigned int)lua_valuetonumber(l, 1);

2. lua_pushnumber(l, (int)unsignedintvalue);

以下結論必須在日常編碼中引起重視:

1.(int)doublevalue操作在c++中是極度危險的「雷區」,應當在編碼規範層次嚴格禁止。

2. (unsigned int)doublevalue操作在c++中是安全的

3. int/unsigned int相互轉換是安全的

3. 想從double得到int,必須使用(int)(unsigned int)doublevalue這樣的操作

4. 無論在linux還是在windows下,將乙個超出int值域範圍[-2147483648,2147483647]的doulbe值,轉換為int時,將只能取到-2147483648(0x80000000)

5. 將乙個超出超出unsigned int值域範圍[0, 4294967295]的double型別,轉換為unsigned int,將安全的取到對應16進製制值的低32位

6. windows優先將常量表示式計算為int,linux優先將常量表示式結果計算為unsigned int(不知為何,這個差異在這個測試用例中沒能體現出來)

我將以上測試**放在這裡:

參考資料:

ieee二進位制浮點數算術標準(ieee 754)

float,double在記憶體中的儲存方式

將17.625換算成 float型。首先,將17.625換算成二進位制位 10001.101 0.625 0.5 0.125,0.5即 1 2,0.125即 1 8 如果不會將小數部分轉換成二進位制,請參考其他書籍。再將 10001.101 向右移,直到小數點前只剩一位 成了 1.0001101 x...

float,double資料型別在記憶體中的儲存方式

float在記憶體中用四個byte表示 符號位 sign 指數字 exponent 尾數 mantissa 1 bit 8 bits 23 bits 符號位 1正0負 指數字 範圍從0 255,但實際的指數等於這裡的指數減去127,所以真正的指數範圍從 127 128。尾數 23bit的尾數實際上表...

用陣列作為函式引數

我們都知道,可以用變數作為函式的引數,而陣列中的每乙個元素也是變數,因此我們也可以用陣列元素來作為函式引數。另外,用陣列名也可以作為實參和形參,傳遞的是陣列的首位址。一 用陣列元素作為函式實參 這與用變數作為實參一樣,是單向傳遞,取 值傳遞 的方式。二 用陣列名作為函式引數 此時,實參和形參都要用陣...