What & How & Why

语句

C++ Primer 笔记 第五章


简单语句

C++ 中以 ; (Smeicolon) 作为语句结束的标志。表达式与 ; 的组合被称为表达式语句(Expression Statement),比如下面这样的:

ival + 5; //useless since the result is discard
由于单纯表达式语句会直接丢掉其运算结果,因此往往这样的语句都会附带其他的运算,比如赋值或者打印。

空语句

空语句(Null Statement)指语句中只有一个 ; 的语句。该种语句用于在逻辑上不需要,但是语法需要的地方, 比如某些只需使用条件就可以结束循环的循环体:

while (cin >> s && s != sought) 
	; //null statement
上面的语句在条件中就制定好了结束的情形:只要 s 读到一个等于 sought 的值就结束输入,因此循环内部的空语句就是为了语法正确而添加的。

为空语句添加注释,这样可以使阅读者注意到该语句是有意忽略的。

漏写、多写分号

漏写分号会导致语法上的问题。而因为空语句的性质,非法的 ; 在程序里往往被视为空语句。这种额外出现的分号往往会导致逻辑上的问题:

while (iter != svec.end()) ;
++iter; // increment is not part of the loop.
上例中处于条件语句末尾的空语句顶替下方的迭代器自加成为了 while 的循环体。

复合语句 / 块

复合语句 (Compound statement) ,又被称为Blocks),指一系列被 {} 包含起来的一系列声明和语句。每个 Block 都是一个 Scope,在 Block 内创建的变量名只能在 Block 内使用(以及 nested Block)。变量名的有效区从其被定义到 Block 结束为止

Block 也可以为空:

while (condtions) 
    { } //empty block, semicolon is not needed.

条件语句

主要的条件语句有if-else / switch-case。具体应用哪种语句根据条件的数量来看。总的来说,条件越多,用switch-case的效率越高。

If-else

  • 使用 curly brace 确定正确的 scope。
  • C++ 中,if-else 如果有嵌套,需要注意 if-else 的配对。C++ 中 if 总是找寻最近的 else 配对,因此有可能出现本来要与外层 if 配对的 else 被内层 If 抢了先(这个问题被称为 Dangling else)。正确的使用 curly brace 可以解决这个问题。

switch-case

Switch 语句应用与具有大量条件,但条件都是固定的情况。一个典型的例子是对一段话中的元音 a/e/i/o/u 出现的次数进行计数:

while (cin >> ch) {
	switch (ch) {
	case 'a':
		++aCnt;
		break;	
	case 'e':
		++eCnt;
		break;
	case 'i':
		++iCnt;
		break;
	case 'o':
		++oCnt;
		break;
	case 'u':
		++uCnt;
		break;
	}	
}
switch 会执行之后 Parenthesis 中的表达式,接着从之后的 case 中寻找是否存在与表达式匹配的 case。如果匹配,就会接着往下查看别的 case,直到 block 结束。如果没有找到匹配结果,会直接跳到 switch{} 之后的第一个语句。这里因为在循环体内,所以返回了 while 进行下一次匹配。

如果希望找到匹配条件后直接进行新一轮的匹配,使用 break 关键字可以直接跳出当前的 switch{}

需要注意的是:

  • 条件表达式(上例中的 ch必须是 integral type(与 C 不同!)。除开可以转换为 intergal 的类、结构体(类中有对应的转换方法,且转换无歧义),任何非 integral type 但又可以转换为 Intergal type 的类型,必须显式的转换为 integral type 才能使用。
  • case label 必须是常量表达式
  • 同一个 switch 语句中不能存在两个值相同的 case。
Control flow within a switch

默认情况下,当某一个 case 匹配以后,switch 的控制流会接着执行之后的 case 匹配。在匹配的 case 之后使用 break 可以中断这样的控制流;但从上例上来说,我们可以利用这样默认的控制流来计算出所有元音的总数。省略 break 之后,默认控制流可以让程序一次匹配多个 case:

unsigned vowelCnt = 0;
while (cin >> ch) {
	switch (ch) {
		case 'a': case 'e': case 'i': case 'o': case: 'u':
		++vowelCnt;
		break;
	}
}

switch-case 语句很少不用到 break。如果不用,最好注释说明为什么不用。同时为了安全期间,推荐在 switch 语句的末尾加一个 break。这样一来,新添 case 就无须再担心跳出的问题了。

The default Label

default 标签后可以接一个默认的 case,当之前所有 case 都没有匹配的时候回选择 default case。需要注意的是 default 本身不能单独存在;如果 default case 不作任何事情,至少需要在 case 中添加一个空语句(或者一个空的 block)来保证语法的正确性。

设置 default case(即便是空的),是向之后的阅读者表明我们考虑到了默认的情况。

Switch 中变量的定义

考虑以下代码:

case true:
	string file_name;
	int ival = 0;
	int jval;
	break;
case false:
	jval = next_num();
	if(file_name.empty()) {/* ... */}
当 case 是 false 的时候会发生什么情况? 首先下结论,这种写法在 C++ 中是被禁止的。case 的跳转会直接 Bypass 掉变量的初始化,导致接下来的被使用的变量都是未初始化的。书上对此下了定义:

It is illegal to jump from a place where a variable with an initializer is out of scope to a place where that variable is in scope.

但实际的学习过程中,有一些问题需要解答:

第一个疑问: switch 语句中的 scope 到底是怎么界定的?

答:以 curly brace 来决定。一般来说,switch 后面跟了一对 curly brace 代表了只存在一个 scope。任何 case 都属于这个 scope,都没有自己独立的 scope。

第二个疑问:既然 case 没有独立的 scope,那么 switch 语句是怎么让某一个 case 单独执行的?

答:与 goto 类似的机制。case 相当于标签,整个过程实际是跳转到同一个 scope 的某个地方。

第三个疑问:书上的解答不是说从 scope 外的初始化点往 scope 里的 variable 处跳转是违法的吗?为什么你上面说只有一个 scope呢?

答:这里的 scope,并不是指代的 switch 的 scope,而是代表了变量的生存周期的 scope。从变量声明的概念可以得知,变量的生存周期是从变量声明的那一刻开始,到变量所在 scope 结束为止。因此,非法的跳转实际上意味着从变量的生存周期外部,跳转到了变量的生存周期内部

按照书上的例子,如果执行了 false,那么跳转点就直接进入了变量 file_nameivaljval 的生存周期中了。按照 C++ 标准的定义:

If transfer of control enters the scope of any automatic variables (e.g. by jumping forward over a declaration statement), the program is ill-formed (cannot be compiled).



那什么是 automatic variables? 总的来说,在函数中声明的变量,其生存周期并不都是从程序开始到程序结束的。C++ 通过存储期Storage Duration)来实现这个概念。我们这里遇到变量赋予被赋予了自动存储期Automatic Storage Duration),因此被称为 automatic variables。这种类型的变量指在变量声明的的时候就创建了其对象,而执行到包含该声明的 block 的结尾(也就是例子中 switch scope 的边界),该对象就会消失。可以看出来的是,file_nameivaljval 都符合上述的定义。

第四个疑问:为什么 jval 就可以? 为什么带了初始化的 ival 就不行?

答:上面的 C++ 标准之后还有一些例外:

- scalar types declared without initializers
- class types with trivial default constructors and trivial destructors declared without initializers
- cv-qualified versions of one of the above
- arrays of one of the above

ival jval 都属于 scalar type,因为 ival 进行了初始化,所以是非法的。

Refs:

解决上述问题的方式是显式的指定 case 自己的 scope:

case true:
{
// ok: declaration statement within a statement block
	string file_name = get_file_name();
// ...
}
	break;
case false:
	if (file_name.empty()) // error: file_name is not in scope

迭代语句

while

while 循环的条件可以由表达式或者已经初始化的变量声明来充当,比如:

int  = 1;
if (i) {//using a declared variable as the condition。
    statment;
}

while 循环的条件中可以定义变量。该变量会在每次迭代开始的时候创建,结束的时候销毁

while 的常见用法

第一种常见用法是循环读取数据:

while (cin >> var) {
    //do sth
}
第二种用法是在 loop 结束之后读取 loop 中的循环控制变量的值:
vector<int> v;
int i;
while(cin >> i)
	v.push_back(i);
//find the first neative element
auto beg = v.begin();
while (beg != v. end() && *beg >= 0)
	++beg;
if (beg = v.end()) //all element in v >= 0
beg 状态就可以得知循环的状态。

For 语句

对于不确定循环的情况来说,用while是很好的。但如果我们希望指定循环多少次,那么我们可以用for。
传统的for由三部分组成,中间用分号分开:

for (init-statement ; condition ; expression)
    statment;
init-statement 可以是声明语句,可以是表达式语句,可以是空语句。

For 的执行流程
  1. 初始化 init statement
  2. 测试条件,如果测试不通过,直接结束循环
  3. 测试通过,执行,循环到条件不成立是结束。
在 for 头部添加多个定义

For 的初始语句可以定义多个变量。但因为语句只有一句,因此要保证所有被定义的变量类型相同

For 头部中可以省略的部分

For 头部中,无论是初始化语句、条件语句,还是表达式语句,都是可以省略的:

  • 不需要初始化的时候可以省略初始化语句,比如在循环外部已经完成了循环的初始化;
  • 省略条件语句意味着条件永远为真;正常情况下需要在循环体内添加退出代码。
  • 省略表达式意味着需要在条件语句中推动循环。比如 cin » i,就可以通过完成所有输入来结束循环。

Range for

在C++11中,我们有了 for 的另外一种表达形式:

for (declaration : expression)
     statments;
这里的expression必须是一个序列(List初始化的序列 or 数组 or string or vector)。declaration 定义一个变量来表示序列里每一个元素。为了保证这个变量和元素的类型一致,我们可以用auto来定义变量。如果我们想对这些元素进行写操作,那么变量类型必须是对应的引用类型。

实际上 range for 等同于:
for (auto beg = v.begin(); beg != v.end(); ++beg) {}
因此 range for 是不能用于为 vector (或者其他容器)添加 元素的,因为元素的个数从一开始就被迭代器定下来了。

Do while

Do / while 会先运行一次循环体,再进行条件的测试

do
	statement
while (condtion);

do / while 后面需要跟 semicolon。

  • do while 要求条件不能为空
  • 条件使用的变量必须在外部定义

跳转语句

break / continue

break 会直接跳出最近的循环,而 continue 只会跳出最近循环的当次迭代。对于不同类型的循环,continue 有不同的执行方式:

  • switch:当 switch 内嵌于某个循环体的时候,才可以使用 contintue.
  • while / do while:continue 会先验证条件,再进行下一次循环。
  • tranditional for:continue 从 expression statement 开始下一轮循环。
  • range for:contintue 会在下一轮循环开始之前初始化循环变量。

goto

goto 提供了一种执行的跳转。这种跳转是无条件的,其跳转的起始点和终点在同一个函数内。 goto 使用 label 进行跳转:

goto label;
label 指代 label statement,由 identifier + colon 组成:
identifier: statement;
label statement 中的 indentifier 是独立的,因此可以与其他标识符公用一个名字;与其他重名的实体也互不影响。

goto 语句的合法使用遵循与 switch 语句相同的准则,即跳转点不能进入变量的 scope

Try Blocks 和异常处理

异常处理分为两个部分:

  • 检测部分,只能检测并报警,在处理模块作用后停止
  • 处理部分,负责处理问题

C++中的异常处理由3部分组成:

  1. throw 表达式,检测异常,并提供信号(抛出异常)。
  2. try / catch: 处理异常。try block 负责执行异常代码,并在 catch 中寻找处理方案。
  3. exception class. 用于抛出异常 (throw) 和 处理异常 (try-catch) 之间传递异常的具体信息。

A throw Expression

throw 的书写方式:

throw  expression;
该表达式的内容决定 trhrow 会抛出什么样的异常。比如:
throw runtime_error("Data must refer to same ISBN");
runtime_error 是 STL 的一种异常类型,定义于 stdexcept。该类型需要一个字符串初始化,用于提供问题的额外信息。

The Try Block

try block 的写法由一个 try block 和 一堆 catch 语句组成:

try {
	program..
}
catch (exception-declaration) {
	handler..
}
catch (exception-declaration) {
	handler..
} //.....
try 会根据异常类型选择 catch。 catch 对异常进行定义,然后运行对应 block 里的处理内容。try / catch 互为独立的 scope,在其内部声明的变量对外不可见。

异常处理的顺序

我们在复杂程序里经常可以遇到一种情况:程序在抛出异常之前,已经执行了很多个try,比如嵌套的try-catch语句。那么检测到异常后,应该如何处理? 异常处理的顺序刚好和整个过程相反,整个过程是一个从内到外的过程:

  1. 异常被抛出的时候,首先搜索抛出异常的函数。
  2. 如果没有找到该函数相对应的 catch 语句,则终止该函数,并在调用该函数的函数中寻找。
  3. 如果还是没有找到,终止这个函数,接着搜索调用他的函数。
  4. 以此类推,按程序执行的顺序逐层回退,直到找到相应的 catch。
  5. 如果最终没有找到对应的 catch,那么会执行 STL 函数 terminate,保证程序停止运行。

总结一下就是:

  • 解决方案总没有问题多
  • 自己处理不了的事交给上级处理
  • 都处理不了就关门 (terminate)
关于 Exception Safety

什么是 exception satety? 在程序抛出异常的时候,程序多半是处于进行中的状态,参与运算的某些资源可能只处理了一部分。Exception safety 指在这种情况下如何去清理、保护或者恢复这些资源。

STL 异常类

C++ 中定义了一组标准类来传递异常的信息。这些异常类被分别定义在 4 个头文件中:



stdexcept 里定义了一些详细的异常类,大概分为 Runtime_errorLogic_error 两部分,详情见下图:



STL 异常类支持的操作
  • 支持创建、拷贝以及互相的赋值。
  • 只允许默认初始化的类对象:exceptionbad_allocbad_cast
  • 使用 string / c-string 初始化的类对象:其余的异常类,初始化值作为额外的信息使用。
  • 操作:what 函数,查询额外信息使用。不接受任何参数,返回指向 c-string 的指针类型,例如下:

try{
    if(condition){
        throw exception("oh its an error!") // the string is what what_function got.
    }
}
catch(exception e){ //define object for exception class.
    cout << e.what() << endl; // using e to access exception class member function what(), print out the error string.
}