• C++入门学习(4)引用 (讲解拿指针比较)


    上期回顾

            在学习完函数重载之后,我们可以使用多个重名函数进行操作,会发现C++真的是弥补了好多C语言的不足之处,真的不禁感概一下,时代的进步是需要人去做出改变的,而不是一味的使用啊!所以我们今天继续学一下C++对C语言的指针的改变吧!

    一、引用的诞生

            在C语言中,指针的使用是很复杂的,涉及了二级指针,三级指针乃至我们很少见的多级指针,这会让我们使用起来很麻烦,程序的可读性很差,如果你不是一个功底很深的程序员,根本就要花上很长时间才会略知一二。

            但是我们在C++中并不是摒弃了指针,而是发明了一个新的东西,在某些场合可以代替指针----引用!

    二、引用的概念

            那什么是引用呢?

            我们语文中的引用是不是给某个东西起个别名,然后再用双引号引起来。在现实生活中,我们会有很多别名,比如李逵,在家叫“铁牛”,江湖人称“黑旋风”。这都是引用,所以我们C++中的引用也不例外,就是给一个变量起别名。

            我们来一起思考一个问题,C++中的引用,会开辟一个新的内存空间吗?因为在C语言中指针会开辟空间的。

            我们还是以现实生活为例,你有很多别名,那就有很多个你吗?肯定不是吧!

            所以我们C++的引用,也是只有一个空间的,我们只是给变量起了个别名,但是引用是跟它引用的变量共用一块内存空间的。

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

    我们了解了概念之后,那引用是如何使用的呢?

    三、引用的使用

    数据类型引用变量名 = 引用实体

    注意:引用的数据类型要和引用实体数据类型一致(不一致的情况我们放在常引用讲解

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. int&a1 = a;
    7. cout << "a = "<< a << endl << "a1 = "<< a1 << endl;
    8. return 0;
    9. }

            看到下面的输出结果,也印证了引用是给变量取别名

    四、引用的特性

    4.1 引用必须初始化

            如果我们写了这样的一段代码:是编译不过去的,会报错,正是因为引用没有初始化出现的错误。我们可以这样理解,引用实体都没有,哪里来的引用呢?就像一个人根本不存在,他就不可能有别名的。

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. int&a1;
    7. return 0;
    8. }

    4.2 引用的改变 会 改变引用实体

     那如果是这样的代码呢?我们在此基础上➕了一行a1++,改变引用会改变引用实体吗?

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. int&a1 = a;
    7. a1++;
    8. cout << "a = "<< a << endl << "a1 = "<< a1 << endl;
    9. return 0;
    10. }

    答案是一定的,因为引用跟引用实体共用同一个空间,改变引用就是改变引用实体。

    4.3 引用不改变指向,也不可以同时引用多个实体

            那一个引用可以引用多个实体吗?或者可以改变引用指向的对象吗?因为我们C语言的指针可以改变指向,所以我们来探讨一下,比如下面这段代码:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. int&a1 = a;
    7. int b = 5;
    8. a1 = b;
    9. cout << "a = "<< a << endl << "a1 = "<< a1 << endl;
    10. cout << "a的地址为:"<< &a << endl;
    11. cout << "a1的地址为:"<< &a1 << endl;
    12. cout << "b的地址为:"<< &b << endl;
    13. return 0;
    14. }

            我们可以看到,虽然把b的值赋给了a1,但是没有改变引用的指向,a1的地址还是跟a一样的,引用跟指针的区别之一也显现出来了,所以我们可以得出下面两个结论:

    1. 引用不可以引用多个对象;

    2. 引用不可以改变指向(指向的对象);

    4.4 一个实体可以有多个引用

    在日常生活中,你可以有很多个别名,所以在C++中,一个实体也可以有多个引用。

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. int& a1 = a;
    7. int& a2 = a;
    8. int& a3 = a;
    9. cout << "a = "<< a << endl;
    10. cout << "a1 = "<< a1 << endl;
    11. cout << "a2 = "<< a2 << endl;
    12. cout << "a3 = "<< a3 << endl;
    13. return 0;
    14. }

    因为一个实体可以有多个引用,所以这个输出就一定都会是3的。

            我相信大家一定会有这样的疑问,我们上面所做的事情,C语言的指针也可以做呀,没什么特别的嘛,大家不要着急,继续看下去,引用使用的地方并不是在这里哦!

            我们可以发现,目前的引用都是在引用变量,那是否可以引用常量或者是常变量呢?接下来是我们要讲的重点之一:

    五、常引用

    在学习之前,我们要先知道这个概念

    权限:一般是指我们可以操作的范围,使用的范围,权限只可以平移和缩小,不可以放大

    了解之后,我们就开始对常引用的学习吧!

    那什么是常引用呢?就是对常量和常变量的引用,但是一定要记住权限,来看下面的代码:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. const int a = 3;
    6. int& a1 = a;
    7. return 0;
    8. }

    这段代码可以正常运行吗?

    我们先来分析一下,在程序中我们定义了一个常变量a,a本来是变量,但是被const修饰了,变得不可被修改,所以权限是 “只读”,然而我们用int类型的引用来去引用a是不可以的,因为我们的引用变量a1是int类型的,权限是“读和写”,可以被修改。所以这里的权限是被放大了,a1可以修改,a不可以修改,权限被放大,该程序错误

    那怎么弄才是对的呢?还是得看权限只能平移和缩小,所以我们更改了代码:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. const int a = 3;
    6. const int& a1 = a;
    7. return 0;
    8. }

    这段代码中,a是常变量,引用变量a1也是常变量,这是权限的平移,程序正确。

    那有没有权限缩小的呢?当然有,来看下面代码:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. const int& a1 = a;
    7. return 0;
    8. }

    这段代码就是a本来可以修改,是“读写”,然后我们的引用变量a1是➕const修饰的,权限是“只读”,这里就是权限的缩小,程序正确。

    以上都是对常变量的引用,那如何来引用常量呢?

    猜对了,还是靠权限来:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. const int &a1 = 3;
    6. return 0;
    7. }

    我们的常量,也是不可被修改的,权限是“只读”,所以要引用常量的时候,要加上const,把权限也变成“只读”

    六、引用和引用实体的数据类型不同

    当我们了解了这些之后,我们要填一下上面埋的坑,当引用和引用实体的数据类型不一样的时候,如何引用呢?

    我们要了解一个概念,当类型不同的时候,一定会发生类型转换,比如下面的代码:

    1. #include
    2. int main()
    3. {
    4. int a = 3;
    5. double b = a;
    6. printf("a = %d\nb = %lf",a,b);
    7. return 0;
    8. }

    我们先定义了一个整形变量a,再将a变量赋值给double类型的b,两个类型不同,会发生隐式类型转换,就是把a转换成double类型,再赋值给b,那我们的a的数据类型是什么呢?

    从上面可以看出a还是整型,但是不是把a转换成double之后,再赋值给b的吗?怎么a还是int类型?我们带着这样的疑问,去学习一个新的知识: 

    凡是涉及到类型的转换,都会产生一个临时变量,存放转换的值。这个临时变量具有常属性

    这也就解释了为什么a还是int类型。

    我们用图解的方法来看一下:

    我们在了解上面的概念之后,看下面的代码:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 3;
    6. const double& b = a;
    7. return 0;
    8. }

    我们定义了一个int类型的变量a,用double类型的引用变量去引用a,因为类型不同,会发生类型转换,但本质是先创建一个double类型的中间变量,再把a的值转换到这个中间变量中,由这个中间变量赋值给我们的b,为什么要➕const,因为这个中间变量具有常属性。

    七、引用的使用场景

    其实在上面我们仅仅是在介绍引用该怎么使用,没有介绍引用都用在哪里,所以接下来我们来学习引用使用场景。

    7.1 做函数参数

    我们在C语言中写一个交换函数,是需要传地址的,也就是用到一级指针:

    1. void Swap(int* x,int* y)
    2. {
    3. int tmp = *x;
    4. *x = *y;
    5. *y = tmp;
    6. }

    而在C++中,我们可以用引用来解决这个问题:

    因为引用就是引用实体的别名,所以改变引用就是改变引用实体。

    1. void Swap(int& x,int& y)
    2. {
    3. int tmp = x;
    4. x = y;
    5. y = tmp;
    6. }

    7.2 做返回值

    第二个用途就是用引用当返回值

    但是有一个条件:函数返用引用作为返回值的时候,返回值不可以被销毁

    下面的例子主要是讲解,我们用引用做返回值的时候,返回值不可以被销毁。

    我们先看这样的一段代码:

    我们是定义了一个函数Count,里面实现的是简单的n++,n最后是1,然后将n的值返回,可是n是函数里面创建的一个临时变量啊,临时变量出了作用域就销毁了

    而我们函数的返回值是n的引用,引用实体销毁了,引用还能正常使用吗?肯定不能的对吧,但是这里要分两种情况,ret可能是1,也可能是随机值,这主要看编辑器销毁的速度。

    1. int& Count()
    2. {
    3. int n = 0;
    4. n++;
    5. // ...
    6. return n;
    7. }
    8. using namespace std;
    9. int main()
    10. {
    11. int& ret = Count();
    12. cout << ret << endl;
    13. return 0;
    14. }

    如果我们是下面的代码呢?打印出来的结果又是什么呢?

    很神奇吧,一个是3可以理解,有可能是z没有被销毁,但是后面怎么是10了呢?

    这是因为我们函数栈帧是可以复用的,所以我们的那个空间本来是3,后来虽然被销毁了,又被征用了,变成10了。而我们的引用还是指向那个位置的,所以就变成10了,但是也可能是随机值。

    1. int& Add(int x,int y)
    2. {
    3. int z = x + y;
    4. return z;
    5. }
    6. using namespace std;
    7. int main()
    8. {
    9. int& ret = Add(1,2);
    10. cout << ret << endl;
    11. Add(3,7);
    12. cout << ret << endl;
    13. return 0;
    14. }

    但是有没有什么办法来解决这样的问题呢?也就是保证返回值不被销毁。

    我们可以定义一个静态变量就可以了,因为静态变量是在堆上开辟的,函数的销毁不会影响静态变量。

    需要注意的一点就是:局部的静态变量只可以初始化一次

    多次进入这个函数,只会执行一次初始化,剩下的都跳过

    这样我们就可以正确的返回z的引用了,

    1. int& Add(int x,int y)
    2. {
    3. static int z = x + y;
    4. return z;
    5. }
    6. using namespace std;
    7. int main()
    8. {
    9. int& ret = Add(1,2);
    10. cout << ret << endl;
    11. return 0;
    12. }

    所以我们的我们函数返回值要是引用的话,返回值不呢被销毁

    八、传值和传引用的效率比较

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

    8.1 函数参数  用  引用和值  的效率比较

    1. #include
    2. #include
    3. using namespace std;
    4. struct A
    5. {
    6. int a[10000];
    7. };
    8. void TestFunc1(A a)
    9. {
    10. }
    11. void TestFunc2(A& a)
    12. {
    13. }
    14. void TestRefAndValue()
    15. {
    16. A a;
    17. // 以值作为函数参数
    18. size_t begin1 = clock();
    19. for (size_t i = 0; i < 10000; ++i)
    20. TestFunc1(a);
    21. size_t end1 = clock();
    22. // 以引用作为函数参数
    23. size_t begin2 = clock();
    24. for (size_t i = 0; i < 10000; ++i)
    25. TestFunc2(a);
    26. size_t end2 = clock();
    27. // 分别计算两个函数运行结束后的时间
    28. cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    29. cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
    30. }
    31. int main()
    32. {
    33. TestRefAndValue();
    34. return 0;
    35. }

    我们可以看到当数据过大的时候,函数参数用引用的效率很快!

    8.1 函数返回值用引用和值的效率比较

    1. #include
    2. #include
    3. using namespace std;
    4. struct A
    5. {
    6. int a[10000];
    7. };
    8. A a;
    9. // 值返回
    10. A TestFunc1()
    11. {
    12. return a;
    13. }
    14. // 引用返回
    15. A& TestFunc2()
    16. {
    17. return a;
    18. }
    19. void TestReturnByRefOrValue()
    20. {
    21. // 以值作为函数的返回值类型
    22. size_t begin1 = clock();
    23. for (size_t i = 0; i < 100000; ++i)
    24. TestFunc1();
    25. size_t end1 = clock();
    26. // 以引用作为函数的返回值类型
    27. size_t begin2 = clock();
    28. for (size_t i = 0; i < 100000; ++i)
    29. TestFunc2();
    30. size_t end2 = clock();
    31. // 计算两个函数运算完成之后的时间
    32. cout << "TestFunc1 time:" << end1 - begin1 << endl;
    33. cout << "TestFunc2 time:" << end2 - begin2 << endl;
    34. }
    35. int main()
    36. {
    37. TestReturnByRefOrValue();
    38. return 0;
    39. }

    当数据过大的时候,函数返回值用引用的效率更快

    通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。这就是引用的优势,不需要开辟临时空间。

    九、引用和指针的区别

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

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

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

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

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

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

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

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

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

    10. 空指针没有任何指向,删除无害,引用是别名,删除引用就删除真实对象

    十、引用的底层原理

    学完这里,你会对引用有一个新的认识

    我们只需要记住引用的底层是指针来实现的,并且是const 指针

    下面是引用和指针的汇编,我们会发现引用的底层和指针的操作是一模一样的,但是在语法角度他们有区别,因为引用不是对象,不会开空间,但是指针是对象,会开空间;在底层的角度又没有区别,都是指针,所以,这里我们以语法为准

  • 相关阅读:
    利用ChatGPT完成2024年MathorCup大数据挑战赛-赛道A初赛:台风预测与分析
    Python高级篇(07):迭代器
    jenkins工具系列 —— 插件 使用Changelog获取commit记录
    夜莺日志采集mtail
    M. My University Is Better Than Yours(思维)
    Rust数据类型——初学者指南
    是机遇还是挑战?AI 2022五大预测
    “大厂”角力移动办公系统市场,钉钉和企微向左、WorkPlus向右
    windows下搭建appium+android测试环境(node.js样例)
    16、JAVA入门——继承和方法重写
  • 原文地址:https://blog.csdn.net/2302_76941579/article/details/134281570