【C++ 学习总结】- 20 - 命名空间

【C++ 学习总结】- 20 - 命名空间

  • 一、基本概念
  • 二、命名空间的访问
    • 1. 通过作用域操作符
    • 2. 通过 using 关键字
  • 三、命名空间的特性
    • 1. 命名空间与宏定义
    • 2. 命名空间与头文件
    • 3. 不连续的命名空间
    • 4. 命名空间的嵌套使用
  • 四、头文件与std命名空间

引言:

命名空间(Namespace)是 C++ 中新增的概念,旨在对全局作用域进行划分管理从而解决命名冲突问题,同时,通过对标识符的归束管理也使得项目的逻辑条理更加清晰。


一、基本概念

  一个中大型软件往往由多名程序员共同开发,项目中会用到数量颇为庞大的变量和函数,这就不可避免地会出现变量或函数等的命名冲突的问题。团队分工的情况下,即使所有人的代码都各自通过测试确认无误,将它们结合到一起时仍有可能出现问题,命名冲突就是其中一个。
  为了解决工程中可能的命名冲突问题,C++ 引入了「 命名空间(Namespace)」的概念。使用命名空间将不同的标识符打包到不同的空间中,实现了对各式各样繁多标识符的有序管理,不仅解决了命名冲突问题,也通过梳理使得代码更加条理清晰。

  命名空间通过 C++ 中新增的 <namespace> 关键字来定义,其语法格式如下:

	// 通过  关键字定义「命名空间」
	namespace spaceName {
		// variables, functions
		// classes, typedefs, etc...
	} 
  • 「命名空间」 可以包含的内容
      命名空间内不仅可以声明或定义 变量,对于其它能在命名空间以外声明或定义的名称如:函数结构体typedef 等等,同样也都能在命名空间内部进行声明或定义。站在编译和链接的角度,变量名、函数名、类名等等都是 标识符(Identifier),有的标识符指代一个内存位置,如变量名、函数名;而有的标识符仅仅是一个新的名称,例如 typedef 定义的类型别名。
      需要注意:命名空间可以包含的标识符中并不包括 宏定义

  • 「命名空间」 的定义位置
      命名空间必须定义在所有函数之外的全局作用域中,这点与全局变量相同。工程中的全局标识符共享同一个作用域,而命名空间的实质作用就是将全局作用域划分为不同的区域进行管理,以解决他们之间相互冲突的问题(不同区域中的同名标识符不会发生冲突)。这同时也说明了,命名空间中的标识符都是全局作用域,只是需要通过命名空间来选择使用。

  使用 <namespace> 定义命名空间的示例代码:

/* 创建一个命名空间: 张三 */
namespace ZhangSan{
	typedef const char * name_str;
    name_str Name = "张三";
    void say (void) {
        printf("-> %s says : I like apples. \n", Name);
    }
}

/* 创建一个命名空间: 李四 */
namespace LiSi{
	typedef const char * name_str;
    name_str Name = "李四";
    void say (void) {
        printf("-> %s says : I like oranges. \n", Name);
    }
}



二、命名空间的访问

1. 通过作用域操作符

  作用域操作符 < :: > 是 C++ 中的新增的操作符,其用途之一便是用来 指明要使用的命名空间,使用方法为在变量前使用 < :: > 衔接变量所属的命名空间,语法格式如下:

	// 使用作用域操作符 < :: > 访问命名空间中的标识符
	spaceName::Identifier

  作用域操作符 < :: > 指定命名空间的使用示例:

================================ 代码示例 ================================
int main (void)
{
	/* 此处使用上文中所定义的两个命名空间 ZhangSan 和 LiSi */
    ZhangSan::name_str Name1 = "王五";
    LiSi::name_str Name2 = "赵六";
    
    ZhangSan::say();
	printf("-> %s talks to %s : I have bought many apples. \n", ZhangSan::Name, Name1);
	
    LiSi::say();
	printf("-> %s talks to %s : I have bought many oranges. \n", LiSi::Name, Name2);
}	
================================ 运行结果 ================================
-> 张三 says : I like apples.
-> 张三 talks to 王五 : I have bought many apples.
-> 李四 says : I like oranges.
-> 李四 talks to 赵六 : I have bought many oranges.

2. 通过 using 关键字

  除 作用域操作符 < :: > 以外,还可以通过 <using> 关键字来指定要使用的命名空间。与作用域操作符只能指定单个标识符的作用域不同,<using> 关键字既可以指定使用某一命名空间中的某个标识符,也可以指定使用某一整个命名空间,使用该空间其中的全部标识符,从而省去一个个指定的繁杂过程。
  使用 <using> 关键字的语法格式如下:

	// 1.指定使用某一命名空间中的某个标识符
	using spaceName::Identifier;
	
	// 2.指定使用某一整个命名空间
	using spaceName;
  • 通过 <using> 使用一整个命名空间有增加命名冲突的风险,在实际工程中应尽量在局部函数而非全局作用域中如此使用。

  ① 指定使用命名空间中的单个标识符的示例:

================================ 代码示例 ================================
int main (void)
{
	/* 此处使用上文中所定义的命名空间 ZhangSan */
	using ZhangSan::Name;			// 指定使用命名空间 ZhangSan 中的变量名 Name
	using ZhangSan::say;			// 指定使用命名空间 ZhangSan 中的函数名 say
	using ZhangSan::name_str;		// 指定使用命名空间 ZhangSan 中的类型名 name_str
	
	name_str Name1 = "王五";
	say();
	printf("-> %s talks to %s : ""I have bought many apples."" \n", Name, Name1);
}
================================ 运行结果 ================================
-> 张三 says : I like apples.
-> 张三 talks to 王五 : I have bought many apples.

  ② 指定使用某一整个命名空间的示例:

================================ 代码示例 ================================
int main (void)
{
	/* 此处使用上文中所定义的命名空间 LiSi */
    using namespace LiSi;		// 指定使用命名空间 LiSi 中的所有标识符

	name_str Name2 = "赵六";
    say();
	printf("-> %s talks to %s : ""I have bought many oranges."" \n", Name, Name2);
}
================================ 运行结果 ================================
-> 李四 says : I like oranges.
-> 李四 talks to 赵六 : I have bought many oranges.



三、命名空间的特性

1. 命名空间与宏定义

  宏定义不能放在命名空间内,因为宏定义不是 C++ 语句。宏定义属于预处理指令;在预处理阶段执行,而 <namespace> 是 C++ 的关键字,属于 C++ 语句,在编译阶段执行。宏定义早在编译之前的预处理阶段就已执行完毕,在 <namespace> 语句起效之前就完成了其文本替换的工作,所以命名空间并不具备对宏定义的管理能力。

  • PS:伪指令都不是 C/C++ 语句,所以它们后面都不加分号,如果加上了分号,分号就会成为宏的一部分,而在编译器不会认为宏中有分号是问题,所以不会报错;但是在编译时,宏标识全部被替换为宏体,此时的分号常带来编译错误。

2. 命名空间与头文件

  命名空间实质上是对全局作用域进行划分管理,所以其中包含的变量等的声明与定义依旧需要遵循其在全局作用域中声明或定义时所需要遵循的规则,比如在头文件中不能定义非 const 类型的变量,或定义函数,否则会导致 “多重定义” 的错误问题。
  为避免这些问题的产生:
    ① 包含 变量定义函数定义 的命名空间应当定义在源文件中;
    ② 包含 extern 声明函数声明类的定义typedef 定义别名 的命名空间可以定义在头文件中。

  • 「命名空间」 变量的跨文件访问
      在 A.cpp 的中定义的变量若想要在 B.cpp 中使用,则必须在 B.cpp 中对该变量进行声明才行。若变量 var1 是在 A.cpp 的某命名空间中定义的变量,我们该如何在 B.cpp 中声明使用呢?没有直接声明命名空间中的某变量的方法,但我们可以这样做:在 A.cpp 的命名空间中定义变量 var1,在 A.h 的同名命名空间中使用 extern 声明此变量,然后在 B.cpp 中包含 A.h 头文件,来实现对 var1 的声明,而后即可在 B.cpp 中访问 var1。

3. 不连续的命名空间

  命名空间可以不连续地分布在不同的位置,他们使用一个统一的命名,然后各自包含不同的标识符,编译器会在编译时将这些散落各处的归属同一命名空间的散落空间组合为一个统一的命名空间整体,包含所有分散的命名空间各自所拥有的的标识符。从程序员的角度来说,即:我们可以在工程中任意位置使用 <namespace> 再次定义来向已有的空间中添加新的标识符。
  这是为了方便程序员而提供的特性,使得程序员不再需要再在繁杂的工程中寻找原始定义的位置,但也一定程度上增加了代码分散和逻辑混乱的可能性。(这取决于程序员自己是否有较好的规范意识)

  不连续的命名空间的代码示例:

================================ 代码示例 ================================
namespace A {
	int numA = 10;		// 在此处在 A 中定义变量 numA
}

namespace A {
	int numB = 10;		// 在此处在 A 中定义变量 numB
}

int main ()
{
	printf("-> Num-A is: %d,  Num-B is: %d \n", A::numA, A::numB);
	return 0;
}
================================ 运行结果 ================================
-> Num-A is: 10,  Num-B is: 10



4. 命名空间的嵌套使用

  命名空间允许嵌套定义,即:在命名空间中再定义命名空间,定义方法为在一个命名空间的定义中再使用 <namespace> 定义被嵌套的命名空间,其语法格式如下:

	// 命名空间的嵌套定义
	namespace space1 {
		// functions, variables, ...
		namespace space2 {
			// functions, variables, ...
			......
		}
	}

  嵌套命名空间的访问方式为逐层逐级地向下寻访,每个层级之间使用 < :: > 连接,其语法格式如下:

	// 逐层逐级地向下寻访, :: 的嵌套连接
	space1::space2:: ... ::spaceN
	
	// 一重嵌套命名空间的访问 1
	space1::space2::Identifier
	
	// 一重嵌套命名空间的访问 2
	using space1::space2::Identifier;
	
	// 一重嵌套命名空间的访问 3
	using namespace space1::space2;

  嵌套空间的完整使用示例:

================================ 定义示例 ================================
/* 最外部的命名空间 */
namespace FSpace {
	typedef unsigned char u8;
	u8 NA = 10;
	int all = 10;

	/* 一重嵌套命名空间 */
	namespace SSpace {
		typedef unsigned int uint;
		uint NB = 20;
		int all = 20;

		/* 二重嵌套命名空间 */
		namespace TSpace {
			typedef signed int sint;
			sint NC = -30;
			int all = 30;
		}
	}
}

================================ 访问示例 ================================
/* 嵌套访问命名空间 */
void Nested_Namespace (int sel) {
	switch (sel) {
		case (1) : {
			/* 通过 <::> 依次访问各层命名空间 */
			printf("-> NA is : %3d \n", FSpace::NA);
			printf("-> NB is : %3d \n", FSpace::SSpace::NB);
			printf("-> NC is : %3d \n", FSpace::SSpace::TSpace::NC);
		}break;

		case (2) : {
			/* 访问一重嵌套的命名空间 SSpace */
			using FSpace::SSpace::all;
			printf("-> all is : %d \n", all);
		}break;

		case (3) : {
			/* 访问二重嵌套的命名空间 TSpace */
			using namespace FSpace::SSpace::TSpace;
			printf("-> all is : %d \n", all);
		}break;

		default: break;
	}
}

int main ()
{
	printf("\n嵌套使用命名空间-方法1:\n");
	Nested_Namespace(1);
	printf("\n嵌套使用命名空间-方法2:\n");
	Nested_Namespace(2);
	printf("\n嵌套使用命名空间-方法3:\n");
	Nested_Namespace(3);
	
	return 0;
}

================================ 运行结果 ================================
嵌套使用命名空间-方法1:
-> NA is :  10
-> NB is :  20
-> NC is : -30

嵌套使用命名空间-方法2:
-> all is : 20

嵌套使用命名空间-方法3:
-> all is : 30



四、头文件与std命名空间

  C++ 是在 C 的基础上开发的,早期的 C++ 还不完善,不支持命名空间,没有自己的编译器,而是将 C++ 代码翻译成 C 代码,再通过 C 编译器完成编译。这个时候的 C++ 仍然在使用 C 的库,stdio.h、stdlib.h、string.h 等头文件依然有效;在此之外 C++ 也开发了一些新的库,增加了自己的头文件,例如:
     - iostream.h :用于控制台输入输出的头文件。
     - fstream.h :用于文件操作的头文件。
     - complex.h :用于复数计算的头文件。
  和 C 一样,新的 C++ 头文件仍然以.h为后缀,它们所包含的类、函数、宏等都是全局范围的。

  后来 C++ 引入了命名空间的概念,计划重新编写自己的库,并将类、函数等等都统一纳入一个命名空间,这个命名空间的名字就是 std。(std 是 standard 的缩写,意思是“标准命名空间”)

  但这时已经有很多用老的 C++ 开发的程序了,它们的代码中并没有使用命名空间,直接修改原来的库会带来一个很严重的后果:程序员会因为不愿花费大量时间修改老的代码而极力反抗,拒绝使用新的 C++ 标准。
  于是 C++ 开发人员想了一个好办法:保留原来的库和头文件,它们在 C++ 中可以继续使用,然后再把原来的库复制一份,在此基础上稍加修改,把类、函数等等纳入命名空间 std 下,就成了新版 C++ 标准库。这样共存在了两份功能相似的库,使用了老式 C++ 的程序可以继续使用原来的库,新开发的程序可以使用新版的 C++ 库。
  为了避免头文件重名,新版 C++ 库也对头文件的命名做了调整,去掉了后缀 .h,所以老式 C++ 的 iostream.h 变成了 iostream,fstream.h 变成了 fstream。而对于原来C语言的头文件,也采用同样的方法,但在每个头文件的名字前额外增加一个字母 ’ c ',于是 C 的 stdio.h 变成了 cstdio,stdlib.h 变成了 cstdlib。
  需要注意的是,旧的 C++ 头文件是官方所反对使用的,已明确提出不再支持,但旧的 C 头文件仍然可以使用,以保证对 C 的兼容性。实际上,编译器开发商不会停止对客户现有软件提供支持,可以预计,旧的 C++ 头文件在未来数年内还是会被支持。

  • C++ 头文件现状的总结:
    • 旧的 C++ 头文件,如 iostream.h、fstream.h 等将会继续被支持,尽管它们不在官方标准中。这些头文件的内容不在命名空间 std 中,其中的标识符都是全局作用范围。
    • 新的 C++ 头文件,如 iostream、fstream 等包含的基本功能和对应的旧版头文件相似,但头文件的内容在命名空间 std 中。
      (注意:在标准化的过程中,库中有些部分的细节被修改了,所以旧的头文件和新的头文件不一定完全对应)
    • 标准 C 头文件,如 stdio.h、stdlib.h 等继续被支持,其的内容不在 std 中。
    • 具有 C 库功能的新 C++ 头文件 名字为老 C 头文件前加一个字母 ’ c ’ 且没有 .h 后缀,如 cstdio、cstdlib。它们提供的内容和相应的旧 C 头文件相同,只是内容在 std 中。
    • 可以发现:  ① 对于不带 .h 后缀的头文件,所有的符号都位于命名空间 std 中,使用时需要声明命名空间 std;
             ② 对于带 .h 后缀的头文件,没有使用任何命名空间,所有符号都位于默认命名空间(全局作用域)。
      这也是 C++ 标准所规定的。



(未完待续)
匿名空间
命名空间的重命名



  —— DaveoCKII
2022.05.04

你可能感兴趣的:(【C/C++】,c++,学习,开发语言)