什么是引用(reference)
引用不是新定义一个变量,而是给已存在对象取了一个别名,从语言逻辑角度看,引用不占用额外的内存空间,而与被引用的对象共用同一块内存空间。使用引用时,需要注意以下几点:
- 引用在定义时必须初始化;
- 一个变量可以有多个引用;
- C++中的引用一旦初始化便不能转移;
- 在语法逻辑角度,引用不占用额外的内存空间,只是某个对象的别名。
引用的使用场景(意义)
做参数
输出型参数
此时形参是实参的一个别名,形参的改变会影响实参,故可以用引用做输出型参数。相比指针,引用做输出型参数显得更加明了和方便。
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
提高效率
传参时,使用引用可以避免参数的压栈过程,减少拷贝,以提高效率。当面对大对象和深拷贝类对象时,引用传参的效率提升非常明显。
做返回值
提高效率
由于函数栈帧会销毁,当函数返回一个值时,总是会先将返回值拷贝到一个临时变量中再返回。若返回返回值的引用,则可以避免该次拷贝而而直接返回该值以提高效率。
获取返回值
返回堆区或静态区的变量时,直接返回变量的引用不仅可以避免临时变量拷贝的开销,而且返回后可以直接对返回值进行修改,以减去一些其他冗余的操作。
struct SeqList
{
int data[100];
int sz;
};
int& SeqAt(SeqList& s, int pos)
{
return s.data[pos];
}
int main()
{
SeqList sq;
//直接对返回值进行修改
SeqAt(sq, 0) = 50;
cout << sq.data[0] << endl;
SeqAt(sq, 0) += 30;
cout << sq.data[0] << endl;
return 0;
}
用引用做返回值时需要注意的是,如果返回时,出了函数作用域返回对象的内存空间还未释放,则可以使用引用返回,如果内存已经释放了,则不能使用引用返回,此时返回的引用对象已被释放,内容不确定,属于非法访问。
关于常引用的问题
关于常引用的问题,涉及到引用的权限问题,引用的权限不能放大,只能平移或缩小,例如,不能以一个变量作为常量的引用,这是因为常量无法被修改,而变量可以被修改,也就是说权限被放大了,这是不允许的。
关于常引用还有会牵涉到其他细节:
- 类型转换会产生一个临时变量,临时变量具有常属性,只能用常量进行引用;
double d = 3.14;
//int& i = d; // 权限放大
const int& i = d; //权限平移
- 上文说到,函数值返回时,会先将返回值存储到一个临时变量中,临时变量具有常属性,所以返回值只能用常量进行引用。
int Add(int x, int y)
{
return x + y;
}
int main()
{
//int& rret = Add(12, 10); //权限放大
const int& rret = Add(12, 10); //权限平移
return 0;
}
引用的底层逻辑
上文说到,引用在语法层面上不占用独立空间,与被引用对象占用同一块空间,但通过观察底层汇编代码,可以发现引用的底层实现逻辑与指针相同,也就是说,从底层汇编角度看,引用是用类似指针的方式实现的。
虽然引用的底层是指针,但是在 C++ 中使用最多的依然是引用,这是因为相对于指针,引用往往更加明了和方便。
引用和指针的区别
- 在语法层面上,引用不具有独立空间,与被引用对象占用同一块空间;指针存储一个对象的地址
- 引用在定义时必须初始化;指针没有要求
- 引用在初始化引用一个实体后就不能引用其它实体;指针可以随时改变指向而指向一个其它实体
- 没有空引用;存在空指针
- 在
sizeof
中的意义不同:计算引用的结果为被引用对象类型的大小;计算指针的结果始终为地址空间所占字节数(4字节或8字节) - 引用与常数运算,即被引用的对象进行运算;指针与常数进行(加减)运算,进行的是指针偏移(移动)
- 有多级指针,但是没有多级引用
- 访问实体的方式不同:指针需要显式解引用;引用编译器自行处理
- 引用使用起来比指针更安全