我知道从无符号类型转换为等等级的有符号类型会产生一个实现定义的值:
C99 6.3.1.3:
- 否则,新类型是有符号的,值不能在其中表示;结果是实现定义的,或者引发了实现定义的信号。
这意味着我不知道如何对有符号数字进行字节交换。例如,假设我从外围设备以小端顺序接收两个字节、二进制补码的有符号值,并在大端 CPU 上处理它们。C 库中的字节交换原语(如ntohs
)被定义为处理无符号值。如果我将我的数据转换为无符号数据以便可以对其进行字节交换,那么之后如何可靠地恢复有符号值?
正如您在问题中所说的那样,结果是实现定义的或产生了实现定义的信号- 即取决于平台/编译器会发生什么。
要对有符号数进行字节交换,同时尽可能避免实现定义的行为,您可以使用更广泛的有符号中间体,它可以表示具有与您想要的有符号值相同的宽度的无符号类型的整个范围字节交换。以 little-endian 16 位数字为例:
// Code below assumes CHAR_BIT == 8, INT_MAX is at least 65536, and
// signed numbers are twos complement.
#include <stdint.h>
int16_t
sl16_to_host(unsigned char b[2])
{
unsigned int n = ((unsigned int)b[0]) | (((unsigned int)b[1]) << 8);
int v = n;
if (n & 0x8000) {
v -= 0x10000;
}
return (int16_t)v;
}
这就是它的作用。首先,它将 little-endian 值转换b
为 host-endian 无符号值(无论主机实际上是哪种字节序)。然后它将该值存储在一个更广泛的有符号变量中。它的值仍在 [0, 65535] 范围内,但它现在是一个有符号数量。因为int
可以表示该范围内的所有值,所以转换完全由标准定义。
现在到了关键的一步。我们测试无符号值的高位,即符号位,如果为真,我们从有符号值中减去 65536 (0x10000)。这将范围 [32768, 655535] 映射到 [-32768, -1],这正是二进制补码有符号数的编码方式。这仍然在更广泛的类型中发生,因此我们保证该范围内的所有值都是可表示的。
最后,我们将更宽的类型截断为int16_t
. 此步骤涉及不可避免的实现定义的行为,但很可能,您的实现将其定义为按您预期的方式运行。在极不可能发生的情况下,您的实现使用符号和大小或补码表示有符号数,值 -32768 将被截断破坏,并可能导致程序崩溃。我不会担心它。
当您没有可用的 64 位类型时,另一种方法可能对字节交换 32 位数字有用,它是屏蔽符号位并单独处理它:
int32_t
sl32_to_host(unsigned char b[4])
{
uint32_t mag = ((((uint32_t)b[0]) & 0xFF) << 0) |
((((uint32_t)b[1]) & 0xFF) << 8) |
((((uint32_t)b[2]) & 0xFF) << 16) |
((((uint32_t)b[3]) & 0x7F) << 24);
int32_t val = mag;
if (b[3] & 0x80) {
val = (val - 0x7fffffff) - 1;
}
return val;
}
我在(val - 0x7fffffff) - 1
这里写了,而不是仅仅val - 0x80000000
,以确保减法发生在有符号类型中。
我知道从无符号类型转换为具有相同等级的有符号类型会产生一个实现定义的值。
它将是实现定义的,只是因为 C 中的签名格式是实现定义的。例如,二进制补码就是这样一种实现定义的格式。
所以这里唯一的问题是,如果传输的任何一方都不是二进制补码,这在现实世界中不太可能发生。我不会费心将程序设计为可移植到黑暗时代的晦涩、已灭绝的计算机。
这意味着我不知道如何对有符号数字进行字节交换。例如,假设我正在从外围设备以 little-endian 顺序接收两个字节、二进制补码的有符号值,并在 big-endian CPU 上处理它们
我怀疑这里的一个混淆来源是你认为一个通用的二进制补码将从一个大端或小端的发送者发送,并由一个大端或小端的发送者接收。数据传输协议不是这样工作的:它们明确指定字节序和签名格式。所以双方都必须适应协议。
一旦指定,这里真的没有火箭科学:你收到 2 个原始字节。将它们存储在原始数据数组中。然后将它们分配给您的补码变量。假设协议指定小端:
int16_t val;
uint8_t little[2];
val = (little[1]<<8) | little[0];
位移位具有与字节序无关的优点。所以无论你的 CPU 是大是小,上面的代码都可以工作。因此,尽管这段代码包含大量丑陋的隐式促销,但它是 100% 可移植的。C保证将上述内容视为:
val = (int16_t)( ((int)((int)little[1]<<8)) | (int)little[0] );
移位运算符的结果类型是其提升的左操作数的结果类型。| 结果类型 是平衡型(通常的算术转换)。
移动带符号的负数会产生未定义的行为,但我们可以避免这种移动,因为单个字节是无符号的。当他们被隐式提升时,这些数字仍然被视为正数。
并且由于int
保证至少为 16 位,因此代码将在所有 CPU 上运行。
或者,您可以使用完全排除所有隐式促销/转换的迂腐风格:
val = (int16_t) ( ((uint32_t)little[1] << 8) | (uint32_t)little[0] );
但这是以可读性为代价的。