17.1.1
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销。异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。
异常与指针
指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割(第 15.3.1节),只抛出基类部分。
17.1.2 栈展开
当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。
为局部对象调用析构函数
如果局部对象是类类型的,就自动调用该对象的析构函数。通常,编译器不撤销内置类型的对象。
17.1.3
找到的 catch 不必是与异常最匹配的那个catch, 相反, 将选中第一个找到的可以处理该异常的 catch。 因此, 在 catch 子句列表中,最特殊的 catch 必须最先出现。
除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
- 允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw可以与指定接受 const 引用的 catch 匹配
- 允许从派生类型型到基类类型的转换
- 将数组转换为指向数组类型的指针, 将函数转换为指向函数类型的适当指针
带有因继承而相关的类型的多个 catch 子句,必须从最低派生类类到最高派生类型排序。
17.1.4 重新抛出
catch 可能确定该异常必须由函数调用链中更上层的函数来处理, catch 可以通过重新抛出将异常传递函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个 throw: throw;
空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。
被抛出的异常是原来的异常对象,而不是 catch 形参。当 catch 形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型, 而不是 catch 形参的静态类型。 例如, 来自带基类类型形参 catch的重新抛出,可能实际抛出一个派生类型的对象。
catch 可以改变它的形参。在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变:
catch (my_error &eObj) { // specifier is a reference type
eObj.status = severeErr; // modifies the exception object
throw; // the status member of the exception object is severeErr
} catch (other_error eObj) { // specifier is a nonreference type
eObj.status = badErr; // modifies local copy only
throw; // the status member of the exception rethrown is unchanged
}
捕获所有异常的 catch 子句:
// matches any exception that might be thrown
catch (...) {
// place our code here
}
如果 catch(...) 与其他 catch 子句结合使用,它必须是最后一个,否则,任何跟在它后面的 catch 子句都将不能被匹配。
为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数 try块:
template <class T> Handle<T>::Handle(T *p)
try : ptr(p), use(new size_t(1))
{
// empty function body
} catch(const std::bad_alloc &e)
{ handle_out_of_memory(e); }
17.1.9
auto_ptr 只能用于管理从 new 返回的一个对象
auto_ptr 对象只能保存一个指向对象的指针,并且不能用于指向动态分配的数组,使用 auto_ptr 对象指向动态分配的数组会导致未定义的运行时行为。
接受指针的构造函数为 explicit(第 12.4.4 节)构造函数,所以必须使用初始化的直接形式来创建 auto_ptr 对象:
// error: constructor that takes a pointer is explicit and can't be used implicitly
auto_ptr<int> pi = new int(1024);
auto_ptr<int> pi(new int(1024)); // ok: uses direct initialization
将基础对象的所有权从原来的 auto_ptr 对象转给副本,原来的 auto_ptr 对象重置为未绑定状态。与其他复制或赋值操作不同,auto_ptr 的复制和赋值改变右操作数,因此,赋值的左右操作数必须都是可修改的左值。 因为复制和赋值是破坏性操作,所以 auto_ptrs 不能将auto_ptr 对象存储在标准容器中。标准库的容器类要求在复制或赋值之后两个对象相等,auto_ptr 不满足这一要求。
对未绑定的 auto_ptr 对象解引用, 其效果与对未绑定的指针解引用相同——程序出错并且没有定义会发生什么:
*p_auto = 1024; // error: dereference auto_ptr that doesn't point to an object
auto_ptr 类型没有定义到可用作条件的类型的转换,相反,要测试auto_ptr 对象,必须使用它的 get 成员,该成员返回包含在 auto_ptr 对象中的基础指针:
// error: cannot use an auto_ptr as a condition
if (p_auto)
*p_auto = 1024;
// revised test to guarantee p_auto refers to an object
if (p_auto.get())
*p_auto = 1024;
auto_ptr 对象与内置指针的另一个区别是,不能直接将一个地址(或者其他指针)赋给 auto_ptr 对象必须调用 reset 函数来改变指针:
p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr
// revised test to guarantee p_auto refers to an object
if (p_auto.get())
*p_auto = 1024;
else
// reset p_auto to a new object
p_auto.reset(new int(1024));
auto_ptr缺陷:
- 不要使用 auto_ptr 对象保存指向静态分配对象的指针,否则,当 auto_ptr 对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。
- 永远不要使用两个 auto_ptrs 对象指向同一对象, 导致这个错误的一种明显方式是, 使用同一指针来初始化或者 reset 两个不同的 auto_ptr 对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset另一个 auto_ptr 对象。
- 不要使用 auto_ptr 对象保存指向动态分配数组的指针。当auto_ptr 对象被删除的时候, 它只释放一个对象——它使用普通delete 操作符,而不用数组的 delete [] 操作符。
- 不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后, 两个对象必须具有相同值, auto_ptr 类不满足这个要求。
定义异常说明:
异常说明跟在函数形参表之后。一个异常说明在关键字 throw 之后跟着一个(可能为空的)由圆括号括住的异常类型列表:
void recoup(int) throw(runtime_error);
空说明列表指出函数不抛出任何异常:
void no_problem() throw();
异常说明是函数接口的一部分, 函数定义以及该函数的任意声明必须具有相同的异常说明。如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。
基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限(基类中的异常列表是虚函数的派生类版本可以抛出的异常列表的超集 )。
可以在函数指针的定义中提供异常说明:
void (*pf)(int) throw(runtime_error);
两个指针的异常说明不必相同,但是,源指针(=右边)的异常说明必须至少与目标指针(=左边)的一样严格:
void recoup(int) throw(runtime_error);
// ok: recoup is as restrictive as pf1
void (*pf1)(int) throw(runtime_error) = recoup;
// ok: recoup is more restrictive than pf2
void (*pf2)(int) throw(runtime_error, logic_error) = recoup;
// error: recoup is less restrictive than pf3
void (*pf3)(int) throw() = recoup;
// ok: recoup is more restrictive than pf4
void (*pf4)(int) = recoup;
17.2.1
命名空间定义以关键字 namespace 开始,后接命名空间的名字。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。
每个命名空间是一个作用域
命名空间外部的代码必须指出名字定义在哪个命名空间中:
cplusplus_primer::Query q =cplusplus_primer::Query("hello");
q.display(cout);
// ...
命名空间可以是不连续的
命名空间定义可以不连续意味着,可以用分离的接口文件和实现文件构成命名空间:
// ---- Sales_item.h ----
namespace cplusplus_primer {
class Sales_item { /* ... */};
Sales_item operator+(const Sales_item&,
const Sales_item&);
// declarations for remaining functions in the Sales_item
interface
}
// ---- Query.h ----
namespace cplusplus_primer {
class Query {
public:
Query(const std::string&);
std::ostream &display(std::ostream&) const;
// ...
};
class Query_base { /* ... */};
}
// ---- Sales_item.cc ----
#include "Sales_item.h"
namespace cplusplus_primer {
// definitions for Sales_item members and overloaded operators
}
// ---- Query.cc ----
#include "Query.h"
namespace cplusplus_primer {
// definitions for Query members and related functions
}
在命名空间内部定义的函数可以使用同一命名空间中定义的名字的简写形式:
namespace cplusplus_primer {
// members defined inside the namespace may use unqualified names
std::istream&
operator>>(std::istream& in, Sales_item& s)
{
// ...
}
在命名空间定义的外部定义命名空间成员,用类似于在类外部定义类成员的方式:
// namespace members defined outside the namespace must use qualified names
cplusplus_primer::Sales_item
cplusplus_primer::operator+(const Sales_item& lhs,
const Sales_item& rhs)
{
Sales_item ret(lhs);
// ...
}
一旦看到完全限定的函数名,就处于命名空间的作用域中。因此, 形参表和函数体中的命名空间成员引用可以使用非限定名引用 Sales_item。
只有包围成员声明的命名空间可以包含成员的定义。例如,operator+ 既可以定义在命名空间 cplusplus_primer 中, 也可以定义在全局作用域中,但它不能定义在不相关的命名空间中。
定义在全局作用域的名字(在任意类、函数或命名空间外部声明的名字)是定义在全局命名空间中的。全局命名空间是隐式声明的,存在于每个程序中。在全局作用域定义实体的每个文件将那些名字加到全局命名空间。可以用作用域操作符引用全局命名空间的成员。因为全局命名空间是隐含的,它没有名字,所以记号::member_name引用全局命名空间的成员。
17.2.2
嵌套命名空间中的名字遵循常规规则:外围命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽。
namespace cplusplus_primer {
namespace QueryLib {
class Query { /* ... */ };
}
namespace Bookstore {
class Item_base { /* ... */ };
class Bulk_item : public Item_base { /* ... */ };
}
}
嵌套命名空间中成员的名字由外围命名空间的名字和嵌套命名空间的名字构成:cplusplus_primer::QueryLib::Query
17.2.3
未命名的命名空间在定义时没有给定名字。未命名的命名空间以关键字 namespace 开头, 接在关键字 namespace 后面的是由花括号定界的声明块。 未命名的命名空间与其他命名空间不同:
- 未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。未命名的命名空间用于声明局部于文件的实体。
- 在未命名的命名空间中定义的变量在程序开始时创建,在程序结束之前一直存在未命名的命名空间中定义的名字可直接使用,不能使用作用域操作符来引用未命名的命名空间的成员。
- 未命名的命名空间中定义的名字只在包含该命名空间的文件中可见。如果另一文件包含一个未命名的命名空间,两个命名空间不相关。两个命名空间可以定义相同的名字,而这些定义将引用不同的实体。
- 未命名空间中定义的名字可以在定义该命名空间所在的作用域中找到。如果在文件的最外层作用域中定义未命名的命名空间,那么,未命名的空间中的名字必须与全局作用域中定义的名字不同:
int i; // global declaration for i
namespace {
int i;
}
// error: ambiguous defined globally and in an unnested, unnamed
namespace
i = 10;
namespace local {
namespace {
int i;
}
}
// ok: i defined in a nested unnamed namespace is distinct from
global i
local::i = 42;
如果头文件定义了未命名的命名空间,那么,在每个包含该头文件的文件中,该命名空间中的名字将定义不同的局部实体。
17.2.4
除了在函数或其他作用域内部,头文件不应该包含using 指示或 using 声明。在其顶级作用域包含using 指示或 using 声明的头文件,具有将该名字注入包含该头文件的文件中的效果。头文件应该只定义作为其接口的一部分的名字,不要定义在其实现中使用的名字。
一个 using 声明(using std::map; )一次只引入一个命名空间成员,它使得无论程序中使用哪些名字,都能够非常明确。using 声明中引入的名字遵循常规作用域规则。从 using 声明点开始,直到包含 using 声明的作用域(及其嵌套作用域 )的末尾,名字都是可见的。外部作用域中定义的同名实体被屏蔽。
using指示:与 using 声明不同,using 指示无法控制使得哪些名字可见——它们都是可见的。
using 声明将名字直接放入出现 using 声明的作用域 ,using 指示具有将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域的效果!!!声明是相当于直接在作用域内定义了所声明的,而指示是将所指示的作用域中所用成员看作是定义在指示出现作用域刚出现之前的外层作用域中,但其作用域是在声明或指示点之后。(关于这个问题可参见这两篇博文using声明、指示,以及C++primer第四版习题17.21)
由 using 指示引起的二义性错误只能在使用处检测, using声明引起的二义性错误在声明点而不是使用点检测,因此更容易发现和修正。
命名空间中的名字查找:
namespace A {
int i;
int k;
class C1 {
public:
C1(): i(0), j(0) { } // ok: initializes C1::i and C1::j
int f1()
{
return k; // returns A::k
}
int f2()
{
return h; // error: h is not defined
}
int f3();
private:
int i; // hides A::i within C1
int j;
};
int h = i; // initialized from A::i
}
// member f3 is defined outside class C1 and outside namespace A
int A::C1::f3()
{
return h; // ok: returns A::h
}
f2 中的 return 语句将不能编译, 它试图引用命名空间 A 中的名字 h, 但 h 还没有定义。如果使 A 中的名字在 C1 的定义之前定义,h 的使用就是合法的。类似地, f3 内部对 h 的使用是正确的, 因为 f3 定义在已经定义了 A::h 之后。
这段程序使用了 std::string 类型,但它不加限制地引用了 getline 函数。
std::string s;
// ok: calls std::getline(std::istream&, const std::string&)
getline(std::cin, s);
接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。当编译器看到 getline 函数的使用getline(std::cin, s);的时候,它在当前作用域,包含调用的作用域以及定义 cin 的类型和string 类型的命名空间中查找匹配的函数。因此,它在命名空间 std 中查找并找到由 string 类型定义的 getline 函数。
当一个类声明友元函数(第 12.5 节)的时候,函数的声明不必是可见的(必须先定义包含成员函数的类,才能将成员函数设为友元。另一方面,不必预先声明类和非成员函数来将它们设为友元)。如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。
namespace A {
class C {
friend void f(const C&); // makes f a member of namespace A
};
}
因为该友元接受类类型实参并与类隐式声明在同一命名空间中,所以使用它时可以无须使用显式命名空间限定符:
// f2 defined at global scope
void f2()
{
A::C cobj;
f(cobj); // calls A::f
}
17.2.6
命名空间对函数匹配有两个影响。一个影响是明显的:using 声明或 using指示可以将函数加到候选集合。另一个是类形参函数查找。
为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。将那些命名空间中带有匹配名字的函数加入候选集合:
namespace NS {
class Item_base { /* ... */ };
void display(const Item_base&) { }
}
// Bulk_item's base class is declared in namespace NS
class Bulk_item : public NS::Item_base { };
int main() {
Bulk_item book1;
display(book1);
return 0;
}
display 函数的实参 book1 具有类类型 Bulk_item。display 调用的候选函数不仅是在调用 display 函数的地方其声明可见的函数,还包括声明Bulk_item 类及其基类 Item_base 的命名空间中的函数。命名空间 NS 中声明的函数 display(const Item_base&) 被加到候选函数集合中。
没有办法编写 using 声明来引用特定函数声明:
using NS::print(int); // error: cannot specify parameter list
using NS::print; // ok: using declarations specify names only
函数名字的 using 声明声明了所有具有该名字的函数。如果命名空间 NS 中有用于 int 和 double 的函数,则 NS::print 的 using 声明使得两个函数都在当前作用域中可见。
同一命名空间下的不同命名空间是平行关系,不存在名字或函数查找。
17.3.1
基类构造函数按照基类构造函数在类派生列表中的出现次序调用,总是按构造函数运行的逆序调用析构函数。
17.3.2
在单个基类情况下,派生类的指针或引用可以自动转换为基类的指针或引用,对于多重继承也是如此, 派生类的指针或引用可以转换为其任意其类的指针或引用。
在多重继承情况下,遇到二义性转换的可能性更大。编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好:
void print(const Bear&);
void print(const Endangered&);
Panda ying_yang("ying_yang");
print(ying_yang); // error: ambiguous
像单继承一样,用基类的指针或引用只能访问基类中定义(或继承)的成员,不能访问派生类中引入的成员(对于虚函数这条是特例)。
17.3.4
成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在成员的类中查找,然后依次查找每个基类。在多重继承下,查找同时检察所有的基类继承子树——在我们的例子中,并行查找Endangered 子树和 Bear/ZooAnimal 子树。如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的。当一个类有多个基类的时候,通过所有直接基类同时进行名字查找。多重继承的派生类有可能从两个或多个基类继承同名成员,对该成员不加限定的使用是二义性的。只有在存在使用该成员的二义性尝试的时候,才会出错(同using指示)。
17.3.7
通常,每个类只初始化自己的直接基类。在应用于虚基类的进修,这个初始化策略会失败。如果使用常规规则,就可能会多次初始化虚基类。在虚派生中,由最低层派生类的构造函数初始化虚基类。
虽然由最低层派生类初始化虚基类, 但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只有创建中间类型的对象时使用。当虚基类具有默认构造函数时,派生类不必为其提供显示初始化式,构造时自动调用默认构造函数。当没有默认构造函数时,报错。
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类。
当创建 Panda 对象的时候:
- 首先使用构造函数初始化列表中指定的初始化式构造 ZooAnimal 部分
- 接下来,构造 Bear 部分。忽略 Bear 的用于 ZooAnimal 构造函数初始化列表的初始化式
- 然后,构造 Raccoon 部分,再次忽略 ZooAnimal 初始化式
- 最后,构造 Panda 部分