6 函数
1. 函数基础
- 函数是一个命名了的代码块,通过调用函数执行相应的代码。函数可以有0个或多个参数,而且(通常)会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。
- 一个典型的 函数 定义包括:返回类型(return type)、函数名字、由0个或多个形式参数(parameter,简称形参)组成的列表 和 函数体(function body)。函数执行的操作在语句块中说明,即函数体。
- 程序通过 调用运算符 来执行函数。调用运算符的形式之一是一对圆括号 (),作用于一个表达式,该表达式是函数或者指向函数的指针&&;圆括号内是一个用逗号隔开的 实际参数(argument,简称实参**) 列表,用来初始化函数形参。调用表达式的类型就是函数的返回类型。
// val的阶乘是val * (val - 1) * (val - 2) . . . * ((val - (val - 1)) * 1)
int fact(int val)
{
int ret = 1; // 局部变量,用于保存计算结果
while (val > 1)
ret *= val--; // 把ret和val的来积赋给ret,然后将val减1
return ret; // 返回结果
}
int main()
{
int j = fact(5); // j equals 120, i.e., the result of fact(5)
cout << "5! is " << j << endl;
return 0;
}
- 函数调用完成两项工作:
- 用实参初始化对应的形参;
- 将控制权从主调函数转移给被调函数。此时,主调函数的执行被暂时中断,被调函数开始执行。
- return语句结束函数的执行过程,完成两项工作:
- 返回 return 语句中的值(如果有的话)。
- 将控制权从被调函数转移回主调函数,函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。
- 形参和实参
(1)实参是形参的初始值,两者的顺序和类型必须一一对应,相应的数量也要一致。
(2)函数的形参列表可以为空,但是不能省略。
void f1() { /* ... */ } // 隐式地定义空形参列表
void f2(void) { /* ... */ } // 显式地定义空形参列表
(3)形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明,即使两个形参类型一样,也必须把两个类型声明都写出来。
int f3(int v1, v2) { /* ... */ } // 错误
int f4(int v1, int v2) { /* ... */ } // 正确
(4)函数的任意两个形参不能同名,而且函数最外层作用域中的局部变量也不能使用与函数形参一样的名字。形参的名字是可选的,但是由于无法使用未命名的形参,所以形参一般都应该有个名字,即使某个形参不被函数使用,也必须为它提供一个实参。
- 函数返回类型
(1)大多数类型都能用作函数的返回类型
(2)一种特殊的返回类型是void,它表示函数不返回任何值
return; // 隐式
return void; //显式
// 或者直接没有return
(3)函数的返回类型不能是数组类型或者函数类型,但可以是指向数组或函数的指针。
1.1 局部对象
- 在C++语言中,名字有作用域,对象有生命周期。
- 名字的作用域是程序文本的一部分, 名字在其中可见;
对象的生命周期是程序执行过程中该对象存在的一段时间。 - 函数体是一个语句块。块构成一个新的作用域,我们可以在其中定义变量。
- 形参和函数体内定义的变量统称为 局部变量,仅在函数的作用域内可见。同时局部变量还会隐藏在外层作用域中同名的其他所有声明中。
- 名字的作用域是程序文本的一部分, 名字在其中可见;
- 自动对象
只存在于块执行期间的对象称为 自动对象(automatic object),当块的执行结束后,块中创建的自动对象的值就变成未定义的了。形参是一种自动对象,在函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。
- 局部静态对象(local static object)
令局部变量(如定义在函数体内)的生命周期贯穿函数调用及之后的时间
局部静态对象在程序的执行路径第一次经过对象定义语句时初始化(只在第一次遇到定义语句时初始化,之后再遇到该定义语句会跳过该初始化),并且直到程序结束才被销毁,对象所在的函数结束执行并不会对它产生影响。在变量类型前添加关键字 static 可以定义局部静态对象。
如果局部静态对象没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0 。
#include <iostream>
using namespace std;
size_t count_calls() {
static size_t ctr = 0; // 调用结束后,这个值仍然有效
return ++ctr;
}
/*
第一次执行count_calls函数时,ctr被创建并初始化为0。之后每次执行count_calls函数,变量ctr的值都已经存在并且等于函数上一次退出时ctr的值
*/
int main () {
for (size_t i = 0; i != 4; ++i)
cout << count_calls() << endl;
return 0;
}
/*
1
2
3
4
*/
1.2 函数声明
- 和其他名字一样,函数的名字也必须在使用之前声明。
- 和变量类似,函数只能定义一次,但可以声明多次,函数声明也叫做 函数原型。
- 函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,函数体用一个分号替代即可。
- 函数声明不包括函数体,所以形参名字可忽略,但写上形参可以帮助使用者更好地理解函数的功能。
- 函数声明三要素:返回类型 函数名(形参类型描述函数的接口)
int max(int x, int y); // 函数的声明
int main(){
int x, y;
cin >> x >> y;
cout << max(x, y) << endl; // 调用函数
return 0;
}
int max(int x, int y){ // 函数的定义
return x >=y ? x : y;
}
- 总结:如果你定义的函数在main函数后面,那你就要在main函数前面进行该函数的声明。如果你定义的函数在main函数前面,那此时定义和声明合二为一,无须再加一条声明语句。
- 在头文件中进行函数声明
- 函数应该在头文件中声明,在源文件中定义。
- 把函数声明直接放在使用该函数的源文件中是合法的,也比较容易接受,但这么做可能会很烦琐且易出错。相反,如果把函数声明放在头文件中就能确保同一函数的所有声明保持一致,而且一旦我们想改变函数的接口,只需改变一条声明即可。
- 定义函数的源文件应该包含含有函数声明的头文件,编译器负责验证函数的定义和声明是否匹配
1.3 分离式编译
- 分离式编译 允许我们把程序按照逻辑关系分割到几个文件中去,每个文件独立编译。这一过程通常会产生后缀名是 .obj(Windows) 或 .o(UNIX) 的文件,该文件包含 对象代码。之后编译器把对象文件 链接(link) 在一起形成可执行文件。
2. 参数传递
- 每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。形参初始化的机理与变量初始化一样。
- 形参的类型决定了形参和实参交互的方式:
- 当形参是引用类型时,它对应的实参被引用传递,函数被 传引用调用。引用形参是它对应实参的别名。——在函数体内形参变化,函数体外的实参也会变化
- 当形参不是引用类型时,形参和实参是两个相互独立的对象,实参的值会被拷贝给形参(值传递),函数被 传值调用。——在函数体内形参变化,函数体外的实参不会变化
- 总结:如果形参是引用类型,它将绑定到对应实参上;否则将实参的值拷贝后赋给形参。
2.1 传值参数:拷贝,传递对象的值,如指针形参,形参改变不影响实参
- 如果形参不是引用类型,则函数对形参做的所有操作都不会影响实参。
- 指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。通过指针可以修改它所指对象的值。
- 指针形参的行为和指针类似,指针形参拷贝指针实参的值。可以通过指针形参改变所指对象的值,指针形参本身值的改变不改变指针实参的值
- 使用指针类型的形参可以访问或修改函数外部的对象。
// 该函数接受一个指针,然后将指针所指的位置为0
void reset(int *ip) {
*ip = 0; // 改变指针ip所指对象的值
ip = 0; // 只改变了ip的局部拷贝,实参未被改变
}
int main(){
int i = 42;
reset(&i); // 改变i的值而非i的地址
cout << "i = " << i << endl; // 输出i = 0
}
/*
调用 reset 函数之后, 实参所指的对象被置为0,但是实参本身(i的地址)并没有改变
*/
- 如果想在函数体内访问或修改函数外部的对象,建议使用引用形参代替指针形参。
- 注意:熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。但在C++语言中建议使用引用形参代替指针形参。
2.2 传引用参数:避免拷贝,直接传入对象,形参改变影响实参
- 引用形参绑定初始化它的对象,即引用直接传入对象而无须传递对象的地址。
- 通过使用引用形参,函数可以改变实参的值。
// 该函数接受一个int对象的引用,然后将对象的位置为0
void reset(int &i) // i是传给reset函数的对象的另一个名字
{
i = 0; // 改变了i所引对象的值
}
- 使用引用避免拷贝
- 使用引用形参可以避免拷贝操作,拷贝大的类类型对象或容器对象比较低效,另外有的类类型(如IO类型)根本就不支持拷贝操作,这时只能通过引用形参访问该类型的对象。
- 注意:除了内置类型、函数对象和标准库迭代器外,其他类型的参数建议以引用方式传递。
- 如果函数无须改变引用形参的值,最好将其声明为常量引用。
- 使用引用形参返隐式返回额外的信息
- 一个函数只能返回一个值,然而有时函数需要同时返回多个值,这个时候可以使用引用形参让函数返回额外信息。——实际上就是通过引用形参直接改变函数体外的对象而已,而不是通过return。
2.3 const形参和实参
- 当形参有 顶层const 时,传递给它常量对象或非常量对象都是可以的。
void fcn(const int i)
{ /* fcn能够读取i,但是不能向i写值 */ }
/*
调用 fcn 函数时,既可以传入 const int 也可以传入 int。
忽略掉形参的顶层 const 可能产生意想不到的结果:
*/
void fcn(const int i) { /* fcn能够读取i,但是不能向i写值 */ }
void fcn(int i) { /* ... */ } // 错误:重复定义了fcn(int)
// 因为两个函数传入的参数一样,重复定义了
指针或引用形参与const
- 变量初始化规则:可以使用非常量对象初始化一个底层 const 形参,但是反过来不行。同时一个普通的引用必须用同类型的对象初始化。
int i = 42;
const int *cp = &i; //正确,但不能用cp改变i
const int &r = i; // 正确:r是常量引用,所以不能用r改变i
const int &r2 = 42; //正确:可以用字面值初始化一个常量引用
int *p = cp; //错误:p的类型和cp的类型不匹配
int &r3 = r; // 错误:r3和r类型不匹配
int &r4 = 42; //错误:不能用字面值初始化一个非常量引用
- 形参初始化同样规则:
void reset(int *ip);
void reset(int &i);
int i = 0;
const int ci = i;
string::size_type ctr =0;
reset(&i); //调用形参类型是int*的reset函数
reset(&ci); //错误:不能用指向const int对象的指针初始化普通指针int*
reset(i); //调用形参类型是int的reset函数
reset(ci); //错误:普通引用形参不能绑定const实参
reset(42); //错误:普通引用形参不能绑定字面值实参
- 尽量使用常量引用
- 把函数不会改变的形参定义成普通引用是一种比较常见错误,会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。
- 此外,使用普通引用而非常量引用也会极大地限制函数所能接受的实参类型。——即我们不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。
string::size_type find_char(string &s, charc;string::size_type &occurs)
find_char("Hello!", 'o', ctr); //错误,第一个形参是非常量引用,不可以用字面值实参"Hello"初始化
2.4 数组形参
- 数组的两个特殊性质对定义和使用作用在数组上的函数有影响,分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。
(1)因为不能拷贝数组,所以无法以值传递的方式使用数组参数,但是可以把形参写成类似数组的形式。
// 尽管形式不同,但这三个print函数是等价的
// 每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]); // 可以看出来,函数的意图是作用于一个数组
void print(const int[10]); // 这里的维度表示我们期望数组含有多少元素,实际不一定
// 这三个函数的形参实际上都是const int*类型
(2)因为数组会被转换成指针,所以当我们传递给函数一个数组时,实际上传递的是指向数组首元素的指针。
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);
- 专门定义一个表示数组大小的形参(在C程序和过去的C++程序中常常使用这种方法):void print(const int ia[], size_t size);
- 以数组作为形参的函数必须确保使用数组时不会越界。
- 数组形参和const
- 如果函数不需要对数组元素执行写操作,应该把数组形参定义成指向 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个整数的整型数组的引用
//该函数只能作用于大小为10的数组
int i = 0, j[2] = {0, 1};
int k[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
print(&i); // 错误:实参不是含有10个整数的数组
print(j); // 错误:实参不是含有10个整数的数组
print(k); // 正确:实参是含有10个整数的数组
- 传递多维数组:形参类型就是多维数组首元素指针类型
- 将多维数组传递给函数时,真正传递的是指向数组首元素的指针,数组第二维(以及后面所有维度)的大小是数组类型的一部分,不能省略,作为额外参数。
// 首元素指针指向一个由10个整数构成的数组
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
// 或者说matrix指向数组首行,rowSize表示数组行数
void print(int (*matrix)[10], int rowSize) { /* ... */ }
// *matrix两端的括号必不可少:
int *matrix[10]; // 10个指针构成的数组
int (*matrix)[10]; // 指向含有10个整数的数组的指针
// 等价定义
void print(int matrix[][10], int rowSize) { /* ... */ }
2.5 main:处理命令行选项
- 可以在命令行中向 main 函数传递参数,形式如下:
- 第一个形参 argc 表示数组中字符串的数量;
- 第二个形参 argv 是一个数组,数组元素是指向C风格字符串的指针
int main(int argc, char *argv[]) { /*...*/ }
int main(int argc, char **argv) { /*...*/ }
- 当实参传递给 main 函数后,argv 的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次传递命令行提供的实参,最后一个指针之后的元素值保证为0。
- 当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。
2.6 含有可变形参的函数:实参数量不定
- C++11新标准提供了两种主要方法处理实参数量不定的函数:
- 如果实参类型相同,可以使用 initializer_list 标准库类型;
- 如果实参类型不同,可以定义可变参数模板。
- C++还可以使用省略符形参传递可变数量的实参,但这种功能一般只用在与C函数交换的接口程序中。
- initializer_list形参:函数实参数量未知但全部实参的类型相同
- initializer_list 是一种标准库类型,定义在头文件 initializer_list 中,表示某种特定类型的值的数组。
- initializer_list 提供的操作:
- 和 vector 一样,initializer_list 也是一种模板类型,定义对象时,必须说明列表中所含元素的类型
- 和 vector 不一样的是,initializer_list 对象中的元素永远是常量值,无法改变
- 使用如下的形式编写输出错误信息的函数,使其可以作用于可变数量的实参:
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " " ;
cout << endl;
}
- 拷贝或赋值一个 initializer_list 对象不会拷贝列表中的元素,拷贝后,原始列表和副本共享元素。initializer_list 对象中的元素永远是常量值。
- 如果想向 initializer_list 形参传递一个值的序列,则必须把序列放在一对花括号内。
// expected和actual是string对象
if (expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg({"functionX", "okay"});
- 因为 initializer_list 包含 begin 和 end 成员,所以可以使用范围for循环 处理其中的元素。
- 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为 varargs 的C标准库功能。通常,省略符形参不应该用于其他目的。
- 省略符形参
- 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为 varargs 的C标准库功能。通常,省略符形参不应该用于其他目的。
- 注意:省略符形参应该仅仅用于C和C++通用的类型,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
void foo(parm_list, ...);
void foo(...);
总结各类形参(引用,const,指针)
// 参考:https://www.cnblogs.com/whl2012/p/3617230.html
#include <stdlib.h>
#include <iostream> //这是一个关于引用形参,const形参,指针形参的程序,用于理解不同形式的区别
using namespace std;
// 形参类型1:非引用形参或顶层const形参——拷贝,常量或普通实参--->常量/普通形参
//非引用形参:是“实参”的一个拷贝副本,修改“形参”不影响“实参”
//const常量:值不可修改
//非引用非const形参和非引用const形参均可由const或非const实参初始化
int use_ptr1(int val)//非引用非const形参
{
return ++val; //可以修改
}
int use_ptr2(const int val) //非引用const形参
{
return val; //不可修改
}
//形参类型2:指针形参——常量或普通实参--->常量形参;普通实参--->普通形参
int use_ptr3(int *s1) //指针形参指向非const对象
{ //只能由指向非const对象的实参指针初始化
return ++(*s1); //可以修改
}
int use_ptr4(const int *s2) //“指向const对象的指针”作为形参
{ //可以由“指向const和非const对象的实参指针”初始化(whl啥意思)
return *s2; //不可修改
}
/*********************************/
// 形参类型3:引用形参——常量或普通实参--->常量形参;普通实参--->普通形参
//以“引用”作为“形参”:直接关联到绑定对象,对“形参”的修改就是对“实参”的修改
int use_ptr5(int &val) //非const引用形参
{ //只能与完全同类型的非const对象关联
return ++val; //不允许传递右值或需要类型转换的对象
}
int use_ptr6(const int &val) //const引用形参
{ //可以由非const或const对象初始化
return val; //也可以传递右值或需要类型转换的对象
}
//传递指针的引用
void ptrswap1(int *&s1,int *&s2) //指向非const对象的指针的引用
{
//形参定义的理解:int *&a->a是一个引用,与指向int型对象的指针相关联。也就是说,a是传递进函数swap的任意指针的别名
//使用“指向非const对象的指针”初始化
int *temp = s2;
s2 = s1;
s1 = temp;
}
void ptrswap2(const int *&s1,const int *&s2) //“指向const对象的指针”的引用
{
//形参定义的理解:const int *&a->a是一个引用,与指向int型const对象的指针相关联。
//使用指向const对象的指针初始化
const int *temp = s2;
s2 = s1;
s1 = temp;
}
/*********************************/
int main()
{
int v1 = 1;
const int v2 = 2;
short v3 = 3;
cout << v1 << " " << v2 << endl;
use_ptr1(v1);
use_ptr1(v2);
cout << v1 << " " << v2 << endl;
use_ptr2(v1);
use_ptr2(v2);
cout << v1 << " " << v2 << endl;
use_ptr3(&v1);
//use_ptr3(&v2); //error:不能使用指向const对象的指针来初始化指向非const对象的指针
cout << v1 << " " << v2 << endl;
use_ptr4(&v1);
use_ptr4(&v2);
cout << v1 << " " << v2 << endl;
use_ptr5(v1);
//use_ptr5(v2); //error:不能使用const对象指初始化非const引用形参
//use_ptr5(v3); //error;不能使用不同类型的对象来初始化形参
//use_ptr5(0); //error:不能使用右值来初始化
//use_ptr5(v1+v2); //error: 不能使用右值来初始化
use_ptr6(v1);
use_ptr6(v2);
use_ptr6(v3); //OK:类型转换
use_ptr6(0); //OK:右值
use_ptr6(v1+v2); //OK:右值
int a = 1, b = 2;
int *p1 = &a;
int *p2 = &b;
const int *p3 = &a;
const int *p4 = &b;
ptrswap1(p1, p2);
//ptrswap1(p3, p4); //error:指向const对象的指针
cout << *p1 << " " << *p2 << endl;
//ptrswap2(p1, p2); //error:指向非const对象的指针
ptrswap2(p3, p4);
cout << *p1 << " " << *p2 << endl;
system("pause");
return 0;
}
3. 返回类型和return语句
- return 语句作用是终止当前正在执行的函数并返回到调用该函数的地方。
- return 语句有两种形式:
return;
return expression;
3.1 无返回值函数:直接忽略return语句或return;
- 没有返回值的 return 语句只能用在返回类型是 void 的函数中。
- 返回 void 的函数可以省略 return 语句,因为在这类函数的最后一条语句后面会隐式地执行 return。
- 通常情况下,如果 void 函数想在其中间位置提前退出,可以使用 return 语句,return 的这种用法有点类似于用 break 语句退出循环。
void swap (int &vl , int &v2) {
// 如果两个值是相等的,则不需要交换,直接退出
if (v1 == v2)
return;
// 如果程序执行到了这里,说明还需要继续完成某些功能
int tmp = v2;
v2 = v1;
v1 = tmp;
// 此处无须显式的return语句
}
- 一个返回类型是 void 的函数也能使用 return 语句的第二种形式,不过此时 return 语句的 expression 必须是另一个返回 void 的函数,强行令 void 函数返回其他类型的表达式将产生编译错误。
3.2 有返回值函数:return expression;
- return 语句的第二种形式提供了函数的结果。只要函数的返回类型不是 void,该函数内的每条 return 语句就必须返回一个值,并且返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型(main 函数例外)。
- return 语句没有返回值是错误的,编译器能检测到这个错误。
- 在含有 return 语句的循环后面应该也有一条 return 语句,否则程序就是错误的,但很多编译器无法发现此错误。
- 函数返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果。如果函数返回引用类型,则该引用仅仅是它所引用对象的一个别名。
当函数的返回类型是引用时
- 函数可以返回引用形参,毕竟引用形参的引用对象在函数体外,函数结束并不会销毁。函数返回类型须和该形参类型相同
const string &shorterstring(const string &s1, const string &s2):
{
return s1.size() >= s2.size() ? s1 : s2
}
- 函数不应该返回局部对象(函数体内对象)的指针或引用,因为一旦函数完成,局部对象将被释放,指针将指向一个不存在的对象。
// 严重错误: 这个函数试图返回局部对象的引用
const string &manip()
{
string ret;
// 以某种方式改变一下ret
if (!ret.empty())
return ret; // 错误:返回局部对象的引用!
else
return "Empty"; // 错误:"Empty"是一个局部临时量
}
- 如果函数返回指针、引用或类的对象,则可以使用函数调用的结果访问结果对象的成员
auto sz = shorterstring(s1, s2).size();
引用返回左值
- 函数的返回类型决定函数调用是否是左值。调用一个返回引用的函数会得到左值,其他返回类型得到右值。特别是,我们能为返回类型是非常量引用的函数的结果赋值。
列表初始化返回值
- 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的返回值
- 如果函数的返回类型不是 void,那么它必须返回一个值,除了main 函数可以没有 return 语句直接结束。
- 如果控制流到达了 main 函数的结尾处并且没有return语句,编译器会隐式地插入一条返回0的 return 语句。
- main 函数的返回值可以看作是状态指示器。*返回0表示执行成功,返回其他值表示执行失败,其中非0值的具体含义依机器而定。
- 为了使 main 函数的返回值与机器无关,头文件 cstdlib 定义了 EXIT_SUCCESS 和 EXIT_FAILURE 这两个预处理变量,分别表示执行成功和失败。
int main()
{
if (some_failure)
return EXIT_FAILURE; // 定义在cstdlib头文件中
else
return EXIT_SUCCESS; // 定义在cstdlib头文件中
}
- 建议:使用预处理变量 EXIT_SUCCESS 和 EXIT_FAILURE 表示 main 函数的执行结果。
递归
- 如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为 递归函数(recursive function)。
// 计算val!,即1 * 2 * 3 . . . * val
int factorial(int val)
{
if (val > 1)
return factorial(val-1) * val;
return 1;
}
- 在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。
- 相对于循环迭代,递归的效率较低,但在某些情况下使用递归可以增加代码的可读性。
- 循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继)
- 而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。
- main函数不能调用它自身。
3.3 返回数组指针
- 因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。
- 法1:使用类型别名
typedef int arrT[10]; //arrT是一个类型别名,表示的类型是含有10个整数的数组
using arrT = int[10]; //arrT的等价声明
arrT* func(int i); // func返回一个指向含有10个整数的数组的指针
- 法2:声明一个返回数组指针的函数:数组的维度必须写上,放在形参列表之后
- 返回数组指针的函数形式如下:
- Type (*function(parameter_list))[dimension]
- 其中 Type 表示元素类型,dimension 表示数组大小,(*function (parameter_list)) 两端的括号必须存在,如果没有这对括号,函数的返回类型将是指针的数组。
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类型
- 法3:使用尾置返回类型:对于返回类型比较复杂的函数最有效
- C++11允许使用 尾置返回类型 简化复杂函数声明。尾置返回类型跟在形参列表后面,并以一个 -> 符号开头。
- 为了表示函数真正的返回类型在形参列表之后,需要在本应出现返回类型的地方添加 auto 关键字。
- 任何函数的定义都能使用尾置返回类型,但是这种形式更适用于返回类型比较复杂的函数。
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];
- 法4:使用decltype:提前知道函数返回的指针指向哪个数组
- 如果我们知道函数返回的指针将指向哪个数组,就可以使用 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; // 返回一个指向数组的指针
}
4、函数重载
- 同一作用域内的几个名字相同但形参列表不同的函数叫做 重载函数。
- 虽然函数名相同(如果两个函数的处理近似,可以取相同名字),但可以根据输入参数的类型不同来调用不同的重载函数
void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[), size_t ze);
- 函数的名字仅仅是让编译器知道它调用的是哪个函数,而函数重载可在一定程度上减轻程序员起名字、记名字的负担。
- main 函数不能重载。
- 不允许两个函数除了返回类型以外的其他所有要素都相同。
Record lookup(const Account&);
bool lookup(const Account&); // 错误:与上一个函数相比只有返回类型不同
- 形参是顶层 const:形参不可区分
- 顶层 const 不影响传入函数的对象,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来
- ps:此时,形参自身是常量还是普通,都不影响实参的传入——只是值传递,普通实参的值可以给常量形参,也可以给普通形参;常量实参的值可以给常量形参,也可以给普通形参。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明了Record lookup(Phone)
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明了Record lookup(Phone*)
- 形参是底层const:形参可区分
- 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
- 如果形参是某种类型的指针或引用,则通过区分其指向的对象是常量还是非常量可以实现函数重载,此时的const是底层的
- ps:此时,常量实参可以被常量引用形参引用,但不能被普通引用形参引用;普通实参既可以被常量引用形参引用也可以被普通引用形参引用,但编译器会优先普通引用形参。
// 对于接受引用或指针的函数来说,对象是常量还是非常量对应的形参不同
// 定义了4个独立的重载函数
Record lookup(Account&); // 函数作用于Account的引用
Record lookup(const Account&); // 新函数,作用于常量引用
Record lookup(Account*); // 新函数,作用于指向Account的指针
Record lookup(const Account*); // 新函数,作用于指向常量的指针
- 当我们传递给重载函数一个非常量对象或者指向非常量对象的指针时,编译器会优先选用非常量版本的函数。
- 如果重载函数的区别在于它们的引用类型的形参是否引用了const,或者指针类型的形参是否指向const,则当调用发生时,编译器通过实参是否是变量决定选用哪个函数,例如:如果实参是指向常量的指针,调用形参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。
- const_cast和重载
- const_cast在重载函数的情景中最有用
- const_cast 将常量对象转换成非常量对象,可以用于函数的重载。(4.11.3节,p145)
- 当函数的实参是常量时,返回的结果仍然是常量的引用。
- 当函数的实参不是常量时,将得到普通引用。
// 当函数的实参是常量时,返回的结果仍然是常量的引用
// 比较两个string对象的长度,返回较短的那个引用
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
// 这个函数的参数和返回类型都是const string的引用
// 可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string
// 因此需要另一函数,当实参不是常量时,得到的结果仍然是一个普通的引用
}
// 当函数的实参不是常量时,将得到普通引用
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string&>(s1),
const_cast<const string&>(s2)); // 强制转换成const引用后调用const版本函数
return const_cast<string&>(r); // 转换回普通引用
}
- 调用重载函数
- 函数匹配 也叫做 重载确定,是指编译器将函数调用与一组重载函数中的某一个进行关联的过程。
- 编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用是哪个函数。
- 调用重载函数时有三种可能的结果:
- (1)编译器找到一个与实参 最佳匹配(best match) 的函数,并生成调用该函数的代码。
- (2)编译器找不到任何一个函数与实参匹配,发出 无匹配(no match) 的错误信息。
- (3)有一个以上的函数与实参匹配,但每一个都不是明显的最佳选择,此时编译器发出 二义性调用(ambiguous call) 的错误信息。
4.1 重载与作用域
- 注意:一般来说,将函数声明置于局部作用域内不是一个明智的选择。
- 在不同的作用域中无法重载函数名。一旦在当前作用域内找到了所需的名字,编译器就会忽略掉外层作用域中的同名实体。
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)被隐藏掉了
}
- 在C++中,名字查找发生在类型检查之前
5. 特殊用途语言特性
- 三种函数相关的语言特性:默认实参、内联函数和constexpr函数
5.1 默认实参
- 默认实参 作为形参的初始值出现在形参列表中。可以为一个或多个形参定义默认值,不过一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。——(某个形参有默认值,则它后面的形参都要有默认值。形参列表中,一般有默认值的在后面,没有默认值的在前面)
typedef string::size_type sz;
string screen(sz ht = 24, sz wid = 80, char backgrnd = ' ');
- 调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。
- 如果想使用默认实参,只要在调用函数的时候省略该实参即可。
- 默认实参声明
- 对于函数声明,通常的习惯是放在头文件中,并且一个函数只能声明一次,但是多次声明同一个函数也是合法的。
- 虽然多次声明同一个函数是合法的,但是在给定的作用域中一个形参只能被赋予一次默认实参。函数的后续声明只能为之前那些没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。
- 默认实参只能出现在函数声明和定义其中一处。通常应该在函数声明中指定默认实参,并将声明放在合适的头文件中。
// 表示高度和宽度的形参没有默认位
string screen(sz, sz, char = ' ');
string screen(sz, sz, char = '*'); // 错误:重复声明
string screen(sz = 24, sz = 80, char); // 正确:添加默认实参
// 最后一条语句中的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,但是没有改变默认值,因为该局部变量wd与函数外的wd没有任何关系
window = screen(); // 调用screen(ht(), 80, '*')
}
5.2 内联函数
- 内联函数:可避免函数调用的开销
- 内联函数会在每个调用点上“内联地”展开,省去函数调用所需的一系列工作。
- 定义内联函数时需要在函数的返回类型前添加关键字 inline
// 内联版本:寻找两个string对象中较短的那个
inline const string &
shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
/*
对于如下调用:
cout << shorterString(s1, s2) << endl;
将在编译过程中展开成类似于下面的形式
cout << (s1.size() < s2.size() ? s1 : s2) << endl;
// 从而消除shorterString的函数运行时开销
*/
- 在函数声明和定义中都能使用关键字 inline,但是建议只在函数定义时使用。
- 一般来说,内联机制适用于优化规模较小、流程直接、调用频繁的函数。
- 内联函数中不允许有循环语句和 switch 语句,否则函数会被编译为普通函数。
- 总结:
- 内联函数的作用:避免了在函数调用时一系列的保存寄存器、拷贝实参等的操作,避免了函数调用的开销,提高了程序的执行效率,而是在调用点上内联的展开
5.3 constexpr函数
- constexpr函数 是指能用于常量表达式(2.4.4节)的函数。
- 约定:constexpr 函数的返回类型及所有形参的类型都得是字面值类型。
constexpr int new_sz() {rerturn 42;}
constexpr int foo = new_sz(); //正确:foo是一个常量表达式
- 另外C++11标准要求 constexpr 函数体中必须有且只有一条 return 语句,但是此限制在C++14标准中被删除。
- constexpr 函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行。如constexpr函数中可以有空语句、类型别名以及using声明。
- 允许constexpr 函数的返回值不是一个常量。
// 如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt)
{
return new_sz() * cnt;
}
- 当 scale 的实参是常量表达式时,它的返回值也是常量表达式;反之则不然:
int arr[scale(2)]; // 正确:scale(2)是常量表达式
int i = 2; // i不是常量表达式
int a2[scale(i)]; // 错误:scale(i)不是常量表达式
- constexpr 函数不一定返回常量表达式。
- 把内联函数和constexpr函数放在头文件内
- 和其他函数不同,内联函数和 constexpr 函数可以在程序中多次定义。因为在编译过程中,编译器需要函数的定义来随时展开函数,仅有函数的声明是不够的。对于某个给定的内联函数或 constexpr 函数,它的多个定义必须完全一致。因此联函数和 constexpr 函数通常定义在头文件中
5.4 调试帮助
- 程序可以包含一些用于调试的代码,但是这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码
assert 预处理宏
- assert 是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数
assert (expr);
- 首先对 expr 求值,如果表达式为假(即0),assert 输出信息并终止程序的执行;如果表达式为真(即非0),assert什么也不做。
- assert宏常用于检查“不能发生”的条件。assert应该仅用于验证那些确实不可能发生的事情。
assert (word.size() > threshold);
NDE8UG 预处理变量
- assert 的行为依赖于于一个名为 NDEBUG 的预处理变量的状态。
- 如果定义了 NDEBUG,则 assert 什么也不做;
- 默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。
- 可以使用 #define 语句定义 NDEBUG,从而关闭调试状态。
- 常用的几个对于程序调试很有用的名字
- 可以使用这些常量在错误消息中提供更多的信息。如输出当前调试的函数名、当前文件名等。
6. 函数匹配
- (1)先确定候选函数和可行函数
- 重载函数集中的函数称为 候选函数(candidate function):
- 一是与被调用的函数同名;
- 二是其声明在调用点可见。
- 可行函数(viable function):在候选函数中筛选
- 一是形参数量与函数调用所提供的实参数量相等;
- 二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。
- 如果没找到可行函数,编译器将报告无匹配函数的错误。
- 重载函数集中的函数称为 候选函数(candidate function):
- (2)寻找最佳匹配(如果有的话):从可行函数中选择
- 函数实参类型与形参类型越接近,它们匹配得越好
- 调用重载函数时应该尽量避免强制类型转换。
- 在设计良好的系统中,不应该对实参进行强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。
6.1 实参类型转换
- 为了确定最佳匹配,编译器将实参类型到形参类型的转换划分成几个等级。具体排序如下所示:
- 精确匹配 包括以下情况:
- 实参类型和形参类型相同
- 实参从数组类型或函数类型转换成对应的指针类型
- 向实参添加顶层 const 或者从实参中删除顶层 const
- 通过 const转换 实现的匹配
- 通过 类型提升 实现的匹配
- 通过 算术类型转换或指针转换 实现的匹配
- 通过 类类型转换 实现的匹配
- 精确匹配 包括以下情况:
- 所有算术类型转换的级别都一样。
- 如果重载函数的区别在于它们的引用或指针类型的形参是否含有底层 const,或者指针类型是否指向 const,则调用发生时编译器通过实参是否是常量来决定函数的版本。
Record lookup(Account&); // 函数的参数是Account的引用
Record lookup(const Account&); // 函数的参数是一个常量引用
const Account a;
Account b;
lookup(a); // 调用lookup(const Account&)
lookup(b); // 调用lookup(Account&)
7. 函数指针
- 函数的类型有它的返回类型和形参类型共同决定,与函数名无关
bool lengthCompare(const string &, const string &);
// 该函数的类型是bool (const string &, const string &)
- 要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
// 比较两个string对象的长度
bool lengthCompare(const string &, const string &);
// pf指向一个函数,该函数的参数是两个const string的引用,返回值是bool类型
bool (*pf)(const string &, const string &); // uninitialized
// *pf 两端的括号必不可少!!!如果不写这对括号,则 pf 是一个返回值为 bool 指针的函数:
// 声明一个名为pf的函数,该函数返回bool*
bool *pf(const string &, const string &);
- (1)使用函数指针
- 把函数名作为一个值使用时,该函数自动地转换成指针。
pf = lengthCompare; // pf指向名为lengthCompare的函数
pf = &lengthCompare; // 等价的赋位语句:取地址符是可选的+
- 可以直接使用指向函数的指针来调用函数,无须提前解引用指针。
bool b1 = pf("hello", "goodbye"); // 调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye");// 另一个等价的调用
- (2)重载函数的指针
- 对于重载函数,上下文必须清晰地界定到底应该选用了哪个函数,编译器通过指针类型决定函数版本,指针类型必须与重载函数中的某一个精确匹配。
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的返回类型不匹配
- (3)函数指针作为形参
- 和前面的数组类似,虽然不能定义函数类型的形参,但是可以把函数的形参定义成指向函数的指针。调用时允许直接把函数名当作实参使用,它会自动转换成指针。
// 第三个形参是函数类型,它会自动地转换成指向函数的指针
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);
- 直接在形参中使用函数指针类型显得冗长而烦琐,使用类型别名进行简化,四种等价形式:
bool lengthCompare(const string &, const string &);
// Func 和 Func2是函数类型,下面两条语句等价
typedef bool Func(const string&, const string &);
typedef decltype(lengthCompare) Func2;
// FuncP 和 FuncP2是函数类型,下面两条语句等价
typedef bool (*FuncP)(const string&, const string &);
typedef decltype(lengthCompare) *FuncP2;
- 关键字 decltype 作用于函数时,返回的是函数类型,而不会自动转成换函数指针类型。。因为decltype的结果是函数类型,所以只有在结果前面加上*才能得到指针。可以使用如下方式重新声明useBigger
//四种等价的声明:使用类型别名
//编码器自动将Func表示的函数类型转换成指针
void useBigger(const string &, const string &, Func);
void useBigger(const string &, const string &, Func2);
void useBigger(const string &, const string &, FuncP);
void useBigger(const string &, const string &, FuncP2);
- (4)返回指向函数的指针
- 和前面的数组类似,虽然不能返回一个函数,但是能返回指向函数类型的指针。
- 和函数类型的形参不一样,编译器不会自动将函数返回类型转换成指针。我们必须显式地把返回类型写成指针形式。
- 1)最简单的方法是使用类型别名
using F = int(int *, int); // F是函数类型,不是指针
using PF = int(*) (int *, int); //PF是指针类型
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F *f1(int); //正确:显式地指定返回类型是指向函数的指针
PF f1(int); //正确:PF是指向函数的指针,f1返回指向函数的指针
- 2)还可以像数组一样声明一个返回函数指针的函数
int (*f1(int)) (int *, int );
- 3)使用尾置返回类型
auto f1(int) -> int (*) (int *, int);
- 4)将auto和decltype用于函数指针类型——明确知道返回函数类型