什么时候会有人使用工会? 这是 C-only 时代的遗留物吗?

我学到了,但是没有真正的工会。我阅读的每一篇 C 或 C + + 文本都会介绍它们(有时是顺便介绍) ,但是它们往往很少给出为什么或在哪里使用它们的实际例子。在现代(甚至是传统)情况下,工会什么时候会有用?我唯一的两个猜测是,当您的工作空间非常有限,或者当您正在开发一个 API (或类似的东西) ,并且您希望强制最终用户一次只拥有多个对象/类型的一个实例时,将编写微处理器。这两个猜测是否接近正确?

87311 次浏览

联合在 C 中提供了多态性。

来自 维基百科关于工会的文章:

工会的主要作用是 至 节省空间,因为它提供了一个 让许多不同的类型 储存在同一空间。工会也 提供粗多态性 , 没有类型检查,所以 是由程序员决定的 正确的字段被访问 不同的背景。相关领域 联合变量的值通常是 由他人的状态决定 变量,可能在一个封闭的 结构。

一种常见的惯用法 联合来执行 C + + 所称的 重新解释 _ 强制转换,方法是将 一个联盟的领域和从 另一个,如在代码中所做的那样 取决于... 的原始表现形式 价值观。

一个例子是在嵌入式领域,其中每个寄存器位可能意味着不同的东西。例如,一个8位整数的并集和一个包含8个独立1位位字段的结构允许您更改一位或整个字节。

来自 http://cplus.about.com/od/learningc/ss/lowlevel_9.htm:

联合的使用很少,在大多数计算机上,大小 一个指针和一个 int 通常是相同的-这是因为两者 通常放入中央处理器的一个寄存器中。因此,如果你想做一个快速 以及指向 int 或其他方式的指针的脏强制转换,则声明一个 工会。

union intptr {   int i;   int * p; };
union intptr x; x.i = 1000;
/* puts 90 at location 1000 */
*(x.p)=90;

联合的另一个用途是在命令或消息协议中,其中 发送和接收不同大小的消息 持有不同的信息,但每个将有一个固定的部分(可能是一个 和一个可变部分位。这是您可能实现它的方式。

struct head {   int id;   int response;   int size; }; struct msgstring50 {    struct head fixed;    char message[50]; } struct

Structmsgstring80{ structhead fix; char message [80] ; }
Struct msgint10{ struct head fix; int message [10] ; } struct Msgack {结构头固定; int ok; }联合消息类型{
Struct msgstring50 m50; struct msgstring80 m80; struct msgint10 构造 msgack ack; }

在实践中,尽管工会的规模相同,但是 只发送有意义的数据而不浪费空间 大小为16字节,而 msgstring80为92字节 初始化 messagetype 变量时,它将设置其大小字段 根据它是什么类型。这可以被其他人使用 函数来传输正确的字节数。

假设您有 n 种不同类型的配置(只是一组定义参数的变量)。通过使用配置类型的枚举,可以定义具有配置类型 ID 的结构,以及所有不同类型配置的联合。

这样,无论您在何处传递配置,都可以使用 ID 来确定如何解释配置数据,但是如果配置非常庞大,您就不必为每个潜在的类型浪费空间而使用并行结构。

我能想到的一个用例是:

typedef union
{
struct
{
uint8_t a;
uint8_t b;
uint8_t c;
uint8_t d;
};
uint32_t x;
} some32bittype;

然后,您可以访问这个32位数据块的8位独立部分; 但是,您可能会被 endianness 咬伤。

这只是一个假设的例子,但是无论何时您想要将字段中的数据分割成像这样的组件部分,您都可以使用联合。

也就是说,还有一种方法是安全的:

uint32_t x;
uint8_t a = (x & 0xFF000000) >> 24;

例如,由于编译器会将该二元运算转换为正确的 endianness。

联合在处理字节级(低级)数据时非常有用。

我最近使用的 IP 地址建模方法如下:

// Composite structure for IP address storage
union
{
// IPv4 @ 32-bit identifier
// Padded 12-bytes for IPv6 compatibility
union
{
struct
{
unsigned char _reserved[12];
unsigned char _IpBytes[4];
} _Raw;


struct
{
unsigned char _reserved[12];
unsigned char _o1;
unsigned char _o2;
unsigned char _o3;
unsigned char _o4;
} _Octet;
} _IPv4;


// IPv6 @ 128-bit identifier
// Next generation internet addressing
union
{
struct
{
unsigned char _IpBytes[16];
} _Raw;


struct
{
unsigned short _w1;
unsigned short _w2;
unsigned short _w3;
unsigned short _w4;
unsigned short _w5;
unsigned short _w6;
unsigned short _w7;
unsigned short _w8;
} _Word;
} _IPv6;
} _IP;

联合通常与一个鉴别器一起使用: 一个变量,指示联合的哪个字段是有效的。例如,假设您想创建自己的 变种类型:

struct my_variant_t {
int type;
union {
char char_value;
short short_value;
int int_value;
long long_value;
float float_value;
double double_value;
void* ptr_value;
};
};

然后你可以这样使用它:

/* construct a new float variant instance */
void init_float(struct my_variant_t* v, float initial_value) {
v->type = VAR_FLOAT;
v->float_value = initial_value;
}


/* Increments the value of the variant by the given int */
void inc_variant_by_int(struct my_variant_t* v, int n) {
switch (v->type) {
case VAR_FLOAT:
v->float_value += n;
break;


case VAR_INT:
v->int_value += n;
break;
...
}
}

这实际上是一个非常常见的习惯用法,特别是在 VisualBasic 内部上。

有关实际示例,请参见 SDL 的 SDL _ Event 联合。(实际的源代码).在联合的顶部有一个 type字段,每个 SDL _ * Event 结构上都重复相同的字段。然后,为了处理正确的事件,您需要检查 type字段的值。

好处很简单: 只有一个数据类型可以处理所有事件类型,而不需要使用不必要的内存。

工会的一些用途:

  • 为未知的外部主机提供一个通用的 endianness 接口。
  • 操作外部 CPU 体系结构浮点数据,例如从网络链路接收 VAX G _ FLOATS并将它们转换为 IEEE 754长雷亚尔进行处理。
  • 提供对更高级别类型的直接的位转动访问。
union {
unsigned char   byte_v[16];
long double     ld_v;
}

使用这个声明,可以很简单地显示 long double的十六进制字节值,更改指数符号,确定它是否是非正态值,或者为不支持它的 CPU 实现长的双算术,等等。

  • 当字段依赖于某些值时节省存储空间:

    class person {
    string name;
    
    
    char gender;   // M = male, F = female, O = other
    union {
    date  vasectomized;  // for males
    int   pregnancies;   // for females
    } gender_specific_data;
    }
    
  • Grep the include files for use with your compiler. You'll find dozens to hundreds of uses of union:

    [wally@zenetfedora ~]$ cd /usr/include
    [wally@zenetfedora include]$ grep -w union *
    a.out.h:  union
    argp.h:   parsing options, getopt is called with the union of all the argp
    bfd.h:  union
    bfd.h:  union
    bfd.h:union internal_auxent;
    bfd.h:  (bfd *, struct bfd_symbol *, int, union internal_auxent *);
    bfd.h:  union {
    bfd.h:  /* The value of the symbol.  This really should be a union of a
    bfd.h:  union
    bfd.h:  union
    bfdlink.h:  /* A union of information depending upon the type.  */
    bfdlink.h:  union
    bfdlink.h:       this field.  This field is present in all of the union element
    bfdlink.h:       the union; this structure is a major space user in the
    bfdlink.h:  union
    bfdlink.h:  union
    curses.h:    union
    db_cxx.h:// 4201: nameless struct/union
    elf.h:  union
    elf.h:  union
    elf.h:  union
    elf.h:  union
    elf.h:typedef union
    _G_config.h:typedef union
    gcrypt.h:  union
    gcrypt.h:    union
    gcrypt.h:    union
    gmp-i386.h:  union {
    ieee754.h:union ieee754_float
    ieee754.h:union ieee754_double
    ieee754.h:union ieee854_long_double
    ifaddrs.h:  union
    jpeglib.h:  union {
    ldap.h: union mod_vals_u {
    ncurses.h:    union
    newt.h:    union {
    obstack.h:  union
    pi-file.h:  union {
    resolv.h:   union {
    signal.h:extern int sigqueue (__pid_t __pid, int __sig, __const union sigval __val)
    stdlib.h:/* Lots of hair to allow traditional BSD use of `union wait'
    stdlib.h:  (__extension__ (((union { __typeof(status) __in; int __i; }) \
    stdlib.h:/* This is the type of the argument to `wait'.  The funky union
    stdlib.h:   causes redeclarations with either `int *' or `union wait *' to be
    stdlib.h:typedef union
    stdlib.h:    union wait *__uptr;
    stdlib.h:  } __WAIT_STATUS __attribute__ ((__transparent_union__));
    thread_db.h:  union
    thread_db.h:  union
    tiffio.h:   union {
    wchar.h:  union
    xf86drm.h:typedef union _drmVBlank {
    

union关键字虽然仍然在 C + + 031中使用,但大多是 C 时代遗留下来的。最明显的问题是它只能与 POD1一起工作。

然而,联合的想法仍然存在,实际上 Boost 库的特点是一个类似联合的类:

boost::variant<std::string, Foo, Bar>

它拥有 union的大部分好处(如果不是全部的话) ,并补充道:

  • 正确使用非 POD 类型的能力
  • 静电式安全装置静电式安全装置

在实践中,已经证明它等同于 union + enum的组合,并且基准测试它的速度一样快(而 boost::any更多的是 dynamic_cast的领域,因为它使用 RTTI)。

1 Union 在 C + + 11(不受限制的工会)中升级,现在可以包含带析构函数的对象,尽管用户必须手动调用析构函数(在当前活动的 union 成员上)。使用变量仍然容易得多。

赫伯 · 萨特(Herb Sutter)大约六年前在 GOTW上写道,强调补充道:

“但不要认为工会只是早期的遗留问题。在当今的现代社会,工会可能最有用的节省空间的方法是允许数据重叠、 这在 C + + 中仍然是可取的和。例如,世界上一些最先进的 < em > C + + 标准库实现现在只使用这种技术来实现“小字符串优化”,一个伟大的优化替代方案,重用字符串对象本身内的存储: 对于大字符串,字符串对象内的空间存储通常指向动态分配的缓冲区和管理信息,如缓冲区的大小; 对于小字符串,相同的空间被重用来直接存储字符串内容,完全避免任何动态内存分配。有关小字符串优化(以及其他相当深入的字符串优化和悲观化)的更多信息,请参见... ...”

对于一个不太有用的例子,请参阅冗长但不确定的问题 Gcc、严格别名和通过联合进行铸造

举个我用工会的例子:

class Vector
{
union
{
double _coord[3];
struct
{
double _x;
double _y;
double _z;
};


};
...
}

这允许我以数组或元素的形式访问数据。

我使用联合使不同的术语指向相同的值。在图像处理中,不管我是在处理列、宽度还是 X 方向的大小,它都会变得令人困惑。为了解决这个问题,我使用了一个联合,这样我就知道哪些描述是一起使用的。

   union {   // dimension from left to right   // union for the left to right dimension
uint32_t            m_width;
uint32_t            m_sizeX;
uint32_t            m_columns;
};


union {   // dimension from top to bottom   // union for the top to bottom dimension
uint32_t            m_height;
uint32_t            m_sizeY;
uint32_t            m_rows;
};

我觉得 C + + 结合很酷。似乎人们通常只考虑用例中希望“就地”更改联合实例的值的情况(这似乎只是为了节省内存或执行可疑的转换)。

事实上,工会作为一种软件工程工具 即使您从未更改任何联合实例的值具有巨大的力量。

用例1: 变色龙

使用联合,您可以在一个名称下重新组合许多任意的类,这与基类及其派生类的情况并非没有相似之处。然而,需要改变的是对给定的联合实例可以做什么和不可以做什么:

struct Batman;
struct BaseballBat;


union Bat
{
Batman brucewayne;
BaseballBat club;
};


ReturnType1 f(void)
{
BaseballBat bb = {/* */};
Bat b;
b.club = bb;
// do something with b.club
}


ReturnType2 g(Bat& b)
{
// do something with b, but how do we know what's inside?
}


Bat returnsBat(void);
ReturnType3 h(void)
{
Bat b = returnsBat();
// do something with b, but how do we know what's inside?
}

当程序员想要使用联合实例时,似乎必须确定给定联合实例的内容类型。这就是上面函数 f的情况。但是,如果一个函数将接收一个联合实例作为一个传递的参数,就像上面的 g一样,那么它就不知道如何处理它。这同样适用于返回联合实例的函数,参见 h: 调用者如何知道其中的内容?

如果一个联合实例从未作为参数或返回值传递,那么它的生命周期肯定是非常单调的,当程序员选择更改其内容时,它会兴奋不已:

Batman bm = {/* */};
Baseball bb = {/* */};
Bat b;
b.brucewayne = bm;
// stuff
b.club = bb;

这是工会最流行的用例。另一个用例是联合实例附带了一些东西来告诉您它的类型。

用例2: “很高兴认识你,我是 object,来自 Class

假设一个程序员选择总是将联合实例与类型描述符配对(我将让读者自行决定是否为这样一个对象想象一个实现)。如果程序员想要的是节省内存,而且相对于联合而言,类型描述符的大小不可忽略,那么这就违背了联合本身的目的。但是,让我们假设联合实例可以作为参数或返回值传递,而被调用方或调用方不知道其中的内容,这一点至关重要。

然后,程序员必须编写一个 switch控制流语句,以告诉布鲁斯韦恩区别于木棒,或类似的东西。当联盟中只有两种内容时,情况还不算太糟,但是很明显,联盟不再扩展了。

用例3:

正如 关于 ISO C + + 标准的建议的作者在2008年所说,

许多重要的问题域需要大量的对象或有限的内存 资源。在这些情况下,节约空间是非常重要的,而联合往往是一个完美的方式来做到这一点。事实上,一个常见的用例是联合在其生存期内从未更改其活动成员的情况。它可以被构造、复制和销毁,就好像它是一个只包含一个成员的结构体一样。这种方法的典型应用程序是创建一个不动态分配的不相关类型的异构集合(可能它们在映射中就地构造,或者是数组的成员)。

现在,举一个 UML 类图的例子:

many compositions for class A

简单地说,A 类 可以的对象有 B1、 ... 、 Bn 中任何类的对象,每种类型最多只有一个,而 N是一个相当大的数字,比如至少10。

我们不想像下面这样向 A 添加字段(数据成员) :

private:
B1 b1;
.
.
.
Bn bn;

因为 N可能会有所不同(我们可能希望将 Bx 类添加到混合中) ,而且这会导致构造函数出现混乱,还因为 A 对象会占用大量空间。

我们可以使用一个奇怪的容器,其中包含 void*指向 Bx对象的指针和类型强制转换来检索这些对象,但是这样做很难,而且是 C 风格的... ... 但是更重要的是,这会让我们有许多动态分配的对象的生命周期来管理。

相反,我们可以这样做:

union Bee
{
B1 b1;
.
.
.
Bn bn;
};


enum BeesTypes { TYPE_B1, ..., TYPE_BN };


class A
{
private:
std::unordered_map<int, Bee> data; // C++11, otherwise use std::map


public:
Bee get(int); // the implementation is obvious: get from the unordered map
};

然后,要从 data获取联合实例的内容,可以使用 a.get(TYPE_B2).b2等类,其中 a是类 A实例。

由于在 C + + 11中联合是不受限制的,所以这个功能更加强大。详细信息请参阅 链接到上面的文件这篇文章

联合提供了一种在单个存储区域中操作不同类型数据的方法,而无需在程序中嵌入任何与机器无关的信息 它们类似于帕斯卡的变体记录

例如,可以在编译器符号表管理器中找到 常量可以是一个整型数、一个浮点数或一个字符指针 必须存储在适当类型的变量中,但是如果该值占用相同的存储量并且不管其类型如何都存储在相同的位置,则对于表管理来说最为方便。这就是联合的目的——一个可以合法保存几种类型中任何一种的单一变量。语法基于结构:

union u_tag {
int ival;
float fval;
char  *sval;
} u;

变量 u 的大小足以容纳三种类型中最大的一种; 具体的大小取决于实现。这些类型中的任何一个都可以赋给 u,然后在 表达式,只要用法是一致的

联合的一个绝妙用法是内存对齐,这是我在 PCL (Point Cloud Library)源代码中发现的。API 中的单一数据结构可以针对两种体系结构: 支持 SSE 的 CPU 和不支持 SSE 的 CPU。例如: PointXYZ 的数据结构是

typedef union
{
float data[4];
struct
{
float x;
float y;
float z;
};
} PointXYZ;

为了 SSE 对齐,这3个浮点被另外一个浮点填充。 所以

PointXYZ point;

用户可以访问 point.data [0]或 point.x (取决于 SSE 支持)来访问 x 坐标。 更多类似的更好的使用细节可以在以下链接中找到: < a href = “ http://pointClouds.org/document/Tutorials/add _ custom_ ptype.php”rel = “ noReferrer”> PCL document PointT type

在最近的 C 标准版本中引入的 严格的别名规则提高了 工会的重要性。

您可以在不违反 C 标准的情况下对 类型双关使用联合 do。
这个程序有 未指明的行为(因为我假设 floatunsigned int具有相同的长度) ,但是没有 未定义行为(参见 给你)。

#include <stdio.h>


union float_uint
{
float f;
unsigned int ui;
};


int main()
{
float v = 241;
union float_uint fui = {.f = v};


//May trigger UNSPECIFIED BEHAVIOR but not UNDEFINED BEHAVIOR
printf("Your IEEE 754 float sir: %08x\n", fui.ui);


//This is UNDEFINED BEHAVIOR as it violates the Strict Aliasing Rule
unsigned int* pp = (unsigned int*) &v;


printf("Your IEEE 754 float, again, sir: %08x\n", *pp);


return 0;
}

我想添加一个使用联合实现公式计算器/解释器或在计算中使用某种类型的解释器(例如,你想使用计算公式的 在运行时可修改部分——数值求解公式——只是例子)的实际例子。 所以你可以像这样定义不同类型的数字/常量(整数、浮点数、甚至复数) :

struct Number{
enum NumType{int32, float, double, complex}; NumType num_t;
union{int ival; float fval; double dval; ComplexNumber cmplx_val}
}

因此,您节省了内存,更重要的是——您避免了对小对象(与通过类继承/多态性实现相比)的可能极端数量(如果您使用了大量运行时定义的数字)的任何动态分配。但是更有趣的是,您仍然可以使用 C + + 多态性的功能(例如,如果您喜欢双重分派;)来处理这种结构。只需将“虚拟”接口指针作为此结构的一个字段添加到所有数字类型的父类,指向 这个例子而不是/除了原始类型之外,或者使用好的老 C 函数指针。

struct NumberBase
{
virtual Add(NumberBase n);
...
}
struct NumberInt: Number
{
//implement methods assuming Number's union contains int
NumberBase Add(NumberBase n);
...
}
struct NumberDouble: Number
{
//implement methods assuming Number's union contains double
NumberBase Add(NumberBase n);
...
}
//e.t.c. for all number types/or use templates
struct Number: NumberBase{
union{int ival; float fval; double dval; ComplexNumber cmplx_val;}
NumberBase* num_t;
Set(int a)
{
ival=a;
//still kind of hack, hope it works because derived classes of   Number    dont add any fields
num_t = static_cast<NumberInt>(this);
}
}

因此,您可以使用多态性来代替通过 switch (type)进行的类型检查——通过内存高效的实现(没有动态分配小对象)——当然,如果您需要的话。

在 C 语言的早期(例如1974年的文档) ,所有结构都为其成员共享一个共同的名称空间。每个成员名与一个类型和一个偏移量相关联; 如果“ wd _ wozle”在偏移量12处是一个“ int”,那么给定一个任何结构类型的指针 pp->wd_woozle将等效于 *(int*)(((char*)p)+12)。该语言要求所有结构类型的所有成员都具有唯一的名称 除了,如果所使用的每个结构都将其作为公共的初始序列,则该语言显式地允许重用成员名称。

结构类型可以混杂地使用这一事实使得结构的行为有可能像它们包含重叠字段一样。例如,给定的定义:

struct float1 { float f0;};
struct byte4  { char b0,b1,b2,b3; }; /* Unsigned didn't exist yet */

代码可以声明一个类型为“ float1”的结构,然后使用“ member”b0... b3访问其中的单个字节。当语言发生变化,以便每个结构为其成员接收一个单独的名称空间时,依赖于以多种方式访问事物的能力的代码就会中断。将不同结构类型的名称空间分离出来的价值足以要求修改这些代码以适应它,但是这些技术的价值足以证明扩展语言以继续支持它是合理的。

编写代码是为了利用访问 struct float1中的存储的能力,就像它是一个 struct byte4一样,可以通过添加一个声明: union f1b4 { struct float1 ff; struct byte4 bb; };,声明对象类型为 union f1b4;而不是 struct float1,并用 ff.f0bb.b0struct byte40等代替对 f0b0b1等的访问,使其在新语言中工作。虽然有更好的方法可以支持这样的代码,但是 struct byte41方法至少在一定程度上是可行的,至少在 C89时代对别名规则的解释中是可行的。