union

Posted xiaojianliu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了union相关的知识,希望对你有一定的参考价值。

联合是一种特殊的类型,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。
当给 union 的某个成员赋值之后,该 union 的其它成员就变成了未定义的状态。
分配给一个 union 对象的存储空间至少能容纳它的最大的数据成员。
和其它类一样,一个union 定义了一种新的类型。union 可以定义包含构造函数和析构函数在内的成员函数,但是因为 union 不能继承自其它类,也不能被作为基类,因此,union 中不能含有虚函数。
union 不能含有引用类型的成员,除此之外,它的类型可以是绝大多数类型。C++11 新标准中,含有构造函数或析构函数的类类型也可以作为 uninon 的成员类型。
union 可以为其成员指定 public、protected、private 等保护标记,默认情况下,union 的成员都是公有的。

定义 union

union 提供了一种有效的途径可以方便地访问一组类型不同的互斥值:

union Token {
    //@ 默认情况下成员是公有的
    char cval;
    int  ival;
    double  dval;
};

使用 union 类型

默认情况下, union 是未初始化的,初始化时使用花括号:

Token first_token = { 'a' };    //@ 初始化 cval 成员
Token last_token;               //@ 未初始化的 Token 对象
Token *pt = new Token;          //@ 指向一个未初始化的 Token 对象的指针

访问 union 对象的成员:

last_token.cval = 'z';
pt->ival = 42;

为 union 的一个数据成员赋值会令其他数据成员变成未定义的状态。因此,使用 union 时,必须清楚知道当前存储在 union 中的值是什么类型。

匿名 union

匿名 union 是指未命名的 union,并且在右花括号和分号之间没有任何声明,一旦定义了一个匿名 union,编译器就自动地为该 union 创建一个未命名的对象:

union {
    //@ 匿名 union
    char cval;
    int  ival;
    double  dval;
};
//@ 可以直接访问匿名 union 对象的成员
cval = 'c';
ival = 42;

匿名 union 不能包含受保护的成员或私有成员,也不能定义成员函数。

含有类类型成员的 union

C++ 早期版本中,union 不能含有定义了构造函数或拷贝控制成员的类类型成员。C++ 11 取消了这一限制。

当 union 中包含的是内置类型的成员时,可以使用普通的赋值语句改变 union 保存的值。但是,如果 union 中含有特殊类类型的成员:

  • 如果想将 union 的值改为类类型成员对应的值,必须运行该类型的构造函数。
  • 如果将 union 的成员的值改成其他的值,必须运行该类型的析构函数。

当 union 包含的是内置类型成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。但是如果 union 含有类类型的成员,并且该类型自定义了默认构造函数或拷贝控制成员,则编译器将为 union 合成对应的版本并将其声明为删除的。

例如,string 类定义了五个拷贝控制成员以及一个默认构造函数,如果 union 含有 string 类型,并且没有自定义默认构造函数或某个拷贝控制成员,则编译器将合成缺少的成员并将其声明成删除的。如果在每个类中含有一个 union 成员,并且 union 含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作也将是删除的。

使用类管理 union 成员

对于 union 类似,要想构造或销毁类类型的成员必须执行非常复杂的操作,因此通常将含有类类型成员的 union 内嵌在另一个类中。这个类可以控制与 union 的类类型成员有关的状态转换。例如,为 union 添加一个 string 成员,并将 union 定义成匿名的 union,最后将它作为 Token 类的一个成员,此时,Token 类将管理 union 成员。

为了追踪 union 中到底存储了什么类型的值,通常会定义一个独立的对象,该对象成为 union 的判别式。可以 使用判别式辨认 union 存储的值。

class Token
{
public:
    //@ union 中含有 string 因此必须定义拷贝控制成员
    Token():tok(INT),ival{0}{}
    Token(const Token& t) :tok(t.tok) { copyUnion(t); }
    Token& operator=(const Token&);

    //@ 如果 union 含有一个 string 成员,则必须销毁它
    ~Token() { if (tok == STR) sval.~string(); }

    //@ 下面的赋值运算符负责设置 union 的不同成员
    Token& operator=(const std::string&);
    Token& operator=(char);
    Token& operator=(int);
    Token& operator=(double);

private:
    enum {INT,CHAR,DBL,STR} tok; //@ 判别式
    union { //@ 匿名 union
        char cval;
        int ival;
        double dval;
        std::string sval;
    };
    
    //@ 检查判别式,然后酌情拷贝 union 成员
    void copyUnion(const Token&);
};

使用 tok 作为判别式,当 union 存储的是一个 int 值,tok 就是 INT。当 union 存储的是一个 string值,tok 就是 STR,以此类推。

类的默认构造函数初始化判别式判别式以及 union 成员,令其保存 int 的值为0。

因为 union 定义了一个含有析构函数的成员,所以 Token 类也定义一个析构函数以及销毁 string 的成员。

和普通的类类型成员不一样,作为 union 组成部分的类成员无法自动销毁。因为析构函数不清楚当前 union 存储的是什么类型,所以无法确定销毁哪个成员。析构函数检查 union 当前存储的是否是 string 类型,如果是 string 类型则调用析构函数,如果是内置类型,则析构函数什么也不干。

管理判别式并销毁 string

类的赋值运算符负责设置 tok 并为 union 的相应成员赋值,和析构函数一样,这些运算符在为 union 赋新值前必须首先销毁 string:

Token& Token::operator=(int i)
{
    if (tok == STR)     //@ 如果当前存储的是 string,则首先释放它
        sval.~string();
    ival = i;       //@ 为成员赋值
    tok = INT;      //@ 更新判别式
    return *this;
}

double 和 char 的赋值运算符版本与 int 的类似,string 版本则有所区别:

Token& Token::operator=(const std::string& s)
{
    if (tok == STR) //@ 如果当前存储的是 string 类型,则可以直接赋值
        sval = s;
    else
        new(&sval) string(s);   //@ 否则需要先构造一个 string

    tok = STR;  //@ 更新判别式

    return *this;
}

如果当前存储的不是 string,则不能找到一个已存在的 string 对象供我们调用运算符。此时必须先利用定位 new 表达式,在内存中为 sval 构造一个 string,然后将该 string 初始化为形参的副本,更新判别会并返回结果。

管理需要拷贝控制的联合成员

拷贝构造函数和赋值运算符一样需要先检验判别式以明确拷贝所采用的的方式,为了完成这个任务,定义一个 copyUnion 的成员。

当在拷贝构造函数中调用 copyUnion 时,union 成员将被默认初始化,这意味着编译器会初始化 union 的第一个成员。因为 string 不是 union 的第一个成员,所以显然 union 成员保存的不是 string。

void Token::copyUnion(const Token& t)
{
    switch (t.tok)
    {
    case Token::INT: ival = t.ival; break;
    case Token::CHAR: cval = t.cval; break;
    case Token::DBL: dval = t.dval; break;
    //@ 拷贝 string 可以使用定位 new 表达式来构造
    case Token::STR:new(&sval) string(t.sval); break;
    }
}

赋值运算符必须处理 string 成员的三种可能情况:

  • 左侧运算对象和右侧对象都是 string。
  • 两个运算对象都不是 string。
  • 只有一个运算对象是 string。
Token& Token::operator=(const Token& t)
{
    //@ 如果此对象的值是 string,t 的值不是,则必须释放原来的 string
    if (tok == STR && t.tok != STR)
        sval.~string();
    if (tok == STR && t.tok == STR)
        sval = t.sval;
    else
        copyUnion(t);   //@ 如果 t.tok 是STR,这需要构造一个string

    tok = t.tok;

    return *this;
}

以上是关于union的主要内容,如果未能解决你的问题,请参考以下文章

c++ union学习

sql 查询结果合并union all用法_数据库技巧

使用 with as 优化SQL

臃肿的 UNION ALL 代码 (MYSQL)

UNION和UNION ALL的作用和语法

Union和Union All 的区别