为什么我们需要C工会?

什么时候应该使用工会?我们为什么需要它们?

160915 次浏览

联合允许互斥的数据成员共享相同的内存。当内存比较稀缺时,例如在嵌入式系统中,这是非常重要的。

示例如下:

union {
int a;
int b;
int c;
} myUnion;

这个联合将占用一个int值的空间,而不是3个独立的int值。如果用户设置了一个的值,然后设置了b的值,它将覆盖一个的值,因为它们都共享相同的内存位置。

当您希望对由硬件、设备或网络协议定义的结构进行建模时,或者当您要创建大量对象并希望节省空间时,可以使用联合。不过,在95%的情况下,你真的不需要它们,坚持使用易于调试的代码。

联合通常用于整数和浮点数的二进制表示之间的转换:

union
{
int i;
float f;
} u;


// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);

尽管根据C标准,这在技术上是未定义的行为(您只应该阅读最近编写的字段),但它将在几乎任何编译器中以定义良好的方式起作用。

联合有时也被用来实现C语言中的伪多态性,通过给一个结构一些标记来指示它包含什么类型的对象,然后将可能的类型联合在一起:

enum Type { INTS, FLOATS, DOUBLE };
struct S
{
Type s_type;
union
{
int s_ints[2];
float s_floats[2];
double s_double;
};
};


void do_something(struct S *s)
{
switch(s->s_type)
{
case INTS:  // do something with s->s_ints
break;


case FLOATS:  // do something with s->s_floats
break;


case DOUBLE:  // do something with s->s_double
break;
}
}

这允许struct S的大小仅为12字节,而不是28字节。

很难想出需要这种灵活结构的特定场合,也许在发送不同大小消息的消息协议中,但即使在这种情况下,也可能有更好、更适合程序员的替代方案。

联合有点像其他语言中的变体类型——它们一次只能保存一个东西,但这个东西可以是int型,浮点型等,这取决于你如何声明它。

例如:

typedef union MyUnion MYUNION;
union MyUnion
{
int MyInt;
float MyFloat;
};

MyUnion只包含一个整型或浮点数取决于你最近设置的。所以这样做:

MYUNION u;
u.MyInt = 10;

U现在持有int = 10;

u.MyFloat = 1.0;

U现在持有一个等于1.0的浮点数。它不再持有int型。显然,如果你尝试printf("MyInt=%d" u.MyInt);那么你可能会得到一个错误,尽管我不确定具体的行为。

联合的大小由其最大字段的大小决定,在本例中为float。

工会是伟大的。我所见过的联合的一个聪明用法是在定义事件时使用它们。例如,您可能决定一个事件是32位的。

现在,在这32位中,您可能希望将前8位指定为事件发送方的标识符……有时你要把事件作为一个整体来处理,有时你要剖析它并比较它的组成部分。工会让你可以灵活地做到这两点。

union Event
{
unsigned long eventCode;
unsigned char eventParts[4];
};

这里有一个来自我自己代码库的联合的例子(来自记忆和转述,所以可能不准确)。它被用来在我构建的解释器中存储语言元素。例如,以下代码:

set a to b times 7.

由以下语言元素组成:

  • 符号(组)
  • 变量(一个)
  • (象征)
  • 变量[b]
  • 符号(*)
  • 常数[7]
  • 符号(。)

语言元素被定义为“#define”值,这样:

#define ELEM_SYM_SET        0
#define ELEM_SYM_TO         1
#define ELEM_SYM_TIMES      2
#define ELEM_SYM_FULLSTOP   3
#define ELEM_VARIABLE     100
#define ELEM_CONSTANT     101

下面的结构被用来存储每个元素:

typedef struct {
int typ;
union {
char *str;
int   val;
}
} tElem;

那么每个元素的大小就是最大联合的大小(typ为4字节,联合为4字节,虽然这些是典型值,但实际的大小取决于实现)。

为了创建一个“set”;元素,您将使用:

tElem e;
e.typ = ELEM_SYM_SET;

为了创建一个“变量[b]”;元素,您将使用:

tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b");   // make sure you free this later

为了创建一个“常量[7]”;元素,您将使用:

tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;

你可以很容易地将其扩展为包括浮点数(float flt)或rationals (struct ratnl {int num; int denom;})和其他类型。

基本前提是strval在内存中不是连续的,它们实际上是重叠的,所以这是在同一块内存上获得不同视图的一种方式,如图所示,其中结构基于内存位置0x1010,整数和指针都是4字节:

       +-----------+
0x1010 |           |
0x1011 |    typ    |
0x1012 |           |
0x1013 |           |
+-----+-----+
0x1014 |     |     |
0x1015 | str | val |
0x1016 |     |     |
0x1017 |     |     |
+-----+-----+

如果只是在一个结构中,它看起来会是这样的:

       +-------+
0x1010 |       |
0x1011 |  typ  |
0x1012 |       |
0x1013 |       |
+-------+
0x1014 |       |
0x1015 |  str  |
0x1016 |       |
0x1017 |       |
+-------+
0x1018 |       |
0x1019 |  val  |
0x101A |       |
0x101B |       |
+-------+

联合在嵌入式编程或需要直接访问硬件/内存的情况下特别有用。这里有一个简单的例子:

typedef union
{
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int dword;
} HW_Register;
HW_Register reg;

然后,您可以按如下方式访问reg:

reg.dword = 0x12345678;
reg.bytes.byte3 = 4;

字节顺序和处理器体系结构当然很重要。

另一个有用的特性是位修饰符:

typedef union
{
struct {
unsigned char b1:1;
unsigned char b2:1;
unsigned char b3:1;
unsigned char b4:1;
unsigned char reserved:4;
} bits;
unsigned char byte;
} HW_RegisterB;
HW_RegisterB reg;

使用这段代码,您可以直接访问寄存器/内存地址中的单个位:

x = reg.bits.b2;

在COM接口中使用的VARIANT呢?它有两个字段——“type”和一个包含实际值的联合,该值根据“type”字段进行处理。

我在为嵌入式设备编码时使用union。我有一个16位的C整数。当我需要从/存储到EEPROM时,我需要检索高8位和低8位。所以我用了这种方法:

union data {
int data;
struct {
unsigned char higher;
unsigned char lower;
} parts;
};

它不需要移动,所以代码更容易阅读。

另一方面,我看到一些旧的c++ stl代码使用联合的stl分配器。如果你感兴趣,你可以阅读sgi stl源代码。下面是其中的一段:

union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1];    /* The client sees this.        */
};
  • 包含不同记录类型的文件。
  • 包含不同请求类型的网络接口。

看看这个:X.25缓冲区命令处理

许多可能的X.25命令中的一个被接收到缓冲区中,并通过使用所有可能结构的UNION进行适当的处理。

我想说,它可以更容易地重用可能以不同方式使用的内存,即节省内存。例如,你想做一些“变体”结构体,能够保存一个短字符串以及一个数字:

struct variant {
int type;
double number;
char *string;
};

在32位系统中,这将导致每个variant实例至少使用96位或12个字节。

使用联合可以将大小减小到64位或8字节:

struct variant {
int type;
union {
double number;
char *string;
} value;
};

如果你想添加更多不同的变量类型,你甚至可以保存更多。这可能是真的,你可以做类似的事情,强制转换一个空指针-但联合使它更容易访问,以及类型安全。这样的节省听起来并不是很大,但是您节省了用于该结构的所有实例的三分之一的内存。

有很多用法。只需执行grep union /usr/include/*或类似目录。大多数情况下,union被包装在struct中,结构体的一个成员告诉要访问联合中的哪个元素。例如,为现实生活中的实现签出man elf

这是基本原则:

struct _mydata {
int which_one;
union _data {
int a;
float b;
char c;
} foo;
} bar;


switch (bar.which_one)
{
case INTEGER  :  /* access bar.foo.a;*/ break;
case FLOATING :  /* access bar.foo.b;*/ break;
case CHARACTER:  /* access bar.foo.c;*/ break;
}

在学校里,我是这样使用联合的:

typedef union
{
unsigned char color[4];
int       new_color;
}       u_color;

我用它来处理颜色更容易,而不是使用>>和<<操作符,我只需要遍历我的char数组的不同下标。

我在几个库中看到过它作为面向对象继承的替代品。

如。

        Connection
/       |       \
Network   USB     VirtualConnection

如果你想让Connection“类”是上面的任何一个,你可以这样写:

struct Connection
{
int type;
union
{
struct Network network;
struct USB usb;
struct Virtual virtual;
}
};

libinfinity中的示例:http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74

低级系统编程就是一个合理的例子。

IIRC中,我使用联合将硬件寄存器分解为组件位。因此,您可以访问一个8位寄存器(在我这样做的那天;-)到组件位。

(我忘记了确切的语法,但是……)这种结构将允许控制寄存器作为control_byte或通过单个位来访问。对于给定的字节顺序,确保位映射到正确的寄存器位是很重要的。

typedef union {
unsigned char control_byte;
struct {
unsigned int nibble  : 4;
unsigned int nmi     : 1;
unsigned int enabled : 1;
unsigned int fired   : 1;
unsigned int control : 1;
};
} ControlRegister;

一个简单而有用的例子是....

想象一下:

你有一个uint32_t array[2]并且想要访问字节链的第3和第4个字节。 你可以做*((uint16_t*) &array[1])。 但遗憾的是,这打破了严格的混叠规则!< / p >

但是已知的编译器允许你做以下事情:

union un
{
uint16_t array16[4];
uint32_t array32[2];
}

严格来说,这仍然是违反规则的。但是所有已知的标准都支持这种用法。

许多答案都涉及从一种类型转换到另一种类型。我从具有相同类型的联合中得到最多的使用(即在解析串行数据流时)。它们使得解析/构造框架包变得非常简单。

typedef union
{
UINT8 buffer[PACKET_SIZE]; // Where the packet size is large enough for
// the entire set of fields (including the payload)


struct
{
UINT8 size;
UINT8 cmd;
UINT8 payload[PAYLOAD_SIZE];
UINT8 crc;
} fields;


}PACKET_T;


// This should be called every time a new byte of data is ready
// and point to the packet's buffer:
// packet_builder(packet.buffer, new_data);


void packet_builder(UINT8* buffer, UINT8 data)
{
static UINT8 received_bytes = 0;


// All range checking etc removed for brevity


buffer[received_bytes] = data;
received_bytes++;


// Using the struc only way adds lots of logic that relates "byte 0" to size
// "byte 1" to cmd, etc...
}


void packet_handler(PACKET_T* packet)
{
// Process the fields in a readable manner
if(packet->fields.size > TOO_BIG)
{
// handle error...
}


if(packet->fields.cmd == CMD_X)
{
// do stuff..
}
}

<强>编辑 关于字节序和结构填充的评论是有效的,而且非常值得关注。我几乎完全在嵌入式软件中使用了这段代码,其中大部分我都可以控制管道的两端

在C的早期版本中,所有结构声明都共享一组公共字段。考虑到:

struct x {int x_mode; int q; float x_f};
struct y {int y_mode; int q; int y_l};
struct z {int z_mode; char name[20];};

编译器实际上会生成一个结构的大小(可能还有对齐)表,以及一个单独的结构成员名称、类型和偏移量表。编译器不会跟踪哪个成员属于哪个结构,并且只有在类型和偏移量匹配的情况下才允许两个结构具有同名的成员(例如struct xstruct y的成员q)。如果p是一个指向任何结构类型的指针,p->q将把“q”的偏移量加到指针p上,并从结果地址中获取一个“int”。

根据上述语义,可以编写一个函数,可以对多种结构交换执行一些有用的操作,前提是函数使用的所有字段都与相关结构中的有用字段对齐。这是一个有用的特性,更改C来验证用于结构访问的成员,以针对所讨论的结构的类型,这将意味着在没有一种方法使结构可以在同一地址包含多个命名字段的情况下失去它。在C语言中添加“联合”类型有助于在一定程度上填补这一空白(恕我直言,尽管没有达到应有的效果)。

工会能够填补这一空白的一个重要部分是,指向工会成员的指针可以转换为指向任何包含该成员的工会的指针,而指向任何工会的指针也可以转换为指向任何成员的指针。虽然C89标准没有明确表示将T*直接强制转换为U*等同于将其强制转换为指向任何包含TU的联合类型的指针,然后将其强制转换为U*,但后一个强制转换序列的已定义行为不会受到所使用的联合类型的影响,而且标准也没有为从T直接强制转换为U指定任何相反的语义。此外,在函数接收到来源未知的指针的情况下,通过T*写入对象,将T*转换为U*,然后通过U*读取对象的行为相当于通过T类型的成员写入联合并读取为U类型,这在少数情况下是标准定义的(例如在访问公共初始序列成员时),其余情况下是实现定义的(而不是未定义)。虽然很少有程序利用联合类型的实际对象的CIS保证,但更常见的是利用指向未知来源对象的指针必须表现为指向联合成员的指针,并具有与之相关的行为保证这一事实。

当你有一个函数,你返回的值可以不同,这取决于函数做了什么,使用联合。

联合用于节省内存,特别是在内存有限的设备上使用,而内存是重要的。 经验:< / p >

union _Union{
int a;
double b;
char c;
};
例如,假设我们在内存有限的系统中需要上述3种数据类型(int,double,char)。如果我们不使用“union”,我们需要定义这3种数据类型。在这种情况下,将分配sizeof(a) + sizeof(b) + sizeof(c)内存空间。但是如果我们使用onion,根据这3种数据类型中最大的数据类型,只分配一个内存空间。因为联合结构中的所有变量将使用相同的内存空间。因此,根据最大数据类型分配的内存空间将是所有变量的公共空间。 例如:< / p >
union _Union{
int a;
double b;
char c;
};


int main() {
union _Union uni;
uni.a = 44;
uni.b = 144.5;
printf("a:%d\n",uni.a);
printf("b:%lf\n",uni.b);
return 0;
}
< p >输出是: 答:0 和b: 144.500000 < / p >

为什么a是0 ?因为联合结构只有一个内存区域,而所有数据结构都共同使用它。最后一个赋值覆盖了旧值。 再举一个例子:

 union _Union{
char name[15];
int id;
};




int main(){
union _Union uni;
char choice;
printf("YOu can enter name or id value.");
printf("Do you want to enter the name(y or n):");
scanf("%c",&choice);
if(choice == 'Y' || choice == 'y'){
printf("Enter name:");
scanf("%s",uni.name);
printf("\nName:%s",uni.name);
}else{
printf("Enter Id:");
scanf("%d",&uni.id);
printf("\nId:%d",uni.id);
}
return 0;
}

注意:联合的大小是其最大字段的大小,因为必须保留足够的字节来存储大尺寸字段。