🌏目录
🤔 为何引入引用?
在回答【为何引入引用?】这个问题之前,我们先回顾下函数的两种调用形式:一种是传值pass by value
,一种是传址pass by reference
。在C语言中,传址方式只能通过指针形式来实现。
对于传值方式,举例如下👇
#include <iostream>
#include <vector>
using namespace std;
void swap(int x, int y); // swap函数声明
void display(vector<int> vec); // display函数声明
int main()
{
int num1 = 1, num2 = 2;
swap(num1, num2); //希望交换num1和num2的值
int num_arr[2] = {num1, num2};
vector<int> num_vec(num_arr, num_arr+2);
display(num_vec);
return 0;
}
/* 函数功能:(设想能够)交换两个int类型对象的数值 */
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
/* 函数功能:打印一个vector模板类对象中的每一个数值 */
void display(vector<int> vec) {
for (int i = 1; i <= vec.size(); ++i) {
cout << "num" << i << " = " << vec[i-1] << endl;
}
}
编译运行结果如下👇
num1 = 1
num2 = 2
可以发现预期结果并未达到。这是因为传值方式在传参时只是单纯的复制一份,在使用swap()
函数时传入的num1
、num2
对象和在swap()
函数内部操作的x
、y
对象实际上除了值完全相同外,不存在其它任何关系,因此在函数内无论对x
和y
对象怎么操作,都不影响swap()
函数外部的num1
、num2
对象。因此,我们必须以传址的方式来实现。
此外,需要说明的是,当我们调用swap()
函数时,在内存中会建立一块特殊区域,称为程序堆栈。程序堆栈为每个函数参数提供了储存空间,也就是说对象num1
和num2
的存于当下这块特殊区域中,我们称位于这块特殊区域中的对象为局部对象,一旦swap()
函数的函数体执行完毕,这块内存便会被立即释放(销毁)。因此,函数执行完毕后,局部对象x
和y
也将被销毁。
而对于传址方式,C++中除了可以以指针形式来实现,还引入了引用机制来实现。下面先以指针形式实现传址,将上例代码更改如下👇
#include <iostream>
#include <vector>
using namespace std;
void swap(int *x, int *y); // 有调整
void display(vector<int> vec);
int main()
{
int num1 = 1, num2 = 2;
swap(num1, num2);
int num_arr[2] = {num1, num2};
vector<int> num_vec(num_arr, num_arr+2);
display(num_vec);
return 0;
}
void swap(int *x, int *y) { // 有调整
int temp = *x; // 有调整。指针形式传址后,使用时需手动解地址。后面会发现使用引用机制传址时,无需手动解地址
*x = *y; // 有调整。
*y = temp; // 有调整
}
/* 函数功能:打印一个vector模板类对象中的每一个数值 */
void display(vector<int> vec) {
for (int i = 1; i <= vec.size(); ++i) {
cout << "num" << i << " = " << vec[i-1] << endl;
}
}
编译运行结果如下👇
num1 = 2
num2 = 1
很明显预期结果已达到。
接下来,再采用引用机制来实现传址方式,只需将最开始的代码里的swap()
函数作如下更改👇
void swap(int &x, int &y);
编译运行结果如下👇
num1 = 2
num2 = 1
可以发现预期效果同样可以达到。
【回到问题上,既然指针已经可以实现传址,那为何还要额外引入引用这个机制来实现传址呢?】思考结果如下👇
相较于指针形式,引用机制无需手动使用解地址运算符,且在函数调用时非常直观,修改函数时函数体整体也完全不用调整。引入引用机制,便是专门针对函数调用时的传址场景需求,在这种场景下可以使用引用机制替代传统的指针形式,从而避免函数体内的解地址操作以及函数传值传址调整的麻烦问题。
🤔 何时传值何时传址?
在定义函数时,如何去确定到底是要传值还是要传址呢?《Essential C++》中的说法如下👇
对于第二个理由,可以用上面那个例子的打印函数display()
来说明。该函数定义如下👇
void display(vector<int> vec) {
for (int i = 1; i <= vec.size(); ++i) {
cout << "num" << i << " = " << vec[i-1] << endl;
}
}
可以发现该函数参数传递方式为传值方式,这意味着,每次进行显示操作时,向量内的所有元素都会被复制一遍。而如果直接传入向量的地址,即采用传址方式,对于大容量的向量来说,可以降低不小开销,执行速度也会更快。更改如下👇
void display(vector<int> &vec) {
for (int i = 1; i <= vec.size(); ++i) {
cout << "num" << i << " = " << vec[i-1] << endl;
}
}
🤔 引用与指针的具体区别在于?
简单总结如下👇
1️⃣ 引用是为“受了限制”的指针,更安全。引用定义时必须初始化,且只与初始时的对象所绑定,作为该对象的别名(而指针本身就是一个对象,使用sizeof(引用)
得到的是所代表的对象的类型大小,而sizeof(指针)
得到的是这个指针对象本身的类型大小)。由于必须初始化且与一个对象一直绑定,因此不存在空引用,但指针指向可以随意更改(指针其实也可以理解为一个普通变量,只不过它的变量值是一个地址值,较特别),且允许存在空指针和野指针(野指针可以在多种场景下产生,其中一种情况是当多个指针指向一块内存,free掉一个指针之后,那么别的指针就成为了野指针)。
2️⃣ 关于常量指针、常量引用、指针常量和引用常量
常量指针/常量引用指的是,通过这个指针/引用没法对所指向/代表的变量进行重新赋值操作。例子如下👇
int main() {
int x = 1;
const int *p = &x; // 常量指针
*p = 2; // 报错,无法这样操作
const int &x1 = x; // 常量引用
x1 = 2; //报错,无法这样操作
return 0;
}
指针常量指的是指针的指向无法修改,其实可以理解为一个普通常量,但这个常量的值是一个地址值,这个地址值无法修改,其实也就是这个指针的指向没法修改了。例子如下👇
int main() {
int x = 1, y = 2;
int* const p = &x; // 指针常量
p = &y; // 报错,p是常量,不能这样操作了
return 0;
}
而对于引用常量,指的是引用的代表无法修改,也就是说无法成为其他对象的别名了,其实就是引用。引用就是引用常量,引用常量就是引用。
3️⃣ 引用是类型安全的,而指针不是。引用比指针多了类型检查。