大话cppC++对函数的补充(函数重载+缺省参数+内联函数)快进收藏夹

Posted 白龙码~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了大话cppC++对函数的补充(函数重载+缺省参数+内联函数)快进收藏夹相关的知识,希望对你有一定的参考价值。


Part I、前言

函数的存在是为了更好地复用一段代码,因为调用一个函数来实现一个功能往往能够很好地提升代码的可读性(如果有良好的注释和函数命名风格的话)。

对于有C语言基础的人,一个福音就是:C++延续了绝大多数C语言函数的语法,同时又补充了几个新语法以解决一些对于C语言比较棘手的问题。

咱们话不多说,开始第二阶段的学习。


Part II、新语法之函数重载

1、存在即合理

在讲C++函数重载时先考虑一个问题:如果在C语言中想写一个交换函数应该如何搞定呢?

void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

这段代码我相信大家咣咣咣就写出来了。

那么第二个问题:交换的数不一定是int,还可能是double、char…如何处理呢?
easy!

void swapInt(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
void swapDouble(double* x, double* y)
{
	double tmp = *x;
	*x = *y;
	*y = tmp;
}
void swapChar(char* x, char* y)
{
	char tmp = *x;
	*x = *y;
	*y = tmp;
}

我想说,无论在C++还是在C语言,语法层面上这样写完全没有问题,但是在使用者的层面上呢?这样写的弊病就浮上水面了——命名啰嗦,使用麻烦。
既然是交换两个数,为什么一定要刻意强调它的类型呢?

也许习惯了C语言的我们刚接触这个会比较不适应,而笔者在刚开始学到重载函数时也认为:swapInt、swapDouble,很清晰明了啊。

不过,我们不妨来看看C++中对于重载的定义:

void swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
void swap(double* x, double* y)
{
	double tmp = *x;
	*x = *y;
	*y = tmp;
}
void swap(char* x, char* y)
{
	char tmp = *x;
	*x = *y;
	*y = tmp;
}

或许在定义上看起来没什么感觉,那使用上呢:

int main()
{
	int a = 10, b = 20;
	double c = 1.0, d = 2.0;
	
	//C++使用函数重载的写法
	swap(&a, &b);
	swap(&c, &d);
	//C语言的写法
	swapInt(&a, &b);
	swapDouble(&c, &d);
	
	return 0;
}

是不是瞬间真香了?
不得不说,函数重载最大的好处之一就是:使用起来更加地简单明了。
具体还有什么其他作用,我们在后面总结时细说。

2、函数重载语法的详细介绍

相信看完swap函数重载之后大家能够观察到一部分重载的必要条件了:

  1. 函数名必须相同
  2. 参数必须不同

重载函数的名字必然相同,这个没什么疑问,那参数必须不同是指哪些方面不同呢?

  1. 参数类型不同
    比如说:
void printArray(int* a, int n);
void printArray(double* a, int n);

第一个重载打印整型数组的内容,第二个重载打印浮点型数组的内容。

  1. 参数顺序不同
    比如说:
void printArray(int* a, int n);
void printArray(int n, int* a);

一个是将指针放在第一个参数的位置,另一个是将数组大小放在第一个位置;
但是必须要注意的是:
参数顺序不同构成重载的前提是:这些参数不能是相同类型的参数
举个例子大家就能很好的明白了:

int add(int a, int b);
int add(int b, int a);

这种情况就是相同类型的参数变换了一下顺序,但是它们本质上就是完全相同的函数,唯一的区别就是形参名字不同。

但事实上形参的名字是可以随意定义的:你可以让第一个形参称为a,第二个称为b,当然也可以反过来,只是习惯上我们选择前者罢了,但在编译器层面,它们完全相同!

  1. 参数个数不同
    举个例子:
int add(int a, int b);
int add(int a, int b, int c);
int add(int a, int b, int c, int d);

它们都叫add函数,完成整型相加的任务,只是相加的整型的个数不同。

  1. 以上三点只要满足至少一个就可以构成重载
    比如说,这两个构成重载,因为它们参数个数不同:
 int add(int a, int b);
int add(int a, int b, int c);

这两个当然也构成重载,因为他们不仅参数个数不同,参数类型也不同:

 int add(double a, double b);
int add(int a, int b, int c);

最后,还要补充最最重要的一点:仅有返回值类型不同是无法构成重载的!
为什么呢?它牵涉到重载的底层原理,且继续往下看!

3、为什么C语言没有重载?(重载底层原理)

在了解这部分之前,我们需要回顾一下编译链接部分的知识,具体可参考这篇博客:程序的编译与链接
接下来我带大家简单回顾一下与我们这部分相关的知识——

首先,工程的每一个源文件都是单独编译的,而编译阶段会有一个步骤:形成符号表。
假如,test.c文件中有这样的内容:

#include <add.h>
int main()
{
	int ret = add(1,2);
	return 0;
}

add函数是定义在其他源文件中的,而add.h中包含了这个函数的声明,也就是:int add(int a, int b);由于我们包含了这个头文件,因此这个头文件中的内容就会在预编译阶段展开,于是这个程序就变成了这样:

int add(int a, int b);
int main()
{
	int ret = add(1,2);
	return 0;
}

那么这个源文件形成的符号表就可能是这样的:

第一列对应的是符号的内容,而第二列对应的是符号的地址。
比如这里,ret的地址是0x14af32bb,但是add函数的地址未知,为什么呢?因为在这个文件中,我们仅仅有add函数的声明,没有它的定义。
声明的作用仅仅是告诉编译器:add函数是的确存在的,只是没在这个源文件中实现。因此,能够使编译通过,但是它无法告诉编译器add函数的地址是多少。

寻址的步骤是在链接阶段。
链接时,编译器将所有源文件的符号表进行合并,那么此时,test.c这个源文件中符号表的???就可以填补上了。

那么我们这里注意到一个问题:C语言的编译器对于符号的修饰规则很简单,函数名是啥,那么它对应的符号就是啥。

于是乎,函数重载对于C语言就可望不可即了。为什么呢?
我们设想一下,在add.c中有两个函数:

int add(int a, int b)
{
	return a + b;
}
double add(double a, double b)
{
	return a + b;
}

那么在编译初步形成符号表的时候就会有一个问题:这里有两个重名的符号,但是对应了两个不同的地址,哪个地址是对的呢?这时候编译器就蒙了。

那么姑且我们认定,一个符号表可以出现两个相同的符号,那么就会是这样:

那么在链接时又会有问题:add.c有两个add,可链接器无法区分哪个才是使用者真正需要的,它懵逼了。

所以,对于C语言,函数重载的实现太难了。那C++又是如何解决这个问题的呢?
很简单,有问题那就解决问题。你不是说两个函数形成的符号时一样的吗,那就修改C++的符号修饰规则,函数的符号不再与函数名相同,而是采取这样的方式(仅作了解即可):
_Z + 函数名长度 + 函数名 + 参数类型的缩写(linux下的修饰规则)
比如,int add(int a, int b)就变成了_Z3addii
ps:windows下对于C++符号的修饰规则更加复杂,这里不再赘述。

我们继续说。

既然函数的修饰规则变了,那么尽管它们是同名函数,但是由于参数的个数、类型、顺序不同,就导致它们形成的符号有所差异,所以在链接的时候链接器就能够很容易的锁定目标。


看到这里想必大家应该理解为什么C++可以支持重载了吧。
那么这里再继续补充解释重载的必要条件:

  1. 为什么参数顺序不同但是必须类型也不同才构成重载。
    因为对于int add(int a, int b)int add(int b, int a),它们的符号都是_Z3addii
  2. 为什么仅有返回值类型不同不能构成重载
    因为C++的符号修饰规则与返回值类型无关,它只考虑了函数名、函数名长度、参数类型以及参数顺序。
    可是为什么C++没有考虑到根据返回值类型不同构成重载呢?
    很简单的一个例子:
int func();
bool func();

或许编译器可以根据接收返回值的变量类型判断调用的是哪一个函数

int main()
{
	int a = func();
	bool b = func();
	return 0;
}

但是肯定也会有这种情况:

int main()
{
	func();
	return 0;
}

此时编译器根本就不能判断应该调用哪个重载了。

4、关于extern “C”

作为C++中的关键字,extern “C”(注意,C是大写)有一个很奇怪的作用:告诉编译器,将接下来的这些内容按照C语言的方式编译。
比如:extern "C" void func();就是将func函数按照C方式编译;
再比如:

extern "C" 
{
	#include "func.h"
}

该操作就是将"func.h"头文件中的函数统统按照C语言的方式编译。
前面我们说了,C++和C的编译中,最大的一个不同就是符号的修饰规则。
而加上extern "C"之后的好处就是:C语言的项目也可以使用C++的头文件,因为符号修饰风格恢复成了C的格式,C的链接器在链接时能够找到需要的符号对应的地址。不过一切的前提是:该头文件中的函数没有使用C语言不支持的语法。
但是这并不意味着将这部分按照C的方式编译,C++就不能使用了。一方面,C++是兼容C语言的,它们的编译大同小异。另一方面,C++链接器看到extern "C"这个关键字后,自动地会按照C的命名方式来寻找符号对应地址。

5、重载小结

首先,重载函数的使用非常方便,我们不必再根据参数考虑应该调用swapInt还是swapDouble。
其次,由于重载函数共用一个名字,一定程度上也减小了命名冲突发生的可能性。
再其次,重载函数也方便函数的命名。(有时候给函数命名是个很头疼的事情,而重载能够让相关函数都用一个名字,爽翻了)
最后,重载其实迎合了模板的要求,这个我们以后讲到模板的时候再说咯。


Part III、新语法之缺省参数

1、什么是缺省参数

缺省,即系统默认状态。

C语言中,我们必须给出所有的函数实参,才能调用一个函数,而在C++中,缺省参数的存在使得调用一个函数不一定需要传所有参数。

int printA(int a = 0)
{
	cout << a << endl;
}

在printA函数中,形参a被赋予了一个默认值0。这种在函数声明或定义时为函数的参数指定一个默认值的行为叫做缺省参数。

在调用这一类函数时,我们可以给缺省的参数传实参,也可以不传。
比如这里,我们可以写printA(10);那么函数将打印10;
如果不传参调用,即:printA();,那么函数将打印a的默认值0。

2、缺省参数的分类

缺省参数分为全缺省和半缺省。

  1. 全缺省
    形如void func(int a = 10, int b = 20);的称为全缺省。
    也就是说,全缺省意味着函数的所有形参都被赋予一个默认值。
    对于这一类函数的调用,我们必须采取从左到右依次传参的方式。
    比如说这里,可行的调用方式为:
func();
func(1);
func(1,2);

但是最经典的错误调用方式为:

func( ,2);

也就是说,第一个参数想用缺省值,第二个参数想用实参值,这是严令禁止的!

  1. 半缺省
    半缺省并不意味着刚好有一般的参数被赋予默认值,而是有一部分参数缺省。
    比如:
int func1(int a, int b = 20, int c = 30);
int func2(int a, int b, int c = 30);

注意,这里必须从最右边一个形参开始连续缺省。
也就是说,出现下列情形是不行的:
情况一、

int func1(int a = 10, int b = 20, int c);

这种情况不满足从最右边一个参数开始缺省。

情况二、

int func1(int a = 10, int b, int c = 30);

这种情况不满足从最右边一个参数开始连续缺省。

而对于这类函数的传参调用也必须满足:从左到右依次连续传参。
为什么?跟一开始我们说的错误调用方式一样,如果不是左到右依次连续传参,那么就会出现形如下面这两种调用方式的错误:

func( ,2, 3);
func(1, ,3);

3、缺省参数的一些其他注意事项及总结

  1. 缺省参数的默认值必须是常量或全局变量
    比如:
int n = 10;
void func(int a = 10, int b = n)
{
	;
}
  1. 缺省参数不能在声明和定义中同时出现
    比如:
void func(int a = 10, int b = n)
{
	;
}
void func(int a = 10, int b = n);

上面的定义中有缺省参数,下面的声明也有缺省参数,编译器会对这种行为报出重定义的错误:

也就是说,声明和定义中只要有一个给缺省值就行了。(一般建议在声明中给出,方便直接到头文件中修改)

  1. 仅有缺省无法构成函数重载
    比如:
void func(int a = 10)
{
	;
}
void func(int a)
{
	;
}

原因很简单:C++的符号修饰规则中没有考虑缺省值,他们两个在linux下的符号都是_Z4funci;

  1. 再度强调:缺省必须从右往左连续缺省,传参必须从左往右连续传参,二者的方向刚好反过来。

Part IV、内联函数

1、回顾宏

在讲内联函数之前,不妨回顾一下C语言中宏函数是如何定义的,具体可参考宏介绍及宏与函数的区别
举一个简单的例子,如果需要写一个两数相减的宏SUB,应该如何写呢?
大家可以先思考一下,试着写一写。



答案在这里:

#define SUB(X,Y) ((X)-(Y))

我相信肯定会有写不出来的情况,为什么呢?
一方面,宏函数并不是经常使用
另一方面,宏函数的定义非常复杂,因为它需要考虑直接进行文本替换的特性,所以才会有这么多的括号。
此外,我想说的是,宏具有很多的缺陷,比如:没有类型检查、宏参数具有副作用…

2、内联函数的介绍

由于宏具有这么多的缺陷,所以C++针对它进行了提升,于是有了这里的内联函数。

以inline修饰的函数称为内联函数,编译时它会在调用的地方直接展开函数体

什么意思呢?举个例子:

inline int sub(int x, int y)
{
	return x - y;
}

这里我们将sub定义为了内联函数,因为它有inline关键字修饰。
我们再将内联函数同宏函数进行对比:

  • 相较于宏,内联函数最大的优势就是:它的写法与普通函数的写法一模一样。

这意味着,下次让你写内联函数,你只需要记住inline这个关键字即可(即使你不一定常用,但记住还是比较容易的)。

  • 其次,内联函数具有严格的类型检查,因为它有参数列表,而参数列表规定了该函数参数的类型。

而宏与内联函数的相同点就是:都会在调用的地方展开。
比如这里:

inline int sub(int x, int y)
{
	return x - y;
}
int main()
{
	int ret = sub(1,2);
	cout << ret << endl;
	return 0;
}

我们调用了内联函数sub,那么经过编译后它就会变成:

inline int sub(int x, int y)
{
	return x - y;
}
int main()
{
	int ret = 1 - 2;
	cout << ret << endl;
	return 0;
}

我们注意到,内联函数经过编译,它的函数体的内容x - y就在调用处被展开了。
那么由于这种直接展开的性质,内联函数相较于普通函数的最大优点就是:效率更高,因为普通函数的调用会有函数栈帧的空间开销以及压栈的时间开销,而内联函数则省去了这些步骤,因此时间效率上更高。

那既然没有栈帧开销,为什么空间效率上没有很高呢?
因为:由于内联函数直接在调用处展开了,那就导致整个程序的代码量增加,如果函数体只有三行,而调用的次数有100次,那么就会展开成300行的代码,消耗了更多的内存空间和更多的指令缓存,这是典型的以空间换时间的做法。

3、内联函数的注意事项

经过介绍,我们可以看到,内联函数的功能还是很强大的,但是想要定义内联函数必须满足以下条件:

  1. 函数体必须比较短,最好不超过10行。
  2. 函数体内不得出现循环、选择、递归等复杂结构和逻辑控制语句

以上两点有一个不满足,那么即使你将函数用inline修饰了,那么编译器依然会将它视作普通函数,因为inline对于编译器只是一个建议,就像我们之前学过的register一样。

此外,还需要注意的一个点就是:由于内联函数是在函数调用的地方直接展开,所以它没有函数地址。这就意味着,如果将内联函数的声明和定义分开,那么就会出现链接错误(具体可参考上面介绍的重载的底层实现)。
因此,工程中常常将内联函数定义在头文件中。


Part V、总结

C++对于函数的补充基本上已经介绍完了,但是这些都是为后面作的铺垫,想要在后面的学习中更加轻松,就要打好这一阶段的基础!

【大话cpp】还在持续更新,欢迎一键三连!

以上是关于大话cppC++对函数的补充(函数重载+缺省参数+内联函数)快进收藏夹的主要内容,如果未能解决你的问题,请参考以下文章

喵呜:C++基础系列:缺省函数函数重载

喵呜:C++基础系列:缺省函数函数重载

函数重载与缺省参数

函数重载与缺省参数

c++之缺省函数函数重载

c++之缺省函数函数重载