大型程序往往会使用多个独立开发的库,这些库又会定义大量的全局名字,如类、函数和模板等。 当应用程序用到多个供应商提供的库时,不可避免地会发生某些名字相互冲突的情况。多个库将名字放置在全局命名空间中将引发命名空间污染(namespace pollution)。
为了防止名字冲突,也为了简化命名操作,C++提供了命名空间(namespace)机制。 命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者(以及用户)可以避免全局名字可能会造成的冲突。
12.1 命名空间的定义
一个命名空间的定义包含三部分:首先是关键字namespace
,随后是命名空间的名字;最后在命名空间名字后面是一个复合语句,复合语句中包含一系列的声明和定义。
命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。
12.11 命名空间的定义形式
定义形式为:
(可选 inline) namespace (可选 命名空间名) 复合语句
对于命名空间的复合语句来说,只要能出现在全局作用域中的声明或定义,就能置于命名空间的复合语句内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间,这些都可以称为该命名空间的成员。
也就是说,命名空间中只能存在各种声明和定义语句,不能存在其他类型的语句。
namespace ns
{
int ins = 35;
void prints() {}
struct Cls {};
template<int inst, class ty>
inline ty prints(double val = inst) { cout << val << "\n"; return ty(); }
namespace nest { double dou = 7.8; }
}
命名空间是无法声明的,只能定义。
// 错误:不能声明命名空间
namespace ns;
// 正确:定义命名空间
namespace ns {}
因为每个复合语句就是一个作用域,所以每个命名空间也就是一个作用域。 对于在大多数命名空间作用域之外的实体来说,如果想使用命名空间内的成员,就必须要使用作用域运算符来操作,形式为:
命名空间名::成员名
namespace ns
{
int ins = 35;
void prints() { cout << ins; }
}
// 输出365
ns::ins = 365;
ns::prints();
12.12 命名空间的不连续性
命名空间可以是不连续的,这一点与其他作用域不太一样。 我们每次定义命名空间时,编译器会检查定义位置的作用域中是否存在同名的命名空间: 如果存在,则我们随后声明或定义的成员将会被添加到之前同名的命名空间中;否则就新建一个命名空间来保存成员。 所以不同作用域中的同名命名空间并不是指的是同一个命名空间。
命名空间的作用域范围是根据第一次定义的的范围所决定的,之后对该命名空间的添加语句的位置不会影响该命名空间的作用域范围。
namespace ns
{
int ins = 35;
void prints();
void prints2();
namespace ns { double dou = 6.8; }
}
namespace ns { string str = "external"; }
// 正确:str为ns的成员
void ns::prints() { cout << str; }
// 错误:dou不为外层ns的成员,是为内层的ns成员
void ns::prints2() { cout << dou; }
因为命名空间的不连续性,我们甚至可以在某个命名空间的作用域外定义或声明该命名空间的成员,只要用作用域运算符显式指明所要被添加成员的命名空间。
namespace ns
{
int ins = 35;
void prints() { cout << ins; }
namespace ns2
{
double dou = 3.45;
void prints2() { cout << dou; }
}
}
namespace ns::ns2
{ void prints3() { cout << dou; } }
// 正确:输出3.45
ns::ns2::prints3();
12.13 命名空间的成员的定义
和类类型一样,命名空间的成员也可以在命名空间之外定义,不过也需要在其命名空间内有声明且定义时要用作用域运算符显式指明为命名空间的成员。
在命名空间内的所有成员都默认为该命名空间的成员或嵌套在该命名空间的命名空间的成员以及其嵌套命名空间的嵌套命名空间的成员(以此类推,直到最里层的命名空间成员),所以不能在命名空间内定义非嵌套在该命名空间的其他命名空间的成员,否则出错。 (反过来说,嵌套的命名空间的成员可以定义在其外层命名空间内,包括外层的外层,以此类推,直到最外层的命名空间内)
所以,我们不把#include
放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。
namespace ns
{
int ins = 35;
void prints();
namespace ns2 { void prints2(); }
void ns2::prints2() { cout << 88; }
}
void ns::prints() { cout << ins; }
ns::ins = 365;
// 输出365
ns::prints();
// 输出88
ns::ns2::prints2();
模板特例化必须要在原始模板所属的命名空间中声明,不能在该命名空间之外声明。 和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在该命名空间外部定义它了。
12.2 命名空间的分类
根据命名空间的性质,可以将所有的命名空间分为以下几种:
- 全局命名空间
- 未命名的命名空间
- 内联命名空间
除了全局命名空间以外,其他所有的命名空间都能为未命名或者内联的。
12.21 全局命名空间
全局命名空间(global namespace)是隐式定义的命名空间,我们不能定义显式全局命名空间。 全局命名空间在所有程序中都存在。 所有在全局作用域中声明或定义的名字(即在所有类、函数及命名空间之外定义的名字)都是全局命名空间的成员。
作用域运算符同样能作用于全局命名空间的成员,因为它是隐式的,所以它并没有名字。 以下表示访问一个全局命名空间中的成员:
::成员名
12.22 未命名的命名空间
未命名的命名空间(unnamed namespace)是指定义时没有名字的命名空间。
未命名的命名空间和普通命名空间一样,可以不连续,但是对于定义在全局作用域中的未命名的命名空间,它的不连续性只能在某个给定的文件内,不能跨越多个文件。
所以每个文件定义自己的全局作用域中的未命名的命名空间,如果两个文件都含有全局作用域的未命名的命名空间,则这两个空间互相无关。 这两个未命名的命名空间中可以定义相同名字的实体,并且这些定义表示的是不同实体。 因此如果一个头文件定义了全局作用域中的未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。
所以和其他命名空间不同,全局作用域中的未命名的命名空间仅在特定的文件内部有效,其作用范围不会橫跨多个不同的文件,所以可以用全局作用域中的未命名的命名空间来代替每个文件中的静态声明。
定义在未命名的命名空间中的名字可以在该命名空间所在的作用域中直接使用,毕竟我们找不到什么命名空间的名字来限定它们;同样的,我们也不能在该命名空间所在的作用域中对其成员使用作用域运算符。
未命名的命名空间中的实体名字可以与该命名空间所在的作用域的其他实体名字相同,不会出现重复定义,但是会出现二义性问题,此时可以用作用域运算符来消除二义性(但因为未命名的命名空间不能用作用域运算符,所以也就用不了其同名成员了)。
namespace ns
{
int ins = 35;
namespace { int ins = 84; }
// 错误:二义性
void prints() { cout << ins; }
// 正确:显式调用ns命名空间的成员
void prints2() { cout << ns::ins; }
}
12.23 内联命名空间
C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace)。 对于每个声明为inline的命名空间,都叫做内联命名空间。
内联的未命名的命名空间以未命名的命名空间的规则为主。
和普通的嵌套命名空间不同,内联命名空间中的名字可以被其外层的命名空间直接使用。 也就是说,我们无须使用作用域运算符,就可以在其外层命名空间的作用域中直接访问它的成员(当然也可以用作用域运算符访问)。
和未命名的命名空间一样,内联命名空间中的名字可以与该命名空间所在的作用域的实体名字相同,不会出现重复定义,但是会出现二义性问题,此时可以用作用域运算符来消除二义性。
namespace ns
{
int ins = 35;
inline namespace ne { int ins = 84; }
// 错误:二义性
void prints() { cout << ins; }
// 正确:显式调用ns命名空间的成员
void prints2() { cout << ns::ins; }
// 正确:显式调用ne命名空间的成员
void prints2() { cout << ne::ins; }
}
内联命名空间与未命名的命名空间的区别也就是全局作用域中的内联的已命名的命名空间可以在多个文件中不连续,且可以使用作用域运算符访问。
12.3 命名空间成员的使用
我们之前介绍过访问命名空间成员的方法是用作用域运算符的方式,但是当命名空间太长时,访问成员就会异常的麻烦,所以为了解决这个问题,我们有两种方法:
- 定义命名空间的别名
- 拓展成员的作用域
using
声明using
指示
12.31 命名空间的别名
命名空间的别名(namespace alias)使得我们可以为命名空间的名字设定一个短得多的同义词,我们可以像对原命名空间一样对该别名作出任何可以的操作。
命名空间的别名只在该别名声明语句所在的作用域中生效。
一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。
命名空间别名的声明语句除了不能存在于类内以外,其他的和类型别名一样,可以存在于全局作用域、命名空间和函数内。
命名空间的别名声明语句是以关键字namespace
开始,后面是别名所用的名字、赋值号=
、命名空间原来的名字以及一个分号;
。
别名声明语句的形式为:
namespace 别名 = 命名空间名;
声明的别名不能是该语句所在作用域中的其他命名空间名,否则会有重复定义错误。 而且声明后的别名不能用于该命名空间成员的声明或定义,只能用于被调用。
#include <iostream>
// 正确,ns命名空间的成员定义
namespace ns
{
int ins = 55;
}
// 正确,ns命名空间的成员定义
namespace ns
{
double dou = 55.5;
}
// 全局作用域中,的命名空间别名nc
namespace nc = ns;
// 函数内,的命名空间别名fns
int foo()
{
namespace fns = ns;
std::cout << fns::dou << std::endl;
}
// 命名空间内,的命名空间别名nns
namespace nestn
{
namespace nns = ns;
nns::ins;
}
// 重复定义错误,别名nc不能用于后续ns命名空间成员的声明或定义
namespace nc
{
double dou2 = 55.5;
}
// 正确,ns命名空间的成员定义
namespace ns
{
double dou3 = 55.55;
}
但可以与所在作用域的外层作用域的命名空间同名,此时会在该语句所在作用域中隐藏掉外层的同名命名空间。
声明语句中的命名空间名可以是嵌套在命名空间中的命名空间,但不能是未命名或者未定义的命名空间。
namespace ns
{
int ins = 35;
namespace nest { void prints() { cout << ins; } }
}
// 别名声明语句
namespace nc = ns::nest;
// 正确:输出35
ns::nest::prints();
nc::prints();
12.32 拓展成员的作用域
我们也可以直接拓展某个命名空间的成员作用域来直接访问该空间的某些成员。
拓展成员的作用域的方法有两种:
using
声明using
指示
这两种方法只在该方法语句所在的作用域中生效。
不管是哪种方法,在被拓展的作用域内出现的实体都可以直接访问这些被拓展的命名空间成员而不需要用到作用域运算符(当然也可以用)。
拓展成员的作用域后,我们还是可以用作用域运算符来访问这些成员,不过要注意: 如果在使用
using
指示后还是用作用域运算符来访问这些成员,则该命名空间中不能有与其同名的内嵌命名空间,否则使用作用域运算符访问的就是内嵌命名空间的成员了。
这两种方法和类型别名一样,可以存在于全局作用域,命名空间、函数内或者是类内。
12.321 using声明
using
声明(using declaration)语句一次只能拓展命名空间的一个非命名空间成员。
被拓展的成员的==作用域范围是从using
声明语句的出现位置到该语句所在作用域的末尾==。
using
声明语句为:
using 命名空间名::成员名;
using
声明语句中的命名空间名可以是嵌套的命名空间,但不能是未命名或者未定义的命名空间。
using
声明语句中的成员名不能为命名空间成员。 而且对于非函数成员名来说,不能与该语句所在作用域中的其他实体同名;对于函数成员名来说,不能与该语句所在作用域中的其他函数定义语句中的函数首部有冲突。否则会有重复定义错误。 但可以与所在作用域的外层作用域的实体同名,此时会在该语句所在作用域中隐藏掉外层的所有同名实体。
当
using
声明语句中的成员名为重载函数名时,该函数在命名空间的所有版本都会被拓展。
namespace ns
{
int ins = 35;
void prints() { cout << ins; }
namespace ns2
{
double dou = 3.45;
void prints2() { cout << dou; }
}
}
// 错误:不能直接访问ns中的ns2的成员
prints2();
// using声明语句
using ns::ns2::prints2;
// 正确:输出3.45
prints2();
12.322 using指示
using
指示(using directive)是另一种拓展成员作用域的方法。 和using
声明不一样的地方是,using
指示会拓展给定命名空间的所有成员的作用域(包括在using
指示语句之后添加进该空间的成员)。
所有被拓展的成员的作用域范围==被提升到与包含该命名空间本身和该using指示语句的最近作用域范围一样的范围==。
using
指示语句的形式为:
using namespace 命名空间名;
using
指示语句中的命名空间名可以是嵌套的命名空间,但不能是未命名或者未定义的命名空间。
对于using
指示语句中给定的命名空间中的成员来说:
- 非函数成员名不能与该语句所在作用域中的其他实体同名,否则在使用该同名时会出现二义性错误。
- 函数成员名不能与该语句所在作用域中的其他函数定义语句中的函数首部有冲突,否则在使用该同名时会出现二义性错误。
但该using
指示语句所提升的作用域中的内层作用域的实体可以与该命名空间的成员同名,此时会在内层作用域中隐藏掉该命名空间的所有同名成员。
namespace ns
{
int ins = 35;
void prints() { std::cout << 49; }
double dou = 3.45;
}
int ins = 56;
namespace ns { string str = "good"; }
int main()
{
// using指示语句
using namespace ns;
// 隐藏了ns的dou成员
double dou = 94.45;
// 正确:输出49
prints();
// 正确:输出good
cout << str;
// 隐藏了ns的dou成员,所以\
输出94.45
cout << dou;
// 错误:ns中的ins成员与外层成员ins起冲突\
导致二义性错误
cout << ins;
// 正确:显式调用ns中的ins成员\
输出35
cout << ns::ins;
}
12.4 命名空间与函数匹配
12.41 命名空间的名字查找
对命名空间内部名字的查找(包括类的成员)遵循常规的查找规则:即由内向外依次査找每个外层作用域。
外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。只有位于使用点之前声明的名字才被考虑。
所以如果某命名空间中不含嵌套的同名命名空间,则该命名空间的成员定义可以使用本身自己的命名空间名及作用域运算符来调用本身自己命名空间的成员。
// 命名空间ns
namespace ns
{
int g_ins = 5;
}
// 命名空间ns
namespace ns
{
// 查找到本身ns的g_ins
int g_ins2 = g_ins;
// 查找到本身ns的g_ins
int g_ins3 = ns::g_ins;
}
// 命名空间ns2
namespace ns2
{
double g_dou = 5.5;
}
// 命名空间ns2
namespace ns2
{
// 命名空间ns2::ns2
namespace ns2
{
double g_dou = 7.99;
}
// 查找到本身ns2的g_dou
double g_dou3 = g_dou;
// 查找到ns2::ns2的g_dou2
double g_dou4 = ns2::g_dou;
}
12.42 函数匹配
命名空间成员的函数匹配与普通的函数匹配的流程大部分一样,只不过有以下的这些区别:
- 函数匹配时的名字查找还会在实参类所属的命名空间中进行: 按照调用表达式的实参顺序,在每个实参类(以及实参类的基类)所属的命名空间中进行查找(只会在这个命名空间中查找,不会在该命名空间的外层作用域(包括外层空间)中进行查找),并将查找到的所有同名函数加入到候选函数集中(即使该函数所属的命名空间在调用点不可见,也会将其加入)。
namespace ns
{
namespace nest
{
struct Cls { string str = "Cls"; };
void prints(Cls obj, double) { cout << obj.str << " nest\n"; }
}
void prints(nest::Cls obj, string) { cout << obj.str << " ns\n"; }
}
void prints(ns::nest::Cls obj, int) { cout << obj.str << " external\n"; }
int main()
{
ns::nest::Cls ob;
// 调用void prints(ns::nest::Cls obj, int)\
输出Cls external
prints(ob, 3);
// 调用void ns::nest::prints(ns::nest::Cls obj, double)\
输出Cls nest
prints(ob, 5.1);
// 错误:void ns::prints(ns::nest::Cls obj, string)在调用点不可见
prints(ob, "str");
}
- 命名空间中的类的友元声明会被隐式当成该类所属命名空间的成员声明: 命名空间中的类的友元声明不再仅仅只是一个权限说明了,也是一个该类所属命名空间的隐式成员声明。 所以命名空间中的类的友元只能是该命名空间的成员。
namespace ns
{
class Cls
{
// 友元声明
friend void prints(Cls obj, int val);
friend void prints2(int val);
int ins = 15;
};
}
// 错误:Cls的友元必须要为ns的成员,该函数不是友元\
所以该函数无法访问Cls的ins成员
void prints(ns::Cls obj, int val) { cout << val << ' ' << obj.ins << " Cls\n"; }
// 正确:为Cls的友元void prints(Cls obj, int val)的定义
void ns::prints(Cls obj, int val) { cout << val << ' ' << obj.ins << " Cls2\n"; }
int main()
{
ns::Cls ob;
// 错误:有多个函数匹配
prints(ob, 3);
// 正确:调用Cls类的友元\
输出3 15 Cls2
ns::prints(ob, 3);
// 错误:prints2为ns的成员,所以不可见
prints2(3);
}
当我们使用了拓展命名空间成员的作用域的方法时,被拓展的函数成员同样遵循普通的函数匹配规则。
namespace ns
{
void prints(int val) { cout << val << " ns\n"; }
void prints(string val) { cout << val << " ns\n"; }
}
void prints(double val) { cout << val << " external\n"; }
// using声明语句
using ns::prints;
int main()
{
// 调用void ns::prints(std::string val)\
输出str ns
prints("str");
// 调用void ns::prints(int val)\
输出5 ns
prints(5);
// 调用void prints(double val)\
输出3.58 external
prints(3.58);
}
namespace ns
{
void prints(double val) { cout << val << " ns\n"; }
void prints(string val) { cout << val << " ns\n"; }
}
void prints(int val) { cout << val << " external\n"; }
void prints(double val) { cout << val << " external\n"; }
int main()
{
// using指示语句
using namespace ns;
// 正确:调用void ns::prints(std::string val)\
输出str ns
prints("str");
// 正确:调用void ns::prints(int val)\
输出5 ns
prints(5);
// 错误:ns中和全局作用域的void prints(double val)函数都是最佳匹配
prints(3.58);
// 正确:显式调用void ns::prints(double val)\
输出3.58 ns
ns::prints(3.58);
}