• C++引用知识点超级清楚的总结


    1 引用的基本概念

    我们可以把引用理解成给一个变量起的别名。

    也可以这样理解:
    (对照下图理解)
    比如,一个变量是一个盒子,盒子里面装的是这个变量的数据。
    变量名就是这个盒子本身的名字。比如a。
    那引用就是贴在这个盒子上的一个标签贴纸。
    贴纸上写的是这个变量的别的名字。比如b,c。
    我们可以通过a改变盒子里放的数字,也可以通过b,c改变盒子里放的数字。

    注意:

    1. 在这篇文章中,所有盒子都指变量,包括指针也是一个盒子,里面放的不是10而是一个地址。
    2. 这个贴画和盒子这种想法,仅供大家记忆和理解引用相关知识点,不涉及也不代表底层实现。

    请添加图片描述

    那既然是贴纸就有下面几个属性:

    • 既然是贴纸,就得先有变量盒子,再在盒子上面贴贴纸。
      • 即,引用在定义的时候必须初始化
    • 一个盒子上可以贴多个贴纸。
      • 即,一个变量可以有多个引用。
    • 贴纸贴到盒子上,一撕就容易烂,一般贴上了就不会再贴到别的变量上了。
      • 即,初始化以后不能更换绑定的目标(其实代码也不能实现)
    • 贴纸得告诉大家这个盒子里装的变量的类型。
      • 即,引用和绑定的目标变量类型要一致。
    • 贴纸贴在盒子上,所以贴纸的位置和盒子的位置是一样的,如果没有盒子,那贴纸本身是没有自己的位置的
      • 即,引用的地址就是其所引用变量的地址
    //引用的语法
    类型 &引用名 = 变量;
    
    • 1
    • 2
    void reference_test(){
        int a = 100;
        int &b = a;//b引用a,b就是a的别名
        cout << "a=" << a << endl;//100
        cout << "b=" << b << endl;//100
        cout << "&a" << &a << endl;//&a0x61fde4
        cout << "&b" << &b << endl;//&a0x61fde4
        cout << "-----------" << endl;
        b++;//对引用做改变,变量本身也会改变
        cout << "a=" << a << endl;//101
        cout << "b=" << b << endl;//101
        cout << "&a" << &a << endl;//&a0x61fde4
        cout << "&b" << &b << endl;//&a0x61fde4
    
        int &r;//这样直接报错,因为引用r没有初始化
        int c = 200;
        b = c;//这个是将c的值赋值给b,相当于赋值给a,不是修改绑定的目标,这个不报错
        
        char& rc = c;//引用和变量类型不一致,报错
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2 常引用、左值和右值

    使用const关键字修饰的因为即为常引用,不能通过常引用修改引用的目标

    这里和const和指针的关系类似。

    const 类型 &引用名 = 变量;
    类型 const &引用名 = 变量;//这两种写法是相同的的
    
    • 1
    • 2

    注意

    是不是const引用只与const&的相对位置有关系。这个和指向常量的指针类似。

    2.1 左值引用和右值引用(难点)

    普通引用:也叫左值引用,只能引用左值
    const引用:也叫万能引用,既能引用左值,也能引用右值。

    左值lvalue:可以放在操作符左边,可以被修改,比如普通变量
    右值rvalue:只能放在操作符右边,不能被修改,比如产量10、被const修饰的变量

    int func(){
        int num = 100;
        cout << "&num = " << &num << endl;
        return num;//编译器会生成一个临时变量,保存这个函数返回的结果
        //临时变量 = num
        //注意:临时变量都是右值,只能用const引用来接
    }
    
    void lrVal_test(){
        //int res = 临时变量(右值);
        //临时变量的声明周期是语句级别的,当int res = func();运行结束以后,临时变量就被释放了
        int res = func();
    
        //int& res_unconst = func();//报错,普通引用只能引用左值
        const int& res_const = func();//正确,const引用可以引用右值
        cout << res_const << endl;//100
        cout << "&res_const = " << &res_const << endl;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3 引用型函数参数

    引用型函数参数的特点:

    • 可以和指针型参数一样,在函数内部修改变量的值
    • 传递的开销很低,和传递指针效率相当
      • 普通变量传递都是将变量拷贝一份,再传入函数,空间时间开销都很大
    
    void swap1(int& x, int& y){
        /*
        int temp = b;
        b = a;
        a = temp;*/
        //使用^异或运算符,效率最高
        x = x ^ y;//011(x) ^ 101(y) = 110(x)
        y = x ^ y;//110(x) ^ 101(y) = 011(y=>3)
        x = x ^ y;//110(x) ^ 011(y) = 101(x=>5)
    }
    
    void swap2(int* x, int* y){
        *x = *x ^ *y;
        *y = *x ^ *y;
        *x = *x ^ *y;
    }
    void refArg_test2(){
        int a = 3;
        int b = 5;
        cout << "a = " << a << ",b = " << b <<endl;
        swap1(a, b);
        cout << "a = " << a << ",b = " << b <<endl;
        swap2(&a, &b);
        cout << "a = " << a << ",b = " << b <<endl;
    }
    
    • 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

    4 引用型函数参数和const

    刚才说了,传递引用型参数开销非常小,所以我们在以后写代码的过程中,只要定义函数要传递参数,都可以定义成引用型函数参数了。

    但是有两个问题

    • 问题1:如果传入的这个变量本身被const修饰怎们办?
    • 问题2:如果不想这个变量在函数中被修改怎么办?

    对问题1:

    如果定义函数时候,形参是普通引用,而要传入的变量(实参)被const修饰,这样是会报错的。
    为什么呢?
    被const修饰的变量是右值,右值只能被右值引用所引用。
    所以,出现这种情况,我们要给引用形参前面加上const。
    让这个引用形参,变成右值引用形参(6.2 常引用、左值和右值)
    右值引用无论左值还是右值都可以接受,所以,如果传入普通变量也是没有问题的。
    问题1解决。

    对问题2:

    我们知道,形参是引用和形参是指针效果一样,都可以在函数内改变变量(实参)值。
    那如果我们在业务逻辑上不想改变变量(实参)的值怎们办?
    给引用形参前面加上const就行啦。
    表示:在这个函数中,这个变量是不可以被修改的,如果修改,则编译不会通过。

    总结:
    通过问题1问题2我们发现,大部分情况下,我们给引用形参前面加上const都是没有任何问题的。只有当我们需要修改传入的变量的时候,去掉const就可以了。

    代码示例:

    struct Student{
        char name[100];
        int age;
    };
    
    void print(const Student& s){
        //形参类型为const引用,函数内部就无法通过引用去修饰这个变量
        //为了提高程序运行效率,则以后就将所有的参数传递都用引用传递
        //为了防止有的不想被改变的变量被修改,则我们用const修饰引用形参
        //const引用可以接受左值也可以接受右值
        cout << "姓名:" << s.name << ",年龄:" << s.age << endl;
    }
    
    void refArg_test1(){
        Student s = {"辛巴达", 99};
        print(s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5 引用型函数返回值

    函数返回值声明为引用类型:
    函数返回的结果就是return后面数据的别名,避免了函数返回数据副本的开销。

    函数中返回引用,一定要保证在函数返回以后,该引用的目标依然有效

    • 可以返回全局变量、静态变量和成员变量的引用
      • 因为这些引用在函数返回以后,目标依然存在,没有被系统释放
    • 可以返回引用型参数本身
    • 可以返回调用对象自身的引用
    • 可以返回堆中动态创建对象的引用
    • 不能返回局部变量的引用
      • 因为局部变量会在函数执行完以后被释放
    
    struct A{
        int data;
        int& func(){
            return data;
        }
    
    };
    
    void refReturn_test(){
        A a = {100};
        cout << a.data << endl;
        
        //返回值是个引用,而且是个左值引用,所以可以直接给这个引用赋值
        //给这个引用赋值就相当于给data赋值
        a.func() = 200;
        cout << a.data << endl;//a.data = 200
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    6 引用和指针(面试会问)

    如果对指针不熟悉的可以看这个:指针知识点总结

    在底层实现上,引用是用指针实现的。
    但是,但是,下面的话很重要:
    在c++中,我们认为:
    引用不是实体的类型,就是,不像整型、指针类型、char型那样。
    在c++中我们认为他没有实体,没有引用给分配地址

    引用指针
    1引用必须初始化指针可以不初始化
    2不能定义引用的指针可以定义指针的指针
    3不能定义引用的引用可以定义指针的引用
    4不能定义引用数组(就是数组内元素都是引用)可以定义指针数组
    5可以定义函数引用可以定义函数指针

    出现上面这些区别的主要原因都是:
    在c++中,我们认为引用不是一个实体的类型,编译器是没有给引用分配地址的。
    那上面的结论就能理解了:
    没有地址,肯定就没有引用的指针(引用的指针就是引用的地址)。
    没有地址,就没有引用的引用。
    没有地址,就没有引用数组,因为数组要求内部元素地址连续。

    其实这样还不是很好理解,这时候又要使用我们的变量盒子与贴画了。
    请添加图片描述

    • 引用必须初始化,指针可以不初始化
      • 贴纸(引用)必须贴在盒子上,盒子(指针变量)里面可以不放东西。
    • 不能定义引用的指针,可以定义指针的指针
      • 每个盒子都有自己摆放的位置(地址)
      • 指针盒子(指针变量)保存的是另一个盒子的地址,指针变量自己也是盒子,也有地址
      • 贴纸贴在盒子上,除开盒子,贴纸本身没有位置
      • 所以,指针变量盒子没法保存引用贴纸的地址
    • 不能定义引用的引用,可以定义指针的引用
      • 贴画上不能再贴一个贴画了,但是指针变量也是个盒子,可以贴贴纸
    • 不能定义引用数组,可以定义指针数组
      • 一个变量是一个盒子,一个数组就是一排盒子。
      • 引用是贴纸,再多的贴纸也排不成一排盒子
      • 一个指针变量是一个盒子,一排指针变量盒子就是指针数组(看下面的图)

    请添加图片描述

    下面分别用代码来展示一下上面5天引用和指针的区别与联系

    1.引用必须初始化,指针可以不初始化

    void test(){
    	int a = 10;
    	int *p;//不会报错
    	int &r;//会报错
    	int &r = a;//不会报错
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.不能定义引用的指针,可以定义指针的指针

    void test(){
    	int a = 10;
    	int *p = &a;//不会报错,定义指针变量p
    	int **pp = &p;//不会报错,定义指针p的指针pp
    
    	int &r = a;//不会报错,定义引用r
    	int& *pr = &r;//会报错,试图定义引用的指针
    	//int&表示这个指针的类型是int类型的引用
    	//*pr表示这是一个指针。从指针定义的语法来说看似合理,实则报错
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.不能定义引用的引用,可以定义指针的引用

    void test(){
    	int a = 10;
    	int *p = &a;//定义指针
    	int* &rp = p;//不会报错,定义指针的引用
    
    	int &r = a;
    	int& &rr = r;//报错,不能定义应用的引用,因为应用本身就不是一个实体
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.不能定义引用数组,可以定义指针数组

    void test(){
    	int a = 1, b = 2, c = 3;
    	int *pr[3] = {&a, &b, &c};//不报错,定义指针数组(数组里都是指针)
    	int &rr[3] = {a, b, c};//报错,试图定义引用数组(数组里都是引用)
    
    	//注意,但是我们可以引用一个数组变量
    	int b[3] = {1, 2, 3};//这是一个int型数组,数组名为b
    	int (&br)[3] = b;//不报错,相当于给数组b起了个别名,叫br
    	
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.可以定义函数引用,可以定义函数指针

    //随便定义一个函数名为func
    void func(int a, int b){
    	cout << "func:" << a << ";" << b << endl;
    }
    
    //定音函数指针(函数指针就是指向函数的指针)
    void (*funcPtr)(int,int) = func;
    funcPtr(10, 20);//输出func:10;20
    
    void (&funcRef)(int, int) = func;
    funcRef(10,20);//输出func:10;20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    Java 泛型
    [实践篇]13.5 QNX侧如何操作进程?
    ASP.NET Core - 选项系统之选项验证
    .Net7发现System.Numerics.Vector矢量化的一个bug,Issues给了dotnet团队
    ES6 Set数据结构
    TensorRT开发环境搭建
    junctions_skeleton
    代码随想录 动态规划 Ⅹ
    母婴服务预约小程序的效果如何
    单例模式深层剖析
  • 原文地址:https://blog.csdn.net/qq_43591406/article/details/127821776