如何在C语言中将Int类型的数据分成两个字节?

3

我正在使用嵌入式于极简硬件的软件,它仅支持ANSI C并且只有最少量的标准IO库。

我有一个整型变量,大小为两个字节,但我需要将其分别分成2个字节才能传输它,然后我可以通过读取这两个字节重新组装原始的整型。

我可以考虑将每个字节进行二进制分割,如下所示:

int valor = 522;  // 0000 0010 0000 1010 (entero de 2 bytes)
byte superior = byteSuperior(valor);  // 0000 0010
byte inferior = byteInferioror(valor);  // 0000 1010
...
int valorRestaurado = bytesToInteger(superior, inferior); // 522

但是我无法简单地通过它的权重将整体分割,并且让我感觉应该是微不足道的(例如通过位移),但我没有找到。

实际上,任何将整个分成2个字节并重新组装的解决方案都可以很好地为我服务。

先提前感谢您!


2
你是否正从一个字节序不同的系统发送数据到另一个系统?如果是,你可以在可用的系统上使用 htons()ntohs()。你可以使用 htons() 将一个两字节的 int 值转换为网络字节序,然后在接收时使用 ntohs() 将其转换回接收主机的主机字节序。 - Andrew Henle
不要被冗长而复杂的答案吓到。这基本上是一个简单的问题,编写能够在您的机器上100%工作的良好代码也很简单。选择其中一个涉及>>& 0xff的答案,根据我的答案的建议仔细测试,那么您应该没问题了。 - Steve Summit
@SteveSummit,强调“在你的机器上”;) 是的,这听起来很简单的东西在C语言中却是一个如此复杂的问题,这有点令人惊讶。我认为所有这些答案都很好,你应该了解这些事情,即使只是为了选择最适合你(唯一的)目标系统的实现定义方法 :) - user2371524
@FelixPalmen 我曾经是我所知道的最注重可移植性的程序员之一,但我想我在年纪大了之后变得软弱了。当然,在所有流行的CPU上,相同大小的有符号和无符号数通常会安静而正确地互相转换(有符号整数溢出和无符号整数溢出一样可预测)。 - Steve Summit
7个回答

7

这并不是一项“简单”的任务。

首先,在C语言中,byte的数据类型是char。在这里,您可能需要unsigned char,因为char可以是有符号或无符号的,它是实现定义的。

int是一个带符号的类型,这使得对它进行右移也是实现定义的。就C语言而言,int必须至少具有16位(如果char为8位,则为2个字节),但可以具有更多位数。但根据您的问题描述,您已经知道您的平台上int具有16位。在您的实现中使用此知识意味着您的代码针对该平台特定,并且不可移植。

在我看来,您有两个选择:

  1. You can work on the value of your int using masking and bit-shifting, something like:

    int foo = 42;
    unsigned char lsb = (unsigned)foo & 0xff; // mask the lower 8 bits
    unsigned char msb = (unsigned)foo >> 8;   // shift the higher 8 bits
    

    This has the advantage that you're independent of the layout of your int in memory. For reconstruction, do something like:

    int rec = (int)(((unsigned)msb << 8) | lsb );
    

    Note casting msb to unsigned here is necessary, as otherwise, it would be promoted to int (int can represent all values of an unsigned char), which could overflow when shifting by 8 places. As you already stated your int has "two bytes", this would be very likely in your case.

    The final cast to int is implementation-defined as well, but will work on your "typical" platform with 16bit int in 2's complement, if the compiler doesn't do something "strange". By checking first whether the unsigned is too large for an int (because the original int was negative), you could avoid this, e.g.

    unsigned tmp = ((unsigned)msb << 8 ) | lsb;
    int rec;
    if (tmp > INT_MAX)
    {
        tmp = ~tmp + 1; // 2's complement
        if (tmp > INT_MAX)
        {
            // only possible when implementation uses 2's complement
            // representation, and then only for INT_MIN
            rec = INT_MIN;
        }
        else
        {
            rec = tmp;
            rec = -rec;
        }
    }
    else
    {
        rec = tmp;
    }
    

    The 2's complement is fine here, because the rules for converting a negative int to unsigned are explicitly stated in the C standard.

  2. You can use the representation in memory, like:

    int foo = 42;
    unsigned char *rep = (unsigned char *)&foo;
    unsigned char first = rep[0];
    unsigned char second = rep[1];
    

    But beware whether first will be the MSB or LSB depends on the endianness used on your machine. Also, if your int contains padding bits (extremely unlikely in practice, but allowed by the C standard), you will read them as well. For reconstruction, do something like:

    int rec;
    unsigned char *recrep = (unsigned char *)&rec;
    recrep[0] = first;
    recrep[1] = second;
    

2
我相信这是目前为止最完整的答案,所以任何投反对票的人都可能需要解释一下他们的疑虑... - user2371524
1
@EricPostpischil 这可以通过一些方法解决,但在处理值位时无法解决。并且我在介绍中明确提到了“实现定义”,我不认为需要到处灌输这个概念。 - user2371524
介绍中指出 char 的符号性是实现定义的。这并不意味着答案中的代码依赖于从 unsignedint 的转换中实现定义的行为。 - Eric Postpischil
1
@EricPostpischil,介绍中提到了更多关于不可移植代码的内容。这种行为确实会让人们不再写回答。无论如何,我在这里甚至添加了完整的解释。 - user2371524
1
在新代码中:再次从msblsb构造的tmp可能为32768。然后,tmp = ~tmp + 1;tmp设置为32768,并且rec = tmp;尝试为int分配一个int无法表示的值。在这种情况下,执行转换,行为是实现定义的,而不是未定义的,根据6.3.1.3 3。OP没有说明实现定义的内容,因此我们无法知道此代码是否有效。 - Eric Postpischil
显示剩余12条评论

2
正如迄今为止的几个答案所示,有多种方法和一些令人惊讶的细微差别。
"数学"方法。您可以使用移位和掩码(或等效地使用除法和余数)将字节分离,并以类似的方式重新组合它们。这是Felix Palmen's answer中的"选项1"。这种方法的优点是完全独立于"字节序"问题。它的复杂性在于它受一些符号扩展和实现定义问题的影响。如果对于组合整数和方程式的每个字节分离部分都使用无符号类型,则最安全。如果使用有符号类型,则通常需要额外的强制转换和/或掩码。(但话虽如此,这是我更喜欢的方法)。
"内存"方法。您可以使用指针或 union 直接访问组成一个 int 的字节。这是Felix Palmen's答案中的"选项2"。这里非常重要的问题是字节顺序或"字节序"。此外,根据您的实现方式,您可能会违反"严格别名"规则
如果你使用“数学”方法,请确保在具有不同字节的高位设置和未设置的值上进行测试。例如,对于16位,完整的测试集可能包括值0x0101,0x0180,0x8001和0x8080。如果你没有正确编写代码(如果你使用有符号类型实现它,或者如果你遗漏了一些必要的掩码),你通常会发现额外的0xff会渗入重建结果中,从而损坏传输。(此外,你可能需要考虑编写一个正式的单元测试,以便可以最大化代码被重新测试的可能性,并在将其移植到影响其的不同实现选择的机器上检测到任何潜在的错误。)
如果您确实想要传输带符号的值,那么会有一些额外的复杂性。特别是,如果您在一个类型int大于16位的机器上重构16位整数,您可能需要显式地进行符号扩展以保留其值。同样,全面的测试应该确保您已经充分解决了这些问题(至少在您迄今为止测试代码的平台上)。
回到我建议的测试值(0x0101、0x0180、0x8001和0x8080),如果您传输无符号整数,它们对应于257、384、32769和32896。如果您传输带符号的整数,则它们对应于257、384、-32767和-32640。如果在另一端您得到像-693或65281(对应十六进制0xff01)这样的值,或者如果您得到了32896而预期的是-32640,这表明您需要回去更加仔细地处理您的带符号/无符号使用、掩码和/或显式符号扩展。
最后,如果您使用“内存”方法,并且您的发送和接收代码在不同字节顺序的机器上运行,您会发现字节被交换了。0x0102将变成0x0201。有各种方法可以解决这个问题,但可能会非常麻烦。(这就是为什么,如我所说,我通常更喜欢“数学”方法,这样我就可以绕过字节顺序问题。)

当然,还有一种混合方法。使用memcpy将数据复制到unsigned类型中,然后分离位并发送。接收时,从位组装一个unsigned类型,然后再使用memcpy复制到int类型中。 - Eric Postpischil

1

我甚至不需要编写函数来完成这个任务。这两个操作都是C语言位运算符的简单应用:

int valor = 522;
unsigned char superior = (valor >> 8) & 0xff;
unsigned char inferior = valor & 0xff;

int valorRestaurado = (superior << 8) | inferior;

尽管看起来很简单,但编写这样的代码时总会有一些微妙之处,很容易出错。例如,由于valor是带符号的,使用>>右移它是实现定义的,尽管通常意味着它可能会进行符号扩展或不进行符号扩展,这不会影响& 0xff所选择并分配给superior的字节的值。

此外,如果superiorinferior中的任何一个被定义为带符号类型,则在重构过程中可能会出现问题。如果它们比int小(当然必须如此),则在重构的其余部分发生之前,它们将立即被符号扩展为int,从而破坏结果。(这就是为什么我在示例中明确声明superiorinferiorunsigned char类型的原因。如果您的byte类型是unsigned char的typedef,那也可以。)

在子表达式 superior << 8 中,即使 superior 是无符号的,也存在一个隐蔽的溢出可能性,尽管在实践中不太可能引起问题。 (有关详细说明,请参见Eric Postpischil的评论。)


valor为负数时,valor >> 8的值是实现定义的。尽管在示例中valor为正数,但此代码并未为一般用途正确设计。此外,对于16位intsuperior << 8可能会溢出,在这种情况下,其行为不符合C标准的规定。 - Eric Postpischil
问题说明 int 是两个字节。假设是 8 位字节,则最大的 int 值为 32767。在这个答案中的代码中,superiorunsigned char。根据 C 2011 (N1570) 6.5.7 3,<< 的操作数会执行整数提升。根据 6.3.1.1 2,整数提升将 unsigned char 提升为 intsuperior 的值可以从 0 到 255。假设它是 128(或任何从 128 到 255 的值)。根据 6.5.7 4,如果 128 × 2^8 在 int 中无法表示,则 128 << 8 的行为是未定义的。由于 128 × 2^8 是 32768,它在 int 中无法表示。 - Eric Postpischil
另外,根据6.5.7 3,<<的结果类型是左操作数提升后的类型,因此它是int。因此,superior << 8尝试将一个unsigned char移位到int的高位。如果unsigned char的高位设置了,这将导致int值溢出。C标准通过数学定义有符号值的左移,而不是作为位操作,因此它会溢出而不是被定义为设置符号位。 - Eric Postpischil
superior * 256也会溢出。需要在更宽的类型中进行算术运算,或者根据不同的值使用不同的表达式进行条件化,或者实现其他解决方法。 - Eric Postpischil
在建议将数值乘以256的评论中,我还在括号中指出,如果这样做,我们将不得不重新开始使用显式有符号值来处理superior等变量,而非无符号。 - Steve Summit
显示剩余6条评论

1

假设一个int占用两个字节,每个字节的位数(CHAR_BIT)为8,并且使用二进制补码,那么名为valorint可以通过以下方式解构为端无关顺序:

unsigned x;
memcpy(&x, &valor, sizeof x);
unsigned char Byte0 = x & 0xff;
unsigned char Byte1 = x >> 8;

并且可以通过以下方式从 unsigned char Byte0unsigned char Byte1 重新组装:

unsigned x;
x = (unsigned) Byte1 << 8 | Byte0;
memcpy(&valor, &x, sizeof valor);

注:

  • intunsigned 在 C 2011 (N1570) 6.2.5 6 中具有相同的大小和对齐方式。
  • 在此实现中,unsigned 没有填充位,因为 C 要求 UINT_MAX 至少为 65535,因此所有 16 位都用于值表示。
  • intunsigned 在 6.2.6.2 2 中具有相同的字节序。
  • 如果实现不是二进制补码,则在同一实现中重新组装的值将恢复原始值,但负值将无法与使用不同符号位语义的实现进行互操作。

在计算Byte0和Byte1时,使用“& 0xff”可能更加一致,或者两者都不使用。 - Steve Summit

0

只需定义一个联合:

typedef union
{
   int           as_int;
   unsigned char as_byte[2];
} INT2BYTE;

INT2BYTE i2b;

将整数值放入i2b.as_int成员中,并从i2b.as_byte [0]i2b.as_byte [1]获取相应的字节等效值。

具有与使用“unsigned char”手动别名相同的含义(例如字节序,填充位)的内容 - user2371524
1
@FelixPalmen 谁说两端具有不同的字节序? - i486
1
我说过了吗?谁说这个平台没有填充位?我只是认为,在建议检查表示时应该有一些谨慎的话。 - user2371524
@SteveSummit的union方法是“另一种”方法。当许多答案解释了移位方法时,我该怎么办-提供其他方法还是再次重复经典的移位转换?而且这是char数组,不是char * - i486
@i486 不用担心,我并不是在说你的答案有什么问题。当我说“太糟糕了”时,我是为这个问题及其所有答案所走的路线感到遗憾,给人留下了移位和掩码技术可怕且应该避免的印象。我不相信它们是可怕的,但是在这里写越多的话,它们似乎就越可怕,所以我现在要停止写作了。 :-)(附:不,你没有使用char *,但使用字符指针是另一种获取int字节的方法。) - Steve Summit
显示剩余2条评论

0
您实际上可以将整数变量的地址转换为字符指针(准确地说是 unsigned char*),读取值,然后将指针递增以再次指向下一个字节来读取值。这符合别名规则。

-1

我使用int short而不是int来进行“干燥”操作,因为在我的目标平台上,int是2个字节,而在PC上则是4个字节。使用unsigned使调试更容易。

该代码可以使用GCC编译(并且几乎可以用任何其他的C编译器)。如果我没有错的话,这取决于体系结构是大端还是小端,但是通过反转重构整数的行应该可以解决问题:

#include <stdio.h>

    void main(){
    // unsigned short int = 2 bytes in a 32 bit pc
    unsigned short int valor;
    unsigned short int reassembled;
    unsigned char data0 = 0;
    unsigned char data1 = 0;

    printf("An integer is %d bytes\n", sizeof(valor));

    printf("Enter a number: \n");
    scanf("%d",&valor);
    // Decomposes the int in 2 bytes
    data0 = (char) 0x00FF & valor;
    data1 = (char) 0x00FF & (valor >> 8);
   // Just a bit of 'feedback'
    printf("Integer: %d \n", valor);
    printf("Hexa: %X \n", valor);
    printf("Byte 0: %d - %X \n", data0, data0);
    printf("Byte 1: %d - %X \n", data1, data1);
    // Reassembles the int from 2 bytes
    reassembled = (unsigned short int) (data1 << 8 | data0);
    // Show the rebuilt number
    printf("Reassembled Integer: %d \n", reassembled);
    printf("Reassembled Hexa: %X \n", reassembled);
    return;
}

1
提问者要求一份将有符号整型的字节分离的代码,但是这个答案给出了无符号短整型的代码,并没有解释如何将其用于整型或者短整型。由于 C 语言的有符号类型、位移和溢出的语义,对于有符号类型来说,改写这段代码容易出错,因此这是一个问题。例如,如果 data1 的高位被设置,则在具有 16 位整型类型的 C 实现中,data1 << 8 将会溢出。这是因为无符号字符 data1 将被提升为有符号的整型,所以移位操作将在有符号的整型类型中进行。 - Eric Postpischil
OP请求一个重新组装字节的解决方案,我已经完成了。 - Mochuelo
问题不在于此代码未提供重新组装两个字节的int的解决方案,正如问题所要求的那样。当请求int时,提供unsigned short的代码并不是一种解决方案。 - Eric Postpischil
我并没有看到他明确要求一个 int,正如你在 OP 中所读到的:“实际上,任何将整个分成2个字节并重新组合的解决方案都可以满足我的需求。” 我认为,老实说,它是一个 int 还是 short 并不重要。 - Mochuelo
我有一个 Int 变量,大小为两个字节,但我需要将其分成两个字节以便能够传输它,然后我可以读取这两个字节,重新组装原始的 Int。【强调添加】 - Eric Postpischil
显示剩余6条评论

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