目录:
1. 什么是引用?
在理解引用概念前,先来说变量名。 变量名实质就是一段连续内存空间的别名。那一段连续的内存空间只能取一个别名吗? 显然不是,那就产生引用的概念。
在C++中,引用是一个已定义变量的别名。其语法是:
类型 & 引用名 = 目标变量名;
如:
void test0() {
int a = 1;
int & ref1 = a;
int & ref2;
}
在使用引用的过程中,要注意以下几点:
- &在这里不再是取地址符号,而是引用符号,相当于&有了第二种用法
- 引用的类型必须和其绑定的变量的类型相同
- 声明引用的同时,必须对引用进行初始化;否则编译时报错
- 一旦绑定到某个变量之后,就不会再改变其指向
- 引用在定义的时候必须要初始化,必须要绑定到一个实体,本身是不能独立存在的
2. 引用的本质
C++中的引用本质上是一种被限制的指针。类似于线性表和栈的关系,栈是被限制的线性表,底层实现相同,只不过逻辑上的用法不同而已。(引用底层实现是指针,* const
==> 指针常量)
由于引用是被限制的指针,所以引用是占据内存的,占据的大小就是一个指针的大小。有很多的说法,都说引用不会占据存储空间,其只是一个变量的别名,但这种说法并不准确。引用变量会占据存储空间,存放的是一个地址,但是编译器阻止对它本身的任何访问,从一而终总是指向初始的目标单元。在汇编里, 引用的本质就是“间接寻址”。
3. 引用作为函数参数
在没有引用之前,如果我们想通过形参改变实参的值,只有使用指针才能到达目的。但使用指针的过程中,不好操作,很容易犯错。 而引用既然可以作为其他变量的别人而存在,那在很多场合下就可以用引用代替指针,因而也具有更好的可读性和实用性。这就是引用存在的意义。
一个经典的例子就是交换两个变量的值。
//用指针作为参数
void swap(int * pa, int * pb)
{
int temp = *pa;
*pa = *pb;
*pb = temp;
}
//引用作为参数
void swap(int & x, int & y)
{
int temp = x;
x = y;
y = temp;
}
参数传递的方式除了上面的指针传递和引用传递两种外,还有值传递。
采用 值传递 时,系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量,即形参变量只是实参变量的副本而已;如果函数传递的是类对象,系统还会调用类中的拷贝构造函数来构造形参对象,假如对象占据的存储空间比较大,那就很不划算了。这种情况下,强烈建议使用引用作为函数的形参,这样会大大提高函数的时空效率。
当用 引用 作为函数的参数时,其效果和用指针作为函数参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或对象的一个别名来使用,也就是说此时函数中对形参的各种操作实际上是对实参本身进行操作,而非简单的将实参变量或对象的值拷贝给形参。
使用 指针 作为函数的形参虽然达到的效果和使用引用一样,但当调用函数时仍需要为形参指针变量在内存中分配空间,而引用则不需要这样,故在C++中推荐使用引用而非指针作为函数的参数
引用的优势:直观,不需要参数开辟额外空间,不需要进行复制,提高程序执行效率
4. 引用作为函数的返回值
语法:
类型 & 函数名(形参列表)
{ 函数体 }
当以引用作为函数的返回值时,返回的变量其生命周期一定是要大于函数的生命周期 的,即当函数执行完毕时,返回的变量还存在。
int gNumber; //全局变量
int func1() // 当函数返回时,会对temp进行复制
{
temp = 100;
return temp;
}
int & func2() //当函数返回时,不会对temp进行复制,因为返回的是引用
{
temp = 1000;
return temp;
}
当引用作为函数的返回值时,必须遵守以下规则:
-
不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。(普通函数直接返回引用没有意义)
-
不能在函数内部返回new分配的堆空间变量的引用。如果返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么该引用所在的空间就无 法释放,会造成内存泄漏。
int & func3()
{
int number = 1;
return number;
}
int & func4()
{
int * pint = new int(1);
return *pint;
}
void test()
{
int a = 2, b = 4;
int c = a + func4() + b;//内存泄漏
}
例:
#include <iostream>
using std::endl;
using std::cout;
int val = 10;
int & func()
{
int val2 = 20;
//return val2; //error val2的生命周期不大于func的生命周期
return val;
}
//返回堆空间的引用,可能会造成内存泄漏,
//没有内存回收的机制
int &getHeapData()
{
int *pInt = new int(10);
return *pInt; //error
}
int arr[10] = {1, 2, 3, 4, 5};
int &getIndex(int idx)
{
//暂时不考虑越界问题
return arr[idx];//没有复制操作
}
//因为对arr[0]的返回值是引用,修改值等于修改arr数组里的值,arr也发生改变
void func2()
{
cout << "getIndex(0) = " << getIndex(0) << endl;
getIndex(0) = 10;
cout << "getIndex(0) = " << getIndex(0) << endl;
cout << "arr[0] = " << arr[0] << endl;
}
void func3()
{
int val3 = 3, val4 = 4;
int val5 = val3 + val4 + getHeapData();//内存泄漏,直接修改了本身
cout << "val5 = " << val5 << endl;
//正确接收getHeapData的返回引用值,打印并回收内存
int & ref = getHeapData();
cout << "ref = " << ref << endl;
delete &ref;
}
int main()
{
func2();
cout << endl;
func3();
return 0;
}
5. 引用总结:
- 在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
- 用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
- 引用与指针的区别是,指针通过某个指针变量指向一个变量后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。