C++学习笔记6:语句

作者: 云中布衣   分类:  学习笔记    热度: (381℃)   时间: 2017-7-2 19:15   标签: #始于C++而不止于C++    

C++语言中的语句类似于自然语言中的句子。C++语言既有只完成单一任务的简单语句,也有作为一个单元执行的由一组语句组成的复合语句。和大多数语言一样,C++也提供了实现条件分支结构的语句以及重复执行同一段代码的循环结构。通常情况下,语句是循序执行的。但是除了最简单的程序外,只有循序执行往往不足够。为此C++定义了一组控制流语句,允许有条件地执行或者重复地执行某部分功能。if和switch语句提供了条件分支结构,而for、while和do while语句则支持重复执行的功能。后几种语句常称为循环或者迭代语句。

1.简单语句

C++中,大多数语句以分号结束。例如像ival+5这样的表达式,在后面加上分号,就是一条表达式语句。表达式语句用于计算表达式。但执行下面的语句:

ival +5; // expression statement
却没有任何意义,因为计算出来的结果没有赋值或其他用途。通常,表达式语句所包含的表达式在计算时会影响程序的状态,使用赋值、自增、输入或输出操作符的表达式就是很好的例子。

程序语句最简单的形式是空语句,它使用以下形式(只有一个单独的分号):

; // null statement
如果在程序的某个地方,语法上需要一个语句,但逻辑上并不需要,此时应该使用空语句。这种用法常见于在循环条件判断部分就能完成全部循环工作的情况下。

2.声明语句

在C++中,对象或类的定义或声明也是语句。尽管定义语句这种说法语序更准确些,但定义语句经常被称为声明语句。

3.复合语句(块)

复合语句,通常被称为块,是用一对花括号括起来的语句序列(也可能是空的)。块标识了一个作用域,在块中引入的名字只能在该块内部或嵌套在块中的子块里访问。通常,一个名字只能从其定义处到该块的结尾这段范围内可见。复合语句用在语法规则要求使用单个语句但程序逻辑却需要不止一个语句的地方。例如,while或for语句的循环体必须是单个语句。然而,大多数情况都需要在循环体里执行多个语句。因而可使用一对花括号将语句序列括起来,使其成为块语句。与其他大多数语句不同,块并不是以分号结束。

像空语句一样,程序员也可以定义空块,用一对内部没有语句的花括号实现:

while(cin >> s && s != sought)
    { } // empty block
4.语句作用域

在语句的控制结构中定义的变量,仅在定义它们的块语句结束前有效。这种变量的作用域限制在语句体内。通常,语句体本身就是一个语句块,其中也可能包含了其他的块。一个在控制结构里引入的名字是该语句的局部变量,其作用域局限在语句的内部。如果程序需要访问某个控制结构中的变量,那么这个变量必须在控制语句外部定义。对于在控制语句中定义的变量,限制其作用域的一个好处是,这些变量名可以重复使用而不必担心它们的当前值在每一次使用时是否正确。对于作用域外的变量,是不可能用到其在作用域内的残留值的。

通俗的讲,语句块里面的变量屏蔽语句块外部的变量,子块的内部变量屏蔽外层语句块的变量。

5.if语句

if语句根据特定表达式是否为真来有条件地执行另一个语句。if语句有两种形式,一种带else分支而另一种没有。根据语法结构最简单的if语句如下,其中condition部分必须用圆括号括起来,它可以是一个表达式或者一个初始化声明。

if(condition) 
         statement

当使用带else分支的if语句时,需要注意可能会产生悬垂else的情况。

if (minVal < ivec[i]){ } //empty block
else if (minVal == ivec[i]) ++occurs;
else {                       //minVal > ivec[i]
    minVal = ivec[i];
    occurs = 1;

}
上面的三路测试精确的控制了所有的情况。然后简单的把前两个情况用一个嵌套if语句实现就会产生问题。
//oops: incorrect rewrite: This code won't work!
if (minVal < ivec[i]){ } //empty block
     if (minVal == ivec[i]) ++occurs;
else {                       //minVal > ivec[i]
    minVal = ivec[i];
    occurs = 1;

}
上述代码表明if语句存在着其潜在的二义性。这种情况往往成为悬垂else问题,产生于一个语句包含的if子句多于else子句时:对于每一个else,究竟他们归属于哪个if语句。因此有些编程风格建议总是在if后面使用花括号。这样做可以避免日后修改代码时产生混乱和错误。

6.switch语句

深层嵌套的if else语句往往在语法上是正确的,但逻辑上却没有 能够正确地反映程序员的意图。例如错误的else if匹配很容易被忽略。添加新的条件和逻辑关系,或者对语句做其他修改都很难保证正确。switch语句提供了一种更方便的方法来实现深层嵌套的if/else逻辑。下面给一个示例,用来统计五个元音在文本里分别出现的次数。

char ch;
//initialize counters for each vowel
int aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0,otherCnt = 0;
while(cin >> ch){
// if ch is a vowel, increment the approprite counter
    switch(ch){\
        case 'a':
            ++aCnt;
            break;
        case 'e':
            ++eCnt;
            break;
        case 'i':
            ++iCnt;
            break;
        case 'o':
            ++oCnt;
            break;
        case 'u':
            ++uCnt;
            break;
        default:
            ++otherCnt;
            break;

        }

cout << "aCnt:\t"<<aCnt<<'\n';
cout << "eCnt:\t"<<eCnt<<'\n';
cout << "iCnt:\t"<<iCnt<<'\n';
cout << "oCnt:\t"<<oCnt<<'\n';
cout << "uCnt:\t"<<uCnt<<'\n';
cout << "otherCnt:\t"<<otherCnt<<'\n';
}
通过对括号内表达式的值与其后列出的关键字做比较,实现switch语句的功能。表达式必须产生一个整数的结果,其值与每个case的值比较。关键字case和它所关联的值称为case标号。每个case标号的值都必须是一个常量表达式,除此之外还有一个特殊的case标号-default标号提供了相当于else子句的功能,如果所有的case标号与switch表达式的值不匹配,并且default标号存在,则执行default标号后面的语句。而break语句用来中断当前的控制流,对于switch来讲,break语句将控制跳出当前switch,继续执行switch语句后面的第一个语句。在这里,就是把控制转移给while。如果没有break语句,程序会从当前匹配的case标号开始,跨越case边界执行后面的语句直到switch结束。

switch求解的表达式可以非常复杂。特别是,该表达式也可以定义和初始化一个变量,其值要与后面每个case标号作比较,变量始终存在于整个switch语句中,在switch结构外该变量就不再有效了。对于switch结构,只能够在它的最后一个case标号或default标号后面定义变量,制定这个规则是为了避免出现代码跳过变量的定义和初始化的情况。如果我们要为某个特殊的case定义变量,则可以引入语句块,在该语句块中定义变量,从而保证这个变量在使用前被定义和初始化。

case true:
      {
             //ok :declaration statement within a statement block
             string file_name = get_file_name();
             //...
      }
      break;

case false:
      // ...
7.while语句

当条件为真时,while语句反复执行目标语句。它的语法形式如下:

while (condition)
        statement
只要条件condition的值为true,执行语句statement(通常为一个块语句),循环条件condition可以是一个表达式,或者提供初始化的变量定义,但是不能为空。在循环条件中定义的变量在每次循环里都要经历创建和撤销的过程。
// arr1 is an array of ints
int *source = arr1;
size_t sz = sizeof(arr1)/sizeof(*arr1); // number of elements
int *dest = new int[sz];
while(*source != arr1 + sz){
    *dest++ = *source++; // copy element and increment points
}
8.for循环语句

for语句的语法形式如下:

for(init-statement condition; expression)
        statement
init-statement(初始化语句)必须是声明语句、表达式语句或空语句。这些语句都以分号结束,因此其语法形式也可以看成:
for(initializer; condition;expression)
        statement
for语句头中,可以省略init-statement(初始化语句)、condition(循环条件)或者expression(表达式)中的任何一个(或全部)。但是分号不能省去!我们可以在for语句的init-statement中定义多个对象;但不管怎么样,该出只能出现一个语句,因此所有的对象必须具有相同的一般类型。

9. do while循环语句

do while语句类似于while语句,但do while循环语句它能保证循环体至少执行一次。与while语句不同的是,do while语句总是以分号结束。其语句的语法形式如下:

do 
    statement
while (condition);
需要注意的是在循环体内部定义的变量,其作用域就被限制在while前的右括号之前,任何在循环条件中引用的变量都必须在do 语句之前就已经存在了。

10.break语句

break语句用于结束最近的while、do while、for或switch语句,并将程序的执行权传递给紧接在被终止语句之后的语句。需要注意的是break只能出现在循环或switch结构中,或者出现在嵌套在循环或switch结构中的语句里。当break出现在嵌套的switch或循环语句中时,将会终止里层的switch或循环语句,而外层的switch或循环不受影响。

11.continue语句

continue语句导致最近的循环语句的当前迭代提前结束。对于while和do while语句,继续求解循环条件。而对于for循环,程序流程接着求解for语句头中的expression表达式。continue语句只能出现在for、while或者do while循环中,包括嵌套在这些循环内部的块语句中。

12.goto语句

goto语句提供了函数内部的无条件跳转,实现从goto语句跳转到同一函数内某个带标号的语句。goto语句的语法规则如下:

goto label;
其中label是用于标识带标号的语句的标识符。在任何语句前提供一个标识符和冒号,即得带标号的语句(labeled statement)。其语法规则如下:
end: return;  //labeled statement, may be target of a goto
形成标号(label)的标识符只能用作goto的目标。因为这个原因,标号标识符可以与变量名以及程序里的其他标识符一样,不与别的标识符重名。goto语句和获得所转移的控制权的带标号的语句必须位于同一个函数内。

goto语句不能跨越变量的定义语句向前跳转,但是向后跳过已经执行的变量定义语句则是合法的。为什么?向前跳过未执行的变量定义语句,意味着变量可能在没有定义的情况下使。向后跳回到一个变量定义之前,则会使系统撤销这个变量,然后再重新创建它。如果确实需要在goto和其跳转对应的标号之间定义变量,则定义必须放在一个块语句中。

// ...
goto end; // ok:jumps to a point where ix is not defined
{
   int ix = 10;
   // ... code using ix
}

end:n// ix no longer visible here

13.try块和异常处理

异常就是运行时出现的不正常,例如运行时耗尽了内存或遇到意外的非法输入。异常存在于程序的正常功能之外,并要求程序立即处理。在设计良好的系统中,异常是程序错误处理的一部分。当程序检查到无法处理的问题时,异常处理就特别的有用。在这些情况下,检测出问题的那部分程序,需要一种方法把控制权转到可以处理这个问题的那部分程序。错误检测程序还必须指出具体出现了什么问题,并且可能需要提供一些附加信息。

异常机制提供程序中错误检测与错误处理部分之间的通信。C++的异常处理中包括:

1)throw表达式(throw expression),错误检测部分使用这种表达式来说明遇到了不可处理的错误,可以说,throw引发(raise)了异常条件。throw表达式的类型决定了所抛出异常的类型。

2)try块(try block),错误处理部分使用它来处理异常。try语句块以try关键字开始,并一个或多个catch子句(catch clause)结束。在try块中执行的代码所抛出(throw)的异常,通常会被其中一个catch子句处理。由于它们“处理”异常,catch子句也称为处理代码(handler)。每个catch子句包括三部分:关键字catch,圆括号内单个类型或者单个对象的声明-称为异常说明符(exception specifier),以及通常用花括号括起来的语句块。如果选择了一个catch子句来处理异常,则执行相关的块语句。一旦catch子句执行结束,程序流程立即继续执行紧随着最后一个catch子句的语句。

try{
    program-statements
}catch(exception-specifier){
    handler-statements
}catch(exception-specifier){
    handler-statements
}// ...

try语句内的programs-statements形成程序的正常逻辑。这里面可以包含任意的C++语句,包括变量的声明。与其他块语句一样,try块引入局部作用域,在try块中声明的变量,包括catch语句中声明的变量,不能在try外面引用。

C++异常处理机制中寻找处理代码的过程与函数调用链刚好相反。抛出一个异常时,首先要搜索的是抛出异常的函数。如果没有找到匹配的catch,则终止这个函数的执行,并在调用这个函数的函数中寻找相配的catch。如果仍然没有找到相应的处理代码,该函数同样要终止,搜索调用它的函数。如此类推,继续按执行路径回退,直到找到适当类型的catch为止。如果不存在处理该异常的catch子句,程序的运行就要跳转到名为terminate的标准库函数,该函数在exception头文件中定义。这个标准库函数的行为依赖于系统,通常情况下,它的执行将导致程序非正常退出。

3)由标准库定义的一组异常类(exception class),用来在throw和相应的catch之间传递有关的错误信息。程序员可在自己年写的程序中使用这些标准异常类。标准库异常类定义在4个头文件中:

exception头文件定义了最常见的异常类,它的类名是exception。这个类通常只通知异常的产生,但不会提供更多的信息。
stdexcept头文件定义了几种常见的异常类。
new头文件中定义了bad_alloc异常类型,提供因无法分配内存而由new抛出的异常。
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
逻辑错误:使用一个超出有效范围的值
标准库异常类只提供了很少的操作,包括创建、复制异常类对象以及异常类型对象的赋值。exception、bad_alloc以及bad_cast类型只定义了默认构造函数,无法在创建这些类型对象时为它们提供初值。其他的异常类型则只定义了一个使用string初始化式的构造函数。此外每一个标准库异常类都定义了名为what的成员函数。这个函数不需要任何参数,并且返回const char*类型的值。下面是一个异常处理的例子:

#include <iostream>
using namespace std;
int main(){
    int cin1,cin2,sum;
    while(cin >> cin1 >> cin2){
        try{
            sum = cin1 + cin2;
            if(sum < 10){
                cout << "sum: " << sum << endl;
            }else{
                throw runtime_error("sum of cin1 and cin2 is greater than 10!");

            }
        }
        catch(runtime_error err){
            cout << err.what() << "\n Try Again?Entry y or n"<<endl;
            char c;
            cin >> c;
            if(cin && c=='n')
                break; //break out of while loop
        }

    }
    return 0;
}
14.使用预处理器进行调试

在第2篇学习笔记中,我们使用预处理变量来避免重复包含头文件。C++程序员有时候也会使用类似的技术有条件地执行用于调试的代码。程序所包含的调试代码仅在开发过程中执行。当应用程序已完成,并且准备提交时,就会将调试代码关闭。可使用NDEGUG预处理变量实现有条件的调试代码。

#include <iostream>
using namespace std;
//#define NDEBUG
int main(){
    #ifndef NDEBUG
    cerr << "staring main" <<endl;
    cerr << "Error: " << __FILE__<< ":line " << __LINE__ <<endl
         << "       Compiled on " << __DATE__ << " at " << __TIME__ <<endl;
    #endif // NDEBUG
    return 0;
}
如果NDEBUG未定义,那么程序就会将信息写到cerr中。如果NDEBUG已经定义了,那么程序执行时将会跳过#ifndef和#endif之间的代码默认情况下,NDEBUG未定义,这也就意味着必须执行#ifndef和#endif之间的代码。在开发程序的过程中,只要保持NDEBUG未定义就会执行其中的调试语句。开发完成后,要将程序交付给客户时,可通过定义NDEBUG预处理变量,(有效地)删除这些调试语句。大多数编译器都提供了NDEBUG的命令行选项:
$CC -DNDEBUG main.c
上面的命令等效于在main.c的开头提供了#define NDEBUG预处理命令。除了NDEBUG,预处理器还定义了四种在调试时非常有用的常量:__FILE__(文件名)、__LINE__(当前行号)、__TIME__(文件被编译的时间)、__DATE__(文件被编译的日期)。可使用这些常量在错误信息中提供更多的信息。

另一个常见的调试技术是使用NDEBUG预处理变量以及assert(断言)预处理宏(pregrocessor macro)。assert宏实在cassert头文件中定义的,所有使用assert的文件都必须包含这个头文件。预处理宏有点用函数调用,assert宏需要一个表达式作为它的条件:

assert(expr)
只要NDEBUG未定义,assert宏就求解条件表达式expr,如果结果为fasle,assert输出信息并终止程序的执行。如果该表达式有一个非零,则assert不做任何操作。与异常不同(异常用于处理程序执行时预期要发生的错误),程序员使用assert来测试“不可能发生”的条件。assert不能用来代替运行时的逻辑检查,也不能带起对程序可能产生的错误检测,它仅用于检查确实不可能的条件,这只是对程序的调试有帮助而已。

(完)

56.8K

发表评论:

© 云中布衣 2015 | Driven by EMLOG  | SiteMap | RunTime: 11.08ms&RSS  | MORE  |   | TOP

文章数量【258】 评论数量【238】 稳定运行【1208天】

Visitor IP Address【54.196.190.32】

Email:ieeflsyu#outlook.com