一、命名空间
命名空间为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域
1、命名空间定义
一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。
1 #include <iostream> 2 #include <string> 3 4 namespace QAQ { 5 int x = 100; 6 } 7 int main() 8 { 9 std::cout << QAQ::x << std::endl; 10 return 0; 11 }
命名空间的名字必须在定义它的作用域内保持唯一。命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或类的内部。
1)每个命名空间都是一个作用域
命名空间的每个名字都必须表示该空间内的唯一实体。因为不同命名空间的作用域不同,所以在不同命名空间内可以有相同名字的成员。
定义在某个命名空间中的名字可以被该命名空间内的其他成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须指出所用的名字属于哪个命名空间。
2)命名空间可以是不连续的
命名空间可以定义在几个不同的部分。
1 #include <iostream> 2 #include <string> 3 4 namespace nsp { 5 int x = 100; 6 } 7 namespace nsp { 8 int y = 100; 9 } 10 int main() 11 { 12 std::cout << nsp::x << ", " << nsp::y << std::endl; 13 return 0; 14 }
如果之前没有nsp的命名空间定义,则创建一个新的命名空间;否则,打开已存在的命名空间定义并为其添加一些新成员的声明。
命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似于我们管理自定义类及函数的方式:
- 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。
- 命名空间成员的定义部分则置于另外的源文件中。
3)定义命名空间的例子
使用接口与实现分离的机制的例子。
1 #pragma once 2 #include <string> 3 4 namespace nsp { 5 class Base { 6 public: 7 Base(const std::string &); 8 void show(); 9 private: 10 std::string s; 11 }; 12 }
1 #include <string> 2 #include <iostream> 3 #include "Base.h" 4 5 namespace nsp { 6 Base::Base(const std::string &_s):s(_s){} 7 8 void Base::show() { 9 std::cout << s << std::endl; 10 } 11 }
1 #include <iostream> 2 #include <string> 3 #include "header/Base.h" 4 int main() 5 { 6 nsp::Base b("hello"); 7 b.show(); 8 return 0; 9 }
注意:在通常情况下,我们不把#include放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。
4)定义命名空间成员
5)模板特例化
模板特例化必须定义在原始模板所属的命名空间中。
6)全局命名空间
全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间中。全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字被隐式地添加到全局命名空间中。
作用域运算符同样可以用于全局作用域的成员,因为全局作用域时隐式的,所以它并没有名字。下面的形式:
::mem
表示全局命名空间中的一个成员。
7)嵌套的命名空间
嵌套的命名空间是指定义在其他命名空间中的命名空间。
嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码想要访问它必须在名字前添加限定符。
1 #include <iostream> 2 #include <string> 3 4 namespace X { 5 namespace Y { 6 std::string s = "hello"; 7 } 8 } 9 int main() 10 { 11 std::cout << X::Y::s << std::endl; 12 return 0; 13 }
8)内联命名空间
C++11新标准引入了一种新的嵌套命名规则,称为内联命名空间。内联命名空间中的名字可以被外层命名空间直接使用。
定义内联命名空间的方式是在关键字namespace前添加关键字inline:
1 #include <iostream> 2 #include <string> 3 4 inline namespace nsp { 5 std::string s = "hello"; 6 } 7 int main() 8 { 9 std::cout << s << std::endl; 10 return 0; 11 }
关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。
9)未命名的命名空间
未命名的命名空间是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。
一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间中可以定义相同的名字,并且这些定义表示的是不同实体。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。
定义在未命名的命名空间中的名字可以直接使用,毕竟我们找不到什么命名空间的名字来限定它们。
未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。未命名的命名空间中的名字一定要与其外层的作用域中的名字区分开,不然有二义性。
1 #include <iostream> 2 #include <string> 3 4 namespace { 5 std::string s = "hello"; 6 } 7 int main() 8 { 9 std::cout << s << std::endl; 10 return 0; 11 }
2、使用命名空间成员
1)命名空间的别名
命名空间的别名使得我们可以为命名空间的名字设定一个短得多得同义词。命名空间的别名声明以关键字namespace开始,后面是别名所用的名字、=符号、命名空间原来的名字以及分号。
命名空间的别名也可以指向一个嵌套的命名空间。一个命名空间可以有好几个别名。
1 #include <iostream> 2 #include <string> 3 4 namespace X{ 5 std::string s = "hello"; 6 } 7 int main() 8 { 9 namespace Y = X; 10 std::cout << Y::s << std::endl; 11 return 0; 12 }
2)using声明:扼要概述
一条using声明语句一次只引入命名空间的一个成员。它使得我们可以清楚地知道程序中所用的到底是哪个名字。
using声明引入的名字遵循作用域规则:它的有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。未加限定的名字只能在using声明所在的作用域及其内层作用域中使用。在有效作用域结束后,我们就必须使用完整的经过限定的名字了。
using声明可以出现在全局作用域、局部作用域、命名空间作用域以及类的作用域中。在类的作用域中,这样的声明语句只能指向基类成员。
3)using指示
using指示使得某个特定的命名空间中所有的名字都可见,这样我们就无须再为它们添加任何前缀提示符了。简写的名字从using开始,一直到using指示所在的作用域结束都能使用。
using指示以关键字using开始,后面是关键字namespace以及命名空间的名字。using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中。
4)头文件与using声明或指示
头文件如果在其顶层作用域中含有using指示或声明,则会将名字注入到所有包含了该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。
3、类、命名空间与作用域
对命名空间内部名字的查找规则:即由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。只有位于开发的块中且在使用点之前声明的名字才被考虑。
对于命名空间的类来说,查找规则:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找。
1)实参相关的查找与类类型形参
对于命名空间中名字的隐藏规则有一个重要的例外:当我们给函数传递给一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。
4、重载与命名空间
1)与实参相关的查找与重载
对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行。这条规则对于我们如何确定候选函数集同样有影响。我们将在每个实参类(以及实参的基类)所属的命名空间中搜寻候选函数。在这些命名空间中所有与被调用函数同名的函数都将被添加到候选集中,即使其中某些函数在调用语句处不可见也是如此。
2)重载与using声明
using声明语句声明的是一个名字,而非一个特定的函数。当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。
一个using声明引入的函数将重载该声明语句所属作用域中已有的其他同名函数。如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域已经有一个函数与新引入的函数同名且形参列表相同,则该using声明将引发错误。除此之外,using声明将为引入的名字添加额外的重载实例,并最终扩展候选函数集的规模。
3)重载与using指示
using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中。
对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。
4)跨越多个using指示的重载
如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分。