65.9K
CodeProject 正在变化。 阅读更多。
Home

不寻常整数大小的二进制补码

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.80/5 (2投票s)

2016年2月18日

CPOL

3分钟阅读

viewsIcon

30935

从 10 位(和其他不寻常大小)整数中获取负数

引言

负整数通常以二进制补码表示。对于大多数程序员来说,这没什么好担心的,因为它是自动处理的,包括分配给更大类型时的转换。但是,当您使用非常规长度的整数(如 5 位、7 位或 10 位)时,您必须自己处理负数表示。此技巧为您提供了一个通用函数来执行此操作。

背景

十进制值 -2 的有符号 8 位整数存储为二进制 11111110,或 0xfe。当将其分配给 16 位整数时,存储的值不是 0x00fe,而是 0xfffe,这保留了补码表示。相反,当您将 0xfe 分配给无符号 16 位整数时,它会存储 0x00fe,因为无符号类型不针对补码进行调整。

但是,在使用自定义硬件或编写设备驱动程序时,您偶尔会遇到非标准整数长度,因为寄存器空间可能受到限制。我最近不得不处理 6 位和 10 位有符号整数。当我读取这些整数时,我将它们分别存储为 8 位和 16 位整数,这弄乱了负数。以补码形式存储 -1 的 10 位整数将包含 0x3ff,但包含 0x03ff 的 16 位整数被认为表示十进制的 1023。它应该是 0xffff,它是 -1 的 16 位补码。

我需要一个转换函数来确保负数被准确表示。

ETA:Philippe Mori 提醒我可以使用位域,它可以为您处理这个问题(前提是您的数据适合 int),所以我编写了一个小的模板类(因为位域必须是成员)来提供第三种选择。

函数

#include <cstdint>
#include <stdexcept>

int16_t UpscaleTwosComplement(int16_t value, size_t length)
{
    // Too large?
    if (length > 15)
    {
        throw std::out_of_range("UpscaleTwosComplement length too large");
    }
    uint16_t mask = (~0) << length;
    // Too small for complement?
    if (length < 2)
    {
        return (~mask & value);
    }
    // Two's complement?
    uint16_t msb = 1 << (length-1);
    if (value & msb)
    {
        return (mask | value);
    }
    else
    {
        return (~mask & value);
    }
}

经过基本的健全性检查后,它会检查 value 的最高有效位,以查看它是否为负数。如果是,则结果中的所有高位都设置为 1,这保留了表示。如果不是,则过滤掉所有较高位。

...
// 10-bit sensor value, 2's complement
int16_t registerTemp; 
// Read...
...
int16_t temp = UpscaleTwosComplement(registerTemp, 10);
// registerTemp temp
//   -1 (0x3ff)    -1 (0xffffffff)          
//   10 (0x00a)    10 (0x0000000a)          
//  -10 (0x3f6)   -10 (0xfffffff6)

我还编写了一个模板化版本,以便更好地控制目标类型,并减少运行时操作

template<typename Target, size_t Length>
Target UpscaleTwosComplement(Target value)
{
    // Template type sanity checks (C++11 and later)
    static_assert(sizeof(Target) * 8 > Length, "Length too large for Target");
    static_assert(Length > 1, "Too short for two's complement");
    Target mask = (~0) << Length;
    // Two's complement?
    Target msb = 1 << (Length-1);
    if (value & msb)
    {
        return (mask | value);
    }
    else
    {
        return (~mask & value);
    }
}

位域

我想起了位域,它是 C 和 C++ 中的一种机制,可让您处理非标准长度的整数。由于硬件库的构建方式,我无法在我的项目中直接使用它们,但您或许可以使用。

但是,我可以使用位域来进行转换,因此我编写了以下代码来替代我上面的自制转换。但是,您需要注意两个限制。首先,位域必须是 struct 或 class 的数据成员。其次,如果为位域分配超出其范围的值,则它所持有的值取决于编译器实现。例如,一个 10 位有符号位域的范围是 -512 (0x200) 到 511 (0x1ff),但如果为其分配一个 512 (0x0200) 的 16 位值,则在 VC2008 中将产生 -512,并且可能大多数编译器都会这样,但不能保证每个编译器都做同样的事情 - 编译器完全可以丢弃符号位,因此您最终会得到 0。在使用它之前,请检查它是否在您的编译器上按您期望的方式工作。

template <typename Target, size_t Length>
class TwosComplementUpscaler
{
    Target bitfield : Length;
    // Private constructors
    TwosComplementUpscaler(Target value)
    : bitfield(value)
    {}
    TwosComplementUpscaler();
public:

    static Target Convert(Target value)
    {
        // Template type sanity checks (C++11 and later)
        static_assert(sizeof(Target) * 8 > Length, "Length too large for Target");
        static_assert(Length > 1, "Too short for two's complement");

        Target mask = (~0) << Length;
        TwosComplementUpscaler temp(value & ~mask);
        return static_cast<Target>(temp.bitfield);
    }
}; 

您可以将其用作独立的模板函数

int16_t tt = TwosComplementUpscaler<int16_t, 10>::Convert(0x3ff); // = -1
tt = TwosComplementUpscaler<int16_t, 10>::Convert(0x1ff); // = 511
tt = TwosComplementUpscaler<int16_t, 10>::Convert(0x200); // = -512

历史

  • 2016 年 2 月 18 日 - 首次发布
  • 2016 年 2 月 19 日 - 根据评论更新。添加了关于位域的部分和相关代码,更改 UpscaleTwosComplement 以使用大小指定的类型并检查长度
© . All rights reserved.