【C++】函数的返回、重载以及匹配、函数指针
函数的返回、重载和匹配、函数指针
这部分内容对应于 C++ Primer 第五版的第六章第6.3节到第6.7节,主要对以下内容:
- 6.3 返回类型和 return 语句;
- 6.4 函数重载;
- 6.5 特殊用途语言特性;
- 6.6 函数匹配;
- 6.7 函数指针
进行整理和学习记录。
返回类型和 return 语句
无返回值函数
没有返回值的 return 语句只能用在返回类型为 void 的函数当中。
有返回值的函数
只要函数的返回类型不是 void,则该函数内的每条 return 语句必须返回一个值。return 语句返回值的类型应该与函数的返回值类型一致,或者可以被隐式地转换为函数类型对应的类型。
C++ 可以确保函数的返回值类型与函数的类型是一致的,如果不一致,这段代码将无法通过编译。
值是如何被返回的
返回一个值的方式和初始化一个变量或形参的方式是完全一样的:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。
一个例子如下:
string make_plural(size_t crr, const string &word, const string &ending) {
return (ctr > 1) ? word + ending : word;
}
上述代码段定义了一个返回值类型为string
的函数make_plural
,作用是判断crr
是单数还是复数,如果是单数将会返回单次,如果是复数则返回单词的复数形式。
函数的返回值类型为string
,意味着返回值将会被拷贝到调用点。
同其它引用类型一样,如果函数的返回值类型是引用,则该引用仅是它所引对象的一个别名。举个例子来说,假定某函数挑出两个string
形参当中较短的那个并返回其引用:
const string &shorterString(const string &s1, const string &s2) {
return s1.size() <= s2.size() ? s1 : s2;
}
其中形参和返回值类型都是const string
的引用,不管是调用函数还是返回结果都不会真正地拷贝string
对象。
不要返回局部对象的引用或指针
函数调用完成后,它所占用的存储空间也会被随之释放掉,函数终止意味着局部变量的引用将指向不再有效的内存区域。同理,返回一个局部对象的指针也是错误的,一旦函数完成,局部对象将被释放,指针将指向一个不存在的对象。
返回类类型的函数和调用运算符
如果函数返回指针、引用或类的对象,我们就可以使用函数调用的结果访问结果对象的成员。
例如,可以使用如下的形式得到较短string
对象的长度:
auto sz = shorterString(s1, s2).size();
引用返回左值
函数的返回类型决定函数是否返回左值。调用一个返回引用的函数将会返回左值,其它返回类型得到的是右值。可以像使用其它左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值:
char &get_val(string &str, string::size_type ix) {
return str[ix];
}
int main() {
string s("a_value");
cout << s << endl;
get_val(s, 0) = 'A'; // 将 s[0] 的值修改为'A'
cout << s << endl;
return 0;
}
上述代码段的执行结果是:
a_value
A_value
列表初始化返回值
C++ 11 标准规定,函数可以返回花括号包围的值的列表。类似于其它返回结果,此处的列表也用来对表示函数返回的临时值进行初始化。如果列表为空,临时量执行值初始化【比如对于以vector类型为返回值的函数,返回空列表将会返回一个空的vector】;否则,返回值由函数的返回类型决定。
主函数 main 的返回值
main 函数的返回值可以被视为状态指示器。返回 0 0 0表示执行成功,返回其他值表示执行失败。
递归
即在函数调用内部调用自身,一个初学者入门的例子是汉诺塔问题。递归也常用于解决多种程序设计算法问题,入门的算法包括回溯和深度优先搜索等。
递归函数的关键在于何时达到递归函数调用的边界,以及如何对函数的状态(通常通过形参)进行更新。
返回数组指针
由于数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。虽然从语法上来说,要想定义一个返回数组的指针或引用的函数比较烦琐,但是这个问题可以通过类型别名来解决:
typedef int aTT[10];
// 另一个等价的类型别名声明如下, 使用 using 来完成
using aTT = int[10];
arrT* func(int i); // func 返回一个指向含有 10 个整数的数组的指针
声明一个返回数组指针的函数
要想在声明func
时不使用类型的别名,必须牢记被定义的名字后面数组的维度:
int arr[10]; // arr 是一个含有 10 个整数的数组
int *p1[10]; // p1 是一个含有 10 个指向整型变量的指针的数组
int (*p2)[10]; // p2 是一个指针, 它指向含有 10 个整数的数组
返回指向数组的指针的函数定义的例子如下:
int (*func(int i))[10];
可以按照以下顺序来逐层解读:
func(int i)
表示调用函数func
需要的形参为int
类型,形参的名字是i
;(*func(int i))
意味着我们可以对函数调用的结果执行解引用;(*func(int i)) [10]
表示解引用func
的调用结果是一个大小为10
的数组;int (*func(int i)) [10]
表示数组中的元素是int
类型。
使用尾置返回类型
在 C++ 11 标准中还有一个方法可以简化上述func
声明的方法,即使用尾置返回类型。尾置返回类型跟在形参列表后面,以->
符号开头。
为了表示函数真正的返回类型跟在形参列表后,我们在本应该出现函数返回类型的位置使用auto
代替。
auto func(int i) -> int (*) [10];
使用 decltype
还有一种情况,如果我们知道函数返回的指针将会指向哪个数组,就可以使用decltype
关键字声明返回类型。
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8, 10};
decltype(odd) *arrPtr(int i) {
return (i % 2) ? &odd : &even;
}
上面的arrPtr
使用关键字decltype
表示它返回类型是一个指针,并且该指针指向的对象与odd
的类型一致,即包含5
个整数的数组。
需要注意的一个细节是,在上面的例子中,odd
和even
都是在函数之外预先定义的数组变量,而非在函数内部定义的。
函数重载
如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数。例如,可以通过函数重载定义以下几个名为print
的函数:
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);
这些函数接受的形参类型不一样,但是执行的操作非常类似。函数的名字让编译器知道了它调用的是哪个函数,而函数重载可以在一定程度上减轻编程人员起名字、记名字的负担。
定义重载函数
尽管在上述例子当中我们定义的三个print
函数的名字是相同的,但编译器可以根据实参的类型来确定在程序运行时应该调用哪个函数。
需要注意的是,函数的形参列表必须不同,才能够完成函数的重载,仅仅改变函数的返回类型是不能够对函数进行重载的。
判断两个形参的类型是否相异
重载和 const 形参
需要强调的是,顶层const
不影响传入函数的对象。一个拥有顶层const
的形参无法和另一个没有顶层const
的形参区分开来,即:
Record lookup(Phone);
Record lookup(const Phone); // 错误, 重复声明了形参类型为Phone的函数lookup, 因为重载忽略顶层 const
此外,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象,可以实现函数重载,即,函数重载不会忽略底层const
,函数重载可以对形参列表当中的底层const
加以区分:
Record loopup(Account&); // 函数作用于 Account 的引用
Record lookup(const Account&); // 经过重载的新函数, 作用于常量引用
Record lookup(Account*); // 经过重载的新函数, 作用于指向 Account 的指针
Record lookup(const Account*); // 经过重载的新函数, 作用于指向常量的指针
const_cast 和重载
在之前的文章当中有提到过,const_cast
在函数重载的情境中最有用。以shorterString
函数为例:
const string &shorterString(const string &s1, const string &s2) {
return s1.size() < s2.size() ? s1 : s2;
}
该函数的参数和返回类型都是const string
的引用。可以对两个非常量的string
类型调用该函数,但是返回的结果将仍然是const string
的引用。因此我们需要一个新的shorterString
函数,当它的实参不是常量时,返回一个普通的引用,使用const_cast
可以实现:
string &shorterString(string &s1, string &s2) {
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
在这个版本的函数中,首先将实参强制转换成对const
的引用,然后调用了shorterString
函数的const
版本。const
版本返回的是对const string
的引用,这个引用事实上绑定在了某个初始的非常量实参上。因此,可以再将其转换回一个普通的string&
。
调用重载的函数
定义了一组重载过后,我们需要以合理的实参来调用不同的重载函数。这就涉及到函数匹配的过程。函数匹配(function matching)指的是这样的一个过程,在这个过程中我们把函数调用与一组重载函数当中的某一个函数关联起来,函数匹配也叫做重载确定(overload resolution)。编译器将会首先将调用的实参与重载集合当中的每一个函数的形参列表进行比较,然后根据比较的结果决定调用哪个函数。
通常来说,重载函数的形参列表当中要么形参数量不同,要么形参的类型毫无关联。但是在一类极端情况下,不仅形参列表当中形参的个数相同,且不同重载函数之间的形参类型还可以通过类型转换而得到,此时则需要一种特殊的处理方法来处理这种情况,将在下文进行介绍,在C++ Primer当中,这种情况被称作二义性调用(ambiguous call)。
重载与作用域
重载对作用域的一般性质并没有什么改变,但是需要注意的是,如果在内层作用域声明名字,它将隐藏外层作用域中声明的同名实体。在不同的作用域当中也无法重载函数名。
特殊用途语言特性
本节将介绍三种与函数相关的 C++ 语言特性,这些特性在大多数程序当中都有用,分别是:默认实参、内联函数和 constexpr 函数。
默认实参
调用含有默认实参的函数时,可以包含该实参,也可以省略。
一个例子如下:
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
此处为每一个形参都提供了默认实参,默认实参作为形参的初始值出现在形参列表当中。
可以为每一个形参都赋予一个默认实参,当然也可以只对部分形参进行上述行为,但是需要注意的是,与Python相同,一旦一个形参在形参列表当中被赋予默认实参,那么它在形参列表当中后面所有的形参都必须有默认实参。
使用默认实参调用函数
只需要在调用函数时省略该实参即可。例如:
string window;
window = screen(); // 等价于 screen(24, 80, ' ');
window = screen(66);
window = screen(66, 256);
window = screen(66, 256, '#');
默认实参声明
在给定的作用域当中,一个形参只能被赋予一次默认实参。换句话说,函数的后续声明(函数通常只声明一次,但是多次声明同一个函数也是合法的)只能为之前那些没有默认值的形参添加默认实参,而且该形参的右侧所有形参必须已经有了默认值。
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误, 重复声明
string screen(sz = 24, sz = 80, char); // 正确, 添加默认实参, 此时最后一个形参的默认值是 ' '
默认实参初始值
局部变量不能改变默认实参。此外,只要表达式的类型可以转为形参所需的类型,表达式就可以作为默认实参。
内联函数和 constexpr 函数
内联函数可避免函数调用的开销
将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。假定我们把shorterString
函数定义为内联函数,则如下调用:
cout << shorterString(s1, s2) << endl;
将在编译过程中被展开为:
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
从而消除了在调用函数时的运行时开销。
在shorterString
的返回类型前加上inline
即可完成内联函数的声明:
inline const string &shorterString(const string &s1, const string &s2) {
return s1.size() < s2.size() ? s1 : s2;
}
一般来说,内联机制用于规模较小、流程直接、频繁调用的函数。
constexpr函数
constexpr函数(constexpr function) 是指能用于常量表达式的函数。定义 constexpr 函数的方法与其它函数类似,但要遵循几项约定:
- 函数的返回类型以及所有的形参类型必须是字面值;
- 函数体中必须有且只有一条 return 语句。
constexpr int new_sz() { return 42; }
constexpr int foo = new_sz(); // 正确, foo 是一个常量表达式
执行该任务时,编译器把 constexpr 函数的调用替换为其结果值。为了能在编译的过程中随时展开,constexpr 函数被隐式地指定为内联函数。
constexpr 函数体内也可以包含其它语句,只要这些语句在运行时不执行任何操作即可。
把内联函数和 constexpr 函数放在头文件内
函数匹配
确定候选函数和可行函数
根据所传入实参的个数以及形参列表当中形参的个数、实参/形参的类型来进行候选函数的确定与匹配。
寻找最佳匹配
基本思想是:实参类型与形参类型越接近,匹配程度越高。
实参类型转换
函数指针
函数指针指向的是函数而非对象。和其它指针一样,函数指针指向某种特定的类型。函数的类型由它的返回类型和形参类型共同确定,与函数名无关。例如:
bool lengthCompare(const string&, const string&);
想要声明一个可以指向该函数的指针,只需要使用指针替换函数名即可:
bool (*pf)(const string&, const string&); // 指针 pf 还未初始化
// *pf 两端的括号不能少
使用函数指针
当把函数命作为值使用时,函数自动地转换成指针。例如,可以按照如下形式将lengthCompare
的地址赋给pf
:
pf = lengthCompare;
pf = &lengthCompare; // 等价的赋值语句, 取地址符是可选的
可以直接使用指向函数的指针调用该函数,无需解引用:
bool b1 = pf("hello", "goodbye");
bool b2 = (*pf)("hello", "goodbye"); // 等价调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价调用
可以为函数指针赋nullptr
或零值,表示该指针目前没有指向函数。
重载函数的指针
编译器通过指针类型决定调用哪个函数,指针类型必须与重载函数当中的某一个精确匹配。
函数指针形参
和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针【意思是,数组不能作为函数的形参,但是指向数组的指针可以,同理,函数不能作为另一个函数的形参,但是指向该函数的指针可以】。此时,形参看起来是函数类型,实际上被当作指针使用:
void useBigger(const string &s1, const string &s2, bool pf(const string &, const string &));
// 第三个形参是函数类型, 它会自动地转换成指向函数的指针
void useBigger(const string &s1, const string &s2, bool (*pf)(const string &, const string &));
// 等价