C++ 复习教程第一章(C++ 和标准库速成)
C++ 高级编程
如果只想学习高级方法,那么就请看第 9 章的零规则,就可以知道,前面的努力很大程度上都只是在为了迁就一些较为落后的代码语法。前面的很重要但不必要!!!
2.7 和 8.1 / 3 的区别,记住比较两个浮点数的大小仅关注极小误差量!!!
第1章 —— C++ 和标准库速成
1.1 C++ 基础知识
1.1 小程序 “hello world”
1 |
|
1.注释
- 单行注释,使用 // 符;
- 多行注释,使用 /* */ 符;
2.预处理指令
生成一个 C++ 程序共有三步:
首先,代码在预处理器中运行,预处理器会识别代码中的 元信息(meta-information)**;
其次,代码被编译或转换为计算机可识别的目标文件;
最后,独立的目标文件被连接在一起变成一个目标文件;
预处理指令以 # 字符开始;
include 指令告诉预处理器:提取
<iostream>
头文件中的所有内容并提供给当前文件;
- 头文件,最常见的用途是定义在其他位置的函数,函数声明会通知编译器如何调用这个函数,并声明函数中参数的个数和类型,以及函数的返回类型。而函数定义包含这个函数的实际代码;
- 在 C++ 中,声明通常放在扩展名为
.h
的文件中,称为头文件;- 在 C++ 中,定义通常包含在扩展名为
.cpp
的文件中,称为源文件;<iostream>
头文件声明了 C++ 如何提供输入输出机制,如果程序没有包含这个头文件,甚至无法执行器仅需要完成的输入输出文本;注意:
在 C 中,标准库头文件以
.h
结尾,如<stdio.h>
,不使用名称空间;在 C++ 中,标准库头文件以
.h
结尾,如<iostream>
,所有文件都在std
名称空间和std
的子名称空间中定义;C 中的标准库头文件在 C++ 中依然存在,但是使用以下两个版本:
不使用
.h
后缀,改用前缀 **c
**;这是新版本,也是推荐使用的版本。这些版本将一切放在
std
名称空间中,如 **<cstdio>
**;使用
.h
后缀;这是旧版本,这些版本不使用名称空间,如 **
<stdio.h>
**;常见预处理指令
#include [filename] // 将指定的文件插入代码指令虽在的位置; // 几乎总是用来包含头文件,是代码可使用在其他位置定义的功能;
1
2
3
4
5
6
- ```c++
#define [key] [value]
// 每个指定的 key 都被替换为指定的 value;
// 在 C 中,常用来定义常数值或宏;
// 在 C++ 中,提供了常数和大多数宏类型的更好机制,此外,宏的使用具有一定风险,故谨慎使用;
#ifdef [key] #endif #ifndef [key] #endif // ifdef("if defined") 块或 ifndef("if not defined") 块中的代码被有条件地包含或者舍弃; // 上述保留或舍弃取决于是否使用 #define 定义了指令的 key;
1
2
3
4
5
- ```C++
pragma [xyz]
// xyz 因编译器而异;
// 如果在预处理期间执行到这一指令通常会显示一条警告或错误信息;下面是使用预处理指令避免重复包含的示例:
#ifndef MYHEADER_H #define MYHEADER_H // ... the contents of this header file #endif
1
2
3
4
- ```c++
#pragma once
// ... the contents of this header file上述两段代码起到的效果相同;
3.main() 函数
main() 函数是程序的入口。
main() 函数返回一个 int 值以指示程序的最终执行状态。
main() 函数中,可忽略显式的
return
语句,此时,会自动返回 0;main() 函数只有两种参数设置:
int main()
> **`argc`** 给出了传递给程序的实参数目,**`argv`** 包含了这些参数; > > 注意: > > **`argv[0]`** 可能是程序的名称,也可能是空字符串,但不应依赖它,相反,应当使用特定于平台的功能来检索程序名。重要是记住,实际参数从索引 1 开始;
1
2
3
2. ```c++
int main(int argc, char* argv[])
4.输入输出流
可以将输出流想象为针对数据的*洗衣滑槽(chute)***;
放入其中的任何内容都可以被正确地输出
std::cout
对应用户控制台或标准输出的滑槽;
<< 用于将信息放入滑槽中;
1
2
3
4
5
6
7
#include <iostream>
int main()
{
std::cout << "There are " << 219 << " ways."
return 0;
}
std::cerr
对应输出错误信息的滑槽;
1
2
3
4
5
6
7
#include <iostream>
int main()
{
std::cerr << "Not right!";
// 用于对错误信息的输出;
return 0;
}
std::endl
用于表示序列的结尾,相当于 \n
此外常见的转义字符有:
\n 换行
\r 回车
\t 制表符
\\ 反斜杠字符
\" 引号
std::cin
用于接收用户的输入,最简单的方法就是在输入流中使用 >> 运算符;
1
2
3
4
5
6
7
8
9
#include <iostream>
int main()
{
int value;
std::cin >> value;
// 由于永远不知道用户会输入什么类型的数据,因此需慎重对待用户的输入;
return 0;
}注意:
在 C 中使用的 printf() 和 scanf() 未提供类型安全,虽然,在 C++ 中仍然使用 printf(),但,仍建议改用流库,更安全;
1.2 名称空间
1.名称空间
名称空间,用于处理不同代码之间的名称冲突问题;
例如,用户自己编辑了一段代码,其中有一个名为 foo() 的函数,但是,有一天,用户决定使用第三方库中,其中也有一个函数名为 foo(),这时编辑器无法自行判断你的代码要使用哪个版本的的 foo() 函数,库名称无法改变,而改变自己代码中函数名称又十分麻烦;
上述情况,可以使用名称空间来解决,使用名称空间来指定定义名称的环境,为某段代码加入名称空间可使用 namespace 块将其包含其中。例如,可在 namespaces.h 声明函数示例:
1
2
3
4
5
6
// namespace.h
namespace mycode
{
void foo();
}
// 可以看到,这里的 mycode 实际上是 namespace 类的一个实例化在名称空间中还可以实现方法或函数,例如,foo() 函数可在 namespaces.cpp 中实现,下给出示例:
1
2
3
4
5
6
7
8
// namespaces.cpp
#include <iostream>
#include "namespaces.h"
void mycode::foo()
{
std::cout << "foo() named in the mycode namespace." << std::endl;
}或者:
1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include "namespaces.h"
namespace mycode
{
void foo()
{
std::cout << "foo() called in the mycode namespace." << std::endl;
}
}这样,就能够将用户书写的 foo() 函数与第三方库的 foo() 函数区分,使用以下调用方式:
1
2
3
4
5
6
7
8
mycode::foo();
// 其中, :: 符称为作用域解析运算符;
// mycode 名称空间中的任何代码都可以调用该名称空间中的其他代码,而不需要显式地说明该名称空间;
// 如果出现名称空间过长的情况,可使用以下方法:
namespace mynamespace = namespace_top::namespace_middle::naespace_bottom;
mynamespaces::foo();
// 即,使用同名来化简
2.using
指令:
可以避免预先指明命名空间,使得代码清晰并且易于阅读;
该指令通知编译器,后面的代码将使用指定名称空间中的名称;
例如:
1
2
3
4
5
6
7
8
9
#include "namespaces.h"
using namespace mycode
int main()
{
foo();
return 0;
}虽然,一个源文件中可以包含多条
using
指令,但是这种方法虽然便捷,但是注意不要过度使用!注意:
极端情况下,如果你使用了已知的所有名称空间,实际上,就是完全取消了名称空间;
如果使用了两个同名的名称空间,将再次出现名称冲突问题;
此外,应当知晓每段代码在哪个名称空间中运行,这样就不会无意中调用错误版本的函数;
注意:切勿在头文件中使用
using
指令或using
声明!
1.头文件中使用可能导致的问题
不允许在头文件中使用
using
指令,否则,可能会出现引入头文件的源文件中的全局命名空间被改变,从而可能引发命名冲突。例如:
1
2
3
4
5
// 在头文件中避免这样的使用
// project_1.h
using namespace std;
// 可能会出现引入头文件的源文件中的全局命名空间被改变,从而可能引发命名冲突使用
using
声明引入头文件的源文件中的特定名称,作用于每个引入文件,同样可能也会导致命名冲突。例如:
1
2
3
4
5
// 在头文件中避免这样的使用
// project_1.h
using std::cout;
// 作用于每个引入文件,同样可能也会导致命名冲突
2.解决方案:
使用权限定名称*
1
2
3
4// 全限定名称示例
namespace_name::entity_name
// 即,在每次声明和定义函数的时候,都指明其命名空间在头文件中使用命名空间别名
1
2
3
4
5
6
7
8
9
10
11// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
namespace mynamespace
{
void myFunction();
// 使用命名空间别名
}
#endif限制使用 using 的范围
1
2
3
4
5
6
7
8
9
10
11
12
13// example.h
#ifndef EXAMPLE_H
#define EXAMPLE_H
namespace mynamespace
{
using std::cout;
// 在特定的命名空间中使用using
void myFunction();
}
#endif
1.3 字面量
1.字面量
字面量用于在代码中编写数字或字符串。C++ 支持大量标准字面量,可以使用以下字面量表示指定数字(列出的示例都表示数字 123):
- 十进制字面量 123
- 八进制字面量 0173
- 十六进制字面量 0x7B
- 二进制字面量 0b1111011
2.其他字面量:
- 浮点值 如:3.14f
- 双精度浮点值 如:3.14
- 单个字符 如:’a’
- 以零结尾的字符数组 如:”character array”
3.自定义自变量类型
自定义自变量类型,这是一种高级功能;
4.数字分隔符
数字分隔符,可以在数值字面量中使用数字分隔符,数字分隔符是一个单引号,例如:
- 23’456’789
- 0.123’456f
5.十六进制浮点字面量
此外,C++17还增加了对十六进制浮点字面量的支持,例如:
- 0x3.ABCp-10
- 0Xb.cp121
1.4 变量
在 C++ 中,可以在任何位置声明变量,并且可以在声明一个变量所在行之后任意位置使用该变量。声明变量时可不指定值,这些未初始化的变量通常会被赋予一个半随机值,这个值取决于当前内存的内容(这是许多 bug 的来源)。在 C++ 中,也可以声明变量时为变量指定初始值。下面给出两种风格的变量声明方式,使用的都是代表整数的 int 类型:
1
2
3
4
int unintializedInt;
int initializedInt = 7;
cout << uninitializedInt << " is a random value" << endl;
cout << initializedInt << " was assigned an initial value" << endl;注意:
当代码使用未初始化的变量时,多数编译器会给出警告或报错信息。当访问未初始化的变量时,某些 C++ 环境可能会报告运行时错误。
1.整型 & size_t:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// (signed) int 正整数或负整数,范围取决于编译器
// 通常占 4 字节
int i = -7;
signed int i = -6;
signed i = -5;
// (signed) short (int) 短整型整数
// 通常占 2 字节
short s = 13;
short int s = 14;
signed short s = 15;
signed short int s = 16;
// (signed) long (int) 长整型整数
// 通常占 4 字节
long l = -7L; // L 可省略
// (signed) long long (int) 超长整型整数,范围取决于编辑器,但不低于长整数
// 通常占 8 字节
long long ll = 14LL; // LL 可省略
// unsigned (int / short int / long int / long long int)
// 对前面的类型加以限制,使其值 >= 0
unsigned int i = 2U;
unsigned j = 5U;
unsigned short s = 23U;
unsigned long l = 5400UL;
unsigned long long = 140ULL;
// 每个值的后缀单词都可以省略关于 size_t 的使用优点:
1
2
// 必须要引入以下头文件才可以使用:
#include <cstddef>
- 无符号性质:
size_t
是无符号整数类型,因此它只表示非负整数值。这有助于避免与负数相关的问题,特别是在处理数组索引和对象大小时。使用int
可能导致符号错误,例如负索引或溢出。- 平台独立性:
size_t
的大小足够大,可以容纳系统中最大可能的对象大小。在不同平台上,int
的大小可能会有所不同,因此在需要确保跨平台一致性时,使用size_t
更为合适。- 与标准库的一致性: C++标准库和STL广泛使用
size_t
,因此在与标准库交互时,使用size_t
使得代码更一致,更容易集成。- 提高代码清晰度: 使用
size_t
作为大小和索引的类型,可以提高代码的可读性和表达能力。它传达了程序员的意图,即该值用于表示大小或索引,而不是一般性的整数。
2.浮点型:
1
2
3
4
5
6
7
8
9
10
11
// float 浮点型数字
// 通常占 4 字节
float f = 7.2f;
// double 双精度浮点型数字
// 通常占 8 字节
double d = 7.2;
// long double 长双精度浮点型数字
// 通常占 8、12、16 等字节,取决于编译器和平台
long double d = 16.78L;
3.字符型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// char 单个字符
// 通常占 1 字节
char ch = 'm';
// chat16_t 单个 16 位字符
// 占 16 位字节
char16_t c16 = u'm'; // u 可省略
// chat32_t 单个 32 位字符
// 占 32 位字节
char32_t c32 = U'm'; // U 可省略
// wchar_t 单个宽字符
// 大小取决于编译器
wchar_t w = L'm';
// 后面三种类型主要用于处理 Unicode 字符
// Unicode 是一种字符编码标准,为每个字符都分配了一个唯一的数字码点,以便在计算机中进行统一字符表示
4.布尔类型:
1
2
3
// bool 布尔类型,取值为 true 或 false
// 占 1 字节
bool b = true;
5.单字节:(需引入 <cstddef>
头文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// std::byte 单个字节
// 在 C++17 之前,字符或无符号字符用于表示一个字节,但那些类型使得像在处理字符。std::byte 却能指明意图,即内存中的单个字节
// std::byte 的初始化需要使用单元素列表进行直接列表初始化
std::byte b{42};
// 相当于是:00101010
std::byte buffer[1024];
// 处理网络数据,读写字节流等
std::byte data[256];
// 读取或写入字节数据到文件或设备
std::byte flags = std::byte{0x0F};
// 00001111 in binary
std::byte b{42};
std::byte mask{0xF0};
std::byte result = b & mask;
// 相当于 按位与
6.类型转换:
C++ 提供三种方式来显式地转换变量类型:
1 |
|
来自于 C,并且依然被广泛使用,但实际上,不推荐使用:
1
int i_1 = (int)myFloat;
初看上去肯自然,但很少使用:
1
int i_2 = int(myFloat);
最复杂,却最整洁,也是推荐的方法,静态类型转换:
1
int i_3 = static_cast<int>(myFloat);
注意:
- 得到的整数是去掉小数部分的浮点部分。在某些环境中,可自动执行类型转换或强制执行类型转换,例如,short 可自动转换为 long,因为 long 代表精度更高的相同数据类型;
1
long someLong = someShort;
- 当自动类型转换变量的类型时,应当了解潜在的数据丢失情况,例如,float 类型转化为 int 类型会丢失掉一部分信息(数字的小数部分)。如果,将一个 float 类型赋给 int 类型而不显示执行类型转换,多数编译器会给出警告信息,如果确信左边的类型和右边的类型完全兼容,那么隐式地转换完全没有问题;
7.获取类型及大小:
1
2
3
4
5
6
7
8
9
10
11
12
13
std::cout << typeid(element).name();
// 为 type_info 类型
或者
#include <typeindex> // 必需 <typeindex>
std::cout << std::type_index(typeid(element)).name();
// 为与 type_info 类似的类,但提供了比 type_info 更好的比较和哈希功能
// 类型大小
sizeof() // 获取内存所占比特数
size() // 获取元素个数
strlen() // 仅获取 C-string 字符串有效个数,不包括 NUL
8.此外:
C++ 没有提供基本的字符串类型,但是作为标准库的一部分提供了字符串的标准实现;
1.5 运算符
在 C++ 中,运算符可以是一元的(操作一个表达式)、二元的(操作两个表达式)、三元的(操作三个表达式)。
1.一元运算符:
1
2
3
4
5
6
=
// 赋值符号
++
--
// 上述两自加加、自减减,仅对变量有效,对常量无效
2.二元运算符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
+
-
*
/
// 加、减、乘、除
%
// mod,取余运算
+=
-=
*=
/=
%=
// 简写
&
|
<<
>>
^
// 按位与、按位或、左移、右移、按位异或
&=
|=
<<=
>>=
^=
// 简写
3.三元运算符(条件运算符):
C++ 中有一个接收三个参数的运算符,称为三元运算符。可将其作为“如果【某事发生了】,那么【执行某个操作】;否则,【执行其他操作】”的条件表达式的简写。
1condition ? expression_if_true : expression_if_false;
条件运算符的优点是几乎可以在任何环境中使用,而且是直接将结果用在代码中,而非执行代码块,这使得它是一个运算符,而非条件语句。
1.6 类型
在 C++ 中,可使用基本类型(int、bool 等)创建更复杂的自定义类型。一旦熟悉 C++ 程序,就会很少使用从 C 中沿袭来的技巧,因为类更强大。虽然如此,但是还是有必要学会以下两种创建类型的方法:
1.枚举类型:
整数代表某个数字序列中的值,枚举类型允许用户定义自己的序列,这样声明的变量就只能使用这个序列中的值;
const 表示法:
1
2
3
4
5
6
7
8
9
// 当希望获取某些不变量的值的时候,使用以下方式并不是很好
// 以国际象棋为例,int 表示所有棋子
const int PieceTypeKing = 0;
const int PieceTypeQueen = 1;
const int PieceTypeRook = 2;
const int PieceTypePawn = 3;
// etc.
int myPiece = PieceTypeKing;
// 这种表示虽然正确,但是存在一定风险,因为,棋子是一个 int,如果另一个程序增加棋子的值,就可以让 King 变成 Queen,这实质上没有意义。更糟糕的是,有人可能将某个棋子的值设置成为 -1,而这个值并没有对应的常量枚举表示法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 利用以下代码生成一个新类型 PieceType
enum PieceType
{
PieceTypeKing, // 不显式设定,默认为 0
PieceTypeQueen, // 不显式设定,默认为 1
PieceTypeRook, // 不显式设定,默认为 2
PieceTypePawn // 不显式设定,默认为 3
};
PieceType myPiece;
myPiece = PieceTypeQueen;
cout << myPiece;
// 正常输出一个整型值
myPiece = 0;
cout << myPiece;
// 出现类型不匹配报错由于实质上,enum 类型是一个整型值,但是由于它本身并不是 int 类型,所以能降低风险;
关于枚举类型语法:
1
2
3
4
5
6
7
8
enum PieceType
{
PieceTypeKing = 1, // 设定为 1
PieceTypeQueen, // 为前驱 +1,为 2
PieceTypeRook = 10, // 设定为 10
PieceTypePawn // 为前驱 +1,为 11
};
// 即,某位置的值若未被设定,则,其值为前驱值 +1
2.强类型枚举:
上面给出的枚举并不是强类型的,这意味着并非是类型安全的,它们总被解释为整形数据,因此可以比较完全不同的枚举类型的枚举值;
意思是说,虽然无法参与整型运算,但是,本质上,又被解释为整形变量,所以,可以参与到整型变量比较,这同样是一种不安全;
强类型的 enum class 枚举解决了这些问题,例如,下面定义前述的 PieceType 枚举类型的安全版本:
1
2
3
4
5
6
7
enum class PieceType
{
King = 1,
Queen,
Rook = 10,
Pawn
};对于 enum class,枚举值名不会超出封闭的作用域,这代表总要使用作用域解析操作符:
1PieceType piece = PieceType::King;
这也意味着,枚举值可以指定更简短的名称,因为有作用域的限定,每次都要作用域解析;
因此,避免了枚举值自动类型转换为整数:
1
2
// 以下代码是不合法的
if (PieceType == 2) {...}此外,默认情况下,枚举值的基本类型是整型,但是可以采用以下方法加以改变:
1
2
3
4
5
6
7
enum class PieceType : unsigned long
{
King = 1,
Queen,
Rook = 10,
Pawn
};注意:
建议用类型安全的 enum class 枚举来代替类型不安全的 enum 枚举;
3.结构(struct):
1
2
3
4
5
6
7
8
9
// employeestruct.h
// 在头文件中声明结构体 Employee
struct Employee
{
char firstInitial;
char lastInitial;
int employeeNumber;
int salary;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// employee.cpp
// 在头文件中实现结构体 Employee
#include <iostream>
#include "employeestruct.h"
using namespace std;
int main()
{
Employee anEmployee;
anEmployee.firstInitial = 'M';
anEmployee.lastInitial = 'G';
anEmployee.employeeNumber = 42;
anEmployee.salary = 80000;
cout << "Employee: " << anEmployee.firstInitial << anEmployee.lastInitial << emdl;
cout << "Number: " << anEmployee.employeeNumber << endl;
cout << "Salary: $" << anEmployee.salary << endl;
return 0;
}
1.7 条件语句
1.if / else 语句
1
2
3
4
5
6
if (condition)
{...}
else if (condition)
{...}
else
{...}0/false 都被视为 false;
非0/true 都被视为 true;
2.if 语句的初始化器
1
if (<initializer>;<conditional_expression>) {<body>}
<initializer>
中引入的任何变量只能在**<conditional_expression>
** 和<body>
中可用,此类变量在 if 语句外不可用,是匿名变量;示例:
1
if (Employee employee = GetEmployee(); employee.salary > 1000) {...}
3.switch 语句
switch 是另一种根据表达式值执行操作的语法。在 C++ 中,switch 语句的表达式必须是整型、能转化为整形的类型、枚举类型或强类型枚举,必须与一个常量比较,每个常量值代表一种“*情况(case)***”,如果表达式与这种情况匹配,随后的代码将会被执行,直到遇到
break
语句为止。此外,还提供default
情况,如果没有其他情况与表达式匹配,表达式值将与default
情况匹配;示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
switch (menuItem)
{
case OpenMenuItem:
// Code to open a file
break;
case SaveMenuItem:
// Code to save a file
break;
default:
// Code to give an error message
break;
}
// 转化为相应的 if/else 语句
if (menuItem == OpenMenuItem)
{
// Code to open a file
}
else if (menuItem == SaveMenuItem)
{
// Code to save a file
}
else
{
// Code to give an error message
}如果要基于多个表达式的多个值(而非对表达式进行一些检测)执行操作 ,通常使用 switch 语句。此时,switch 语句可以避免级联使用 if-else 语句。
注意:
一旦找到与 switch 条件匹配的 case 表达式,就执行其后的语句,知道遇到 break 语句为止。即使遇到另一个 case 表达式,执行也会继续,这种语法称为 fallthrough;
1
2
3
4
5
6
7
8
9
10
switch (backgroundColor)
{
case Color::DarkBlue:
case Color::Black:
// Code to execute for both a dark blue or black background color
break;
case Color::Red:
// Code to excute for a red background color
break;
}如果是无意忘记 break 语句,fallthough 将成为 bug 的来源,因此如果在 switch 语句中,检测到 fallthough,编译器将会生成警告信息,除非像上例那样 case 为空。
此外,可以通过使用
[[fallthougn]]
特殊性,来告诉编辑器某个 fall though 是有意为之的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch (backgroundColor)
{
case Color::DarkBlue:
doSomethingForDarkBlue();
[[fallthough]];
case Color::Black:
// Code to execute for both a dark blue or black background color
doSomethingForBlackOrDarkBlue();
break;
case Color::Red:
case Color::Green:
// Code to excute for a red or green background color
break;
}
4.switch 语句的初始化器
1
switch (<initializer>, <expression>) {<body>}
与 if 语句一致,**
<initializer>
** 中引入的任何变量只能在**<conditional_expression>
** 和<body>
中可用,它们在 switch 语句外不可用;
5.条件运算符
略,详见三元运算符;
1.8 逻辑比较运算符
逻辑比较运算符(conditional operator):
1
2
3
4
5
6
7
8
9<
>
<=
>=
==
!=
!
&&
||
在 C++ 中,对表达式求值时,会采用短路逻辑,即,一旦发现最终结果可以确定,就不再对后面的表达式求值;
短路的做法对性能有好处:在使用逻辑短路时,可将代价更低的测试放在前面,以避免执行代价更高的测试。通过逻辑短路还可以避免在指针上下文中,避免指针无效时执行表达式的一部分的情况;
1.9 函数
对于大型程序来说,将所有代码都放到 main() 函数中是无法管理的。为了使程序便于理解,需要将代码分解为简单明了的程序。
在 C++ 中,为了让其他代码能够使用某个函数,首先,应当声明该函数。如果函数在某个特定的文件内部被使用,通常会在源文件中声明并定义这个函数。如果函数是供其他模块或文件使用的,通常在头文件中声明函数,并在源文件中定义函数。
函数声明通常被称为“函数原型”或“函数头”,以强调这代表函数的访问方式,而不是具体代码,术语“函数签名”指将函数名和参数列表与形参列表组合在一起,但没有返回值。
当没有与函数声明匹配的函数定义时,在编译过程中,会出现链接阶段错误;
注意:与 C 不同,在 C++ 中没有形参的函数仅需要一个空的参数列表,不需要使用 void 指出此处没有形参;然而,如果没有返回值,那么仍需要 void 来指明这一点;
函数返回类型的推断:
C++14 允许要求编辑器自动推出函数的返回值,要使用这个功能,需要把
auto
指定为返回类型;
1
2
3
4
auto addnumber(int number1, int number2)
{
return number1 + number2;
}注意:函数中可有多个
return
语句,但是它们应解析为相同的类型。这种函数甚至可以包含递归调用(调用自身),但函数中的第一个return
语句必须时非递归调用的;当前函数的名称:
每个函数都有一个预定义的局部变量 **
_ _func_ _
**,其中包括当前函数的名称。这个变量的一个用途是用于日志记录:
1
2
3
4
5
6
7
8
int addNumbers(int number1, int number2)
{
std::cout << "Entering function " << __func__ << std::endl;
// 左右两边各两个下划线,用于包括当前函数的名称
// __func__ 为 const char[] 类型
// __func__ 是编译器提供的宏,在编译时展开,而不是在运行时展开,这使得它在程序执行期间不会产生额外的运行开销,主要用于调试和日志记录,以便在运行时了解代码的执行流程,而不会影响实际的程序性能
return number1 + number2;
}
1.10 C 风格的数组
注意:在 C++ 中,尽量避免使用这种 C 风格的数组,而改用标准库功能,例如:**
std::array
** 和 **std::vector
**;
1.一维数组:
数组具有一系列值,所有值的类型相同,每个值都可以根据它在数组中的位置进行访问。在 C++ 中声明数组时,必须声明数组大小。数组大小不能用变量表示——必须用常量或常量表示式(coonstexpr) 表示数组大小;
1
2
3
4
5
int myArray[3];
myArray[0] = 0;
myArray[1] = 0;
myArray[2] = 0;
// Index 的起点始终是 0
- 不使用循环的初始化机制:
1
2
3
4
5
6
7
8
9
10
11
12
int myArray[3] = {0};
int myArray[3] = {};
// 都起到将所有列表中元素置零的作用
int myArray[] = {1, 2, 3, 4};
// 自动推导出列表长度,并初始化每个位置的值
int myArray[3] = {2};
// 将数组第一个元素置为 2,其余置为 0
int myArray[3] = {1, 2};
// 将数组第一个元素置为 1,将数组第二个元素置为 2,其余置为 0
获取数组大小(元素个数):
1
2
3
4
5
6
7
8
9
10
// 遇到数组长度等,都使用 size_t 的方式来描述大小!!!
#include <array> // 必须 <array>
unsigned int arraySize = std::size(myArray);
// 使用 unsigned int 接收数组大小
std::cout << std::size(myArray);
或者:
unsigned int arraySize = sizeof(myArray) / sizeof(myArray[0]);
2.二维数组:
1
char ticTacToeBoard[3][3];
3.三维数组、更高维数组:
难以描绘,极少使用;
1.11 std::array
在 C++ 中,有一种大小固定的特殊容器
std::array
,这种容器在<array>
头文件中定义。它详细用法在之后学习,但基本就是对 C 风格的数组进行简单包装:用
std::array
代替 C 风格的数组优点:
- 它总是知道自身大小;
- 不会自动转化为指针,从而避免了某些类型的 bug;
- 具有迭代器,可以方便地遍历元素;
示例:
1
2
3
std::array<int, 3> arr = { 9, 8, 7 };
std::cout << "Array size = " << arr.size() << std::endl;
std::cout << "2nd element = " << arr[1] << std::endl;注意:
C 风格和 std::array 的数组都具有固定的大小,在编译过程中不会改变;
如果希望数组的大小是动态的,推荐使用
std::vector
,在 vector 中添加新元素时,vector 会自动增加其大小;
1.12 std::vector
- 标准库提供了多个不同的非固定大小容器,可用于存储信息。**
std::vector
** 就是其中的一个示例。它在<vector>
头文件中被声明,用一种更灵活更安全的机制取代 C 中的数组概念。用户不必担心内存的管理,因为 vector 将自动分配足够的内存来存放元素。vector 是动态的,意味着可以在运行时添加和删除元素,而且它的用法十分简单,示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a vector of integers
std::vector<int> myVector = (11, 12);
// Add some more integers to the vector using push_back()
myVector.push_back(33);
myVector.push_back(44);
// Delete some more integer to the vector using pop_back()
// 删除最后一个元素
myVector.pop_back();
// 删除中间元素,后面元素顺位向前移动,使用 erase()
myVector.erase(myVector.begin()); // 删除索引为 0 元素
myVector.erase(myVector.begin() + 2) // 删除索引为 2 元素
// Access elements
std::cout << "1st element: " << myVector[0] << endl;
vector 中尖括号用来指定模板函数,与之前的
std::array
一样,vector 时一个泛类容器,几乎可以容纳任何类型的对象;但是必须使用简括号指定要在 vector 中存放的对象类型;为向 vector 中添加元素,可以使用 push_back() 方法;
为访问 vector 中各个元素,可使用类似于数组的语法,即,operator[];
1.13 结构化绑定
结构化绑定*(structured budings)*,允许声明多个变量,这些变量使用数组、结构、
std::pair
关键词或std::tuple
中的元素来初始化。它允许你以一种简洁的方式从复合类型(例上面所提的**std::pair
** 或 **std::tuple
**)或结构体中提取成员,并将其绑定到命名变量上。结构化绑定的主要目的是提高代码的可读性和简洁性,特别是在处理复杂数据结构时;
1
2
// 基本语法:
auto [var1, var2, ...] = expression;使用特点:
- 假定有以下数组:
1
std::array<int, 3> values = {11, 22, 33};
- 可声明三个变量 x、y、z,使用其后数组中的三个值进行初始化。注意,必须为结构化绑定使用 auto 关键字(例,不能用 int 替代 auto):
1
auto [x, y, z] = values;
注意:使用结构化绑定声明的变量数量必须与右侧表达式中的值数量匹配;
此外,如果所有非静态成员都是公有的,也可以将结构化绑定用于结构;
使用结构化绑定优点,示例:
简化**
std::pair
** 、std::tuple
或结构体使用,无需显式地访问元素索引或成员变量;std::tuple<int, double, std::string> myTuple = std::make_tuple(42, 3.14, "Hello"); auto [a, b, c] = myTuple;
提高代码可读性,直观显示内容;
struct Point { double mX; double mY; double mZ; }; Point point; point.mX = 1.0; point.mY = 2.0; point.mZ = 3.0; auto [x, y, z] = point;
减少错误风险,减少手动索引或访问机构提而引起的错误;
std::pair<int, std::string> myPair = std::make_pair(42, "Hello"); auto [num, text] = myPair;
1.14 循环
在 C++ 中,提供了 4 种循环结构:
1.while 循环
1
2
3
4
5
while (condition_expr)
{...}
// 在循环中使用 break 关键字立即跳出循环并执行之后程序
// 在循环中使用 continue 关键字可返回循环顶部并对 while 表达式重新求值
// 这两种风格都不提倡使用,因为它们会使程序的执行产生无规则的跳转,应该慎用
2.do/while 循环
1
2
3
4
do
{...}
while(condition_expr);
// 使得程序至少执行一次
3.for 循环
1
2
for (init_state; loop_conditon; iter_expr)
{...}
4.基于区间的 for 循环(Range-Based for Loop)**
这种循环类似于 Python,允许方便地迭代容器中的元素。这种循环可用于 C 风格的数组、初始化列表等,也可用于具有返回迭代器的 begin() 和 end() 函数的类型,例如,
std::array
、std::vector
等其他所有标准库容器;示例:
1
2
3
4
5
std::array<int, 4> arr = {1, 2, 3, 4};
for (int i : arr)
{
std::cout << i << std::endl;
}
1.15 初始化列表
初始化列表在
initializer_list
头文件中定义;利用初始化列表,可以轻松地编写能接收可变数量参数的函数。initializer_list
类是一个模板,要求在尖括号之间指定列表中的元素类型,这类似于指定 vector 中存储的对象类型;示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <initializer_list>
using namespace std;
// 定义可变数量参数累加函数
int makeSun(initializer_list<int> lst)
{
int total = 0;
for (int value : lst)
{
total += value;
}
return total;
}
int a = makeSun({1, 2, 3});
int b = makeSun({10, 20, 30, 40, 50, 60});
// 初始化列表是类型安全的,会定义列表中允许的类型。对于此处的 makeSun() 函数,初始化列表所有元素必须都是整数。
// 尝试使用 double 数值进行调用,将会导致编译器生成错误或警告
int c = makeSun({1, 2, 3.0});
1.16 小结
至此,C++ 程序设计的基本要点已经复习完成,这些是很简单的内容;
1.2 深入研究 C++
2.1 C++ 中的字符串
在 C++ 中使用字符串有三种方法。
1.C 风格的字符串
将字符看成字符数组;
2.C++ 风格的字符串
将字符串封装到一种易于使用的 string 类型中,需要引入
<string>
头文件;
1
2
3
4
5
// C++ 中 string 的用法与基本类型几乎相同
// 与 I/O 流一样,string 类型位于 std 名称空间
std::string myString = "Hello, World!";
std::cout << "The value of myString is " << myString << std::endl;
std::cout << "The second letter is " << myString[1] << std::endl;
3.非标准的普通类
略,详见第二章;
2.2 指针和动态内存
动态内存允许所创建的程序具有在编译时大小可变的数据,大多数复杂程序都会以某种方式使用动态内存;
1.堆栈和堆
在 C++ 的内存中,分为两部分:堆栈 和 堆。
堆栈
堆栈就像一副扑克牌,当前顶部的牌代表程序当前的作用域,通常时当前正在执行的函数;当前函数中声明的所有变量将占用顶部堆栈帧(也就是最上面的那张牌)的内存。如果当前函数(将其称为 foo())调用了另一个函数 bar(),就会翻开一张新牌,这样 bar() 就会拥有自己的堆栈帧供其运行。任何从 foo() 传递给 bar() 的参数都会从 foo() 堆栈帧复制到 bar() 堆栈帧;
堆栈帧很好,因为它为每个函数提供了独立的内存空间。如果在 foo() 堆栈帧 中声明了一个变量,那么除非专门要求,否则调用 bar() 函数不会更改该变量。此外,foo() 函数执行完毕时,堆栈帧 就会消失,该函数声明的所有变量都不会再占用内存。堆栈上的分配内存的变量不需要程序员**释放内存(删除)**,这个过程是自动完成的;
堆
堆是与当前函数与堆栈帧完全没有关系的内存区域。如果想在函数调用结束后仍保存其中声明的变量,可以将变量放到堆中。堆的结构并不复杂,可以将堆当作一个堆位。程序可在任何时候向堆中添加新位或修改堆中已有的位。**必须确保释放(删除)**在堆中分配的任何内存,这个过程不会自动完成,除非使用了智能指针;
2.使用指针
在数据类型后加
*
,将使之变为其类型的指针,但声明时,如果未初始化那么它可能指向一个随机的位置,而这时,这个指针很可能使得程序崩溃;所以,必须要在同时证明和初始化指针,如果不希望立即分配地址,则可以将它们初始化为空指针 nullptr;
1
int* myIntegerPointer = nullptr;
nullptr 是一个特殊默认值,可以在布尔表达式中被转化为 false;
使用 **new 操作符 **分配内存:
1
myIntegerPointer = new int;
指针的解除引用
1
2
3
*myIntegerPointer = 8;
// 这并不是将 myIntegerPointer 的值设定为 8,而是将 myIntegerPointer 指向的内存设为 int 类型的整型 8
// 而如果真不是解除引用,而是调整 myIntegerPointer 为 8 则很有可能是一个随机无用的内存单元,最终导致程序崩溃可将解除引用看成沿着指针箭头方向寻找堆中实际的值;
使用完 new 动态分配后的内存,需要使用 delete 操作符进行释放内存,为防止在释放指针指向的内存后再使用指针,建议把指针设置为 nullptr;
1
2
delete myIntegerPointer;
myIntegerPointer = nullptr;警告:
在解除引用前指针必须有效!对 NULL 或未初始化的指针解除引用会导致不可确定的行为,程序可能崩溃,也可能继续运行,但可能会给出奇怪的结果;
指针并被总是指向堆内存,可声明一个指向堆栈中变量甚至指向其他指针的指针。为让指针指向某个变量,**需要使用“取”址运算符 &**;
1
2
int i = 8;
int* myIntegerPointer = &i;在 C++ 中,使用特殊语法来处理指向结构的指针。从技术角度上说,如果指针指向某个结构体,可以先用 * 对指针进行解除引用,然后使用普通的 . 语法来访问结构中的字段,以一个名为 getEmployee() 的函数作为示例:
1
2
3
Employee* anEmployee = getEmployee();
// getEmployee() 是对 Employee 结构(类)的封装
std::cout << (*anEmployee).salary << endl;除此以外,还可以使用 -> 运算符同时对指针进行解引用并访问字段:
1
2
Employee* amEmployee = getEmployee();
std::cout << anEmployee->salary << std::endl;注意:
记住前面所提到的短路逻辑,示例:
1
bool isValidSalary = (anEmployee && anEmployee->salary > 0);
还可以用以下详细的方式复写:
1
bool isValidSalary = (anEmlpoyee != nullptr && anEmployee->salary > 0);
这样,可以使得仅当 anEmployee 指针有效的时候,才可对其进行解除引用以获取薪水。如果它是一个空指针,则逻辑运算短路,不再解除引用 anEmployee 指针;
3.动态分配的数组
堆也可以用于动态分配数组。使用 new[] 操作符给数组分配内存;
示例:
1
2
int arraySize = 8;
int* myVariableSizeArray = new int[arraySize];指针变量仍在堆栈中,但动态创建的数组在堆中;
完成这个数组后,应该将其堆中删除,这样其他变量就可以使用这块内存,在 C++ 中,可使用
delete[]
操作符完成:
1
2
delete [] myVariableSizeArray;
myVariableSizeArray = nullptr;注意:
避免使用 C 中的 malloc() 和 free(),而使用 new 和 delete,或者使用 new[] 和 delete[];
delete
后的方括号表明所删除的是一个数组;注意:
在 C++ 中,每次调用
new
时,都必须相应地调用 delete;在 C++ 中,每次调用
new []
时,必须相应地调用 **delete []
**,以避免内存泄漏;如果未调用
delete
或 **delete []
**,或者调用不匹配,会导致内存泄漏。之后会详细讨论内存泄漏;
4.空指针常量
在 C++11 之前,常量 NULL 用于表示空指针。将 NULL 定义为常量 0,会导致一些问题,下面给出示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func (char* str)
{
std::cout << "char* version" << std::endl;
}
void func (int i)
{
std::cout << "int version" << std::endl;
}
int main()
{
func(NULL);
return 0;
}在上述情况,由于使用的 NULL 指针等价于整数 0,所以调用的是 func 的整数版本,而非指针版本;
可引入真正的空指针常量 nullptr 来解决这个问题,给出代码:
1
2
3
4
5
6
int main()
{
func(nullptr);
return 0;
}
5.智能指针
为避免常见的内存错误,应使用智能指针代替通常的 C 风格的 “裸” 指针,智能指针对象在超出作用域时(例如,在函数执行完毕后),会自动释放内存,在 C++ 中,有两个最重要的智能指针;
智能指针有时被视为右值引用,一般通过 std::move() 进行右值引用化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 详见后面关于雇员记录系统设计的内容
std::unique_ptr<Employee>& Database::addEmployee(const std::string& firstName,
const std::string& lastName)
{
auto theEmployee = std::make_unique<Employee>(firstName, lastName);
theEmployee->setEmployeeNumber(mNextEmployeeNumber++);
theEmployee->hire();
mEmployees.push_back(std::move(theEmployee));
return mEmployees[mEmployees.size() - 1];
}
std::unique_ptr<Employee> Database::getEmployee(int employeeNumber)
{
for (auto& employee : mEmployees)
{
if (employee->getEmployeeNumber() == employeeNumber)
{
return std::move(employee);
}
}
throw std::logic_error("No employee found.");
}
std::unique_ptr<Employee> Database::getEmployee(const std::string& firstName,
const std::string& lastName)
{
for (auto& employee : mEmployees)
{
if (employee->getFirstName() == firstName && employee->getLastName() == lastName)
{
return std::move(employee);
}
}
throw std::logic_error("No employee found.");
}
1.std::unique_ptr
std::make_unique<elementType>()
数组,允许:
std::make_unique<elementType[]>(listSize)
std::unique_ptr<elementType> elementName (new elementType);
数组,允许:
std::unique_ptr<elementType[]> elementName (new elementType[listSize]);
std::unique_ptr
类似于普通指针,但在它超出作用域或者被删除时,会自动释放内存或资源。**std::unique_ptr
** 只属于它指向的对象。它的优点是:内存和资源始终被释放,即使执行返回语句或抛出异常(见稍后的讨论)。这极大地简化了代码,例如,如果有一个函数有多个返回语句,可以不必记着每个返回语句前释放资源。要创建 **
std::unique_ptr
**,应当使用 **std::make_unique<>()
**,例如,不要编写以下代码:
1
2
3
Employee* anEmployee = new Employee;
// ...
delete anEmployee;
- 而应当编写以下代码:
1
2
3
4
5
6
7
8
9
10
auto anEmployee = std::make_enique<Employee>();
// 关于 () 内参数设定,示例:
class MyClass
{
public:
MyClass(int value1, double value2);
// Other members...
};
auto myObject = std::make_unique<MyClass>(42, 3.14);
注意,这样一来,将不再需要调用 delete,因为这将自动完成。本章后面的类型推断将详细讲解 auto 关键字(这里先不详细说明)。这里只需要了解,auto 关键字告诉编译器自动推断变量的类型,因此你不必手动指定完整类型;
std::unique_ptr 是一个通用的智能指针,他可以指向任意类型的内存,所以,它本质上是一个模板。模板需要尖括号来指定模板类型参数。在尖括号中必须指定 unique_ptr 要指向的内存类型。模板详见第12章、第22章,而智能指针在本书开头介绍,可见,事实上,它们使用起来很简单;
make_unique() 在 C++14 中被引入,如果用户编译器与 C++14 不兼容,可使用如下形式的 unique_ptr(注意,现在必须将 Employee 类型指定两次):
1
std::unique_ptr<Employee> anEmployee (new Employee);
可以像普通指针那样使用 anEmployee 智能指针,例如:
1
2
3
4
if (anEmployee)
{
std::cout << "Salary: " << anEmployee->salary << std::endl;
}此外,unique_ptr 也可以储存 C 风格的数组,下例创建了一个包含 10 个 Employee 示例的数组,将其存储在 unique_ptr 中,并显示如何访问数组中的元素:
1
2
3
4
5
auto employees = std::make_unique<Employee[]>(10);
std::cout << "Salary: " << employees[0].salary << std::endl;
// 使用 C 风格的不兼容处理
std::unique_ptr<Employee[]> employees(new Employee[10]);
辨析:
上面的代码使用
anEmployee->salary
而非**anEmployee.salary
,是因为anEmployee
** 是一个指向Employee
对象的 **unique_ptr
**,而不是直接的对象,-> 的作用是解引用并访问成员,. 的作用是访问成员;下面的代码使用**
employees[0].salary
** 而非employees[0]->salary
,因为,**employees
** 是一个指向动态分配数组的std::unique_ptr
,可见,[] 符能起到解引用的作用,. 的作用认识访问成员;
2.std::shared_ptr
std::make_shared<elementType>()
数组,不允许:
std::make_shared<elementType[]>(listSize)
std::shared_ptr<elementType> elementName (new elementType);
数组,仅允许:
std::shared_ptr<elementType[]> elementName (new elementType[listSize]);
std::shared_ptr
允许数据的分布式“所有权”,每次指定std::shared_ptr
时,都递增一个引用计数,指出数据又多出了一位“拥有者”。当它超出作用域时,就递减引用计数,当引用计数为 0 时,就表示数据不再拥有任何拥有者,于是释放指针引用的对象;要创建 **
std::shared_ptr
**,应当使用std::make_shared<>()
,它与std::make_unique<>()
:
1
2
3
4
5
auto anEmployee = std::make_unique<Employee>();
if (anEmployee)
{
std::cout << "Salary: " << anEmployee->salary << std::endl;
}
- 从 C++17 开始,也可以将数组存储在
std::shared_ptr
中,而旧版的 C++ 是不允许的。但注意,此时不能使用 C++17 中的 **make_shared<>()
**,示例:
1
2
std::shared_ptr<Employee[]> employees(new Employee[10]);
std::cout << "Salary: " << employees[0].salary << std::endl;
第七章将详细阐述内存管理和智能指针,但由于 std::unique_ptr 和 std::shared_ptr 的基本用法十分简单,所以,在这里阐述;
3.注意
普通的裸指针仅允许在不涉及所有权时使用,否则默认使用
std::unique_ptr
;如果有需要共享所有权,就使用
std::shared_ptr
;如果知道
auto_ptr
,应当忘记它,因为 C++ 11/14 不赞成使用它,而 C++17 已经废弃它;如果第二种表示法:
std::XXX_ptr<elementType> elementName (new elementType);
中构造函数抛出异常,那么就会出现内存泄漏
2.3 const 的多种用法
可以使用 auto 来去除 const 函数性质
1.使用 const 定义常量
在 C 中,通常使用预处理器的 #define 机制来声明一个符号名称,其值在程序执行时不会改变;
在 C++ 中,鼓励使用 const 代替 #define 定义常量,使用 const 定义常量就像定义变量一样,只是编译器保证代码不会改变这个值;
示例:
1
2
3
const int versionNumberMajor = 2;
const int versionNumberMinor = 1;
const std::string productName = "Super Hyper Net Modulator";
2.使用 const 保护参数
在 C++ 中,可将非 const 变量转换为 const 变量,这可以提供一定保护,防止其他代码修改变量。如果程序试图改变参数的值,编译不会完成;
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
void mysteryFunction(const std::string* someString)
{
*someString = "Test";
// Will not complie
}
int main()
{
std::string myString = "The string";
mysteryFunction(&myString);
return 0;
}
2.4 引用
- C++ 允许使用给已有变量定义另一个名称:
1
2
3
4
5
6
7
int x = 42;
int& xReference = x;
std::cout << xReference;
// 对比上下两者的不同
int* xPointer = &x;
std::cout << *xPointer;给类型附加 &,则指示相应的变量是引用。在幕后他是一个指向原始变量的指针;
1.按引用传递
区别于值传递(制作副本),不会改变原始变量的值;
按引用传递参数是引用而非指针,在执行函数时,会改变原始变量的值;
1
2
3
4
5
6
7
8
9
10
11
12
13
// 传值版本
void addOne(int i)
{
i++;
// Has no real effect because this is a copy of the original
}
// 传引用版本
void addOne(int& i)
{
i++;
// Actually change the original variable
}注意:
对于两个版本的 addOne() 函数:
当前者传入字面量时,是可行的;
当后者传入字面量时,会导致编译错误;
1
2
3
addOne(3);
// 这对后者来说需要改变 3 的值,显然是不可能的
// 此外,还可以通过右值引用来解决,这将在之后讨论此外,在 C++11 之前推荐使用这种非 const 引用,但 C++11 开始,再也不这么做了,因为存在了 move 语义(之后讨论);
2.按 const 引用传递
由于制作副本,代价较大,所以,使用不改变值的引用传递,示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
void printString(const std::string& myString)
{
std::cout << myString << std::endl;
}
int main()
{
std::string someString = "Hello World";
printString(someString);
printString("Hello World"); // Passing literals works
return 0;
}
3.注意
如需要给函数传递对象,最好按 const 引用(而非值)传递,这样可以防止多余复制;
如果需要修改对象,则为其传递非 const 引用;
2.5 异常 &
C++是一种非常灵活的语言,但并不是非常安全,编译器通编写改变随机内存地址或尝试除以 0 的代码(计算机无法处理无穷大的数值)。异常就是试图增加一点安全性的语言特性;
异常是一种无法预料的情形,例如如果编写一个获取web页面的函数 ,就有几件事可能出错,包含页面的 Internet 主机可能被关闭,页面可能是空白的,或者连接可能会丢失。处理这种情况的一种方法是从函数返回特定的值,如 nullptr 或其他错误代码。异常提供了处理此类问题的更好方法;
一场伴随着一些新的术语。当某段代码检测到异常时就会抛出一个异常,另一段代码会捕捉这个异常并执行恰当的操作。 下例给出一个名为 divideNumber() 的函数,如果调用者传递给分母的值为0,就会抛出一个异常。使用
std::invalid_arugment
时需要**<stdexcept>
**:
1
2
3
4
5
6
7
8
9
10
double divideNumbers(double numerator, double denominator)
{
if (denominator == 0)
{
throw std::invalid_argument("Denominator cannot be 0.");
// 此处为抛出异常
// 详细内容将在异常章节讲述
}
return numerator / denominator;
}当执行 throw 行时,程序会立即结束而且不会返回值。如果调用者将函数调用放到 try / catch块中就可以辅助捕获异常并进行处理,示例:
1
2
3
4
5
6
7
8
9
10
try
{
std::cout << divideNumbers(2.5, 0.5) << std::endl;
std::cout << divideNumbers(2.3, 0) << std::endl;
std::cout << divideNumbers(4.5, 2.5) << std::endl;
}
catch (const std::invalid_argument& exception)
{
std::cout << "Expression caught: " << exception.what() << std::endl;
}
第一次调用 divideNumbers() 成功执行,结果会输出给用户;
第二次调用 divideNumbers() 会抛出一个异常,不会返回值,唯一的输出是捕获异常时输出的错误信息;
第三次调用 根本不会执行,因为第二次调用抛出了一个异常,导致程序跳转到 catch 块;
1
2
5
Expression caught: Denominator cannot be 0.C++ 的异常非常灵活,为了正确使用异常,需要理解抛出异常时堆栈变量的行为,必须正确捕获并处理必要的异常。前面的示例中使用了内建的 **
std::invalid_argument
**类型,但最好根据所抛出的具体错误编写自己的异常类型。最后,C++ 编译器并不强制要求捕获可能发生的所有异常。如果代码从不捕获任何异常,但有异常抛出,程序自身会捕获异常并终止。第14章将进一步讨论异常的这些更复杂方面;
2.6 类型推断
- 类型推断允许编译器自动推断出表达式的类型。类型推断有两个关键词 auto 和 decltype;
1.关键字 auto
多种完全不同的含义:
推断函数的返回类型如前所述结构化绑定,如前所述;
推断表达式的类型,如前所述;
推断非类型模板参数的类型,见第12章;
decltype(auto),见第12章;
其他函数语法,见第12章。
通用 Lambda 表达式,见第18章;
auto 可用于告诉编译器在编译时自动推断变量的类型。下面的代码演示了在这种情况下关键字 auto 最简单的用法:
1
2
auto x =123;
// x will be of type int在这个示例中输入 auto 和输入 int 的效果没有区别,但 auto 对于较复杂的类型会更有用。假定 getFoo() 函数有一个复杂的返回类型。如果希望把调用该函数的结果赋予一个变量,就可以输入该复杂类型,也可以简单的使用 auto 让编译器推断出该类型:
1
auto result = getFoo();
这样,你可以方便地更改函数的返回类型,而不需要更新代码中调用该函数的所有位置;
但使用 auto 去除了引用和 const 限定符号。假设有以下函数:
1
2
3
4
5
6
7
8
#include <string>
const std::string message = "Test";
const std::string& foo()
{
return message;
}可以调用 Foo(),把结果存储在一个变量中,将该变量的类型指定为auto,如下所示:
1
auto f1 = foo();
因为 auto 去除了引用和 const 限定符,且 f1 是 string 类型,所以建立一个副本。如果希望 f1 是一个 const 引用,就可以明确将它建立为一个引用,并标记为 const 如下所示:
1
2
3
4
5
6
7
8
9
10
11
const auto& f2 = foo();
// 补充
std::cout << typeid(element).name();
// 为 type_info 类型
或者
#include <typeindex> // 必需 <typeindex>
std::cout << std::type_index(typeid(element)).name();
// 为与 type_info 类似的类,但提供了比 type_info 更好的比较和哈希功能注意:
始终要记住,auto 去除了引用和 const 限定符,从而会创建副本!如果不需要副本,可使用 auto& 或 const auto&;
2.关键字 decltype
关键词 decltype 把表达式作为实参,计算出该表达式的类型,示例:
1
2
int x = 123;
decltype(x) y = 456;在这个示例中,编译器会推断出 y 的类型是 int,因为这是 x 的类型;
auto 与 decltype 的区别在于,decltype 未除引用和 const 限定符。再来分析返回 const string引用的 foo() 函数。按照如下方式使用 decltype 定义 f2,导致 f2 的类型为 const string&,从而不生成副本:
1
decltype(foo()) f2 = foo();
刚开始不会觉得 decltype 有多大价值。但在模板环境中,decltype 会变得十分强大,详见第12 和第 22 章;
1.3 作为面向对象语言的 C++
- 如果你是一位 C 程序员,可能会认为本章讲述的内容到目前为止只是传统 C 语言的补充最好顾名思义,C++语言在很多方面只是“更好的C”。这种观点忽略了一个重点:与 C 不同 C++ 是一种面向对象的语言;
- 面向对象程序设计 (OPP) 是一种完全不同的、更趋自然的编码方式。如果习惯使用过程语言,如 C 或者 Pascal,不要担心。第五章的讲述将观念转换到面向对象范型所需的所有背景知识。如果你已经了解OPP的理论,下面的内容将帮助你加速了解 (或者回顾) 基本的 C++ 对象语法;
3.1 定义类
类定义了对象的特征。在 C++ 中,类通常在头文件 (.h) 中声明,在对应的源文件 (.cpp) 中定义其并 非内联 方式和静态数据成员;
下面示例定义了一个基本的机票类,这个类可根据飞行的里程数以及顾客是不是“精英超级奖励计划”的成员计算票价。这个定义首先声明一个类名,在大括号内声明了类的数据成员(属性)以及方法(行为)。 每个数据成员以及方法都具有特定的访问级别:public、protected 或 private。这些标记可按任意顺序出现,也可重复使用。public 成员可在类的外部访问,private 成员不能在类的外部访问,推荐把所有的数据成员都声明为 private,在需要时,可通过 public 读取器和设置器来访问它们。 这样,就很容易改变数据的表达方式,同时使 public 接口保持不变。关于 protected 的用法,将在第 5 和 10 章中介绍“继承”时讲解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// AirlineTicket.h
#pragma once
#include <string>
class AirlineTicket
{
public:
// 构造类
AirlineTicket();
// 析构类
~AirlineTicket();
double calculatePriceInDollars() const;
const std::string& getPassengerName() const;
void setPassageName(const std::string& name);
int getNumberOfMiles() const;
void setNumberOfMiles(const int miles);
bool hasEliteSuperRewardsStatus() const;
void setHasEliteSuperRewardsStatus(const bool status);
private:
std::string mPassengerName;
int mNumberOfMiles;
bool mHasEliteSuperRewardsStatus;
};
// 约定:在类的每个数据成员之前加上小写字母 m, 如 mPassengefName注意:
为遵循 const 正确性原则,最好将不改变对象的任何数据成员的成员函数声明为 const。相对于非 const 成员函数“修改器”,这些成员函数也称为 “检测器”。
- 构造函数的初始化:
更推荐的,使用构造函数初始化器(constructor initializer)
1
2
3
4
5
6
7
8
9
// 构造函数
// AirlineTicket.h
// 推荐使用构造函数初始化器
AirlineTicket::AirlineTicket()
: mPassengerName("Unknown Passenger")
, mNumberOfMiles(0)
, mHasEliteSuperRewardsStatus(false)
{
}将初始化任务放在构造函数体内
1
2
3
4
5
6
7
8
// AirlineTicket.h
AirlineTicket::AirlineTicket()
{
//Initialize data member
mPassengerName = "Unknown Passsenger";
mNumberOfMiles = 0;
mHasEliteSuperRewardsStatus = false;
}
如果构造函数只是初始化数据成员,而不做其他事情,实际上就没必要使用构造函数,因为可在类定义中直接初始化数据成员。例如,不编写 AirlineTicket 构造函数,而是修改类定义中数据成员的定义,如下所示:
1
2
3
4
5
// AirlineTicket.h
private:
std::string nPassengerName = "Unknown Passenger";
int mNumberOfMiles = 0;
bool mHasEliteSuperRewardsStatus = false;如果类还需要执行其他的一些初始化类型,如打开文件、分配内存等,则需要编写构造函数进行处理;
析构函数
如下所示,为 AirlineTicket 类的析构函数:
1
2
3
4
AirlineTicket::~AirlineTicket()
{
// Nothing much to do in terms of cleanup
}这个析构函数什么都不做,因此可以从类中删除,这里之所以需要显示它,是为了更好了解析构函数的语法;如果需要执行一些清理,如关闭文件、释放内存等,则需要使用析构函数;
AirlinTicket 的其他类方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// AirlineTicket.cpp
double AirlineTicket::calculatePriceInDollars() const
{
if (hasEliteSuperRewardsStatus())
{
// Elite Super Rewards customers fly for free
return 0;
}
// The cost of the ticket is the number of mile times 0.1.
// Real airlines probably have a more complicated formula!
return getNumberOfMiles() * 0.1;
}
const std::string& AirlineTicket::getPassengerName() const
{
return mPassengerName;
}
void AirlineTicket::setPassengerName(const std::string& name)
{
mPassengerName = name;
}
int AirlineTicket::getNumberOfMiles() const
{
return mNumberOfMiles;
}
void AirlineTicket::setNumberOfMiles(const int miles)
{
mNumberOfMiles = miles;
}
bool AirlineTicket::hasEliteSuperRewardsStatus() const
{
return mHasEliteSuperRewardsStatus;
}
void AirlineTicket::setHasEliteSuperRewardsStatus(const bool status)
{
mHasEliteSuperRewardsStatus = status;
}
3.2 使用类
下面示例程序给出了如何使用 AirlineTicket 类。这个示例创建的两个 AirlineTicket 对象分别给予堆栈和堆:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// testAirlineTicket.cpp
// Stack-based(堆栈) AirlineTicket
// 优点:
// 1.简单直观,不需要动态内存管理。
// 缺点:
// 1.对象的生命周期受限于其所在的作用域,一旦超出作用域,对象将被销毁。
AirlineTicket myTicket;
myTicket.setPassengerName("Sherman T. Socketwrench");
myTicket.setNumberOfMiles(700);
double cost = myTicket.calculatePriceInDollars();
std::cout << "This ticket will cost $" << cost << std::endl;
/*--------------------------------------------------------------------------*/
// Heap-based(堆) AirlineTicket with smart pointer
// 推荐,使用
// 优点:
// 1.动态分配的内存由 std::unique_ptr 管理,无需手动释放内存。
// 2.可以更灵活地控制对象的生命周期
// 缺点:
// 1.相对于栈上创建,略微复杂
auto myTicket2 = std::make_unique<AirlineTicket>();
myTicket2->setPassengerName("Laudimore M. Hallidue");
myTicket2->setNumberOfMiles(2000);
myTicket2->setHasEliteSuperRewardsStatus(true);
double cost2 = myTicket2->calculatePriceInDollars();
std::cout << "This other ticket will cost $" << cost2 << std::endl;
// No need to delete myTicket2, happens automatically
/*--------------------------------------------------------------------------*/
// Heap-based AirlineTicket without smart pointer (not recommended)
// 不推荐,也不要使用
// 优点:
// 1.可以手动控制对象的生命周期。
// 缺点:
// 1.容易出现内存泄漏或释放已删除的内存,因为没有智能指针进行内存管理。
// 2.必须手动调用 delete 来释放内存,容易出现忘记释放或者释放多次的问题。
AirlineTicket* myTicket3 = new AirlineTicket();
// ... Use ticket 3
delete myTicket3;
1.4 统一初始化
1.结构与类的初始化
在 C++之前,初始化类型并非总是统一的。例如,考虑下面的两个定义,其中一个作为结构,另一个作为类,示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct CircleStruct
{
int x, y;
double radius;
};
class CircleClass
{
public:
// 使用成员初始化列表可以提高性能
CircleClass(int x, int y, double radius)
: mX(x), mY(y), mRadius(radius)
{
}
/*
CircleClass(int x = 1, int y = 1, double radius = 3)
: mX(x), mY(y), mRadius(radius)
{
// 这样实现默认值填充
}
*/
private:
int mX, mY;
double mRadius;
};
// 复写 Circle 类,使之更符合现代 C++ 关于默认初始变量设置的通常做法
class CircleClass
{
public:
Circle() = default;
// 使用成员初始化列表可以提高性能
CircleClass(int x, int y, double radius)
: mX(x), mY(y), mRadius(radius)
{
}
private:
int mX = 1;
int mY = 1;
double mRadius = 3;
};在 C++11 之前,CircleStruct 类型变量和CircleClass 类型变量的初始化是不同的,对于结构版本,可使用 {…} 语法。然而,对于类版本,需要使用函数符号 (…) 调用构造函数。 :
1
2
CircleStruct myCirclel = {10, 10, 2.5};
CircleClass myCircle2(10, 10, 2.5);自 C++11 以后,允许一律使用 {… }语法初始化类型,如下所示:
1
2
CircleStruct myCircle3 = {10, 10, 2.5};
CircleClass myCircle4 = {10, 10, 2.5};定义 myCircle4 时将自动调用 CircleClass 的构造函数。甚至等号也是可选的,因此下面的代码与前面的代码等价:
1
2
CircleStruct myCircle5{10, 10, 2.5};
CircleClass myCircle6{10, 10, 2.5};统一初始化并不局限于结构和类,它可以用于初始化 C++ 中的任何内容,示例:
1
2
3
4
5
// 均将变量初始化为 3
int a = 3;
int b(3);
int c = {3};
int d{3};
2.默认统一初始化:
1
2
3
4
5
6
7
8
// 示例
// 对于基本整型
char、int -> 0
ptr -> nullprt
// 为此只需要指定一系列空大括号
int e{}; // Uniform initialization,e will be 0
3.**阻止窄化 (narrowing) **
一般情况下,C++ 隐式地执行窄化,但存在部分编译器报错,部分不报错的情况,为解决不统一问题,示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void func(int i)
{/*...*/}
int main()
{
int x = 3.14;
func(3.14);
// 上面两种情况窄化,将 3.14 截取成为 3
// 对于窄化,部分编译器报错,部分不报
// 但是,使用 统一初始化 则都会生成编译错误
}
// 替换为:
void func(int i)
{ /* ... */ }
int main()
{
int x = {3.14}; // Error because narrowing
func({3.14}); // Error because narrowing
}
4.其他类型的统一初始化
动态分配的数组的统一初始化:
1
int* pArray = new int[4]{0, 1, 2, 3};
构造函数初始化器的统一初始化:
1
2
3
4
5
6
7
8
9
10
11
12
class Myclass
{
public:
Myclass()
: mArray{0, 1, 2, 3}
{
> }
private:
int Array[4];
}此外,统一初始化还可以用于标准库容器,之后讨论;
5.两种统一初始化的初始化列表
复制列表初始化: T obj = {arg1, arg2, …};
直接初始化: T obj {arg1, arg2, …}
在 C++17 后,与 auto 结合有以下结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initial!zer_list<int>
// Direct list initialization
auto c {11); // int
auto d {11, 22}; // Error, too many elements.
// 注意,auto 作为 std::initializer_list<int> 类型,不可以直接 std::cout << 输出,使用下面方法:
> // 以 a 为例
auto a = { 11 };
// 使用范围遍历输出
for (const auto& element : a)
{
std::cout << element << " ";
}
// 或者使用迭代器
for (auto item = a.begin(); item != a.end(); ++item)
{
std::cout << *item << " ";
}
1
2
3
4
5
6
注意:
1. **对于复制列表初始化,放在大括号中的初始化器的所有元素都必须使用相同的类型**。例如,以下代码无法编译:
> ``` c++
> auto b = {11, 12.33}; // Compilation error
- 区分结构化绑定;
1
2
3
4
5
6
7
8
9
10
11
**在早期版本 (C++11/14) 中,复制初始化列表和直接列表初始化会推导出 std::initializer_list<>:**
```c++
// Copy list initialization
auto a = {11}; // initializer_list<int>
auto b = {11, 22}; // initializer_list<int>
// Direct list initialization
auto c {11}; // initial!zer_list<int>
auto d {11, 22}; // initializer_list<int>
1.5 标准库
C++ 具有标准库,其中包含许多有用的类,在代码中可方便地使用这些类。使用标准库中类的好处是不需要重新创建某些类,也不需要浪费时间去实现系统已经自动实现的内容。另一好处是标准库中的类己经过成千上万用户的严格测试和验证。标准库中类的性能也比较高,因此使用这些类比使用自己的类效率更高。
标准库中可用的功能非常多。第 16~20 章将详细讲述标准库。当开始使用 C++时,最好立刻了解标准库可以做什么。如果你是一位 C 程序员,这一点尤其重要。作为 C 程序员,你使用 C++时可能会以 C 的方式解决问题。然而使用 C++ 的标准库类可以更方便、安全地解决问题。
本章前面己经介绍了标准库中的一些类,例如 std::string、std::array、std::vector、std::unique_ptr 和std::shared_ptr 第 16~20 章将介绍更多的类。
1.6 第一个有用的 C++ 程序
6.1 雇员记录系统
管理公司雇员记录的程序应该灵活并具有有效的功能,这个程序包含的功能有:
- 添加雇员
- 解雇雇员
- 雇员晋升
- 查看所有雇员,包括过去和现在的雇员
- 查看所有当前雇员
- 查看所有之前雇员
程序的代码分为三部分:Employee 类封装了单个雇员的信息,Database 类管理公司的所有雇员,单独的用户界面提供程序的接口;
6.2 Employee 类
Employee 类维护了某个雇员的的全部信息,该类的方法提供了查询以及修改信息的途径。Employee 类还知道如何在控制台显示自身。此外,还存在调整雇员薪水和就业状态的方法。
1.Employee.h
注意:使用以下约定:给常量加前缀 k(小写字母)。这源于德语单词 Konstant, 意思是“顾问” ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*第一行包括 #pragma once,以防止文件被包含多次。
此外还包括 string 功能。*/
#pragma once
#include <string>
// 不推荐,也不要将这个参量定义于头文件命名空间外,因为当引入到实现文件后,文件之间耦合性可能增加,可能引起错误
// const int kDefaultStartingSalary = 30000;
/*代码还声明后面的代码(包括在大括号中)将位于Records名称空间。
为使用特定代码,整个程序都会用到 Rewards 名称空间;*/
namespace Records
{
/*下面的常量代表新雇员的默认起薪,位于 Records 名称空间。
Records 名称空间中的其他代码可以将这个常量作为 kDefhultStartingSalary 访问。
在其他位置,必须通过 Records::kDefaultStartingSalary 来引用它。*/
// 这里在命名空间内定义这个参量,使得 kDefaultStartingSalary 作用域限定于命名空间域,避免耦合
const int kDefaultStartingSalary = 30000;
class Employee
{
public:
Employee() = default;
Employee(const std::string& firstName,
const std::string& lastNmae);
void promote(int raiseAmount = 1000);
void demote(int demeritAmount = 1000);
void hire(); // Hires or rehires the employee
void fire(); // Dissmisses the employee
void display() const; // Output employee info to console
// Getters and setters
void setFirstName(const std::string& firstName);
const std::string& getFirstName() const;
void setLastName(const std::string& lastName);
const std::string& getLastName() const;
void setEmployeeNumber(int employeeNumber);
int getEmployeeNumber() const;
void setSalary(int newSalary);
int getSalary() const;
bool isHired() const;
/*最后将数据成员声明为 private, 这样其他部分的代码将无法直接修改它们。
获取器和设置器提供了修改或查询这些值的唯一公有途径。
数据成员也在这里(而非构造函数中)进行初始化。*/
private:
std::string mFirstName;
std::string mLastName;
int mEmployeeNumber = -1; // 雇员编号而非雇员数量
int mSalary = kDefaultStartingSalary; // 默认起始薪资
bool mHired = false; // 受雇状态
};
}
2.Employee.cpp
注意,整型参数的默认值不显示在源文件中;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <iostream>
#include "Employee.h"
// using namespace std;
// 课本上这样讲了,但是这事实上引入 std 名称空间是一种不好的实践,尤其是在头文件中
// 下面给出更好的做法,引入所需要的标识符:
using std::cout;
using std::endl;
namespace Records
{
/*构造函数接收姓名,只设置相应的数据成员;*/
Employee::Employee(const std::string& firstName,
const std::string& lastName)
: mFirstName(firstName), mLastName(lastName)
{
}
/*promote() 和 demote() 方法只是用一些新值调用 setSalary() 方法。
注意,整型参数的默认值不显示在源文件中;
它们只能出现在函数声明中,不能出现在函数定义中。*/
void Employee::promote(int raiseAmount)
{
setSalary(getSalary() + raiseAmount);
}
void Employee::demote(int demoteAmount)
{
setSalary(getSalary() - demoteAmount);
}
/*hire() 和 fire() 方法正确设置了 mHired 数据成员*/
void Employee::hire()
{
mHired = true;
}
void Employee::fire()
{
mHired = false;
}
/*display()方法使用控制台输出流显示当前雇员的信息。
由于这段代码是 Employee 类的一部分,因此可直谈访问数据成员(如 mSalary), 而不需要使用 getSalaryo获取器。
然而,使用获取器和设置器(当存在时)是一种好的风格,甚至在类的内部也是如此。*/
void Employee::display() const
{
cout << "Employee: " << getLastName() << ", " << getFirstName() << endl;
cout << "-------------------------" << endl;
cout << (isHired() ? "Current Employee" : "Former Employee") << endl;
cout << "Employee Number : " << getEmployeeNumber() << endl;
cout << "Salary: $" << getSalary() << endl;
cout << std::endl;
}
/*许多获取器和设置器执行获取值以及设置值的任务。
即使这些方法看起来微不足道,但是使用这些微不足道的获取器和设置器,仍然优于将数据成员设置为 public。
可能想在 setSalary() 方法中执行边界检查,它们也能简化调试,因为可在其中设置断点,在检索或设置值时检查它们。
另一个原因是决定修改类中存储数据的方式时,只需要修改这些获取器和设置器。*/
// Getters and setters
void Employee::setFirstName(const std::string& firstName)
{
mFirstName = firstName;
}
const std::string& getFirstName() const
{
return mFirstName;
}
void Employee::setLastName(const std::string& lastName)
{
mLastName = lastName;
}
const std::string& getLastName() const
{
return mLastName;
}
void Employee::setEmployeeNumber(int employeeNumber)
{
mEmployeeNumber = employeeNumber;
}
int Employee::getEmployeeNumber() const
{
return mEmployeeNumber;
}
void Employee::setSalary(int newSalary)
{
mSalary = newSalary;
}
int Employee::getSalary() const
{
return mSalary;
}
bool Employee::isHired() const
{
return mHired;
}
}
3.EmployeeTest.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include "Employee.h"
using namespace std;
using namespace Records;
int main()
{
cout << "Testing the Employee class." << endl;
Employee emp;
emp.setFirstName("John");
emp.setLastName("Doe");
emp.setEmployeeNumber(71);
emp.setSalary(50000);
emp.promote();
emp.promote(50);
emp.hire();
emp.display();
return 0;
}以上为书中给出的测试文件书写方法,但下面给出我认为更好的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
#include "Employee.h"
using std::cout;
using std::endl;
int main()
{
using namespace Records; // 这里选择性地引入 Records 命名空间
// 因为是对这个文件的测试所以引入了,但一般大型工程中不使用
cout << "Testing the Employee class." << endl;
Employee emp;
emp.setFirstName("John");
emp.setLastName("Doe");
emp.setEmployeeNumber(71);
emp.setSalary(50000);
emp.promote();
emp.promote(50);
emp.hire();
emp.display();
return 0;
}
// 乃至使用以下的方式:
#include <iostream>
#include "Employee.h"
using std::cout;
using std::endl;
int main()
{
using namespace Records;
cout << "Testing the Employee class." << endl;
auto emp = std::make_unique<Employee>();
emp->setFirstName("John");
emp->setLastName("Doe");
emp->setEmployeeNumber(71);
emp->setSalary(50000);
emp->promote();
emp->promote(50);
emp->hire();
emp->display();
return 0;
}
- 当确信 Employee 类可正常运行后,应删除这个文件,或将这个文件注释掉,这样就不会编译具有多个 main() 函数的代码;
- 一种测试各个类的方法是使用单元测试,详见第 26 章中的讨论;
6.3 Database 类
Database 类使用标准库中的 std::vector 类来存储 Employee 对象
1.Database.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*由于数据库会自动给新雇员指定一个雇员号,因此定义一个常量作为编号的开始*/
#pragma once
#include <iostream>
#include <vector>
#include "Employee.h"
namespace Records
{
const int kFirstEmployeeNumber = 1000;
/*数据库可根据提供的姓名方便地添加一个新雇员。为方便起见,这个方法返回一个新雇员的引用。
外部代码也可通过调用 getEmployee() 方法来获得雇员的引用。
为这个方法声明了两个版本,一个允许按雇员号进行检索,另一个要求提供雇员的姓名。*/
class Database
{
public:
std::shared_ptr<Employee>& addEmployee(const std::string& firstName,
const std::string& lastName);
std::shared_ptr<Employee>& getEmployee(int employeeNumber);
std::shared_ptr<Employee>& getEmployee(const std::string& firstName,
const std::string& lastName);
/*由于数据库是所有雇员记录的中心存储库,因此具有输出所有雇员、当前在职雇员以及己离职雇员的方法。*/
void displayAll() const;
void displayCurrent() const;
void displayFormer() const;
/*mEmployees 包含 Employee 对象。
数据成员 mNextEmployeeNumber 跟踪新雇员的雇员号,使用 kFirstEmployeeNumber 常量进行初始化*/
private:
// std::vector<std::make_unique<Employee>()> mEmployee;
// 上是最一开始写错的版本,下面给出正确版本
std::vector<std::shared_ptr<Employee>> mEmployees;
int mNextEmployeeNumber = kFirstEmployeeNumber;
};
}
2.Database.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/*addEmployeeo方法创建一个新的 Employee 对象,在其中填充信息并将其添加到 vector 中。
注意当使用了这个方法后,数据成员 mNextEmployeeNumber 的值会递增,因此下一个雇员将获得新编号*/
#include <iostream>
#include <stdexcept>
#include "Database.h"
using std::cout;
using std::endl;
namespace Records
{
std::shared_ptr<Employee>& Database::addEmployee(const std::string& firstName,
const std::string& lastName)
{
auto theEmployee = std::make_shared<Employee>(firstName, lastName);
theEmployee->setEmployeeNumber(mNextEmployeeNumber++);
theEmployee->hire();
mEmployees.push_back(theEmployee);
return mEmployees[mEmployees.size() - 1];
}
std::shared_ptr<Employee>& Database::getEmployee(int employeeNumber)
{
for (auto& employee : mEmployees)
{
if (employee->getEmployeeNumber() == employeeNumber)
{
return employee;
}
}
throw std::logic_error("No employee found.");
}
std::shared_ptr<Employee>& Database::getEmployee(const std::string& firstName,
const std::string& lastName)
{
for (auto& employee : mEmployees)
{
if (employee->getFirstName() == firstName && employee->getLastName() == lastName)
{
return employee;
}
}
throw std::logic_error("No employee found.");
}
void Database::displayAll() const
{
for (const auto& employee : mEmployees)
{
employee->display();
}
}
void Database::displayCurrent() const
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == true)
{
employee->display();
}
}
}
void Database::displayFormer() const
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == false)
{
employee->display();
}
}
}
}
3.DatabaseTest.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*用于数据库基本功能的简单测试*/
#include <iostream>
#include "Database.h"
using std::cout;
using std::endl;
int main()
{
Records::Database myDB;
auto& emp1 = myDB.addEmployee("Greg", "Wallis");
emp1->fire();
auto & emp2 = myDB.addEmployee("Marc", "White");
emp2->setSalary(100000);
auto & emp3 = myDB.addEmployee("John", "Doe");
emp3->setSalary(10000);
emp3->promote();
cout << "all employees: " << endl << endl;
myDB.displayAll();
cout << endl << "current employees: " << endl << endl;
myDB.displayCurrent();
cout << endl << "former employees: " << endl << endl;
myDB.displayFormer();
return 0;
}
4.std::unique_ptr版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
// Database.h
#pragma once
#include <iostream>
#include <vector>
#include "Employee.h"
namespace Records
{
const int kFirstEmployeeNumber = 1000;
class Database
{
public:
std::unique_ptr<Employee>& addEmployee(const std::string& firstName,
const std::string& lastName);
std::unique_ptr<Employee> getEmployee(int employeeNumber);
std::unique_ptr<Employee> getEmployee(const std::string& firstName,
const std::string& lastName);
void displayAll() const;
void displayCurrent() const;
void displayFormer() const;
private:
std::vector<std::unique_ptr<Employee>> mEmployees;
int mNextEmployeeNumber = kFirstEmployeeNumber;
};
}
/*-----------------------------------------------------------------------------------*/
// Database.cpp
#include <iostream>
#include <stdexcept>
#include "Database.h"
using std::cout;
using std::endl;
namespace Records
{
std::unique_ptr<Employee>& Database::addEmployee(const std::string& firstName,
const std::string& lastName)
{
auto theEmployee = std::make_unique<Employee>(firstName, lastName);
theEmployee->setEmployeeNumber(mNextEmployeeNumber++);
theEmployee->hire();
mEmployees.push_back(std::move(theEmployee));
return mEmployees[mEmployees.size() - 1];
}
std::unique_ptr<Employee> Database::getEmployee(int employeeNumber)
{
for (auto& employee : mEmployees)
{
if (employee->getEmployeeNumber() == employeeNumber)
{
return std::move(employee);
}
}
throw std::logic_error("No employee found.");
}
std::unique_ptr<Employee> Database::getEmployee(const std::string& firstName,
const std::string& lastName)
{
for (auto& employee : mEmployees)
{
if (employee->getFirstName() == firstName && employee->getLastName() == lastName)
{
return std::move(employee);
}
}
throw std::logic_error("No employee found.");
}
void Database::displayAll() const
{
for (const auto& employee : mEmployees)
{
employee->display();
}
}
void Database::displayCurrent() const
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == true)
{
employee->display();
}
}
}
void Database::displayFormer() const
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == false)
{
employee->display();
}
}
}
}
/*-----------------------------------------------------------------------------------*/
// DatabaseTest.cpp
#include <iostream>
#include "Database.h"
using std::cout;
using std::endl;
int main()
{
Records::Database myDB;
auto& emp1 = myDB.addEmployee("Greg", "Wallis");
emp1->fire();
auto & emp2 = myDB.addEmployee("Marc", "White");
emp2->setSalary(100000);
auto & emp3 = myDB.addEmployee("John", "Doe");
emp3->setSalary(10000);
emp3->promote();
cout << "all employees: " << endl << endl;
myDB.displayAll();
cout << endl << "current employees: " << endl << endl;
myDB.displayCurrent();
cout << endl << "former employees: " << endl << endl;
myDB.displayFormer();
return 0;
}
6.4 用户界面(UI)**
程序的最后一部分是基于菜单的用户界面,可让用户方便地使用雇员数据库。main()函数是一个显示菜单的循环,执行被选中的操作,然后重新开始循环。对于大多数的操作都定义了独立的函数。对于显示雇员之类的简单操作,则将实际代码放在对应的情况(case)中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Display.cpp
#include <iostream>
#include <stdexcept>
#include <exception>
#include <limits> // 用于清除输入缓冲区
#include "Database.h"
using std::cout;
using std::endl;
using std::string;
int displayMenu();
void doHire(Records::Database& db);
void doFire(Records::Database& db);
void doPromote(Records::Database& db);
void doDemote(Records::Database& db);
int main()
{
Records::Database employeeDB;
bool done = false;
while (!done)
{
int selection = displayMenu();
switch (selection)
{
case 0:
done = true;
break;
case 1:
doHire(employeeDB);
break;
case 2:
doFire(employeeDB);
break;
case 3:
doPromote(employeeDB);
break;
case 4:
employeeDB.displayAll();
break;
case 5:
employeeDB.displayCurrent();
break;
case 6:
employeeDB.displayFormer();
break;
default:
std::cerr << "Unknown command." << endl;
break;
}
}
return 0;
}
/* displayMenu() 函数输出菜单获取用户输入。
在此假定用户能够“正确地输入”,当需要一个数字时就输入一个数字,这一点很重要。
在阅读了第 13 章有关 I / O 的内容后,你就会知道如何防止输入错误信息*/
int displayMenu()
{
int selection;
cout << endl;
cout << "Employee Database" << endl;
cout << "-----------------" << endl;
cout << "1) Hire a new employee" << endl;
cout << "2) Fire an employee" << endl;
cout << "3) Promote an employee" << endl;
cout << "4) List all employees" << endl;
cout << "5) List all current employee" << endl;
cout << "6) List all former employee" << endl;
cout << "0) Quit" << endl;
cout << "---> ";
// 循环,直到得到有效的输入
while (true)
{
// 尝试读取用户输入
try
{
std::cin >> selection;
// 检查输入流的状态
if (std::cin.fail())
{
throw std::invalid_argument("Invalid input. Please enter a number.");
}
// 如果程序能够执行到这里,说明输入是有效的
break;
}
catch (const std::invalid_argument& exception)
{
std::cerr << "Error: " << exception.what() << endl;
// 清除错误状态
std::cin.clear();
// 忽略缓冲区中的无效字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
}
return selection;
}
/* doHire() 函数获取用户输入的新雇员的姓名,并通知数据库添加这个雇员*/
void doHire(Records::Database& db)
{
string firstName;
string lastName;
cout << "First name? input: ";
std::cin >> firstName;
cout << "Last name? input: ";
std::cin >> lastName;
db.addEmployee(firstName, lastName);
}
/* doFire() 、doPromote() 以及 doDemote() 函数都要求数据库根据雇员号找到雇员,然后使用 Employee 对象的 public 方法进行修改*/
void doFire(Records::Database& db)
{
int employeeNumber;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
// 将 try 遇到的问题抛出,并继续执行程序
try
{
std::unique_ptr<Records::Employee> emp = db.getEmployee(employeeNumber);
emp->fire();
cout << "Employee " << employeeNumber << " terminated." << endl;
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to terminate employee: " << exception.what() << endl;
}
}
void doPromote(Records::Database& db)
{
int employeeNumber;
int raiseAmount;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
cout << "How much of a raise? input: ";
std::cin >> raiseAmount;
try
{
std::unique_ptr<Records::Employee> emp = db.getEmployee(employeeNumber);
emp->promote(raiseAmount);
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to promote employee: " << exception.what() << endl;
}
}
void doDemote(Records::Database& db)
{
int employeeNumber;
int demeritAmount;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
cout << "How much of a demerit? input: ";
std::cin >> demeritAmount;
try
{
std::unique_ptr<Records::Employee> emp = db.getEmployee(employeeNumber);
emp->demote(demeritAmount);
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to promote employee: " << exception.what() << endl;
}
}
6.5 评估程序
前面的程序涵盖了许多主题,从最简单的到较复杂的都有。可采用多种方法扩展这个程序。例如,用户界面(UI)没有公开 Database 或 Employee 类的全前功能。可修改 UL 以包含这些特性。还可修改 Database 类,以从 mEmployees 中删除被解雇的雇员。
如果不理解程序的某些部分,参考前面的内容以回顾这些主题。如果仍不甚明了,最好的学习方法是编写代码并查看结果。例如,如果不确定如何使用条件运算符,可编写一个简单的 main()函数进行测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
// Employee.h
#pragma once
#include <string>
namespace Records
{
const int kDefaultStartingSalary = 30000;
class Employee
{
public:
Employee() = default;
Employee(const std::string& firstName,
const std::string& lastNmae);
void promote(int raiseAmount = 1000);
void demote(int demeritAmount = 1000);
void hire(); // Hires or rehires the employee
void fire(); // Dissmisses the employee
void display() const; // Output employee info to console
// Getters and setters
void setFirstName(const std::string& firstName);
const std::string& getFirstName() const;
void setLastName(const std::string& lastName);
const std::string& getLastName() const;
void setEmployeeNumber(int employeeNumber);
int getEmployeeNumber() const;
void setSalary(int newSalary);
int getSalary() const;
bool isHired() const;
private:
std::string mFirstName;
std::string mLastName;
int mEmployeeNumber = -1; // 雇员编号而非雇员数量
int mSalary = kDefaultStartingSalary; // 默认起始薪资
bool mHired = false; // 受雇状态
};
}
/*--------------------------------------------分隔符---------------------------------------------------*/
// Employee.cpp
#include <iostream>
#include "Employee.h"
using std::cout;
using std::endl;
namespace Records
{
Employee::Employee(const std::string& firstName,
const std::string& lastName)
: mFirstName(firstName), mLastName(lastName)
{
}
void Employee::promote(int raiseAmount)
{
setSalary(getSalary() + raiseAmount);
}
void Employee::demote(int demeritAmount)
{
setSalary(getSalary() - demeritAmount);
}
void Employee::hire()
{
mHired = true;
}
void Employee::fire()
{
mHired = false;
}
void Employee::display() const
{
cout << "Employee: " << getLastName() << ", " << getFirstName() << endl;
cout << "-------------------------" << endl;
cout << (isHired() ? "Current Employee" : "Former Employee") << endl;
cout << "Employee Number : " << getEmployeeNumber() << endl;
cout << "Salary: $" << getSalary() << endl;
cout << std::endl;
}
void Employee::setFirstName(const std::string& firstName)
{
mFirstName = firstName;
}
const std::string& Employee::getFirstName() const
{
return mFirstName;
}
void Employee::setLastName(const std::string& lastName)
{
mLastName = lastName;
}
const std::string& Employee::getLastName() const
{
return mLastName;
}
void Employee::setEmployeeNumber(int employeeNumber)
{
mEmployeeNumber = employeeNumber;
}
int Employee::getEmployeeNumber() const
{
return mEmployeeNumber;
}
void Employee::setSalary(int newSalary)
{
mSalary = newSalary;
}
int Employee::getSalary() const
{
return mSalary;
}
bool Employee::isHired() const
{
return mHired;
}
}
/*--------------------------------------------分隔符---------------------------------------------------*/
// Database.h
#pragma once
#include <iostream>
#include <vector>
#include "Employee.h"
namespace Records
{
const int kFirstEmployeeNumber = 1000;
class Database
{
public:
std::unique_ptr<Employee>& addEmployee(const std::string& firstName,
const std::string& lastName);
std::unique_ptr<Employee>& getEmployee(int employeeNumber);
std::unique_ptr<Employee>& getEmployee(const std::string& firstName,
const std::string& lastName);
void removeEmployee(const std::string& firstName,
const std::string& lastName);
void removeEmployee(int employeeNumber);
void removeUnhiredEmployee();
void displayAll() const;
void displayCurrent() const;
void displayFormer() const;
private:
std::vector<std::unique_ptr<Employee>> mEmployees;
int mNextEmployeeNumber = kFirstEmployeeNumber;
};
}
/*--------------------------------------------分隔符---------------------------------------------------*/
// Database.cpp
#include <iostream>
#include <stdexcept>
#include "Database.h"
using std::cout;
using std::endl;
namespace Records
{
std::unique_ptr<Employee>& Database::addEmployee(const std::string& firstName,
const std::string& lastName)
{
auto theEmployee = std::make_unique<Employee>(firstName, lastName);
theEmployee->setEmployeeNumber(mNextEmployeeNumber++);
theEmployee->hire();
mEmployees.push_back(std::move(theEmployee));
return mEmployees[mEmployees.size() - 1];
}
std::unique_ptr<Employee>& Database::getEmployee(int employeeNumber)
{
if (!mEmployees.empty())
{
for (auto& employee : mEmployees)
{
if (employee->getEmployeeNumber() == employeeNumber)
{
return employee;
}
}
throw std::logic_error("No employee found.");
}
else
{
std::cerr << "No employees in the database." << std::endl;
// 返回一个空指针或者抛出异常,具体取决于你的需求
throw std::logic_error("No employees in the database.");
}
}
std::unique_ptr<Employee>& Database::getEmployee(const std::string& firstName,
const std::string& lastName)
{
if (!mEmployees.empty())
{
for (auto& employee : mEmployees)
{
if (employee->getFirstName() == firstName && employee->getLastName() == lastName)
{
return employee;
}
}
throw std::logic_error("No employee found.");
}
else
{
std::cerr << "No employees in the database." << std::endl;
// 返回一个空指针或者抛出异常,具体取决于你的需求
throw std::logic_error("No employees in the database.");
}
}
//void Database::removeEmployee(const std::string& firstName,
// const std::string& lastName)
//{
// auto item = std::find(mEmployees.begin(), mEmployees.end(), getEmployee(firstName, lastName));
// if (item != mEmployees.end() || ((*mEmployees.end())->getFirstName() == firstName && (*mEmployees.end())->getFirstName() == lastName))
// {
// mEmployees.erase(item);
// }
// else
// {
// std::cerr << "Element not found." << std::endl;
// }
//}
//void Database::removeEmployee(int employeeNumber)
//{
// auto item = std::find(mEmployees.begin(), mEmployees.end(), getEmployee(employeeNumber));
// if ((*item)->getEmployeeNumber() == employeeNumber)
// {
// mEmployees.erase(item);
// }
//}
void Database::removeEmployee(const std::string& firstName, const std::string& lastName)
{
if (!mEmployees.empty())
{
mEmployees.erase(
std::remove_if(mEmployees.begin(), mEmployees.end(), [&firstName, &lastName](const auto& employee) {
return employee->getFirstName() == firstName && employee->getLastName() == lastName;
}),
mEmployees.end()
);
cout << "This employee is removed" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
void Database::removeEmployee(int employeeNumber)
{
if (!mEmployees.empty())
{
mEmployees.erase(
std::remove_if(mEmployees.begin(), mEmployees.end(), [employeeNumber](const auto& employee) {
return employee->getEmployeeNumber() == employeeNumber;
}),
mEmployees.end()
);
cout << "This employee is removed" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
void Database::removeUnhiredEmployee()
{
if (!mEmployees.empty())
{
mEmployees.erase
(std::remove_if(mEmployees.begin(),
mEmployees.end(),
[](const auto& employee)
{
return !employee->isHired(); // 移除所有未雇佣的员工
}),
mEmployees.end()
);
cout << "Fired employees are removed" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
void Database::displayAll() const
{
if (!mEmployees.empty())
{
for (const auto& employee : mEmployees)
{
employee->display();
}
cout << "All employees are printed above" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
void Database::displayCurrent() const
{
if (!mEmployees.empty())
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == true)
{
employee->display();
}
}
cout << "Current employees are printed above" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
void Database::displayFormer() const
{
if (!mEmployees.empty())
{
for (const auto& employee : mEmployees)
{
if (employee->isHired() == false)
{
employee->display();
}
}
cout << "Former employees are printed above" << endl;
}
else
{
std::cerr << "No employees in the database." << std::endl;
}
}
}
/*--------------------------------------------分隔符---------------------------------------------------*/
// UItest.cpp
#include <iostream>
#include <stdexcept>
#include <exception>
#include <limits> // 用于清除输入缓冲区
#include "Database.h"
using std::cout;
using std::endl;
using std::string;
int displayMenu();
void doHire(Records::Database& db);
void doFire(Records::Database& db);
void doPromote(Records::Database& db);
void doDemote(Records::Database& db);
int main()
{
Records::Database employeeDB;
bool done = false;
while (!done)
{
int selection = displayMenu();
switch (selection)
{
case 0:
done = true;
break;
case 1:
doHire(employeeDB);
break;
case 2:
doFire(employeeDB);
break;
case 3:
doPromote(employeeDB);
break;
case 4:
employeeDB.displayAll();
break;
case 5:
employeeDB.displayCurrent();
break;
case 6:
employeeDB.displayFormer();
break;
case 7:
employeeDB.removeUnhiredEmployee();
break;
default:
std::cerr << "Unknown command." << endl;
break;
}
}
return 0;
}
/* displayMenu() 函数输出菜单获取用户输入。
在此假定用户能够“正确地输入”,当需要一个数字时就输入一个数字,这一点很重要。
在阅读了第 13 章有关 I / O 的内容后,你就会知道如何防止输入错误信息*/
int displayMenu()
{
int selection;
cout << endl;
cout << "Employee Database" << endl;
cout << "-----------------" << endl;
cout << "1) Hire a new employee" << endl;
cout << "2) Fire an employee" << endl;
cout << "3) Promote an employee" << endl;
cout << "4) List all employees" << endl;
cout << "5) List all current employee" << endl;
cout << "6) List all former employee" << endl;
cout << "7) Remove all unhired employees" << endl;
cout << "0) Quit" << endl;
cout << "---> ";
// 循环,直到得到有效的输入
while (true)
{
// 尝试读取用户输入
try
{
std::cin >> selection;
// 检查输入流的状态
if (std::cin.fail())
{
throw std::invalid_argument("Invalid input. Please enter a number.");
}
// 如果程序能够执行到这里,说明输入是有效的
break;
}
catch (const std::invalid_argument& exception)
{
std::cerr << "Error: " << exception.what() << endl;
// 清除错误状态
std::cin.clear();
// 忽略缓冲区中的无效字符,直到遇到换行符
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
}
return selection;
}
/* doHire() 函数获取用户输入的新雇员的姓名,并通知数据库添加这个雇员*/
void doHire(Records::Database& db)
{
string firstName;
string lastName;
cout << "First name? input: ";
std::cin >> firstName;
cout << "Last name? input: ";
std::cin >> lastName;
db.addEmployee(firstName, lastName);
}
/* doFire() 、doPromote() 以及 doDemote() 函数都要求数据库根据雇员号找到雇员,然后使用 Employee 对象的 public 方法进行修改*/
void doFire(Records::Database& db)
{
int employeeNumber;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
// 将 try 遇到的问题抛出,并继续执行程序
try
{
if (db.getEmployee(employeeNumber)->isHired())
{
db.getEmployee(employeeNumber)->fire();
cout << "Employee " << employeeNumber << "is terminated." << endl;
}
else
{
std::cerr << "Employee has been fired" << endl;
}
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to terminate employee: " << exception.what() << endl;
}
}
void doPromote(Records::Database& db)
{
int employeeNumber;
int raiseAmount;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
cout << "How much of a raise? input: ";
std::cin >> raiseAmount;
try
{
if (db.getEmployee(employeeNumber)->isHired())
{
db.getEmployee(employeeNumber)->promote(raiseAmount);
cout << "Employee " << employeeNumber << "is promoted." << endl;
}
else
{
std::cerr << "Employee has been fired" << endl;
}
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to promote employee: " << exception.what() << endl;
}
}
void doDemote(Records::Database& db)
{
int employeeNumber;
int demeritAmount;
cout << "Employee number? input: ";
std::cin >> employeeNumber;
cout << "How much of a demerit? input: ";
std::cin >> demeritAmount;
try
{
db.getEmployee(employeeNumber)->demote(demeritAmount);
}
catch (const std::logic_error& exception)
{
std::cerr << "Unable to promote employee: " << exception.what() << endl;
}
}
1.7 本章小结
- 我上面所书写的代码并不是按照书上的格式写的,我使用了智能指针来实现这个工程,而不是书上说书写的 Employee 类的 vector,而是它的智能指针的 vector;
- 现在已经了解了 C++的基本知识,为成为专业 C++程序员做好了准备。在开始深入学习本书后面的 C++ 语言知识时,可查阅本章以回顾需要复习的内容。为了回顾那些被遗忘的概念,只需要查看本章的一些示例代码。
- 编写的每个程序都必须以这样或那样的方式使用字符串。为此,下一章将深入讲解如何在 C++ 中处理字符串。