将C语言中的12位有符号数转换

6
假设我有一个用不寻常位数(例如12位)编码的带符号数字,如何将其高效地转换为标准的C值? 以下方法可行但需要一个中间变量:
#include <stdio.h>
int main() { 
  unsigned short U12=0xFFF; // 12-bit signed number, as coded in hex
  unsigned short XT=U12<<4; // 16 bits minus 12 is 4...
  short SX=(*(short*)&XT)>>4; // Signed shift. Is that standard C ?
  printf("%08X %d\n", SX, SX);
  return 0;
}

输出:

U12=0x0:   00000000 0
U12=0x1:   00000001 1
U12=0x7FF: 000007FF 2047
U12=0x800: FFFFF800 -2048
U12=0x801: FFFFF801 -2047
U12=0xFFF: FFFFFFFF -1

是否有更直接的方式来做这件事,而无需中间变量?


1
“Efficient” 是指什么?一个12位的值可以适合于一个相当小的查找表中。 - William Pursell
4
“用十六进制编码的不寻常数字”这个说法并没有具体指明使用了哪种编码方式,是采用的一补数、二补数还是原码? - Lundin
3
将其放入内联函数中并检查生成的代码。中间临时变量可能已经消失。 - wildplasser
只需使用位域,编译器就会为您完成繁重的工作。 - 0___________
@Lundin 只需假定编码与处理器相同(是的,我知道这并非必然)。 - dargaud
4个回答

7

这里是一种可移植的方法(未经过测试):

 short SX = (short)U12 - ((U12 & 0x800) << 1);

(将0x800和0x1000替换为(1<<某数)以获得不同的位数)。

直接使用位移操作来操作符号位的方法往往会引发未定义行为。


5
您的方法存在一个问题,即您似乎假设shortunsigned short的宽度为16位。然而,这在ISO C标准中并不保证。标准仅指定宽度必须至少为16位。
如果您想使用保证为16位宽的数据类型,则应改用数据类型uint16_tint16_t
另一个问题是,您的代码假设运行程序的平台以与您的“12位有符号数”相同的方式表示带负值的整数。然而,与最新的ISO C ++标准不同,ISO C标准允许平台使用以下任何一种表示形式:
  1. 二进制补码
  2. 反码
  3. 原码
假设你的“12位带符号数”采用二进制补码表示,但你想让你的代码在所有平台上都能正确运行,无论平台内部使用哪种表示法,那么你需要编写以下代码:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdbool.h>

int main( void )
{
    uint16_t U12=0xFFF;
    int16_t converted;

    //determine sign bit
    bool sign = U12 & 0x800;

    if ( sign )
    {
        //value is negative
        converted = -2048 + ( U12 & 0x7FF );
    }
    else
    {
        //value is positive
        converted = U12;
    }

    printf( "The converted value is: %"PRId16"\n", converted );
}

这个程序的输出如下:
The converted value is: -1

正如评论部分所指出的那样,这个程序可以简化为以下内容:

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main( void )
{
    uint16_t U12=0xFFF;

    int16_t converted = U12 - ( (U12 & 0x800) << 1 );

    printf( "The converted value is: %"PRId16"\n", converted );
}

在负数的情况下,应该不是-int16_t((~U12)&0x7FF+1)吗? - user8143588
为什么要使用 converted = -2048 + ( U12 & 0x7FF ); 而不是简单的 converted = U12 - 4096;?因此 converted = U12 - (sign ? 4096 : 0);。因此 converted = U12 - ((U12 & 0x800) << 1); - Eric Postpischil
@EricPostpischil:-2048只是符号位的值,而U12&0x7FF是将符号位屏蔽后的U12的值。您说得对,程序可以按照您描述的方式进行优化。因此,我已编辑我的答案以引用您的评论。 - Andreas Wenzel
@AndreasWenzel,你的解决方案不可用。 https://godbolt.org/z/n7xrK9Yaj 对于负数,表达式应该是-((int16_t)(((~n)&0x7FF)+1)) - user8143588
1
@AndreasWenzel 这是真的,我错误地将0x7FF误认为是0xFFF。 - user8143588
显示剩余11条评论

5

假设0xFFF是一个使用 二进制补码 表示的12位数字(不一定是这种情况),相当于 -1,并且假设我们的CPU也使用2进制补码(极有可能),那么:

在位运算中使用小型整数类型例如(无符号)charshort 是危险的,因为会存在隐式类型提升。在32位系统上假设16位short,如果这样的变量(有符号或无符号)传递到移位的左操作数中,则始终会被提升为(有符号)int

在上述假设下,那么:

  • U12<<4 得到一个类型为 int 的结果0xFFF0,然后将其转换为无符号的short进行赋值。
  • 转换*(short*)&XT虽然奇怪但在C语言中是允许的。内存内容现在被重新解释为CPU的有符号格式。
  • the_signed_short >> 4 当左操作数为负数时调用实现定义的行为。它不一定会产生您期望的算术移位。它也可以是一个逻辑移位。
  • %X%d 分别期望传递 unsigned intint,所以传递short是错误的。在这种情况下您得到救赎是因为必须将可变参数函数的参数默认提升为 int

总的来说,这里存在很多代码问题。

在上述32位系统上更好和大部分被定义的方法是:

int32_t u12_to_i32 (uint32_t u12)
{
  u12 &= 0xFFF; // optionally, mask out potential clutter in upper bytes

  if(u12 & (1u<<11)) // if signed, bit 11 set?
  {
    u12 |= 0xFFFFFFu << 12; // "sign extend"
  }

  return u12; // unsigned to signed conversion, impl.defined  
}

这里所有的位操作都是在一个无符号类型上进行的,这种类型不会在32位系统上自动提升。该方法还有使用纯十六进制位掩码和没有“魔法数字”的优点。

完整的示例,包括测试用例:

#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>

int32_t u12_to_i32 (uint32_t u12)
{
    u12 &= 0xFFF; // optionally, mask out potential clutter in upper bytes

    if(u12 & (1u<<11)) // if signed, bit 11 set?
    {
    u12 |= 0xFFFFFFu << 12; // "sign extend"
    }

    return u12; // unsigned to signed conversion, impl.defined  
}

int main (void) 
{ 
  uint32_t u12;
  int32_t  i32;
  
  u12=0; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0x7FF; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0x800; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0xFFF; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  return 0;
}

输出结果(gcc x86_64 Linux):

00000000-> 00000000 = 0
000007FF-> 000007FF = 2047
00000800-> FFFFF800 = -2048
00000FFF-> FFFFFFFF = -1

4

让编译器来做艰苦的工作:

#define TOINT(val, bits) (((struct {int val: bits;}){val}).val)

或更一般地说

#define TOINT(type, v, bits) (((struct {type val: bits;}){v}).val)

用法:

int main(void)
{
    int int12bit = 0xfff;

    printf("%d\n", TOINT(int, int12bit, 12));
}

或者更简单的版本:
int main(void)
{
    int int12bit = 0x9B2;

    printf("%d\n", TOINT(int12bit, 12));
}

编译器将根据目标平台和类型选择最高效的方法:

int convert12(int val)
{
    return TOINT(val,12);
}

long long convert12ll(unsigned val)
{
    return TOINT(long long, val,12);
}

你可以用类似的方法将 N 位有符号整数转换为 'M' 位有符号整数。

https://godbolt.org/z/P4b4rM4TT

#define TOINT(v, bits) (((struct {long long val: bits;}){v}).val)
#define FROM_N_TO_M(val, N, M) TOINT(TOINT(val, N), M)

我现在才刚刚发现位域的威力!谢谢。 - dargaud
1
如果将0xFFF分配给大小为12的位域,则未定义会发生什么,而且除了int、char和bool之外,支持哪些类型也未定义。字节序或位顺序也未定义。截至C17,二进制整数常量是非标准的。总的来说,您正在对使用非常特定的编译器和系统做出许多假设。我真的没有看到与原始代码相比有任何改进,尽管不是这么多实现定义的行为。 - Lundin
1
例如,您能解释一下来自您的godbolt示例的clang警告吗:“格式指定了类型'long long',但参数的类型为'int'[-Wformat]”? - Lundin
在现实生活中,使用流行的工具链(如gcc/binutils、keil、IAR、msvc、GHC、clang等)都是明确定义的。我同意,在语言律师的水平上,位域并不是很常见,但在嵌入式系统编程中,位域非常普遍,编译器开发人员知道他们必须在下一个版本/其他目标实现中保持一致性。0b确实是gcc特定的-已被十六进制值取代。 - 0___________

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接