c++的引用你真的了解吗?你知道引用相比于指针有什么优势吗?引用真的能完全替代指针吗?

发布于:2022-10-17 ⋅ 阅读:(442) ⋅ 点赞:(0)

目录

  <一>引用的概念

<二>引用的特性

<三>常引用(重要)

<四>引用的使用场景(灰常重要)

1,做参数

2,做返回值

1,怎么引用返回的呢?

2,引用返回和传值返回的区别

3,引用返回的对象为什么非要是静态的,出作用域不销毁的对象。

 <五>传值、传引用效率比较

<六>值和引用作为返回值类型的性能比较

<七>指针和引用的区别

  <一>引用的概念

先来介绍一下引用的基本概念吧,什么是引用呢?引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。

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

注意:引用和实体必须是同一类型。

<二>引用的特性

1,引用在定义的时候必须初始化。

2,一个变量可以有多个引用。

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

void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错//引用在定义的时候必须初始化。
   int& ra = a;
   int& rra = a;
   int& rrra = rra;//这种方法也是可以的。
   printf("%p %p %p\n", &a, &ra, &rra); //这样打印的地址都是相同的。
   //这也间接说明了引用只是给变量取了一个别名没有真正去创建空间。

   int b = 10;
   int& rb = b;
   //int& rb = a;//这样也是不被允许的 (一旦引用了一个实体就不能更改),和指针有很大的区别。

}

<三>常引用(重要)

常引用就是引用常数或者const修饰的变量(具有常属性)。先给大家介绍一下这里有关权限的规定。指针和引用在赋值的时候权限可以缩小或者平移,但是不能放大。

void TestConstRef()
{
    const int a = 10;
    //int& ra = a;   // 该语句编译时会出错,a为常量(这里就是放大权限的问题)
    //将一个只可读的变量,变为了一个可读又可改的变量,放大了权限。
    const int& ra = a;//这里是权限的平移


    // int& b = 10; // 该语句编译时会出错,b为常量(这里也是权限的放大)
    const int& b = 10; //平移

    int c = 10;
    const int& rc = c;//这里是权限的缩小没有问题。由可读可写变成了只可读。

    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错,类型不同(本质上并不是类型不同)
    const int& rd = d;//该语句没有编译错误,你知道是为什么吗?类型也是不同为什么能编译通过?
    
}

接下来我们来解释,为什么最后那个会编译通过。在我们强制类型转换的时候,并没有修改原来的变量,而是产生了一个临时变量。修改就是这个临时变量。这个临时变量是具有常属性的(const修饰的)。强转,截断,整型提升,都会产生临时变量。

int main()
{
	double b = 10.98;
	cout <<(int)b << endl;//这里修改是这个临时变量。
	//打印也是这个临时变量。//打印的是10
	return 0;
}

到这里大家懂了吧,为什么上面的不加const就会编译出错呢。

    double d = 12.34;
    //int& rd = d; // 该语句编译时会出错
    const int& rd = d;//该语句没有编译错误
    //不加const 就是权限提升,当然会出错。
    //但是一般不这样用。一般还是要引用相同的类型。大家注意。

注意:函数的传值返回的时候,也会创建一个临时变量,这个临时变量也是具有常属性的,所以建议大家,如果大家想用引用接收的时候要加const。不然会编译出错。

int main()
{
	int& r = Add(2, 3); //这里编译会出错,因为权限放大了,编译器会识别出来。

	const int& r = Add(2, 3);//这里编译通过,没问题
    
    //大家可是编译试一下

	return 0;
}

<四>引用的使用场景(灰常重要)

1,做参数

在C语言中我们传一些 ,输出型参数的时候,一般用传指针的方法。在c++中我们会有更好的方法来代替他。

//指针的方法
void swap(int* a, int* b)//代码的可读性并不好
{
	int tmp = *a;
	*a = *b;
	*b = tmp;
}
//引用的方法
void swap(int& a, int& b)//代码的可读性更好了一点
{
	int tmp = a;
	a = b;
	b = tmp;
}

int main()
{
	int a = 10;
	int b = 20;
	printf("src:a=%d,b=%d\n", a, b);

	swap(a, b);
	printf("swap:a=%d,b=%d\n", a, b);
	a = 10;
	b = 20;
	swap(&a, &b);
	printf("swap:a=%d,b=%d", a, b);
	return 0;
}

编译结果是:

注意:

  • 还有一点,引用作参数的时候,可不可以使用缺省值呢。是可以的,但是会有一些要求。只有const修饰的参数才可以,使用缺省值。原因是:一个引用要指向一个常数。这样会扩大他打权限。所以要用const修饰一下。
    int Add(const int& x1 = 0, const int& x2 = 0)//这种是可以的,但是
    {
    	return x1 + x2;
    }
    int Add(int& x1 = 0, int& x2 = 0)//这种就扩大了常数的权限。不可以的。
    {
    	return x1 + x2;
    }
  • 减少拷贝,提高效率。作为输出型参数,在函数中,形参改变会改变实参。一般引用做参数的(函数中形参不需要改变)都要用const引用

2,做返回值

1,怎么引用返回的呢?

int& count()
{
	static int n = 0;
	n++;
	return n;//注意这里返回值(n)必须出作用域不销毁的,静态的,malloc的,全局的 等等
}

2,引用返回和传值返回的区别

我们知道在传值返回的时候会生成一个临时变量,这个临时变量的类型就是定义函数的时候函数名前面写的东西。所以传值返回定义的那个临时变量的类型是 ’int ‘而传引用返回临时的类型是‘int&' 的,所以两者最大的区别就是

传值返回会开辟空间,而且还要拷贝两次.

传引用返回不会开辟新的空间,拷贝一次(int 变量接收)或者不拷贝(int&引用接收)

//传引用
int& count1()
{
	static int n = 0;//注意这里返回值(n)必须出作用域不销毁的,静态的,malloc的,全局的等等。
	n++;
	return n;
}
//传值
int count2()
{
	int n = 0;//这里是不是静态的无所谓。
	n++;
	return n;
}

int main()
{
	int& ra = count1();//不拷贝。
	int a = count1();//拷贝1次。

	int b = count2();//拷贝2次
	const int& rb = count2();//注意这里如果想用引用接收必须用const修饰。
	return 0;
}

3,引用返回的对象为什么非要是静态的,出作用域不销毁的对象。

这里为什么会这样呢,这就有关函数栈帧有关的东西了,这里的n被引用返回了,但是没有ra是n的别名,但是变量n已经被销毁了,空间已经还给操作系统了,也就意味着这块空间不受保护了,可能会被覆盖。

注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回  

 <五>传值、传引用效率比较

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


#include<time.h>
#include<iostream>
using namespace std;
struct A
{
	int a[10000];
};

void fun1(struct A a){}
void fun2(struct A& ra) {}

int main()
{
	struct A a;
	int begin1 = clock();
	for (int i = 0; i < 10000; i++)
	{
		fun1(a);
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0;i < 10000l; i++)
	{
		fun2(a);
	}
	int end2 = clock();

	cout << "fun1time" << end1 - begin1 << endl;
	cout << "fun1time" << end2 - begin2 << endl;

	return 0;
}

运行结果

<六>值和引用作为返回值类型的性能比较


#include<time.h>
#include<iostream>
using namespace std;
struct A
{
	int a[10000];
};
struct A a;

struct A fun1(){ 
	return a;
}

struct A& fun2() {
	return a;
}

int main()
{
	
	int begin1 = clock();
	for (int i = 0; i < 10000; i++)
	{
		fun1();
	}
	int end1 = clock();

	int begin2 = clock();
	for (int i = 0;i < 10000l; i++)
	{
		fun2();
	}
	int end2 = clock();

	cout << "fun1time" << end1 - begin1 << endl;
	cout << "fun1time" << end2 - begin2 << endl;

	return 0;
}

 运行结果:

引用的意义:

  • 做返回值:1,提高效率,2做返回值(修改返回值 eg: 把顺序表上的一个数返回就可以修改这个数)
  • 做参数:1,做输出型参数,2,提高效率,减少空间的开辟。

注意:

  • 语法上引用ra是变量a的别名,但是底层实现的时候ra时使用指针实现的。 

<七>指针和引用的区别

1、语法上引用是变量的别名,不开空间,而指针存储一个变量地址。但是引用的底层是用指针实现的。

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

3、引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体 。这就导致了C++的引用代替了一部分指针的功能,但不能完全代替。例如链表中的next必须用指针实现。如果采用引用的方式引用节点,插入删除节点要修改next,没办法通过引用改变next指向的值。

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

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

位平台下占4个字节)。

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

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

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

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

 


网站公告

今日签到

点亮在社区的每一天
去签到