联合体声明
也被称为非受限联合体
我们知道,在C/C++中,联合体是一种构造类型的数据结构。在一个联合体内,可以定义多种不同的数据类型,这些数据会共享相同的内存空间,这在一些需要复用内存的情况下,可以达到节省空间的目的,不过,根据C++98标准,并不是所有的数据类型都能够称为联合体的数据成员,比如:
struct Student{
Student(bool g, int a) : gender(g), age(a) {}
bool gender;
int age;
};
union T{
Student s; // C98中编译出错
int id;
char name[10];
};
在C++99中,因为Student是非POS的,所以编译不过(目的是为了和C兼容。实际上,除了非POD类型外,C++98也不允许联合体拥有静态或者引用类型的成员)。但在C++11中,取消了联合体对数据成员类型的限制。
标准规定,任何非引用类型都可以称为联合体的数据类型,这样的联合体就是所谓的非受限联合体
定义
联合体是特殊的类类型,它在一个时刻只能保有一个非静态数据成员
union attr 类头名 { 成员说明 }
解释
联合体union的定义方式与结构体一样,但是二者有根本区别。
- 在结构体中各成员有自己的内存空间,一个结构变量的总长度是各个成员长度之和
- 在联合体中,各成员共享一段内存空间,一个联合变量的长度等于各成员中最大的长度
共用体使不同的类型变量存放到同一段内存单元中,所以共用体在同一时刻只能存储一个数据成员的值,共用体的大小等于最大成员的大小(结构体变量的大小是所有数据成员大小的总和)。
#include <iostream>
union var{
long int l;
int i;
};
int main()
{
union var v;
v.l = 5;
printf("v.l is %d\n",v.i);
v.i = 6;
printf("now v.l is %ld! the address is %p\n",v.l,&v.l);
printf("now v.i is %d! the address is %p\n",v.i,&v.i);
}
- 从上面可以看出,联合体是共用一个内存首地址。
- 在程序中改变共用体的一个成员,其他成员也会随之改变。不是强制类型转换!而是因为他们的值的每一个二进制位都被新值所覆盖。
联合体的大小仅足以保有其最大的数据成员。其他的数据成员分配于该最大成员的一部分相同的字节。分配的细节是实现定义的,而且从非最近写u人的联合体读取数据是未定义行为。许多编译器作为非标准语言扩展,实现读取联合体不活跃成员的能力
#include <variant>
#include <iostream>
union S{
std::int32_t n; // 4字节
std::uint16_t s[2]; // 4字节
std::uint8_t c; // 1字节
}; // 整个联合体占用4字节
int main()
{
S s = {0x12345678}; // 初始化首个成员,s.n现在是活跃成员
// 于此点,从s.s和s.c读取是未定义行为
std::cout << std::hex << "s.n = " << s.n << "\n";
s.s[0] = 0x0011; // s.s现在是活跃成员
// 在这里,从s.n或者s.c读取是UB但是的大叔编译器对齐都有地暖管一
std::cout << "s.c is now " << +s.c << '\n' // 11 或 00,取决于平台
<< "s.n is now " << s.n << '\n'; // 12340011 或 00115678
}
每个成员都如同她是类的仅有成员一样分配
联合体与
{}
初始化
#include <iostream>
using namespace std;
union T{
int x;
double d;
char b[sizeof(double)];
};
int main(){
T t = {0}; //到底是初始化第一个成员还是所有成员呢?
printf("%d, %f, %f\n", t.x, t.d, t.d);
T t1 = {1};
printf("%d, %f, %f\n", t1.x, t1.d, t1.d);
// T t2 = {1, 2.0}; // error
// printf("%d, %f, %f\n", t2.x, t2.d, t2.d);
}
被删除的默认构造函数
在C++98中,标准规定了联合体会自动对未在初始化成员列表中出现的成员赋默认初值。然而,对于联合体而言,这种初始化常常会带来疑问,因为在任何时刻都只有一个成员可以是有效的。比如:
union T{
int x;
double d;
char b[sizeof(double)]
};
T t = {0}; //到底是初始化第一个成员还是所有成员呢?
而在C++中,为了减少这样的疑问,标准会默认删除一些非受限联合体的默认函数,比如,非受限联合有一个非POD成员,而该非POD成员类型拥有非平凡的构造函数,那么非受限联合体成员的默认构造函数将会被编译器删除。其他的特殊成员函数,比如默认拷贝函数、拷贝赋值函数、析构函数等,也遵从此规则:
#include <iostream>
using namespace std;
union T{
string s; //string有非平凡的构造函数
int n;
};
int main(){
T t; // 失败,因为T的构造函数被删除了
}
上面联合体T拥有一个非POD的成员s,而string有非平凡的构造函数, 因此 T的构造函数被删除,其类型的变量t也就无法声明成功。解决这个问题的方法是,由程序员自己为非受限联合体定义构造函数。通常情况下,布局new会发挥很大的作用:
#include <iostream>
using namespace std;
union T{
string s;
int n;
public:
// 自定义构造函数和析构函数
T() {new (&s) string;}
~T() {s.~string();}
};
int main(){
T t; // ok
}
上面构造时采用布局new将s构造在其地址&s上。这里布局new的唯一作用是调用了一下string的构造函数。在析构时,又调用了string的析构函数。
特殊的成员:类
在C++11起,如果联合体的成员是拥有用户定义的构造函数和析构函数的类,则为了切换其活跃成员,通常需要显式析构函数和布置 new:
#include <vector>
#include <iostream>
union S{
std::string str;
std::vector<int> vec;
~S(){}; // 需要知道哪个成员活跃,仅在联合体式的类中可行
}; // 整个联合体占有 max(sizeof(string), sizeof(vector<int>)) 的内存
int main()
{
S s = {"Hello world"};
// 在此点,从s.vec读取是未定义行为
std::cout << "s.srr = " << s.str << "\n";
s.str.~basic_string();
new (&s.vec) std::vector<int>;
// 现在, s.vec是联合体活跃成员
s.vec.push_back(10);
std::cout << s.vec.size() << '\n';
s.vec.~vector();
}
若两个联合体成员均为标准布局类型,则在任何编译器上检验其公共子序列都是良好定义的
联合体中的静态成员(不建议使用)
#include <iostream>
using namespace std;
union U{
static int k;
static int k1;
int v;
static long Get(){
return 32;
}
};
int U::k = 11;
int U::k1 = 12;
int main(){
U v;
printf("%d, %d, %d, %ld", U::k, U::k1, v.v, U::Get());
}
匿名联合体与类
匿名联合体可以用于类中,这样的类也叫做“枚举式的类”
#include <iostream>
#include <memory>
#include <cstring>
using namespace std;
struct Student{
Student(bool g, int a) : gender(g), age(a){}
bool gender;
int age;
};
class Singer{
public:
enum Type {STUDENT, NATIVE, FOREIGNER};
Singer(bool g, int a) : s(g, a){
t = STUDENT;
}
Singer(int i) : id(i){
t = NATIVE;
}
Singer(const char *n, int s){
int size = (s > 9) ?9 : s;
memcpy(name, n, size);
name[s] = '\0';
t = FOREIGNER;
}
private:
Type t;
union {
Student s;
int id;
char name[10];
};
};
int main(){
Singer(true, 13);
Singer(31023);
Singer("J Michael", 9);
}
应用
将一个32位的整型数拆分为4个单字节的数
如将一个数0x12345678拆分为4个单字节的数为:0x78、0x56、0x34、0x12,则主要实现代码如下:
#include <iostream>
union data{
int i;
char ch[4];
}data;
int main()
{
data.i = 0x12345678;
std::cout << std::showbase << std::hex
<< "data: " << data.i << "\n"
<< "ch[0] - ch[3]: " << int(data.ch[0]) << "\t"
<< int(data.ch[1]) << "\t"
<< int(data.ch[2]) << "\t"
<< int(data.ch[3]) << "\t" ;
}
判断当前操作系统字节序的大小端问题
原理: 在联合体中定义一个4字节整数i和一个单字节整数ch,而且赋值i为1(16禁止表示0x00000001),利用联合体中所有变量共享内存区域的特性,如果系统端是小端序,则该联合体的低字节段存放的是地位字节(0x01),反之存在的是高位字节(0x00),那么可以根据ch的值(存放在该联合体的低地址端,非0即1)来判断当前系统的字节序问题。
#include <iostream>
bool checkByteOrder(){
union {
int i;
char ch;
} a;
a.i == 1;
return (a.ch == 1);
}
int main()
{
if(checkByteOrder() == 1){
printf("当前操作系统是小端序");
}else{
printf("当前操作系统是大端序");
}
}
- 联合体可以拥有成员函数(包含构造函数和析构函数),但不能有虚函数
- 联合体不能有基类而且不能用作基类
- 联合体不能有引用类型的非静态数据成员
- 正如结构体的声明中一般,联合体的默认成员访问是 public
在C++11前,联合体不能拥有带平凡特殊成员函数(赋值构造函数、赋值赋值运算符或者析构函数)的非静态数据成员
这句话怎么理解呢?比如string可以作为结构体的成员但是不能作为共用体的成员
union DateUnion
{
int iInt1;
int iInt2;
double douDou;
std::string strChar;
};
错误原因:String 是一个类,有自己的构造函数,析构函数。
分析:Union的一大特征在于,一个Union类中的所有数据共享同一段内存。如果union类的成员包含自己的构造函数,析构函数,那么同一Union类的成员在初始化时,就有可能会执行不同的构造函数。这是无法预料的。所以,我们在定义Union类时要尽量避免成员变量是对象(含有自己的构造函数)
在C++11起,若联合体含有带非平凡默认构造函数的非静态数据成员,则该联合体的默认构造函数默认被弃置,除非联合体的变体成员拥有一个默认成员初始化器。
我们来看个例子
#include <iostream>
struct TestUnion
{
TestUnion() {}
};
typedef union
{
TestUnion obj;
} UT;
int main ()
{
return 0;
}
而如果去掉那个什么也没干的构造函数,则一切OK。
问:为什么编译器不允许union成员有构造函数呢?
推测:如果C++标准允许union的成员有构造函数,那么,在进行空间分配的时候要不要执行这个构造函数呢?如果要,它们如果TestUnion的构造函数中包含了一些内存分配的操作,或者其它对整个application状态的修改,那么,如果我今后要用到obj的话,事情可能还比较合理,但是如果我根本就不使用obj这个成员呢?由于obj的引入造成的对系统状态的修改显然是不合理的;反之,如果不允许有,那么一旦今后我们选中了obj来进行操作,那么所有的信息都没有初始化(如果是普通的struct,没什么问题,但是,如果有虚函数呢?)。更进一步,假设现在我们的union不是只有一个 TestUnion obj,还有一个TestUnion2 obj2,二者均有构造函数,并且都在构造函数中执行了一些内存分配的工作(甚至干了很多其它事情),那么,如果先构造obj,后构造obj2,则执行的 结果几乎可以肯定会造成内存的泄漏。
鉴于以上诸多麻烦(可能还有更多麻烦),在构造union函数时,编译器只负责分配空间,而不负责去执行附加的初始化工作。为了简化工作,只要我们提供了构造函数,就会错误:error C2620: union '__unnamed' : member 'obj' has user-defined constructor or non-trivial default constructor
同理,除了不能加构造函数,析构函数/拷贝构造函数/赋值运算符也不可以加
https://www.pianshen.com/article/9233174821/
https://www.icode9.com/content-1-560781.html
https://www.jb51.net/article/66711.htm(没有看完)
官方文档