接之前的文章,最近在啃C++ primer这本书,不得不说这本书写的真的非常详细了。
于是经过了几天的时间,我啃到了函数这一章,本来前面觉得挺简单的,一到函数指针,看完以后感觉头发又要少几根了。
接下来分成几个方面记录函数方面的知识,个人认为内容重要程度不亚于第一天发的内置类型
目录
1、函数基础
1.1 函数
先简单回顾一下C++中的函数的组成,一个函数包括:返回类型,函数名,参数列表,函数体。其中参数列表和函数体都可以为空,但是不能省略。函数体必须是复合语句块,即用{}包起来的0条或多条语句。
1.2 调用运算符
调用运算符(),其实就是所谓的圆括号,优先级与点运算符(.),箭头运算符(->)相同,并且都是左结合的。作用于左侧的函数或者是函数指针。
1.3 函数调用流程
简单提一句函数调用的过程,函数调用时主要完成两项工作,一是用实参初始化函数对应的形参,二是将控制权交给被调函数。主调函数的执行将暂时被中断,被调函数开始执行。
1.4 实参、形参和形参列表
1、实参与实参
简单来说就是实际的参数,就是调用函数时里面的参数,比如我使用标准库的max函数求a,b之中的最大值,int res = max(a, b);这里面的a和b就是实参,而对应的形参,则是在函数声明的时候例如我自己声明了一个仅限于求两个int类型的最大值的函数 int max(int a,int b);这里面的int a和int b就是形参。实参用于初始化形参。并且与前面表达式求值顺序一样,C++没有规定实参求值的顺序,所以和表达式一样,多次使用同一个变量的时候,尽量不要去修改这个变量。
2、参数列表
前文提到,参数列表可以为空,但是不能省略。并且大部分情况下,参数列表有多少个形参,函数在调用的时候就要提供多少个实参。有一些例外情况,一是可变长度的参数列表,另一种是参数列表设置有默认参数。
参数列表也有一点规则,首先是要写明所有形参的类型,这一点不同于变量的声明。然后是参数列表内不能有同名的变量,我想这个不用过多解释。另外比较重要的一点是在函数最外层作用域不能有与函数参数列表同名的变量。听描述比较抽象,直接上一个例子,还是上文提到的自定义的max函数
int max(int a, int b) {
int a = 1;
return a > b ? a : b;
}
直接看报错:形参“a”的重定义。可以简单的理解为,函数的形参是定义在函数内的最外层作用域的。
3、默认参数
C++允许给参数列表设置默认实参,但有默认值的形参,之后的所有形参都必须有默认值。简单来说,就是给函数设置默认参数要从右往左设置。反过来,在调用函数时,提供的实参,会从左往右填。
局部变量不能作为默认参数,因此全局变量可以。因此表达式只要能转换为形参所需要的类型,那就是合法的。
这样就有一种情况,可以改变提供的默认参数的值。所以当函数函数默认值是一个全局变量的时候hi,改变全局变量的值就能改变函数默认参数的值。
int x = 10;//全局变量x
int max(int a, int b=x) {
//x = 5;
return a > b ? a : b;
}
//测试主函数内部:
x = 5;
std::cout << max(1) << std::endl;//b的默认值是x,现在等于5
1.5 函数声明
函数与变量一样,必须先声明后使用,并且可以声明多次,但只能定义一次,并且必须定义一次,但是也有一个例外,就是纯虚函数,纯虚函数没有定义。为函数提供默认参数只能在函数的声明里,并且在给同一个函数的多次声明提供不同的默认参数时,只能添加默认参数,不能修改默认参数或者是减少默认参数。例如下面这段代码,看上去第二个声明违背了默认参数设置的原则,但事实上给b设置参数会导致b的重定义。
int max(int a, int b = 1);
int max(int a = 0, int b);//正确,声明给函数添加默认参数a = 0
int max(int a, int b) {
return a > b ? a : b;
}
2、参数传递
接下来是函数参数的传递:
2.1 值传递与引用传递与const
函数参数传递有值传递与引用传递两种,如果传递的是指,那就是值传递,调用的时候称作传值调用,同样的,如果传递的是引用,那就是引用传递和传引用调用。
参数传递简单的说就是用实参去初始化形参。
1、值传递
因此如果使用值传递,那么就会把实参复制一份,例如我们传递了一个长度为100的vector<string>对象,那么在值传递的时候就会调用100次string的拷贝构造函数,因为在复制的时候是进行深复制。
另一方面,在函数内部修改参数的值,也不会影响函数外部的对象,因为修改的只是复制品。
2、引用传递
如果使用引用传递,那就相当于将函数形参列表的引用绑定了外部对象,因此在函数内部使用时,是通过引用使用对象本身,因此使用引用传递不会进行拷贝操作,在内部也可以修改对象的值。
同样的,例如我们传递了一个长度为100的vector<string>对象,然后传给一个如下形式的函数
void fun(vector<string> &vec);
那么调用拷贝构造函数的次数将是0次,并且因为没有声明为const类型,如果在内部进行如下操作
for(auto &v:vec){
v="Hello,world!";
}
那么这个长度为100的vector<string>对象内部的值会全部被永久性的修改为“Hello,world!”
因此使用引用调用可以避免拷贝操作,同时能够改变形参绑定的实参对象的值,这一点应用于swap函数。引用传递很好的改进了C语言一个小问题:即要想改变所传递参数的值必须使用指针。
另外,有些类型不支持拷贝,例如IO类型,因此参数传递尽量使用引用类型,即使真的需要一个副本,也可以使用引到然后到函数内部去拷贝。
3、参数传递与const
根据const类型的那一篇文章提到的,参数传递会忽略顶层const,也就是说形参的const会被忽略。当形参有顶层const 的时候,传递常量和非常量都是可以的。例如以下两种形式属于同一个函数:
int fun(int i){}
int fun(const int i){}//错误,函数重定义
但是当传递的参数时指针或者是引用的时候,情况会稍微复杂一点。引用与指针之前的const都是底层const,带有底层const的参数与普通参数时两种不同的类型。
int fun(const int* i) {
return *i;
}
int fun(int* i) {
return *i;
}
int fun(const int& i) {
return i;
}
int fun(int& i) {
return i;
}
在传递引用或者是指针的时候,尽量使用带底层const的形参,因为带const的形参能够接受非常量参数与常量参数,而普通形参只能接受非常量参数。形参声明为普通类型会限制函数接受参数的类型。
2.2 传递数组参数
数组有两个特性,会影响函数的参数传递:其一是数组不允许拷贝,其二是使用数组是会被转化成指针。因为数组不能拷贝,因此也就不能使用值传递的方式传递数组参数,传递数组参数的时候,实际上是传递指向数组首元素的指针。
即使不能传递数组参数,也可以将参数声明为数组形式。
因为不知道数组的确切尺寸,因此管理指针形参的方式有三种。
1、使用标记:例如C语言风格的字符串,在末尾有一个‘\0’提示字符串结束
2、使用标准库规范:就是传递数组的首位指针,可以使用begin(),与end()函数
3、使用形参指示数组长度,类似于C语言常用的方式,将数组的长度作为参数之一传递即可
另外还可以传递数组的引用:但是使用方式比较局限。
void print(int(&arr)[10]) {
for (auto& a : arr) {
std::cout << a << std::endl;
}
}
还有另一种方式是使用指向数组的指针,后面会详细介绍,指向数组的指针也用于传递多维数组,也可以直接声明为多维数组的形式,但是除了第一维度以外,其他维度都要指明数组的长度。
2.3 可变参数列表
传递变长的参数的方式也有两种
1、initializer_list,本质上也是定长的,整个initializer_list对象可以看做一个对象,但是长度是可变的。
2、另一种是可变参数模板,以后(之后的文章)再介绍
另外还可以用省略符形参,但是只能出现在形参列表的最后一个位置。这个形式应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
3、函数重载
函数重载的定义与作用就不细说了
函数重载的区分方式是形参列表。重载函数同名,但形参列表必须有能够区分的地方。不能根据返回类型确定函数的重载。
区分参数列表有下面几个标准
1、参数列表要么参数数量不同,要么类型不同
2、顶层const无法区分,例如前文提到的,参数列表的const int i与int i无法用于区分重载函数
3、底层const可以用于区分,也如同前文提到的,参数列表的const int &i与int &i可以用于区分重载函数。我们知道底层const类型既可以接受常量也可以接受非常量,当我们一个非常量实参面对一个const形参版本和一个非const形参版本的重载函数时,若没有其他区分条件,则会优先选择非const版本。
const_cast和重载
const_cast主要用于有函数重载的上下文。
例如我们有一个函数形式如下:
using std::string;
const string& shorterString(const string& s1, const string& s2) {
return s1.size() < s2.size() ? s1 : s2;//返回size较小的字符串
}
现在我们需要一个返回非常引用的形式,可以按照如下方式改写:
string& shorterString(string& s1, string& s2) {
auto &r=const_cast<string&>(s1).size() < const_cast<string&>(s2).size() ? s1 : s2;
return const_cast<string&>(r);
}
当他的结果不是实参时,得到的事一个普通的引用。
函数匹配
函数匹配的第一步是选定本次调用的重载函数集,集合中的函数称为候选函数集。
集合中的函数称为候选函数,候选函数必须具有两个特征,其一是必须与被调函数同名,其二是声明在调用点可见。
选出候选函数之后,再通过实参确定可行函数,可行函数也具有两个特征,其一是形参的数量与本次调用提供的是参数量相等,其二是每个实参与对应的形参类型相同,或者能转换成形参的类型。
如果无法选出最优的,则会出现二义性。
4、函数返回值与函数指针
首先函数可以无返回值,就是声明返回类型为void的函数,没有返回值也可以使用return;显式的提前停止函数的调用。
有返回值的函数:
return语句返回的类型必须与声明的类型相同,或者是可以转化。
4.1 函数值返回的方式
返回值的方式与初始化一个变量或形参的方式一样。
注意不要返回局部对象的指针或引用,因为函数内部的局部对象声明周期仅限于函数调用期间,返回一个局部对象的指针或引用会绑定一个意想不到的对象。
返回值的类型:
如果返回值是引用类型,则返回左值,否则返回右值。
返回指针类型:
函数不能返回数组或者是函数类型,但是能返回指向数组的指针和指向函数的指针,个人认为,函数指针和数组指针都相当的头疼。
4.2 数组指针
声明一个返回数组指针的函数:
int(*func(int i))[10];
要理解上面这个定义,要一层一层的解读
int(*func(int i))[10];//原型
//最内层func(int i)表示是一个函数,接受一个int型实参
//然后是* 表示结果是一个指针,可以对结果进行解引用
//之后是(*func(int i))[10] ,表示解引用结果是一个长度为10的数组
//最后是 int(*func(int i))[10];表示数组中元素类型是int
个人表示上面那种形式,多看一眼就会减少几天的寿命。
所以使用别名:虽然但是,typedef个人觉得还是有些抽象,因为[10]在arrT后面而不是在int后面就离谱,可以使用更加直观的using
typedef int arrT[10];//其实是将arrT声明为int[10]的别名
arrT *fnc(int i);//形式看起来更加简洁
//另一种方式
using arrT = int[10];//将arrT声明为int[10]的别名,与typedef的等价
arrT *fnc(int i);
使用尾置类型:
使用auto 然后使用尾置类型指明函数的返回类型:
auto func(int i)->int(*)[10];
使用decltype类型:
int(*p)[10];
decltype(p) func(int i);
不管是那种方式都好,想多活几年就别去用他原本的形式。保护自己也是保护别人的头发。
4.3 函数指针
声明一个指向函数的指针,和声明一个指向数组的指针类似:
//声明函数指针原型:int (*p)(int a, int b);
using pf = int (*)(int a, int b);//pf是指针类型
using f = int(int a, int b);//f是函数类型
typedef int tf(int a, int b);//tf是函数类型
typedef int (*tpf)(int a, int b);//tpf是指针类型
//同样可以使用auto或者decltype,还是不建议使用原型
函数指针无需解引用,如果将函数作为另一个函数的参数传递,可以声明为函数参数形式,也可以声明为函数指针的形式,传递的时候只需要直接传递函数名即可。
对于普通的函数,可以直接定义对应的指针指向一个函数
int func(int i, int j);//声明
int main() {
int (*p)(int i, int j) = 0;
p = func;
}
对于重载函数,上下文必须清晰界定到底该选择哪一个函数
同样也可以将auto和decltype用在这个地方。
int func(int i, int j);
int main() {
auto p = func;
decltype(func) *fp;
}
但当函数有重载函数时,auto和decltype就无法推导对应的类型,必须有清晰的上下文指示。
int func(int i);
int func(int i, int j);
int main() {
auto (*p)(int, int) = func;
decltype(p) *fp;
}
5、其他特性
内联函数:加上inline关键字,运行时该函数直接在调用点展开,可以提升性能,但是内联函数不要用于体积过大的函数。内联机制用于优化规模较小,流程直接,频繁调用的函数。内敛函数可以避免函数调用的开销。
constexpr函数:返回类型与形参类型都是字面值。constexpr函数被隐式的指定为内联函数。内部其它语句不能执行任何操作,并且必须有且只有一条return语句。
调试帮助:直接看例子,如果定义了NDEBUG,就不会执行ifndef到endif之间的语句,对调试有一定的帮助。
int print(const int i) {
#ifndef NDEBUG
cout << __func__ << endl;
#endif // !NDEBUG
return i;
}
6、总结
函数时C/C++语言核心内容之一,因为对C语言的兼容,同时也继承了很多C语言的问题,C++的一些新特性也对这些问题进行了优化,特别强调一下函数指针与数组指针,尽量使用using关键字定义别名,否则使用原型会导致可读性非常差,特别是形式更加复杂之后。另外,因为还没涉及到C++相比C语言的新增的面向对象部分,因此函数的特性大部分还是继承自C语言,涉及到C++类才有的构造函数,虚函数,或者是函数模板等内容将在以后的学习记录中涉及。