引用概念的引出
我们都知道,在很多地方都会应用到将两个数交换的函数,
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
但是这作为一个函数,却无法真正的实现两个数的交换,
这个问题的原因就是,在底层,传递的a和b作为实参,被拷贝了一份放到了swap函数中,实际交换的是a的拷贝和b的拷贝,并不是a本身和b本身,如果要让这个函数实现它交换的功能,就应该将函数参数改为指针,
void swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
这样就是将a和b的指向交换了,也就实现了数据交换的功能,
但是,这样的代码可读性比较差,我要交换的是int类型的数据,必须要求我传个指针,并且指针的安全性系数不高,如果传了个野指针就崩溃了;
那这个swap函数就是哪哪都是缺陷?
传值,无法通过形参修改实参;
传地址,可读性差,安全性不高;
这时候就要用到一个新概念–引用
引用,可以理解为别名,并不是一个新变量,而是给已经存在的变量取了个别名;结合了他们两个的优势,可以按照传值的方式,保证了安全性和可读性,但是可以同时实现通过形参修改实参;
引用的使用规则
##引用定义的时候必须要初始化
int main()
{
int a = 1;
int& ra;
}
不初始化是会报错的;
其实可以理解为,取别名必须要有具体的物体才能取别名,不然这个别名取的有什么意义…
一个变量可以有多个引用
int main()
{
int a = 1;
int& ra = a;
int& rra = a;
int& rrra = a;
int& rrrra = a;
}
这样也是行得通的,不过是一个物体多了个几个别名而已,无伤大雅。
某个引用变量一旦引用一个实体,就不可以再引用其他实体
其实与其说是不可以再引用,不如说不知道如何再去引用;
假如说现在
int a = 1;
int& ra = a;
有一个a变量,ra是他的引用变量;那如果要让ra再去引用一个b变量,该怎么写???
ra = b;
这样是修改变量a的值,不是再让ra去引用b;
&ra = b;
这样写是将b赋值给ra变量的地址,更加离谱了…
所以其实压根就没办法修改一个引用的引用对象;
常引用
首先要明确什么是常量;
常量只有数字??
我们都知道,在定义数组的时候,数组的长度只能用常量来定义,所以给定一个确定的数字是绝对ok的,
const int n = 100;
int a[n];
但其实这样写,也不会报错;
所以常量分为两种,一种是数字,一种是const类型的引用
在常引用,也就是const类型的引用中,有几点需要注意的
const类型变量的引用也必须是const类型的
const int a = 10;
int& ra = a;
这样是100%报错的,因为ra是a的别名,可以通过ra来修改a,但是a是一个const类型的变量,是不能修改的,这样无法保证const的常性;
引用类型的“怪异”
我们都知道在定义引用变量的时候,肯定是将其定义成 和要引用的变量相同类型的形式,
比如,要定义int类型的变量a的引用变量ra,那肯定给ra定义成int&,不会往char&,double&这样去想,但是如果这样乱定义能不能行呢?
int main()
{
double d = 12.34;
const int& rd = d;
}
这样居然是不报错的…
为什么???????
这样是正确的,那rd输出出来是多少呢?
rd不是d的别名吗?为什么输出的不是d呢?
那如果再把代码改成
int main()
{
double d = 12.34;
const int& rd = d;
d = 459812375.324570;
cout << "rd = " << rd << endl;
}
这个时候rd变不变呢?
他居然没变
这里是因为,再定义rd这个引用变量的时候,因为double和int可以发生隐式类型转化,所以d这12.34转化成了一个不知名变量12,而且rd指向的是12,并不是d;所以d修改之后rd并没有变化,
而且,既然叫隐式类型转化,我们就肯定看不见中间过程,也就无法知道它的转化之后的变量和该变量的地址,也就无法修改这个变量,不就实现了常性吗~?
引用的应用
简化复杂的结构体变量嵌套
struct A
{
int a;
int b;
};
struct B
{
A c;
int d;
};
struct C
{
B e;
int f;
};
int main()
{
struct C t;
t.f = 1;
t.e.d = 2;
t.e.c.b = 3;
t.e.c.a = 4;
}
如果有三个结构体嵌套,就已经能混乱成这样…
而且还是在变量和结构体名只有abc的情况下…
如果再多一点,名字再复杂一点,看的人都要疯了
struct A
{
int a;
int b;
};
struct B
{
A c;
int d;
};
struct C
{
B e;
int f;
};
int main()
{
struct C t;
struct B& rb = t.e;
rb = { {1,2},3 };
t.f = 4;
}
这样写就会舒服一点
通过形参修改实参
这就是swap函数的后续了
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
可以通过传值的方式,通过形参修改实参
作为函数返回值
引用其实之所以好用,就是它别名的属性,就像上一条里面的swap函数,看似是传值,其实是传地址,传了个别名上去;
这里作为函数返回值也是一样,它可以省一次拷贝,如果我们用一个类型的变量接收函数返回值
但是需要注意两点:
1、函数返回值 是引用类型,必须用引用类型接受
2、引用作为函数返回值,一定注意不能返回函数栈上的空间,局部变量(上面的图只是做个例子,不要模仿)
具体是为什么,可以看一下这里
int& add(int l, int r)
{
int ans = 0;
ans = l + r;
return ans;
}
int main()
{
int& res = add(10, 20);
printf("%d\n", res);
printf("%d\n", res);
}
它的输出结果是
为什么会这样呢??
这里涉及到部分调用约定的知识,我们先看一下这个函数的调用约定是什么;
可见调用约定是: _cdcel
_cdcel有一条约定是:函数结束运行之后的栈帧中的垃圾数据不需要清理,下一个函数进来运行之前先清理;
可以理解为在食堂吃饭,吃完不需要把吃完的垃圾倒掉,下一个人来的时候要吃饭会先收拾你吃的垃圾;
那其实画个图就很好理解了;
1.
当还没走到main函数中的add(10,20),整个栈是这样的;
2.
走到add(10,20)之后,将add函数入栈
res指向结果30
3.add函数结束之后,并不会清理自己栈帧中的数据,所以这个时候ans30还在
只不过是ebp和esp位置变了
4.
现在要运行printf函数了,但是要这个时候还有add函数的数据
也就是说,printf将res这个参数入栈的时候,res指向的数据还是存在的;所以第一次会输出30;
但是当第一个printf运行完之后,add函数的数据就肯定被销毁了,这个时候res指向的值也随之销毁,成了随机值,所以会输出随机值;
所以还是不要将引用返回局部变量;
传值,传指针,传引用的效率对比
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
struct A
{
int a[100000];
int b;
};
void f1(A a)
{
}
void f2(A* a)
{
}
void f3(A& a)
{
}
int main()
{
A a;
size_t b1 = clock();
for (int i = 0; i < 10000; ++i)
{
f1(a);
}
size_t e1 = clock();
size_t b2 = clock();
for (int i = 0; i < 10000; ++i)
{
f2(&a);
}
size_t e2 = clock();
size_t b3 = clock();
for (int i = 0; i < 10000; ++i)
{
f3(a);
}
size_t e3 = clock();
cout << e1 - b1 << endl << e2 - b2 << endl << e3 - b3 << endl;
}
可以看出传值和传引用传指针的效率差距巨大,
而传值和传引用的效率差不多,这是为什么??
int main()
{
int a = 1;
int* pa = &a;
int& ra = a;
}
看这个代码的反汇编,看看能不能找到什么信息
我们在定义引用和指针的时候,反汇编居然显示结果相同??!
也就是说底层逻辑中,引用就是指针…
引用和指针的区别
1、使用方式有区别
指针可以不初始化但是引用不可以
2、赋值有区别
引用类似于 类型* const 的指针,无法改变它的指向,但是有的指针可以改变指向有的不行;
3、++结果也不同
对引用类型++,结果是对其引用的变量++,但是对指针++,会在一块连续的内存中向后移动一个空间,或者直接访问未申请的空间runtime error
4、概念区别
引用的概念实体变量的一个别名,是和实体共用一个空间,但是指针就是一个指针变量,自己占一个空间
5、sizeof的区别
sizeof引用的结果是 引用类型的大小 比如在32位操作系统下 int大小位4,char为1,double为8;但是sizeof指针在所有32位操作系统下 大小都是4;
6、有多级指针,但没有多级引用
7、访问实体方式不同
引用不需要显式解引用,但是指针需要;