一、函数重载
1.1什么是函数重载?
函数重载可以理解为一词多义,在C语言中,我i们不允许同名函数存在,但C++中,我们可以在同一个作用域下声明几个功能类似的同名函数,他们参数列表的参数个数或参数类型或类型顺序不同。
如:
F1(int a,char b){//...}
F1(char a,int b){//...}
就构成了一种函数重载。
1.2一种特殊情况的分析
如果两个命名空间b1和b2中,我们都定义了相同参数列表的同名函数:
b1中:
void Swap(int* a,int* b)
{
*a=*b;
}
b2中:
void Swap(int* a,int* b)
{
*b=*a;
}
然后我们直接把这两个命名空间都给展开了,那么此时程序运行起来会报错吗?
答:问题中的操作很明显并不符合函数重载的定义,所以不是函数重载,但是我们要知道,命名空间的展开并不是把命名空间的值放到了全局域中,而是允许外部直接调用命名空间的内容,仅此而已。所以说,只要我们不去在主函数中调用Swap这一函数,程序是不会报错的。
1.3定义重载函数时应注意的问题
①函数的返回值不作为重载的条件,因为无法区分例如有int f()和char f()同时存在,我们用f()究竟调用了谁。
②重载函数的调用不可以出现歧义,尤其注意与缺省参数的配合使用
例如:
void f(){//...}
void f(int a=10){//...}
我们无法分清在用f()时究竟调用了哪一个函数,这种情况是不被允许的。
1.4为什么C语言不支持函数重载而C++支持函数重载
要解释清楚这一问题,我们需要把注意力转移到编译过程中产生的汇编代码与连接过程中发生的查找操作中去
1.4.1程序在编译链接过程中发生了什么?
假如我们创建了三个项目Stack.cpp Stack.h Test.cpp来实现一个工程,那么我们在编译链接的过程会发生如下改变:
1.4.2汇编代码中的关联(此处使用VS2019编译器)
在工程中,假设我们有一个这样的函数来完成栈的初始化
void StackInit();
转移目光到汇编代码,当我们调用这个函数的时候,会有一句汇编代码
call StackInit(地址1)
call的本质其实是跳转,根据地址1,我们会跳转到对应这样一句语句的位置
jmp StackInit(地址2)
----地址2本质上就是函数的第一条语句的地址,因为我们在书写程序的时候不会知道中间要经过jmp,因此我们大可认为函数名就是函数首语句的地址。----
此时我们就可以跳转到地址2对应的函数语句的位置实现函数功能了。
1.4.3链接过程中的操作
首先我们要明确编译链接的过程都是单向的,即Stack.cpp一路生成Stack.o的过程,与Test.cpp一路生成Test.o的过程没有任何交集,唯有两个cpp文件共同引用的Stack.h内容二者都有。
然后我们就具体情况分析,Test文件中包含我们要执行的主程序main函数,所以目光聚焦到Test文件,我们只有函数的声明与要进行的函数调用语句,而这两者是有关联的,有关联说明存在语法规则,会在编译的第二阶段进行验证,查看二者是否匹配,只要匹配上,虽然还不知道函数是否实现,但还是会通过这一步(生成对应的的函数名/修饰后函数名)。
接下来,就到了Stack.o与Test.o两个文件的链接过程了。
在C语言中,我们根据的是函数名去Stack.o中查找对应的实现(此时编译器已经生成了一张符号表,其中存在函数名与地址的一一映射,查找的依据便是这一符号表),找到后,会成功编过并运行程序。
而在C++中,我们则是根据被编译器另行修饰后的函数名去Stack.o中查找对应的实现,这意味着不再是根据简单的StackInit函数名,而是配合了函数名和参数列表等要素构成的新名字,
----如果我们使用了函数重载,那么这一步就是作用的体现了。之所以要参数列表不同,也与函数新名的修饰规则有关。----
找到后,会成功编过并运行程序。
1.4.4总结结论
C语言在链接时用函数名直接查找,故不支持重载;但是C++是用修饰后的函数名进行查找,就支持重载了。
二、引用类型
2.1什么是引用类型?
引用就是起别名,比如
int a=0;
int& b=a;
此时b就是a的别名,也就是引用类型,他的出现是为了在一些情况下避开指针的使用,指针使用起来容易混淆。
2.2引用类型使用时一些奇特的允许情况
2.2.1可以给变量a起多个别名
如:
int& c=a;
int& d=a;
int& e=a;
此时b,c,d,e都是a的别名,如果这时我们赋值给a以10,那么b,c,d,e都会变成10.
2.2.2可以给别名起别名
如:
int& c=a;
int& d=c;
int& e=d;
虽然代码不一样,但是作用与2.2.1中完全相同。
2.2.3可以给指针变量取别名
int arr=11;
int* parr=&arr;
int*& pb=parr;
这样一来,改变*pb的值就可以改变parr中存的值了,也就是改变parr指向。
2.2.4在汇编层面跳过解引用去存变量的地址
如果我们在编译器中输入了下列两行指令,并不会报错
int* ptr=NULL;
int& r=*ptr;
我们知道,空指针一旦被解引用是一定会出问题的。
那么这两行代码跑通唯一的可能性就是*ptr并没有被直接执行,而是阴差阳错的错开了执行过程,究竟发生了什么呢?
观察一下汇编代码
不难发现,ptr在被正常创建以后,生成了有自己地址的指向NULL的指针变量,第二个语句直接找的是ptr自己的地址,存入了引用类型r中,所以没有出现错误。
这一情况告诉我们,从汇编代码的角度来说,指针和引用都是在存变量的地址,汇编中二者无区别。
2.3引用类型的意义(举例说明)
2.3.1做函数参数
①比如交换函数
void Swap(int& a,int& b)
{
int tmp=a;
a=b;
b=tmp;
}
在主函数中无需再传地址,只要传两个变量就可完成他们的数值交换了。
②一些特殊情况还可以提高效率
比如定义了这样一个结构体
struct A
{
int a[10000];
}
如果我们后续在主函数中创建了struct A类型的变量,又在函数参数列表中包含了
struct A al
此时为传值调用,因为a数组的存在要开一个很大的临时空间,但是如果我们传
struct A& _al
这一别名,就可以避免开这部分空间了。
③数据结构的顺序表的尾插等等需要改变结构体变量内容的操作时,我们就可以
void SeqPushBack(struct SeqList& sl,int x);
在主函数直接传结构体变量就好,不用再取地址进行传址调用了。
2.3.2接收临时变量
我们在进行类型转换(包括隐性/强制类型转换)和表达式运算的时候,会经历这样一个过程:
①原数据发生改变,产生一个不可以被修改的通用类型(即可以被任意类型的变量接收)临时变量。
②这一临时变量配合要求发生改变,赋值给目标变量。
那么引用类型就相当于可以改变第二步,直接接收临时变量。
如:
double d=12.14;
int i=d;//隐性类型转换
//
int& r1=d;//会报错,因为临时变量有不可修改属性,
//这一做法会使权限放大。
//
const int& r2=d;//成功接收
//
//
double f=13.13;
const double& r3=d+f;//也会发生存储临时变量
2.4引用类型的一些特性
①引用类型在定义的时候必须要初始化
②一个变量可以有多个引用
③引用一旦指向一个实体,就不能再引用其他实体了
如:
int a=0;
int& b=a;
int c=11;
b=c;//此处只会是把11赋值给a变量,不会改变b的引用对象,当然,执意要改变需要int& b=c;
//但很明显这会出现重定义的问题,不被允许
2.5关于引用类型,指针类型在权限问题上的分析
权限的问题主要与const修饰符有关,我们遵循一个原则:
权限可以缩小,可以平移,但绝不能放大
2.5.1引用类型放大和缩小权限
const int m=0;
int& n1=m;//这个语句不被允许,因为它放大了权限
const int& n2=m;//这个语句被允许,它平移了权限
int x=0;
const int& y1=x;//这个语句被允许,他缩小了权限
int& y2=x;//这个语句被允许,他平移了权限
2.5.2指针类型放大和缩小权限
int a=0;
const int* m=&a;
int* n1=m;//这个语句不被允许,因为它放大了权限
const int* n2=m;//这个语句被允许,它平移了权限
int b=0;
int* x=&b;
const int* y1=x;//这个语句被允许,他缩小了权限
int* y2=x;//这个语句被允许,他平移了权限
2.5.3正常赋值
const int p=0;
int q=p;
2.5补充说明
回顾一下const对于指针和指针指向内容的修饰作用
int m=0;
const int* p1=&m;//const在int*之前,意味着p1指向的内容m不可以被修改
const int* const p2=&m;//int*前后都有const,意味着p1指向的内容m,p1本身的值都不可以修改
int* const p3=&m;//consr在int*之后,意味着p1本身的值不可以修改
2.6引用与指针的区别
2.6.1引用在语法上是起别名,而指针在语法上是创建新变量
这会直接导致下列区别:
①引用不开新空间,指针要开新空间
②sizeof对应类型的结果,引用是别名对应变量的大小,指针则是地址大小4/8
③没用空引用,只有空指针
④引用一个实体后,引用就不可以更改,指针可以在任何时候指向相同类型的实体
⑤有多级指针,没有多级引用
⑥访问实体的方式不同,指针需要解引用,但引用不需要