如何知道Union中哪个变量被使用了?

16

如果我声明一个Union如下:

union TestUnion
{
    struct 
    {
      unsigned int Num;
      unsigned char Name[5];
    }TestStruct;
    unsigned char Total[7];
};
现在,我如何知道是使用了Total[7]还是TestStruct?
我正在使用C语言!我重新学习了联合体和结构体,这个问题就浮现在我的脑海中。sizeof不能用,因为它们的大小相同,都是7个字节。这里又出现了另一个问题。
当我只将"Total"填充为字符'a'并尝试sizeof(TestUnionInstance)时,它返回12(字符的大小是1个字节,对吗?)。所以我将结构体与其分离,发现结构体的大小为12个字节,而不是5+2=7个字节……奇怪!!有人能解释一下吗?
附注:我正在使用Visual Studio 2008。
6个回答

22
你不能这样做。这是 union 的一个特点。
如果你需要辨别,可以使用所谓的 tagged union。一些语言内置了对此的支持,但在 C 中,你需要自己实现。基本思路是在 union 中包含一个标记,你可以用它来判断其版本,例如:
enum TestUnionTag {NUM_NAME, TOTAL};

struct {
    enum TestUnionTag tag;
    union {
        struct {
            unsigned int Num;
            unsigned char Name[5];
        } TestStruct;
        unsigned char Total[7];
    } value;
} TestUnion;

在您的代码中,确保始终设置标记以说明联合体的使用方式。

关于sizeof:结构体的大小为12个字节,因为有4个字节用于int(大多数现代编译器都有4字节的int,与长整型相同),然后是三个字节的填充和五个字节的字符(我不知道填充是在字符之前还是之后)。填充存在是为了使结构体成为一个完整的单词长度,以便内存中的所有内容都保持在单词边界上对齐。由于结构体的长度为12个字节,所以联合体必须有12个字节长才能容纳它;联合体的大小不会根据其中的内容而改变。


2
填充位出现在char之后,而不是之前。严格来说,实现可以在它之前放置填充位,但是如果你从char [5]更改为char[6],它还必须在它之前放置相同数量的填充位,这将非常荒谬。 C要求具有共同初始元素序列的结构是兼容的。 - R.. GitHub STOP HELPING ICE

6

使用的成员是您最后写信的那个; 其他成员不可用。 您知道您上次写信给哪个成员,对吧? 毕竟,编程是您写的 :-)


至于您的次要问题:编译器允许在结构体中插入“填充字节”以避免未对齐的访问并使其性能更佳

以下是可能在您的结构体内部字节分布的示例
序号 |名称 |填充 - - - -|- - - - -|x x x 0 1 2 3|4 5 6 7 8|9 a b

1
+1 是因为您花时间展示了对象的内存布局。 - Chris Lutz
1
有时候你只需要访问其他成员。联合的一个重要功能是为同一数据提供不同的视图。 - thkala
1
关于“禁止访问”的评论,需要注意的是,就编译器而言,没有任何限制。程序员必须手动执行任何此类策略。 - thkala
2
@thkala - 标准将此行为定义为未定义的,因此除非您明确依赖于平台相关的行为,否则它是禁止的。实际上,我知道 int is_little_endian(void) { union { int i; char c[sizeof(int)]; } u; u.i = 1; return u.c[0] == 1; } 将会做什么,但按照标准,该代码是UB并且是有害的。 - Chris Lutz
它在字节序和结构内存布局方面是未定义的。已定义的是,联合体的所有“备选项”都将从相同的地址开始,这使得知道自己在做什么的人可以执行小奇迹 :-) - thkala
访问不同的联合成员而非最后一次写入的成员,其实现是由编译器定义的(参考6.5.2.2/5)。 - pmg

4
简短回答:除了在联合体外部的结构体中添加枚举变量之外,没有其他方法。
enum TestUnionPart
{
  TUP_STRUCT,
  TUP_TOTAL
};

struct TestUnionStruct
{
  enum TestUnionPart Part;
  union
  {
    struct
    {
      unsigned int Num;
      unsigned char Name[5];
    } TestStruct;
    unsigned char Total[7];
  } TestUnion;
};

现在您需要控制联合的创建,以确保枚举正确设置,例如使用类似以下函数的函数:
void init_with_struct(struct TestUnionStruct* tus, struct TestStruct const * ts)
{
  tus->Part = TUP_STRUCT;
  memcpy(&tus->TestUnion.TestStruct, ts, sizeof(*ts));
}

现在,对于正确的值进行分发只需要一个开关:

void print(struct TestUnionStruct const * tus)
{
  switch (tus->Part)
  {
    case TUP_STRUCT:
      printf("Num = %u, Name = %s\n",
             tus->TestUnion.TestStruct.Num,
             tus->TestUnion.TestStruct.Name);
      break;
    case TUP_TOTAL:
      printf("Total = %s\n", tus->TestUnion.Total);
      break;
    default:
      /* Compiler can't make sure you'll never reach this case */
      assert(0);
  }
}

作为一则旁注,我想提到这些结构最好在ML语言家族中处理。
type test_struct = { num: int; name: string }
type test_union = Struct of test_struct | Total of string

2

首先,现在大多数架构上的sizeof(int)都是4。如果您想要2,请查看short或C99中stdint.h头文件中的int16_t

其次,C使用填充字节来确保每个struct对齐到字边界(4)。因此,您的结构体看起来像这样:

+---+---+---+---+---+---+---+---+---+---+---+---+
|      Num      |   N   a   m   e   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+

结尾处有3个字节。否则,数组中的下一个structNum字段将处于尴尬对齐的位置,这会使访问效率降低。
第三,联合体的sizeof将是其最大成员的sizeof。即使没有使用所有空间,sizeof也会返回最大的结果。
正如其他答案所提到的,您需要一些其他方式(如enum)来确定使用联合体的哪个字段。

1

无法确定。您应该有一些额外的标志(或其他与您的联合体外部相关的手段),以表明实际使用的联合体部分。


0

将联合体与枚举一起使用以确定存储的内容的另一个示例。我发现这样更加清晰明了。

from: https://www.cs.uic.edu/~jbell/CourseNotes/C_Programming/Structures.html

作者: 约翰·贝尔博士


为了知道实际存储的联合字段,通常将联合体嵌套在结构体中,并使用枚举类型指示实际存储的内容。例如:
typedef struct Flight {
    enum { PASSENGER, CARGO } type;
    union {
        int npassengers;
        double tonnages;  // Units are not necessarily tons.
    } cargo;
} Flight;

Flight flights[ 1000 ];

flights[ 42 ].type = PASSENGER;
flights[ 42 ].cargo.npassengers = 150;

flights[ 20 ].type = CARGO;
flights[ 20 ].cargo.tonnages = 356.78;

是的!Tom Anderson 的回答被接受,并且说的完全一样。 - Swanand

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