C语言程序设计(七) 函数
第七章 函数
分而治之:
- 把较大的任务分解成若干较小、较简单的任务,并提炼出公用任务的方法
- 函数是C语言中模块化程序设计的最小单位,既可以把每个函数都看作一个模块,也可以将若干相关的函数合并成一个模块
信息隐藏:
- 把函数内部的信息对不需要这些信息的其他模块隐藏起来,让使用者不必关注函数内部是如何做的
- 只知道它能做什么以及如何使用它即可,从而使得整个程序的结构更加紧凑,逻辑也更清晰
标准库函数:使用ANSIC的库函数,必须在程序的开头将该函数所在的头文件包含进来
自定义函数
函数在使用之前必须定义
函数名是函数的唯一标识,用于说明函数的功能
为了便于区分,通常变量名用小写字母开头的单词组合而成,函数名则用大写字母开头的单词组合而成
Windows风格:
- 函数名使用“动词”或者“动词+名词”的形式
- 变量名使用“名词”或者“形容词+名词”的形式
函数体必须用一对花括号{}包围,这里的花括号是函数体的界定符
在函数内部定义的变量只能在函数体内访问,称为内部变量
函数头部参数表里的变量,称为形式参数,也是内部变量
形参表是函数的入口
函数名相当于运算的规则,形参表里的形参相当于运算的操作数,函数的返回值就是运算的结果
若函数没有返回值,则需用void定义返回值的类型
若函数不需要入口参数,则用void代替函数头文件形参表中的内容
在函数定义的前面写上一段注释来描述函数的功能及其形参,是一个非常好的编程习惯
函数调用:有main()的程序才能运行
- 主调函数
- 被调函數
- 参数传递
//L7-1
#include <stdio.h>
long Fact(int n); /* 函数原型声明 */
main()
{
int m;
long ret;
printf("Input m:");
scanf("%d", &m);
ret = Fact(m); /* 调用函数Fact(),并将函数的返回值存入ret */
printf("%d! = %ld\n", m, ret);
}
/* 函数功能:用迭代法计算n! */
long Fact(int n) /* 函数定义 */
{
int i;
long result = 1;
for (i=2; i<=n; i++)
{
result *= i;
}
return result;
}
函数的返回值只有一个,函数的返回值的类型可以是除数组以外的任何类型
函数中的return语句可以由多个,但不表示函数可以有多个返回值
防御性程序设计:在程序中增加一些代码,专门用于处理某些异常情况的技术
程序的健壮性(Robustness):在函数的入口处增加对函数参数合法性的检查,就是一种常用的增强程序健壮性的方法
//L7-2
#include <stdio.h> long Fact(int n); int main() { int m; long ret; printf("Input m:"); scanf("%d", &m); ret = Fact(m); if (ret == -1) /* 增加对函数返回值的检验 */ printf("Input data error!\n"); else printf("%d! = %ld\n", m, ret); return 0; } /* 函数功能:用迭代法计算n! 当n>=0时,返回n!的值;否则返回-1 */ long Fact(int n) { int i; long result = 1; if (n < 0) /* 增加对函数入口参数合法性的检查 */ { return -1; } else { for (i=2; i<=n; i++) result *= i; return result; } }
实参的数量必须与形参相等,它们的类型必须匹配,匹配的原则与变量赋值的原则一致
//L7-3
#include <stdio.h> unsigned long Fact(unsigned int n); int main() { int m; do{ printf("Input m(m>0):"); scanf("%d", &m); }while (m<0); /* 增加对输入数据的限制,确保输入的数据为无符号整数 */ printf("%d! = %lu\n", m, Fact(m)); /* 无符号长整型格式输出阶乘值 */ return 0; } /*函数功能:用迭代法计算无符号整型变量n的阶乘,当n>=0时返回n!;否则返回-1 */ unsigned long Fact(unsigned int n) { unsigned int i; unsigned long result = 1; for (i=2; i<=n; i++) result *= i; return result; }
//L7-4
#include <stdio.h> unsigned long Fact(unsigned int n); int main() { 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); p = Fact(m) / (Fact(k) * Fact(m-k)); printf("p = %lu\n", p); return 0; } /* 函数功能:用迭代法计算无符号整型变量n的阶乘 */ unsigned long Fact(unsigned int n) { unsigned int i; unsigned long result = 1; for (i=2; i<=n; i++) result *= i; return result; }
函数设计的基本原则:
- 函数的规模要小
- 函数的功能要单一
- 每个函数只有一个入口和一个出口
- 在函数的接口中清楚地定义函数的行为
- 在函数的入口处,对参数的有效性进行检查
- 在执行某些敏感性操作时(除法、开方、取对数、赋值、参数传递),应检查操作数及其类型的合法性以避免发生除零、数据溢出、类型不匹配等因思维不缜密而引起的错误
- 要考虑到函数调用失败应如何处理
- 当函数需要返回值时,应确保函数中的所有控制分支都有返回值。函数没有返回值时应用void声明
递归:一个对象部分地由它自己组成或按它自己定义
递归的典型实例:字典、阶乘
//L7-5
#include <stdio.h> long Fact(int n); int main() { int n; long result; printf("Input n:"); scanf("%d", &n); result = Fact(n); /* 调用递归函数Fact()计算n! */ if(result == -1) /* 处理非法数据 */ printf("n<0, data error!\n"); else /* 输出n!值 */ printf("%d! = %ld\n", n, result); return 0; } /* 函数功能:用递归法计算n!,当n>=0时返回n!,否则返回-1 */ long Fact(int n) { long result = 1; if (n < 0) /* 处理非法数据 */ return -1; else if (n==0 || n==1) /* 基线情况,即递归终止条件 */ return 1; else /* 一般情况 */ return (n * Fact(n-1)); /* 递归调用,利用(n-1)!计算n! */ }
递归是一种可根据自身来定义或求解问题的编程技术,它是通过将问题逐步分解为与原始问题类似的更小规模的子问题来求解问题的
一个递归调用函数必须包含如下两个部分
- 一般情况
- 基线情况:递归调用的最简形式
- 递归是一种比迭代更强的循环结构
- 迭代显式地使用重复结构,而递归使用选择结构,通过重复函数调用实现重复结构
- 迭代和递归都涉及终止测试,迭代在循环条件为假时终止循环,递归则在遇到基线条件时终止递归
- 迭代不断修改循环控制变量,直到它使循环条件为假时为止,迭代则不断产生最初问题的简化副本,直到简化为递归的基线情况
- 如果循环条件测试永远为真,则迭代变成无限循环,如果递归永远无法推回到基线情况,则将变成无穷递归
//L7-6(Fibonacci数列)
#include <stdio.h>
long Fib(int a);
int main()
{
int n, i, x;
printf("Input n:");
scanf("%d",&n);
for (i=1; i<=n; i++)
{
x = Fib(i); /* 调用递归函数Fib()计算Fibonacci数列的第n项 */
printf("Fib(%d)=%d\n", i, x);
}
return 0;
}
/* 函数功能:用递归法计算Fibonacci数列中的第n项的值 */
long Fib(int n)
{
if (n == 0) return 0; /* 基线情况 */
else if (n == 1) return 1; /* 基线情况 */
else return (Fib(n-1) + Fib(n-2)); /* 一般情况 */
}
//运行结果
Input n:10
Fib(1)=1
Fib(2)=1
Fib(3)=2
Fib(4)=3
Fib(5)=5
Fib(6)=8
Fib(7)=13
Fib(8)=21
Fib(9)=34
Fib(10)=55
变量的作用域:
程序中被花括号括起来的区域叫做语句块,函数体是语句块,分支语句和循环语句也是语句块
变量的作用域规则是,每个变量仅在定义它的语句块内有效,并且拥有自己的存储空间
不在任何语句块内定义的变量称为全局变量,全局变量的作用域为整个程序,即全局变量在程序的所有位置均有效
相反,在除整个程序以外的其他语句块内定义的变量称为局部变量
全局变量从程序运行开始起就占据内存,仅在程序结束时才将其释放(将内存中的值恢复为随机值)
因此,在程序运行期间的任何时候,在程序的任何地方,都可以访问全局变量的值
//L7-7
#include <stdio.h>
long Fib(int a);
int count; /*全局变量count用于累计递归函数被调用的次数,自动初始化为0*/
int main()
{
int n, i, x;
printf("Input n:");
scanf("%d", &n);
for (i=1; i<=n; i++)
{
count = 0; /* 计算下一项Fibonacci数列时将计数器count清零 */
x = Fib(i);
printf("Fib(%d)=%d, count=%d\n", i, x, count);
}
return 0;
}
/* 函数功能:用递归法计算Fibonacci数列中的第n项的值 */
long Fib(int n)
{
count++; /* 累计递归函数被调用的次数,记录于全局变量count中 */
if (n == 0) return 0; /* 基线情况 */
else if (n == 1) return 1; /* 基线情况 */
else return (Fib(n-1) + Fib(n-2)); /* 一般情况 */
}
//运行结果
Input n:10
Fib(1)=1, count=1
Fib(2)=1, count=3
Fib(3)=2, count=5
Fib(4)=3, count=9
Fib(5)=5, count=15
Fib(6)=8, count=25
Fib(7)=13, count=41
Fib(8)=21, count=67
Fib(9)=34, count=109
Fib(10)=55, count=177
如果一个变量的类型固定,只有很有限的几个地方需要修改它的值,而且这个变量的值经常被程序中多个模块和函数使用,大多数地方只是读取它的值,而不修改它的值,那么这时就比较适合将这个变量定义为全局变量
使用全局变量使函数之间的数据交换更容易,也更高效
但由于全局变量可以在任何函数中被访问,任何函数都可以对它进行改写
所以很难确定是哪个函数在什么地方改写了它,这就给程序的调试和维护带来困难
由于全局变量破坏了函数的封装性,因此建议尽量不要使用全局变量,不得不使用时一定要严格限制,尽量不要在多个地方随意修改它的值
变量的存储类型:变量的存储类型是指编译器为变量分配内存的方式,它决定变量的生存期
存储类型
- 自动变量
- 静态变量
- 外部变量
- 寄存器变量
自动变量:
auto 类型名 变量名;
如果没有指定变量的存储类型,那么变量的存储类型就缺省为auto
自动变量在进入语句块时自动申请内存,退出语句块时自动释放内存,它仅能被语句块内的语句访问,在退出语句块后不能再进行访问
在不同的并列语句块内可以定义同名变量,不会相互干扰
因为它们各自占据着不同的内存单元,并且有着不同的作用域
//L7-8
#include <stdio.h>
void Swap(int a, int b);
int main()
{
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;
b = temp;
printf("In Swap():a = %d, b = %d\n", a, b);
}
程序每次运行进入一个语句块,就像进入了一个屏蔽层,因此同名变量不会相互干扰
函数的参数传递是单向值的传递,只能把实参的值单向传递给形参,而不能反向将形参的值传递给实参
在并列的语句块之间,只能通过一些特殊通道传递数据,如函数参数、返回值以及全局变量
传给函数形参的是函数实参值的一个副本,因此实参的值是不能在被调函数内被修改的
如果不希望形参值在函数内被修改,只要将关键词const放在形参前面,将形参值声明为常量即可
静态变量:
static 类型名 变量名;
//L7-9
#include <stdio.h>
long Func(int n);
int main()
{
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;
return p;
}
//运行结果
Input n:10
1! = 1
2! = 2
3! = 3
4! = 4
5! = 5
6! = 6
7! = 7
8! = 8
9! = 9
10! = 10
//L7-10
#include <stdio.h>
long Func(int n);
int main()
{
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; /*定义静态局部变量*/
p = p * n;
return p;
}
//运行结果 Input n:10 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800
静态变量是与程序共存亡的,而自动变量是程序块共存亡的
静态变量与全局变量都是在静态存储区分配内存的,都只分配一次存储空间,并且仅被初始化一次,都能自动初始化为0
在函数内定义的静态变量称为静态局部变量,静态局部变量只能在定义它的函数内访问
而在所有函数外定义的静态变量称为静态全局变量,静态全局变量可以在定义它的文件内的任何地方被访问
但不能像非静态的全局变量那样被程序的其他文件所访问
不同于自动变量的是,静态局部变量在退出函数后仍能保持其值到下一次进入函数时
而自动变量占据的内存在退出函数后立即被释放了,在每次调用函数时都需要重新初始化
外部变量:
extern 类型名 变量名;
如果在所有函数之外定义的变量没有指定存储类别,那么它就是一个外部变量,外部变量是全局变量,它的作用域是从它的定义点到本文件的末尾
外部变量保存在静态存储区内,在程序运行期间分配固定的存储单元,其生存期是整个程序的运行期
没有显示初始化的外部变量,由编译程序自动初始化为0
寄存器变量
register 类型名 变量名;
寄存器是CPU内部的一种容量有限但速度极快的存储器
将使用频率较高的变量声明为寄存器型,可以避免CPU对存储器的频繁数据访问,使程序更小,执行速度更快
模块化程序设计:
模块化程序设计思想最早出现在汇编语言中,在结构程序设计的概念提出以后,逐步完善并形成了模块化程序设计方法
C语言中的函数是功能相对独立的用于模块化程序设计的最小单位
在C语言中可把每个子任务设计成一个子函数,总任务由一个主函数和若干子函数组成的程序完成,主函数起着任务调度的总控作用
无论结构化方法还是面向对象方法,模块化的基本指导思想都是“信息隐藏”
模块分解的基本原则是:高聚合、低耦合,保证每个模块的相对独立性
高聚合:模块内部的联系越紧密越好,内聚性越强越好,模块的功能要相对独立和单一,让模块各司其职,每个模块只专心负责一件事情
低耦合:模块之间的联系越松散越好,模块之间仅仅交换那些为完成系统功能必须交换的信息,模块对外的接口越简单越好
模块化程序设计的好处是:
- 可以先将各个模块逐个击破,最后再将它们集成在一起完成总任务
- 便于进行单个模块的设计、开发、调试、测试和维护等工作
- 可以使得开发人员能够团队合作,按模块分配和完成子任务,实现并行开发,有利于缩短软件开发的周期
- 有利于模块的复用,使得构建一个新的软件系统时不必从零做起,直接使用已有的经过反复验证的软件库中现成的模块组装或在此基础上构建新的系统,有利于提高软件生产率和程序质量
逐步求精技术:
逐步求精技术就是按照先全局后局部、先整体后细节、先抽象后具体的过程,组织人们的思维活动
从最能反映问题体系结构的概念出发,逐步精细化、具体化,逐步补充细节,直到设计出可在机器上执行的程序
- 自底向上方法:先编写出基础程序段,然后再扩大、补充和升级
- 自顶向下方法:先写出结构简单清晰的主程序来表达整个问题,在此问题中包含的各个复杂问题用子程序来实现
逐步求精技术可以理解为是一种由不断的自底向上修正所补充的自顶向下的程序设计方法,特点是:
- 结构清晰,容易阅读,容易修改
- 可以简化程序的正确性验证
//L7-11(模块化设计实例)
#include <stdio.h> #include <time.h> #include <stdlib.h> #include <assert.h> #define MAX_NUMBER 100 #define MIN_NUMBER 1 #define MAX_TIMES 10 int MakeNumber(void); void GuessNumber(const int number); int IsValidNum(const int number); int IsRight(const int number, const int guess); int main() { int number; /* 计算机生成的随机数 */ char reply; /* 用户对于是否继续猜数的回答 */ srand(time(NULL)); /* 初始化随机种子 */ do{ number = MakeNumber(); /* 计算机生成一个随机数 */ GuessNumber(number); /* 用户猜数字 */ printf("Do you want to continue(Y/N or y/n)?"); /*提示是否继续*/ scanf(" %c", &reply); /* %c前有一个空格 */ }while (reply=='Y' || reply=='y'); /* 输入Y或y则程序继续 */ return 0; } /* 函数功能: 用户猜数字 函数参数: number是计算机生成的数 函数返回值: 无 */ void GuessNumber(const int number) { int guess; /* 用户猜的数*/ int count = 1; /* 用户猜的次数 */ int right = 0; /* 猜的结果对错与否 */ int ret; /* 记录scanf()的返回值,即读入的数据项数 */ do{ printf("Try %d:", count); ret = scanf("%d", &guess); /* 读入用户猜的数 */ /* 处理用户输入,判断是否有输入错误,是否在合法的数值范围内 */ while (ret != 1 || !IsValidNum(guess)) { printf("Input error!\n"); while (getchar() != '\n'); /* 清除输入缓冲区中的错误数据 */ printf("Try %d:", count); ret = scanf("%d", &guess); /* 读入用户猜的数 */ } count++; /* 记录用户猜的次数 */ right = IsRight(number, guess); /* 判断用户猜的数是大还是小 */ }while (!right && count <= MAX_TIMES); if (right) /* 若用户猜对了,则输出相应的提示信息 */ printf("Congratulations! You're so cool!\n"); else /* 若超过MAX_TIMES次仍未猜对,输出相应的提示信息 */ printf("Mission failed after %d attempts.\n", MAX_TIMES); } /* 函数功能: 计算机生成一个随机数 函数参数: 无 函数返回值: 返回计算机生成的随机数 */ int MakeNumber(void) { int number; number = (rand() % (MAX_NUMBER - MIN_NUMBER + 1) ) + MIN_NUMBER; assert(number >= MIN_NUMBER && number <= MAX_NUMBER); return number; } /* 函数功能: 判断用户的输入是否在合法的数值范围(1-100)之内 函数参数: number是用户输入的数 函数返回值: 若合法,则返回非0值;否则,返回0 */ int IsValidNum(const int number) { if (number >= MIN_NUMBER && number <= MAX_NUMBER) return 1; else return 0; } /* 函数功能: 判断guess和number谁大谁小 猜大了,提示"Wrong! Too high.",猜小了,提示"Wrong! Too low." 函数参数: number是被猜的数,guess是猜的数息 函数返回值:如果猜对,则返回1;否则,返回0 */ int IsRight(const int number, const int guess) { if (guess < number) /* 若猜小了,输出相应的提示信息 */ { printf("Wrong! Too small!\n"); return 0; } else if (guess > number) /* 若猜大了,输出相应的提示信息 */ { printf("Wrong! Too big!\n"); return 0; } else return 1; }
assert():断言
断言可以用于测试算法的正确性,当后面括号内的表达式为真时,它静如淑女,为假时,它宣判程序的死刑
断言仅用于调试程序,不能作为程序的功能
在以下情况下使用断言:
- 检查程序中各种假设的正确性
- 证实或测试某种不可能发生的情况确实不会发生
代码风格:
代码风格是一种习惯,养成良好的代码风格对保证程序的质量至关重要,因为很多程序错误都是由程序员的不良编程习惯引起的
代码风格包括程序的版式、标识符命名、函数接口定义、文档等内容
虽然程序的版式不会影响程序的功能,但却影响程序的可读性,它是保证代码整洁、层次清晰的主要手段
一行内只写一条语句,一行代码只定义一个变量
在定义变量的同时初始化该变量
if、for、while、do等语句各占一行,分支和循环体内的语句一律用花括号括起来
分节符{}一般都占一行,且位于同一列,同时与引用它们的语句左对齐
采用阶梯层次对应好各层次,同层次的代码放在同层次的缩进层上
一般用设置为4个空格的tab键缩进
在每个函数定义结束后加一空行,能起到使程序布局更加美观、整洁和清晰的作用
在一个函数体内,相邻的两组逻辑上紧密相关的语句块之间加空行
关键字之后加空格,以突出关键字
函数名之后不加空格,紧跟左括号
赋值、算术、关系、逻辑等运算符的前后各加一个空格
函数参数的逗号分隔符和for中分号后面加一个空格
如果代码行太长,在适当位置进行拆分
良好的注释应使用简明易懂的语言,来对程序中的特殊部分的功能和意义进行说明
既简单明了,又准确易懂,能精确地表达和清晰地展现程序的设计思想,并能揭示代码背后隐藏的重要信息
在重要的程序文件的首部对程序的功能、编程者、编程日期以及其他相关信息,加以注释说明
在用户自定义函数的前面,对函数接口加以注释说明
在一些重要的语句行的右方,如定义一些非通用的变量、函数调用、造成多重嵌套的语句块
在一些重要的语句块的上方,尤其是语义转折处
注释可长可短,但应画龙点睛
边写代码,边写注释
修改代码的同时也修改注释