如何强制 C 将变量解释为有符号或无符号值?

How to force C to interpet variables as signed or unsigned values?

我正在做一个项目,我经常需要将某些变量解释为有符号或无符号值,并对它们进行有符号操作;然而,在许多情况下,细微的、看似微不足道的更改将无符号解释交换为带符号解释,而在其他情况下,我不能强制 C 将其解释为带符号值,它仍然是无符号的。这里有两个例子:

int32_t pop();

//Version 1
push((int32_t)( (-1) * (pop() - pop()) ) );

//Version 2
int32_t temp1 = pop();
int32_t temp2 = pop();
push((int32_t)( (-1) * (temp1 - temp2) ) );

/*Another example */

//Version 1
int32_t get_signed_argument(uint8_t* argument) {
  return (int32_t)( (((int32_t)argument[0] << 8) & (int32_t)0x0000ff00 | (((int32_t)argument[1]) & (int32_t)0x000000ff) );
}

//Version 2
int16_t get_signed_argument(uint8_t* argument) {
  return (int16_t)( (((int16_t)argument[0] << 8) & (int16_t)0xff00 | (((int16_t)argument[1]) & (int16_t)0x00ff) );
}

在第一个示例中,版本 1 似乎没有将值乘以 -1,而版本 2 有,但唯一的区别是在一种情况下将计算的中间值存储在临时变量中,或者在某些情况下不这样做另一个。

在第二个示例中,版本 1 返回的值是对与版本 2 返回值相同字节的无符号解释,版本 2 将其解释为 2 的补码。唯一的区别是使用 int16_t 或 int32_t.

在这两种情况下,我都使用有符号类型(int32_t、int16_t),但这似乎不足以将它们解释为有符号值。您能否解释为什么这些差异会导致签名差异?我在哪里可以找到这方面的更多信息?我怎样才能使用第一个示例的较短版本,但仍然得到有符号的值?提前致谢!

我假设 pop() returns 是无符号类型。如果是这样,表达式 pop() - pop() 将使用无符号算术执行,它是模块化的并且如果第二个 pop() 大于第一个则环绕(顺便说一句,C 没有指定特定的评估顺序, 所以不能保证弹出的值是第一个还是第二个)。

因此,您乘以 -1 的值可能与您预期的不同;如果有环绕,它可能是一个大的正值而不是负值。

如果您直接转换至少一个函数调用,您可以获得临时对象的等价物。

push(-1 * ((int32_t)pop() - pop()));

C 中表达式的结果类型由该表达式的组成操作数的类型决定,而不是由您可能应用于该结果的任何强制转换决定。正如上面的 Barmar 评论,要强制结果的类型,您必须转换操作数之一。

如果您只是想将二进制缓冲区转换为更长的有符号整数,例如从某处接收到的整数(我假设是小端)

int16_t bufftoInt16(const uint8_t *buff)
{
    return (uint16_t)buff[0] | ((uint16_t)buff[1] << 8);
}

int32_t bufftoInt32(const uint8_t *buff)
{
    return (uint32_t)buff[0] | ((uint32_t)buff[1] << 8) | ((uint32_t)buff[2] << 16) | ((uint32_t)buff[3] << 24) ;
}

int32_t bufftoInt32_2bytes(const uint8_t *buff)
{
    int16_t result = (uint16_t)buff[0] | ((uint16_t)buff[1] << 8);
    return result;
}


int main()
{
    int16_t x = -5;
    int32_t y = -10;
    int16_t w = -5567;

    printf("%hd %d %d\n", bufftoInt16(&x), bufftoInt32(&y), bufftoInt32_2bytes(&w));

    return 0;
}

将字节转换为有符号整数的工作方式与无符号移位完全不同。

I am working on a project where I often need to interpret certain variables as signed or unsigned values and do signed operations on them.

这似乎令人担忧。我认为你的意思是你想在不同的情况下将对象的 表示 重新解释为具有不同的类型(仅在符号上有所不同),或者你可能想像重新解释一样转换值对象表示。这种事情通常会造成混乱,但如果你足够小心,你可以处理它。如果您愿意依赖实现的细节,例如各种类型的表示,那会更容易。

在这种情况下,必须知道和理解所有 ,包括整数提升和通常的算术转换,以及它们在什么情况下适用。必须了解这些规则对表达式求值的影响——所有中间结果和最终结果的类型和值。

例如,关于演员表的最佳选择

push((int32_t)( (-1) * (temp1 - temp2) ) );

就是没用。如果该值在该类型中不可表示,那么(它是有符号整数类型)可能会发出信号,如果不能,则结果为 implementation-defined。但是,如果值 可表示的,则转换不会更改它。在任何情况下,结果都不能免除进一步转换为 push() 的参数类型。

再举一个例子,第一个例子的版本 1 和版本 2 之间的区别主要在于转换哪些值,何时转换(但另请参见下文)。如果两者确实产生不同的结果,那么可以得出 pop() 的 return 类型不同于 int32_t。在那种情况下,如果您想将它们转换为不同的类型以对它们执行操作,那么您实际上必须这样做。您的版本 2 通过将 pop() 结果分配给所需类型的变量来实现这一点,但是通过强制转换执行转换会更加惯用:

push((-1) * ((int32_t)pop() - (int32_t)pop()));

但是请注意,如果 pop() 调用的结果取决于它们的顺序——例如,如果它们从堆栈中弹出元素——那么你还有一个问题:评估哪些操作数是未指定的,您不能安全地假设它将是一致的。出于 的原因,而不是出于打字方面的考虑,您的版本 2 在这里更可取。

但是,总的来说,如果您有一个堆栈,其元素可能表示不同类型的值,那么我建议将元素类型设为联合(如果每个元素的类型从上下文中隐含)或标记联合 (如果元素需要携带有关其自身类型的信息。例如,

union integer {
    int32_t signed;
    uint32_t unsigned;
};

union integer pop();
void push(union integer i);

union integer first = pop();
union integer second = pop();
push((union integer) { .signed = second.signed - first.signed });

为了帮助您了解代码中发生的情况,我提供了解释自动类型转换(对于整数)如何完成的标准文本,以及关于按位移位的部分,因为它的工作方式有点不同.然后,我将逐步查看您的代码,以准确了解每次操作后存在哪些中间类型。

标准的相关部分

6.3.1.1 布尔值、字符和整数

  1. If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.

6.3.1.8 常用算术转换

(这里我只是总结了相关部分。)

  1. 整数提升完成。
  2. 如果它们都是有符号的或都是无符号的,则它们都转换为较大的类型。
  3. 如果无符号类型较大,则有符号类型转换为无符号类型。
  4. 如果有符号类型可以表示无符号类型的所有值,则无符号类型转换为有符号类型。
  5. 否则,它们都被转换为与有符号类型大小相同的无符号类型。

(基本上,如果你有 a OP b,所使用的类型的大小将是 int、type(a)、type(b) 中最大的,并且它 将更喜欢可以表示 type(a) 和 type(b) 可表示的所有值的类型。最后,它支持签名类型。 大多数时候,这意味着它将是整数。)

6.5.7 移位运算符

  1. The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is $E1 x 2^{E2}$,reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and $E1 x 2^{E2}$ is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

所有这些如何适用于您的代码。

我现在跳过第一个例子,因为我不知道什么类型的 pop() returns。如果您将该信息添加到您的 问题,我也可以解决这个例子。

让我们逐步了解此表达式中发生的情况(请注意,在您的版本中,在第一次转换后有一个额外的 (;我已将其删除):

(((int32_t)argument[0] << 8) & (int32_t)0x0000ff00 | (((int32_t)argument[1]) & (int32_t)0x000000ff) )

其中一些转换取决于类型的相对大小。 设 INT_TYPE 为 int32_t 和系统上的整数中的较大者。

((int32_t)argument[0] << 8)

  1. argument[0] 明确转换为 int32_t
  2. 8 已经是一个 int,所以不会发生转换
  3. (int32_t)参数[0]转换为INT_TYPE。
  4. 发生左移,结果类型为 INT_TYPE。

(请注意,如果参数 [0] 可能为负数,则移位将是未定义的行为。但由于它最初是无符号的,所以你在这里是安全的。)

a 表示这些步骤的结果。

a & (int32_t)0x0000ff00

  1. 0x000ff0 显式转换为 int32_t.
  2. 常规算术转换。两边都转换为INT_TYPE。结果类型为 INT_TYPE.

b 表示这些步骤的结果。

(((int32_t)argument[1]) & (int32_t)0x000000ff)

  1. 两个显式转换都发生了
  2. 常规算术转换已完成。双方现在INT_TYPE.
  3. 结果的类型为 INT_TYPE。

c 表示该结果。

b | c

  1. 常规算术转换;没有变化,因为它们都是 INT_TYPE.
  2. 结果的类型为 INT_TYPE。

结论

所以 none 的中间结果在这里是未签名的。 (另外,大多数显式转换都是不必要的,特别是如果您的系统上 sizeof(int) >= sizeof(int32_t) )。

此外,由于您从 uint8_ts 开始,永远不会移动超过 8 位,并且将所有中间结果存储在至少 32 位的类型中,因此前 16 位将始终为 0,并且值都将是 non-negative,这意味着有符号和无符号类型代表您可以在此处拥有的所有值 完全相同

您到底观察到什么让您认为它在应该使用有符号类型的地方使用无符号类型?我们可以看到示例输入和输出以及您期望的输出吗?

编辑: 根据您的评论,它没有按您预期的方式工作的原因似乎不是因为类型是 unsigned,而是因为您正在生成 16 位有符号的按位表示整数,但将它们存储在 32 位有符号整数中。摆脱除 (int32_t)argument[0] 以外的所有强制转换(并将它们更改为 (int)argument[0]int 通常是系统运行效率最高的大小,因此您的操作要使用int 除非您有特定原因使用其他尺寸)。然后将 final 结果转换为 int16_t.