【C++】命名空间的讲解

858 阅读9分钟

​​​一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第14天,点击查看活动详情。​

0x00 问题引入

在C/C++中,变量、函数和类都是大量存在的,这些变量、函数和类的名称都会存在于全局作用域中,这么一来就会导致命名的冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染。 我们刚才 HelloWorld 代码中的 namespace 的出现,就是针对这个问题的。

💬 为了能够让大伙理解命名空间的存在是多么的合理,我们来故意踩一下命名冲突的坑。

​ 在 stdlib 库中有一个生成随机数的函数 rand() ,相信大家都认识,但是我们假装某个人不知道 stdlib 库中有一个叫 rand 的函数存在,因此在定义变量时给变量取名为 rand

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int rand = 233;
	printf("%d\n", rand);	 // 这里到底是打印我们自己定义的rand,还是stdlib里的?

	return 0;
}

​ 我们知道,#include 包含头文件,头文件里的内容是会被展开来的。当展开头文件时,stdlib 库中有一个叫 rand 的函数,我这里又定义了一个叫 rand 的变量,此时就冲突了!

​ 冲突了,那么问题来了,我们这里 printf 打印出来的会是什么呢?

编译器的寻找规则: 局部找 → 全局找 → 找不到(报错)。

或许他打印出什么并不重要,但是大家应该能够体会到命名冲突多是一件倒霉事了。

😂 问题是在C语言里几乎没有办法很好地解决这种问题。

所以为了很好地解决这种冲突的问题,C++ 就加入了命名空间的特性!

在 C++ 里,我们就可以利用 "命名空间" 来解决这个问题,所以 C++ 提出了一个新语法 —— 命名空间 namespace!

0x01 命名空间的定义

​ 定义命名空间,肯定得用到我们刚才提到的 namespace 关键字,namespace 后面可以取一个空间名,然后再接上一对大括号就可以了,上图!

​ 那我们该如何使用它呢?

💬 我们来拿上面的代码来举例子,试试用 namespace 来解决命名冲突的问题:

namespace nb {  
	int rand = 233;
}

int main(void)
{
	// int rand = 233;
	printf("%d\n", nb::rand);    // 这样我们就能看出rand是nb这块命名空间里的变量了

	return 0;
}

🔑 画图详解:

💡 这里的 : : 叫做 "作用域限定符" 。这么一来,就不怕冲突了,问题就这么轻松地解决了。

命名空间能够达到一种类似于 "隔离" 的效果。

📌 注意事项:

​ 注意!!!

① 命名空间必须在全局作用域下定义!

其次,正是因为命名空间是全局的,所以这个 rand 变量也自然而然地变成了全局变量。

② 命名空间长得有点像结构体,但是他和结构体不是一个东西,结构体是定义一个类型,它们的性质是完全不一样的。还有,命名空间大括号外不用加分号,手残党要注意了。

0x02 命名空间里的内容

📚 命名空间里的内容,不仅仅可以存放变量,还可以在里面放函数,结构体,甚至是 类(我们后面会讲)。

💬 代码演示:

#include <stdio.h>

namespace N1 {
	int a = 10;
	int b = 20;
	int Add(int x, int y) {
		return x + y;
	}
}

namespace N2 {
	int c = 0;
	struct Node {
		struct Node* next;
		int val;
	};
}

int main(void)
{
	int res = N1::Add(N1::a, N1::b);
	printf("result = %d", res);

	struct N2::Node node1;

	return 0;
}

0x03 命名空间的嵌套

📚 没想到吧,命名空间是可以套娃的,命名空间可以嵌套命名空间。

💬 嵌套方法演示:

// 国家5A级景区...
namespace AAAAA {
	int a5 = 10000;
	namespace AAAA {
		int a4 = 1000;
		namespace AAA {
			int a3 = 100;
			namespace AA {
				int a2 = 10;
				namespace A {
					int a1 = 1;
				}
			}
		}
	}
}

int main(void)
{
	// 取出 a1 
	int ret = AAAAA::AAAA::AAA::AA::A::a1;

	return 0;
}

📌 注意事项:虽然套了这么多层,但是它们仍然都是全局的,只是套在了一个命名空间内而已。

0x04 空间名重名问题

❓ 命名空间是用来解决命名冲突问题的,那我项目中定义的某个命名空间的名字和其他命名空间的名字冲突了怎么办?

​ 这话可能有点绕,不过没有关系,让我品一品82年的拉菲先。

其实, 在同一个工程中是允许存在多个相同名称的命名空间的。

编译器最后会将他们合成到同一个命名空间中的。有两个相同名字的命名空间,就会合二为一。有三个相同名字的命名空间,就会三合一……

​就算是有七颗龙珠,也召唤不出神龙,只会变成一个更大的 "龙珠"

💬 不信?这就上号给你们证明一波!

​(证明:编译器会将重名的命名空间融合)

🔺 总结:同一个工程中允许存在多个相同的命名空间,编译器最后会将它们合成到一起。

0x05 展开的方式

💬 假设有这样的一种情况,一个命名空间中的某个成员是我们经常要使用的:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

int main(void)
{
	 printf("%d\n", N1::a);
	 int res = N1::a;
	 func(N1::a);
	 printf("hello, %d\n", N1::a);

	return 0;
}

​ 指定的作用域,能够做到最好的命名隔离,但是使用起来好像不是很方便。

每次使用都要调 : : ,好捏🐎烦!这也太难受了,有办法能解决吗?

这位同学请不要激动!命名空间是可以展开的。

💡 我们可以用 using namespace 将整个命名空间展开,因为命名空间是在全局土生土长的,所以展开后,里面的东西自然会被展开到全局。

using namespace 空间名;

💬 我们试一试:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

using namespace N1;  // 将N1这个命名空间展开

int main(void)
{
	 printf("%d\n", a); // 这样我们就可以直接使用了,就不需要 "::" 了
	 int res = a;
	 func(a);
	 printf("hello, %d\n", a);

	return 0;
}

​ 但是!!! 全部展开,虽然使用起来方便,但是隔离的效果失效了!

❓ 那我们岂不是白写命名空间了?

​ 所以一定要慎用!我们还是不建议大家把命名空间都展开的。

**🔑 解决方法:**我既要用起来方便,又要保持隔离的效果!小孩子才做选择!我全都要!

📚 指定展开某一个,直接使用 using 将命名空间中某个成员引入。

可以用来展开命名空间中最常用的成员:

using 空间名::成员

💬 演示如何做到全都要:

#include <stdio.h>

namespace N1 {
	int a = 10;  // 假设a经常需要使用
	int b = 20;
	int c = 30;
}

void func(int n) {
	printf("HI, %d\n", n);
}

// using namespace N1;

using N1::a;  // 单独展开一个,其他的不展开

int main(void)
{
	 printf("%d\n", a); // 这样我们就可以直接使用了,就不需要 "::" 了
	 int res = a;
	 func(a);
	 printf("hello, %d\n", a);

	return 0;
}

​ (既保持了方便,又保持了隔离,岂不美哉)

0x06 匿名命名空间

​ 为了能够安心享受极致的嘴臭,祖安人是非常喜欢匿名喷人的。

📚 我们在C语言学习结构体的时候,我们就提到过匿名结构体。

命名空间这里也可以匿名!如果一个命名空间没有名称,我们就称它为匿名结构体。

// 匿名命名空间
namespace {
	char c;
	int i;
	double d;
}

这种情况,编译器会在内部给这个没有名字的 "匿名命名空间" 生成一个惟一的名字。

并且还会为该匿名命名空间生成一条 using 指令,所以上面的代码会等同于:

namespace _UNIQUE_NAME {
	char c;
	int i;
	double d;
}
using namespace _UNIQUE_NAME;

这里我们只需要知道有这么一个东西就可以了,如果感兴趣可以深究下去。

0x07 使用方式总结

学到这里,想必大伙已经对命名空间了解的差不多了,我们来总结一下命名空间使用的三种方式。

💬 方式一:

空间名 + 作用域限定符

namespace N {
    int a = 10;
}

int main()
{
    printf("%d\n", N::a);

    return 0;
}

💬 方式二:

使用 using namespace 命名空间名称引入 (会破坏隔离效果)

namespace N {
    int a = 10;
}

using namespace N;

int main()
{
    printf("%d\n", a);

    return 0;
}

💬 方式三:

使用 using 将命名空间中成员引入

namespace N {
    int a = 10;
}

using N::a;

int main()
{
    printf("%d\n", a);

    return 0;
}