侧边栏壁纸
博主头像
LittleAO的学习小站 博主等级

在知识的沙漠寻找绿洲

  • 累计撰写 125 篇文章
  • 累计创建 27 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

C++之 联合体union

LittleAO
2024-09-25 / 0 评论 / 0 点赞 / 20 阅读 / 0 字
温馨提示:
本文最后更新于2024-09-25,若内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

参考书目:《C++ Primer 第五版》

简介

联合体(union)是一种特殊的类,和其他类一样,union可以有多个数据成员。与其它类不同的是,在任意时刻只有一个数据成员有值。这个特性能够赋予union节省空间的性质。union的大小取决于数据成员中最大的那一个。

union也有一些与其它类不同的特性:

  • 数据成员不能含有引用类型;

  • 默认情况下成员是公用的。

  • union不能继承自其他类,也不能作为基类,因此成员函数不能有虚函数。

定义

定义union的语法规则如下:

union Token{
    int intVal;
    char charVal;
    float floatVal;
};

定义一个union,首先是关键字union,然后是一个可选的名字,最后是花括号的声明。

上面的代码就定义了Token,其值的类型可能是charintdouble的一种。

使用

Token t = {'a'};

上面这段代码就是将Token的所有成员赋值为'a'。查看赋值情况:

也发现了联合体的数据成员不能单独赋值。处理任一个数据成员都会对修改其他成员。如果我们错误的使用了数据成员,这是未定义的行为,程序可能导致崩溃。

匿名union

把可选的名字去掉,则将这样的联合体称为匿名union。在匿名union所在的定义域,可以直接访问其成员:

int main()
{
    union {
        int intVal;
        char charVal;
        float floatVal;
    };
    charVal = {'a'};
    return 0;
}

需要注意的是,匿名union不能包含受保护的成员或私有成员,也不能定义为成员函数。

union中含有类成员

union中只能包含特定的类类型成员

在早期的C++中,union的成员类型中如果有类,则该类不能定义自己的构造函数和拷贝控制成员。在C++11中取消了这一限制。看下面的场景:

如果一个union中含有两个类成员,将其中一个类成员的值改为其他值,就需要分别执行依次构造(构造新的值)和析构(析构旧的值),如果改为另一个类类型的值,有需要一次构造和析构。

如果union中包含了类类型的成员,则编译器就会按照次序合成默认构造函数和拷贝控制成员。如果union中的类类型自定义了默认构造函数或拷贝控制成员,那么union自己合成的版本将会被声明为删除的。

struct Example
{
    char data;
};
int main()
{
    union {
        int intVal;
        char charVal;
        float floatVal;
        Example classVal;
    };
    return 0;
}

上面这段代码能够编译通过,因为我们定义的类没有自定义的默认构造函数。假如说我们定义了默认构造函数:

struct Example
{
    Example() : data('a') {}
    char data;
};
int main()
{
    union {
        int intVal;
        char charVal;
        float floatVal;
        Example classVal;
    };
    return 0;
}

则会报错,报错内容如下:

playground\main.cpp(9): error C2280: “main::<unnamed-type-$S1>::<unnamed-type-$S1>(void)”: 尝试引用已 删除的函数
playground\main.cpp(14): note: 编译器已在此处生成“main::<unnamed-type-$S1>::<unnamed-type-$S1>”       
playground\main.cpp(14): note: “main::<unnamed-type-$S1>::<unnamed-type-$S1>(void)”: 由于“main::<unnamed-type-$S1>”的变量数据成员“main::<unnamed-type-$S1>::classVal”包含不常用的 默认构造函数,因此已隐式删除函数
playground\main.cpp(13): note: 参见“main::<unnamed-type-$S1>::classVal”的声明

表明union不支持自定义的默认构造函数,假如说我们声明了默认构造函数为自动生成的,但是还有我们定义的其他构造函数,像下面这样:

struct Example
{
    Example() = default;
    Example(char c) : data(c) {}
    char data;
};
int main()
{
    union {
        int intVal;
        char charVal;
        float floatVal;
        Example classVal;
    };
    return 0;
}

就可以通过编译。

使用类来管理union成员

一般的union不能含有string成员,我们可以通过定义一个独立的类中嵌入union来间接实现这个需求:

// 一些实现省略
class Token {
public:
    Token() : tok(INT), ival(0) {}
    Token(const Token& t): tok(t.tok) { copyUnion(t); }
    Token& operator=(const Token&);
    ~Token() { if (tok == STR) sval.~basic_string(); }
    Token& operator=(const std::string&);
    Token& operator=(char);
    Token& operator=(int);
    Token& operator=(double);

private:
    enum {INT, CHAR, DBL, STR} tok; // 判别式
    union {
        char cval;
        int ival;
        double dval;
        std::string sval;
    };
    void copyUnion(const Token&);
};

通过在类中嵌套union的操作,也能间接弥补union的不足。为了确定当前类中是哪个数据类型,我们定义了一个判别对象。这个判别对象会在析构函数中起作用。

下面是一个完整的Token实现:

#include <iostream>
#include <string>

class Token {
public:
    Token() : tok(INT), ival(0) {} // 默认构造函数

    Token(const Token& t) : tok(t.tok) {
        copyUnion(t); // 复制 union 的内容
    }

    Token& operator=(const Token& t) {
        if (this != &t) { // 自赋值检查
            // 如果当前是字符串,需要先析构
            if (tok == STR) {
                sval.~basic_string();
            }
            tok = t.tok; // 更新判别式
            copyUnion(t); // 复制 union 的内容
        }
        return *this;
    }

    ~Token() {
        if (tok == STR) {
            sval.~basic_string(); // 只在 tok 是 STR 时析构
        }
    }

    Token& operator=(const std::string& str) {
        if (tok == STR) {
            sval.~basic_string(); // 先析构原有字符串
        }
        new(&sval) std::string(str); // 在 union 中构造新字符串
        tok = STR; // 更新判别式
        return *this;
    }

    Token& operator=(char c) {
        if (tok == STR) {
            sval.~basic_string(); // 先析构原有字符串
        }
        cval = c; // 赋值
        tok = CHAR; // 更新判别式
        return *this;
    }

    Token& operator=(int i) {
        if (tok == STR) {
            sval.~basic_string(); // 先析构原有字符串
        }
        ival = i; // 赋值
        tok = INT; // 更新判别式
        return *this;
    }

    Token& operator=(double d) {
        if (tok == STR) {
            sval.~basic_string(); // 先析构原有字符串
        }
        dval = d; // 赋值
        tok = DBL; // 更新判别式
        return *this;
    }

    void print() const {
        switch (tok) {
            case INT:
                std::cout << "Integer: " << ival << std::endl;
                break;
            case CHAR:
                std::cout << "Character: " << cval << std::endl;
                break;
            case DBL:
                std::cout << "Double: " << dval << std::endl;
                break;
            case STR:
                std::cout << "String: " << sval << std::endl;
                break;
        }
    }

private:
    enum { INT, CHAR, DBL, STR } tok; // 判别式
    union {
        char cval;
        int ival;
        double dval;
        std::string sval; // 使用 std::string 作为 union 的成员
    };

    void copyUnion(const Token& t) {
        switch (t.tok) {
            case INT:
                ival = t.ival;
                break;
            case CHAR:
                cval = t.cval;
                break;
            case DBL:
                dval = t.dval;
                break;
            case STR:
                new(&sval) std::string(t.sval); // 在 union 中构造字符串
                break;
        }
    }
};

int main() {
    Token t1;
    t1 = 42;
    t1.print();

    Token t2;
    t2 = 'A';
    t2.print();

    Token t3;
    t3 = 3.14;
    t3.print();

    Token t4;
    t4 = std::string("Hello, World!");
    t4.print();

    return 0;
}

0

评论区