第18章 用于大型程序的工具 18.2命名空间
18.2 命名空间
命名空间可以有效的防止变量名重复,命名空间就是一个作用域,可以在命名空间中定义命名空间,但是不能在类和函数中定义。
18.2.1 命名空间定义
使用这种方法来定义命名空间
namespace xxx{
}
命名空间都是一个作用域,所以和作用域中的变量是一样的,变量名字不能重复。
和作用域不一样的是,命名空间可以不是连续的。我们定义一个命名空间,如果之前没有这个空间,则创建这个空间,如果之前就已经有了这个空间,那么就是在这个空间中添加新的成员。
对于头文件和cpp文件,我们可以分别将其写在命名空间中
头文件通常不包含在命名空间中,因为这样做会导致头文件拥有的成员也在该空间中。
全局命名空间通过
::来访问
命名空间是可以嵌套的,但是外层的命名无法直接访问内层命名空间定义的成员,使用inline命名空间可以解决这个问题
namespace B{
inline namespace A{
}
}
命名空间B可以直接访问命名空间A中的变量了。
我们可以不为命名空间命名,这样的空间叫做
未命名的命名空间,在未命名的命名空间中声明的变量都是静态类型,可以直接访问,因为它没有名字,所以我们无法通过类作用域符来访问。 但是其作用域在当前的文件中,无法跨多个文件,这点和全局变量不一样。
因为它可以直接访问,所以这就要求全局变量的名字和未命名空间的名字不能一样,否则会有二义性。
修饰为static的全局变量作用域为当前的文件,无法通过extern在其他文件中访问。
18.2.2 使用命名空间
我们可以通过namespace::变量名来访问变量,但是这样比较繁琐,尤其是变量名字很长的时候。
我们可以为命名空间起别名
namespace c_p_p = C_plus_plus;
namespace A = BB::CC::AA;
这对于嵌套的命名空间也是适用的。
using声明
我们也可以适用using来引入命名空间的一个成员,using声明可以适用在任何作用域中。包含全局作用域,局部作用域,命名空间作用域和类作用域等中。。。
using指示
using指示使用
using namespace std;
的方式,将命名空间中的所有成员都引入当前的作用域,因为这么做我们无法确定引入了什么成员,所以这么做是由风险的。
同时using指示只能用在全局,局部、命名空间作用域中。
using指示会导致命名空间的左右成员对于使用的地方都是全部可见的,如果有多个命名空间使用了using指示,可能会导致多个命名空间之间由相同名字的变量,从而导致冲突。
另一个问题是,可能当前作用域中定义的变量和命名空间的成员名字有冲突,导致报错。
所以使用using指示是存在一定风险的,但是如果写一般的小代码,不是大型的项目的话,使用一下还是很方便的。
18.2.3 类、命名空间和作用域
在命名空间中名字查找是从内开始向外找的,同时名字查找只从其声明语句之前的语句中查找,
namespace A{
int a;
int b;
class cls_A{
public:
void f(){
++a;//正确
};
void f1(){
++c;//错误
}
}
int c;
}
在命名空间中的类,在使用名字的时候,首先从当前函数的作用域中查找,然后从类的数据成员中找,然后从基类的数据成员中找,然后再从外部命名空间找,逐层向外的方式。
有一个例外,在我们使用
stirng s;
cout<<s;
的时候,对于<<是重载在string中的,但是如果不显式的引入该成员,上面的操作依旧是对的。
当我们给函数传递一个类类型的对象时,除了在常规的作用域查找之外还会查找实参类所属的命名空间。这对传递类的引用或者指针的调用同样有效。
对于<<,其形参是cout和string类型,所以编译器会在ostream和stirng的命名空间中查找对应的<<操作。此operator<<(std::cout,string s)定义在std中,找得到所以会直接调用。
对于友元函数,之前学习到,如果一个类中声明了一个友元函数,但是在类外面没有该函数的声明,那么这个友元函数是不可见的。
但是如果在命名空间中,在命名空间的类名中定义了友元,但是没有任何声明。编译器会认为这些友元函数是它最近的外层命名空间的成员。 尽管我们并没有对此做任何声明和定义。
书上的例子就是一个很好的例子,定义了cobj。f(cobj),此时A命名空间是没有在当前作用域中的,但是f(cobj)调用成功了,因为他会在实参的命名空间中查找这个函数是否存在,答案是存在的,所以调用成功,但是对于f2(),因为其没有形参,无法进行推断,所以是错误的。
我觉得这种方法反而让C++变得更加复杂化了。
练习
18.18
如果是string,那么调用的是string的命名空间std中定义的swap版本,而如果是int则使用的默认版本,如果我们定义了自己版本的string或者int类的swap,那么调用的就是自己定义的swap。
18.19
如果使用的是std::swap()那么调用的一致就是std标准库的版本。
18.2.4 重载和命名空间
之前学习到,一可以通过函数的实参推算出具体调用哪一个函数。以及在该实参类型的命名空间中寻找这个函数,其实这就以及构成了重载。
就和swap一样,首先使用using std::swap();然后定义自己的版本,我们调用了swap(),就可以根据实参类型来推断。
重载和using声明
当我们使用using namespace::func;的时候,在该命名空间下的所有func都对于当前作用域是可见的,所以和当前作用域的同名函数构成了重载的关系,因此在当前作用域中调用func的时候,所以的func都是候选函数。
但是如果命名空间和当前作用域的函数有一个一摸一样的时候,则编译器会报错。。
但是使用using指示是不会的,就算所有参数都一样,using指示依旧不会报错
所以使用using声明,看起来更好。
跨越多个using指示的重载
其实这个和上面的是一样的,只要是同名函数就会被选入候选列表。
练习
18.20
所有compute函数都是候选函数,
其中
compute(const void*);
compute(int);
compute(double,double=3.4);
compute(char*,char*=0);
是可行函数,但是除了int,其余的函数都需要类型转换,所以comput(int)是最终匹配的函数。
如果using放在main函数中,那么外面的compute函数都会被覆盖,此时mian函数中可见的函数时
compute();
compute(const void*);
但是compute()不是可行的,compute(const void*);是可行的,所以直接调用的是compute(const void*);