[英]What is the correct way to convert 2 bytes to a signed 16-bit integer?
將來自外部源的兩個字節數據轉換為 16 位有符號整數的正確方法是使用如下輔助函數:
#include <stdint.h>
int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 8) |
(((uint32_t)data[1]) << 0);
return ((int32_t) val) - 0x10000u;
}
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
uint32_t val = (((uint32_t)data[0]) << 0) |
(((uint32_t)data[1]) << 8);
return ((int32_t) val) - 0x10000u;
}
上述哪個函數合適取決於數組是包含小端還是大端表示。 字節序不是這里的問題,我想知道為什么0x10000u
從轉換為int32_t
的uint32_t
值中減去0x10000u
。
為什么這是正確的方法?
轉換為返回類型時如何避免實現定義的行為?
既然您可以假設 2 的補碼表示,那么這個更簡單的轉換將如何失敗: return (uint16_t)val;
這個幼稚的解決方案有什么問題:
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
如果int
是 16 位,那么如果return
語句中的表達式值超出int16_t
的范圍,則您的版本依賴於實現定義的行為。
但是第一個版本也有類似的問題; 例如,如果int32_t
是int
的 typedef,並且輸入字節都是0xFF
,則 return 語句中的減法結果是UINT_MAX
,當轉換為int16_t
時會導致實現定義的行為。
恕我直言,您鏈接的答案有幾個主要問題。
這應該是迂腐正確的,並且也適用於使用符號位或1 的補碼表示的平台,而不是通常的2 的補碼。 假設輸入字節為 2 的補碼。
int le16_to_cpu_signed(const uint8_t data[static 2]) {
unsigned value = data[0] | ((unsigned)data[1] << 8);
if (value & 0x8000)
return -(int)(~value) - 1;
else
return value;
}
由於分支的原因,它會比其他選項更貴。
這樣做的目的是避免任何關於int
表示如何與平台上的unsigned
表示相關的假設。 需要轉換為int
以保留適合目標類型的任何數字的算術值。 由於反轉確保 16 位數字的最高位為零,因此該值將適合。 然后一元-
和 1 的減法應用 2 的補碼否定的通常規則。 根據平台的不同,如果INT16_MIN
不適合目標上的int
類型,它仍可能溢出,在這種情況下應使用long
。
問題中與原始版本的區別在於返回時間。 雖然原始總是減去0x10000
和 2 的補碼讓有符號溢出將其包裝到int16_t
范圍,但此版本具有明確的if
避免有符號包裝( 未定義)。
現在在實踐中,當今使用的幾乎所有平台都使用 2 的補碼表示。 事實上,如果平台具有定義int32_t
的符合標准的stdint.h
,則它必須使用 2 的補碼。 這種方法有時派上用場的是一些根本沒有整數數據類型的腳本語言 - 您可以修改上面顯示的浮點數操作,它會給出正確的結果。
表達式(uint16_t)data[0] | ((uint16_t)data[1] << 8)
的算術運算符移位和按位或 (uint16_t)data[0] | ((uint16_t)data[1] << 8)
不適用於小於int
類型,因此這些uint16_t
值被提升為int
(或unsigned
if sizeof(uint16_t) == sizeof(int)
)。 盡管如此,這應該會產生正確的答案,因為只有較低的 2 個字節包含該值。
big-endian 到 little-endian 轉換的另一個迂腐正確的版本(假設 little-endian CPU)是:
#include <string.h>
#include <stdint.h>
int16_t be16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
memcpy(&r, data, sizeof r);
return __builtin_bswap16(r);
}
另一種方法 - 使用union
:
union B2I16
{
int16_t i;
byte b[2];
};
在節目中:
...
B2I16 conv;
conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;
first_byte
和second_byte
可以根據小端或大端模型交換。 這種方法不是更好,而是替代方法之一。
這是另一個僅依賴於可移植和明確定義的行為的版本(標頭#include <endian.h>
不是標准的,代碼是):
#include <endian.h>
#include <stdint.h>
#include <string.h>
static inline void swap(uint8_t* a, uint8_t* b) {
uint8_t t = *a;
*a = *b;
*b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
for(int i = 0, j = data_len / 2; i < j; ++i)
swap(data + i, data + data_len - 1 - i);
}
int16_t be16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
uint8_t data2[sizeof r];
memcpy(data2, data, sizeof data2);
reverse(data2, sizeof data2);
memcpy(&r, data2, sizeof r);
#else
memcpy(&r, data, sizeof r);
#endif
return r;
}
little-endian 版本使用clang
編譯為單個movbe
指令, gcc
版本不太理想,請參閱assembly 。
我要感謝所有貢獻者的回答。 以下是集體作品的內容:
uint8_t
、 int16_t
和uint16_t
必須使用沒有任何填充位的二進制補碼表示,因此表示的實際位是數組中 2 個字節的明確位,在由函數名指定的順序。(unsigned)data[0] | ((unsigned)data[1] << 8)
計算無符號的 16 位值(unsigned)data[0] | ((unsigned)data[1] << 8)
(unsigned)data[0] | ((unsigned)data[1] << 8)
(對於小端版本)編譯為一條指令並產生一個無符號的 16 位值。uint16_t
類型的值轉換為有符號類型int16_t
具有實現定義的行為。 對於精確定義表示的類型沒有特別規定。INT_MAX
並通過減去0x10000
來計算相應的有符號值。 按照zwol 的建議對所有值執行此操作可能會產生具有相同實現定義行為的int16_t
范圍之外的值。0x8000
位的測試顯式導致編譯器生成低效代碼。memcpy
可移植地執行類型雙關並定義行為。結合第 2 點和第 7 點,這是一個可移植且完全定義的解決方案,它可以使用gcc和clang有效地編譯為單個指令:
#include <stdint.h>
#include <string.h>
int16_t be16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
memcpy(&r, &u, sizeof r);
return r;
}
int16_t le16_to_cpu_signed(const uint8_t data[2]) {
int16_t r;
uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
memcpy(&r, &u, sizeof r);
return r;
}
be16_to_cpu_signed(unsigned char const*):
movbe ax, WORD PTR [rdi]
ret
le16_to_cpu_signed(unsigned char const*):
movzx eax, WORD PTR [rdi]
ret
為什么不直接使用您的“天真的解決方案”,而是將每個元素轉換為int16_t
而不是uint16_t
?
int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
return (int16_t)data[0] | ((int16_t)data[1] << 8);
}
那么你就不必處理將無符號整數轉換為有符號整數(並且可能超出有符號整數范圍)。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.