• c++基础2


    目录

    引用:

    引用特性

    引用的价值

    理解传引用返回:

    传引用返回:

    总结:

    引用的作用:

    传引用返回:

     引用做参数的作用:

     指针和引用的权限大小问题:

    常见的易错点

     const引用可以接收常量

    引用在底层

    引用和指针的不同点:

    auto关键字

     范围for


    引用:

    1. #include
    2. using namespace std;
    3. int main()
    4. {
    5. int a = 10;
    6. int&ra = a;
    7. printf("%p\n", a);
    8. printf("%p\n", ra);
    9. return 0;
    10. }

    所以:引用并不会创建额外空间,本质上是对所修饰变量的别名,相当于:

     

    引用特性

     特性1:

    1. int main()
    2. {
    3. int a = 10;
    4. int &ra;
    5. a = ra;
    6. return 0;
    7. }

    特性2:

    1. int main()
    2. {
    3. int a = 10;
    4. int &ra = a;
    5. int&rb = a;
    6. return 0;
    7. }

    引用的价值

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

    引用可以作为输出型参数,和指针的作用类似。

    引用还可以作为返回值来使用:

    1. int&Count()
    2. {
    3. static int n = 0;
    4. n++;
    5. return n;
    6. }

    这样理解:相当于把n的别名传引用返回。

    理解传引用返回:

    要理解传引用返回,先理解传值返回。

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

    用一个简图来解释:

    首先调用main函数,形成main函数的栈帧,栈帧中有一个变量ret。

    接下来调用Count 函数,Count函数形成栈帧,参数n虽然在Count函数内部,但是有static修饰,所以n是静态变量,不会随函数调用结束而销毁。

    Count函数返回的过程是一个传值返回,把n的值进行拷贝,存储在寄存器或者上一个函数栈帧(main函数)中,这个变量就叫做临时变量,再把临时变量赋给ret。

    为什么必须要用传值返回:

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

    假设我们没有传值返回:调用Count函数,用ret接受返回值n,这时候Count函数已经调用完毕,参数n值被释放,释放之后我们再对对应位置访问就是越界访问。

    假设我们有传值返回:在调用Count函数进行返回时,首先用一个临时变量拷贝n的值,然后栈帧释放,n值销毁,我们再把临时变量赋给ret,就不会产生越界访问了。

    传引用返回:

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

    并没有报错,ret的值也没有问题,分析具体的步骤:

    因为我们在Count函数中传引用返回,在返回时,我们用一个临时变量接受n,这里的临时变量就等于n的别名了,然后Count函数调用结束,n值销毁,我们再访问n就是越界了,这里没有越界的原因:我们用ret接收Count的返回值,这个过程也是传值,所以我们的ret和Count函数中的n指向的并不是同一块空间。 

     传引用返回的第二种情况:

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

    这里和上一段代码的不同点在于我们使用了两个引用,首先在Count函数中用引用接收到了n的别名,又传引用给了ret,那么ret现在就是Count函数中的n,我们进行访问就会越界。

    为什么这里会出现1?

    答: 虽然对应的n的空间已经销毁,我们调用cout函数,因为cout函数也是只有一个参数,ret就和原来属于n的空间重合了,我们打印就是n值。

    总结:

    出了函数作用域,返回变量不存在了,就不能用传引用返回,因为传引用返回的结果是未定义的。

    除了函数作用域,返回变量还存在,就可以用传引用返回。

     争取的写法:

    1. int Count1()
    2. {
    3. int a = 0;
    4. a++;
    5. return a;
    6. }
    7. int&Count2()
    8. {
    9. static int a = 0;
    10. a++;
    11. return a;
    12. }
    13. int main()
    14. {
    15. int ret1 = Count1();
    16. int ret2 = Count2();
    17. cout << ret1 << "," << ret2 << endl;
    18. return 0;
    19. }

     对于静态变量作为返回值,我们可以用传引用返回,因为静态变量不随函数的调用结束而销毁。

    引用的作用:

    传引用返回:

    作用:可以提高效率,减少拷贝。

    1. #include
    2. struct A{
    3. int a[10000];
    4. };
    5. A a;
    6. A TestFunc1()
    7. {
    8. return a;
    9. }
    10. A& TestFunc2()
    11. {
    12. return a;
    13. }
    14. void TestReturnByRefOrValue()
    15. {
    16. size_t begin1 = clock();
    17. for (size_t i = 0; i < 100000; i++)
    18. {
    19. TestFunc1();
    20. }
    21. size_t end1 = clock();
    22. size_t begin2 = clock();
    23. for (size_t i = 0; i < 100000; i++)
    24. {
    25. TestFunc2();
    26. }
    27. size_t end2 = clock();
    28. cout << "TestFunc1 time:" << end1 - begin1 << endl;
    29. cout << "TestFunc2 time:" << end2 - begin2 << endl;
    30. }
    31. int main()
    32. {
    33. TestReturnByRefOrValue();
    34. return 0;
    35. }

    我们写了一个结构体,一个这样的结构体所占的空间是40000个字节。

    函数1和函数2的返回值都是该结构体类型,函数1会发生拷贝,函数2因为是传引用返回,我们分别调用两个函数100000次,看他们花费的时间。

    可以看到,传值返回和传引用返回的效率差了70倍,所以传引用返回可以减少拷贝,提高效率。

     引用做参数的作用:

    作用1:输出型参数,在函数中修改形参,实参也跟着改变:

    1. void Swap1(int& left, int& right)
    2. {
    3. int temp = left;
    4. left = right;
    5. right = temp;
    6. }
    7. //void Swap2(int left, int right)
    8. //{
    9. // int temp = left;
    10. // left = right;
    11. // right = temp;
    12. //}
    13. int main()
    14. {
    15. int a = 1, b = 2;
    16. /*Swap2(a, b);
    17. cout << a << " " << b << endl;*/
    18. Swap1(a, b);
    19. cout << a << " " << b << endl;
    20. return 0;
    21. }

    作用2:减少拷贝,提高效率:

     指针和引用的权限大小问题:

    1. int main()
    2. {
    3. int a = 0;
    4. int&ra = a;
    5. const int b = 1;
    6. int&rb = b;
    7. return 0;
    8. }

    因为我们的b是只读类型,rb是可读可写的,从只读变成可读可写的是权限的放大,权限的放大是不允许的。

    1. int main()
    2. {
    3. int a = 0;
    4. int&ra = a;
    5. /*const int b = 1;
    6. int&rb = b;*/
    7. int b = 1;
    8. const int&rb = b;
    9. return 0;
    10. }

    而权限的缩小或者权限的平移是允许的。

    注意:这里的权限放大缩小平移针对的只是指针或者引用。

    1. int main()
    2. {
    3. const int a = 10;
    4. int b = 20;
    5. b = a;
    6. return 0;
    7. }

    这里虽然是权限的放大,但是这里并没有指针或者引用,这种问题就不会出现错误。

    常见的易错点

    1. void Func(int&x)
    2. {
    3. ;
    4. }
    5. int main()
    6. {
    7. int a = 10;
    8. int&ra = a;
    9. const int&rra = a;
    10. const int b = 1;
    11. Func(a);
    12. Func(rra);
    13. Func(b);
    14. return 0;
    15. }

    报错原因:我们传递的实参rra,b都是只读的,而我们的形参是可读可写的,从只读到可读可写是权限的放大。

     所以一般用引用作为参数调用函数一般要用const修饰:

    1. void Func(const int&x)
    2. {
    3. ;
    4. }
    5. int main()
    6. {
    7. int a = 10;
    8. int&ra = a;
    9. const int&rra = a;
    10. const int b = 1;
    11. Func(a);
    12. Func(rra);
    13. Func(b);
    14. return 0;
    15. }

    这时候就不可能出现权限的放大。

    但是这种问题并不会大量出现,因为如果我们不需要在函数中对被引用对象进行修改的话,我们可以在引用前加上const,假如我们要对被引用对象进行修改时,我们传递的实参也一定是可修改的,不能用const修饰。

     const引用可以接收常量

    1. int main()
    2. {
    3. int&a = 10;
    4. return 0;
    5. }
    1. int main()
    2. {
    3. const int&a = 10;
    4. return 0;
    5. }

    下面这种写法对吗?

    1. int main()
    2. {
    3. const int&a = 10;
    4. double d = 12.34;
    5. int&ri = d;
    6. return 0;
    7. }

    错误,因为d是double类型的,而ri是int类型的,是这样吗?

     

    1. int main()
    2. {
    3. const int&a = 10;
    4. double d = 12.34;
    5. /*int&ri = d;*/
    6. int ri = d;
    7. return 0;
    8. }

    那么为什么以下这种写法是正确的呢?

    我们再举一个例子:

    1. int main()
    2. {
    3. const int&a = 10;
    4. double d = 12.34;
    5. /*int&ri = d;*/
    6. int ri = d;
    7. cout << (int)d << endl;
    8. cout << d << endl;
    9. return 0;
    10. }

    这里的强制类型转化是把d转化成了整型12吗?

    不对:本质上是有一个临时变量当作桥梁。

    临时变量是整型,先把d传递给临时变量,再把临时变量赋给i,所以这里的d并没有发生改变。

     而临时变量具有常性,我们又知道,const引用可以接收常量,所以以下的写法就正确了:

    const int&ri = d;

    这里并不是把d赋给ri,而是先把d传递给临时变量,const引用可以接收临时变量,所以没问题。

    我们再举一个新的例子:

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

     Count函数的返回是传值返回,中间产生临时变量,临时变量具有常性,所有我们要用const来修饰。

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

     

    引用在底层

    1. {
    2. int a = 10;
    3. int&ra = a;
    4. ra += 10;
    5. int*pa = &a;
    6. *pa = 30;
    7. }

    引用在语法上就是别名,不开空间。

    而引用在底层呢?

    const引用可以修饰常量(临时变量或者常数等)

    我们转到反汇编

    可以发现,引用在底层和指针的情况很相同。

    所以引用在底层也是使用指针实现的。

    引用和指针的不同点:

    auto关键字

    1. int main()
    2. {
    3. int a = 10;
    4. auto b = a;
    5. return 0;
    6. }

    auto关键字的作用是根据a的类型自动推导b的类型。

     范围for

    1. int main()
    2. {
    3. int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    4. for (auto e : arr)
    5. {
    6. cout << e;
    7. }
    8. cout << endl;
    9. }

     范围for的作用:依次取arr数组中的每一个元素赋值给e,自动判断结束,自动迭代。

  • 相关阅读:
    C#进阶07——反射和特性
    XAML标记扩展(3)
    电脑文件自动备份到百度网盘 实时备份
    【线程池】概述及创建
    正点原子《嵌入式linux c应用编程》视频教程学习笔记,持续更新中
    8月1日第壹简报,星期一,农历七月初四
    杭州市IT行业人才需求地图信息系统的设计与实现
    【数据结构】二叉搜索树(Java + 链表实现)
    Linux中查找最大文件的命令是什么?
    LeetCode第225题—用队列实践栈
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/132714411