目录
9.3 名称空间
在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。当随着项目的增大,名称相互冲突的可能性也将增加。使用多个厂商的类库时,可能导致名称冲突。例如,两个库可能都定义了名为List、Tree和Node的类,但定义的方式不兼容。用户可能希望使用一个库的List类,而使用另一个库的Tree类。这种冲突被称为名称空间问题。
C++标准提供了名称空间工具,以便更好地控制名称的作用域。经过了一段时间后,编译器才支持名称空间,但现在这种支持很普遍。
9.3.1 传统的C++名称空间
介绍C++中新增的名称空间特性之前,先复习一下C++中已有的名称空间属性,并介绍一些术语,让读者熟悉名称空间的概念。
第一个需要知道的术语是声明区域(declaration region)。声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其声明所在的代码块。
第二个需要知道的术语是潜在的作用域(potential scope)。变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
然而,变量并非在其潜在作用域内的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。例如,在函数中声明的局部变量(对于这种变量,声明区域为整个函数)将隐藏在同一个文件中声明的全局变量(对于这种变量,声明区域为整个文件)。变量对程序而言可见的范围被称为作用域(scope),前面正是以这种方式使用该术语的。下图对术语声明区域、潜在作用域和作用域进行了说明。
C++关于全局变量和局部变量的规则定义了一种名称空间层次。每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。
9.3.2 新的名称空间特性
C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。例如,下面的代码使用新的关键字namespace创建了两个名称空间:Jack和Jill。
namespace Jack {
double pail; // variable declaration
void fetch(); // function prototype
int pal; // variable declaration
struct Well { ... }; // structure declaration
}
namespace Jill {
double bucket(double n) { ... }; // function prototype
double fetch; // variable declaration
int pal; // variable declaration
struct Hill { ... }; // structure declaration
}
名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。
除了用户定义的名称空间外,会存在另一个名称空间——全局名称空间(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::pail = 12.34; // use a variable
Jill::Hill mole; // create a type structure
Jack::fetch(); // use a function
未被装饰的名称(如pail)称为未限定的名称(unqualified name);包含名称空间的名称(如Jack::pail)称为限定的名称(qualified name)。
9.3.2.1 using声明和using编译指令
我们并不希望每次使用名称时都对它进行限定,因此C++提供了两种机制(using声明和using编译指令)来简化对名称空间中名称的使用。using声明使特定的标识符可用,using编译指令使整个名称空间可用。
using声明由被限定的名称和它前面的关键字using组成:
using Jill::fetch; // a using declaration
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; // put fetch into local namespace
double fetch; // Error! Already have a local fetch
cin >> fetch; // read a value into Jill::fetch
cin >> ::fetch; // read a value into global fetch
...
}
由于using声明将名称添加到局部声明区域中,因此这个示例避免了将另一个局部变量也命名为fetch。另外,和其他局部变量一样,fetch也将覆盖同名的全局变量。
在函数的外面使用using声明时,将把名称添加到全局名称空间中:
void other();
namespace Jill {
double bucket(double n) { ... }
double fetch;
struct Hill { ... };
}
using Jill::fetch; // put fetch into global namespace
int main()
{
cin >> fetch; // read a value into Jill::fetch
other();
...
}
void other()
{
cout << fetch; // display Jill::fetch
...
}
using声明使一个名称可用,而using编译指令使所有的名称都可用。using编译指令由名称空间名和它前面的关键字using namespace组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符:
using namespace Jack; // make all the names in Jack available
在全局声明区域中使用using编译指令,将使该名称空间的名称全局可用。这种情况已出现过多次:
#include<iostream> // places names in namespace std
using namespace std; // make names available globally
在函数中使用using编译指令,将使其中的名称在该函数中可用,下面是一个例子:
int main()
{
using namespace Jack; // make names available in Jack
...
}
在本书前面中,经常将这种格式用于名称空间std。
有关using编译指令和using声明,需要记住的一点是,它们增加了名称冲突的可能性。也就是说,如果有名称空间Jack和Jill,并在代码中使用作用域解析运算符,则不会存在二义性:
Jack::pal = 3;
Jill::pal = 10;
变量Jack::pal和Jill::pal是不同的标识符,表示不同的内存单元。然而,如果使用using声明,情况将发生变化:
using Jack::pal;
using Jill::pal;
pal = 4; // which one? now have a conflict
事实上,编译器不允许您同时使用上述两个using声明,因为这将导致二义性。
9.3.2.2 using编译指令和using声明之比较
使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析运算符。使用using声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而,使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。在下面的示例中,名称空间为全局的。如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。不过仍可以像下面的示例中那样使用作用域解析运算符:
namespace Jill {
double bucket(double n) { ... }
double fetch;
struct Hill { ... };
}
char fetch; // global namespace
int main()
{
using namespace Jill; // import all namespace names
Hill Thrill; // create a type Jill::Hill structure
double water = bucket(2); // use Jill::bucket();
double fetch; // not an error; hides Jill::fetch
cin >> fetch; // read a value into the local fetch
cin >> ::fetch; // read a value into global fetch
cin >> Jill::fetch; // read a value into Jill::fetch
...
}
int foom()
{
Hill top; // ERROR
Jill::Hill crest; // valid
}
在main()中,名称Jill::fetch被放在局部名称空间中,但其作用域不是局部的,因此不会覆盖全局的fetch。然而,局部声明的fetch将隐藏Jill::fetch和全局fetch。然而,如果使用作用域解析运算符,则后两个fetch变量都是可用的。读者应将这个示例与前面使用using声明的示例进行比较。
需要指出的另一点是,虽然函数中的using编译指令将名称空间的名称视为在函数之外声明的,但它不会使得该文件中的其他函数能够使用这些名称。因此,在前一个例子中,foom()函数不能使用未限定的标识符Hill。
注意:
假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。
一般说来,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。
下面是本书的大部分示例采用的方法:
#include<iostream>
int main()
{
using namespace std;
首先,#include语句将头文件iostream放到名称空间std中。然后,using编译指令是该名称空间在main()函数中可用。有些示例采取下述方式:
#include<iostream>
using namespace std;
int main()
{
这将名称空间std中的所有内容导出到全局名称空间中。使得这种方法的主要原因是方便。它易于完成,同时如果系统不支持名称空间,可以将前两行替换为:
#include<iostream.h>
然而,名称空间的支持者希望有更多的选择,既可以使用解析运算符,也可以使用using声明。也就是说,不要这样做:
using namespace std; // avoid as too indiscriminate
而应这样做:
int x;
std::cin >> x;
std::cout << x << std::endl;
或者这样做:
using std::cin;
using std::cout;
using std::endl;
int x;
cin >> x;
cout << x << endl;
可以用嵌套式名称空间(将在下一节介绍)来创建一个包含常用using声明的名称空间。
9.3.2.3 名称空间的其他特性
可以将名称空间声明进行嵌套:
namespace elements
{
namespace fire
{
int flame;
...
}
float water;
}
这里,flame指的是element::fire::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::cin >> myth::fetch;
当然,由于它也位于Jill名称空间中,因此仍然可以称作Jill::fetch:
Jill::fetch;
std::cout << Jill::fetch; // display value read into myth::fetch
如果没有与之冲突的局部变量,则也可以这样做:
using namespace myth;
cin >> fetch; // really std::cin and Jill::fetch
现在考虑将using编译指令用于myth名称空间的情况。using编译指令是可传递的。如果A op B且B op C,则A op C,则说操作op是可传递的。例如,>运算符是可传递的(也就是说,如果A>B且B>C,则A>C)。在这个情况下,下面的语句将导入名称空间myth和elements:
using namespace myth;
这条编译指令与下面两条编译指令等价:
using namespace myth;
using namespace elements;
可以给名称空间创建别名。例如,假设有下面的名称空间:
namespace my_very_favorite_things{ ... };
则可以使用下面的语句让mvft成为my_very_favorite_things的别名:
namespace mvft = my_very_favorite_things;
可以使用这种技术来简化对嵌套名称空间的使用:
namespace MEF = myth::elements::fire;
using MEF::flame;
9.3.2.4 未命名的名称空间
可以通过省略名称空间的名称来创建未命名的名称空间:
namespace // unnamed namespace
{
int ice;
int bandycoot;
}
这就像后面跟着using编译指令一样,也就是说,在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。从这个方面看,它们与全局变量相似。然而,由于这种名称空间没有名称,因此不能显式地使用using编译指令或using声明来使它在其他位置都可用。具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。例如,假设有这样的代码:
static int counts; // static storage, internal linkage
int other();
int main()
{
...
}
int other()
{
...
}
采用名称空间的方法如下:
namespace
{
int counts; // static storage, internal linkage
}
int other();
int main()
{
...
}
int other()
{
...
}
9.3.3 名称空间示例
现在来看一个多文件示例,该示例说明了名称空间的一些特性。该程序的第一个文件(参见程序清单9.11)是头文件,其中包含头文件中常包含的内容:常量、结构定义和函数原型。在这个例子中,这些内容被放在两个名称空间中。第一个名称空间叫做pers,其中包含Person结构的定义和两个函数的原型——一个函数用人名填充结构,另一个函数显示结构的内容;第二个名称空间叫做debts,它定义了一个结构,该结构用来存储人名和金额。该结构使用了Person结构,因此,debts名称空间使用一条using编译指令,让pers中的名称在debts名称空间可用。debts名称空间也包含一些原型。
程序清单9.11 namesp.h
// namesp.h
#include<string>
// create the pers and debts namespaces
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所示。另外,文件namesp.cpp演示了如何使用using声明和作用域解析运算符来使名称空间std中的元素可用。
程序清单9.12 namesp.cpp
// namesp.cpp -- namespaces
#include <iostream>
#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演示了多种使名称空间标识符可用的方法。
程序清单9.13 namessp.cpp
// namessp.cpp -- using namespaces
#include<iostream>
#include "namesp.h"
void other(void);
void another(void);
int main(void)
{
using debts::Debt;
using debts::showDebt;
Debt golf = { {"Benny", "Goatsniff"},120.0 };
showDebt(golf);
other();
another();
return 0;
}
void other(void)
{
using std::cout;
using std::endl;
using namespace debts;
Person dg = { "Doodles", "Glister" };
showPerson(dg);
cout << endl;
Debt zippy[3];
int i;
for (i = 0; i < 3; i++)
getDebt(zippy[i]);
for (i = 0; i < 3; i++)
showDebt(zippy[i]);
cout << "Total debt: $" << sumDebts(zippy, 3) << endl;
return;
}
void another(void)
{
using pers::Person;
Person collector = { "Milo", "Rightshift" };
pers::showPerson(collector);
std::cout << std::endl;
}
在程序清单9.13中,main()函数首先使用了两个using声明:
using debts::Debt; // makes the Debt structure definition available
using debts::showDebt; // makes the showDebt function available
注意,using声明只使用了名称,例如,第二个using声明没有描述showDebt的返回类型或函数特征标,而只给出了名称;因此,如果函数被重载,则一个using声明将导入所有的版本。另外,虽然Debt和showDebt都使用了Person类型,但不必导入任何Person名称,因为debt名称空间有一条包含pers名称空间的using编译指令。
接下来,other()函数采用了一种不太好的方法,即使用一条using编译指令导入整个名称空间:
using namespace debts; // make all debts and pers names available to other()
由于debts中的using编译指令导入了pers名称空间,因此other()函数可以使用Person类型和showPerson()函数。
最后,another()函数使用using声明和作用域解析运算符来访问具体的名称:
using pers::Person;
pers::showPerson(collector);
下面是程序清单9.11~程序清单9.13组成的程序的运行情况:
Goatsniff, Benny: $120
Glister, Doodles
Enter first name: Arabella
Enter last name: Binx
Enter debt: 100
Enter first name: Cleve
Enter last name: Delaproux
Enter debt: 120
Enter first name: Eddie
Enter last name: Fiotox
Enter debt: 200
Binx, Arabella: $100
Delaproux, Cleve: $120
Fiotox, Eddie: $200
Total debt: $420
Rightshift, Milo
9.3.4 名称空间及其前途
随着程序员逐渐熟悉名称空间,将出现统一的编程理念。下面是当前的一些指导原则。
- 使用在已命名的名称空间中声明的变量,而不是使用外部局部变量。
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量。
- 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间std中,这种做法扩展到了来自C语言中的函数。例如,头文件math.h是与C语言兼容的,没有使用名称空间,但C++头文件cmath应将各种数学库函数放在名称空间std中。实际上,并非所有的编译器都完成了这种过渡。
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计。
- 不要在头文件中使用using编译指令。首先,这样做掩盖了要让哪些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。
- 导入名称时,首选使用作用域解析运算符或using声明的方法。
- 对于using声明,首选将其作用域设置为局部而不是全局。
别忘了,使用名称空间的主旨是简化大型编程项目的管理工作。对于只有一个文件的简单程序,使用using编译指令并非什么大逆不道的事。
正如前面指出的,头文件名的变化反映了这些变化。老式头文件(如iostream.h)没有使用名称空间,但新头文件iostream使用了std名称空间。