函 数 C++语言继承了C语言的全部语法,也包括函数的定义与使用方法。在面向过程的结 构化程序设计中,函数是模块划分的基本单位,是对处理问题过程的一种抽象。在面向对象 的程序设计中,函数同样有着重要的作用,它是面向对象程序设计中对功能的抽象。 一个较为复杂的系统往往需要划分为若干子系统,然后对这些子系统分别进行开发和 调试。高级语言中的子程序就是用来实现这种模块划分的。C和C++ 语言中的子程序体 现为函数。通常将相对独立的、经常使用的功能抽象为函数。函数编写好以后,可以被重复 使用,使用时可以只关心函数的功能和使用方法而不必关心函数功能的具体实现。这样有 利于代码重用,可以提高开发效率、增强程序的可靠性,也便于分工合作和修改维护。 3.1 函数的定义与使用 第2章例题中出现的main就是一个函数,它是C++程序的主函数。一个C++程序可 以由一个主函数和若干子函数构成。主函数是程序执行的开始点。由主函数调用子函数, 子函数还可以再调用其他子函数。 调用其他函数的被称为主调函数,被其他函数调用的称为被调函数。一个函数很可能 既调用别的函数又被另外的函数调用,这样它可能在某一个调用与被调用关系中充当主调 函数,而在另一个调用与被调用关系中充当被调函数。 3.1.1 函数的定义 1.函数定义的语法形式 类型说明符 函数名(含类型说明的形式参数表) { 语句序列 } 2.形式参数 形式参数(简称形参)表的内容如下: type1 name1, type2 name2, …, typen namen type1、type2、……、typen是类型标识符,表示形参的类型。name1、name2、……、namen是 形参名。形参的作用是实现主调函数与被调函数之间的联系,通常将函数所处理的数据、影 响函数功能的因素或者函数处理的结果作为形参。 如果一个函数的形参表为空,则表示它没有任何形参,例如第2章例题中的main函数 都没有形参。main函数也可以有形参,其形参也称命令行参数,由操作系统在启动程序时 初始化。不过命令行参数的数量和类型有特殊要求,请读者参考学生用书中本章的实验指 导,尝试编写带命令行参数的程序。 函数在没有被调用时是静止的,此时的形参只是一个符号,它标志着在形参出现的位置 应该有一个什么类型的数据。函数在被调用时才执行,也是在被调用时才由主调函数将实 际参数(简称实参)赋予形参。这与数学中的函数概念相似,例如在数学中我们都熟悉这样 的函数形式: f(x)=x2 +x +1 这样的函数只有当自变量被赋值以后,才能计算出函数的值。 3.函数的返回值和返回值类型 函数可以有一个返回值,函数的返回值是需要返回给主调函数的处理结果。类型说明 符规定了函数返回值的类型。函数的返回值由return语句给出,格式如下: return 表达式; 除了指定函数的返回值外,return语句还有一个作用,就是结束当前函数的执行。 例如,主函数main的返回值类型是int,主函数中的return0语句用来将0作为返回 值,并且结束main函数的执行。main函数的返回值最终传递给操作系统。 一个函数也可以不将任何值返回给主调函数,这时它的类型标识符为void,可以不写 return语句,但也可以写一个不带表达式的return语句,用于结束当前函数的调用,格式 如下: return; 3.1.2 函数的调用 1.函数的调用形式 在2.2.3小节曾经提到过,变量在使用之前需要首先声明,类似地,函数在调用之前也 需要声明。函数的定义就属于函数的声明,因此,在定义了一个函数之后,可以直接调用这 个函数。但如果希望在定义一个函数前调用它,则需要在调用函数之前添加该函数的函数 原型声明。函数原型声明的形式如下: 类型说明符 函数名(含类型说明的形参表); 与变量的声明和定义类似,声明一个函数只是将函数的有关信息(函数名、参数表、返回 值类型等)告诉编译器,此时并不产生任何代码;定义一个函数时除了同样要给出函数的有 关信息外,主要是要写出函数的代码。2.5节讲解过使用decltype来获取某个变量或表达 式的类型,对于函数返回值的使用方法类似,以简化函数返回值类型定义: int a=10, b=5; decltype(a) myMax(decltype(a) lhs, decltype(a) rhs){ //返回值类型与a 保持一致 return lhs>rhs? lhs:rhs; } 第3章 函 数·65· 如上定义了函数返回值和形参类型与变量a类型一致的取最大值函数。 细节 声明函数时,形参表只要包含完整的类型信息即可,形参名可以省略,也就是说, 原型声明的形参表可以按照下面的格式书写: type1, type2, ..., typen 但这并不是值得推荐的写法,因为形参名可以向编程者提示每个参数的含义。 如果是在所有函数之前声明了函数原型,那么该函数原型在本程序文件中任何地方都 有效。也就是说,在本程序文件中任何地方都可以依照该原型调用相应的函数。如果是在 某个主调函数内部声明了被调函数原型,那么该原型就只能在这个函数内部有效。 声明了函数原型之后,便可以按如下形式调用子函数: 函数名(实参列表) 实参列表中应给出与函数原型形参个数相同、类型相符的实参,每个实参都是一个表达 式。函数调用可以作为一条语句,这时函数可以没有返回值。函数调用也可以出现在表达 式中,这时就必须有一个明确的返回值。 调用一个函数时,首先计算函数的实参列表中各个表达式的值,然后主调函数暂停执 行,开始执行被调函数,被调函数中形参的初值就是主调函数中实参表达式的求值结果。当 被调函数执行到return语句,或执行到函数末尾时,被调函数执行完毕,继续执行主调 函数。例 3-1 编写一个求x的n次方的函数。 //3_1.cpp #include using namespace std; //计算x 的n 次方 double power(double x, int n) { double val=1.0; while (n--) val *=x; return val; } int main() { cout<<"5 to the power 2 is "< using namespace std; //计算x 的n 次方 double power(double x, int n); int main() { int value=0; cout<<"Enter an 8 bit binary number: "; for (int i=7; i>=0; i--) { char ch; cin>>ch; if (ch=='1') value+=static_cast(power(2, i)); } cout<<"Decimal value is "< using namespace std; double arctan(double x) { double sqr=x*x; double e=x; double r=0; int i=1; while (e/i>1e-15) { double f=e/i; r=(i%4==1) ? r+f : r-f; e=e*sqr; i+=2; } return r; } int main() { double a=16.0*arctan(1/5.0); double b=4.0*arctan(1/239.0); //注意: 因为整数相除结果取整,如果参数写1/5,1/239,结果就都是0 cout<<"PI="< ·68· C++语言程序设计(第5版) using namespace std; //判断n 是否为回文数 bool symm(unsigned n) { unsigned i=n; unsigned m=0; while (i>0) { m=m*10+i%10; i /=10; } return m==n; } int main() { for (unsigned m=11; m <1000; m++) if (symm(m) && symm(m*m) && symm(m*m*m)) { cout<<"m="<>seed; //输入随机数种子 srand(seed); //将种子传递给rand() sum=rollDice(); //第一轮投骰子、计算和数 switch (sum) { case 7: //如果和数为7 或11 则为胜,状态为WIN case 11: status=WIN; break; case 2: //和数为2、3 或12 则为负,状态为LOSE case 3: case 12: status=LOSE; break; default: //其他情况,游戏尚无结果,状态为PLAYING,记下点数,为下一轮做准备 第3章 函 数·71· status=PLAYING; myPoint=sum; cout<<"point is "< using namespace std; int fun2(int m) { return m*m; } int fun1(int x,int y) { return fun2(x)+fun2(y); } int main() { int a, b; cout<<"Please enter two integers(a and b): "; cin>>a>>b; cout<<"The sum of square of a and b: "<0) 这是一个递归形式的公式,在描述“阶乘”算法时又用到了“阶乘”这一概念,因而编程时 也自然采用递归算法。递归的结束条件是n=0。 源程序: //3_8.cpp ·74· C++语言程序设计(第5版) #include using namespace std; //计算n 的阶乘 unsigned fac(unsigned n) { unsigned f; if (n==0) f=1; else f=fac(n-1)*n; return f; } int main() { unsigned n; cout<<"Enter a positive integer: "; cin>>n; unsigned y=fac(n); cout< using namespace std; //计算从n 个人里选k 个人的组合数 int comm(int n, int k) { 第3章 函 数·75· if (k>n) return 0; else if (n==k || k==0) return 1; else return comm(n-1, k)+comm(n-1, k-1); } int main() { int n, k; cout<<"Please enter two integers n and k: "; cin>>n>>k; cout<<"C(n, k)="< using namespace std; //把src 针的最上面一个盘子移动到dest 针上 void move(char src, char dest) { cout<"<>m; cout<<"the steps to move "<C A -->B C -->B A -->C B -->A B -->C A -->C 3.1.3 函数的参数传递 在函数未被调用时,函数的形参并不占有实际的内存空间,也没有实际的值。只有在函 数被调用时才为形参分配存储单元,并将实参与形参结合。每个实参都是一个表达式,其类 型必须与形参相符。函数的参数传递指的就是形参与实参结合(简称形实结合)的过程,形 实结合的方式有值传递和引用传递。 第3章 函 数·77· 1.值传递 值传递是指当发生函数调用时,给形参分配内存空间,并用实参来初始化形参(直接将 实参的值传递给形参)。这一过程是参数值的单向传递过程,一旦形参获得了值便与实参脱 离关系,此后无论形参发生了怎样的改变,都不会影响实参。 例3-11 将两个整数交换次序后输出。 //3_11.cpp #include using namespace std; void swap(int a, int b) { int t=a; a=b; b=t; } int main() { int x=5, y=10; cout<<"x="<lst:默认初始化;T类型元素的空列表。 initializer_listlst{a,b,c…}:lst的元素数量和初始值一样多;lst的元素是对应 初始值的副本;列表中的元素是const。 lst2(lst),lst2=lst:复制或者赋值一个initializer_list对象但不复制列表中的元素;复 制后原始列表和副本共享元素。 lst.size():列表中的元素数量。 lst.begin():返回指向lst首元素的指针。 lst.end():返回指向lst尾元素下一位置的指针。 initializer_list是一个类模板,模板相关内容会在第9章详细介绍,在这里先试着使 用它。使 用模板时,需要在模板名字后面跟一对尖括号,括号内给出类型参数。例如: initializer_list ls; //initializer_list 的元素类型是string initializer_listli; //initializer_list 的元素类型是int 第3章 函 数·81· initializer_list比较特殊的一点是,其对象中的元素永远是常量值,人们无法改变 initializer_list对象中元素的值。 接下来使用initializer_list编写一个错误信息输出函数,使其可以作用于可变数量的 形参: void print_err(initializer_listlst) { for (auto beg=lst.begin(); beg !=lst.end();++beg) cout<<*beg<<‘'; cout<lst, int error_code); //第二个形参为int 类型 3.2 内联函数 在本章的开头提到,使用函数有利于代码重用,可以提高开发效率、增强程序的可靠性, 也便于分工合作,便于修改维护。但是,函数调用也会降低程序的执行效率,增加时间和空 间方面的开销。因此,对于一些功能简单、规模较小又使用频繁的函数,可以设计为内联函 数。内联函数不是在调用时发生控制转移,而是在编译时将函数体嵌入在每一个调用处。 这样就节省了参数传递、控制转移等开销。 内联函数在定义与普通函数的定义方式几乎一样,只是需要使用关键字inline,其语法 形式如下: inline 类型说明符 函数名(含类型说明的形参表) { 语句序列 } 需要注意的是,inline关键字只是表示一个要求,编译器并不承诺将inline修饰的函数 作为内联函数。而在现代编译器中,没有用inline修饰的函数也可能被编译为内联函数。 通常内联函数应该是比较简单的函数,结构简单、语句少,调用频繁。如果将一个复杂的函 ·82· C++语言程序设计(第5版) 数定义为内联函数,反而会造成代码膨胀,增大开销。这种情况下,多数编译器都会自动将 其转换为普通函数来处理。到底什么样的函数会被认为太复杂呢? 不同的编译器处理起来 是不同的。此外,有些函数是肯定无法以内联方式处理的,例如存在对自身的直接递归调用 的函数。 例3-14 内联函数应用举例。 //3_14.cpp #include using namespace std; const double PI=3.14159265358979; //内联函数,根据圆的半径计算其面积 inline double calArea(double radius) { return PI*radius*radius; } int main() { double r=3.0; //r 是圆的半径 //调用内联函数求圆的面积,编译时此处被替换为CalArea 函数体语句, //展开为area=PI*radius*radius; double area=calArea(r); cout<