注 本章的重在理解函数以及函数之间的调用、结构等等,代码虽然简单,但是重在理解,同样希望有可能自己过一遍代码,书中所有例题文末都会给以参考。
第七章 函数与模块化程序设计
7.1 分而治之与信息隐藏
1 分而治之的方法指的是将较大的问题分解成若干给较小的、较简单的问题,并提炼出公用任务的方法。
2 模块化程序设计就体现了这种分而治之的思想,通过功能分解实现,功能分解是一个自顶向下、逐步求精的过程。
3 一个C程序可以由一个或多个源程序组成,一个源文件程序又可以由一个或多个函数组成。
4 信息隐藏也就是将程序的复杂性和实现细节隐藏起来,只对外提供必要的接口或功能,使得程序更易于理解和维护。
7.2 函数的定义
1 函数是构成程序的基本模块。程序执行从main()入口开始,出口结束,中间会调用一个又一个函数。
2 函数可分为标准库函数和自定义函数。
3 标准库函数是符号ANSI C标准的C语言编译器都会提供的库函数,使用时必需把函数所在的头文件包含进来。
4 自定义函数是自行编写来完成自己所需功能的函数,也可以分享给他人使用。
5 函数在使用前必需先定义,函数定义的基本格式为:
6 形参表是函数的入口,形参表里的形参相当于运算的操作数,函数的返回值就相当于运算的结果。
例7.1a见文末
7.3 向函数传递值和从函数返回值
1 函数必需被main()直接或间接调用才能生效,称为函数调用。
2 main()函数调用其他函数是,必需提供一个实际参数(实参)的表达式给被调用的函数。
3 调用其他函数的称为主调函数,被调用的函数称为被调函数。主调函数把实参的值复制给形参的过程,叫参数传递。
例7.1b见文末
4 函数的返回值只能有一个,类型可以是除数组外任何类型。
5 return语句可以有多个(不同条件返回不同值),但是整体只会返回一个值。
6 返回类型为void时可以省略return语句;也可写成return;
例7.1见文末
7 函数的使用必需包括定义和声明。在程序中有两种写法。
8 函数封装是将一段实现特定功能的代码块组织成一个独立单元(即函数),并隐藏其内部实现细节,仅通过接口(参数和返回值)与外部交互的过程。
9 考虑函数的健壮性可以通过在函数入口检查参数合理性的方法。
10 在程序中增加一些代码,用于专门处理某些异常情况的技术,被称为防御式编程。
例7.2~例7.4见文末
11 函数设计的基本原则(了解):(1)函数规模要小,尽量50行内;(2)函数功能要单一;(3)只有一个出口和入口,尽量不要用全局变量向函数传递信息;(4)清楚定义函数接口;(5)函数入口尽量进行有效性检查;(6)执行敏感性操作前尽量考虑可能出错的可能;(7)考虑好调用失败的处理方法;(8)调用函数时应校验函数的返回值,以及判断函数是否成功;(9)确保实参和形参类型相匹配;(10)函数没有返回值应用void声明,有返回值应确保所有控制分支都有返回值。
7.4 函数的递归调用和递归函数
1 如果一个对象部分地由它自己组成或按它自己定义,则称之是递归的,递归是根据自身来定义或求解问题的编程技术。
例7.5见文末
2 递归函数必需包含一般情况(自身定义的与原始问题类似的更小规模的子问题)和基线情况(结束递归调用过程的条件)两部分。
3 在函数内直接或间接地自己调用自己的函数调用,就称为递归调用,对应的函数称为递归函数。
例7.6见文末
7.5 变量的作用域和生存期
1 语句块是程序中被花括号括起来的部分,函数体、分支语句和循环语句都是语句块。
2 变量的作用域指的是每个变量仅在定义它的语句块(包含下级语句块)内有效,并拥有自己的存储空间。
3 不在任何语句块内定义的变量,称为全局变量,其作用域是整个程序(与main()平行的位置不在任何语句块下),相反,在除整个程序以外的其他语句块内定义的变量是局部变量。
例7.7和例7.8见文末
4 在自己定义的函数中,全局变量破坏了函数的封装性,因此不建议使用。
5 简单变量作为函数的形参不能实现函数之间的数据交换。因为形参作为局部变量时,离开定义它们的函数,为其分配的存储空间就会被释放,因此它们的值又变为随机数,不能再作用域之外访问。
6 变量的生存期是指变量从生成到被撤销的时间段,即从分配内层到释放内存的时间段。
7变量的存储类型的一般声明方式:
存储类型 数据类型 变量名表;
8 C语言提供的存储类型有:自动变量、静态变量、外部变量和寄存器变量。
9 自动变量的标准定义:
auto 类型名 变量名;
但由于极其常用,auto可以省略不写。如果没有指定变量的类型,那么类型就缺省为auto。
10 自动变量也被称为动态局部变量;在定义时不会被初始化;退出函数后,其分配的内存立即被释放。
例7.9见文末
11 用static关键字定义的变量称为静态变量,定义格式:
static 类型名 变量名;
静态变量与程序共存亡,只有程序结束才会被释放;而自动变量与程序块共存亡。
12 外部变量是在所有函数之外定义的变量并且没有指定其存储类别。在定义点之前或者其他文件中使用时应该用关键字extern对其声明,定义格式:
extern 类型名 变量名;
外部变量生存期是整个程序的运行期,自动初始化为0。
13 寄存器变量是用寄存器存储的变量,定义格式:
register 类型名 变量名;
可以将使用频率高的变量定义为该类型,执行更快。
7.6 模块化程序设计
1 模块化程序设计是将一个大程序划分为若干独立的、可重用的模块(或组件),每个模块具有特定的功能和接口,以便于编程、测试、维护和扩展。
2 模块分解的基本原则是:高聚合、低耦合,保证没干过模块的相对独立性。
3 逐步求精是具体的抽象技术,先全局后局部、先整体后细节、先抽象后具体。
4 自底向上是先编写出基础程序段,然后再扩大、补充和升级。
5 自顶向下是先写出结构简单、清晰的主程序,再逐步用子程序实现。
6 逐步求精技术可以理解乘一种由不断的自底向上修正所补充的自顶向下的程序设计方法。
7 多个源文件构成一个项目。
8 模块分解后,每个模块都由一个扩展名为.c的源文件和一个扩展名为.h的头文件构成。main()函数所在文件称为主模块。
9 把需要共享的函数放在单独的.c文件中,把共享函数的函数原型、宏定义和全局变量声明等放在单独的.h头文件中,其他需要共享这个函数的程序用#include包含这个头文件后就可以调用。
10 标准库函数的头文件用尖括号包含,而用户自定义的头文件用双引号包含,如#include<stdio.h>和#include”myself.h”。
11 头文件里对全局变量的声明要加上extern关键字,来声明该变量为外部变量,表示该变量是要再其他模块定义的变量。
12 编译器不为变量声明分配内存。
例7.11见文末
13 断言可以再调试程序的过程中发现错误,还不影响程序执行的效率,断言仅能用于调试程序,而不作为程序的功能。
14 assert()函数的功能是在程序的调试阶段验证程序中某个表达式的真与假。
15 条件编译可以解决头文件多重包含的问题,比如头文件的格式为:
#ifndef _GuessNumber _H_
#define _GuessNumber _H_
……
#endif
代码:
7.1 a
用函数编写计算整数n的阶乘n!
// 7.1a 用函数编写计算整数n的阶乘n!
函数功能:用迭代法计算n!
函数返回值:整型变量n表示阶乘的阶数
函数返回值:返回n!的值
long Fact(int n)//函数的定义,返回值是long 型,形参是int型
{
int i;
long result=1;
for(i=2;i<=n;i++)
{
result*=i;//等价于result=result*i
}
return result;//把result的值返回,result也是long型
}
7.1b
编写main()函数,调用函数Fact()来计算m!,其中m的值由用户从键盘输入。
//7.1b编写main()函数,调用函数Fact()来计算m!,其中m的值由用户从键盘输入。
#include<stdio.h>
int main(void)
{
int m;
long ret;
printf("Input a:");
scanf("%d",&m);
ret=Fact(m);//m是int型实参,调用函数后用ret来接收返回值
printf("%d!=%ld\n",m,ret);
return 0;
}
7.1 写法一
将7.1a和7.1b合成一个完整的程序,写法一
//例7.1 将7.1a和7.1b合成一个完整的程序,写法一
#include<stdio.h>
long Fact(int n)//位置在预编译命令后,并且包含了函数的定义和声明
{
int i;
long result=1;
for(i=2;i<=n;i++)
{
result*=i;
}
return result;//把result的值返回,result也是long型
}
int main(void)//此时再写main函数
{
int m;
long ret;
printf("Input a:");
scanf("%d",&m);
ret=Fact(m);//m是int型实参,调用函数后用ret来接收返回值
printf("%d!=%ld\n",m,ret);
return 0;
}
7.1 写法二
将7.1a和7.1b合成一个完整的程序,写法二
//例7.1 将7.1a和7.1b合成一个完整的程序,写法二
#include<stdio.h>
long Fact(int n);//先进行声明,这样能保证,main函数的内容更加清晰
int main(void)
{
int m;
long ret;
printf("Input a:");
scanf("%d",&m);
ret=Fact(m);//m是int型实参,调用函数后用ret来接收返回值
printf("%d!=%ld\n",m,ret);
return 0;
}
long Fact(int n)//函数的定义,放在main函数结束后
{
int i;
long result=1;
for(i=2;i<=n;i++)
{
result*=i;
}
return result;//把result的值返回,result也是long型
}
7.2
在例7.1的基础上,增加对函数入口参数合法性的检查
//例7.2在例7.1的基础上,增加对函数入口参数合法性的检查
#include<stdio.h>
long Fact(int n);//常采用这种写法
int main(void)
{
int m;
long ret;
printf("Input a:");
scanf("%d",&m);
ret=Fact(m);//m是int型实参,调用函数后用ret来接收返回值
if(ret==-1)//结合Fact函数,如果出错就会返回-1,因此这是对函数返回值的检查
printf("Input data error!\n");
else
printf("%d!=%ld\n",m,ret);
return 0;
}
long Fact(int n)
{
int i;
long result=1;
if(n<0)//增加函数入口参数合法性检查,要求n大于0
{
return -1;//出错就返回-1
}
else
{
for(i=2;i<=n;i++)
{
result*=i;
}
return result;//这个return应该放在else里,表示这种情况下返回的值
}
}
7.3
在例7.1的基础上,加对函数入口参数合法性的检查,和对函数返回值的检查
//例7.3 在例7.1的基础上,加对函数入口参数合法性的检查,和对函数返回值的检查
#include<stdio.h>
unsigned long Fact(unsigned int n);//定义返回值为无符号长整型 ,形参为无符号整型
int main(void)
{
int m;
do
{
printf("Input m(m>0):");
scanf("%d",&m);
}while(m<0);//当m<0时就要一直提醒用户输入
printf("%d!=%lu\n",m,Fact(m)); //可以直接把函数调用放在输出函数里,注意这时候对应输出是lu型
return 0;
}
unsigned long Fact(unsigned int n)
{
unsigned int i;
unsigned long result=1;
for(i=2;i<=n;i++)
result*=i;//只有一条语句,可以不用花括号
return result;
}
7.4
编写计算组合数的程序
// 例7.4编写计算组合数的程序
#include<stdio.h>
unsigned long Fact(unsigned int n);
int main(void)
{
int m,k;
unsigned long p;
do
{
printf("Input m,k(m>=k>0):");
scanf("%d,%d",&m,&k);
}while(m<k||m<=0||k<0);//只要这三个条件其中一个为真,则不满足m,k的条件,需要程序输入
p=Fact(m)/(Fact(k)*Fact(m-k));//组和数计算公式
printf("p=%lu\n",p);
return 0;
}
unsigned long Fact(unsigned int n)//应用前面编写的函数
{
unsigned int i;
unsigned long result=1;
for(i=2;i<=n;i++)
result*=i;
return result;
}
7.5
用递归方法计算整数n的阶乘n!
//例7.5 用递归方法计算整数n的阶乘n!
#include<stdio.h>
long Fact(int n);
int main(void)
{
int n;
long result;
printf("Input n:");
scanf("%d",&n);
result=Fact(n);//调用递归函数
if(result==-1)//处理非法数据
printf("n<0,data error!\n");
else
printf("%d!=%ld\n",n,result);
return 0;
}
long Fact(int n)
{
if(n<0)
return -1;//方法数据
else if(n==0||n==1)//基线情况
return 1;
else
return (n*Fact(n-1));//一般情况就不断递归,直到遇到基线情况
}
7.6
用递归方法计算Fibonacci数列
// 例7.6 用递归方法计算Fibonacci数列
#include<stdio.h>
long Fib(int n);
int main(void)
{
int n,i,x;
printf("Input n:");
scanf("%d",&n);
for(i=1;i<=n;i++)//用循环能打印出每一项
{
x=Fib(i);//调用递归函数计算第n项
printf("Fib(%d)=%d\n",i,x);
}
return 0;
}
long Fib(int n)
{
if(n==1) return 1;//基线情况
else if(n==2) return 1;//基线情况
else return(Fib(n-1)+Fib(n-2));//一般情况
}
7.7
在例7.6的基础上。同时打印出计算每一项所需的递归调用次数
// 例7.7在例7.6的基础上。同时打印出计算每一项所需的递归调用次数
#include<stdio.h>
long Fib(int n);
int count;//全局变量,用于统计函数调用次数
int main(void)
{
int n,i,x;
printf("Input n:");
scanf("%d",&n);
for(i=1;i<=n;i++)//用循环能打印出每一项
{
count=0;//每次计算时应该归零
x=Fib(i);//调用递归函数计算第n项
printf("Fib(%d)=%d,counter=%d\n",i,x,count);
}
return 0;
}
long Fib(int n)
{
count++;//调用一次这个函数,count值就加1
if(n==0) return 1;//基线情况
else if(n==1) return 1;//基线情况
else return(Fib(n-1)+Fib(n-2));//一般情况
}
7.8
分析下列程序是否能实现两数互换(答案是不能)
//例7.8 分析下列程序是否能实现两数互换(答案是不能)
#include<stdio.h>
void Swap(int a,int b);
int main(void)
{
int a,b;
printf("%Input a,b:");
scanf("%d,%d",&a,&b);
Swap(a,b);
printf("In main():a=%d,b=%d\n",a,b);
return 0;
}
void Swap(int a,int b){
int temp;//用来存储
temp=a;//先把a赋值给b,实现暂 存a 值,这样后面a值被覆盖也能保留a
a=b;//再把b赋值给a,此时,a的值被覆盖
b=temp;//把刚刚存在temp的a值,赋值给b,完成两数交换
printf("In Swap():a=%d,b=%d\n",a,b);
}
7.9
//例7.9 通过分析程序理解自动变量
#include<stdio.h>
long Func(int n);
int main(void)
{
int i,n;
printf("Input n:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
printf("%d!=%ld\n",i,Func(i));
}
return 0;
}
long Func(int n){
auto long p=1;//自动变量,每次函数执行完后都会释放其内存空间
p=p*n;//每次p都会变成1,因此该语句相当于1*n
return p;
}
7.10
利用静态变量计算n的阶乘
//例7.10 利用静态变量计算n的阶乘
#include<stdio.h>
long Func(int n);
int main(void)
{
int i,n;
printf("Input n:");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
printf("%d!=%ld\n",i,Func(i));
}
return 0;
}
long Func(int n){
static long p=1;//静态变量,只会在第一次为1,后面执行都不会重新变成1,而是保存现有值
p=p*n;//实现阶乘
return p;
}
7.11
利用“自顶向下,逐步求精”的方法实现猜数游戏
篇幅较长,点击链接