• C++特性——引用与指针详解


    引用

    简单来说,引用就是给一个变量起一个别名。例如:

    int a = 1;
    int& b = a;
    
    • 1
    • 2

    对于上面的代码,我们就说ba的别名,我们可以看看ba的地址:

    在这里插入图片描述

    我们可以发现,别名ba共用一块地址,不会开辟新的空间,我们可以将下面三段代码进行比较:

    //代码一
    int a = 1;
    int& b = a;
    
    //代码二
    int a = 1;
    int b = a;
    
    //代码三
    int a = 1;
    int* b = &a;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 代码一:ba别名,前面已经讲过,这里不再赘述

    • 代码二:创建了一个新的整形变量b,并将a的值赋值给b

    • 代码三:创建了一个指针变量b指向变量a

    我们可以画图来更加明确他们的关系:

    在这里插入图片描述

    1. 引用的作用

    1.1 引用可以做函数参数:

    既然我们知道引用就表示给一个变量取别名,那么我们就可以用引用做函数实参,来简化用C语言写时较为复杂的代码,例如:

    • 交换两个数的值Swap
    /*
    	形参a,b是两个别名,和实参共用一个地址。
    	因此形参的改变就会影响实参的改变
    	故可以不要像C语言一样传实参的地址
    */
    void Swap(int& a, int& b)
    {
    	int temp = a;
    	a = b;
    	b = temp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 无哨兵位单链表的尾插SLPushBack
    typedef struct ListNode
    {
    	struct ListNode* next;
    	int val;
    }SL;
    
    //如果不用别名,那么形参就应该是二级指针,只有这样才能改变原本为一级指针的实参
    void SLPushBack(SL*& phead, int data)
    {
    	assert(phead);
    
    	SL* newNode = (SL*)malloc(sizeof(SL));
    	newNode->val = data;
    	newNode->next = NULL;
    
    	if (phead == NULL)
    		phead = newNode;
    	else
    	{
    		SL* cur = phead;
    		while (cur->next)
    			cur = cur->next;
    
    		cur->next = newNode;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    总结:

    引用做函数实参,有以下好处:

    1. 形参的改变可以影响实参,这样可以降低函数的复杂度并提高函数的可读性
    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 << ret << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    大家来想一想,这两次打印的ret的结果分别是什么?

    在这里插入图片描述

    第一次为3大家可能认为没问题,但为什么第二次明明ret没有接收函数Add(3, 4)的返回值,为什么他的值会变成7呢?

    原因是:

    • Add函数的返回值类型是一个引用,即局部变量c的别名,而接收对象ret也是引用,因此ret也是局部变量c的别名
    • 所以尽管第二次ret没有接受返回值,但是c的改变也会影响其别名ret的改变,所以最后ret的值也会变为7。

    其实,上面的分析并不完全正确

    • 应该清楚,随着函数栈帧的销毁,其函数内部的局部变量也会被销毁
    • 因此ret打印出的结果应该是不确定的
    • 这里之所以可以打印出看似正确的结果3和7,可能因为博主用的编译器并没有清理函数Add的栈帧

    而要使代码正确,我们可以将变量c设置为静态变量,这样就不会随着函数栈帧的销毁而被清除了。

    清楚了这一点,再来看下面的两串代码:

    代码一:

    int& Add(int a, int b)
    {
    	static int c = a + b;
    	return c;
    }
    
    int main()
    {
    	int& ret = Add(1, 2);
    	cout <<"ret = " <<  ret << endl;
    
    	Add(3, 4);
    
    	cout << "ret = " << ret << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    代码二:

    int& Add(int a, int b)
    {
    	static int c;
    	c = a + b;
    	return c;
    }
    
    int main()
    {
    	int& ret = Add(1, 2);
    	cout <<"ret = " <<  ret << endl;
    
    	Add(3, 4);
    
    	cout << "ret = " << ret << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    大家认为,这两串代码打印的结果一样吗?

    在这里插入图片描述

    如果想错了的小伙伴,可能忽略了一点:静态变量只能被初始化一次

    因此,对于代码一,定义静态变量c时,将其初始化为a + b = 3,那么之后就不会被初始化,ret的值也就不会变了。

    经过上面的一系列分析,可以得出结论:

    如果将局部变量的引用作为返回值,那么出了函数作用域,返回对象就被销毁了,不能用引用返回,否则结果就是不正确的。

    接下来我们举一个正确使用引用做返回值的例子:

    //功能:将顺序表的每个值++
    
    typedef struct SeqList
    {
    	int data[100];
    	int size;
    }SL;
    
    int& SLPosModify(SL* sl, int pos)
    {
    	return (sl->data)[pos];
        
        //返回顺序表每个位置的别名,从而改变返回对象就可以改变顺序表内的内容
    }
    
    int main()
    {
    	SL* sl = (SL*)malloc(sizeof(SL));
    	sl->size = 100;
    	memset(sl->data, 0, sizeof(int) * sl->size);
    
    	for (int i = 0; i < sl->size; i++)
    		SLPosModify(sl, i)++;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    可以看到,引用做函数返回值有以下好处:

    1. 可以直接修改返回对象
    2. 可以简化代码,提高代码可读性
    3. 可以提高效率

    2 常引用

    常引用(const reference)是一种引用变量的方式,用于表示引用的目标对象不可被修改

    需要注意以下几点:

    1. 取别名时不能将原来的权限放大,只能权限平移或者权限缩小。例如:

      const int a = 1;
      int& b = a;	//权限放大
      
      • 1
      • 2

      会有如下报错信息:

      在这里插入图片描述

      而下面的写法就是正确的:

      int a = 1;
      const int& b = a;	//权限缩小
      const int& c = b;	//权限平移
      
      • 1
      • 2
      • 3
    2. 可以给常量取别名。例如:

      const int& d = 10;
      
      • 1
    3. 当引用涉及到类型转换时,要注意临时变量的常性。例如:

      int a = 1;
      double& b = a;
      
      • 1
      • 2

      错误的。而

      int a = 1;
      const double& b = a;
      
      • 1
      • 2

      就是正确的

      • 这是由于

        当给整型变量a取一个浮点型的别名b时,就会发生隐式转换,中间就会产生一个临时变量
        而由于临时变量具有常性(const),因此由上面我们所说的取别名不能放大权限,因此正确的写法为const double& b = a;

      • 注:具体的,临时变量到底是怎么产生的以及他有什么特性,感兴趣可以看看👉C/C++陷阱——临时变量的产生和特性

    3 引用和指针

    在上面的介绍中,我们例举了很多引用替代指针来实现指针功能的例子。那么我们是否可以这样认为:C++中,引用可以完全取代指针?

    答案是:不能!

    首先我们应该清除引用有两点特性:

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

    2. 引用不能被修改指向,只能修改其值(例如,如果c原来是a的引用,那么c就只能是a的引用)

    例如:

    在这里插入图片描述

    而我们又清楚,指针可以随意改变它的指向,这是引用无法做到的,因此在C++中,引用是无法完全取代只针的,应该说,引用和指针是互补的。

    3.1 引用和指针在语法层面和底层的异同

    1. 语法上引用就是给一个变量取别名,不会开辟额外空间,和原来的变量共用一个地址。而指针就会开辟空间用来存储这个指针变量。
    2. 底层上,引用和指针一样,都是用汇编实现的。因此我们可以说,实际上声明和指针都是传地址

    那么日常情况下,我们是以语法为主还是底层为主呢?

    答案是以语法为主。我们来看一个例子:

    char a = 1;
    char& b = a;
    
    cout << sizeof(b) << endl;
    
    • 1
    • 2
    • 3
    • 4

    32位系统,如果输出的是4,那就说明以底层为主,如果输出为1,那就说明以语法为主。

    在这里插入图片描述

    我们可以看到,输出的结果是1,因此日常情况下,以语法为主

    3.1 引用和指针的不同

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

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

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

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

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

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

    7. 有多级指针,但是没有多级引用(一个变量可以有多个引用)

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

    9. 引用比指针使用起来相对更安全,但不是绝对安全


    本篇完。

  • 相关阅读:
    C#面:C#面向对象的思想主要包括什么?
    基于BP神经网络的含水率预测模型matlab仿真
    PyTorch:计算图
    【端到端存储解决方案】Weka,让企业【文件存储】速度飞起来!
    为什么Open3D可视化TensorFlow张量速度超慢
    g2o的hessian矩阵、b怎么存储
    【数据结构与算法系列3】有序数组的平方 (C++ & Python)
    java-php-python-ssm医药网络挂号系统计算机毕业设计
    前端项目代码学习笔记
    《Java基础知识》Java 浅拷贝与深拷贝1
  • 原文地址:https://blog.csdn.net/l315225/article/details/133917408