目录

  • 1 C++语言导论
    • 1.1 C++语言与问题求解
    • 1.2 人类如何求解问题
      • 1.2.1 过程化思维
      • 1.2.2 面向对象思维
      • 1.2.3 泛型思维
    • 1.3 计算机的工作原理
      • 1.3.1 计算机组成
      • 1.3.2 计算机如何执行程序
      • 1.3.3 计算机如何存储程序和数据
      • 1.3.4 关于存储的约定
    • 1.4 从人机需求到计算机语言
      • 1.4.1 机器语言(只考虑计算机的需求)
      • 1.4.2 汇编语言(考虑一点点人的需求)
      • 1.4.3 高级语言(兼顾人机需求)
    • 1.5 从人机需求到C++语言
      • 1.5.1 过程化编程
      • 1.5.2 面向对象编程
      • 1.5.3 泛型编程
  • 2 动手写个小程序
    • 2.1 求解素数判断问题
    • 2.2 程序的一般结构
    • 2.3 程序的编译和链接
    • 2.4 C++程序是字符序列
    • 2.5 C++程序是单词序列
    • 2.6 C++程序是语句序列
    • 2.7 C++程序是函数集合
    • 2.8 C++程序是文件集合
  • 3 C++过程化编程
    • 3.1 过程化编程的主要工作
    • 3.2 描述简单数据
      • 3.2.1 C++语言的数据类型
        • 3.2.1.1 C++语言的内置数据类型
        • 3.2.1.2 C++语言的复合数据类型
        • 3.2.1.3 定义自己的数据类型
      • 3.2.2 描述字面量
      • 3.2.3 描述变量
    • 3.3 描述数据处理过程
      • 3.3.1 数据的基本运算处理
      • 3.3.2 控制数据处理流程
      • 3.3.3 流程控制实验练习
      • 3.3.4 输入输出处理
        • 3.3.4.1 键盘输入
        • 3.3.4.2 屏幕输出
        • 3.3.4.3 控制输入输出格式
        • 3.3.4.4 文件输入输出处理
    • 3.4 制造数据处理机器:函数
      • 3.4.1 定义函数
      • 3.4.2 调用函数
      • 3.4.3 函数调用的形参生成
      • 3.4.4 函数嵌套和递归调用
      • 3.4.5 函数内联
      • 3.4.6 函数重载
      • 3.4.7 函数的默认形参值
    • 3.5 结构化编程与程序组织
      • 3.5.1 函数声明的组织方式
      • 3.5.2 程序的多文件组织
      • 3.5.3 作用域和生存期
        • 3.5.3.1 作用域
        • 3.5.3.2 生存期
      • 3.5.4 函数和变量的共享:链接性
      • 3.5.5 名字空间域
    • 3.6 描述复杂数据:复合数据类型
      • 3.6.1 常变量
      • 3.6.2 数组变量
      • 3.6.3 指针变量
        • 3.6.3.1 定义指针变量
        • 3.6.3.2 访问指针变量
        • 3.6.3.3 指针与动态变量
        • 3.6.3.4 动态数组
      • 3.6.4 描述字符串变量
      • 3.6.5 引用:给变量取个别名
      • 3.6.6 嵌套复合的数据类型
        • 3.6.6.1 常量数组与枚举类型
        • 3.6.6.2 指针数组与数组指针
        • 3.6.6.3 常量指针与指针常量
        • 3.6.6.4 多维数组和多级指针
        • 3.6.6.5 常变量、指针、数组的引用
        • 3.6.6.6 简化嵌套复合声明:typedef
      • 3.6.7 复合数据类型与函数
        • 3.6.7.1 数组与函数
        • 3.6.7.2 指针与函数
        • 3.6.7.3 引用与函数
  • 4 面向对象编程
    • 4.1 面向对象编程的主要工作
    • 4.2 描述需要什么样的对象
      • 4.2.1 定义类
      • 4.2.2 产生对象
      • 4.2.3 访问对象的成员
      • 4.2.4 描述对象的成员
    • 4.3 约定对象生死时刻的行为
      • 4.3.1 必须编写析构函数
      • 4.3.2 浅拷贝和深拷贝
      • 4.3.3 构造和析构函数的调用时机
    • 4.4 约定对象运算的行为
      • 4.4.1 友元函数和友元类
      • 4.4.2 两种重载操作符的方式比较
      • 4.4.3 重载赋值运算符
      • 4.4.4 深拷贝的赋值运算符
    • 4.5 约定类成员的常量性
      • 4.5.1 常量数据成员
      • 4.5.2 常成员函数
      • 4.5.3 常对象和常成员函数
      • 4.5.4 mutable成员
      • 4.5.5 volatile成员
    • 4.6 约定类成员的静态性
      • 4.6.1 静态数据成员
      • 4.6.2 静态成员函数
      • 4.6.3 const和static成员小结
    • 4.7 对象组合
    • 4.8 对象继承与派生
      • 4.8.1 选择继承方式
      • 4.8.2 改造基类成员
      • 4.8.3 重写构造和析构函数
      • 4.8.4 派生类与基类的赋值兼容
        • 4.8.4.1 派生类对象与基类对象的兼容性
        • 4.8.4.2 派生类对象与基类引用/指针的兼容性
        • 4.8.4.3 针对基类引用/指针进行通用编程
    • 4.9 对象行为的多态:重载和虚函数
      • 4.9.1 编译期多态:重载
      • 4.9.2 运行期多态:虚函数
        • 4.9.2.1 在类层次中定义虚函数
        • 4.9.2.2 使用虚函数实现通用输出操作符
        • 4.9.2.3 虚函数运行期绑定原理
        • 4.9.2.4 运行期绑定的优势与不足
        • 4.9.2.5 虚析构函数
        • 4.9.2.6 不产生对象的类:抽象类
        • 4.9.2.7 只有纯虚函数的类:接口类
    • 4.10 运行期类型识别RTTI
    • 4.11 异常处理机制
  • 5 泛型编程
异常处理机制

程序代码编写正确以并且编译正确,不代表程序执行就不会发生问题。例如,下面的代码同样会导致执行错误,

int main() {

    int a, b;    cin>>a>>b;

    cout << a/b << endl;              //除零问题

    return 0;

}

程序非常简单,编译没有问题。但是,在程序执行时如果输入10,就会发生除零错误,然后程序执行终止。事实上,程序在执行过程中不可避免地需要面对许多突发状况,例如,除零错误、数组越界、类型转换失败、打开文件不存在、内存耗尽等,这些往往会导致程序执行处于不正常的状态,我们称之为异常exception)。

异常会伤害程序执行的一致性,甚至导致程序执行不正常终止。此时,程序员在编写程序时应该针对异常做一些事前防御性措施或者事后补救性措施。其中,事前防御性措施,是指在异常发生之前将其消灭在萌芽状态。例如在做除法运算之前先检测除数是否为0或者接近0,若是则不进行除法运算,从而避免发生除零异常。

但是,即使我们程序员再怎么小心谨慎,再怎么事前防御,也不可能穷举所有异常状况,完全避免异常的发生。这时就要求我们在程序代码中做一些异常发生后的补救性措施,尽量是程序执行恢复到一个一致的状态,避免资源泄漏乃至程序崩溃等问题。为此,C++语言引入了异常处理(ExceptionHandling)的语法机制。

C++异常处理机制

当函数代码可能发生某种异常且暂时无法处理这种异常时,就可以抛出这种异常(即抛出异常);然后,函数调用者就可以检测是否真正发生了异常(即捕获异常),并且说明若发生了这种异常就进行什么样的补救处理(即处理异常)。

为此,C++语言的异常处理机制就提供了如下的语法工具和设施,包括,

1throw语句:用于函数抛出特定异常,通知异常发生;

2try-catch语句try用于捕获异常,catch用以处理异常。

抛出异常:throw语句

抛出异常,通过throw语句。例如,

class MyExcp {

       string  errmsg;

public:

       MyExcp(stringmsg) : errmsg(msg) {}

       string  getmsg() { return errmsg; }

};

class DivByZeroExcp : public MyExcp {

public:

    DivByZeroExcp() : MyExcp(“除数为零异常”) {}

}

class LogExcp : public MyExcp {

public:

    LogExcp() : MyExcp(“零求对数异常”) {}

}

double divide(double a, double b) {

    if ( b < 1e-5 )  throw DivByZeroExcp();   //抛出除零异常

    return a/b;

}

double log10(double a, double N) {

    if ( a<1e-5 || a ==1.0 || n < 1e-5)  throwLogExcp();       //抛出对数异常

    return log(n);

}

在这里,MyException是我们定制的异常类,有一个数据成员errmsg用于保存异常的说明信息。然后,在定义divide函数时,如果参数b等于0,就会发生除零错误,因此,我们构造一个异常对象MyException(“除数为零异常”),将其抛出。

可以看出,抛出异常的语法非常简单:throw 异常对象。其中,异常对象可以是包括内置数据类型的任意类型,通常会为抛出的异常定制相应的类。


捕获并处理异常:try-catch语句

注意,在抛出异常时我们的函数并没有处理异常,只是将错误信息保存在抛出的异常对象中。只有当未来有人调用divide函数时,发现divide函数可能会引发除零错误的异常,他就可以通过try-catch语句来捕获并处理这种异常。例如,

int main() {

    int a, b; cout<<"请输入两个整数:"<<endl;

    while(true) {

        cin>>a>>b;

        try{              //捕获异常

           cout<< divide(a, b) << endl;//可能会产生异常

           break;

        }catch (DivByZeroExcp e) {             //处理异常

            cout<< e.getmsg() <<endl;

            cout<<"请重新输入两个整数:"<<endl;

        }

    }

    return 0;

}

main函数中,函数调用divide(a,b)可能会引发除零异常。如果输入正常,没有引发异常,则之后的break语句会跳出循环,于是程序执行正常终止。

如果输入除数为0,则divide函数会抛出除零异常。此时,try语句块首先会捕获到该除零异常;然后,异常处理机制会在try语句块之后寻找可以处理除零异常的catch语句块,也就是参数类型与异常类型相匹配的第一个catch语句块。

找到处理异常的catch语句块,程序执行流程就进入该catch语句块执行其中的代码,即处理异常,这里就是输出异常信息并提示重新输入两个整数。

错误处理完成之后,程序再次恢复到正常的执行流程,继续while循环。