C++名字空间
基本概念
名字空间本质上是自定义作用域,由于C++设计的初衷是开发大规模软件,大量的软件库必然会加剧全局符号(变量、函数)的冲突,因此名字空间最基本的作用就是给不同的库和模块拥有自己的独特的作用域,处于不同名字空间中的重名符号相安无事,互不冲突,以此来大大提高编程的便利性。
1.1 定义与使用
定义一个名字空间,实际上就是定义一个作用域,在名字空间中可以定义变量、函数等,示例代码如下:
// ns.cpp
// 定义一个名字空间,叫ns
namespace ns
{
// 在 ns 中定义变量
int a = 1;
// 在 ns 中定义函数
float f(int x)
{
return x/2;
}
}
在以上名字空间 ns 中,定义了一个变量 a 和一个函数 f,但实际上它们的名字是 ns::a 和 ns::f,比如如下示例代码显示了如何正确地引用它们:
// ns.h
namespace ns
{
// 对名字空间 ns 中的符号进行声明
extern int a;
extern float f(int x);
}
// main.cpp
#include "ns.h"
int main()
{
// 调用名字空间中的符号,使用全称
cout << ns::a << endl;
cout << ns::f(8) << endl;
}
此处出现了一个新的操作符 ==> :: ,其用法是:
名字空间::某符号
类::某符号
这个双冒号的操作符,称为作用域引用符,很显然,双冒号前面必须是一个作用域,在C++中,除了名字空间是作用域之外,后续会讲到的类也是最常见的作用域。
很显然,将全局变量 a 和函数 f() 放在名字空间中之后,可以极大避免由于不同程序文件或库的重名而引起的冲突。例如,在另外一个名字空间中,出现跟 ns 一样的变量或函数,它们一起使用相安无事:
// another_ns.cpp
namespace another_ns
{
int a = 100;
}
// ns.h
namespace ns
{
extern int a;
extern float f(int x);
}
namespace another_ns
{
extern int a;
}
// main.cpp
#include <iostream>
#include "ns.h"
int main(void)
{
cout << ns::a << endl; // 输出1
cout << another_ns::a << endl; // 输出100
}
另外值得注意的是,名字空间是对变量和函数的定义的作用域规定范围,因此是出现在源文件 *.cpp 中的,而对这些符号的声明,跟原来做法的一样 —— 在对应的头文件中进行声明,只不过在带有名字空间的场合中,头文件的声明语句也同样要包含在名字空间中。
1.2 using语句
在上述代码 main.cpp 中,使用了全称 ns::a 和 ns:f 来引用符号,在实际应用中很显然是很不方便的,有没有办法不需要重复写名字空间的名字 ns 也能使用里面定义的符号呢?答案是肯定的,只需要使用 using 语句即可,比如上述源码 main.cpp 可改成如下形式:
// main.cpp
#include "ns.h"
using namespace ns; // 导入名字空间:ns
int main()
{
// 调用名字空间 ns 中的符号,不再需要写全名了
cout << a << endl;
cout << f(8) << endl;
}
对上述代码,需要强调的一点是,using 语句其实有两种形式:
// 形式一:导入整个名字空间中的所有符号
using namespace ns;
// 形式二:导入名字空间中的指定符号
using ns::a;
using ns::f;
由于在上述例子中,名字空间 ns 仅仅包含极少量符号,因此不管采用哪种形式的 using 语句都没有什么区别,但如果某个名字空间包含大量符号(比如标准名字空间std),而程序中仅需用到其中的少量符号,那么导入整个名字空间的所有符号的做法也许是不明智的,因为这会使得大量未被使用的符号成为潜在的符号冲突候选人,这种情形被称为名字空间污染,因此,实际编码中我们应在追求便利的同时,尽量避免引入不使用的符号。
2. 进阶语法
2.1 内嵌名字空间
C++允许嵌套定义名字空间,即一个名字空间内部再出现另一个名字空间,这其实是作用域的常规特性,早在C语言时代就可以有嵌套的作用域的概念,只不过C语言中的作用域都是匿名的,而C++给这些作用域赋予了特定的名字。
// ns.cpp
namespace ns
{
int a = 1; // 注意,此处a的全称是 ns::a
// 在名字空间中嵌套另一个名字空间
namespace nested_ns
{
int a = 2; // 注意,此处a的全称是 ns::nested_ns::a
int x = 100;
}
}
声明与使用:
// ns.h
namespace ns
{
extern int a;
namespace nested_ns
{
extern int a;
extern int x;
}
}
// main.cpp
#include "ns.h"
int main()
{
cout << ns::a << endl;
cout << ns::nested_ns::a << endl;
cout << ns::nested_ns::x << endl;
}
2.2 扩展性
// main.cpp
#include <iostream>
#include "ns.h"
namespace ns
{
// 在名字空间 ns 中增添一个新的符号b
int b = 666;
}
using namespace ns;
int main(void)
{
cout << a << endl; // 访问名字空间 ns 中原有的符号 a
cout << b << endl; // 访问名字空间 ns 中新增的符号 b
}
当程序在多处定义了相同的名字空间时,它们将会融合成一个统一的作用域。如:
// main.cpp
#include <iostream>
#include "ns.h"
namespace ns
{
// 在名字空间 ns 中增添一个新的符号b
int b = 666;
}
using namespace ns;
int main(void)
{
cout << a << endl; // 访问名字空间 ns 中原有的符号 a
cout << b << endl; // 访问名字空间 ns 中新增的符号 b
}
2.3 全局作用域
全局作用域是从C语言就开始有的一种作用域,在C++中,有时为了强调某符号的全局特性,或为了避免与导入的名字空间中的重名符号冲突,会在使用全局符号的时候加上 作用域解析符
示例代码:
int global = 100;
int main()
{
int global = 200;
// 重名的标识符,外层的作用域会被内层的掩盖
cout << global << endl; // 输出200
// 使用双冒号引用全局作用域中的标识符
cout << ::global << endl; // 输出100
}
全局作用域的名字空间是匿名的,引用全局作用域符号只需加 :: 即可。
名字空间的本质就是作用域,遵守C语言关于作用域的基本原则,如内层作用域重名符号会掩盖外层作用域的重名符号。
3. 小结
自定义名字空间,实际上是将原来C语言中的全局作用域做了更加细致的规划,在原先的全局作用域中,人为地将某个区域内的符号(变量、对象、函数)命个名圈起来,避免与全局作用域中的其余符号冲突。
如果不可避免地要引入不同的名字空间(比如space1和space2),并且不同的名字空间中恰巧有重名的符号(比如var),那么引用方只需将符号带上其所属的名字空间的名称即可(比如space1::var 和 space2::var)。这大大拓展了大型程序对不同函数库符号引入的灵活性,大大缓解了符号的冲突的可能。
引入名字空间的初衷,是解决大型软件中的各个不同产商提供的第三方库的名字冲突问题,这是从C语言到C++的一个重大的改变。