Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

My Blog

从此烟雨落金城,一人撑伞两人行。

C++ 引用

引用简介

比指针更加便捷的传递聚合类型数据的方式,那就是引用(Reference)

引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。

引用的定义方式类似于指针,只是用&取代了*,语法格式为:

1
type &name = data;

引用在定义时需要添加&,在使用时不能添加&,使用时添加&表示取地址。

不希望通过引用来修改原始的数据,那么可以在定义时添加 const 限制,形式为:

1
2
const type &name = value;
type const &name = value;

将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有“在函数内部影响函数外部数据”的效果。

引用除了可以作为函数形参,还可以作为函数返回值。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;
int &plus10(int &r) {
r += 10;
return r;
}
int main() {
int num1 = 10;
int num2 = plus10(num1);
cout << num1 << " " << num2 << endl;
return 0;
}

不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了。

引用与指针

引用只是对指针进行了简单的封装,它的底层依然是通过指针实现的,引用占用的内存和指针占用的内存长度一样,在 32 位环境下是 4 个字节,在 64 位环境下是 8 个字节,之所以不能获取引用的地址,是因为编译器进行了内部转换。

区别:

  • 引用必须在定义时初始化,并且以后也要从一而终,不能再指向其他数据
  • 可以有 const 指针,但是没有 const 引用
  • 指针可以有多级,但是引用只能有一级
  • 指针和引用的自增(++)自减(–)运算意义不一样

引用与临时数据

在内存中的,例如定义的变量、创建的对象、字符串常量、函数形参、函数体本身、newmalloc()分配的内存等,这些内容都可以用&来获取地址,进而用指针指向它们。

临时数据,例如表达式的结果、函数的返回值等,它们可能会放在内存中,也可能会放在寄存器中。一旦它们被放到了寄存器中,就没法用&获取它们的地址了,也就没法用指针指向它们了。

只能将较小的临时数据放在寄存器中。int、double、bool、char 等基本类型的数据往往不超过 8 个字节,用一两个寄存器就能存储,所以这些类型的临时数据通常会放到寄存器中;而对象、结构体变量是自定义类型的数据,大小不可预测,所以这些类型的临时数据通常会放到内存中。

常量表达式由于不包含变量,没有不稳定因素,所以在编译阶段就能求值。编译器不会分配单独的内存来存储常量表达式的值,而是将常量表达式的值和代码合并到一起,放到虚拟地址空间中的代码区。从汇编的角度看,常量表达式的值就是一个立即数,会被“硬编码”到指令中,不能寻址。

常量表达式的值虽然在内存中,但是没有办法寻址,所以也不能使用&来获取它的地址,更不能用指针指向它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}

它的参数是引用类型,只能传递变量,不能传递常量或者表达式。

绑定临时数据

当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。

将常引用绑定到临时数据时,编译器采取了一种妥协机制:编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。注意,临时变量也是变量,所有的变量都会被分配内存。

1
2
3
4
5
6
S operator+(const S &A, const S &B){
S C;
C.a = A.a + B.a;
C.b = A.b + B.b;
return C;
}

const 引用和普通引用不一样,只能通过 const 引用读取数据的值,而不能修改它的值,所以不用考虑同步更新的问题,也不会产生两份不同的数据,为 const 引用创建临时变量反而会使得引用更加灵活和通用。

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 100;
isOdd(a); //正确
isOdd(a + 9); //正确
isOdd(27); //正确
isOdd(23 + 55); //正确

bool isOdd(const int &n){ //改为常引用
if(n/2 == 0){
return false;
}else{
return true;
}
}

对于第 2 行代码,编译器不会创建临时变量,会直接绑定到变量 a;对于第 3~5 行代码,编译器会创建临时变量来存储临时数据。也就是说,编译器只有在必要时才会创建临时变量。

引用与转换类型

编译器禁止指针指向不同类型的数据。

1
2
3
4
5
6
int n = 100;
int *p1 = &n; //正确
float *p2 = &n; //错误
char c = '@';
char *p3 = &c; //正确
int *p4 = &c; //错误

float *类型的指针不能指向 int 类型的数据,int *类型的指针也不能指向 char 类型的数据。

当对引用添加 const 限定后,编译器允许引用绑定到类型不一致的数据。

1
2
3
4
5
6
int n = 100;
int &r1 = n; //正确
const float &r2 = n; //正确
char c = '@';
char &r3 = c; //正确
const int &r4 = c; //正确

当引用的类型和数据的类型不一致时,如果它们的类型是相近的,并且遵守「数据类型的自动转换」规则,那么编译器就会创建一个临时变量,并将数据赋值给这个临时变量(这时候会发生自动类型转换),然后再将引用绑定到这个临时的变量,这与「将 const 引用绑定到临时数据时」采用的方案是一样的。

总结

给引用添加 const 限定后,不但可以将引用绑定到临时数据,还可以将引用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。

当引用作为函数参数时,如果在函数体内部不会修改引用所绑定的数据,那么请尽量为该引用添加 const 限制。

评论