前言
C++是本贾尼博士在C语言的基础上发明的一种新的编程语言,C++兼容C的语法,解决了C语言的一些问题,并在其C的基础上增加了一些新的功能。本篇文章来讲述一下命名空间的概念,命名空间有助于避免命名冲突,特别是对于大型项目有多个库文件的情形。
下面我们来讲个具体例子来引入命名空间。
一、为什么要有命名空间?
在C语言中,如果我们在没有包含任何头文件下写出下面的代码:
int rand = 5;
int main()
{
rand = 6;
return 0;
}
这代码看起来没有什么问题。但是如果我们引入了头文件<stdlib.h>那就会出问题了。
#include <stdlib.h>//引入 rand()函数的头文件
int rand = 5;
int main()
{
rand = 6;
return 0;
}
此时就会出现编译的报错
报错为“rand重定义”,我们有C语言的基础就知道,当我们没有引入任何头文件之前,我们声明一个名为rand的变量是没什么问题的,但是我们引入了rand()函数的头文件后,此时rand就成了函数指针变量,此时我们再将其声明定义为int类型的变量就会出现重定义的报错。而这是我们C语言无法处理的命名冲突的问题。
上面的例子就是头文件里的函数名和我们定义的类型名出现了命名冲突,而我们在实践过程当中,不仅仅是函数名和变量名会有冲突,在多个项目中,会有很多的命名冲突的问题,要解决这个问题,C++在C语言的基础上进行了升级,C++有命名空间(namespace)解决这个问题。
二、命名空间的使用
1、封装变量
下面我们将上面的代码修改一下解决一下命名冲突问题:
#include <stdlib.h>//引入 rand()函数的头文件
#include <stdio.h>
namespace space1//rand在命名空间space1的域里
{
int rand = 1;
}
int main()
{
int rand = 2;//rand在main函数的域内
printf("%d", rand);
return 0;
}
这里就有三个同名的rand了,当我们用在main函数里用printf输出rand时,究竟会输出什么呢?答案是:
虽然有三个同名的rand,但是他们所处的作用域不同,我们库函数里面的rand()是在全局域下的,而我们自己开的命名空间space1里的rand是在命名空间的域内,而我们main函数里的rand时在main函数的局部域内。当我们在main函数里对rand不加修饰的引用时,则会先在其当前作用域内寻找这个变量,所以就输出了我们main函数里面的int rand = 2。
那我们该如何访问到main函数外面的两个rand呢?这个时候我们就要用到新的运算符“作用域限定符”,也就是“::”。
这种访问命名空间内的成员的方法标明了访问的作用域。
下面是对rand标明作用域来访问rand的代码:
#include <stdlib.h>//引入 rand()函数的头文件
#include <stdio.h>
namespace space1//rand在命名空间space1的域里
{
int rand = 1;
}
int main()
{
int rand = 2;//rand在main函数的域内
printf("rand = %d\n", rand);//指当前局部域下的int rand = 2
printf("space1::rand = %d\n", space1::rand);//指定在space1里的int rand = 1
printf("::rand = %p\n", ::rand);//::前什么都不写就代表在全局域下的rand函数
return 0;
}
我们用作用域限定符指定了在具体的域内搜索变量,这样就可以访问到main函数外面的两个rand了。
其输出结果为
2、封装函数
命名空间不仅可以在里面声明定义变量,也可以声明定义函数。来防止出现命名冲突的问题。
在命名空间内定义函数
下面是在命名空间里定义函数的代码:
#include <stdio.h>
#include <stdlib.h>
namespace space2
{
int num = 0;
void rand()//space2里定义rand函数
{
printf("space2::rand()\n");
printf("num = %d\n", num);
}
}
int main()
{
space2::rand();//访问space2里的rand函数
space2::num = rand();//调用<stdlib.h>库里的rand函数赋值给num
space2::rand();//访问space2里的rand函数
return 0;
}
运行结果:
上述代码首先调用了space2命名空间里的rand()函数来打印出num的值为0,然后调用库函数里面的伪随机数函数rand()来生成随机值并把随机值赋值给num,在第二次调用space2命名空间里的rand()函数时,我们可以看到,num从0变成了伪随机数41。
函数声明和定义分开写
我们也可以将命名空间里的函数声明和定义分开写:
namespace space2
{
int num = 0;
void rand();//space2里声明函数
}
void space2::rand()//这里要加上作用域限定操作符来表明定义的是space2里的rand函数
{
printf("space2::rand()\n");
printf("num = %d\n", num);
}
3、展开命名空间
对于上述代码,如果我们要频繁的使用命名空间域里面的num,就要在每次使用了num变量的前面加上作用域限定符space2::,这样会让代码变得冗长而且不方便,这个时候我们可以采用展开命名空间(using)的方法来解决这个问题。
展开命名空间的方式有两种,一种是半展开,另一种是全展开。
半展开:
#include <stdio.h>
namespace space3
{
int num1 = 1;
void Func()
{
printf("space3::Func()\n");
}
}
int main()
{
//没有展开命名空间里的num1需要指定域名
printf("space3::num1 = %d\n", space3::num1);
space3::num1 = 44;
printf("space3::num1 = %d\n", space3::num1);
space3::Func();//调用命名空间里的Func函数
putchar('\n');
//展开命名空间
using space3::num1; //展开space3里面的num1
//展开了命名空间里的num1后在下面使用num1就可以不指定域名
printf("num1 = %d\n", num1);
space3::num1 = 55;
printf("num1 = %d\n", num1);
//Func(); //错误,因为Func()并没有展开,需要加上作用域限定符
//展开命名空间
using space3::Func;//展开space3里面的Func函数
//展开Func函数后使用不需要指定域名
Func();
return 0;
}
半展开的方法是 using (命名空间域)::(要展开的变量/函数);,一次只能展开命名空间里的一个成员,展开后在其展开的作用域范围内使用该成员可以不指定其作用域来使用,这就是半展开。
上述代码的运行结果:
全展开:
#include <stdio.h>
namespace space4
{
int num1 = 1;
int num2 = 2;
int num3 = 3;
void Func()
{
printf("space4::Func()\n");
}
}
//将space4全展开
using namespace space4;
int main()
{
//输出num1 num2 num3
printf("num1 = %d\n", num1);
printf("num2 = %d\n", num2);
printf("num3 = %d\n", num3);
Func();
putchar('\n');
//指定修改num1 num2 num3
space4::num1 = 2;
space4::num2 = 4;
space4::num3 = 6;
//输出num1 num2 num3
printf("num1 = %d\n", num1);
printf("num2 = %d\n", num2);
printf("num3 = %d\n", num3);
putchar('\n');
int num1 = 0;//这里在main域内声明定义了一个同名的num1
printf("num1 = %d\n", num1);//这里访问到的是main域内的num1
printf("space4::num1 = %d\n", space4::num1);
return 0;
}
运行结果:
全展开的方法是 using namespace (要展开的命名空间);,当我们将一个命名空间全展开后,我们可以使用命名空间里面的所有变量和函数名,访问时可以不加作用域限定符。但是如果在展开域后又声明定义了同名变量,访问同名变量时不加作用域限定符访问到的是局部域声明定义的变量,而不是我们展开命名空间里面的变量,这样会导致我们代码的可读性变差,所以不管是半展开还是全展开,我们都要谨慎展开,尽量不展开,因为展开后会对原有的空间域内定义的变量造成污染,而使用标明作用域的方法访问命名空间域内的成员就比较清晰明了。
三、同名命名空间的合并
在一个工程中,多个库文件合并时如果发现了多个相同名称的命名空间,那么会不会出现编译报错呢?答案是不会,这个时候编译器会将多个相同名称的命名空间合并在一起。但是如果合并后的命名空间内有重定义的内容,那么就会出现编译报错。
命名空间的合并:
//合并命名空间
#include <stdio.h>
namespace space6//其中一个文件中的同名space6命名空间
{
int a1 = 1;
}
namespace space6//其中一个文件中的同名space6命名空间
{
int b1 = 1;
}
int main()
{
printf("space6::a1 = %d\n", space6::a1);
printf("space6::b1 = %d\n", space6::b1);
return 0;
}
运行结果:
四、命名空间的嵌套
命名空间可以嵌套包含别的命名空间。
代码如下:
//命名空间的嵌套
#include <stdio.h>
namespace space7
{
int a1 = 2;
namespace space8//嵌套space8
{
int b1 = 3;
}
}
int main()
{
printf("space7::a1 = %d\n", space7::a1);
printf("space7::space8::b1 = %d\n", space7::space8::b1);
return 0;
}
运行结果:
五、编译系统查找变量名的规则
在我们访问变量时,编译器会默认先后在不同的作用域内查找变量的定义。
a、当前作用域(如main函数的局部域内定义的变量)
b、在全局域内查找和在展开的命名空间查找(这两者是同一级别的)
如果在全局域内和展开的命名空间域内有相同的变量名和函数名,我们使用时不表明作用域就容易出现二义性的问题,当产生二义性后会导致编译器不知道用户要访问的是哪个变量名或函数,就会报错。
我们来看如下代码:
//二义性
#include <stdio.h>
void Func1()//全局域下的Func1函数
{
printf("::Func1()\n");
}
namespace space5
{
void Func1()
{
printf("space5::Func1()\n");
}
}
//展开命名空间
using namespace space5;
int main()
{
Func1();//错误,产生二义性
return 0;
}
当我们在main函数里调用Func1时就产生了编译错误:
编译器不知道我们要调用的函数是全局域里定义的Func1函数还是我们展开的命名空间域的Func1函数,这就产生了二义性。这种情况下,我们就只能在调用Func1时标明其作用域来消除二义性。
修改的代码如下:
int main()
{
::Func1();//指定编译器在全局域中找Func1函数
space5::Func1();//指定编译器在space5的命名空间中Func1函数
return 0;
}
运行结果:
以上就是对于命名空间的介绍,我们使用命名空间要考虑许多因素。
以下是一些关于C++命名空间的考虑因素:
- 命名冲突:如果你有两个或多个库或模块,它们定义了相同名称的类、函数或变量,那么使用命名空间可以避免这些冲突。你可以将每个库或模块的实体封装在它们自己的命名空间中。
- 代码组织:命名空间可以用来组织你的代码,使其更加结构化和易于维护。例如,你可以按照功能、模块或项目阶段来划分命名空间。
- 可读性和清晰度:通过适当地使用命名空间,你可以提高代码的可读性和清晰度。这有助于其他开发人员理解你的代码,并更容易地找到和使用相关的类、函数和变量。
- 性能:虽然使用命名空间本身并不会直接影响性能,但过度使用或不当使用可能会导致性能问题。例如,如果你在头文件中频繁地包含大量命名空间,这可能会增加编译时间和内存消耗。
- 编程习惯:不同的编程团队和项目可能有不同的命名空间和代码组织习惯。因此,你应该遵循你所在团队或项目的编码规范,以确保代码的一致性和可维护性。
关于是否展开命名空间,这主要取决于你的使用场景。如果你在使用某个命名空间中的多个实体,并且不想每次都使用namespace::
前缀,那么你可以使用using namespace
语句来展开该命名空间。但是,请注意,在头文件中过度使用using namespace
可能会导致命名冲突和不可预测的行为。因此,通常建议在实现文件(.cpp)中局部地展开命名空间,而不是在头文件中全局地展开。