前言
本章的重点:函数的参数传递、函数重载。在参数传递中涉及到了很多第二章关于const的内容,建议可以先复习一下相应内容再看会轻松很多。函数重载主要是要清楚重载函数匹配的过程,编译器匹配的规则的排序是什么。函数指针记录得不多,可以把它看成复杂一点的数组指针。
一、函数基础
一个函数包括四部分:返回类型、函数名称、形参列表以及函数体。写好了函数,通过调用运算符()
来执行函数。函数的调用完成两项工作,第一是用实参去初始化函数对应的形参;第二是主调函数暂时中断,把控制权转移给被调函数,当在函数体中遇到return
或者到了函数体末端}
,被调函数又把控制权转移给主调函数。
形参和实参
实参是形参的初始值,并且实参会按照顺序依次去初始化形参。实参的类型必须与形参的类型匹配
函数返回类型
大多数类型都可以作为函数的返回类型,特殊的返回类型是void
,表示函数不返回任何值。函数的返回类型不能是数组或函数类型,但是可以是指向它们的指针。
1.1局部对象
名字有作用域,对象有生命周期。一个函数体就构成了一个新的作用域,形参和函数体内部定义的变量统称为局部变量。
自动对象
这类对象当程序执行到函数体定义变量的语句时创建,到达函数体末尾时销毁,只存在函数体执行期间的对象就称为自动对象。形参是一种自动对象,我们可以用实参进行初始化,而定义在函数体内部的变量,如果没有初始值就会进行默认初始化,所以内置类型的未初始化局部变量将产生未定义的值。
局部静态对象
当我们需要把某些局部对象的生命周期延续的函数体结束后,可以通过定义局部静态对象的方式达到效果。凡是定义的局部静态对象在第一次函数执行被创建后就会等到程序结束时才会被销毁。下面的例子就可以计算出函数fn
的调用次数。局部静态变量如果没有进行显示初始化,就会执行值初始化,这跟类型是自动对象的内置类型不同。
int fn() {
static int res = 0;
return res++;
}
int main() {
for(...) {
fn()
}
cout << res << endl;
return 0;
}
二、参数传递
形参的类型决定了形参和实参交互的方式,如果形参是引用类型,它将绑定在对应的实参上,否则将实参的值拷贝后赋给形参。所以当形参是引用类型,函数调用就是引用调用;如果是形参的值是实参拷贝的,它们就是两个独立的对象,函数调用被称为传值调用。
2.1指针形参
在使用指针形参时,一定要注意和其他非引用类型一样,拷贝是值拷贝,因此拷贝之后的两个指针是不同的指针,但是由于通过指针我们可以间接访问到实参指针指向的对象,所以可以通过形参指针去改变它的值。【编程风格:建议用引用类型来代替指针的使用】
2.2引用参数
使用引用最大的好处就是可以避免拷贝,减少开销。对于像类类型或者容器对象等较大的对象,最好使用引用来传递参数。在引用的基础上,如果该引用的值不需要改变,如前面章节所诉,最好使用常量引用的形式。
2.3const形参和实参
关于顶层const和底层const
可以在第二章中查看。实参初始化形参的过程会忽略掉顶层const
,当形参有顶层const时,传给它的常量对象或者非常量对象都是可以的。下面的例子展示了忽略形参的顶层const
出现的错误。
void fn(const int i);
void fn(int i);
咋一看会以为进行了函数重载,但是由于形参的顶层const
被忽略掉了,所以这两个函数是完全一样的,也就出现了重复定义的错误。
2.4指针或引用形参和const
在介绍函数参数传递时先回顾一下在第二章学的const
相关内容。下面的例子展示了不同const
类型的初始化。如果不能推断下面定义错误的原因,可以直接看第二章关于const的内容。
int i = 42;
const int *cp = &i; // 不能通过cp指针改变i的值,所以cp是一个底层const
const int &r = i; // r是一个常量引用,不能通过r去改变i的值
const int &r2 = 42; // 字面值类型可以看做是一个常量,所以可以用来初始化常量引用。
int *p = cp; // p不含有底层const,类型不匹配,定义错误
int &r3 = r; // 非常量引用不能绑定到一个常量对象上
int &r4 =42; // 不能用字母初始化一个非常量引用
接下来再看函数参数传递的形式。
void reset(int &p); //reset函数接收一个引用类型的形参
void reset(int *p); //reset函数接收一个指针类型的形参
int i = 0;
cosnt int ci = i;
string:size_type ctr = 0; // ctr是字符串
reset(&i); // 正确调用,形式为int*
reset(i); // 正确调用,形式为int&
reset(&ci); // 错误,指向cconst int的指针不能去初始化int*类型,也可以这样理解,ci的值是不能改变的,如果p指向的ci,就可以通过p去改变ci的值,出现矛盾。
reset(ci); // 错误,普通引用不能绑定在常量引用上
reset(42); // 错误,普通引用不能绑定在字面值上
reset(ctr); // 类型不匹配
2.5数组形参
第三章提到,我们不能对数组进行拷贝,使用数组时起始也是将其转换成指针的形式。由于无法拷贝数组,所以不能以值传递的方式使用数组参数,而数组会转换为指针,所以传递数组实际上是在传递数组首元素的指针。
数组引用形参
C++
允许将变量定义成数组的引用,形参也可以是数组的引用。引用形参绑定到对应的实参上,也就是绑定到数组上。下面声明中的()
必不可少,否则会出现歧义,
void print(int (&arr)[10]) { // arr是具有10个整数的整型数组的引用
for(...){};
}
void print(int &arr[10]); // 错误,将arr声明成了引用的数组
如果要把数组作为函数的形参,有三种方式:
- 声明为指针
- 声明为不限维度的数组
- 声明为维度确定的数组。这里要注意,当我们确定了一个维度n,如果我们传入的数组大小m大于n,同样会编译成功并且输入前n个元素。
main:处理命令行选项
main
函数也能接收参数,有两个参数,第一个参数argc
表示第二个参数argv
数组中字符串的数量。argv
是数组,里面的元素是C风格字符串的指针,所以有下面两种定义方式。
int main(int argc, char *argv[]);
int main(int argc, char **argv); // argv指向 char*
当我们在有一个可执行文件prog
,里面含有数据a 1 2 file data
,我们可以向程序传递prog a 1 2 file data
。那么可以通过argv
来对数据进行访问。argv[0]是程序的名字,最后一个元素保证为0
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;
2.6可变形参函数
很多时候在写一个函数时往往无法确定到底需要传入多少个形参,C++11
新标准提供了两种方式:1)
如果所有的实参类型相同,可以传递一个名为initializer_list
的标准库类型;2)
如果不同,则可以使用可变参数模板,后面章节会讲。
initializer_list形参
下图是initializer_list
的操作方法。和vector
一样,initializer_list
也是一种模板类型,但是initializer_list中值永远都是常量,所以无法进行修改。
举个例子简单说明一下,比如我们要通过initializer_list
计算列表中所有元素的和。
int coutAll(initializer_list<int> il) {
int sum = 0;
for(auto val : il) {sum += val};
return sum;
}
int main() {
cout << coutAll({1,2,4,6,7,}) << endl;
}
三、返回类型和return语句
值的返回
返回值的方式和初始化一个变量或者形参的方式完全一样,返回的值用于调用点的一个临时量,该临时量就是函数调用的结果。
不要返回局部对象的引用或指针
局部变量的声明周期值存在与函数块的内部,当函数执行完毕后就会被销毁,如果返回则是一个未定义的值。哪怕在函数内部返回一个字面值同样也是按照局部对象处理。同样的,返回局部对象的指针也是错误的。
引用返回左值
函数的返回类型决定函数调用是否是左值,调用一个返回引用的函数得到左值,其他返回类型得到右值。
3.1返回数组指针
使用类型别名的方式
typedef int arrT[10];
using arrT = int[10]; // 与上面等价
arrT *func(int i); // func返回一个指向含有10个整数的数组的指针
声明返回数组指针的函数
返回数组指针的函数形式如下。Type
是元素的类型,dimension
表示数组大小。对于这种方式如果不喜欢也可以使用上面类型别名的方式。
Type (*func(paramers)) [dimension]
int (*fn(int i)) [10]; // 返回一个含有10个int元素的数组指针
使用尾置返回类型
尾置的方法更加的明显,建议用这种形式。
auto func(int i) -> int(*)[10];
四、函数重载
在同一个作用域内,如果几个函数名字相同但形参列表不同就是函数重载。
4.1重载和const形参
一个拥有顶层const
的形参无法和另一个没有顶层const
的形参区分开。前面也提到过,例如:
int fn(int);
int fn(const int); // 错误,fn重复定义
如果形参是某种类型的指针或者引用,通过区分指向的对象是常量还是非常量可以进行区分,此时的const是底层的。
int fn(int &); // 作用于一个整形引用
int fn(const int &); // 作用于一个常量引用
4.2const_cast和重载
第四章类型转换提到过const_cast
,它最好的一个用处就是在重载函数中。下面一个例子,该函数接收两个字符串常量的实参,返回一个字符串常量,当我们传入的实参不是常量时,程序任然能进行,但是返回的类型缺失一个常量字符串。
const string &shorterStr(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
如果我们想要实现当实参不是常量时,得到的结果是一个普通引用,可以使用const_cast
。如下,调用shorterStr
先使用const_cast
转换为常量字符串,最后的结果再转换为非常量从而达到效果。
string &shorterStr(string &s1, string &s2) {
auto &r = shorterStr(const_cast<const string&> s1,const_cast<const string&> s2);
return const_cast<string&> r;
}
4.3调用重载函数
这个过程也叫做函数匹配或重载确定,当我们定义了一堆重载函数,程序是如何找到我们需要的目标函数?简单的来看,编译器得到实参后会与重载集合中的每一个函数的形参进行比较,然后根据结构选择最优函数。最后的匹配结果有三种:
- 找到一个跟实参最佳匹配的函数
- 找不到任何一个函数匹配传入的实参,编译器直接报错
- 找到多个可以匹配的函数,此时也会发生错误,称为二义性调用。(这种情况往往发生在两个重载函数的形参虽然类型不同,但是在某些情况下可以进行类型转换)
五、特殊用途语言特性
5.1默认实参
一些函数在很多次调用中都被赋予了一个相同的值,这个反复出现的值称为函数的默认实参,在传入实参时,可以忽略默认实参也可以传入新的值去覆盖默认实参的默认值。如果定义了多个默认实参,函数会根据实参传入的个数,从左开始去覆盖默认实参的值。不能出现第一个默认实参值不覆盖,而直接跳过覆盖第二个默认实参。局部变量不能出现在默认实参中。
int fn(int c,int a = 11, int b = 12) {
cout << a << b << c << endl;
}
fn(12,23); // a = 23 b = 12 c = 12
fn(1,2,3); // a = 2 b = 3 c = 1
5.2内联函数和constexpr函数
内联函数
如果定义了一些功能比较简单的函数,可以试着定义为内联函数,这样不仅阅读、修改等操作更加简单方便,也会减少函数调用的开销。例如上面例子中使用到的shorterStr
函数,第二段代码会消除函数调用时的开销。
cout << shorterStr(s1,s2) << endl;
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
如何定义内联函数,很简单,只需要在函数定义时在最前面加上inline
,就说明该函数是内联函数。内联函数就像是程序对编译器说的一个指令,让它进行特殊处理,当然,编译器也能拒绝这个指令,有很多编译器都不支持内联递归函数。
inline const string &shorterStr(...) {}
constexpr函数
定义constexpr函数
需要遵循几个规定:函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且一条return
语句。
constexpr int fn1() { return 32;}
constexpr int fn2 = fn1();
六、函数匹配
函数匹配的第一步是选定本次调用的重载函数集,重载函数集中的函数(候选函数)都有两个特征:第一是函数同名;第二是声明在调用点可以使用。第二步是考虑本次调用提供的实参,然后从重载函数集中选出能被这组实参调用的函数(可行函数),特征:第一形参数量与本次调用传入的实参数量相等,第二实参的类型与形参类相同,或者能进行转换。第三步就是从可行函数中选出最佳匹配的函数。
上面提到匹配过程中会出现二义性问题,如下。当我们传入fn(32,23.5)
,先看第一个参数,明显fn(int, int)
可以达到一个精确匹配,而另一个则需要进行内置类型转换;而如果再看第二个参数fn(double, double)
又能达到精确匹配,所以这里就产生了二义性错误。
fn(int, int);
fn(double, double);
最佳匹配
为了达到一个精确匹配,编译器制定了系列规则,按照排序分为5
个等级。(2-4的内容在第四章,5的内容后面章节会讲)
- 精确匹配:
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加
顶层const
或者从实参中删除顶层const
- 通过
const
转换实现的匹配 - 通过类型提升
- 通过算术类型转换
- 通过类类型转换
七、函数指针
函数指针指向的是函数而非对象。定义方式:
int add(int a, b);
int (*ptr)(int a, int b); 、、
ptr
是一个指向函数的指针,()
不可缺少,否则ptr
就是一个返回bool
指针的函数。
使用函数指针
当把一个函数名作为一个值使用时,该函数就自动地转换成指针。
pf = add; // pf指向名为add的函数
pf = &add; // 同上
还能直接使用指向函数的指针调用该函数,无需提前解引用指针。以下3个都是等价的调用。
int a1 = pf(1,2);
int a2 = (*pf)(1,2);
int a3 = add(1,2);
重载函数的指针
使用重载函数时,函数指针必须指定要使用重载函数的类型。
void ff(int *);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; //匹配void ff(unsigned int);
void (*pf2)(int) = ff // 错误,没有匹配函数
函数指针形参
虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。
void cmp(const int a, const int b, bool pf(const int &, const int &));
void cmp(const int a, const int b, bool (*pf)(const int &, const int &)); // 显示的将形参定义成指向函数的指针
也可以直接把函数作为实参使用,会自动转换成指针。
cmp(1,2,numCmp); // numCmp为一个函数
使用类型别名简化操作
typedef bool Func(cosnt string&, const string&);
typedef decltype(lengthCmp) Func2; // 等价于Func,lengthCmp的形参为(cosnt string&, const string&)