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






3.80/5 (2投票s)
从 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
以使用大小指定的类型并检查长度