分析程序
程序遵循模块化的编程思想, 使用独立函数(模块) 来验证输入和管理显示。 程序越大, 使用模块化编程就越重要。
main()函数管理程序流, 为其他函数委派任务。 它使用 get_long()获取值、 while 循环处理值、 badlimits()函数检查值是否有效。
输入流和数字
is 28 12.4
对 C程序而言, 这是一个字节流。 第1个字节是字母i的字符编码, 第2个字节是字母s的字符编码, 第3个字节是空格字符的字符编码, 第4个字节是数字2的字符编码。如果get_long()函数处理这一行输入, 第1个字符是非数字, 那么整行输入都会被丢弃, 包括其中的数字。
42
如果在scanf()函数中使用%c转换说明, 它只会读取字符4并将其储存在char类型的变量中。 如果使用%s转换说明, 它会读取字符4和字符2这两个字符, 并将其储存在字符数组中。 如果使用%d转换说明, scanf()同样会读取两个字符, 但是随后会计算出它们对应的整数值: 4×10+2, 即42, 然后将表示该整数的二进制数储存在 int 类型的变量中。
如果使用%f 转换说明,scanf()也会读取两个字符, 计算出它们对应的数值42.0, 用内部的浮点表示法表示该值, 并将结果储存在float类型的变量中。
输入由字符组成, 但是scanf()可以把输入转换成整数值或浮点数值。 使用转换说明(如%d或%f) 限制了可接受输入的字符类型, 而getchar()和使用%c的scanf()接受所有的字符。
菜单管理
用户输入程序所列选项之一, 然后程序根据用户所选项完成任务。 作为一名程序员, 自然希望这一过程能顺利进行。 因此, 第1个目标是: 当用户遵循指令时程序顺利运行; 第2个目标是: 当用户没有遵循指令时, 程序也能顺利运行。 显而易见, 要实现第 2 个目标难度较大, 因为很难预料用户在使用程序时的所有错误情况。
现在的应用程序通常使用图形界面, 可以点击按钮、 查看对话框、 触摸图标, 而不是我们示例中的命令行模式。 但是, 两者的处理过程大致相同:给用户提供选项、 检查并执行用户的响应、 保护程序不受误操作的影响。 除了界面不同, 它们底层的程序结构也几乎相同。 但是, 使用图形界面更容易通过限制选项控制输入。
任务
一个菜单程序需要执行哪些任务。 它要获取用户的响应, 根据响应选择要执行的动作。 另外, 程序应该提供返回菜单的选项。C 的 switch 语句是根据选项决定行为的好工具, 用户的每个选择都可以对应一个特定的case标签。 使用while语句可以实现重复访问菜单的功能。
使执行更顺利
顺利运行指的是, 处理正确输入和错误输入时都能顺利运行。
get_choice()的用法和getchar()相同, 两个函数都是获取一个值, 并与终止值(该例中是'q') 作比较。
混合字符和数值输入
关键概念
C程序把输入作为传入的字节流。 getchar()函数把每个字符解释成一个字符编码。 scanf()函数以同样的方式看待输入, 但是根据转换说明, 它可以把字符输入转换成数值。 许多操作系统都提供重定向, 允许用文件代替键盘输入, 用文件代替显示器输出。
程序通常接受特殊形式的输入。对于一个小型程序, 输入验证可能是代码中最复杂的部分。
本章小结
许多程序使用 getchar()逐字符读取输入。 通常, 系统使用行缓冲输入,即当用户按下 Enter 键后输入才被传送给程序。 按下Enter键也传送了一个换行符, 编程时要注意处理这个换行符。 ANSI C把缓冲输入作为标准。
通过标准I/O包中的一系列函数, 以统一的方式处理不同系统中的不同文件形式, 是C语言的特性之一。 getchar()和 scanf()函数也属于这一系列。当检测到文件结尾时, 这两个函数都返回 EOF(被定义在stdio.h头文件中) 。 在不同系统中模拟文件结尾条件的方式稍有不同。
许多操作系统(包括UNIX和DOS) 都有重定向的特性, 因此可以用文件代替键盘和屏幕进行输入和输出。 读到EOF即停止读取的程序可用于键盘输入和模拟文件结尾信号, 或者用于重定向文件。
混合使用 getchar()和 scanf()时, 如果在调用 getchar()之前, scanf()在输入行留下一个换行符, 会导致一些问题。
函数
函数(function) 是完成特定任务的独立程序代码单元。 语法规则定义了函数的结构和使用方式。 虽然C中的函数和其他语言中的函数、 子程序、 过程作用相同, 但是细节上略有不同。
使用函数可以省去编写重复代码的苦差。 如果程序要多次完成某项任务, 那么只需编写一个合适的函数, 就可以在需要时使用这个函数, 或者在不同的程序中使用该函数, 就像许多程序中使用putchar()一样。函数让程序更加模块化, 从而提高了程序代码的可读性, 更方便后期修改、 完善。
还要编写4个函数readlist()、 sort()、 average()和bargraph()的实现细节。 描述性的函数名能清楚地表达函数的用途和组织结构。
创建并使用简单函数
分析程序
程序在3处使用了starbar标识符: 函数原型(function prototype) 告诉编译器函数starbar()的类型; 函数调用(function call) 表明在此处执行函数;函数定义(function definition)。
函数和变量一样, 有多种类型。 任何程序在使用函数之前都要声明该函数的类型。
void starbar(void);
圆括号表明starbar是一个函数名。 第1个void是函数类型, void类型表明函数没有返回值。 第2个void(在圆括号中) 表明该函数不带参数。 分号表明这是在声明函数, 不是定义函数。 也就是说, 这行声明了程序将使用一个名为starbar()、 没有返回值、 没有参数的函数, 并告诉编译器在别处查找该函数的定义。
void starbar();
一般而言, 函数原型指明了函数的返回值类型和函数接受的参数类型。
这些信息称为该函数的签名(signature) 。 对于starbar()函数而言, 其签名是该函数没有返回值, 没有参数。
starbar();
程序中strarbar()和main()的定义形式相同。 首先函数头包括函数类型、函数名和圆括号, 接着是左花括号、 变量声明、 函数表达式语句, 最后以右花括号结束 。 注意, 函数头中的starbar()后面没有分号, 告诉编译器这是定义starbar(), 而不是调用函数或声明函数原型。
程序把 starbar()和 main()放在一个文件中。把函数都放在一个文件中的单文件形式比较容易编译, 而使用多个文件方便在不同的程序中使用同一个函数。 如果把函数放在一个单独的文件中, 要把#define 和#include 指令也放入该文件。
starbar()函数中的变量count是局部变量(local variable) , 意思是该变量只属于starbar()函数。 可以在程序中的其他地方(包括main()中) 使用count, 这不会引起名称冲突, 它们是同名的不同变量。
如果把starbar()看作是一个黑盒, 那么它的行为是打印一行星号。 不用给该函数提供任何输入, 因为调用它不需要其他信息。 而且, 它没有返回值, 所以也不给 main()提供(或返回) 任何信息。 简而言之, starbar()不需要与主调函数通信。
函数参数
show_n_char()(显示一个字符n次) 。 唯一要改变的是使用内置的值来显示字符和重复的次数, show_n_char()将使用函数参数来传递这些值。
show_n_char()与starbar()很相似, 但是show_n_char()带有参数。
定义带形式参数的函数
void show_n_char(char ch, int num)
该行告知编译器show_n_char()使用两个参数ch和num, ch是char类型,num是int类型。 这两个变量被称为形式参数(formal argument, 但是最近的标准推荐使用formal parameter) , 简称形参。 和定义在函数中变量一样, 形式参数也是局部变量, 属该函数私有。 这意味着在其他函数中使用同名变量不会引起名称冲突。 每次调用函数, 就会给这些变量赋值。
声明带形式参数函数的原型
void show_n_char(char ch, int num);
当函数接受参数时, 函数原型用逗号分隔的列表指明参数的数量和类型。
void show_n_char(char, int);
在原型中使用变量名并没有实际创建变量, char仅代表了一个char类型的变量,
调用带实际参数的函数
在函数调用中, 实际参数(actual argument, 简称实参) 提供了ch和num的值。
实际参数可以是常量、 变量, 或甚至是更复杂的表达式。 无论实际参数是何种形式都要被求值, 然后该值被拷贝给被调函数相应的形式参数。
实际参数是出现在函数调用圆括号中的表达式。 形式参数是函数定义的函数头中声明的变量。 调用函数时, 创建了声明为形式参数的变量并初始化为实际参数的求值结果。
使用return从函数中返回值
关键字return后面的表达式的值就是函数的返回值。该函数返回的值就是变量min的值。 因为min是int类型的变量, 所以imin()函数的类型也是int。
变量min属于imin()函数私有, 但是return语句把min的值传回了主调函数。
函数调用imin(evil1, evil2)只是把两个变量的值拷贝了一份。返回值不仅可以赋给变量, 也可以被用作表达式的一部分。
实际得到的返回值相当于把函数中指定的返回值赋给与函数类型相同的变量所得到的值。
return语句返回确实int类型的值1。 return 语句的另一个作用是, 终止函数并把控制返回给主调函数的下一条语句。
函数类型
声明函数时必须声明函数的类型。 带返回值的函数类型应该与其返回值类型相同, 而没有返回值的函数应声明为void类型。
类型声明是函数定义的一部分。函数类型指的是返回值的类型, 不是函数参数的类型。
ANSI C标准库中, 函数被分成多个系列, 每一系列都有各自的头文件。
ANSIC函数原型
问题所在
第1次调用printf()时省略了imax()的一个参数, 第2次调用printf()时用两个浮点参数而不是整数参数。
主调函数把它的参数储存在被称为栈(stack) 的临时存储区, 被调函数从栈中读取这些参数。主调函数根据函数调用中的实际参数决定传递的类型, 而被调函数根据它的形式参数读取值。
ANSI的解决方案
针对参数不匹配的问题, ANSI C标准要求在函数声明时还要声明变量的类型, 即使用函数原型(function prototype) 来声明函数的返回类型、 参数的数量和每个参数的类型。
无参数和未指定参数
void print_name();
一个支持ANSI C的编译器会假定用户没有用函数原型来声明函数, 它将不会检查参数。
void print_name(void);
支持ANSI C的编译器解释为print_name()不接受任何参数。 然后在调用该函数时, 编译器会检查以确保没有使用参数。
一些函数接受(如, printf()和scanf()) 许多参数。
函数原型的优点
函数原型是C语言的一个强有力的工具, 它让编译器捕获在使用函数时可能出现的许多错误或疏漏。
有一种方法可以省略函数原型却保留函数原型的优点。之所以使用函数原型, 是为了让编译器在第1次执行到该函数之前就知道如何使用它。 因此, 把整个函数定义放在第1次调用该函数之前, 也有相同的效果。 此时, 函数定义也相当于函数原型。
// 下面这行代码既是函数定义, 也是函数原型
int imax(int a, int b) { return a > b ? a : b; }
int main()
{ int
x, z;
...
z = imax(x, 50);
...
}