C++ Primer 学习笔记 第五章 语句
C++中大多语句以分号;结束。一个表达式,如ival + 5,末尾加上分号就变成了表达式语句。表达式语句的作用是执行表达式并丢弃掉求值结果:
ival + 5; // 一条没什么用的表达式语句
cout << ival; // 一条有用的表达式语句
最简单的语句是空语句:
; // 空语句中只含有一个分号
如果在程序中某个地方,语法上需要一条语句但逻辑上不需要,此时应该使用空语句。还有当循环的全部工作在条件部分就可以完成时,通常需要空语句:
// 重复读入数据直至到达文件末尾或某次输入的值等于sought
while (cin >> s && s != sought)
; // 空语句
使用空语句时应该加上注释,表示有意忽略该语句。
多余的空语句并非总是无害的,如无意加到循环体或判断语句后会出现错误。
复合语句指用花括号括起来的(可能为空的)语句和声明的序列,复合语句也被称为块。一个块就是一个作用域,在块中引入的名字只能在块内部以及嵌套在块中的子块里访问。
在程序某个地方,语法上需要一条语句,但逻辑上需要多条语句,则应该使用复合语句,如while或for的循环体必须是一条语句,但我们通常要在循环内干很多事情,因此需要将多条语句用花括号括起来,从而把语句序列转变成块。
块不以分号作为结束。
空块指内部没有任何语句的一对花括号,作用等价于空语句。
可以在if、switch、while和for语句的控制结构内定义变量,定义在控制结构当中的变量只在相应语句的内部可见:
while (int i = get_num())
cout << i << endl;
i = 0; // 错误,循环外不能访问i
以上代码中i只会定义一次,定义是在编译阶段完成,但每次循环是否会重复创建并销毁变量,取决于是内置类型还是类类型。对于循环来说,内置类型一般编译器都会优化避免每次重复分配空间;对于类类型,最好在循环体外定义,否则每次循环都会经历变量的销毁和创建。
if语句:
// 形式一:
if (condition)
statement
// 形式二:
if (condition)
statement
else
statement2
也可以嵌套if语句:
if (condition)
statement
else if (condition)
statement2
else
statement3
为避免因丢失了在if语句后的花括号而引起的错误,有些编码风格要求在if和else(对while、for也有此要求)后必须写上花括号。
悬垂else:当程序中出现多个else和if时,else与最近的if相匹配。可以使用花括号改变此规则。
switch语句:
// 为每个元音字母计数
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char c;
while (cin >> c) {
switch (c) {
case 'a':
++aCnt;
break;
case 'e':
++eCnt;
break;
case 'i':
++iCnt;
break;
case 'o':
++oCnt;
break;
case 'u':
++uCnt;
break;
}
}
cout << aCnt << " " << eCnt << " " << iCnt << " " << oCnt << " " << uCnt << endl;
switch语句首先对括号里的表达式求值,该表达式可以是一个初始化的变量声明。表达式的值转化成整数类型,然后与每个case标签的值比较。如果和某个case标签的值匹配成功,程序从该标签之后的第一条语句开始执行,直到到达了switch的结尾或者是遇到一条break语句为止。
break语句作用是中断当前的控制流,此例中break语句将控制权移到switch语句外面。
如果switch语句的表达式和所有case都没有匹配上,将直接跳到switch结构之后第一条语句。
case关键字和它的对应值一起被称为case标签,case标签必须是整型常量表达式:
char ch = getVal();
int ival = 42;
switch(ch) {
case 3.14: // 错误,case标签不是一个整数
case ival: // 错误:case标签不是一个常量
}
case标签的值不能相同,否则会引发错误。default也是一种特殊的case标签。
// 当我们想统计所有元音字母出现总次数
unsigned cnt = 0;
char c;
while (cin >> c) {
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++cnt;
break;
}
}
// switch语句部分也可以写成:
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u':
++cnt;
break;
}
一般不要省略case分支最后的break语句。如果没写break语句,最好加一段注释。
如果没有任何一个case标签能匹配上switch表达式的值,程序将执行紧跟在default标签后边的语句:
// 统计输入的字母不是元音的个数和元音的个数
unsigned cnt = 0, cnt2 = 0;
char c;
while (cin >> c) {
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
++cnt;
break;
default:
++cnt2;
break;
}
}
cout << cnt << " " << cnt2 << endl;
defalut标签可以不在最后面:
unsigned cnt = 0, cnt2 = 0;
char c;
while (cin >> c) {
switch (c) {
case 'a':
case 'e':
case 'i':
default:
++cnt2;
break;
case 'o':
case 'u':
++cnt;
break;
}
}
cout << cnt << " " << cnt2 << endl; // 当输入aeiouv时,输出2 4
即使不准备在default标签下做任何操作,定义一个default标签也有用,目的在于告诉程序读者已经考虑到了默认情况,只是目前什么也没有做。
标签不应该孤零零地出现,它后面必须跟上一条语句或另外一个case标签,如果switch结构以一个空的default标签作为结束,则该default标签后面必须跟一条空语句或空块。(我自己测试时,当default语句不在最后一个case出现时,其后可以不加空语句或空块)。
switch内部如果有变量定义,由于程序可能会跳过某些分支,如果被跳过的分支中有变量定义时,C++不允许跨过变量的初始化语句直接跳转到该变量所在作用域的另一个位置:
case true:
string file_name; // 错误,控制流可能绕过该处一个隐式初始化的变量
int ival = 0; // 错误,控制流可能绕过该处一个显式初始化的变量
int jval; // 正确,jval未初始化
case false:
jval = next_num(); // 正确,即使程序没有进入true标签,jval也被定义了,在使用jval前必须赋值,否则报错使用了未初始化的变量
如果需要为某个case分支定义并初始化一个变量,需要将变量定义在块内:
case true:
{
string file_name = get_file_name();
}
break;
case false:
if (file_name.empty()); // 错误,file_name作用域不涉及到此处
使用一系列if语句统计从cin读入的文本中有多少元音字母:
char c;
int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
while (cin >> c) {
if (c == 'a') {
++aCnt;
}
if (c == 'e') {
++eCnt;
}
if (c == 'i') {
++iCnt;
}
if (c == 'o') {
++oCnt;
}
if (c == 'u') {
++uCnt;
}
}
cout << "有" << aCnt << "个a." << endl;
cout << "有" << eCnt << "个e." << endl;
cout << "有" << iCnt << "个i." << endl;
cout << "有" << oCnt << "个o." << endl;
cout << "有" << uCnt << "个u." << endl;
统计各个元音字母数量,大小写都要统计:
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
char c;
while (cin >> c) {
switch (c) {
case 'a':
case 'A':
++aCnt;
break;
case 'e':
case 'E':
++eCnt;
break;
case 'i':
case 'I':
++iCnt;
break;
case 'o':
case 'O':
++oCnt;
break;
case 'u':
case 'U':
++uCnt;
break;
}
}
cout << aCnt << " " << eCnt << " " << iCnt << " " << oCnt << " " << uCnt << endl;
统计元音字母数量的同时,统计空格、制表符和回车的数量:
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, spaceCnt = 0, tabCnt = 0, lineBreakCnt = 0;
char c;
while ((c = cin.get()) != '#') { // 以#作为输入结束符
switch (c) {
case 'a':
case 'A':
++aCnt;
break;
case 'e':
case 'E':
++eCnt;
break;
case 'i':
case 'I':
++iCnt;
break;
case 'o':
case 'O':
++oCnt;
break;
case 'u':
case 'U':
++uCnt;
break;
case ' ':
++spaceCnt;
break;
case '\t':
++tabCnt;
break;
case '\n':
++lineBreakCnt;
break;
}
}
cout << aCnt << " " << eCnt << " " << iCnt << " " << oCnt << " " << uCnt << " " << spaceCnt << " " << tabCnt << " " << lineBreakCnt << endl;
cin.get函数可以获取到空白字符,但这时不能再用Ctrl+Z(文件结束符)再按回车来终止输入了。
统计元音字母的同时,也统计含有以下两个字符的字符序列的数量:ff、fl和fi:
unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0, flCnt = 0, fiCnt = 0, ffCnt = 0;
char c = 0, prec = 0; // prec为上个字符
bool flag = false; // 引入flag确保fff不会被算成两个ff
while (cin >> noskipws >> c) { // noskipws表示不会忽略空白字符,即被空白字符隔开的双字符匹配不会成功
switch (c) {
case 'a':
case 'A':
++aCnt;
break;
case 'e':
case 'E':
++eCnt;
break;
case 'i':
if (prec == 'f') {
++fiCnt;
flag = true; //记录两个字符的条件匹配成功
}
case 'I':
++iCnt;
break;
case 'o':
case 'O':
++oCnt;
break;
case 'u':
case 'U':
++uCnt;
break;
case 'f':
if (prec == 'f') {
++ffCnt;
flag = true;
}
break;
case 'l':
if (prec == 'f') {
++flCnt;
flag = true;
}
break;
}
if (flag) { // 如果本次匹配成功
prec = 0; // 初始化prec
flag = false; // 初始化flag
} else { // 如果本次匹配失败
prec = c; // 继续正常匹配
}
}
cout << aCnt << " " << eCnt << " " << iCnt << " " << oCnt << " " << uCnt << " " << ffCnt << " " << flCnt << " " << fiCnt << endl;
while语句:
while (condition)
statement
只要condition求值结果为真就一直执行statement(常常是一个块)。条件部分可以是一个表达式或者是一个带初始化的变量声明。
定义在while条件部分或者while循环体内的变量每次迭代都经历从创建到销毁的过程。(一般编译器会优化内置类型,每次循环不会创建和销毁,但对于类类型,还是会每次循环创建和销毁)。
当不确定要迭代多少次时,使用while循环比较合适。
从标准输入读取若干string对象,并输出最大连续出现的string及其连续出现的次数,如不存在重复,输出一条消息说明没有单词重复过:
string s, pres, res; // 记录当前串、当前串之前的串、出现次数最多的串
int count = 1, maxCount = 0;
bool flag = 0; // flag记录是否出现过连续重复字符串
while (cin >> s) {
if (s == pres) { // 如果与上个字符串相同
++count;
flag = true;
if (count > maxCount) { // 如果当前串连续重复出现的次数创纪录
maxCount = count; // 更新记录
res = pres; // 更新创纪录的串
}
} else {
count = 1; // 初始化count
}
pres = s;
}
if (flag == false) {
cout << "没有连续重复的字符串。" << endl;
} else {
cout << res << "出现了" << maxCount << "次。" << endl;
}
传统for语句:
for (init-statement; condition; expression)
statement
关键字for及括号里的部分称作for语句头。init-statement必须是声明语句、表达式语句、空语句其中一个。
执行过程:
1.首先执行一次init-statement。
2.判断condition,为true时才进入循环。
3.执行statement。
4.最后执行expression。
第一步只有在循环开始时执行一次,二三四步会重复执行直到condition为假。
for语句头中定义的对象只在for循环体内可见。
init-statement可以定义多个对象,但init-statement只能有一条声明语句,因此,所有变量的基础类型必须相同。
for语句头能省略掉三项中的任何项,但分号必须保留。省略condition的效果等价于在condition处写了一个true。
检验短vector是否是长vector的前缀:
vector<int> ivec1 = { 1,2,3,4,5,6,7,8,9 };
vector<int> ivec2 = { 1,2,3,4,5 };
int i = 0;
for ( ; i < ivec2.size(); ++i) {
if (ivec1[i] != ivec2[i]) {
cout << "匹配失败。" << endl;
break;
}
}
if (i == ivec2.size()) {
cout << "匹配成功。" << endl;
}
C++11新标准引入了范围for语句,可以遍历容器或其他序列的所有元素:
for (declaration : expression)
statement
expression必须是一个序列,如花括号括起来的初始值列表、数组或者vector、string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin和end成员(数组有非成员的begin、end函数)。
declaration定义一个变量,序列中的每个元素都得能转换成该变量的类型。如需要对序列中的元素执行写操作,循环变量需声明成引用类型。
每次迭代都会重新定义循环控制变量,并将其初始化成序列中的下一个值,之后才会执行statement。
不能通过for语句增加vector对象(或其他容器中的对象)的元素,因为在范围for语句中,预存了end()函数的值。
do while语句先执行循环体后检查条件:
do
statement
while (condition);
对于do while语句来说,先执行statement再判断condition,因此条件部分不允许定义变量。
在do while循环体中定义的变量的作用域不包括条件部分:
do {
int i = 2;
} while (i); // 错误,i未定义
就像for循环一样:
for ( ; k < 5; ++k) { // 错误,k未定义
int k = 0;
}
循环重复地输入两个string对象,然后挑出短的输出:
do {
string s1, s2;
cin >> s1 >> s2;
if (s1.size() < s2.size()) {
cout << s1 << endl;
} else {
cout << s2 << endl;
}
} while (1);
break语句负责终止离它最近的while、do while、for、或switch语句。
从标准输入读取非空string直到连续出现两个相同串或所有单词都读完,输出连续出现的那个串或输出消息说明没有连续出现的串:
string s, pres;
bool flag = false;
while (cin >> s) {
if (s == pres) {
flag = true;
cout << s << endl;
break;
} else {
pres = s;
}
}
if (!flag) {
cout << "没有连续重复的字符串。" << endl;
}
continue终止最近的循环中的当前迭代并立即开始下一次迭代。
从标准输入读取非空string直到连续出现两个相同串并且该串以大写字母为开头或所有单词都读完,输出连续出现的那个以大写为开头的串或输出消息说明没有出现的这样的串:
string s, pres;
bool flag = false;
while (cin >> s) {
if (s == pres) {
if (!isupper(s[0])) {
continue;
}
flag = true;
cout << s << endl;
break;
} else {
pres = s;
}
}
if (!flag) {
cout << "没有连续重复的以大写为开头的字符串。" << endl;
}
goto作用是从goto语句跳转到同一函数内的另一条语句。不要在程序中使用goto语句,它使得程序既难理解又难修改:
goto label;
label是用于标识一条语句的标识符。带标签语句是一种特殊的语句:
end: return; // 带标签语句,可以作为goto的目标
标签标识符独立于其他标识符的名字。
和switch相似,它也不能跳过变量的声明初始化语句:
goto end;
int ix = 10; // 错误,跳过了带初始化的变量定义
end: ix = 42;
向后跳过一个已经执行的定义是合法的,跳回到变量的定义之前会销毁该变量,然后重新定义它:
begin: int sz = get_size();
if (sz <= 0) {
goto begin; // 执行后会销毁sz,sz将重新定义并初始化
}
改写以上代码:
int sz = get_size();
while (sz <= 0) {
sz = get_size();
}
异常处理机制为程序中异常检测和异常处理的协作提供支持。C++中,异常处理机制包括:
1.throw表达式,异常检测部分使用throw表达式来表示它遇到了无法处理的问题。我们说throw引发了异常。
2.try语句块,异常处理部分使用try语句块处理异常。try语句块以关键字try开始,并以一个或多个catch子句结束。try语句块中代码抛出的异常通常会被某个catch子句处理。因为catch子句处理异常,它们也被称为异常处理代码。
3.一套异常类,用于在throw表达式和相关catch子句间传递异常的具体信息。
throw表达式包含关键字throw和紧随其后的一个表达式,表达式的类型就是抛出异常的类型。throw表达式后面通常紧跟一个分号,构成一条表达式语句:
if (a != b) {
throw runtime_error("Data must be same");
}
这段代码中,如果a与b不相等就抛出一个异常,该异常是类型runtime_error的对象。抛出异常将终止当前函数,并把控制权转移给能处理该异常的代码。
类型runtime_error是标准库异常类型的一种,定义在stdexcept头文件中。我们必须初始化runtime_error类型对象,方式是给它提供一个string对象或一个C风格的字符串。
try语句块语法形式:
try {
program-statements
} catch (exception-declaration) {
handler-statements
} catch (exception-declaration) {
handler-statements
} //...
catch后的括号中是一个(可能未命名的)对象的声明(异常声明)。try块中声明的变量在块外无法访问,特别在catch子句中也无法访问。
编写异常处理代码:
int a, b;
cin >> a >> b;
try {
if (a != b) {
throw runtime_error("a != b");
}
} catch (runtime_error err) {
cout << "进入异常处理代码。" << endl;
cout << err.what() << endl;
}
程序执行try块内代码时,抛出了类型为runtime_error的异常,接下来程序控制权交给了catch子句中异常处理代码。异常处理代码中用到了what函数,在这里它是runtime_error类的一个成员函数。实际上每个标准库异常类都定义了名为what的成员函数,这些函数没有参数,返回值是抛出并初始化异常时所用的字符串副本。
复杂系统中,程序在遇到抛出异常的代码前,其执行路径可能已经经过了多个try语句块,如在一个try语句块中可能调用了包含另一个try语句块的函数,这个函数中的try块中可能又调用了另一个包含try语句块的函数。在寻找处理代码时的过程与函数调用链刚好相反,异常被抛出时,首先搜索抛出该异常的函数。如果没找到匹配的catch子句,终止该函数,并在调用该函数的函数中继续寻找。如果还是没有找到,重复以上步骤直到找到适当类型的catch子句,如还是没有找到匹配的catch子句,程序转到名为terminate的标准库函数,该函数行为与操作系统有关,一般情况下该函数会导致程序非正常退出。对于没有try块的异常来说,也按以上步骤处理,没有try块意味着当前函数没有匹配的catch子句。
异常会中断程序的正常流程,可能函数的一部分计算已经完成,会导致对象处于无效或未完成的状态,或者资源没有正常释放,在异常期间正确执行了清理工作的程序被称为异常安全的代码。编写这种代码非常困难。
C++标准库定义了一组类,用于报告标准库函数遇到的问题,这些异常类也可以在用户编写的程序中使用,它们定义在四个头文件中:
1.exception:定义了最通用的异常类exception。它只报告异常的发生,不提供额外信息。
2.stdexcept:定义了常用的异常类,内容在以下表格中。
3.new:定义了bad_alloc异常类型。
4.type_info:定义了bad_cast异常类型。
头文件stdexcept中定义的异常类:
类型 | 说明 |
---|---|
exception | 最常见的问题 |
runtime_error | 只有在运行时才能检测的问题 |
range_error | 运行时错误,生成的结果超出了有意义的值域范围 |
overflow_error | 运行时错误:计算上溢 |
underflow_error | 运行时错误:计算下溢 |
logic_error | 程序逻辑错误 |
domain_error | 逻辑错误:参数对应的结果值不存在 |
invalid_ argument | 逻辑错误:无效参数 |
length_error | 逻辑错误:试图创建一个超出该类型最大长度的对象 |
out_of_range | 逻辑错误:使用一个超出有限范围的值 |
以上的异常类型与catch子句的匹配关系:throw出的类型是运行时(程序逻辑)错误的,要么进入与异常类型完全相同的catch子句,要么进入runtime_error(logic_error)的catch子句,要么进入exception的catch子句。进入顺序优先选最近的catch子句。
如以下代码会进入第二个catch子句:
int a = 3, b = 5;
try {
if (a != b) {
throw runtime_error("a != b");
}
} catch (range_error err) {
cout << "进入range_error异常处理代码。" << endl;
cout << err.what() << endl;
} catch (exception err) { // 会进入这个异常处理代码
cout << "进入exception异常处理代码。" << endl;
cout << err.what() << endl;
} catch (runtime_error err) {
cout << "进入runtime_error异常处理代码。" << endl;
cout << err.what() << endl;
}
标准库定义的异常类只定义了创建或拷贝异常类型的对象以及为异常类型的对象赋值的方法。
我们只能默认初始化exception(但我测试时可以用一个string或C风格字符串初始化)、bad_alloc和bad_cast对象,不允许为这些对象提供初始值。而头文件stdexcept中的异常恰好相反,应该使用string或C风格字符串初始化这类对象,不能默认初始化,创建时必须提供初始值,初始值中包含与错误相关的信息。
异常类型只定义了一个名为what的成员函数,该函数没有任何参数,返回值是一个指向C风格字符串的const char *,用以提供关于异常的一些文本信息。如果该异常类型有一个字符串类型的初始值,则what返回该字符串,对于无初始值的异常类型,what返回内容由编译器决定。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2020-09-09 LeetCode 面试题 04.02. 最小高度树