2.3 用户自定义类型
用户自定义类型(User-Defined Type)是用户可以定义的类型。用户自定义类型有三大类:
❑ 枚举类型:最简单的用户自定义类型。枚举类型可以取的值被限制在一组可能的值中。枚举类型是对分类概念进行建模的最佳选择。
❑ 类:功能更全面的类型,它使我们可以灵活地结合数据和函数。只包含数据的类被称为普通数据类(Plain-Old-Data,POD),见2.3.2节。
❑ 联合体:浓缩的用户自定义类型。所有成员共享同一个内存位置。联合体本身很危险,容易被滥用。
2.3.1 枚举类型
使用关键字enum class来声明枚举类型,关键字后面是类型名称和它可以取的值的列表。这些值是任意的字母-数字字符串,代表任意想代表的类别。在实现内部,这些值只是整数,但它们允许使用程序员定义的类型而不是可能代表任何东西的整数来编写更安全、更有表现力的代码。例如,代码清单2-13声明了一个名为Race的枚举类,它可以取七个值中的一个。
代码清单2-13 一个包含尼尔·斯蒂芬森(Neal Stephenson)小说《七夏娃》中所有种族的枚举类
要将枚举变量初始化为一个值,使用类型的名称后跟两个冒号::和所需的值即可实现。例如,下面的代码展示了如何声明变量langobard_race并将其初始化为Aidan:
注意 从技术上讲,枚举类是两种枚举类型中的一种:它被称为作用域枚举。为了与C语言兼容,C++也支持非作用域枚举类型,它是用enum而非enum class声明的。主要的区别是,作用域枚举需要在值前面加上枚举类型和::,而非作用域枚举则不需要。非作用域枚举类比作用域枚举类使用起来更不安全,所以除非绝对必要,否则请不要使用它们。C++支持它们主要是出于历史原因,特别是基于与C代码的互操作。详情请参见Scott Meyers的Effective Modern C++的第10项。
1.switch语句
switch语句根据condition(条件)值将控制权转移到几个语句中的一个,condition值可以是整数或枚举类型的。switch关键字表示一个switch语句。
switch语句提供了条件性分支。当switch语句执行时,控制权将转移到符合条件的情况(case语句),如果没有符合条件表达式的情况,则转移到默认情况。每个case关键字都表示一种情况,而default关键字表示默认情况。
有点令人困惑的是,执行过程将持续到switch语句结束或break关键字。几乎总能在每个条件的末尾发现一个break。
switch语句有很多case语句。代码清单2-14显示了它们是如何组合在一起的。
代码清单2-14 switch工作框架
所有的switch语句都以switch关键字❶开始,后面紧跟着用括号括起来的条件(condition)❷。每个case语句都以case关键字❸开头,后面跟着枚举值或整数值❹。例如,如果条件值❷等于case-a❹,那么包含Handle case a here的代码块将被执行。在每条case语句之后❺,都要放置break关键字❻。如果条件值与所有case中的值都不匹配,则执行默认的情况default❼。
注意 每个case的大括号可有可无,但强烈推荐使用。没有它们,有时会得到令人惊讶的行为。
2.对枚举类使用switch语句
代码清单2-15对Race枚举类使用switch语句来生成定制的问候语。
代码清单2-15 一个根据所选种族打印问候语的程序
enum class❶声明了枚举类型Race,我们可以用它将race初始化为Dinan❷。switch语句❸评估条件race,以确定将控制权交给哪个case语句。因为已在前面将race硬编码为Dinan,因此将执行第一条case语句❹,它将打印You work hard.。第一条case语句后的break❺将终止switch语句。
default语句❻是一个安全功能。如果有人在枚举类中添加了新的race值,那么在运行时将检测到这个未知的race,并打印出错误信息。
试着把race❷设置为不同的值,看看输出有什么变化?
2.3.2 普通数据类
类是用户自定义的包含数据和函数的类型,它们是C++的核心和灵魂。最简单的类是普通数据类(Plain-Old-Data,POD)。POD是简单的容器。我们可以把它们看作一种潜在的不同类型的元素的异构数组。类的每个元素都被称为一个成员(member)。
每个POD都以关键词struct开头,后面跟着POD的名称,再后面要列出成员的类型和名称。考虑下面这个有四个成员的Book类声明:
Book包含一个名为name❶的char数组、一个int year❷、一个int pages❸和一个bool hardcover❹。
声明POD变量就像声明其他变量一样:通过类型和名称。我们可以使用点运算符(.)访问变量的成员。
代码清单2-16使用了Book类型。
代码清单2-16 使用POD类Book来读写成员的例子
首先,声明一个Book变量neuromancer❶。然后,使用点运算符(.)将neuro-mancer的页数设置为271❷。最后,打印一条信息,并从neuromancer中提取页数,同样使用点运算符❸。
注意 POD有一些有用的底层特性:它们与C语言兼容,我们可以使用高效的机器指令来复制或移动它们,而且它们可以在内存中有效地表示出来。
C++保证成员在内存中是按顺序排列的,尽管有些实现要求成员沿着字的边界对齐,这取决于CPU寄存器的长度。一般来说,应该在POD定义中从大到小排列成员。
2.3.3 联合体
联合体(union)类似于POD,它把所有的成员放在同一个地方。我们可以把联合体看作对内存块的不同看法或解释。它们在一些底层情况下是很有用的,例如,处理必须在不同架构下保持一致的结构时,处理与C/C++互操作有关的类型检查问题时,甚至在包装位域(bitfield)时。
代码清单2-17说明了如何声明联合体:用union关键字代替struct即可。
代码清单2-17 一个联合体的例子
联合体Variant可以被解释成char[10]、int double。它占用的内存与它最大的成员(在本例中可能是string)占用的内存一样多。
我们可以使用点运算符(.)来指定联合体的解释。从语法上看,这看起来像访问POD的成员,但它在内部是完全不同的。
因为联合体的所有成员都在同一个地方,所以很容易造成数据损坏。代码清单2-18说明了这种危险。
代码清单2-18 使用代码清单2-17中联合体Variant的程序
首先,声明Variant v❶。接着,把v解释为整数,把它的值设置为42❷,并打印它❸。然后,把v重新解释为浮点数,重新赋值❹,把它打印到控制台,一切看起来很好❺。到目前为止还不错。
只有当再次将v解释为整数时,灾难才会降临❻。在赋值为欧拉数❹时,会把v的原值(42)❷给破坏了。
这就是联合体存在的主要问题:要靠程序员自己来跟踪哪种解释是合适的。编译器不会提供帮助。
除了罕见的情况,应该避免使用联合体,本书中就不会使用它们。12.1.6节讨论了当需要多类型变量时应选择的一些更安全的选择。