c++命名空间namespace虽然使用命名空间的方法,有多种可供选择。但是不能贪图方便,一味使用using指令,这样就完全背离了设计命名空间的初衷,也失去了命名空间应该具有的防止名称冲突的功能。
一般情况下,对偶尔使用的命名空间成员,应该使用命名空间的作用域解析运算符来直接给名称定位。而对一个大命名空间中的经常要使用的少数几个成员,提倡使用using声明,而不应该使用using编译指令。只有需要反复使用同一个命名空间的许多数成员时,使用using编译指令,才被认为是可取的。
例如,如果一个程序()只使用一两次cout,而且也不使用std命名空间中的其他成员,则可以使用命名空间的作用域解析运算符来直接定位。如:#include
……
std::cout<<“hello,world!”<<std::endl;std::cout<<“outer::i=”<<outer::i<<“,inner::i=”<<outer::inner::i<<std::endl;又例如,如果一个程序要反复使用std命名空间中的cin、cout和cerr(),而不怎么使用其他std命名空间中的其他成员,则应该使用using声明而不是using指令。如:
#include
……
usingstd::cout;cout<<“hello,world!”<<endl;cout<<“outer::i=”<<outer::i<<“,inner::i=”<<outer::inner::i<<endl;4)命名空间的名称l
命名空间别名标准c++引入命名空间,主要是为了避免成员的名称冲突。若果用户都给自己的命名空间取简短的名称,那么这些(往往同是全局级的)命名空间本身,也可能发生名称冲突。如果为了避免冲突,而为命名空间取很长的名称,则使用起来就会不方便。这是一个典型的两难问题。
标准c++为此提供了一种解决方案——命名空间别名,格式为:namespace别名=命名空间名;例如:(at&t美国电话电报公司)
namespaceamerican_telephone_and_telegraph{//命名空间名太长
classstring{
string(constchar*);
//……
}}
american_telephone_and_telegraph::strings1//使用不方便
=newamerican_telephone_and_telegraph::string(“grieg”);
namespaceatt=american_telephone_and_telegraph;//定义别名
att::strings2=newatt::string(“bush”);//使用方便att::strings3=newatt::string(“nielsen”);
l
无名命名空间标准c++引入命名空间,除了可以避免成员的名称发生冲突之外,还可以使代码保持局部性,从而保护代码不被他人非法使用。如果你的目的主要是后者,而且又为替命名空间取一个好听、有意义、且与别人的命名空间不重名的名称而烦恼的话,标准c++还允许你定义一个无名命名空间。你可以在当前编译单元中(无名命名空间之外),直接使用无名命名空间中的成员名称,但是在当前编译单元之外,它又是不可见的。无名命名空间的定义格式为:namespace{
声明序列可选}实际上,上面的定义等价于:(标准c++中有一个隐含的使用指令)
namespace$$${
声明序列可选}usingnamespace$$$;例如:namespace{
inti;
voidf(){/*……*/}}intmain(){
i=0;//可直接使用无名命名空间中的成员i
f();//可直接使用无名命名空间中的成员f()}
基本技能12.5:命名空间
我们曾经在第一章中对命名空间进行简单的介绍。这里我们将对命名空间进行深入的讨论。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突。在c++中,变量、函数和类都是大量存在的。如果没有命名空间,这些变量、函数、类的名称将都存在于全局命名空间中,会导致很多冲突。比如,如果我们在自己的程序中定义了一个函数toupper(),这将重写标准库中的toupper()函数,这是因为这两个函数都是位于全局命名空间中的。命名冲突还会发生在一个程序中使用两个或者更多的第三方库的情况中。此时,很有可能,其中一个库中的名称和另外一个库中的名称是相同的,这样就冲突了。这种情况会经常发生在类的名称上。比如,我们在自己的程序中定义了一个stack类,而我们程序中使用的某个库中也可能定义了一个同名的类,此时名称就冲突了。
namespace关键字的出现就是针对这种问题的。由于这种机制对于声明于其中的名称都进行了本地化,就使得相同的名称可以在不同的上下文中使用,而不会引起名称的冲突。或许命名空间最大的受益者就是c++中的标准库了。在命名空间出现之前,整个c++库都是定义在全局命名空间中的(这当然也是唯一的命名空间)。引入命名空间后,c++库就被定义到自己的名称空间中了,称之为std。这样就减少了名称冲突的可能性。我们也可以在自己的程序中创建自己的命名空间,这样可以对我们认为可能导致冲突的名称进行本地化。这点在我们创建类或者是函数库的时候是特别重要的。命名空间基础
namespace关键字使得我们可以通过创建作用范围来对全局命名空间进行分隔。本质上来讲,一个命名空间就定义了一个范围。定义命名空间的基本形式如下:
namespace名称{//声明}
在命名空间中定义的任何东西都局限于该命名空间内。
下面就是一个命名空间的例子,其中对一个实现简单递减计数器的类进行了本地化。在该命名空间中定义了计数器类用来实现计数;其中的upperbound和lowerbound用来表示计数器的上界和下界。//演示命名空间
namespacecounternamespace{
intupperbound;
intlowerbound;
classcounter{
intcount;
public:
counter(intn){
if(n<=upperbound){
count=n;}
else{
count=upperbound;}}
voidreset(intn){
if(n<upperbound){
count=n;}}
intrun(){
if(count>lowerbound){
returncount--;}
else
{
returnlowerbound;
}
}};}
其中的upperbound,lowerbound和类counter都是有命名空间counternamespace定义范围的组成部分。
在命名空间中声明的标识符是可以被直接引用的,不需要任何的命名空间的修饰符。例如,在counternamesapce命名空间中,run()函数中就可以直接在语句中引用lowerbound:
if(count>lowerbound){
returncount--;}
然而,既然命名空间定义了一个范围,那么我们在命名空间之外就需要使用范围解析运算符来引用命名空间中的对象。例如,在命名空间counternamespace定义的范围之外给upperbound赋值为10,就必须这样写:
counternamespace::upperbound=10;或者在counternamespace定义的范围之外想要声明一个counter类的对象就必须这样写:
counternamespace::counterobj;
一般来讲,在命名空间之外想要访问命名空间内部的成员需要在成员前面加上命名空间和范围解析运算符。
下面的程序演示了如何使用counternamespace这个命名空间://演示命名空间
#include
usingnamespacestd;
namespacecounternamespace{intupperbound;
intlowerbound;
classcounter{
intcount;
public:
counter(intn){
if(n<=upperbound){
count=n;}
else{
count=upperbound;}}
voidreset(intn){
if(n<upperbound){
count=n;}}
intrun(){
if(count>lowerbound){
returncount--;}else
returnlowerbound;}};}
intmain(){
counternamespace::upperbound=100;
counternamespace::lowerbound=0;
counternamespace::counterob1(10);
inti;
do{
i=();
cout<<i<<“”;
}while(i>counternamespace::lowerbound);
cout<<endl;
counternamespace::counterob2(20);
do{
i=();
cout<<i<<“”;
}while(i>counternamespace::lowerbound);
cout<<endl;
(100);
do{
i=();
cout<<i<<“”;
}while(i>counternamespace::lowerbound);
cout<<endl;
return0;}
请注意:counter类以及upperbound和lowerbound的引用都是在前面加上了counternamespace修饰符。但是,一旦声明了counter类型的对象,就没有必须在对该对象的任何成员使用这种修饰符了。()是可以被直接调用的。其中的命名空间是可以被解析的。
相同的空间名称是可以被多次声明的,这种声明向相互补充的。这就使得命名空间可以被分割到几个文件中甚至是同一个文件的不同地方中。例如:
namespacens{
inti;}
//...namespacens{
intj;}
其中命名空间ns被分割成两部分,但是两部分的内容却是位于同一命名空间中的。也就是ns。最后一点:命名空间是可以嵌套的。也就是说可以在一个命名空间内部声明另外的命名空间。using关键字
如果在程序中需要多次引用某个命名空间的成员,那么按照之前的说法,我们每次都要使用范围解析符来指定该命名空间,这是一件很麻烦的事情。为了解决这个问题,人们引入了using关键字。using语句通常有两种使用方式:
usingnamespace命名空间名称;using命名空间名称:成员;
第一种形式中的命名空间名称就是我们要访问的命名空间。该命名空间中的所有成员都会被引入到当前范围中。也就是说,他们都变成当前命名空间的一部分了,使用的时候不再需要使用范围限定符了。第二种形式只是让指定的命名空间中的指定成员在当前范围中变为可见。我们用前面的counternamespace来举例,下面的using语句和赋值语句都是有效的:
usingcounternamespace::lowerbound;//只有lowerbound当前是可见的lowerbound=10;//这样写是合法的,因为lowerbound成员当前是可见的usingcounternamespace;//所有counternamespace空间的成员当前都是可见的
upperbound=100;//这样写是合法的,因为所有的counternamespace成员目前都是可见的
下面是我们对之前的程序进行修改的结果://使用using
#includeusingnamespacestd;
namespacecounternamespace{intupperbound;
intlowerbound;
classcounter{
intcount;
public:
counter(intn){
if(n<upperbound){
count=n;}
else{
count=upperbound;}}
voidreset(intn){
if(n<=upperbound){
count=n;}}
intrun(){
if(count>lowerbound){
returncount--;}
else{
returnlowerbound;}}};}
intmain(){
//这里只是用counternamespace中的upperboundusingcounternamespace::upperbound;
//此时对upperbound的访问就不需要使用范围限定符了upperbound=100;
//但是使用lowerbound的时候,还是需要使用范围限定符的counternamespace::lowerbound=0;counternamespace::counterob1(10);
inti;
do{
i=();cout<<i<<“”;
}while(i>counternamespace::lowerbound);cout<<endl;
//下面我们将使用整个counternamespace的命名空间
usingnamespacecounternamespace;counterob2(20);
do{
i=();cout<<i<<“”;
}while(i>counternamespace::lowerbound);cout<<endl;
(100);lowerbound=90;
do{
i=();cout<<i<<“”;}while(i>lowerbound);
return0;}
上面的程序还为我们演示了重要的一点:当我们用using引入一个命名空间的时候,如果之前有引用过别的命名空间(或者同一个命名空间),则不会覆盖掉对之前的引入,而是对之前引入内容的补充。也就是说,到最后,上述程序中的std和counternamespace这两个命名空间都变成全局空间了。没有名称的命名空间
有一种特殊的命名空间,叫做未命名的命名空间。这种没有名称的命名空间使得我们可以创建在一个文件范围里可用的命名空间。其一般形式如下:namespace{
//声明}
我们可以使用这种没有名称的命名空间创建只有在声明他的文件中才可见的标识符。也即是说,只有在声明这个命名空间的文件中,它的成员才是可见的,它的成员才是可以被直接使用的,不需要命名空间名称来修饰。对于其他文件,该命名空间是不可见的。我们在前面曾经提到过,把全局名称的作用域限制在声明他的文件的一种方式就是把它声明为静态的。尽管c++是支持静态全局声明的,但是更好的方式就是使用这里的未命名的命名空间。std命名空间
标准c++把自己的整个库定义在std命名空间中。这就是本书的大部分程序都有下面代码的原因:
usingnamespacestd;
这样写是为了把std命名空间的成员都引入到当前的命名空间中,以便我们可以直接使用其中的函数和类,而不用每次都写上std::。
当然,我们是可以显示地在每次使用其中成员的时候都指定std::,只要我们喜欢。例如,我们可以显示地采用如下语句指定cout:
std::cout<<“显示使用std::来指定cout”;
如果我们的程序中只是少量地使用了std命名空间中的成员,或者是引入std命名空间可能导致命名空间的冲突的话,我们就没有必要使用usingnamespacestd;了。然而,如果在程序中我们要多次使用std命名空间的成员,则采用usingnamespacestd;的方式把std命名空间的成员都引入到当前命名空间中会显得方便很多,而不用每次都单独在使用的时候显示指定。
对于命名空间,官方文档已经说得很详细[查看],我在这里做了一下实践和总结。
命名空间一个最明确的目的就是解决重名问题,php中不允许两个函数或者类出现相同的名字,否则会产生一个致命的错误。这种情况下只要避免命名重复就可以解决,最常见的一种做法是约定一个前缀。例:项目中有两个模块:article和messageboard,它们各自有一个处理用户留言的类comment。之后我可能想要增加对所有用户留言的一些信息统计功能,比如说我想得到所有留言的数量。这时候调用它们comment提供的方法是很好的做法,但是同时引入各自的comment类显然是不行的,代码会出错,在另一个地方重写任何一个comment也会降低维护性。那这时只能重构类名,我约定了一个命名规则,在类名前面加上模块名,像这样:article_comment、messageboard_comment可以看到,名字变得很长,那意味着以后使用comment的时候会写上更多的代码(至少字符多了)。并且,以后如果要对各个模块增加更多的一些整合功能,或者是互相调用,发生重名的时候就需要重构名字。当然在项目开始的时候就注意到这个问题,并规定命名规则就能很好的避免这个问题。另一个解决方法可以考虑使用命名空间。
注明:
本文提到的常量:php5.3开始const关键字可以用在类的外部。const和define都是用来声明常量的(它们的区别不详述),但是在命名空间里,define的作用是全局的,而const则作用于当前空间。我在文中提到的常量是指使用const声明的常量。
基础
命名空间将代码划分出不同的空间(区域),每个空间的常量、函数、类(为了偷懒,我下边都将它们称为元素)的名字互不影响,这个有点类似我们常常提到的‘封装'的概念。创建一个命名空间需要使用namespace关键字,这样:复制代码代码如下:
//创建一个名为'article'的命名空间namespacearticle;?>
要注意的是,当前脚本文件的第一个命名空间前面不能有任何代码,下面的写法都是错误的:复制代码代码如下://例一
//在脚本前面写了一些逻辑代码
//例二
//在脚本前面输出了一些字符
为什么要说第一个命名空间呢?因为同一脚本文件中可以创建多个命名空间。
下面我创建了两个命名空间,顺便为这两个空间各自添加了一个comment类元素:复制代码代码如下:
//创建一个名为'article'的命名空间namespacearticle;//此comment属于article空间的元素classcomment{}
//创建一个名为'messageboard'的命名空间namespacemessageboard;//此comment属于messageboard空间的元素classcomment{}?>
在不同空间之间不可以直接调用其它元素,需要使用命名空间的语法:复制代码代码如下:
namespacearticle;classcomment{}
namespacemessageboard;classcomment{}//调用当前空间(messageboard)的comment类$comment=newcomment();//调用article空间的comment类
$article_comment=newarticlecomment();?>
可以看到,在messageboard空间中调用article空间里的comment类时,使用了一种像文件路径的语法:空间名元素名
除了类之外,对函数和常量的用法是一样的,下面我为两个空间创建了新的元素,并在messageboard空间中输出了它们的值。复制代码代码如下:
namespacemessageboard;constpath='/message_board';functiongetcommenttotal(){return300;}classcomment{}//调用当前空间的常量、函数和类echopath;///message_boardechogetcommenttotal();//300$comment=newcomment();//调用article空间的常量、函数和类echoarticlepath;///articleechoarticlegetcommenttotal();//100$article_comment=newarticlecomment();?>
然后我的确得到了article空间的元素数据。子空间
命名空间的调用语法像文件路径一样是有道理的,它允许我们自定义子空间来描述各个空间之间的关系。抱歉我忘了说,article和messageboard这两个模块其实都是处于同一个blog项目内。如果用命名空间来表达它们的关系,是这样:复制代码代码如下:
//我用这样的命名空间表示处于blog下的article模块namespaceblogarticle;classcomment{}
//我用这样的命名空间表示处于blog下的messageboard模块namespaceblogmessageboard;classcomment{}//调用当前空间的类
$comment=newcomment();//调用blogarticle空间的类
$article_comment=newblogarticlecomment();?>
而且,子空间还可以定义很多层次,比如说blogarticlearchivesdate
公共空间
我有一个脚本文件,里面有一些好用的函数和类:复制代码代码如下:
functiongetip(){}classfilterxss{}?>
在一个命名空间里引入这个脚本,脚本里的元素不会归属到这个命名空间。如果这个脚本里没有定义其它命名空间,它的元素就始终处于公共空间中:复制代码代码如下:
namespaceblogarticle;//引入脚本文件
include'./';$filter_xss=newfilterxss();//出现致命错误:找不到blogarticlefilterxss类$filter_xss=newfilterxss();//正确?>
调用公共空间的方式是直接在元素名称前加就可以了,否则php解析器会认为我想调用当前空间下的元素。除了自定义的元素,还包括php自带的元素,都属于公共空间。
要提一下,其实公共空间的函数和常量不用加也可以正常调用(不明白php为什么要这样做),但是为了正确区分元素,还是建议调用函数的时候加上
名称术语
在说别名和导入之前,需要知道关于空间三种名称的术语,以及php是怎样解析它们的。官方文档说得非常好,我就直接拿来套了。
1.非限定名称,或不包含前缀的类名称,例如$comment=newcomment()。如果当前命名空间是blogarticle,comment将被解析为blogarticlecomment。如果使用comment的代码不包含在任何命名空间中的代码(全局空间中),则comment会被解析为comment。
2.限定名称,或包含前缀的名称,例如$comment=newarticlecomment()。如果当前的命名空间是blog,则comment会被解析为blogarticlecomment。如果使用comment的代码不包含在任何命名空间中的代码(全局空间中),则comment会被解析为comment。
3.完全限定名称,或包含了全局前缀操作符的名称,例如$comment=newarticlecomment()。在这种情况下,comment总是被解析为代码中的文字名(literalname)articlecomment。
其实可以把这三种名称类比为文件名(例如)、相对路径名(例如。/article/)、绝对路径名(例如/blog/article/),这样可能会更容易理解。我用了几个示例来表示它们:复制代码代码如下:
//创建空间blognamespaceblog;classcomment{}//非限定名称,表示当前blog空间//这个调用将被解析成blogcomment();$blog_comment=newcomment();//限定名称,表示相对于blog空间
//这个调用将被解析成blogarticlecomment();$article_comment=newarticlecomment();//类前面没有反斜杆//完全限定名称,表示绝对于blog空间//这个调用将被解析成blogcomment();$article_comment=newblogcomment();//类前面有反斜杆//完全限定名称,表示绝对于blog空间
//这个调用将被解析成blogarticlecomment();$article_comment=newblogarticlecomment();//类前面有反斜杆
//创建blog的子空间articlenamespaceblogarticle;classcomment{}?>
其实之前我就一直在使用非限定名称和完全限定名称,现在它们终于可以叫出它们的名称了。
别名和导入
别名和导入可以看作是调用命名空间元素的一种快捷方式。php并不支持导入函数或常量。它们都是通过使用use操作符来实现:复制代码代码如下:
namespaceblogarticle;classcomment{}
//创建一个bbs空间(我有打算开个论坛)namespacebbs;//导入一个命名空间useblogarticle;//导入命名空间后可使用限定名称调用元素$article_comment=newarticlecomment();//为命名空间使用别名useblogarticleasarte;//使用别名代替空间名
$article_comment=newartecomment();//导入一个类
useblogarticlecomment;//导入类后可使用非限定名称调用元素$article_comment=newcomment();//为类使用别名
useblogarticlecommentascomt;//使用别名代替空间名
$article_comment=newcomt();?>
我注意到,如果导入元素的时候,当前空间有相同的名字元素将会怎样?显然结果会发生致命错误。
例:
复制代码代码如下:
namespaceblogarticle;classcomment{}
namespacebbs;classcomment{}classcomt{}
//导入一个类
useblogarticlecomment;$article_comment=newcomment();//与当前空间的comment发生冲突,程序产生致命错误//为类使用别名
useblogarticlecommentascomt;$article_comment=newcomt();//与当前空间的comt发生冲突,程序产生致命错误?>动态调用
php提供了namespace关键字和__namespace__魔法常量动态的访问元素,__namespace__可以通过组合字符串的形式来动态访问:复制代码代码如下:
namespaceblogarticle;constpath='/blog/article';classcomment{}
//namespace关键字表示当前空间echonamespacepath;///blog/article$comment=newnamespacecomment();//魔法常量*泡面作文*__namespace__的值是当前空间名称echo__namespace__;//blogarticle//可以组合成字符串并调用
$comment_class_name=__namespace__.'comment';$comment=new$comment_class_name();?>
字符串形式调用问题
上面的动态调用的例子中,我们看到了字符串形式的动态调用方式,如果要使用这种方式要注意两个问题。
1.使用双引号的时候特殊字符可能被转义复制代码代码如下:
namespaceblogarticle;classname{}//我是想调用blogarticlename$class_name=__namespace__.“name”;//但是n将被转义为换行符$name=new$class_name();//发生致命错误?>
2.不会认为是限定名称
php在编译脚本的时候就确定了元素所在的空间,以及导入的情况。而在解析脚本时字符串形式调用只能认为是非限定名称和完全限定名称,而永远不可能是限定名称。复制代码代码如下:
namespaceblog;//导入common类
useblogarticlecommon;//我想使用非限定名称调用blogarticlecommon$common_class_name='common';//实际会被当作非限定名称,也就表示当前空间的common类,但我当前类没有创建common类$common=new$common_class_name();//发生致命错误:common类不存在//我想使用限定名称调用blogarticlecommon$common_class_name='articlecommon';//实际会被当作完全限定名称,也就表示article空间下的common类,但我下面只定义了blogarticle空间而不是article空间
$common=new$common_class_name();//发生致命错误:articlecommon类不存在
namespaceblogarticle;classcommon{}?>总结
我对php的命名空间刚刚接触,也不能随便给一些没有实践的建议。我个人认为命名空间的作用和功能都很强大,如果要写插件或者通用库的时候再也不用担心重名问题。不过如果项目进行到一定程度,要通过增加命名空间去解决重名问题,我觉得工作量不会比重构名字少。也不得不承认它的语法会对项目增加一定的复杂度,因此从项目一开始的时候就应该很好的规划它,并制定一个命名规范。