C Primer Plus(第五版)9

时间:2023-12-30 19:16:08

第 9 章 函数

在本章中你将学习下列内容:

· 关键字: return (返回)

· 运算符 * (一元) & (一元)

· 函数及其定义方式。

· 参数和返回值的使用方法。

· 使用指针变量作为函数参数。

· 函数类型。

· ANSI C 原型。

· 递归。

如何组织一个程序? C 的设计原则是把函数作为程序的构成模块。前几章你使用了 printf(),scanf(),getchar(),putchar()以及 strlen() 等标准 C 库函数,本章将介绍更有效的方法,即编写你自己的函数。前几章中我们已经涉及了该内容,本章将巩固以前的知识并做进一步拓展。

9.1 函数概述

首先,什么是函数?函数(function)是用于完成特定任务的程序代码的自包含单元。尽管 C 中的函数和其他语言中的函数,子程序或子过程等扮演着相同的角色,但是在细节上会有所不同。某些函数会导致执行某些动作,比如 printf()可使数据呈现在屏幕上;还有一些函数能返回一个值以供程序使用,如 strlen()将指定字符串的长度传递给程序。一般来讲,一个函数可同时具备以上两种功能。

为什么使用函数?第一,函数的使用可以省去重复代码的编写。如果程序中需要多次使用某种特定的功能,那么只需编写一个合适的函数即可。程序可以在任何需要的地方调用该函数,并且同一个函数可以在不同的程序中调用,就像在许多程序中需要使用 putchar()函数一样。第二,即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使用程序更加模块化,从而有利于程序的阅读,修改和完善。例如,假设你想编写一个实现以下功能的程序:

· 读入一行数字。
· 对数字进行排序。
· 找到它们的平均值。
· 打印出一个柱状图。

可以编写如下程序;

#include <stdio.h>
#define SIZE 50
int main (void)
{
float list[SIZE];

readlist(list,SIZE);
sort(list,SIZE);
average(list,SIZE);
bargraph(list,SIZE);
}

当然,4个函数 readlist(),sort(),average()和 bargraph()的实现细节需要你自己编写。描述性的函数名可以清楚地表明程序的功能和组织结构,然后可以对每个函数进行独立设计直至完成需要的功能。如果这些函数足够通用化,那么还可以在其他程序中调用它们。

许多程序员喜欢把函数看作“黑盒子”,即对应一定的输入会产生特定的结果或返回某个数值,而黑盒的内部行为并不需要考虑,除非是该函数的编写者。例如使用 printf()时,只需向其传递一个控制字符串,或许还有其他的一些参数,然后就可以预测到 printf()的执行结果,而无须了解 printf()内部的代码。以这种方式看待函数有助于把精力投入到程序整体设计而不是其实现细节。因此,编写函数代码之前首先需要考虑的是函数的功能以级函数和程序整体上的关系。

对函数需要了解什么?你需要掌握如何正确定义函数,如果调用函数和如何建立函数间的通信。为了让你对这些有个清晰的思路,我们首先给出一个非常简单的例子,然后进行详细讲述。

9.11 编写和使用一个简单的函数

我们的第一个目标很容易实现,只是编写一个在一行中输出 40 个星号的函数。然后我们在一个程序中使用该函数打印一个简单的信头。程序清单 9.1 给出了完整的程序,它由 main()函数 和 starbar()函数组成。

程序清单 9.1 lethead1.c 程序
--------------------------------------------------------------
/* lethead1.c 驱动一个打印星号的函数 */

#include <stdio.h>
#define NAME "GIGATHINK,ING."
#define ADDRESS "101 Megabuck Plazd"
#define PLACE "Megapolis,CA 94904"
#define WIDTH 40

void starbar (void); // 声明函数原型

int main (void)
{
starbar();
printf ("%s\n",NAME);
printf ("%s\n",ADDRESS);
printf ("%s\n",PLACE);
starbar(); // 使用函数
return 0;
}

void starbar (void) // 定义函数
{
int count;

for (count = 1; count <=WIDTH; count++)
putchar('*');
putchar('\n');
}

程序输出如下:

****************************************
GIGATHINK,ING.
101 Megabuck Plazd
Megapolis,CA 94904
****************************************
------------------------------------------------------------------------------------------

9.1.2 程序分析

关于这个程序有以下几点需要注意:

1. starbar 标识符在不同位置被使用了 3 次:函数原型(function prototype)告知编译器 starbar()的函数类型,函数调用(function call)导致该函数的执行,而函数定义(function definition)则确切指定了该函数的具体功能。

2. 函数同变量一样有多种类型。任何程序在使用函数之前都需要声明该函数的类型。因此,下面这个 ANSI C 风格的原型出现在 main()函数的定义之前:

void starbar (void);

圆括号表明 starbar 是一个函数名。第一个 void 指的是函数类型;它的意思是该函数没有返回值。第二 void(位于圆括号内)表明该函数不接受任何参数。分号的作用是表示该语句是进行函数声明而不是函数定义。也就是说,这一行声明了程序将使用一个名为 starbar()且函数类型为 void 的函数,同时通知编译器需要在劳动保险位置找到该函数的定义。对于不识别 ANSI C 原型的编译器,只需声明函数的类型,就像下面这样:

void starbar ();

注意:一些老版本的编译器不能识别 void 类型。这时,需要把没有返回值的函数声明为 int 类型。

3. 程序把 starbar()原型置于 main()之前;也可将其置于 main()之内,可以放置变量声明的任何位置。这两种方法都正确。

4. 程序在 main()中通过使用函数名后跟圆括号和分号的格式调用函数 starbar(),语句如下:

starbar();

这是 void 类型函数的一般调用形式。当计算机执行到 starbar();语句时,它找到 starbar()函数并执行其中的指令。执行后 starbar()中的代码后,计算机返回到调用函数(calling function)的下一行继续执行。在本例中,调用函数是 main()

5. 程序中 starbar()和 main()具有相同的定义格式,即首先以类型,名称和圆括号开始,接着是开始花括号,变量声明,函数语句定义以及结束花括号。注意此处的 starbar()后没有分号,这告诉编译器你是在定义函数 starbar(),而不是在调用它或声明它的原型。

6. 程序把 starbar()和 main()包含在同一个文件中,你也可以将它们放在不同的两个文件之中,单文件形式比较容易编译,而使用两个文件则有利于在不同的程序中使用相同的函数。如果你把函数写在了另外一个单独的文件中,则在那个文件中必须加入#define 和 #include 指令。在后续内容中我们将讲述两个或多个文件的使用。就目前而言,我们将所有函数都包含在一个中。main()结束花括号告诉编译器该函数在这里结束,后面的 starbar()函数头 表示 starbar()是一个函数。

7. starbar()中的变量 count 是一个局部(local)变量。这意味着该变量只在 starbar()中可用。即使你在其他函数(包括 main()函数)中使用名称 count,也不会出现任何冲突,你将得到具有同一个名称的多个单独的,互不相关的变量。

如果把 starbar()看作是一个黑盒子,那么它的执行结果是打印出一行星号。因为不需要来自调用函数的任何信息,所以它没有输入参数。同时它不向 main()提供(返回)的任何信息,因此 starbar()也没有返回值,简言之,starbar()不需要同调用函数进行任何通信。

9.1.3 函数参数

在上例中,如果文字居中显示那么信头就会更漂亮。可以通过在打印文字之前打印一定数目的空格来达到此目的。这和 starbar()函数类似。在 starbar()中打印的是一定数量的星号,而现在要打印的是一定数目的空格。遵循 C 的设计思想,我们不应为每个任务编写一个单独的函数,而应该编写一个可以同时胜任这两个任务的更为通用的函数。新函数将命名为 show_n_char()(意思是把某字符显示 n 次)。唯一的改变是要显示的字符和显示次数将被作为参数传递给函数 show_n_char(),而不是把它们置于函数内部。

具体一点说,假如一行是 40 个字符宽。 40个星号恰好填满一行,调用函数 show_n_char(‘*’,40)可以同 starbar()一样实现该功能。而将 GIGATHINK,INC。居中需要多少个空格?因为 GIGATHINK,INC。是15个字符宽,因此在前面的例子中该短语后跟有 25 个空格。为使其趸,必须先输出 12 个空格。这样该短语两边就会分别有 13 个和 12 空格。所以,可以调用 show_n_char (‘’,12)输出 12 个空格。

除了使用参数外,在其他方面 show_n_char()函数和 starbar()非常相似。两者的一个不同之处是 show_n_char()不像 starbar()那样输出换行符,因为在同一行中可能还需要输出其他文字。程序清单 9.2 给出了改进后的程序。为了强调参数的使用,程序中使用了多种参数形式。

程序清单 9.2 lethead2.c 程序
---------------------------------------------------------------------------
/* lethead2.c */
#include <stdio.h>
#include <string.h> /* 为 strlen 提供原型 */
#define NAEM "GIGATHINK,INC. "
#define ADDRESS "101 Megabuck Plaza"
#define PLACE "Megapolis,CA 94904"
#define WIDTH 40
#define SPACE ' '

void show_n_char (char ch, int num);

int main (void)
{
int spaces;

show_n_char ('*',WHDTH); // 使用常量作为参数
putchar('\n');
show_n_char (SPACE,12); // 使用常量作为参数
printf ("%s \n",NAME);
spaces = (WIDTH - strlen(ADDRESS)) / 2; /* 让程序计算需要跳过多少空格 */

show_n_char (SPACE,spaces); /* 用一个变量作为参数 */
printf ("%s \n",ADDRESS);
show_n_char (SPACE,(WIDTH - strlen(PLACE)) /2); /* 用一个表达式作为参数 */

printf ("%s \n",PLACE);
show_n_char ('*',WIDTH);
putchar('\n');

return 0;
}

/* show_n_char () 定义 */
void show_n_char (char ch, int num)
{
int count;

for (count = 1; count <= num; count++)
putchar(ch);
}

程序的执行结果如下:

****************************************
GIGATHINK,INC.
101 Megabuck Plaza
Megapolis,CA 94904
****************************************

下面我们将详细讨论如何编写使用参数的函数,然后介绍这种函数的使用方法。

9.1.4 定义带有参数的函数:形式参量

函数定义以下面的 ANSI C 函数头开始:

void show_n_char (char ch, int num)

这行代码通知编译器 show_n_char()使用名为 ch 和 num 的两个参数,并且这两个参数的类型分别是 char 和 int 。变量 ch 和 num 被称为形式参数(formal argument)或形式参量(formal parameter,现在这个名称更为正式)。如果函数内部定义的变量一样,形式参量是局部变量,它们是函数所私有的。这意味着可以在其他函数中使用相同的变量名。每当调用函数时,这些变量就会被赋值。

注意:ANSI C 形式要求在每个变量前声明其类型。也就是说,不能像通常的变量声明那样使用变量列表来声明同一类型的变量,如下所示:

void dibs (int x,y,z) /* 不正确的函数头 */
void bubs (int x, int y, int z) /* 正确的函数头 */

ANCI C 也接受 ANSI 之前的形式,但将其视为废弃不用的形式:

void show_n_char (ch,num)
char ch;
int num;

此处圆括号内是参数名列表,而参数类型的声明在后面给出。注意参数声明需要位于标志函数体开始的花括号之前,而普通的局部变量在开始花括号之后声明。使用这种形式时,对于相同类型的变量,可以使用逗号分隔的变量名列表,如下所示:

void dibs (x,y,z)
int x,y,z; // 正确

制定标准的目的是为了淘汰 ANSI 之前的形式。为了理解以前的代码,你也需要了解 ANSI 之前的形式。但是,以后的程序中应尽量使用新的形式。

尽管函数 show_n_char()接收来自 main()的数值,但是它没有返回值。因此,show_n_char()的类型是 void 。

下面讨论函数 show_n_char () 的使用。

-------------------------------------------------------------------

9.1.5 带参数函数的原型声明

使用函数之前需要用 ANSI 原型声明该函数:

void show_n_char (char ch, int num);

当函数接受参数时,函数原型通过使用一个逗号分隔的类型列表指明了参数的个数和类型。在函数原型中可以根据你自己的喜好省略变量名:

void show_n_char ( char,int);

在原型中使用变量名并没有实际地创建变量。这只是说明 char 代表了一个 char 类型变量,类推。

ANSI C 也支持旧的函数声明形式,即圆括号内不带有任何参数:

void show_n_char ();

这种形式最终将会被从标准中删除。即没有被删除,原型形式设计比它更具有优势,正如在下文中将要讲述的那样。了解这种形式的主要原因只是为了你能正确识别并理解以前的代码。

--------------------------------------------------------------------------------

9.1.6 调用带有参数的函数:实际参数

函数调用中,通过使用实际参数(actual argument)对 ch 和 num 赋值。请考虑对 show_n_char()的第一次使用:

show_n_char (SPACE,12);

实际参数是空格字符和 12 。这两个数值被赋给 show_n_char()中相应的形式参量:变量 ch 和 num。 换句话说,形式参量是被调函数中的变量,而实际参数是调用函数分配给被调函数变量的特定数值。正如上例所示,实际参数可以是常量,变量或一个复杂的表达式。但是无论何种形式的实际参数,执行时首先要计算其值,然后将该值复制给被调函数中相应的形式参量。以最后一次使用 show_n_char()的语句为例:

show_n_char (SPACE,(WIDTH - strlen(PLACE)) / 2 );

求得构成第二个实际参数的表达式的值为 10 。然后把数值 10 赋给变量 num 。被调函数不知道也不必知道这个数值是来自于常量,变量或是更一般的表达式。再次,实际参数是赋给被称为形式参量的函数变量的具体值。因为被调函数使用的值是从调用函数中复制而来的,所以不管在被调函数中对复制数值进行什么操作,调用函数中的原数值不会受到任何影响。

--------------------------------------------------------------------

PS: 实际参数和形式参量

实际参数是函数调用时出现在圆括号中的表达式。而形式参量则是函数中在函数头部声明的变量。当一个函数被调用时,将创建被声明为形式参量的变量,然后用计算后得到的实际参数的值初始化该变量。在程序清单 9.2 中,‘*’和 WIDTH 是第一次调用 show_n_char()时的实际参数,而 SPACE 和 11 则是第二次调用该函数的实际参数。在函数定义部分,ch 和 num 是函数的形式参量。

---------------------
int main (void)
{
...
...
show_n_char(SPACE,12); /* 实际参数是 SPACE 和 12. main()把这两个值传递给 show_n_char()
... 并把它们赋给变量 ch 和 num */
}
----------------------

----------------------------------
void show_n_char (char ch,int num) /* 形式参量是声明在函数头部的两个变量 ch 和 num */
{
....
....
}
----------------------------------

------------------------------------------------------------------

9.1.7 黑盒子观点

现在我们以黑盒子的观点来考察函数 show_n_char()。输入是要显示的字符和显示次数,而执行结果是打印出指定数目的字符。输入以参数的形式传递给函数。这些信息清楚地表明了在 main()中调用这个函数的方法。同时,这也可以作为编写该函数的设计说明。

黑盒子方法的核心部分在于 ch ,num 和 count 都是 show_n_char()私有的局部变量。也就是说,如果 main()中使用相同名字的变量,它们相互独立,互不影响。例如,如果 main()中存在一个 count 变量,那么该变量值的改变不会影响 show_n_char()中的 count 变量,其余变量也是如此。黑盒子内的一切操作对调用函数来说是不可见的。

----------------------------------------------------------------------------------

9.18 使用 return 从函数中返回一个值

前面讨论了从调用函数到被调函数的通信方法。需要沿相反方向传递信息时,可以使用函数返回值。为了进一步说明,我们将构建一个比较两个参数大小并将较小数值返回的函数。因为比较的是 int 类型的数值,所以函数被命名为 imin()。同时,为了检查 imin()的执行结果,需要编写一个简单的 main()函数。这种用来测试函数的程序有时被称作驱动程序(driver)。驱动程序实际调用了被测试的函数。如果该函数成功地通过了测试,那么它就可以在一个更为重要的程序中使用。程序清单 9.3 中是驱动程序和最小值函数。

程序清单 9.3 lesser.c 程序
--------------------------------------------------------------
/* lesser.c -- 找出两个整数中的较小者 */

#include <stdio.h>

int imin (int,int);

int main (void)
{
int evil1,evil2;

printf (" Enter a pair of integers (q to quit): \n");
while (scanf (" %d %d", &evil1, &evil2) == 2)
{
printf (" The lesser of %d and %d is %d \n",evil1,evil2,imin(evil1,evil2));
printf (" Enter a pair of integers (q to quit) : \n");
}
printf (" Bye \n");
return 0;
}

int imin (int n, int m)
{
int min;
if (n < m)
min = n;
else
min = m;
return min;
}

下面是一个运行示例:

Enter a pair of integers (q to quit):
509 333
The lesser of 509 and 333 is 333
Enter a pair of integers (q to quit) :
-9393 6
The lesser of -9393 and 6 is -9393
Enter a pair of integers (q to quit) :
q
Bye

关键字 return 指明了其后的表达式的数值即是该函数的返回值。在本例中,函数返回变量 min 的数值。因为 min 的类型是 int ,所以函数 imin()的类型也是 int。

变量 min 是 imin()私有的,但是 return 语句把 min 的数值返回给了调用函数。下面这个语句的作用相当于把 min 的值赋给 lesser:

lesser = imin(n,m);

能否用下面这个语句代替上句?

imin(n,m);
lesser = min;

答案是否定的,因为调用函数并不知道 min 变量的存在。imin()中的变量是该函数的局部变量。函数调用 imin(evil1,evil2)只是复制了两个变量的数值。

返回值不仅可以被赋给一个变量,也可以被用作表达式的一部分。例如,可以使用下列语句:

answer = 2 * imin(z, zstar) + 25;
printf (" %d \n ",imin (-32 + answer,LIMIT);

返回值可以由任何表达式计算得出,而不是仅仅来自于变量。例如,可以使用以下代码来简化示例程序:

/* 最小值函数的第 2 个版本 */
imin (int n, int m)
{
return (n < m) ? n: m;
}

条件表达式的值是 n 和 m 中的较小者,并且该数值被返回给调用函数。尽管这里并不要求使用圆括号,但如果你想让程序更清晰或风格更好,可以把返回值放在圆括号内。

当函数返回值的类型和声明的类型不相同时会有什么结果呢?

int what_if (int n)
{
double z = 100.0 / (double) n;
return z; // 会有什么结果?
}

这时,实际返回值是当把指定要返回的值赋给一个具有所声明的返回类型的变量时得到的数值。因此,在本例中,执行结果相当于把 z 的数值赋给一个 int 类型的变量,然后返回该数值。例如,考虑以下的函数调用语句:

result = what_if(64);

这将把数值 1.5625 赋给变量 z 。然而,return 语句返回的则是 int 类型的数值 1.

return 语句的另一作用是终止执行函数,并把控制返回给调用函数的下一个语句。即使 return 语句不是函数的最后一个语句,其执行结果也是如此。因此,可以用下面的方式 imin()函数;

/* 最小值函数的第 3 个版本 */
imin (int n, int m)
{
if (n < m)
return n;
else
return m;
}

许多(但不是全部)C 程序员更倾向于只在函数结尾使用一次 return 语句,因为这样做更有利于阅读程序的人明白函数的执行流程。但是,在像以上的这种小函数中多次使用 return 语句并没有大错。不管怎样,对用户来说,以上 3 个版本的函数是相同的,因为输入和输出完全相同。不同的只是内部的程序语句。下面的程序也具有同样的执行结果:

/* 最小值函数的第 4 个版本 */
imin (int n, int m)
{
if (n < m)
return n;
else
return m;
printf ("Professor Fleppard is like totally a fopdoodle \n");
}

return 语句使 printf()语句永远不会执行。如果 Fleppard 教授在自己的程序中只使用该函数编译后的版本,那么他永远不会知道这个函数的学生对他的真正看法。

你也可以使用以下语句:

return;

这个语句会终止执行函数并把控制返回给调用函数。因为 return 后没有任何表达式,所以没有返回值,这种形式只能用于 void 类型的函数之中。

9.1.9 函数类型

函数应该进行类型声明。同时其类型应和返回值类型相同。而无返回值的函数应该被声明为 void 类型。在早期版本的 C 语言中,如果函数没有进行类型声明,则该函数具有默认的函数类型 int 。使用这种默认类型的原因是早期大多数 C 语言函数都是 int 类型的。但是,C99 标准不再支持函数的 int 类型的默认设置。

类型的声明是函数定义的一部分,但需要注意的是该类型指的是返回值类型,而不是函数参数类型。例如,以下的函数头表示函数使用两个 int 类型的参数而返回值类型是 double。

double klink (int a, int b)

为了正确使用函数,程序在首次调用函数之前需要知道该函数的类型。途径之一是在第一次调用之前进行完整的函数定义。但是,这种方式会使得程序难于阅读。而且,需要的函数可能在 C 库或其他文件中,因此,通常的做法是预先对函数进行声明,以便将函数的信息通知给编译器。例如,程序清单 9.3 中 main()函数包含以下几行:

#include <stdio.h>
int imin (int,int)
int main (void)
{
int evil1,evil2,lesser;

第二行代码说明 imin 是一个函数名称并且该函数返回一个 int 类型的数值。这样当在程序中调用函数 imin()时,编译器就会有相应的处理方法。

在上面的代码中函数的预先声明被放在了调用函数之外。也可以在调用函数内部预先声明被调函数。例如,程序 lesser.c 的开始部分也可以写成如下形式:

#include <stdio.h>
int main (void)
{
int imin (int,int); // imin()声明
int evil1,evil2,lesser;

在以上两种形式中,需要重点注意的是函数声明要在使用函数之前进行。

在 ANSI C 标准库中,函数被分成几个系列,每一系列都有各自的头文件。这些头文件包含了本系列函数的声明部分。例如,头文件 stdio.h 包含了标准 I/O 库函数的声明,像 printf(),scanf()。而头文件 math.h 是对各种数学函数进行声明。例如它使用以下代码通知编译函数 sqrt() 返回 double 类型的数值:

double sqrt (double);

但是不要把函数声明和函数定义混淆。函数声明只是将函数类型告诉编译器,而函数定义部分则是函数的实际实现代码。引用 math.h 头文件只向编译器说明了 sqtr()的返回值类型 double,但是 sqtr()的实现代码则位于另外一个库函数文件中。

9.2 ANSI C 的函数原型

在 ANSI C 规范之前的传统的函数声明形式是不够准确的,因为它只声明了函数的返回值类型,而没有声明其参数。下面我们看一下使用旧的函数声明形式时所产生的问题。

下面的 ANSI 之前形式的声明通知编译器 imin()返回一个 int 类型的数值:

int imin ();

然而,该语句并没有说明 imin()的参数个数和类型。因此,如果在函数 imin()中使用错误的参数类型或参数个数不对,编译器就不能发现这种错误。

--------------------------------------------------------------

9.2.1 产生的问题

下面我们讨论几个使用 imax()函数的例子,该函数和 imin()类似。在程序清单 9.4 中的程序以旧的形式声明函数 imax(),然后错误地使用该函数。

程序清单 9.4 misuse.c 程序
------------------------------------------------------------------------
/* misuse.c --- 不正确地使用函数 */

#include <stdio.h>
int imax(); /* 旧式的函数声明 */
int main (void)
{
printf (" The maximum of %d and %d is %d \n",3,5,imax(3));
printf (" The maximun of %d and %d is %d \n",3,5,imax(3.0,5.0));
return 0;
}

int imax (n,m)
int n,m;
{
int max;

if (n > m)
max = n;
else
max = m;
return max;
}

在第一个 printf()中调用 imax()时漏掉了一个参数,而在第二次调用 imax()时使用了浮点参数而不是整数参数。尽管存在这些错误,该程序仍可以编译执行。

以下是使用 Metrowerks Codewarrior Development Studio 9 时的输出

The maximum of 3 and 5 is 1245056
The maximun of 3 and 5 is 1074266112

Digital Mars 8.4 将生成数值 1245120 和 1074266112 。 使用两种编译器都可以编译通过,只不过它们因为程序没有使用函数原型而产生了错误。

程序运行时发生了些什么?因为各操作系统的内部机制不同,所以出现以上错误的具体情况也不相同。当使用 PC 或 VAX 时,程序执行过程是这样的:调用函数首先把参数放在一个称为堆栈(stack)
的临时存储区域里,然后被调函数从堆栈中读取这些参数。但是这两个过程并没有相互协调进行。调用函数根据调用过程中的实际参数类型确定需要传递的数值类型,但是被调函数是根据其形式参数的类型进行数据读取的。因此,函数调用 imax(3)把一个整数放在堆栈中。当函数 imax()开始执行时,它会从堆栈中读取两个整数。而实际上只有一个需要的数值被存储在堆栈中,所以第二个读出的数据就是当时恰好在堆栈中的其他数值。

第二次使用函数 imax()时,传递的是 float 类型的数值。这时两个 double 类型的数值就被放在堆栈中(回收一下,作为参数传递时, float 类型数据会被转换成 double 类型数据)。而在我们使用的系统中,这意味着两个 64 位的数值,即共 128 位的数据存储在堆栈中。因为这个系统中的 int 类型是 32位,所以当 imax()从堆栈中读取两个 int 类型的数值时,它会读出堆栈中前面 64 位的数据,把这些数据对应于两个整数,基本较大的一个就是 1074266112 。

--------------------------------------------------------------------------

9.2.2 ANSI 的解决方案

针对以上的参数错误匹配问题,ANSI 标准的解决方案是在函数声明中同时说明所使用的参数类型。即使用函数原型(function prototype)来声明返回值类型,参数个数以及各参数的类型。为了表示 imax()需要两个 int 类型的参数,可以使用下面原型中的任意一个进行声明:

int imax (int, int);
int imax (int a, int b);

第一种形式使用逗号对参数类型进行分隔;而第二种形式在类型后加入了变量名。需要注意的是这些变量名只是虚设的名字,它们不必和函数定义中使用的变量名相匹配。

使用这种函数原型信息,编译器就可以检查函数调用语句是否和其原型声明相一致。比如检查参数个数是否正确,参数类型是否匹配。如果有一个参数类型不匹配但都是数值类型,编译器会把实际参数值转换成和形式参数类型相同的数值。例如,会把 imac(3.0,5.0) 换成 imax(3,5)。当使用函数原型时,上例中的程序清单 9.4 会变成如下的程序清单 9.5

程序清单 9.5 proto.c 程序
--------------------------------------------------------
/* proto.c -- 使用函数原型 */

#include <stdio.h>
int imax (int,int); /* 原型 */
int main (void)
{
printf (" The maximum of %d and %d is %d \n",3,5,imax(3));
printf (" The maximum of %d and %d is %d \n",3,5,imax(3.0,5.0));
return 0;
}

int imax (int n, int m)
{
int max;

if (n < m)
max = n;
else
max = m;
return max;
}

当编译程序清单 9.5 时,编译器会给出一个错误信息,声称调用函数 imax()时传递的参数太少。

当存在类型错误时会出现什么结果呢?为了说明这一点,我们用 imax(3,5) 代替 imac(3)后重新进行编译。这一次并没有出现任何错误信息。执行程序后结果如下:

The maximum of 3 and 5 is 3
The maximum of 3 and 5 is 3

正如上文所述,第二次调用时 3.0 和 5.0 被转换成 3 和 5,因此被调函数就可以对传入的数据进行正确处理。虽然编译中没有出现错误信息,但是编译器给出了一条警告信息,提示 doulbe 类型被转换成了 int 类型的数据,因此可能会损失数据。例如,以下函数调用:

imax(3.9,5.4);
等价于语句:
imax(3, 5);

错误和警告的不同之处在于前者阻止了编译的继续进行而后者不阻止。一些编译器进行这个类型转换,但不显示警告信息,因为 C 标准并没有要求进行警告提示。不过,大多数编译器允许用户通过选择警告级别来控制编译器在显示警告时的详细程序。

-------------------------------------------------------------------------------

9.2.3 无参数和不确定参数

假设使用以下函数原型:

void print_name ();

这时一个 ANSI C 编译器会假设你没有用函数原型声明函数,因此,为了表示一个函数确实不使用参数,需要在圆括号内加入 void 关键字:

void print_name (void);

ANSI C 会把上句解释为 print_name()不接受任何参数,因此当对该函数进行调用时编译器就是检查以保证你确实没有使用参数。一些函数(比如 printf()和 scanf())使用的参数个数是变化的。例如在 printf()中,第一个参数是一个字符串,而其余参数的类型以及参数的个数并不固定。对于这种情况,ANSI C 允许使用不确定的函数原型。例如,对于 printf()可以使用下面的原型声明:
int printf (char *,...);

这种原型表示第一个参数是一个字符串(在第 11 章“字符串和字符串函数”中详细解释这一知识点),而其余的参数不能确定。

对于参数个数不确定的函数,C 库通过 stdarg.h 头文件提供了定义该类函数的标准方法。本书第 16 章 “ C 预处理器和 C 库”详细讲述了有关内容。

-------------------------------------------------------------------------

9.2.4 函数原型的优点

函数原型是对语言的有力补充。它可以使编译器发现函数使用时可能出现的错误或疏漏。而这些问题如果不被发现的话,是很难跟踪调试出来的。你可以不使用函数原型,而使用旧的函数声明形式(不说明参数的函数声明),但是这么做不仅没有任何优势反而存在许多缺点。

有一种方法可以不使用函数原型却保留函数原型的优点。之所以使用函数原型,是为了在编译器编译第一个调用函数的语句之前向其表明该函数的使用方法。因此,可以在首次调用某函数之前对该函数进行完整的定义。这样函数定义部分就和函数原型有着相同的作用。通常对较小的函数会这样做:

// 下面既是一个函数的定义,也是它的原型

int imax (int a, int b){return a < b ? a: b;}
int main ()
{
...
z = imax(x,50);
...3
}

9.3 递归

C 允许一个函数调用其本身。这种调用过程被称作递归(recursion)。递归有时很难处理,而有时却很方便实用。当一个函数调用自己时,如果编程中没有设定可以终止递归的条件检测,它会无限制地进行递归调用,所以需要进行谨慎处理。

递归一般可以代替循环语句使用。有些情况下使用循环语句比较好,而有些时候使用递归更有效。递归方法吃虽然使程序结构优美,但其执行效率却没有循环语句高。

-----------------------------------------------------------

9.3.1 递归的使用

为了具体说明,请看下面的例子。程序清单 9.6 中函数 main() 调用了函数 up_and_down()。我们把这次调用称为“第 1 级递归”。然后 up_and_down()调用其本身,这次调用叫做“第 2 级递归”。第 2 级递归调用第 3 级递归,依此类推。本例*有 4 级递归。为了深入其中看看究竟发生了什么,程序不仅显示出了变量 n 的值,还显示出了存储 n 的内存的地址 &n (本章稍后部分将更全面讨论 & 运算符。printf()函数使用 %p 说明符来指示地址)。

程序清单 9.6 recur.c 程序
----------------------------------------------------------------
/* recur.c --- 递归举例 */

#include <stdio.h>
void up_and_down (int);

int main (void)
{
up_and_down (1);
return 0;
}

void up_and_down (int n)
{
printf ("Level %d : n location %p \n",n,&n); /* 1 */指针
if (n < 4)
up_and_down (n + 1);
printf ("LEVEL %d : n location %p \n",n,&n); /* 2 */
}

输出如下:

Level 1 : n location 0012FF50
Level 2 : n location 0012FF44
Level 3 : n location 0012FF38
Level 4 : n location 0012FF2C
Level 4 : n location 0012FF2C
Level 3 : n location 0012FF38
Level 2 : n location 0012FF44
Level 1 : n location 0012FF50

我们来分析程序中递归的具体工作过程。首先 main()使用参数 1 调用了函数 up_and_down()。于是 up_and_down() 中形式参量 n 的值 为 1 ,故打印语句 1 输出了 level 1 。然后,由于 n 的数值小于 4 ,所以 up_and_down()(第 1 级)使用参数 n + 1 即数值 2 调用了 up_and_down()(第 2 级)。这使得 n 在第 2 级调用中被赋值为 2, 打印语句 1 输出的是 level 2 。与之类似,下面的两次调用分别打印出 Level 3 和 Level 4 。

当开始执行第 4 级调用时,n 的值是 4 ,因此 if 语句的条件不满足。这时不再继续调用 up_and_down()函数。第 4 级调用接着执行打印语句 2 ,即输出 LEVEL 4 ,因为 n 的值是 4 。现在函数需要执行 return 语句,此时第 4 级调用结束,把控制返回给该函数的调用函数,也就是 第 3 级调用函数。第 3 级调用函数中前一个执行过的语句是在 if 语句中进行第 4 级调用。因此,它开始继续执行其后续的代码,即执行打印语句 2 ,这将会输出 LEVLE 3 。当第 3 级调用结束后, 第 2 级调用函数开始继续执行,即输出了 LEVEL 2 。依此类推。

注意,每一级的递归都使用经自己私有的变量 n 。你可以通过想看地址的值来得出这个结论(当然,不同的系统通常会以不同的格式显示不同的地址。关键点在于,调用时的 Level1 地址和返回时的 Level1 地址是相同的)。

如果你对此感到有些迷惑,可以假想进行一系列函数调用,即使用 fun1()调用 fun2(),fun2()调用 fun3(),fun3() 调用 fun4()。 fun4()执行完后,fun3()会继续执行。而 fnu3()执行完后,开始执行 fnu2()。最后 fun2()返回到 fun1()中并执行后续代码。递归过程也是如此,只不过 fun1(),fun2(),fun3()和 fun4()是相同的函数。

----------------------------------------------------------------------------

9.3.2 递归的基本原理

刚接触递归可能会感到迷惑,下面将讲述几个基本要点以使于理解该过程。

第一,每一级的函数调用都有自己的变量。也就是说,第 1 级调用中的 n 不同于第 2 级调用中的 n,因此程序创建了 4 个独立的变量,虽然每个变量的名字都是 n ,但是它们分别具有不同的值。当程序最终返回到对 up_and_down()的第 1 级调用时,原来的 n 仍具有其初始值 1 。

第二,每一次函数调用都会有一次返回。当程序流执行到某一级递归的结尾处时,它会转移到前第 1 级递归继续执行。程序不能直接返回到 main()中的初始调用部分,而通过递归的每一级逐步返回,即从 up_and_down()某一级递归返回到调用它的那一级。

第三,递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。例如,在程序清单 9.6 中,打印语句 1 位于递归调用语句之前。它按照递归的调用顺序被执行了 4 次,即依次为 第 1 级,第 2 级,第 3 级和第 4 级。

第四,递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反。例如,打印语句 2 位于递归调用语句之后,其执行顺序是: 第 4 级,第 3 级,第 2 级和第 1 级。递归调用的这种特性在解决涉及反向顺序的编程问题时很有用。下文中将给出这样的一个例子。

第五,虽然每一级递归都有自己的变量,但是函数并不会得到复制。函数代码是一系列的计算机指令,而函数调用就是从头执行这个指令集的一条命令。一个递归调用会使程序从头执行相应函数的指令集。除了为每次调用创建变量,递归调用非常类似于一个循环语句。实际上,递归有时可被用来代替循环,反之亦然。

最后,递归函数中必须包含可以终止递归调用的语句。通常情况下,递归函数会使用一个 if 条件语句或其他类似的语句以便当函数参数达到某个特定的值时结束递归函数调用。比如在上例中, up_and_down(n)调用了 up_and_down(n+1)。最后,实际参数的值达到 4 时,条件语句 if (n<4)得不到满足,从而结束递归。

---------------------------------------------------------------------------------

9.3.3 尾递归

最简单的递归形式是把递归调用语句放在函数结尾即恰在 return 语句之前。这种形式被称作尾递归(tail recursion)或结尾递归(end recursion),因为递归调用出现在函数尾部。由于尾递归的作用相当于一条循环语句,所以它是最简单的递归形式。

下面我们讲述分别使用循环和尾递归完成阶乘计算的例子。一个整数的阶乘就是从 1 到该数的乘积。例如,3 的阶乘(写作 3!)是 1 X 2 X 3. 0! 等于 1,而且负数没有阶乘。程序清单 9.7 中,第一个函数使用 for 循环计算阶乘,而第二个函数用的是递归方法。

程序清单 9.7 factor.c 程序
----------------------------------------------------- 7! 5004 6 10!3628800
/* factor.c --- 使用循环和递归计算阶乘 */

#include <stdio.h>
long fact (int n);
long rfact (int n);
int main (void)
{
int num;

printf ("This program calculates factorials \n");
printf (" Enter a value in the range 0-12 (q to quit) : \n");
while (scanf ("%d",&num) == 1)
{
if (num < 0)
printf ("No negative numbers,Please \n");
else if (num > 12)
printf (" Keep input under 13 \n");
else
{
printf ("loop: %d factorial = %ld \n",num,fact(num));
printf ("recursion: %d factorial = %ld \n",num,rfact(num));
}
printf (" Enter a value int the range 0-12 (q to quit) : \n");
}
printf ("bye \n");
return 0;
}

long fact (int n) // 使用循环计算阶乘
{
long ans;

for (ans = 1; n > 1; n--)
ans *= n;
return ans;
}

long rfact (int n) // 使用递归计算阶乘
{
long ans;

if ( n > 0)
ans = n * rfact(n-1);
else
ans = 1;
return ans;
}

用来测试的驱动程序把输入限制在整数 0 到 12 之间。因为 12! 稍大于 5 亿,而 13! 比我们的系统中的 long 类型数据大得多,所以如果计算大于 13! 的阶乘,就必须使用范围更大的类型,比如 double 类型或 long long 类型。

下面是一个运行示例:

This program calculates factorials
Enter a value in the range 0-12 (q to quit) :
5
loop: 5 factorial = 120
recursion: 5 factorial = 120
Enter a value int the range 0-12 (q to quit) :
10
loop: 10 factorial = 3628800
recursion: 10 factorial = 3628800
Enter a value int the range 0-12 (q to quit) :
q
bye

使用循环方法的函数把 ans 初始化为 1 ,然后将其依次和从 n 到 2 依次递减的整数相乘。根据公式, ans 还应该和 1 相乘,但这并不会改变结果。

下面我们来研究使用递归方法的函数。其中关键的一点是 n! = n x (n - 1)!。 是 1 到 n -1 的所有正数之积,所以该数乘以 n 就是 n 的阶乘。这也暗示了可以采用递归的方法。调用函数 rfact()时,rfact(n)就等于 n x rfact(n - 1)。这样就可以通过调用 rfact(n-1)来计算 rfact(n),如程序清单 9.7 所示。当然,递归必须在某个地方结束,可以在 n 为 0 时把返回值设为 1 ,从而达到结束递归的目的。

在程序清单 9.7 中,两个函数的输出结果相同。虽然对 rfact()的递归调用不是函数中的最后一行,但它是在 n > 0 的情况下执行的最后一条语句,因此这也属于尾递归。

既然循环和递归都可以用来实现函数,那么究竟选择哪一个呢?一般来讲,选择循环更好一些。首先,因为每次递归调用都拥有自己的变量集合,所以就需要占用较多的内存;每次递归调用需要把新的变量集合存储在堆栈中。其次,由于进行每次函数调用需要花费一定的时间,所以递归的执行速度较慢。既然如此,那么我们为什么还要讲述以上例子呢?因为尾递归是最简单的递归形式,比较容易理解;而且在某些情况下,我们不能使用简单的循环语句代替递归,所以就有必要学习递归的方法。

--------------------------------------------------------------------------

9.3.4 递归和反向计算

下面我们考虑一个使用递归处理反序的问题(在这类问题中使用递归比使用循环更简单)。问题是这样的:编写一个函数将一个整数转换成二进制形式。二进制形式的意思是指数值以 2 为底数进行表示。例如 234 在十进制下表示为 2 x 10^1 + 3 x 10^2 + 4 x 10^0,而二进制 101 意思是
1 x 2^2 + 0 x 2^1 + 1 x 2^0 。 二进制数只使用数字 0 和 1 表示。

解决上述问题,需要使用一个算法(algorithm)。例如,怎样得到 5 的进制数表示? 因为奇数的二进制形式的最后一位一定是 1 ,而偶数的二进制的最后一位是 0 ,所以可以通过计算 5%2 得出 5 的二进制形式中最后一位数字是 1 或都 0 。一般来讲,对于数值 n ,其二进制数的最后一位是 n%2 ,因此计算出的第一个数字恰是需要输出的最后一位数字。这就需要使用一个递归函数实现。在函数中,首先在递归调用之前计算 n%2 的数值,然后在递归调用语句之后进行输出。这样,计算出的第一个数值反而在最后一个输出。

为了得出下一个数字,需要把值除以 2 。这种计算就相当于在十进制下把小数点左移一位。如果此时得出的数值是偶数,则下一个二进制的数值是 0;若得出的数值为奇数,则下一个二进制位的数值就是 1 。例如 5 /2 的数值是 2 (整数除法),所以下一位值是 0 。这时已经得到了数值 01 。重复上述计算,即使用 2 除以 2 得出 1 ,而 1%2的数值是 1,因此下一位值是 0 。这时得到的数值是 101 。那么何时停止这种计算呢?因为只要被 2 除的结果等于或大于 2 ,那么就还需要一位二进制进行表示,所以只有被 2 除的结果小于 2 时才停止计算。每次除以 2 就可以得出一位二进制位值,直到计算出最后一位为止(如果读者对此感到不解,可以在十进制下做类似的运算。 628 除以 10 的余数是 8 ,因此 8 就是最后一位。上步计算的商是 62 而 62 除以 10 的余数是 2 ,所以 2 就是下一位值,依此类推)。在程序清单 9.8 中实现了上述算法。

程序清单 9.8 binary.c 程序
---------------------------------------------------------
/* binary.c --- 以二进制形式输出整数 */

#include <stdio.h>
void to_binary (unsigned long n);

int main (void)
{
unsigned long number;

printf (" Enter an integer (q to quit) : \n");
while ( scanf ("%ul", &number) == 1)
{
printf (" Binary equivalent : ");
to_binary(number);
putchar('\n');
printf (" Enter an integer (q to quit) : \n");
}
printf (" Done \n");
return 0;
}

void to_binary (unsigned long n) /* 递归函数 */
{
int r;

r = n % 2;
if (n >= 2)
to_binary(n/2);
putchar('0'+r);
return;
}

以上程序中,如果 r 是 0 ,表达式 ‘0’+ r 就是字符 "0";当 r 为 1 时,则该表达式的值为字符"1"。得出这种结果的前提假设是字符 "1"的数值编码比字符 "0" 的数值编码大 1 。ASCII 和 EBCDIC 两种编码都满足上述条件。更一般的方式,你可以使用如下方法;

putchar(r ? '1':'0');

下面是一个简单的运行示例:

Enter an integer (q to quit) :
9
Binary equivalent : 1001
Enter an integer (q to quit) :
255
Binary equivalent : 11111111
Enter an integer (q to quit) :
1024
Binary equivalent : 10000000000
Enter an integer (q to quit) :
q
Done

当然,不使用递归也能实现这个算法。但是由于本算法先计算出最后一位数值,所以在显示结果之前必须对所有的数值进行存储(比如放在一个数组之中)。第 15 章“位操作”给出了不用递归实现这个算法的例子。

----------------------------------------------------------------------------

9.3.5 递归的优缺点

使用递归既有优点也有缺点。其优点在于为某些编程问题提供了最简单的解决方法,而缺点是一些递归算法会很快耗尽计算机的内存资源。同时,使用递归的程序难于阅读和维护。从下面的例子中可以看出使用递归的优缺点。

斐波纳契列定义如下:第一个和第二个数字都是 1 ,而后续的每个数字是其两个数字之和。例如,数列中前几个数字是1,1,2,3,5,8 和 13 。斐波纳契列在数学上很受重视,甚至有专门的刊物讨论它。本书不对此做深层次的探讨。下面我们创建一个函数,它接受一个正整数 n 作为 参数,返回相应的斐波纳契数值。

首先,关于递归深度,递归提供了一个简单的定义。如果调用函数 Fibonacci(),当 n 为 1 或 2 时 Fibonacci(n)应返回 1 ; 对于其他数值应返回 Fibonacci(n-1) + Fibonacci(n-2):

long Fibonacci (int n)
{
if (n > 2)
return Fibonacci (n-1) + Fibloacct (n-2);
else
return 1;
}

这个 C 递归函数只是重述了递归的数字定义(为使问题简化,函数不处理小于 1 的数值)。同时本函数使用了双重函数(double recursion);也就是说,函数对本身进行了两次调用。这就是导致一个弱点。

为了具体说明这个弱点,先假设调用函数 Fibonacci(40)。第 1 级递归会创建变量 n ,接着它两次调用 Fibonacci(),在第 2 级递归中又会创建两个变量 n 。上述的两次调用中的每一次又进行了两次调用,因而在第 3 级调用中需要 4 个变量 n,这时变量总数为 7 。因为每级调用需要的变量数是上一级变量数的 2 位,所以变量的个数是以指数规律增长的! 正如第 5 章“运算符,表达式和语句”中的小麦粒数的例子,按指数规律增长会很快产生很大的数值。这种情况下,指数增长的变量数会占有用大量内存,这就可能导致程序瘫痪。

当然,以上是一个比较极端的例子,但它也表明了必须小心使用递归,是当效率处在第一位的时候。

-------------------------------------------------------------------------------

PS 所有 C 函数的地位同等

一个程序中的每个 C 函数和其他函数之间是平等关系。每一个函数都可以调用其他任何函数或被其他任何函数调用。就这使得 C 函数 和 Pascal 以及 Modula-2 中的过程略有不同,因为这些过程可以嵌入在其他过程之中,而且嵌入在不同处的过程之间不能相互调用。

main()函数是否与其他函数不同? 是的,函数 main()是一个有点特殊的函数。因为在程序中当几个函数放在一起时,计算机将从 main()中的第一个语句开始执行,但这也是其他局限之处。同时 main()也可以被其本身递归调用或被其他函数调用----尽管很少这这么做。

9.4 多源代码文件程序的编译

使用多个函数的最简单方法是将它们放在同一文件中,然后就像编译单个函数的文件一样对该文件进行编译。而其他方法就根据操作系统的不同而不同,以下几节就举例说明这些方法。

-----------------------------------------------------

9.4.1 UNIX

首先假定 UNIX 系统下安装了标准的 UNIX C 编译器 cc 。文件 file1.c 和 file2.c 中包含有 C 函数。下面的命令将把这两个文件编译在一起并生成可执行文件 a.out:

cc file1.c file2.o

在 UNIX 系统下有一个 make 命令可以自动管理多文件程序,本书不对此做深入讨论。

-----------------------------------------------------------

9.4.2 Linux

首先假定 Linux 系统下 GNU C 编译器 gcc 。 文件 file1.c 和 file2.c 中包含有 C 函数。下面的命令将把这两个文件编译在一起并生成可执行文件 a.out:

gcc file1.c file2.c

另外还将生成两个目标文件 file1.o 和 file2.o 。如果随后只对 file1.c 进行了改动而 file2.c不变,下面的命令可以对其他进行编译并链接到 file2.c 的目标代码:

gcc file1.c file2.o

-----------------------------------------------------------

9.4.3 DOS 命令行编译器

大多数 DOS 命令行编译器的工作机制同 UNIX 系统下的 cc 命令类似。一个不同之处在于 DOS 系统下目标文件的扩展名是 .obj 而不是 .o 。而且有些编译器并不生成目标代码文件,而是生成汇编语言或其他特殊代码的中间文件。

--------------------------------------------------------------

9.4.4 Windows 和 Macintosh 编译器

Whidows 和 Macitosh 系统下的编译器是面向工程的。工程(project)描述了一个特定的程序所使用的资源。这些资源中包括源代码文件。使用这种编译器运行单文件程序时,必须创建工程。而对于多文件程序,需要使用相应的菜单命令将源代码文件加入到一个工程之中,而且,工程必须包括所有的源代码文件(扩展名为 .c 的文件)。但是,头文件(扩展名为 .h 的文件)不能包含在工程之中。因为工程只管理所使用的源代码文件,而使用哪些头文件需要由源代码文件中的 #include 指令确定。

9.4.5 头文件的使用

如果把 main()函数放在第一个文件中而把自定义函数放在第二个文件中实现,那么第一个文件仍需要使用函数原型。如果把函数原型放在一个头文件中,就不必每次使用这些函数时输入其原型声明。这也是 C 标准库的做法,比如把输入/输出函数的原型声明放在 stdio.h 中,把数学函数的原型声明放在 math.h 之中。对于包含自定义函数的文件也可以这样做。

编写程序的过程中需要经常使用 C 的预处理器定义常量。而定义的常量只能用于包含相应 #define 语句的文件。 如果程序中的函数分别放在不同的文件之中,那么就必须使定义常量的 #define 指令对每个文件都可用。而直接在每个文件中键入该指令的方法既耗时又容易出错,同时也会带来一个维护上的问题:即如果修改了一个使用 #define 定义的数值,那么必须在每一文件中对其进行修改。比较好的解决方法是把所有的 #define 指令放在一个头文件中,然后在每个源代码文件中使用 # include 语句引用该头文件。

总之,把函数原型和常量定义放在一个头文件中是一个很好的编程习惯。我们考虑这样一个例子。假设需要管理 4 个连锁的旅馆。每一个旅馆都有不同的收费标准,但是对于一个特定的旅馆,其中的所有房间都符合同一种收费标准。对于预定住宿时间超过一天的人来说,第 2 天的收费是第 1 天的 95% 。而第 3 天的收费则是第 2 天的 95%,等等(先不考虑这种策略的经济效益)。我们需要这样一个程序,即对于指定的旅馆和总的住宿天数可以计算出收费总额。同时程序中要实现一个菜单,从而允许用户反复进行数据输入直到选择退出。

程序清单 9.9 ,程序清单 9.10 以及程序清单 9.11 列出了上述程序的源代码。第一个程序清单包含了 main()函数,在 main(0函数中可以看出整个程序的组织结构。第二个程序清单包含所使用的函数,而且我们假设这些函数放在单独的文件中。最后,程序清单 9.11 列出了一个头文件,其中包含了程序的所有源文件使用的自定义常量和函数原型。前面讲过,在 UNIX 和 DOS 环境下,指令
#include"hotels.h" 中的双引号表示被包含的文件位于当前工作目录下(该目录一般包含源代码)。

程序清单 9.9 usehotel.c 控制模块
------------------------------------------------------------------------------------
/* usehotel.c --- 旅馆房间收费程序 */
/* 与程序清单 9.10 一起编译 */

#include <stdio.h>
#include "hotel.h" /* 定义常量,声明函数 */

int main (void)
{
int nights;
double hotel_rate;
int code;

while ((code = menu()) != QUIT)
{
switch(code)
{
case 1: hotel_rate = HOTEL1;
break;
case 2: hotel_rate = HOTEL2;
break;
case 3: hotel_rate = HOTEL3;
break;
case 4: hotel_rate = HOTEL4;
break;
default: hotel_rate = 0.0;
printf ("Oops! \n");
break;
}
nights = getnights();
showprice (hotel_rate,nights);
}
printf ("thank you and googby ");
system("PAUSE");
return 0;
}

----------------------------------------------------------------------------------

程序清单 9.10 hotel.c 函数支持模块

/* hotel.c --- 旅馆管理函数 */

#include <stdio.h>
#include "hotel.h"

int menu (void)
{
int code,status;

printf ("\n%s%s\n",STARS,STARS);
printf ("Enter the number of the desired hotel : \n");
printf (" 1) Fairfield Arms 2) Hotel Olympic \n");
printf (" 3) Chertworthy Plaza 4) The Stockton \n");
printf (" 5) Quit \n");
printf ("%s%s \n",STARS,STARS);
while ((status = scanf ("%d",&code)) != 1 || (code < 1 || code > 5))
{
if (status != 1)
scanf ("%*s");
printf ("Enter an integer from 1 to 5 , please \n");
}
return code;
}

int getnights (void)
{
int nights;

printf ("How many nights are needed ?");
while (scanf ("%d",&nights) != 1)
{
scanf("%*s");
printf ("Please enter an integer,such as 2 \n");
}
return nights;
}

void showprice (double rate, int nights)
{
int n;
double total = 0.0;
double factor = 1.0;
for (n = 1; n <= nights; n++,factor *= DISCOUNT)
total += rate * factor;
printf ("The total cost will be $%.02f \n",total);
}

------------------------------------------------------------------------------------------

程序清单 9.11 hotel.h 头文件

/* hotel.h ---- hotel.c 中的常量定义和函数声明 */

#define QUIT 5
#define HOTEL1 80.00
#define HOTEL2 125.00
#define HOTEL3 155.00
#define HOTEL4 200.00
#define DISCOUNT 0.95
#define STARS"*****************************"

//给出选项列表
int menu (void);

//返回预定天数
int getnights (void);

//按饭店的星级和预定天数计算价格并显示出来
void showprice (double,int);

-----------------------------------------------------------------------------------------

下面是一个运行示例:

**********************************************************
Enter the number of the desired hotel :
1) Fairfield Arms 2) Hotel Olympic
3) Chertworthy Plaza 4) The Stockton
5) Quit
**********************************************************
3
How many nights are needed ?1
The total cost will be $155.00

**********************************************************
Enter the number of the desired hotel :
1) Fairfield Arms 2) Hotel Olympic
3) Chertworthy Plaza 4) The Stockton
5) Quit
**********************************************************
4
How many nights are needed ?3
The total cost will be $570.50

**********************************************************
Enter the number of the desired hotel :
1) Fairfield Arms 2) Hotel Olympic
3) Chertworthy Plaza 4) The Stockton
5) Quit
**********************************************************
5
thank you and googby

顺便提一下,程序中有几处很具有特色。比如函数 menu() 和 getnights()通过检测 scanf()的返回值来跳过输入的非数字数据,并且调用函数 scanf("%*s") 来跳至下一空白字符。请注意 menu()中的以下代码如何检查出非数字的输入和超出范围的数据:

while ((status = scanf ("%d",&code)) != 1 || (code < 1 || code > 5))

这段代码运用了 C 的两个运算规则:逻辑表达式从左向右运算;并且一量结果明显为假,运算会立刻停止。在配合中,只有确定 scanf()已成功读取了一个整形数值后,变量 code 的数值才会被检查。

用函数分别实现各个独立产的功能需要使用这种精练的语句。当然第一次编写 menu()和
getnights()时可能只使用了简单的 scanf()函数而没有数据检查功能。然后,就可以根据基本程序的运行情况对每个模块进行改进。

9.5 地址运算符 &

C 中最重要的(有时也是最复杂的)概念之一就是指针(pointer)也就是用来存储地址的变量。在上文中函数 scanf()就是使用地址作为参数。更一般地,当需要改变调用函数中的某个数值时,任何被调用的无返回值的 C 函数都需要使用地址来完成该任务。接下来我们讨论使用地址参数的函数,首先介绍一元运算符 & 的使用方法(下一章继续研究指针并介绍它的使用方法)。

一元运算符 & 可以取得变量的存储地址。假设 pooh 是一个变量的名字,那么 &pooh 就是该变量的地址。一个变量的地址可以被看作是该变量在内存中的位置。假定使用了以下语句:

pooh = 24;

并且假定 pooh 的存储地址是 0B78 (PC 的地址一般以 4位十六进制数的形式表示)。那么语句:

printf ("%d %p\n",pooh,&pooh);

将输出如下数值 (%p 是输出地址的说明符):

24 0B76

在程序清单 9.12 中,我们使用地址运算符获得不同函数中具有相同名称的变量的存储地址。

程序清单 9.12 loccheck.c 程序
----------------------------------------------------------------------
/* loccheck.c --- 查看变量的存储地址 */

#include <stdio.h>
void mikado (int); /* 声明函数 */
int main (void)
{
int pooh = 2, bah = 5; /* main()函数中的局部变量 */

printf ("In main(),pooh = %d and &pooh = %p \n",pooh,&pooh);
printf ("In main(),bab = %d and &bab = %p \n",bah,&bah);
mikado (pooh);
return 0;
}

void mikado (int bah) /* 定义函数 */
{
int pooh = 10; /* mikado()函数中的局部变量 */

printf ("In mikado(),pooh = %d and &pooh = %p \n",pooh,&pooh);
printf ("In mikado(),bah = %d and &bah = %p \n",bah,&bah);
}

程序清单 9.12 使用了 ANSI C 中的 %p 格式对地址进行输出。在我们的系统中程序的输出结果如下:

In main(),pooh = 2 and &pooh = 0012FF50
In main(),bab = 5 and &bab = 0012FF4C
In mikado(),pooh = 10 and &pooh = 0012FF3C
In mikado(),bah = 2 and &bah = 0012FF48

%p 输出地址的方式随着实现的不同而不同。但是在多数实现中,地址是像本例中这样以十六进制形式显示的。

上述输出结果说明了以下问题:首先,两个 pooh 变量具有不同的地址,两个 bah 变量也是如此。因此,正如我们所料,计算机会把它们看作 4 个独立的变量。其次,函数调用 mikado(pooh)确实把实际参数 (main()中的 pooh)的数值(2)传递给了形式参数 (mikado()中的 bah)。需要注意的是这种传递只是进行了数值传递,两个变量(main()中的 pooh 和 mikado()中的 bah)仍分别保持原来的特性。

我们提出第二点是因为并非所有语言都如此。例如在 FORTRAH 语言中,子程序会改变调用程序中的原变量的数值。尽管在子程序中变量的名称可能不同,但是其地址是相同的。而在 C 语言中并不是这样。每一个 C 函数都使用自己的变量。这么做更可取,因为它可以使变量不因被调函数中操作的副作用而意外地被改变。然而,正如下节要讲述的那样,这种做法也会带有一些麻烦。

9.6 改变调用函数中的变量

有时我们需要用一个函数改变另一个函数中的变量。例如,排序问题的一个常见任务是交换两个变量的数值。假设要交换两个变量 x 和 y 的数值:

x = y;
y = x;

上面这段简单的代码并不能实现这个功能,因为当执行第二行时,x 的原数值已经被 y 的原数值所代替。这就需要另外一行语句对 x 的原数值进行存储。

temp = x;
x = y;
y = temp;

现在这段代码就可以实现数值交换的功能,可以将其编写成函数并构造一个驱动程序进行测试。在程序清单 9.13中,为了清楚地表明某变量属于函数 main()还是属于函数 interchange(),前者使用了变量 x 和 y,而后者使用的是 u 和 v。

程序清单 9.12 swap1.c 程序
----------------------------------------------------------------------
/* swap1.c --- 交换函数的第一个版本 */

#include <stdio.h>
void interchange (int u, int v);
int main (void)
{
int x = 5, y = 10;

printf ("Originally x = %d and y = %d \n", x,y);
interchange(x,y);
printf (" Now x = %d y = %d \n", x,y);
return 0;
}

void interchange (int u, int v)
{
int temp;

temp = u;
u = v;
v = temp;
}

令人吃惊的是数值并没有发生交换。下面我们在 interchange()中加入一些打印语句来检查错误(请参数程序清单 9.14 )。

程序清单 9.14 swap2.c 程序
-------------------------------------------------------------------
/* swap2.c -- 分析 swap1.c 程序 */

#include <stdio.h>
void interchange (int u, int v);
int main (void)
{
int x = 5, y = 10;

printf ("Originally x = %d and y = %d \n", x,y);
interchange(x,y);
printf (" Now x = %d y = %d \n", x,y);
return 0;
}

void interchange (int u, int v)
{
int temp;

printf ("Originally u = %d and v = %d \n",u,v);
temp = u;
u = v;
v = temp;
printf ("Now u = %d and v = %d \n",u,v);
}

新的输出结果是:

Originally x = 5 and y = 10
Originally u = 5 and v = 10
Now u = 10 and v = 5
Now x = 5 y = 10

函数 interchange()并没有错误,u 和 v 的数值确实得到了交换。问题出现在把执行结果传递给 main()的时候。正如已指出的那样,interchange()使用的变量独立于函数 main(),因此交换 u 和 v 的数值对 x 和 y 的数值没有任何影响! 使用 return 语句可以吗?可以在 interchange()的结尾处加入下面一行语句: return(u);

然后改变 main()中该对函数的调用方式;

x = interchange(x,y);

做了上述更改后,x 被赋予了新值,而 y 的数值并没有改变。因为 return 语句只能把一个数值传递给调用函数,但现在我们需要传递两个数值。这并非不能实现! 只需用指针就可以了。

9.7 指针简介

究竟什么叫做指针? 一般来讲,指针是一个其数值为地址的变量(或更一般地说是一个数据对象)。正如 char 类型的变量用字符作为其数值,而 int 类型变量的数值是整数,指针变量的数值表示的是地址。指针在 C 中有很多用途,本章将研究把它作为函数参数的方法和理由。

如果你将某个指针变量命名为 ptr ,就可以使用如下语句:

ptr = &pooh; /* 把 pooh 的地址赋给 ptr */

对于这个语句,我们称 ptr “指向”pooh。 ptr 和 & pooh 的区别在于前者为一变量,而后者是一个常量。当然,ptr 可以指向任何地方:

ptr = & bah; /* 令 ptr 指向 bah 而不是 pooh */

这时 ptr 的值是 bah 的地址。

要创建一个指针变量,首先需要声明其类型。假设你想把 ptr 声明为可以存放一个 int 数值的地址,就需要使用下面介绍的新运算符。让我们来研究这种新运算符。

-------------------------------------------------------------------------------

9.7.1 间接运算符: *

假定 ptr 指向 bah ,如下所示:

ptr = &bah;

这时就可以使用间接(indirection)运算符 * (也称作取值(dereferencing)运算符)来获取 bah 中存放的数值(不要把这种一元运算符和表示乘法的二元运算符 * 相混淆)。

val = *ptr /* 得到 ptr 指向的值 */

语句 ptr = &bah; 以及语句 val = *ptr; 放在一起等同于下面的语句:

val = bah;

由此看出,使用地址运算符和间接运算符可以间接完成上述语句的功能,这也正是“间接运算符”名称的由来。

--------------------------------------------------------------------------------

PS 总结: 与指针相关的运算符

地址运算符: &

总体注解: 后跟一个变量名时,&给出该变量的地址。 /* 地址是指在计算机系统的内存地址 */

例如: &nurse 表示变量 nurse的地址

间接运算符: *

总体注解: 当后跟一个指针名或地址时,* 给出存储在被指向地址中的数值。

例如:

nurse = 22;
ptr = &nurse; /* 指向 nurse 的指针 */
val = *ptr; /* 将 ptr 的指向的值赋给 val */

上述语句实现的功能是把数值 22 赋给变量 val 。

个人见解: & 和 * 实际上是对内存地址进行操作的赋值运算符,但功能正好是相反的。

&变量 等于是获取该变量在内存中的实际地址。(计算机系统的内存地址)

*指针或地址 等于是获取该内存地址中的实际数值。

----------------------------------------------------------------------------------------

9.7.2 指针声明

我们已讲述了 int 类型变量以及其他基本数据类型变量的声明方法。那么应该如何声明指针呢?
你也许会猜想到其声明形式如下:

pointer ptr; /* 不能这样声明一个指针 */

为什么不能这要声明? 因为这对于声明一个变量为指针是不够的,还需要说明指针所指向变量的类型。原因是不同的变量类型占用的存储空间大小不同,而有些指针操作需要知道变量类型所占用的存储空间。同时,程序也需要了解地址中存储的是何种数据。例如,long 和 float 两种类型的数值可能使用相同大小的存储空间,但是它们的数据存储方式完全不同。指针的声明形式如下:

int *pi /* pi 是指向整数变量的指针 */
char *pc; /* pc 是指向字符变量的指针 */
float *pf,*pg; /* pf 和 pg 是指向浮点变量的指针 */

类型标识符表明了被指向变量的类型,而星号(*)表示该变量为一指针。声明 int *pi; 意思是 pi 是一个指针,而 *pi 是int 类型的 (请参见图 9.5)(在本目录的图例)

* 和 指针名之间的空格的可选的。通常程序员在声明中使用空格,而在指向变量时将其省略。

pc 所指向的值 (*pc)是 char 类型的。而 pc 本身又是什么类型? 我们把它描述为“指向 char 的指针”类型。pc 的值是一个地址,在大多数系统内部,它由一个无符号整数表示。但是,这并不表示可以把指针看作是整数类型。一些处理整数的方法不用用来处理指针,反之亦然。例如,可以进行两整数相乘,而指针则不能。因此指针的确是一种新的数据类型,而不是一种整数类型。所以,正如前面提到的, ANSI C 专门为指针提供了 %p 输出格式。

------------------------------------------------------------------------------

9.7.3 使用指针在函数间通信

我们只是刚刚接触到丰富有趣的指针知识的很小一部分,但这里我们的重点是讲述如何通过指针解决函数间通信的问题。在程序清单 9.15 中的程序中,函数 interchange()使用指针参数,我们将对该函数进行详细的讨论。

程序清单 9.15 swap3.c 程序
------------------------------------------------------------
/* swap3.c --- 使用指针完成交换 */
#include <stdio.h>
void interchange (int * u, int * v);
int main (void)
{
int x = 5, y = 10;

printf (" Originally x = %d and y = %d \n",x,y);
interchange (&x, &y); /* 向函数传送变量的内存地址 */
printf (" Now x = %d and y = %d \n",x,y);
return 0;
}

void interchange (int * u, int * v)你可以把u,v理解为两个变量,变量类型是地址型的。int * u, int * v是其声明方式
{
int temp;

temp = *u; /* temp 得到 *u指向的值 这里是指 x 的值 */
*u = *v;
*v = temp;
}

对全部源文件进行编译后,该程序能否正常运行?

Originally x = 5 and y = 10
Now x = 10 and y = 5

答案是肯定的,结果如下所示。

下面我们分析程序清单 9.15 的运行情况。首先,函数调用语句如下:

interchange (&x, &y);

可以看出,函数传递的是 x 和 y 的地址而不是它们的值。这就意味着 interchange()函数原型声明和定义中的形式参数 u 和 v 将使用地址作为它们的值。因此,它们应该声明为指针。由于 x 和 y 都是整数,所以 u 和 v 是指向整数的指针。其声明如下:

void interchange (int * u, int * v);

接下来,函数体进行了如下声明:

int temp;

从而提供了所需的临时变量。为了把 x 的值存在 temp 中,需要使用以下语句;

temp = *u;

注意,因为 u 的值是 &x, 所以 u 指向 x 的地址。这就意味着 *u 代表了 x 的值,而这正是我们需要的数值。不要写成如下这样:

temp = u; /* 这样做不行 */

上面的语句中,因为赋值给变量 temp 的只是 x 的地址而不是 x 的值,所以不能实现数值的交换。

同样,把 y 的值赋给 x ,需使用下面的语句:

*u = *v;

其执行结果相当于:

x = y;

在示例程序中,我们用一个函数实现 x 和 y 的数值交换。首先函数使用 x 和 y 的地址作为参数,这使它可以访问 x 和 y 变量。通过使用指针和运算符 * ,函数可以获得相应存储地址的数据,从而就可以改变这些数据。

在 ANSI 原型中可以省略变量名称。这样,函数原型可以按如下形式进行声明:

void interchange (int * , int *);

通常情况下,可以把关于变量的两类信息传递给一个函数。如果函数调用形式为:

function1(x);

这时传递的是 x 的值。但是如果使用下面这种函数调用形式:

function2(&x);

那么会把 x 的地址传递给函数。第一种调用形式要求函数定义部分必须包含一个和 x 具有相同数据类型的形式参数。如下所示:

int function1 (int num)

而第二种形式要求函数定义部分的形式参数必须是指向相应数据类型的指针:

int function2 (int * ptr);

使用函数进行数据计算等操作时,可以使用第一种调用形式。但是,如果需要改变调用函数中的多个变量的值时,就需要使用第二种调用形式。其实使用 scanf()时已经使用了第二种形式。例如,当需要为变量 num 读取一个数值时,可以调用函数 scanf("%d",&num)。该函数调用的意思是先读取一个数值,然后将其存储到通过参数获得的地址中。

尽管 interchange()只是使用局部变量,但是通过使用指针,该函数可以操作 main()中变量的值。

使用过 Pascal 和 Modula-2 的读者可能已经看出第一种调用形式和 pascal 中的值参数相同,而第二种形式与 Pascal 中的变量参数相似(尽管不完全相同)。对于 BASIC 程序员来说,整个程序可能比较难于理解。如果你感到本书晦涩难懂,那么可以进行一些实际的编程练习,这时就会发现使用指针非常简单方便(请参见图 9.6)

PS 变量:名称,地址以及数值

在前文有关指针的讨论中,变量名称,地址以及数值之间的关系是其关键所在。下面我们将对此进行深入讲解。

编写程序时,一个变量一般有两种属性:变量名和数值(当然还有其他属性,如数据类型等,但它们与这个主题无关)。程序被编译和加载后,同一个变量在计算机中的两个属性是地址和数值。变量的地址可以被看作是在计算机中变量的名称。

在许多编程语言中,变量地址只由计算机处理,对于编程人员来讲完全不可见。但是在 C 中,可以使用运算符 & 对变量的地址进行操作。

&barn 就表示变量 barn 的地址。

可以通过使用变量名获得变量的值。

例如 printf("%d\n",barn) 输出的是 barn 的数值。

当然,也可以通过使用运算符 * 从地址中获取相应的数值。

对于语句 pbarn = &barn;, *pbarn 是存储在地址 &barn 中的数值。

总之,普通的变量把它的数值作为基本数值量,而通过使用运算符 & 将它的地址作为间接数值量。但是对于指针来讲,地址是它的基本数值量,使用运算符 * 后,该地址中存储的数值是它的间接数值量。

某些读者也许会将地址打印出来以满足好奇心,但这并不是 & 运算符的主要用途。更重要的是,使用 & * 和 指针可以文件地操作地址以及地址中的内容,如程序 swap3.c(程序清单 9.15)中所示。

PS 总结:函数

形式:

ANSI C 函数的典型定义形式如下:

name (parameter declaration list) /* 函数名(参数,声明,列表)*/
function body /* 函数主体 */

参数声明列表是由逗号隔开的一系列变量的声明,非参数的变量只能在由花括号界定的函数体内部的声明。

例如:

int diff (int x, int y) // ANSI C
{ // 函数体开始
int z; // 声明局部变量

z = x - y;
return z; //返回一个值
} // 函数体结束

数值传递:

参数用于把调用函数中的数值传递给被调函数。例如变量 a 和 b 的数值分别为 5 和 2 ,则下面的函数调用语句会把数值 5 和 2 分别传递给变量 x 和 y:

c = diff (a,b);

数值 5 和 2 被称为实际参数,而 diff()中的变量 x 和 y 被称为形式参量。关键字 return 把函数中的某一数值返回到调用函数中去。在上例中,变量 c 获得了变量 z 的数值,也就是 3 。一般来讲,函数不会改变其调用函数中的变量。当需要在某函数中直接操作其调用函数中的变量时,可以使用指针作为参数。同时,指针参数也可以用来把多个数值返回到调用函数中。

函数返回值类型:

函数返回值类型指的是函数返回给它的调用函数的数值类型。如果函数返回值的类型和声明的类型不相同时,实际返回值是当把指定要返回的值赋给一个具有所声明的返回的变量时得到的数值。

例如:

int main (void)
{
double q,x,duff(); // 调用函数中的声明
int n;
...
q = duff(x,n);
...
}

double duff (u,k) // 函数定义中的声明
double u;
int k;
{
double tor;
...
return tor; // 返回一个 double 类型
}

9.8 关键概念

要想用 C 编写出灵活高效的程序,你必须正确理解函数的使用。把较大的程序组织成若干个函数的形式是很有用的,甚至是很关键的。如果每个函数实现某一特定功能,那么,这样的程序既易于理解又便于调试。另外,你还需要理解函数之间的信息传递机制,也就是明白函数参数以及返回值是如何工作的。因为函数的参数和其他局部变量是函数所私有的,所以在不同函数中声明的同名变量是完全不同的。而且任何函数不能直接访问其他函数中声明的变量。这种操作的局限性有助于保护数据的完整性。然而,当确实需要在一个函数中访问其他函数中的数据时,可以使用指针参数。

9.8 总结

函数可以作为大型程序的组成模块。每个函数应该实现某个明确的功能。使用参数可以向函数传递数值,并且通过关键字 return 让函数返回一个数值。如果函数返回值的类型不是 int ,那么必须在函数定义中以及调用函数的声明部分指定函数的返回值类型。如果需要在一个函数中操作它的调用函数中的变量,那么可以使用地址以及指针。

在 ANSI C 中可以使用函数原型声明,以便编译器检查函数调用时反传递的参数个数及类型是否正确。

C 函数可以调用其自身,这种调用被称作递归。有些编程问题借用递归解决方案,但是递归可能会在内存使用和时间花费方面效率低下。

9.10 复习题

--------------------------------------------------------------

1. 实际参数和形式参量有何不同?

形式参量(也被称为形式参数)是一个变量,它在被调函数中进行定义。实际参数是在函数调用中出现的值,它被赋值给形式参量。可以把实际参数认为是在函数被调用时用来初始化形式参量的值。

--------------------------------------------------------------

2. 写出下面所描述的各个函数的 ANSI 函数头。注意:只写出函数头即可,不需要实现。

a. donut() 接受一个 int 类型的参数,然后输出若干个 0 ,输出 0 的等于参数的值。

void donut (int n);

b. gear() 接受两个 int 类型的并返回 int 类型的值

int egar (int t1, int t2);

c. stuff_it() 的参数包括一个 double 类型的值以及一个 double 类型变量的地址,功能是把第一
个数值存放在指定的地址中。

void stuff_it (double d, double *pd); /* 第二个命名比较好 很形象 p d */

-----------------------------------------------------------------------------------------

3. 只写出下列函数的 ANSI C 函数头,不需要实现函数。

a. n_to_char()接受一个 int 类型的参数并返回一个 char 类型的值

char n_to_char (int n);

b. bigits() 接受的参数是一个 double 类型的和一个 int 类型的数值,返回值类型是 int 。

int bigits (double t1; int t2);

c. random() 不接受参数,返回 int 类型的数值。

int random (void);

--------------------------------------------------------------------------

4. 设计一个实现两整数相加并将结果返回的函数。

答:

int add (int t1, int t2)
{
int n;
n = t1 + t2;
return n;
}

或:

int add (int t1, int t2)
{
return t1 + t2;
}

最简洁的是在声明时就定义

int add (int t1, int t2){return t1 + t2 ;};

-------------------------------------------------------------------

5. 假如问题 4 中的函数实现两个 double 类型的数值相加,那么应该如何修改原函数?

答: 用 double 代替所有 int

double add (double t1, double t2)
{
return t1 + t2;
}

--------------------------------------------------------------------

6. 设计函数 alter(),其输入参数是两个 int 类型的变量 x 和 y,功能是分别将这两个变量的数值改为它们的和以及它们的差。

答:

这个函数需要使用指针,并且需要一个临时变量

void alter (int *pa ,int *pb)
{
int temp;
temp = *pa - *pb;
*pb = *pa - *pb;
*pa = temp;
}

或者

void alter (int *pa, int *pb)
{
*pa += *pb;
*pb = *pa - 2 * *pb;
}

---------------------------------------------------------------------------

7. 判断下面的函数定义是否正确。

void salami (num)
{
int num,count;

for (count = 1; count <= num; num++)
printf (" 0 salami mio \n");
}

答:有错误,num 应该在 salami()的参数列表中而不是在花括号之后声明,而且应该是 count++ 而不是 num++ 。

----------------------------------------------------------------------------------------

8. 编写一个函数,使其返回 3 个整数参数中的最大值。

答:

int max (int x, int y, int n)
{
int temp = x;

if ( y > temp)
temp = y;
if ( n > temp)
temp = n;
return temp;
}

注: 三个数比较最简洁有效的原则,先定义一个临时变量 temp 并把第一个数 x 赋给 temp;
temp 与 第二个数 y 比较 y>tmep 则 tmep = y;
然后再拿第三个数 n 进行比较, n>temp 则 tmep =n;

--------------------------------------------------------------------------------------

9. 给定下面的输出;

Please choose one of the following:
1) copy files 2) move files
3) remove files 4)quit

Enter the number of your choice

a. 用一个函数实现菜单的显示,且该菜单 有 4个用数字编号的选项并要求你选择其中之一(输出应该如题设中所示)。

b. 编写一个函数,该函数接受两个 int 类型的参数;一个下界和一个上界。在函数中,首先从输入终端读取一个整数,如果该整数不在上下界规定的范围内,则函数重新显示菜单(使用本题目 a 部分中的函数)以再次提醒用户输入新值。如果输入数值在规定范围内,那么函数应将该数值返回给调用函数。

c. 使用本题目 a 和 b 部分中的函数编写一个最小的程序。最小的意思是该程序不需实现菜单中所描述的功能;它只需要时数这些选项并能获取正确的响应即可。

答:

#include <stdio.h>
#include <string.h>
#define STR "Please choose ont of the following"
void menu (void);
int iput_int (int x, int y);
void status (char ch, int n);
int main (void)
{
int num;
menu();
while ((num = iput_int(1,4)) != 4)
printf (" I like choice %d \n",num);
printf ("Bye \n");
system("PAUSE");
return 0;
}

void menu (void)
{
status ('-',strlen(STR));
printf ("%s \n",STR);
printf (" 1) copy files 2) move files \n");
printf (" 3) remove files 4) quit \n");
printf (" Enter the numbqr of your choice \n");
status ('-',strlen(STR));
}

int iput_int (int x, int y)
{
int num;
scanf ("%d", &num);
while ((num < x) || (num > y))
{
printf (" %d is not a valid choice; try again \n",num);
menu;
scanf("%d", &num);
}
return num;
}

void status (char ch, int n) // 菜单 外围框
{
int temp;

for (temp =1 ; temp <= n; temp++)
putchar(ch);
putchar('\n');

}

注意: 不知道是编译器的问题还是什么,一旦出现数值有差别或者是编译不了但代码基本正确的时候
这些时间都花在了 少了一个括号或者分号之上。 统计到现在 因为括号或者分号原因去找解 解决问题的时间起码有二个小时了。

9.11 编程练习

------------------------------------------------------------------------
1. 设计函数 min(x,y),返回两个 double 数值中较小的数值,同时用一个简单的驱动程序测试该函数。

解:

#include <stdio.h>
double min (double x, double y){ return (x < y ? x: y);};
int main (void)
{
double a = 3.259999, b = 3.259998;
double c = 0;
c = min (a,b);
printf (" a = %lf b = %lf min %lf \n",a,b);
system("PAUSE");
return 0;
}
或:
#include <stdio.h>
double min (double x, double y);
int main (void)
{
double a = 3.259999, b = 3.259998;
double c = 0;
c = min (a,b);
printf (" a = %lf b = %lf min %lf \n",a,b);
system("PAUSE");
return 0;
}

double min (double x, double y)
{
if (x < y)
return x;
else
return y;
}

注: 二点, 一是 double 类型的说明符为 %lf 二是 函数要记得使用 return 返回 特别是在解答一这种形式的函数声明和定义在一起的例子。

------------------------------------------------------------------------------------------

2. 设计函数 chline (ch,i,j);实现指定字符在 i 列 到 j 列的输出,并用一个简单的驱动测试该函数。

解;
#include <stdio.h>
void chline (char ch, int i, int j);
int main (void)
{
char ch;
int x,y;
printf (" 请输入字母或者数字 \n");
scanf ("%c",&ch);
printf (" 请输入起始列 \n");
scanf ("%d",&x);
printf (" 请输入终止列 \n");
scanf ("%d",&y);
chline(ch,x,y);
system("PAUSE");
return 0;
}

void chline (char ch, int i, int j)
{
int count,num;

for (count = 0; count <i-1; count++) /* 此处的 i-1 是为了显示时 左对齐 */
putchar(' ');
for (num = i; num <=j; num++)
putchar(ch);
}

----------------------------------------------------------------------------------------

3. 编写一个函数。函数的 3 个参数是一个字符和两个整数。字符参数是需要输出的字符。第一个整数说明了在每行中该字符输出的个数,而第二个整数指的是需要输出的行数。编写一个调用该函数的程序。

解:

#include <stdio.h>
void chline (char ch, int i, int j);
int main (void)
{
char ch;
int x,y;
printf (" 请输入字母或者数字 \n");
scanf ("%c",&ch);
printf (" 请输入字符的个数 \n");
scanf ("%d",&x);
printf (" 请输入输出多少列 \n");
scanf ("%d",&y);
chline(ch,x,y);
system("PAUSE");
return 0;
}

void chline (char ch, int i, int j)
{
int count,num;

for (num = 0; num <j; num++) // 这类排列的原则是 行 在外部循环
{
for (count = 0; count < i; count++) //而字符个数在内部循环
putchar(ch);
putchar('\n');
}
}
----------------------------------------------------------------------------

4. 两数值的谐均值可以这样计算:首先对两数值的倒数取平均值,最后再取倒数。编写一个带有两个double 参数的函数,计算这两个参数的谐均值。

解:

#include <stdio.h>
double rave (double x, double y);
int main (void)
{
double a = 0, b = 0, c = 0;

printf ("请输入两个数字,程序将计算它们的谐均值 : ");
scanf ("%lf %lf",&a,&b);
c = rave(a,b);
printf (" 两个数的谐均值为 %0.2lf \n",c);
system("PAUSE");
return 0;
}

double rave (double x, double y)
{
return 2/(1/x + 1/y); // 谐均值实现的算法
}

注: 说句实在话 不知所谓

-------------------------------------------------------------------------------

5. 编写并测试函数 larger_of(),其功能是将两个 double 类型变量的数值替换成它们中的较大值。例如,larger_of(x,y) 会把 x 和 y 中的较大数值重新赋给变量 x 和 y。

解:

#include <stdio.h>
void larger_of (double *x, double *y);
int main (void)
{
double a = 0, b = 0;

printf ("请输入两个数,程序将分析数值然后将最大的值重置 : ");
printf ("第一个数 :");
scanf ("%lf",&a);
printf ("第二个数 :");
scanf ("%lf",&b);
printf ("a = %lf b = %lf \n", a,b);
printf ("交换后 \n");
larger_of (&a,&b);
printf (" a = %lf b = %lf \n", a,b);
system("PAUSE");
return 0;
}

void larger_of (double *x, double *y)
{
double temp = 0;

if (*x < *y)
{
temp = *x;
*x = *y;
*y = temp ;
}
}

注: 重要是记住 & * 这二个运算符的使用。

--------------------------------------------------------------------------

6. 编写一个程序,使其从标准输入读取字符,直到遇到文件结尾。对于每个字符,程序需要检查并报告该字符是否是一个字母。如果是的话,程序还应报告该字母在字母表中在数值位置。例如,c 和 C 的字母位置都是 3 。可以先实现这样一个函数:接受一个字符参数,如果该字符为字母则返回该字母的数值位置,否则返回 -1 。

解:
#include <stdio.h>
#include <ctype.h> // 为了调用 islower 和 isupper 函数

int place (char ch);
int main (void)
{
char ch;
int num ;

do // 因为用 while 循环被调函数不能正确的传递正确的值做调用函数
{
printf ("请输入一个字符,程序将报告它在字母表中的位置(Ctrl+c quit ): \n");
ch = getchar();
num = place(ch);
if (num < 0)
printf ("你输入的不是字母,请重新输入\n");
else{
putchar(ch);
printf (" 在字母表的位置是 %d \n",num);
}
} while((ch=getchar())!=EOF);
system("PAUSE");
return 0;
}

int place (char ch)
{
if (islower(ch))
return ch - 96; //利用 ASCII 码表
else if (isupper(ch))
return ch - 64;
else
return -1;
}

注:这个程序调试了好久,主要的原因是一直习惯用 while 来循环让用户输入
但 这个程序 利用 while循环的话, place()函数传递回来的值不正确。
所以只能用 do while 循环了。

---------------------------------------------------------------------------

7. 在第 6 章“ c 控制语句:循环”的程序清单 6.20中,函数 power()的功能是返回一个 double 类型的某个正整数次幂。现在改进该函数,使其能正确的计算负幂。同时,用该函数实现 0 的任何次幂都为 0 ,并且任何数值的 0 次幂都为1 ,使用循环的方法编写该函数并在一个程序中测试它。

解:

#include <stdio.h>
double power (double n, int p);
int main (void)
{
double x, xpow;
int exp;

printf ("Enter a number and the positive integer power");
printf ("to which \nthe number will be raised .Enter q");
printf ("to quit \n");
while (scanf ("%lf %d",&x,&exp) == 2)
{
xpow = power (x,exp);
printf ("%.3g to the power %d is %.5g \n",x,exp,xpow);
printf ("enter next pair of number or q to quit \n");
}
printf ("hope you enjoyed this power trip --- bye \n");
return 0;
}

double power (double n, int p)
{
double pow = 1;
int i;

if (n == 0) // 0 的任何次幂 都为 0
pow = 0;
else if (p == 0) // 任何数值的 0 次幂 都为 1
pow = 1;
else if(p<0){ // 整数的负幂次方是这个数的倒数, a^-n = 1 /a^n (a≠0,n∈N)
for (i = 1; i <= -p; i++)
pow *= n;
pow = 1/pow;
}
else {
for (i = 1; i <= p; i++)
pow *= n;
}
return pow;
}

---------------------------------------------------------------------------------------
8. 使用递归函数重做练习 7

解:

#include <stdio.h>
double power (double n, int p);
int main (void)
{
double x, xpow;
int exp;

printf ("Enter a number and the positive integer power");
printf ("to which \nthe number will be raised .Enter q");
printf ("to quit \n");
while (scanf ("%lf %d",&x,&exp) == 2)
{
xpow = power (x,exp);
printf ("%.3g to the power %d is %.5g \n",x,exp,xpow);
printf ("enter next pair of number or q to quit \n");
}
printf ("hope you enjoyed this power trip --- bye \n");
return 0;
}

double power (double n, int p)
{
double pow = 1;
int i;

if (p == 0) // 任何数值的 0 次幂 都为 1
{
if (n == 0) // 0 的任何次幂 都为 0
printf ("0 to the 0 undefined;using 1 as the value \n");
pow = 1.0;
}
else if (n == 0)
pow = 0.0;
else if (p > 0)
for (i = 1; i <= p; i++)
pow *= n;
else // p < 0 使用递归
pow = 1.0 / power(n,-p); //整数的负幂次方是这个数的倒数, a^-n = 1 /a^n
return pow;
}

----------------------------------------------------------------------------
9. 为了使程序清单 9.8 中的函数 to_binary()更一般化,可以在新的函数 to_base_n()中使用第二个参数,且该参数的范围从 2 到 10 。然后,这个新函数输出第一个参数在第二个参数规定的进制数下的数值结果。例如,to_base_n(129,8)的输出是 201,也就是129的 八进制数值。最后在一个完整的程序中对该函数进行测试。

解:

#include <stdio.h>
void to_base_n (unsigned long n, int base);
int main (void)
{
unsigned long number;
int num;

printf ("Enter an integer (q to quit) : ");
while (scanf ("%lu",&number) == 1)
{
printf ("\nEnter an inteeger (2 - 10) : ");
scanf("%d",&num);
if ((num>=2)&&(num<=10)){
to_base_n(number,num);
}
else{
printf("Error \n");
break;
}
putchar('\n');
printf("Enter an integer (q to quit):\n");
}
printf("Done.\n");
return 0;
}

void to_base_n(unsigned long n,int base)
{
int r;

r = n % base;
if (n >= base)
to_base_n(n / base,base);
putchar('0' + r);
return;
}

注: 又被一个分号花了5分钟,以后有找不出的问题 直接看括号分号就行了 -。-

--------------------------------------------------------------------------------

10. 编写并测试一个函数 Fibonacci(),在该函数中使用循环代替递归完成斐波纳契数列的计算。

解:
#include <stdio.h>
int fibonacci(int num);
int main(void)
{
int coun;
printf("输入你想要多少个斐波纳契数列:\n");
scanf("%d",&coun);
fibonacci(coun);
system("PAUSE");
return 0;
}

int fibonacci(int num)
{
int n;
unsigned long long t1,t2,temp;
t1=t2=1;

for(n=0;n<num;n++){
if((n==0)||(n==1)){
printf(" %d",t1);
}

else if(n>1){
temp = t1 + t2;
t1 = t2;
t2 = temp;
printf(" %d",t1);
}
}
}

PS: 这个是网上抄录的, 懒的写了
-----------------------------------------------------------------------------------