在C语言中将int存储到char缓冲区中,然后检索相同的int。

9
我正在编写一个socket客户端-服务器应用程序,其中服务器需要向客户端发送一个大缓冲区,并且所有缓冲区都应该分别处理,所以我想把缓冲区长度放入缓冲区中,以便客户端可以从缓冲区读取数据的长度并进行相应的处理。
为了存储长度值,我需要将整数值分为一个字节的四部分,并将其存储在要发送到套接字的缓冲区中。我能够将整数分成四部分,但在合并时无法检索正确的值。为了演示我的问题,我编写了一个示例程序,在其中将int分成四个char变量,然后在另一个整数中将它们合并。目标是在合并后获得相同的结果。
这是我的小程序。
#include <stdio.h>

int main ()
{
    int inVal = 0, outVal =0;
    char buf[5] = {0};

    inVal = 67502978;

    printf ("inVal: %d\n", inVal);

    buf[0] = inVal & 0xff;
    buf[1] = (inVal >> 8) & 0xff;
    buf[2] = (inVal >> 16) & 0xff;
    buf[3] = (inVal >> 24) & 0xff;

    outVal = buf[3];
    outVal = outVal << 8;
    outVal |= buf[2];
    outVal = outVal << 8;
    outVal |= buf[1];
    outVal = outVal << 8;
    outVal |= buf[0];

    printf ("outVal: %d\n",outVal);
    return 0;
}

输出

输入值: 67502978 输出值: -126

我做错了什么?


3
可能是 整数溢出和未定义行为 的重复问题。 - LPs
你们的架构是否支持64位的整型变量? - Peter - Reinstate Monica
7个回答

19
一个问题是你在有符号数上使用了按位操作符。这总是一个坏主意,几乎总是不正确的。请注意,char具有实现定义的有符号性,而int始终是有符号的。
因此,你应该将int替换为uint32_t,char替换为uint8_t。使用这些无符号类型可以消除对负数进行位移的可能性,这将是一个错误。同样,如果你将数据移入已签名数字的符号位,就会得到错误。
不用说,如果整数大小不是4字节,代码也无法运行。

1
我的想法也是这样。通用算法思想是,如果我没有弄错的话,与字节顺序无关,因为位移运算符是与字节顺序无关的(或-不变的,或任何其他:左移始终是乘法,右移除法)。这是实现ntohl和其它类似函数而无需考虑架构字节顺序的方法。 - Peter - Reinstate Monica
我不理解"如果您在负数上使用位移,可能会出现错误"。如果他将类型更改为uint32_tuint8_t,他怎么可能有负数呢? - simon
@gurka 的重点是使用指定宽度的无符号类型来防止此类错误。由于你误解了,答案显然没有用最佳措辞。 - Daniel Fischer
不幸的是,C语言非常不一致和不合理。这里没有提到的是,即使使用uint8_t,由于C语言中的整数提升规则,您也可能得到负数。例如,~uint8 << n总是会引发未定义行为,而~uint32 << n则始终完全正常(在32位系统上)。这是因为前者等同于~(int)uint8 << n。避免这些微妙但灾难性的错误的关键是学习C语言中各种形式的隐式类型提升。 - Lundin

12

您的方法存在潜在的实现定义行为以及未定义行为:

  • 将值存储到类型为char的数组中,超出了类型char的范围,会产生实现定义的行为:buf[0] = inVal & 0xff;以及接下来的3个语句(如果默认情况下char类型是有符号的,则inVal & 0xff可能大于CHAR_MAX)。

  • 左移负值会引发未定义的行为:如果数组中前3个字节中的任何一个由于将大于CHAR_MAX的值存储到其中而成为负值,则结果outVal变为负值,对其进行左移是未定义的。

根据您的具体示例,您的体系结构使用二进制补码表示负值,并且char类型为有符号。存储到buf[0]中的值为67502978 & 0xff = 130,变成了-126。最后一条语句outVal |= buf[0];设置了outVal的第7到31位,结果为-126

您可以通过使用unsigned char数组和unsigned int类型的值来避免这些问题:

#include <stdio.h>

int main(void) {
    unsigned int inVal = 0, outVal = 0;
    unsigned char buf[4] = { 0 };

    inVal = 67502978;

    printf("inVal: %u\n", inVal);

    buf[0] = inVal & 0xff;
    buf[1] = (inVal >> 8) & 0xff;
    buf[2] = (inVal >> 16) & 0xff;
    buf[3] = (inVal >> 24) & 0xff;

    outVal = buf[3];
    outVal <<= 8;
    outVal |= buf[2];
    outVal <<= 8;
    outVal |= buf[1];
    outVal <<= 8;
    outVal |= buf[0];

    printf("outVal: %u\n", outVal);
    return 0;
}

请注意,上述代码仍然假定为32位整数。


实际上,它并不假定8位字节。它假设无符号字符可以至少容纳8位(这是标准保证的)。 (对于outval的计算,它还假定buf仅包含范围在0..255内的有效值。) - Martin Bonner supports Monica
该程序没有未定义的行为。由于在字符中存储的所有值都被0xff掩码处理,因此它也没有实现定义的行为。这些值在char中表示,并且保证至少有8位。 - Peter - Reinstate Monica
@PeterA.Schneider:在C语言中,它确实具有实现定义的行为:6.3.1.3有符号和无符号整数: 当将具有整数类型的值转换为除_Bool之外的另一种整数类型时,如果该值可以由新类型表示,则它不变。 2否则,如果新类型是无符号的,... 3否则,新类型是有符号的,且该值无法在其中表示;结果要么是实现定义的,要么是引发实现定义的信号。0xff掩码的值可能超出类型char的范围,而130确实超出了它的范围。 - chqrlie
@PeterA.Schneider:该程序对特定值inVal = 67502978不会引起未定义行为,但所使用的方法在许多其他值(例如inVal = 32768)时会出现未定义行为。 - chqrlie
为了避免疑义,例如使用具有固定宽度的std类型uint8_tuint32_t。作为嵌入式编码器,我只使用这些类型(从不仅使用int),因为我已经学到了这种假设最终会让你吃亏。 - John U

6

尽管对有符号值进行位移操作可能会存在问题,但本例并非如此(所有左侧的值都为正数,并且所有结果都在32位无符号整数的范围内)。

具有一定不直观语义的有问题的表达式是最后一个按位或操作:

outVal |= buf[0];

buf[0]是一个有符号字符,在你和我的架构上值为-126,这是因为在67502978的最低有效字节中最高位被设置了。在C语言中,算术表达式中的所有操作数都要经过算术转换。具体而言,它们要经过整型提升,规定为:“如果int类型可以表示原始类型的所有值[...],则将其转换为int”。因此,有符号字符buf[0]被转换为一个(有符号的)int保留其值为-126。负有符号int已经设置了符号位,与另一个有符号int ORing也会设置结果的符号位,使该值为负数。这正是我们所看到的。

将字节变成unsigned char可以解决问题,因为将无符号字符转换为临时整数的值是一个简单的8位值130。


4
使用 unsigned char buf[5] = {0};unsigned int 代替 inValoutVal,这样就可以正常工作了。
使用有符号整数类型时会出现两种问题:
首先,如果 buf[3] 是负数,则由于 outVal = buf[3],变量 outVal 变为负数;然后对 outVal 进行的位移操作是未定义的行为,详情请参见cppreference.com concerning bit shift operators

对于有符号和正数 a,如果 a << b 在返回类型中可表示,则其值为 a * 2b,否则行为未定义。(在 C++14 之前)对于能在返回类型的无符号版本中表示的 a << b 的值(然后转换为有符号类型:这使得创建 INT_MIN 合法),其值为 a * 2b,否则行为未定义。(自 C++14 起)

对于负数 a,a << b 的行为未定义。

需要注意的是,在 OP 的 inVal = 67502978 中不会出现这种情况,因为 buf[3]=4; 但对于其他的 inVal,可能会出现这种情况,从而导致“未定义行为”带来问题。
第二个问题是,在使用 outVal |= buf[0] 时,如果 buf[0]=-126,则将以二进制格式为 10000010 的值 (char)-126 转换为以二进制格式为 11111111111111111111111110000010 的值 (int)-126,然后再应用运算符 |=,这将导致 outVal 填充大量的 1 位。转换的原因在conversion rules for arithmetic operations (cppreference.com)中有定义:

如果两个操作数都是有符号的或都是无符号的,则具有较小转换级别的操作数将被转换为具有较大整数转换级别的操作数

因此,OP 的情况实际上不是由于任何未定义行为引起的,而是由于字符 buf[3] 是负值,这在执行 |= 操作之前被转换为 int

请注意,如果buf[2]或者buf[1]是负数,这将使outVal为负数,并会导致后续的移位操作产生未定义的行为。


你在哪里看到整数溢出操作?所有的移位都是在(据我所知没有溢出的)int上进行的。 - Peter - Reinstate Monica
2
这不是整数溢出,而是直接操作符号位。根据C11 6.5.7/4,这是未定义的行为:“E1 << E2的结果是将E1左移E2个位置;如果E1具有带符号类型和非负值,并且E1×2E2可以在结果类型中表示,则该值是结果;否则,行为未定义。” - Lundin
@PeterA.Schneider - 请忽略我的评论。那是错误的。 - Martin Bonner supports Monica
buf[3] 不是负数;对于32位小端,它为4。当移位时,outval 永远不会是负数。你的答案完全错误。 - Peter - Reinstate Monica
如果OP关心in/out值中的最高位,我也建议使用“unsigned int”来表示inVal和outVal。 - bronekk
显示剩余6条评论

4

C++标准N3936关于移位运算符的引用:

E1 << E2的值是将E1左移E2位; 空出的位填充为零。

如果E1具有无符号类型,

则结果的值为E1 × 2^E2, 对结果类型最大可表示值加一取模后得到。

否则,如果E1具有带符号类型并且非负值,

并且E1 × 2^E2可以在结果类型的相应无符号类型中表示,则该值转换为结果类型即为结果值;否则,行为是未定义的

因此,为避免未定义的行为,建议使用无符号数据类型,并确保数据类型长度为64位


1
嗯,我看到每次移位之前 outval 的以下值:(32 位小端整数):4;1030;263680。它们都不是负数,并且结果都在范围内,因此它们都是定义良好的。最后一次移位后的值为 67502848。是最后一个 OR 运算创建了负值。 - Peter - Reinstate Monica

3
这可能是一个糟糕的想法,但我会在这里发布它以供大家参考 - 您可以使用 联合体
union my_data
{
    uint32_t one_int;

    struct
    {
        uint8_t  byte3;
        uint8_t  byte2;
        uint8_t  byte1;
        uint8_t  byte0;
    }bytes;
};


// Your original code modified to use union my_data
#include <stdio.h>

int main(void) {
    union my_data data;
    uint32_t inVal = 0, outVal = 0;
    uint8_t buf[4] = {0};

    inVal = 67502978;

    printf("inVal: %u\n", inVal);

    data.one_int = inVal;

    // Populate bytes into buff    
    buf[3] = data.bytes.byte3;
    buf[2] = data.bytes.byte2;
    buf[1] = data.bytes.byte1;
    buf[0] = data.bytes.byte0;

    return 0;
}

我不知道这个方法是否可行,但我认为应该是可行的:

union my_data
{
    uint32_t one_int;
    uint8_t  bytes[4];
};

1
+1 这是最佳的做法。虽然它既不可移植,也是未定义行为,但任何假设可以在int和四个chars之间进行转换的东西都是如此。然而,我会结合Jim D.的答案来确保大小端正确性。 - Jack Aidley
另外,我相信你在代码中出现了错误,从outVal = data.bytes.byte3开始等。 - Jack Aidley
谢谢Jack,发现得好。我已经更新了代码以反映手头的任务(从inVal中将字节放入buf)。 - John U
这正是联合体不应该使用的地方。无论如何,您忘记了#pragma pack 1。或者, uint8_t [4]比4个独立字段更安全,因为它避免了打包问题。 - Agent_L
我确实说过这可能是个糟糕的想法,所以我是正确的。 - John U

2

由于架构之间存在字节序差异,最佳实践是将数字值转换为 网络字节序,即大端字节序。接收时,可以将它们转换为本机主机字节序。我们可以通过使用 htonl()(主机到网络“长” = uint32_t)以可移植的方式进行转换,并在接收时使用 ntohl() 进行主机字节序转换。例如:

#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char **argv) {
  uint32_t inval = 67502978, outval, backinval;

  outval = htonl(inval);
  printf("outval: %d\n", outval);
  backinval = ntohl(outval);
  printf("backinval: %d\n", backinval);
  return 0;
}

这是在我的64位x86上的结果,它是小端的:
$ gcc -Wall example.c
$ ./a.out
outval: -2113731068
backinval: 67502978
$

2
虽然@chqrlie有一定的观点,但强制指向“htonl”及其相关内容也值得+1。我怀疑,如果人们停止尝试重新实现“htonl”等,我的家庭中的许多嵌入式系统将避免许多自发重置。传播最佳实践是人类的一项服务。特别是因为这些尝试通常不是能力的标志,因此注定会失败。 - Peter - Reinstate Monica
2
我真的非常非常不喜欢 htonl 等函数的接口。 htonl 应该返回一个八位字节数组,而不是一个整数(我可以想象在某些平台上,对于一个有效的整数,调用 htonl 可能会返回一个陷阱值)。 - Martin Bonner supports Monica
@MartinBonner 我做不到。如果有填充或其他应该定义值(例如0)的情况,htonl 应该确保符合该体系结构;在这种棘手的环境中,任何业余 DIY 尝试都会更容易失败。 - Peter - Reinstate Monica
1
一个小端的补码或反码机器,其中负零是一个陷阱。一个0x00000080会变成大端的0x80000000,这是陷阱值。 - Martin Bonner supports Monica
1
虽然它被称为htonl(),但它并不返回int类型,而是返回uint32_t类型。我预期在所有架构上,0x80000000都是一个有效的无符号32位整数。 - JimD.
显示剩余5条评论

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