章节目录

命名冲突和名称空间简介

本节阅读量:

假设你第一次开车去朋友家,给你的地址是米尔城前街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的命名空间中,则数学函数不会与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 预处理器简介

下一节