4.函数重载:
在解释函数重载之前,让我们先想想C语言中一种比较头疼的情况:
假如我想编写一个支持不同类型进行相同处理的函数,我们知道函数的特性是规定参数类型,规定函数的返回值类型,那样就导致我们处理浮点型需要写一个函数,处理整型又需要写另一个函数,但本质上他们的函数操作时相同的,但我们使用函数的时候又必须得让其叫不同的函数名,处理函数少的程序还好,倘若是几百个函数组成的大程序,我们基本通过名字知道其功能,但频繁的改动名字就容易造成混乱,故C++为我们提供了一种良好的方式,让我们可以对相同功能但参数类型和返回值类型不同的情况使用同一个名字,这便是函数重载。
1.概念:
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
如下:
#include<iostream> using namespace std; // 1、参数类型不同 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; } // 2、参数个数不同 void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } // 3、参数类型顺序不同 void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; } int main() { Add(10, 20); Add(10.1, 20.2); f(); f(10); f(10, 'a'); f('a', 10); return 0; }
观察上面的程序你会发现:虽然参数数量不同,种类不同,返回值不同,但他们都可以叫一个名字且不会报错,这就是函数重载的效果,运行结果如下:
那为什么C++可以支持函数重载呢?C++是如何支持函数重载的呢?C语言为何不支持函数重载呢?我们来探究一下:
2.函数重载原理:
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
由我们前面已经学到的C语言的编译链接的知识点可知,
- 实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们
可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标
文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么
怎么办呢? - 所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就
会到b.o的符号表中找Add的地址,然后链接到一起。(老师要带同学们回顾一下) - 那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的
函数名修饰规则。
也就是说,实际上编译阶段编译器对函数的处理是首先识别函数的名字,但仅仅是识别名字,类似:—我知道你要用这个函数了,我清楚这件事情了----,然后最关键的是在链接阶段,由我们之前学过的知识点知道,链接阶段是程序去链接各种库函数和各种程序文件脚本的阶段,在这个阶段计算机将真正去识别相应的函数名字并且为其链接相应的函数的地址和库的地址,而我们C与C++编译器也是在这一阶段产生了不同。
. 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使
用了g++演示了这个修饰后的名字。
如下:
首先写一个函数:
int Add(int a,int b) { return a+b; } void Func(int a,double b,int*p) { } int main() { Add(1,2); func(1,2,0); return 0; }
C语言是如何识别链接函数的呢?
我们发现C语言是直接去识别函数名字分配地址,即看我们写的函数名字
C++呢?
我们发现C++识别的函数名与原函数名字有很大不同,加了一些字母和其他字符
是的,我想到这里,你应该知道为什么C++支持函数重载的原因了。
. 我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】,也就是说,C++识别的不是函数的名字,而是函数对应的参数的类型,参数的个数,通过参数的信息在链接阶段用对应的一些修饰规则修饰函数名字,这样就可以让同名函数通过修饰变得可以被分辨出来,这就是C++支持函数重载的原理。而反观C语言,只能识别函数名且不会修饰,这导致C语言写函数名字必须不同,否则无法识别。
一个关键的细节:
注意:函数修饰主要看的就是参数,是不看返回值的,返回值不是函数修饰的原因,返回类型是任意的都可
即:如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
5.内联函数:(关键字inline)
看到名字或许我们会感到很抽象和陌生,内联是什么意思呢?
不要着急,首先让我们再一次回想C语言的两个知识点:第一个是我们熟知的函数,第二个是我们有时候很少用的宏。
函数和宏的优缺点是什么呢?
宏的优缺点
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
函数的优缺点
优点:
1.可以实现复杂的程序功能
2.方便调试
3.代码逻辑清楚
缺点:
1.代码冗长,影响性能
2.实现简单功能时比较麻烦
1.概念:
那么,倘若有一种东西,它弥补了函数和宏的缺点,同时又保留了函数和宏的优点,这样的东西一定十分受欢迎,这便是C++的内敛函数。
例如:宏书写两数相加:
#define hbw(x,y) x+y; printf("%d",hbw(1,2)
如果用内联函数来写:
inline int hbw(int x,int y) { return x+y; }
我们会发现:内联函数的代码格式更类似函数,这意味着内敛函数是可以调试的,但它又与函数不同,以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。
2.内联函数的特点:
- inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。 - inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
也就是说,inline更多是向计算机提出一种想要使用内联函数的建议,但倘若计算机发现函数过大,就会无视这种建议依旧按照函数开辟栈帧。
注意一点:!!!!
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到
6.引用(最为重要的一个知识点!)
1.引用的概念:
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
使用的格式:类型& 引用变量名(对象名) = 引用实体;
例如:我已经创建了一个整型变量a,即 int a=100;接下来我使用引用,int&b=a;这样之后,b这个名字就相当于是变量a的别名,对b的影响本质上也就是对a的影响,这就是引用。
注意:引用类型必须和引用实体是同种类型的
例如:
void TestRef() { int a = 10; int& ra = a;//<====定义引用类型 printf("%p\n", &a); printf("%p\n", &ra); }
这里,打印a的地址和pa的地址,我们发现他们两个的地址是相同的,这也让我们思考到一个底层问题:引用的底层可能也是通过操纵指针去执行的。
2.引用的几点特性:
1. 引用在定义时必须初始化,即使用了引用就必须让其对应一个实体,不能像指针那样置空
2. 一个变量可以有多个引用,但一个引用不能对应多个实体
3. 引用一旦引用一个实体,再不能引用其他实体
3.常引用:权限的严格检查
看下段代码:
void TestConstRef() { const int a = 10; int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a; int& b = 10; // 该语句编译时会出错,b为常量 const int& b = 10; double d = 12.34; int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d; }
1.从第一个看起,我们定义了一个const int不可改变的整型,那我们接下来是不能使用int&来引用a的,这是由于计算机的一个底层逻辑:即权限是只能缩小或者同层,但权限是不能放大的,我们已经const int来赋予a的权限,倘若我们不加int引用a,反而导致了不可改变这一条权限被放大了,这是违反了基层逻辑的,故第二条编译会出错。
2.接着看第二个,我们直接int&b=10,但10本身是常量,相当于const int类型的,这样我们的问题就跟上一个一样了,故会报错,故我们也要加const
3.再看第三个,对于double类型的d,我们用int&来引用double,这样按理来说没错呀?**但这里我们就要提到一个知识点:对于包括存在类型转换的赋值情况,计算机首先会把右值拷贝一份出来,利用这个拷贝的右值来进行下面的赋值和操作,我们称这个拷贝的右值为临时变量,临时变量默认是常属性,即const 类型的,所以说到这里我们就知道了,依旧是权限的放大问题,故我们要加const **
4.引用与指针的区别和相对应的特点:
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
一个同类型实体
4. 没有NULL引用,但有NULL指针,引用必须初始化 - 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节,64位8字节)
6. 引用自加即引用的实体增加1,即对应的变量去加1,指针自加即指针向后偏移一个类型的大小,相当于位置的改变
7. 有多级指针,但是没有多级引用,一个引用只能对应一个实体 - 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
我们在语法概念的表层上可能感觉引用是不占用空间的,但其实本质上在底层引用是占用空间的,不过,我们现在的理解,就把引用理解为不占用空间就好
5.引用的应用场景:
1. 做参数
我们以冒泡函数的交换为例子:
倘若我们用C语言想要交换,就只能使用传指针的方式进行,这样我们还要反复写解引用符号*,有的时候容易忘记还麻烦,但倘若C++我们就可以用引用来操作,如下:
void Swap(int& left,int& right) { int temp=left; left=right; right=temp; } int main() { int a=0; int b=2; Swap(a,b); }
引用传参,最关键的是可以提升效率,且操作简便
这里我们的参数left right,实际上就是 a和b,我们就相当于直接对a b直接交换,而不是之前的传地址解引用后指代a b本身。
2. !!!做返回值(易错)!!!
我们之前已经见过2种返回值的情况:
1.传值返回
2.传址返回
而今天我们学完引用后我们就接触到一个新的返回方式:3.引用返回
首先要清楚一个问题:在函数中开辟的栈区变量,最终终会被系统销毁回收,而且函数的空间是可以重复利用的,一块空间销毁的位置下一次再开辟空间也会利用相同的位置,甚至变量的开辟也会从原来的位置开辟,注意,我说的是栈区空间,对于堆区和静态区变量,即使函跳出函数作用域,函数被销毁,这两部分空间也不会被销毁
既然清楚上面的特点,我们就要知道三种返回值的特点:传值返回实际上就是将要返回的变量的数值临时拷贝一份返回,而不是之前函数里面的变量了,传地址返回也是返回的就是拷贝到一份地址,而引用是将返回的变量本身返回回来,它是不经过拷贝过程的,返回什么就是它本身。
故我们必须要强调:引用返回应该作用于那些堆区和静态区的变量,因为他们在函数结束后不会被销毁,不会导致引用到非法空间的情况
而传值和传地址返回则不需要特别强调这个情况,因为本身有拷贝的过程,这也是引用返回容易出现错误的关键原因
如下面的情况:
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); Add(3, 4); cout << "Add(1, 2) is :"<< ret <<endl; return 0; }
那么我们这段代码的结果是什么呢?
我们发现,第一个数字是打出来的,而第二个则是一堆乱码,这是为什么呢?就像我之前所说的,函数作用域跳出后就会自动销毁函数栈帧,相应的函数里面的变量也不见了,故我们的引用指向的位置非法,但还是返回回来了,故其实我们的第一个3的位置就应该已经是一堆乱码了,但VS编译器可能仍然帮助保存了起来,但我只要改一下就会变成乱码,如下:
int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); cout<<"sssss "<<ret<<endl; Add(3, 4); cout <<"ssssss " << ret << endl; return 0; }
虽然是乱码,但我们发现两个乱码却是相同的,这说明了函数的开辟是可重复性的,引用对应的位置没发生改变,对应的的变量都开辟在同一个位置。
不信我们再这样写:
using namespace std; int& Add(int a, int b) { int c = a + b; return c; } int main() { int& ret = Add(1, 2); cout << ret << endl; Add(3, 4); cout << ret << endl; return 0; }
同样的位置,值是不同的,且符合函数的返回结果,正好证实了我的结论:这说明了函数的开辟是可重复性的,引用对应的位置没发生改变,对应的的变量都开辟在同一个位置。**
综上:我们要再一次强调,引用返回应该适用于跳出函数作用域后不会被销毁的变量,比如堆区和静态区,它是不适用于栈区变量的!!!!!!!
7.总结:
以上便是我初入C++掌握到的一些知识点,你会发现很多知识点在展开的时候都和C语言有联系,没错,要知道任何事物的产生都有其意义,倘若C语言是完美的,那么C++便没有诞生的可能性了,C++就是为了优化C语言的语法和错误而诞生的语言,我们接下来的学习也应该参考C语言的语法去思考,去举一反三,从而了解C++语法为何而诞生。