0
点赞
收藏
分享

微信扫一扫

【C++】引入“引用”机制原因的简单记录

jjt二向箔 2022-02-15 阅读 12

🌏目录


🤔 为何引入引用?

在回答【为何引入引用?】这个问题之前,我们先回顾下函数的两种调用形式:一种是传值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()函数时传入的num1num2对象和在swap()函数内部操作的xy对象实际上除了值完全相同外,不存在其它任何关系,因此在函数内无论对xy对象怎么操作,都不影响swap()函数外部的num1num2对象。因此,我们必须以传址的方式来实现。
此外,需要说明的是,当我们调用swap()函数时,在内存中会建立一块特殊区域,称为程序堆栈。程序堆栈为每个函数参数提供了储存空间,也就是说对象num1num2的存于当下这块特殊区域中,我们称位于这块特殊区域中的对象为局部对象,一旦swap()函数的函数体执行完毕,这块内存便会被立即释放(销毁)。因此,函数执行完毕后,局部对象xy也将被销毁。

而对于传址方式,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️⃣ 引用是类型安全的,而指针不是。引用比指针多了类型检查。


举报

相关推荐

0 条评论