1、引用的意义
引用作为变量别名而存在,因此在一些场合可以替代指针,引用相对于指针来说具有更好的可读性和实用性
// swap函数的实现对比
#include <iostream>
using namespace std;
void swap1(int a, int b);
void swap2(int *p1, int *p2);
void swap3(int &r1, int &r2);
int main() {
int num1, num2;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap1(num1, num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap2(&num1, &num2);
cout << num1 << " " << num2 << endl;
cout << "Input two integers: ";
cin >> num1 >> num2;
swap3(num1, num2);
cout << num1 << " " << num2 << endl;
return 0;
}
//直接传递参数内容,无法达到交换目的,因为是形参进行了交换,而实参没有交换
void swap1(int a, int b) {
int temp = a;
a = b;
b = temp;
}
//传递指针,实现交换
void swap2(int *p1, int *p2) {
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
//按引用传参,实现交换
void swap3(int &r1, int &r2) {
int temp = r1;
r1 = r2;
r2 = temp;
}
注意: 函数中的引用形参不需要进行初始化,初始化是在调用的时候完成的
2、特殊的引用
在C++中可以声明const引用,具体用法如下:
const Type& name = var;
const引用让变量拥有只读属性,这个只读属性是针对当前的这个别名,变量是可以通过其它方式进行修改
int a = 4; // a是一个变量
const int & b = a; // b是a的一个引用,但是b具有只读属性
int * p = (int *)&b; // p = &a
b = 5; // err, 引用b 被const修饰,b是一个只读变量
a = 6; // ok
printf("a = %d\n", a);
*p = 5; // ok
printf("a = %d\n", a);
当使用常量对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
#include <stdio.h>
void Example()
{
printf("Example:\n");
int a = 4;
const int& b = a;
int* p = (int*)&b;
//b = 5;
*p = 5;
printf("a = %d\n", a);
printf("b = %d\n", b);
}
void Demo()
{
printf("Demo:\n");
const int& c = 1;
int* p = (int*)&c;
//c = 5;
*p = 5;
printf("c = %d\n", c);
}
int main(int argc, char *argv[])
{
Example();
printf("\n");
Demo();
return 0;
}
结论: 使用常量对const引用初始化后将产生一个只读变量
3.问题:引用有自己的存储空间吗?
在研究这个问题之前,我们先来看一下c++中数据在内存中的存储:
一个运行的程序在内存中主要表示为这四种空间区域。那这几种空间区域存储的是什么?
代码区:存放的是程序的执行代码(编译后的二进制代码)。
全局数据区:存放全局变量、静态变量、常量和文字量(文字量和常量有区别的)。
堆区:存放动态内存,供程序随机申请使用。C的malloc、free,C++的new和delete内存分配就是在堆中进行。
栈区:又称局部数据区,它动态反映了程序运行中的函数状态。存储函数的形参、局部变量以及函数的返回值和返回地址。
这其中栈区的存储方式较为特别,按照自下至上内存地址递增来算,栈区中按照变量定义顺序自高地址向低地址依次分配,也就是自上至下进行分配。
#include <stdio.h>
struct TRef
{
char& r; // 字符类型引用
};
int main(int argc, char *argv[])
{
char c = 'c';
char & rc = c;
TRef ref = { c }; // 用C进行初始化, TRef.r 就是 c的别名了
printf("sizeof(char&) = %d\n", sizeof(char&)); // char引用的大小,引用即变量本身,求所对应的变量本身的大小,即sizeof(char) = 1
printf("sizeof(rc) = %d\n", sizeof(rc)); // rc是一个引用,即sizeof(c) = 1
printf("sizeof(TRef) = %d\n", sizeof(TRef)); // sizeof(TRef) = 4
printf("sizeof(ref.r) = %d\n", sizeof(ref.r)); // TRef.r是 c的别名,sizeof(c) = 1
// sizeof(TRef) = 4
// 指针变量本身也是占4个字节
// 引用和指针的关系
return 0;
}
4、引用的本质
引用在C++中的内部实现是一个指针常量
注意:
1、C++编译器在编译过程中用 指针常量 作为引用的内部实现,因此引用所占用的空间大小于指针相同
2、从使用的角度,引用只是一个别名,C++为了使用性而隐藏了引用的存储空间这一细节。
#include <stdio.h>
// 在内存中结构体内部从低地址向高地址依次存储
// 如果以局部变量来定义结构体时,其整体存放在栈区
// [参考资料](https://blog.csdn.net/u011770174/article/details/77369259)
struct TRef
{
char* before; // 4字节
char& ref; // 4字节
char* after; // 4字节
};
int main(int argc, char* argv[])
{
char a = 'a';
char& b = a;
char c = 'c';
TRef r = {&a, b, &c};
printf("sizeof(r) = %d\n", sizeof(r)); // sizeof(r) = 12
printf("sizeof(r.before) = %d\n", sizeof(r.before)); // sizeof(r.before) = 4
printf("sizeof(r.after) = %d\n", sizeof(r.after)); // sizeof(r.after) = 4
printf("&r.before = %p\n", &r.before); // &r.before = 0xbuf8a300c
printf("&r.after = %p\n", &r.after); // &r.after = 0xbuf8a3014
/*
0xbuf8a3014 - 0xbuf8a300c = 8
before占了4个字节,所以ref也是占4个字节
*/
return 0;
}
引用的意义: C++中的引用旨在大多数的情况下替代指针
功能性:可以满足多数需要使用指针的场合
安全性:可以避开由于指针操作不当带来的内存错误
操作性:简单易用,又不失功能强大
5. 引用的易出现的问题
在将引用作为函数返回值时应该注意一个小问题,就是不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++ 编译器检测到该行为时也会给出警告。
#include <iostream>
using namespace std;
int &plus10(int &r) {
int m = r + 10;
return m; //返回局部数据的引用
}
int main() {
int num1 = 10;
int num2 = plus10(num1);
cout << num2 << endl;
int &num3 = plus10(num1);
int &num4 = plus10(num3);
cout << num3 << " " << num4 << endl;
return 0;
}
// 在 Visual Studio 下的运行结果:
// 20
// -858993450 -858993450
6. 总结
- 引用作为变量别名而存在旨在代替指针
- const引用可以使得变量具有只读属性
- 引用在编译器内部使用指针常量实现
- 引用的最终本质为指针
- 引用可以尽可能地避开内存错误