【C++入门】引用详解(引用的特性、引用的使用、引用与指针的区别)

  • Post author:
  • Post category:其他





1 引用概念


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



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



注意:

引用类型和引用实体是同种类型的。

引用示例:

引用示例图




2 引用特性


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


    示例:

    引用初始化


  2. 一个变量可以有多个引用,也可以对引用再进行引用(或者说可以为变量取多个别名,也可以为别名再取别名)


    示例:

    引用示例


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

示例




3 使用场景


  1. 引用做参数
void Swap(int& num1, int& num2)
{
	int temp = num1;
	num1 = num2;
	num2 = temp;
}

示例


  1. 引用做返回值


前言:传值返回

  • 当用值作为返回值时,不是直接将变量返回,而是返回变量的一份临时拷贝(临时变量)。考虑到对于函数中的局部变量,在调用完函数时即被销毁,此时需要返回的变量也会被销毁(空间不再属于该变量),因此选择在销毁前将变量值拷贝到一个临时变量中(这个临时变量一般由寄存器(4/8个字节大小)充当,如eax;如果返回值过大,则会选择在上层栈帧中创建临时变量),再将临时变量作为返回值返回。

    示例

  • 那如果作为返回值的变量出作用域后不会被销毁,那还会产生临时变量吗?

    事实上,编译器不管需要返回的变量出作用域后会不会被销毁,在返回时都会产生临时变量。


    示例


传引用返回

  • 那既然需要返回的变量都不会被销毁,将其再拷贝到临时变量中是不是麻烦了些?因此,在下面我们以引用作为返回值,但这不是说就不会创建临时变量了,临时变量依然会被创建,但这个临时变量相当于返回变量的一个别名,因为该变量出作用域后不会被销毁,因此我们依然可以进行访问,此时临时变量与返回变量共用一块空间,相比于传值返回,中间就减少了拷贝的过程,返回时同样将临时变量赋值给接收的变量即可,可以认为传引用返回就相当于将变量直接返回了。

    示例

    下面再看一段示例代码:

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

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

示例输出结果:

示例


说明:对于以出了作用域即被销毁的变量作为返回值的,如果使用传引用返回实际上是无意义的,因为局部变量的空间已被销毁,不再属于该变量,再通过传引用返回的别名去访问该空间时,其值是不确定的,无法确保得到的还是正确结果。

引用做返回值示例


注意:如果函数返回时,出了函数作用域,如果返回对象还在(空间还没还给系统,如:静态变量、全局变量、上一层栈帧中的变量、malloc开辟的等等),则可以使用引用返回,否则,必须使用传值返回。

  • 传引用返回还可修改返回值。

    示例代码:

#define N 10
typedef struct Array {
	int a[N];
	int size;
}AY;

//查找数组中某个下标位置的元素
int& PosAt(AY& ay, int i) {
	assert(i < N);//保证不越界
	return ay.a[i];
}

int main() {
	AY ay;
	for (int i = 0; i < N; i++) {
		PosAt(ay, i) = i * 10;//用返回的别名进行修改
	}

	for (int i = 0; i < N; i++) {
		cout << PosAt(ay, i) << " ";
	}
	cout << endl;
	return 0;
}

示例输出结果:

示例




4 常引用


指针和引用,在赋值和初始化时,权限可以保持或缩小,但不能放大。

示例代码:

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

void TestConstRef() {
	//示例1
	const int a = 10;//const修饰的变量,具有常属性,不可修改
	//错误语法,权限被放大。ra引用与a共用一块空间,而a具有常属性,
	//如果ra引用不具有常属性,则修改ra的值时则违背了a的常属性
	//int& ra = a;//编译时会报错
	const int& ra = a;//正确语法

	//示例2
	//int& b = 10;//错误语法,10为常量,引用权限放大
	const int& b = 10;//正确语法,使引用变量b也具有常属性

	//示例3
	//Count函数是传值返回的,返回的是临时变量,临时变量具有常属性
	//int& ret = Count();//编译报错
	const int& ret = Count();//正确语法,ret为返回的临时变量的常引用
	int ret2 = Count();//正确语法,将返回的临时变量赋值给ret2

	//示例4
	double d = 3.14;
	//int& rd = d;//编译报错,引用类型与引用实体不是同一种类型
	//解释:
	//在进行类型转换(无论是强制类型转换还是自动类型提升)时都会产生临时变量
	//如有int i = 0; (double)i:其并不是直接将int型变量i转换为double型,
	//而是通过一个临时变量将i转化为double,如果是double d = i;则是再将临时变量赋值给d
	
	//下句代码则表示rd为double型变量d强制转换为int型变量时产生的 临时变量 的常引用
	//但注意,rd本质上并不是d的引用,二者也不是共用一个空间
	//d仍是double型变量,rd则为int型
	const int& rd = d;//编译通过
	printf("d = %f\nrd = %d\nd的地址为:%p\nrd的地址为:%p\n", d, rd, &d, &rd);
}

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

示例




5 传值与传引用的效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时拷贝,因此用值作为参数或返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低了。

下面用两段测试代码来比较传值与传引用的效率:



5.1 值和引用作为函数参数的性能比较

测试代码:

#include <iostream>

using namespace std;

#include <time.h>
struct A { int a[100000]; };
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;
}

测试结果:

性能比较结果

分析:从测试结果来看,值作为参数的函数运行时间要比引用作为参数的函数运行时间多得多,以引用作为参数能很好提升程序运行效率。



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

测试代码:

#include <iostream>

using namespace std;

#include <time.h>
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 < 1000000; ++i)
		TestFunc1();
	size_t end1 = clock();
	// 以引用作为函数的返回值类型
	size_t begin2 = clock();
	for (size_t i = 0; i < 1000000; ++i)
		TestFunc2();
	size_t end2 = clock();
	// 计算两个函数运算完成之后的时间
	cout << "TestFunc1 time:" << end1 - begin1 << endl;
	cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

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

测试结果:

测试结果

分析:从测试结果来看,值作为返回值类型的函数运行时间要比引用作为返回值类型的函数运行时间多得多,以引用作为返回值类型能很好提升程序运行效率。




6 引用和指针的区别



  • 语法概念上

    引用就是一个别名,没有独立空间,和其引用实体共用一块空间。而指针是有开辟独立空间的,空间中存放的是指向的变量的空间地址。

    示例代码:

int main()
{
	int a = 10;
	int& ra = a;//引用a
	int* pa = &a;//指向a
	cout << "&a = " << &a << endl;
	cout << "&ra = " << &ra << endl;
	cout << "&pa = " << &pa << 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个字节)。
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个所指向实体类型的大小。
  7. 有多级指针,但没有多级引用。
  8. 访问实体方式不同,指针需要显示解引用,引用是由编译器自己处理。
  9. 引用比指针使用起来相对安全。不会有类似空指针等的问题。

以上是我对C++中引用相关的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!



版权声明:本文为qq_67216978原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。