【C++】C++引用 (引用不会?详细解决引用诸多细节!)

C++引用目录:

一、引用的概念

二、引用的特性

2.1引用在定义的时候必须初始化

2.2一个变量可以有多个引用

2.3引用一旦引用了一个实体,就不可以再引用其他实体

三、常引用(带const的引用)

3.1临时变量有常性不能修改(传值返回,隐式/强制类型转换时产生)

3.2指针/引用在赋值中,权限可以缩小,但是不能放大

3.3常引用做参数

3.4缺省参数如何引用?

四、引用的使用场景

4.1做参数(减少拷贝提高效率,实形一体)

4.2做返回值(减少拷贝提高效率,修改返回值)

4.2.1内存空间销毁意味着什么?& 访问销毁的内存空间会怎样?

4.2.2引用做返回值提高效率

4.2.3引用做返回值可被修改

五、引用和指针的区别


小知识:vscode查看汇编代码:

在调试控制器的位置输入:

-exec disassemble /m

一、引用的概念

基本形式为:类型& 引用变量名(对象名) = 引用实体

引用就是给一个已经存在的变量取别名:引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

引用的符号和我们C语言中取地址的符号一样,为 &;在某类型名的后面加上引用符号 (&) 就变为了引用类型。设计引用的目的是简化指针的使用,但是引用不能代替指针 (实际上引用的底层是用指针实现的)

难点正是符号与地址符相同,比如见到*&就蒙圈,而且一定得记住在类型后面的都不是取地址,都是起别名

void TestRef()
{
	int a = 10;
	int& ra = a;//<====定义引用类型
	printf("%p\n", &a);
	printf("%p\n", &ra);
}

a和ra全部指向这个空间

注意:引用类型必须和引用实体是同种类型的(这个原因在常引用的权限放大与缩小会讲解原因)


二、引用的特性

2.1引用在定义的时候必须初始化

注意:const int&b=10;这个也是可以通过的,这个也算是初始化(至于为什么加const请看下面常引用的知识)

2.2一个变量可以有多个引用

int a = 10;
int& b = a;
int& c = a;
int& d = a;

此时,b、c、d都是变量a的引用

2.3引用一旦引用了一个实体,就不可以再引用其他实体

这句话的意思也间接表明,C++引用代替不了指针,因为指针是可以更改链接地址的

(在Java和python中,引用是可以更改的)

	int a = 10;
	int& b = a;

此时,b已经是a的引用了,b不能再引用其他实体。如果你写下以下代码,想让b转而引用另一个变量c:

	int a = 10;
	int& b = a;
	int c = 20;
	b = c;//你的想法:让b转而引用c

但该代码并没有随你的意,该代码的意思是:将b引用的实体赋值为c,也就是将变量a的内容改成了20


三、常引用(带const的引用)

常引用就是在引用类型前面用 const 来进行修饰;和 const 修饰变量一样,被 cosnt 修饰的引用只能够读,而不能够写,即不能修改引用变量的值

3.1临时变量有常性不能修改(传值返回,隐式/强制类型转换时产生)

C++ 中的临时变量指的是那些由编译器根据需要在栈上产生的,没有名字的变量。主要的用途主要有两类,临时变量都是具有常性的

1) 函数的返回值

这个图片很重要,一定得看:

 我们再举一个不用引用接收的例子:

在C++中,如果一个函数返回一个临时变量(通过传值返回),那么这个临时变量是一个右值,它不能被修改。`Add(a,b)` 返回一个临时变量,因此不能直接对其进行赋值操作。如果想要修改结果,可以将其存储在一个变量中,然后对变量进行操作。

int Add(int a, int b) {
    return a + b;
}

int main() {
    int a = 0;
    int b = 3;
    int result = Add(a, b);
    result += 1;
    return 0;
}

我还是有个小问题,为什么const类型可以传给int类型的呢??(单纯赋值权限不说放大放小)

只有引用和指针需要考虑,其他的没问题的,所以在这里面就是最单纯的:

int给const错(因为const只可以在定义的时候初始化)

const给int对(可以将一个const类型的值赋给一个非const类型的变量,不能将一个非const类型的值赋给一个const类型的变量,因为这会破坏了const的只读性质)

但在下面的引用赋值的时候,小心混淆!

int& a=10 这是const给int 是权限放大 这是不对的(原来const只读,结果int鬼子入侵,当然不行)

int b=2;const int& c=b; 这是int给const 是权限放小 合理(原来我大大咧咧,然后被妻管严)

2) 类型转换时的中间变量

int main()
{
	int d=10;

	int i=(int)d;//强制类型转换,并不是改变了变量d,而是产生临时变量
	int i=d;//隐式类型转换,也是产生了临时变量

	double d=12.34;
	const int& ri=d;//这里引用的实体其实就是从double d 到int类型转换中间的临时变量
	return 0;
}

把这张图吃透,const关于临时变量的问题就可以告一段落了。题外话结束,进入正题

当然,这段代码是不可以运行的,因为有大量的重定义

3.2指针/引用在赋值中,权限可以缩小,但是不能放大

int main()
{
	const int a = 10;
	//int& ra = a;    //该语句编译时会出错,a为常量,权限放大
	const int& ra = a;//正确,权限平移
	
	//int& b = 10;    //该语句编译时会出错,10为常量,权限的放大
	const int& b = 10;//正确,权限平移

    int c=10;
    const int& rc=c;  //正确,权限缩小
	return 0;
}

权限只能被缩小和平移,不能被放大

注: 这里的权限指的是读和写的权限,且只针对于指针和引用

3.3常引用做参数

a.一般引用做参数都是用常引用,也就是const+引用,如果不用const会有可能产生权限放大的问题,而常引用既可以接收只读的权限,又可以接收可读可写的权限。

b.常引用做参数并不是为了修改参数,而是为了减少拷贝提高效率。

当然,并不是所有的参数加const就好,比如我们的swap函数,如果加上const反而会适得其反

3.4缺省参数如何引用?

缺省参数如果想做为引用的话,必须用常引用,因为缺省参数是一个常量,是不允许被修改的,只可以读

void func(const int& N = 10)
{

}

四、引用的使用场景

4.1做参数(减少拷贝提高效率,实形一体)

在调用函数时,形参是要做拷贝的,在它所在的函数栈帧里面,所以如果你要是传值调用,那必然在调用函数时,会做一份实参的临时拷贝,如果你是传址调用,指针变量也要开辟自己的空间,所以这些都是对程序性能的消耗

引用可以直接改变实参,作为输入性参数及输出型参数可以不再传递指针;比如下面的 Swap 函数,我们在 Swap 函数内部交换的其实就是两个实参的值,不用再像以前一样需要传递实参的指针

但如果我们用引用做参数就不会有这些问题了,因为操作系统并不会给引用变量单独开辟一块空间,并且引用变量也不需要对实参进行拷贝,那就可以减少拷贝提高效率。
并且由于引用实质上就是实参本身,那么它也可以作为输出型参数,对函数外面的实参进行修改

//单链表
typedef int SLTDataType;  //数据类型重命名
typedef struct SListNode   //链表的一个节点
{
	SLTDataType data;
	struct SListNode* next;  //存放下一个节点的地址
}SLTNode;

//在头部插入数据
void SListPushFront(SLTNode*& rphead, SLTDataType x)  //引用做形参,直接操作plist
{
	SLTNode* newNode = BuySLTNode(x);  //开辟新节点
	newNode->next = *rphead;
	*rphead = newNode;
}

在单链表的头插等操作,由于我们之前实现的单链表是不带头的,所以我们需要传递 plist 的指针,方便插入第一个数据时改变 plist,而现在,我们可以直接使用 plsit 的引用即可,不用再传递二级指针了,从而使代码变得更易理解。当然,我们不能把 SListNode 中的 next 指针也设计为引用,因为尾结点的 next 是在不断改变的,而引用一旦引用一个实体,就不能再引用其他实体,这也从侧面说明了引用不能代替指针,只能简化指针

4.2做返回值(减少拷贝提高效率,修改返回值)

我们先来探讨局部变量空间销毁的问题

4.2.1内存空间销毁意味着什么?& 访问销毁的内存空间会怎样?

内存空间销毁并不是把这块内存空间撕烂了,永久性的毁灭这块儿空间,内存空间是不会消失的,他会原封不动的在那里,只不过当内存空间销毁之后,他的使用权不属于我们了,我们存到里面的数据不被保护了,有可能发生改变了

销毁之后,我们依然可以访问到这块空间,只是访问的行为是不确定的,我们对空间的数据进行读写的行为是无法预料的

把空间当作酒店,把数据当作苹果:
a.苹果没丢    b.房间没丢,但是成了随机水果    c.苹果被覆盖成固定水果

示例一:

int& Func()
{
	static int n = 0;
	n++;
	printf("&n:%p\n", &n);
	return n;
}

int main()
{
	int ret = Func();
	cout << ret << endl;
	printf("&ret:%p\n", &ret);

	int& rret = Func();
	printf("&rret:%p\n", &rret);
	return 0;
}

n的地址和rret相同,说明引用返回的是变量n本身,而不仅仅是n的值

在 Func 函数中,n 由于是静态变量,而静态变量在静态区中开辟空间,不在栈区上开辟空间,所以当 Func 函数的栈帧销毁后 n 并不会被销毁

同时,我们用引用做返回值,相当于直接将 n 这个变量返回给了函数调用者,所以,当我们再用一个引用来接收的话,就等价于给 n 再起了一个别名;

示例二:(注:下面对引用的使用方式有部分是错误示范,一定得跟着博主一起慢慢吃透)

分析:对于左侧代码,离开Count函数栈帧,变量n会被销毁,返回的别名指向的是一块被销毁后的变量空间名,这时如果使用返回后的别名,出现的结果是无法预料的(上方举的apple例子),由于n变量被销毁,所以一定不可以用引用返回(但是不会有语法编译错误,一定得小心)

对于右侧代码,变量n不会被销毁,存放在的是静态区,所以可以使用引用返回

分析:函数都已经被销毁,返回值为什么还可以被带出来呢?是因为在执行return语句前,系统会自动创建一个临时变量temp,这个变量的存储地址是根据return内容的大小分配的,可能是寄存器,也可能是堆,我们无法预知,接着操作系统会把即将要被释放的变量的值赋值给这个临时变量,通过临时变量将返回值带出来

#include<iostream>
using namespace std;

int& Count()
{
	int n = 0;//n存放在栈区
	n++;
	//...
	return n;
}
void Func()
{
	int x=100;
}
int main()
{
	int &ret = Count();
	cout << ret << endl;

	cout << ret << endl;

	Func();
	cout << ret << endl;
	return 0;
}

分析:

1、Func 函数中的 n 变量是局部变量,函数调用完成后被销毁,但是我们这里是引用做返回值,所以返回的是 n 那块栈区空间;

2、ret 是Func 函数返回值的引用,而函数返回值是局部变量 n 的引用,所以我们第一次打印的是 n 的值1;

3、我们在打印 ret 的时候调用了 printf 函数,该函数使用了Count被销毁的空间,所以 n 空间中原本的数据被覆盖,而 ret 是 n 的引用,所以打印 ret 就是打印 printf 函数中原本属于 n 的那块内存空间的数据,所以打印出来的是一个随机值。

4、我们调用了Func函数,然后发现Func中的 x 变量的地址和原本 n 的地址相同,说明 Cover 函数又使用了 printf 函数的空间,且 x 的空间恰好位于之前 n 的位置,所以打印 ret 的结果为100;

空间销毁的问题解决完毕,继续给出一段引用做返回值的错误代码,继续理解分析:

4.2.2引用做返回值提高效率

根据上面传值返回得知会创建临时变量,所以传引用返回提高效率是显而易见的

4.2.3引用做返回值可被修改

下面这段代码给大家演示了C++中利用引用作为返回值来修改返回值的场景。
将数组中的偶数全部扩大二倍

int& change(int* arr,int i)
{
	return arr[i];
}
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (int i = 0; i < 10; i++)
	{
		if (arr[i] % 2 == 0)
		{
			change(arr, i) *= 2;
		}
	}
	for (int i = 0; i < 10; i++)
	{
		cout << arr[i] << " ";
	}
}

总结:

出了函数作用域返回变量不存在了,不能用引用返回因为引用返回的结果是未定义的
出了函数作用域返向变量存在,才能用引用返回


五、引用和指针的区别

语法概念上:引用就是一个别名,没有独立空间,和其引用实体共用同一块空间

底层实现上:引用实际是有空间的,因为引用是按照指针方式来实现的

我们调试代码,然后转到反汇编后可以发现,引用和指针的汇编代码是完全一样的,即引用的底层实际上就是指针

引用和指针的不同点(9大点)

  • 引用概念上定义一个变量的别名,指针存储一个变量地址;
  • 引用在定义时必须初始化,指针没有要求;
  • 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;
  • 没有NULL引用,但有NULL指针;
  • 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节);
  • 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;
  • 有多级指针,但是没有多级引用;
  • 访问实体方式不同,指针需要显式解引用,引用编译器自己底层处理;
  • 引用比指针使用起来相对更安全。

希望这篇费劲心思的c++引用全文可以对你们有所帮助!!! 加油!!

猜你喜欢

转载自blog.csdn.net/weixin_62985813/article/details/132724155
今日推荐