书接上文
条件语句
条件语句可以根据某事是否为真来执行代码。如下面的小节中所展示的,C++中有三种主要的条件语句类型:if/else语句,switch语句和条件运算符。
if/else语句
最常用的条件语句是if语句,它可以跟else搭配使用。如果if语句中的条件为真,一行或一块代码被执行。否则,如果else存在的话就执行它,或都执行条件语句后面的代码。下面的代码示例了一个级联的if语句,一个有趣的理解是这个if语句带着一个else语句,else语句反过来又包含另一个if语句,依次类推:
if (i > 4) {
// Do something.
} else if (i > 2) {
// Do something else.
} else {
// Do something else.
}
if语句括号中的表达式必须是一个布尔值或者可以计算出一个布尔值。0值为false,非0值为true。例如:if(0)等价于if(false)。稍后介绍的逻辑运算符提供了一种将表达式计算为true或false布尔值的一种方法。
条件语句初始化设置
C++17 使用下面的语法可以在条件语句里面提供一个初始化设置:
if (<initializer> ; <conditional_expression>) { <body> }
<initializer>中的变量只在 <conditional_expression>和 <body>中有效。它们在条件语句之外是无效的。在本书中给出这个特性的有用示例还为时过早,但下面是它的样子:
if (Employee employee = GetEmployee() ; employee.salary > 1000) { ... }
这个例子中,初始化设置获取一个雇员,条件语句判断他的薪水是否超过1000。只有在大于1000的情况下条件语句的代码块才会被执行。本书会给出更多具体的例子。
switch语句
switch语句是另一种根据表达式的值执行操作的语法。在C++中,switch语句中表达式的值必须为整型、可转换为整型的类型、枚举类型或者强枚举型,并具必须与常量进行比较。每一个常量值代表一个''case"。如果表达式与case匹配,则接下来的语句会被执行,直到一个break语句为至。也可以提供一个默认的case,如果其它case没有匹配的话就匹配到这个。下面的伪代码展示了switch语句的常见用法:
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;
}
switch语句通常可以转换成if/else语句。上面的switch语句可以转换成下面的形式:
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语句。如果是测试单个值,那么if或者if-else语句是最好的选择。
一旦找到一个与表达式匹配的case,那么它下的所有语句被执行直到一个break语句结束。即使遇到另一个case表达式(称为fallthrough),也会继续执行。下面的例子中有几个不同的case匹配时都会执行代同一个代码块:
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 execute for a red background color
break;
}
switch (backgroundColor) {
case Color::DarkBlue:
doSomethingForDarkBlue();
[[fallthrough]];
case Color::Black:
// Code is executed for both a dark blue or black background color
doSomethingForBlackOrDarkBlue();
break;
case Color::Red:
case Color::Green:
// Code to execute for a red or green background color
break;
}
switch语句初始化器(Initializers for switch Statements)
switch (<initializer> ; <expression>) { <body> }
<initializer> 中的变量只在<expression>和<body>中有效。在switch语句之外,它们是无效的。
std::cout << ((i > 2) ? "yes" : "no");
i > 2的括号是可选的,所以下面是等价形式:
std::cout << (i > 2 ? "yes" : "no");
条件运算符的优势是几乎可以在任何上下文中使用。前面的例子中,条件运算符是在输出环境中使用。一种简便记住它的使用方法是将问号看作它前面的语句确定是一个问题。例如,“i是否大于2?如果是,则结果是'yes';否则结果是'no'。"
与if语句或switch语句不同的是条件运算符不依赖结果执行代码块。相反,它的使用正如前面的例子。从这种角度看,它确实是一个运算符(像+和-),而不是像if和swich那样是直正的条件语句。
运算符 | 描述 | 用法 |
< <= > >= |
检验左边是否小于,小于或等于,大于,大于或等于右边
|
if (i < 0) {
std::cout << "i is negative";
}
|
== |
检验左边是否等于右边。不要和=(赋值)运算符混淆!
|
if (i == 3) {
std::cout << "i is 3";
}
|
!= |
不相等。如果左边不等于右边则结果为真。
|
if (i != 3) {
std::cout << "i is not 3";
}
|
! |
逻辑非。对布尔表达式的真/假状态取反。这是一个一元运算符。
|
if (!someBoolean) {
std::cout << "someBoolean is false";
}
|
&& |
逻辑与。如果表达式的两个部分都为真是结果为真。
|
if (someBoolean && someOtherBoolean) {
std::cout << "both are true";
}
|
|| |
逻辑或。如果表达式的任何部分为真结果就为真。
|
if (someBoolean || someOtherBoolean) {
std::cout << "at least one is true";
}
|
bool result = bool1 || bool2 || (i > 7) || (27 / 13 % i + 1) < 2;
这个例子中,如果bool1是真,那么整个表达式一定为真,所以其它部分就不被计算。通过这种方式,C++语言就可以避免程序运行不必要的代码。当然,如果后面的表达式在某种程度上影响程序的状态(例如,对过调用一个单独的函数),那么这可能成为难以寻找bugs的地方。下面的代码示例了一个使用&&的语句,由于第二项导致短路,因为0的计算结果始终为假。
bool result = bool1 && 0 && (i > 7) && !done;
短路对性能是有好处的。可以将速度快的测试放到前面这样当逻辑短路发生时耗时的测试就不会被执行。在有指针的环境下也很有用,当指针非法时可以避免表达式其它部分被执行。本章后面会讨论指针和指针短路。
void myFunction(int i, char c);
没有对应于函数声名的实际定义,在编译的链接阶段会出错,因为使用此函数的程序在调用不存在的代码。下面的定义打印两个参数的值:
void myFunction(int i, char c)
{
std::cout << "the value of i is " << i << std::endl;
std::cout << "the value of c is " << c << std::endl;
}
在程序的其它地方调用myFunction()并给两个参数输入值。一些简单的函数调用示例如下:
myFunction(8, 'a');
myFunction(someInt, 'b');
myFunction(5, someChar);
注意 不像C,在C++中一个没有参数的函数仅仅有一个空参数列表。没有必要使用void来指明不需参数。然而,当没有返回值时仍然需要使用void来指示。
int addNumbers(int number1, int number2)
{
return number1 + number2;
}
这个函数可以像下面这样调用:
int sum = addNumbers(5, 3);
函数返回类型推导
auto addNumbers(int number1, int number2)
{
return number1 + number2;
}
编译器根据返回语句的表达式来推导返回类型。函数中可以有多条返回语句,但它们应该都解析成相同类型。这样的函数甚至可以包含递归调用(自我调用),但是函数中的第一个返回语句必须是非递归调用。
当前函数的名字
每一个函数都有一个本地预定义变量__func__,它包含当前函数的名字。此变量的一个用途是日志输出:
int addNumbers(int number1, int number2)
{
std::cout << "Entering function " << __func__ << std::endl;
return number1 + number2;
}
int myArray[3];
myArray[0] = 0;
myArray[1] = 0;
myArray[2] = 0;
警告 在C++中,数组的第一个元素的位置总是0,不是1。
int myArray[3] = {0};
甚至可以像下面这样不要0:
int myArray[3] = {};
数组也可以使用初始化列表初始化,这种情况下编译器可以自动推断数组的大小。如下:
int myArray[] = {1, 2, 3, 4}; // The compiler creates an array of 4 elements.
如果确实指定了数组大小,并且初始化列表的元素小于数组的大小,那么数组余下的元素被设置成0。例如,下面的代码只把数组的第一个元素设置为2,其它所有的元素都设置成0:
int myArray[3] = {2};
在C++17中可以使用std::size()(需要<array>)函数来获取基于栈的C风格数组的大小。例如:
unsigned int arraySize = std::size(myArray);
如果你的编译器还不支持C++17,获取基于栈的C风格数组大小的诀窍是使用sizeof运算符。sizeof运算符以字节单位返回其参数的大小。为了获取基于栈的数组元素数量,需要用以字节为单位的数组大小除以以字节为单位的第一个元素的大小。例如:
unsigned int arraySize = sizeof(myArray) / sizeof(myArray[0]);
前面的例子展示了一维数组,可以想像成一行整数,每一个都有属于自己的带编号隔间。C++支持多维数组。可以把二维数组想像成一个棋盘,每一个位置都有一个x坐标和一个y坐标。三维和更高维很难图形化,并且使用的也很少。下面的代码展示了分配一个二维的字符数组来当井字板,然后在方形中央放一个“o”:
char ticTacToeBoard[3][3];
ticTacToeBoard[1][1] = 'o';
图1-1显示了该板的可视化表示和每个正方形的位置。
TicTacToeBoard[0][0]
|
TicTacToeBoard[0][1]
|
TicTacToeBoard[0][2]
|
TicTacToeBoard[1][0]
|
TicTacToeBoard[1][1]
|
TicTacToeBoard[1][2]
|
TicTacToeBoard[2][0]
|
TicTacToeBoard[2][1]
|
TicTacToeBoard[2][2]
|
array<int, 3> arr = {9, 8, 7};
cout << "Array size = " << arr.size() << endl;
cout << "2nd element = " << arr[1] << endl;
注意 C风格数组和std::arrays都有固定的大小,必须在编译阶段确定。它们无法在运行时扩大和缩小。
// Create a vector of integers
vector<int> myVector = { 11, 22 };
// Add some more integers to the vector using push_back()
myVector.push_back(33);
myVector.push_back(44);
// Access elements
cout << "1st element: " << myVector[0] << endl;
myVector被声明为vector<int>。正如std::array一样,需要用尖括号来指定模板参数。vector是通用容器,它几乎可以盛装任何类型;这就是需要在vector的尖括号里指定期望保存数据类型的原因。12章和22章讨论模板细节。可以使用push_back()方法向vector添加元素。可以使用类似数组的语法来访问每个元素,例如[]操作符。
std::array<int, 3> values = { 11, 22, 33 };
声明三个变量x、y、 z,如下用数组的三个值来初始化它们。注意必须使用关键字auto来进行结构化绑定。比如不能使用int来替代auto。
auto [x, y, z] = values;
声明的结构化绑定变量的数量必须要和表达式右边值的数量匹配。如果结构体的所有非静态成员都是公有属性,那么也可以使用结构体来结构化绑定。例如,
struct Point { double mX, mY, mZ; };
Point point;
point.mX = 1.0; point.mY = 2.0; point.mZ = 3.0;
auto [x, y, z] = point;
在第17章和20章会分别给出使用std::pair和std::tuple的例子。
int i = 0;
while (i < 5) {
std::cout << "This is silly." << std::endl;
++i;
}
关键字break可以在循环中用来立即跳出循环并继续执行程序。关键字continue可以用来返回到循环的顶端,并重新评估while表达式的值。然而在循环中使用continue被认为是一种不好的风格,因为它会使程序的执行随意的跳转到某个地方,所以要保守的使用它。
int i = 100;
do {
std::cout << "This is silly." << std::endl;
++i;
} while (i < 5);
for循环
for (int i = 0; i < 5; ++i) {
std::cout << "This is silly." << std::endl;
}
基于范围的for循环
std::array<int, 4> arr = {1, 2, 3, 4};
for (int i : arr) {
std::cout << i << std::endl;
}
#include <initializer_list>
using namespace std;
int makeSum(initializer_list<int> lst)
{
int total = 0;
for (int value : lst) {
total += value;
}
return total;
}
函数makeSum()用一个整型初始化器列表作为参数。函数体利用基于范围的for循环来累加总和。这个函数可以像下面这样使用:
int a = makeSum({1,2,3});
int b = makeSum({10,20,30,40,50,60});
初始化器列表是类型安全的并且定义何种类型是列表可以接收的。对于这里展示的makeSum()函数,初始化器列表的所有元素必须是整型的。尝试用一个double来调用会导致编译错误或者警告,如下所示:
int c = makeSum({1,2,3.0});
string myString = "Hello, World";
cout << "The value of myString is " << myString << endl;
cout << "The second letter is " << myString[1] << endl;
指针和动态内存
int* myIntegerPointer;
int类型后面的*表明声明的变量引用或指向一些整型内存。可以将指针视为指向动态分配的堆内存的箭头。它还没有指向任何特定的东西因为还没有把它赋给任何东西;它是一个未初始化变量。任何时候都应该避开未初始化变量,特别是未初始化的指针,因为它们指向内存中某个随机的位置。使用这类指针很大概率导致程序挂死。这就是为什么应该总是在指针声明的同时初始化。如果不想立即分配内存可以把指针初始化为null指针(nullptr——参见“NULL指针常量”一节获得更多信息):
int* myIntegerPointer = nullptr;
空指针是一个特殊的默认值,任何有效的指针都不会有这个值,并且在布尔表达式中可以转换为false。例如:
if (!myIntegerPointer) { /* myIntegerPointer is a null pointer */ }
使用new运算符分配内存:
myIntegerPointer = new int;
像这种情况,指针指向仅有一个整型值的地址。为了访问这个值,需要解引用指针。可以把解引用想像成顺着指针箭头找到位于堆上的实际值。为了给刚才在堆上分配的整型赋值,使用像下面这样的代码:
*myIntegerPointer = 8;
注意,这与把myIntegerPointer 设置成8不同。没有改变指针,只是改变了指针指向的内存。如果重新分配指针的值,将它指向内存地址8,这个位置很可能是一个随机的无效值,最终会导致程序崩溃。使用完动态分配的内存后,需要使用delete运算符来释放内存。为避免使用已释放的指针,推荐将(已释放的)指针置为nullptr:
delete myIntegerPointer;
myIntegerPointer = nullptr;
警告 有效的指针才能解引用。对null指针或未初始化的指针解引用导致未定义的行为。程序可能崩溃,也可能继续运行并产生异常结果。
int i = 8;
int* myIntegerPointer = &i; // Points to the variable with the value 8
C++有处理结构体指针的特殊语法。在技术层面上,如果有一个指向结构体的指针,可首先使用*进行解引用,然后使用正常的.语法来访问它的域,如下代码所示,假定存在一个叫getEmployee()的函数。
Employee* anEmployee = getEmployee();
cout << (*anEmployee).salary << endl;
这个语法有点啰嗦。->(箭头)运算符可以一步同时完成解引用和域访问。下面的代码与前面的等价,但更易读:
Employee* anEmployee = getEmployee();
cout << anEmployee->salary << endl;
还记得本章稍早时讨论的短路逻辑的概念么?它跟指针组合以避免使用无效的指针非常有用。如下例子所示:
bool isValidSalary = (anEmployee && anEmployee->salary > 0);
或稍详细点:
bool isValidSalary = (anEmployee != nullptr && anEmployee->salary > 0);
只有anEmployee是有效指针时才解引用来获取salary。如果它是null指针,短路逻辑运算使得anEmployee不被解引用。
int arraySize = 8;
int* myVariableSizedArray = new int[arraySize];
这为arraySize 个整数分配足够的内存。图1-3展示了这段代码执行后堆和栈的样子。正如你所见的,指针变量依然驻留在栈上,但是动态分配的数组在堆上。现在内存已经分配好了,可以像使用常规的栈数组那样使用它。
myVariableSizedArray[3] = 2;
delete[] myVariableSizedArray;
myVariableSizedArray = nullptr;
delete后面的括号指示正在删除一个数组!
void func(char* str) {cout << "char* version" << endl;}
void func(int i) {cout << "int version" << endl;}
int main()
{
func(NULL);
return 0;
}
main()函数使用NULL参数来调用func函数。NULL被认为是空指针常量。也就是说你希望用空指针作为参数来调用char*版本的func()。然而,由于NULL不是指针,而是整数0,所以整数版本的func()被调用。引入真正的空指针常量nullptr可以解决这个问题。下面的代码调用char*版本的func():
func(nullptr);
智能指针
Employee* anEmployee = new Employee;
// ...
delete anEmployee;
应该这样写:
auto anEmployee = make_unique<Employee>();
注意不再需要调用delete了;它会自动调用的。在本章后面的“类型推断”一节会详细讨论auto关键词。现在知道auto关键词告诉编译器自动推导变量的类型就足够了,这样可以不用手动输入完整的类型了。unique_pt是一个通用智能指针,它可以指向任何类型的内存。这就是它是一个模板的原因。模板需要尖括号<>来指定模板参数。在括号内需要指定期望unique_ptr指向的内存类型。第12和22章讨论模板,但是智能指针在第1章介绍,这样在整本书中就可以使用它们了——你也将看到他们的用法很简单。
从C++14才开始支持make_unique()。如果编译器不支持C++14则可以按如下方式创建unique_ptr(注意现在需要两次指定类型Employee):
unique_ptr<Employee> anEmployee(new Employee);
if (anEmployee) {
cout << "Salary: " << anEmployee->salary << endl;
}
unique_ptr可以用来保存C风格的数组。下面的例子创建一个具有10个Employee实例的数组,保存在unique_ptr中;也展示了如何从数组中访问元素的:
auto employees = make_unique<Employee[]>(10);
cout << "Salary: " << employees[0].salary << endl;
shared_ptr允许共享数据所有权。每次shared_ptr 被赋值一次,引用计数就是自增1,代表数据又多了一个所有者。当一个shared_ptr超出作用域时,引用计数就自减1。当引用计数减为0时意味着数据已没有所有者了,指针所指向的对象被释放掉。应当使用std::make_shared<>()创建shared_ptr,与make_unique<>()类似:
auto anEmployee = make_shared<Employee>();
if (anEmployee) {
cout << "Salary: " << anEmployee->salary << endl;
}
从C++17开始,shared_ptr也可用来保存数组,老的C++是不允许这样的。但注意C++17中make_shared<>() 不能用于这种情况。这里有个例子:
shared_ptr<Employee[]> employees(new Employee[10]);
cout << "Salary: " << employees[0].salary << endl;
第7章更详细的讨论内存管理和智能指针,但是因为unique_ptr和shared_ptr的基本用法非常直白,所以它们已在全书的例子中使用。