C++ 第八章 结构、联合与枚举 - 8.3 联合
2021/12/24 11:07:32
本文主要是介绍C++ 第八章 结构、联合与枚举 - 8.3 联合,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
第八章 结构、联合与枚举
- 8.3 联合
- 8.3.1 联合和类
- 8.3.2 匿名union
8.3 联合
union是一种特殊的struct,它的所有成员都分配在同一个地址空间上。因此,一个union实际占用的空间大小与其最大的成员一样。自然地,在同一时刻union只能保存一个成员的值。例如,考虑一个符号表项存放名字和值对的情况:
enum Type{str, num}; struct Entry{ char* name; Type t; char* s; //如果t == str,使用s int i; //如果t == num,使用i }; void f(Entry* p) { if(p->t == str) cout << p->s; //... }
在这个例子中,成员s和i永远不会被同时使用,因此空间都浪费了。我们可以把它们指定成union的成员,以解决上述问题:
union Value{ char* s; int i; };
语言本身并不负责追踪和管理union到底存的是哪些值,这是程序员的责任:
struct Entry{ char* name; Type t; Value v; //如果 t==str,使用v.s;如果t==num,使用v.i }; void f(Emtry* p) { if(p->t == str) cout << p->v.s; //... }
为了避免可能出现的错误,程序员最好把union封装起来,从而确保访问union成员的方式与该成员的类型永远保持一致(见8.3.2节)。
联合有时候会被误用于“类型转换”的目的。这种误用的情况常常发生在一些特定的程序员身上,他们曾经使用的编程语言缺少显式类型转换的功能,因此不得不采用这种方式。例如,下面所示的 int 向 int* 的“转换”以这两种类型逐位等价为前提:
union Fudge{ int i; int* p; }; int* cheat(int i) { Fudge a; a.i = i; return a.p; //错误的用法 }
这根本算不上是一种类型转换。在一些机器环境中,int和int*占用的空间大小并不一样;而在另外一些机器中,整数的地址不能是奇数。因此,像这样使用union不但危险,而且无法移植。如果你确实需要类似的转换,最好使用显式类型转换符(见11.5.2节),这样读者就能清楚地知道到底发生了什么。例如:
int* cheat2(int i) { return reinterpret_cast<int*>(i); //显然这个转换本身既不美观,也很容易出错 }
无论如何,在上面的代码中,如果转换前后对象的尺寸不一致,那么编译器至少有机会给出报错信息;它比使用union的版本强多了。
使用union的目的无非是让数据更紧密,从而提高程序的性能。然而,大多数程序即使用了union也不会提高太多;同时,使用union的代码更容易出错。因此,我认为union是一种被过度使用的语言特性,最好不要出现在你的程序中。
8.3.1 联合和类
在很多非平凡的union中,存在一个不太常用的成员,它的尺寸比其他常用成员的尺寸都大得多。因为union的尺寸与它最大的成员一样大,所以避免地存在空间浪费的情况。我们可以使用一组派生类(见3.2.2节和第20章)代替union,从而避免空间的浪费。
从技术上来说,union是一种特殊的struct(见8.2节),而struct是一种特殊的class(第16章)。然而,很多提供给类的功能与联合无关,因此对union施加了一些限制:
- union不能含有虚函数
- union不能含有引用类型的成员
- union不能含有基类
- 如果union的成员含有用户自定义的构造函数、拷贝操作、移动操作或者析构函数,则此类函数对于union来说被delete掉了(见3.3.4节和17.6.4节)。换句话说,union类型的对象不能含有这些函数
- 在union的所有成员中,最多只能有一个成员含类内初始化器(见17.4.4节)
- union不能被用作其他类的基类
这些约束规则有效地阻止了很多错误的发生,同时简化了union的实现过程。后面一点非常重要,因为union的主要作用是优化代码的性能,所以我们肯定不希望在使用union的过程中引入“隐形的代价”。
如果union的成员含有构造函数(及其他),则必须delete掉这些函数。这条规则使得简单的union使用起来确实简单。如果需要用到更复杂的操作,那么由程序员来实现。例如,因为Entry的成员不含构造函数、析构函数及赋值操作,所以我们能自由地创建Entry的副本:
void f(Entry a) { Entry b = a; };
如果对一个复杂的union执行同样的操作,则不仅实现起来很难,而且容易出错:
union U{ int m1; complex<double> m2; //复数含有构造函数 string m3; //string含有构造函数(维护一个重要的不变量) };
要想拷贝U,程序员必须决定使用哪个拷贝操作。例如:
void f2(U x) { U u; //错误:哪个默认构造函数? U u2 = x; //错误:哪个拷贝构造函数? u.m1 = 1; //给int成员赋值 string s = u.m3; //程序灾难:从string成员中读取内容 return; //错误:x、u和u2使用的是哪个析构函数? }
一般来说,先把值写入某个成员然后读取另一个成员的值通常是非法的,但是人们常常忽略这一点(从而产生了错误)。在此例中,程序以无效实参调用了string的拷贝构造函数。程序员应该庆幸这段代码无法通过编译,否则后果不堪设想。如果确实需要,用户可以定义一个包含union的类,该类可以正确地处理union中含有构造函数、析构函数和赋值操作(见8.3.2节)的成员。同时,该类还能防止先把值写入某个成员然后读取另一个成员的错误。
C++允许为联合的最多一个成员指定类内初始化器。此时,该初始化器被用于默认初始化。例如:
union U2{ int a; const char* p{“”}; }; U2 x1; //执行默认初始化,使得x1.p == “” U2 x2{7}; //x2.a == 7
8.3.2 匿名union
下面的程序建立了Entry(见8.3节)的一个变形,从中可以看出如何编写一个类来解决误用union带来的问题:
class Entry2{ private: enum class Tag{number, text}; Tag type; //判别式 union{ //表示形式 int i; string s; //string有默认构造函数、拷贝操作及析构函数 }; public: struct Bad_entry{ }; //用于处理异常 string name; ~Entry2{}; Entry2& operator=(const Entry2&); //因为存在string变量,所以是必需的 Entry2(const Entry2&); //... int number() const; string text() const; void set_number(int n); void set_text(const string&); //... };
我不是get/set函数的拥趸,但是在这个例子中,我们确实需要为每种访问操作自定义非平凡的形式。我用Tag的取值为“get”函数命名,并且在“set”函数前加上set_前缀。这是一种我自己比较习惯的命名方式。
执行读操作的函数的定义如下所示:
int Entry2::number() const{ if(type!=Tag::number)throw Bad_entry{}; return i; }; string Entry2::text() const { if(type!=Tag::text) throw Bad_entry(); return s; };
这两个访问函数首先检查type标签,如果是我们想执行的访问,则返回对应值的引用;否则,抛出异常。这样的union称为标签联合(tagged union)或者可判别联合(discriminated union)。
执行写操作的函数检查type标签的方式与执行读操作的函数基本相同,但是在设置新值的时候必须考虑之前的值的情况:
void Entry2::set_number(int n) { if(type == Tag::text){ s.~string(); //显式地销毁string(见11.2.4节) type = Tag::number; } i = n; } void Entry2::set_text(const string& ss) { if(type == Tag::text) s = ss; else{ new(&s) string{ss}; //new的作用是显式地构造string(见11.2.4节) type = Tag::text; } }
union的用法使得我们必须用其他一些晦涩的、底层的语言特性(显式构造函数和析构函数)来管理union的元素的生命周期。这是应该避免使用union的另一个原因。
在Entry2中声明的union没有命名,它是一个匿名联合(anonymous union)。匿名联合是一个对象而非一种类型,我们无须对象名就能直接访问它的成员。因此,我们使用匿名联合的成员的方式与使用类成员的方式完全一样,只要谨记同一时刻只能使用union的一个成员就可以了。
Entry2含有一个string类型的成员,而在string类型中有用户自定义的赋值运算符,因此Entry2的赋值运算符被delete掉了(见3.3.4节和17.6.4节)。要想为Entry2的对象赋值,就必须先定义Entry2::operator=()。赋值运算兼具读/写两种操作的复杂性,但是它在逻辑上与访问函数很相似:
Entry2& Entry2::operator=(const Entry2& e) //因为存在string变量,所以是必需的 { if(type == Tag::text && e.type == Tag::text){ s = e.s; //常规的string赋值 return *this; } if(type == Tag::text) s.~string(); //显式地销毁(见11.2.4节) switch(e.type){ case Tag::number: i = e.i; break; case Tag::text: new(&s)(e.s); //new的作用是显式地构造string(见11.2.4节) type = e.type; } return *this; }
构造函数与移动赋值操作的定义方式可以很类似。我们至少需要一到两个构造函数来建立type标签和值之间的对应关系。析构函数必须能处理string类型:
Entry2::~Entry2() { if(type == Tag::text) s.~string(); //显式地销毁(见11.2.4节) }
这篇关于C++ 第八章 结构、联合与枚举 - 8.3 联合的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-08CCPM如何缩短项目周期并降低风险?
- 2025-01-08Omnivore 替代品 Readeck 安装与使用教程
- 2025-01-07Cursor 收费太贵?3分钟教你接入超低价 DeepSeek-V3,代码质量逼近 Claude 3.5
- 2025-01-06PingCAP 连续两年入选 Gartner 云数据库管理系统魔力象限“荣誉提及”
- 2025-01-05Easysearch 可搜索快照功能,看这篇就够了
- 2025-01-04BOT+EPC模式在基础设施项目中的应用与优势
- 2025-01-03用LangChain构建会检索和搜索的智能聊天机器人指南
- 2025-01-03图像文字理解,OCR、大模型还是多模态模型?PalliGema2在QLoRA技术上的微调与应用
- 2025-01-03混合搜索:用LanceDB实现语义和关键词结合的搜索技术(应用于实际项目)
- 2025-01-03停止思考数据管道,开始构建数据平台:介绍Analytics Engineering Framework