> 文章列表 > 函数的缺省参数,函数重载与底层函数名修饰解释,引用的初步介绍

函数的缺省参数,函数重载与底层函数名修饰解释,引用的初步介绍

函数的缺省参数,函数重载与底层函数名修饰解释,引用的初步介绍

TIPS

  1. 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
  2. 在日常练习中,建议直接using namespace std即可,这样就很方便。
  3. using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
  4. 头文件在预处理阶段之后就已经默认在.c或.cpp文件上展开了。编译器在实际预处理完了之后就没有头文件什么事儿了,头文件的内容早已经全部展开了,要处理的都是.c.cpp。

函数的缺省参数(备胎)

  1. 缺省参数是声明或定义函数,是为函数的参数指定一个缺省值,然后再调用该函数时,如果说没有指定实参,那么就用形参的缺省值,否则就用指令的实参。
  2. 是在c语言当中闻所未闻的,C语言的语法当中,如果定义函数的时候,参数必须是一个数据类型加上一个形参名称,不可能再说去等于一个什么值,是在c++里面就可以,因为他语法上面更完善了,有了缺省参数这个含义,你实参没有传过来的时候,那我在函数里面的形参就用之前定义函数时设定的缺省值
  3. 缺省参数又可以分为全缺省参数与半缺省参数,全缺省参数就是说这个函数里面所有参数都是缺省参数在这里插入图片描述
  4. 半全省参数,就是说这个函数的参数部分从右往左连续依次为缺省参数,但在最开头一些就不是缺省参数,缺省参数的分布必须是从右向左依次来给出,从右向左连续,不能间隔着给。在这里插入图片描述
  5. 对于全缺省参数的函数,它的调用方式就非常多了,在调用是全缺省参数的函数的时候,假设这个函数的参数有五个,全部都是缺省参数,你传一个参数,那这个参数就是给第一个缺省参数的,你传两个参数,那么这两个参数是给头两个缺省参数的,那么这样子其他后面三个缺省参数在函数里面用的就是事先设置的缺省值,不能跳跃的传。
  6. 对于半缺省参数的话,都必须得从右往左缺省,这些都是规定死的。
  7. 函数里面的缺省参数设定的那个缺省值,只不过是个备胎而已,有实参肯定优先用实参
  8. 如果说一个函数有缺省参数,在函数的定义与声明当中,不能同时给缺省值。首先如果说你给的两个缺省值不一样,那就相当于有两个备胎,那么当你没有实参传进来的时候,我到底该用哪个缺省值呢?其次就算是你给了两个缺省值,这两个缺省值是一样的,编译器还是会报错的。所以说不要同时去给。一般推荐的写法是在声明当中去给,函数具体定义的时候就不要去给了,倒过来也不可以。在这里插入图片描述
  9. 然后具体说到这个缺省值,也就是这个备胎的话,一般都是用常量与全局变量去充当缺省值。一般情况下都不会用全局变量,就用常量
  10. 反正对于缺省参数,只要记住,因为我们在写大一点的项目的时候,函数的声明与定义肯定是分开写的。然后这时候如果说你在这个函数参数里面想要使用缺省参数的话,这个缺省值在函数声明的时候去给啊,不要到函数具体定义的时候。更加不能两种情况同时给缺省值。

程序环境与预处理的复习回顾

  1. 在这边必须得不得不把程序的编译链接等知识回顾一下。程序运行的环境分为翻译环境与执行环境。在这个翻译环境之下,又分为编译加链接。然后这个编译的过程又分为预处理,编译,汇编。当我们把源代码test.c写完之后,我们的这个代码首先需要经过预处理阶段,这个预处理阶段主要干的是:删除注释,处理预处理指令,处理预定义符号,这都是一些文本操作,主要是替换与删除之类,诸如#include #define 等等都是在这个时候被处理掉的。然后这时候可以用gcc命令生成xxx.i文件;预处理完了之后,接下来就是编译环节,这个环节主要就是把预处理完之后的源代码给他翻译成汇编代码,在这个过程当中有很多细节,其中有一点是符号汇总:所谓的符号就是具有全局性质的变量名与函数名,在这个过程当中会把这些符号给他汇总记录下来,在这个过程当中会默认生成xxx.s文件。接下来是汇编阶段:汇编阶段就是把翻译好的汇编代码首先给他分成一段一段,然后变成可以执行的机器指令,也就是二进制指令,这时候会默认生成xxx.obj目标文件,当然在这个过程当中也会有很多的细节,其中一个就是形成符号表,相当于就是把那些符号与这些符号所对应的地址给联系结合起来,如果说地址暂时找不到,那就是随便一个随机值。此时编译阶段已经完成,上述的这个阶段每一个.c文件都是单独的经过编译器去编译生成单独的一个.obj文件,各个之间互不干涉,互不影响,各自都独立完成。接下来是链接阶段,这时候我已经生成了这么多个互相独立的obj文件,是时候该把它整合起来了,链接阶段分为合并段表,符号表的合并与重定义,在这个过程当中,首先把之前生成的obj文件里面的所有二进制指令一段一段的嘛,先给他全部合成起来,然后把符号表也全部合成起来,与此同时,如果有地址随机值的话就用正确的地址去代替随机值。最后把这么多的.obj目标文件全部整合成一整个.exe文件,就是可执行程序,里面全部都是二进制代码,计算机能够看懂,我们看不懂。在这里插入图片描述
    在这里插入图片描述

同作用域内的函数重载

  1. c语言当中不允许同名函数的存在,也就是说函数名不能相同。什么地方我们就会用这些所谓的同名函数呢?比如说现在我有两个add函数,一个函数实现两个整形的相加,另一个函数实现两个浮点数的相加。在c语言当中,这两个函数不能够都叫做add,必须得以示区分:比如addi, addd。但这样子就会非常的不方便。然后在c++当中就增加了一个新的东西,叫做函数重载。
  2. 函数重载,它允许不同函数函数名相同,但是他有另外一个要求,必须参数不同。那什么叫做参数不同呢?包含三个方面:一个是参数个数不同,还有一个是参数类型不同,还有一个是类型顺序不同。
  3. 函数重载,也就意味着相同名字的函数,我在代码当中具体定义了不止一个。那当具体在main函数里面的这么一个名字的函数在实现的时候用哪个定义实现呢?它是会自动识别类型。
  4. 反正函数重载,尽管这些函数定义它们的函数名称都是相同的,但是要么是参数数量不同,要么是参数类型之间不同,要么是参数类型的顺序不同,反正你在具体传入实参的时候,编译器会根据你传入的参数的类型,数量与顺序等,去自动识别到底是去用哪个函数定义去实现
  5. 函数重载是指允许在同一作用域中声明几个功能类似的同名函数。注意,必须是在同一作用域当中。比如说都定义在全局域中。说都定义在同一个命名空间域当中。比如说你们在不同的命名空间当中,那就不行了,就不是重载了,必须在同一个作用域当中,然后函数名称可以相同,但是必须是参数的个数,类型或类型顺序不同。
    在这里插入图片描述

同作用域内函数重载的底层解释(函数名称修饰)

  1. 那这样子会不会相较于c语言而言变慢了呢?实际上不会。如果说你是在运行的时候再去匹配类型,这样一折腾肯定会变慢。但刚才这个匹配同名函数的过程实际上并不是在运行阶段完成。而是在编译的时候就已经匹配完成了,编译过后整个代码实际上都是一个二进制指令了,函数调用代码实际上也会变成call 函数地址。所以说如果说放慢的话***,也应该是编译速度变慢了一点,与运行没有任何一点关系***。而编译的速度我们也根本就不是很在乎。
  2. 那接下来就深挖底层:编译的时候这个编译器是怎么指定或识别是哪个同名函数的呢?C++其实相对于c语言是做了很多处理的。这个不一样的处理就是c++对函数名进行了一些修饰。
  3. 首先我们需要知道的是和我们之前学的函数栈帧,程序环境与预处理的知识,当在编译完成就是说把代码给他转换为汇编代码的时候,这个add函数转化为汇编代码,其实就是一条call指令,call的后面就是这个add函数的地址(我们现在讨论的是汇编代码这个层次)。然后代码执行到这边的时候就会去跳到地址所在的地方,然后接下来到那个调用函数里面,在一开始我们都知道就又是创建函数栈帧啊等等,所以说函数的地址本质上最后走走走走过去就是一串指令,因此说函数的地址本质上就是说函数里面一堆指令当中第一句指令的地址(我这边说的指令不是可执行的机器指令,我这边说的指令也不是二进制指令,我现在说的是汇编代码)。
    在这里插入图片描述
    在这里插入图片描述
  4. 那然后我们现在言归正传,比如说现在我有两个add函数(当然是参数不同的喽),那然后到时候转到汇编代码的时候,他是怎么去知道call后面跟哪个add函数的地址的呢?其实呢c++里面做了一个事情叫做函数名修饰规则
  5. c的话是区分不了的,在c语言当中,它直接通过函数名去找地址,然后在c++这边的话,他把这个函数名给他修饰了一下,然后具体修饰的规则的话,不同的平台上也会有不同,我们这边VS 2019还比较麻烦与复杂,在Linux环境下,***这个函数名修饰成这样:_Z(这个是规定的前缀)3(这个函数的长度)add(函数名)ii(他这边取的是这个参数类型的首字母),***因此尽管你在肉眼上看到两个一模一样函数名的函数,但实际上经过它内部的修饰的话,这两个函数的函数名在内部还是不同的,那么因此也有他各自的一个地址。
  6. 比如说_ZAdd3ii有他自己的地址;_ZAdddd也有他自己的地址。然后我到时候去寻找地址的时候我是拿修饰后的函数名去寻找,修饰的规则还是一样的,然后就这么拿着个修饰后的名字去找,然后找到地址,找到了之后我就放到call的后面。C话是直接用纯裸的名字去找,然后如果说你有两个函数名称相同的话,那不就直接over了吗?会冲突。
  7. 所以说再回到之前,如果说你的参数个数不同,你的类型不同,你的类型之间顺序不同,都会对修饰后的函数名产生影响,修饰后的名字不同,那么去找的时候就能够找到对应的真正的对象,就可以很好的以示区分了。在这里插入图片描述
    在这里插入图片描述
  8. 如果说两个函数函数名是一样,这个就不多说了,然后类型数量,类型名称,类型前后顺序也一样,但就是返回值不一样,那请问这时候能不能构成函数重载的呢?那这肯定是不能的。那为什么不能呢?有些人说在函数修饰的规则里面,在对函数名修饰的时候,没有把返回值给他纳入进去,这个回答大错特错,就算我把函数的返回值也纳入进去,还是不能够进行函数重载,为什么呢?是这样的,我在转汇编的时候,进行修饰函数名的时候,即使我把函数的返回类型也纳入进修饰过程当中,但我刚在调用函数的时候,比如说前后两行代码为add(1,2)与add(1,2),那我在这时候我在进行修饰的时候,我该怎么区分呢?对于这个返回值根本就无能为力我怎么知道你到底是要调谁呢?到底是有返回值的那个还是没有返回值的那个呢?所以说不是因为函数名修饰规则没有带返回值,而是因为调用的时候只写了参数,没办法区分。
  9. 再举个例子,比如说int func1(int*p, double x);这个函数的修饰后函数名是_Z5func1Pid

在这里插入图片描述
这样子也不能算函数重载,反正函数重载的话,一定要从底层函数名修饰的角度去理解

引用的简单介绍 (内存数据类型)& 外号 = 变量名or已有外号 一定要从内存角度理解

  1. 应用其实是补了c语言当中指针的大坑。指针确实在有些场景之下让人用起来很不舒服。
  2. 引用就是取别名,诸如蛋哥。首先我们必须得谈一下正常的赋值操作。比如说现在有个变量a是4,然后我又新去创建了一个变量b,然后b=a。这种就是赋值操作,就是说我又先去创建了一个四个字节的一块内存空间,然后把a里面的数据给他拷贝给到了b,就是这么一个正常的赋值操作。
  3. 然后这边还得插一句:C++里面有很多符号是共用的,比如说到目前为止我们已经知道至少<<就是共用的,这个不仅仅是移位运算,这还是一个流插入符号,也叫作输出。然后这边在引用过程中,也出现了符号共用的现象。
  4. &C语言当中它只有一个含义,就是取地址的意思。引用就是取别名的意思。比如说这一串代码的意思就是:int i = 3,int& k = i。我们已经知道有一个变量i,而它的值是3,我给这块空间又取了一个名字叫做k,k就是i,i就是k。注意:如果&放在类型后面(如:int&),那就是引用。然后这个符号&如果放在变量的前面,那就是地址的意思
  5. int& k=i,你只需要记牢:k就是i,i就是k(但两者只是同一块内存空间的不同名字而已)。这个需要与赋值进行区分,你把赋值赋给一个变量之后,这两个变量本质上是两块各自独立的不同内存空间,但这边引用的话只是同一块内存空间的两个不同的名称而已。然后可以对他本身再取一个别名,然后你也可以对别名再取别名。反正也只要记牢,是具有传递性的。
  6. 你说我有一个交换两个数的这么一个swap函数,我在往swap函数里面传入参数的时候,肯定不能传数值,肯定需要传这两个数的地址。一定要传址调用,因为不然的话,形参的改变不会影响实参。在c语言的背景之下,需要用指针去解决。在c++里面就不一定必须要用指针。注意:引用的一个最核心的特质就在于:空间不变,空间不变,空间不变,空间不变,只是同一块空间用了两个不同的称呼而已。相较于c语言那种指针的方式就是看起来更稍微舒服一点。

在这里插入图片描述
8. 所以在函数参数里面用引用,他的作用就凸显出来了。然后任何类型都可以取别名,你也可以对指针进行取别名,(那块空间的数据类 型)&别名。引用的作用就是如果说你希望形参的改变能够影响实参,在c当中你必须传地址,这是毋庸置疑的。是在c++当中,可以不用去传 地址,直接引用一下,直接原原本本原空间空降到这个函数里面。(一定要从内存的角度去理解)