一.引用
目录
1.什么是引用
水浒传一百单八将都外号有自己的外号,引用可以理解为取外号。
int a = 0;
int& s = a;
int b = a;
a++;//调试,取地址
s++;//测试别名是否会改变原数
int& ss = s;//给别名取别名
cout << "a="<<a<< " " << "b=" << b << endl;
printf("a=%d b=%d", a, b);//此时用printf比较舒服
在这段代码中,我们定义了三个变量,a,s,b,其中s是a的引用,输出:
a=2 b=0
a=2 b=0
那能否给指针取别名呢?当然可以,通过调试,发现三者都指向同一块空间。
int* c = &a;
int* & p = c;
也就是说,对别名或者原变量的操作是同时发生的,你变我也变,你有我就有。
void Swap(int& x, int& y)//输出型参数
{
int tmp = x;
x = y;
y = tmp;
}
Swap(a, b);
printf("\na=%d b=%d", a, b);
我们在c语言交换两个数用指针进行操作,c++提供了一种新的操作,引用。它们底层逻辑相同,但又有所区别。
在学习数据结构的时候我们可能会看到这样的传址方式,重命名声明一个*PNode指针,然后实现一个尾插,用PNode&的方式接收,这样就避免了传递二级指针使其复杂化,需要注意的是PNode是个指针,不能在使用时在前面加上*,因为*是解引用,属于具体对象。例如*int就是错误的。
引用做参数
typedef struct Node
{
struct Node* next;
int val;
}Node, * PNode;
//void PushBack(Node*& phead, int x)
void pushback(PNode &phead,int x)
{
Node* newNode = (Node*)malloc(sizeof(Node));
if (phead == nullptr)
{
phead = newNode;
}
}
2.引用的特点
C++作为c的延申,使其兼容了复杂的指针,但并不意味这指针就被淘汰了,一些场景下必须用到指针,比如对单链表的删除操作,next指针如果作为下个节点地址的引用,引用不能改变指向。那么一旦那个节点被删除,引用也会跟着删除。这或许就是各美其美吧。
指针如果不使用可以将它初始化为空指针,而引用则必须对其初始化个对象,类似“黑旋风——李逵”,而且一旦指明,则不允许引用其他对象,即张冠李戴。
引用做返回值
我们先搞清楚返回值的返回过程吧,我们知道,函数调用会创建函数栈帧,该程序会创建main函数栈帧和Count函数栈帧,这块空间在创建时是被保护的,而在调用结束后会被操作系统回收,此时这块空间的所有权发生了变化。
那问题是我们怎么在摧毁空间后返回这个值呢,答案是编译器会创建一块临时变量空间去接受这个返回值而这个值一般通过寄存器返回。
那么被static修饰的n返回时是通过临时变量还是直接通过静态区返回呢?
结论:都会通过创建临时变量的方式返回。
很多人可能不太理解,是否多此一举,我们可以简单理解为编译器的设定一定是有他的道理吧~。
所以我们引出传引用返回
int& Count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = Count();
return 0;
}
而引用可以减少拷贝,对返回值进行一个引用,也就是取别名,即返回这个数本身。
因为出了作用域n这个数还在,所以返回其引用不会造成非法访问。
使用条件:出了作用域不被销毁,像静态的、全局的、堆区开辟、上一层栈帧等都可以使用传引用返回。
调用者可以修改返回对象
引用返回的作用还不止这点,引用还能作为左值使返回对象放生改变。
typedef struct Array//数组
{
int a[N];
int size;
}AY;
int& Pos(AY& array, int x)//返回数组值
{
assert(x < N);//判断是否越界
return array.a[x];
}
我们定义了一个数组和一个Pos函数,通过引用返回改变其返回值,而不用引用返回做不到是因为临时变量是一个常量,不能被修改,而引用返回解决了之一问题。
再来看一段代码
typedef struct Array//数组
{
int& operator[](int i)//调用函数
{
assert(i < N);//检查越界
return a[i];
}
int a[N];
int size;
}AY;
使用了后面会讲到的运算符重载,通过上面的例子可以稍微感受引用返回的好处。
注意事项
下面来看一段错误代码:
int& Add(int x, int y)
{
//错误return x + y;
int c = x + y;
return c;
}
int& num = Add(2, 4);
Add(3, 4);
cout << "Add(2, 4):" << num << endl;
cout << "Add(2, 4):" << num << endl;//这时可能被清理了
通过引用接收c的别名,即c这块被销毁的变量空间,这种行为是非法的,会导致结果是未定义的,再次调用Add后发现第一次打印结果变成了7,说明我们第二次创建栈帧改变了里面的c的值,导致num发生了改变
还有一个易错点是接收返回值的这个引用去掉对不对的问题,因为在去掉后发现结果是正确的,但是正确不代表没问题,这取决于编译器什么时候清理这块空间,重要的不是结果,而是过程。
再次论证了引用返回不能用在出了作用域后被销毁的情况。
赋值权限问题
//权限平移
int a = 1;
int &b = a;
const int* a1 = NULL;
const int* b1 = a1;
//权限放大
const int* c = 0;
int &d = c;
//权限缩小
int s = 0;
const int &ss = s;
int *p = NULL;
const int *pp = p;
权限的放大/缩小只存在与指针与引用。
权限只能缩小,不能放大。
//单纯拷贝,不适用
const int m = 10;
int n = m;
这段代码向我们展示了临时变量的常属性,如果想使用引用接收则添加const。
另一个例子:
int i = 0;
//double ii = i;
const double& x = i;//产生临时变量场景一
cout << (double)i << endl;//场景二
我们知道,从double到int转换会发生精度的变化,这中间会产生一个double类型的临时变量,同样地,添加const就行了
引用和指针区别
从汇编角度看,引用和指针的底层逻辑是相同的,感兴趣的可以自己去看看汇编相关知识。
区别:
1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
2. 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4. 没有NULL引用,但有NULL指针
5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小7. 有多级指针,但是没有多级引用
8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9. 引用比指针使用起来相对更安全