【C++】为何引入“引用“? 指针和引用有何区别?

📣🥳🥳🥳📣

✨Hello! 如果这篇【文章】对你有帮助😄,希望可以给博主点个赞👍鼓励一下😘

📣🥳🥳🥳📣



🤔 为何引入引用?

在回答【为何引入引用?】这个问题之前,我们先回顾下C语言中函数的两种调用形式:一种是传值,一种是传址(准确来说其实也是传值,只不过这个实参是指针,它的值是一个地址,因此形参拷贝到这个地址,可以通过这个地址去访问这个地址所在的对象)。

对于传值方式,举例如下👇

#include <iostream>
using std::cout;
using std::endl;

void swap(int x, int y);    // swap函数声明

int main() 
{
	int num1 = 1, num2 = 2;
	swap(num1, num2);    //希望交换num1和num2的值
	cout << "num1=" << num1 << " " << "num2=" << num2 << endl;
	return 0;
}

/* 函数功能:(设想能够)交换两个int类型对象的数值 */
void swap(int x, int y) {
	int temp = x;
	x = y;
	y = temp;
}

编译运行结果如下👇

num1=1 num2=2

可以发现预期结果并未达到。这是因为传值方式在传参时只是单纯的复制一份,在使用swap()函数时传入的num1num2对象和在swap()函数内部操作的xy对象实际上除了值完全相同外,不存在其它任何关系,因此在函数内无论对xy对象怎么操作,都不影响swap()函数外部的num1num2对象。因此,我们必须以"传址"的方式来实现。
此外,需要说明的是,当我们调用swap()函数时,在内存中会建立一块特殊区域,称为程序堆栈。程序堆栈为每个函数参数提供了储存空间,也就是说对象num1num2的存于当下这块特殊区域中,我们称位于这块特殊区域中的对象为局部对象,一旦swap()函数的函数体执行完毕,这块内存便会被立即释放(销毁)。因此,函数执行完毕后,局部对象xy也将被销毁。

对于"传址"方式,C++中除了可以以指针形式来实现,还引入了引用机制来实现。下面先以指针形式实现传址,将上例代码更改如下👇

#include <iostream>
using std::cout;
using std::endl;

void swap(int *px, int *py);    // swap函数声明

int main() 
{
	int num1 = 1, num2 = 2;
	swap(&num1, &num2);    //希望交换num1和num2的值
	cout << "num1=" << num1 << " " << "num2=" << num2 << endl;
	return 0;
}

/* 函数功能:(设想能够)交换两个int类型对象的数值 */
void swap(int *px, int *py) {
	int temp = *px;
	*px = *py;
	*py = temp;
}

编译运行结果如下👇

num1=2 num2=1

很明显预期结果已达到。

接下来,再采用引用机制来实现传址方式,只需将最开始的代码里的swap()函数作如下更改👇

void swap(int &x, int &y);   

编译运行结果如下👇

num1=2 num2=1

可以发现预期效果同样可以达到。

【回到问题上,既然指针已经可以实现传址,那为何还要额外引入引用这个机制来实现"传址"呢?】思考结果如下👇

相较于指针形式,引用机制无需手动使用解地址运算符,且在函数调用时非常直观,修改函数时函数体整体也完全不用调整。引入引用机制,便是专门针对函数调用时的传址场景需求,在这种场景下可以使用引用机制替代传统的指针形式,从而避免函数体内的解地址操作以及函数传值传址调整的麻烦问题。此外,引用时形参只作为实参的别名,不需要额外的内存,指针的形参仍然是要重新拷贝的。

🤔 何时传值何时传址?

在定义函数时,如何去确定到底是要传值还是要传址呢?《Essential C++》中的说法如下👇

传址的一个理由是:希望得以直接对所传入的对象进行修改。这个理由极为重要。(就比如上面所写到的swap交换函数,如果不采用传址方式,根本无法实现预期要求。)
传址的另一个理由是:希望降低复制大型对象的额外负担。但这个理由相较之下不那么重要,涉及程序的效率问题。

对于第二个理由,以下面这个打印函数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(指针)得到的是这个指针对象本身的类型大小】。

2️⃣ 引用是类型安全的,而指针不是。

由于必须初始化且与一个对象一直绑定,因此不存在空引用,但指针指向可以随意更改(指针其实也可以理解为一个普通变量,只不过它的变量值是一个地址值,较特别),且允许存在空指针和野指针(野指针可以在多种场景下产生,其中一种情况是当多个指针指向一块内存,free掉一个指针之后,那么别的指针就成为了野指针)。因此引用是类型安全的,而指针不是。类型安全即内存安全。

2️⃣ 关于指向常量的引用/指针、常量指针的概念

这部分内容繁杂,单独写在另一篇博文【C++】指向常量的引用/指针、常量指针、顶层底层const的分析

🤔 *传值传址的个人思考

我的理解是,只要是能引用的情况,就别拷贝,但写代码的人要能给足它【安全性】。

下面解释下这个安全性👇

因为大多数情况下,无论我们是打算传引用还是传指针进入一个函数,其实都是为了能在函数里面对函数外的对象进行【改值】操作(也就是本博文第三章“何时传值何时传址”的第一个理由)。因此,如果我们要声明的一个函数它没有改变实参的需求,一般都建议采取传值的方式,这样形参每次都只能拿到实参的一份拷贝,完全独立的拷贝与实参毫无关联,且函数体执行完毕后即可便被销毁。

但着眼于【拷贝】二字,我们就知道,这必然带来程序堆栈的内存消耗,必然需要额外空间,特别是对于与字符串处理相关的函数来说,消耗更是巨大,因此我们最好还是采用引用的机制,将形参作为实参的别名,无需消耗额外的内存空间(指针也行,但指针也需要一点点额外空间来存放一份地址值的拷贝,另外函数体内部还需不断地进行解引用*,且【最重要的是】,指针安全性相比引用而言【差了很多】)

但对一个没有改值需求的函数进行传引用,如果代码块很大很复杂,万一某处地方进行改值而我们没有发现,那将对程序造成不可知的后果;而如果使用指针,如果函数内部不小心将指针指向了其他处(比如指针自增自减)并进行了改值,那后果将更加严重。

综上,我的总结是👇

1.平时,对于函数也好循环体也好,都要养成优先使用引用的习惯,传值和指针作为次选项。
2.此外,如果想在没有改值需求的情况下使用引用,那么要根据自己的能力大小(对当前代码块的理解能力)来决定最终是否要使用引用。【最为安全最为稳妥的方式就是使用【指向常量的引用/指针】。此处见另一篇文章

对于2,下面有两个例子👇

#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;

int main() {
    vector<int> ivec = {1, 2, 3, 4};
    //下面这个for循环体的代码块理解起来很简单,你肯定觉得这里很安全,那完全可以采用引用
    for (auto &val : ivec) {  //val作为别名,而不是拷贝,避免内存消耗
        cout << val << " ";
    }
    return 0;
}
1 2 3 4
#include <iostream>
#include <vector>
using std::cout;
using std::endl;

void display(int &n);  //函数声明

int main() {
    int age = 20;
    cout << "I'm born in 2000.I'm " << age << " years old." << endl;
    display(age);
    cout << "I'm born in 2000.I'm " << age << " years old." << endl;
    return 0;
}

//display函数定义。设想只用这个函数来进行打印,没有改值需求,但仍然想用引用
void display(int &n) {  //形参n是实参val的别名
    while (n != 0) cout << n-- << " ";  //n自减,作为age的别名,影响了age
    cout << endl;
}
I'm born in 2000.I'm 20 years old.
20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1
I'm born in 2000.I'm 0 years old.

最安全最稳妥的方式👇

void display(const int &n) {  //n是指向常量的引用(即常量引用),通过n无法更改其所绑定的对象
    while (n != 0) cout << n-- << " ";  //此时n自减便会报错:“试图改变一个常量”
    cout << endl;
}

此外,在函数形参中善用【常量引用】还可以带来一些便利👇

display(10);  //对于将普通引用作为形参的函数,这种函数调用是错误的
display(10)//对于将常量引用作为形参的函数,该调用无误。因为C++允许用字面值初始化常量引用

✨如有问题欢迎在底下评论留言或私信!

如果这篇【文章】对你有帮助😄,希望可以给博主【点个赞👍】鼓励一下😘

❤️Thanks for your encouragement❤️

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

John Chen1223

点赞是美意!打赏是鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值