参考整理自
以上两篇博客对C++命名空间的说明都非常详细,按我的逻辑整理一下。
一、来源及定义
在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。
与命名空间相关的概念有:
声明域(declaration region)—— 声明标识符的区域。如在函数外面声明的全局变量,它的声明域为声明所在的文件。在函数内声明的局部变量,它的声明域为声明所在的代码块(例如整个函数体或整个复合语句)。
潜在作用域(potential scope)—— 从声明点开始,到声明域的末尾的区域。因为C++采用的是先声明后使用的原则,所以在声明点之前的声明域中,标识符是不能用的。即,标识符的潜在作用域,一般会小于其声明域。
作用域(scope)—— 标识符对程序可见的范围。标识符在其潜在作用域内,并非在任何地方都是可见的。例如,局部变量可以屏蔽全局变量、嵌套层次中的内层变量可以屏蔽外层变量,从而被屏蔽的全局或外层变量在其倍屏蔽的区域内是不可见的。所以,一个标识符的作用域可能小于其潜在作用域。
命名空间是ANSIC++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
在C语言中定义了3个层次的作用域,即文件(编译单元)、函数和复合语句。C++又引入了类作用域,类是出现在文件内的。在不同的作用域中可以定义相同名字的变量,互不于扰,系统能够区别它们。
背景举例:
在文件A中定义一个成员函数fun(),在文件B中定义一个同名成员函数fun(); , 注意fun();不是类的成员函数,只是定义在文件中的普通函数,否则可以通过类::形式来访问。在主文件(含main函数的文件)中若同时需要A和B的部分功能,即会同时include文件A/B,此时,若在主文件中调用fun函数,则无从调起,出现重名二义冲突。
全局命名空间污染(global namespace pollution)。
在程序中还往往需要引用一些库(包括C++编译系统提供的库、由软件开发商提供的库或者用户自己开发的库),为此需要包含有关的头文件。如果在这些库中包含有与程序的全局实体同名的实体,或者不同的库中有相同的实体名,则在编译时就会出现名字冲突。
为了避免这类问题的出现,人们提出了许多方法,例如:将实体的名字写得长—些(包含十几个或几十个字母和字符);把名字起得特殊一些,包括一些特殊的字符;由编译系统提供的内部全局标识符都用下划线作为前缀,如_complex(),以避免与用户命名的实体同名;由软件开发商提供的实体的名字用特定的字符作为前缀。但是这样的效果并不理想,而且增加了阅读程序的难度,可读性降低了。
C语言和早期的C++语言没有提供有效的机制来解决这个问题,没有使库的提供者能够建立自己的命名空间的工具。人们希望ANSI C++标准能够解决这个问题,提供—种机制、一种工具,使由库的设计者命名的全局标识符能够和程序的全局实体名以及其他库的全局标识符区别开来。
所以,命名空间:实际上就是一个由程序设计者命名的内存区域,程序设计者可以根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。
二、定义命名空间
如: namespace ns1 //指定命名中间nsl
{
int a;
double b;
class A{...};
}
namespace是定义命名空间所必须写的关键字,nsl是用户自己指定的命名空间的名字(可以用任意的合法标识符,这里用ns1是因为ns是namespace的缩写,含义请楚),在花括号内是声明块,在其中声明的实体称为命名空间成员(namespace member)。现在命名空间成员包括变量a和b,注意a和b仍然是全局变量,仅仅是把它们隐藏在指定的命名空间中而已。如果在程序中要使用变量a和b,必须加上命名空间名和作用域分辨符“::”,如nsl::a,nsl::b。这种用法称为命名空间限定(qualified),这些名字(如nsl::a)称为被限定名(qualified name)。C++中命名空间的作用类似于操作系统中的目录和文件的关系,由于文件很多,不便管理,而且容易重名,于是人们设立若干子目录,把文件分别放到不同的子目录中,不同子目录中的文件可以同名。调用文件时应指出文件路径。
命名空间的作用:是建立一些互相分隔的作用域,把一些全局实体分隔开来。
可以根据需要设置许多个命名空间,每个命名空间名代表一个不同的命名空间域,不同的命名空间不能同名。这样,可以把不同的库中的实体放到不同的命名空间中,或者说,用不同的命名空间把不同的实体隐蔽起来。过去我们用的全局变量可以理解为全局命名空间,独立于所有有名的命名空间之外,它是不需要用namespace声明的,实际上是由系统隐式声明的,存在于每个程序之中。
在声明一个命名空间时,花括号内不仅可以包括变量,而且还可以包括以下类型:
·变量(可以带有初始化);
·常量;
·数(可以是定义或声明);
·结构体;
·类;
·模板;
·命名空间(在一个命名空间中又定义一个命名空间,即嵌套的命名空间)。
注意:
- 可以看到命名空间的声明方法和使用方法与类差不多。但它们之间有一点差别:在声明类时在右花括号的后面有一分号,而在定义命名空间时,花括号的后面没有分号。
- 在头文件中,不要把#include命令放在命名空间中,在上一小节的叙述中可以知道,命名空间中的内容不包括命令行,否则编译会出错。(即include语句要放在namespace外)
- 若namespace ns1{...}, 在主文件中定义对象 ns1::A aobj;, 则aobj是主文件中的成员,它并不在命名空间内,作用域在主文件内的一部分,所以通过aobj调用A的成员函数时,直接aobj.get();,不能加ns1::类型的前缀
- 对于嵌套命名空间,不能在命名空间的定义中声明(另一个嵌套的)子命名空间,只能在命名空间的定义中定义子命名空间。
- 也不能直接使用“命名空间名::成员名 ……”定义方式,为命名空间添加新成员,而必须先在命名空间的定义中添加新成员的声明。
- 另外,命名空间是开放的,即可以随时把新的成员名称加入到已有的命名空间之中去。方法是,多次声明和定义同一命名空间,每次添加自己的新成员和名称。
- 还可以在一个命名空间中,组合其他命名空间,如在新命名空间内部using多个已有命名空间,使用时直接using新命名空间一个即可
三、使用命名空间
在引用命名空间成员时,要用命名空间名和作用域分辨符对命名空间成员进行限定,以区别不同的命名空间中的同名标识符。即:
命名空间名::命名空间成员名
这种方法是有效的,能保证所引用的实体有惟一的名字。但是如果命名空间名字比较长,尤其在有命名空间嵌套的情况下,为引用一个实体,需要写很长的名字。在一个程序中可能要多次引用命名空间成员,就会感到很不方便。
1 、使用命名空间别名
可以为命名空间起一个别名(namespace alias),用来代替较长的命名空间名。如
namespace Television //声明命名空间,名为Television
{ ... }
可以用一个较短而易记的别名代替它。如:
namespace TV=Television; //别名TV与原名Television等价
也可以说,别名TV指向原名Television,在原来出现Television的位置都可以无条件地用TV来代替。
2、使用using命名空间成员名
using后面的命名空间成员名必须是由命名空间限定的名字。例如:
using nsl::Student;
以上语句声明:在本作用域(using语句所在的作用域)中会用到命名空间ns1中的成员Student,在本作用域中如果使用该命名空间成员时,不必再用命名空间限定。例如在用上面的using声明后,在其后程序中出现的Student就是隐含地指nsl::Student。
using声明的有效范围是从using语句开始到using所在的作用域结束。如果在以上的using语句之后有以下语句:
Student studl(101,"Wang",18); //此处的Student相当于ns1::Student
上面的语句相当于
nsl::Student studl(101,"Wang",18);
显然,这可以避免在每一次引用命名空间成员时都用命名空间限定,使得引用命名空间成员变得方便易用。
但是要注意:在同一作用域中用using声明的不同命名空间的成员中不能有同名的成员。否则再次出现二义性,编译错误。
3、使用using namespace命名空间名
用上面介绍的using命名空间成员名,一次只能声明一个命名空间成员,如果在一个命名空间中定义了10个实体,就需要使用10次using命名空间成员名。能否在程序中用一个语句就能一次声明一个命名空间中的全部成员呢?
C++提供了using namespace语句来实现这一目的。using namespace语句的一般格式为
using namespace 命名空间名;
例如
using nanlespace nsl;
声明了在本作用域中要用到命名空间nsl中的成员,在使用该命名空间的任何成员时都不必用命名空间限定。
用usmgnamespace声明的作用域中,命名空间nsl的成员就好像在全局域声明的一样。因此可以不必用命名空间限定。显然这样的处理对写程序比较方便。但是如果同时用usingnamespace声明多个命名空间时,往往容易出错。若多个命名空间内存在同名成员,则二义性,编译出错。因此只有在使用命名空间数量很少,以及确保这些命名空间中没有同名成员时才用using namespace语句。
注意:
using指令与using声明的比较
using编译指令和using声明,都可以简化对命名空间中名称的访问。
using指令使用后,可以一劳永逸,对整个命名空间的所有成员都有效,非常方便。而using声明,则必须对命名空间的不同成员名称,一个一个地去声明,非常麻烦。
但是,一般来说,使用using声明会更安全。因为,using声明只导入指定的名称,如果该名称与局部名称发生冲突,编译器会报错。而using指令导入整个命名空间中的所有成员的名称,包括那些可能根本用不到的名称,如果其中有名称与局部名称发生冲突,则编译器并不会发出任何警告信息,而只是用局部名去自动覆盖命名空间中的同名成员。特别是命名空间的开放性,使得一个命名空间的成员,可能分散在多个地方,程序员难以准确知道,别人到底为该命名空间添加了哪些名称。
虽然使用命名空间的方法,有多种可供选择。但是不能贪图方便,一味使用using 指令,这样就完全背离了设计命名空间的初衷,也失去了命名空间应该具有的防止名称冲突的功能。
一般情况下,对偶尔使用的命名空间成员,应该使用命名空间的作用域解析运算符来直接给名称定位。而对一个大命名空间中的经常要使用的少数几个成员,提倡使用using声明,而不应该使用using编译指令。只有需要反复使用同一个命名空间的许多数成员时,使用using编译指令,才被认为是可取的。
四、无名命名空间
标准C++引入命名空间,除了可以避免成员的名称发生冲突之外,还可以使代码保持局部性,从而保护代码不被他人非法使用。如果你的目的主要是后者,而且又为替命名空间取一个好听、有意义、且与别人的命名空间不重名的名称而烦恼的话,标准C++还允许你定义一个无名命名空间。你可以在当前编译单元中(无名命名空间之外),直接使用无名命名空间中的成员名称,但是在当前编译单元之外,它又是不可见的。
无名命名空间的定义格式为:
namespace {
声明序列可选
}
实际上,上面的定义等价于:(标准C++中有一个隐含的使用指令)
namespace $$$ {
声明序列可选
}
using namespace $$$;
如:namespace //命名空间没有名字
{ void fun( ) //定义命名空间成员
{ cout<<"OK."<<endl;}
}
由于命名空间没有名字,在其他文件中显然无法引用,它只在本文件的作用域内有效。无名命名空间的成员fun函数的作用域为文件A(确切地说,是从声明无名命名空间的位置开始到文件A结束)。在文件A中使用无名命名空间的成员,不必(也无法)用命名空间名限定。
可以联想到:在C浯言中可以用static声明一个函数,其作用也是使该函数的作用域限于本文件。C++保留了用static声明函数的用法,同时提供了用无名命名空间来实现这一功能。随着越来越多的C++编译系统实现了ANSI C++建议的命名空间的机制,相信使用无名命名空间成员的方法将会取代以前习惯用的对全局变量的静态声明。
五、标准命名空间std
为了解决C++标准库中的标识符与程序中的全局标识符之间以及不同库中的标识符之间的同名冲突,应该将不同库的标识符在不同的命名空间中定义(或声明)。标准C++库的所有的标识符都是在一个名为std的命名空间中定义的,或者说标准头文件(如iostream)中函数、类、对象和类模板是在命名空间std中定义的。std是standard(标准)的缩写,表示这是存放标准库的有关内容的命名空间
这样,在程序中用到C++标准库时,需要使用std作为限定。如
std::cout<<"OK."<<endl; //声明cout是在命名空间std中定义的流对象
在有的C++书中可以看到这样的用法。但是在每个cout,cm以及其他在std中定义的标识符前面都用命名空间std作为限定,显然是很不方便的。在大多数的C++程序中常用usmgnamespace语句对命名空间std进行声明,这样可以不必对每个命名空间成员一进行处理,在文件的开头加入以下using namespace声明:
using namespace std;
这样,在std中定义和声明的所有标识符在本文件中都可以作为全局量来使用。但是应当绝对保证在程序中不出现与命名空间std的成员同名的标识符,例如在程序中不能再定义一个名为cout的对象。由于在命名空间std中定义的实体实在太多,有时程序设计人员也弄不请哪些标识符已在命名空间std中定义过,为减少出错机会,有的专业人员喜欢用若干个"using命名空间成员”声明来代替“using namespace命名空间”声明,如
using Std::string;
using Std::cout;
using Std::cin;
等。为了减少在每一个程序中都要重复书写using声明,程序开发者往往把编写应用程序时经常会用到的命名空间std成员的usmg声明组成一个头文件,然后在程序中包含此头文件即可。
六、使用早期的函数库
C语言程序中各种功能基本上都是由函数来实现的,在C语言的发展过程中建立了功能丰富的函数库,C++从C语言继承了这份宝贵的财富。在C++程序中可以使用C语言的函数库。
如果要用函数库中的函数,就必须在程序文件中包含有关的头文件,在不同的头文件中,包含了不同的函数的声明。
在C++中使用这些头文件有两种方法。
1、用C语言的传统方法
头文件名包括后缀.h,如stdio.h,math.h等。由于
C语言没有命名空间,头文件并不存放在命名空间中,因此在C++程序文件中如果用到带后缀.h的头文件时,不必用命名空间。只需在文件中包含所用的头文件即可。如
#include<math.h>
2、用C++的新方法
C++标准要求系统提供的头文件不包括后缀.h,例如iostream、string。为了表示与C语言的头文件有联系又有区别,
C++所用的头文件名是在C语言的相应的头文件名(但不包括后缀.h)之前加一字母c。例如,C语言中有关输入与输出的头文件名为stdio.h在C++中相应的头文件名为cstdio。C语言中的头文件math.h,在C++中相应的头文什名为cmath。C语言中的头文件string.h在C++中相应的头文件名为cstring。注意
在C++中,头文件csting和头文件string不是同一个文件。前者提供C语言中对字符串处理的有关函数(如strcmp,ctrcpy)的声明,后者提供C++中对字符串处理的新功能。
此外,由于这些函数都是在命名空间std中声明的,因此在程序中要对命名空间std作声明。如:
#include<cstdio>
#include<cmath>
using namespace std;
目前所用的大多数C++编译系统既保留了c的用法,又提供丁C++的新方法。下面两种用法等价,可以任选。
C传统方法 C++新方法
#include<stdio.h> #include<cstdio>
#include<math.h> #include<cmath>
#include<string.h> #include<cstring>
using namespace std;
可以使用传统的c方法,但应当提倡使用C++的新方法。