C++ 之一般概念(General Concepts)
命名空间(namespace) std
当我们采用不同的模块和程序库时,经常会出现名称冲突现象,这是因为不同模块和程序库可能针对不同的对象使用相同的标识符。namespace 可以解决这个问题,前一篇博客已有了一定介绍。namespace 可以出现在任何源码文件中,因此,可以利用 namespace 来定义一些组件,而它们可散布于多个实质模块上。这类组件的典型例子就是 C++ 标准程序库,因为 C++ 标准程序库使用了一个 namespace。事实上,C++ 标准程序库中的所有标识符都被定义于一个名为 std 的 namespace 中。
由于 namespace 的概念,使用 C++ 标准程序库的任何标识符时,有三种选择:
1. 直接指定标识符。例如:std::ostream
,而不是ostream
。完整语句类似这样:std::cout <<std::hex <<3.14 <<std::endl;
2. 使用 using declaration。例如,我们增加以下片段,便不用在写出作用域修饰符 std::,而可以直接使用 cout 和 endl:using std::cout; using std::endl;
,然后上面一条语句可以写成这样:cout <<std::hex <<3.14 <<endl;
3. 使用 using directive,这是最简便的选择。如果对 namespace std 采用 using directive,便可以让 std 内定义的所有标识符都有效(曝光),就好像它们被声明为全局标识符一样。因此,写下using namespace std;
之后,就可以直接写cout <<hex <<3.14 <<endl;
。注意,由于某些晦涩的重载(overloading)规则,在复杂的程序中,这种方式可能导致意外的命名冲突,更糟糕的是导致不一样的行为。如果场合不够明确(例如在头文件、模块或程序库中),就应该避免使用 using directive。
头文件(headers)的名称与格式
将 C++ 标准程序库中所有标识符都定义于 namespace std 里面,这种做法是标准化过程中引入的。这个做法不具有向下兼容性,因为原来的 C/C++ 头文件都将 C++ 标准程序库的标识符定义于全局范围(global scope)。既然有必要重新定义标准头文件的名称,正好借此机会把头文件的扩展名加以规范化。于是,便有了现在的标准头文件扩展名:根本就没有扩展名。于是,便有了这样的语句#include <iostream>
,这种写法也适用于 C 标准头文件,但必须采用前缀字符 c,而不再是扩展名 .h:#include <cstdlib> // was: <stdlib.h>
。在这些头文件中,每一个标识符都被定义于 namespace std。这种命名方式的优点之一是可以区分旧文件中的 char* C
函数,和新文件中的标准 C++ string class
:#include <string> // C++ class string
;#include <cstring> // char* functions from C
。为了向下兼容 C,旧的 C 标准头文件仍然有效。
注意:从操作系统角度看,新头文件命名方式并不意味着标准头文件没有扩展名。标准头文件的 #include 该如何处理,由编译器决定。一般而言,为自己写的头文件加上一个良好的扩展名,有助于轻易识别出这些文件的性质。
错误(error)和异常(exception)处理的一般概念
C++ 标准程序库由不同的成分构成。来源不同,设计的实现风格迥异,而错误处理和异常处理正是体现这种差异的体现。标准程序库中有一部分,例如 string class,支持具体的错误处理,他们会检查所有可能发生的错误,并与错误发生时抛出异常。至于其它部分如 STL 和 valarrays,效率重于安全,因此几乎不检查逻辑错误,并且只在执行期(runtime)发生错误时才抛出异常。
Standard Exception Classes(标准异常类别)
语言本身或标准程序库所抛出的所有异常,都派生自基类 exception。这是其它数个标准异常类别的基类,它们共同构成一个类体系。这些标准异常类可分为三组:
1. Language support(语言本身支持的异常)
2. Logic errors(逻辑错误)
3. Runtime errors(运行时错误)
我们常常可以避免逻辑错误,因为逻辑错误在程序作用域范围内,例如前提违例(precondition violation)1。由于运行时错误不在程序作用域范围内,很难被避免。
Exception Classes for Language Support(语言本身支持的异常类)
此类异常常用于支撑某些语言特性,所以,从某种角度来说,它们不是标准程序库的一部分,而是核心语言的一部分。如果以下操作失败,就会抛出这一类异常。
- 运行时,当一个加诸与 reference 身上的”动态类型转换操作“失败时,dynamic_cast 会抛出一个 bad_cast 异常。
- 运行期间类型辨识过程中,如果交给 typeid 的参数为零或者空指针,typeid 操作符就会抛出 bad_typeid 异常。
- 如果发生非预期的异常, bad_exception 异常会接手处理。当函数抛出异常规格(exception specification)以外的异常,但是,C++ 11 不赞成使用。
Exception Classes for Logic Errors(逻辑错误异常类)
C++ 标准程序库总是派生自 logic_error。从理论上讲,我们能够通过一些手段,在程序中避免逻辑错误——例如对函数参数进行额外测试等等。
- invalid_argument 表示无效参数,例如将 bitset(array of bits)以 char 而非 ‘0’ 或 ‘1’ 进行初始化。
- length_error 指出某个行为“可能超越了最大极限”,例如对某个字符串附加太多字符。
- out_of_range 指出参数值“不在预期范围内”,例如在诸如 array 的容器或字符串 string 中采用一个错误索引。
domain_error 指出专业领域范畴内的错误。
一般来说,逻辑错误类被定义在
<stdexcept>
中,但是,类future_error
被定义在<future>
。
Exception Class for Runtime Errors(运行时错误异常类)
派生自 runtime_error 的异常,用来指出“不在程序范围内,且不容易回避的事件”。C++ 标准库提供了以下 runtime errors:
- range_error 指出内部计算时发生区间错误(range error)
- overflow_error 指出算术运算发生上溢位(overflow)
- underflow_error 指出算术运算发生下溢位(underflow)
- system_error 指出底层操作系统的错误,例如并发环境(context of concurrency)
- bad_alloc 会在操作符 new 操作失败时抛出这个异常(若采用 new 的 nothrow 版本,另当别论)。由于这个异常可能于任何时间在任何比较复杂的程序中发生,所以可以说是最重要的一个异常
- bad_weak_ptr 在创建一个弱共享指针失败时被抛出
- bad_function_call 在调用函数封装对象却没有目标时抛出
Exceptions Thrown by the Standard Library(标准程序库所抛出的异常)
C++ 标准程序库可以抛出几乎所有的异常,然而,由于标准程序库会用到语言特性及客户所写的程序代码,所以也可能间接抛出任何异常。尤其是,无论何时分配存储空间,都有可能抛出 bad_alloc 异常。
标准程序库的任何具体实作品,都可能提供额外的异常类别(或作为兄弟类别,或派生为子类别)。使用这些非标准类别将导致程序难以移植,因为一旦采用其它标准程序库版本,就不得不痛苦地修改程序。所以,最好使用标准异常。
异常类别的头文件
异常类定义在许多不同的头文件,因此,若要处理这些程序库抛出的异常,需要包含这些头文件
#include <exception> // for classes exception and bad_exception
#include <stdexcept> // for most logic and runtime error classes
#include <system_error> // for system errors
#include <new> // for out-of-memory exceptions
#include <ios> // for I/O exceptions
#include <future> // for errors with async() and futures
#include <typeinfo> // for bad_cast and bad_typeid
异常类别成员
为了在 catch 子句中处理异常,必须采用异常所提供的接口。所有标准异常接口只含有一个成员函数:waht(),用于获取“类型本身以外的附加信息”。它返回一个以 null 结束的字符串。
抛出标准异常
你可以在自己的程序库或程序内部抛出某些标准异常,允许我们运用各个标准异常,生成时都只需要一个 string 参数,它将成为被 what() 返回的描述字符串。
从标准异常类别中派生新类别
另一个在程序中采用标准异常类别的可能情况是,定义一个直接或间接派生自 exception 的特定异常类别。要这么做吗,首先必须要确保 what() 机制正常运作。what() 是个虚拟函数,所以提供 what() 的方法之一就是自己实现 what()。
- 前提违例(precondition violation):指调用一个实际上没有包含函数对象的函数对象包装类,很像试图用一个空的函数指针调用函数,并抛出一个 bad_function_call 异常。 ↩