当前位置: 首页 > news >正文

广州乐地网站建设公司成都市建设领域网站咨询电话

广州乐地网站建设公司,成都市建设领域网站咨询电话,职业生涯规划大赛的意义,微博 分享 wordpress文章目录 第15章 编写大型程序15.1 源文件15.2 头文件15.2.1 #include指令15.2.2 共享宏定义和类型定义15.2.3 共享函数原型15.2.4 共享变量声明15.2.5 嵌套包含15.2.6 保护头文件15.2.7 头文件中的#error指令 15.3 把程序划分成多个文件15.4 构建多文件程序15.4.1 makefile15.… 文章目录 第15章 编写大型程序15.1 源文件15.2 头文件15.2.1 #include指令15.2.2 共享宏定义和类型定义15.2.3 共享函数原型15.2.4 共享变量声明15.2.5 嵌套包含15.2.6 保护头文件15.2.7 头文件中的#error指令 15.3 把程序划分成多个文件15.4 构建多文件程序15.4.1 makefile15.4.2 链接期间的错误15.4.3 重新构建程序15.4.4 在程序外定义宏 问与答写在最后 第15章 编写大型程序 ——计算机领域的进步是很难找到恰当的时间单位来衡量的。有些大教堂用了一个世纪才建成。你能想象耗时如此之久的程序该有多么庞大、多么壮观吗 虽然某些C程序小得足够放入一个单独的文件中但是大多数程序不是这样的。程序由多个文件构成的原则更容易让人接受。 你将在本章看到常见的程序由多个源文件source file组成通常还有一些头文件header file。源文件包含函数的定义和外部变量而头文件包含可以在源文件之间共享的信息。15.1节讨论源文件15.2节详细地介绍头文件15.3节描述把程序分割成源文件和头文件的方法15.4节说明如何“构建”即编译和链接由多个文件组成的程序以及在改变程序的部分内容后如何“重新构建”。 15.1 源文件 到现在为止一直假设C程序是由单独一个文件组成的。事实上可以把程序分割成任意数量的源文件。根据惯例源文件的扩展名为.c。每个源文件包含程序的部分内容主要是函数和变量的定义。其中一个源文件必须包含一个名为main的函数此函数作为程序的起始点。 例如假设打算编写一个简单计算器程序用来计算按照逆波兰表示法reverse polish notation, RPN输入的整数表达式在逆波兰表示法中运算符都跟在操作数的后边。如果用户输入表达式30 5 - 7 *利用栈10.2节记录中间结果那么计算逆波兰表达式的值是很容易的。如果程序读取的是数就把此数压入栈。如果程序读取的是运算符则从栈顶弹出两个数进行相应的运算然后把结果压入栈。当程序执行到用户输入的末尾时表达式的值将在栈中。 例如程序将按照下列方式计算表达式30 5 – 7 *的值: 把30压入栈。把5压入栈。从栈顶弹出两个数用30减去5结果为25然后把此结果压回到栈中。把7压入栈。从栈顶弹出两个数将它们相乘然后把结果压回到栈中。 完成这些步骤后栈将包含表达式的值即175。程序的main函数将用循环来执行下列动作: 读取“记号”数或运算符。如果记号是数那么把它压入栈。如果记号是运算符那么从栈顶弹出它的操作数进行运算然后把结果压入栈中。 当像这样把程序分割成文件时将相关的函数和变量放入同一文件中是很有意义的。可以把读取记号的函数和任何需要用到记号的函数一起放到某个源文件比如说token.c中。push、pop、make_empty、is_empty和is_full这些与栈相关的函数可以放到另一个文件stack.c中。表示栈的变量也可以放入stack.c文件而main函数则可以在另一个文件calc.c中。 把程序分成多个源文件有许多显著的优点: 把相关的函数和变量分组放在同一个文件中可以使程序的结构清晰。可以分别对每一个源文件进行编译。如果程序规模很大而且需要频繁改变这一点在程序开发过程中是非常普遍的的话这种方法可以极大地节约时间。把函数分组放在不同的源文件中更利于复用。在示例中把stack.c和token.c从main函数中分离出来使得今后更容易复用栈函数和记号函数。 15.2 头文件 当把程序分割为几个源文件时问题也随之产生了某文件中的函数如何调用定义在其他文件中的函数呢函数如何访问其他文件中的外部变量呢两个文件如何共享同一个宏定义或类型定义呢答案取决于#include指令此指令使得在任意数量的源文件中共享信息成为可能这些信息可以是函数原型、宏定义、类型定义等。 #include指令告诉预处理器打开指定的文件并且把此文件的内容插入当前文件中。因此如果想让几个源文件可以访问相同的信息可以把此信息放入一个文件中然后利用#include指令把该文件的内容带进每个源文件中。按照此种方式包含的文件称为头文件有时称为包含文件。本节后面将更详细地讨论头文件。根据惯例头文件的扩展名为.h。 注意!! C标准使用术语“源文件”来指代程序员编写的全部文件包括.c文件和.h文件。本书中的“源文件”仅指.c文件。 15.2.1 #include指令 #include指令主要有2种书写格式。第一种格式用于属于C语言自身库的头文件 #include 文件名第二种格式用于所有其他头文件也包含任何自己编写的文件 #include 文件名这两种格式间的细微差异在于编译器定位头文件的方式。下面是大多数编译器遵循的规则: #include 文件名搜寻系统头文件所在的目录或多个目录。例如在UNIX系统中通常把系统头文件保存在目录/usr/include 中。#include 文件名先搜寻当前目录然后搜寻系统头文件所在的目录或多个目录。 通常可以改变搜寻头文件的位置这种改变经常利用如-I路径 这样的命令行选项来实现。 请注意!!不要在包含自己编写的头文件时使用尖括号 #include myheader.h /*** WRONG ***/这是因为预处理器可能在保存系统头文件的地方寻找myheader.h但显然是找不到的。 在#include指令中的文件名可以含有帮助定位文件的信息比如目录的路径或驱动器号 #include c:\cprogs\utils.h /* Windows path */ #include /cprogs/utils.h /* UNIX path */ 虽然#include指令中的双引号使得文件名看起来像字面串但是预处理器不会把它们作为字面串来处理。这是幸运的因为在上面的Windows例子中字面串中出现的\c和\u会被作为转义序列处理。 可移植性技巧通常最好的做法是在#include指令中不包含路径或驱动器的信息。当把程序转移到其他机器上或者更糟的情况是转移到其他操作系统上时这类信息会使编译变得很困难。 例如下面的这些#include指令指定了驱动器或路径信息而这些信息不可能一直是有效的 #include d:utils.h #include \cprogs\include\utils.h #include d:\cprogs\include\utils.h 下列这些指令相对好一些。它们没有指定驱动器而且使用的是相对路径而不是绝对路径 #include utils.h #include ..\include\utils.h #include指令还有一种不太常用的格式 #include 记号其中记号是任意预处理记号序列。预处理器会扫描这些记号并替换遇到的宏。宏替换完成以后#include指令的格式一定与前面两种之一相匹配。第三种#include指令的优点是可以用宏来定义文件名而不需要把文件名“硬编码”到指令里面去如下所示 #if defined(IA32) #define CPU_FILE ia32.h #elif defined(IA64) #define CPU_FILE ia64.h #elif defined(AMD64) #define CPU_FILE amd64.h #endif #include CPU_FILE15.2.2 共享宏定义和类型定义 大多数大型程序包含需要由几个源文件或者最极端的情况是用于全部源文件共享的宏定义和类型定义。这些定义应该放在头文件中。 例如假设正在编写的程序使用名为BOOL、TRUE和FALSE的宏。C99中不需要这么做因为stdbool.h头中定义了类似的宏。我们把这些定义放在一个名为boolean.h的头文件中这样做比在每个需要的源文件中重复定义这些宏更有意义 #define BOOL int #define TRUE 1 #define FALSE 0任何需要这些宏的源文件只需简单地包含下面这一行 #include boolean.h 类型定义在头文件中也是很普遍的。例如不用定义BOOL宏而是可以用typedef创建一个Bool类型。如果这样做boolean.h文件将有下列显示 #define TRUE 1 #define FALSE 0 typedef int Bool;把宏定义和类型定义放在头文件中有许多显而易见的好处。首先不把定义复制到需要它们的源文件中可以节约时间。其次程序变得更加容易修改。改变宏定义或类型定义只需要编辑单独的头文件而不需要修改使用宏或类型的诸多源文件。最后不需要担心由于源文件包含相同宏或类型的不同定义而导致的矛盾。 15.2.3 共享函数原型 假设源文件包含函数f的调用而函数f是定义在另一个文件foo.c中的。调用没有声明的函数f是非常危险的。如果没有函数原型可依赖编译器会假定函数f的返回类型是int类型的并假定形式参数的数量和函数f的调用中的实际参数的数量是匹配的。通过默认实参提升9.3节实际参数自身自动转换为“标准格式”。编译器的假定很可能是错误的但是因为一次只能编译一个文件所以是没有办法进行检查的。如果这些假定是错误的那么程序很可能无法工作而且没有线索可以用来查找原因。基于这个原因C99禁止在编译器看到函数声明或定义之前对函数进行调用。 请注意!!当调用在其他文件中定义的函数f时要始终确保编译器在调用之前已看到函数f的原型。 我们的第一个想法是在调用函数f的文件中声明它。这样可以解决问题但是可能产生维护方面的“噩梦”。假设有50个源文件要调用函数f如何能确保函数f的原型在所有文件中都一样呢如何能保证这些原型和foo.c文件中函数f的定义相匹配呢如果以后函数f发生了改变如何能找到所有用到此函数的文件呢 解决办法是显而易见的把函数f的原型放进一个头文件中然后在所有调用函数f的地方包含这个头文件。既然在文件foo.c中定义了函数f我们把头文件命名为foo.h。除了在调用函数f的源文件中包含foo.h还需要在foo.c中包含它从而使编译器可以验证foo.h中函数f的原型和foo.c中f的函数定义相匹配。 请注意!!在含有函数f定义的源文件中始终包含声明函数f的头文件。如果不这样做则可能导致难以发现的错误因为在程序别处对函数f的调用可能会和函数f的定义不匹配。 如果文件foo.c包含其他函数大多数函数应该在包含函数f的声明的那个头文件中声明。毕竟文件foo.c中的其他函数大概会与函数f有关。任何含有函数f调用的文件都可能会需要文件foo.c中的其他一些函数。然而仅用于文件foo.c的函数不需要在头文件中声明如果声明则容易造成误解。 为了说明头文件中函数原型的使用一起回到15.1节的RPN计算器示例。文件stack.c包含函数make_empty、is_empty、is_full、push和pop的定义。这些函数的原型应该放在头文件stack.h中 void make_empty(void); int is_empty(void); int is_full(void); void push(int i); int pop(void);为了避免使示例复杂化函数is_empty和函数is_full将不再返回Boolean类型值而返回int类型值。文件calc.c中将包含stack.h以便编译器检查在后面的文件中出现的栈函数的任何调用。文件stack.c中也将包含stack.h以便编译器验证stack.h中的函数原型是否与stack.c中的定义相匹配。 15.2.4 共享变量声明 外部变量10.2节在文件中共享的方式与函数的共享很类似。为了共享函数要把函数的定义放在一个源文件中然后在需要调用此函数的其他文件中放置声明。共享外部变量的方法和此方式非常类似。 目前不需要区别变量的声明和它的定义。为了声明变量i可以这样写 int i; /* declares i and defines it as well */ 这样不仅声明i是int类型的变量而且也对i进行了定义从而使编译器为i留出了空间。为了声明变量i而不是定义它需要在变量声明的开始处放置extern关键字18.2节 extern int i; /* declares i without defining it */ extern告诉编译器变量i是在程序中的其他位置定义的很可能是在不同的源文件中因此不需要为i分配空间。 顺便说一句extern可以用于所有类型的变量。在数组的声明中使用extern时可以省略数组的长度 extern int a[]; 因为此刻编译器不用为数组a分配空间所以也就不需要知道数组a的长度了。 为了在几个源文件中共享变量i首先把变量i的定义放置在一个文件中 int i;如果需要对变量i初始化可以把初始化器放在这里。在编译这个文件时编译器会为变量i分配内存空间而其他文件将包含变量i的声明 extern int i; 通过在每个文件中声明变量i使得在这些文件中可以访问或修改变量i。然而由于关键字extern的存在编译器不会在每次编译这些文件时都为变量i分配额外的内存空间。 当在文件中共享变量时会面临和共享函数时相似的挑战确保变量的所有声明和变量的定义一致。 请注意!!当同一个变量的声明出现在不同文件中时编译器无法检查声明是否和变量定义相匹配。例如一个文件可以包含定义 int i;同时另一个文件包含声明 extern long i;这类错误可能导致程序的行为异常。 为了避免声明和变量的定义不一致通常把共享变量的声明放置在头文件中。需要访问特定变量的源文件可以包含相应的头文件。此外含有变量定义的源文件需要包含含有相应变量声明的头文件这样编译器就可以检查声明与定义是否匹配。 虽然在文件中共享变量是C语言界中的长期惯例但是它有重大的缺陷。在19.2节中你将看到存在的问题并且学习如何设计不需要共享变量的程序。 15.2.5 嵌套包含 头文件自身也可以包含#include指令。虽然这种做法可能看上去有点奇怪但实际上是十分有用的。思考含有下列原型的stack.h文件 int is_empty(void); int is_full(void);由于这些函数只能返回0或1将它们的返回类型声明为Bool类型而不是int类型是一个很好的主意: Bool is_empty(void); Bool is_full(void);当然我们需要在stack.h中包含文件boolean.h以便在编译stack.h时可以使用Bool的定义。在C99中应包含stdbool.h而不是boolean.h并把这两个函数的返回类型声明为bool而不是Bool。 传统上C程序员避免使用嵌套包含。C语言的早期版本根本不允许嵌套包含。但是这种对嵌套包含的偏见正在逐渐减弱一个原因就是嵌套包含在C语言中很普遍。 15.2.6 保护头文件 如果源文件包含同一个头文件两次那么可能产生编译错误。当头文件包含其他头文件时这种问题十分普遍。例如假设file1.h包含file3.hfile2.h包含file3.h而prog.c同时包含file1.h和file2.h那么在编译prog.c时file3.h就会被编译2次。 两次包含同一个头文件不总是会导致编译错误。如果文件只包含宏定义、函数原型和/或变量声明那么不会有任何困难。然而如果文件包含类型定义则会导致编译错误。 安全起见保护全部头文件避免多次包含可能是个好主意那样的话可以在稍后添加类型定义不用冒因忘记保护文件而可能产生的风险。此外在程序开发期间避免同一个头文件的不必要重复编译可以节省一些时间。 为了防止头文件多次包含用#ifndef和#endif指令来封闭文件的内容。例如可以用如下方式保护文件boolean.h #ifndef BOOLEAN_H #define BOOLEAN_H #define TRUE 1 #define FALSE 0 typedef int Bool; #endif在首次包含这个文件时没有定义宏BOOLEAN_H所以预处理器允许保留#ifndef和#endif之间的多行内容。但是如果再次包含此文件那么预处理器将把#ifndef和#endif之间的多行内容删除。 宏的名字BOOLEAN_H并不重要但是给它取类似于头文件名的名字是避免和其他的宏冲突的好方法。由于不能把宏命名为BOOLEAN.H标识符不能含有句点像BOOLEAN_H这样的名字是个很好的选择。 15.2.7 头文件中的#error指令 #error指令14.5节经常放置在头文件中用来检查不应该包含头文件的条件。例如如果头文件中用到了一个在最初的C89标准之前不存在的特性为了避免把头文件用于旧的非标准编译器可以在头文件中包含#ifdef指令来检查__STDC__宏14.3节是否存在 #ifndef __STDC__ #error This header requires a Standard C compiler #endif15.3 把程序划分成多个文件 现在应用我们已经知道的关于头文件和源文件的知识来开发一种把一个程序划分成多个文件的简单方法。这里将集中讨论函数但是同样的规则也适用于外部变量。假设已经设计好程序换句话说已经决定程序需要什么函数以及如何把函数分为逻辑相关的组。第19章将讨论程序设计。 下面是处理的方法。把每个函数集合放入一个不同的源文件中比如用名字foo.c来表示一个这样的文件。另外创建和源文件同名的头文件只是扩展名为.h在此例中头文件是foo.h。在foo.h文件中放置foo.c中定义的函数的函数原型。在foo.h文件中不需要也不应该声明只在foo.c内部使用的函数。下面的read_char函数就是一个这样的例子。每个需要调用定义在foo.c文件中的函数的源文件都应包含foo.h文件。此外foo.c文件也应包含foo.h文件这是为了编译器可以检查foo.h文件中的函数原型是否与foo.c文件中的函数定义相一致。 main函数将出现在某个文件中这个文件的名字与程序的名字相匹配。如果希望称程序为bar那么main函数就应该在文件bar.c中。main函数所在的文件中也可以有其他函数前提是程序中的其他文件不会调用这些函数。 为了说明刚刚论述的方法现在把它用于一个小型的文本格式化程序justify。我们用一个名为quote的文件作为justify的输入样例quote文件包含下列未格式化的引语这些引语来自Dennis M. Ritchie写的“The Development of the C programming language”一文参见History of Programming Language II一书由T. J. Bergin, Jr.和R. G. Gibson, Jr.编写第671~687页 C is quirky, flawed, and an enormous success. Although accidents of history surely helped, it evidently satisfied a need for a system implementation language efficient enough to displace assembly language, yet sufficiently abstract and fluent to describe algorithms and interactions in a wide variety of environments. -- Dennis M. Ritchie为了在UNIX或Windows的命令行环境下运行这个程序输入命令 justify quote# 请注意!! windows系统下使用cmd执行上述命令powershell会报错符号告诉操作系统程序justify将从文件quote而不是从键盘读取输入。由UNIX、Windows和其他操作系统支持的这种特性称为输入重定向input redirection22.1节。当用给定的文件quote作为输入时程序justify将产生下列输出 C is quirky, flawed, and an enormous success. Although accidents of history surely helped, it evidently satisfied a need for a system implementation language efficient enough to displace assembly language, yet sufficiently abstract and fluent to describe algorithms and interactions in a wide variety of environments. -- Dennis M. Ritchie 程序justify的输出通常显示在屏幕上但是也可以利用输出重定向output redirection22.1节把结果保存到文件中 justify quote newquote程序justify的输出将放入到文件newquote中。 通常情况下justify的输出应该和输入一样区别仅在于删除了额外的空格和空行并对代码行做了填充和调整。“填充”行意味着添加单词直到再多加一个单词就会导致行溢出时才停止“调整”行意味着在单词间添加额外的空格以便于每行有完全相同的长度60个字符。必须进行调整只有这样一行内单词间的间隔才是相等的或者几乎是相等的。对输出的最后一行不进行调整。 假设没有单词的长度超过20个字符。把与单词相邻的标点符号看作单词的一部分。当然这样是做了一些限制不过一旦完成了程序的编写和调试我们就可以很容易地把这个长度上限增加到一个事实上不可能超越的值。如果程序遇到较长的单词它需要忽略前20个字符后的所有字符用一个星号替换它们。例如单词 antidisestablishmentarianism 将显示成 antidisestablishment* 现在明白了程序应该完成的内容接下来该考虑如何设计了。首先发现程序不能像读单词一样一个一个地写单词而必须把单词存储在一个“行缓冲区”中直到足够填满一行。在进一步思考之后我们决定程序的核心将是如下所示的循环 for (;;) { 读单词;if (不能读单词) { 输出行缓冲区的内容不进行调整; 终止程序; } if (行缓冲区已经填满){ 输出行缓冲区的内容进行调整; 清除行缓冲区; } 往行缓冲区中添加单词; }因为我们需要函数处理单词并且还需要函数处理行缓冲区所以把程序划分为3个源文件。把所有和单词相关的函数放在一个文件word.c中把所有和行缓冲区相关的函数放在另一个文件line.c中第3个文件fmt.c将包含main函数。除了上述这些文件还需要两个头文件word.h和line.h。头文件word.h将包含word.c文件中函数的原型而头文件line.h将包含line.c文件中函数的原型。 通过检查主循环可以发现我们只需要一个和单词相关的函数——read_word。如果read_word函数因为到了输入文件末尾而不能读入单词那么将通过假装读取“空”单词的方法通知主循环。因此文件word.h是一个短小的文件 /* word.h */ #ifndef WORD_H #define WORD_H /************************************************************ * read_word: Reads the next word from the input and ** stores it in word. Makes word empty if no ** word could be read because of end-of-file. ** Truncates the word if its length exceeds ** len. *************************************************************/ void read_word(char *word, int len); #endif 注意宏WORD_H是如何防止多次包含word.h文件的。虽然word.h文件不是真的需要它但是以这种方式保护所有头文件是一个很好的习惯。 文件line.h不会像word.h那样短小。主循环的轮廓显示了需要执行下列操作的函数: 输出行缓冲区的内容不进行调整。检查行缓冲区中还剩多少字符。输出行缓冲区的内容进行调整。清除行缓冲区。往行缓冲区中添加单词。 我们将要调用下面这些函数flush_line、space_remaining、write_line、clear_line和add_word。下面是头文件line.h的内容: /* line.h */ #ifndef LINE_H #define LINE_H/********************************************************** * clear_line: Clears the current line. * **********************************************************/ void clear_line(void); /********************************************************** * add_word: Adds word to the end of the current line. * * If this is not the first word on the line, * * puts one space before word. * **********************************************************/ void add_word(const char *word); /********************************************************** * space_remaining: Returns the number of characters left * * in the current line. * **********************************************************/ int space_remaining(void); /********************************************************** * write_line: Writes the current line with * * justification. * **********************************************************/ void write_line(void); /********************************************************** * flush_line: Writes the current line without * * justification. If the line is empty, does * * nothing. * **********************************************************/ void flush_line(void); #endif在编写文件word.c和文件line.c之前可以用在头文件word.h和头文件line.h中声明的函数来编写主程序justify.c。编写这个文件的主要工作是把原始的循环设计翻译成C语言: /* justify.c --Formats a file of text */ #include string.h #include line.h #include word.h #define MAX_WORD_LEN 20 int main(void) { char word[MAX_WORD_LEN2]; int word_len; clear_line(); for (;;) { read_word(word, MAX_WORD_LEN1); word_len strlen(word); if (word_len 0) { flush_line(); return 0; } if (word_len MAX_WORD_LEN) word[MAX_WORD_LEN] *; if (word_len 1 space_remaining()) { write_line(); clear_line(); } add_word(word); } }包含line.h和word.h可以使编译器在编译justify.c时能够访问到这两个文件中的函数原型。 main函数用了一个技巧来处理超过20个字符的单词。在调用read_word函数时main函数告诉read_word截短任何超过21个字符的单词。当read_word函数返回后main函数检查word包含的字符串长度是否超过20个字符。如果超过了那么读入的单词必须至少是21个字符长在截短前所以main函数会用星号来替换第21个字符。 现在开始编写word.c程序。虽然头文件word.h只有一个read_word函数的原型但是如果需要我们可以在word.c中放置更多的函数。不难看出如果添加一个小的“辅助”函数read_char函数read_word的编写就容易一些了。read_char函数的任务就是读取一个字符如果是换行符或制表符则将其转换为空格。在read_word函数中调用read_char函数而不是getchar函数就解决了把换行符和制表符视为空格的问题。下面是文件word.c /* word.c */ #include stdio.h #include word.h int read_char(void) { int ch getchar(); if (ch \n || ch \t) return ; return ch; } void read_word(char *word, int len) { int ch, pos 0; while ((ch read_char()) ) ; while (ch ! ch ! EOF) { if (pos len) word[pos] ch; ch read_char(); } word[pos] \0; }在讨论read_word函数之前先对read_char函数中的getchar函数的使用讲两点: 第一getchar函数实际上返回的是int类型值而不是char类型值因此read_char函数中把变量ch声明为int类型而且read_char函数的返回类型也是int。第二当不能继续读入时通常因为读到了输入文件的末尾getchar的返回值为EOF22.4节。 read_word函数由两个循环构成。第一个循环跳过空格在遇到第一个非空白字符时停止。EOF不是空白所以循环在到达输入文件的末尾时停止。第二个循环读字符直到遇到空格或EOF时停止。循环体把字符存储到word中直到达到len的限制时停止。在这之后循环继续读入字符但是不再存储这些字符。read_word函数中的最后一个语句以空字符结束单词从而构成字符串。如果read_word在找到非空白字符前遇到EOFpos将为0从而使得word为空字符串。 唯一剩下的文件是line.c。这个文件提供在文件line.h中声明的函数的定义。line.c文件也会需要变量来跟踪行缓冲区的状态。一个变量line将存储当前行的字符。严格地讲line是我们需要的唯一变量。然而出于对速度和便利性的考虑还将用到另外两个变量line_len当前行的字符数量和num_words当前行的单词数量。下面是文件line.c /* line.c */ #include stdio.h #include string.h #include line.h #define MAX_LINE_LEN 60 char line[MAX_LINE_LEN1]; int line_len 0; int num_words 0; void clear_line(void) { line[0] \0; line_len 0; num_words 0; } void add_word(const char *word) { if (num_words 0) { line[line_len] ; line[line_len1] \0; line_len; } strcat(line, word); line_len strlen(word); num_words; } int space_remaining(void) { return MAX_LINE_LEN - line_len; } void write_line(void) { int extra_spaces, spaces_to_insert, i, j; extra_spaces MAX_LINE_LEN - line_len; for (i 0; i line_len; i) { if (line[i] ! ) putchar(line[i]); else { spaces_to_insert extra_spaces / (num_words - 1); for (j 1; j spaces_to_insert 1; j) putchar( ); extra_spaces - spaces_to_insert; num_words--; } } putchar(\n); } void flush_line(void) { if (line_len 0) puts(line); }文件line.c中大多数函数很容易编写唯一需要技巧的函数是write_line。这个函数用来输出一行内容并进行调整。函数write_line向line中一个一个地写字符如果需要添加额外的空格那么就在每对单词之间停顿。额外空格的数量存储在变量spaces_to_insert中这个变量的值由extra_spaces / (num_words -1)确定其中extra_spaces初始值是最大行长度和当前行长度的差。因为在打印每个单词之后extra_spaces和num_words都发生变化所以spcaes_to_insert也将变化。如果extra_spaces初始值为10并且num_words初始值为5那么第1个单词之后将有2个额外的空格第2个单词之后将有2个额外的空格第3个单词之后将有3个额外的空格第4个单词之后将有3个额外的空格。 15.4 构建多文件程序 在2.1节中我们研究了对单个文件的程序进行编译和链接的过程。现在将把这种讨论推广到由多个文件构成的程序中。构建大型程序和构建小程序所需的基本步骤相同: 编译。必须对程序中的每个源文件分别进行编译。不需要编译头文件。编译包含头文件的源文件时会自动编译头文件的内容。对于每个源文件编译器会产生一个包含目标代码的文件。这些文件称为目标文件object file在UNIX系统中的扩展名为.o在Windows系统中的扩展名为.obj。链接。链接器把上一步产生的目标文件和库函数的代码结合在一起生成可执行的程序。链接器的一个职责是解决编译器遗留的外部引用问题。外部引用发生在一个文件中的函数调用另一个文件中定义的函数或者访问另一个文件中定义的变量时。 大多数编译器允许一步构建程序。例如对于GCC来说可以使用下列命令行来构建15.3节中的justify程序 gcc –o justify justify.c line.c word.c首先把三个源文件编译成目标代码然后自动把这些目标文件传递给链接器链接器会把它们结合成一个文件。选项-o表明我们希望可执行文件的名字是justify。 15.4.1 makefile 把所有源文件的名字放在命令行中很快变得枯燥乏味。更糟糕的是如果重新编译所有源文件而不仅仅是最近修改过的源文件重新构建程序的过程中可能会浪费大量的时间。 为了更易于构建大型程序UNIX系统发明了makefile的概念这个文件包含构建程序的必要信息。makefile不仅列出了作为程序的一部分的那些文件而且还描述了文件之间的依赖性。假设文件foo.c包含文件bar.h那么就说foo.c“依赖于”bar.h因为修改bar.h之后将需要重新编译foo.c。 下面是针对程序justify而设的UNIX系统的makefile它用GCC进行编译和链接 justify: justify.o word.o line.o gcc -o justify justify.o word.o line.o justify.o: justify.c word.h line.h gcc -c justify.c word.o: word.c word.h gcc -c word.c line.o: line.c line.h gcc -c line.c 这里有4组代码行每组称为一条规则。每条规则的第一行给出了目标文件跟在后边的是它所依赖的文件。第二行是待执行的命令当目标文件所依赖的文件发生改变时需要重新构建目标文件此时执行第二行的命令。下面看一下前两条规则后两条类似。 在第一条规则中justify可执行程序是目标文件 justify: justify.o word.o line.o gcc -o justify justify.o word.o line.o 第一行说明justify依赖于justify.o、word.o和line.o这三个文件。在程序的上一次构建完成之后只要这三个文件中有一个发生改变justify都需要重新构建。下一行信息说明如何重新构建justify通过使用gcc命令链接三个目标文件。 在第二条规则中justify.o是目标文件 justify.o: justify.c word.h line.h gcc -c justify.c 第一行说明如果justify.c、word.h或line.h文件发生改变那么justify.o需要重新构建。提及word.h和line.h的理由是justify.c包含这两个文件它们的改变都可能会对justify.c产生影响。下一行信息说明如何更新justify.o通过重新编译justify.c。选项-c通知编译器把justify.c编译为目标文件但是不要试图链接它。 一旦为程序创造了makefile就能使用make实用程序来构建或重新构建该程序了。通过检查与程序中每个文件相关的时间和日期make可以确定哪个文件是过期的。然后它会调用必要的命令来重新构建程序。 如果你想试试make下面是一些需要了解的细节: makefile中的每个命令前面都必须有一个制表符不是一串空格。在我们的例子中命令看似缩进了8个空格但实际上是一个制表符。 makefile通常存储在一个名为Makefile或makefile的文件中。使用make实用程序时它会自动在当前目录下搜索具有这些名字的文件。 用下面的命令调用make make 目标其中目标是列在makefile中的目标文件之一。为了用我们的makefile构建justify可执行程序可以使用命令 make justify如果在调用make时没有指定目标文件则将构建第一条规则中的目标文件。例如命令 make将构建justify可执行程序因为justify是我们的makefile中的第一个目标文件。除了第一条规则的这一特殊性质外makefile中规则的顺序是任意的。 make非常复杂复杂到可以用整本书来介绍所以这里不打算深入研究它的复杂性。真正的makefile通常不像我们的示例那样容易理解。有很多方法可以减少makefile中的冗余使它们更容易修改。但是这些技术同时也极大地降低了它们的可读性。 顺便说一句不是每个人都用makefile的。其他一些程序维护工具也很流行包括一些集成开发环境支持的“工程文件”。 15.4.2 链接期间的错误 一些在编译期间无法发现的错误会在链接期间被发现。尤其是如果程序中丢失了函数定义或变量定义那么链接器将无法解析外部引用从而导致出现类似“undefined symbol”或“undefined reference”的消息。 链接器检查到的错误通常很容易修改。下面是一些最常见的错误起因: 拼写错误。如果变量名或函数名拼写错误那么链接器将进行缺失报告。例如如果在程序中定义了函数read_char但调用时把它写为read_cahr那么链接器将报告说缺失read_cahr函数。缺失文件。如果链接器不能找到文件foo.c中的函数那么它可能不知道存在此文件。这时就要检查makefile或工程文件来确保foo.c文件是列出了的。缺失库。链接器不可能找到程序中用到的全部库函数。UNIX系统中有一个使用了math.h的经典例子。在程序中简单地包含该头可能是不够的很多UNIX版本要求在链接程序时指明选项-lm这会导致链接器去搜索一个包含math.h函数的编译版本的系统文件。不使用这个选项可能会在链接时导致出现“undefined reference”消息。 15.4.3 重新构建程序 在程序开发期间极少需要编译全部文件。大多数时候我们会测试程序进行修改然后再次构建程序。为了节约时间重新构建的过程应该只对那些可能受到上一次修改影响的文件进行重新编译。 假设按照15.3节的框架方法设计了程序并对每一个源文件都使用了头文件。为了判断修改后需要重新编译的文件的数量我们需要考虑2种可能性: 第一种可能性是修改只影响一个源文件。这种情况下只有此文件需要重新编译。当然在此之后整个程序将需要重新链接。思考程序justify。假设要精简word.c中的函数read_char: int read_char(void) { int ch getchar(); return (ch \n || ch \t) ? : ch; } 这种改变没有影响word.h所以只需要重新编译word.c并且重新链接程序就行了。 第二种可能性是修改会影响头文件。这种情况下应该重新编译包含此头文件的所有文件因为它们都可能潜在地受到这种修改的影响。有些文件可能不会受到影响但是保守一点是值得的。 作为示例思考一下程序justify中的函数read_word。注意为了确定刚读入的单词的长度main函数在调用read_word函数后立刻调用strlen。因为read_word函数已经知道了单词的长度read_word函数的变量pos负责跟踪长度所以使用strlen就显得多余了。修改read_word函数来返回单词的长度是很容易的。首先改变word.h文件中read_word函数的原型 /*********************************************************** * read_word: Reads the next word from the input and * * stores it in word. Makes word empty if no * * word could be read because of end-of-file. * * Truncates the word if its length exceeds * * len. Returns the number of characters * * stored. * ***********************************************************/ int read_word(char *word, int len);当然要仔细修改read_word函数的注释。接下来修改word.c文件中read_word函数的定义 int read_word(char *word, int len) { int ch, pos 0; while ((ch read_char()) ) ; while (ch ! ch ! EOF) { if (pos len) word[pos] ch; ch read_char(); } word[pos] \0; return pos; }最后再来修改justify.c方法是删除对string.h的包含并按如下方式修改main函数 int main(void) { char word[MAX_WORD_LEN2]; int word_len; clear_line(); for (;;) { word_len read_word(word, MAX_WORD_LEN1); if (word_len 0) { flush_line(); return 0; } if (word_len MAX_WORD_LEN) word[MAX WORD LEN] *; if (word_len 1 space_remaining()) { write_line(); clear_line(); } add_word(word); } }一旦做了上述这些修改就需要重新构建程序justify方法是重新编译word.c和justify.c然后再重新链接。不需要重新编译line.c因为它不包含word.h所以也就不会受到word.h改变的影响。对于GCC可以使用下列命令来重新构建程序 gcc –o justify justify.c word.c line.o 注意这里用的是line.o而不是line.c。 使用makefile的好处之一就是可以自动重新构建。通过检查每个文件的日期make实用程序可以确定程序上一次构建之后哪些文件发生了改变。然后它会把那些改变的文件和直接或间接依赖于它们的全部文件一起重新编译。例如如果我们对word.h、word.c和justify.c进行了修改并重新构建了justify程序那么make将执行如下操作。 编译justify.c以构建justify.o因为修改了justify.c和word.c。编译word.c以构建word.o因为修改了word.c和word.h。链接justify.o、word.o和line.o以构建justify因为修改了justify.o和word.o。 15.4.4 在程序外定义宏 在编译程序时C语言编译器通常会提供一种指定宏的值的方法。这种能力使我们很容易对宏的值进行修改而不需要编辑程序的任何文件。当利用makefile自动构建程序时这种能力尤其有价值。 大多数编译器包括GCC支持-D选项此选项允许用命令行来指定宏的值 gcc –DDEBUG1 foo.c 在这个例子中定义宏DEBUG在程序foo.c中的值为1其效果相当于在foo.c的开始处这样写 #define DEBUG 1如果-D选项命名的宏没有指定值那么这个值被设为1。 许多编译器也支持-U选项这个选项用于删除宏的定义效果相当于#undef。我们可以使用-U选项来删除预定义宏14.3节或之前在命令行方式下用-D选项定义的宏的定义。 问与答 问1这里没有任何例子是使用#include指令来包含源文件的。如果这样做了会发生什么 答这是合法的但不是个好习惯。这里给出一个出问题的例子。假设 foo.c中定义了一个在bar.c和baz.c中需要用到的函数f我们在bar.c和baz.c中都加上了如下指令 #include foo.c这些文件都会很好地被编译。但当链接器发现函数f的目标代码有两个副本时问题就出现了。当然如果只是bar.c包含此函数而baz.c没有那么将没有问题。为了避免出现问题最好只用#include包含头文件而非源文件。 问2针对#include指令的精确搜索规则是什么 答这与所使用的编译器有关。C标准在#include的表述中故意模糊不清。如果文件名用尖括号围起来那么预处理器会到一些“由实现定义的地方”搜索。如果文件名用双引号围起来那么就“以实现定义的方式搜索”文件如果没有找到再按前一种方式搜索。原因很简单不是所有操作系统都有分层的树形的文件系统。 更加有趣的是标准根本不要求尖括号内的名字是文件名因此使用的#include指令有可能完全在编译器内部处理。 问3我不理解为什么每个源文件都需要它自己的头文件。为什么没有一个大的头文件包含宏定义、类型定义和函数原型呢通过包含这个文件每个源文件都可以访问所需要的全部共享信息。 答只用“一个大的头文件”确实可行许多程序员使用这种方法。而且这种方法有一个好处因为只有一个头文件所以要管理的文件较少。然而对于大型程序来说这种方法的坏处大于它的好处。 只使用一个头文件不能为以后阅读程序的人提供有用的信息。如果有多个头文件读者可以迅速了解到特定的源文件需要使用程序的其他哪些部分。 此外因为每个源文件都依赖于这个大的头文件所以改变它会导致要对全部源文件重新编译这是大型程序中的一个显著缺陷。更糟的是因为包含了大量信息所以头文件可能会频繁地改变。 问4本章说到共享数组应该按照下列方式声明 extern int a[];既然数组和指针关系密切那么用下列写法代替是否合法呢 extern int *a;答不合法。在用于表达式时数组“衰退”成指针。当数组名用作函数调用中的实际参数时我们已经注意到这种行为。但在变量声明中数组和指针是截然不同的两种类型。 问5如果源文件包含了不是真正需要的头会有损害吗 答不会除非头中的声明或定义与源文件中的冲突。否则可能发生的最坏情况就是在编译源文件时时间会有少量增加。 问6我需要调用文件foo.c中的函数所以包含了匹配的头文件foo.h。程序可以通过编译但是不能通过链接。为什么 答在C语言中编译和链接是完全独立的。头文件的存在是为了给编译器而不是给链接器提供信息。如果希望调用文件foo.c中的函数那么需要确保对foo.c进行了编译还要确保链接器知道必须在foo.c的目标文件中搜索该函数。通常情况下这就意味着在程序的makefile或工程文件中命名foo.c。 问7如果程序调用stdio.h中的函数这是否意味着stdio.h中的所有函数都将和程序链接呢 答不是的。包含stdio.h或者任何其他头对链接没有任何影响。在任何情况下大多数链接器只会链接程序实际需要的函数。 问8从哪里可以得到make实用程序 答make是标准的UNIX实用程序。GNU的版本称为GNU Make包含在大多数Linux发行版中也可以从自由软件基金会的网站上直接获取。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much
http://www.zqtcl.cn/news/179142/

相关文章:

  • 有网站怎样做推广精品网站源码资源程序下载
  • 怎么建设淘宝联盟的网站梧州网站设计公司
  • 注册查询官方网站网站建设pad版本是什么
  • 做网站先得注册域名吗网站cdn+自己做
  • 甘肃省建设厅网站非织梦做的网站能仿吗
  • 天元建设集团网站苏州门户网站建设
  • 建设网站需要学习什么语言福州优化搜索引擎
  • 网站开发大致多少钱手机上怎么制作网站吗
  • 重庆网站seo营销模板wordpress学习 知乎
  • 桃子网站logowordpress post meta
  • 做网站一般需要什么青岛网络推广
  • 东莞网站建设 光龙wordpress4.6 nodejs
  • 宁海县建设局网站网站建设行业前景
  • 2003网站的建设谷歌seo新手快速入门
  • 网站建设服务开发网页制作下载链接怎么做
  • 网站更改域名河源建网站
  • 陕西培训网站建设校园网站建设目的
  • 做网站赚钱容易吗怎么创建自己网站平台
  • 肥料网站建设江门好的建站网站
  • 女朋友在互联网公司做网站规范网络直播平台的可行性建议
  • wordpress酷站微信推广平台自己可以做
  • 下载类网站如何做wordpress 文章分页 插件
  • 什么做书籍的网站好梅县区住房和城乡规划建设局网站
  • 网站开发的研究方法网站内容规划流程
  • 什么网站可以做数据调查深圳住房城乡建设局网站
  • 民治网站建设yihe kj程序外包公司
  • 男人与女人做视频网站wordpress无法上传图片
  • 二手手表回收网站海外推广渠道有哪些
  • 怎么把地图放到网站上如何做色流量网站
  • 常见的导航网站有哪些郑州核酸vip服务