章节目录

命名冲突和名称空间简介

本节阅读量:

假设你第一次开车去朋友家,给你的地址是米尔城前街245号。到达米尔城后,拿出地图,却发现米尔城实际上有两条不同的前街,彼此隔城相望!去哪一个?除非有其他线索帮助你做出决定(例如,记得朋友的房子在河边),否则必须打电话给你的朋友,询问更多信息。由于这是混乱和低效的(特别是对于邮递公司),大多数国家,城市内的所有街道名称和家庭地址都需要唯一。

类似地,C++要求所有标识符都是不模糊的。如果编译器或链接器无法区分它们,将两个相同标识符引入同一程序,则编译器或链接器会提示程序出错。此错误称为命名冲突。

如果将冲突标识符引入同一文件,则是编译器错误。如果将冲突标识符引入到属于同一程序的不同文件中,则是链接错误。


命名冲突的示例

a.cpp:

1
2
3
4
5
6
#include <iostream>

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

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. 两个同名函数(或全局变量)在同一文件中存在,导致编译错误。

随着程序变得更大使用更多标识符,引入命名冲突的几率显著增加。好消息是C++提供了大量避免命名冲突的机制。局部作用域就是这样一种机制,可以防止不同函数内定义的局部变量相互冲突。但局部范围不适用于函数名。那么如何防止函数名相互冲突呢?


什么是命名空间?

回到开始地址类比,有两条前街是有问题的,因为这些街道存在于同一个城市中。另一方面,如果必须将邮件发送到两个地址,一个在米尔城的前街209,另一个在琼斯维尔的前街417。换句话说,城市名提供分组,消除可能彼此冲突的地址的歧义。

这个类比中,名称空间的作用类似于城市。

命名空间是一个区域,在其中声明名称,以消除歧义。命名空间为其内声明的名称提供了一个范围区域(称为命名空间范围)—— 意味着在命名空间内声明的任何名称都不会被误认为是其他范围内的相同名称。

在同一命名空间中,所有名称必须唯一,否则将导致命名冲突。

名称空间通常用于对大型项目中的相关标识符进行分组,以帮助确保不会无意中与其他标识符冲突。例如,如果将所有数学函数放在名为math的命名空间中,则数学函数不会与数学命名空间外同名函数冲突。

以后课程中,将讨论如何创建名称空间。


全局命名空间

在C++中,任何未在类、函数或命名空间内定义的名称都被认为是全局命名空间(有时也称为全局范围)的隐式定义命名空间的一部分。

本课顶部的示例中,函数main()和两个版本的myFcn()都是在全局命名空间中定义的。示例中的命名冲突是因为myFcn()的两个版本都在全局命名空间中存在,违反了命名空间中所有名称必须唯一的规则。

全局命名空间中只能出现声明和定义语句。意味着尽量避免在全局命名空间中定义变量。也意味着其他类型的语句(如表达式语句)不能放在全局命名空间中(全局变量的初始值设定项是例外)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream> // 被预处理器处理

// 下面所有的语句都是在全局命名空间中
void foo();    // 前向函数声明
int x;         // 可以编译通过但不推荐,全局命名空间中的未初始化变量
int y { 5 };   // 可以编译通过但不推荐,全局命名空间中的初始化变量
x = 5;         // 编译失败,可执行语句

int main()     // 函数定义
{
    return 0;
}

void goo();    // 前向函数声明

std命名空间

最初设计C++时,C++标准库中所有标识符(包括std::cin和std:∶cout)都可以在没有std::前缀的情况下使用(它们是全局命名空间的一部分)。然而,这意味着标准库中的任何标识符都可能与用户的标识符(也在全局命名空间中定义)冲突。当引用标准库中的新文件时,之前正常的代码可能会发生命名冲突。或者更糟,在C++的一个版本下编译的程序可能无法在未来的C++版本下编译,因为标准库引入的新标识符可能与用户已编写的代码发生命名冲突。因此,C++将标准库中的所有功能移动到名为“std”(standard的缩写)的命名空间中。

std::cout实际上只是cout,而std是标识符cout所属的命名空间的名称。因为cout是在std命名空间中定义,所以名称cout不会与全局命名空间中创建的名为cout的对象或函数冲突。

类似地,访问在名称空间中定义的标识符(例如,std::cout)时,需要告诉编译器,正在寻找在名称空间(std)内定义的标识符。

有几种不同的方法可以做到这一点。


显式命名空间限定符std::

告诉编译器从std命名空间中使用cout的最直接的方法是显式使用std::前缀。例如:

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

int main()
{
    std::cout << "Hello world!"; // 使用cout, 标明是在 std 命名空间中
    return 0;
}

「::」 符号是一个称为范围解析运算符。:: 符号左侧表示命名空间, :: 符号右侧则是在命名空间中的符号。如果 :: 符号左侧没有提供标识符,则认为是全局命名空间。

因此,使用std::cout时,是在说“使用命名空间std中的cout”。

这是使用cout的最安全的方法,因为引用的cout(std名称空间中)没有歧义。


using namespace(以及为什么避免使用它)

访问命名空间内标识符的另一种方法是使用using指令语句。下面是最初“Hello world”程序,带有using指令:

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

using namespace std; // 这条语句允许之后的代码,直接访问std命名空间中的符号,而不用加限定符

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

using指令可在不使用命名空间前缀的情况下访问命名空间中的名称。因此,上面示例中,编译器要确定标识符cout时,因为using指令,将与std::cout匹配。

许多教程甚至一些IDE建议在程序顶部使用using指令。然而,这种方式不推荐。

考虑以下程序:

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

using namespace std; // 让我们可以用 cout 访问 std::cout
 
int cout() // 在全局命名空间中定义cout
{
    return 5;
}
 
int main()
{
    cout << "Hello, world!"; // 编译失败!  因为有两个cout
 
    return 0;
}

上面程序不能编译,因为编译器不能确定使用定义的cout函数,还是使用在std命名空间中定义的cout。

以这种方式使用using指令时,定义的任何标识符都可能与std命名空间中的同名标识符冲突。更糟的是,也许标识符名称现在不会冲突,但可能会与未来的语言修订中添加到std命名空间的新标识符冲突。这就是将标准库中的所有标识符移动到std命名空间的好处!


2.7 多代码文件程序

上一节

2.9 预处理器简介

下一节