1月24日 C Primer Plus学习
9.4 编译多源代码文件的程序
使用多个函数最简单的方法是把它们都放在同一个文件中,然后像编译只有一个函数的文件那样编译该文件即可。
9.4.1 UNIX
假定在UNIX系统中安装了UNIX C编译器cc(最初的cc已经停用,但是许多UNIX系统都给cc命令起了一个别名用作其他编译器命令,典型的是gcc 或clang)。假设file1.c和file2.c是两个内含C函数的文件,下面的命令将编译两个文件并生成一个名为a.out的可执行文件:
cc file1.c file2.c
另外,还生成两个名为file1.o和file2.o的目标文件。如果后来改动了 file1.c,而file2.c不变,可以使用以下命令编译第1个文件,并与第2个文件的目标代码合并:
cc file1.c file2.o
UNIX系统的make命令可自动管理多文件程序,但是这超出了C Primer Plus的讨论范围。
注意,OS X的Terminal工具可以打开UNIX命令行环境,但是必须先下载命令行编译器(GCC和Clang)。
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不变,可以使用以下命令编译第1个文件,并与第2个文件的目标代码合并:
gcc file1.c file2.o
9.4.3 DOS命令行编译器
绝大多数DOS命令行编译器的工作原理和UNIX的cc命令类似,只不过使用不同的名称而已。其中一个区别是,对象文件的扩展名是.obj,而不
是.o。一些编译器生成的不是目标代码文件,而是汇编语言或其他特殊代码的中间文件。
9.4.4 Windows和苹果的IDE编译器
Windows和Macintosh系统使用的集成开发环境中的编译器是面向项目的。项目(project)描述的是特定程序使用的资源。资源包括源代码文件。这种IDE中的编译器要创建项目来运行单文件程序。对于多文件程序,要使用相应的菜单命令,把源代码文件加入一个项目中。要确保所有的源代码文件都在项目列表中列出。许多IDE都不用在项目列表中列出头文件(即扩展
名为.h的文件),因为项目只管理使用的源代码文件,源代码文件中的
#include指令管理该文件中使用的头文件。但是,Xcode要在项目中添加头文件。
9.4.5 使用头文件
如果把main()放在第1个文件中,把函数定义放在第2个文件中,那么第 1个文件仍然要使用函数原型。把函数原型放在头文件中,就不用在每次使用函数文件时都写出函数的原型。C 标准库就是这样做的,例如,把I/O函数原型放在stdio.h中,把数学函数原型放在math.h中。你也可以这样用自定义的函数文件。
另外,程序中经常用C预处理器定义符号常量。这种定义只储存了那些包含#define指令的文件。如果把程序的一个函数放进一个独立的文件中,你也可以使用#define指令访问每个文件。最直接的方法是在每个文件中再次输入指令,但是这个方法既耗时又容易出错。另外,还会有维护的问题:如果修改了#define 定义的值,就必须在每个文件中修改。更好的做法是,把 #define 指令放进头文件,然后在每个源文件中使用#include指令包含该文件即可。
总之,把函数原型和已定义的字符常量放在头文件中是一个良好的编程习惯。我们考虑一个例子:假设要管理 4 家酒店的客房服务,每家酒店的房价不同,但是每家酒店所有房间的房价相同。对于预订住宿多天的客户,第 2天的房费是第1天的95%,第3天是第2天的95%,以此类推(暂不考虑这种策略的经济效益)。设计一个程序让用户指定酒店和入住天数,然后计算并显示总费用。同时,程序要实现一份菜单,允许用户反复输入数据,除非用户选择退出。
程序清单9.9、程序清单9.10和程序清单9.11演示了如何编写这样的程序。第1个程序清单包含main()函数,提供整个程序的组织结构。第 2 个程序清单包含支持的函数,我们假设这些函数在独立的文件中。最后,程序清单9.11列出了一个头文件,包含了该程序所有源文件中使用的自定义符号常量和函数原型。前面介绍过,在UNIX和DOS环境中,#include "hotels.h"指令中的双引号表明被包含的文件位于当前目录中(通常是包含源代码的目录)。如果使用IDE,需要知道如何把头文件合并成一个项目。
程序清单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 goodbye.\n"); 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 $%0.2f.\n", total);
}
程序清单9.11 hotel.h头文件
/* hotel.h -- 符号常量和 hotel.c 中所有函数的原型 */
#define QUIT 5
#define HOTEL1 180.00
#define HOTEL2 225.00
#define HOTEL3 255.00
#define HOTEL4 355.00
#define DISCOUNT 0.95
#define STARS "**********************************"
// 显示选择列表
int menu(void);
// 返回预订天数
int getnights(void);
// 根据费率、入住天数计算费用
// 并显示结果
void showprice(double rate, int nights);
下面是这个多文件程序的运行示例:
******************************************************************** Enter the number of the desired hotel:
1) Fairfield Arms 2) Hotel Olympic
2) Chertworthy Plaza 4) The Stockton
3) quit
3
How many nights are needed? 1 The total cost will be $255.00. ******************************************************************** Enter the number of the desired hotel:
1) Fairfield Arms 2) Hotel Olympic
2) Chertworthy Plaza 4) The Stockton
3) quit
4
How many nights are needed? 3
The total cost will be $1012.64.
******************************************************************** Enter the number of the desired hotel:
1) Fairfield Arms 2) Hotel Olympic
2) Chertworthy Plaza 4) The Stockton
3) quit
5
Thank you and goodbye.
顺带一提,该程序中有几处编写得很巧妙。尤其是,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 语言最重要的(有时也是最复杂的)概念之一,用于储存变量的地址。
前面使用的scanf()函数中就使用地址作为参数。
概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值。
一元&运算符给出变量的存储地址。如果pooh是变量名,那么&pooh是
变量的地址。可以把地址看作是变量在内存中的位置。假设有下面的语句:
pooh = 24;
假设pooh的存储地址是0B76(PC地址通常用十六进制形式表示)。那么,下面的语句:
printf("%d %p\n", pooh, &pooh);
将输出如下内容(%p是输出地址的转换说明):
24 0B76
实现不同,%p表示地址的方式也不同。许多实现都以十六进制显示地址。顺带一提,每个十六进制数对应4位。
/* 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(), bah = %d and &bah = %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);
}
(教材中给出的)输入出的结果如下:
In main(), pooh = 2 and &pooh = 0x7fff5fbff8e8
In main(), bah = 5 and &bah = 0x7fff5fbff8e4
In mikado(), pooh = 10 and &pooh = 0x7fff5fbff8b8
In mikado(), bah = 2 and &bah = 0x7fff5fbff8bc
首先,两个pooh的地址不同,两个bah的地址也不同。因此,和前面介绍的一样,计算机把它们看成4个独立的变量。其次,函数调用mikado(pooh)把实际参数(main()中的pooh)的值(2)传递给形式参数(mikado()中的bah)。注意,这种传递只传递了值。涉及的两个变量(main()中的pooh和mikado()中的bah)并未改变。
我们强调第2 点,是因为这并不是在所有语言中都成立。例如,在FORTRAN中,子例程会影响主调例程的原始变量。子例程的变量名可能与原始变量不同,但是它们的地址相同。但是,在 C语言中不是这样。每个C 函数都有自己的变量。这样做更可取,因为这样做可以防止原始变量被被调函数中的副作用意外修改。然而,正如下节所述,这也带来了一些麻烦。
9.6 更改主调函数中的变量
有时需要在一个函数中更改其他函数的变量。例如,普通的排序任务中交换两个变量的值。假设要交换两个变量x和y的值。简单的思路是:
temp = x;
x = y;
y= temp;
上面这 3 行代码便可实现交换值的功能,可以编写成一个函数并构造一个驱动程序来测试。在程序清单9.13中,为清楚地表明变量属于哪个函数,在main()中使用变量x和y,在intercharge()中使用u和v。
程序清单9.13 swap1.c程序
/* swap1.c -- 第1个版本的交换函数 */
\#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) /* 定义函数 */
{
int temp;
temp = u;
u = v;
v = temp;
}
运行该程序后,输出如下:
Originally x = 5 and y = 10.
Now x = 5 and y = 10.
两个变量的值并未交换!我们在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 and 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 and y = 10.
看来,interchange()没有问题,它交换了 u 和 v 的值。问题出在把结果传回 main()时。interchange()使用的变量并不是main()中的变量。因此,交换 u和v的值对x和y的值没有影响!是否能用return语句把值传回main()?当然可以,在interchange()的末尾加上下面一行语句:
return(u);
然后修改main()中的调用:
x = interchange(x,y);
这只能改变x的值,而y的值依旧没变。用return语句只能把被调函数中的一个值传回主调函数,但是现在要传回两个值。这没问题!不过,要使用指针。
程序清单运行结果

浙公网安备 33010602011771号