← 学习库 C++程序设计教程 本册目录

第9章 函数

原书第 159 页

第9章 可灵活方便地互换套用的一段程序——函数

看书要用到人的眼睛、大脑、手等器官和肢体,跑步也要用到人的眼睛、大脑、手等器官和肢体,还要用到腿、脚等肢体,在日常生活中,有许多事物是要被重复利用的,是具有通用性和普遍性的。所以在解决问题时也同样会遇到共性的处理方法和过程,那么,用 C++ 语言去处理这些共性的问题是不是也要重复地编写程序呢?当然不用,C++ 很聪明,它把处理这些共性问题的程序编写成了一个个函数,即把一个大家庭(只有主函数 main 的单函数)进行分家,分为了多个函数(必须有主函数 main,且带着 1 个或 1 个以上的子函数)。例如,针对人的眼睛、大脑和手分别编写出 3 个函数,当看书或跑步时,调用这 3 个函数,再加上其他函数,就可以编写出看书的程序和跑步的程序了。这就节省了编写程序的时间、人力及物力等资源,而且程序非常清晰。函数是可以灵活方便地互换和套用的程序段,它使得整个程序的可读性、可维护性、可移植性都很好。另外,C++ 考虑得还很周到,预先编写了许多 C++ 的标准函数,在编写程序时,不论是 C++ 的标准函数,还是自己编写的函数都可以用。

其实,这章的景点并不新颖,单函数读者已经会编写了,本章只需掌握把大家庭(单函数)分为多个家庭(多函数)的方法即可。

9.1 计算长方形面积和半圆形面积的多函数的例题详解

【例 9-1】有一个房地产销售商,在销售房屋时要计算面积,再算出房价,该房地产商销售的房间大多数是长方形的,阳台是长方形再加半圆形,请编写一个计算长方形面积和半圆形面积的程序,以帮助该房地产商进行销售运作。

编程时,请注意如下几点。

1)将计算长方形面积和半圆形面积的程序编写成两个子函数。

2)主函数和子函数构成一个程序。

3)该程序的功能:在主函数中输入长方形的长、宽和半径,调用各子函数算出面积,并返回面积值到主函数,在主函数中打印输出返回的面积值。此过程循环进行,直到输入的长度为0时,程序就结束了。

这样,房地产商将此程序移植并稍加修改,就可以实际应用于求房间面积了。

1. 用心分析人想通,机随人行写提纲

本题的要求是编写循环求出长方形面积和半圆形面积的程序,人的做法如图 9-1 所示。

2. 对号入座写文章,头脑运行改文章

因为求长方形面积和半圆形面积的程序段是很常用,所以题目要求把它们编写成子函数,这主要是为加强程序的可读性、可移植性和可维护性而考虑的。

原书第 160 页
Image
图 9-1 求长方形面积和半圆形面积的流程图

有了子函数,即有了儿子,谁是父亲,自然就是主函数了。前面编写的程序都是主函数,即只有父亲,现在有了儿子,父亲就可以将一些事情安排给儿子做,例如,此题叫儿子计算长方形面积和半圆形面积,做完事情后要回家(回到主函数)向父亲汇报(返回计算结果给主函数),父亲(主函数)就打开结果看(显示结果),看完后又要求儿子再去做这些事,并给出了新的条件(新的长度、宽度和半径),如此循环往复,直到父亲说,可以休息了,程序就结束了。

此题的执行过程是:

主函数 $ \rightarrow $ 调求长方形面积的子函数 $ \rightarrow $ 返回主函数 $ \rightarrow $ 显示长方形面积值 $ \rightarrow $ 调求半圆形面积的子函数 $ \rightarrow $ 返回主函数 $ \rightarrow $ 显示半圆形面积值 $ \rightarrow $ 输入长、宽和半径 $ \rightarrow $ 程序结束否? $ \downarrow $ 结束

根据流程图和上述分析,编写程序如下:

//ex9-1.cpp
#include<iostream>//名字空间 std 包含了此头文件,所以此处不加.h,
//如果不使用名字空间 std(即没有下一句),就要加.h
using namespace std;//使用名字空间 std
double rectangle(double l1,double w1);//声明求长方形面积的子函数
double circular(double r1);//声明求半圆形面积的子函数

const double pi=3.1415926;//声明圆周率 pi 为全局常量

void main()
{
double l,w,r;//定义长度 l、宽度 w 和半径 r,三者均为局部变量
cout<<"Please input l,w and r"<<endl;
cin>>l;
原书第 161 页
cin>>w;
cin>>r;
while(l!=0)
{
    cout<<"the rectangle area is:"<<rectangle(l,w)<<endl;
    //rectangle(l,w)是带着长和宽两个实参调用求长方形面积的子函数,
//其返回面积值到此处并被显示出来,但没有传给任何变量,即没保存下来
cout<<"the half circular area is:"<<circular(r)<<endl;
//circular(r)是带着半径一个实参调用求半圆形面积的子函数,
//其返回面积值到此处并被显示出来,但没有传给任何变量
cout<<"Please input l,w and r"<<endl;
cin>>l;
cin>>w;
cin>>r;
}

double rectangle(double l1,double w1)//定义求长方形面积的子函数
{
    double V1;//声明长方形面积 V1,V1 为局部变量
    V1=l1*w1;//计算长方形面积
    return V1;//返回长方形面积值给主函数
}

double circular(double r1)//定义求半圆形面积的子函数
{
    double V2;//解释与上述类似
    V2=pi*r1*r1/2;
    return V2;
}

程序分析:

(1)定义子函数的方法(以定义求长方形面积的子函数为例)

这里,通过编程三部曲来介绍定义子函数的方法。

首先,要算长方形的面积必然要知道其长和宽,那么长和宽从何而来?即输入这部曲如何唱?根据题意可知,应该通过主函数传递参数的方式来获取长和宽。形参声明了两个变量,均是 double 型,即数据类型就是 double,形参名是 l1 和 w1。

处理这部曲就是计算长方形面积,通过表达式计算即可。

到了输出这部曲又怎么唱?根据题意,应将曲子(结果)送回给父亲(主函数)去唱(显示),则有了“return V1;”这一句,V1的数据类型也是double。

该程序采用了子函数声明和定义分开写的方式。

(2) 声明句的写法

(3) 调用句的写法

rectangle(1,w)就是调用计算长方形面积子函数的调用句,与以前用过的调用 C++标准函

原书第 162 页

数的方法一样。注意:不要把数据类型写在调用句中。

3. C++帮助且裁判,人定胜机凯歌扬

运行结果:

Please input l,w and r

111

the rectangle area is:1

the half circular area is:1.5708

Please input l,w and r

2352

the rectangle area is:115

the half circular area is:6.28319

Please input l,w and r

073

结果正确。

9.2 勤奋实践,总结评分

9.2.1 习题

将第8章的第1~10题用多函数的方式编写程序。

9.2.2 思考题
  1. 编写程序,打印输出 100!(100 的阶乘)。

2.在主函数中输入两个整数,然后分别调用求最大公约数的子函数和求最小公倍数的子函数,并在主函数中输出结果,试编程实现。

9.2.3 总结评分,继续提高

浏览体验了例题、习题和思考题之后,说说自己的体会和收获,并认真总结,按表 9-1 给自己评判一下可以得多少分。

表9-1 总结评分表
成绩ABCD不足的原因改进的方法备注
C++知识完全理解和会用基本理解和会用肤浅理解和会用基本不理解和不会用
程序原创(独立编程)的比例(%)≥9080~8960~79<60

不论得分多少,大家都要总结经验,扬长避短,勤奋努力,不断提高。

最后浏览一下本章的第4节,可以先考考自己,然后再浏览,以便使重点知识掌握得更牢固。

原书第 163 页

9.3 函数知识

C++的标准函数那么丰富,读者又可以自己编写 C++的函数,可见其作用非常强大。那么,读者在之前的学习中编写过、使用过 C++的函数吗?答案是肯定的,例如,每个程序都必不可少地有 main 这个主函数,此外还用过 C++的标准函数,如指数函数 pow 等。C++的程序无论多么庞大,或多么简短,它都是由一个或一个以上的函数组成的,也就是说,函数是 C++的细胞、器官和肢体,由它们组成了 C++功能强大的整体——程序。

那么,C++函数的庐山真面目是什么呢?且往下观赏吧。

9.3.1 在什么情况下用到函数

凡是为了解决问题或处理事情,在编写程序时都要用到 C++ 的函数。

主函数 main 是一个程序必不可少的,且是唯一的,就像一个部队里只有一个总司令,而不能有两个以上的总司令一样。而且 C++ 程序的运行均是从 main 开始的。程序中的其余函数均称为子函数,子函数视需要而编写。前面已经学过及应用过的 C++ 标准函数均是子函数,只不过这些 C++ 标准函数是别人编写的并称为 C++ 标准函数罢了。

如果能使用 C++ 标准函数,那当然是再好不过了,就用现成的。如果没有标准函数可用,那就把解决共性问题的程序段编写成子函数来使用。

9.3.2 关于函数

1. 什么是函数

函数是由一系列的 C++ 语句构成的,用来解决、处理某个(些)问题的 C++ 程序段。

C++函数与 C++程序之间的关系是:

主函数+子函数(可有可无)→函数(一个或若干个)→程序。

2. C++函数的定义

(1)C++函数的定义格式

无论是主函数还是子函数都属于函数,都是程序,只不过是处理的事情不同而已。既然都是函数,那么它们的定义格式就应该相同,所以参照以前学的主函数的定义格式,可以写出函数的定义格式如下:

初始说明部分

函数返回的数据类型 函数名([形参数据类型 1 形参 1,...,形参数据类型 n 形参 n])

{
    编程三部曲
    [return 返回的数据:]
}

再看看以前编写的主函数的定义格式:

初始说明部分

void main() //从唯一必经的入口——main()主函数进入并运行程序
{
    //程序从此开始运行
原书第 164 页

编程三部曲 //处理问题的过程:输入—处理—输出

//程序到此运行结束

二者的定义格式一模一样。主函数的数据类型是 void,函数名是 main,没带形式参数,因为主函数返回的数据类型是空类型 void,即没有返回值,所以不写 return 语句。

另外,主函数还有一种写法(标准 C++ 的写法):

初始说明部分

int main()
{
    编程三部曲
    return 0;
}

这也与函数的定义格式一样。

(2)子函数中的输入和输出

子函数的概念大家已经有所了解了,下面来介绍一下子函数中输入和输出这两部曲的几种唱法。

1)输入的三种唱法。

唱法一:自编自唱,即子函数中的输入这部曲由自己编写(自己输入数据),就不需要别人传曲子(参数)来了,这种唱法称为无参函数。例如,例9-2中的total子函数。

唱法二:别人送曲子给你唱,即在子函数中没有输入这部曲,则需要别人传曲子(参数)进来,这种唱法称为有参函数。例如,例9-1中的两个子函数。

唱法三:用别人的曲子唱,即调用另一个子函数,在它返回时获得曲子(输入数据),这种唱法也是无参函数。例如,例9-2中的display子函数。

2)输出的三种唱法。

唱法一:自己唱,即独立自主输出结果。

唱法二:通过别人唱,将曲子(处理的结果)送给别人,让别人唱。

唱法三:别人来拿曲子唱,即不用送曲子给别人,自己也不唱,而是别人主动来拿曲子唱。例如,全局变量就不用回传结果,不管是主函数还是子函数均可以拿到曲子(结果),但一般不采用全局变量;再如,有类的数据成员,该类中的函数成员均可随意取到其值(可含有结果)来使用,如例15-1中的display子函数中的num就是某人(total子函数)编写了曲子(求出num的值),别人(display子函数)自动去拿曲子(num的值)来输出显示的。

注意:函数定义不能嵌套,即不能将函数定义放在另一个函数或自身的{}内。

(3)函数定义中的几大亮点

1)函数返回的数据类型。

① 函数若没有返回值,则此项写 void,同时不加 return 语句。

② 函数若有返回值,则此项按 return 语句返回的数据类型填写。

例如:

int a;

return a;

原书第 165 页

则此项填写 int,如例 9-1 中的两个子函数。

2)就像变量名一样,也要给函数起个名字,函数的命名法则与变量的命名法则一样。

3 )形式参数(简称形参)。

当需要外部传送数据给子函数时,就要有形参来接收这些数据,则圆括号“()”中就要有形参;否则,形参项就空着,但圆括号“()”还要保留。

当某个函数调用另一个函数时,若有形参,主调函数就会将实际参数(简称实参)传送给被调函数的形参。关于形参和实参,在后面的章节中还会继续进行讲解。

4 )一系列的 C++ 语句——函数体。

函数体就是函数中解决和处理问题的部分。下面是例9-1中的子函数,其中一对花括号内的一系列语句就是函数体。

double rectangle(double l1, double w1)//定义求长方形面积的子函数
{
    double V1;//声明长方形面积 V1,V1 为局部变量
    V1=l1*w1;//计算长方形面积
    return V1;//返回长方形面积值给主函数
}

5)初始说明部分包括预处理命令(如文件包含处理、宏定义、条件编译等)、函数的声明或定义、变量的声明或定义、常量的定义、类的声明、对象的定义等。

3. C++函数的声明

在这里,首先要区分函数的声明和定义。

函数的声明是函数的原型,要设置在调用它的函数之前,函数的声明格式是函数定义格式的第一行加“;”,如例9-1的函数声明句:

double circular(double r1);//声明求半圆形面积的子函数

还有一种简单写法,即形参名不用写出,如例9-1的声明句:

double rectangle(double l1, double w1);//声明求长方形面积的子函数

double circular(double r1);//声明求半圆形面积的子函数

可改为:

double rectangle(double, double);//声明求长方形面积的子函数

double circular(double);//声明求半圆形面积的子函数

函数的定义是函数功能的确立,可放置在所有函数之外的任何地方(函数不能嵌套)。如例9-1中定义求半圆形面积的子函数:

double circular(double r1)//定义求半圆形面积的子函数
{
    double V2;
    V2=pi*r1*r1/2;
    return V2;
}
原书第 166 页

C++函数也像变量一样要先定义,然后才能使用。声明和定义函数的方式有以下几种。

方式一:C++函数的声明与定义分开。

函数的声明语句要放在调用此函数的函数之前,一般放在main这个主函数之前。

函数的定义段可放在程序中所有函数的定义段之外的任何地方,例如,放在主函数之前,或放在主函数之后,或放在其他函数之后等。例9-1用的就是这种方式。

方式二:C++函数的声明与定义合二为一。

这种方式就是本节2中所讲的定义格式,该声明和定义要放在调用此函数的函数之前,一般放在main这个主函数之前。例9-2和9-3用的就是这种方式。

方式三:C++标准函数的声明和定义(已由C++完成了)。

包括 C++ 标准函数声明的语句格式为:

$$ \#include< 包含所用的 C++ 标准函数的头文件名 > $$

4. 怎样调用函数

该语句要放在调用 C++ 标准函数的函数之前,一般放在 main 这个主函数之前。

从例 9-1 可以看出,只有子函数 rectangle 和 circular,而没有主函数或其他调用子函数,程序是不能运行的,所以要加上主函数或调用子函数。那么主函数或其他调用子函数是怎样调用函数的呢?

调用函数的语句格式为:

函数名([实参1,实参2,...,实参n]);

即将函数声明语句中所有关于数据类型的词去掉,并以实参代替形参,就形成了调用函数的语句,实参依需要而定,可有可无。

注意:虽然函数定义不能嵌套,但函数调用可以嵌套,如例 9-2 中子函数 display 又调用了子函数 total。

5. 形式参数(形参)和实际参数(实参)

形参肯定在函数的声明语句和定义段中;实参肯定在调用函数的语句中。对应的形参与实参名称可以相同,也可以不相同,但最好不相同,如例9-1所示。

注意:形参和实参间必须做到 3 个一一对应,即个数对应相等,数据类型对应相同,位置一一对应,如下所示。

函数声明语句:函数返回类型 函数名([形参1,形参2,...,形参n]);

调用函数语句:

$$ \begin{aligned}& 函数名 ([ 实参 \;1, 实参 \;2,\ldots, 实参 \;n]);\\ \end{aligned} $$

如果形参是3个,那么实参也得是3个;如果形参的数据类型是int,那么实参的数据类型也得是int。位置一一对应是指第一个位置的实参肯定是传递给第一个位置的形参,……,第n个位置的实参肯定是传递给第n个位置的形参,如果圆的半径传递给高,而高又传递给半径,那结果肯定错误。

如例 9-1 中 l、w 这两个实参和 l1、w1 这两个形参就符合三个一一对应的原则。

切记,三个一一对应的原则必须要遵守。

原书第 167 页

6. 全局变量和全局常量、局部变量和局部常量、静态局部变量

前面所述的声明或定义的变量和常量没有根据其声明或定义所在的位置加以区分,若根据其声明或定义所在的位置和其他一些因素加以区分,则可分为全局变量和全局常量、局部变量和局部常量、静态局部变量。

1)全局变量和全局常量:定义它的位置是在所有函数的外面,一般放在主函数main之前或在头文件中。如例9-1中的“const double pi=3.1415926;”定义圆周率pi为全局常量。

2)局部变量和局部常量:声明或定义它的位置是在某函数里面,并放在使用该变量或常量的语句之前。如例 9-1 的 rectangle 子函数中 “double V1;” 声明了长方形面积 V1 为局部变量。

3)静态局部变量:声明或定义它的位置也是在某函数里面,同时前面要加上 static(表示静态),并放在使用该变量的语句之前。如例 9-2 的 total 子函数中 “static int num;” 声明了存储入场人数的静态局部变量。

全局变量(包括一般全局变量、静态全局变量)和静态局部变量一经定义,如果没有给它们赋初值,则其初值为0。

局部变量一经定义,如果没有给它们赋初值,则其初值为随机值(不确定的数)。

所以,当局部变量用于存放累加和时,要先给局部变量赋初始值 0;当局部变量用于存放累乘积时,要先给局部变量赋初始值 1。往往程序的运行结果错误就是源于没有给局部变量赋初值。

全局变量和全局常量可以被程序中的所有函数使用。一般情况下不要用全局变量,尽量采用带参数传递的函数形式。因为会有可能不知道全局变量的值被哪个函数改变了,也不知道其变为什么值了。给全局变量赋初始值也是在第一次定义全局变量时进行的,它要在整个程序运行结束时才消失。全局常量的使用可视程序需要而定。

当在程序运行中一直要保存上次运行完的数据时,可以用静态局部变量,它的使用范围是在声明或定义它的函数中,它要在整个程序运行结束时才消失。如例 9-2 和例 9-3 中的“static int num:”。

当对变量没什么特殊要求时选用局部变量或局部常量为好。因为局部变量或局部常

量的使用范围只在声明它的函数中,其值的有效性自然也只在该函数中,当函数运行结束时,局部变量或局部常量就消失了,其值也就不存在了。

【例9-2】电影院统计入场人数。

假设电影院用自动控制系统来统计每场入场人数,例如,当有人从入口处进入时,光电传感器或其他传感器就感应一次,并传递一个信号给计算机,使计数单元加1,当入场完毕,该计数单元的数字就是总入场人数。请用C++语言编写多函数程序实现此功能。

人的做法如图9-2所示。

Image
图 9-2 电影院统计入场人数的流程图
原书第 168 页

程序代码如下:

//ex9-2.cpp
#include<iostream>
using namespace std;

int total()//定义统计入场人数的子函数
{
    static int num;//声明存储入场人数的静态局部变量,
    //此处不进行初始化,即默认其初始值为0,
    //而且第二次及以后进入该子函数运行时都不再进行初始化,
    //即保留上次运行结束时的值
    char d;//声明局部变量d
    cout<<"如果有人进场看电影时,请按键1'。"
    <<"结束统计先按Enter键,再同时按下Ctrl键和z键,然后再按Enter键n";
    while((d=cin.get())!=EOF)//d接收从键盘输入的字符,
    //并与结束符EOF比较决定循环与否,
    //当结束输入时,就同时按下Ctrl键和z键,即代表结束符EOF
    if(d=='1')
        num++;  //累计入场人数
    return num;//返回入场人数给调用它的子函数display
}

void display()//定义打印子函数
{
    int num1;//声明局部变量
    num1=total();//子函数display调用子函数total,这是嵌套调用,
    //并且接收子函数total返回的入场人数
    cout<<"入场看电影的总人数是:<<num1<<endl;
}

void main()
{
    display();//调用打印子函数
}

程序的执行过程是:

主函数调用 display 子函数→display 调用 total 子函数→统计开始:有人入场看电影就按 1→直到按下结束符 EOF(Ctrl 键和 z 键)→返回入场人数 num 给 display→total 运行完,返回到子函数 display→display 的 num1 接收返回的入场人数→显示入场总人数→返回到主函数→程序结束。

运行结果:

如果有人进场看电影时,请按键'1'。结束统计先按 Enter 键,再同时按下 Ctrl 键和 z 键,然后再按 Enter 键

12343421453271

2141274210

原书第 169 页

$ ^{2} $

入场看电影的总人数是:6

结果正确。

【例9-3】电影院统计入场人数。

此题与例 9-2 的不同之处是,中途可以小计入场人数,多次循环小计,最后才输出累计的总人数。此题的关键在于设置 num 这个静态局部变量,第一次进入 total 子函数时对其初始化,第二次以后进入 total 子函数运行时,不再对其初始化,而是使用上次运行完 total 子函数时的值(即上次小计的入场人数),并继续在该值上累加入场人数,所以最后显示总人数时取的 num 值就是正确无误的。

程序代码如下:

//ex9-32.cpp
#include<iostream>
using namespace std;

int total()//统计入场人数
{
    static int num;//存储入场人数的静态局部变量,
    //注意:第二次以后进入该子函数运行时都不再进行初始化,
    //即保留上次运行结束时的值

    char d;
    cout<<"如果有人进场看电影时,请按键'1';如暂时小计入场人数请先按 k 键,"
    <<"再按 Enter 键\n";
    while((d=cin.get())!='k')//k 代表暂时结束输入的字符
        if(d=='1')
            num++;//用心观察每次进入此函数时的值
        //与上次运行完此函数时的值的特点
    return num;
}

void display()//打印输出
{
    char c1;
    int stanum=0;//存储统计次数的局部变量
    int num1;
    do
    {
        num1=total();
        stanum++;
        cout<<"第"<<stanum<<"次累计"<<"入场看电影的人数是:"<<num1<<endl;
        cout<<"您还要统计吗(y/n)?(是就按 y 键,不是就按 n 键,然后按 Enter 键)"<<endl
        cin>>c1;
    } while(c1=='y');
    cout<<"最后统计入场看电影的总人数是:"<<num1<<endl;
}
原书第 170 页
void main()
{
    display();
}

在例 9-3 中,程序第一次调用 total 子函数时,遇到静态局部变量 num 的定义语句,因为没有初始化,即没有赋初值,则默认其初始值为 0。以后再遇到此语句时,不再进行初始化,而是保留上次运行完的值。

当要小计时,按下 k 键时,num 得到了第一次小计的入场人数值,并返回给调用它的函数 display 中的 num1。

第二次调用 total 子函数时,又遇到静态局部变量 num 的定义语句,这时不再对 num 初始化,即 num 保留上次(第一次)运行返回的小计入场人数值,然后在该值的基础上又继续累计入场人数。所以依此循环,最后当 display 子函数的统计循环结束时,num 中存放的就是最终累计的入场总人数值。

本例的调试记录如下。

1)出错现象如下:

error C2018: unknown character '0xa3'

E:\C++\hc++\C++BOOK\九章\ex9-32.cpp(31): error C2018: unknown character '0xbb'

E:\C++\hc++\C++BOOK\九章\ex9-32.cpp(32): error C2146: syntax error : missing ''; before identifier 'cout'

第一个错误是:有不认识的字符 “0xa3”。

第二个错误是:有不认识的字符 “0xbb”。

第三个错误是:在标识符 “cout” 前丢失了“;”。

这些错误的产生都是因为语句 “}while((c=cin.get()!='n');” 用了中文的分号,将其改为英文的分号,这 3 个错误均被纠正。

2)出错现象为:在程序运行中出现了如下死循环。

如果有人进场看电影时,请按键'1';如暂时小计入场人数就同时按 Ctrl 和 z 键,再按 Enter 键第一858991246 次累计入场看电影的人数是:5

您还要统计吗(y/n)?(是就按 y 键,不是就按 n 键,然后按 Enter 键)

如果有人进场看电影时,请按键'1';如暂时小计入场人数就同时按 Ctrl 和 z 键,再按 Enter 键第一858991245 次累计入场看电影的人数是:5

您还要统计吗(y/n)?(是就按 y 键,不是就按 n 键,然后按 Enter 键)

首先发现“int stanum;//存储统计次数的局部变量”中没有将 stanum 初始化为 0,所以打印时,累计次数会显示为 -858991246 或 -858991245 等。将“int stanum;”改为“int stanum=0;”,同时将“int c1;”改为“char c1;”,然后再次运行程序,发现还是会出现死循环。

经过分析,将 “while((c1=cin.get())!='n');” 改为 “while((c1=cin.get())=='y');” 后,错误得以纠正。

原书第 171 页

运行结果为:

如果有人进场看电影时,请按键'1';如暂时小计入场人数就同时按 Ctrl 和 z 键,再按 Enter 键

1'21//这三行是从键盘输入的数字

12715

571151

^z

第1次累计入场看电影的人数是:7

您还要统计吗(y/n)?(是就按y键,不是就按n键,然后按Enter键)

最后统计入场看电影的总人数是:7

3)出错现象为:没有按y键或n键就结束了,不正确。

将“}while((c1=cin.get())=='y');”改为:

cin>>c1;

}while(c1=='y');

还是不正确。

再将上句中的“y”改为“n”,依然不正确。

将“while((d=cin.get())!=EOF)”改为“while((d=cin.get())!=n')”,然后运行程序,运行结果如下:

如果有人进场看电影时,请按键'1';如暂时小计入场人数就按n键,再按Enter键

4378514678001311113446616

n

第1次累计入场看电影的人数是:7

您还要统计吗(y/n)?(是就按y键,不是就按n键,然后按Enter键)

y

最后统计入场看电影的总人数是:7

由运行结果可知,还是有错误。

又将“while((d=cin.get())!='n')”改为“while((d=cin.get())!='a')”,还是有错误。经过分析,发现是将原来的“while(c1=='y');”改为了“while(c1=='n');”,所以,改回“while(c1=='y');”,同时将“while((d=cin.get())!='a')”改为“while((d=cin.get())!='k')”,然后运行程序,发现结果终于正确了。到此为止才水落石出,原来罪魁祸首就是“while((d=cin.get())!=EOF)”,因为用 EOF 是错误的,把 EOF 换成某个字符(如本例中的“k”),就正确了。

最终的运行结果为:

如果有人进场看电影时,请按键'1';如暂时小计入场人数请先按k键,再按Enter键21414275715115

174297129

k

第1次累计入场看电影的人数是:7

您还要统计吗(y/n)?(是就按y键,不是就按n键,然后按Enter键)

y

原书第 172 页

如果有人进场看电影时,请按键1';如暂时小计入场人数请先按k键,再按Enter键11183811

k

第2次累计入场看电影的人数是:12

您还要统计吗(y/n)?(是就按y键,不是就按n键,然后按Enter键)

n

最后统计入场看电影的总人数是:12

结果正确。

注意:由于对静态局部变量的初始化是在第一次调用声明它的函数时执行,之后不再进行初始化,而是一直保存上次运行完的值,所以,静态局部变量的初始化值要正确决定。

9.3.3 如何运用函数

其实,读者已经在运用函数了,只不过是单函数,只有主函数 main,而现在要运用多函数,只要将原来单函数中具有通用性的程序段变为子函数,然后运用函数的知识,调用这些子函数就可以了。下面请浏览函数运用中的各种景观。

1. 调用 C++ 标准函数的应用

如例 3-2 中调用 pow 函数:

a=pow(g,1.0/2);//调用带有两个实参g和1.0/2的C++标准指数函数,该函数将返回的计算结果传//送给a

2. 调用不带参数的子函数的应用

如例 9-2 中调用 display 函数:

display();//调用无参的打印子函数,没有返回结果

又如例 9-2 中调用 total 函数:

num1=total();//调用无参的统计人数子函数,其返回值传给 num1

3. 调用带参数的子函数的应用

(1)调用基本数据类型(如 int 等)作为参数的函数

如例 9-1 中调用 circular(r) 函数:

cout<<"the half circular area is:"<<circular(r)<<endl;
//circular(r)是带着半径一个实参调用求半圆形面积的子函数,
//其返回面积值到此处并被显示出来,但没有传给任何变量,即没保存下来

请与“num1=total();”进行比较。

(2)调用数组作为参数的函数

【例 9-4】请编写一个多函数的程序,在主函数中给一个数组赋值,显示该数组的值,然后调用排序子函数,运用选择排序法,按从小到大的顺序对数组元素进行排序,最后返回主函数,显示排序后的数组元素值(此题是将例 6-4 的程序分为多函数)。

程序代码如下:

原书第 173 页
#include<iostream.h>
const int N=10;

int *sort1(int array1[])//把排序的事情安排给子函数做,
//实参(数组 array)传递给了形参(数组 array1)

int i,j,seat,temp;
int *pal1;//因为要返回数组的值,所以设个指针 pal1

pal1=array1;//指针 pal1 指向数组 array1 的首地址

for(i=0;i<N;i++)  //对数组 array 排序,外循环负责排多少轮
{
    seat=i;
    for(j=i+1;j<N;j++) //内循环负责每一轮的排序
    {
        if(array1[j]<array1[seat]) //查找最小值
        seat=j;  //记录该次比较出的最小值的位置
    }
    if(seat!=i)
        temp=array1[i],array1[i]=array1[seat],array1[seat]=temp;//元素交换
}
return pal1;//返回指针 pal1 就是返回了数组 array1 的首地址,即返回了数组
//因为通过地址可找到数组的存放房间
}

void main()
{
    int array[N],i,j;
    int *pa;
    pa=array;
    int seat,temp;
    cout<<"请输入数组的"<<N><"个元素值"<<endl;
    for(i=0;i<N;i++) //给数组 array 的元素赋值
        cin>>array[i];
    cout<<"array 排序前的所有元素值\n";
    for(i=0;i<N;i++) //输出数组 array 排序前的所有元素值
        cout<<array[i]<="";
    pa=sort1(array);//调用数组作为实参的排序子函数,括号中的实参只写数组名即可,
        //不要加[]等;因数组元素的个数 N 声明为全局常量,所以此处不传递
        cout<<endl<<endl;
        cout<<"array 排序后的所有元素值\n";
        for(i=0;i<N;i++)  //输出数组 array 排序后的所有元素值
        cout<<array[i]<="";
    }
}

运行结果:

请输入数组的10个元素值

342

677

原书第 174 页

64

21

465

67

8

79

57

776

array 排序前的所有元素值

342 677 64 21 465 67 8 79 57 776

array 排序后的所有元素值

8 21 57 64 67 79 342 465 677 776

结果正确。

(3)调用指针作为参数的函数

【例 9-5】有一个商场,想显示 10 天内的销售金额数,主函数中有一个已初始化的 10 天销售金额数的数组,调用打印输出子函数,显示 10 天的销售金额数,请编程实现(此题是将例 7-3 的程序分为多函数)。

程序代码如下:

//ex9-5.cpp
#include<iostream.h>

void mprint(double *pm1, int num1) //打印输出子函数
{
    cout.precision(10); //设置实数的精度为10位
    for (int i=0; i<num1; i++) //输出数组 array 的所有元素值
    {
        cout << *pm1 << endl; // 把指针指向的数组 marray 的内容输出,即取内容
        pm1++; //指针向前移动一个数(即一个数组单元)
    }
}

void main()
{
    double marray[10] = {13434.45, 25322.03, 9824.48, 12566, 8205.69, 27536.05, 7932.61, 9368.04, 1498.31, 2008.11};
    //对数组初始化,即赋予10个日销售金额数
    double *pm = marray; //将双精度数组 marray 的地址赋给了双精度指针变量 pm, //一维数组就等同于一级指针,注意两者都是 double
    mprint(pm, 10); //带着双精度指针变量 pm 和 10 这两个实参,调用打印输出子函数 mprint, //指针变量写变量名即可,
    //因为数组元素的个数没有声明为全局常量,所以此处要传递数组元素的个数
}
原书第 175 页

运行结果:

13434.45

25322.03

9824.48

12566

8205.69

27536.05

7932.61

9368.04

1498.31

2008.11

结果正确。

(4) 调用结构作为参数的函数

【例 9-6】在主函数中声明一个有 n 个学生,且每个学生的数据包括学号、姓名、m 成绩的结构数组,然后调用子函数给此结构数组赋值,并将赋了值的结构数组返回主函数。主函数又调用打印子函数显示该结构数组的值。请编程实现该过程(此题是将例 8-1 的程序分为多函数)。

程序代码如下:

//ex9-6.cpp
#include<iostream.h>
#include<iomanip.h>
#define MAX 3 //自定义 MAX 代表课程的数目
#define NUM 1 //自定义 NUM 代表学生的人数
struct student //声明结构 student
{
    int id;
    char name[10];
    int score[MAX];
};

student *instu(student stu1[])//输入结构数组数据子函数

{
    student *pinstu=stu1;
    for(int i=0;i<NUM;i++)
    {
        cout<<"学号:";
        cin>>stu1[i].id;
        cout<<"姓名:";
        cin>>stu1[i].name;
        cout<<"英语 计算机 中文成绩:";
        for(int j=0;j<MAX;j++)
            cin>>stu1[i].score[j];
        }
        return pinstu;
}
原书第 176 页
void pristu(student stu2[])//打印结构数组数据子函数
{
    cout<<setiosflags(ios::left)//左对齐输出 stu 中数据的字段名栏
    <<setw(10)||<=="学号:"//每个字段宽度为 10 个字符
    <<setw(10)||<=="姓名:"
    <<setw(10)||<=="英语"
    <<setw(10)||<=="计算机"
    <<setw(10)||<=="中文成绩"||<=="endl;
for(int i=0;i<NUM;i++) //左对齐输出 stu 中的数据
{
    cout<<setw(10)||<stu2[i].id
    <<setw(10)||<stu2[i].name;
    for(int j=0;j<MAX;j++)
        cout<<setw(10)||<stu2[i].score[j];
    cout<<endl;
}
}

void main()
{
    student stu[NUM]; //定义 student 这种结构类型的数组变量 stu
    student *mstu=stu;//定义 student 这种结构类型的指针变量 mstu,且 mstu 指向 stu
    mstu=instu(stu);//带着结构数组变量 stu 这个实参,调用输入结构数组数据子函数,
    //返回的指针值赋给 mstu
    pristu(stu);//因为在上两行中 mstu 指向 stu,所以返回的指针值 mstu 指向的是赋了值的数组 stu
    //则打印的实参可用数组 stu,也可用 mstu
}

运行结果:

学号:1

姓名:李英

英语 计算机 中文成绩:78

85

90

学号:___ 姓名:___ 英语 计算机 中文成绩:___

1 李英 78 85 90

结果正确。

(5)调用引用作为参数的函数

可参考例 10-5。

(6)调用指针的引用作为参数的函数可参考例10-4。

(7)调用链表作为参数的函数

原书第 177 页

【例 9-7】本题是将链表的创建、删除和添加集成为一个多函数的程序,读者可与第8章中分开编写的链表的创建、删除和添加的程序加以比较。作者是先编写了此题,再将此题分成第8章的那几题,分开写比编写此题要麻烦得多。如果分开写是写成多函数的程序,那移植就会方便许多,读者不妨尝试一下。

程序代码如下:

//ex8-22-2.cpp
#include<iostream.h>

struct plist//声明结构名为plist的结构,即一个链圈的结构,用于链表中
{
    int num;//链圈的内容之一:职工号
    int age;//链圈的内容之二:年龄
    plist *p;//链圈的内容之三:声明链钩(链钩指针)
};

plist *set();//声明创建链表子函数

void lprint(plist *pp);
plist *del(plist *head,int num);
plist *add1(plist *head,plist *inp);

void main()
{
    plist *head1,*inp1;//定义两个指针:
        //链表的链首指针 head1 和待插入链圈的指针 inp1
    int num1;//职工号
    head1=set();//调用创建链表子函数
    lprint(head1);//调用显示链表子函数
    cout<<"请输入要删除的职工号";
    cin>>num1;
    head1=del(head1,num1);//调用删除链圈子函数
    lprint(head1);
    inp1=new plist;//新开一间待加入链圈的房间
    cout<<"请输入要添加的职工号和年龄";
    cin>>inp1->num>>inp1->age;
    head1=add1(head1,inp1);//调用插入链圈子函数
    lprint(head1);
}

plist *set()//定义创建链表子函数

{
    plist *head;//声明链表首部记号(链首指针),简称链首
    plist *pn;//声明待加入链圈的指针
    plist *pe;//声明链表尾部记号(链尾指针),简称链尾
    pn=new plist;//先拿一个待加入链圈,即开一个链圈的房间
    cout<<"循环输入待加入的职工号和年龄;若要结束输入,输入职工号为
        <<"和任一年龄,并按 Enter 键"<endl;
    cin>>pn->num>>pn->age;//给这个链圈加内容
    head=NULL;//一开始没有链条,则令链表首部记号为空
}
原书第 178 页
while(pn->num!=0)//当待加入链圈的职工号不等于0时,循环加入链圈
{
    if(head==NULL)//如果链首为空
        head=pn;//这个待加入的链圈就送给链首
    else
        pe->p=pn;//否则,链尾的链钩钩住这个待加入的链圈
    pe=pn;//这个待加入的链圈就送给链尾
    pn=new plist;//又拿下一个链圈
    cin>>pn->num>>pn->age;
}

pe->p=NULL;//链尾的链钩指向空,代表这条链到此为止

delete pn;//释放之前开的链圈的房间

return head;//返回链首
}
/*创建链表子函数的程序解释(行数从 plist *set()算起):

第3~8行如图8-6所示,第一次运行9~12、15行如图8-7所示,第二次运行13~15行如图8-9所示,第一次循环结束时如图8-8所示*/

void lprint(plist *pp)//定义显示链表子函数
{
    cout<<"职工号 "<<"年龄"<<endl;
    while(pp)
    {
        cout<<pp->num<<" "<<pp->age<<endl;
        pp=pp->p;
    }
}

plist * del(plist *head,int num)//定义删除链圈子函数

{
    plist *pd;//声明要拆链圈的指针
    plist *pf;//声明查找链圈的指针
    if(!head)//如果是空链表
    {
        cout<<"这是空链表\n";
        return head;//返回链首
    }
    if(head->num==num)//如果要删除的职工号在链首
    {
        pd=head;//将链首的链圈送给要拆链圈
        head=head->p;//将链首后面的链圈送给链首
        delete pd;//删除要拆链圈,如图8-11所示
        cout<<"删除了职工号为"<<num<<"的数据"<<endl;
        return head;//返回链首
    }
    for(pf=head;pf->p;pf=pf->p)//从链首的下一个链圈起,逐个查找,
                //找到要拆的链圈或找到链尾(pf->p=NULL)时,结束循环
        if(pf->p->num==num)//如果查找指针的链钩钩住的链圈的职工号等于要找的职工号
        {
原书第 179 页

pd=pf->p;//将下一个链圈送给要拆链圈

pf->p=pd->p;//用要拆链圈前面的链圈(即查找指针指向的链圈)的链钩钩住到链圈后面的链圈(则要拆链圈就脱钩了,即拆下了这个要拆链圈的链圈,如图8-12或图8-13所示)

delete pd;//删除要拆链圈,如图8-12或图8-13所示

cout&lt;&lt;"删除了职工号为"&&num&&"的数据"&&endl;
return head;//返回链首

cout&lt;&lt;"没有此职工。\n"&&endl;//当从头到尾都找不到时,应给予温馨的提示
return head;//记住:也要返回链首

plist * add1(plist *head,plist *inp)//定义插入链圈子函数

plist *pi;//声明查找指针

if(head==NULL)//如果是空链表
{
    cout&lt;&lt;"原来是空链表\n";
    head=inp;//这个待插入链圈就送给链首
    inp->p=NULL;//待插入链圈的链钩指向NULL,代表这条链到此为止,
    //如图8-15所示
    cout&lt;&lt;"现已插入数据\n";
    return head;//返回链首
}

if(head->num>inp->num)//如果要插入的职工号在链首
{
    inp->p=head;//链首送给待插入链圈的链钩
    head=inp;//这个待插入链圈就送给链首,如图8-16所示
    return head;//返回链首
}

for(pi=head;pi;pi=pi->p)//从链首的下一个链圈起,逐个查找,
    //找到插入位置或找到链尾(pi->p=NULL)时,结束循环
    if((pi->p==NULL))((pi->p->num>inp->num))//如果查找指针的链钩
    //已到了链尾(NULL)或钩住的下一个链圈的职工号大于要插入的职工号
    {
        inp->p=pi->p;//查找指针的链钩送给待插入指针的链钩
        pi->p=inp;//待插入链圈送给查找指针的链钩,如图8-17所示
        return head;//返回链首
}

运行结果:

循环输入待加入的职工号和年龄;若要结束输入,输入职工号为0和任一年龄,并按Enter键100

40

200

50

0

原书第 180 页

47

职工号 年龄

100 40

200 50

请输入要删除的职工号201

没有此职工。

职工号 年龄

100 40

200 50

请输入要插入的职工号和年龄99

20

职工号 年龄

99 20

100 40

200 50

结果正确。

(8)函数自己调用自己——递归函数(又称递归调用)

递归函数,即函数自己调用自己,实际上是一个循环结构,即循环地调用自己这个函数做的工作,但这个循环是有条件的,这样才能保证循环会终止,即这个递归能结束,不至于限入死循环。它相当于 while 语句的循环结构,下面就用 while 语句的流程图和设计思路来编写递归函数的程序。

【例 9-8】请编写一个程序,在主函数中输入求 $ n! $ 的 n 值,再调用递归函数 $ n! $,然后又回到主函数输出结果。

人的做法如图9-3和图9-4所示。

Image
图 9-3 求 n! 的主函数流程图

程序代码如下:

Image
图 9-4 递归子函数计算 n!的流程图
原书第 181 页
#include<iostream>
using namespace std;

using namespace std;

long doself(int n) //求 n!的递归子函数
{
    if(n==1||n==0)
        return 1;//当 n=1 或 n=0 时,满足递归的结束条件,结束递归
    return n*doself(n-1);//实参 n-1 后,再自己调用自己,
//即进行递归调用,此式的详细解释见表 9-2
}

void main()
{
    long nmul;
    int n1;
    while(n1<0)//当 n<0 时,在此输入 n,直到 n 大于等于 0 为止
    {
        cout<<"请输入求 n!的数字 n(n 大于等于 0)";
        cin>>n1;
    }
    nmul=doself(n1);
    cout<<n1<<"!="<<nmul<<endl;
}

运行结果:

请输入求 n! 的数字 n(n 大于等于 0):-4

请输入求 n! 的数字 n(n 大于等于 0):-5

请输入求 n! 的数字 n(n 大于等于 0):5

5!=120

结果正确。

递归循环的执行过程如表9-2所示。

表9-2 求5!的递归循环的执行过程
递归循环的次数nreturn n*doself(n-1)n-1
第一次55*doself(4)4
第二次45*4*doself(3)3
第三次35*4*3*doself(2)2
第四次25*4*3*2*doself(1)\n=5*4*3*2*1=120\n返回 120 给主函数1,并返回 1 给 doself 函数,即 doself(1)=1

9.4 函数的亮点

1. 在什么情况下用函数的亮点

凡是为了解决问题和处理事情,编写程序时都要用到 C++ 的函数。

原书第 182 页

主函数 main 是一个程序必不可少的,且是唯一的。

如果能使用 C++ 标准函数,那当然是再好不过了,就用现成的。如果没有标准函数可用,就把解决共性问题的程序段编写成子函数来使用。

2. 函数具体知识的亮点

1)函数是由一系列的 C++语句构成的,用来解决、处理某个(些)问题的 C++程序段。

C++函数与C++程序之间的关系是:

主函数+子函数(可有可无)→函数(一个或若干个)→程序。

2)函数的定义格式如下。

初始说明部分

函数返回的数据类型 函数名([形参数据类型1形参1,...,形参数据类型n形参n])

C++函数也像变量一样要先定义,然后才能使用。

3 )调用函数的语句格式为:

函数名([实参1,实参2,...,实参n]);

形参和实参间必须做到 3 个一一对应,即个数对应相等,数据类型对应相同,位置一一对应。

4)全局变量和全局常量:定义它的位置是在所有函数的外面,一般放在主函数 main 之前或在头文件中。局部变量和局部常量:声明或定义它的位置是在某函数里面,并放在使用该变量或常量的语句之前。静态局部变量:声明或定义它的位置也是在某函数里面,并放在使用该变量的语句之前,同时前面要加上 static。

全局变量(包括一般全局变量、静态全局变量)和静态局部变量一经定义,如果没有给它们赋初值,则其初值为0。

局部变量一经定义,如果没有给它们赋初值,则其初值为随机值(不确定的数)。

一般情况下不要用全局变量,尽量采用带参数传递的函数形式。

当在程序运行中一直要保存上次运行完的数据时,可以用静态局部变量。

当对变量没什么特殊要求时选用局部变量或局部常量为好。

3. 运用函数的亮点

按照函数知识将单函数分为多函数,其分家原则是:首先把谁(函数)要负责做的事情分给相应的谁(相应的函数),然后再按 C++ 的语句格式将做事情的工具和条件配备齐全(即书写主函数和子函数)。

← 第8章 结构第10章 引用 →