目录
6.1 函数基础
形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。
int f3(int v1, v2) { /* ... */ } // 错误
int f4(int v1, int v2) { /* ... */ } // 正确
实际参数的数据类型要与形式参数类型一致,如果实在不能一致,也必须保证实际参数类型能转换成形式参数类型
函数的形参列表可以为空,但是不能省略。
void f1() { /* ... */ } // 隐式地定义空形参列表
void f2(void) { /* ... */ } // 显式地定义空形参列表
形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。
函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。
6.1.1 局部对象
1. 自动对象
只存在于块执行期间的对象称为 自动对象(automatic object),当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象,在函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
2. 局部静态对象
在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序结束才被销毁,对象所在的函数结束执行并不会对它产生影响。在变量类型前添加关键字 static 可以定义局部静态对象。
size_t count calls() {
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ctr;
}
int main () {
for (size_t i = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
6.1.2 函数声明
和其他名字一样,函数的名字也必须在使用之前声明。和变量类似,函数只能定义一次,但可以声明多次,函数声明也叫做 函数原型(function prototype)。函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
在函数声明中经常省略形参的名字,但是写上形参的名字可以帮助使用者更好的理解函数的功能。
函数应该在头文件中声明在源文件中定义,含有函数声明的头文件应该被包含到定义函数的源文件中
6.1.3 分离式编译
6.2 参数传递
形参的类型决定了形参和实参交互的方式: (1)当形参是引用类型时,它对应的实参被引用传递(passed by reference),函数被传引用调用(called by reference)。引用形参是它对应实参的别名。 (2)当形参不是引用类型时,形参和实参是两个相互独立的对象,实参的值会被拷贝给形参(值传递,passed by value),函数被传值调用(called by value)。
6.2.1 传值参数
指针形参:可以改变原数据的值,但是不能改变原数据的地址
// 该函数接受一个指针,然后将指针所指的位置为0
void reset(int *ip) {
*ip = 0; // 改变指针ip所指对象的值
ip = 0; // 只改变了ip的局部拷贝,实参未被改变
}
int i = 42;
reset(&i); // 改变i的值而非i的地址
cout << "i = " << i << endl; // 输出i = 0
建议使用引用类型的形参代替指针
6.2.2 传引用参数
使用引用避免拷贝,如果不需改变值,则形参声明为常量引用
使用形参可以返回额外的信息,实现函数返回多值的目的
6.2.3 const形参和实参
用实参初始化形参时会忽略掉顶层const,当形参有顶层 const 时,传递给它常量对象或非常量对象都是可以的。
void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }
void fcn(int i) { /* ... */ } // 错误:重复定义了fcn(int)
注意C++允许使用字面值初始化常量引用,但是不允许普通引用用字面值初始化。
因此在不需要改变实际参数的值时,尽量使用常量引用
6.2.4 数组形参
数组的两个特殊性质对定义和使用作用在数组上的函数有影响,分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式。因为数组会被转换成指针,所以当我们传递给函数一个数组时,实际上传递的是指向数组首元素的指针。
// 尽管形式不同,但这三个print函数是等价的
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
int i = 0, j[2] = {0, 1};
print(&i); // 正确:&i的类型是int*
print(j); // 正确: j转换成int*并指向j[0]
以数组作为形参的函数必须确保使用数组时不会越界。管理指针形参有三种常用的技术:
要求数组本身包含一个结束标记:C风格字符串存储在字符数组中,并且最后一个字符后面跟着一个空字符
传递指向数组首元素和尾后元素的指针:
void print(const int *beg, const int *end){
while(beg < end){
cout << *beg++ << endl;
}
}
int j[2] = {1,2};
print(begin(j), end(j));
void print(const int ia[], size_t size){
for(size_t i = 0; i != size; ++i){
cout << ia[i] <<endl;
}
}
int j[2] = {1,2};
print(begin(j), end(j)-begin(j));
如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向 const 的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。
形参可以是数组的引用,但此时维度是形参类型的一部分,函数只能作用于指定大小的数组。
//正确: 形参是数组的引用,维度是类型的一部分
void print(int (&arr) [10]) {
for (auto elem : arr)
cout << elem << endl;
}
// &arr两端的括号必不可少:
// f(int &arr[10]) // 错误:将arr声明成了引用的数组
// f(int (&arr)[10]) // 正确:arr是具有10个整数的整型数组的引用
将多维数组传递给函数时,真正传递的是指向数组首元素的指针,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略。第一个维度作为额外参数传入,matrix的声明看起来是个二维数组,实际上形参是指向含有10个整数的数组的指针。
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowSize) { /* ... */ }
// *matrix两端的括号必不可少:
int *matrix[10]; // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针
// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }
6.2.5 main:处理命令行选项
可以在命令行中向 main 函数传递参数,形式如下:
int main(int argc, char *argv[]) { /*...*/ } //指向C风格字符串的指针,argc定义字符串的数量
int main(int argc, char **argv) { /*...*/ }
6.2.6 含有可变形参的函数
为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要方法处理实参数量不定的函数:
如果实参类型相同,可以使用 initializer_list 标准库类型;
如果实参类型不同,可以定义可变参数模板。
C++还可以使用省略符形参传递可变数量的实参,但这种功能一般只用在与C函数交换的接口程序中。
1. initializer_list形参
和 vector 一样,initializer_list 也是一种模板类型,定义对象时,必须说明列表中所含元素的类型
和 vector 不一样的是,initializer_list 对象中的元素永远是常量值,无法改变
使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参(如果想向initializer_list形参中传递一个值的序列,必须把序列放在一对花括号内):
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
error_msg({"functionX", "okay"});
含有initializer_list形参的函数也可以拥有其他形参。例如,调试系统可能有个名为ErrCode的类用来表示不同类型的错误,因此我们可以改写之前的程序,使其包含一个initializer_list形参和一个ErrCode形参
void error_msg(ErrCode e, initializer_list<string> il)
{
cout << e.msg() << ":";
for(const auto &elem : il)
cout << elem << " ";
cout << endl;
}
error_msg(ErrCode(42), {"functionX", "okay"});
error_msg(ErrCode(0), {"functionX", "okay"});
2. 省略符形参
省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
void foo(parm_list, ...);
void foo(...);
6.3 返回类型和return语句
return 语句有两种形式,作用是终止当前正在执行的函数并返回到调用该函数的地方。
return;
return expression;
6.3.1 无返回值函数
一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过此时 return 语句的 expression 必须是另一个返回 void 的函数,强行令 void 函数返回其他类型的表达式将产生编译错误。
6.3.3 有返回值函数
函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。
函数不应该返回局部对象的指针或引用,因为一旦函数完成,局部对象将被释放,指针将指向一个不存在的对象。
// 严重错误: 这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
// 以某种方式改变一下ret
if (!ret.empty())
return ret; // 错误:返回局部对象的引用!
else
return "Empty"; // 错误:"Empty"是一个局部临时量
}
如果函数返回指针、引用或类的对象,就能使用函数调用的结果访问结果对象的成员
auto sz = shorterString(s1, s2).size(); //函数返回的是string对象
调用一个返回引用的函数会得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用。
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';
cout << s << endl;
}
C++11规定,函数可以返回用花括号包围的值的列表。同其他返回类型一样,列表也用于初始化表示函数调用结果的临时量。如果列表为空,临时量执行值初始化;否则,返回的值由函数的返回类型决定。
如果函数返回内置类型,则列表内最多包含一个值,且该值所占空间不应该大于目标类型的空间。
如果函数返回类类型,由类本身定义初始值如何使用。
vector<string> process()
{
// . . .
// expected和actual是string对象
if (expected.empty())
return {}; // 返回一个vector对象
else if (expected == actual)
return {"functionX", "okay"}; // 返回列表初始化的vector对象
else
return {"functionX", expected, actual};
}
main 函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。为了使 main 函数的返回值与机器无关,头文件 cstdlib 定义了 EXIT_SUCCESS 和 EXIT_FAILURE 这两个预处理变量,分别表示执行成功和失败。
int main()
{
if (some_failure)
return EXIT_FAILURE; // 定义在cstdlib头文件中
else
return EXIT_SUCCESS; // 定义在cstdlib头文件中
}
在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。相对于循环迭代,递归的效率较低,但在某些情况下使用递归可以增加代码的可读性。
循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继)
而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)
6.3.3 返回数组指针
定义数组别名
typedef int arrT[10]; //arrT是一个类型别名,他表示的类型是含有10个整数的数组
using arrT = int[10]; //等价声明
arrT *func(int i); //func返回一个指向含有10个整数的数组的指针
因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。返回数组指针的函数形式如下:
Type (*function(parameter_list))[dimension]
int (*func(int i))[10];
// func(int i)表示调用func函数时需要一个int类型的实参
// (*func(int i))意味着可以对函数调用的结果执行解引用操作
// (*func(int i))[10]表示解引用func的调用将得到一个大小是10的数组
// int(*func(int i))[10]表示数组中的元素是int类型
其中 Type 表示元素类型,dimension 表示数组大小,(*function (parameter_list)) 两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。
C++11允许使用 尾置返回类型(trailing return type) 简化复杂函数声明。尾置返回类型跟在形参列表后面,并以一个 -> 符号开头。为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加 auto 关键字。
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。
如果我们知道函数返回的指针将指向哪个数组,就可以使用 decltype 关键字声明返回类型。但 decltype 并不会把数组类型转换成指针类型,所以还要在函数声明中添加一个 * 符号。
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
返回数组指针的操作之所以比其他的麻烦是因为数组指针必须指明数组的容量
6.4 函数重载
同一作用域内的几个名字相同但形参列表不同的函数叫做重载函数。
main 函数不能重载。
函数重载要求形参数量或者形参类型存在不同,不能仅仅返回值类型不同
顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明了Record lookup(Phone*)
如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的const是底层的。当我们传递给重载函数一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了4个独立的重载函数
Record lookup(Account&); // 函数作用于Account的引用
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*); // 新函数,作用于指向Account的指针
Record lookup(const Account*); // 新函数,作用于指向常量的指针
const_cast 可以用于函数的重载。
当函数的实参是常量时,返回的结果仍然是常量的引用。
// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
当函数的实参不是常量时,将得到普通引用(实现函数返回值类型的转换)。
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2));
return const_cast<string&>(r);
}
函数匹配(function matching) 也叫做重载确定(overload resolution),是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。调用重载函数时有三种可能的结果:
编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
编译器找不到任何一个函数与实参匹配,发出 无匹配(no match) 的错误信息。
一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出 二义性调用(ambiguous call) 的错误信息。
在不同的作用域中无法重载函数名。一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
string read();
void print(const string &);
void print(double); // 重载print函数
void fooBar(int ival)
{
bool read = false; // 新作用域:隐藏了外层的read
string s = read(); // 错误:read是一个布尔值,而非函数
// 不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
void print(int); // 新作用域:隐藏了之前的print
print("Value: "); // 错误:print(const string &)被隐藏掉了
print(ival); // 正确:当前print(int)可见
print(3.14); // 正确:调用print(int); print(doub1e)被隐藏掉了
}
6.5 特殊用途语言特性
6.5.1 默认实参
默认实参作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。
虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
// 表示高度和宽度的形参没有默认位
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误:重复声明
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
默认实参只能出现在函数声明和定义其中一处。通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中。
局部变量不能作为函数的默认实参。用作默认实参的名字在函数声明所在的作用域内解析,但名字的求值过程发生在函数调用时。
// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen(); // 调用screen(ht(), 80,' ')
void f2()
{
def = '*'; // 改变默认实参的值
sz wd = 100; // 隐藏了外层定义的wd,但是没有改变默认值
window = screen(); // 调用screen(ht(), 80, '*')
}
6.5.2 内联函数和constexpr函数
内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。
定义内联函数时需要在函数的返回类型前添加关键字 inline。
// 内联版本:寻找两个string对象中较短的那个
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。内联函数中不允许有循环语句和 switch 语句,否则函数会被编译为普通函数。
constexpr 函数是指能用于常量表达式的函数。constexpr 函数的返回类型及所有形参的类型都得是字面值类型。
constexpr 函数的返回值可以不是一个常量。constexpr 函数不一定返回常量表达式。
// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
int arr[scale(2)]; // 正确:scale(2)是常量表达式
int i = 2; // i不是常量表达式
int a2[scale(i)]; // 错误:scale(i)不是常量表达式
和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此内联函数和 constexpr 函数通常定义在头文件中。
6.5.3 调试帮助
1. assert预处理宏
assert 是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。
预处理名字由预处理器而非编译器管理,因此可以直接使用预处理名字而无需提供using声明。
首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行;如果表达式为真(即非0),assert什么也不做。
assert宏常用于检查“不能发生”的条件,例如对一个输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值
assert (expr);
assert (word.size() > threshold);
2. NDEBUG预处理变量
assert 的行为依赖于于一个名为 NDEBUG 的预处理变量的状态。 (1)如果定义了 NDEBUG,则 assert 什么也不做; (2)默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
可以使用 #define 语句定义 NDEBUG,从而关闭调试状态。
除了使用assert外,也可以使用NDEBUG编写自己 的条件调试代码。如果NDEBUG未定义,将执行 #ifndef 和 #endif 之间的代码;如果定义了NDEBUG,这些代码将被忽略。
void print(const int ia[], size_t size){
#ifndef NDEBUG
//_ _func_ _ 是编译器定义的一个局部静态变量,用于存放函数的名字
cerr << _ _func_ _ << ": array size is " << size <<endl;
#endif
}
变量名称
内容
_ _func_ _
输出当前调试的函数的名称
_ _FILE_ _
存放文件名的字符串字面值
_ _LINE_ _
存放当前行号的整型字面值
_ _TIME_ _
存放文件编译时间的字符串字面值
_ _DATE_ _
存放文件编译日期的字符串字面值
6.6 函数匹配
函数实参类型与形参类型越接近,它们匹配得越好。
重载函数集中的函数称为候选函数(candidate function):
可行函数(viable function):
一是形参数量与函数调用所提供的实参数量相等
二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型
调用重载函数时应该尽量避免强制类型转换。
6.6.1 实参类型转换
精确匹配包括以下情况:
实参类型和形参类型相同
实参从数组类型或函数类型转换成对应的指针类型
向实参添加顶层 const 或者从实参中删除顶层 const
通过 const 转换实现的匹配
通过类型提升实现的匹配
通过算术类型转换或指针转换实现的匹配
通过类类型转换实现的匹配
6.7 函数指针
要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。*pf 两端的括号必不可少!!!如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数
// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); // uninitialized
// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
当把函数名作为一个值使用是,该函数自动转换成指针
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋位语句:取地址符是可选的
bool b1 = pf("hello", "goodbye"); // 调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用
函数与指针类型必须精确匹配(返回值和形参类型均需匹配)
void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int) = ff; // pf1指向ff(unsigned)
void (*pf2)(int) = ff; // 错误:没有任何一个ff与该形参列表匹配
double (*pf3)(int*) = ff; // 错误:ff和pf3的返回类型不匹配
可以把函数的形参定义成指向函数的指针。调用时允许直接把函数名当作实参使用,它会自动转换成指针。
// 第三个形参是函数类型,它会自动地转换成指向函数的指针
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 &));
// 自动将函数lengthCompare转换成指向该函数的指针
useBigger(s1, s2, lengthCompare);
返回指向函数的指针
using F = int (int*, int); //返回函数类型,这里函数不能自动转化成指针
using PF = int (*)(int*, int); //返回指针类型
PF fl(int); //正确:PF是指向函数的指针,f1返回指向函数的指针
F fl(int); //错误:F函数类型,f1不能返回一个函数
F *fl(int); //正确:显示地指定返回类型是指向函数的指针
int (*f1(int))(int *, int); //正确:f1是一个函数——返回值是一个指针——返
//回值是一个函数指针——该函数指针指向的函数返回一个int值
关键字 decltype 作用于函数时,返回的是函数类型,而不是函数指针类型。函数可以返回指向函数的指针。但返回类型不会像函数类型的形参一样自动地转换成指针,必须显式地将其指定为指针类型,即加上 *。