• 9.15c++基础


    目录

    引用:

    引用的实际的用处:

    const引用:

    临时变量如何处理:

    引用的实质:

    引用的总结:

    c++11小语法

    auto


    引用:

    接着我们上节课讲的一些引用的基础知识:

    1:引用就是别名,所以当变量a的引用为ra时,ra不仅值与a相同,连地址也与a相同。

    引用需要注意的问题:

    1:引用必须进行初始化,不能凭空引用:

    2:引用一旦引用一个实体,就不能再引用另外的实体。

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

    如图所示,我们进行调试:

    当main函数结束的时候,假如b和ra的地址是相同的,那么引用就可以引用多个实体,否则引用只能引用一个实体。

     如图所示:我们可以发现,ra和b的值是相同的,ra和b的地址是不同的,这证明了引用多个实体是不行的。

    引用的实际的用处:

    1:作为输出型函数的参数:

    我们先举一个输出型参数的例子:

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

    输出型参数表示我们要对参数本身进行修改。

    输入型参数:

    1. void Sort(int*a, int n)
    2. {
    3. }

    输入型参数就是我们平常使用的排序函数,输入要排序的对象和对应数组的元素个数,我们在函数内部实现调用。

    2:引用来做返回值。

    在这里,我们首先讲一下内存的划分:

    内存分为一下四部分:

    1:常量区(代码段):存放普通常量。

    2:静态区:存放的static修饰的静态变量。

    3:堆区:动态内存申请的空间

    4:栈帧:函数调用所开辟的空间

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

    要理解这个引用返回,我们可以先理解一下传值返回:

    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. }

    要理解这个传值返回,我们把最简单的传值返回讲解一下:

    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函数,调用函数,形成Count函数对应的栈帧,局部变量n就在栈帧空间内部,n++,n变成1,我们把n返回,这里的返回并不是直接把n返回给ret的,这里的返回是通过我们创建一个临时变量,用这个临时变量来接收n,但接收的只是n的值,然后Count函数调用结束,对应的栈帧销毁,对应的空间返还给操作系统,我们再把临时变量的值返回给Count值。

    注意:当这个临时变量比较小时,被存放在寄存器中。

    当这个临时变量比较大时,被存放在上一层函数的栈帧中,也就是main函数的栈帧中。

    要证明以上的想法,我们需要判断ret和n的地址是否相同:

    对应的ret和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. }

     这段代码和上面的唯一的不同就是变量n用了static修饰,static修饰的话就是静态变量,静态变量存放在静态区,所以不会随着栈帧的销毁而释放。当我们调用函数完毕后,返回n,这时候,我们还是创建一个临时变量来接收n,再由临时变量返回给ret。

    对应的ret还是1.

    但是当我们使用引用返回时,我们需要小心:

    我们举出一个错误案例:

    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,创建变量n,n++后等于1,返回1,这时候因为我们的返回值类型用的引用,所以我们返回的就是n的别名,但是函数调用完毕之后,属于n的那部分空间也已经被操作系统回收,这时候我们对属于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. return 0;
    12. }

    进行编译:

     打印的结果仍为1,原因是什么呢?

    答:我们画图进行解释:当我们调用Count函数时,我们对应的内存图像是这样的。

     但是当我们函数调用完毕后,我们对应的空间被释放:

     虽然我们的空间不属于我们了,但是对应的n值仍旧存在,没有被覆盖,我们用对应的同样的引用ra来接收a,a其实是n的引用,所以ra就是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. }

     为什么剩下的两次打印都是随机值?

    答:因为我们的ra是n的引用,当我们刚刚调用完毕Count函数时,我们对应的图象是这样:

     当我们第一次调用打印函数时,对应的图像是这样:

     我们对应的n值已经被覆盖,但是注意:当我们调用打印函数的时候传参的时候,n值仍是存在的,n值为1,所以我们打印的n为1.

    我们第一次打印函数调用完毕的图象是:

    我们第二次调用打印函数:我们的参数n对应的空间现在是打印函数内部的随机值,所以我们打印的结果是随机值

    第三次调用也是同样。

    正确的写法:

    1. int& Count()
    2. {
    3. static int n = 0;
    4. n++;
    5. return n;
    6. }
    7. void p()
    8. {
    9. int x = 100;
    10. }

    我们进行检测:

     正确的原因如下:

    因为我们是静态变量,静态变量n并不会随着函数的调用结束而被释放,并且我们返回的是n的引用,所以我们并不需要把返回值拷贝到临时变量中,进而能够节省空间。

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

    总结:传引用返回的作用

    1:减少拷贝,提高效率

    2:修改返回值。

    引用做参数的作用:

    我们写两个交换函数进行分析:

    1. void Swap(int a, int b)
    2. {
    3. int tmp = a;
    4. a = b;
    5. b = tmp;
    6. }
    7. void Swap(int&a, int &b)
    8. {
    9. int tmp = a;
    10. a = b;
    11. b = tmp;
    12. }

    其中,对于第一个函数,我们在调用函数时,需要拷贝实参作为形参,而对于第二个函数,我们只是引用了实参,所以第二个函数的效率更高。

    对于第一个函数,我们的交换并不能成功,原因是形参的改变并不影响实参。

    而对于第二个函数,我们的交换可以成功,原因是我们的形参是实参的引用,所以形参修改,实参也跟着修改了。

    总结:

    引用做参数的作用:

    1:减少拷贝,提高效率

    2:对于输出型参数,当形参改变的时候,实参也跟着改变。

    这两个函数构不构成重载:

    构不构成重载是根据编译器对于两个函数参数的理解,当编译器把int &识别成int时,这两个函数就不构成重载,当编译器把int&的类型识别为引用类型时,这两个函数就构成重载。

    我们进行运行检测:

    1. void Swap(int a, int b)
    2. {
    3. int tmp = a;
    4. a = b;
    5. b = tmp;
    6. }
    7. void Swap(int&a, int &b)
    8. {
    9. int tmp = a;
    10. a = b;
    11. b = tmp;
    12. }
    13. int main()
    14. {
    15. }

    代码并没有报错,证明函数构成了重载。

    但是这里其实构不构成重载无关紧要,因为无论构不构成重载,我们都无法调用这两个函数,因为Swap(2,3)同时适用于这两个函数,造成歧义性,所以无法调用。

    const引用:

    1. int a = 0;
    2. int&ra = a;

    这种写法是正确的。

    1. const int b = 1;
    2. int&rb = b;

    但是这种写法却不对。

    错误的原因是什么?

    因为b是只能读而不能写的参数,而rb是可读可写的参数,所以对应的b的权限增大了,所以导致错误。

    我们写一个权限缩小的例子:

    1. int b = 1;
    2. const int&rb = b;

     b是可读可写的,而ra是只读不可写的,所以由b到ra是权限的缩小,我们进行编译:

    我们再写一个权限不变的例子。

    1. const int a = 10;
    2. const int&ra = a;

    这两个都是只读而不可写的参数,所以由a到ra,权限是不变的。

    所以,我们进行总结:

    在指针或引用中,权限可以缩小,但是不可以扩大。 

    1. void Func(int&ra)
    2. {
    3. }

    假如我们要调用这个引用的函数用来减少拷贝来提高效率可以吗?

    答:不可以,原因是当我们调用这个函数的时候,我们的传参是受限的。

    1. void Func(int&ra)
    2. {
    3. }
    4. int main()
    5. {
    6. int a = 10;
    7. const int b = 20;
    8. Func(a);
    9. Func(b);
    10. }

    如图所示,我们调用Func(a)是可以的,但是我们无法调用Func(b),因为b是只读的参数,我们传参的时候,我们把只读的参数传给可读可写的参数。

    所以我们正确的写法是这样:

    1. void Func(const int&ra)
    2. {
    3. }
    4. int main()
    5. {
    6. int a = 10;
    7. const int b = 20;
    8. Func(a);
    9. Func(b);
    10. }

    但是,注意这种情况

    1. int a = 10;
    2. const int b = 20;
    3. a = b;

    b是只读的,a是可读可写的,把b赋给a是不是导致权限的放大?

    并不是,因为这里根本不涉及引用和指针,这里这是简单的赋值操作,传递的是参数值,所以没有任何影响。

    临时变量如何处理:

    1. void Func(int a=10)
    2. {
    3. }

    这个是普通的缺省参数。

    当我们加入一个引用符号时:

    这里错误的原因我们可以这样理解:

    10是一个常量,我们要对一个常量引用的话,我们要保证自身要是一个常量,所以我们可以用const修饰。

    1. void Func(const int&a=10)
    2. {
    3. }

     

     

    我们再举一个例子:

     为什么这里会发生错误?

    许多人的想法是类型不同,但是为什么下面这种写法可以呢?

     

    这里发生了隐式类型转换,转换的方式:首先,创建一个临时变量,临时变量接收了(d的数值被转换成int类型的形式),然后再把临时变量赋给变量i。(注意:d并没有改变,只是d的数值在传给临时变量的时候被转换了)

    我们可以进行实验:

     如图所示,我们可以发现d被赋值给i后,其本身并没有发生改变。

    这种写法其实就等价于强制类型转换。

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

    无论是强制类型转换,还是隐式类型转换,或者是整型提升,亦或者是传值返回,都有临时变量的参与。

    那这个时候,对应的i值是多少呢?

     

    临时变量的一大特点就是:临时变量具有常性。

    这个时候,我们就能解释为什么int&ri=d为什么报错了:

    d首先要进行转换,转换的时候,媒介是临时变量,我们要把临时变量赋给ri,而临时变量具有常性,我们要对具有常性的整型进行引用,那我们本身也要是常数才行。

    const int&ri = d;

    所以当我们这样写的时候,就不会报错。

    我们再写一串代码加强理解:

    为什么这里会有错误呢?

    答:因为这里是传值返回,传值返回的媒介就是临时变量,临时变量具有常性,所以我们无法用普通的引用来进行接收。

    我们需要加上const。

     

    引用的实质:

    在语法角度,我们的引用相当于给变量取一个别名,所以不会额外开辟空间,但是在底层实现的话,引用需要额外开辟空间。

    我们拿引用和指针解引用进行对比:

    1. int main()
    2. {
    3. int a = 10, b = 20;
    4. int &ra = a;
    5. ra = 15;
    6. int*pb = &b;
    7. *pb = 25;
    8. }

     第一个是通过引用把a的值修改为15.

    第二个是通过指针解引用把pb的值修改为25.

    我们转到汇编代码看一下:

    我们虽然看不懂,但是可以发现mov和lea都出现多次,我们查一下mov和lea对应的意思:

     mov:

     这个很容易理解,既然我们要修改a和b的值,我们就肯定要用数据传送指令。

    lea

    这里就能看出问题了,在我们的印象中,指针相关的操作是需要传递地址的,引用并不需要。

    这里的lea就证明了在底层实现的逻辑上来看:引用也是需要额外开辟空间的。

    引用的总结:

     

    c++11小语法

    auto

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

    auto的意思就是根据a的类型推导b的类型,所以b的类型也是int了。

    auto在for循环中的使用:

    我们现在写for循环要这么写:

    1. int main()
    2. {
    3. int i = 0;
    4. int a[5] = { 1, 2, 3, 4, 5 };
    5. for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
    6. {
    7. cout << a[i] << " ";
    8. }
    9. cout << endl;
    10. }

    是不是非常复杂,我们使用auto可以这样写:

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

    这串代码的意思是:我们首先调用for循环,把数组a中的内容赋给e,然后自动判断结束,自动迭代,打印出所有的数组值。

  • 相关阅读:
    Rock18框架之整体框架介绍
    【目标检测】数据增强:YOLO官方数据增强实现/imgaug的简单使用
    2022上海生物发酵展-品牌企业纷纷入驻抢占先机,谁来赴盛宴参邀您的参与
    0730~Mysql优化
    2022牛客多校(二)
    Linux环境变量
    利用浩客无代码开发API集成客服系统,提升用户服务质量
    java语言的核心特点、特性&核心机制
    裸辞—躺平—刷题—大厂(Android面试的几大技巧)
    Pytest测试中的临时目录与文件管理!
  • 原文地址:https://blog.csdn.net/qq_66581313/article/details/126909418