多个库将名字放置在全局命名空间中将引发命名空间污染。
命名空间为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域。
命名空间定义
一个命名空间的定义包含两部分:关键字 namespace,随后是命名空间的名字。
只要能在全局作用域中的声明就能置于命名空间内:类,变量(及其初始化操作),函数(及其定义),模板,和其他命名空间。
命名空间后面不需要加分号,这一点与块类似。
命名空间既可以定义在全局作用域,也可以定义在其他命名空间中,但是不能定义在类或函数的内部。
每个命名空间都是一个作用域
同一个命名空间内每个名字必须表示该空间内唯一的实体。不同的命名空间可以有相同名字的成员。
- 定义在某个命名空间中的名字可以被该命名空间内的其它成员直接访问,也可以被这些成员内嵌入作用域的任何单位访问。
- 位于该空间外的代码必须明确指出所用的名字属于哪个命名空间。
命名空间可以是不连续的
命名空间可以定义在几个不同的部分。
name nsp{
//@ 声明
}
上面可能是定义了一个名为 nsp 的新命名空间,也可能为已经存在的命名空间添加一些新的成员:
- 如果之前没有名为 nsp 的命名空间定义,则上述代码创建一个新的命名空间。
- 如果之前出现了 nsp 的命名空间,上面的代码将为命名空间添加一些新成员的声明。
命名空间可分离,可以将几个独立的接口和实现文件组成一个命名空间:
- 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件被包含在使用了这些成员的文件中。
- 命名空间成员的定义则部分置于另外的源文件中。
程序中某些实体只能定义一次:非内联函数,静态数据成员,变量等。命名空间中定义的名字也需要满足这一要求。
定义命名空间
//@ Sales_data.h
#include <string>
namespace cpluscplus_primer {
class Sales_data {//@ 成员定义};
Sales_data operator+(const Sales_data&, const Sales_data&);
}
//@ Sales_data.cpp
#include "Sales_data.h"
namespace cpluscplus_primer {
//@ Sales_data 成员定义
//@ Sales_data 重载运算符定义
}
//@ 使用定义的库,需要包含头文件;user.cpp
#include "Sales_data.h"
int main()
{
using cpluscplus_primer::Sales_data;
Sales_data trans1, trans2;
//@ .....
return 0;
}
通常不将 #include 放在命名空间内部,如果这么做了,隐含的意思就是将头文件中所有的名字定义成该命名空间的成员。
模板特例化
模板特例化必须定义在原始模板所属的命名空间中,和其他命名空间名字类似,只要在命名空间中声明了特例化,就能在命名空间外部定义它:
namespace std {
template <> struuct hash<Sales_data>;
}
//@ 在 std 中添加了模板特例化声明后,就可以在命名空间 std 外部定义它
template <> struct std::hash<Sales_data>
{
size_t operator()(const Sales_data& s)const
{
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
};
全局命名空间
全局作用域中定义的名字也就是定义在全局命名空间中,全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字都被隐式的添加到全局命名空间中。
作用域运算符可以用于全局作用域的成员,因为全局作用域是隐式的,所以它并没有名字,使用下面的形式:
::member_name
嵌套的命名空间
嵌套命名空间是指定义在其他命名空间中的命名空间。
namespace cpluscplus_primer {
namespace QueryLib {
class Query {/*...*/};
}
namespace BookStore {
class Quote{/*...*/};
}
}
命名空间 cpluscplus_primer 分割为两个嵌套的命名空间,分别是 QueryLib、BookStore。
- 内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。
- 嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间的代码想要访问它必须在名字前面添加限定符。
cpluscplus_primer::QueryLib::Query
内联命名空间
C++ 11 引入了一种新的嵌套命名空间,称为内联命名空间。
定义内联命名空间的方式是在关键字 namespace 前添加前缀 inline。关键字 inline 必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写 inline ,也可以不写。
内联命名空间中的名字可以被外层命名空间直接使用。
inline namespace FifthEd {
}
namespace FifthEd { //@ 隐式内联
class Query_base {/*...*/};
}
namespace FourthEd {
class Item_base{/*...*/};
class Query_base{/*...*/};
}
命名空间 cpluscplus_primer 将同时使用这个名字的命名空间:
namespace cpluscplus_primer{
#include "FifthEd.h"
#include "FourthEd.h"
}
因为 FifthEd 是内联的,所以形如 cpluscplus_primer:: 的代码可以直接使用 FifthEd 的成员。但是如果想使用 FourthEd 的成员,则必须使用:cpluscplus_primer::FourthEd ::Query_base。
未命名的命名空间
未命名的命名空间是指关键字 namespace 后紧跟花括号括起来的一系列声明语句。
未命名的命名空间中定义的变量将拥有静态声明周期:它们在第一次使用前创建,并且直到程序结束时才销毁。
未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间中可以定义相同的名字,并且这些定义表示不同的实体。
定义在未命名空间中的名字可以直接使用,也不能对未命名空间的成员使用作用域运算符。
未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域的名字有所区别,否则将会引起命名冲突:
int i;
namespace {
int i;
}
int main(void)
{
i = 10; //@ 歧义
return 0;
}
未命名的命名空间也可以嵌套在其他命名空间当中,此时,未命名的命名空间中的成员可以通过外层命名空间的名字来访问:
namespace local{
namespace {
int i;
}
}
local::i = 42;
未命名的命名空间取代文件中的静态声明
在标准 C++ 引入命名空间的概念之前,程序需要将名字声明成 static 以使得其在整个文件有效,在文件之外不可见。
在文件中进行静态声明的做法已经被 C++ 标准取消了,现在的做法是使用未命名的命名空间。
使用命名空间成员
命名空间的别名
命名空间别名可以为命名空间设定一个简短的名字:
namespace cpluscplus_primer{/*...*/}
namespace primer = cpluscplus_primer;
命名空间的别名也可以指向一个嵌套的命名空间:
namespace Qlib = cpluscplus_primer::QueryLib;
Qlib::Query q;
一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。
using 声明:扼要概述
一条 using 声明语句一次只引入命名空间的一个成员。
using 声明引入的名字的有效范围从 using 声明的地方开始,一直到 using 声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。
一条 using 声明语句可以出现在全局作用域,局部作用域,命名空间作用域以及类作用域。在类的作用域中,这样的语句只能指向基类成员。
using 指示
using 指示和 using 声明类似的地方是:可以使用命名空间的简写形式,和 using 声明不同的是,using 指示所有的名字都将是可见的。
using 指示使用形式为:
using namespace nsp; //@ nsp 表示具体的命名空间
这里的命名空间必须是已经定义好的命名空间名字,否则程序将发生错误。
using 指示可以出现在全局作用域,局部作用域和命名空间作用域,但是不能出现在类的作用域。
using 指示与作用域
using 声明的名字的作用域于 using 声明语句本身的作用域一致。
using 指示具有将命名空间成员提升到包含命名空间本身和 using 指示的最近作用域的能力。
namespace bilp {
int i = 16, j = 15, k = 23;
}
int j = 0; //@ 正确:blip 的 j 隐藏在命名空间中
int main(void)
{
using namespace bilp;
++i; //@ bilp::i 设定为17
++j; //@ 二义性错误
++::j; //@ 全局作用域的j设定为1
++bilp::j; //@ bilp::j 设定为17
int k = 97; //@ 当前作用域k隐藏了bilp::k
++k; //@ 当前局部作用域的k设置为98
return 0;
}
头文件与 using 声明或指示
头文件如果在其顶层作用域中含有 using 指示或者 using 声明,则会将名字注入到所有包含了该头文件的文件中。
通常,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用 using 指示或 using 声明。
using 指示一次性注入某个命名空间的所有名字,这种用法看似简单实则充满了风险:只用一条语句就突然将命名空间中所有成员的名字变得可见了,有可能导致全局命名空间的污染。
类、命名空间与作用域
namespace A
{
int i;
int k;
class C1 {
public:
C1() :i(0), j(0) {} //@ 初始化 C1::i,C1::j
int f1() { return k; } //@ ok,返回A::k
int f2() { return h; } //@ error,h未定义
int f3();
private:
int i; //@ 隐藏 A::i
int j;
};
int h = i; //@ A::i 进行初始化
}
int A::C1::f3() { return h; } //@ 正确,返回A::h
实参相关的查找与类类型形参
std::string s;
std::cin >> s;
上面的调用等价于:
operator >> (std::cin,s);
operator >> 定义在标准库 string 中,string 又定义在命名空间 std 中。但是这里不使用 std:: 限定符和 using 声明就可以调用 operator >>。
对于命名空间中名字隐藏规则有一个重要的例外:当给函数穿第一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间,这个例外对于传递类的引用或指针的调用同样有效。
上面的例子,编译器发现对 operator >> 的调用,首先在当前作用域中寻找合适的函数,接着查找输出语句的外层作用域。又因为 >> 表达式的形参是类类型,所以编译器会查找 cin 和 s 的类所属的命名空间。也就是说,编译器会查找定义了 istream 和 string 的命名空间 std。在 std 中编译器找到了 string 的输出运算符函数。
查找与 std::move 和 std::forward
通常情况下,如果在应用程序中定义了一个标准库中已有的名字,则将出现以下两种情况中的一种:
- 要么根据一般的重载规则确定某次调用应该执行函数的哪个版本。
- 要么应用程序根本就不会执行函数的标准库版本。
move、forward 都是模板函数,所以如果应用程序也定义一个版本的 move函数,一定会发生冲突。
因此,对于这两个函数的调用通常加上 std:: 作用域符。
友元声明与实参相关的查找
当类声明一个友元时,该友元声明并没有使得友元本身可见。
如果一个未声明的类或函数第一次出现友元声明中,则我们认为它是最近的外层命名空间的成员。
namespace A
{
class C {
//@ 两个友元,在友元声明之外没有其他的声明
//@ 这些函数隐式地称为命名空间A的成员
friend void f2(); //@ 除非另有声明,否则不会被找到
friend void f(const C&); //@ 根据实参相关的查找规则可以被查找到
};
}
int main(void)
{
A::C cobj;
f(cobj); //@ ok,通过A::C 中的友元声明找到 A::f
f2(); //@ A::f2 没有被声明
return 0;
}
重载与命名空间
与实参相关的查找与重载
对于接受类类型实参的函数来说,起名字查找将在实参类所属的命名空间中进行。这条规则对于我们如何确定候选函数集同样也有影响:会在每个实参类以及实参类的基类所属的命名空间中搜寻候选函数,在这些命名空间中所有与被调用函数同名的函数都将被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此:
namespace NS {
class Quote {/*...*/};
void display(const Quote&){/*...*/}
}
class Bulk_item:public NS::Quote{/*...*/};
int main(void)
{
Bulk_item book1;
display(book1);
return 0;
}
传递给 display 的实参属于类类型 Bulk_item,因此该调用语句的候选函数不仅应该在调用语句所在的作用域中查找,并且会在 Bulk_item 及其基类所属的命名空间中查找。命名空间 NS 中声明的函数 display 也将被添加到候选函数集当中。
重载与 using 声明
using 声明语句声明的是一个名字,而非一个特定的函数:
using NS::print(int); //@ error,不能指定形参列表
using NS::print; //@ ok,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 指示把名字添加到 print 调用的候选函数集
using namespace libs_R_us;
//@ print 调用此时包含的函数集:
//@ libs_R_us 的 print(int)
//@ libs_R_us 的 print(double)
//@ 显示声明的 print(const std::string&)
int main(void)
{
print("Values: "); //@ 调用显示的 print(const std::string&)
print(ival); //@ 调用 libs_R_us 的 print(int)
return 0;
}
与 using 声明不同的是,对于 using 指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们指明调用的命名空间中的函数版本还是当前作用域的版本即可。
跨越多个 using 指示的重载
如果存在多个 using 指示,则来自每个命名空间的名字都会成为候选函数集的一部分:
namespace AW {
int print(int);
}
namespace Primer {
double print(double);
}
//@ using 指示从不同的命名空间中创建了一个重载函数集合
using namespace AW;
using namespace Primer;
long double print(long double);
int main(void)
{
print(1); //@ AW::print(int)
print(1.1); //@ Primer::print(double)
return 0;
}
全局作用域中,函数 print 的重载集合包括:
- AW::print(int)
- Primer::print(double)
- print(long double)