热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

ANSI/ISOC++ProfessionalProgrammer'sHandbook8

ANSIISOC++ProfessionalProgrammersHandbook8命名空间

ANSI/ISO C++ Professional Programmer's Handbook


Contents




8

命名空间
by Danny Kalev



命名空间(Namespaces)在1995年引入C++标准。本章说明了什么是命名空间以及为什么将加入语言。你将看到命名空间如何避免命名冲突以及在大型工程中命名空间如何推动配置管理和版本控制。最后,你将学习命名空间如何与语言的其他特性结合。


命名空间后面的基本原理


为 了理解为什么将命名空间加入语言,这里有一个类比:想象一下如果电脑的文件系统完全没有目录和子目录。所有的文件将存储在一个目录下,在任何时刻每一个用 户和应用程序都能看见所有文件。结果,发生了可怕的问题:文件名将冲突(在只允许8.3的文件名的系统中,这更容易发生),列表、拷贝查找文件之类的简单 操作都将变得及其困难。另外,安全和授权限制将十分脆弱。


C++的命名空间等价与目录。他们可以很容易的嵌套,他们使你的代码避免命名冲突,他们使你可以隐藏申明,他们不会产生任何运行期开销。C++标准库的大多数组件在命名空间std。命名空间再细分成另外的命名空间比如包含STL重载运算符定义的std::rel_ops


简要的历史背景


90年代早期,当C++变得日益流行时,许多厂商出售了各种各样组件类的私有实现。字符处理、数学函数和数据容器的类库成为了许多frameworks的一部分MFC、STL、OWL等等。可重用组件的激增导致了命名冲突的问题。例如叫vector的类可能同时出现在数学库和其他容器库中,而这两个库同时被使用;叫string的类在大多数framework和类库都能找到。要求编译器识别两个具有相同名字的类是不可能的。同样的,连接器也不能应付有同样名字的类成员函数。例如,成员函数



vector::operator==(const vector&);

不能定义在两个不同的类中——迪格可能属于数学库,而第二个可能属于容器库。


大型工程更容易受命名冲突的影响


命名冲突不止出现第三方软件库中。在大型软件工程中,类、函数、常量的简短优雅的名字也可能导致命名冲突,因为很可能不同的开发者用相同的名字来表示不同的实体。在没有命名空间的时代,只好在标识符上加上不同的词缀。但这不仅繁琐而且容易出错。考虑下面的代码:



class string //简短但危险。其他人可能已经使用了//这个名字已存在了...
{
//...
};
class excelSoftCompany_string //长的名字安全但冗长。//如果公司改变了名字将是一场恶梦...
{
//...
};

命名空间使你能安全的使用便利、简短、形象的名字。作为笨拙的词缀的替代,你可以将你的申明放在一个命名空间中而不管词缀,就像:



//file excelSoftCompany.h
namespace excelSoftCompany { //命名空间定义
class string {/*..*/};
class vector {/*..*/};
}

命名空间成员与类成员一样,其定义与申明可以分开。例如




#include
using namespace std;
namespace A
{
void f(); //申明
}
void A::f() //定义在不同的文件中
{
cout<<"in f"<
}
int main()
{
A::f();
return 0;
}

命名空间的属性


命名空间更像一个名字容器。他们用于快速简单的移植遗留的代码,而没有任何开销。命名空间有一些使它 更容易使用的属性。下面的章节讨论了这些属性。


完整名字


命名空间是其中申明和定义的范围。为了在其他范围中引用这些申明和定义,需要完整名字(fully qualified name)。标识符的完整名字由三部分组成命名空间,类名字和标识符自己,期间由范围运算符分开(::)。因为命名空间和类的名字都可能嵌套,结果名字可能很长——但是确保了唯一性:



unsigned int maxPossibleLength =
std::string::npos; //完整名字。npos是string的一个成员;//string属于命名空间std
int *p = ::new int; //区别全局new和重载的new

但是,重复完整名字是冗长、没有必要的。你可以使用 using declarationusing directive


using Declaration和using Directive


一个using申明由关键字using和紧接着的namespace::member组成。它指示编译器将遇到的标识符限制在指定的命名空间中,就像完整命名一样。例如



#include //STL vector;在命名空间std中定义
int main()
{
using std::vector; //using申明;遇到的每一个vector//都在std中查找
vector vi;
return 0;
}

另一方面,using指示为指示范围内所有可访问的名字指定了命名空间。它有下列序列组成:using namespace接着的是命名空间的名字。例如



#include //属于命名空间std
#include //iostream类和运算符也在命名空间std内
int main()
{
using namespace std; //using-directive;所有 //申明现在都可见了
vector vi;
vi.push_back(10);
cout<
return 0;
}

回头看一看string类的例子(为了方便在这里重复这个例子):



//文件excelSoftCompany.h
namespace excelSoftCompany
{
class string {/*..*/};
class vector {/*..*/};
}

现在可以在同样的程序中像标准的string一样来使用自己的string,看下面的代码:



#include // std::string
#include "excelSoftCompany.h"
int main()
{
using namespace excelSoftCompany;
string s; //引用类excelSoftCompany::string
std::string standardstr; //现在实例化的是一个ANSI string
return 0;
}

命名空间可以扩展


C++标准化委员会意识到相关联的申明可以跨越好几个translation units。所以命名空间可以定义一部分。例如



//文件proj_const.h
namespace MyProj
{
enum NetProtocols
{
TCP_IP,
HTTP,
UDP
}; // enum
}
//文件proj_classes.h
namespace MyProj
{ // 扩展了命名空间MyProj
class RealTimeEncoder{ public: NetProtocols detect(); };
class NetworkLink {}; //全局的
class UserInterface {};
}

在分开的文件中,一样的命名空间可以通过附加申明扩展。


完整的命名空间MyProj可以由两个文件引出就像下面这样:



//文件app.cpp
#include "proj_const.h"
#include "proj_classes.h"
int main()
{
using namespace MyProj;
RealTimeEncoder encoder;
NetProtocols protocol = encoder.detect();
return 0;
}

命名空间的别名


就像你观察到的,命名空间短的名字最终导致命名冲突。但是过长的命名空间不容易使用。为了这个目的,使用命名空间别名(namespace alias)。下面的例子为笨拙的Excel_Software_Company命名空间定义了别名ESC。命名空间的别名还有其他用途,就像你将看到的。



//文件decl.h
namespace Excel_Software_Company
{
class Date {/*..*/};
class Time {/*..*/};
}
//文件calendar.cpp
#include "decl.h"
int main()
{
namespace ESC = Excel_Software_Company; //ESC是Excel_Software_Company
//的别名
ESC::Date date;
ESC::Time time;
return 0;
}

Koenig查找


Andrew Koenig,C++的创建者之一,为解决命名空间成员的查找问题设计了一种算法。这种算法也叫参数依赖查找(argument dependent lookup),现在为所有适应标准的编译器用来处理下面情况:





警告:有些现存的编译器并不完全支持Koenig查找。因此,下面的程序——依赖于Koenig 查找——在并不完全支持ANSI/ISO标准的编译器下可能不能编译通过。




namespace MINE
{
class C {};
void func;
}
MINE::C c; //类型为MINE::C的全局对象
int main()
{
func( c ); //OK,调用了MINE::f
return 0;
}

在程序中既没有使用using申明也没有使用using指示。但是,编译器做了正确的事情——通过应用Koenig查找,它正确的将未加限制的func识别为在命名空间MINE中申明的函数。


Koenig查找指示编译器不仅仅在平常应该找的地方,比如局域范围内查找,还要在包含参数类型的命名空间中查找。因此接着的代码行中检查函数func()的参数对象cc属于命名空间MINE。编译器将命名空间MINE视为在func()的申明之中,“猜”出了程序员的意图:



func( c ); // OK,调用MINE::f

没有Koenig查找的话,命名空间将给程序员不想接受的麻烦,程序员不得不重复完整名字或使用许多using申明。为了进一步将参数推给Koenig查找,考虑下面的例子:



#include
using std::cout;
int main()
{
cout<<"hello"; //OK,通过Koenig查找运算符<<被带到了范围里面
return 0;
}

using申明将std::cout注入main()的范围中,从而使程序员可以使用未加限制的cout。然而,你可能想起重载运算符<<不是std::cout的成员。它是在命名空间std内定义的友元函数,它接受一个std::ostream对象作为参数。没有Koenig查找,程序员不得不写成下面的形式:



std::operator<<(cout, "hello");

作为选择,程序员也可以提供using namespace std;指示。然而没有一种方法是领人赏心悦目的,因为他们使代码散乱而且是混淆和错误的源头。(using指示至少有利于使名字在当前范围可见,因为他们使得命名空间的所有成员不加区别的都可见了)。幸运的是,Koenig查找“做了正确的事情”,以文雅的方式使你从这些烦劳中解脱出来。


Koenig查找是自动使用的。激活它不需要特殊的指示或配置开关,也没有办法关闭它。必须时刻注意这个事实,因为在有些情况下它可能带来令人吃惊的结果。例如



namespace NS1
{
class B{};
void f;
};
void f(NS1::B);
int main()
{
NS1::B b;
f; //不明确;NS1::f()还是f(NS1::B)?
return 0;
}

对于这种情况,适应标准的编译器一定会给出错误。但是不符合标准的编译器不会指出调用的不明确;他们简单的选择f()的一个版本。这可能是程序员不想要的结果。此外,问题可能发生在开发的后期,在工程中加入f()的附加版本的时候——可能妨碍编译器的查找算法。这种不明确不仅仅限制在全局名字。它也可能出现在两个命名空间互相有关的时候——例如,命名空间申明的类,而类被在不同命名空间申明的类成员函数作为参数。


命名空间实践


从 前面的例子中可以总结出,和语言其他特性一样必须明智的使用命名空间。对于只包含少数类和几个源文件的小工程,命名空间是不需要的。在大多数情况下,这样 的程序由一个程序员编码和维护,他们使用的组件数目有限。命名空间的可能性很小。如果还是出现了命名冲突,一般是重命名存在的类或函数,或简单的在后面增 加命名空间。


另一方面,大型工程——如前面说明的——更容易受到命名冲突的影响;因此他们需要系统的命名空间。有上百程序员合作的 工程或许多开发小组一起合作的大项目十分常见。例如Microsoft Visual C++ 6.0的开发历时18个月,超过1000的程序员与开发过程有关。管理这样的大型工程需要良好文档的编码方针——命名空间正是这样一个工具。


在大型工程利用命名空间的方针


为 了展示命名空间如何在结构管理中应用,假象一个国际信用卡公司的在线事务处理系统。项目由好几个开发小组组成。其中数据库管理小组负责创建和维护数据库的 表、索引和访问授权。数据小组也必须提供访问例程和对数据库中数据进行检索、操作的数据对象。第二个小组负责图形用户界面(GUI)。第三个小组处理来自 电影院、饭店、商店以及任何旅行者可能使用他们的国际Unicard付帐的地方提出的国际在线请求。在每个信用卡的所有者交易之前,每一个购买,电影票、 珠宝或艺术书都必须通过Unicard确认。每个确认的过程检查信用卡的有效性、它的有效期限和信用卡所有者的收支平衡。对于国内购买有一个相似的确认过 程。但是,国际确认要求用卫星传送,而国内确认一般使用电话。


在软件项目里,代码的可重用性是十分重要的。因为,同样的处理逻辑同 时适用与国内和国际确认,所以检索相关信息和执行必要计算的数据库访问对象应该是相同的。虽然如此,国际确认还需要经常使用通讯协议堆栈,以接收通过卫星 传过来的请求,解释它再返回发送者一个加密的回答。基于卫星确认的典型实现可以是将数据库访问对象和必要的通讯对象捆绑在一起,通讯对象封装了协议、通讯 层、优先权管理、消息队列、加密和解密等过程。不难想象通讯对象和数据库访问对象同时使用了一个名字而引起了命名冲突。


例如,两个对象——一个封装了数据库连接而另一个指的是卫星连接——可能有同样的名字:Connection。但是如果通讯组件和数据库访问对象申明在两个不同的命名空间中,潜在的命名冲突将减至最小。因为,com::Connectiondba::Connection可以在同一个应用程序中同时使用。系统的方法可以是给每一个小组分配一个独立的命名空间。这样的方针可以帮助你在不同的小组以及第三方代码中避免命名冲突。


命名空间和版本控制


成 功的软件项目不会只有一个版本。在大多数项目中,基于前辈的新版本不断发布。此外,前一个版本必须支持打补丁和调整以适应新的操作系统、新的应用环境和新 的硬件。Web浏览器、商业数据库、字处理软件和多媒体工具就是这样的例子。同样的开发小组必须对同一软件产品的不同版本提供支持是一种常见情况。大量的 软件能共享同样产品的不同版本,但是每一个版本也有它特殊的组件。这种情况下可以使用命名空间别名迅速从一个版本切换到另一个版本。


一般连续项目有一些经常使用的基础软件组件。另外,每一个版本有自己独有的特殊组件。命名空间别名可以提供动态命名空间(dynamic namespaces);命名空间别名可以指给定时间的版本X的命名空间,在其他时候、它也可以指不同的命名空间。例如



namespace ver_3_11 //16 bit
{
class Winsock{/*..*/};
class FileSystem{/*..*/};
};
namespace ver_95 //32 bit
{
class Winsock{/*..*/};
class FileSystem{/*..*/};
}
int main()//实现16 bit版
{
namespace current = ver_3_11; //current是 ver_3_11的别名
using current::Winsock;
using current::FileSystem;
FileSystem fs; // ver_3_11::FileSystem
//...
return 0;
}

在这个例子中,别名current是一个可以指ver_3_11也可以指ver_95的符号。为了在不同版本之间切换,程序员仅仅只需要将不同的命名空间赋值给它。


命名空间不会带来额外的开销


命名空间的实现包括Koenig查找都是静态的。命名空间的底层实现使用名字分裂(name mangling),就是编译器通过函数的参数列表,函数的类名以及函数的命名空间名字来合成出函数的独一无二的名字(参见第十三章“C语言的兼容性问题”,有名字分裂的具体细节)。因此,命名空间不会成生运行时间和内存的开销。


命名空间和语言其他特性的互相影响


命名空间和语言的其他特性结合影响了程序设计技术。命名空间使得C++中的有些特性变的可有可无或是不受欢迎。


不需用范围运算符来指定全局变量


在一些frameworks(例如MFC),它习惯于加范围运算符到全局函数前面来显式的指明,这是全局函数而非类成员函数(象下面的例子那样):



void String::operator = (const String& other)
{
::strcpy (this->buffer, other.getBuff());
}

这种习惯使不推荐的。许多曾经是全局函数的标准函数现在都被归到命名空间内部了。例如,现在strcpy属于命名空间std,大多数标准库的函数都是如此。前面那些有范围运算符的函数可能使编译器的查找算法混淆;另外,这样做破坏了分割全局命名空间的思想。因此,不推荐你在函数名之前加上范围运算符。


将外部函数变为文件内函数


在标准C中,没有申明为静态的非局域变量有内部连接(internal linkage),这意味着仅仅在同一translation unit中申明的才是可见的(参见第二章“标准简报:ANSI/ISO C++的最新附加部分”)。这种技术用于信息隐藏(如下面的例子):



//File hidden.c
static void decipher(FILE *f); //仅仅在本文件中可见
//现在在当前源文件中使用该函数
decipher ("passwords.bin");
//文件结束

尽管在C++中仍然支持这样,这个习惯现在已成为了反对的特性(deprecated feature)。你编译器未来的版本中,编译器在找到非类成员函数的静态标识符时可能会给出警告。为了使函数只在自己的translation unit中可见,使用未命名命名空间(unnamed namespace)。下面的例子示范了这种用法:



//File hidden.cpp
namespace //未命名
{
void decipher(FILE *f); //仅仅在本文件中可见
}
//现在在当前文件中使用函数
//不需要使用命名空间申明或命名空间指示
decipher ("passwords.bin");

尽 管在未命名命名空间中的名字可能需要外部连接(external linkage),他们在其他translation unit中却不可见;产生的效果是在未命名命名空间出现的名字有静态连接(static linkage)。如果你在其他文件的未命名命名空间中申明同名的其他函数,两个函数互相隐藏,把他们不会产生命名冲突。


标准头文件


现在所有的标准C++头文件必须这样包括:



#include //注意:没有“.h”扩展

.h扩展被忽略了。标准C头文件也服从这个习惯,只是有一个附加的字母c。因此,以前叫的C头文件现在叫。例如



#include //以前: 注意前缀'c' 和//忽略".h"

C头文件的旧的习惯,仍然被支持;但是,是被反对的特性,因此在新的C++代码中不使用。原因是C 头文件将其申明的名字加入到了全局命名空间中。然而在C++中,大多数标准申明归到了命名空间std中,像这样的标准C头文件一样。不能从在头文件的物理地址使用的实际命名习惯或它的实际名字得出什么推论。事实上,大多数编译器为和对应的符号共享一个物理文件。通过一些隐藏的预处理技巧很容易办到。记住你必须使用using申明、using指示或完整名字来访问新风格标准头文件中的申明。例如



#include
using namespace std;
void f()
{
printf ("Hello World/n");
}

命名空间的限制


C++标准给命名空间的使用定义了几条限制。这些限制式为了有意避开可能搞得一团糟的不规则和不明确的地方。


不能改变命名空间std


一般的命名空间是开放的,所以通过交叉在许多文件中的附加申明和定义可以完全合法的扩展命名空间。唯一的例外是命名空间std。根据标准,通过附加申明改变命名空间std的结果——不要动已存在地那个——是为定义的行为,也是不允许的。这条限制可能很武断,但是它只是公共的认识——任何篡改命名空间std的行为破坏了标准申明在专有排外的命名空间中的基本观念。


用户定义的new和delete不能在命名空间中申明


标准禁止在命名空间中申明newdelete运算符。下面的例子说明了为什么:



char *pc; //全局
namespace A
{
void* operator new ( std::size_t );
void operator delete ( void * );
void func ()
{
pc = new char ( 'a'); //使用A::new
}
} //A
void f() { delete pc; } //调用A::delete还是//::delete?

有些程序员可能希望选中运算符A::delete,因为它匹配用来分配内存的new运算符;另一些程序员可能希望选中标准运算符delete,因为A::delete对于函数f()是不可见的。通过禁止在命名空间中申明运算符newdelete,C++避免了这样的麻烦。


总结


命名空间是最新加入C++标准的。因此,有些编译器不完全支持这些特性。不过,所有的编译器厂商将在将来的版本中完全支持命名空间。不必过分强调命名空间的重要性。如你所见,许多著名的C++程序使用了STL的组件、iostream库和其他标准头文件——所有这些都是命名空间的成员


大型软件项目可以明智的使用命名空间来避免公共缺陷和使得版本控制更容易,就像你已经看到的。


C++提供三种方法,将命名空间的要素加入到当前的范围中。第一种是使用using指示,它将使命名空间的所有成员在当前范围中可见。第二种是使用using申明,它将更有选择性的使命名空间中的单一组件可见。最后一种使用独一无二的完整名字来标志命名空间成员。另外,依赖参数查找或Koenig lookup,不必强迫程序员提及命名空间就可以捕捉程序员的正确意图。







Contents







© Copyright 1999, Macmillan Computer Publishing. All
rights reserved.


 


推荐阅读
author-avatar
PHP小龙
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有