0.引用是变量的别名
在 C 语言中,使用指针(Pointer)可以间接获取、修改某个变量的值。
在 C++ 中,使用引用(Reference)可以起到跟指针类似的功能。
引用相当于是变量的别名,对引用的操作与对变量直接操作完全一样,因为引用不重新生成洗的呢内存地址,引用与指向的变量内存地址是同一个。一般用 & 符号表示,&在此不是求地址运算,而是起标识作用,基本数据类型、枚举、结构体、类、指针、数组等都可以有引用。在使用上跟普通变量一样,引用也当成普通变量使用,内部实现上引用是对指针的封装, int &p 相当于 int* const p,常引用 const int &p 相当于 const int* const p
引用必须绑定到变量上去,不能绑定到常量和表达式上去,引用指向的类型要相同。
对引用做计算,就是对引用所指向的变量做计算,反过来对指向变量做修改也会影响引用。
在定义的时候引用就必须初始化,一旦指向了某个变量,就不可以再改变引用指向关系,“从一而终”。
1.左值引用
左值引用只能绑定到左值上,不能绑定到右值上。
左值引用不能绑定到临时变量上,因为临时变量被系统当作右值,像常量,表达式,函数返回值都是右值,只能有const引用,不能直接引用。
所有变量都是左值,因为它们是有地址的。
任何函数里边的形参都是左值。
根据《C++ Primer plus》分为了两类:
- 右值类,所谓右值即只能出现在
=
右边的值,他们不能被赋值,不能被取地址
典型的如,常量(" "
字符串除外,因为他本质上是指针变量)、表达式、非引用返回值的函数(引用返回值函数最后说)等。 - 类型转化类
如上一节中先将" "
字符串转为string对象,构造了一个string临时变量
需要注意,在执行上述引用时,都会在栈帧中分配空间来存放临时变量,使之不再临时,而引用就是他们的名字,这样就让临时变量和普通变量一样,有自己的名字和内存空间,可以通过引用来赋值和取址。
#include <iostream>
using namespace std;int main()
{
int& ref1 = 20; // 错误,左值引用不能绑定到右值上int a = 10, b = 20;
int& ref2 = a; // 正确,左值引用能绑定到左值上
int& ref3 = a + b; // 错误,左值引用不能绑定到右值上
return 0;
}
返回左值引用的函数、赋值运算符、下标运算符、解引用运算符、前置递增运算符、前置递减运算符等,返回的都是左值,可以将左值引用绑定到这类表达式的结果上。
#include <iostream>
using namespace std;int main()
{
int i = 10;
int& ref = ++i; // 左值引用可以绑定到左值上,ref就变成了i的别名
i += 5;
cout << i << endl; // 16
cout << ref << endl; // 16return 0;
}
2.const引用
引用可以被 const 修饰,这样就无法通过引用修改数据了,称为常引用。
const 类型 &引用名=目标变量名;
const 必须写在 & 符号的左边,才能算是常引用。
引用 int &p 相当于 int* const p,常引用 const int &p 相当于 const int* const p。
#include <iostream>
using namespace std;int main() {
int height = 20;
int age = 10;// p1不能修改指向,但是可以利用p1间接修改所指向的变量
int* const p1 = &age;
//p1 = &height; // 报错
*p1 = 30;
cout << age << endl; // 30// ref1不能修改指向,但是可以通过ref1间接修改所指向的变量
int & const ref1 = age; //错误,不是常引用
ref1 = 40;
cout << age << endl; // 40// p2可以修改指向,但是不可以利用p2间接修改所指向的变量
int const* p2 = &age;
p2 = &height;
//*p2 = 30; // 报错// ref2不能修改指向,也不可以通过ref2间接修改所指向的变量
int const &ref2 = age; // 常引用
//ref2 = 40; // 报错return 0;
}
2.1 const引用可以指向临时数据
举例1:const引用指向常量
#include <iostream>
using namespace std;int main()
{
const int& ref = 30; //正确return 0;
}
举例2:const引用指向表达式
#include <iostream>
using namespace std;int main()
{
int a = 1;
int b = 2;
const int& ref = a + b; //正确return 0;
}
举例3:const引用指向函数返回值
#include <iostream>
using namespace std;int func()
{
return 8;
}int main()
{
const int& ref = func(); //正确return 0;
}
将常引用绑定到临时数据时,编译器采取了一种妥协机制:编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。注意,临时变量也是变量,所有的变量都会被分配内存。
为什么编译器为常引用创建临时变量是合理的,而为普通引用创建临时变量就不合理呢?
将引用绑定到一份数据后,就可以通过引用对这份数据进行操作了,包括读取和写入(修改);尤其是写入操作,会改变数据的值。而临时数据往往无法寻址,是不能写入的,即使为临时数据创建了一个临时变量,那么修改的也仅仅是临时变量里面的数据,不会影响原来的数据,这样就使得引用所绑定到的数据和原来的数据不能同步更新,最终产生了两份不同的数据,失去了引用的意义。
const 引用和普通引用不一样,我们只能通过 const 引用读取数据的值,而不能修改它的值,所以不用考虑同步更新的问题,也不会产生两份不同的数据,为 const 引用创建临时变量反而会使得引用更加灵活和通用。
以上节的 isOdd() 函数为例:
bool isOdd(const int &n){ //改为常引用
if(n/2 == 0){
return false;
}else{
return true;
}
}
由于在函数体中不会修改 n 的值,所以可以用 const 限制 n,这样一来,下面的函数调用就都是正确的了:
int a = 100;
isOdd(a); //正确
isOdd(a + 9); //正确
isOdd(27); //正确
isOdd(23 + 55); //正确
对于第 2 行代码,编译器不会创建临时变量,会直接绑定到变量 a;对于第 3~5 行代码,编译器会创建临时变量来存储临时数据。也就是说,编译器只有在必要时才会创建临时变量。
const 引用与类型转换
「类型严格一致」是为了防止发生让人匪夷所思的操作,但是这条规则仅仅适用于普通引用,当对引用添加 const 限定后,情况就又发生了变化,编译器允许引用绑定到类型不一致的数据。请看下面的代码:
int n = 100;
int &r1 = n; //正确
const float &r2 = n; //正确
char c = '@';
char &r3 = c; //正确
const int &r4 = c; //正确
当引用的类型和数据的类型不一致时,如果它们的类型是相近的,并且遵守「数据类型的自动转换」规则,那么编译器就会创建一个临时变量,并将数据赋值给这个临时变量(这时候会发生自动类型转换),然后再将引用绑定到这个临时的变量,这与「将 const 引用绑定到临时数据时」采用的方案是一样的。
注意,临时变量的类型和引用的类型是一样的,在将数据赋值给临时变量时会发生自动类型转换。请看下面的代码:
float f = 12.45;
const int &r = f;
printf("%d", r);
该代码的输出结果为 12,说明临时变量和引用的类型都是 int(严格来说引用的类型是 int &),并没有变为 float。
当引用的类型和数据的类型不遵守「数据类型的自动转换」规则,那么编译器将报错,绑定失败,例如:
char *str = "http://c.biancheng.net";
const int &r = str;
char *
和int
两种类型没有关系,不能自动转换,这种引用就是错误的。
总结起来说,给引用添加 const 限定后,不但可以将引用绑定到临时数据,还可以将引用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。
3.引用作为函数的返回值
用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本。
注意下面几点:
1)不能返回局部变量的引用。如上面的例子,如果temp是局部变量,那么它会在函数返回后被销毁,此时对temp的引用就会成为“无所指”的引用,程序会进入未知状态。
2)不能返回函数内部通过new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(有new分配)无法释放的情况(由于没有具体的变量名,故无法用delete手动释放该内存),从而造成内存泄漏。
3)当返回类成员的引用时,最好是const引用。这样可以避免在无意的情况下破坏该类的成员。
4)可以用函数返回的引用作为赋值表达式中的左值
示例代码如下:
#include<iostream>
using namespace std;
int value[10];
int error=-1;
int &func(int n){
if(n>=0&&n<=9)
return value[n];//返回的引用所绑定的变量一定是全局变量,不能是函数中定义的局部变量
else
return error;
}
int main(){
func(0)=10;
func(4)=12;
cout<<value[0]<<endl; //10
cout<<value[4]<<endl; //12
return 0;
}
4.引用作为函数参数
当引用作为函数参数时,有时候很容易给它传递临时数据。下面的 isOdd() 函数用来判断一个数是否是奇数:
bool isOdd(int &n){
if(n%2 == 0){
return false;
}else{
return true;
}
}
int main(){
int a = 100;
isOdd(a); //正确
isOdd(a + 9); //错误
isOdd(27); //错误
isOdd(23 + 55); //错误
return 0;
}
isOdd() 函数用来判断一个数是否为奇数,函数参数是引用类型,只能传递变量,不能传递常量或者表达式。但用来判断奇数的函数不能接受一个数字又让人感觉很奇怪,所以类似这样的函数应该坚持使用值传递,而不是引用传递。
下面是更改后的代码:
bool isOdd(int n){ //改为值传递
if(n%2 == 0){
return false;
}else{
return true;
}
}
int main(){
int a = 100;
isOdd(a); //正确
isOdd(a + 9); //正确
isOdd(27); //正确
isOdd(23 + 55); //正确
return 0;
}
5.结构体的引用
#include <iostream>
using namespace std;
struct Date {
int year;
int month;
int day;
};
int main() {
Date d = { 2022, 1, 25 };
Date& ref = d;
ref.year = 2023;
cout << d.year << endl; // 2023
return 0;
}
6.指针的引用
指针是一个存放地址的变量,而指针引用指的是这个指针变量的引用。
#include <iostream>
using namespace std;int main() {
int age = 10;
int *p = &age;
int *&ref = p; //int *是指针类型
*ref = 30;
cout << age << endl; // 30int height = 40;
ref = &height;
cout << *ref << endl; // 40return 0;
}
在 C++ 中,如果想改变指针所指的对象(即想要改变指针里面存的地址),就要使用指针引用。
举例1:指针
#include <iostream>
using namespace std;struct Point
{
int x;
int y;
};void change1(Point* pp)
{
pp = new Point;
pp->x = 4;
}int main()
{
Point* p = new Point;
p->x = 10;
cout << "指针前:" << p->x << endl;
change1(p);
cout << "指针后:" << p->x << endl;return 0;
}
输出结果:
举例2:指针的引用
#include <iostream>
using namespace std;struct Point
{
int x;
int y;
};void change2(Point* &pp)
{
pp = new Point;
pp->x = 4;
}int main()
{
Point* p = new Point;
p->x = 10;
cout << "指针引用前:" << p->x << endl;
change2(p);
cout << "指针引用后:" << p->x << endl;return 0;
}
输出结果:
7.数组的引用
举例1:区分指针数组、数组指针、数组的引用
#include <iostream>
using namespace std;int main() {
int array[3] = {1, 2, 3};
// 指针数组,数组里面可以存放3个int*
int *arr1[3] = {array, array, array};
// 数组指针,用于指向数组的指针
int (*arr2)[3] = &array;// 数组的引用
int (&ref)[3] = array;
ref[0] = 10;
cout << array[0] << endl; // 10return 0;
}
举例2:引用数组的两种格式
#include <iostream>
using namespace std;int main() {
// 数组名arr其实是数组的地址,也是数组首元素的地址
// 数组名arr可以看做是指向数组首元素的指针(int *)
int arr[] = {1, 2, 3};cout << *(arr + 2) << endl; // 3
// 等价于arr[2]
// 数组的引用格式1
int (&ref)[3] = arr;
cout << ref[2] << endl; // 3// 数组的引用格式2
int * const &refArr = arr;
cout << refArr[2] << endl; // 3return 0;
}
8.用引用实现多态
在C++中,引用是除了指针外另一个可以产生多态效果的手段。也就是说一个基类的引用可以用来绑定其派生类的实例
class Father;//基类(父类) class Son:public Father{.....}//Son是Father的派生类 Son son;//son是类Son的一个实例 Father &ptr=son;//用派生类的对象初始化基类对象的使用
特别注意:
ptr只能用来访问派生类对象中从基类继承下来的成员。如果基类(类Father)中定义的有虚函数,那么就可以通过在派生类(类Son)中重写这个虚函数来实现类的多态。
9.引用和指针的其他区别
1) 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据;而指针没有这个限制,指针在定义时不必赋值,以后也能指向任意数据。
2) 可以有 const 指针,但是没有 const 引用。也就是说,引用变量不能定义为下面的形式:
- int a = 20;
- int & const r = a;
因为 r 本来就不能改变指向,加上 const 是多此一举。
3) 指针可以有多级,但是引用只能有一级,例如,int **p
是合法的,而int &&r
是不合法的。如果希望定义一个引用变量来指代另外一个引用变量,那么也只需要加一个&
,如下所示:
- int a = 10;
- int &r = a;
- int &rr = r;
4) 指针和引用的自增(++)自减(--)运算意义不一样。对指针使用 ++ 表示指向下一份数据,对引用使用 ++ 表示它所指代的数据本身加 1;自减(--)也是类似的道理。
参考:
【深入理解C++】左值引用、const引用、右值引用、std::move()函数_std::tie函数的形参为左值引用吗_早睡身体好呀的博客-优快云博客
【深入理解C++】引用_c++引用_早睡身体好呀的博客-优快云博客
引用进阶可以看下面两篇: