我们都知道在一个作用域中定义的每个标志符在该作用域中应该是唯一的,独一无二的。但对于庞大,复杂的系统应用程序而言,这个要求有时候很难达满足。对于庞大的应用程序,有些标识符几乎无可避免的发生冲突,这种标志符冲突问题被称之为“命名空间污染问题”。
C语言定义了3中层次的作用域,文件(编译单元)、函数和符合语言。C++又引入了类作用域,类是出现在文件内的。在不同的作用域中可以定义相同名字的变量,互不于扰,系统能够区别它们。我们现在分析存在的命名空间污染问题。
全局变量命名冲突。全局变量的作用域是整个程序,在全局域中不应有两个或多个同名的标识符,包括变量、函数和类等。如果你开发的工程是小型工程,参与人员几个人。这是很容易办不到的。但是如果你开发的工程是大型工程,开发人员100人以上。若要保证全局作用域中不出现同名的标识符这是很难做到。这类问题不乏例子。如果你多年从事C/C++程序开发,应该是深有体会的。
假设小王和小刘都参与了某个大型项目。他们彼此不知道对方参加了此大型项目的开发。小王实现了下面这段代码
int g_system_init = 0;
......
bool JudgeSystem()
{
if (0 != g_system_init)
.....
}
小刘实现了下面的这段代码:
int g_system_init = 0;
......
bool Init()
{
if (0 != g_system_init)
.....
}
也许你会说这么简单的错误,谁看不出来。但是如果是一个大型项目,代码行数上千万呢?我想那时去查找这样的错误,应该没你说的这么容易。而且小王和小刘彼此不认识。事实上,查找和避免这样的问题是很难的,因为在开发之前,你无法告诉小王和小刘那些变量名是不可以定义的,那些是可以定义的。这就是全局变量的命名冲突问题。
实际上,问题还不仅如此。出现上述问题,我们可通过修改项目代码实现。但是如果你的项目中使用了很多开源库,那就更麻烦了。你知道的,开源库是不能修改的。要么设计到知识产权问题,要么根本没修改的权限。
在程序中引用库(包括C++编译系统提供的库、由软件开发商提供的库或用户自己开发的库),为此需要包含有关的头文件。如果在这些库中包含有与程序的全局实体同名的实体,或者不同的库中有相同的实体名,则在编译时就会出现名字冲突。
为了避免这类问题的出现,人们提出了许多方法,例如:将实体的名字写得长—些(包含十几个或几十个字母和字符);把名字起得特殊一些,包括一些特殊的字符;由编译系统提供的内部全局标识符都用下划线作为前缀,如_complex(),以避免与用户命名的实体同名;由软件开发商提供的实体的名字用特定的字符作为前缀。但是这样的效果并不理想,而且增加了阅读程序的难度,可读性降低了。
C语言和早期的C++语言没有提供有效的机制来解决这个问题,没有使库的提供者能够建立自己的命名空间的工具。人们希望ANSI C++标准能够解决这个问题,提供—种机制、一种工具,使由库的设计者命名的全局标识符能够和程序的全局实体名以及其他库的全局标识符区别开来。
这些都是都可以成为“命名空间污染问题”。为了解决这些问题,C++于是就引入了命令空间。命名空间:实际上就是一个由程序设计者命名的内存区域,程序设计者可以根据需要指定一些有名字的空间域,把一些全局实体分别放在各个命名空间中,从而与其他全局实体分隔开来。 命名空间定义,以关键字namespace开始,其后接命名空间的名字:
namespace cpp_primer
{
class Sales_item
{
};
Sales_item operator+ (const Sales_item&, const Sales_item&);
class Query
{
public:
Query (const std::string());
std::ostream&display(std::ostream&) const;
}
class Query_base
{
};
}
小心陷阱
- 命名空间的名字在其所在作用域中是唯一的,命名控件可以在全局作用域或者其它作用域内部定义,但是不能在函数或者类的内部定义。命名空间作用域不能以分号结束。
- namespace是定义命名空间所必须写的关键字,cpp_prime是用户自己指定的命名空间的名字(可以用任意的合法标识符),在花括号内是声明块,在其中声明的实体称为命名空间成员(namespace member)。C++中命名空间的作用类似于操作系统中的目录和文件的关系,由于文件很多,不便管理,而且容易重名,于是人们设立若干子目录,把文件分别放到不同的子目录中,不同子目录中的文件可以同名。调用文件时应指出文件路径。
- 在声明一个命名空间时,花括号内不仅可以包括类,而且还可以包括:变量(可以带有初始化);
- 常量;数(可以是定义或声明); 结构体;类; 模板; 命名空间(在一个命名空间中又定义一个命名空间,即嵌套的命名空间)。
命名空间的作用:是建立一些互相分隔的作用域,把一些全局实体分隔开来。我们看下面这个例子:假设某所学校有3名学生叫李正国。当老师点名叫李正国时,3个人都会站起来应答,这就是名字冲突,因为他们无法辨别老师想叫的是哪一个李正国。为了避免同名混淆,学校把他们3个分在3个不同的班里。这样,在班级点名叫李正国时,只会有一个人应答。也就是说,在该班的范围(即班作用域)内名字是惟一的。如果在全校集合时校长点名,需要在全校范围内找这个学生,就需要考虑作用域问题。如果校长叫李正国,全校学生中又会有3人一齐喊“到”,因为在同一作用域(校作用域)中存在3个同名学生。为了在全校范围内区分这3名学生,校长必须在名字前加上班号,如高三甲班的李正国,或高三乙班的李正国,即加上班名限定。这样就不致产生混淆。
最佳实践:可以根据需要设置许多个命名空间,每个命名空间名代表一个不同的命名空间域,不同的命名空间不能同名。这样,可以把不同的库中的实体放到不同的命名空间中,或者说,用不同的命名空间把不同的实体隐蔽起来。过去我们用的全局变量可以理解为全局命名空间,独立于所有有名的命名空间之外,它是不需要用 namespace声明的,实际上是由系统隐式声明的,存在于每个程序之中。
讨论了命名空间的定义,接着分析命名空间的使用。
1)每个命名空间都是一个作用域
命名空间中的每个名字必须引用该命名空间中的唯一实体,命名空间中的实体称为命名空间的成员,不同命名空间的成员可以具有相同的名字。
命名空间内部各成员之间可以直接访问,外部的代码必须指出所引用成员名字定义在哪个命名空间中。例如:
cpp_primer::Query q = cpp_primer::Query("Hello");
2)从命名空间外部使用命名空间成员
使用限定名namespace_name::member_name。使用这种方法引用成员可能有些麻烦,可以使用using声明
using cpp_primer::Query;
在此声明之后,程序无需使用cpp_primer限定符,可直接使用成员名字Query。
3)命名空间定义可以是不连续的
与其他作用域不同,命名空间可以在不同的部分定义,命名空间由这些分离的定义部分组成,从这点来看,命名空间的定义是可以累积的。命名空间可以分散在不同的文件中,在不同的文件中的命名空间定义也是累积的。
由于名字只在声明名字的文件中可见,在这一规定的限制下,如果命名空间的一部分需要定义在另外的文件中,那么被定义部分的名字依然要再次声明。也就是说,如果该成员之前不存在,那就新增该成员及定义;如果该成员已经在其他文件中声明,那么在此处只是由于要定义所以再次出现了声明。
namespace namespace_name
{
}
4)接口和实现的分离
命名空间可以不连续意味着可以用分离的接口文件和实现文件构成命名空间。因此,可以使用管理类和函数定义相同的方法来组织命名空间。
- 定义类的命名空间成员,以及作为类接口的一部分函数声明、对象声明可以放在头文件中,使用命名空间成员的文件只需要包含这些头文件;
- 对命名空间中成员的定义可以放在单独的源文件中。
注意:根据这个方法,我们也可以通过不同的文件来对同一命名空间中不同类型的成员进行组织,将其放在不同的文件中。
5)命名空间成员的定义
可以在命名空间内部直接定义其成员;
也可以在命名空间外部定义命名空间的成员,其方式如下:
cpp_primer::Sales_item;
cpp_primer::operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs);
}
6)不能在不相关的命名空间中定义成员
7)全局命名空间
定义在全局作用域的名字(在任意类、函数或者命名空间外部声明的名字)是定义在全局命名空间(global namespace)中的。全局命名空间事隐式声明的,存在于每个程序中。其引用方式如下:
::member_name;
请谨记
|