网站容易出现的问题吗搜外滴滴友链
C++的函数特性
前言
在C++中,函数加入了许多特性,例如:a、函数缺省参数 b、函数重载 c、内联函数 等等……,这里我会和大家详细去探讨这些特性。以及探讨这些特性的一些细节,同时在内联部分,我们还会把C语言的宏再拿来和内联进行对比,提出一些可行性建议!
1. 函数缺省参数
1.1 缺省参数
缺省,缺省顾名思义,就是缺省函数的形参参数。我们先来看这样的场景:
//在使用C语言写一个栈的时候,会这个样初始化
typedef int STDataType;typedef struct Stack
{STDataType* a;int top;int capacity;
}ST;void InitST(ST* p,int DefaultCapacity)
{STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * DefaultCapacity);if (tmp == NULL){perror("malloc");return;}p->a = tmp;p->top = 0;p->capacity = DefaultCapacity;
}
这样写就会让我们每次都传初始化的栈大小,对于我们来说是否是太过麻烦了。C++就引入了函数缺省参数的性质。可以让我们在传参的时候,可以不用进行传递一些参数。例如:
void InitST(ST* p,int DefaultCapacity = 10) // 声明的同时定义函数
{STDataType* tmp = (STDataType*)malloc(sizeof(STDataType) * DefaultCapacity);if (tmp == NULL){perror("malloc");return;}p->a = tmp;p->top = 0;p->capacity = DefaultCapacity;
}
我们就可以使用默认的参数值10了,如果我们确定我们需要的空间大小,直接显示传递就行。
函数缺省参数也有两种类型:
- 全缺省。全缺省:就是指将函数全部参数都给默认值,这样我们调用这个函数就可以像无参的函数一样调用了。同时也要求了,函数参数需要从左向右传递,且不能跳过。例如:
#include<iostream>
using namespace std;
void Print(int a = 10, int b = 20, int c = 30)
{cout << "a == " << a << endl;cout << "b == " << b << endl;cout << "c == " << c << endl;
}int main()
{Print(1,2,3);cout << endl;// 不完全传参:Print(1,2);cout << endl;Print(1);cout << endl;Print();cout << endl;return 0;// 规定:不能跳过传参 -- 只能从左往右依次传参//Print(1, , 3);
}
- 半缺省。半缺省:就是函数参数部分给了默认值。由于我们函数传参顺序(这里的传参不是指调用的时候在栈里形参入栈的时候顺序)是从左向右传递,所以缺省参数也要求,函数形参的默认值的缺省是从右向左给。这样就能保持一致性。
#include<iostream>
using namespace std;
void Print(int a, int b = 20, int c = 30)
{cout << "a == " << a << endl;cout << "b == " << b << endl;cout << "c == " << c << endl;
}int main()
{Print(1,2,3);cout << endl;Print(1,2);cout << endl;Print(1);cout << endl;return 0;
}
1.2 声明和定义分离
对于函数的缺省参数,还需要注意的是:不能声明和定义处同时给缺省。(上面的例子都是声明和定义没有分离)要求的是在声明处给缺省值。这是为什么呢?
很显然的是,举个例子:
#include<iostream>
using namespace std;
int ADD(int a, int b = 4); //声明int main()
{int ret = ADD(1);cout << ret << endl;return 0;
}int ADD(int a, int b = 8) //定义
{return a + b;
}
看了上面这个例子,很快就能明白:如果在函数的声明和定义处都给了缺省值是不是会造成参数的二义性?编译器到底是听声明的还是听定义的?对于这样的情况,编译器是会直接报错的,直接杜绝这种情况。了解了编译链接过程后我们知道,声明就是一个承诺,而定义就是实现承诺的方法。在编译过程,编译器是向上去寻找是否声明了调用的函数的,具体的实现过程是在链接过程需要看函数是如何定义的。这样以来,体会一下下面这个例子:
#include<iostream>
using namespace std;
int ADD(int a, int b);int main()
{int ret = ADD(1); //--> ADD(1,4)cout << ret << endl;return 0;
}int ADD(int a, int b = 4)
{return a + b;
}
根据编译链接过程我们不难推断出,这样的语法是存在问题的。为什么?在main函数内调用了Add函数,那么在编译过程中,编译器就往上找Add函数的声明,结果是找到了,再检查参数类型、数量…结果很显然:编译器发现实参和形参个数不匹配!!所以编译器选择了报错。
对于上面的示例:这里给出正确的示范
#include<iostream>
using namespace std;
int ADD(int a, int b = 4);int main()
{int ret = ADD(1);cout << ret << endl;return 0;
}int ADD(int a, int b = 4)
{return a + b;
}
我们还是需要牢记以下缺省参数的语法特性:
- 函数缺省参数从右向左。
- 函数传递参数从左向右。
- 函数缺省参数,声明定义分离,声明给缺省,定义不给缺省。
紧接着我们来看下面一个特性:函数重载
2. 函数重载
再进入函数重载之前,我们来看这样的场景。如果现在我想写一个打印函数,可以打印类型:int,double,float,char…如果是C语言我们只能这样操作
#include<stdio.h>void Print_Int(int /*...*/)
{//...
}
void Print_Double(double /*...*/)
{//...
}
//...
这样的命名方式真的叫人头大!!C++为了方便使用,推出了一种新的语法:函数重载。下面来具体介绍(实际上对于这样的函数我们采用函数模板更好,把这些活交给编译器来干,但是这里是为了举例,所以例子可能不太恰当)
2.1 函数重载
所谓函数重载(overloading):在同一作用域下(这一点十分重要,因为在后面继承多态部分会用其它概念综合)多个函数的函数名相同,但是这些函数的参数列表不同的情况。我们称为函数重载。
首先来看第一个问题:什么是参数列表?参数列表包括:a、参数个数 b、参数类型 c、参数顺序
下面来举个例子讲解:
#include<iostream>
using namespace std;void func() //1
{cout << "func()" << endl;
}
void func(int a)//2
{cout << "func(int a)" << endl;
}
void func(int a, char c)//3
{cout << "func(int a, char c)" << endl;
}void func(char c, int a)//4
{cout << "func(char c, int a)" << endl;
}int main()
{func(); //调用1func(1,'a'); //调用3func('a',1);//调用4func(1);//调用2return 0;
}
同时在这里指出一些细节:
- 函数重载会走类型最匹配的函数,例如:
#include<iostream>
using namespace std;
void func(int a)//1
{cout << "func(int a)" << endl;
}
void func(char a)//2
{cout << "func(char a)" << endl;
}int main()
{char c = 1;func(c);return 0;
}
如果上面函数没有匹配char类型的,我们的func©当然可以走类型int型的,注意整型提升。
- 当无参的函数和全缺省的函数在一起的时候,会造成调用歧义
#include<iostream>
using namespace std;
void f()
{cout << "f()" << endl;
}void f(int a = 10)
{cout << "f(int a = 10)" << endl;
}int main()
{func();return 0;
}
思考一下,这里会调用那个函数呢?编译器会报错,直接说明调用歧义的,因为我们从调用的角度来看,无法说明到底是调用无参的还是全缺省的。
- 函数调用的时候,如果不需要使用形参,可以不需要具体形参接受传参,以类型接受即可。
#include<iostream>
using namespace std;
void func(int)//1
{cout << "func(int)" << endl;
}
void func(char)//2
{cout << "func(char)" << endl;
}int main()
{char c = 'a';func(c);return 0;
}
当然上面的这种写法,肯定还是不允许以类型来作为实参传递的!只是说不会以实际的形参去接受实参,而是单纯地匹配类型。
2.2 函数重载的原因
为什么C语言不支持函数同名,而C++支持函数同名呢?
回顾编译链接过程,编译器会在链接时候形成一个符号表 – 记录全局的变量、函数名及其地址,如果C语言的函数名冲突,那么在符号表内的函数调用就会不明确。因为编译器是通过函数名来找到对应的函数的地址的。
例如:
那么C语言是无法做到的。C++是如何做到的呢?我们可以简单地制造一些错误。(这里是故意制造的,因为VS2022包装比较厉害,从汇编代码看不出太大的区别)
//这里是对下面函数的声明,但是没有进行实现
//在链接的时候符号表类就没有对应的函数地址,就会报链接错误,这个时候方便我们看无法解析的外部符号(函数名)
void func(int x);void func(char x);int main()
{func(1);func('a');return 0;
}
仔细对比这个错误提示。函数名是什么呢?上面的函数名是:?func@@YAXH@Z 和 ?func@@YAXN@Z。对比这两个函数名:发现它们其实是不一样的!!所以我们有理由大胆地猜想:C++是通过一套函数名修饰规则让C++的同名函数在符号表内呈现出不同的名字。而这套函数名修饰规则依靠的是:参数列表。事实证明确实是如此的。同时我们提出一个疑问:是否可以将函数的返回值纳入到函数名修饰规则来呢?为什么?这里的答案当然是不可以…原因也很简单:我们无法从调用的函数的方式上体现返回类型,C++语法也不支持。这样就导致编译器无法识别到底调用的哪个函数…
最后在这里小结一下:
- 函数重载三要素:a、同一作用域下 b、同函数名 c、不同参数列表
- 注意点:a、避免调用歧义 b、注意参数传递可以不用接受
- 函数重载的原因:函数名修饰规则 – 函数是在编译时确定调用的
3. 内联函数
3.1 内联函数
在谈论内联函数之前我们需要先回忆宏。宏可以进行文本替换。但是带有副作用(特别是带有参数的宏)
宏的优点:
- 不消耗空间,效率高
宏的缺点:
- 没有类型检查
- 不方便调试
- 可读性差,容易出错
- …
宏的缺点还是特别明显的,举个例子:
#define MAX(a,b) a > b ? a : b
该宏是为了选出a,b中的更最大值,却不乏有人这样调用:
int a = 2, b = 1;
MAX(a++, ++b);
MAX(1 + 5, 2 * 4);
MAX(MAX(a + 1), b + 3);
//...
很多调用都会让上面的宏出现很多问题,所以为了优先级,我们决定加上括号:
#define MAX(a,b) ((a) > (b) ? (a) : (b))
这样写就看起来就特别麻烦,在一些复杂的宏中,我们要了解其功能就特别麻烦了…有什么办法可以适当解决宏这个麻烦呢?
C++中内联函数应运而生!内联函数非常完美地解决了宏的缺点。语法如下:
关键字inline
inline Max(int a, int b) //一般宏喜欢全大写
{return a > b ? a : b
}
这样以来,我们定义了一个内联函数Max,作用是返回a,b之中的较大值。那么内联函数的原理是什么呢?我们探讨一下。我们都知道调用函数的时候汇编代码是执行call指令的,所以对于普通的函数,在汇编代码中都是执行的call指令,而对比宏,则是直接的文本替换,并没有执行call指令。我们的内联函数也是类似于宏一样也不执行call指令,直接在调用的地方展开。来看下面一个程序的反汇编
#include<iostream>
using namespace std;
inline int Max(int a, int b)
{return a > b ? a : b;
}void Func()
{}int main()
{Func();Max(1, 2);return 0;
}
结果是不是令人大吃一惊?这个Max仍然调用了call指令啊?但是这是为什么呢?这就需要提出内联函数的其它特性了…根据宏的缺点,我们看到了内联函数可以解决:a、类型检查 b、可读性差 c、调试…没错,我们上面的程序正在进入调试,在调试(DEBUG)的状态下如果不执行call指令,我们怎么调试?可以思考一下,所以在这里我想说的是:内联函数在DEBUG版本下是不起作用的 – 因为我们需要调试;但是在RELEASE版本下就是有效的!
其次还需要补充的点是:
- 内联函数使用短小且经常调用的函数。为什么呢?原因是这样的,因为内联函数是以指令展开的形式进行的,所以很显然的是:如果一个内联函数的指令太多了,且调用次数很多,会发生什么呢?假设现有一个内联函数的指令有50语句,现在这个项目调用了10000行这个内联函数。对于内联函数来说:调用了多少指令?10000 * 50 = 50w行指令;对于普通调用来说:调用了多少指令? 10000 + 50 = 10050…这个差距也很明显了…这会导致最后可执行程序所占空间变大。这是十分不合理的,所以呢?编译器才不会冒这个风险,它不相信你能把握好这个度,所以内联函数对于编译器来说只是一个建议,最后是否成为内联函数还是由编译器决定。不能成为内联函数的几种函数:a、函数指令较多 b、递归函数。
- 内联函数是不可以声明定义分离的。为什么呢?这又要谈到我们的编译链接过程了。还记得符号表吗? 还记得内联函数是在调用出展开吗?很显然内联函数不执行call指令,所以他是不会进入符号表的,符号表内是不会存内联函数的地址的 所以想要在链接过程调用,是不可能的。所以建议:将内联函数的声明定义在同一个头文件中。
- …
* 3.2 尽量用const、enum、inline代替#define
这条建议是由衷的建议。
- 用const修饰。因为#define是不会被编译器看见的。例如:
#define PI 3.1415926
标记PI文本替换为3.1415926,但是如果PI发生报错了,我们的编译器的不会反馈出PI这个标识符,而是其它内容…为什么呢?因为我们的PI没有进入符号表内,在预处理阶段就被文本替换了。所以为了让编译器更能识别,建议:
const double Pi = 3.1415926;
- 建议使用enum。因为enum和#define很类似在int类型方面,例如:
enum Number
{N = 100;
}#define N 100
上面两种定义具有类似的效果:a、都可以作为整型使用 b、都不可以取地址…
但是很有区别的是:
枚举类型会 a、会类型检查 b、会注重作用域(#define只能通过#undef取消替换)…
- 使用inline代替宏。这个就不再赘述了,上面也探讨了内联的好处。
总体来说:C++推出内联函数这个语法极大的解决了宏的问题,使代码可以更好的维护…
到这里内容就结束了,但是实际上内联函数还有很多细节,比如说在类、多态(虚函数) 中就还有很多细节,到时候再处理。如果有什么错误失误,欢迎读者指出,作者虚心接受多多改进!!
本文章有参考:《Effective C++》