C++_引用

2023-12-14 02:14:29

目录

1、引用的使用

1.1?对“别名”的修改

1.2 “别名”的“别名”?

1.3 对“别名”进行赋值

2、引用的意义?

2.1 指针的“别名”

3、函数返回值作为引用

3.1 返回值作为引用的意义

4、引用的权限

4.1 引用的类型转换

5、指针与引用

5.1 指针与引用的相似处

5.2?指针与引用的区别

结语:


前言:

? ? ? ? 在C++中,引用的符号为”&“,和取地址的符号是一样的。使用引用的前提是必须得有一个变量作为引用的对象,通常我们把对一个变量的引用叫做给该变量“取别名”,因此调用该变量的“别名”实则就是调用该变量。而且“别名”与“本名”之间建立了联系,既对“别名”的修改会影响“本名”,对“本名”的修改也会影响“别名”。

????????注意:在同一作用域下,一个变量可以有多个“别名”,但是一个“别名”只要确定有了一个“本名”后就不能再成为其他本名的“别名”,通俗点来说就是“别名”已经确定“指向”一个“本名”后,就不能更改他的“指向”了。

1、引用的使用

? ? ? ? 写法:类型+&+”别名称“=引用对象变量。引用的使用代码如下:

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& c = a;//引用a,并且别名叫c(注意别名类型与本名类型保证一致)
	cout << a << endl;
	cout << c << endl;

    //观察别名和本名的地址是否相同
    cout << &a << endl;
	cout << &c << endl;

	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果中可以看到,别名c的地址实质上就是a的地址,既虽然别名也进行了初始化但是别名和本名用的是同一个空间,并没有产生新的空间,与现实中一个人的”别名"和“本名”逻辑相同,都是作用在同一个人上。(注意别名类型与本名类型保证一致)


????????注意:别名创建的时候必须进行初始化,不然会报错。

1.1?对“别名”的修改

? ? ? ? 上文提到对别名的修改会影响本名,测试代码如下:

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& c = a;
	int b = a;
	cout << &a << endl;
	cout << &c << endl;
	cout << &b << endl;

	c++;
	cout << a << endl;
	cout << c << endl;
	cout << b << endl;

	return 0;
}

? ? ? ? 运行结果:

?????????因此,a、b、c的关系图如下:

1.2 “别名”的“别名”?

? ? ? ? 在C++中,“别名”也可以有属于他自己的“别名”,可以嵌套使用,当对“别名”的“别名”进行修改时,会修改与之相关的所有“本名”和“别名”。

? ? ? ? “别名”嵌套代码如下:

#include<iostream>
using namespace std;

int main()
{
	int a = 10;
	int& c = a;
	int b = a;
	cout << b << endl;

	cout << endl;
	int& ra = a;//一个本名可以有多个别名
	int& rc = c;

	rc++;//改变其中一个别名的值
	cout << a << endl;
	cout << c << endl;
	cout << ra << endl;
	cout << rc << endl;

	return 0;
}

? ? ? ? 运行结果:

1.3 对“别名”进行赋值

? ? ? ? 上文提到“别名”一旦初始化后就不能修改“别名”的“指向”,所以对“别名”进行赋值也只能修改“别名”和“本名”的值,并不会让“别名”指向其他“本名”。

#include<iostream>
using namespace std;

int main()
{
	int a = 12;
	int& c = a;//初始化c为a的别名

	int b = 20;
	c = b;//将b的值赋给c
	b = b + 102;//更改b的值,观察c是否变化

	cout << a << endl;
	cout << c << endl;
	a++;//更改a的值,观察c是否变化
	cout << a << endl;
	cout << c << endl;

	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果可以看到,c依然是a的别名,则对“别名”进行赋值不会更改“别名”的“指向”,只会更改“别名”和其“本名”的值。?

2、引用的意义?

? ? ? ? 通过以上例子可以发现引用和指针在某些方面很相似,引用是:通过更改一个变量的“别名”就能达到更改该变量的效果。指针是:通过访问一个变量的地址,对其解引用后也可以更改该变量的值。所以在某些场景下,引用可以代替指针,并且可以实现和指针一样的效果。

? ? ? ? 体现引用意义的代码如下:

#include<iostream>
using namespace std;

void swap(int& x, int& y)//x为实参a的引用,y为b的引用
{
    //修改变量的“别名”,既修改变量本身
	int temp = x;
	x = y;
	y = temp;
}

int main()
{
	int a = 12;
	int b = 24;
	cout << a << endl;
	cout << b << endl;
	cout << endl;

	swap(a, b);

	cout << a << endl;
	cout << b << endl;
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果可以看到,没有使用传统的传址操作依然可以做到形参的改变影响实参。?

2.1 指针的“别名”

? ? ? ? 引用的类型不止局限于int类型,还可以引用char、double、float以及指针类型,只是引用的类型必须跟引用对象类型保持一致。

? ? ? ? 引用指针类型代码如下:

#include<iostream>
using namespace std;

int main()
{
	int a = 12;
	int* pa = &a;
	int*& rpa = pa;

	*rpa = 20;//对别名进行解引用同样可以找到a
	cout << a << endl;
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 这里的pa虽然等价于&a,但是引用时不能写成:?int*& rpa = &a,因为引用的对象必须为一个变量,&a不是一个变量,他具有常属性,因此不能作为引用的对象。

3、函数返回值作为引用

? ? ? ? 在讲述函数的返回值作为引用的作用前,要先引入一个概念,既函数调用结束后空间理应是还给操作系统的,那么函数的返回值是如何给到main函数中的呢。把函数调用时向系统申请的空间叫做栈帧空间,具体示意图如下:

? ? ? ? 因此在func函数被系统收回前会先完成一个动作,就是把n的值拷贝给main函数中的临时变量,这样即使func函数的空间被收回了也无所谓,因为n的值已经传递过来了,然后临时变量在把值给到poi,完成返回值的传递,并不是直接把n的值直接给到poi的。?


? ? ? ? 可以发现以上的过程是相对复杂的,因为过程中涉及临时变量的拷贝与赋值,似乎认为造成这一现象的原因就是n变量的生命周期随着函数空间被收回而结束了。如果可以延长n的生命周期是不是就能够直接把n的值返回给到poi呢?

? ? ? ? 示意图如下:

? ? ? ? 其实即使加了static,n的值也没有直接返回给到poi,还是和上面情况一样先拷贝给临时变量,然后再赋值给poi,所以以上的两种情况效率上没什么太大的区别,都是要经过一系列的过程。


? ? ? ? 但是关键点来了,上面两种情况只有第二种情况是适合将函数返回值作为引用的,首先把函数的返回值作为引用的意思是返回的不再是一个值了,而是一个“别名”,拿上面的例子来说,返回的是n的别名而不是n的拷贝。

????????其次为什么第一种情况不适合做返回值引用,因为第一种情况的n出了函数后就有可能会被销毁,也就是n的值可能会变成随机值,这时候如果返回他的“别名”则poi有可能接收到的是随机值(也有可能不是随机值,具体根据编译器而定了),总之是一种不稳定的情况。

? ? ? ? 而第二种情况的n放在静态区,他的值是“有保障”的,不会因为函数栈帧被收回而变成随机值,这时候返回n的别名则一定是有效值。因此得出结论:当一个函数的返回值出了作用域他的生命周期还在就可以用传引用返回(既返回值作为引用),否则只能用传值返回(既返回临时变量)。

????????返回值引用代码如下,并且测试对返回值别名的修改是否会影响返回值本名:

#include<iostream>
using namespace std;

int& func()
{
	static int n = 12;//本名
	cout << n << endl;
	return n;
}

int main()
{
	int& poi = func();//poi为别名
	cout << poi << endl;
	poi++;//poi变化则n也会变化
	func();//观察n是否发生变化
	cout << poi << endl;//观察poi和n是否一致
	return 0;
}

? ? ? ? 运行结果:?

?????????从结果可以看到,改变别名poi的值,则n的值也受到了变化,说明该函数返回的是n的别名,而不是n的拷贝。


? ? ? ? 那么问题来了,因为此时用的别名poi来接收,所以可以通过poi来改变n的值,这么一看应该是别名poi在发挥影响,如果传的是n值的拷贝,用别名poi来接收按理来说应该也可以做到更改poi去影响n的效果,毕竟传n的拷贝和传n的别名,poi接收到的值都是一样的,都是12。

? ? ? ? 运行结果:

? ? ? ? 发现此时编译器报错了,原因就是传值返回的是临时变量,而临时变量是具有常属性的,是不可被修改的,因此不能用变量的引用来接收他。可以在int& poi前面加上const,让poi也具有常属性即可,所以这里引出了引用权限的概念。

3.1 返回值作为引用的意义

? ? ? ? 为什么要用返回值作为引用呢,因为传引用返回相比于传值返回(临时变量作为返回值)在效率上要快一些,尤其是在处理大量的数据时。

? ? ? ? 效率测试代码如下:

#include<iostream>
#include <time.h>
using namespace std;

//创建一个大的对象,方便测试
struct A
{
	int a[10000];
};

A a;

// 值返回
A Return_by_value()
{
	return a;
}
// 引用返回
A& Return_by_reference()
{
	return a;

}
void TestReturnByRefOrValue()
{
	// 临时变量作为返回值
	int begin1 = clock();//clock函数是返回系统开始运行到调用到该函数的时间,单位毫秒
	for (int i = 0; i < 100000; ++i)
		Return_by_value();
	int end1 = clock();

	// 把返回值的引用作为返回值
	int begin2 = clock();
	for (int i = 0; i < 100000; ++i)
		Return_by_reference();
	int end2 = clock();

	// 比较两种方法的效率
	cout << "Return_by_value time:" << end1 - begin1 << endl;
	cout << "Return_by_reference time:" << end2 - begin2 << endl;
}

int main()
{
	TestReturnByRefOrValue();
	return 0;
}

? ? ? ? 运行结果:

? ? ? ? 从结果可以看到,?传引用返回的效率在处理大的数据时是优于传值返回的效率的。

4、引用的权限

? ? ? ? 在c\c++中,变量是可以被修改的,而常量是不可被修改的,因此如果引用对象为常量,则不能用引用变量的写法来引用常量,一般会在前面加const,让“别名”具有常属性就可以引用常量了。可以把变量的权限理解为:“可读”、“可改”,常量的权限理解为:“只可读”。那么在引用变量时可以加const,既允许权限缩小。当引用常量时就不能不加const,既不允许权限放大。

? ? ? ? 引用权限代码如下:

#include<iostream>
using namespace std;

int main()
{
	//权限缩小(允许)
	int a = 12;
	const int& c = a;
	
	//权限保持(允许)
	int e = 12;
	int& f = e;

	const int* p2 = NULL;
	const int*& rp2 = p2;

	//权限放大(不允许)
	//const int g = 12;
	//int& h = g;

	//const int* p1 = &g;
	//int*& rp1 = p1;

	return 0;
}

4.1 引用的类型转换

? ? ? ? 在上文提到过“别名”类型要与“本名”类型一致,不然编译器会报错,这里可以很好理解为什么“别名”类型和“本名”类型不一致会导致报错,第一反应肯定是因为他们的类型不匹配导致报错的。那么来看以下代码:

? ? ? ? 发现在double前面加了个const就能正常编译,说明不是类型不匹配引起的错误,而是在转换的过程中,并不是直接将i转换double,而是在这过程中产生了一个临时变量(该临时变量是double类型),i先把值给到临时变量中,然后临时变量再被引用至ri。

????????ri是中间产生的临时变量的别名,并不是变量i的别名,但是临时变量是具有常属性的,ri本身是没有常属性的,因此在前面加了个const让ri具有常属性就不会报错了。

5、指针与引用

5.1 指针与引用的相似处

? ? ? ? 上文说到引用和实体是共用同一个空间的,说明引用不会另外开一个空间,但是在底层实际是给引用开了空间的,并且底层引用是按照指针的逻辑实现的。

? ? ? ? 指针和引用的汇编语言实现图:

? ? ? ? 从上图可以得出引用的底层逻辑和指针是一样的。

5.2?指针与引用的区别

? ? ? ? 指针与引用的具体区别如下:?

1、指针存在空指针,引用不存在“空”的概念。

2、在32位机器下,指针的大小始终为4字节,而引用的大小是取决于实体对象的类型大小。

3、指针可以不初始化,但是引用必须初始化。

4、有二级指针的概念,但是没有二级引用的概念。

5、指针可以更改其指向,但是引用初始化后就不能更改他的“指向”了。

6、在运算中,对指针自加1操作是让指针指向原本地址的后一位,引用自加1是数值+1。

7、由于指针涉及到空指针以及非法空间的访问的情况,因此引用相比于指针会更加安全。

8、在语法上,指针会开辟一块空间用于存放地址,而引用不会另外再开辟一块空间。

9、若要更改实体变量本身,用指针的方法则要先解引用后才能找到变量,才能更改变量的值。用引用的方法无需解引用,直接更改“别名”的值,就能更改变量的值。

结语:

????????以上就是关于C++_引用的讲解,引用与指针之间有微妙的关系,他们的目的基本都相同,都是间接的更改实体变量的值。熟悉引用的使用场景可以更好的理解引用的意义,最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!( ̄︶ ̄)↗ 

文章来源:https://blog.csdn.net/efls111/article/details/134879533
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。