c++的派生类类型一般都是指可以自己定义某种类型的方式。 派生类类型有以下几种。

  • 派生类类型
    • 函数类型
    • 自定义类型(类类型)(struct/class)
    • 枚举类型(enum)
    • 共用体类型(union)

之前我们讲述过函数和类类型的用法,接下来我们介绍的是枚举类型和共用体类型的使用方法。

11.1 枚举类型

枚举类型(enumeration)使我们可以将一组整型常量组织在一起。和类类型一样,每个枚举类型定义的是一种新的类型。每种定义的枚举类型都是属于字面值类型。

C++包含两种枚举:

  • 限定作用域
  • 不限定作用域的

11.11 枚举类型的定义

11.111 枚举类型的定义形式

11.1111 限定作用域的枚举类型的定义形式

C++11新标准引入了限定作用域的枚举类型(scoped enumeration) 声明限定作用域的枚举类型的一般语句形式是:

enum class/struct 枚举类型名(可选 :成员类型说明符) (可选 {枚举成员列表});

如果加上后面的枚举成员列表就是定义,不加则只是声明。

// 我们定义了一个名为open_modes的枚举类型
// 它包含三个枚举成员:input、output 和 append。
enum class open_modes {input, output, append}; 
11.1112 不限定作用域的枚举类型的定义形式

定义不限定作用域的枚举类型的一般语句形式和限定作用域类似,只需要省略关键字classstruct

不限定作用域的枚举类型的定义:

enum (可选 枚举类型名) (可选 :成员类型说明符) {枚举成员列表};

不限定作用域的枚举类型的声明:

enum 枚举类型名 : 成员类型说明符;

如果enum是未命名的,则我们只能在定义该enum时定义它的对象。和类的定义类似,我们需要在enum定义的右侧花括号和最后的分号之间提供逗号分隔的声明列表。

枚举类型不是对象,所以枚举类型的定义中不能加各种类型修饰符。

不能只声明未命名的不限定作用域的枚举类型而不定义。

同一个作用域内,所有不限定作用域的枚举类型中不能有同名的枚举成员,每个枚举成员的成员名必须唯一,否则会导致重复定义的错误

C++11新标准中,我们可以只声明enum类型而不定义,但是声明enum必须指定其成员的类型,枚举类型的成员类型必须为非字符和布尔的整型变量。

// 不限定作用域的枚举类型的前置声明
enum intValues; // 错误: 不限定作用域的枚举类型,必须要指定其成员类型。
enum intValues2: unsigned long long; // 正确: 已指定成员类型的不限定作用域枚举类型。
enum class open_modes; // 正确: 限定作用域的枚举类型可以使用默认成员类型int。

和其他声明语句一样,enum的声明和定义必须严格匹配,因此所有的声明和定义必须对该enum是限定作用域的还是不限定作用域的保持一致,不能声明两个同名不同限定作用域的枚举类型

// 错误: 所有的声明和定义必须对该enum是限定作用域的还是不限定作用域的保持一致。
enum class intValues;
enum intValues; // 错误: intValues已经被声明成限定作用域的enum。
enum intValues: long; // 错误: intValues已经被声明成int。

11.112 枚举成员

枚举类型中的枚举成员都是constexpr的非字符和布尔的整型变量,且每个枚举成员的类型都一样。 默认情况下,限定作用域的枚举成员类型为int。而不限定作用域的枚举成员没有默认类型,只知道成员的类型足够大,肯定能够容纳枚举值。

不限定作用域的枚举成员的类型为其中枚举成员尺寸最大的类型。其中的枚举成员没有显式赋值时默认为int型。

我们可以在声明枚举类型的语句中显式指定枚举类型中的成员类型(必须为非字符和布尔的整型变量),语句形式为:

  • enum class/struct 枚举类型名: 成员类型说明符 (可选:{枚举成员列表});

  • enum (可选:枚举类型名): 成员类型说明符 {枚举成员列表};

枚举类型中的每个枚举成员都代表着一个整型值,也叫做枚举值。 一个枚举类型中的枚举值不一定唯一,也就是类型中不同枚举成员可以是同一个整型值

默认情况下,一个枚举类型中的每个枚举成员的枚举值从0开始,从左到右,枚举值依次加1

我们也能在定义枚举类型的语句中为一个或几个枚举成员显式指定其枚举值,显式指定的==枚举值必须是常量表达式==,且其类型必须要为该枚举类型中的成员类型或者能隐式转换成成员类型。如果我们提供的值超出了该成员类型所能容纳的范围,将引发程序错误。

如果我们只提供了某些枚举成员的枚举值,则没有显式指定值的枚举成员的值为上一个枚举成员的值加1。

enum class intTypes {
charTyp = 8, shortTyp = 16, intTyp = 16, 
longTyp = 32, long_longTyp = 64
};

enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535, 
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL};

如果枚举对象没有显式初始化,则该对象的枚举值是未定义的,所以最好在定义时显式进行初始化。

enum class EnumType {
    mem1 = 2, mem2 = -48, mem3 = 4
};

enum class EnumType2 {
    mem21 = 0, mem22 = -1, mem23 = 0
};

EnumType obj;
EnumType2 obj2;

int main()
{   
    // obj值未定义
    std::cout << (obj == EnumType::mem1);
    // obj2值未定义
    std::cout << (obj2 == EnumType2::mem21);
}

11.12 枚举类型的使用

在限定作用域的枚举类型中,枚举成员的名字遵循常规的作用域准则,所以在该枚举类外是不可直接访问的。要想访问限定作用域的枚举类成员,要用该类名加作用域运算符来访问

枚举类类名::成员名

而不限定作用域的枚举类型中的枚举成员在类本身的作用域中可以直接访问,不需要用作用域运算符来访问(当然也可以用)

enum color { red, yellow, green}; // 不限定作用域的枚举类型
enum stoplight {red, yellow, green}; // 错误:重复定义了枚举成员
enum class peppers {red, yellow, green}; // 正确:枚举成员被隐藏了
color eyes = green; //正确:不限定作用域的枚举类型的枚举成员位于有效的作用域中 
//错误:peppers的枚举成员不在有效的作用域中
// color::green在有效的作用域中,但是类型错误
peppers p = green; 
// 正确:允许显式地访问枚举成员
color hair = color::red;
// 正确:使用pappers的red
peppers p2 = peppers::red;

对于有名字的枚举类型,我们可以

  • 将一个enum类型作为switch语句的表达式,而将枚举值作为case标签。
  • 定义并初始化该类型的对象,还能为该对象赋值,但必须使用该类型的枚举成员来为其初始化或赋值(对于没有显式初始化的枚举对象,执行默认初始化)。
  • 对于不限定作用域的枚举类型(限定作用域的枚举类型不行)来说,其对象或枚举成员可以用在所有需要整型常量表达式的地方,其对象或枚举成员将会自动地转换为整型常量表达式。

    注意:整型常量表达式或整型变量都不能隐式转换为枚举类型的对象(无论是限定作用域还是不限定的),只能显式转换。

enum color { red, yellow, green}; // 不限定作用域的枚举类型
enum class peppers {red, yellow, green}; // 限定作用域的枚举类型

int i = color::red; // 正确:不限定作用域的枚举类型的枚举成员隐式地转换成int 
int j = peppers::red; // 错误:限定作用域的枚举类型不会进行隐式转换

color om = 2;  // 错误:2不属于类型color,不能隐式转换为类型color
om = color::green; // 正确:green是color的一个枚举成员
om = static_cast<color>(1); // 正确:整型表达式1被转换为枚举成员yellow。

11.2 共用体类型

共用体类型也叫做联合(union)类型,它是一种特殊的类型。 共用体类型和类类型很相似,可以含有除了引用类型的数据、任意函数(包括构造函数和析构函数)和任意类类型成员(类类型成员包括共用体类型),且成员也有静态和非静态之分。 共用体类型还可以为其成员指定publicprotectedprivate等保护标记。默认情况下,共用体类型的成员都是公有的,这一点与struct相同。 共用体类型既不能继承自其他类,也不能作为基类使用,所以也不能含有虚函数。

共用体类型与类类型最大的区别就是对于共用体类型的所有非静态数据成员来说,任意时刻只有一个成员可以有值。

11.21 共用体类型的定义和声明

共用体类型的定义和声明和类类型类似:

11.211 共用体类型的定义

共用体类型的定义和类类型的差不多,遵循类类型的规则:

union (可选 类名) 类体 (可选 类对象列表);

共用体类型和类类型不一样的是,共用体可以在定义时使用存储说明符声明该共用体类型。

共用体类型的类体和类类型的类体一样,遵循类体的规则。 类体里的成员也可以在类外定义;非静态数据成员可以有类内初始值(因为同一时刻只有一个非静态数据成员可以有值,所以类体中只能存在最多一个非静态数据成员的类内初始值)。

对于定义共用体类型的构造函数来说,每个构造函数的初始值列表最多只能有一个成员。 当非静态数据成员的类内初始值与构造函数同时存在时,只有构造函数中被初始化的成员才有值,类内初始值会被忽略。

// 共用体类型Un
union Un
{
    int ins;
    double dou = 6.15;
    static string sta_str;
    void prints() { cout << sta_str; }
};
string Un::sta_str = "sta_str";

当一个共用体类型没有类名和类对象列表,且类体中只含有非静态数据成员,这些成员都是公有成员时,该共用体类型就是一个特殊的共用体类型,叫做匿名union(anonymous union)。 一旦我们定义了一个匿名union,编译器就自动地为该union创建一个未命名的对象,在其作用域内,该union成员都是可以直接访问的。

对于全局作用域或者命名空间中定义的匿名union,必须要将其声明为static才行。

static union
{
    int ins;
    double dou = 6.15;
    bool bo;
};
// 输出6.15
cout << dou;

对于共用体类型的合成默认构造函数和合成的拷贝控制成员的合成条件,是这样的: 当union包含的是内置类型的非静态数据成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。 但是如果union含有类类型的非静态数据成员,并且该类型显式定义了默认构造函数或拷贝控制成员,或者该类型的默认构造函数或拷贝控制成员为已删除的,则编译器将为union合成对应的版本并将其声明为删除的(显式的合成也是这样的)。

11.212 共用体类型的声明

共用体类型的声明和类类型的声明一样:

union 类名;

共用体类型的声明中不能含有存储说明符。

union Un;

只声明而未定义的共用体类型也是不完全类型,遵循不完全类型的规则。

11.22 共用体类型的使用

共用体类型的使用和类类型一样,可以调用或者创建类对象。

对于共用体类型对象的初始化,我们既可以直接初始化,也可以拷贝初始化,只要与可用的构造函数匹配就行。

对于一个满足如下条件的共用体类型,该类型还可以使用聚合类的初始化方式:

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 所有非静态数据成员都没有类内初始值。

但是因为任意时刻只有一个非静态数据成员可以有值,所以聚合类的初始化方式只能初始化一个成员,且因为是按顺序初始化的,所以只能初始化第一个成员。

union Un
{
    int ins;
    bool bo;
    double dou;
};
// 错误:只能初始化一个成员
Un ob{6, true, 5.48};
// 错误:第一个成员为int,不是double
Un ob{5.48};
// 正确:初始化ins成员
Un ob{6};

我们也可以通过成员访问符来对共用体类对象的成员进行操作。 不过为共用体类对象的一个非静态数据成员赋值会令其他的非静态数据成员变成未定义的状态。

union Un
{
    int ins;
    bool bo;
    double dou;
};
// 正确:初始化ins成员
Un ob{6};
// 正确:输出6
cout << ob.ins;
// 正确:赋值dou成员,其他非静态数据成员变成未定义
ob.dou = 6.78;
// 错误:ins成员未定义
cout << ob.ins;

11.23 管理union成员

对于共用体来说,要想构造或销毁类类型的成员必须执行非常复杂的操作,因此我们通常把含有类类型成员的共用体内嵌在另一个类当中。这个类可以管理并控制与共用体的类类型成员有关的状态转换。

举个例子,我们为共用体添加一个string成员,并将我们的共用体定义成匿名union,最后将它作为Token类的一个成员。此时,Token类将可以管理该类的成员。

为了追踪共用体中到底存储了什么类型的值,我们通常会定义一个独立的对象,该对象称为union的判别式(discriminant)。我们可以使用判别式辨认共用体存储的值。 为了保持共用体与其判别式同步,我们通常将判别式也作为该共用体的成员。我们的共用体类通常还将定义一个枚举类型的成员来追踪其共用体成员的状态。