C++ 基础系列——命名空间
一些大型软件往往由多人共同开发,会使用到大量的变量和函数,不可避免容易出现变量或者函数名的命令冲突。即使所有人代码测试通过,但将它们结合到一起时,也极有可能出现命名冲突。
命名空间(namespace)为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间都是一个作用域。
1. 命名空间定义
C++ 使用 namespace 关键字来定义一个命名空间,随后是命名空间的名字。语法格式为:
namespace name
{
// 类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间
}
如
namespace cpp_primer
{
class Sales_data{};
class Query{};
void func(){};
int x = 100;
}
命名空间的名字必须在定义它的作用域内保持唯一。
命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但不能定义在函数或类的内部。
命名空间作用域后面无须分号
1.1 每个命名空间都是一个作用域
定义在命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域的任何单位访问。
命名空间之外访问必须指明所用的属于哪个命名空间。
例如又定义了一个额外的命名空间,该空间与 cpp_primer 命名空间部分变量名字相同,那么使用里面的成员时,需要指定是那一个命名空间:
namespace Addison_cpp_primer
{
class Sales_data{};
class Query{};
void func(){};
int x = 100;
}
cpp_primer::Query q = cpp_primer::Query();
Addison_cpp_primer::Query q = Addison_cpp_primer::Query();
1.2 命名空间可以不连续
命名空间可以定义在几个不同的部分。
namespace nsp
{
//
}
上述代码可能定义了一个新的命名空间,也可能为已存在的命名空间添加一些新成员。
命名空间定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。
定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型。
通过接口和实现分离机制,将 cpp_primer 库定义在几个不同文件中。
// Sales_data.h
// #include 应该出现在打开命名空间的操作之前
#include <string>
namespace cpp_primer
{
class Sales_data{};
Sales_data operator+(const Sales_data&,const Sales_data&);
// Sales_data 类的其他接口声明
}
// Sales_data.cc
#include "Sales_data.h"
namespace cpp_primer
{
// 定义
}
如果需要使用定义的Sales_data 库,必须包含头文件。
#include "Sales_data.h"
int main()
{
using cpp_primer::Sales_data;
Sales_data trans1,trans2;
// ...
return 0;
}
通常情况下,不把 #include
放在命名空间内部。如果这样做了,意味着把头文件中所有的名字定义成该命名空间的成员。
1.3 定义命名空间成员
假定作用域中存在合适的声明语句,则命名空间中的代码可以使用直接同一命名空间定义的其他成员
#include "Sales_data.h"
namespace cpp_primer
{
std::istream& operator>>(std::istream& in,Sales_data& s){/* */}
}
也可以在命名空间外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。
cpp_primer::Sales_data cpp_primer::operator+(const Sales_data& lhs, const &Sales_data& rhs)
{
Sales_data ret(lhs);
// ...
}
1.4 模板特例化
模板特例化必须定义在原始模板所属的命名空间中。和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了。
// 必须将末班特例化声明成 std 的成员
namespace std
{
template <> struct 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);
}
}
1.5 全局命名空间
全局作用域(即所有类、函数及命名空间之外)定义的名字是定义在全局命名空间中,全局命名空间以隐式的方式声明,并且在所有程序中都存在。
全局作用域中定义的名字被隐式地添加到全局命名空间中。
作用域运算符同样可以用于全局作用域的成员,因为全局作用域是隐式的,所以它并没有名字:
::member_name
1.6 嵌套的命名空间
嵌套的命名空间是指定义在其他命名空间中的命名空间
namespace cpp_primer
{
namespace QueryLib
{
// ...
}
namespace Bookstore
{
// ...
}
}
内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间要想访问它必须添加限定符。
cpp_primer::QueryLib::Query
1.7 内联命名空间
C++11 引入了一种新的嵌套命名空间,称为内联命名空间。
和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。
定义内联命名空间的方式是在关键字 namespace 前添加关键字 inline:
inline namespace FifthEd
{
// ...
}
namespace FifthEd // 隐式内联,第二次定义,内联关键字 inline 可加可不加
{
// ...
}
假设命名空间 cpp_primer 同时使用 FifthEd 和 FourthEd 两个命名空间
namespace cpp_primer
{
#include "FifthEd.h" // 内联
#include "FourthEd.h" // 非内联
}
那么 cpp_primer 命名空间可以不通过限定符可以直接访问 FifthEd 命名空间的成员,但需要完整的命名空间名才能访问 FourthEd 的成员。
1.8 未命名的命名空间
未命名的命名空间是指关键字 namespace 后紧跟着花括号的一系列声明语句。
未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束时才销毁。
未命名空间仅在特定的文件内有效,其作用范围不会跨越多个不同的文件,它可以在特定文件内不连续。
在文件中进行静态声明的做法已经被 C++ 标准取消了,现在的做法是使用未命名的命名空间。
2. 使用命名空间成员
像 namespace_name::member_name 这样使用命名空间的成员显然非常繁琐,可以使用其他更简便的方法使用命名空间的成员。
2.1 命名空间的别名
命名空间的别名声明以关键字 namespace 开始。
namespace primer = cpp_primer;
- 不能在命名空间还没有定义之前就声明别名。
- 一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。
2.2 using声明:扼要概述
一条 using 声明语句一次只引入命名空间的一个成员。
它的有效范围:从 using 声明的地方开始,一直到 using 声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。
一条 using 声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类作用域中。在类的作用域中,这样的声明语句只能指向基类成员。
2.3 using 指示
using 指示以关键字 using 开始,后面是关键字 namespace 以及命名空间的名字。
using 指示可以出现在全局作用域、局部作用域和命名空间作用域中,但不能出现在类的作用域中。
2.4 using 指示与作用域
using 声明和 using 指示在作用域上的区别直接决定了它们工作方式的不同。对于 using 声明来说,只是简单地令名字在局部作用域内有效。相反,using 指示是令整个命名空间的所有内容变得有效。
命名空间中通常会含有一些不能出现在局部作用域的定义,因此,using 指示一般被看作是出现在最近的外层作用域中。
让我们看一个例子
namespace blip
{
int i = 16, j = 15, k = 23;
}
int j = 0;
void maip()
{
// using 指示,blip 中的名字被“添加”到全局作用域中
using namespace blip; // 如果使用 j,则将在::j 和 blip::j 之间产生冲突
++i; // blip::i
++j; // 二义性错误:是全局 j 还是 blip::j?
++::j; // 全局j
++blip::j; // blip::j
int k = 100; // 隐藏 blip::k
++k; // 当前局部 k
}
2.5 头文件与 using 声明或指示
头文件如果在其顶层作用域中含有 using 指示或 using 声明,则会将名字注入到所有包含了该头文件的文件中。
通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用 using 指示或 using 声明。
3. 类、命名空间与作用域
对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域,外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。只有位于开放的块中且在使用点之前声明的名字才会被考虑。
对于位于命名空间中的类来说,常规的查找规则仍然适用:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找,这时一个或几个外层作用域可能就是命名空间:
namespace A
{
int i;
int k;
class C1
{
public:
C1():i(0),j(0){} // 正确,初始化 C1::i 和 C1::j
int f1(){ return k;} // 正确,返回 A::k
int f2(){ return h; } // 错误,h未定义
int f3();
private:
int i;
int j;
};
int h = i;
}
// 成员 f3 定义在 C1 和命名空间 A 的外部
int A::C1::f3()
{
return h; // 正确,返回 A::h f3 的定义位于 A::h 的定义之后,所以是合法的
}
限定符A::C1::f3
指出了查找类作用域和命名空间作用域的相反次序。首先查找函数 f3 的作用域,然后查找外层类 C1 的作用域,最后检查命名空间 A 的作用域以及包含着 f3 定义的作用域。
3.1 实参相关的查找与类类型形参
考虑下面这个简单的程序:
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 的输出运算符函数。
如果没有这个规则,那么此例中需要专门提供 using 声明。
using std::operator>>;
// std::operator>>(std::cin,s);
3.2 查找与 std::move 和 std::forward
通常如果在程序中定义了一个标准库中已有的名字,则会出现以下两种情况的一种:
- 根据一般的重载规则确定调用应该执行的函数版本;
- 程序根本不执行函数的标准库版本。
标准库的 move 和 forward 函数,都是模板函数,在标准库定义中它们都接受一个右值引用的函数参数,而右值引用可以匹配任何类型(c++ primer 611页)。因此如果我们定义了一个接受单一形参的 move 函数,不管形参是什么类型,都将与标准库版本的冲突。
因此通常书写 std::move 而非 move,这样就能明确知道想用的是标准库版本。
3.3 友元声明与实参相关的查找
友元的声明仅仅指定了访问的权限,不是一个通常意义上的函数声明。当类声明了一个友元时,该友元声明并没有使得友元本身可见,因此必须在友元声明之外再专门对函数进行一次声明。
一个另外的未声明的类或函数第一次出现在友元声明中,通常认为它是最近的外层空间的成员。
namespace A
{
class C
{
// 两个友元,在友元声明之外没有其他声明
// 这些函数隐式地称为命名空间 A 的成员
friend void f2(); // 除非另有声明,否则不会被找到
friend void f(const C&); // 根据实参相关的查找规则可以被找到
};
}
此时 f 和 f2 都是命名空间 A 的成员。即使 f 不存在其他声明,也能通过实参相关的查找规则调用 f:
int main()
{
A::C cobj;
f(cobj); // 正确,通过接受类类型实参找到
f2(); // 错误,A::f2 没有声明
}
4. 重载与命名空间
4.1 与实参相关的查找与重载
对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行,这条规则对于如何确定候选函数集同样有影响。
namespace NS
{
class Quote{/* */};
void display(const Quete&) {/* */}
}
class Bulk_item : public NS::Quote{ /* */ };
int main()
{
Bulk_item book1;
display(book1); // 其候选函数不仅在调用语句所在作用域查找,还会在 Bulk_item 及其基类 Quote 所属的命名空间中查找。
return 0;
}
4.2 重载与 using 声明/指示
using 声明语句声明的是一个名字,而非一个特定的函数,using 声明函数会将该函数名所有的版本都引入到当前域中
- 如果 using 声明出现在局部作用域中,则引入的名字会隐藏外层作用域的相关声明。
- 如果 using 声明所在的作用域已有无法再重载的函数,则 using 声明将引发错误。
using 指示将命名空间提升到外层作用域中。
与 using 声明不同的是,如果引入一个与已有函数形参列表完全相同(无法重载)的函数不会报错,调用时指明要调用的是哪个版本即可。
5. 总结
- 命名空间可以定义在全局作用域内和其他命名空间中,不能定义在函数或类内。
- 命名空间作用域后面无须分号。
- 命名空间的定义具有不连续的特性,同一个命名空间可以定义在不同的文件中。
- 全局命名空间以隐式方式声明,无名字,作用于全局作用域
::member_name
。 - 内联命名空间中的名字可以被外层命名空间直接访问。inline 只需声明一次。
- 未命名的命名空间中的变量拥有静态生命周期。
- 命名空间的别名声明以关键字 namespace 开始,可以有多个别名。
- using 声明/指示。using 指示被看作是出现在最近的外层作用域。
- 头文件中不要使用 using 指示/声明,最多只在它的函数或命名空间中使用。
- 命名空间常规查找规则:由内至外逐层查找。
- 当函数实参是类对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。
- 友元声明只是指定访问权限,要使用该友元函数还得声明。
- using 声明出现重复无法重载情况时会报错,using 指示不会报错,调用时需要指明。