为引出本文主题,先举个例子,以便后文对照理解:某学校一年招了3个叫李明的学生,为便于区分:1)按年龄排序分别称大、中、小李明;2)把他们分到不同班,这样各班内部就没有同名的李明,而学校范围内可用1班/2班李明区分;3)大李明加入绘画协会,中李明加入书法协会,小李明加入绘画和外语协会,于是绘画协会里还要用大小或班级区分两个李明。
基本几点:
1.谈到重名,需要指定具体集合范围。全校集里3个李明,各班内部无重名,绘画协会里2个李明。学术化表述就是(校/协会)域内存在命名冲突。
2.方法一:把存在重名元素的集合分成若干子集,同名元素分到不同子集,用子集名+元素名方式区分(123班李明)。
3.方法二:利用重名元素其他固有属性,即名字+属性进行区分(大中小李明)。两种方法可同时使用。
作用域及域内/域间重名
C/C++中变量、函数及类等元素起作用的范围,称为作用域。在某作用域内为元素命名时,可选择的命名范围称为命名空间(C++新增的一个feature:用户自定义命名空间,也常被简称为命名空间namespace,为区别下文称前者为generalized namespace(GNS).后者user defined namespace(UDNS)。举例:
int c; //①
fun1()
{
int a,b ;
int c; //②
c=a+b; //③
}
fun2()
{
int a, b;
c=a+b; //④
}
int a,b;定义于两个函数内局部作用域,这两个局部作用域的GNS相互独立不干扰,即同层次不同作用域间GNS无重叠,因此两套a,b不冲突,这很好理解。
①处的c作用于全局域,和②处fun1内的c属于不同层的域间同名,这种情况的选择原则是向上就近优先,即:fun1域内③处引用的c就近选择同一域内②定义的局部变量c而不是全局域①的全局变量c;而fun2内没有定义c,④处才向上选择全局域①。
C语言大致分3层作用域:全局、单个文件及局部(函数及更小的{}功能模块内,如for{}, while{}或独立{}),C++开始引入类作用域,后来又加入用户自定义的命名空间作用域(后半部分讲解)。处于不同层次作用域内部和域间的元素,命名冲突及覆盖关系如下:
1)函数或{}模块内的变量(即局部变量)数量有限,命名选择充裕(除非某些懒人总用单个字母a,b,c,d命名变量:))。而不同函数或模块间的命名又互不干扰,因此局部域没有命名冲突问题。
2)C++的类域情况相似,只不过处于同一类域中同名不同参的成员函数还可通过重载机制区分(重载其实利用大中小李明的方法,即参数加函数名区分)。
3)C文件域内不能有同名函数和变量,C++单个文件域内重名的独立函数同样可重载区分。
4)全局作用域包含程序所有源文件、头文件和库,对每个程序唯一,类和非static的独立函数都作用于全局域。实践中大型软件要先由多人分别实现不同模块,最后组合,模块开发阶段类和函数独立命名,因而随意占用全局域的GNS,组合时不同模块间就可能出现类或函数的同名冲突。
可见作用域和命名冲突密切相关,几点原则:
1)作用域内部不能有同名元素,如:函数内不能有同名局部变量;类内不能有同名成员变量;文件和程序全局内不能有同名类。表面上C++重载函数可支持域内重名,实际它额外利用参数信息,真正的名字变成大中小李明,而不仅是李明,所以还是不同名。
2)同层次的域有相互独立的GNS空间,如:函数fun1和fun2中的局部变量名互不干扰;Class中的成员变量和函数也不必担心与其它类中的元素同名;位于不同文件域中的元素名也互不干扰,有人问,两个文件中同名函数不是冲突么?注意:定义于某文件不代表作用域就位于该文件域内,只有static修饰的变量和函数,其作用域才被限制在单个文件域内,非static的变量和函数,其作用域突破单个文件扩散到程序全局。因此所谓不同文件中不能有同名全局变量和函数,并非不同文件域的GNS空间会重叠,而是因为这些变量和函数实际都作用于全局域,而单个全局域内自然不能有同名元素。
3)不同层次域中的同名元素不冲突,而是遵循向上就近原则决定引用对象。如前例fun1和fun2中c的选择,更进一步,如下例:
extern int c //①
static int c; //②
class A
{
public:
int c; //③
void fun1();
};
A::fun1()
{
int c; //④
int a,b;
c = a+b; //⑤
}
在A::fun1()内的局部域④、 classA的类域③、文件内部②及外部全局域①均定义有int c。引用c的语句④c=a+b位于fun1内,因此就近选④,如未定义④,次近就是类域里的③,如③也没有,再选文件域内的②,最后是全局的①。换种说法:局部域、类域、文件域到全局域逐层向上屏蔽。
4)补充:编译器只考虑该元素调用前声明的名字,所以元素使用前须先声明,放在使用后的声明不被考虑在内。如上例改为:
A::fun1()
{
int a,b;
c = a+b; //⑤
…
int c; //④
}
⑤处引用c时,函数局部域内的int c④还未定义,搜索时会把它排除在外,而选择更上层类域中的③。
5)C++新增域限定符::,可用该符号指定引用哪个域的元素,如前例fun1中可用::c=a+b;或A::c=a+b分别指代全局①和类域内③处的c。注意对A::fun1()这种类成员函数定义形式,限定符::之后包括函数名、参数类型、函数定义在内均被拖回了类域作用范围,因此A::fun1()定义部分可直接使用类成员而不必再用::限定,但函数返回值类型位于::号之前,不受影响,仍位于类域之外。因此:
typedef string Mytype; //①
class B
{
typedef int MyType; //②
public:
MyType fun2(MyType a); //③
};
MyType A::fun2(MyType a) //④::使所有阴影部分位于类域,而位于::前的返回值除外
{
……
}
④处A::fun2不只把fun2名字纳入类域,其后的形参MyType、函数定义体{}部分都进入类域,即④处形参MyType为类域中的int型②。但返回值MyType型位于类域之外,为①处的string型,这样函数返回值与③处声明不同。应改为:B::MyType A::fun2(MyTypea)
域限定符::在类域中应用很多,后续和C++继承多态等结合还会产生很多引申规则,不再深入,可参考<<C++ Premier>>。
自定义命名空间(UDNS)作用域
如上文所述由于模块化编程,大型软件在全局作用域很容易产生命名冲突。实践中解决方法之一是在函数前加上公司或项目缩写做模块前缀,如add()变为Microsoft_add(),以降低冲突的可能性(分级命名),但这样逐个手动添加,不灵活,最好有种机制让编译器批量定义。自定义命名空间UDNS就是ANSIC++新引入的由用户命名的作用域,它的功能可认为是给成员自动添加命名前缀。
C++中定义UDNS的语法为:namespace XXX {},{}内可包括:变量、常量、数(定义或声明)、结构体、类、模板、子命名空间等。其定义方法与类相似,只不过声明类时}后有分号,而定义命名空间时}后无分号。如:
namespace test1 //定义命名空间test1
{
int a;
int fun1();
class MyClass
{
};
}
test1是用户指定的UDNS名,{}中的元素为UDNS的成员(namespace member)。注意这些成员仍是全局的,只是把它们隐藏在指定命名空间中(理解为test1_a, test1_fun1, test1_MyClass)。
UDNS作用域的层次位于全局域之下,其他域之上。可根据需要设置多个不同名的UDNS,把易冲突的全局元素分散放在不同UDNS域中,防止全局命名冲突。从这点看,UDNS和全局变量类似操作系统中目录和文件的关系:文件很多时,管理困难且易重名,于是设立若干子目录,把文件分散到不同子目录中;不同子目录中的文件可以同名;同一子目录下则不能同名;子目录还可嵌套子目录;调用文件时要指出文件路径。
UDNS的使用方法
1)using UDNS名::成员名
UDNS名::成员名是最直接调用成员的方式,如testl::MyClass obj1;,但每次都这样重复写很麻烦,可先用using UDNS名::成员名的形式声明某成员,然后代码里就可以不加前缀直接使用,如:
using testl::MyClass; //声明其后出现的MyClass属于命名空间testl中的MyClass
MyClass obj1; //相当于testl::MyClass obj1
MyClass obj2;
其作用就是:本using语句所在作用域中会用到命名空间test1中的成员MyClass,如用到它则不必再用命名空间test1::限定。强调:using声明的有效范围是从using语句开始到using所在的作用域结束。只有此范围内的MyClass才代表test1::MyClass。
实践中可以把常用的using UDNS::成员名组成头文件,用到时包含此头文件。
2)用using namespace UDNS名
用方式1)一次只能声明单个UDNS成员,而用using namespace UDNS名的方式则可一次声明全部UDNS的成员,如:
using namespace test1;
……
MyClass obj1;
fun1();
即用using namespace UDNS名声明后可直接使用该UDNS中所有成员而不必再用::限定。这种方式更简单,但别忘了创建UDNS的本意是防止命名冲突,轻易放开UDNS的全部成员又会增加冲突的概率,如:
void main()
{
using namespace test1;//test1的成员可在main所在局部作用域内直接使用
using namespace test2;//test2的成员可在main所在局部作用域内直接使用
fun1();
}
假如testl和test2中都包含fun1(),而它们又用using namespace方式全部放开,编译器就无法判定fun1()属于哪个UDNS。所以用哪种方式引用UDNS成员要综合考虑。
无名命名空间
C++允许没有名字的命名空间,如:
namespace //命名空间没有名字
{
void fun1( ) //无名空间成员fun1
{ cout<<"hello world"<<endl;}
}
命名空间没有名字,其他文件显然无法引用,只能在当前文件起效。使用其成员无需也无法用UDNS::限定,直接调用fun1();即可。与此类似,static也限制元素作用域到当前文件,只不过无名空间能一次包含多个成员。C++保留static同时提供无名命名空间。
标准命名空间std
标准C++库所有标识符都包含在一个std命名空间中,程序用到C++标准库的元素时需用std::限定。如常见的std::cout<<"hello world"<<endl;同样为避免频繁的std::限定,可用using namespace std;声明,但要保证程序中不出现与std成员同名的元素。早期C++库中的元素一般没放在std命名空间中,因此可以不必声明std。
总结:
无论形式怎样变化,命名冲突问题本质上基本是靠分班和附加属性(大中小李明)两种方式解决,而且分班也需要适度,如果分得太多,班级名也同样存在重名可能啊。突发奇想:C/C++中为什么不直接用文件名做UDNS,妄自推测下:不同库中常常有同名文件?每次调用文件外的函数都要指定哪个文件定义,不友好?相比UDNS,以文件划分缺少灵活性?
原文:http://blog.csdn.net/ipmux/article/details/44565807