题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。
作者: tyc611, 2007-03-01
本文主要讨论C++的名字空间机制及相关技术。
如果文中有错误或遗漏之处,敬请指出,谢谢!
名字空间定义
名字空间是一个作用域,其形式以关键字namespace开始,后接名字空间的名字,然后一对大括号内写上名字空间的内容。例如:
namespace test {
class Foo {
// ...
};
Foo operator+ (const Foo&, const Foo&);
int var;
}
名字空间可以在全局作用域或其他作用域内部定义,但不能在函数或类内部定义。可以在名字空间中放入可以出现在全局作用域的任意声明:类、变量(以及它们的初始化)、函数(以及它们的定义)、模板以及其他名字空间。
名字空间可以是不连续的,可以在几个部分中定义,而名字空间由它的分离定义的部分的总和构成,即名字空间是累积的。一个名字空间的分离部分可以分散在多个文件中,在不同文本文件中的名字空间定义也是累积的。但是,名字只在声明名字的文件中可见,这一常规限制继续应用,所以,如果名字空间的一个部分需要定义在另一文件中的名字,仍然必须声明该名字。
同时,名字空间的不连续性也意味着,可以用分离的接口文件和实现文件构成名字空间。因此,可以用与管理自己的类和函数定义相同的方法来组织名字空间:
1)定义类的名字空间成员,以及作为类接口的一部分的函数声明与对象声明,可以放在头文件中,使用名字空间成员的文件可以包含这些头文件;
2)名字空间成员的定义可以放在单独的源文件中。
在名字空间内部定义成员时,跟在全局范围内定义是一样的。也可以在名字空间定义的外部定义名字空间成员,用类似于在类外部定义类成员的方式:名字的名字空间声明必须在作用域中,并且定义必须指定该名字所属的名字空间。例如:
test::Foo test::operator+ (const Foo &lhs, const Foo &rhs){
Foo ret(lhs);
//...
}
这个定义看起来类似于定义在类外部的类成员函数,返回类型和函数名由名字空间限定。一旦看到完全限定的函数名,就处于名字空间的作用域中。因此,形参表和函数体中的名字空间成员引用可以使用非限定名引用。
定义在全局作用域的名字是定义在
全局名字空间(global namespace)中的。全局名字空间是隐式声明的,存在于每个程序中。在全局作用域定义实体的每个文件将那些名字加到全局名字空间中。因为全局名字空间是隐式的,没有名字,所以用作用域操作符直接引用全局名字空间的成员。
未命名的名字空间
名字空间也可以是未命名的,
未命名的名字空间(unnamed namespace)在定义时没有给定名字。未命名的名字空间与其他名字空间不同,未命名的名字空间的定义局限于特定文件,从不跨越多个文件。未命名的名字空间中定义的名字只在包含该名字空间的文件中可见,但其中的变量的生存期却从程序开始到程序结束。如果有多个文件包含未命名的名字空间,这些名字空间是不相关的,即使这些名字空间中定义了相同的名字,这些名字也代表不同的对象。未命名的名字空间中定义的名字可以直接使用,毕竟,没有名字空间来限定它们。未命名的名字空间中定义的名字可以在定义该名字空间所在的作用域中找到。如果在文件的最外层作用域中定义未命名的名字空间,那么,未命名的名字空间中的名字必须与全局作用域中的名字不同。特别注意:如果头文件中定义了未命名的名字空间,那么,在每个包含该头文件的文件中,该名字空间中的名字将定义不同的局部对象。
未命名的名字空间的这种静态特性,在C++中被用来取代C语言继承而来的文件中的静态声明。在标准C++中引入名字空间之前,程序必须将名字声明为static,使它们局部于一个文件。而引入名字空间后,C++不赞成文件静态声明。不赞成的特征也暗示着可能在将来的新标准中将不再支持这些特征,所以应该避免文件静态而使用未命名的名字空间代替。
名字空间成员的使用
除了利用namespace_name::member_name这种形式使用名字空间中的成员外,可以利用三种方式实现更简洁的使用,那就是using声明、名字空间别名和using指示。
using声明形如:using namespace_name::member_name;
一个using声明一次只引入一个名字空间成员,它使得无论程序中使用哪些名字,都能够非常明确。
using声明的作用域遵循常规作用域规则:从using声明点开始,直到包含该using声明的作用域的末尾,名字都是可见的;外部作用域中定义的同名对象被屏蔽。
using声明可以出现在全局作用域、局部作用域或者名字空间作用域中。类作用域中的using声明局限于被定义类的基类中定义的名字。
名字空间别名(namespace alias)将较短的名字与已存在的名字空间名相关联。语法:以关键字namespace开头,接(较短的)名字空间别名名字,再接=,再接原来的名字空间名字和分号。如果原来的名字空间名字是未定义的,则出错。名字空间别名还可以引用嵌套的名字空间。例如:
namespace QLib = Database::QueryLib;
using指示以关键字using开头,后接关键字namespace,再接名字空间名字。如果该名字不是已经定义的名字空间名字,则出错。using指示使得特定名字空间的所有名字可见,没有限制。
用using指示引入的名字的作用域比using声明的更复杂,它将名字空间成员提升到包含名字空间本身和using指示的最近作用域的效果。这样,该名字空间中的名字就可能与所提升到的作用域内的对象名发生冲突。而using声明将名字直接放入出现using声明的作用域,好像using声明是名字空间成员的
局部别名一样。所以,使用using声明是更好的选择。
实参相关的查找与类类型形参
对名字空间内部使用的名字的查找遵循常规C++查找规则,但有一个屏蔽名字空间名字规则的一个重要例外:接受类类型形参(或类类型指针及引用形参)的函数(包括重载操作符),以及与类本身定义在同一名字空间中的函数(包括重载操作符),在用类类型对象(或类类型的引用及指针)作为实参的时候是可见的(参见下面的重载与名字空间部分的例子)。例如:
std::string s;
// ok: calls std::getline(std::istream&, const std::string&)
getline(std::cin, s);
当编译器看到getline函数的使用getline(std::cin, s);的时候,它在当前作用域、包含调用的作用域以及定义cin的类型和string类型的名字空间中查找匹配的函数。因此,它在名字空间std中查找并找到由string类型定义的getline函数。
如果函数具有类类型形参就使得函数可见,其原因在于,允许无须单独的using声明就可以使用概念上作为类接口组成部分的非成员函数。能够使用非成员操作对操作符函数特别有用。例如:
std::string s;
cin >> s;
如果没有上面这个规则,我们必须将其编写为下面两种形式之一:
using std::operator >>; // neeed to allow cin >> s;
std::operator>>(std::cin, s); // ok: explicitly use std::>>
显然,没上面的规则,对于这样的IO操作是极不方便的。
隐式友元声明与名字空间
当一个类声明友元函数的时候,函数的声明不必是可见的。如果不存在可见的声明,那么,友元声明具有将该函数或类的声明放入外围作用域的效果。如果类在名字空间内部定义,则该友元函数相当于在这个名字空间中声明了该函数。例如:
namespace A {
class C {
friend void f(const C&); // makes f a member of namespace A
};
}
因为该友元接受类类型实参并与类隐式声明在同一名字空间中,所以使用它时可以无须使用显式名字空间限定符:
// f2 defined at global scope
void f2() {
A::C cobj;
f(cojb); // calls A::f
}
重载与名字空间
由于每个名字空间有自己的作用域,因此,作为两个不同名字空间的成员的函数不能互相重载。但是,给定名字空间可以包含一组重载函数成员。一般而言,名字空间内部的函数匹配以与我们已经见过的方式进行:
1)找到候选函数集合。如果一个函数在调用时其声明可见并且与被调用函数同名,这个函数就是候选者;
2)从候选集合中选择可行函数。如果函数的形参数目与函数调用的实参数目相同,并且每个形参都可用对应实参匹配,这个函数就是可行的。
3)从可行集合中选择一个最佳匹配,并产生代码调用该函数。如果可行集合为空,则调用出错,没有匹配;如果可行集合非空且没有最佳匹配,则调用有二义性。
名字空间对函数匹配有两个影响:一个影响是明显的,即可用using声明和using指示将函数加到候选集合;另一个是微秒的。如前面所介绍,有一个或多个类类型形参的函数的名字查找包括定义每个形参类型的名字空间。这个规则还影响怎样确定候选集合,为找候选函数而查找定义形参类(以及定义其基类)的每个名字空间,将那些名字空间中任意与被调用函数名字相同的函数加入候选集合。即使这些函数在调用点不可见,也将之加入候选集合。例如:
namespace NS {
class Item_base { };
void display(const Item_base&) {}
}
class Bulk_item: public NS::Item_base { };
int main () {
Bulk_item book1;
display(book1);
return 0;
}
名字空间与模板
在名字空间内部声明模板影响着怎样声明模板特化:模板的显式特化必须在定义通用模板的命名空间中声明,否则,该特化将与它所特化的模板不同名。
有两种定义特化的方式:一种是重新打开名字空间并加入特化定义;或者,可以用与在名字空间定义外部定义名字空间成员相同的方式来定义特化:使用由名字空间名字限定的模板名定义特化。
如果文中有错误或遗漏之处,敬请指出,谢谢!
参考文献:
[1] C++ Primer(Edition 4)
[2] Thinking in C++(Volume Two, Edition 2)
[3] International Standard:ISO/IEC 14882:1998
[2] Thinking in C++(Volume Two, Edition 2)
[3] International Standard:ISO/IEC 14882:1998