【C++】命名引发的“战争”

前言

在我们刚开始学习C++的时候,总是从打印“hello world”开始。

#include <iostream>
using namespace std;

int main()
{
    cout << "hello world\n" <<endl;

    return 0;
}

那么你知道这里的 using namespace std; 起到什么作用吗?还是说只是把它记了下来,当成和头文件一样的必写项呢?看完这篇文章,我相信你一定能对它有更深刻的认识。


C语言中的命名冲突

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

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;
    // 这里使用起来有点别扭,Node才是这个结构体的名字

	printf("%d\n", xxx::N1::Mul(2, 2)); // 嵌套命名空间的使用
	
	return 0;
}

: :  是域作用限定符,用于指定命名空间、类、结构体或枚举的范围,以明确标识特定成员的所属关系。通过 : : ,我们能够访问全局命名空间下的变量、函数,或者类中的静态成员。这个操作符提供了一种明确的方式来解决命名冲突,同时允许我们在不同的作用域中使用相同的标识符。

printf("%d\n", Add(1, 2));
// 这里就报错了,因为Add函数是定义在域里的

没有使用域作用符指定命名空间时,默认不会在命名空间中查找。因此,我们可以将需要定义的变量、函数等放入相应的命名空间域中。需要注意的是,当我们指定在命名空间中访问时,编译器将仅在该命名空间中查找。


命名空间展开 

全部展开

想象一下,我们定义了一个命名空间,在里面定义了程序中所需要的一切变量、函数等。在我们的代码中,每次使用它们都要用域作用限定符来指定命名空间,是不是非常的麻烦。(当然,我这里指的是它们不会存在命名冲突的情况下)这时候我们可以展开命名空间。

using namespace xxx // xxx 是命名空间的名字

展开命名空间后,编译器会先在当前作用域(局部作用域)查找符号,如果找不到,才会在全局作用域中查找,最后才会去命名空间中查找。这样在访问该命名空间的成员时就无需再使用域作用符。这使得在当前作用域中可以直接访问命名空间中的成员,简化了代码。如下所示:

int main()
{
	printf("%d\n", Add(1, 2));
	
	return 0;
}

注意:不要轻易的展开命名空间,除非确定不会发生命名冲突。

namespace xxx
{
	int a = 20;
}

using namespace xxx;

int main()
{
	int a = 10;
	cout << a << endl;

	return 0;
}

这里展开后命名空间里的 a 就是普通的全局变量,它和 main 函数里定义的 a 不会冲突,因为 main 函数中定义的是局部变量。在同名情况下,局部作用域中的变量会遮蔽全局作用域和命名空间中的同名变量,也就是局部优先原则。因此编译器会选择使用最内层的作用域中的符号,所以打印的结果是 10。 


现在你应该明白为什么要包含 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 <iostream>
#include <assert.h>

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 <iostream>
#include <assert.h>

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 被分配了存储空间。 

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值