• 【C++入门】烦人的引用


    1、引用的概念

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

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

    比如:

    int a = 10;
    int& ai = a;
    //正如一个人可以取多个外号,实体也可以有多个引用。
    int& aii = a;
    
    • 1
    • 2
    • 3
    • 4

    ai 就是 a的别名,ai 是 a的另一个称号。
    在这里插入图片描述
    所以有很明显的几个规则:

    1. 得先有本体,再有别名,所以引用不能空引用,且引用必须有初始值
    2. ai作为a的别名后,不能再作为其它的别名,不然会产生歧义。
    3. 如果改变ai的值,那么a的值也会改变,因为它们都是同一个。
    4. 正如一个人可以取多个外号,实体也可以有多个引用。
    5. 引用类型必须和引用实体是同种类型的

    2、引用的价值

    2.1 做参数

    看代码:
    输出型参数

    可以有效避免实参的拷贝。

    void swap(int& x, int& y)
    {
    	int temp = x;
    	x = y;
    	y = temp;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    //一些OJ题中,参数返回值
    int* func(int* a, int size, &returnSize)
    {}
    
    • 1
    • 2
    • 3

    有效避免二级指针的使用

    typedef struct ListNode
    {
    	struct ListNode* next;
    	int val;
    }LTNode, *PLTNode;
    
    //struct ListNode*& phead
    void SlistPushBack(PLTNode& phead, int x)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可见,有些指针能实现的地方,引用也能实现。

    2.1 做返回值

    引用的正确使用:

    传值返回
    先看一串代码:

    int Count()
    {
    	static int n = 0;
    	n++;
    
    	return n;
    }
    
    int main()
    {
    	int ret = Count();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    要想清晰的理解这段代码,我们得借助函数栈帧知识。

    在图中栈帧往下开辟,静态变量n存储在静态区,Count函数在返回后销毁,销毁之前会为一个临时变量开辟空间,返回的n=1作为一个临时变量放在寄存器中(数据较小将放入CPU的寄存器中,如果数据较大就会在上一个栈帧中开好空间存放),再将寄存器的内容返回给ret。

    为什么要返回临时变量?
    因为如果是单单普通的Int n 返回,函数中销毁后n也销毁,获取的n也就没有意义了。
    在这里插入图片描述

    引用返回

    int& Count()
    {
    	static int n = 0;
    	n++;
    
    	return n;
    }
    
    int main()
    {
    	int ret = Count();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Count栈帧销毁后,由于n存储在静态区,直接返回n的引用,这一过程虽然产生了临时变量但是返回引用所以不开辟空间。
    在这里插入图片描述

    引用的错误使用:

    传值返回(这是正确的)

    int Count()
    {
    	int n = 0;
    	n++;
    
    	return n;
    }
    
    int main()
    {
    	int ret = Count();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    与之前传值相比,n的值存放在栈区随着Count函数销毁也会被销毁,但销毁之前,也会传一个临时变量给ret,这个临时变量开辟空间。

    注意:这里的销毁指的不是开辟Count的那块区域被销毁了,而是依然能访问也能修改,只是非法的,并且那块区域的数据已经不受保护了,读取的数据都是不确定的,相当酒店退房一样。
    在这里插入图片描述

    引用返回
    这是一个错误示范

    int& Count()
    {
    	int n = 0;
    	n++;
    
    	return n;
    }
    
    int main()
    {
    	int ret = Count(); //非法访问
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    由于引用返回产生的临时变量不开辟空间,那么在n销毁后,返回的n的引用也就构成了非法访问。
    在这里插入图片描述

    总结

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

    3、引用的意义

    3.1 传引用做参数的意义

    减少拷贝,提高效率,能修改参数
    对函数传参,并不会对函数直接传实参,而是会传递一个临时拷贝的形参,尤其当数据量大时,造成效率低下。通过传引用不需要拷贝,很好的解决效率问题。

    #include 
    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 < 1000000; ++i)
    		TestFunc1(a);
    	size_t end1 = clock();
    	// 以引用作为函数参数
    	size_t begin2 = clock();
    	for (size_t i = 0; i < 1000000; ++i)
    		TestFunc2(a);
    	size_t end2 = clock();
    	// 分别计算两个函数运行结束后的时间
    	cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    	cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    4w字节的数据,100w次的调用。
    在这里插入图片描述

    3.2 传引用返回值的意义


    减少拷贝,提高效率,能修改参数
    由于函数在返回值的时候,需要返回一个开辟空间的临时变量,在数据量足够多的时候,效率是比较低下的,而返回引用值的时候,返回的临时变量不需要开辟内存空间,这就提高了效率,但是得正确使用引用

    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    当数据量或返回次数越多,这个差距也会越大。
    在这里插入图片描述

    4、引用和const

    4.1 引用的权限

    const使得变量不能修改,使得变量只有只读权限,如果引用该变量,那么对应的引用类型也必须带上const,权限也只有只读。(权限不能放大)

    如果变量有读和写的权限,那么引用的权限可以只读。(权限的缩小)

    int main()
    {
    	int a = 0;
    
    	// 权限平移
    	int& ra = a;
    
    	// 指针和引用赋值中,权限可以缩小,但是不能放大。
    	const int b = 1;
    
    	//拷贝
    	a = b;
    
    	//权限放大不行
    	//int& rb = b;
    
    	//权限缩小可以
    	const int& rra = a;
    	//rra++   //err
    	a++;  //改变rra和a
    	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

    const引用避免传参的限制
    看代码:
    因为权限的问题,在对参数是引用的函数进行传参,需要考虑实参权限是否可以对应。
    为了在多种场景下都能使用该函数,许多的函数实现在设计引用参数时都加上了const起到保证作用。

    //void Func(int& x){}
    
    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;
    
    	//权限平移
    	const int& rb = b;
    
    	Func(a);
    	Func(ra);
    	Func(rra);
    	Func(b);
    	Func(10); //下一节会提到为什么可以
    
    	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
    • 27
    • 28
    • 29
    • 30

    以上的函数调用都能在func(const int&x)中成功。

    并且上面两个函数构成函数重载。

    4.2 const 引用修饰常量


    const 引用可以初始化引用常量

    const int& b = 10;
    
    • 1

    所以之前Func(10)为什么可以调用,也就解决了。

    截断、强转和整型提升都会产生临时变量

    	double d = 12.34;
    
    	cout << (int)d << endl;//打印临时变量
    
    	int i = d; //(截断) 实际临时变量12赋予给了i
    
    • 1
    • 2
    • 3
    • 4
    • 5


    临时变量有常量的特性

    int Count()
    {
    	int n = 0;
    	n++;
    
    	return n;
    }
    
    
    int main()
    {
    	const int& b = 10;
    
    	double d = 12.34;
    
    	//int& ri = d; //不行
    
    	const int& ri = d; //引用临时变量 临时变量具有常性
    
    	const int& ret = Count(); //函数返回临时变量
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    5、引用和指针区别

    语法概念上引用和本体共用一块空间,指针需要开辟一块内存存储地址。
    底层实现中,有空间的,它们实现都是一样的。本质引用和地址都是传地址,只是引用是由编译器做。

    int main()
    {
    	int a = 10;
    	int& ra = a;
    	ra = 20;
    	int* pa = &a;
    	*pa = 20;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    具体不同点:

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

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

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

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

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

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

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

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

  • 相关阅读:
    【MATLAB教程案例6】基于Costas环的载波同步matlab仿真
    C# 基础(四)
    Linux15 --- 信号量
    15种下载文件的方法&文件下载方法汇总&超大文件下载
    英语中的提问方式(问法)(bug提问、bug描述)
    FRP内网穿透教程
    JAVA DatagramSocket通信
    shiro授权-SSM
    听,引擎的声音「GitHub 热点速览 v.22.33」
    你找到“活着”的意义了吗?
  • 原文地址:https://blog.csdn.net/Ahaooooooo/article/details/126914440