C++/语法@引用

本篇文章篇幅过长,但是希望读者能一点点边看边思考的看下去。我相信,一定会让你对于引用这一语法有所收获的。

引用

概念

  在 C++ 中,引用是一个别名,是对一个已存在的变量提供另一个名称。引用可以被认为是变量的一个别名,通过这个别名可以访问和修改变量存储的数据。引用提供了一种简洁的方式来操作变量,同时避免了指针的一些复杂性。

精简:

1、引用是对已有的变量另外起一个新的名称
2、新的名称有权限访问修改被引用的变量。



引用特性

1、引用在定义时必须初始化, 如下:

#include <iostream>
int main()
{
	int a = 10;
	//int& b;    // 编译器报错编译不通过,引用时没有初始化。
	int& c = a;   // 引用时初始化,编译成功
}

2、一个变量可以有多个引用,如以下,b、c、d都是变量a的引用

#include <iostream>
int main()
{
	int a = 10;
	int& b = a;
	int& c = a;   
	int& d = a;
}

3、引用一旦引用一个实体,再也不能引用其他的实体。如:

#include <iostream>
int main()
{
	int a = 10;
	int& b = a;
	int c = 6;
	//int& b = c;   // 编译报错,b已经是变量a的引用,不能再当任何其他的实体的引用
}



定义形式:


引用的使用形式type& referenceName = originalVariable;
其中,

type:       被引用的变量类型
referenceName : 被引用的变量新的名称
originalVariable:  被引用的变量


(club:不要忘了类型后面的“ & ”,
  C语言中是取地址的操作符。在C++中还是引用的操作符,区别是:
不带类型,在变量、对象前面的 “ & ”,是取地址
带类型的,在变量、对象的前面 “ & ”,是引用
ps:在C++中,定义出来,并且开空间的,都可以叫对象


如以下例子:
#include <iostream>
 
int main()
{
	int a = 10;    // 定义变量a
	int& b = a;	   // 引用,给变量a起个别名b
	return 0;
}

在程序中,定义了一个 int 类型的变量a,并且初始化为10。
然后,对变量a引用,给变量a起了个别名为b。我们可以说 “ b 是 a 的引用 ”,也可以说“ b 是 a 的别名 ”,两者说法本质上是一个意思,通常习惯性说第二种说法,b是a的别名。接着,我们进行以下实验:
注意:在这个程序中,“b” 是一个引用变量,当它并不是一个独立的变量,而是“a” 的别名


#include <iostream>
 
int main()
{
	int a = 10;    // 定义变量a
	int& b = a;	   // 引用,给变量a起个别名 b
	cout << "a: " << a << endl;	 // 输出打印变量a的值
	cout << "b: " << b << endl;  // 输出打印变量a的别名,b的值
	
	b = 20;				// 更改变量a的别名b的值
	cout<<endl;			// 换行
	cout << "a: " << a << endl;	// 输出打印变量a的值
	cout << "b: " << b << endl; // 输出打印变量a的别名,b的值	

	return 0;
}

运行结果:


在这里插入图片描述

从控制台的输出打印结果中,我们可以发现以下两点:
声明定义了变量a的别名b后,打印 a 和 b 的值,发现变量a的别名b的值,和变量a的值相同。
对变量a的别名b的值进行修改后,打印 a 和 b 的值,发现 b 的值变为修改后的值,同时 a 的值也变为了 b 修改后的值。

对此,我们做一个大胆的假设,别名b指向的地址,就是变量a的地址。即 a 和 b 的地址是相同的。是不是呢?通过调试发现如下:

调试结果

调试结果,证明了我们的假设,成立。


总结:

图形结合
  如图所示,我们在程序中,创建了变量a,同时初始化为10。编译器分配内存地址 0x00f9fafc 给变量a。
然后,我们又对变量a进行引用,起了个别名b。如图所示,a的别名b就是地址0x00f9fafc 的另一个名称。
这里,b对这块地址空间具有访问和更改的权限,因此当我们对别名b的值更改为20时,0x00f9fafc 这块地址上存储的10,也就被更改为了20。所以当我们打印a的值时,输出打印结果也为20。因为变量a就是这块地址空间起初的名称。


至此,对于引用的概念、使用的形式以及原理有了一定的了解。让我们想这么个问题,

引用有什么用处?C++中弄出这么个语法功能作用是什么?

抱着这么些个问题,进入下面的学习,引用能做什么、分别的作用以及效果。




引用做参数

概念

使用引用作为函数的参数。如:

#include <iostream>

void Swap(int& x1,int& x2)
{}

在函数Swap中,参数x1、x2都是引用,当有实参传过来时,x1、x2便是实参的引用。这种传递称作引用传递

这里,在我学习时便有个疑问? 调用函数时,实参变量传过去给函数形参的是什么?我们一起来思考这个问题。在那之前,我们先回忆一下C语言中值传递的细节。

首先我们知道,在C语言中学习的函数参数传递为,值传递,如下:

#include <iostream>

void Swap1(int x1,int x2)
{}
int main()
{
	int a = 10;
	int b = 20;
	Swap1(a,b);
	return 0;
}

  函数Swap1被调用时,变量a和b传过去的是变量a和b各自的拷贝副本。Swap1函数会给形参x1和x2分别分配一块空间,形参x1和x2分别接收到是来自实参变量a和变量b各自的拷贝副本。因此在函数中对形参x1和x2进行更改时,并不会对Swap1函数外的变量a和b产生影响( 因为形参和实参地址不同 )。
  那么,引用作为函数的形参时呢?调用函数时,实参传过去的是什么?很明显不可能拷贝副本了。因为当函数形参为引用时,我们对形参更改,函数外的实参,也就是调用函数时传入的变量也会被更改,这跟上面的值传递是不一样的。我们对此进行实验:

#include <iostream>
using namespace std;

void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a,b);
	cout << a << " " << b << endl;   // 输出打印变量a和b的值
	return 0;
}

  在上面程序中,我们调用Swap函数,在参数为引用的函数里面将x1和x2的值进行了交换。之后将实参,即变量a和b的结果输出打印。结果如下:

在这里插入图片描述

  结果显示,变量a和b的数值也发生了交换,这与我们前面的学习不谋而合。这是必然的,因为函数Swap的参数x1和x2分别是传过来的实参变量a和b的引用。即a是x1的别名,b是x2的别名。
根据前面的学习,我们知道对x1和x2进行更改时,a和b也会被更改。
如若还想更进一步验证,不妨利用调试,查看变量a和b以及形参x1和x2。如我们上面的学习思路正确的话,变量a 和形参 x1 的地址一致,变量b和形参x2的地址一致。调试结果如下:

在这里插入图片描述

验证正确。

回到我一开始提到的问题,函数调用时,变量a和b传过去的是什么?形参x1、x2分别引用的是什么?
首先,排除值传递中的传递拷贝副本。经过我查找,我得到了以下的观点:
  在C++中,当声明一个变量时,变量在被内存中被分配了一个地址。这个地址就是该变量的引用。当我们调用函数使用引用传递时,变量传递给函数的,实际上就是将变量的引用( 也就是地址 )传递给了函数参数。
  所以在上面程序中,变量a和b是已经存在的变量,在内存中有着相应的地址,这个地址也可以叫做它们的引用。当调用Swap函数时,变量a和b传过去的就是各自的引用,也就是各自的地址。函数形参中接收到实参时,可以理解为int& x1 = a (传过来的是a的引用/地址) , int& x2 = b(传过来的是b的引用/地址)。这个过程叫做引用传递

至此,我们知道了函数参数作为引用时,变量传递过程是怎样进行。下面继续学习引用的作用。



作用

一样的,拿上面举的例子:

#include <iostream>
using namespace std;

void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a,b);
	cout << a << " " << b << endl;   // 输出打印变量a和b的值
	return 0;
}

引用的作用:
①:可以作为输出型参数。所谓的输出型参数就是,一种在函数中用于返回结果的参数。这样的参数允许函数修改调用者提供的变量的值,以便将计算的结果传递回调用者。输出型参数通常通过引用或指针传递,使得函数能够直接修改调用者提供的变量。
  我们发现,引用作为函数参数时,作用和指针有点类似。通过外部的函数Swap能够更改main函数内的变量。但是切勿将引用和指针等价了,二者是有所区别的,后面单独阐述二者的区别。


②:引用传递值传递效率更高。因为引用传递比起值传递,少了拷贝副本。我们对此做个实验,见如下代码:

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

struct A {
	int a[10000];
};
// 结构体A的大小是40000 byte

void TestFunc1(A a) {}			// 值作为参数
void TestFunc2(A& a) {}			// 引用作为参数

int main()
{
	A a;

	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; i++)
		TestFunc1(a);
	size_t end1 = clock();

	//以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; i++)
		TestFunc2(a);
	size_t end2 = clock();

	cout << "TestFunc1(A) - time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&) - time:" << end2 - begin2 << endl;
	//以引用作为函数参数 比 以值作为函数参数 性能更高
	return 0;
}
}

运行结果:

运行结果


通过控制台输出打印可以看到,**引用传递** 比 **值传递** 的性能要高得多。下面简单分析一下程序流程。

程序分析:
1、我们在程序中,定义了一个结构体A,结构体A里面申请了一个int类型的数组,数组有10000个元素。整个的结构体A大小为40000字节。

2、定义了两个函数,一个使用变量参数,一个使用引用参数。

3、然后在main函数中,分别记录两个函数传入结构体变量a时,多次调用各自调用的时间。因为值传递时需要拷贝副本,而引用传递直接传的引用(也就是地址)。所以引用传递的耗费的时间要比值传递耗费的时间要少得很多。


至此,对于引用作为参数时的作用的学习大致完成,但是在结束之前,不知大家有没有和我一样有一个疑问?

问题:
那就是前面引用概念上说到,引用定义时必须初始化,那为什么作为函数参数时不需要呢?大家可以想一想,文章结尾给出答案。

效果

如上面所举的例子:

#include <iostream>
using namespace std;

void Swap(int& x1,int& x2) // int& x1 = a, int& x2 = b;
{
	int tmp = x1;
	x1 = x2;
	x2 = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	Swap(a,b);
	cout << a << " " << b << endl;   // 输出打印变量a和b的值
	return 0;
}

运行效果:

在这里插入图片描述

  我们可以通过引用传递,达到跟指针一样的效果。将变量a和b传参给外部函数Swap,在外部函数中实现变量的更改。

下面来看看,引用作为返回值的情况。






引用做返回值

概念

引用作为函数的返回值。如:

int& func(int i)    // 返回值 int& 为引用
{
	return i;
}

其中函数func的返回值类型为,int& ,也就是引用。



作用

首先,并不是所有的返回值都适合作为引用的。有两种情况适合引用作为返回值,
返回值是全局变量
返回值时静态变量

在讲述这两种情况前,我们先对了解一下其他情况的引用返回值的弊端,这样更有利于我们的理解。如:

#include <iostream>
using namespace std;

//		2、引用做返回值
int Count1()
{
	int n = 0;
	n++;

	return n;  // 传值返回   int tmp = n;
}

int& Count2()
{
	int m = 0;
	m++;

	return m;	// 传引用返回  int& tmp = n;
}

int main()
{
	//		传值会产生一个拷贝变量(临时变量具有常性),传引用则不会	
//	int& r1 = Count1();//编译不通过,因为Count1返回的是拷贝了n的值的临时变量,
					  //临时变量具有常性,只能读,此处r1为int类型可读可写,属于权限放大
	const int& r1 = Count1();
	int& r2 = Count2();

	return 0;
}

我们先来对以上程序进行分析:
1、在程序中,定义了两个函数Count1 和 Count2。两个函数各自都返回了函数内定义的局部变量。其中Count1的返回是值返回,Count2的返回是引用返回

2、我们在main函数中有三行代码,我们逐行进行分析,第一行代码

int& r1 = Count1();

这行代码中,会先调用函数Count1,函数Count1返回了int类型的变量,变量存储的是函数内局部变量n的值,然后给这个变量做引用,即起个别名为r1。结果显示编译器会报错,编译不通过。

原因:首先我们要清楚,函数Count1中,return n 时,返回的并不是局部变量n自身,而是编译器创建的一个临时变量,我们假如临时变量记为tmp。临时变量tmp的类型为函数返回值的类型int,然后编译器会将局部变量的值拷贝给临时变量tmp,即 int tmp = n。最终效果是函数想要返回局部变量n的值,但是实际上返回的是临时变量tmp,tmp中拷贝了n的值。
所以这行代码会演变为:int &r1 = tmp;r1是临时变量tmp的别名。这将出现两个问题:
①、临时变量具有常性( 只能读不能写 ),而这是引用r1的类型是int,是可读可写的。引用赋值时有权限大小的问题,现在我们将一个只能读不能写的类型 赋值给 可读可写的类型,这将会导致权限的放大。而权限可以缩小,但是却不能放大,因此编译器会报错不通过,这时问题一。

②、还有一个问题需要注意,那就是临时变量tmp作用完后是会被编译器销毁的,也就是空间地址使用权会被编译器收回。而这时引用r1依旧是“指着”这块地址。这时不管我们是利用r1去访问还是更改这块空间上的数值,都将导致程序崩溃,因为非法访问了属于编译器的空间地址。因此r1这时是一个危险引用,也叫做悬空引用(dangling reference),这是问题二。

如何解决这两个问题?
问题一,权限放大的问题,我们可以将引用的权限一开始便缩小,与右边的权限做匹配,也就是第二行代码

const int& r1 = Count1;

此时引用r1的类型是 const int,和函数返回类型的权限一致,便能解决权限的问题。


问题二:危险引用的问题,解决方法就是,让函数内的局部变量n,变为静态变量( 在前面加个static );或者将局部变量n改为全局变量,或者接收返回值时不适用引用,这留到待会再讲。


我们接着来看第三行代码

int& r2 = Count2();

函数Count2内返回的是局部变量m,一样的编译器会创建一个临时变量tmp,临时变量类型为int& ,即 int& tmp = m;看到这里我们知道,此时临时变量tmp是局部变量m的引用,是m的别名。因此第三行代码会转变为: int& r2 = tmp; 即给临时变量tmp起个别名r2,而临时变量tmp本就是局部变量m,因此相当于有给Count2内的局部变量m起了个新的别名r2。即局部变量m有两个别名,tmp和r2。这时会有权限的问题吗?不会,因为tmp是m的别名,相当于tmp就是m,r2是tmp的别名,就相当于又给m的起一个别名,而局部变量m的类型是 int 可读可写的类型,因此不存在权限放大的问题。但是 ,依旧会存在危险引用的问题。不管是创建的临时变量tmp( 但是是作为m的引用了 ),还是 r2 ,都是m这块地址上的名称。访问操作的都是这块空间地址,但是局部变量出了作用域后,便会被编译器销毁,即收回空间地址。因此和上面问题二一样的问题,危险引用的问题。


至此,对于函数返回值作为引用时,返回值不是静态变量或全局变量的情形做了一个大概的了解。下面学习一下,返回值适用当引用的情况,即返回的变量是静态变量或者全局变量。



返回全局变量时的,返回值引用

看以下示例:

#include <iostream>
using namespace std;

int global_val = 0;   // 全局变量,初始化为0

int& func()
{
	global_val = 10;

	return global_val;
}


int main()
{
	cout << global_val << endl;
	func();
	cout << global_val << endl;

	int& f1 = func();
	f1 = 20;
	cout << global_val << endl;

	return 0;
}

运行结果:

在这里插入图片描述

程序分析:
在程序中,声明定义了一个全局变量 global_val 并且初始化为0。func函数里将全局变量global_val 赋值为10。
在main函数中,一开始输出全局变量global_val 的值;然后调用函数func,再输出全局变量的值;再给func函数的返回值做个引用f1,对f1赋值为20,然后输出打印全局变量的值。
输出打印结果为0,10,20。
0 和 10 都很容易理解, 我们着重讲一下20的由来:
根据前面的学习,我们知道在func函数中,返回全局变量global_val 时,实际上为 int& tmp = gloval_val ,即返回的是 gloval_val的引用,一个别名。f1 又是tmp的引用,而tmp是全局变量的别名,再给tmp做引用起个别名,相当又全局变量global_val的起了一个新的别名。这时全局变量global_val这块地址上有三个名称,分别是global_val 、 tmp 、 f1。这时我们对f1进行更改,相当在更改global_val全局变量这块地址上的数值,因此输出打印global_val 变量地址上的数值时,为更改后的20。


这便是返回值是全局变量时,返回值做引用的效果。下面看看静态变量是怎么样的。
返回静态变量时,返回值引用

看以下示例:


#include <iostream>
using namespace std;

int& func1()
{
	static int val = 0;

	return val;
}

int main()
{
	int& b = func1();
	b = 20;
	func1();

	return 0;
}


为了方便观察,我们利用调试,查看函数内局部的静态变量val。

调试结果:

在这里插入图片描述


根据前面的知识,我们能知道,函数func1返回值是编译器创建的临时变量tmp,即int& tmp = val。tmp是静态局部变量val的别名,又对tmp进行引用,int& b = tmp。相当于又给静态局部变量val起了个新的别名b。对b进行更改时,其实改的就是静态局部变量val地址上的值。而静态局部变量是存储在静态区的,因此并不会因为出了func函数的作用域而被销毁。因此可以放心的引用。



总结:

  当使用引用作为返回值时,需要注意,返回值的作用域问题,会不会又危险引用的情况,有则要避免。当返回值的变量不受函数作用域的影响时,能尽量用引用作返回值尽量使用。因为根据上面的学习,我们知道利用引用作返回值时,比起值返回而言少了拷贝副本的流程,这无疑是更好的选择。


下面来看看引用和指针的区别,或者说引用相对于指针的优势在哪。




引用比起指针的区别/优势

1、引用概念上定义一个变量的别名。指针存储一个变量地址

2、引用在定义时必须初始化( 作为函数时可以不用初始化 )。指针没有要求

3、引用在初始化时引用一个实体后,就不能再引用其他实体。而指针可以在任何时候指向任何一个同类型实体

4、没有NULL引用,但有NULL指针

5、在sizeof中含义不同:引用结果为引用类型的大小。但指针始终是地址空间所占字节个数( 32位平台下占4个字节

6、引用自加即引用的实体增加1。指针自加即指针向后偏移一个类型的大小

7、有多级指针,但是没有多级引用

8、访问实体方式不同,指针需要显式解引用。引用编译器自己处理

9、引用比指针使用起来相对更安全


语法概念上:b是a的别名,不开空间
底层实现上:(去汇编里查看,调试右键有个‘转汇编 ’选项),引用跟指针的实现是一样的,是存地址去实现的
所以,语法是语法,底层是底层




下面是我罗集到的信息,就不多加修改直接给大家作参照思考了:

在 C++ 中,使用引用和使用指针都允许函数修改调用者提供的变量。但是,引用和指针之间有几个关键的区别,这些区别可以影响代码的可读性、安全性和使用方式。

  1. 空指针问题:

①.引用不能为 null,它必须始终指向一个合法的对象。这有助于避免由空指针引起的错误。
②.指针可以为空(null),这意味着在使用指针之前必须检查指针是否为 null,以避免在尝试访问空指针时出现未定义的行为。

  1. 语法和使用:

③.使用引用时,不需要像指针一样使用解引用运算符(*)来访问所引用的对象。
④.指针需要通过解引用运算符来访问所指向的对象。

  1. 安全性:

⑤.引用通常被认为比指针更安全,因为它们在创建时必须被初始化,并且无法被重新指向其他对象。一旦引用被初始化,它将始终引用同一个对象。
⑥.指针可以在程序运行时重新指向不同的对象,这可能导致错误,例如在没有适当检查的情况下修改了不应该修改的对象。

  1. 传递参数:

⑦.传递参数时,引用更直观且更易读,因为函数参数的语法更简洁。
⑧.指针需要额外的解引用运算符和取址运算符,可能使得代码看起来更复杂。

综上所述,引用通常更安全且更易用,因为它们在语法上更简洁,不会为空,并且不会被重新指向其他对象。然而,在某些情况下,指针提供了更灵活的操作,例如动态分配内存或者需要在函数中传递空值的情况。选择使用引用还是指针取决于特定的情况和需求。




结局彩蛋:

回答一开始留的问题:引用定义时必须初始化,那为什么作为函数参数时不需要呢?

答:

  当引用作为函数参数时,不要求立即初始化。这是因为函数参数的引用,会在函数调用时被绑定到传递给它的对象上。也就是说,函数参数的引用声明时定义时虽然没被初始化,但是在函数被调用时会被绑定到相应的实参。
我的理解是,编译器在编译时,检查语法的时候,对引用会强制要求一定要有绑定的对象。对于绑定到创建的变量的引用时,编译器编译到时发现引用没有初始化绑定对象就会立马报错。而对于函数来说,如果没有被调用,那么编译器便不会去编译检查函数的语法( 虽然函数写了,但是没用到程序没用到时,编译器编译的时候是不会白费功夫去检查的 ),也就不存在引用没对象的问题。
  而当函数被调用时,编译器检查语法的时候,这时函数的参数的引用又自动绑到传过来的实参上了,同样的也就不会出现引用没绑定对象的情况

关键:编译(检查语法)时,引用必须绑定对象

以上便是我个人的见解,如有不对,可以点拨改正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值