目录
引用:
接着我们上节课讲的一些引用的基础知识:
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)同时适用于这两个函数,造成歧义性,所以无法调用。
const引用:
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就证明了在底层实现的逻辑上来看:引用也是需要额外开辟空间的。
引用的总结:
c++11小语法
auto
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,然后自动判断结束,自动迭代,打印出所有的数组值。