C++:64---特殊工具与技术之union(联合/共用体,一种节省空间的类)

一、联合(union)概述

  • 联合(union)是一种特殊的类
  • 一个union可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给union的某个成员赋值之后,该union的其它成员就变成未定义的状态了。分配给一个union对象的存储空间至少要能容纳它的最大的数据成员
  • 类的某些特性对union同样适用,但并非所有特性都如此:
    • union不能含有引用类型的成员,union的成员可以是绝大多数类型。在C++11标准中,含有构造函数或析构函数的类类型也可以作为union的成员类型
    • union可以为其成员指定public、protected、private等标记默认情况下union的成员都是公有的
    • union可以定义包括构造函数和析构函数在内的成员函数。但是由于union既不能继承自其它类,也不能作为基类使用,所以在union中不能含有虚函数

二、定义union

  • union提供了一种有效的途径使得我们可以方便地表示一组类型不同的互斥值
  • 例如我们需要处理一些不同类型的数字数据和字符数据,则在此过程中可以定义一个union来保存这些值:
//Token类型的对象只有一个成员,该成员的类型可能是下列类型中的任意一种
union Token {
    char cval;
    int ival;
    double dval;
};

三、使用union类型

  • 默认情况下union是未初始化的。我们也可以像显式初始化聚合类一样使用一对花括号来初始化一个union
  • 例如:
Token first_token = { 'a' }; //初始化cval成员
Token last_token;            //未初始化的Token对象
Token *pt = new Token;       //指向一个未初始化的Token对象的指针
  • 如果提供了初始值,则该初始化被用于初始化第一个成员。因此,first_token的初始化过程实际上是给cval成员赋了一个初值
  • 我们可以通过成员访问运算符访问一个union对象的成员。例如:
Token last_token;
last_token.cval = 'z';
std::cout << last_token.cval << std::endl; //z
std::cout << last_token.ival << std::endl; //乱值
std::cout << last_token.dval << std::endl; //乱值

Token *pt = new Token;
pt->ival = 42;
std::cout << pt->cval << std::endl; //乱值
std::cout << pt->ival << std::endl; //42
std::cout << pt->dval << std::endl; //乱值
  • 为union的一个数据成员赋值之后,其他数据成员变为了未定义的状态。因此,当我们使用union时,必须清楚的2知道当前存储在union中的值到底是什么类型。如果我们使用错误的数据成员或为错误的数据成员赋值,则程序可能会出现异常行为
  • 因为union共用一个数据,因此当我们为一个数据赋值之后,就可以将所有的联合成员当做一个使用。但是使用的时候必须根据union赋值的那个值的类型来进行使用
Token last_token;

last_token.cval = 'c';
printf("%c\t%c\t%c\n", last_token.cval, last_token.ival, last_token.dval);

last_token.ival = 50;
printf("%d\t%d\t%d\n", last_token.cval, last_token.ival, last_token.dval);

四、匿名union

  • 匿名union是一个未命名的union,并且在右花括号和分号之间没有任何声明
  • 一旦我们定义了一个匿名union,编译器就自动地为该union创建一个未命名的对象
  • 匿名union不能定义在全局作用域中
  • 例如:
int main()
{
    //匿名union,不能定义在全局作用域中
    union {
        char cval;
        int ival;
        double dval;
    };

    cval = 'c';
    //此时union中的值都为cval指定的值
	printf("%c\t%c\t%c\n", cval, ival, dval);
	printf("%d\t%d\t%d\n", cval, ival, dval);

	ival = 42;
    //此时union中的值都为ival指定的值
	printf("%c\t%c\t%c\n", cval, ival, dval);
	printf("%d\t%d\t%d\n", cval, ival, dval);
    return 0;
}

 

五、含有类类型成员的union

  • C++早期版本中,union中不能含有定义了构造函数或拷贝控制成员的类类型成员。C++11标准取消了这一限制
  • 如果union的成员类型定义了自己的构造函数/或拷贝控制成员,则该union的用法要比只含有内置类型成员的union复杂得多
  • union的赋值与析构:
    • 当union包含的是内置类型的成员时:我们可以使用普通的赋值语句改变union保存的值
    • 当union含有特殊类类型成员时:当我们将union的值改为类类型成员对应的值时,必须运行该类型的构造函数;如果将类类型成员的值改为另外的值时,必须运行该类型的析构函数
  • union的构造/拷贝:
    • 当union包含的是内置类型的成员时:编译器按照成员的次序依次合成默认构造函数或拷贝控制成员
    • 当union含有特殊类类型成员时:
      • 如果该类型自定义了默认构造函数或拷贝控制成员,则编译器将为union合成对应的版本并将其声明为删除的。例如一个union含有一个string类型的成员(string定义了5个拷贝控制成员以及一个默认构造函数,并且没有自定义默认构造函数或某个拷贝控制成员),编译器将合成缺少的成员并将其声明为删除的
      • 如果在某个类中含有一个union成员,而且该union含有删除的拷贝控制成员,则该类与之对应的拷贝控制操作也将是删除的

使用类管理union成员

  • 对于union来说,想要构造或销毁类类型成员必须执行非常复杂的操作,因此我们通常把含有类类型成员的union内嵌在另一个类中。这个类可以管理并控制与union的类类型成员有关的状态转换
  • 例如:
    • 我们为union添加一个string成员,并将我们的union定义为匿名的union,最后将它作为Token类的一个成员
    • 为了追踪union中到底存储了什么类型的值,我们通常会为union定义一个判别式。我们可以使用判别式辨认union存储的值。为了保持union与其判别式同步,我们将判别式也作为Token的成员(此处定义一个枚举类型作为判别式)
    • 在类中定义的函数包括默认构造函数、拷贝控制成员以及一组赋值运算符,这些赋值运算符可以将union的某种类型赋给union成员
class Token
{
public:
    //因为union含有一个string成员,所以Token必须定义拷贝控制成员
    Token() :tok(INT), ival(0) {}
    Token(const Token &t) :tok(t.tok) { copyUnion(t); }
    Token &operator=(const Token&);
    ~Token() {
        //如果union含有一个string成员,则我们必须销毁它
        if (tok == STR)
            sval.~string();
    }

    //下面的赋值运算符负责设置union的不同成员
    Token &operator=(char);
    Token &operator=(int);
    Token &operator=(double);
    Token &operator=(const std::string&);
private:
    //判别式
    enum { INT, CHAR, DBL, STR }tok;
    //匿名union
    union {
        char cval;
        int ival;
        double dval;
        std::string sval;
    };

    //检查判别式,然后酌情拷贝union成员
    void copyUnion(const Token&);
};
  • 我们的类定义了:
    • 一个枚举,并将其作为tok成员的类型,我们使用tok作为判别式:当union存储的是一个int值时,tok的值为INT;当union存储的是一个string值时,tok的值为STR......以此类推
    • 我们的union含有一个析构函数:因为union可能存储string成员,因此如果union存储的是string,那么string的析构将被定义为删除的,因此我们需要手动的删除它
    • 其他内容在下面一一介绍

赋值运算符的定义

  • 在Token中我们定义了4个赋值运算符
  • 下面是前三个赋值运算符的定义,其形式都是类似的:
Token &Token::operator=(char i)
{
    //如果当前存储的string,需要释放
    if (tok == STR)
        sval.~string();
    //进行一系列赋值
    cval = i;
    tok = CHAR;
    return *this;
}

Token &Token::operator=(int i)
{
    if (tok == STR)
        sval.~string();
    ival = i;
    tok = INT;
    return *this;
}

Token &Token::operator=(double i)
{
    if (tok == STR)
        sval.~string();
    dval = i;
    tok = DBL;
    return *this;
}
  • 如果为union的string成员赋值,则与上面的运算符稍有不同:
Token &Token::operator=(const std::string& i)
{
    //如果当前存储的就是string类型,那么直接赋值即可
    if (tok == STR)
        sval = i;
    //如果不是,那么使用定位new,在sval的地址上创建一个string对象
    else
        new(&sval) string(i);
    tok = STR;

    return *this;
}

拷贝控制函数(copyUnion()函数)、拷贝赋值运算符

  • 拷贝控制函数:
    • 我们先将拷贝当前类的判别式tok,然后再调用copyUnion()函数
    • copyUnion()函数定义如下:
      • 根据参数传入的Token对象,我们解析出其tok,然后再进行分别的拷贝赋值
      • 对于内置类型,直接赋值即可;对于string,我们采用定位new的方式进行构造
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;
    case Token::STR:
        new(&sval) string(t.sval);
        break;
    }
}
  • 赋值运算符的定义如下:
    • 我们必须能处理string成员的三种可能情况:左侧运算对象和右侧运算对象都是stirng;两个运算对象都不是string;只有一个运算对象时string
Token &Token::operator=(const Token& t)
{
    //如果当前对象union存储的是string,那么需要先释放当前string
    if ((tok == STR) && (t.tok != STR))
        sval.~string();
    
    //下面开始赋值操作
    //如果当前对象union与要被拷贝的对象union中存储的都是string,那么直接赋值即可
    if ((tok == STR) && (t.tok == STR))
        sval = t.sval;
    else  //如果t.tok是STR,则需要构造一个string
        copyUnion(t);

    //判别式的设定
    tok = t.tok;
    return *this;
}
发布了1504 篇原创文章 · 获赞 1063 · 访问量 43万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104729105