目录
接着我们上节课讲的一些引用的基础知识:
1:引用就是别名,所以当变量a的引用为ra时,ra不仅值与a相同,连地址也与a相同。
引用需要注意的问题:
1:引用必须进行初始化,不能凭空引用:
2:引用一旦引用一个实体,就不能再引用另外的实体。
- int main()
- {
- int a = 10;
- int b = 20;
- int&ra = a;
- ra = b;
- return 0;
- }
如图所示,我们进行调试:
当main函数结束的时候,假如b和ra的地址是相同的,那么引用就可以引用多个实体,否则引用只能引用一个实体。
如图所示:我们可以发现,ra和b的值是相同的,ra和b的地址是不同的,这证明了引用多个实体是不行的。
1:作为输出型函数的参数:
我们先举一个输出型参数的例子:
- void Swap(int&a, int&b)
- {
- int tmp = a;
- a = b;
- b = tmp;
- }
输出型参数表示我们要对参数本身进行修改。
输入型参数:
- void Sort(int*a, int n)
- {
-
- }
输入型参数就是我们平常使用的排序函数,输入要排序的对象和对应数组的元素个数,我们在函数内部实现调用。
2:引用来做返回值。
在这里,我们首先讲一下内存的划分:
内存分为一下四部分:
1:常量区(代码段):存放普通常量。
2:静态区:存放的static修饰的静态变量。
3:堆区:动态内存申请的空间
4:栈帧:函数调用所开辟的空间
- int&Count()
- {
- static int n = 0;
- n++;
- return n;
- }
要理解这个引用返回,我们可以先理解一下传值返回:
- int Count()
- {
- static int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int ret = Count();
- return 0;
- }
要理解这个传值返回,我们把最简单的传值返回讲解一下:
- int Count()
- {
- int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int ret = Count();
- return 0;
- }
这个最简单的传值返回对应的内存图像是这样的:
具体的步骤是这样的:
我们先调用Count函数,调用函数,形成Count函数对应的栈帧,局部变量n就在栈帧空间内部,n++,n变成1,我们把n返回,这里的返回并不是直接把n返回给ret的,这里的返回是通过我们创建一个临时变量,用这个临时变量来接收n,但接收的只是n的值,然后Count函数调用结束,对应的栈帧销毁,对应的空间返还给操作系统,我们再把临时变量的值返回给Count值。
注意:当这个临时变量比较小时,被存放在寄存器中。
当这个临时变量比较大时,被存放在上一层函数的栈帧中,也就是main函数的栈帧中。
要证明以上的想法,我们需要判断ret和n的地址是否相同:
对应的ret和n的地址不同。
接下来,我们来分析更难的传值调用:
- int Count()
- {
- static int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int ret = Count();
- return 0;
- }
这段代码和上面的唯一的不同就是变量n用了static修饰,static修饰的话就是静态变量,静态变量存放在静态区,所以不会随着栈帧的销毁而释放。当我们调用函数完毕后,返回n,这时候,我们还是创建一个临时变量来接收n,再由临时变量返回给ret。
对应的ret还是1.
但是当我们使用引用返回时,我们需要小心:
我们举出一个错误案例:
- int& Count()
- {
- int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int &ret = Count();
- return 0;
- }
我们首先对代码分析:
调用函数Count,创建变量n,n++后等于1,返回1,这时候因为我们的返回值类型用的引用,所以我们返回的就是n的别名,但是函数调用完毕之后,属于n的那部分空间也已经被操作系统回收,这时候我们对属于n的空间进行访问实际上就是一种越界访问了。
我们进行测试:
- int& Count()
- {
- int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int &ret = Count();
- cout << ret << endl;
- return 0;
- }
进行编译:
打印的结果仍为1,原因是什么呢?
答:我们画图进行解释:当我们调用Count函数时,我们对应的内存图像是这样的。
但是当我们函数调用完毕后,我们对应的空间被释放:
虽然我们的空间不属于我们了,但是对应的n值仍旧存在,没有被覆盖,我们用对应的同样的引用ra来接收a,a其实是n的引用,所以ra就是n的引用。
我们再打印几次:
- int& Count()
- {
- int n = 0;
- n++;
- return n;
- }
- int main()
- {
- int &ret = Count();
- cout << ret << endl;
- cout << ret << endl;
- cout << ret << endl;
- return 0;
- }
为什么剩下的两次打印都是随机值?
答:因为我们的ra是n的引用,当我们刚刚调用完毕Count函数时,我们对应的图象是这样:
当我们第一次调用打印函数时,对应的图像是这样:
我们对应的n值已经被覆盖,但是注意:当我们调用打印函数的时候传参的时候,n值仍是存在的,n值为1,所以我们打印的n为1.
我们第一次打印函数调用完毕的图象是:
我们第二次调用打印函数:我们的参数n对应的空间现在是打印函数内部的随机值,所以我们打印的结果是随机值
第三次调用也是同样。
正确的写法:
- int& Count()
- {
- static int n = 0;
- n++;
- return n;
- }
- void p()
- {
- int x = 100;
- }
我们进行检测:
正确的原因如下:
因为我们是静态变量,静态变量n并不会随着函数的调用结束而被释放,并且我们返回的是n的引用,所以我们并不需要把返回值拷贝到临时变量中,进而能够节省空间。
结论:出了函数作用域,返回的变量不存在了,不能用引用返回,因为引用返回的结果是未定义的。
总结:传引用返回的作用
1:减少拷贝,提高效率
2:修改返回值。
引用做参数的作用:
我们写两个交换函数进行分析:
- void Swap(int a, int b)
- {
- int tmp = a;
- a = b;
- b = tmp;
- }
- void Swap(int&a, int &b)
- {
- int tmp = a;
- a = b;
- b = tmp;
- }
其中,对于第一个函数,我们在调用函数时,需要拷贝实参作为形参,而对于第二个函数,我们只是引用了实参,所以第二个函数的效率更高。
对于第一个函数,我们的交换并不能成功,原因是形参的改变并不影响实参。
而对于第二个函数,我们的交换可以成功,原因是我们的形参是实参的引用,所以形参修改,实参也跟着修改了。
总结:
引用做参数的作用:
1:减少拷贝,提高效率
2:对于输出型参数,当形参改变的时候,实参也跟着改变。
这两个函数构不构成重载:
构不构成重载是根据编译器对于两个函数参数的理解,当编译器把int &识别成int时,这两个函数就不构成重载,当编译器把int&的类型识别为引用类型时,这两个函数就构成重载。
我们进行运行检测:
- void Swap(int a, int b)
- {
- int tmp = a;
- a = b;
- b = tmp;
- }
- void Swap(int&a, int &b)
- {
- int tmp = a;
- a = b;
- b = tmp;
- }
- int main()
- {
-
- }
代码并没有报错,证明函数构成了重载。
但是这里其实构不构成重载无关紧要,因为无论构不构成重载,我们都无法调用这两个函数,因为Swap(2,3)同时适用于这两个函数,造成歧义性,所以无法调用。
- int a = 0;
- int&ra = a;
这种写法是正确的。
- const int b = 1;
- int&rb = b;
但是这种写法却不对。
错误的原因是什么?
因为b是只能读而不能写的参数,而rb是可读可写的参数,所以对应的b的权限增大了,所以导致错误。
我们写一个权限缩小的例子:
- int b = 1;
- const int&rb = b;
b是可读可写的,而ra是只读不可写的,所以由b到ra是权限的缩小,我们进行编译:
我们再写一个权限不变的例子。
- const int a = 10;
- const int&ra = a;
这两个都是只读而不可写的参数,所以由a到ra,权限是不变的。
所以,我们进行总结:
在指针或引用中,权限可以缩小,但是不可以扩大。
- void Func(int&ra)
- {
-
- }
假如我们要调用这个引用的函数用来减少拷贝来提高效率可以吗?
答:不可以,原因是当我们调用这个函数的时候,我们的传参是受限的。
- void Func(int&ra)
- {
-
- }
- int main()
- {
- int a = 10;
- const int b = 20;
- Func(a);
- Func(b);
- }
如图所示,我们调用Func(a)是可以的,但是我们无法调用Func(b),因为b是只读的参数,我们传参的时候,我们把只读的参数传给可读可写的参数。
所以我们正确的写法是这样:
- void Func(const int&ra)
- {
-
- }
- int main()
- {
- int a = 10;
- const int b = 20;
- Func(a);
- Func(b);
- }
但是,注意这种情况
- int a = 10;
- const int b = 20;
- a = b;
b是只读的,a是可读可写的,把b赋给a是不是导致权限的放大?
并不是,因为这里根本不涉及引用和指针,这里这是简单的赋值操作,传递的是参数值,所以没有任何影响。
- void Func(int a=10)
- {
-
- }
这个是普通的缺省参数。
当我们加入一个引用符号时:
这里错误的原因我们可以这样理解:
10是一个常量,我们要对一个常量引用的话,我们要保证自身要是一个常量,所以我们可以用const修饰。
- void Func(const int&a=10)
- {
-
- }
我们再举一个例子:
为什么这里会发生错误?
许多人的想法是类型不同,但是为什么下面这种写法可以呢?
这里发生了隐式类型转换,转换的方式:首先,创建一个临时变量,临时变量接收了(d的数值被转换成int类型的形式),然后再把临时变量赋给变量i。(注意:d并没有改变,只是d的数值在传给临时变量的时候被转换了)
我们可以进行实验:
如图所示,我们可以发现d被赋值给i后,其本身并没有发生改变。
这种写法其实就等价于强制类型转换。
- int main()
- {
- double d = 12.34;
- /*int&ri = d;*/
- int i = (int)d;
- return 0;
- }
无论是强制类型转换,还是隐式类型转换,或者是整型提升,亦或者是传值返回,都有临时变量的参与。
那这个时候,对应的i值是多少呢?
临时变量的一大特点就是:临时变量具有常性。
这个时候,我们就能解释为什么int&ri=d为什么报错了:
d首先要进行转换,转换的时候,媒介是临时变量,我们要把临时变量赋给ri,而临时变量具有常性,我们要对具有常性的整型进行引用,那我们本身也要是常数才行。
const int&ri = d;
所以当我们这样写的时候,就不会报错。
我们再写一串代码加强理解:
为什么这里会有错误呢?
答:因为这里是传值返回,传值返回的媒介就是临时变量,临时变量具有常性,所以我们无法用普通的引用来进行接收。
我们需要加上const。
在语法角度,我们的引用相当于给变量取一个别名,所以不会额外开辟空间,但是在底层实现的话,引用需要额外开辟空间。
我们拿引用和指针解引用进行对比:
- int main()
- {
- int a = 10, b = 20;
- int &ra = a;
- ra = 15;
-
- int*pb = &b;
- *pb = 25;
- }
第一个是通过引用把a的值修改为15.
第二个是通过指针解引用把pb的值修改为25.
我们转到汇编代码看一下:
我们虽然看不懂,但是可以发现mov和lea都出现多次,我们查一下mov和lea对应的意思:
mov:
这个很容易理解,既然我们要修改a和b的值,我们就肯定要用数据传送指令。
lea
这里就能看出问题了,在我们的印象中,指针相关的操作是需要传递地址的,引用并不需要。
这里的lea就证明了在底层实现的逻辑上来看:引用也是需要额外开辟空间的。
- int main()
- {
- int a = 10;
- auto b = a;
- }
auto的意思就是根据a的类型推导b的类型,所以b的类型也是int了。
auto在for循环中的使用:
我们现在写for循环要这么写:
- int main()
- {
- int i = 0;
- int a[5] = { 1, 2, 3, 4, 5 };
- for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
- {
- cout << a[i] << " ";
- }
- cout << endl;
- }
是不是非常复杂,我们使用auto可以这样写:
- int main()
- {
- int i = 0;
- int a[5] = { 1, 2, 3, 4, 5 };
- for (auto e : a)
- {
- cout << e << " ";
- }
- cout << endl;
- }
这串代码的意思是:我们首先调用for循环,把数组a中的内容赋给e,然后自动判断结束,自动迭代,打印出所有的数组值。