Skip to content

2.9 - 命名冲突和命名空间

想象一下,你第一次开车去朋友家,他给你的地址是西红市金融街 245 号。你在去之前,百度地图了一下这个地址,结果发现西红市竟然有两条金融街。那么你应该去那条街的 245 号呢?除非你还有一些其他的信息(例如你印象中他的房子靠着一条江),不然你只能打电话给朋友问个清楚了。在现实中,如果一个程序中的街和门牌号不是唯一的,那么无疑会非常令人困惑,也会降低城市的运行效率(例如快递效率)。

类似的,C++需要保证其所有的标识符都是没有歧义的。如果一个程序中有两个相同的标识符,那么编译器(compiler)或者链接将无法区分它们,因此必然会报告错误。这个错误通常是关于命名冲突的。

命名冲突实例

a.cpp:

1
2
3
4
5
6
#include <iostream>

void myFcn(int x)
{
    std::cout << x;
}

COPY

main.cpp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <iostream>

void myFcn(int x)
{
    std::cout << 2 * x;
}

int main()
{
    return 0;
}

当编译器在编译此程序时,它会独立地编译 a.cpp 和 main.cpp 两个文件,并且不会有任何问题。

不过,当链接器开始工作时,它会把 a.cpp 和 main.cpp 中的定义关联起来,然后它就会发现  myFcn 函数的定义冲突了。随后链接器便会因为报错而停止工作。注意,即使 myFcn 始终没有被调用,错误仍然会发生!

大多数命名冲突由以下两种情况引起:

  1. 一个函数(或局部变量)的两个(或多个)定义被定义在了不同的文件中,但被编译到了同样的程序中。这样一来就会导致链接器报错,如上面描述的那样;
  2. 一个函数(或局部变量)的两个(或多个)定义被引入了同样的函数(通常是通过 #include 实现的)。这会导致编译器报错。

随着程序规模的增长,程序中的标识符也越来越多,发生命名冲突的概率也随之增长。好消息是,C++ 程序提供了很多的机制来避免命名冲突。局部作用域(scope)可以避免定义在函数内部的局部变量与其他变量发生命名冲突,这是其中一种机制。但是,该机制不能避免函数名的冲突。那么,应该如何避免函数名冲突呢?

命名空间是什么?

回想一下刚才有个地址的例子,两条同名的街之所以会带来麻烦,只因为它们是同一个城市中的两条街。如果你想要把信寄到西红市的金融街 209 和芒果市的金融街 417,那么根本不会有任何问题。换句话说,基于城市创建的分组可以避免同名街道产生的歧义。所谓的命名空间(namespace)就像这个例子中的城市一样。

为了避免歧义,我们可以选择在命名空间中声明一个变量名。命名空间可以为其中定义的名称提供一个作用域范围(称为命名空间作用域),简单来说,在一个命名空间中定义的名称,不会和其他作用域中同名的标识符产生冲突。

关键信息

在一个命名空间中定义的名称,不会和其他作用域中同名的标识符产生冲突。

在一个命名空间中,所有的名称都必须是唯一的,否则照样会产生冲突。

命名空间通常用于在大型项目中对变量名进行分组,以此来确保标识符(identifier)不会在不经意间与其他标识符发生冲突。例如,如果你将所有的数学函数都定义在 math 命名空间中,那么它们就不会和其他空间中相同名称的函数发生冲突。

我们会在后续的课程中介绍如何创建命名空间。

全局命名空间

在 C++ 中,任何没有被定义在类、函数或命名空间中的标识符,都属于一个隐含的命名空间,称为全局命名空间(有时候也称为全局作用域(scope))。

在文章开始的例子中,函数 main() 和两个版本的 myFcn() 都定义在全局命名空间中。这个例子中的命名冲突也是因为两个 myFcn() 都处于全局命名空间,也就违反了命名空间中标识符必须唯一的规则。

只有声明和定义语句可以出现在全局命名空间中。这也就意味着我们可以在全局命名空间中定义变量,尽管通常我们应该避免这么做(我们会在6.4 - 全局变量一节中详细介绍全局变量)。也同样也意味着,其他类型的语句(例如表达式)不能位于全局命名空间(全局变量的初始化是一个例外):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream> // handled by preprocessor

// 以下所有语句都位于全局命名空间
void foo();    // 可以这么做: 可以在全局命名空间中进行函数的前向声明
int x;         // 可以编译但强烈建议不要这么做:未初始化的变量被定义在全局命名空间中
int y { 5 };   // 可以编译但不推荐这么做:变量被定义在全局命名空间中且使用初始化值初始化
x = 5;         // 编译错误:可执行的语句不能放置在全局命名空间中

int main()     // 可以这么做:函数定义在全局命名空间中
{
    return 0;
}

void goo();    // 可以这么做: 在全局命名空间中对其他函数进行前向声明

std 命名空间

C++ 在最初设计时,所有 C++ 标准库中的标识符(包括std::cinstd::cout)都可以直接使用而无需添加std::前缀(都属于全局命名空间)。不过,这样也意味着标准库中的任何标识符都可能会和其他用户所使用的标识符(同样定义在全局命名空间)发生冲突。本来可以工作的代码,可能会因为#include了一个标准库中的新文件而出现命名冲突。更坏的是,基于老版本C++可以使用的代码,在新版本中可能无法使用,因为新添加到标准库中的标识符可能会与已有的代码发生命名冲突。因此,C++ 将所有标准库中的标识符都移动到了std命名空间中(std表示standard,即标准)。

实际上,std:: cout的名字并不是 std:: cout而是 coutstd只是它所处命名空间的限定符(qualifier)罢了。因为,cout定义在std命名空间中,因此它不会和其他任何被定义在全局命名空间中的cout发生冲突。

同样的,当需要使用定义在某个命名空间中的标识符时(例如 std::cout),你必须告诉编译器到哪个命名空间中去查找它(例如std)。

关键信息

当需要使用定义在某个命名空间中的标识符时(例如 std::cout),你必须告诉编译器该标识符被定义在命名空间中。

有两种方式可以完成该操作。

命名空间修饰符 std::

最直接的办法就是通过std::前缀告诉编译器,我们希望使用std命名空间中的cout,例如:

1
2
3
4
5
6
7
#include <iostream>

int main()
{
    std::cout << "Hello world!"; // when we say cout, we mean the cout defined in the std namespace
    return 0;
}

:: 符号实际上是一个运算符,称为空间解析运算符(scope resolution operator)。该运算符左侧是命名空间的名字,右侧则是该空间中包含的标识符。如果左侧未指定任何命名空间,则默认为全局命名空间。

因此,std:: cout表示cout被定义在命名空间std中。

通过这种方式使用 cout 是最安全的,因为不存在任何歧义(coutstd中唯一的)。

最佳实践

使用命名空间前缀来使用命名空间中的标识符。

using namespace std 以及为什么要避免这么做

另外一种使用命名空间中标识符的方式是使用 using 指令语句。以下例子基于该方法修改了原 “Hello world” 程序:

1
2
3
4
5
6
7
8
9
#include <iostream>

using namespace std; // 该using指令语句可以使我们访问标识符时无需使用命名空间前缀

int main()
{
    cout << "Hello world!";
    return 0;
}

using 指令语句可以使我们访问标识符时无需使用命名空间前缀。因此,在上面的例子中,当编译器需要确定 cout的定义时,它会匹配到std:: cout,在使用了using指令后,就可以以cout这种形式来使用该函数了。

很多文章、教程、甚至是一些编译器会建议在程序的开头使用using指令语句。其实,这并不是最佳实践,而且极为不推荐使用。

考虑下面这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <iostream> // 导入 `std::cout` 的定义。

using namespace std; // 使得 std::cout 能够以 "cout" 方式使用

int cout() // 在全局命名空间中定义我们的cout函数
{
    return 5;
}

int main()
{
    cout << "Hello, world!"; // 编译报错,这里使用的cout是我们定义的,还是std定义的?

    return 0;
}

上面的代码是不能够编译的,因为此时编译器无法区分我们想要使用自定义的cout还是std命名空间中的cout

当以这种方式使用using指令时,我们定义的任何名称都可能和std命名空间中定义的任何名称发生冲突。更糟糕的是,也许此时并没有冲突,但是在新版本的C++中,由于std中新加入了其他标识符,则可能导致命名冲突的发生。这也是将标准库中所有标识符都移动到了std命名空间中的初衷。

注意

应该在程序或头文件中避免使用 using 指令 (例如 using namespace std;),这么做就违背了使用命名空间的初衷。

相关内容

我们会在 6.12 - using 声明和 using 指令中介绍 using 语句以及如何使用它。