【C++】C++引用

发布于:2022-11-15 ⋅ 阅读:(653) ⋅ 点赞:(0)

前言

对于习惯使用C进行开发的朋友们,在看到c++中出现的&符号,可能会犯迷糊,因为在C语言中这个符号表示了取地址符,取地址符常常用来用在函数传参中的指针赋值引用是C++引入的新语言特性,是C++常用的一个重要内容之一。在C++中它却有着不同的用途,掌握C++的&符号,是提高代码执行效率和增强代码质量的一个很好的办法。

一、引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空

间,它和它引用的变量共用同一块内存空间。

引用的声明方法:类型标识符 &引用名=目标变量名;

类型**&** 引用变量名**(对象名) =** 引用实体;

#include <iostream>
using namespace std;

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

在这里插入图片描述

注意:引用类型必须和引用实体同种类型

【说明】

​ (1)&在此不是求地址运算,而是起标识作用。

(2)类型标识符是指目标变量的类型。

(3)声明引用时,必须同时对其进行初始化。

(4)引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,且不能再把该引用名作为其他变量名的别名。ra=1; 等价于 a=1;

(5)声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元系统也不给引用分配存储单元。故:对引用求地址,就是对目标变量求地址。&ra与&a相等。

(6)不能建立数组的引用。因为数组是一个由若干个元素所组成的集合,所以无法建立一个数组的别名。

二、引用的特性

1.引用在定义时必须初始化

2.一个变量可以有多个引用

3.引用一旦引用一个实体,再不能引用其他实体

int main()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错
   int& ra = a;
   int& x = a;
   int& y = a;
   
    x++;
    y++;
    a++;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

<在这里插入图片描述

三、常引用

常引用声明方式:const 类型标识符 &引用名=目标变量名;

用这种方式声明的引用,不能通过引用对目标变量的值进行修改,从而使引用的目标成为const,达到了引用的安全性。

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量
    const int& ra = a;
    // int& b = 10; // 该语句编译时会出错,b为常量
    const int& b = 10;
    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同
    const int& rd = d;
}

权限可以缩小和平移,但不可以放大。

一个只读的变量,不能用可读可写的变量做引用,这样权限放大了。

四、引用的使用场景

1.做参数

以前的C语言中函数参数传递是值传递,如果有大块数据作为参数传递的时候,采用的方案往往是指针,因为 这样可以避免将整块数据全部压栈,可以提高程序的效率。但是现在(C++中)又增加了一种同样有效率的选择(在某些特殊情况下又是必须的选择)。

引用做参数的好处:

1.减少拷贝,提高效率。

2.输出型参数,函数中修改形参,实参也修改了。

void Swap1(int& left, int& right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

void Swap2(int left, int right)
{
	int tmp = left;
	left = right;
	right = tmp;
}

int main()
{
	int a = 1, b = 2;
	Swap1(a, b);
	Swap2(a, b);
    return 0;
}

<在这里插入图片描述

​ (1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给 形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效 率和所占空间都好。

(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的 形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

引用传参,形参是实参的别名,不需要拷贝,也不开辟空间。

传值或者传址传参,要发生临时拷贝,形参的实参的拷贝。

//减少拷贝
//一般引用做参数都用const引用

void Func(const int& x)
{

}

int main()
{
	int a = 0;
	//权限平移
	int& ra = a;

	//指针和引用中赋值中,权限可以缩小,但不可以放大
	const int b = 1;

	//拷贝
	a = b;

	//我引用你,权限放大,不行
	//int& rb = b;


	//我引用你,我的权限缩小,可以
	const int& rra = a;
	//rra++;
	a++;


	//权限平移
	const int& rrb = b;

	Func(a);
	Func(b);
	Func(rra);
	Func(rrb);
	return 0;
}

指针和引用中赋值中,权限可以缩小,但不可以放大。

const引用做参数,有没有const修饰的变量都可以传递,这里只有权限平移或缩小。

int main()
{
	const int a = 10;
	double d = 12.34;

	cout << (int)d << endl;

	const int& ri = d;//可以
	return 0;
}

类型转换,提升,截断都会产生临时变量,临时变量具有常性。

在这里插入图片描述

语法上面,ra是a的别名,不开空间

底层实现,引用是使用指针

在这里插入图片描述

2.做返回值

要以引用返回函数值,则函数定义时要按以下格式:

类型标识符 &函数名(形参列表及类型说明)
{函数体}

说明:

(1)以引用返回函数值,定义函数时需要在函数名前加&

(2)用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}
int main()
{
	int& ret = Add(1, 2);
	cout << ret << endl;
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}

<在这里插入图片描述

第一次Add函数运行结束后,该函数对应的栈空间就被回收了,即c变量就没有意义了,在main中ret引用Add函数返回值,实际引用的就是一块已经释放的空间。

第二次Add函数运行结束后,该函数对应的栈空间被回收了,即c变量就没有意义了,注意空间被回收指的是空间不能使用了,但是空间本身还在,而ret引用的c位置被修改成7了,因此ret的值就改变了。

【注意】

1.函数运行时,系统需要给该函数开辟独立的栈空间,用来保存该函数的形参,局部变量以及一些寄存器信息等。

2.函数运行结束后,该函数对应的栈空间就会被系统回收了。

3.空间被收回指该块空间暂时不能被使用,但是内存还在,比如:上课申请教室,上完课之后教室归还学校,但是教室本身还在,不能说归还了之后,教室就没有了。

4.如果函数返回时,出了函数作用域,如果返回对象还在**(还没还给系统)****,则可以使用**

引用返回,如果已经还给系统了,则必须使用传值返回。

在这里插入图片描述

#include <iostream>
using namespace std;

//引用返回
int& Count()
{
	static int n = 0;
	n++;
	//...
	return n;
}

//传值返回
int Count()
{
	static int n = 0;
	n++;
	//...
	return n;
}

//引用返回
int& Count()
{
	int n = 0;
	n++;
	//...
	return n;
}

//传值返回
int Count()
{
	int n = 0;
	n++;
	//...
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

对于第一、二个函数,n的值存放在静态区(代码段)中,函数栈帧销毁了之后n还在,可以用引用返回、也可以使用传值返回,传值返回是将n的值存放到一个临时变量中再返回。

对于第三个函数来说,函数调用结束,函数栈帧销毁,再使用传值返回就会越界访问,返回值就是一个不确定的值,所以不能这样使用。

对于第四个函数来说,传值返回的是n拷贝到临时变量的值(该过程发生在函数栈帧销毁之前)。所以没有问题。

在这里插入图片描述

【注意】

内存空间销毁之后,空间还在,只是使用权不是我们的,我们存的数据不被保护,我们还能够访问,只是我们读写的数据都是不确定的。

内存申请和释放,就像住酒店。空间读写数据,就像在房间寄存的东西。

【结论】

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

出了函数作用域,返回变量存在,才能用引用返回。

五、性能比较

1.传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直

接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效

率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

比如一下例子:

#include <iostream>
#include <time.h>
using namespace std;
struct A
{
	int a[10000]; 
};
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2(a);
	size_t end2 = clock();
	// 分别计算两个函数运行结束后的时间
	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

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

在这里插入图片描述

2.值和引用的作为返回值类型的性能比较

传值和指针在作为传参以及返回值类型上效率相差很大

比如下面的例子:

#include <iostream>
#include <time.h>
using namespace std;
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
	// 以值作为函数的返回值类型
	size_t begin1 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 100000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

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

在这里插入图片描述

六、引用和指针的区别

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

int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}

在这里插入图片描述

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

int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}

我们来看下引用和指针的汇编代码对比:

在这里插入图片描述

引用和指针的不同点:

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

2.引用在定义时必须初始化,指针没有要求

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

一个同类型实体

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

5.sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32

位平台下占4个字节,64位平台下占8个字节)

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

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

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

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

七、总结

(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。

(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。

(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。

5.sizeof中含义不同引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32

位平台下占4个字节,64位平台下占8个字节)

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

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

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

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