int a = 99; int &r = a; cout << a << ", " << r << endl; cout << &a << ", " << &r << endl;
运行结果:
99,99
0x28ff44,0x28ff44
引用可以看作一个数据的别名,必须在定义的同时初始化,并且不能更改引用对象。
引用在定义时需要添加 &,使用时不能添加 &,使用时 & 表示取地址。
当将函数的形参指定为引用的形式时,实参和形参会绑定在一起,函数中对形参的修改会直接作用于实参的数据。
当引用作为函数返回值时,不能返回局部变量的引用。例如下面例子:
int &plus10(int &r) { int m = r + 10; return m; //返回局部数据的引用 } int main() { int num1 = 10; int num2 = plus10(num1); cout << num2 << endl; // 20 int &num3 = plus10(num1); // 不同版本编译器上有不同的结果 int &num4 = plus10(num3); // 不同版本编译器上有不同的结果 cout << num3 << " " << num4 << endl; return 0; }
引用只是对指针进行简单的封装,它的底层是通过指针实现的,引用占用的内存和指针占用的内存长度一样,在32位环境下是4个字节,64位环境下是8个字节,之所以不能获取引用的地址,是因为编码器进行了内部转换。
int a = 99; int &r = a; r = 18; cout << &r << endl;
编译时会被转换成如下形式:
int a = 99; int *r = &a; *r = 18; cout << r << endl;
当使用 &r 取地址时,编译器会进行隐式的转换,使得输出的是 r 的内容(a 的地址),而不是 r 的地 址,这就是为什么获取不到引用变量的地址的原因。也就是说,不是变量 r 不占用内存,而是编译器不让获取它的地址。
当引用作为函数参数时,也会有类似的转换。以下面的代码为例:
//定义函数 void swap(int &r1, int &r2){ int temp = r1; r1 = r2; r2 = temp; } // 调用函数 int num1 = 10, num2 = 20; swap(num1, num2);
会被转换成如下形式:
//定义函数 void swap(int *r1, int *r2){ int temp = *r1; *r1 = *r2; *r2 = temp; } //调用函数 int num1 = 10, num2 = 20; swap(&num1, &num2);
C++ 中引入引用的直接目的是为了让代码的书写更加漂亮, 尤其是在运算符重载中,不借助引用有时候会使得运算符的使用很麻烦。
int & const r = a;
引用不能绑定到放在寄存器的临时数据。下面代码表达式产生的都是临时结果,会放到寄存器中,使用 & 取地址是错误的。
int n = 100, m = 200; int *p1 = &(m + n); //m + n 的结果为 300 int *p2 = &(n + 100); //n + 100 的结果为 200 bool *p4 = &(m < n); //m < n 的结果为 false int func() { int n = 100; return n; } int *p = &(func());
int、double、bool、char 等基本类型的数据往往不超过 8 个字节,这些类型的临时数据通常会放到寄存器中;而对象、结构体变量是自定义类型的数据,大小不可预测,这些类型的临时数据通常会放到内存中。
对于常量表达式,如 100、200+34、34.5*23、3+7/3 等,会在编译阶段就求值,相当于一个立即数,不能寻址。
总起来说,常量表达式的值虽然在内存中,但是没有办法寻址,所以也不能使用&来获取它的地址,更不能用 指针指向它。
// 错误情况 int *p1 = &(100); int *p2 = &(23 + 45 * 2);
当引用作为函数参数时,有时候很容易给它传递临时数据。
// 参数是引用类型,不能接受常量或者表达式 bool isOdd(int &n) { if( n%2 == 0 ) return false; else return true; } bool isOdd2(int n) { if( n%2 == 0 ) return false; else return true; } //改为常引用,正确 bool isOdd3(const 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); //错误 }
引用不能绑定到临时数据,大多数情况是正确的,但当使用 const 关键字对引用加以限定后,引用就可以绑定到临时数据了。
int m = 100, n = 36; const int &r1 = m + n; const int &r2 = m + 28; const int &r3 = 12 * 3; const int &r4 = 50;
这块代码在 GCC 和 Visual C++ 下都能通过。(不同编译器下对临时变量引用的容忍程度不一样)。编译器会为临时数据创建一个新的、无名的临时变量,并将临时数据放入该临时变量中,然后再将引用绑定到该临时变量。临时变量也是变量,也会被分配内存。
为什么编译器为常引用创建临时变量是合理的,而为普通引用创建临时变量就不合理呢?
编译器禁止指针指向不同类型的数据,对于引用也是一样。(可以强转)
int n = 100; int *p1 = &n; //正确 float *p2 = &n; //错误 char c = '@'; char *p3 = &c; //正确 int *p4 = &c; //错误
对于不同类型,程序对它们的处理方式不同:
但对于 const 引用,其处理方式与将 const 引用绑定到临时数据时采用的方案一样,编译器会创建一个临时变量,再将引用绑定到临时变量上。而将数值赋值给临时变量时,会发生自动类型转换。
float f = 12.45; const int &r = f; // r:12
当自动类型转换不遵循数据类型的自动转换规则时,编译器会报错。
给引用添加 const 限定后,不但可以将引用绑定到临时数据,还可以将引 用绑定到类型相近的数据,这使得引用更加灵活和通用,它们背后的机制都是临时变量。
当引用作为函数参数时,如果不会修改数据,尽量为引用添加 const 限制。
概括起来说,将引用类型的形参添加 const 限制的理由有三个: