【c++笔记四】深入浅出的谈谈:引用(&)

来源:互联网 发布:ios 模仿淘宝地址选择 编辑:程序博客网 时间:2024/06/07 04:47
2015年1月25日 周日阴雨
昨天因为评测了win10就没有更新笔记,今天刚好周日时间比较多,就好好来说下“引用”吧。
个人觉得引用还是有很多知识的,也有很多值得注意的地方。
——————————————————————————华丽的分割线——————————————————————————————————

一、什么是引用?

引用,即别名
什么是别名?举例:关云长、关二爷都是关羽的别名;9527、华安都是唐伯虎的别名等等。
虽然是别名,但最终都是同一个东西。如上左图,b和c都是a的引用,也即是别名(同右图)。
形如:int& b = a,c = a;b 和 c就是a的引用。也就是在变量名前加上小麻花一样的符号“&”。

二、引用的使用

1.引用的定义:
格式:数据类型 & 对象名 = 目标对象;
如:int & a = 100; const string & c = "hello world";
2.引用必须初始化
int& b;   // error!!!千万不要这么写
int& b = 20; 
3.引用不能为
int& a = NULL; // error!!!
4.引用不能更换目标!
int& a = 10;

引用就是一个对象的别名,所以在内存中只有一份,所以引用是不占内存的。不信?看程序:
#include <iostream>using namespace std;int main(){    int a = 2;    int& b = a;    cout<<&a<<" "<<&b<<endl;//打印a和b的地址    b = 100;<span style="white-space:pre"></span>    //通过b改变a的值    cout<<a<<" "<<b<<endl;  //打印a和b的值    return 0;}

由程序我们很好的证明了这一点:引用只是一个变量的别名,本质上是同一个东西,是没有自己的内存空间的。

三、引用类型的参数

1.引用型参数

让你写一个最简单的程序,交换a和b的程序。你肯定马上就能写出来,那让你用调用子函数的方法交换呢?你说,简单,看程序:
#include <iostream>using namespace std;void swap1(int a,int b){    a = a ^ b;    b = a ^ b;    a = a ^ b;}int main(){    int a = 100,b = 20;    swap1(a,b);    cout<<a<<" "<<b<<endl;    return 0;}

结果一运行就傻眼了,怎么没有交换呢?如果你有这样的疑问,说明你对函数还不够理解,你还不能很好的区分实参和形参的区别(建议回去好好补补课本吧)。懂的人应该知道,swap1接受的两个形参a和b的值的确是交换了。可是形参只是实参的副本,并不是实参本身,就算他们的值发生了改变也不会影响实参。
可我们上面说的引用就不一样了。如果我们使用引用类型的参数,那我们操作的形参,实际就是在操作实参!所以这里引入一个概念:引用型参数。

我来帮大家写一下吧:
#include <iostream>using namespace std;void swap1(int& a,int& b){    a = a ^ b;    b = a ^ b;    a = a ^ b;}int main(){    int a = 100,b = 20;    swap1(a,b);    cout<<a<<" "<<b<<endl;    return 0;}
我们注意看swap1这个子函数。他的两个参数都是引用类型的参数。仅仅只是在形参前加上了&号就达到了我们的预期效果


在函数中改变实参的值。这只是引用型参数的一个好处,他还有一个好处:避免对象复制带来的开销!看程序:
#include <iostream>using namespace std;void acc(int a,int& b){    cout<<&a<<" "<<&b<<endl;}int main(){    int a = 100,b = 200;    cout<<&a<<" "<<&b<<endl;    acc(a,b);    return 0;}

子函数中的a是形参,是主函数中a的一个副本,所以两个变量的地址不一样。既然是副本,就会发生对象的复制,开辟新的内存空间并且赋值,由此可见开销如何。但是子函数中的b是引用型的形参,实际就是主函数中b的别名,本质上是同一个东西,所以他们的地址一样。说明程序直接拿b过来用,这就省去了上面所说的复制带来的开销啦。

2.常引用型参数

但是我们还得提一个概念:常引用型参数。(const修饰的引用型参数)
格式:const  类型& 变量名= 值;
有时候你可能想把实参的值传入函数但不改变实参的值,又能避免复制带来的开销,你就可以选用常引用型参数。例如:
#include <iostream>using namespace std;void acc(int& a,const int& b){    a = a+b;//  b = 100;//error,对b的修改是非法的}int main(){    int a = 100,b = 200;    acc(a,b);    cout<<a<<" "<<b<<endl;    return 0;}

acc函数中的形参b就是:常引用型参数。既可以不改变b的值,也可以省去复制带来的开销。

当然,常引用型参数的第二个作用是:接受常量型的实参。这也是一种兼容性的考虑,我们来举例说明:
#include <iostream>using namespace std;void acc(int& a){}int main(){    const int a = 100;    acc(a);    return 0;}

acc函数的形参a只是一个普通的引用,但是主函数中的a却是const int类型的,编译器不干了,它说:你不能引用const int的实参!这里我们只要把形参改为:const int & a编译器就会放过你了。在实际编程中,你可能会接受的参数const类型的变量,使用常引用型参数就既可以接收普通变量和常属性的变量,这其实是出于一种兼容性的考虑。

四、引用类型的返回值

在说这个知识点之前,我们先来回顾一下c语言的函数返回值类型能干嘛:c语言中非指针类型的函数返回值只能作为右值,不能作为左值
不知道大家了不了解左值和右值,我还是简单的讲下吧。说简单点,能放在等号左边的就叫左值,只能放在等号右边的就叫右值。
比如:我们可以写a=123;但是不能写123=a。这里,123就是一个右值,a就是一个左值。一般的,不能改变的都是右值,一般的变量都能做左值。我们还是继续体会第一句话吧:

我们可以看见只有13行报错,因为add函数返回的是int类型而不是指针类型,所以编译器报错了,它说add(2,3)不是一个左值。
但fun函数返回的是int*类型的指针。这就证明了那就话:c语言中非指针类型的函数返回值只能作为右值,不能作为左值

我们回到正题,先给出一个定论吧:c++ 中如果一个函数返回引用类型,则代表这个函数的返回值可以作为左值
我先来考你一个题目吧,写出下列函数的输出结果:
#include <iostream>using namespace std;int& imax(int& a,int& b){    return a>b?a:b;}int main(){    int x = 1,y = 2;    imax(x,y) = 100;    cout<<x<<" "<<y<<endl;    return 0;}
我们来看看你的答案对不对吧:

这段程序的功能就是把两个数中大的那个数字的值变成100。因为,imax函数返回的是引用型形参a和b中值最大的那个变量的引用。上面那句话说了,引用型的返回值可以作为左值,所以将这个值修改为100。懂了不?

但是,一定要注意这个但是!!!并不是所有类型都能作为引用型返回值的!!!!
为什么呢?因为引用型的返回值,从函数中返回的目标的引用,一定要保证在函数返回以后,该引用的目标依然有效。换句话说,就是你返回的这个东西,要在函数结束之后仍然存在(不会因为生命周期结束而在内存中被释放了)。
因此,千万不要返回局部变量的引用
因为,局部变量在函数结束之后因为生命周期结束,而被内存释放了,内存中已经没有这个变量了。在前面我们就说过了引用本身没有内存,不能为空。局部变量本身都被消灭了,这个引用也跟着没有了。
那么,我们能返回哪些类型的变量的引用呢?
  1. 全局、静态、成员变量的引用 
  2. 在堆中动态创建的对象的引用
  3. 引用型参数本身 
我们把他们三个都写出来你就明白了:
#include <iostream>using namespace std;int g_int = 1024;   //全局变量struct Count{    int num;    int& add(){        return ++num;   //返回成员变量的引用    }};          //结构体对象int& g_fun(){    return g_int;   //返回全局变量的引用}int& s_fun(){    static int a = 2048;    return a;   //返回静态变量的引用}int& n_fun(){    return * new int(512);}int& r_fun(int& num){    return num;}int main(){    int& a = g_fun();   //接受返回的全局变量的引用    cout<<a<<endl;    int& b = s_fun();   //接受返回的静态变量的引用    cout<<b<<endl;    int& c = n_fun();   //接受返回的动态建立的变量的引用    cout<<c<<endl;    int tmp = 256;    int& d = r_fun(tmp);//接受返回的引用变量本身的引用    Count cn;    cn.num = 0;    ++cn.add();    cout<<cn.num<<endl;    return 0;}

还有不懂的,欢迎评论留言,我尽量解答。

五、引用与指针:

1.在实现层面,引用就是指针

引用之所以拥有这么强大的功能,都是因为引用是通过指针实现的。
你可以自己想一想,引用是不是一种 可以修改所指向的值 但是不能修改所指对象 的指针?其实我们可以运用 * 和 const,模拟一下引用的实现。

你能说出const int* 和 int* const的 区别吗?
你还天真的以为他们是一样的?那你就错了。看程序:

我们可以观察一下编译器报错的位置:分别是第8行和第12行。
编译器说,*a这个指针是只读(read-only)的,b这个变量是只读的。
仔细观察一下:对a的操作,第8行是改变它所指向的值(报错),第9行是改变它的指向(没报错)。说明如果const在*之前的话,我们不可以改变指针所指向的变量的值但是能改变指针的指向。
对b的操作,第11行是改变它所指向的值(通过),第12行是改变它的指向(报错)。说明如果const在*之后的话,可以改变指针指向的值但是不能改变指针的指向。是不是很像我们所说的引用?
其实,引用就类似于Type* const
PS:学了编译原理的话我们知道,最右推导是规范推导,所以编译器判别一个变量的类型的时候是从右往左看到的。
比如:const int* a:编译器首先知道这个变量叫做a,然后读到”*“说明这是个指针,再读到”int“说明这是个整型的指针,最后读到”const“就下结论:这是一个常量型的指针,到头来还是个指针,只不过指向的是常量。所以地址可以改变,指向的值不能改变。
又如:int* const a:编译器首先知道这个变量叫做a,然后读到”const“知道这个变量是个常量,再读到”*“说明这是个指针型的常量,最后读到”int“就知道了,a是一个指针型的常量,说到底这是一个常量。所以这个指针的值(指针的地址,即指针的指向)不能变,但是指针所指向的值随你便。

2.在语言层面,引用不是实体类型,因此与指针存在明显的差别

这里我们就要谈谈引用和指针的区别了:
(1)指针可以不初始化,其目标可在初始化后随意变更 (除非是指针常量),而引用必须初始化,且一旦初始化就无法变更其目标 
这个我们在最开始就强调了,就不在过多的解释了。

(2)存在空指针,不存在空引用
最开始我们也说明了这一点。

  (3)存在指向指针的指针,不存在引用引用的引用。
看看程序你就懂了:


(4)存在引用指针的引用,不存在指向引用的指针 
依然来看程序:
编译器说了:不能定义指向int& 的指针。

(5)存在指针数组,不存在引用数组,但存在数组引用。
一切代码说了算!
编译器又说了,你定义了引用数组是不对滴!
关于数组引用,我想多说几句,因为这个很实用的哦。
比如,给你一个数组,怎么求数组的大小呢?
你肯定会想到这么做:sizeof arr / sizeof arr[0]。当然这么做没错,但是把数组传到子函数中能这么做吗?且看代码:

为什么在主函数里面显示的大小是3,在子函数里面显示的就是1呢?那是因为你在主函数定义的,arr数组名代表的是数组整体,而你把arr作为参数传到子函数中之后arr数组名仅仅代表数组的首地址。那我们如何在子函数中也能求数组大小呢?这就需要用到数组引用,把数组作为整体传入到子函数中去。且看代码:
#include <iostream>using namespace std;void ssize(int (&arr)[3]){    int len = sizeof arr / sizeof arr[0];    cout<<len<<endl;}int main(){    int arr[3] = {1,2,3};    int len = sizeof arr / sizeof arr[0];    cout<<len<<endl;    ssize(arr);    return 0;}

我们将sszie子函数中的形参改为int (&arr)[3],这是个数组引用,本质上是个引用,所以可以代表数组整体。

————————————————————————————————华丽的分割线———————————————————————————————————
不知道看完这篇之后你有什么收获呢?还有什么疑问呢?都欢迎评论给我留言。
如有不正确的地方还望各位不吝赐教啊!
引用还是很实用的,以后编程会经常用到,所以掌握它是很有必要的。







1 0
原创粉丝点击