在C++中使用联合体是一个好的编程实践吗?

28
我需要定义一个类,像这样:
class Color
{
private:
   union Data
   {
       unsigned int intValue;
       unsigned char argbBytes[4];
   }

private:
    Data m_data;
};

另一种方法当然是将数据定义为整数,每当需要时将其转换为字符数组。

我想知道哪种方法更受欢迎。这里的矛盾在于我有一个人提醒我不再使用 union,但在这种情况下它似乎是一种更清晰的解决方案。


4
你的意思是 unsigned char rgbaBytes[4];,对吗?现在你只有一个字符。 - GManNickG
2
是的,intValue 的大小为 4 字节,而 rgbBytes 只有 1 字节,因此 rgbBytes 只能访问值的 1 字节。 - stefanB
请参考以下关于编程的内容:查看此主题的stackoverflow问题(“什么是严格别名规则?”) - OJW
联合体以分号结尾,你需要添加它。 :) - enthusiasticgeek
类型游戏不是联合的目的,而且从一个联合元素读取,这个元素不是最后被写入的,这是未定义的行为。请参考https://dev59.com/cXE95IYBdhLWcg3wb9db。 - legends2k
显示剩余3条评论
12个回答

26

只要你谨慎使用,联合体可以很好地发挥作用。

它们可以用于两种方式:

  1. 允许以多种方式访问单个数据类型(例如,访问颜色作为int或四个char)

  2. 创建多态类型(一个值可以容纳整型或浮点型等不同数据类型)

情况(1)是可以的,因为你没有改变类型的含义——你可以读写联合体中的任何成员而不破坏任何内容。这使得以稍微不同的形式访问相同数据的方式非常方便和高效。

情况(2)可能有用,但极其危险,因为您需要始终从联合体内部访问正确类型的数据。如果您将一个int写入并尝试将其作为float读回,则会获得无意义的值。除非内存使用是您主要考虑的因素,否则最好使用一个具有两个成员的简单结构体。

在C语言中,联合体曾经是至关重要的。在C ++中,通常有更好的方法实现相同的目的(例如,类可用于包装一个值并允许以不同方式访问它)。但是,如果需要原始性能或存在关键的内存情况,则联合体仍然可能是一种有用的方法。


我不确定我是否同意你的第二种情况。因为lyxera显然是使用union来访问具有整数和字符数组表示的相同数据,所以一个具有两个数据成员的结构体将无法起作用。 - Nixuz
7
在第二种情况下,我肯定会使用 BOOST.any 或 BOOST.variant。 - TimW
5
  1. 是错误的。类型游戏不是联合体的目的,写入一个联合体元素并从另一个读取是未定义的行为。请参见此链接:https://dev59.com/cXE95IYBdhLWcg3wb9db
- legends2k
@legends2k:“未定义”只是意味着语言规范不能保证在这种情况下会发生什么。这并不意味着您永远不能使用联合。您需要了解您的数据 - 字节序、结构填充、编译器实现、缓存优化等的影响,并意识到您的代码可能不可移植。联合是使两个不同的东西占用相同内存字节的一种方式(虽然不是同时),因此您必须极其小心地使用它们,并意识到您正在进行非常低级别的操作。(这就是为什么它很有趣!) - Jason Williams
@Jason:尽管如此,_预期的目的_仍然只是为了节省空间,因为这就是它被创建的原因,将其用于双关语只相当于说“我使用钢笔作为纸镇”。 - legends2k
这就是用户的麻烦所在,他们总是创新并从一个功能中获得比最初预期更多的东西 :-) - Jason Williams

10

这是一种好的实践吗?是的,但有一些要注意的地方。

在内存稀缺的早期,联合体很受欢迎,用于重复使用内存。那些日子已经过去了,使用联合体来实现这个目的会增加不必要的复杂性。不要这样做。

如果一个联合体真正描述了您的数据,就像您所给出的示例一样,那么这是完全合理的。然而,请注意,您正在构建一些平台依赖性。在具有不同整数大小或不同字节顺序的不同平台上,您可能无法得到您所期望的结果。


2
如果数据大小很重要(例如,线路格式),则inttypes.h及其uint8_t等是正确的方法,但不要忘记字节序问题! - Chris Huang-Leaver

6

在C++中,联合体的使用受制于其成员必须是POD(普通旧数据)的限制。例如,联合体成员不能有构造函数或析构函数等其他限制。


1
在C++中,您可以为union创建构造函数、方法、虚函数等。 - Niki Yoshiuchi
请注意,联合体成员不能有构造函数等。 - anon
2
这是不正确的。例如,看看C ++14 [class.union] / 3,它特别展示了一个Union包含一个std :: string的示例。 - M.M
将非 POD 对象(例如 std::string)放入 C++ 联合中听起来像是等待发生数据损坏的行为。这里没有足够的空间举例,但强烈建议避免这种做法。 - Michael J

3
这段代码如果按照你描述的方式使用将会导致未定义行为。
在C++中,联合体(union)只有最后一个写入的成员处于活动状态。访问其他成员就像访问未初始化的变量一样。
如需深入讨论,请参见此线程
联合体可以用于节省空间,例如实现Variant。但不能用于类型转换。

3
在我的假设的C ++编码标准中,联合体将被禁止,因为它们往往违反“正确性、简洁性和清晰性优先”的规则。然而,这并不是广泛推荐的做法,在Sutter和Alexandrescu的C ++编码标准中,据我所知,他们并没有剔除它们。幸运的是,我认识的所有人都觉得难以正确使用联合体,所以他们不会使用它们。如果他们也觉得在API中使用void *很难处理就好了 :)

3
C++ 中有很多应该很少使用但在非常有限的情况下非常有用的东西。我认为“联合体”就是其中之一,尽管我同意它通常应该很少使用。一个明显的应用场景是在解码字节级协议时。 - David Thornley
1
@DavidThornley:在我看到它们被用于字节级协议的100%的时间里,都存在着关于对齐方式的未定义行为。我们使用序列化库是有原因的。 - Mooing Duck

2
我不喜欢工会的原因是它们不加区分;它们不提供有关底层类型的任何信息,很容易通过访问联合的错误侧来违反类型安全。 Boost::variant解决了许多这些问题。正如文档所指出的那样,联合在面向对象的环境中“几乎没有用处”,而boost::variant提供了一种非常面向对象的方法来解决实际的联合问题。它的接口被设计为只有在使用正确的类型时才允许访问变体,并且它们提供的“访问者”模式示例在将联合扩展到您未预期的类型时会产生编译时错误。
至于它是否有用,我认为是有用的。我曾经用它们来简化界面。
 class some_xml_class {
 public:
    void set_property(const string&, const string&);
    void set_property(const string&, const vector<string>&);
    void set_property(const string&, const set<string>&);
    void set_property(const string&, int);

    void set_super_property(const string&, const string&);
    void set_super_property(const string&, const vector<string>&);
    void set_super_property(const string&, const set<string>&);
    void set_super_property(const string&, int);

诗歌

 class some_xml_class {
 public:
    typedef boost::variant<string, vector<string>, set<string>, int> property_type;
    void set_property(const string&, const property_type&);
    void set_super_property(const string&, const property_type&);

“模板在这里也可能有用,但是假设实现已经足够长了,我不想将其内联。”

1
更糟糕的是,union 不能包含具有析构函数定义的类型。因此,基于这个和其他原因,C++ 中使用 union 的唯一明智应用是以不同的方式访问低级类型。在其他任何地方,我绝对倾向于使用 boot::variant。 - lispmachine

2

仍然可以使用联合体作为一种可接受的做法。只需将rgbBytes更改为数组即可:)

在C中,联���体可用于不同的目的。有时它们被用作变体类型,即在相同的内存位置上保存不同类型的值。在C++中这样的使用方式是值得质疑的,因为你会使用继承/多态性。然而,联合体的另一种用途是为相同数据提供不同的“接口”。这种使用方式对于C++仍然是有效的。


2

由于您正在使用C ++,我认为这不是一个好的实践。如果您只能使用纯C,当然可以。

在我看来,最大的问题是联合体的大小始终是最大“成员”的大小,因此如果您想存储一个字节或大量数据,则大小为sizeof(shitloadofdata)而不是一个字节。

多态性比联合体更好。


0

请记住,C++11标准规定使用是未定义行为,请参见M.M的答案(在我看来这是最好的答案)。未来的标准可能会定义它,但多字节数字仍然可以以大端或小端方式存储。因此,如果可移植性是一个问题,您不应该使用联合体或类型转换进行类型切换。

如果这不是问题,让我尝试升级您的代码,以展示联合体如何在C++中发挥作用。

class Color {
    union {
        uint32_t value;
        struct {
            uint8_t b, g, r; // LSB value expected
        } part;
    } data;
public:
    uint32_t &value;
    uint8_t &r, &g, &b;
    Color()
        : value(data.value)
        , r(data.part.r)
        , g(data.part.g)
        , b(data.part.b)
    { value = 0; }
    Color(Color const& c) : Color() { value = c.value; }
    Color(uint32_t _value) : Color() { value = _value; }
    Color& operator=(Color const& c) { value = c.value;}
    Color& operator=(uint32_t _value) { value = _value;}
};

说明

  • 联合体内的结构体应该有名称:ISO C++禁止匿名结构体,但g++可以编译它们
  • 其他构造函数将委托给默认构造函数来复制值,而不是引用(C++11)
  • 第三个构造函数在这里是为了方便隐式调用
  • 该构造假定多字节数字以LSB顺序存储

用法

Color c1 = 0xAABBCC; // implicit call
std::cout << hex << +c1.r << endl; // AA
Color c2 = c1; // copy constructor
c2.g = 127;
std::cout << hex << +c1.g << " " << +c2.g << endl; // BB 7F
c1 = 0xDEADCD; // assignment operator
std::cout << hex << +c1.g << " " << +c2.g << endl; // AD 7F

-1
这绝对是C++中使用联合的有效方法。根据你想要做什么,你可以将你的类更改为联合,以避免嵌套。联合可以有方法并使用继承。如果不可能(存在其他数据成员),那么你可能想要像这样使用匿名联合:
class Color
{
private:
   union
   {
       unsigned int intValue;
       unsigned char argbBytes[4];
   };
public:
    unsigned int GetIntValue() { return intValue; }
};

1
它也适用于结构体和类:union { unsigned int intValue; struct { unsigned char r, b, g, a; }; }; - Niki Yoshiuchi
1
@NikiYoshiuchi:这是未定义行为,只在某些编译器上有效,并不保证可靠。 - Mooing Duck
联合体不能使用继承。请参见C++14 [class.union]/2:“联合体不得具有基类。联合体不得用作基类。” - M.M

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