C++11 非受限联合体
【1】受限联合体
一个联合体内,可以定义多种不同数据类型的成员,这些数据成员将会共享相同内存空间,在一些需要复用内存的情况下,可以达到节省空间的目的。
不过,根据C++98标准,并不是所有的数据类型都能够成为联合体的数据成员。如下代码:
1 struct Student 2 { 3 Student(bool g, int a) : gender(g), age(a) {} 4 5 bool gender; 6 int age; 7 }; 8 9 union T 10 { 11 Student s; // 编译失败,Student不是一个POD类型 12 int id; 13 char name[10]; 14 };
声明了一个Student的类型。根据之前POD类型的知识,由于Student自定义了一个构造函数,所以该类型是非POD类型的。
在C++98标准中,union T是无法通过编译的。事实上,除了非POD类型之外,C++98标准也不允许联合体拥有静态或引用类型的成员。
这样虽然可能在一定程度上保证了和C的兼容性,不过也为联合体的使用带来了很大的限制。
【2】非受限联合体
在C++11标准中,取消了联合体对于数据成员类型的限制。
标准规定,任何非引用类型都可以成为联合体的数据成员,而这样的联合体即所谓非受限联合体(Unrestricted Union)。
(1)非受限联合体中的静态成员(静态成员变量和静态成员函数)
联合体拥有静态成员(在非匿名联合体中)的限制,也在C++11新标准中被删除了。
不过从实践中,发现C++11的规则不允许静态成员变量的存在(否则所有该类型的联合体将共享一个值)。
而静态成员函数存在的唯一作用,大概就是为了返回一个常数,如下示例:
1 #include <iostream> 2 using namespace std; 3 4 union T 5 { 6 static long Get() { return 2020; } 7 }; 8 9 int main() 10 { 11 cout << T::Get() << endl; // 2020 12 }
定义了一个有静态成员函数的联合体,不过看起来这里的union T更像是一个作用域限制符,并没有太大的实用意义。
(2)非受限联合体的初始化
C++98标准规定,联合体会自动对未在初始化成员列表中出现的成员赋默认初值。
然而对于联合体而言,这种初始化常常会带来疑问,因为在任何时刻只有一个成员可以是有效的。如下示例:
1 union T 2 { 3 int x; 4 double d; 5 char b[sizeof(double)]; 6 }; 7 8 T t = { 0 }; // 到底是初始化第一个成员还是所有成员呢?
使用了花括号组成的初始化列表,试图将成员变量x初始化为零,即整个联合体的数据t中低位的4字节被初始化为0,然而实际上,t所占的8个字节将全部被置0。
而在C++11中,为了减少这样的疑问,标准会默认删除一些非受限联合体的默认函数。
比如,非受限联合体有一个非POD的成员,而该非POD成员类型拥有非平凡的构造函数,那么非受限联合体成员的默认构造函数将被编译器删除。
其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将遵从此规则。
如下示例:
1 #include <string> 2 using namespace std; 3 4 union T 5 { 6 string s; // string有非平凡的构造函数 7 int n; 8 }; 9 10 int main() 11 { 12 T t; // 构造失败,因为T的构造函数被删除; ERROR:尝试引用已删除的函数 13 }
联合体T拥有一个非POD的成员变量s。而string却有非平凡的构造函数,因此T的构造函数被删除,其类型的变量t也就无法声明成功。
解决这个问题的办法:由程序员自己为非受限联合体定义构造函数。通常情况下,placement new会发挥很好的作用,改为如下:
1 #include <string> 2 using namespace std; 3 4 union T 5 { 6 string s; // string有非平凡的构造函数 7 int n; 8 public: 9 T() { new (&s) string; } // 自定义构造函数 10 ~T() { s.~string(); } // 自定义析构函数 11 }; 12 13 int main() 14 { 15 T t; // 编译通过 16 }
自定义了union T的构造和析构函数。构造时,采用placement new将s构造在其地址&s上。这里placement new的唯一作用只是调用了一下string的构造函数。
而在析构时,又调用了string的析构函数。
必须注意的是,析构的时候union T也必须是一个string对象,否则可能导致析构的错误(或者让析构函数为空,至少不会造成运行时错误)。
这样一来,变量t的声明就可以通过编译。
【3】匿名非受限联合体
匿名非受限联合体可以运用于类的声明中,这样的类也称为“枚举式的类”。如下示例:
1 #include <string> 2 #include <iostream> 3 using namespace std; 4 5 struct Student 6 { 7 Student(bool g, int a) : gender(g), age(a) {} 8 9 bool gender; 10 int age; 11 }; 12 13 class Singer 14 { 15 public: 16 enum Type { STUDENT, NATIVE, FOREIGNER }; 17 Singer(bool g, int a) : s(g, a) 18 { 19 t = STUDENT; 20 } 21 Singer(int i) : id(i) 22 { 23 t = NATIVE; 24 } 25 Singer(const char* n, int s) 26 { 27 int size = (s > 9) ? 9 : s; 28 memcpy(name, n, size); 29 name[s] = '\0'; 30 t = FOREIGNER; 31 } 32 ~Singer() {} 33 void print() 34 { 35 switch (t) 36 { 37 case STUDENT: 38 cout << "s.gender: " << s.gender << endl; 39 cout << "s.age: " << s.age << endl; 40 break; 41 case NATIVE: 42 cout << "id: " << id << endl; 43 break; 44 case FOREIGNER: 45 cout << "name: " << name << endl; 46 break; 47 default: 48 break; 49 } 50 } 51 private: 52 Type t; 53 union 54 { 55 Student s; 56 int id; 57 char name[10]; 58 }; 59 }; 60 61 int main() 62 { 63 Singer objSer1(true, 13); 64 objSer1.print(); 65 66 Singer objSer2(20200129); 67 objSer2.print(); 68 69 Singer objSer3("kaizenliu", 9); 70 objSer3.print(); 71 72 system("pause"); 73 } 74 75 /*运行结果 76 s.gender: 1 77 s.age: 13 78 id: 20200129 79 name: kaizenliu 80 */
把匿名非受限联合体成为类Singer的“变长成员”(variant member)。可以看到,这样的变长成员给类的编写带来了更大的灵活性。
good good study, day day up.
顺序 选择 循环 总结