C++命名空间详解

背景

大型程序往往会使用多个独立开发的库,这些库又会定义大量的全局名字,如类、函数和模板等。当应用程序用到多个供应商提供的库时,不可避免地会发生某些名字相互冲突的情况。

多个库将名字放置在全局命名空间中将引发命名空间污染

传统上,程序员通过将其定义的全局实体名字设得很长来避免命名空间污染问题,这样的名字中通常包含表示名字所属库的前缀部分:

cpp 复制代码
class cplusplus_primer_Query [ ... };
string cplusplus_primer_make_plural(size_t, string&);

这种解决方案显然不太理想:对于程序员来说,书写和阅读这么长的名字费时费力且过于烦琐。

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

命名空间定义

一个命名空间的定义包含两部分;首先是关键字 namespace,随后是命名空间的名字。在命名空间名字后面是一系列由花括号括起来的声明和定义。

注意末尾不需要分号

cpp 复制代码
namespace 命名空间名
{
//声明,定义

}//命名空间结束没有分号,这和块类似

只要能出现在全局作用域中的声明就能置于命名空间内,主要包括:类、变量(及其初始化操作)、函数(及其定义)、模板和其他命名空间:

和其他名字一样,命名空间的名字也必须在定义它的作用域内保持唯一。

cpp 复制代码
namespace A
{
	int a;
	int a;//错误,重复定义
}

命名空间既可以定义在全局作用域内,也可以定义在其他命名空间中,但是不能定义在函数或者类内部。

cpp 复制代码
#include<iostream>
using namespace std;
namespace A//定义在全局作用域内
{
	int a;
	int a;
	namespace B//定义在A内
	{}
}
void C()
{
	namespace C//错误,不能在函数内定义命名空间
	{
	}
}
class D
{
	namespace D//错误,不能在类内定义命名空间
	{ }
};
int main()
{}

每个命名空间都是一个作用域

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

cpp 复制代码
namespace A
{
	int a;
	int a;//错误,重复定义
	int b;
}
namespace B
{
	int b;//没有问题
}

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

cpp 复制代码
#include<iostream>
using namespace std;
namespace A//定义在全局作用域内
{
	int a;
	namespace B
	{
	int b=a;//使用外层命名空间A的变量a
	}
class AA
{};
}

int main()
{
	A::a = 4;//使用::来指明a是A命名空间的

}

通过::来调用不同命名空间里的同名变量

cpp 复制代码
#include<iostream>
using namespace std;

namespace B
{
	class AA{};
}
namespace A
{
	class AA
	{};
}


int main()
{
	A::AA;//调用A中的AA
	B::AA;//调用B中的AA
}

命名空间可以是不连续的

命名空间可以定义在几个不同的部分,这一点与其他作用域不太一样。

编写如下的命名空间定义:

cpp 复制代码
namespace A
{}
  1. 如果之前定义了名为 A的命名空间,则这段代码打开已经存在的命名空间定义并为其添加一些新成员的声明。
  2. 如果之前没有名为 A的命名空间定义,则上述代码创建一个新的命名空间。
cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a;
}
namespace A//打开已有的名称空间A
{
	int b;//添加新元素
}
int main()
{
	A::a = 9;
	A::b = 1;
}

命名空间的定义可以不连续的特性使得我们可以将几个独立的接口和实现文件组成个命名空间。

此时,命名空间的组织方式类似于我们管理自定义类及函数的方式:

  1. 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于头文件中。
  2. 这些头文件将被包含在使用了这些成员的文件中。命名空间成员的定义部分则置于另外的源文件中。

在程序中某些实体只能定义一次:如非内联函数、静态数据成员、变量等,命名空间中定义的名字也需要满足这一要求,我们可以通过上面的方式组织命名空间并达到目的。

这种接口和实现分离的机制确保我们所需的函数和其他名字只定义一次,而只要是用到这些实体的地方都能看到对于实体名字的声明。

我们看个例子

有一点需要注意,在通常情况下,我们不把#include放在命名空间内部。

如果我们把#include放在命名空间内部,隐含的意思是把头文件中所有的名字定义成该命名空间的成员。

例如,如果A.h在包含iostream头文件前就已经打开了命名空间A,则程序将出错,因为这么做意味着我们试图将命名空间 std 嵌套在命名空间A中。

定义命名空间成员

上面我们提了一点,我们可以将函数和类的声明和定义都放到命名空间里面,其实我们也可以在命名空间的外部定义该命名空间的成员。

不过命名空间对于名字的声明必须在作用域内,同时该名字的定义需要明确指出其所属的命名空间:

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a;
	void b();
}
void A::b()// 命名空间之外定义的成员必须使用含有前缀的名字
{
	cout << "定义成功" << endl;
}
int main()
{
	A::b();
}

和定义在类外部的类成员一样,一旦看到含有完整前缀的名字,我们就可以确定该名字位于命名空间的作用域内。在命名空间A内部,我们可以直接使用该命名空间的其他成员。

尽管命名空间的成员可以定义在命名空间外部,但是这样的定义必须出现在所属命名空间的外层空间中。

换句话说,我们可以在 命名空间A 或全局作用域中定义void b(),但是不能在一个不相关的作用域中定义这个运算符。

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a;
	void b();
}
void c(){
	void A::b()//这是错误的
	{
		cout << "定义成功" << endl;
	}
}
int main()
{
	A::b();
}

模板特例化

模板特例化必须定义在原始模板所属的命名空间中。

和其他命名空间名字类似,只要我们在命名空间中声明了特例化,就能在命名空间外部定义它了:

cpp 复制代码
// 我们必须将模板特例化声明成std的成员
natimespace std {
template <> struct hash<Sales_data>;
}
//在std中添加了模板特例化的声明后,就可以在命名空间std的外部定义它了
template > struct std::hash<Sales_data>
{
size_t operator() (const Sales_data s) const
freturn hash<string>()(s.bookNo)
hash<unsigned>()(s.units sold) ^
hash<double>()(s.revenue);
//其他成员与之前的版本一致
}

全局命名空间

全局作用域中定义的名字(即在所有类、函数及命名空间之外定义的名字)也就是定义在全局命名空间中。

全局命名空间以隐式的方式声明,并且在所有程序中都存在。全局作用域中定义的名字被隐式地添加到全局命名空间中。

cpp 复制代码
#include<iostream>
using namespace std;
int a;
char b;
//a,b位于全局命名空间中

int main()
{

}

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

下面的形式

cpp 复制代码
::member_name

表示全局命名空间中的一个成员。

我们看个例子

cpp 复制代码
#include<iostream>
using namespace std;
int a;
char b;
int main()
{
	::a+=3;//使用全局命名空间的a变量
	cout << ::a << endl;
	::b = 'a';//使用全局命名空间中的b变量
	cout << ::b << endl;
}

嵌套的命名空间

嵌套的命名空间是指定义在其他命名空间中的命名空间:

cpp 复制代码
namespace A
{
	namespace B//B是一个嵌套的命名空间
	{
	}
}

嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。

嵌套的命名空间中的名字遵循的规则与往常类似:

  • 内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。
  • 在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码要想访问它必须在名字前添加限定符。

内联命名空间

C++11新标准引入了一种新的嵌套命名空间,称为内联命名空间。

定义内联命名空间的方式是在关键字namespace 前添加关键字 inline

cpp 复制代码
inline namespace 命名空间名
{
}

和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。(注意是嵌套命名空间)

也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。

cpp 复制代码
#include<iostream>
using namespace std;
//隐式嵌套
inline namespace A//A命名空间是被嵌套在全局命名空间里
{
	int c;
}
int a=c;//可以直接使用内联命名空间里的c

int main()
{	}

关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。

cpp 复制代码
#include<iostream>
using namespace std;

inline namespace A
{
	int c;
}
int a=c;
namespace A/可以不写inline
{
	int b;
}
int d = b;

int main()
{
}

如果我们想使用一般的嵌套命名空间,则必须像其他嵌套的命名空间一样加上完整的外层命名空间名字。

cpp 复制代码
#include<iostream>
using namespace std;

namespace A
{
	int c;
}
int a=c;//错误
int b=A::c;//正确

int main()
{
}

未命名的命名空间

未命名的命名空间是指关键字namespace后紧跟花括号括来的一系列声明语句。

cpp 复制代码
namespace{
}

未命名的命名空间中定义的变量拥有静态生命周期:它们在第一次使用前创建,并且直到程序结束才销毁。

  1. 一个未命名的命名空间可以在某个给定的文件内不连续,但是不能跨越多个文件。
  2. 每个文件定义自己的未命名的命名空间,如果两个文件都含有未命名的命名空间,则这两个空间互相无关。在这两个未命名的命名空间中可以定义相同的名字,并且这些定义表示的是不同实体。
  3. 如果一个头文件定义了未命名的命名空间,则该命名空间中定义的名字将在每个包含了该头文件的文件中对应不同实体。

和其他命名空间不同,未命名的命名空间仅在特定的文件内部有效,其作用范围不会横跨多个不同的文件。

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

未命名的命名空间中定义的名字的作用域与该命名空间所在的作用域相同。如果未命名的命名空间定义在文件的最外层作用域中,则该命名空间中的名字一定要与全局作用域中的名字有所区别:

cpp 复制代码
int i;
namespace
{
int i;
}
i=10;//存在二义性

其他情况下,未命名的命名空间中的成员都属于正确的程序实体。

嵌套

和所有命名空间类似,一个未命名的命名空间也能嵌套在其他命名空间当中。

此时,未命名的命名空间中的成员可以通过外层命名空间的名字来访问:

cpp 复制代码
namespace local
{
namespace{
int i;
}
}
//正确:定义在嵌套的未命名的命名空间中的i与全局作用域中的i不同
local::i= 42;

注意

未命名的命名空间取代文件中的静态声明

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

在文件中进行静态声明的做法已经被C++标准取消了,现在的做法是使用未命名的命名空间。

命名空间的别名

命名空间的别名使得我们可以为命名空间的名字设定一个短得多的同义词。

例如,一个很长的命名空间的名字形如

cpp 复制代码
namespace cplusplus primer ( /*...*/);

我们可以为其设定一个短得多的同义词:

cpp 复制代码
namespace primer = cplusplus_primer;

命名空间的别名声明以关键字 namespace 开始,后面是别名所用的名字、=符号、命名
空间原来的名字以及一个分号,不能在命名空间还没有定义前就声明别名,否则将产生
错误。

命名空间的别名也可以指向一个嵌套的命名空间:

cpp 复制代码
namespace A
{
namespace B
{}
}

using b=A::B;

一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。

使用名称空间

首先我们来了解一下未限定的名称,限定的名称;

未限定的名称:不包含名称空间的名称(比如cout)

限定的名称:包含名称空间的名称(比如std::cout)

我们不希望每次使用名称时都对它进行限定,所以我们引入了using声明和using编译指令来简化对名称空间中名称的使用

using声明

using声明由被限定的名称和它前面的关键字using组成

比如

cpp 复制代码
using std::endl;
using std::cout;

using声明将特定的名称添加到它所属的声明区域中。完成该声明后使用该特定名称时可以直接使用未限定的名称,不用再使用限定的名称。

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

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

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

我们可以看个例子

cpp 复制代码
#include<iostream>
using std::cout;
using std::endl;
int main()
{
	cout << "加入成功" << endl;//可以使用未限定的名称
}

如果没有用using声明,(不用using编译指令的情况下),我们就得用限定的名称

cpp 复制代码
#include<iostream>
int main()
{
	std::cout << "加入成功" << std::endl;//使用限定的名称
}

使用using声明相当于引用了一个变量,我们可以把这条语句当作引用声明,就像引用别的文件的全局变量一样,using声明引入的变量/函数的特性和文件间用extern引用的全局变量的特性差不多我们可以看看几个例子

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a = 1;
}
int main()
{
	using A::a;
	{
		cout << a << endl;//结果是1
		int a = 2;
		cout << a << endl;//结果是2
	}
	cout << a<< endl;//结果是1
}

这我们就不作解释了,就作用域的问题,内部隐藏外部的问题

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a = 1;
}
int main()
{
	using A::a;
int a=4;//这是不行的,因为using声明相当于引入了一个变量
//在同一声明区域内,不能再定义重名的变量
}

注意using声明的防重名的机制

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a = 1;
}
using A::a;
int main()
{	
    {
		cout << a << endl;//结果是1
		int a = 2;
		cout << a << endl;//结果是2
	}
	cout << a<< endl;//结果是1
}

这时a作为全局变量

还有一点要注意啊,不能声明多个名称空间里的相同名称

cpp 复制代码
using A::a;
using B::a;//这是不行的
cout<<a<<endl;

using指示

using指示和using声明类似的地方是,我们可以使用命名空间名字的简写形式;

和using声明不同的地方是,我们无法控制哪些名字是可见的,因为所有名字都是可见的。 using声明使一个名称可用,而using指示使名称空间里所有名称都可用。

using 指示以关键字using开始,后面是关键字namespace以及命名空间的名字。

cpp 复制代码
using namespace 名称空间名;

如果这里所用的名字不是一个已经定义好的命名空间的名字,则程序将发生错误。

using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中。

cpp 复制代码
#include<iostream>
...
using namespace A;//可以

class D
{
using namespace H;//不可以
}
int main()
{
using namespace std;//可以
}

using指示使得某个特定的命名空间中所有的名字都可见,这样我们就无须再为它们添加任何前缀限定符了。简写的名字从using指示开始,一直到using指示所在的作用域结束都能使用。

cpp 复制代码
#include<iostream>
using namespace std;

namespace A
{
	int c;
}

int main()
{
	{
		using namespace A;
		c = 9;
		cout << c << endl;//正确
	}
	c = 9;//错误
}

如果我们提供了一个对 std 等命名空间的 using指示而未做任何特殊控制的话,将重新引入由于使用了多个库而造成的名字冲突问题。

using指示示例

让我们看一个简单的示例:

cpp 复制代码
namespace blip {
int i = 16, j = 15, k = 23;
//其他声明
}
 int j=0;// 正确:blip的j隐藏在命名空间中
void manip()
{
//using指示,blip中的名字被"添加"到全局作用域中
using namespace blip;//如果使用了j,则将在::j和blip::j之间产生冲突

++i;//将blip::i设定为17
++j;//二义性错误:是全局的j还是blip::j?
++::j;  //正确:将全局的j设定为1

++blip::j; //正确:将blip::j设定为16
int k = 97;// 当前局部的k隐藏了blip::k
++k;//将当前局部的k设定为98
}

manip的using指示使得程序可以直接访问blip的所有名字,也就是说,manip的代码可以使用blip中名字的简写形式。

blip的成员看起来好像是定义在blip和manip所在的作用域一样。假定manip定义在全局作用域中,则blip的成员也好像是定义在全局作用域中一样。

当命名空间被注入到它的外层作用域之后,很有可能该命名空间中定义的名字会与其外层作用域中的成员冲突。

例如在manip中,blip的成员j 我们就必须明确指出名字生了冲突。这种冲突是允许存在的,但是要想使用冲突的名字,我们必须明确指出名字的版本。manip中所有未加限定的j都会产生二义性错误

为了使用像j这样的名字,我们必须使用作用域运算符来明确指出所需的版本。我们使用::j来表示定义在全局作用域中的j,而使用blip::j来表示定义在blip中的。

因为manip的作用域和命名空间的作用域不同,所以manip内部的声明可以隐藏命名空间中的某些成员名字。例如,局部变量k隐藏了命名空间的成员blip::k。在manip内使用k不存在二义性,它指的就是局部变量k。

using声明和using指示的区别

使用using指示和使用多个using声明是不一样的。

使用using声明时,就好像声明了相应的名称一样,如果某个名称已经在函数中声明了,则不能再用using声明导入相同的名称。

然而,使用using指示时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。

我们可以看个例子

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a = 1;
}
int main()
{
    using namespace A;
	cout << a << endl;//结果是1
	int a = 2;
	cout << a << endl;//结果是2
}

我们惊奇的发现,再定义一个重名的变量a,系统居然没有报错。这是using指示和using声明的区别

我们可以看看using声明

cpp 复制代码
#include<iostream>
using namespace std;
namespace A
{
	int a = 1;
}
int main()
{
	using A::a;
	cout << a << endl;
	int a = 2;//发现这不可以
	cout << a << endl;
}

编译器将禁止这么做

假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using指示将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本

所以说,使用using声明比使用using编译指令安全很多。我们应该使用using声明而不是using指示。或者我们直接使用作用域解析运算符::,这也不错

cpp 复制代码
#include<iostream>
int main()
{
std::cout<<"hello world"<<std::endl;
}

小结

using指示引入的名字的作用域远比using声明引入的名字的作用域复杂。

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

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

因此,using指示一般被看作是出现在最近的外层作用域中。

在最简单的情况下,假定我们有一个命名空间a和一个函数f,它们都定义在全局作用域中。如果f含有一个对A的using指示,则在f看来,A中的名字仿佛是出现在全局作用域中f之前的位置一样:

cpp 复制代码
//命名空间A和函数王定义在全局作用城中
namespace A{
int i, j;
}
void f()

{using namespace A; // 把A中的名字注入到全局作用域中
cout << i*j <<endl; //使用命名空间A中的i和j
//...

}

头文件与using 声明或指示

头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。

通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。

因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。

提示:避免 using 指示

using指示一次性注入某个命名空间的所有名字,这种用法看似简单实则充满了风险:只使用一条语句就突然将命名空间中所有成员的名字变得可见了。

如果应用程序使用了多个不同的库,而这些库中的名字通过using指示变得可见,则全局命名空间污染的问题将重新出现。

而且,当引入库的新版本后,正在工作的程序很可能会编译失败。如果新版本引入了一个与应用程序正在使用的名字冲突的名字,就会出现这个问题。

另一个风险是由 using 指示引发的二义性错误只有在使用了冲突名字的地方才能被发现。这种延后的检测意味着可能在特定库引入很久之后才爆发冲突。直到程序开始使用该库的新部分后,之前一直未被检测到的错误才会出现。

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

类、 命名空间与作用域

对命名空间内部名字的查找遵循常规的查找规则:即由内向外依次查找每个外层作用域。

外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。

只有位于开放的块中且在使用点之前声明的名字才被考虑:

cpp 复制代码
namespace A {

int i;
namespace B {

int i; //在B中隐藏了A::i
int j;

int f1()
{
int j; // 是f1的局部变量,隐藏了A::B::j
return i;// 返回B::i
}

}//命名空间B结束,此后B中定义的名字不再可见

int f2() {
return j; // 错误:j没有被定义
int j = i; //用A::i进行初始化
}

对于位于命名空间中的类来说,常规的查找规则仍然适用:当成员函数使用某个名字时,首先在该成员中进行查找,然后在类中查找(包括基类),接着在外层作用域中查找,这时一个或几个外层作用域可能就是命名空间:

cpp 复制代码
namespace  A
{
int i;
int k;
class C1 {
public: 
C1():i(0),j(0) ()// 正确:初始化C1::i和C1::j
int f1() { return k; } //返回A::k
int f2() { return h; } // 错误:h未定义
int f3();
private:
int i; //在C1中隐藏了A::i
int j;
};
int h=i;//用A::i进行初始化
}

//成员f3定义在C1和命名空间的外部
int A::C1::f3() { return h; } // 正确:返回A::h

除了类内部出现的成员函数定义之外,总是向上查找作用域。

名字必须先声明后使用,因此f2的return语句无法通过编译。该语句试图使用命名空间A的名字h,但此时h尚未定义。如果h在A中定义的位置位于C1的定义之前,则上述语句将合法。类似的,因为f3的定义位于A::h之后,所以f3对于h的使用是合法的。

可以从函数的限定名推断出查找名字时检查作用城的次序,限定名以相反次序
指出被查找的作用域。

限定符A::C1::f3指出了查找类作用域和命名空间作用域的相反次序。首先查找函数f3的作用域,然后查找外层类C1的作用域,最后检查命名空间A的作用域以及包含着f3定义的作用域。

实参相关的查找与类类型形参

考虑下面这个简单的程序:

cpp 复制代码
std::string s;
std::cin >> s;

如我们所知,该调用等价于(参见14.1节,第491页):

cpp 复制代码
operator>>(std::cin,s);

operator>>函数定义在标准库string中,string又定义在命名空间std中。但是我们不用std::限定符和usinq声明就可以调用operator>>。

对于命名空间中名字的隐藏规则来说有一个重要的例外,它使得我们可以直接访问输出运算符。这个例外是,当我们给函数传递一个类类型的对象时,除了在常规的作用域查找外还会查找实参类所属的命名空间。这一例外对于传递类的引用或指针的调用同样有效。

在此例中,当编译器发现对operator>>的调用时,首先在当前作用域中寻找合适的函数,接着查找输出语句的外层作用域。

随后,因为>>表达式的形参是类类型的,所以编译器还会查找cin和s的类所属的命名空间。

也就是说,对于这个调用来说,编译器会查找定义了 istream和string 的命名空间std。当在std中查找时,编译器找到了string的输出运算符函数。

查找规则的这个例外允许概念上作为类接口一部分的非成员函数无须单独的using声明就能被程序使用。假如该例外不存在,则我们将不得不为输出运算符专门提供一个using声明:

cpp 复制代码
using std::operator>>; //要想使用cin >> s就必须有该using声明

或者使用函数调用的形式以把命名空间的信息包含进来:

cpp 复制代码
std::operator>>(std:;cin,s); // 正确:显式地使用 std::>>

在没有使用运算符语法的情况下,上述两种声明都显得比较笨拙且无形中增加了使用IO库的难度

友元声明与实参相关的查找

当类声明了一个友元时,该友元声明并没有使得友元本身可见。

然而,一个另外的未声明的类或函数如果第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。

这条规则与实参相关的查找规则结合在一起将产生意想不到的效果:

cpp 复制代码
namespace A{
class C {
//两个友元,在友元声明之外没有其他的声明
//这些函数隐式地成为命名空间A的成员
friend void f2(); //除非另有声明,否则不会被找到
friend void f(const C&);// 根据实参相关的查找规则可以被找到
};
}

此时,f和f2都是命名空间A的成员。即使f不存在其他声明,我们也能通过实参相关的查找规则调用f:

cpp 复制代码
int main()
{
A::C cobj;
f(cobj);// 正确:通过在A::C中的友元声明找到 A::f

f2();//错误:A::f2没有被声明
}

因为f接受一个类类型的实参,而且f在C所属的命名空间进行了隐式的声明,所能被找到。相反,因为f2没有形参,所以它无法被找到。

重载与命名空间

命名空间对函数的匹配过程有两方面的影响。其中一个影响非常明显:using声明或using指示能将某些函数添加到候选函数集。另外一个影响则比较微妙。

与实参相关的查找与重载

对于接受类类型实参的函数来说,其名字查找将在实参类所属的命名空间中进行。

这条规则对于我们如何确定候选函数集同样也有影响。

我们将在每在这些命名空间中所有个实参类(以及实参类的基类)所属的命名空间中搜寻候选函数。
与被调用函数同名的函数都将被添加到候选集当中,即使其中某些函数在调用语句处不可见也是如此:

cpp 复制代码
namespace NS {
class Quote { /*...*/ };
void display(const Quote&) { /*...*/}
}
// Bulk item的基类声明在命名空间NS中
class Bulk_item : public NS::Quote { /*...*/ };
int main() {
Bulk item book1;
display(book1);
return 0;
}

我们传递给display的实参属于类类型 Bulk_item,因此该调用语句的候选函数不仅应该在调用语句所在的作用域中查找,而且也应该在Bulk item 及其基类Quote所属的命名空间中查找。

命名空间 NS 中声明的函数display(const Quote&)也将被添加到候选函数集当中。

重载与using 声明

要想理解using声明与重载之间的交互关系,必须首先明确一条:using声明语句声明的是一个名字,而非一个特定的函数:

cpp 复制代码
using NS::print(int); // 错误:不能指定形参列表
using NS::print; //正确:using声明只声明一个名字

当我们为函数书写using声明时,该函数的所有版本都被引入到当前作用域中

**一个using 声明囊括了重载函数的所有版本以确保不违反命名空间的接口。**库的作着为某项任务提供了好几个不同的函数,允许用户选择性地忽略重载函数中的一部分但不是全部有可能导致意想不到的程序行为。

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

重载与 using 指示

using 指示将命名空间的成员提升到外层作用域中,如果命名空间的某个函数与该命名空间所属作用域的函数同名,则命名空间的函数将被添加到重载集合中;

cpp 复制代码
namespace libs_R_us {

extern void print(int);
extern void print (double) ;
//普通的声明
}
void print(const std::string &);
// 这个using 指示把名字添加到print 调用的候选函数集
using namespace libs_R_us;
// print调用此时的候选函数包括
// libs R us的print(int)
// libs R us 的print(double)
// 显式声明的print(const std::string &)
void fooBar(int ival)
{
print("Value: "); //调用全局函数print (const string &)
print (ival); //调用libs R_us::print(int)
}

与using声明不同的是,对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们指明调用的是命名空间中的函数版本还是当前作用域的版本即可。

跨越多个using 指示的重载

如果存在多个using指示,则来自每个命名空间的名字都会成为候选函数集的一部分:

cpp 复制代码
namespace AW {
int print(int);
}
namespace Primer 
{
double print(double);
}
// using指示从不同的命名空间中创建了一个重载函数集合

u+sing namespace AW;
using namespace Primer;
long double print(long double);
int main() 
{
print(1); //调用AW::print(int)
print(3.1); //调用Primer::print(double)
return0;
}

在全局作用域中,函数print的重载集合包括print(int)、print(double)和print(long double),尽管它们的声明位于不同作用域中,但它们都属于main函数中print 调用的候选函数集。

相关推荐
SoraLuna16 分钟前
「Mac玩转仓颉内测版26」基础篇6 - 字符类型详解
开发语言·算法·macos·cangjie
weixin_4143219816 分钟前
Linux 编译Ubuntu24内核
linux·运维·服务器
出逃日志41 分钟前
JS的DOM操作和事件监听综合练习 (具备三种功能的轮播图案例)
开发语言·前端·javascript
前端青山1 小时前
React事件处理机制详解
开发语言·前端·javascript·react.js
black0moonlight2 小时前
ISAAC Gym 7. 使用箭头进行数据可视化
开发语言·python
时光の尘3 小时前
C语言菜鸟入门·关键字·int的用法
c语言·开发语言·数据结构·c++·单片机·链表·c
C++忠实粉丝3 小时前
计算机网络socket编程(6)_TCP实网络编程现 Command_server
网络·c++·网络协议·tcp/ip·计算机网络·算法
坊钰3 小时前
【Java 数据结构】时间和空间复杂度
java·开发语言·数据结构·学习·算法
Edward-tan3 小时前
c语言数据结构与算法--简单实现线性表(顺序表+链表)的插入与删除
c语言·开发语言·链表
武昌库里写JAVA3 小时前
一文读懂Redis6的--bigkeys选项源码以及redis-bigkey-online项目介绍
c语言·开发语言·数据结构·算法·二维数组