本文为《C++ Primer》的读书笔记
目录
枚举类型
限定 / 不限定 作用域的枚举
C++ 包含两种枚举:
- 限定作用域的 (scoped enumeration) (由 C++11 新标准引入)
- 定义限定作用域的枚举类型的一般形式是:首先是关键字
enum class
(或者等价地使用enum struct
), 随后是枚举类型名字以及用花括号括起来的以逗号分隔的枚举成员列表:
- 定义限定作用域的枚举类型的一般形式是:首先是关键字
// 枚举类型 open_modes 包含三个枚举成员: input、output 和 append
enum class open_modes {input, output, append};
- 不限定作用域的 (unscoped enumeration)
- 定义不限定作用域的枚举类型时省略掉关键字
class
(或struct
), 枚举类型的名字是可选的 - 如果
enum
是未命名的, 则我们只能在定义该enum
时定义它的对象
- 定义不限定作用域的枚举类型时省略掉关键字
enum color {red, yellow, green}; // 不限定作用域的枚举类型
// 未命名的、不限定作用域的枚举类型
enum {floatPrec = 6, doublePrec = 10, double_doublePrec = 10};
枚举成员
- 在限定作用域的枚举类型中, 枚举成员的名字遵循常规的作用域准则,并且在枚举类型的作用域外是不可访问的
- 在不限定作用域的枚举类型中, 枚举成员的作用域与枚举类型本身的作用域相同
enum color { red, yellow, green }; // 不限定作用域的枚举类型
enum stoplight { red, yellow, green }; // 错误:重复定义了枚举成员
enum class peppers { red, yellow, green }; //正确:枚举成员被隐藏了
color eyes = green; // 正确: 不限定作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green; // 错误: peppers 的枚举成员不在有效的作用域中
// color::green 在有效的作用域中, 但是类型错误
color hair = color::red; // 正确: 允许显式地访问枚举成员
peppers p2 = peppers::red; // 正确: 使用 peppers 的 red
- 默认情况下, 枚举值从0开始, 依次加1。如果我们没有显式地提供初始值, 则当前枚举成员的值等于之前枚举成员的值加 1
- 不过我们也能为一个或几个枚举成员指定专门的值,且 枚举值不一定唯一
enum class intTypes {
charTyp = 8, shortTyp = 16, intTyp = 16,
longTyp = 32, long_longTyp = 64
};
- 枚举成员是
const
, 因此在初始化枚举成员时提供的初始值必须是常量表达式。因此,我们可以在任何需要常量表达式的地方使用枚举成员- 例如,我们可以定义枚举类型的
constexpr
变量:
- 例如,我们可以定义枚举类型的
constexpr intTypes charbits = intTypes::charTyp;
和类一样,枚举也定义新的类型
- 只要
enum
有名字, 我们就能定义并初始化该类型的成员 - 要想初始化
enum
对象或者为enum
对象赋值, 必须使用该类型的一个枚举成员或者该类型的另一个对象:
open_modes om = 2; // 错误: 2 不属于类型 open_modes
om = open_modes::input; // 正确: input 是 open_modes 的一个枚举成员
- 一个不限定作用域的枚举类型的对象或枚举成员自动地转换成整型:
int i = color::red; // 正确:不限定作用域的枚举类型的枚举成员隐式地转换成int
int j = peppers::red; // 错误: 限定作用域的枚举类型不会进行隐式转换
指定 enum
的大小
- 尽管每个
enum
都定义了唯一的类型, 但实际上enum
是由某种整数类型表示的 - 在 C++11 新标准中, 我们可以在
enum
的名字后加上冒号以及我们想在该enum
中使用的类型;如果我们指定了枚举成员的潜在类型, 则一旦某个枚举成员的值超出了该类型所能容纳的范围, 将引发程序错误
enum intValues : unsigned long long {
charTyp = 255, shortTyp = 65535, intTyp = 65535,
longTyp = 4294967295UL,
long_longTyp = 18446744073709551615ULL
};
- 如果我们没有指定
enum
的潜在类型,则默认情况下限定作用域的enum
成员类型是int
- 对于不限定作用域的枚举类型来说, 其枚举成员不存在默认类型, 我们只知道成员的潜在类型足够大, 肯定能够容纳枚举值
- 指定
enum
潜在类型的能力使得我们可以确保在一种实现环境中编译通过的程序所生成的代码与其他实现环境中生成的代码一致
枚举类型的前置声明
在 C++11 新标准中, 我们可以提前声明enum
- 不限定作用域的枚举类型的前置声明必须指定其成员的类型
- 限定作用域的枚举类型的前置声明可以不指定,默认为
int
// 不限定作用域的枚举类型intValues的前置声明
enum intValues : unsigned long long; // 不限定作用域的, 必须指定成员类型
enum class open_modes; // 限定作用域的枚举类型可以使用默认成员类型int
- 我们不能在同一个上下文中先声明一个不限定作用域的
enum
名字, 然后再声明一个同名的限定作用域的enum
:
enum class intValues;
enum intValues; // 错误: intValues已经被声明成限定作用域的enum
enum intValues : long; // 错误:intValues已经被声明成int
形参匹配与枚举类型
- 要想初始化一个
enum
对象, 必须使用该enum
类型的另一个对象或者它的一个枚举成员。因此, 即使某个整型值恰好与枚举成员的值相等, 它也不能作为函数的enum
实参使用:
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main() {
Tokens curTok = INLINE;
ff(128); // 精确匹配ff(int)
ff(INLINE); // 精确匹配ff(Tokens)
ff(curTok); // 精确匹配ff(Tokens)
return 0;
}
- 尽管我们不能直接将整型值传给
enum
形参, 但是可以将一个不限定作用域的枚举类型的对象或枚举成员传给整型形参。此时,enum
的值提升成int
或更大的整型, 实际提升的结果由枚举类型的潜在类型决定:
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); // 调用newf(int)
- 枚举类型
Tokens
只有两个枚举成员, 其中较大的那个值是129。该枚举类型可以用unsigned char
来表示, 因此很多编译器使用unsigned char
作为Tokens
的潜在类型。不管Tokens
的潜在类型到底是什么, 它的对象和枚举成员都提升成int
。尤其是,枚举成员永远不会提升成unsigned char
, 即使枚举值可以用unsigned char
存储也是如此
union: 一种节省空间的类
- 联合 (union) 是一种特殊的类。一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当我们给 union 的某个成员赋值之后,该 union 的其他成员就变成未定义的状态了。分配给一个 union 对象的存储空间至少要能容纳它的最大的数据成员。和其他类一样, 一个 union 定义了一种新类型
- 类的某些特性对 union 同样适用, 但并非所有特性都如此。union 不能含有引用类型的成员,除此之外,它的成员可以是绝大多数类型。在 C++11 标准中, 含有构造函数或析构函数的类类型也可以作为 union 的成员类型。union 可以为其成员指定
public
、protected
和private
等保护标记。默认情况下, union 的成员都是公有的 - union 可以定义包括构造函数和析构函数在内的成员函数。但是由于 union 既不能继承自其他类, 也不能作为基类使用, 所以在 union 中不能含有虚函数
定义 union
- 定义 union 时,它的名字可选
// Token类型的对象只有一个成员, 该成员的类型可能是下列类型中的任意一种
union Token {
// 默认情况下成员是公有的
char cval;
int ival;
double dval;
};
使用 union 类型
- union 的名字是一个类型名。默认情况下 union 是未初始化的。我们可以像显式地初始化聚合类 一样使用一对花括号内的初始值显式地初始化一个 union,如果提供了初始值,则该初始值被用于初始化第一个成员
Token first_token = {'a'}; // 初始化 cval 成员
Token last_token; // 未初始化的Token对象
Token *pt= new Token; // 指向一个未初始化的 Token 对象的指针
- 为 union 的一个数据成员赋值会令其他数据成员变成未定义的状态。因此,当我们使用 union 时,必须清楚地知道当前存储在 union 中的值到底是什么类型。如果我们使用错误的数据成员或者为错误的数据成员赋值,则程序可能崩溃或出现异常行为
last_token.cval = 'z';
pt->ival = 42;
匿名 union
- 匿名 union 是一个未命名的 union, 并且在右花括号和分号之间没有任何声明。一旦我们定义了一个匿名 union, 编译器就自动地为该 union 创建一个未命名的对象
- 在匿名 union 的定义所在的作用域内该 union 的成员都是可以直接访问的
- 匿名 union 不能包含受保护的成员或私有成员,也不能定义成员函数
union { // 匿名 union
char cval;
int ival;
double dval;
}; // 定义一个未命名的对象,我们可以直接访问它的成员
cval = 'c'; // 为刚刚定义的未命名的匿名union对象赋一个新值
ival = 42; // 该对象当前保存的值是42
含有类类型成员的 union
- 当 union 包含的是内置类型的成员时,编译器将按照成员的次序依次合成默认构造函数或拷贝控制成员。我们可以使用普通的赋值语句改变 union 保存的值
- 如果 union 含有类类型的成员, 并且该类型自定义了默认构造函数或拷贝控制成员,但 union 没有自定义默认构造函数或拷贝控制成员,则编译器将为 union 合成对应的版本并将其声明为删除的。如果我们想将 union 的值改为类类型成员对应的值, 必须运行该类型的构造函数; 反之, 当我们将类类型成员的值改为一个其他值,则必须运行该类型的析构函数
- 例如,
string
类定义了五个拷贝控制成员以及一个默认构造函数。如果 union 含有string
类型的成员,并且没有自定义默认构造函数或某个拷贝控制成员, 则编译器将合成缺少的成员并将其声明成删除的
- 例如,
使用类管理 union 成员
- 对于 union 来说, 要想构造或销毁类类型的成员必须执行非常复杂的操作, 因此我们通常把含有类类型成员的 union 内嵌在另一个类当中。这个类可以管理并控制与 union 的类类型成员有关的状态转换
- 例如,我们为 union 添加一个
string
成员,并将我们的union
定义成匿名 union, 最后将它作为Token
类的一个成员。此时,Token
类将可以管理 union 的成员
- 例如,我们为 union 添加一个
- 为了追踪 union 中到底存储了什么类型的值,我们通常会定义一个独立的对象, 该对象称为 union 的判别式。为了保持 union 与其判别式同步, 我们将判别式也作为
Token
的成员。我们的类将定义一个枚举类型的成员来追踪其 union 成员的状态
class Token {
public:
// 因为 union 含有一个 string 成员, 所以 Token 必须定义拷贝控制成员
// 定义移动构造函数和移动赋值运算符的任务留待本节练习完成
Token(): tok(INT), ival(0) {} // 初始化判别式及 union 成员
Token(const Token &t): tok(t.tok) { copyUnion(t); }
Token &operator=(const Token&);
// 和普通的类类型成员不一样,作为 union 组成部分的类成员无法自动销毁
// 因为析构函数不清楚 union 存储的值是什么类型,所以它无法确定应该销毁哪个成员
// 因为我们的 union 含有一个定义了析构函数的成员,所以必须为 union 也定义一个析构函数以销毁 string 成员
~Token() { if(tok == STR) sval.~string(); }
// 下面的赋值运算符负责设置 union 的不同成员
// 和析构函数一样, 这些运算符在为 union 赋新值前必须首先销毁 string
Token &operator=(const std::string&);
Token &operator=(char);
Token &operator=(int);
Token &operator=(double);
private:
enum {INT, CHAR, DBL, STR} tok; // 判别式
union { // 匿名union
char cval;
int ival;
double dval;
std::string sval;
}; // 每个 Token 对象含有一个该未命名 union 类型的未命名成员
// 检查判别式, 然后酌情拷贝 union 成员
void copyUnion(const Token&);
};
// double 和 char 版本的赋值运算符与 int 版类似,在此省略
Token &Token::operator=(int i)
{
if(tok == STR) sval.~string(); // 如果当前存储的是string, 释放它
ival = i; // 为成员赋值
tok = INT; // 更新判别式
return *this;
}
Token &Token::operator=(const std::string &s)
{
if (tok == STR) // 如果当前存储的是 string, 可以直接赋值
sval = s;
else
new(&sval) string(s); // 否则需要先构造一个 string (使用定位 new)
tok = STR; // 更新判别式
return *this;
}
- 和依赖于类型的赋值运算符一样,拷贝构造函数和赋值运算符也需要先检验判别式以明确拷贝所采用的方式。为了完成这一任务, 我们定义一个名为
copyUnion
的成员- 当我们在拷贝构造函数中调用
copyUnion
时,union 成员将被默认初始化, 这意味着编译器会初始化union
的第一个成员。因为string
不是第一个成员,所以显然 union 成员保存的不是string
;因此只调用copyUnion
即可 - 在赋值运算符中情况有些不一样, 赋值运算符必须处理
string
成员的三种可能情况:左侧运算对象和右侧运算对象都是string
、两个运算对象都不是string
、只有一个运算对象是string
- 当我们在拷贝构造函数中调用
void Token::copyUnion(const Token &t)
{
switch (t.tok) {
case Token::INT:
ival = t.ival;
break;
case Token::CHAR:
cval = t.cval;
break;
case Token::DBL:
dval = t.dval;
break;
// 要想拷贝一个 string 可以使用定位 new 表达式构造它
case Token::STR:
new(&sval) string(t.sval);
break;
}
}
Token &Token::operator= (const Token &t)
{
// 如果此对象的值是 string 而 t 的值不是, 则我们必须释放原来的 string
if(tok == STR && t.tok != STR)
sval.~string();
if(tok == STR && t.tok == STR)
sval = t.sval; // 无须构造一个新 string
else
copyUnion(t); // 如果 t.tok 是 STR, 则需要构造一个 string
tok = t.tok;
return *this;
}