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

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],这是个数组引用,本质上是个引用,所以可以代表数组整体。

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







  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值