目录
1. 枚举类型
1.1 引入
枚举类型允许你定义自己的序列,这样你就可以使用这个序列中的值声明变量。例如,在一个国际象棋程序中,可以用int代表所有棋子,用常量代表棋子的类型,代码如下所示。代表棋子类型的整数用const标记,表明这个值永远不会改变。
const int PieceTypeKing { 0 };
const int PieceTypeQueen { 1 };
const int PieceTypeRook { 2 };
const int PieceTypePawn { 3 };
// etc.
int myPiece { PieceTypeKing };
这种表示法存在一定风险。因为棋子只是一个int,如果另一个程序增加棋子的值,那么会发生什么?加1就可让王变成王后,而这实际上没有意义。更糟糕的是,有人可能将某个棋子的值设置为-1,而这个值并没有对应的常量。
1.2 强类型枚举
强类型的枚举类型通过定义变量的取值范围解决了上述的问题。下面的代码声明了一个新类型PieceType,这个类型有4个可能的值,分别代表4种国际象棋棋子。
enum class PieceType { King, Queen, Rook, Pawn };
这种新的类型可以像下面这样使用。
PieceType piece { PieceType::King };
事实上,枚举类型只是一个整型值。King、Queen、Rook、Pawn的实际值分别是0、1、2、3。还可为枚举成员指定整型值,其语法如下:
enum class PieceType {
King = 1,
Queen,
Rook = 10,
Pawn
};
如果你没有给当前的枚举成员赋值,编译器会将上一个枚举成员的值递增1,再赋予当前的枚举成员。如果没有给第一个枚举成员赋值,编译器就给它赋予0。所以,在此例中,PieceType::King具有整型值1,编译器为PieceType::Queen赋予整型值2,PieceType::Rook的值为10,编译器自动为PieceType::Pawn赋予值11。
尽管枚举值内部是由整型值表示的,它却不会自动转换为整数。因此,下面的代码是不合法的:
if (PieceType::Queen == 2) {
...
}
默认情况下,枚举值的基本类型是整型,但可采用以下方式加以改变:
enum class PieceType : unsigned long {
King = 1,
Queen,
Rook = 10,
Pawn
};
对于enum class,枚举值名不会自动超出封闭的作用域,这意味着它们不会与定义在父作用域的其他名字冲突。所以,不同的强类型枚举可以拥有同名的枚举值。例如,以下两个枚举是完全合法的:
enum class State {
Unknown,
Started,
Finished
};
enum class Error {
None,
BadInput,
DiskFull,
Unknown
}
这一特性的好处就是可以给枚举值取较短的名字。然而,这同时意味着必须使用枚举值的全名,或者使用using enum或using声明,像下文描述的那样。
从C++20开始,可以使用using enum声明来避免使用枚举值的全名,示例如下:
using enum PieceType;
PieceType piece { King };
另外,可以使用using声明避免使用某个特定枚举值的全名。例如,在下面的代码中,King可以不用全名就被使用,但是其他枚举值仍需要使用全名。
using PieceType::King;
PieceType piece { King };
piece = PieceType::Queen;
警告:即使C++20允许不使用枚举值的全名,仍然建议审慎地使用这个特性。至少要使using或using enum声明的作用域尽量小。如果作用域很大的话,有可能重新引入名称冲突。
1.3 旧式风格枚举类型(不建议)
新的代码总是应该使用上一节提到的强类型枚举。然而,在遗留代码库中,你可能会遇到旧式风格的枚举:enum而不是enum class。这是一个定义为旧式枚举的PieceType:
enum PieceType {
PieceTypeKing,
PieceTypeQueen,
PieceTypeRook,
PieceTypePawn
};
这种枚举类型的值会被导出到外层的作用域,这意味着可以在父作用域不通过全名而直接使用它们。例如:
PieceType myPiece { PieceTypeQueen };
当然,这同时意味着它们可能会与父作用域中的名称产生冲突,导致编译错误。例如:
bool ok { false };
enum Status { error, ok };
这段代码不会成功编译,因为名字ok首先被定义为一个布尔类型的变量,之后同一个名称又作为枚举值的名称被使用。Visual C++会给出如下的错误提示:“
因此,必须确保旧式风格的枚举使用独一无二的枚举值名称。此外,这些旧式风格的枚举不是强类型的,意味着它们不是类型安全的并且它们总是会被解释为整型。因此,你可能会无意中比较来自完全不同枚举类型的枚举值,或者将错误枚举类型的枚举值传递给函数。
2. 结构体
结构体允许将一个或多个已有类型封装到一个新类型中。数据库记录是结构体的经典示例,如果想要建立一个人事系统来跟踪雇员的信息,那么需要存储名首字母、姓首字母、雇员编号及每个雇员的薪水。下面代码给出了employee.ixx(.ixx是MSVC对C++20模块接口文件的命名 )模块接口文件中的一个结构体,这个结构体包含所有这些信息。模块接口文件中的第一行是模块声明,并声明该文件正在定义一个名为employee的模块。此外,模块需要明确说明其导出的内容,即,在导入该模块的其他位置时可见的内容。从模块导出类型是通过在struct前面使用export关键字完成的。
export module employee;
export struct Employee {
char firstInitial;
char lastInitial;
int employeeNumber;
int salary;
};
声明为Employee类型的变量将拥有全部内建的字段。可使用.运算符访问结构体的各个字段。下面的示例创建了一条员工记录,然后将其输出。请注意,导入自定义模块时,不得使用尖括号。
import std.core;
import employee;
int main() {
// create and populate an employee.
Employee anEmployee;
anEmployee.firstInitial = 'J';
anEmployee.lastInitial = 'D';
anEmployee.employeeNumber = 42;
anEmployee.salary = 80000;
// output the values of an employee.
std::cout << std::format("Employee: {}{}\n", anEmployee.firstInitial, anEmployee.lastInitial);
std::cout << std::format("Number: {}\n", anEmployee.employeeNumber);
std::cout << std::format("Salary: {}\n", anEmployee.salary);
}
3. 条件语句
条件语句允许根据某件事情的真假来执行代码。C++中有两种主要的条件语句:if/else语句和switch语句。
3.1 if/else语句
最常见的条件语句是if语句,其中可能伴随着else语句。如果if语句中给定的条件为true,就执行对应的代码行或代码块,否则执行else语句(如果存在else语句)或者执行条件语句之后的代码。下面的代码显示了一种级联的if语句,这是一种奇特方式:if语句伴随着else语句,而else语句又伴随着另一个if语句。
if (i > 4) {
// do something.
} else if ( i > 2 ) {
// do something else.
} else {
// do something else.
}
if语句的圆括号中的表达式必须是一个布尔值,或者求值的结果必须是布尔值。零值是false,非零值算作true。
3.2 if语句的初始化器
C++允许在if语句中包括一个初始化器,语法如下:
if ( <initializer>; <conditional_expression> ) {
<if_body>
} else if ( <else_if_expression> ) {
<else_if_body>
} else {
<else_body>
}
<initializer>中引入的任何变量只在<conditional_expression>、<if_body>、<else_if_expression>、<else_if_body>、<else_body>中可用。此类变量在if语句以外不可用。
3.3 switch语句
switch是另一种根据表达式值执行操作的语法。在C++中,switch语句的表达式必须是整型、能转换成整型的类型、枚举类型或强类型枚举,必须与一个常量进行比较,每个常量值代表一种情况,如果表达式与这种情况匹配,随后的代码将会执行,直到遇到break语句为止。此外还可提供default情况,如果没有其他情况与表达式值匹配,表达式值将与default情况匹配。
一旦找到与switch条件匹配的case表达式,就执行其后的所有语句,直至遇到break语句为止。即使遇到另一个case表达式,执行也会继续,这称为fallthrough。在下面的示例中,对Mode::Standard和Mode::Default都执行了一组语句。如果mode为Custom,则首先将value从42更改为84,然后执行与Default和Standard相同的语句。换句话说,Custom情况跌落进了Standard/Default案例。此代码还展示了一个很好的示例,该示例使用适当范围的enum声明来避免为不同的case标签编写Mode::Custom、Mode::Standard和Mode::Default。
enum class Mode { Default, Custom, Standard };
int value { 42 };
Mode mode { /*..*/ };
switch ( mode ) {
using enum Mode;
case Custom:
value = 84;
case Standard:
case Default:
// do something with value...
break;
}
如果你无意间忘掉了break语句,fallthrough将成为bug的来源。因此,如果在switch语句中检测到fallthrough,编译器将生成警告信息,除非case为空。在上例中,编译器不会发出Standard情况跌落到Default情况的警告,但是可能会对Custom情况的fallthrough生成警告信息。为了阻止编译器发出警告,可以使用[[fallthrough]]特性,告诉编译器某个fallthrough是有意为之,如下所示。
switch ( mode ) {
using enum Mode;
case Custom:
value = 84;
[[fallthrough]];
case Standard:
case Default:
// do something with value ...
break;
}
3.4 switch语句的初始化器
与if语句一样,可以在switch语句中使用初始化器。语法如下:
switch ( <initializer>; <expression> ) {
<body>
}
<initializer>中引入的任何变量将只在<expression>和<body>中可用。它们在switch语句外不可用。
4. 条件运算符
C++有一个接收3个参数的运算符,称为三元运算符。可将其作为“如果[某事发生了],那么[执行某个操作],否则[执行其他操作]”的条件表达式的简写。这个条件运算符由一个?和一个:组成。下面的代码中,如果变量i的值大于2,将输出yes,否则输出no。
std::cout << (( i > 2 ) ? "yes" : "no" );
i>2两边的括号是可选的,因此与下面的代码行是等效的。
std::cout << ( i > 2 ? "yes" : "no" );
条件运算符的优点是它是一个表达式而不是像if或者switch语句那样的语句。因此,条件运算符几乎可在任何环境中使用。在上例中,这个条件运算符在输出语句中执行。记住这个语法的简便方法是将问号前的语句真的当作一个问题。例如,“i大于2吗?如果是真的,结果就是yes,否则结果就是no。”
参考
[比] 马克·格雷戈勒著 程序喵大人 惠惠 墨梵 译 C++20高级编程(第五版)