C++Primer学习笔记(十三)用于大型程序的工具

本章的主题就是这些特征,即异常处理、命名空间和多重继承。

大规模应用程序往往具有下列特殊要求:
  1. 更严格的正常运转时间以及更健壮的错误检测和错误处理。错误处理经常必须跨越独立开发的               多个子系统进行。
  2. 能够用各种库(可能包含独立开发的库)构造程序。
  3. 能够处理更复杂的应用概念。

17.1. 异常处理

通过异常我们能够将问题的检测和问题的解决分离,这样程序的问题检测部分可以不必了解如何处理问题。

C++ 的异常处理中,需要由问题检测部分抛出一个对象给处理代码,通过这个对象的类型和内容,两个部分能够就出现了什么错误进行通信。

有效使用异常处理需要理解:在抛出异常时会发生什么,在捕获异常时又会发生什么,还有用来传递错误的对象的含义

抛出类类型的异常

异常是通过抛出对象而引发的。该对象的类型决定应该激活哪个处理代码。被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那个。

异常以类似于将实参传递给函数的方式抛出和捕获。异常可以是可传给非引用形参的任意类型的对象,这意味着必须能够复制该类型的对象。

执行 throw 的时候,不会执行跟在 throw 后面的语句,而是将控制从 throw转移到匹配的 catch,该 catch 可以是同一函数中局部的 catch,也可以在直接或间接调用发生异常的函数的另一个函数中。

控制从一个地方传到另一地方,这有两个重要含义:
  1. 沿着调用链的函数提早退出。第 17.1.2 节将讨论函数因异常而退出时会发生什么。
  2. 一般而言,在处理异常的时候,抛出异常的块中的局部存储不存在了。

因为在处理异常的时候会释放局部存储,所以被抛出的对象就不能再局部存储,而是用 throw 表达式初始化一个称为异常对象的特殊对象。异常对象由编译器管理,而且保证驻留在可能被激活的任意 catch 都可以访问的空间。这个对象由 throw 创建,并被初始化为被抛出的表达式的副本。异常对象将传给对应的 catch,并且在完全处理了异常之后撤销。

异常对象通过复制被抛出表达式的结果创建,该结果必须是可以复制的类型。

异常对象与继承

当抛出一个表达式的时候,被抛出对象的静态编译时类型将决定异常对象的类型。

异常与指针

用抛出表达式抛出静态类型时,比较麻烦的一种情况是,在抛出中对指针解引用。

对指针解引用的结果是一个对象,其类型与指针的类型匹配。如果指针指向继承层次中的一种类型,指针所指对象的类型就有可能与指针的类型不同。无论对象的实际类型是什么,异常对象的类型都与指针的静态类型相匹配。如果该指针是一个指向派生类对象的基类类型指针,则那个对象将被分割,只抛出基类部分。

抛出指针通常是个坏主意:抛出指针要求在对应处理代码存在的任意地方存在指针所指向的对象。

栈展开

抛出异常的时候,将暂停当前函数的执行,开始查找匹配的 catch 子句。首先检查 throw 本身是否在 try 块内部,如果是,检查与该 catch 相关的catch 子句,看是否其中之一与抛出对象相匹配。如果找到匹配的 catch,就处理异常;如果找不到,就退出当前函数(释放当前函数的内在并撤销局部对象),并且继续在调用函数中查找。

如果对抛出异常的函数的调用是在 try 块中,则检查与该 try 相关的catch 子句。如果找到匹配的 catch,就处理异常;如果找不到匹配的 catch,调用函数也退出,并且继续在调用这个函数的函数中查找。

这个过程,称之为栈展开(stack unwinding),沿嵌套函数调用链继续向上,直到为异常找到一个 catch 子句。只要找到能够处理异常的 catch 子句,就进入该 catch 子句,并在该处理代码中继续执行。当 catch 结束的时候,在紧接在与该 try 块相关的最后一个 catch 子句之后的点继续执行。

为局部对象调用析构函数

栈展开期间,释放局部对象所用的内存并运行类类型局部对象的析构函数。

如果一个块直接分配资源,而且在释放资源之前发生异常,在栈展开期间将不会释放该资源。例如,一个块可以通过调用 new 动态分配内存,如果该块因异常而退出,编译器不会删除该指针,已分配的内在将不会释放。

析构函数应该从不抛出异常

栈展开期间会经常执行析构函数。在执行析构函数的时候,已经引发了异常但还没有处理它。如果在这个过程中析构函数本身抛出新的异常,将会导致调用标准库 terminate 函数。一般而言,terminate 函数将调用 abort 函数,强制从整个程序非正常退出。标准库类型都保证它们的析构函数不会引发异常。

异常与构造函数

构造函数内部所做的事情经常会抛出异常。如果在构造函数对象的时候发生异常,则该对象可能只是部分被构造,它的一些成员可能已经初始化,而另一些成员在异常发生之前还没有初始化。即使对象只是部分被构造了,也要保证将会适当地撤销已构造的成员。

在初始化数组或其他容器类型的元素的时候,也可能发生异常,同样,也要保证将会适当地撤销已构造的元素。

未捕获的异常终止程序

不能不处理异常。异常是足够重要的、使程序不能继续正常执行的事件。如果找不到匹配的 catch,程序就调用库函数 terminate。

捕获异常

catch 子句中的异常说明符看起来像只包含一个形参的形参表,异常说明符是在其后跟一个(可选)形参名的类型名。

说明符的类型决定了处理代码能够捕获的异常种类。类型必须是完全类型,即必须是内置类型或者是已经定义的程序员自定义类型。类型的前向声明不行。

当 catch 为了处理异常只需要了解异常的类型的时候,异常说明符可以省略形参名;如果处理代码需要已发生异常的类型之外的信息,则异常说明符就包含形参名,catch 使用这个名字访问异常对象。

查找匹配的处理代码

查找匹配的 catch 期间,找到的 catch 不必是与异常最匹配的那个catch,相反,将选中第一个找到的可以处理该异常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必须最先出现。

异常与 catch 异常说明符匹配的规则比匹配实参和形参类型的规则更严格,大多数转换都不允许——除下面几种可能的区别之外,异常的类型与 catch 说明符的类型必须完全匹配:
  • 允许从非 const 到 const 的转换。也就是说,非 const 对象的 throw可以与指定接受 const 引用的 catch 匹配。
  • 允许从派生类型型到基类类型的转换。
  • 将数组转换为指向数组类型的指针,将函数转换为指向函数类型的适当指针。

在查找匹配 catch 的时候,不允许其他转换。具体而言,既不允许标准算术转换,也不允许为类类型定义的转换。

异常说明符

进入 catch 的时候,用异常对象初始化 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。

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 所指向的动态分配对象。

为异常安全的内存分配使用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 对象来代替,将会自动释放内存

auto_ptr 是可以保存任何类型指针的模板

将auto_ptr 绑定到指针

在最常见的情况下,将 auto_ptr 对象初始化为由 new 表达式返回的对象的地址:
auto_ptr<int> pi(new int(1024));

接受指针的构造函数为 explicit(第 12.4.4 节)构造函数,所以必须使用初始化的直接形式来创建 auto_ptr 对象:
// error: constructor that takes a pointer is explicit and can't beused 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 对象重置为未绑定状态。

auto_ptr<string> ap1(new string("Stegosaurus"));
// after the copy ap1 is unbound
auto_ptr<string> ap2(ap1); // ownership transferred from ap1 toap2

当复制 auto_ptr 对象或者对 auto_ptr 对象赋值的时候,右边的auto_ptr 对象让出对基础对象的所有职责并重置为未绑定的 auto_ptr 对象之后,在上例中,删除 string 对象的是 ap2 而不是 ap1,在复制之后,ap1 不再指向任何对象。

与其他复制或赋值操作不同,auto_ptr 的复制和赋值改变右操作数,因此,赋值的左右操作数必须都是可修改的左值。

赋值删除左操作数指向的对象

除了将所有权从右操作数转给左操作数之外,赋值还删除左操作数原来指向的对象——假如两个对象不同。通常自身赋值没有效果。

因为复制和赋值是破坏性操作,所以auto_ptrs 不能将auto_ptr 对象存储在标准容器中。标准库的容器类要求在复制或赋值之后两个对象相等,auto_ptr 不满足这一要求,如果将ap2 赋给 ap1,则在赋值之后 ap1 != 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 两次。

reset 操作

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 析构函数将不抛出异常。我们必须定义自己的析构函数来恢复析构函数不抛出异常的承诺。

异常说明与虚函数

基类中虚函数的异常说明,可以与派生类中对应虚函数的异常说明不同。但是,派生类虚函数的异常说明必须与对应基类虚函数的异常说明同样严格,或者比后者更受限。

这个限制保证,当使用指向基类类型的指针调用派生类虚函数的时候,派生类的异常说明不会增加新的可抛出异常。

函数指针的异常说明

异常说明是函数类型的一部分。这样,也可以在函数指针的定义中提供异常说明:
void (*pf)(int) throw(runtime_error);

该函数只能抛出 runtime_error 类型的异常。如果不提供异常说明,该指针就可以指向能够抛出任意类型异常的具有匹配类型的函数。

在用另一指针初始化带异常说明的函数的指针,或者将后者赋值给函数地址的时候,两个指针的异常说明不必相同,但是,源指针的异常说明必须至少与目标指针的一样严格。

17.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;
    // ...
  };
  class Query_base { /* ... */};
}

每个命名空间是一个作用域

在命名空间中定义的名字可以被命名空间中的其他成员直接成员,命名空间外部的代码必须指出名字定义在哪个命名空间中

从命名空间外部使用命名空间成员

总是使用限定名引用命名空间成员可能非常麻烦。可以编写 using 声明来获得对我们知道将经常使用的名字的直接访问:

     using cplusplus_primer::Query;

命名空间可以是不连续的

命名空间可以在几个部分中定义。命名空间由它的分离定义部分的总和构成,命名空间是累积的。一个命名空间的分离部分可以分散在多个文件中,在不同文本文件中的命名空间定义也是累积的。如果命名空间的一个部分需要定义在另一文件中的名字,仍然必须声明该名字。

编写命名空间定义:
  namespace namespace_name {
  // declarations
  }

如果名字 namespace_name 不是引用前面定义的命名空间,则用该名字创建新的命名空间,否则,这个定义打开一个已存在的命名空间,并将这些新声明加到那个命名空间。

接口和实现的分离

命名空间定义可以不连续意味着,可以用分离的接口文件和实现文件构成命名空间,因此,可以用与管理自己的类和函数定义相同的方法来组织命名空间:
  1. 定义类的命名空间成员,以及作为类接口的一部分的函数声明与对象声明,可以放在头文件中,使用命名空间成员的文件可以包含这些头文件。
  2. 命名空间成员的定义可以放在单独的源文件中。

定义多个不相关类型的命名空间应该使用分离的文件,表示该命名空间定义的每个类型。

定义命名空间成员

在命名空间内部定义的函数可以使用同一命名空间中定义的名字的简写形式(无需命名空间前缀)

也可以在命名空间定义的外部定义命名空间成员:

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 后面的是由花括号定界的声明块。

未命名的命名空间的定义局部于特定文件,从不跨越多个文本文件。

每个文件有自己的未命名的命名空间。在未命名的命名空间中定义的变量在程序开始时创建,在程序结束之前一直存在。未命名的命名空间中定义的名字可直接使用,毕竟,没有命名空间名字来限定它们。不能使用作用域操作符来引用未命名的命名空间的成员。

posted @ 2020-08-24 18:09  thsj  阅读(149)  评论(0)    收藏  举报