🏖️作者:@malloc不出对象
⛺专栏:C++的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
目录
- 前言
- 一、命名空间产生的背景
- 二、命名空间的定义
- 三、命名空间的使用
- 四、从多个角度分析为什么不提倡使用using namespace
- 4.1 如何合理使用using namespace std
- 4.2 \<iostream> 与<iostream.h>的关系
- 五、如何合理使用命名空间来避免命名冲突
前言
本篇文章将给大家讲述的是命名空间,它是C++中为了解决命名冲突所引申出的一种解决方案,我们使用C++常用的using namespace std;std就是C++标准库的命名空间名称,也许有小伙伴兴许还不知道这句代码表示什么含义,下面就让我们进行命名空间的学习吧!!
一、命名空间产生的背景
我们首先来看一个例子:
C++是向下兼容C语言的语法的,在这段代码中使用rand发生了重定义,因为在C中rand是头文件stdlib.h中的一种库函数,而现在我们将rand作为普通变量名来使用这就造成了重命名,编译器不知道到底使用哪个rand,而在C语言中只能通过更换名称来解决这种命名冲突,可这其实是一个非常蛋疼的问题。
大型应用程序经常使用来自不同厂商的开发库,几乎不可避免会使用相同的名字,也就是说一个库中定义的名字可能与其他库中的名字相同而产生冲突。假如不同的程序员分别定义了类和函数,放在了不同的头文件中,在主函数的文件中需要使用这些类和函数时,就用#include指令将这些头文件包含进来,我们知道在预编译后#include头文件会将头文件的内容展开在源文件中,这样就在同一个程序文件中可能就会出现了多个名字相同的类或函数,这就造成了命名冲突,即在同一个作用域中有两个或者多个同名的实体,使得程序员不能组合各自独立的开发库到一个程序中。
对此ANSI C++引入的可以由用户命名的作用域,用来处理程序中常见的同名冲突。
命名空间是用来限定名字的解析和使用范围,它是C++开发大型程序的工具之一。命名空间的原理是将全局作用域划分为一个一个的命名空间,每个命名空间是一个独立的作用域,在不同命名空间内部定义的名字彼此之间互不影响,从而有效的避免了命名污染。
二、命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
下面就是一个简单的命名空间:
命名空间可以在全局作用域或其他命名空间内部定义,但不能在函数、结构体或类内部定义,且要保证命名空间之间不会出现名字冲突。
在命名空间作用域内,可以包含:变量、对象以及它们的初始化、枚举常量、函数声明以及函数定义、类、结构体声明与实现、模板、其他命名空间。每个命名空间是一个作用域,定义在命名空间中的实体称为命名空间成员,命名空间中的每个名字必须是该命名空间中的唯一实体,但不同命名空间可以具有同名成员。
// 1. 正常的命名空间定义
namespace Curry1
{
// 变量、函数、结构体....
int a;
void func1()
{
//...
}
struct Node
{
struct Node* next;
int val;
};
//...
}
// 2. 嵌套的命名空间定义
namespace Curry1
{
// 变量、函数、结构体....
int a;
void func1()
{
//...
}
struct Node
{
struct Node* next;
int val;
};
namespace Curry2
{
int b;
void func2()
{
//...
}
}
//...
}
// 3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中
namespace Curry1
{
// 变量、函数、结构体....
int a;
void func1()
{
//...
}
//...
}
namespace Curry1
{
int b;
int Add(int a, int b)
{
return a + b;
}
//...
}
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
三、命名空间的使用
命名空间有三种使用方式:
第一种使用方式:命名空间名称::xxx
这种命名空间的方式是三种之中最繁琐
的一种方式,因为它需要我们一个个去指定命名空间域,但同时它却是最安全
的一种方式,它避免了命名空间被污染。
这个地方还有一种使用域运算符引用全局命名空间的成员的方式,下面我们简单的看一个例子:
::xxx
表示引用全局命名空间成员,我们可以这样来简单的理解一下,域运算符左边为空就代表全局命名空间域。所以 ::xxx就可以表示直接引用xxx全局命名空间成员。
下面这里再补充一下嵌套命名空间的使用方式,我们一起来简单的看下:
第二种:using 命名空间名称::xxx
将std C++标准库中经常使用的少数几个成员使用using 命名空间名称::xxx部分展开,使用一部分暴露一部分,这样在一定程度上也减少了命名空间被污染,到后面我个人也是偏提倡这种使用命名空间的方式。
第三种:using namespace 命名空间名称
using namespace Curry1将Curry1命名空间域中的内容全局展开,所以我们的变量a以及Add函数可以直接进行使用,,这种方式最为方便直接令整个命名空间成员都有效,但同时这种全局展开的方式是最不安全的,下面我会带大家详细探究这个问题。
四、从多个角度分析为什么不提倡使用using namespace
1.首先我们这里特别强调尤其不要在头文件中使用using namespace。
我们知道#include头文件会在预处理阶段将头文件的内容展开到源文件中,如果将using namespace包含在头文件中那么随着会在源文件中全局展开,那么此时的命名空间就被污染了,换而言之就是使用using namespace全局展开之后命名空间域就消失了,我们在.cpp中写的代码很有可能会与using namespace命名空间域中的变量、函数、类等发生重名冲突。
在大工程中如果出现此类问题是很难去追查错误源头的,尤其像有多个文件嵌套包含了using namespace,你该如何去进行追查错误源头?这无疑为我们带来了巨大的维护成本。
上述关于尽量不要在头文件中使用using namespace,这就话本质就是在 “尽量不扩大using namespace xxx所影响到的域”,所以我们可以写在cpp文件中使其控制在一个编译单元内,也可以写在函数里控制在一个函数中,这样都一定程度上控制了using namespace所影响到的域的范围。
对于在源文件中使用using namespace,有一部分大佬则是持开放的态度认为源文件中可以随便在源文件中使用using namespace,因为.cpp文件不影响他人的使用;而对于这一点我的看法还是不能在任何情况下滥用using namespace,在源文件中也会有一定的风险,下面我们来看一个例子:
上图例子由于A和B的命名空间域都是全展开的,此时编译器就不确定变量b到底使用哪个命名空间的。
同样的角度,假设命名空间A中定义了一个函数Add_1,命名空间B中定义了一个函数Add_2,它们两者全局展开,,此时我们即可以使用函数Add_1又可以使用函数Add_2,但是以后假设A命名空间升级了也加入了一个函数Add_2,那么这时候我们原来的代码就有问题了,我们编译器到底使用的是命名空间A的Add_2还是命名空间B的Add_2呢?此时就发生了命名冲突,如果是一个大工程的话,假设命名空间A就是我们的C++标准命名空间,命名空间B是公司成员写的代码,此时C++标准命名空间升级了,那么此时这个公司之前写的代码就跑不过去了,这样就带来很大的损失…
2. 其次使用using namespace与命名空间设计的初衷背道而驰了。
本身namespace就是给你一个限定名字域的作用的,但是你using之后就相当于没有了命名空间域,因为using namespace将这个命名空间域全部展开暴露在文件中了嘛!!更通俗的来讲站在命名空间的角度使用using namespace跟没使用是一样的!!!
这里可能有读者还不明白,这里我举一个例子:当你用C++标准库STL的cout时,同时引用了命名空间域A中的cout,此时编译器不知道你要用的是std::cou
t还是A::cout
,所以要想避免命名冲突你还是得显式的写std::cout
与A::cout
,这样看来using namespace
是不是就没什么用了而且还带了一些麻烦。
Q:那么有读者可能会问那为啥要设计出using namespace这种使用命名空间的方式呢?
首先我认为设计者觉得在一些情况下使用using namespace是非常方便的,就如我们上个话题谈到的不涉及大量重名的、大型项目中的;其次也许设计者在设计时没考虑全展开重名呢,仅仅是觉得有些场景下使用起来方便所以设计出来的呢,,不管处于什么原因我认为存在即合理!!
总的来说,我认为不应该抹杀任何一种使用方式,我们只是站在不同的角度去辩证的看待这三种方式带来的问题。
4.1 如何合理使用using namespace std
通过上述我们对using namespace做的一系列分析,我们也知道要尽量使用using namespace全局展开,std是C++标准库的命名空间,如何展开std使用更合理呢?
1.在日常练习中,建议直接using namespace std即可,这样就很方便。
2.using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现,所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 +using std::cout展开常用的库对象/类型等方式。
4.2 <iostream> 与<iostream.h>的关系
在我们之前的C语言学习当中,我们见到的想必都是.h头文件这种形式,但是在C++引入命名空间之后,C++新的标准为了和C区别开来规定头文件不使用后缀.h,另外C++为了兼容C,在C++标准化过程中,原有C语言头文件标准化后头文件名前带个c,例如cstdio、cstring、cstdlib等…
Q:那么在C++新的标准中<iostream>与<iostream.h>一样吗?
答案是不一样,这俩者是两个不同的文件,当使用<iostream.h>时相当于在C中调用库函数,使用的是全局命名空间,也就是早期C++的实现;当使用<iostream>时,该文件没有定义全局命名空间必须加上using namespace std;这样才能使用C++标准库中的类、函数等…这也就是我们经常写.cpp时通常头文件和using namespace std;缺一不可的原因。
五、如何合理使用命名空间来避免命名冲突
一般情况下,对偶尔使用命名空间的成员应该使用命名空间的作用域解析运算符
来直接给名称定位,这是最为安全的一种方式;而对一个大空间中经常要使用的少数几个命名空间成员提倡使用using 命名空间名称::
声明,我个人也是偏提倡一点这种使用方式,对于少数几个命名空间成员来讲它污染的命名空间域范围较小,因为毕竟第一种方式确实对于一个繁琐使用的少数命名空间成员不是很友好;对于最后一种using namespace
全展开命名空间域其实是最不提倡使用的,因为它会带来很多风险。但对于现阶段的我们来说,在平时的一些练习、竞赛以及项目小没有很多重名的时候,我们使用using namespace是可行的,因为它确实很方便并不需要我们过多去考虑命名冲突的风险,但使用using namespace终究还是有未知的风险的,我们不要在平时太依赖using namespace这种命名方式了。糖果虽甜,但吃多了就会有蛀牙🙈🙈
对于以后自己参加工作编写项目时,我个人的看法是将自己写的函数、类放在自己的命名空间,使用的时候不管是哪个命名空间里的,都要指明出处,都不要全部展开!!!因为你不知道什么时候风险会降临到你身边orz~
本篇文章的讲解就到这里了,如果有任何错处或者疑问欢迎大家评论区交流哦~~ 🙈 🙈