13. 名称空间
一、什么是名称空间
在 C++ 中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大,名称相互冲突的可能性也将增加。使用多个厂家的类库时,可能导致名称冲突。这种冲突被称为名称空间问题。C++ 标准提供了名称空间工具,以便更好地控制名称的作用域。
在介绍名称空间之前,我们先介绍一些专用术语:
- 声明区域(declaration region):可以在其中进行声明的区域。
- 潜在作用域(potential scpre):变量的潜在作用域从声明点开始到其声明区域结尾。
潜在作用域比声明区域小,这时由于变量必须定义后才能使用。然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它有可能被另一个在嵌套声明区域中的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。变量对程序而言,可见的范围被称为 作用域。
C++ 关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其它声明区域中声明的名称。在一个函数中声明的局部变量不会在另一个函数中生命的局部变量发生冲突。
二、定义名称空间
C++ 中新增了这样一种功能,即通过定义一种新的声明空间区域来创建命名的名称空间。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其它部分使用该名称空间中声明的东西。
namespace person
{
string name;
string gender;
int age;
string getInfo(void)
{
string info = "{name: " + name + ", gender: " + gender + ", age: " + to_string(age) + "}";
return info;
}
}
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。处理用户定义的名称空间外,还存在另一个名称空间 —— 全局名称空间(global namespace)。它对应于文件级声明区域,因此全局变量就被描述位于全局名称空间中。
任何名称空间中的名称都不会与其它名称空间中的名称发生冲突。并且名称空间是开放的,我们可以把名称加入到已有的名称空间中。
namespace person
{
string name;
string gender;
int age;
string getInfo(void);
}
string person::getInfo(void)
{
string info = "{name: " + name + ", gender: " + gender + ", age: " + to_string(age) + "}";
return info;
}
我们还可以把名称空间中的声明和实现分开。新建一个 person.h 头文件,用来存储名称空间的声明。
#include <iostream>
using namespace std;
namespace person
{
string name;
string gender;
int age;
string getInfo(void);
}
新建一个 person.cpp 源文件,用来存储名称空间的实现。
#include "person.h"
string person::getInfo(void)
{
string info = "{name: " + name + ", gender: " + gender + ", age: " + to_string(age) + "}";
return info;
}
名称空间的定义必须是全局的。
三、名称空间的使用
定义名称空间之后,我们可以通过作用域解析运算符 ::
使用名称空间来限定该名称。未被装饰的名称称为 未限定的名称(unqualified name),包含名称空间的名称称为 限定的名称(qualifed name)。
person::name = "Sakura";
person::gender = "女";
person::age = 10;
person::getInfo();
如果我们不希望每次使用名称时都对它进行限定,C++ 提供了两种机制(using 声明 和 using 编译指令)来简化对名称空间的使用。using 声明 使特定的标识符可用,using 编译指令 使整个名称空间可用。
3.1、using声明
using 声明将特定的名称添加到它所属的声明区域中。using 声明由被限定的名称和它前面的关键字 using 组成:
using person::name;
3.2、using编译指令
using 编译指令由名称空间和它前面的关键字 using namespace 组成,它使名称空间中的所有名称都可用,而不需要作用域解析运算符 :: 。
using namespace person;
3.3、using声明和using编译指令对比
使用 using 编译指令导入一个名称空间中所有的名称和使用多个 using 声明是不一样的,而更像是大量使用作用域解析运算符。使用 using 声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用 using 声明导入相同的名称。然而,使用 using 编译指令时,将进行名称解析,就像在包含 using 声明和名称空间本身的最小声明区域声明了名称一样。
假设名称空间和声明区域定义了相同的名称。如果试图使用 using 声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而错误。如果使用 using 编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
一般来说,使用 using 声明比使用 using 编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器会发出指示。using 编译指令导入所有名称,包含可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器不会发生警报。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
四、名称空间的其它特性
名称空间可以进行嵌套。
namespace singlechip
{
namespace stm32
{
string name;
int sram;
int flash;
}
}
在 C++ 17 中,对名称空间的嵌套定义进行了简化。
namespace singlechip::stn32
{
string name;
int sram;
int flash;
}
也可以在名称空间中使用 using 编译指令和 using 声明。
namespace MyNamespace
{
using person::name;
using namespace std;
}
还可以给名称空间创建别名。
namespace my_favorite_foods
{
string foods[10];
}
namespace foods = my_favorite_foods;
可以使用这种技术简化对嵌套名称空间的使用。
namespace stm32 = singlechip::stm32;
可以通过省略名称空间的名称来创建未命名的名称空间。
namespace
{
string name;
int age;
}
这就像后面跟着 using 编译指令一样,也就是说,在该名称空间中声明的名称潜在作用域为:从声明点到该声明区域末尾。从这个方面看,它们与全局变量相似。然而,由于这种名称空间没有名称,因此不能显示的使用 using 指令或 using 声明来使它在其它位置都可用。具体来说,不能在未命名名称空间所属文件之外的其它文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。
五、名称空间的用途
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量或静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++ 当前提倡将标准函数库放在名称空间 std 中,这种做法扩展到了来自 C 语言的函数。
- 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计。
- 不要在头文件中使用 using 编译指令。这样做掩盖了要让哪些名称可用。另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令 using,应将其放在所有预处理器编译指令 #include 之后。
- 导入名称时,首选使用作用域解析运算符或 using 声明的方法。
- 对于 using 声明,首选将其作用域设置为局部而不是全局。