返回介绍

9.3 名称空间

发布于 2024-10-08 23:14:07 字数 11922 浏览 0 评论 0 收藏 0

在 C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大,名称相互冲突的可能性也将增加。使用多个厂商的类库时,可能导致名称冲突。例如,两个库可能都定义了名为 List、Tree 和 Node 的类,但定义的方式不兼容。用户可能希望使用一个库的 List 类,而使用另一个库的 Tree 类。这种冲突被称为名称空间问题。

C++标准提供了名称空间工具,以便更好地控制名称的作用域。经过了一段时间后,编译器才支持名称空间,但现在这种支持很普遍。

9.3.1 传统的 C++名称空间

介绍 C++中新增的名称空间特性之前,先复习一下 C++中已有的名称空间属性,并介绍一些术语,让读者熟悉名称空间的概念。

第一个需要知道的术语是声明区域(declaration region)。声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。

第二个需要知道的术语是潜在作用域(potential scope)。变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。

然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。变量对程序而言可见的范围被称为作用域(scope),前面正是以这种方式使用该术语的。图 9.5 和图 9.6 对术语声明区域、潜在作用域和作用域进行了说明。

C++关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

图 9.5 声明区域

图 9.6 潜在作用域和作用域

9.3.2 新的名称空间特性

C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。例如,下面的代码使用新的关键字 namespace 创建了两个名称空间:Jack 和 Jill。

名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。

除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。

任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此,Jack 中的 fetch 可以与 Jill 中的 fetch 共存,Jill 中的 Hill 可以与外部 Hill 共存。名称空间中的声明和定义规则同全局声明和定义规则相同。

名称空间是开放的(open),即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称 goose 添加到 Jill 中已有的名称列表中:

同样,原来的 Jack 名称空间为 fetch( ) 函数提供了原型。可以在该文件后面(或另外一个文件中)再次使用 Jack 名称空间来提供该函数的代码:

当然,需要有一种方法来访问给定名称空间中的名称。最简单的方法是,通过作用域解析运算符::,使用名称空间来限定该名称:

未被装饰的名称(如 pail)称为未限定的名称(unqualified name);包含名称空间的名称(如 Jack::pail)称为限定的名称(qualified name)。

1.using 声明和 using 编译指令

我们并不希望每次使用名称时都对它进行限定,因此 C++提供了两种机制(using 声明和 using 编译指令)来简化对名称空间中名称的使用。using 声明使特定的标识符可用,using 编译指令使整个名称空间可用。

using 声明由被限定的名称和它前面的关键字 using 组成:

using 声明将特定的名称添加到它所属的声明区域中。例如 main( ) 中的 using 声明 Jill::fetch 将 fetch 添加到 main( ) 定义的声明区域中。完成该声明后,便可以使用名称 fetch 代替 Jill::fetch。下面的代码段说明了这几点:

由于 using 声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为 fetch。另外,和其他局部变量一样,fetch 也将覆盖同名的全局变量。

在函数的外面使用 using 声明时,将把名称添加到全局名称空间中:

using 声明使一个名称可用,而 using 编译指令使所有的名称都可用。using 编译指令由名称空间名和它前面的关键字 using namespace 组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符:

在全局声明区域中使用 using 编译指令,将使该名称空间的名称全局可用。这种情况已出现过多次:

在函数中使用 using 编译指令,将使其中的名称在该函数中可用,下面是一个例子:

在本书前面中,经常将这种格式用于名称空间 std。

有关 using 编译指令和 using 声明,需要记住的一点是,它们增加了名称冲突的可能性。也就是说,如果有名称空间 jack 和 jill,并在代码中使用作用域解析运算符,则不会存在二义性:

变量 jack::pal 和 jill::pal 是不同的标识符,表示不同的内存单元。然而,如果使用 using 声明,情况将发生变化:

事实上,编译器不允许您同时使用上述两个 using 声明,因为这将导致二义性。

2.using 编译指令和 using 声明之比较

使用 using 编译指令导入一个名称空间中所有的名称与使用多个 using 声明是不一样的,而更像是大量使用作用域解析运算符。使用 using 声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用 using 声明导入相同的名称。然而,使用 using 编译指令时,将进行名称解析,就像在包含 using 声明和名称空间本身的最小声明区域中声明了名称一样。在下面的示例中,名称空间为全局的。如果使用 using 编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。不过仍可以像下面的示例中那样使用作用域解析运算符:

在 main( ) 中,名称 Jill::fetch 被放在局部名称空间中,但其作用域不是局部的,因此不会覆盖全局的 fetch。然而,局部声明的 fetch 将隐藏 Jill::fetch 和全局 fetch。然而,如果使用作用域解析运算符,则后两个 fetch 变量都是可用的。读者应将这个示例与前面使用 using 声明的示例进行比较。

需要指出的另一点是,虽然函数中的 using 编译指令将名称空间的名称视为在函数之外声明的,但它不会使得该文件中的其他函数能够使用这些名称。因此,在前一个例子中,foom( ) 函数不能使用未限定的标识符 Hill。

注意:

假设名称空间和声明区域定义了相同的名称。如果试图使用 using 声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用 using 编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。

一般说来,使用 using 声明比使用 using 编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using 编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

下面是本书的大部分示例采用的方法:

首先,#include 语句将头文件 iostream 放到名称空间 std 中。然后,using 编译指令是该名称空间在 main( ) 函数中可用。有些示例采取下述方式:

这将名称空间 std 中的所有内容导出到全局名称空间中。使用这种方法的主要原因是方便。它易于完成,同时如果系统不支持名称空间,可以将前两行替换为:

然而,名称空间的支持者希望有更多的选择,既可以使用解析运算符,也可以使用 using 声明。也就是说,不要这样做:

而应这样做:

或者这样做:

可以用嵌套式名称空间(将在下一节介绍)来创建一个包含常用 using 声明的名称空间。

3.名称空间的其他特性

可以将名称空间声明进行嵌套:

这里,flame 指的是 element::fire::flame。同样,可以使用下面的 using 编译指令使内部的名称可用:

另外,也可以在名称空间中使用 using 编译指令和 using 声明,如下所示:

假设要访问 Jill::fetch。由于 Jill::fetch 现在位于名称空间 myth(在这里,它被叫做 fetch)中,因此可以这样访问它:

当然,由于它也位于 Jill 名称空间中,因此仍然可以称作 Jill::fetch:

如果没有与之冲突的局部变量,则也可以这样做:

现在考虑将 using 编译指令用于 myth 名称空间的情况。using 编译指令是可传递的。如果 A op B 且 B op C,则 A op C,则说操作 op 是可传递的。例如,>运算符是可传递的(也就是说,如果 A>B 且 B>C,则 A>C)。在这个情况下,下面的语句将导入名称空间 myth 和 elements:

这条编译指令与下面两条编译指令等价:

可以给名称空间创建别名。例如,假设有下面的名称空间:

则可以使用下面的语句让 mvft 成为 my_very_favorite_things 的别名:

可以使用这种技术来简化对嵌套名称空间的使用:

4.未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间:

这就像后面跟着 using 编译指令一样,也就是说,在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。从这个方面看,它们与全局变量相似。然而,由于这种名称空间没有名称,因此不能显式地使用 using 编译指令或 using 声明来使它在其他位置都可用。具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。例如,假设有这样的代码:

采用名称空间的方法如下:

9.3.3 名称空间示例

现在来看一个多文件示例,该示例说明了名称空间的一些特性。该程序的第一个文件(参见程序清单 9.11)是头文件,其中包含头文件中常包含的内容:常量、结构定义和函数原型。在这个例子中,这些内容被放在两个名称空间中。第一个名称空间叫做 pers,其中包含 Person 结构的定义和两个函数的原型——一个函数用人名填充结构,另一个函数显示结构的内容;第二个名称空间叫做 debts,它定义了一个结构,该结构用来存储人名和金额。该结构使用了 Person 结构,因此,debts 名称空间使用一条 using 编译指令,让 pers 中的名称在 debts 名称空间可用。debts 名称空间也包含一些原型。

程序清单 9.11 namesp.h

第二个文件(见程序清单 9.12)是源代码文件,它提供了头文件中的函数原型对应的定义。在名称空间中声明的函数名的作用域为整个名称空间,因此定义和声明必须位于同一个名称空间中。这正是名称空间的开放性发挥作用的地方。通过包含 namesp.h(参见程序清单 9.11)导入了原来的名称空间。然后该文件将函数定义添加入到两个名称空间中,如程序清单 9.12 所示。另外,文件 names.cpp 演示了如何使用 using 声明和作用域解析运算符来使名称空间 std 中的元素可用。

程序清单 9.12 namesp.cpp

最后,该程序的第三个文件(参见程序清单 9.13)是一个源代码文件,它使用了名称空间中声明和定义的结构和函数。程序清单 9.13 演示了多种使名称空间标识符可用的方法。

程序清单 9.13 namessp.cpp

在程序清单 9.13 中,main( ) 函数首先使用了两个 using 声明:

注意,using 声明只使用了名称,例如,第二个 using 声明没有描述 showDebt 的返回类型或函数特征标,而只给出了名称;因此,如果函数被重载,则一个 using 声明将导入所有的版本。另外,虽然 Debt 和 showDebt 都使用了 Person 类型,但不必导入任何 Person 名称,因为 debt 名称空间有一条包含 pers 名称空间的 using 编译指令。

接下来,other( ) 函数采用了一种不太好的方法,即使用一条 using 编译指令导入整个名称空间:

由于 debts 中的 using 编译指令导入了 pers 名称空间,因此 other( ) 函数可以使用 Person 类型和 showPerson( ) 函数。

最后,another( ) 函数使用 using 声明和作用域解析运算符来访问具体的名称:

下面是程序清单 9.11~程序清单 9.13 组成的程序的运行情况:

9.3.4 名称空间及其前途

随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间 std 中,这种做法扩展到了来自 C 语言中的函数。例如,头文件 math.h 是与 C 语言兼容的,没有使用名称空间,但 C++头文件 cmath 应将各种数学库函数放在名称空间 std 中。实际上,并非所有的编译器都完成了这种过渡。
  • 仅将编译指令 using 作为一种将旧代码转换为使用名称空间的权宜之计。
  • 不要在头文件中使用 using 编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令 using,应将其放在所有预处理器编译指令#include 之后。
  • 导入名称时,首选使用作用域解析运算符或 using 声明的方法。
  • 对于 using 声明,首选将其作用域设置为局部而不是全局。

别忘了,使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用 using 编译指令并非什么大逆不道的事。

正如前面指出的,头文件名的变化反映了这些变化。老式头文件(如 iostream.h)没有使用名称空间,但新头文件 iostream 使用了 std 名称空间。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文