1. 前言

C++是本贾尼博士在C语言的基础上发明的一种新的编程语言, C++兼容C的语法,解决了C语言的一些问题,并在其C语言的基础上增加了一些新的功能。
本篇文章来讲述一下命名空间:命名空间是新定义一个作用域,里面可以放函数,变量,定义类等,主要用来防止命名冲突,特别是对于大型项目有多个库文件的情形。

2. 为什么要有命名空间?

在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”: 重定义;以前的定义是“函数”
“=”:“int (__cdecl *)(void)”与“int”的间接级别不同
“=”: 左操作数必须为左值

报错为“rand 重定义” ,我们有C语言的基础就知道,当我们没有引入任何头文件之前,我们声明一个名为 rand 的全局变量是没什么问题的,但是我们引入了 rand() 函数的头文件后,此时 rand 就成了函数指针变量 ,此时我们再声明定义一个同名的 int 的变量就会出现重定义的报错。而这是我们C语言无法处理的命名冲突的问题。

上面讲解了头文件里的函数名和我们定义的类型名出现了命名冲突的典例,而我们在实践过程当中,不仅仅是函数名和变量名会有冲突,在多个项目中,会有很多的命名冲突 的问题,要解决这个问题, C++在C语言的基础上进行了改进,C++有命名空间(namespace)解决这个问题。

3. 命名空间的使用

3.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;
}

这里就有3个同名的 rand 了,当我们用在 main 函数里用 printf 输出 rand 时,究竟会输出什么呢? 答案是:

2

虽然有三个同名的 rand, 但是他们所处的作用域不同,我们库函数里面的 rand() 是在全局作用域下的,而我们自己开的命名空间 space1 里的 rand 是在命名空间的作用域内 ,而我们 main 函数里的 rand 时在 main 函数的局部作用域内。当我们main 函数里对 rand 不加任何修饰符来引用时,则会先在其当前作用域内寻找这个变量,所以就输出了我们main函数里面的 int rand = 2

那我们该如何访问到 main 函数 外面的两个 rand 呢?这个时候我们就要用到新的运算符“作用域限定符” ,也就是 ::

作用域限定符(Scope Resolution Operator):在C++中用于明确指定变量或函数的作用域,解决同名标识符在不同作用域中的访问优先级问题。

使用作用域限定符来访问命名空间内的成员和方法标明了访问的作用域。

下面是对 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 了。

其输出结果为

rand = 2
space1::rand = 1
::rand = 6CC790F0

3.2. 封装函数

命名空间不仅可以在里面声明定义变量,也可以声明定义函数。来防止出现命名冲突的问题。

3.2.1. 在命名空间内定义函数

下面是在命名空间里定义函数的代码:

#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
space2::rand()
num = 41

上述代码首先调用了 space2 命名空间里的 rand() 函数 来打印出 num 的值为0 ,然后调用 <stdlib.h> 库函数里面的伪随机数函数 rand() 来生成随机值并把随机值赋值给 num,在第二次调用 space2 命名空间里的 rand() 函数时,我们可以看到, num从0变成了伪随机数41。

3.2.2. 函数声明和定义分离

我们也可以将命名空间里的函数声明和定义分开写:

namespace space2
{
	int num = 0;
	void rand();//space2里声明函数
}
 
void space2::rand()//这里要加上作用域限定操作符来表明定义的是space2里的rand函数
{
	printf("space2::rand()\n");
	printf("num = %d\n", num);
}

3.3. 展开命名空间

对于上述代码,如果我们要频繁的使用命名空间域里面的 num,就要在每次使用了 num 变量的前面加上作用域限定符 space2::,这样会让代码变得冗长而且不方便,这个时候我们可以采用展开命名空间(使用 using)的方法来解决这个问题。

展开命名空间的方式有两种,一种是半展开,另一种是全展开。

3.3.1. 半展开

#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 = 22;
	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 = 33;
	printf("num1 = %d\n", num1);
	//Func(); //错误,因为Func()并没有展开,需要加上作用域限定符
 
    //展开命名空间
	using space3::Func;//展开space3里面的Func函数
	//展开Func函数后使用不需要指定域名
	Func();
	return 0;
}

半展开的方法是 using (命名空间域)::(要展开的变量/函数); ,一次只能展开命名空间里的一个成员,展开后在其展开的作用域范围内使用该成员可以不指定其作用域来使用,这就是半展开。

上述代码的运行结果:

space3::num1 = 1
space3::num1 = 22
space3::Func()

num1 = 22
num1 = 33
space3::Func()

3.3.2. 全展开

#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;
}

运行结果:

num1 = 1
num2 = 2
num3 = 3
space4::Func()

num1 = 2
num2 = 4
num3 = 6

num1 = 0
space4::num1 = 2

全展开的方法是 using namespace (要展开的命名空间);,当我们将一个命名空间全展开后,我们可以使用命名空间里面的所有变量和函数名,访问时可以不加作用域限定符。 但是如果在展开域后又声明定义了同名变量,访问同名变量时不加作用域限定符访问到的是局部域声明定义的变量,而不是我们展开命名空间里面的变量,这样容易导致我们代码的可读性变差。不管是半展开还是全展开,我们都要谨慎展开,尽量不展开。展开整个命名空间会对原有的空间域造成“污染”,而使用标明作用域的方法访问命名空间域内的成员是最清晰明了的。

4. 同名命名空间的合并

在一个工程中,多个库文件合并时如果发现了多个相同名称的命名空间,那么会不会出现编译报错呢?答案是不会,这个时候编译器会将多个相同名称的命名空间合并在一起。但是如果合并后的命名空间内有重定义的内容,那么就会出现编译报错。

命名空间的合并:

//合并命名空间
#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;
}

运行结果:

space6::a1 = 1
space6::b1 = 1

5. 命名空间的嵌套

命名空间可以嵌套包含别的命名空间。

代码如下:

//命名空间的嵌套
#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;
}

运行结果:

space7::a1 = 2
space7::space8::b1 = 3

6. 编译系统查找变量名的规则

在我们访问变量时,编译器会默认先后在不同的作用域内查找变量的定义。

有以下规则:

  1. 当前作用域(如 main 函数的局部域内定义的变量)
  2. 在全局域内查找和在展开的命名空间查找(这两者是同一级别的)

如果在全局域内和展开的命名空间域内有相同的变量名和函数名,我们使用时不表明作用域就容易出现二义性的问题,编译器不知道用户要访问的是哪个变量名或函数就会报错。

我们来看如下代码:

#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 函数还是我们展开的命名空间域的 Func1 函数,这就产生了二义性。 这种情况下,我们就只能在 调用 Func1 时标明其作用域来消除二义性。

修改的代码如下:

int main()
{
	::Func1();//指定编译器在全局域中找Func1函数
	space5::Func1();//指定编译器在space5的命名空间中Func1函数
	return 0;
}

运行结果:

::Func1()
space5::Func1()

7. 最佳实践

以上就是对于命名空间的介绍,我们使用命名空间要考虑许多因素。

以下是一些关于C++命名空间的考虑因素:

  1. 命名冲突 :如果你有两个或多个库或模块,它们定义了相同名称的类、函数或变量,那么使用命名空间可以避免这些冲突。你可以将每个库或模块的实体封装在它们自己的命名空间中。
  2. 代码组织 :命名空间可以用来组织你的代码,使其更加结构化和易于维护。例如,你可以按照功能、模块或项目阶段来划分命名空间。
  3. 可读性和清晰度 :通过适当地使用命名空间,你可以提高代码的可读性和清晰度。这有助于其他开发人员理解你的代码,并更容易地找到和使用相关的类、函数和变量。
  4. 性能 :虽然使用命名空间本身并不会直接影响性能,但过度使用或不当使用可能会导致性能问题。例如,如果你在头文件中频繁地包含大量命名空间,这可能会增加编译时间和内存消耗。
  5. 编程习惯 :不同的编程团队和项目可能有不同的命名空间和代码组织习惯。因此,你应该遵循你所在团队或项目的编码规范,以确保代码的一致性和可维护性。

关于是否展开命名空间,这主要取决于你的使用场景。如果你在使用某个命名空间中的多个实体,并且不想每次都使用namespace::前缀,那么你可以使用 using namespace 语句来展开该命名空间。但是,请注意,在头文件中过度使用using namespace 可能会导致命名冲突和不可预测的行为。因此,通常建议在实现文件(.cpp)中局部地展开命名空间,而不是在头文件中全局地展开 。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐