C语言中联合体与结构体的区别

10

此问题的思路是了解使用union的更深层次概念以及如何以不同方式使用它来节省内存。我的问题是 -

假设有一个结构体

struct strt
{
   float f;
   char c;
   int a;
}

并且相同的结构在联合中表示

union unin
{
   float f;
   char c;
   int a;
}
如果我按顺序为结构体成员分配值,然后打印它们,就可以打印出来。但在联合的情况下,会发生一些覆写操作。
因此,我需要找到一种方法,可以使用联合存储f、c、a的值,然后我可以打印相同的值。(应用任何操作或任何东西...)但我正在寻找这种技术...有人能指导我或给我任何想法吗?
6个回答

55

如果你想了解结构体如何存储它的值,它大致是这个样子:

|0---1---2---3---|4---|5---6---7---8---|
|ffffffffffffffff|    |                | <- f: Where your float is stored
|                |cccc|                | <- c: Where your char is stored
|                |    |aaaaaaaaaaaaaaaa| <- a: Where your int is stored

当你改变f的值时,你实际上是在改变0-3字节的内容。当你改变char的值时,你实际上是在改变第4个字节的内容。当你改变int的值时,你实际上是在改变5-8字节的内容。

现在,如果你看一下union如何存储其值,就会像这样:

|0---1---2---3---|
|ffffffffffffffff| <- f: where your float is stored
|cccc------------| <- c: where your char is stored
|aaaaaaaaaaaaaaaa| <- a: where your int is stored
所以现在,当我改变f的值时,我正在更改字节0-3。由于c存储在字节0中,因此当您更改f时,也会更改c和a!当您更改c时,您正在更改f和a的一部分 - 而当您更改a时,您正在更改c和f。这就是您的“覆盖”发生的地方。当您将三个值打包到一个内存地址中时,您根本没有“节省空间”;您只是创建了3种不同的查看和更改相同数据的方法。您实际上没有一个int、一个float和一个char在那个union中 - 在物理层面上,您只有32位,可以视为int、float或char。更改其中一个意味着要更改其他内容。如果您不希望它们彼此更改,则使用struct。
这就是为什么gcc告诉您您的struct长度为9字节,而您的union只有4字节 - 它并没有节省空间 - 只是因为struct和union不是同一件事情。

关于您的免责声明,字节必须位于0处,否则它将不存在于与浮点数和整数相同的地址。字节序无关紧要,因为小端和大端的浮点数或整数占用相同的空间,但仍将从与字符相同的地址开始。 - dreamlax
好的,明白了。我已经将其删除了。谢谢! - Smashery
1
请注意,在许多机器上,结构体中 int 类型变量 a 的起始地址将在 c 变量结束后的 3 字节处,编译器会添加填充以确保对 a 的访问正确对齐。 - Jonathan Leffler

39

我认为您误解了union的目的。

union,顾名思义,定义了一种结构,其中所有成员都占用相同的内存空间。而struct会将每个成员放置在单独的内存中,但仍在单个连续区域内。

在您的union中,当您编写以下代码时:

union foo;
foo.c = 3;

那么foo.afoo.f都会被更改。这是因为.a.c.f都存储在同一个内存位置上。因此,联合体的每个成员都是相同内存的不同“视图”。这种情况在结构体中不会发生,因为所有成员都是独立的且彼此分离。

没有任何方法可以避免这种行为,因为它是有意为之。


是的,我清楚这个逻辑背后的想法,但我想使用联合代替结构体,以便使用更少的内存空间,并且可以获取所有值(如果使用某些操作等)。使用联合的想法是为了节省内存空间。 - AGeek
该想法(在语言中)是将数据存储在相同的内存地址中,而不是进行数据压缩。对元素的每次写入都会改变其所有成员,您将无法恢复它们。 - David Rodríguez - dribeas
我猜测,我声明的上述结构在gcc中使用9个字节,而联合只使用4个字节(最长尺寸)...因此内存自动减少了...但是不可能使用联合代替结构体,以便使用某些操作逐个打印所有值吗? - AGeek
1
另一种解释是:联合(Union)给相同的位模式和位置赋予多个“含义”。 - vrdhn
3
使用结构体时,您使用9个字节来存储3个不同的值/变量,因此不能说您“缩减”或“节省”内存。而使用联合体时,您只使用4个字节来存储一个值/变量。这就好比说,购买一个Big Mac比购买3个Big Mac更加省钱一样。 - Petruza
显示剩余2条评论

12

我认为你对于联合体有一些误解。

使用联合体的想法是为了节省内存...

是的,这是一个原因。

... 并且获得与结构体等效的结果...

不是的。

它们在源代码中看起来很相似,但实际上却完全不同,就像苹果和飞机一样。联合体是一种非常低级别的结构,可以让您将一段内存视为存储其任何"成员",但您每次只能使用一个。即使使用单词"成员"本身就非常误导人, 它们应该被称为"视图"或其他名称,而不是成员。

当你写下:

union ABCunion
{
    int a;
    double b;
    char c;
} myAbc;
你说:"取一个足够大以容纳int、char和double中最大的一个的内存块,然后称其为myAbc。"
在这个内存块里,你现在可以存储一个int、一个double或一个char。如果你存储了一个int,然后再存储一个double,那么int就永远消失了。
那这样做有什么意义呢?
联合体有两个主要用途。
a) 区分式存储
这就是我们上面所做的。我选择一块内存并根据上下文给它赋予不同的含义。有时上下文是明确的(你保留了一些变量来指示你存储的“变量”种类),有时它可以是隐含的(基于代码段,你可以知道哪个变量必须在使用中)。无论哪种方式,代码都需要能够弄清楚,否则你将无法对变量进行任何明智的操作。
一个典型(显式)的例子是:
struct MyVariantType
{
    int typeIndicator ;  // type=1 -> It's an int, 
                         // type=2 -> It's a  double, 
                         // type=3 -> It's a  char
    ABCunion body;
};
例如,VB6的“变量”是类似于上面所述(但更复杂)的联合体。
b) 拆分表示 当您需要将变量视为“整体”或部分组合时,此方法有时非常有用。以下示例更易于理解:
union DOUBLEBYTE
{
    struct
    {
        unsigned char a;
        unsigned char b;
    } bytes;
    short Integer;        
} myVar;

这里有一个短整型的联合体和一对字节。现在,您可以将相同的值视为短整型 (myVar.Integer),也可以轻松地分析构成该值的单个字节 (myVar.bytes.a 和 myVar.bytes.b)。

请注意,第二种用法不可移植(我非常确定);这意味着它不能保证在不同的计算机架构上工作;但是这种用法对于 C 设计的任务(操作系统实现)绝对必要。


苹果和飞机看起来完全不像彼此 :-) - paxdiablo
不是,但这两个词有点相似,它们都有A..pl..es,所以这个比喻还是很好的。 - Eclipse

9
一个联合包含一组互斥的数据。
在你的特定示例中,你可以将float(f),char(c)或int(a)存储在联合中。然而,内存只会为联合中最大的项分配。联合中的所有项将共享同一部分内存。换句话说,将一个值写入联合,然后再写入另一个值,将导致第一个值被覆盖。
你需要回过头来问自己你要建模什么:
- 你真的希望f、c和a的值是互斥的(即一次只能存在一个值)吗?如果是这样,请考虑使用联合结合枚举值(存储在联合外部)来指示联合中的哪个成员在任何特定时间点是“活动”的。这将允许你获得使用联合的内存优势,但代价是更危险的代码(因为维护代码的人需要知道这些值是互斥的 - 即确实是一个联合)。只有在创建许多这些联合并且内存保留至关重要时(例如在嵌入式CPU上),才考虑此选项。你甚至可能不会节省内存,因为你需要在堆栈上创建枚举变量,这也会占用内存。 - 你想让这些值同时处于活动状态而不互相干扰吗?如果是这样,你需要使用一个结构体(就像你在第一个示例中所写的)。这将使用更多的内存 - 当你实例化一个结构体时,分配的内存是所有成员的总和(加上一些填充到最近字边界的空间)。除非内存保留至关重要(参见前面的示例),否则我会支持这种方法。
编辑:
如何在联合中使用枚举的(非常简单)示例:
typedef union
{
    float f;
    char c;
    int a;
} floatCharIntUnion;

typedef enum
{
    usingFloat,
    usingChar,
    usingInt
} unionSelection;

int main()
{
    floatCharIntUnion myUnion;
    unionSelection selection;

    myUnion.f = 3.1415;
    selection = usingFloat;
    processUnion(&myUnion, selection);

    myUnion.c = 'a';
    selection = usingChar;
    processUnion(&myUnion, selection);

    myUnion.a = 22;
    selection = usingInt;
    processUnion(&myUnion, selection);
}

void processUnion(floatCharIntUnion* myUnion, unionSelection selection)
{

    switch (selection)
    {
    case usingFloat:
        // Process myUnion->f
        break;
    case usingChar:
        // Process myUnion->c
        break;
    case usingInt:
        // Process myUnion->a
        break;
    }
}

一个布尔值在联合体之外只适用于其中有两个变量的联合体... - DeadHead
是的,我在编辑时意识到了这一点。已将其更改为枚举类型。 - LeopardSkinPillBoxHat
非常感谢,但是我在Turbo C中执行时遇到了一些错误。 - AGeek
如果执行上述代码,则始终只会执行最后一个“usingInt”情况。将switch case块放在某个函数中并在每次分配给联合成员后调用此函数,对于解释目的来说会更好。在我看来 :) - xk0der
@xk0der - 是的,那只是一个基本的例子来传达思想。无论如何,我已根据您的建议更新了代码。 - LeopardSkinPillBoxHat
@Young - 我放进去的代码没有经过测试。你看到了什么错误? - LeopardSkinPillBoxHat

1
这是使用联合体根据外部标记存储数据的经典示例。
整数、浮点数和字符*在联合中占用相同的位置,它们不是连续的,因此如果您需要存储它们所有,那么您需要寻找一个结构体,而不是联合体。
由于结构体位于联合体外部,因此结构体的大小等于联合体中最大的元素加上类型的大小。
#define TYP_INT 0
#define TYP_FLT 1
#define TYP_STR 2

typedef struct {
    int type;
    union data {
        int a;
        float b;
        char *c;
    }
} tMyType;

static void printMyType (tMyType * x) {
    if (x.type == TYP_INT) {
        printf ("%d\n", x.data.a;
        return;
    }
    if (x.type == TYP_FLT) {
        printf ("%f\n", x.data.b;
        return;
    }
    if (x.type == TYP_STR) {
        printf ("%s\n", x.data.c;
        return;
    }
}

printMyType函数将正确检测结构中存储的内容(除非你对它撒谎),并打印出相关值。

当您填充其中之一时,必须执行以下操作:

x.type = TYP_INT;
x.data.a = 7;

或者

x.type = TYP_STR;
x.data.c = "Hello";

给定的x一次只能是一件事情。

谁试图这样做,就会遭到不幸。

x.type = TYP_STR;
x.data.a = 7;

他们自讨苦吃。


0

联合通常用于在任何给定时间点仅存储以下一种类型的实例时。即您可以在任何时刻存储一个浮点数,一个字符或一个整数。这是为了节省内存-当您只想将其用于存储一个字符时,不需要为浮点数和整数分配额外/独立的内存。分配的内存量=联合中最大的类型。

union unin
{
   float f;
   char c;
   int a;
}

另一个使用联合的方法是当你想要存储具有部分的东西时,比如你可能想将寄存器建模为一个包含高字节、低字节和组合值的联合体。因此,你可以将组合值存储到联合体中,并使用成员通过其他成员获取相应的部分。

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