簡體   English   中英

將 2 個字節轉換為有符號的 16 位整數的正確方法是什么?

[英]What is the correct way to convert 2 bytes to a signed 16-bit integer?

這個答案中zwol提出了這個主張:

將來自外部源的兩個字節數據轉換為 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_tuint32_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_tint的 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);
}

memcpy用於復制int16_t的表示,這是符合標准的方法。 這個版本也編譯成 1 條指令movbe ,見匯編

另一種方法 - 使用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_bytesecond_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

我要感謝所有貢獻者的回答。 以下是集體作品的內容:

  1. 根據 C 標准7.20.1.1 精確寬度整數類型:類型uint8_tint16_tuint16_t必須使用沒有任何填充位的二進制補碼表示,因此表示的實際位是數組中 2 個字節的明確位,在由函數名指定的順序。
  2. (unsigned)data[0] | ((unsigned)data[1] << 8)計算無符號的 16 位值(unsigned)data[0] | ((unsigned)data[1] << 8) (unsigned)data[0] | ((unsigned)data[1] << 8) (對於小端版本)編譯為一條指令並產生一個無符號的 16 位值。
  3. 根據 C 標准6.3.1.3 有符號和無符號整數:如果值不在目標類型的范圍內,則將uint16_t類型的值轉換為有符號類型int16_t具有實現定義的行為。 對於精確定義表示的類型沒有特別規定。
  4. 為了避免這種實現定義的行為,可以測試無符號值是否大於INT_MAX並通過減去0x10000來計算相應的有符號值。 按照zwol 的建議對所有值執行此操作可能會產生具有相同實現定義行為的int16_t范圍之外的值。
  5. 0x8000位的測試顯式導致編譯器生成低效代碼。
  6. 沒有實現定義行為的更有效的轉換通過聯合使用類型雙關語,但關於這種方法的定義性的爭論仍然存在,即使在 C 標准委員會級別也是​​如此。
  7. 可以使用memcpy可移植地執行類型雙關並定義行為。

結合第 2 點和第 7 點,這是一個可移植且完全定義的解決方案,它可以使用gccclang有效地編譯為單個指令:

#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;
}

64 位程序集

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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM