结构体和联合体的区别

466
有没有一个好的例子可以说明structunion之间的区别? 基本上我知道struct使用其成员的所有内存,而union使用最大成员的内存空间。在操作系统层面上还有其它的区别吗?
17个回答

751

使用 union 时,你只需要使用其中的一个元素,因为它们都存储在同一位置。这使得当你想要存储多种类型中的一种时非常有用。另一方面,结构体对于其每个元素都有单独的内存位置,因此它们都可以同时使用。

为了给出一个具体的使用示例,我之前正在开发一个 Scheme 解释器,实际上是将 Scheme 数据类型覆盖到 C 数据类型上。这涉及到在结构体中存储表示值类型的枚举和一个 union 来存储该值。

union foo {
  int a;   // can't use both a and b at once
  char b;
} foo;

struct bar {
  int a;   // can use both a and b simultaneously
  char b;
} bar;

union foo x;
x.a = 3; // OK
x.b = 'c'; // NO! this affects the value of x.a!

struct bar y;
y.a = 3; // OK
y.b = 'c'; // OK
编辑: 如果你想知道将x.b设置为'c'会改变x.a的值,严格来说是未定义的。在大多数现代计算机上,char是1个字节,int是4个字节,因此给x.b赋值'c'也会使x.a的第一个字节具有相同的值:
union foo x;
x.a = 3;
x.b = 'c';
printf("%i, %i\n", x.a, x.b);

打印
99, 99

为什么这两个值相等?因为整数3的最后3个字节都是零,所以也可以被解释为99。如果我们给x.a一个更大的数,你会发现这并不总是成立:
union foo x;
x.a = 387439;
x.b = 'c';
printf("%i, %i\n", x.a, x.b);

打印

387427, 99

为了更好地查看实际的内存值,让我们将值设置为十六进制并打印出来:
union foo x;
x.a = 0xDEADBEEF;
x.b = 0x22;
printf("%x, %x\n", x.a, x.b);

打印

deadbe22, 22

你可以清楚地看到0x22覆盖了0xEF的位置。

但是

在C语言中,一个int类型中字节的顺序未定义。这个程序在我的Mac电脑上用0x22覆盖了0xEF,但是在其他平台上,它可能用相同的方法覆盖0xDE,因为组成int类型的字节的顺序会被反转。因此,在编写程序时,永远不要依赖于联合中覆盖特定数据的行为,因为它是不可移植的。

有关字节顺序的更多阅读,请查看字节顺序


1
使用这个例子,在 union 中,如果 x.b='c',x.a 中存储了什么?是字符的引用号码吗? - kylex
1
希望这更详细地解释了当设置x.b时存储在x.a中的内容。 - Kyle Cronin
1
@KyleCronin 我明白了。在你的情况下,你有一组类型,知道你只需要使用其中一个,但直到运行时你才知道具体是哪个 - 所以联合类型允许你这样做。谢谢 - user12345613
2
@user12345613 联合体可以用作结构体的一种基类。您可以使用结构体的联合体来模拟面向对象的层次结构。 - Morten Jensen
1
@Lazar 多字节类型中的字节顺序取决于字节序。我建议阅读维基百科上的相关文章。 - Kyle Cronin
显示剩余6条评论

95

这里是简短的答案: 结构体是一种记录结构:结构体中的每个元素都会分配新的空间。因此,像下面这样的结构体

struct foobarbazquux_t {
    int foo;
    long bar;
    double baz; 
    long double quux;
}

每个实例在内存中至少分配了(sizeof(int)+sizeof(long)+sizeof(double)+sizeof(long double))字节。这是因为体系结构对齐限制可能会强制编译器填充结构体。(“至少”是因为体系结构对齐限制可能会强制编译器填充结构体。)

另一方面,

union foobarbazquux_u {
    int foo;
    long bar;
    double baz; 
    long double quux;
}

分配一个内存块并给它四个别名。因此,sizeof(union foobarbazquux_u) ≥ max((sizeof(int),sizeof(long),sizeof(double),sizeof(long double)),再加上对齐的可能性。


64

有没有一个好的例子可以说明'struct'和'union'之间的区别?

一个想象中的通信协议

struct packetheader {
   int sourceaddress;
   int destaddress;
   int messagetype;
   union request {
       char fourcc[4];
       int requestnumber;
   };
};

在这个虚构的协议中,规定了基于“消息类型”,标头中的以下位置将是请求编号或四个字符代码之一,但不会同时出现。 简而言之,联合允许同一存储位置表示多个数据类型,其中保证您每次只想存储一种数据类型。

联合通常是 C 语言作为系统编程语言的遗产中的低级细节,有时会以这种方式使用“重叠”的存储位置。 当您有一个数据结构,在其中仅保存其中几种类型之一时,有时可以使用联合来节省内存。

总的来说,操作系统并不关心结构体和联合体 - 对它们而言,它们都只是一块内存块。 结构体是存储多个数据对象的内存块,其中这些对象不重叠。 联合体是存储多个数据对象的内存块,但仅具有这些对象中最大的存储空间,因此一次只能存储一个数据对象。


2
假设您有一个 packetheader ph;,如何访问 requestnumber?是 ph.request.requestnumber 吗? - justin.m.chase

39

正如您在问题中所提到的,unionstruct的主要区别在于union成员重叠彼此的内存空间,因此union的大小是其中最大成员的大小,而struct成员则一个接一个地排列(之间可以选择性地添加填充)。此外,一个 union 大到足以容纳其所有成员,并具有适合其成员的对齐方式。假设说,int只能存储在 2 字节地址上,宽度为 2 字节,long只能存储在 4 字节地址上,长度为 4 字节。以下是一个 union:

union test {
    int a;
    long b;
}; 

一个sizeof大小可以为4,并且对齐需求为4。 union和struct在结尾处都可以有填充,但在开头不能有。写入struct仅更改已写入成员的值。写入union的成员将使所有其他成员的值无效。如果之前没有写入它们,则无法访问它们,否则行为未定义。 GCC作为扩展提供了一种方法,即使您最近没有写入它们,您也可以实际上从union的成员中读取。对于操作系统来说,用户程序是向联合体还是结构体写入并不重要。这实际上只是编译器的问题。

另一个重要的union和struct的属性是它们允许指向它们的指针指向其任何成员类型。因此,以下是有效的:

struct test {
    int a;
    double b;
} * some_test_pointer;

some_test_pointer可以指向int*double*。如果您将类型为test的地址强制转换为int*,它将实际上指向其第一个成员a。同样,联合也是如此。因此,由于联合始终具有正确的对齐方式,您可以使用联合使指向某些类型变得有效:

union a {
    int a;
    double b;
};

该联合体实际上能够指向一个int和一个double:

union a * v = (union a*)some_int_pointer;
*some_int_pointer = 5;
v->a = 10;
return *some_int_pointer;    

根据C99标准,实际上是有效的

对象的存储值只能被具有以下类型之一的lvalue表达式访问:

  • 与对象的有效类型兼容的类型
  • ...
  • 包含上述类型之一的聚合或联合类型中的一个成员

编译器不会优化掉v->a = 10;,因为它可能会影响*some_int_pointer的值(函数将返回10而不是5)。


20

union在一些场景中非常有用。比如,union可以用于非常低级别的操作,例如为内核编写设备驱动程序。

一个例子是通过使用structfloat的位域的union来分解float数值。我将一个数保存在float中,稍后可以通过那个struct访问float的特定部分。该示例展示了如何使用union从不同角度查看数据。

#include <stdio.h>                                                                                                                                       

union foo {
    struct float_guts {
        unsigned int fraction : 23;
        unsigned int exponent : 8;
        unsigned int sign     : 1;
    } fg;
    float f;
};

void print_float(float f) {
    union foo ff;
    ff.f = f;
    printf("%f: %d 0x%X 0x%X\n", f, ff.fg.sign, ff.fg.exponent, ff.fg.fraction);

}

int main(){
    print_float(0.15625);
    return 0;
}

请查看维基百科上有关单精度的描述,我使用了那里的例子和魔数0.15625。


union也可用于实现具有多个备选项的代数数据类型。我在 O'Sullivan、Stewart 和 Goerzen 的《Real World Haskell》书中找到了一个例子。请在“区分联合”部分查看。

干杯!


12

是的,结构体和联合体之间的主要区别与您所述的相同。

结构体使用其成员的所有内存,而联合使用最大成员的内存空间。

但所有的区别都在于内存的使用需求。联合的最佳用法可以在 Unix 进程中看到,我们在其中利用信号,例如一个进程一次只能对一个信号进行操作。因此,一般声明将为:

union SIGSELECT
{
  SIGNAL_1 signal1;
  SIGNAL_2 signal2;
  .....
};
在这种情况下,过程仅使用所有信号中最高的内存。但是,如果您在此情况下使用结构,则内存使用量将是所有信号的总和。 这会产生很大的差异。
总之,如果您知道一次只访问一个成员,则应选择Union。

11

非技术性地讲意味着:

假设:椅子=内存块,人=变量

结构体:如果有3个人,他们可以相应地坐在大小合适的椅子上。

联合体:如果有3个人,只有一个椅子可供使用,当他们想要坐下时,所有人都需要使用相同的椅子。

技术性地讲意味着:

下面的程序深入探讨了结构体和联合体。

struct MAIN_STRUCT
{
UINT64 bufferaddr;   
union {
    UINT32 data;
    struct INNER_STRUCT{
        UINT16 length;  
        UINT8 cso;  
        UINT8 cmd;  
           } flags;
     } data1;
};

对于缓冲区地址和联合体的sizeof(UNIT32),再加上取决于处理器架构的填充位32位,主要结构的总大小为128位。

对于结构体,所有成员都会连续地得到内存块。

联合体获取一个最大成员(这里是32位)的内存块。在联合体内部还有一个结构体(INNER_STRUCT),其成员获得总大小为32位(16+8+8)的内存块。在联合体中,可以访问INNER_STRUCT(32位)成员数据(32位)。


11
"union" 和 "struct" 是 C 语言的构造。如果使用其中一个关键字,编译器会生成不同的代码。因此,在它们之间谈论「操作系统级别」的差异是不合适的。请注意保留原文中的 constructscompiler 标签。

11

你已经拥有它了,这就是全部。

那么,联合的要点是什么?

你可以在同一个位置存储不同类型的内容。你必须知道你存储在联合中的类型(所以通常把它放在具有类型标记的struct中...)。

为什么这很重要?这并不是为了节省空间。是的,你可以节省一些位或进行一些填充,但这不再是主要因素。

这是为了类型安全,它使你能够进行某种形式的“动态类型检查”:编译器知道你的内容可能有不同的含义,而你在运行时精确解释它的含义取决于你自己。如果你有一个指针可以指向不同类型,则必须使用联合,否则由于别名问题,你的代码可能是不正确的(编译器会对自己说“哦,只有这个指针可以指向这个类型,所以我可以优化掉那些访问...”,会发生糟糕的事情)。


10

结构体分配的内存大小是其中所包含元素大小总和。

联合体只会分配最大成员所需的内存空间。


2
你可能还想补充一下,联合成员在“叠加”时彼此重叠,因为它们都从分配的联合“结构”的起始地址开始。 - Jim Buck

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