C++/C++11中命名空间(namespace)的使用

2023-01-10 08:48:44

大型程序往往会使用多个独立开发的库,这些库又会定义大量的全局名字,如类、函数和模板等。当应用程序用到多个供应商提供的库时,不可避免地会发生某些名字相互冲突的情况。多个库将名字放置在全局命名空间中将引发命名空间污染(namespace pollution)。

传统上,程序员通过将其定义的全局实体名字设得很长来避免命名空间污染问题,这样的名字中通常包含表示名字所属库的前缀部分。这种解决方案显然不太理想:对于程序员来说,书写和阅读这么长的名字费时费力且过于繁琐。

命名空间(namespace)为防止名字冲突提供了更加可控的机制。命名空间分割了全局命名空间,其中每个命名空间是一个作用域。通过在某个命名空间中定义库的名字,库的作者以及用户可以避免全局名字固有的限制。

命名空间定义:一个命名空间的定义包含两部分:首先是关键字namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其它命名空间。命名空间结束后无须分号,这一点与块类似。和其它名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。命名空间既可以定义在全局作用域内,也可以定义在其它命名空间中,但是不能定义在函数或类的内部。命名空间作用域后面无须分号。

每个命名空间都是一个作用域:和其它作用域类似,命名空间中的每个名字都必须表示该空间内的唯一实体。因为不同命名空间的作用域不同,所以在不同命名空间内可以有相同名字的成员。

定义在某个命名空间中的名字可以被该命名空间内的其它成员直接访问,也可以被这些成员内嵌作用域中的任何单位访问。位于该命名空间之外的代码则必须明确指出所用的名字属于哪个命名空间。

命名空间可以是不连续的:命名空间可以定义在几个不同的部分,这一点与其它作用域不太一样。命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成一个命名空间。此时,命名空间的组织方式类似于我们管理自定义类及函数的方式:命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中,这些头文件将被包含在使用了这些成员的文件中。命名空间成员的定义部分则置于另外的源文件中。

在程序中某些实体只能定义一次:如非内联函数、静态数据成员、变量等,命名空间中定义的名字也需要满足这一要求。这种接口和实现分离的机制确保我们所需的函数和其它名字只定义一次,而只要是用到这些实体的地方都能看到对于实体名字的声明。定义多个类型不相关的命名空间应该使用单独的文件分别表示每个类型(或关联类型构成的集合)。

#include 应该出现在打开命名空间的操作之前。在通常情况下,我们不把#include放在命名空间内部。如果我们这么做了,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。

定义命名空间成员:假定作用域中存在合适的声明语句,则命名空间中的代码可以使用同一命名空间定义的名字的简写形式。也可以在命名空间定义的外部定义该命名空间的成员。命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间。命名空间之外定义的成员必须使用含有前缀的名字。和定义在类外部的类成员一样,一旦看到含有完整前缀的名字,我们就可以确定该名字位于命名空间的作用域内。尽管命名空间的成员可以定义在命名空间外部,但是这样的定义必须出现在所属命名空间的外层空间中。

模板特例化:模板特例化必须定义在原始模板所属的命名空间中。和其它命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了。

全局命名空间:全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间(global namespace)中。全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字被隐式地添加到全局命名空间中。

作用域运算符(::)同样可以用于全局作用域的成员,因为全局作用域是隐式的,所以它并没有名字。

嵌套的命名空间:是指定义在其它命名空间中的命名空间。嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符。

内联命名空间:C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace)。和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。定义内联命名空间的方式是在关键字namespace前添加关键字inline。关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。当应用程序的代码在一次发布和另一次发布之间发生了改变时,常常会用到内联命名空间。

未命名的命名空间(unnamed namespace):是指关键字namespace后紧跟花括号括起来的一系列声明语句。未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。

一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。和其它命名空间不同,未命名的命名空间仅在特定的文件内有效,其作用范围不会横跨多个不同的文件。

定义在未命名的命名空间中的名字可以直接使用,毕竟我们找不到什么命名空间的名字来限定它们;同样的,我们也不能对未命名的命名空间的成员使用作用域运算符。

未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别。其它情况下,未命名的命名空间中的成员都属于正确的程序实体。和所有命名空间类似,一个未命名的命名空间也能嵌套在其它命名空间当中。此时,未命名的命名空间中的成员可以通过外层命名空间的名字来访问。

未命名的命名空间取代文件中的静态声明:在标准C++引入命名空间的概念之前,程序需要将名字声明成static的以使得其对于整个文件有效。在文件中进行静态声明的做法是从C语言继承而来的。在C语言中,声明为static的全局实体在其所在的文件外不可见。在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

使用命名空间成员:命名空间的别名(namespace alias)使得我们可以为命名空间的名字设定一个短得多的同义词。命名空间的别名声明以关键字namespace开始,后面是别名所用的名字、=符号、命名空间原来的名字以及一个分号。不能在命名空间还没有定义前就声明别名,否则将产生错误。命名空间的别名也可以指向一个嵌套的命名空间。一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。

using声明:一条using声明(usingdeclaration)语句一次只引入命名空间的一个成员。它使得我们可以清楚地知道程序中所用的到底是哪个名字。

using声明引入的名字遵守与过去一样的作用域规则:它的有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。未加限定的名字只能在using声明所在的作用域以及其内层作用域中使用。在有效作用域结束后,我们就必须使用完整的经过限定的名字了。

一条using声明语句可以出现在全局作用域、局部作用域、命名空间作用域以及类的作用域中。在类的作用域中,这样的声明语句只能指向基类成员。

using指示(usingdirective):和using声明类似的地方是,我们可以使用命名空间名字的简写形式;和using声明不同的地方是,我们无法控制哪些名字是可见的,因为所有名字都是可见的。using指示以关键字using开始,后面是关键字namespace以及命名空间的名字。如果这里所用的名字不是一个已经定义好的命名空间的名字,则程序将发生错误。using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中。

using指示使得某个特定的命名空间中的所有名字都可见,这样我们就无须再为它们添加任何前缀限定符了。简写的名字从using指示开始,一直到using指示所在的作用域结束都能使用。如果我们提供了一个对std等命名空间的using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题。

using指示与作用域:using指示引入的名字的作用域远比using生命引入的名字的作用域复杂。using声明的名字的作用域与using声明语句本身的作用域一致,从效果看就好像using声明语句为命名空间的成员在当前作用域内创建了一个别名一样。using指示所做的绝非声明别名这么简单。相反,它具有将命名空间成员提升到包含命名空间本身和using指示的最近作用域的能力。

using声明和using指示在作用域上的区别直接决定了它们工作方式的不同。对应using声明来说,我们只是简单地令名字在局部作用域内有效。相反,using指示是令整个命名空间的所有内容变得有效。通常情况下,命名空间中会含有一些不能出现在局部作用域中的定义,因此,using指示一般被看作是出现在最近的外层作用域中。

当命名空间被注入到它的外层作用域之后,很有可能该命名空间中定义的名字会与其外层作用域中的成员冲突。这种冲突是允许存在的,但是要想使用冲突的名字,我们就必须明确指出名字的版本。

头文件与using声明或指示:头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。

避免using指示:using指示一次性注入某个命名空间的所有名字,这种用法看似简单实则充满了风险:只使用一条语句就突然将命名空间中所有成员的名字变得可见了。如果应用程序使用了多个不同的库,而这些库中的名字通过using指示变得可见,则全局命名空间污染的问题将重新出现。而且,当引入库的新版本后,正在工作的程序很可能会编译失败。如果新版本引入了一个与应用程序正在使用的名字冲突的名字,就会出现这个问题。另一个风险是由using指示引发的二义性错误只有在使用了冲突名字的地方才能被发现。这种延后的检测意味着可能在特定库引入很久之后才爆发冲突。直到程序开始使用该库的新部分后,之前一直未被检测到的错误才会出现。

相比于使用using指示,在程序中对命名空间的每个成员分别使用using声明效果更好,这么做可以减少注入到命名空间中的名字数量。using声明引起的二义性问题在声明处就能发现,无须等到使用名字的地方,这显然对检测并修改错误大有益处。

using指示也并非一无是处,例如在命名空间本身的实现文件中就可以使用using指示。

类、命名空间与作用域:对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。对于位于命名空间中的类来说,常规的查找规则仍然适用:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找。可以从函数的限定名推断出查找名字时检查作用域的次序,限定名以相反次序指出被查找的作用域。

实参相关的查找与类类型形参:对于命名空间中名字的隐藏规则来说有一个重要的例外。这个例外是,当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。查找规则的这个例外运行概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。

using声明语句声明的是一个名字,而非一个特定的函数。当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中。一个using声明囊括了重载函数的所有版本以确保不违反命名空间的接口。库的作者为某项任务提供了好几个不同的函数,允许用户选择性地忽略重载函数中的一部分但不是全部有可能导致意想不到的程序行为。

一个using声明引入的函数将重载该声明语句所属作用域中已有的其它同名函数。如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域中已经有一个函数或新引入的函数同名且形参列表相同,则该using声明将引发错误。除此之外,using声明将为引入的名字添加额外的重载实例,并最终扩充候选函数集的规模。

using指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中。与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数将不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分。

在C++语言中,命名空间是一种实体(entity),使用namespace来声明,并使用{}来界定命名空间体(namespacebody)。和C语言的全局作用域兼容,C++具有全局命名空间作用域,对应的命名空间是全局命名空间。全局命名空间不需要声明。使用时,可以用前缀为::的qualified-id显示限定全局命名空间作用域中的名称。

命名空间可以在另一命名空间之中嵌套声明;但不能声明在类和代码块之中。在命名空间中声明的名称,默认具有外部链接属性(除非声明的是const对象,它默认是具有内部链接属性)。

按照是否有名字,可分为有名字的命名空间和匿名命名空间。匿名命名空间中的名字具有文件作用域。这些名字在本编译单元中可以直接使用;也可以用前缀为::的qualified-id显示限定后使用。匿名命名空间中的名字具有内部链接属性。

命名空间的成员,是在命名空间体的花括号内声明了的名称。可以在命名空间体之外,给出命名空间成员的定义。即命名空间的成员声明与定义可以分开。命名空间内的名字,只能有一次定义,但可以多次声明。嵌套的子命名空间必须定义在上层命名空间体之内。禁止把子命名空间的声明与定义分开。不能以”命名空间名::成员名;”方式,在命名空间体之外为命名空间添加新成员。必须在命名空间体之中添加新成员的声明。可以多次声明和定义同一命名空间,每次给这一命名空间添加新成员。同名的命名空间即便在声明位置不同,仍然是同一个实体。可以在一个命名空间中引入其他命名空间的成员。

C++11起支持内联命名空间。使用inline namespace作为声明的起始。内联命名空间的名称在名称查找时被特别对待,使用qualified-id引用其中的名称时,被内联的命名空间名称可以省略。也即,内联命名空间内的标识符被提升到包含着内联的命名空间的那个父级的命名空间中。内联命名空间可以在修改命名空间名称的同时避免在二进制文件中生成的符号改变,因此不同内联命名空间的名称可以用于标识接口兼容的不同版本,有助于保持二进制兼容性。这也在标准库的实现中被使用,如libstdc++和libc++。

下面是从其他文章中copy的测试代码,详细内容介绍可以参考对应的reference:

#include "namespace.hpp"
#include <iostream>
#include <vector>
#include <string>

namespace namespace_ {


// reference: http://en.cppreference.com/w/cpp/language/namespace
namespace A {
	int x;
}

namespace B {
	int i;
	struct g { };
	struct x { };
	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
	void f(double) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
	void g(char) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); }; // OK: function name g hides struct g
}

int test_namespace_1()
{
	int i;
	//using B::i; // error: i declared twice

	void f(char);
	using B::f; // OK: f(char), f(int), f(double) are overloads
	f(3.5); // calls B::f(double)

	using B::g;
	g('a');      // calls B::g(char)
	struct g g1; // declares g1 to have type struct B::g

	using B::x;
	using A::x;  // OK: hides struct B::x
	x = 99;      // assigns to A::x
	struct x x1; // declares x1 to have type struct B::x

	return 0;
}

/
// reference: http://en.cppreference.com/w/cpp/language/namespace
namespace D {
	int d1;
	void f(char) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
}
using namespace D; // introduces D::d1, D::f, D::d2, D::f,
//  E::e, and E::f into global namespace!

int d1; // OK: no conflict with D::d1 when declaring
namespace E {
	int e;
	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
}
namespace D { // namespace extension
	int d2;
	using namespace E; // transitive using-directive
	void f(int) { fprintf(stdout, "%s, %d\n", __FUNCTION__, __LINE__); };
}

int test_namespace_2()
{
	//d1++; // error: ambiguous ::d1 or D::d1?
	::namespace_::d1++; // OK
	D::d1++; // OK
	d2++; // OK, d2 is D::d2
	e++; // OK: e is E::e due to transitive using
	//f(1); // error: ambiguous: D::f(int) or E::f(int)?
	f('a'); // OK: the only f(char) is D::f(char)

	return 0;
}

//
// reference: http://en.cppreference.com/w/cpp/language/namespace
namespace vec {

	template< typename T >
	class vector {
		// ...
	};

} // of vec

int test_namespace_3()
{
	std::vector<int> v1; // Standard vector.
	vec::vector<int> v2; // User defined vector.

	//v1 = v2; // Error: v1 and v2 are different object's type.

	{
		using namespace std;
		vector<int> v3; // Same as std::vector
		v1 = v3; // OK
	}

	{
		using vec::vector;
		vector<int> v4; // Same as vec::vector
		v2 = v4; // OK
	}

	return 0;
}

///
// reference: https://msdn.microsoft.com/en-us/library/5cb46ksf.aspx
/*namespace Test {
	namespace old_ns {
		std::string Func() { return std::string("Hello from old"); }
	}

	inline namespace new_ns {
		std::string Func() { return std::string("Hello from new"); }
	}
}

int test_namespace_4()
{
	using namespace Test;
	using namespace std;

	string s = Func();
	std::cout << s << std::endl; // "Hello from new"
	return 0;
} */

///
// reference: https://www.tutorialspoint.com/cplusplus/cpp_namespaces.htm
// first name space
namespace first_space {
	void func() {
		std::cout << "Inside first_space" << std::endl;
	}

	// second name space
	namespace second_space {
		void func() {
			std::cout << "Inside second_space" << std::endl;
		}
	}
}

int test_namespace_5()
{
	using namespace first_space::second_space;

	// This calls function from second name space.
	func();

	return 0;
}

/
// reference: http://www.geeksforgeeks.org/namespace-in-c/
// A C++ code to demonstrate that we can define methods outside namespace.
// Creating a namespace
namespace ns {
	void display();

	class geek {
	public:
		void display();
	};
}

// Defining methods of namespace
void ns::geek::display()
{
	std::cout << "ns::geek::display()\n";
}

void ns::display()
{
	std::cout << "ns::display()\n";
}

int test_namespace_6()
{
	ns::geek obj;
	ns::display();
	obj.display();

	return 0;
}

} // using namespace_

GitHub:  https://github.com/fengbingchun/Messy_Test 
  • 作者:fengbingchun
  • 原文链接:https://blog.csdn.net/fengbingchun/article/details/78575978
    更新时间:2023-01-10 08:48:44