引用的概念
引用是一个变量或对象的别名。当声明引用时,用一个目标对象的名字对引用作初始化,从而让引用和目标对象建立起联系,对引用的操作就是对目标对象的操作。
好比在一个班里,有个学生叫“张小明”,同时他有个绰号叫“小猪”,那么班里的同学都明白,叫“张小明”和叫“小猪”实际上都是指同一个人。引用具有类似的概念。
需要注意的是,引用必须声明时初始化(赋值)。
引用的声明需要用到引用运算符“&”,其一般形式为:目标对象数据类型 &变量 = 变量;
即先写上目标对象的数据类型,然后是引用运算符“&”,接着写引用的名字。
#include<iostream>
using namespace std;
int main()
{
int a=10; //定义一个整型变量
int &ra =a; //声明对变量a的引用ra(目标对象数据类型 &变量 = 变量)
cout<<a<<" "<<&a<<endl; //10 0x7ff7b38a15bc
cout<<ra<<" "<<&ra<<endl; //10 0x7ff7b38a15bc
}
从运行结果可以看出,对引用的操作实际上就是对其引用的目标对象本身的操作,包括取引用的地址,得到的结果都是和其引用的目标对象的地址是一样的。
所以一旦声明了引用,对引用的所有操作实际上都是对其引用的目标对象的操作。
引用的操作
指针也是变量,所以也可以声明指针的引用,如下面程序中声明指针pa的引用rpa。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int* pa = &a; //指向a的指针,*pa=10
int& ra = a; //a的引用,ra=10
int*& rpa = pa; //pa的引用,int*表示rpa引用的类型是整数指针,*rpa=10
cout << &a << " " << &ra << endl; //0x7ff7baf805ac 0x7ff7baf805ac
cout << ra << " " << *pa << endl; //10 10
ra += 10;
cout << ra << " " << *rpa << endl; //20 20
}
下面是引用的程序示意图
- 声明ra是a的引用, 即是变量a的一个别名。
- 一旦ra同a的内存对象发生了联系,就不能改变。对a的访问就是对ra的访问,对ra的访问也是对a的访问。
- 变量a和引用ra共用同一内存空间。
需要注意的是,引用运算符与地址符使用的符号相同,尽管它们显然是彼此相关的,但它们却不一样。 - 引用运算符只在声明时使用,它放在类型名后面。(例如:int &ri = i ;)
- 其它的”&”的使用都是地址操作符。(例如: int *ip = & i ; cout << &ip ;)
同时,为提高可读性,不应在同一行上同时声明引用、指针和变量。
引用与指针的区别
通过引用和指针,都可以操作它们所指向的目标对象,但是它们在使用上有很大的差别。这种差别主要体现在:
- 指针是个变量,可以在程序中改变指针所指向的目标;
- 引用是一个声明,引用不是值,不占内存空间,在声明引用时必须给它初始化,并且引用在初始化后,不可以再关联其它的目标对象。
引用的类型
声明引用时,需要指定引用的数据类型,但是并不是所有的类型都可以被引用。
- 不能对void进行引用(因为void只是在语法上相当于一个类型,但其本质上并不是一个类型)
- 不能对数组建立引用(因为数组名是数组空间的首地址,其本身并不是一个数据类型)
- 不能声明引用的引用(因为引用本身不是一种数据类型,所以没有引用的引用)
- 有空指针,但是没有空引用(例如不能这样写:int &r = NULL)
- 可以建立指针变量的引用(如上述例子),但不能定义指向引用类型的指针变量(因为引用不是类型,是声明)
引用与函数
函数传引用是最常用的传参改变参数值方式。
从下面可以看到,函数传引用也可以改变参数的值,这跟指针的作用是一样的,但却比指针更加方面,只需要在函数虚参中使用引用变量格式即可,其他都跟普通变量一样的使用。这背后是因为引用和变量共享了同一个地址空间。
#include <iostream>
using namespace std;
void swap1(int a,int b)
{
int temp;
temp=a;
a=b;
b=temp;
}
void swap2(int &a,int &b) //传引用,只是在这里加一个引用符号,其他都一致
{
int temp;
temp=a;
a=b;
b=temp;
}
int main()
{
int n=10,m=20;
cout<<n<<" "<<m<<endl; //10 20
swap1(n,m);
cout<<n<<" "<<m<<endl; //10 20 传值不改变参数
swap2(n,m);
cout<<n<<" "<<m<<endl; //20 10 传引用改变参数
}
返回引用类型的函数
这部分需要理解如下代码的区别
#include<iostream>
using namespace std;
double temp; //全局变量
double fn1(double r) //返回值,返回的是值的备份,不能修改
{
temp = r * r * 3.14;
return temp;
}
double& fn2(double r) { //返回引用,可以修改
temp = r * r * 3.14; //也可以在这里使用 static 或者 动态内存分配 做到全局
return temp; //返回的必须是全局变量,出了函数仍在
}
int main()
{
double a = fn1(5.0); //返回值方式的内存布局(temp78.5作为临时变量赋值给a,a和temp完全独立)
cout << a << endl; //78.5
a += 10;
cout << a << " "<< temp << endl; //88.5 78.5
//double& b=fn1(5.0); //error,返回的是局部变量的引用,编译错误(a和temp完全独立,不能进行引用)
double c = fn2(5.0); //返回引用的内存布局,修改c,不影响temp(将temp78.5写进c)
cout << c << endl; //78.5
c += 10;
cout << c << " "<< temp << endl; //88.5 78.5
double& d = fn2(5.0); //返回引用的值作为引用的初始化,修改d,temp值被改变(将d作为temp78.5的别名引用)
cout << d << endl; //78.5
d += 10;
cout << d << " "<< temp << endl; //88.5 88.5
return 0;
}
关于引用与函数,上述代码已经注释得很清晰了。同时,上述代码也可以借助下面几个图进行理解。
double a = fn1(5.0);
//返回值方式的内存布局
double &b = fn1(5.0);
//返回值初始引用的内存布局,返回的是局部变量的引用,编译错误。
double c = fn2(5.0);
//返回引用的内存布局,修改c,不影响temp。
double &d = fn2(5.0); //返回引用的值作为引用的初始化,修改d,temp值被改变。
限定引用
在程序中,为保证传递的参数不被修改,可以用传递const指针或引用的方法
#include<iostream>
using namespace std;
void fun(const int &a) //传参加const,保证不改变参数的值,也可以传const指针
{
a += 10;
}
int main()
{
int n=10;
fun(n); //error,fun函数的参数是const,不能修改
cout<<n<<endl;
return 0;
}
既然都是为了保证函数传参不改变参数值,那为什么不直接就使用函数传值呢?这是因为函数传值在计算机中相当于拷贝备份(例如上面例子的fun1),使用const引用可以减少拷贝,减少内存占用。
函数调用作为左值
使用引用的方法,可以将一个函数调用作为左值
我们前面讲过,函数返回引用,是有空间的,所以直接对返回的函数进行赋值。
如上述fn2的例子中,可以使用fn2(5.0) += 10
,讲返回的引用作为左值,这个时候可以看到输出的temp将为88.5(78.5+10)。