命名空间namespace
背景
先来看一段代码及其运行结果。
#include<stdio.h>
#include<stdlib.h>
int rand = 10;
int main()
{
printf("%d ", rand);
return 0;
}

如图所示,程序报错称rand重定义,这就涉及到了C语言里边的命名冲突问题。我们知道在stdlib这个头文件里边有一个rand函数是用来生成随机数的,而我们又定义了这个rand变量,就导致了这个问题。
为了解决,C++给我们提供了一种语法叫命名空间。
命名空间的定义
namespace 标识符
{
//变量,函数,类型……
}
我们知道C语言里边有全局域跟局部域的概念,在不同的域里边定义的同名的变量是不会发生重命名之类的问题的。在C++里,namespace定义的也是一个域,叫命名空间域。这个域可以解决全局域中同名的变量函数之间的名字冲突问题。也就是说命名空间只能定义在全局。为什么只解决全局的呢?是因为在局部域里边一般是不可能定义同名的东西的。还有一个值得注意的点是命名空间不会影响变量的生命周期。
有了命名空间,我们就可以解决上边C代码的命名冲突问题了。如下:
#include<stdio.h>
#include<stdlib.h>
namespace change
{
int rand = 10;
}
int main()
{
printf("%d ", rand);
return 0;
}
那如何去访问change命名空间里边的rand呢?在C++里边,编译器是先局部后全局的原则,如果全局域跟局部域有同名的变量,编译器会优先去访问局部的。要是只想访问全局的呢?这就要用到域作用限定符(::),在变量名前加上::就可以访问全局的那个同名变量了。同理,要访问命名空间里边的同名变量,在::前再加上命名空间的名字就可以了。
#include<stdio.h>
#include<stdlib.h>
namespace change
{
int rand = 10;
}
int x = 100;
int main()
{
int x = 1000;
printf("%d \\n", change::rand);
printf("%d \\n", x);
printf("%d \\n", ::x);//访问全局的x
return 0;
}
命名空间的嵌套定义
假如现在在一家公司里边有两个部门共同去完成一个项目,那为了解决这两个部门里边可能出现的命名冲突问题,就可以分别用两个不同的命名空间去解决这个问题,同样的,在一个部门里的不同小组也有可能会出现这样的问题,这时候就可以在这个部门的命名空间里边再嵌套各小组的命名空间去解决。
#include<iostream>
using namespace std;
namespace change
{
namespace A
{
int a = 10;
}
namespace B
{
int a = 100;
}
}
int main()
{
cout << change::A::a << endl;
cout << change::B::a << endl;
return 0;
}
命名空间的分文件定义
在工程里边经常需要声明和定义分离,这里命名空间的分文件定义主要指的就是不同文件里边的同名命名空间,也就是特指声明和定义分离的情况,此时编译器会自动认为它们是同一个命名空间。
命名空间的使用
前边已经说过,编译器的默认查找逻辑是先局部后全局,它不会主动去命名空间里边查找,那如何使用命名空间里边的变量和函数呢?有三种方式,如下:
#include<iostream>
//std是C++标准库所在的命名空间
//展开命名空间的全部成员
using namespace std;
namespace change
{
int a = 10;
int b = 20;
}
int main()
{
//指定命名空间访问
cout << change::a << endl;
//using将命名空间的某个成员展开
using change::b;
cout << b << endl;
return 0;
}
引用
引用就是给已经存在的变量起一个别名,引用变量跟它所引用的变量共用同一块内存空间。引用必须要在定义的时候就初始化。
类型& 引用别名 = 引用对象
int a = 10;
//一个变量可以有多个引用
int& b = a;
int& c = a;
int d = 20;
//引用不改变指向,这里是赋值
b = d;
引用的使用
引用对标的是C语言的指针,在某种程度上可以简化代码的理解成本。
引用功能1:做函数形参,修改形参影响实参
#include<iostream>
using namespace std;
//交换两个变量的值
void Swap(int& x, int& y)
{
int z = x;
x = y;
y = z;
}
int main()
{
int a = 10;
int b = 20;
Swap(a, b);
cout << a << " " << b << endl;
return 0;
}
引用功能2:做函数形参,减少拷贝,提高效率
struct A
{
int a[100];
int b;
//……..
};
void Func(struct A& aa)
{
//……..
}
引用功能3:引用做返回值类型,修改返回对象
引用功能4:引用做返回值类型,减少拷贝,提高效率
//代码1
int& SLAt(SL& sl, int i)
{
assert(i < sl.size);
return sl.a[i];
}
//代码2
int SLAt(SL& sl, int i)
{
assert(i < sl.size);
return sl.a[i];
}
说明:上边的代码是截取的C语言实现顺序表的部分,SLAt函数的作用就是获取顺序表里下标为i的值。代码1是传引用返回,代码2是传值返回。
注意:有时候在一个函数里边有那种临时变量,假设为ret,然后该函数还传引用返回了ret。此时,用普通的变量去接收这个值的话,获得的就可能不是ret本来的值了,因为ret出作用域会销毁,里边就有可能是随机值,如果再去用就会产生问题。更有甚者,用引用变量假设为a去接收这个函数的返回值,由于函数是传引用返回,那a就成了ret的别名,如果还a++什么的话就变成越界访问了,因为原来的Func函数的栈帧空间早就释放还给内存了,而a还在使用。虽然编译器对越界访问是一种抽查行为,可能不会报错,但是这种做法很显然是不可取的。
const引用
我们知道被const修饰的变量具有常性,像是const int y = 10;这样的代码,y就具有常性。那如果要写一个y的引用该怎么写呢?
先知道一个概念,就是权限只能缩小不能放大。因此如果用类似int& a = y;这样的方式去给y起别名的话,就有权限放大的问题,仔细想一想,y作为一个常变量,只读不可写的,可a作为y的别名居然可读可写了,这肯定是有问题的。const引用解决的就是这样的问题。
const int y = 10;
const int& a = y;
int x = 100;
int& b = x;
const int& c = x;//权限的缩小
//还有一些场景
double d = 1.1;
/* 类型转换本质是先会产生一个临时变量
* 里边存的是d的整数部分
* 而临时变量具有常性
* 本质就跟被const修饰了一样
* 如下的l本质引用的是那个临时变量
*
* 注:编译器在内存里开的一块没有名字的空间去存放表达式的结果
* 这块空间叫临时变量
*/
const int& l = d;
const int& q = x * 10;//表达式求值结果也会存在临时变量里
指针和引用的关系
这里主要就说两个点,引用在汇编层是用指针实现的,但是一般以语法层面的理解为主,即指针变量是开空间存变量的地址,引用不开空间,它仅是变量的别名。还有一个点就是引用虽然跟指针的功能很像,但引用无法代替指针,原因就是引用无法改变指向,所以在遇到链表这样的链式结构需要改变指向的情景时,引用无法代替指针。
inline函数
在C语言里有一个叫宏的东西,它的功能跟函数类似。简单的说一下它的优缺点,具体内容暂不多说。缺点:复杂/类型安全检查/不能调试。优点:预处理阶段替换,不用建立栈帧,本质是一种提效。
内联函数就是为了解决宏函数的各种缺点,它的功能跟宏函数类似,就是直接在调用处展开,不会去建立函数栈帧。在函数名前边加上inline就可认为此函数是内联函数了。
inline int Add(int x, int y)
{
return x + y;
}
注意点
inline函数对于编译器来说只是一种建议,不一定会真的展开。但是内联函数一般使用于短小且频繁调用的函数。仔细想想,若是一个100行的函数在一个程序里的100个不同的位置被调用,如果这个函数是inline函数,那这100个地方都要被展开,程序的代码量直接扩大了100*100行左右(100个地方调用,每个地方都展开100行的函数代码),反观如果是普通函数的话,那每次的调用其实都是开函数栈帧,调用完就销毁了,程序的代码量只会增加100+100行左右(100个call函数地址,每次call都是100行函数代码)。所以这也就是inline仅是一种建议的原因。
内联函数直接在.h文件里边定义就可以了。它不能声明跟定义分开,否则会有链接错误。具体原因看下边的一张图。
C++里的代码都是要经过编译链接运行才能跑起来,在编译的时候会先预处理,就是将.h文件里边的内容包含到对应有#include声明的文件里,编译先检查一下语法错误什么的,之后会生成一个.obj文件,并且此时每个源文件都有一个符号表,最后就将所有文件一起链接起来,而像test这种只有函数声明没有函数定义的文件就会在链接的时候到别的文件的符号表里边去找对应函数地址,从而找到函数定义(调用函数的时候是call地址,只要有地址就能找到定义)。此时问题来了,inline函数只是一种替换,这就意味着它不会进符号表,那这就导致链接的时候只有函数声明但找不到函数定义。因此,inline函数不能声明和定义分离。





