Visual Studio C++ 位域结构体大小问题

Posted

技术标签:

【中文标题】Visual Studio C++ 位域结构体大小问题【英文标题】:Visual Studio C++ bitfield struct size problem 【发布时间】:2018-11-29 07:35:15 【问题描述】:

我已经使用打包属性在 GCC 中创建并编译了以下代码,它按预期工作。但在 Visual Studio 中的结果与 GCC 不同。

#pragma pack(push, 1)
typedef struct

    uint8_t TargetID: 6;
    enum_OPCode OPCode: 3;
    uint8_t CRC7: 7;
 struct_commDataPack_request;
#pragma pack(pop)

如您所见,所有元素的大小必须为 16bits = 2Bytes,这在 GCC 中是正确的,但在 Visual Studio 中它返回 3Bytes!如果我将它的大小减小到 15 位,它将返回 2 字节。

我该如何解决?

【问题讨论】:

请发minimal reproducible example。 我每天都在 VS 上工作。关于位域,我曾经意识到,当基类型不同时,位域不会打包在一起。 IE。 struct Pack unsigned a: 3; bool b: 1; ; 需要 2 个字节而不是预期的 1 个字节。这也可能是这里的问题。不幸的是,我不能说这方面的标准是什么。 你写 "如果我将它的大小减小到 15bits,它将返回 2Bytes":你如何减小它?请edit您的问题并添加相应的代码。同时告诉我们enum_OPCode到底是什么。 你用的是哪个版本的VS?使用您的示例(和enum enum_OPCode OPCode1, OPCode2, OPCode3, OPCode4, OPCode5, OPCode6, NOPCodes ;),我在 VS2013 中获得了 6 个字节(尝试了 Debug 和 Release )。将enum_OPCode 替换为uint8_t,我仍然得到3。 不要使用位域或打包。位字段过于依赖于实现,并且打包只是非标准的。在这种情况下,将存储实现为单个 uint16_t,并为使用位移位和掩码实现的每个字段编写 getter 和 setter。 【参考方案1】:

嗯,打包太依赖平台了,不能真正依赖它。

在 VS 中,当您执行“pack(push, 1)”时,您实际上将填充设置为 8 位,并且如您所见,以三个字节结束(其中 6 位是填充)。

但是 GCC 上的填充(如果你使用 __attribute__((packed)) 或它的一些别名)可以完全关闭。这就是为什么您只看到 2 个字节。

为了保持可移植性,您为什么不编写自己的小型序列化例程?像这样的:

struct_commDataPack_request s;
short wire = s.TargetID | (s.OPCode<<6) | (s.CRC7 << 9);

【讨论】:

这与我刚刚在 VS2013 中观察到的完全一致。 是的,小错字 :) 好吧,在 VS 中,您会在 OPCode 中获得 5 个额外的填充位,并在 CRC7 末尾获得 1 个额外的位。我玩过它,似乎 8 位是最小可能的填充!【参考方案2】:

我用这段代码在VS2017中复现:

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

typedef enum  A,B,C  enum_OPCode;

#pragma pack(push, 1)
typedef struct

  uint8_t TargetID : 6;
  uint8_t OPCode : 3;
  uint8_t CRC7 : 7;
 struct_commDataPack_request;
#pragma pack(pop)

int main()

  printf("%zd\n", sizeof(struct_commDataPack_request));

这里的大小是3。

但是当我改变时

uint8_t OPCode : 3;
uint8_t CRC7 : 7;

uint8_t OPCode : 2;
uint8_t CRC7 : 8;

(整体大小保持16位),大小为2。

如前所述,最好是编写自己的序列化/反序列化。

【讨论】:

我认为 Visual Studio 不会将 OPCode : 3 分配给两个不同的 uint8_t。这就是 OPCode : 2 减小大小的原因,因为现在 OPCode 适合保存 TargetID 的 uint8_t。 @AndreasH。这是有道理的。 在 cppreference.com 上找到了一段文字:“[...] 在某些平台上,位字段不跨越字节,而在其他平台上却可以”。所以使用 uint16_t 应该可以解决您的问题。【参考方案3】:

如MS document中所述:

位域的基础类型必须是整数类型。 如果位字段会溢出声明类型的边界(在您的情况下是 uint8_t),则会分配新的存储单元。

解决问题的一种方法是使用具有更大边界的声明类型 (uint16_t)。

这是我使用的代码:

#include "pch.h"
#include <stdio.h>
#include <stdint.h>

typedef enum  A, B, C  enum_OPCode;

#pragma pack(push, 1)
typedef struct

    uint16_t TargetID : 6;
    uint16_t OPCode : 3;
    uint16_t CRC7 : 7;
 struct_commDataPack_request;
#pragma pack(pop)

int main()

    struct_commDataPack_request packet;

    packet.TargetID = 0;
    packet.OPCode = 7;
    packet.CRC7 = 0;

    unsigned char * pData = (unsigned char *)&packet;

    printf("Packet size : %zd\n", sizeof(struct_commDataPack_request));
    for (int i = 0; i < sizeof(packet); i++) 
        printf("byte %d is [%02X]\n", i, pData[i] );
    

结果:

Packet size : 2
byte 0 is [C0]
byte 1 is [01]

(0x01C0 位:0000 0001 1100 0000)

【讨论】:

以上是关于Visual Studio C++ 位域结构体大小问题的主要内容,如果未能解决你的问题,请参考以下文章

关于位域在结构体的应用

C语言结构体中冒号的作用——位域

C语言中位域大小与宽度该怎么算?

c语言中使用结构体位段的结构体大小

Visual Studio 调试中将结构体指针转换为结构体数组查看

如何通过 Visual Studio COM 包装器(或其他方式)在 C# 中使用 C++ 属性联合