第十七章 用于大型程序的工具
1. 异常处理
异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个. 执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw 转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中.
因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销. 异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型.
当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型.
抛出指针通常是个坏主意:无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。如果抛出指针本身,可能会引发比分割对象更严重的问题。具体而言,抛出指向局部对象的指针总是错误的,其理由与从函数返回指向局部对象的指针是错误的一样抛出指针的时候,必须确定进入处理代码时指针所指向的对象存在.
栈展开: 抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。首先检查 throw 本身是否在 try 块内部,如果是,检查与该 catch 相关的 catch 子句,看是否其中之一与抛出对象相匹配。如果找到匹配的 catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内存并撤销局部对象),并且继续在调用函数中查找. 如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的 catch 子句。如果找到匹配的 catch,就处理异常;如果找不到匹配的 catch,调用函数也退出,并且继续在调用这个函数的函数中查找. 沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行.
栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。
析构函数应该从不抛出异常:栈展开期间会经常执行析构函数。在执行析构函数的时候,已经引发了异常但还没有处理它。如果在这个过程中析构函数本身抛出新的异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出。标准库类型都保证它们的析构函数不会引发异常。
构造函数内部所做的事情经常会抛出异常。如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员。在初始化数组或其他容器类型的元素的时候,也可能发生异常,同样,也要保证将会适当地撤销已构造的元素。
如果找不到匹配的 catch,程序就调用库函数 terminate。
捕获异常:catch 子句中的异常说明符看起来像只包含一个形参的形参表,异常说明符是在其后跟一个(可选)形参名的类型名。说明符的类型决定了处理代码能够捕获的异常种类。类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。类型的前向声明不行。当 catch 为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名。
在查找匹配的 catch 期间,找到的 catch 不必是与异常最匹配的那个 catch,相反,将选中第一个找到的可以处理该异常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必须最先出现。
异常与 catch 异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许——除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
1)允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw 可以与指定接受 const 引用的 catch 匹配。
2)允许从派生类型型到基类类型的转换。
3)将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。
在查找匹配 catch 的时候,不允许其他转换。具体而言,既不允许标准算术转换,也不允许为类类型定义的转换。
进入 catch 的时候,用异常对象初始化 catch 的形参。像函数形参一样,异常说明符类型可以是引用。异常对象本身是被抛出对象的副本。是否再次将异常对象复制到 catch 位置取决于异常说明符类型。如果说明符不是引用,就将异常对象复制到 catch 形参中,如果说明符是引用,则像引用形参一样,不存在单独的 catch 对象,catch 形参只是异常对象的另一名字。对 catch 形参所做的改变作用于异常对象。
异常说明符的静态类型决定 catch 子句可以执行的动作。如果被抛出的异常对象是派生类类型的,但由接受基类类型的 catch 处理,那么,catch 不能使用派生类特有的任何成员。
如果 catch 形参是引用类型,catch 对象就直接访问异常对象,catch 对象的静态类型可以与 catch 对象所引用的异常对象的动态类型不同。如果异常说明符不是引用,则 catch 对象是异常对象的副本,如果 catch 对象是基类类型对象而异常对象是派生类型的,就将异常对象分割为它的基类子对象。对象(相对于引用)不是多态的。
catch 子句的次序必须反映类型层次:因为 catch 子句按出现次序匹配,所以使用来自继承层次的异常的程序必须将它们的 catch 子句排序,以便 派生类型的处理代码出现在其基类类型的 catch 之前。
重新抛出:有可能单个 catch 不能完全处理一个异常。在进行了一些校正行动之后,catch 可能确定该异常必须由函数调用链中更上层的函数来处理,catch 可以通过重新抛出将异常传递函数调用链中更上层的函数。重新抛出是后面不跟类型或表达式的一个 throw:
throw;
空 throw 语句将重新抛出异常对象,它只能出现在 catch 或者从 catch 调用的函数中。如果在处理代码不活动时碰到空 throw,就调用 terminate 函数。
虽然重新抛出不指定自己的异常,但仍然将一个异常对象沿链向上传递,被抛出的异常是原来的异常对象,而不是 catch 形参。当 catch 形参是基类类型的时候,我们不知道由重新抛出表达式抛出的实际类型,该类型取决于异常对象的动态类型,而不是 catch 形参的静态类型。catch 可以改变它的形参。在改变它的形参之后,如果 catch 重新抛出异常,那么,只有当异常说明符是引用的时候,才会传播那些改变。
捕获所有异常的处理代码:捕获所有异常的 catch 子句形式为 (...)。例如:
// matches any exception that might be thrown
catch (...) {
// place our code here
}
如果 catch(...) 与其他 catch 子句结合使用,它必须是最后一个,否则,任何跟在它后面的 catch 子句都将不能被匹配。
函数测试块与构造函数:异常可能发生在构造函数中,或者发生在处理构造函数初始化式的时候。在进入构造函数函数体之前处理构造函数初始化式,构造函数函数体内部的 catch 子句不能处理在处理构造函数初始化时可能发生的异常。为了处理来自构造函数初始化式的异常,必须将构造函数编写为函数 try 块。可以使用函数测试块将一组 catch 子句与函数联成一个整体。作为例子,可以将第十六章的 Handle 构造函数包装在一个用来检测 new 中失败的测试块当中:
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); }
关键字 try 出现在成员初始化列表之前,并且测试块的复合语句包围了构造函数的函数体。catch 子句既可以处理从成员初始化列表中抛出的异常,也可以处理从构造函数函数体中抛出的异常。
异常类层次:exception 类型所定义的唯一操作是一个名为 what 的虚成员,该函数返回 const char* 对象,它一般返回用来在抛出位置构造异常对象的信息。
用类管理资源分配:可能存在异常的程序以及分配资源的程序应该使用类来管理那些资源。异常安全意味着,即使发生异常,程序也能正确操作。在这种情况下,“安全”来自于保证“如果发生异常,被分配的任何资源都适当地释放“通过定义一个类来封闭资源的分配和释放,可以保证正确释放资源。这一技术常称为“资源分配即初始化”,简称 RAII。
class Resource {
public:
Resource(parms p): r(allocate(p)) { }
~Resource() { release(r); }
// also need to define copy and assignment
private:
resource_type *r; // resource managed by this type
resource_type *allocate(parms p); // allocate this resource
void release(resource_type*); // free this resource
};
Resource 类是分配资源和回收资源的类型,它保存表示该资源的数据成员。Resource 的构造函数分配资源,而析构函数释放它。当使用这个类的时候
void fcn()
{
Resource res(args); // allocates resource_type
// code that might throw an exception
// if exception occurs, destructor for res is run automatically
// ...
} // res goes out of scope and is destroyed automatically
自动释放资源。如果函数正常终止,就在 Resource 对象超出作用域时释放资源;如果函数因异常而提早退出,编译器就运行 Resource 的析构函数作为异常处理过程的一部分。
auto_ptr 类:标准库的 auto_ptr 类是异常安全的“资源分配即初始化”技术的例子。auto_ptr 类是接受一个类型形参的模板,它为动态分配的对象提供异常安全。auto_ptr 类在头文件 memory 中定义。auto_ptr 只能用于管理从 new 返回的一个对象,它不能管理动态分配的数组。
当 auto_ptr 被复制或赋值的时候,有不寻常的行为,因此,不能将 auto_ptrs 存储在标准库容器类型中。auto_ptr 对象只能保存一个指向对象的指针,并且不能用于指向动态分配的数组,使用 auto_ptr 对象指向动态分配的数组会导致未定义的运行时行为。每个 auto_ptr 对象绑定到一个对象或者指向一个对象。当 auto_ptr 对象指向一个对象的时候,可以说它“拥有”该对象。当 auto_ptr 对象超出作用域或者另外撤销的时候,就自动回收 auto_ptr 所指向的动态分配对象。
如果通过常规指针分配内存,而且在执行 delete 之前发生异常,就不会自动释放该内存:
void f()
{
int *ip = new int(42); // dynamically allocate a new object
// code that throws an exception that is not caught inside f
delete ip; // return the memory before exiting
}
如果在 new 和 delete 之间发生异常,并且该异常不被局部捕获,就不会执行 delete,则永不回收该内存。
如果使用一个 auto_ptr 对象来代替,将会自动释放内存,即使提早退出这个块也是这样:
void f()
{
auto_ptr<int> ap(new int(42)); // allocate a new object
// code that throws an exception that is not caught inside f
}
// auto_ptr freed automatically when function ends
auto_ptr 是可以保存任何类型指针的模板。在最常见的情况下,将 auto_ptr 对象初始化为由 new 表达式返回的对象的地址:
auto_ptr<int> pi(new int(1024));
接受指针的构造函数为 explicit构造函数,所以必须使用初始化的直接形式来创建 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
pi 所指的由 new 表达式创建的对象在超出作用域时自动删除。如果 pi 是局部对象,pi 所指对象在定义 pi 的块的末尾删除;如果发生异常,则 pi 也超出作用域,析构函数将自动运行 pi 的析构函数作为异常处理的一部分;如果 pi 是全局对象,就在程序末尾删除 pi 引用的对象。
auto_ptr 的主要目的,在保证自动删除 auto_ptr 对象引用的对象的同时,支持普通指针式行为。
auto_ptr 对象的复制和赋值是破坏性操作:auto_ptr 和内置指针对待复制和赋值有非常关键的重要区别。当复制 auto_ptr 对象或者将它的值赋给其他 auto_ptr 对象的时候,将基础对象的所有权从原来的 auto_ptr 对象转给副本,原来的 auto_ptr 对象重置为未绑定状态。
auto_ptr<string> ap1(new string("Stegosaurus"));
// after the copy ap1 is unbound
auto_ptr<string> ap2(ap1); // ownership transferred from ap1 to ap2
当复制 auto_ptr 对象或者对 auto_ptr 对象赋值的时候,右边的 auto_ptr 对象让出对基础对象的所有职责并重置为未绑定的 auto_ptr 对象之后,在上例中,删除 string 对象的是 ap2 而不是 ap1,在复制之后,ap1 不再指向任何对象。与其他复制或赋值操作不同,auto_ptr 的复制和赋值改变右操作数,因此,赋值的左右操作数必须都是可修改的左值。
除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象——假如两个对象不同。通常自身赋值没有效果。
auto_ptr<string> ap3(new string("Pterodactyl"));
// object pointed to by ap3 is deleted and ownership transferred from ap2 to ap3;
ap3 = ap2; // after the assignment, ap2 is unbound
将 ap2 赋给 ap3 之后:
1)删除了 ap3 指向的对象。
2)将 ap3 置为指向 ap2 所指的对象。
3)ap2 是未绑定的 auto_ptr 对象。
auto_ptr 类型没有定义到可用作条件的类型的转换,相反,要测试 auto_ptr 对象,必须使用它的 get 成员,该成员返回包含在 auto_ptr 对象中的基础指针:
// revised test to guarantee p_auto refers to an object
if (p_auto.get())
*p_auto = 1024;
应该只用 get 询问 auto_ptr 对象或者使用返回的指针值,不能用 get 作为创建其他 auto_ptr 对象的实参:在任意时刻只有一个 auto_ptrs 对象保存给定指针,如果两个 auto_ptrs 对象保存相同的指针,该指针就会被 delete 两次。
auto_ptr 对象与内置指针的另一个区别是,不能直接将一个地址(或者其他指针)赋给 auto_ptr 对象:
p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr
相反,必须调用 reset 函数来改变指针:
// 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 对象的 reset 函数时,在将 auto_ptr 对象绑定到其他对象之前,会删除 auto_ptr 对象所指向的对象(如果存在)。但是,正如自身赋值是没有效果的一样,如果调用该 auto_ptr 对象已经保存的同一指针的 reset 函数,也没有效果,不会删除对象。
auto_ptr 缺陷:要正确地使用 auto_ptr 类,必须坚持该类强加的下列限制:
1)不要使用 auto_ptr 对象保存指向静态分配对象的指针,否则,当 auto_ptr 对象本身被撤销的时候,它将试图删除指向非动态分配对象的指针,导致未定义的行为。
2)永远不要使用两个 auto_ptrs 对象指向同一对象,导致这个错误的一种明显方式是,使用同一指针来初始化或者 reset 两个不同的 auto_ptr 对象。另一种导致这个错误的微妙方式可能是,使用一个 auto_ptr 对象的 get 函数的结果来初始化或者 reset 另一个 auto_ptr 对象。
3)不要使用 auto_ptr 对象保存指向动态分配数组的指针。当 auto_ptr 对象被删除的时候,它只释放一个对象——它使用普通 delete 操作符,而不用数组的 delete [] 操作符。
4)不要将 auto_ptr 对象存储在容器中。容器要求所保存的类型定义复制和赋值操作符,使它们表现得类似于内置类型的操作符:在复制(或者赋值)之后,两个对象必须具有相同值,auto_ptr 类不满足这个要求。
异常说明:异常说明指定,如果函数抛出异常,被抛出的异常将是包含在该说明中的一种,或者是从列出的异常中派生的类型。异常说明跟在函数形参表之后。一个异常说明在关键字 throw 之后跟着一个(可能为空的)由圆括号括住的异常类型列表:
void recoup(int) throw(runtime_error);
空说明列表指出函数不抛出任何异常:
void no_problem() throw();
异常说明是函数接口的一部分,函数定义以及该函数的任意声明必须具有相同的异常说明。如果一个函数声明没有指定异常说明,则该函数可以抛出任意类型的异常。
违反异常说明:如果函数抛出了没有在其异常说明中列出的异常,就调用标准库函数 unexpected。默认情况下,unexpected 函数调用 terminate 函数,terminate 函数一般会终止程序。
因为不能在编译时检查异常说明,异常说明的应用通常是有限的。
在 const 成员函数声明中,异常说明跟在 const 限定符之后:
virtual const char* what() const throw();
异常说明与析构函数:
class isbn_mismatch: public std::logic_error {
public:
virtual ~isbn_mismatch() throw() { }
};
isbn_mismatch 类从 logic_error 类继承而来,logic_error 是一个标准异常类,该标准异常类的析构函数包含空 throw() 说明符,它们承诺不抛出任何异常。当继承这两个类中的一个时,我们的析构函数也必须承诺不抛出任何异常。isbn_mismatch 类有两个 string 类成员,这意味着 isbn_mismatch 的合成析构函数调用 string 析构函数。C++ 标准保证,string 析构函数像任意其他标准库类析构函数一样,不抛出异常。但是,标准库的析构函数没有定义异常说明,在这种情况下,我们知道,但编译器不知道,string 析构函数将不抛出异常。我们必须定义自己的析构函数来恢复析构函数不抛出异常的承诺。
异常说明与虚函数:基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。
class Base {
public:
virtual double f1(double) throw ();
virtual int f2(int) throw (std::logic_error);
virtual std::string f3() throw
(std::logic_error, std::runtime_error);
};
class Derived : public Base {
public:
// error: exception specification is less restrictive than Base::f1's
double f1(double) throw (std::underflow_error);
// ok: same exception specification as Base::f2
int f2(int) throw (std::logic_error);
// ok: Derived f3 is more restrictive
std::string f3() throw ();
};
函数指针的异常说明:异常说明是函数类型的一部分。这样,也可以在函数指针的定义中提供异常说明:
void (*pf)(int) throw(runtime_error);
该函数只能抛出 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;
2. 命名空间
命名空间的定义: 命名空间名字后面接着由花括号括住的一块声明和定义,可以在命名空间中放入可以出现在全局作用域的任意声明:类、变量(以及它们的初始化)、函数(以及它们的定义)、模板以及其他命名空间. 命名空间作用域不能以分号结束. 命名空间的名字在定义该命名空间的作用域中必须是唯一的。命名空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义.
命名空间定义以关键字 namespace 开始,后接命名空间的名字。
namespace cplusplus_primer {
class Sales_item { /* ... */};
Sales_item operator+(const Sales_item&,const Sales_item&);
class Query {
public:
Query(const std::string&);
std::ostream &display(std::ostream&) const;
// ...
};
}
在命名空间中定义的名字可以被命名空间中的其他成员直接访问,命名空间外部的代码必须指出名字定义在哪个命名空间中.
总是使用限定名引用命名空间成员可能非常麻烦。可以编写 using 声明来获得对我们知道将经常使用的名字的直接访问:
using cplusplus_primer::Query;
命名空间可以是不连续的:命名空间可以在几个部分中定义。命名空间由它的分离定义部分的总和构成,命名空间是累积的。一个命名空间的分离部分可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。如果命名空间的一个部分需要定义在另一文件中的名字,仍然必须声明该名字。
编写命名空间定义:
namespace namespace_name {
// declarations
}
如果名字 namespace_name 不是引用前面定义的命名空间,则用该名字创建新的命名空间,否则,这个定义打开一个已存在的命名空间,并将这些新声明加到那个命名空间。
接口和实现的分离:
// ---- 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
}
// ---- user.cc ----
// defines the cplusplus_primer::Sales_item class
#include "Sales_item.h"
int main()
{
// ...
cplusplus_primer::Sales_item trans1, trans2;
// ...
return 0;
}
定义命名空间成员:在命名空间内部定义的函数可以使用同一命名空间中定义的名字的简写形式(无需命名空间前缀)。
也可以在命名空间定义的外部定义命名空间成员:
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
引用全局命名空间的成员。
嵌套命名空间:一个嵌套命名空间即是一个嵌套作用域——其作用域嵌套在包含它的命名空间内部。嵌套命名空间中的名字遵循常规规则:外围命名空间中声明的名字被嵌套命名空间中同一名字的声明所屏蔽。嵌套命名空间内部定义的名字局部于该命名空间。外围命名空间之外的代码只能通过限定名引用嵌套命名空间中的名字:cplusplus_primer::QueryLib::Query。
未命名的命名空间:未命名的命名空间在定义时没有给定名字。未命名的命名空间以关键字 namespace 开头,接在关键字 namespace 后面的是由花括号定界的声明块。未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。每个文件有自己的未命名的命名空间。在未命名的命名空间中定义的变量在程序开始时创建,在程序结束之前一直存在。未命名的命名空间中定义的名字可直接使用,毕竟,没有命名空间名字来限定它们。不能使用作用域操作符来引用未命名的命名空间的成员。
未命名空间中定义的名字可以在定义该命名空间所在的作用域中找到。如果在文件的最外层作用域中定义未命名的命名空间,那么,未命名的空间中的名字必须与全局作用域中定义的名字不同:
int i; // global declaration for i
namespace {
int i;
}
// error: ambiguous defined globally and in an unnested, unnamed namespace
i = 10;
在 C 语言中,声明为 static 的局部实体在声明它的文件之外不可见。在C++中应该避免文件静态而使用未命名空间代替。
using 声明:一个 using 声明一次只引入一个命名空间成员,它使得无论程序中使用哪些名字,都能够非常明确:
using std::map;
using std::pair;
using std::size_t;
using 声明中引入的名字遵循常规作用域规则。从 using 声明点开始,直到包含 using 声明的作用域的末尾,名字都是可见的。外部作用域中定义的同名实体被屏蔽。一旦该作用域结束了,就必须使用完全限定名。using 声明可以出现在全局作用域、局部作用域或者命名空间作用域中。类作用域中的 using 声明局限于被定义类的基类中定义的名字。
命名空间别名:namespace primer = cplusplus_primer;
一个命名空间可以有许多别名,所有别名以及原来的命名空间名字都可以互换使用。
using 指示:像 using 声明一样,using 指示使我们能够使用命名空间名字的简写形式。与 using 声明不同,using 指示无法控制使得哪些名字可见——它们都是可见的。
using 指示以关键字 using 开头,后接关键字 namespace,再接命名空间名字。如果该名字不是已经定义的命名空间名字,就会出错。
using 指示具有将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域的效果。
namespace blip {
int bi = 16, bj = 15, bk = 23;
// other declarations
}
int bj = 0; // ok: bj inside blip is hidden inside a namespace
void manip()
{
// using directive - names in blip "added" to global scope
using namespace blip;
// clash between ::bj and blip::bj
// detected only if bj is used
++bi; // sets blip::bi to 17
++bj; // error: ambiguous
// global bj or blip::bj?
++::bj; // ok: sets global bj to 1
++blip::bj; // ok: sets blip::bj to 16
int bk = 97; // local bk hides blip::bk
++bk; // sets local bk to 98
}
blip 的成员看来好像是在定义 blip 和 manip 的作用域中定义的一样。如果在全局作用域中定义 blip,则 blip 的成员看来好像是声明在全局作用域的一样。因为名字在不同的作用域中,manip 内部的局部声明可以屏蔽命名空间的某些成员名字。对 manip 而言,blip 成员 bj 看来好像声明在全局作用域中,但是,全局作用域存在另一名为 bj 的对象。这种冲突是允许的,但为了使用该名字,必须显式指出想要的是哪个版本。
类内部所定义的成员可以使用出现在定义文本之后的名字。当类包在命名空间中的时候,确定类成员定义中使用的名字:首先在成员中找,然后在类(包括基类)中找,再在外围作用域中找,外围作用域中的一个或多个可以是命名空间:
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
}
除了定义的成员例外,总是向上查找作用域:名字在使用之前必须声明。如果使 A 中的名字在 C1 的定义之前定义,h 的使用就是合法的。类似地,f3 内部对 h 的使用是正确的,因为 f3 定义在已经定义了 A::h 之后。
可以从函数的限定名推断出查找名字时所检查作用域的次序,限定名以相反次序指出被查找的作用域。限定符 A::C1::f3 指出了查找类作用域和命名空间作用域的相反次序,首先查找函数 f3 的作用域,然后查找外围类 C1 的作用域。在查找包含 f3 定义的作用之前,最后查找命名空间 A 的作用域。
屏蔽命名空间名字规则的一个重要例外:接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一命名空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的。其原因在于,允许无须单独的 using 声明就可以使用概念上作为类接口组成部分的非成员函数。能够使用非成员操作对操作符函数特别有用:
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 函数。
std::string s;
cin >> s;
如果没有查找规则的这个例外,我们将必须编写下面二者之一:
using std::operator>>; // need to allow cin >> s
std::operator>>(std::cin, s); // ok: explicitly use std::>>
隐式友元声明与命名空间:当一个类声明友元函数的时候,函数的声明不必是可见的。如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在命名空间内部定义,则没有另外声明的友元函数在同一命名空间中声明。
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
}
有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的命名空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个命名空间,将那些命名空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。将那些命名空间中带有匹配名字的函数加入候选集合:
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 声明声明一个名字。如果命名空间内部的函数是重载的,那么,该函数名字的 using 声明声明了所有具有该名字的函数。由 using 声明引入的函数,重载出现 using 声明的作用域中的任意其他同名函数的声明。如果 using 声明在已经有同名且带相同形参表的函数的作用域中引入函数,则 using 声明出错,否则,using 定义给定名字的另一重载实例,效果是增大候选函数集合。
重载与 using 指示:using 指示将命名空间成员提升到外围作用域。如果命名空间函数与命名空间所在的作用域中声明的函数同名,就将命名空间成员加到重载集合中:
namespace libs_R_us {
extern void print(int);
extern void print(double);
}
void print(const std::string &);
// using directive:
using namespace libs_R_us;
// using directive added names to the candidate set for calls to print:
// print(int) from libs_R_us
// print(double) from libs_R_us
// print(const std::string &) declared explicitly
void fooBar(int ival)
{
print("Value: "); // calls global print(const string &)
print(ival); // calls libs_R_us::print(int)
}
命名空间与模板:在命名空间内部声明模板影响着怎样声明模板特化:模板的显式特化必须在定义通用模板的命名空间中声明,否则,该特化将与它所特化的模板不同名。有两种定义特化的方式:一种是重新打开命名空间并加入特化的定义,可以这样做是因为命名空间定义是不连续的;或者,可以用与在命名空间定义外部定义命名空间成员相同的方式来定义特化:使用由命名空间名字限定的模板名定义特化。为了提供命名空间中所定义模板的自己的特化,必须保证在包含原始模板定义的命名空间中定义特化。
3. 多重继承与虚继承
class Bear : public ZooAnimal {
};
class Panda : public Bear, public Endangered {
};
派生类的构造函数可以在构造函数初始化式中给零个或多个基类传递值:
// explicitly initialize both base classes
Panda::Panda(std::string name, bool onExhibit)
: Bear(name, onExhibit, "Panda"),
Endangered(Endangered::critical) { }
构造函数初始化式只能控制用于初始化基类的值,不能控制基类的构造次序。基类构造函数按照基类构造函数在类派生列表中的出现次序调用。总是按构造函数运行的逆序调用析构函数。
派生类的指针或引用可以转换为其任意其类的指针或引用。
编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好。
void print(const Bear&);
void print(const Endangered&);
Panda ying_yang("ying_yang");
print(ying_yang); // error: ambiguous
基于指针类型或引用类型的查找:当一个类继承于多个基类的时候,那些基类之间没有隐含的关系,不允许使用一个基类的指针访问其他基类的成员。
假定所有根基类都将它们的析构函数适当定义为虚函数,那么,无论通过哪种指针类型删除对象,虚析构函数的处理都是一致的:
// each pointer points to a Panda
delete pz; // pz is a ZooAnimal*
delete pb; // pb is a Bear*
delete pp; // pp is a Panda*
delete pe; // pe is a Endangered*
析构函数调用的次序是构造函数次序的逆序:通过虚机制调用 Panda 析构函数。随着 Panda 析构函数的执行,依次调用 Endangered、Bear 和 ZooAnimal 的析构函数。
如果具有多个基类的类定义了自己的析构函数,该析构函数只负责清除派生类。如果派生类定义了自己的复制构造函数或赋值操作符,则类负责复制(赋值)所有的基类子部分。只有派生类使用复制构造函数或赋值操作符的合成版本,才自动复制或赋值基类部分。
假定 Panda 类使用默认复制控制成员。ling_ling 的初始化
Panda ying_yang("ying_yang"); // create a Panda object
Panda ling_ling = ying_yang; // uses copy constructor
使用默认复制构造函数调用 Bear 复制构造函数,Bear 复制构造函数依次在执行 Bear 复制构造函数之前运行 ZooAnimal 复制构造函数。一旦构造了 ling_ling 的 Bear 部分,就运行 Endangered 复制构造函数来创建对象的那个部分。最后,运行 Panda 复制构造函数。
在多重继承下,成员函数中使用的名字和查找首先在函数本身进行,如果不能在本地找到名字,就继续在成员的类中查找,然后依次查找每个基类。在多重继承下,查找同时检察所有的基类继承子树。如果在多个子树中找到该名字,则那个名字的使用必须显式指定使用哪个基类;否则,该名字的使用是二义性的
首先发生名字查找:即使两个继承的函数有不同的形参表,也会产生错误。类似地,即使函数在一个类中是私有的而在另一个类中是公用或受保护的,也是错误的。最后,如果在 ZooAnimal 类中定义了 print 而Bear 类中没有定义,调用仍是错误的。名字查找总是以两个步骤发生:首先编译器找到一个匹配的声明(或者找到两个匹配的声明,这导致二义性),然后,编译器才确定所找到的声明是否合法
可以通过指定使用哪个类解决二义性:
ying_yang.Endangered::print(cout);
避免潜在二义性最好的方法是,在解决二义性的派生类中定义函数的一个版本。例如,应该给选择使用哪个 print 版本的 Panda 类一个 print 函数:
std::ostream& Panda::print(std::ostream &os) const
{
Bear::print(os); // print the Bear part
Endangered::print(os); // print the Endangered part
return os;
}
虚继承:一个类继承多个直接基类的时候,那些类有可能本身还共享另一个基类。在这种情况下,中间类可以选择使用虚继承,声明愿意与层次中虚继承同一基类的其他类共享虚基类。用这种方法,后代派生类中将只有一个共享虚基类的副本。
istream 和 ostream 类对它们的基类进行虚继承。通过使基类成为虚基类,istream 和 ostream 指定,如果其他类(如 iostream 同时继承它们两个,则派生类中只出现它们的公共基类的一个副本。通过在派生列表中包含关键字 virtual 设置虚基类:
class istream : public virtual ios { ... };
class ostream : virtual public ios { ... };
// iostream inherits only one copy of its ios base class
class iostream: public istream, public ostream { ... };
即使基类是虚基类,也照常可以通过基类类型的指针或引用操纵派生类的对象。
虚基类成员的可见性:假定通过多个派生路径继承名为 X 的成员,有下面三种可能性:
1)如果在每个路径中 X 表示同一虚基类成员,则没有二义性,因为共享该成员的单个实例。
2)如果在某个路径中 X 是虚基类的成员,而在另一路径中 X 是后代派生类的成员,也没有二义性——特定派生类实例的优先级高于共享虚基类实例。
3)如果沿每个继承路径 X 表示后代派生类的不同成员,则该成员的直接访问是二义性的。
像非虚多重继承层次一样,这种二义性最好用在派生类中提供覆盖实例的类来解决。
特殊的初始化语义:通常,每个类只初始化自己的直接基类。如果使用常规规则,就可能会多次初始化虚基类。类将沿着包含该虚基类的每个继承路径初始化。为了解决这个重复初始化问题,从具有虚基类的类继承的类对初始化进行特殊处理。在虚派生中,由最低层派生类的构造函数初始化虚基类。虽然由最低层派生类初始化虚基类,但是任何直接或间接继承虚基类的类一般也必须为该基类提供自己的初始化式。只要可以创建虚基类派生类类型的独立对象,该类就必须初始化自己的虚基类,这些初始化式只有创建中间类型的对象时使用。
如果 Panda 构造函数不显式初始化 ZooAnimal 基类,就使用 ZooAnimal 默认构造函数;如果 ZooAnimal 没有默认构造函数,则代码出错。
无论虚基类出现在继承层次中任何地方,总是在构造非虚基类之前构造虚基类:
class Character { /* ... */ };
class BookCharacter : public Character { /* ... */ };
class ToyAnimal { /* ... */ };
class TeddyBear : public BookCharacter,
public Bear, public virtual ToyAnimal
{ /* ... */ };
TeddyBear 的虚基类的构造次序是先 ZooAnimal 再 ToyAnimal。一旦构造了虚基类,就按声明次序调用非虚基类的构造函数:首先是 BookCharacter,它导致调用 Character 构造函数,然后是 Bear。
在这里,由最低层派生类 TeddyBear 指定用于 ZooAnimal 和 ToyAnimal 的初始化式。
在合成复制构造函数中使用同样的构造次序,在合成赋值操作符中也是按这个次序给基类赋值。保证调用基类析构函数的次序与构造函数的调用次序相反。