编程视界:C++命名空间
目录
命名空间
为什么要使用命名空间
什么是命名空间
命名空间的使用方式
关键点总结
命名空间的嵌套使用
匿名命名空间
跨模块调用问题
命名空间可以多次定义
总结
首先从C++的hello,world程序入手,来认识一下C++语言
#include <iostream>
using namespace std;
int main(int argc, char * argv[]){
cout << "hello,world" << endl;
return 0;
}>
(1)iostream是C++的头文件,为什么没有后缀?—— 模板阶段再作讲解
(2)using namespace std是什么含义?—— 命名空间的使用
(3) cout << "hello,world" << endl; 实现了输出hello,world的功能,如何理解这行代码?—— cout的使用
命名空间
为什么要使用命名空间
一个大型的工程往往是由若干个人独立完成的,不同的人分别完成不同的部分,最后再组合成一个完整的程序。由于各个头文件是由不同的人设计的,有可能在不同的头文件中用了相同的名字来命名所定义的类或函数,这样在程序中就会出现名字冲突。不仅如此,有可能我们自己定义的名字会与C++库中的名字发生冲突。
名字冲突就是在同一个作用域中有两个或多个同名的实体,为了解决命名冲突 ,C++中引入了命名空间,所谓命名空间就是一个可以由用户自己定义的作用域,在不同的作用域中可以定义相同名字的变量,互不干扰,系统能够区分它们。
C语言中避免名字冲突,只能进行起名约定
int hw_cpp_tom_num = 100;
int wd_cpp_bob_num = 200;
什么是命名空间
命名空间又称为名字空间,是程序员命名的内存区域,程序员根据需要指定一些有名字的空间域,把一些全局实体分别存放到各个命名空间中,从而与其他全局实体分隔开。通俗的说,每个名字空间都是一个名字空间域,存放在名字空间域中的全局实体只在本空间域内有效。名字空间对全局实体加以域的限制,从而合理的解决命名冲突。
C++中定义命名空间的基本格式如下:
namespace wd
{
int val1 = 0;
char val2;
}// end of namespace wd
在声明一个命名空间时,大括号内不仅可以存放变量,还可以存放以下类型:
变量、常量、函数、结构体、引用、类、对象、模板、命名空间等,它们都称为实体
(1)请尝试定义命名空间,并在命名空间中定义实体。
(2)命名空间中的实体如何使用呢?
命名空间的使用方式
命名空间一共有三种使用方式,分别是using编译指令、作用域限定符、using声明机制。
-
作用域限定符
每次要使用某个命名空间中的实体时,都直接加上作用域限定符::,例如:
namespace wd { int number = 10; void display() { //cout,endl都是std空间中的实体,所以都加上'std::'命名空间 std::cout << "wd::display()" << std::endl; } }//end of namespace wd int main(void) { std::cout << "wd::number = " << wd::number << endl; wd::display(); return 0; }
好处:准确,只要命名空间中确实有这个实体,就能够准确调用(访问)
坏处:繁琐
-
using编译指令
我们接触的第一个C++程序基本上都是这样的,其中std代表的是标准命名空间。
#include <iostream> using namespace std; //using编译指令 int main(int argc, char * argv[]){ cout << "hello,world" << endl; return 0; }
其中第二行就使用了using编译指令。如果一个名称空间中有多个实体,使用using编译指令,就会把该空间中的所有实体一次性引入到程序之中;对于初学者来说,如果对一个命名空间中的实体并不熟悉时,直接使用这种方式,有可能还是会造成名字冲突的问题,而且出现错误之后,还不好查找错误的原因,比如下面的程序就会报错,当然该错误是人为造成的。
#include <iostream> using namespace std; double cout() { return 1.1; } int main(void) { cout(); return 0; }
-
using声明机制
using声明机制的作用域是从using语句开始,到using所在的作用域结束。要注意,在同一作用域内用using声明的不同的命名空间的成员不能有同名的成员,否则会发生重定义。
作用域示例
namespace A {
void foo() {}
int x = 10;
}
void func1() {
using A::foo; // 作用域开始
foo(); // 正确:调用 A::foo()
} // 作用域结束(到 func1 末尾)
void func2() {
// foo(); // 错误:此处无法使用 A::foo
}
重定义示例
namespace B {
void foo() {} // 与 A::foo 同名但实现不同
int x = 20; // 与 A::x 同名
}
// 全局作用域中的 using 声明
using A::x; // 引入 A::x
using B::x; // 错误:重定义!x 已存在(来自 A)
int main() {
// x = 30; // 若单独使用一个 using 声明,此处可访问
return 0;
}
合法作用域示例
void safe_func1() {
using A::foo;
foo(); // 调用 A::foo
}
void safe_func2() {
using B::foo;
foo(); // 调用 B::foo(不同作用域,无冲突)
}
关键点总结
-
作用域限定:
using
声明的作用域从声明处开始,到当前代码块({}
)结束 -
冲突规则:同一作用域内不允许通过
using
引入同名标识符 -
隔离方案:通过函数/代码块划分作用域可避免冲突
-
与
using namespace
区别:using
声明精确引入单个标识符,using namespace
会污染当前作用域的所有标识符
实际开发中建议:
-
优先在函数内局部使用
using
声明 -
避免在头文件的全局作用域使用
using
-
对频繁使用的标识符,使用
namespace alias
(如namespace fs = std::filesystem;
)
#include <iostream>
using std::cout;
using std::endl;
namespace wd
{
int number = 10;
void display()
{
cout << "wd::display()" << endl;
}
}//end of namespace wd
using wd::number;
using wd::display;
int main(void)
{
cout << "wd::number = " << number << endl;
wd::display();
return 0;
}
在这三种方式之中,我们推荐使用的就是第三种,需要哪个实体的时候就引入到程序中,不需要的实体就不引入,尽可能减小犯错误的概率。
命名空间的嵌套使用
类似于文件夹下还可以建立文件夹,命名空间中还可以定义命名空间。那么内层命名空间中的实体如何访问呢?尝试一下
匿名命名空间
命名空间还可以不定义名字,不定义名字的命名空间称为匿名命名空间(简称匿名空间),其定义方式如下:
namespace {
int val1 = 10;
void func();
}//end of anonymous namespace
使用匿名空间中实体时,可以直接使用,也可以加上作用域限定符(没有空间名),但是如果匿名空间中定义了和全局位置中同名的实体,会有冲突,即使使用::作用域限定符也无法访问到匿名空间中重名的实体,只能访问到全局的实体。
在C++代码中可以直接使用一些C语言的函数,就是通过匿名空间实现(体现了C++对C的兼容性),在本文件使用匿名命名空间的实体时不必用命名空间限定。
printf本身可以直接用,和C语言中的效果一致。但是经过匿名空间改写后,效果不一样了 —— 不要随意改写
匿名空间注意事项:
(1)匿名空间不要定义与全局空间中同名的实体;
(2)匿名空间中支持改写兼容C语言的函数,但是最好不要改写;
(3)匿名空间中的实体不能跨模块调用。
补充:匿名空间和有名空间(具名空间)统称为命名空间(名称空间)。
跨模块调用问题
一个.c/.cc/*.cpp的文件可以称为一个模块。
(1)全局变量和函数是可以跨模块调用的
externA.cc
externB.cc
对externA.cc和externB.cc联合编译,实现跨模块调用
(2)有名命名空间中的实体可以跨模块调用
命名空间中的实体跨模块调用时,要在新的源文件中再次定义同名的命名空间,进行联合编译时,这两次定义被认为是同一个命名空间。
使用规则:如果要同时从全局位置和命名空间中外部引入实体,要么让它们不要重名,要么在使用时采取作用域限定的方式。
using wd2 :: num 放在test2外面就发生冲突,使用using namespace wd2 也会发生冲突,看似不在一个作用域也会冲突,所以就用上面的解决办法。
(3)静态变量和函数只能在本模块内部使用
(4)匿名空间的实体只能在本模块内部使用
匿名空间中的实体只能在本文件的作用域内有效,它的作用域是从匿名命名空间声明开始到本文件结束。
补充:extern外部引入的方式适合管理较小的代码组织,用什么就引入什么,但是如果跨模块调用的关系不清晰,很容易出错;
include头文件的方式在代码组织上更清晰,但是会一次引入全部内容,相较而言效率比较低。
命名空间可以多次定义
函数可以声明多次,但是只能定义一次;命名空间可以多次定义。
在同一个源文件中可以多次定义同名的命名空间,被认为是同一个命名空间,所以不能进行重复定义。
在命名空间中可以声明实体、定义实体,但是不能使用实体。使用命名空间中的实体一定在命名空间之外,可以理解为命名空间只是用来存放实体。
总结
命名空间的作用:
-
避免命名冲突:命名空间提供了一种将全局作用域划分成更小的作用域的机制,用于避免不同的代码中可能发生的命名冲突问题;
-
组织代码:将相关的实体放到同一个命名空间;
-
版本控制:不同版本的代码放到不同的命名空间中;
总之,需要用到代码分隔的情况就可以考虑使用命名空间。
还有一个隐藏的好处:声明主权。
下面引用当前流行的命名空间使用指导原则:
-
提倡在已命名的名称空间中定义变量,而不是直接定义外部全局变量或者静态全局变量。
-
如果开发了一个函数库或者类库,提倡将其放在一个命名空间中。
-
对于using 声明,首先将其作用域设置为局部而不是全局。
-
不要在头文件中使用using编译指令,这样,使得可用名称变得模糊,容易出现二义性。
-
包含头文件的顺序可能会影响程序的行为,如果非要使用using编译指令,建议放在所有#include预编译指令后。