参考书目:《C++ Primer 第五版》
简介
联合体(union)是一种特殊的类,和其他类一样,union可以有多个数据成员。与其它类不同的是,在任意时刻只有一个数据成员有值。这个特性能够赋予union节省空间的性质。union的大小取决于数据成员中最大的那一个。
union也有一些与其它类不同的特性:
数据成员不能含有引用类型;
默认情况下成员是公用的。
union不能继承自其他类,也不能作为基类,因此成员函数不能有虚函数。
定义
定义union的语法规则如下:
union Token{
int intVal;
char charVal;
float floatVal;
};定义一个union,首先是关键字union,然后是一个可选的名字,最后是花括号的声明。
上面的代码就定义了Token,其值的类型可能是char、int或double的一种。
使用
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;
}
评论区