目录
前言
C语言中的命名冲突
命名空间
定义
使用
命名空间展开
全部展开
指定展开(部分展开)
补充
在我们刚开始学习C++的时候,总是从打印“hello world”开始。
#include
using namespace std;
int main()
{
cout << "hello world\n" <
那么你知道这里的 using namespace std; 起到什么作用吗?还是说只是把它记了下来,当成和头文件一样的必写项呢?看完这篇文章,我相信你一定能对它有更深刻的认识。
#include
#include
int rand = 1;
int main()
{
printf("%p\n", rand);
return 0;
}
运行下这段代码,可以发现编译器会报一个重定义的错误,说 rand 以前的定义是一个函数。因为我们包含了 stdlib 这个头文件,在这个头文件里 rand 被定义为一个函数。从这里就可以看出在C语言中的命名冲突问题了,这还只是和库中的命名冲突,还有程序员之间的命名冲突。当我们在一个大项目开发时,我们需要定义的变量和函数非常多,不同程序员之间难免会用到相同的名字,这时候就出问题了,只能让其中一方更改名字,非常麻烦。C++引入了命名空间来解决这个问题,使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的。
namespace xxx // xxx 是这个命名空间的名字,可以随便起
{
int rand = 1; // 命名空间定义变量
int Add(int left, int right) // 命名空间定义函数
{
return left + right;
}
struct Node // 命名空间定义结构体,包括类也是可以在这里定义的
{
struct Node* next;
int val;
};
namespace N1 // 命名空间可以嵌套
{
int Mul(int left, int right)
{
return left * right;
}
}
}
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员,可以是变量、函数或者类型,并且命名空间可以嵌套使用。这时候我们再去运行C语言中冲突的那段代码,就不会报错了。因为这个命名空间定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中,默认情况下不会去访问该命名空间里的内容。可以理解为把里面的内容围起来了,这样就很好的做到了名字的隔离,解决了C语言命名冲突的问题。
注意:在命名空间域内定义的成员可以在全局范围内使用,只需使用域作用符来指明其所属的命名空间。
int main()
{
printf("%d\n", xxx::rand);
printf("%d\n", xxx::Add(1, 2));
struct xxx::Node node;
// 事实上这里的struct可以省略,学到类和对象那部分就能理解了
// Node就是类型的名字
printf("%d\n", xxx::N1::Mul(2, 2)); // 嵌套命名空间的使用
return 0;
}
: : 是域作用限定符,用于指定命名空间、类、结构体或枚举的范围,以明确标识特定成员的所属关系。通过 : : ,我们能够访问全局命名空间下的变量、函数,或者类中的静态成员。这个操作符提供了一种明确的方式来解决命名冲突,同时允许我们在不同的作用域中使用相同的标识符。
printf("%d\n", Add(1, 2));
// 这里就报错了,因为Add函数是定义在域里的
在没有使用域作用符指定命名空间时,默认情况下不会在命名空间中查找。因此,我们可以将需要定义的变量、函数等放入相应的命名空间域中。
想象一下,我们定义了一个命名空间,在里面定义了程序中所需要的一切变量、函数等。在我们的代码中,每次使用它们都要用域作用符来指定命名空间,是不是非常的麻烦。(当然,我这里指的是它们不会存在命名冲突的情况下)这时候我们可以展开命名空间。
using namespace xxx // xxx 是命名空间的名字
这时候就默认了优先从 xxx 命名空间里搜索,接下来使用该命名空间的成员时,就不需要再加域作用符限定符了。如下所示:
int main()
{
printf("%d\n", Add(1, 2));
return 0;
}
注意:不要轻易的展开命名空间,除非确定不会发生命名冲突。
现在你应该明白为什么要包含 using namespace std; 这句代码了。这里的目的是展开 std 这个命名空间,它是C++官方库定义的命名空间。在工程项目中,最好不要展开 std,以免发生命名冲突。然而,在日常学习C++的过程中,展开 std 可以更方便地使用库中的功能。
当我们只需要使用命名空间的一些成员时,可以明确指定展开,这有助于避免将整个命名空间的内容全部引入,从而有效防止命名冲突的发生。下面以 std 命名空间来举例:
using std::cout;
using std::endl;
cout 和 endl 都是定义在 std 命名空间中的,后续会介绍它们的功能。这里只需要知道我们将 std 命名空间中的 cout 和 endl 展开了,这样在后续使用时就无需添加域作用限定符,可以直接使用这两个成员。
当命名空间中的函数声明和定义分离时,应该如何编写呢?接下来,我们通过代码演示一下:
假设下面这个是在 .h 文件中的:
namespace xxx
{
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack; // 这里其实无需 typedef,同理学到类和对象就明白了
void StackInit(Stack* ps, int n = 4);
void StackPush(Stack* ps, int x);
}
下面这个是 .cpp 文件中的:
#include "Stack.h"
namespace xxx
{
void StackInit(Stack* ps, int n)
{
ps->a = NULL;
ps->top = 0;
ps->capacity = n;
}
void StackPush(Stack* ps, int x)
{
// ...
}
}
在同一个工程中,如果存在多个同名的命名空间,编译器会将它们合并成一个。因此,在声明和定义分离时,只需编写一个同名的命名空间,并在其中正常编写定义即可。
注意:同一个变量不能在多个同名的命名空间中重复定义,否则在合并时会导致重定义错误。
合并:在 C++ 中,合并同名的命名空间是在编译器进行链接阶段完成的。在链接阶段,编译器将所有编译单元中的符号进行合并(符号表是在编译阶段形成的),包括函数、变量和同名的命名空间。这确保了在最终生成的可执行文件中,只有一个命名空间被保留,避免了重复定义和冲突。
这时候还会有一个问题,如果我想在命名空间里定义一个变量,这个变量要在我的main函数源文件中使用,这时候就有一点变化了。
这是 .h 文件:
#include
#include
using namespace std;
namespace xxx
{
int c = 10;
int Add(int a, int b);
void func();
}
这是 .cpp 文件:
#include "print.h"
namespace xxx
{
int Add(int a, int b)
{
return a + b;
}
void func()
{
cout << "func()" << endl;
}
}
这是main函数源文件:
#include "print.h"
using namespace xxx;
int main()
{
cout << c << endl;
return 0;
}
这时候一运行就会报错,如下所示:
很明显这个是链接阶段的错误,原因是重定义了。因为我们在两个源文件中都包含了这个 .h 文件,最后在命名空间合并时,就会出现 c 这个全局变量被定义了两次,想解决这个问题,只需要在 .h 文件和 .cpp 文件中稍作更改。
这是 .h 文件:
#include
#include
using namespace std;
namespace xxx
{
extern int c;
int Add(int a, int b);
void func();
}
这是 .cpp 文件:
#include "print.h"
namespace xxx
{
int c = 10;
int Add(int a, int b)
{
return a + b;
}
void func()
{
cout << "func()" << endl;
}
}
为了解决这个问题,我们使用 extern 关键字在 print.h 中声明了变量 int c,这告诉编译器,这个变量是在其他地方定义的,不要在当前文件中为它分配存储空间。然后,我们在 print.cpp 中进行了实际的定义,确保只有一个全局变量 c 被分配了存储空间。