名称空间在C中名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大名称相互冲突的可能性也将增加。使用多个厂商的类库时可能导致名称冲突。例如两个库可能都定义了名为List、Tree 和Node 的类但定义的方式不兼容。用户可能希望使用一个库的List 类而使用另一个库的Tree 类。这种冲突被称为名称空间问题。C标准提供了名称空间工具以便更好地控制名称的作用域。经过了一段时间后编译器才支持名称空间但现在这种支持很普遍。传统的C名称空间介绍C中新增的名称空间特性之前先复习一下C中已有的名称空间属性并介绍一些术语让读者熟悉名称空间的概念。第一个需要知道的术语是声明区域declaration region。声明区域是可以在其中进行声明的区域。例如可以在函数外面声明全局变量对于这种变量其声明区域为其声明所在的文件。对于在函数中声明的变量其声明区域为其声明所在的代码块。第二个需要知道的术语是潜在作用域potential scope。变量的潜在作用域从声明点开始到其声明区域的结尾。因此潜在作用域比声明区域小这是由于变量必须定义后才能使用。然而变量并非在其潜在作用域内的任何位置都是可见的。例如它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如在函数中声明的局部变量对于这种变量声明区域为整个函数将隐藏在同一个文件中声明的全局变量对于这种变量声明区域为整个文件。变量对程序而言可见的范围被称为作用域scope前面正是以这种方式使用该术语的。图9.5 和图9.6 对术语声明区域、潜在作用域和作用域进行了说明。C关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。新的名称空间特性C新增了这样一种功能即通过定义一种新的声明区域来创建命名的名称空间这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突同时允许程序的其他部分使用该名称空间中声明的东西。例如下面的代码使用新的关键字namespace 创建了两个名称空间Jack 和Jill。namespace Jack { doouble pail; void fetch(); int pal struct Well{...}; } namespace Jill { double bucket(double n){...} double fetch; int pal; struct Hill{...}; }名称空间可以是全局的也可以位于另一个名称空间中但不能位于代码块中。因此在默认情况下在名称空间中声明的名称的链接性为外部的除非它引用了常量。除了用户定义的名称空间外还存在另一个名称空间——全局名称空间global namespace。它对应于文件级声明区域因此前面所说的全局变量现在被描述为位于全局名称空间中。任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此Jack 中的fetch 可以与Jill中的fetch 共存Jill 中的Hill 可以与外部Hill 共存。名称空间中的声明和定义规则同全局声明和定义规则相同。名称空间是开放的open即可以把名称加入到已有的名称空间中。例如下面这条语句将名称goose添加到Jill 中已有的名称列表中namespace Jill { char *goose(const char *); }同样原来的Jack 名称空间为fetch( )函数提供了原型。可以在该文件后面或另外一个文件中再次使用Jack 名称空间来提供该函数的代码namespace Jack { void fetch() { ... } }当然需要有一种方法来访问给定名称空间中的名称。最简单的方法是通过作用域解析运算符::使用名称空间来限定该名称Jack::pail12.24; Jill::Hill mole; Jack::fetch();未被装饰的名称如pail称为未限定的名称unqualified name包含名称空间的名称如Jack::pail称为限定的名称qualified name。1using 声明和using 编译指令我们并不希望每次使用名称时都对它进行限定因此C提供了两种机制using 声明和using 编译指令来简化对名称空间中名称的使用。using 声明使特定的标识符可用using 编译指令使整个名称空间可用。using 声明由被限定的名称和它前面的关键字using 组成using Jill::fetch;using 声明将特定的名称添加到它所属的声明区域中。例如main( )中的using 声明Jill::fetch 将fetch 添加到main( )定义的声明区域中。完成该声明后便可以使用名称fetch 代替Jill::fetch。下面的代码段说明了这几点namespace Jill { double bucket(double n){...} double fetch; struct Hill{...}; } char fetch; int Main() { using Jill::fetch; double fetch; cinfetch; cin::fetch; ... }由于using 声明将名称添加到局部声明区域中因此这个示例避免了将另一个局部变量也命名为fetch。另外和其他局部变量一样fetch 也将覆盖同名的全局变量。在函数的外面使用using 声明时将把名称添加到全局名称空间中void other(); namespace Jill { double bucket(double n){...} double fetch; struct Hill{...}; } using Jill::fetch; int main() { cinfetch; other(); } void other() { coutfetch; ... }using 声明使一个名称可用而using 编译指令使所有的名称都可用。using 编译指令由名称空间名和它前面的关键字using namespace 组成它使名称空间中的所有名称都可用而不需要使用作用域解析运算符using namespace Jack;在全局声明区域中使用using 编译指令将使该名称空间的名称全局可用。这种情况已出现过多次#includeiostream using namespace std;在函数中使用using 编译指令将使其中的名称在该函数中可用下面是一个例子int main() { using namespace jack; ... }在本书前面中经常将这种格式用于名称空间std。有关using 编译指令和using 声明需要记住的一点是它们增加了名称冲突的可能性。也就是说如果有名称空间jack 和jill并在代码中使用作用域解析运算符则不会存在二义性jack::pal3; jill::pal10;变量jack::pal 和jill::pal 是不同的标识符表示不同的内存单元。然而如果使用using 声明情况将发生变化using jack::pal; using jill::pal; pal4事实上编译器不允许您同时使用上述两个using 声明因为这将导致二义性。2using 编译指令和using 声明之比较使用using 编译指令导入一个名称空间中所有的名称与使用多个using 声明是不一样的而更像是大量使用作用域解析运算符。使用using 声明时就好像声明了相应的名称一样。如果某个名称已经在函数中声明了则不能用using 声明导入相同的名称。然而使用using 编译指令时将进行名称解析就像在包含using 声明和名称空间本身的最小声明区域中声明了名称一样。在下面的示例中名称空间为全局的。如果使用using 编译指令导入一个已经在函数中声明的名称则局部名称将隐藏名称空间名就像隐藏同名的全局变量一样。不过仍可以像下面的示例中那样使用作用域解析运算符using Jill; namespace Jill { double bucket(double n){...} double fetch; struct Hill{...}; } char fetch; int main() { using namespace Jill; Hill Trill; double waterbacket(2); double fetch; cinfetch; cin::fetch; cinJill::fetch; ... } int foom() { Hill top; Jill::Hill crest; }在main( )中名称Jill::fetch 被放在局部名称空间中但其作用域不是局部的因此不会覆盖全局的fetch。然而局部声明的fetch 将隐藏Jill::fetch 和全局fetch。然而如果使用作用域解析运算符则后两个fetch 变量都是可用的。读者应将这个示例与前面使用using 声明的示例进行比较。需要指出的另一点是虽然函数中的using 编译指令将名称空间的名称视为在函数之外声明的但它不会使得该文件中的其他函数能够使用这些名称。因此在前一个例子中foom( )函数不能使用未限定的标识符Hill。注意假设名称空间和声明区域定义了相同的名称。如果试图使用using 声明将名称空间的名称导入该声明区域则这两个名称会发生冲突从而出错。如果使用using 编译指令将该名称空间的名称导入该声明区域则局部版本将隐藏名称空间版本。一般说来使用using 声明比使用using 编译指令更安全这是由于它只导入指定的名称。如果该名称与局部名称发生冲突编译器将发出指示。using 编译指令导入所有名称包括可能并不需要的名称。如果与局部名称发生冲突则局部名称将覆盖名称空间版本而编译器并不会发出警告。另外名称空间的开放性意味着名称空间的名称可能分散在多个地方这使得难以准确知道添加了哪些名称。下面是本书的大部分示例采用的方法#includeiostream int main() { using namespace std; }首先#include 语句将头文件iostream 放到名称空间std 中。然后using 编译指令是该名称空间在main( )函数中可用。有些示例采取下述方式#includeiostream using namespace std; int main() { }这将名称空间std 中的所有内容导出到全局名称空间中。使用这种方法的主要原因是方便。它易于完成同时如果系统不支持名称空间可以将前两行替换为#includeiostream.h然而名称空间的支持者希望有更多的选择既可以使用解析运算符也可以使用using 声明。也就是说不要这样做using namespace std;而应这样做int x; std::cinx std::coutxstd::endl;或者这样做using std::cin; using std::cout; using std::endl; int x; cinx; coutxendl;可以用嵌套式名称空间将在下一节介绍来创建一个包含常用using 声明的名称空间。3名称空间的其他特性可以将名称空间声明进行嵌套namespace elements { namespace fire { interface flame; ... } float water; }这里flame 指的是element::flame。同样可以使用下面的using 编译指令使内部的名称可用using namespace elements::fire另外也可以在名称空间中使用using 编译指令和using 声明如下所示namespace myth { using Jill::fetch; using namespace elements; using std::cout; using std::cin; }假设要访问Jill::fetch。由于Jill::fetch 现在位于名称空间myth在这里它被叫做fetch中因此可以这样访问它std::cinmyth::fetch;当然由于它也位于Jill 名称空间中因此仍然可以称作Jill::fetchJill::fetch: std::coutJill::fetch;如果没有与之冲突的局部变量则也可以这样做using namespace myth; cinfetch;现在考虑将using 编译指令用于myth 名称空间的情况。using 编译指令是可传递的。如果A op B 且B opC则A op C则说操作op 是可传递的。例如运算符是可传递的也就是说如果AB 且BC则AC。在这个情况下下面的语句将导入名称空间myth 和elementsusing namespace myth;这条编译指令与下面两条编译指令等价using namespace myth; using namespace elements;可以给名称空间创建别名。例如假设有下面的名称空间namespace my_very_favorite_things{...};则可以使用下面的语句让mvft 成为my_very_favorite_things 的别名namespace mvftmy_vert_facorite_things;可以使用这种技术来简化对嵌套名称空间的使用namespace MEFmyth::elements::fire; using MEF::flame;4.未命名的名称空间可以通过省略名称空间的名称来创建未命名的名称空间namespace{ int ice; interface bandycoot; }这就像后面跟着using 编译指令一样也就是说在该名称空间中声明的名称的潜在作用域为从声明点到该声明区域末尾。从这个方面看它们与全局变量相似。然而由于这种名称空间没有名称因此不能显式地使用using 编译指令或using 声明来使它在其他位置都可用。具体地说不能在未命名名称空间所属文件之外的其他文件中使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。例如假设有这样的代码static int counts; int other(); int main() { ... } int other() { ... }采用名称空间的方法如下namespace { int counts; } int other(); int main() { ... } int othher() { ... }名称空间示例现在来看一个多文件示例该示例说明了名称空间的一些特性。该程序的第一个文件参见程序清单9.11是头文件其中包含头文件中常包含的内容常量、结构定义和函数原型。在这个例子中这些内容被放在两个名称空间中。第一个名称空间叫做pers其中包含Person 结构的定义和两个函数的原型——一个函数用人名填充结构另一个函数显示结构的内容第二个名称空间叫做debts它定义了一个结构该结构用来存储人名和金额。该结构使用了Person 结构因此debts 名称空间使用一条using 编译指令让pers 中的名称在debts 名称空间可用。debts 名称空间也包含一些原型。#include iostream namespace pers { struct Person { std::string fname; std::string lname; }; void getPerson(Person); void showPerson(const Person); } namespace debts { using namespace pers; struct Debt { Person name; double amount; }; void getDebt(Debt); void showDebt(const Debt); double sumDebts(const Debt ar[], int n); }第二个文件见程序清单9.12是源代码文件它提供了头文件中的函数原型对应的定义。在名称空间中声明的函数名的作用域为整个名称空间因此定义和声明必须位于同一个名称空间中。这正是名称空间的开放性发挥作用的地方。通过包含namesp.h参见程序清单9.11导入了原来的名称空间。然后该文件将函数定义添加入到两个名称空间中如程序清单9.12 所示。另外文件names.cpp 演示了如何使用using声明和作用域解析运算符来使名称空间std 中的元素可用。#includeiostream #include namesp.h namespace pers { using std::cout; using std::cin; void getPerson(Person rp) { cout Enter first name:; cin rp.fname; cout Enter last name:; cin rp.lname; } void showPerson(const Person rp) { std::cout rp.lname , rp.fname; } } namespace debts { void getDebt(Debt rd) { getPerson(rd.name); std::cout Enter debt:; std::cin rd.amount; } void showDebt(const Debt rd) { showPerson(rd.name); std::cout :$ rd.amount std::endl; } double sumDebts(const Debt ar[], int n) { double total 0; for (int i 0; i n; i) total ar[i].amount; return total; } }最后该程序的第三个文件参见程序清单9.13是一个源代码文件它使用了名称空间中声明和定义的结构和函数。程序清单9.13 演示了多种使名称空间标识符可用的方法。名称空间及其前途随着程序员逐渐熟悉名称空间将出现统一的编程理念。下面是当前的一些指导原则。使用在已命名的名称空间中声明的变量而不是使用外部全局变量。使用在已命名的名称空间中声明的变量而不是使用静态全局变量。如果开发了一个函数库或类库将其放在一个名称空间中。事实上C当前提倡将标准函数库放在名称空间std 中这种做法扩展到了来自C 语言中的函数。例如头文件math.h 是与C 语言兼容的没有使用名称空间但C头文件cmath 应将各种数学库函数放在名称空间std 中。实际上并非所有的编译器都完成了这种过渡。仅将编译指令using 作为一种将旧代码转换为使用名称空间的权宜之计。不要在头文件中使用using 编译指令。首先这样做掩盖了要让哪些名称可用另外包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using应将其放在所有预处理器编译指令#include 之后。导入名称时首选使用作用域解析运算符或using 声明的方法。对于using 声明首选将其作用域设置为局部而不是全局。using 编译指令并非什么大逆不道的事。正如前面指出的头文件名的变化反映了这些变化。老式头文件如iostream.h没有使用名称空间但新头文件iostream 使用了std 名称空间。