C语言模块化编程

1.多文件编译、链接的原理

在讲解 extern 和 static 关键字的时候,我们已经给出了几个简单的多文件编程的例子,现在不妨再看一个例子。

main.c 源码:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. // 也可以不写 extern;为了程序可读性,建议写上
  4. extern long sum(int, int);
  5. // 必须写 extern
  6. extern char* OS;
  7. int main()
  8. {
  9. int n1 = 1, n2 = 100;
  10. printf("从%d加到%d的和为%ld [By %s]", n1, n2, sum(n1, n2), OS);
  11. getch();
  12. return 0;
  13. }

module.c 源码:

  1. #include <stdio.h>
  2. // 当前操作系统
  3. char *OS = "Windows 7";
  4. long sum(int fromNum, int endNum){
  5. int i;
  6. long result = 0;
  7. // 参数不符合规则,返回 -1
  8. if(fromNum<0 || endNum<0 || endNum<fromNum){
  9. return -1;
  10. }
  11. for(i=fromNum; i<=endNum; i++){
  12. result += i;
  13. }
  14. // 返回大于等于0的值
  15. return result;
  16. }

运行结果:

从1加到100的和为5050 [By Windows 7]

这个程序,我们按照“编译 --> 链接 --> 运行”的步骤来生成,不要按 F5 或者 F7(对于 Visual C++)直接生成 exe。如果已经生成,可以清理掉相关文件。

注意:一个程序有且只能有一个 main() 函数,即使它有多个源文件。main() 函数是程序的入口函数,双击运行程序时就从这里开始执行。

编译和链接的原理

对于C,首先要把源文件编译(Compile)成目标文件(Object File),也就是Windows下的 .obj 文件。然后再把单个或多个 Object File 合并成可执行文件,也就是Windows下是
.exe 文件,这个动作叫作链接(Link)。

编译时,编译器需要检查语法是否正确,函数、变量的声明是否正确;只有函数、变量的声明但没有定义是完全正确的。函数声明是告诉编译器该函数已经存在,但是入口地址还未确定,暂时在此做个标记,链接时编译器会找到函数入口地址,并将标记替换掉。

这些都校验通过,编译器就可以编译出中间目标文件。一般来说,编译是针对单个源文件的,多个源文件需要编译多次,每个源文件都会生成一个对应的目标文件。

编译产生的 .obj 文件已经是二进制文件,与 .exe 的组织形式类似,只是有些函数的入口地址还未找到,程序不能执行。链接的作用就是找到函数入口地址,将所有的源文件组织成一个可以执行的二进制文件。

链接时,主要是链接函数和全局变量。链接器并不管函数或变量所在的源文件,只管中间目标文件(Object File)。

总结一下:源文件首先会生成中间目标文件,再由中间目标文件生成可执行文件。在编译时,编译器只检测程序语法、函数声明、变量声明是否正确。如果函数未被声明,编译器会给出一个警告,但可以生成Object
File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是 Link 2001 错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File。

一步一步生成 exe 文件

知道了编译链接的原理,接下来我们使用VC一步步的生成 .exe 文件。

首先切换到 main.c 面板,按下 Ctrl+F7 键(编译),打开项目目录下的 Release 或 Debug 文件夹,可以看到多出了一个 main.obj 文件,这就是 main.c
生成的目标文件。

再切换到 module.c 面板,进行同样的操作,会生成 module.obj 文件,至此编译完毕(所有的源文件都编译过了)。

最后,按下 F7 键(链接),就生成了 main.exe,双击运行就可以看到上面的输出结果了。

2.C语言模块化编程中的头文件

上节我们编写了
main.c 和 module.c 两个源文件,并在 module.c 中定义了一个函数和一个全局变量,然后在 main.c 中进行了声明。

不过实际开发中很少这样做,一般是将函数和变量的声明放到头文件,再在当前的源文件中 #include 进来。而且,全局变量最好声明为 static,只在当前文件中可见,不要对外暴露;如果必须对外暴露,可以使用宏定义代替,请看下面的代码。

main.c 源码:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. #include "module.h"
  4. int main()
  5. {
  6. int n1 = 1, n2 = 100;
  7. printf("从%d加到%d的和为%ld [By %s]", n1, n2, sum(n1, n2), OS);
  8. getch();
  9. return 0;
  10. }

module.c 源码:

  1. #include <stdio.h>
  2. long sum(int fromNum, int endNum){
  3. int i;
  4. long result = 0;
  5. // 参数不符合规则,返回 -1
  6. if(fromNum<0 || endNum<0 || endNum<fromNum){
  7. return -1;
  8. }
  9. for(i=fromNum; i<=endNum; i++){
  10. result += i;
  11. }
  12. // 返回大于等于0的值
  13. return result;
  14. }

module.h 源码:

  1. // 用宏定义来代替全局变量
  2. #define OS "Windows 7"
  3. // 也可以省略 extern;不过为了程序可读性,建议写上
  4. extern long sum(int, int);

运行结果:

从1加到100的和为5050 [By Windows 7]

与上节中的例子相比,我们用宏定义代替了全局变量,将函数声明和宏定义都放在了头文件 module.h,这样在 main.c 中只需要将 module.h 包含就来就可以。

.c 文件和 .h 文件都是源文件,除了后缀不一样便于区分外和管理外,其他的几乎相同,在 .c 中编写的代码同样也可以写在 .h 中,例如函数定义、预处理等。

但是 .h 文件和 .c 文件在项目中承担的角色不一样:.c 文件主要负责实现,也就是定义函数;.h 文件主要负责声明,比如函数声明、宏定义等。这些不是C语法规定的内容,而是约定成俗的规范。

下面是关于头文件的事实标准:

  • 可以声明函数,但不可以定义函数。
  • 可以申明常量,但不可以定义变量。
  • 可以“定义”一个宏函数。注意:宏函数很象函数,但却不是函数。其实还是一个声明。
  • 结构的定义、自定义数据类型一般也放在头文件中。
  • 除了主文件(有 main() 函数的文件),其他的 .c 文件一般只定义函数,并向外暴露(可以使用 extern,也可以不使用)。
  • 可以将一个或多个相关的函数定义在一个 .c 文件。

在很多场合,为了商业目的,项目的源代码不便(或不准)向用户公布,必须编译成 .obj 文件(二进制库)。我们只要向用户提供库文件和头文件就可以,用户会按照头文件中的函数的声明来调用库中的函数,而不需要知道函数是怎么实现的,这就很好的保护了我们的版权。

3.C语言标准库以及标准头文件

源文件通过编译可以生成
.obj 文件(二进制库文件),并提供一个头文件向外暴露接口,除了保护版权,还可以将散乱的文件打包,便于发布和使用。

实际上我们一般不直接向用户提供 .obj 文件,而是将多个 .obj 文件打包成 .lib 文件(静态库)或 .dll 文件(动态库)。

.obj 打包成 .lib 或 .dll 也要经过链接的过程来找到函数入口、变量声明等,在VC中可以直接创建相应的工程来生成(与创建 Win32 Console Application
类似,后续会讲解)。

.lib 和 .dll 可以看成是一堆 .obj 的集合,虽然有入口函数,但不能直接运行,必须被链接到 .exe 或被 .exe 调用。

C语言在发布时已经将常用的函数、宏、类型定义等打包到了静态库,并提供了相应的头文件。如果你使用的是VC,那么在安装目录下的 \VC98\Include\ 文件夹中会看到很多头文件,包括我们常用的
stdio.h、stdlib.h 等;在 \VC98\Lib\ 文件夹中有很多 .lib 文件,这就是我们链接时要用到的静态库。

例如我的 VC6.0 安装在 C:\Program Files\Microsoft Visual Studio\ 目录,那么 VC6.0 附带的所有头文件都在 C:\Program
Files\Microsoft Visual Studio\VC98\Include\ 目录下,所有 .lib 文件都在 C:\Program Files\Microsoft Visual Studio\VC98\Include\Lib\ 目录下。

如果忘记 VC6.0 的安装目录或者头文件不在安装目录下,可以通过以下方式找到:

1) 在工具栏中点击“工具”按钮

2) 在二级菜单中选择“选项”

3) 在弹出的对话框中选择“目录”标签

4) 然后选择名字为“目录”的下拉菜单中的“Include files”一项,如下图所示:

ANSI C 规范共定义了 15 个头文件,称为“C标准库”,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平:

  • 合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h>
  • 熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h>
  • 优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>

各个头文件的具体内容请查看:C语言标准库

除了C标准库,编译器一般也会附带自己的库,以增加功能,方便用户开发,争夺市场份额。这些库中的每一个函数都在对应的头文件中声明,可以通过 #include 预处理命令导入,编译时会被合并到当前文件。

注意:引入编译器自带的头文件(包括标准头文件)用尖括号,引入自定义头文件用双引号,例如:

复制纯文本新窗口
  1. #include <stdio.h>
  2. #include "myFile.h"

4.C语言头文件的特性和规范

头文件通过
#include 命令包含到当前文件,效果与直接复制头文件的内容相同;编译器在预处理阶段实际上也是这样做的。

不管是标准头文件还是我们自己编写的头文件,都应该遵循等幂性:可以多次包含相同的头文件,但效果与只包含一次相同。

等幂性很容易实现,对于大多数的头文件可以使用宏保护。例如,在 stdio.h 中可以有如下的宏定义:

  1. #ifndef _STDIO_H
  2. #define _STDIO_H
  3. /* 声明部分 */
  4. #endif

第一次包含头文件,会定义宏 _STDIO_H,并执行声明部分的代码;第二次因为已经定义了宏 _STDIO_H,不会重复执行声明部分的代码。也就是说,头文件只在第一次包含时起作用,再次包含无效。

C标准库的头文件,也具有相互独立性:任何标准头文件的正常工作都不需要以包含其他标准头文件为前提,也没有任何标准头文件包含了其他标准头文件。

在C程序员中所达成的一个约定是:C源文件的开头部分要包含所有要用到的头文件。在 #include 指令之前只能有一句注释语句。引入的头文件可以按任意顺序排列。

如果我们自己编写的头文件(例如a.h)会用到其他头文件(例如 b.h)中的定义或声明,也可以在 a.h 的开头 #include “b.h”。这样,就不会在 .c 文件中忘记包含
b.h,也不会有顺序问题。这正是利用了头文件的等幂性。

5.C语言头文件的路径

引入编译器自带的头文件(包括标准头文件)用尖括号,引入自定义头文件用双引号,例如:

  1. #include <stdio.h>
  2. #include "myFile.h"

这是由头文件的路径决定的。理论上,你可以将头文件放在磁盘上的任何位置,只要带路径包含进来就可以。下面我们以 VC 6.0 为例进行讲解。

VC 6.0 在安装时会自动添加多个环境变量。例如我的 VC 6.0 安装在 C:\Program Files\Microsoft Visual Studio 目录下,那么:

  • 环境变量 include 中会包含 C:\Program Files\Microsoft Visual Studio\VC98\include,用以指向头文件所在的目录(编译时需要头文件);
  • 环境变量 lib 中会包含 C:\Program Files\Microsoft Visual Studio\VC98\lib,用以指向静态库所在的目录(链接时需要静态库)。

使用尖括号,编译器会到环境变量 include 指定的目录去查找头文件。

使用双引号,编译器首先会在当前目录查找,找到就包含就来,找不到再到环境变量 include 指定的目录去查找。

注意:这里说的当前目录不是指当前工程的根目录,而是要编译的文件所在的目录。

例如我们通过 VC 6.0 创建了一个工程,命名为 demo,保存在 E:\demo(工程根目录)。现在 E:\demo 目录下有 main.c,E:\demo\include
下有 func.h,E:\demo\module 下有 func.c,如下图所示:

如果 main.c 需要包含 func.h,就要:

  1. #include "include/func.h"

如果 func.c 需要包含 func.h,就要:

  1. #include "../include/func.h" /* ../ 表示上级目录 */

注意,包含头文件时你可以使用正斜杠(/),也可以使用反斜杠(/),它们都可以表示目录的层次。

上面说的是相对路径,以当前文件所在的目录为起点开始查找。一般情况下头文件都在工程目录,使用相对路径比较方便。

如果头文件不在工程目录并且离当前文件比较远,你也可以使用绝对路径,例如:

  1. #include "E:\demo\include\func.h"

绝对路径以分区(盘符)为起点开始查找。

6.一个比较规范的C语言多文件编程的例子

这一节向大家展示一个比较规范的多文件编程的例子,将前面几节的知识运用起来。

通过 VC 6.0 创建一个工程,保存到 E:\demo 目录,工程文件有:

main.c 源码:

  1. #include <stdio.h>
  2. #include <conio.h>
  3. #include "include/func.h"
  4. int main()
  5. {
  6. int n1 = 1, n2 = 10;
  7. printf("从%d加到%d的和为%ld\n", n1, n2, sum(n1, n2));
  8. printf("从%d乘到%d的积为%ld\n", n1, n2, mult(n1, n2));
  9. printf("OS:%s\n",OS);
  10. printf("Power By %s(%s)", getWebName(), getWebURL());
  11. getch();
  12. return 0;
  13. }

math.c 源码:

  1. // 没有使用到 func.h 中的函数声明或宏定义,也可以不包含进来
  2. #include "../include/func.h"
  3. // 从 fromNum 加到 endNum
  4. long sum(int fromNum, int endNum){
  5. int i;
  6. long result = 0;
  7. // 参数不符合规则,返回 -1
  8. if(fromNum<0 || endNum<0 || endNum<fromNum){
  9. return -1;
  10. }
  11. for(i=fromNum; i<=endNum; i++){
  12. result += i;
  13. }
  14. // 返回大于等于0的值
  15. return result;
  16. }
  17. // 从 fromNum 乘到 endNum
  18. long mult(int fromNum, int endNum){
  19. int i;
  20. long result = 1;
  21. // 参数不符合规则,返回 -1
  22. if(fromNum<0 || endNum<0 || endNum<fromNum){
  23. return -1;
  24. }
  25. for(i=fromNum; i<=endNum; i++){
  26. result *= i;
  27. }
  28. // 返回大于等于0的值
  29. return result;
  30. }

web.c 源码:

  1. // 使用到了 func.h 中的宏定义,必须包含进来,否则编译错误
  2. #include "../include/func.h"
  3. char* getWebName(){
  4. return WEB_NAME;
  5. }
  6. char* getWebURL(){
  7. return WEB_URL;
  8. }

func.h 源码:

  1. #ifndef _FUNC_H
  2. #define _FUNC_H
  3. // 用宏定义来代替全局变量
  4. #define OS "Windows 7"
  5. #define WEB_URL "http://c.biancheng.net"
  6. #define WEB_NAME "C语言中文网"
  7. // 也可以省略 extern,不过为了程序可读性,建议都写上
  8. extern long sum(int, int);
  9. extern long mult(int, int);
  10. extern char* getWebName();
  11. extern char* getWebURL();
  12. #endif

运行结果:

从1加到10的和为55

从1乘到10的积为3628800

OS:Windows 7

Power By C语言中文网(http://c.biancheng.net)

时间: 2024-08-29 08:50:59

C语言模块化编程的相关文章

嵌入式 Linux C语言(十一)——C语言模块化编程

嵌入式 Linux C语言(十一)--C语言模块化编程 一.C语言模块化编程 所谓模块化编程,就是指一个程序包含多个源文件(.c 文件和 .h 文件),每个模块即是一个.c文件和一个.h文件的结合,头文件(.h)中是对于该模块接口的声明.C语言模块化编程中对.c..h文件的潜规则: 1..c 文件主要负责实现,也就是定义函数:.h 文件主要负责声明,比如函数声明.宏定义等,结构的定义.自定义数据类型一般也放在头文件中,不能在.h文件中定义变量.将一个功能模块的代码单独编写成一个.c文件,然后把该

【C语言探索之旅】 第二部分第一课:模块化编程

内容简介 1.课程大纲 2.第二部分第一课: 模块化编程 3.第二部分第二课预告: 进击的指针,C语言王牌 课程大纲 我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案.还会带大家用C语言编写三个游戏. C语言编程基础知识 什么是编程? 工欲善其事,必先利其器 你的第一个程序 变量的世界 运算那点事 条件表达式 循环语句 实战:第一个C语言小游戏 函数 练习题 习作:完善第一个C语言小游戏 C语言高级技术 模块化编程 进击的指针,C语言王牌 数组 字符串 预处理 创建你自己的变量

Javascript模块化编程(二):AMD规范

作者: 阮一峰 日期: 2012年10月30日 这个系列的第一部分介绍了Javascript模块的基本写法,今天介绍如何规范地使用模块. (接上文) 七.模块的规范 先想一想,为什么模块很重要? 因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块. 但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了. 目前,通行的Javascript模块规范共有两种

JS的模块化编程

今天学了一下JS的模块化编程,感觉JavaScript真的是博大精深,CommonJS,requireJS,NodeJS,Seajs,在此记录一下经验.JavaScript设计之初并不是一种模块化编程语言,不支'类'和'模块'的概念,但ES6中却将正式支持"类"和"模块".有了模块,我们可以更方便地使用别人的代码,想要什么功能,就加载什么模块. 不过这样做的前提是大家必须以同样的方式编写模块,考虑到Javascript模块现在还没有官方规范,因此各种规范应运而生.

黑马程序员---C基础6【#include指令】【模块化编程】【计算机的进制】【原码、反码、补码】【位运算符】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- [#include指令] 1.文件包含命令的格式: 1)#include “”双引号是包含用户自己书写定义的文件(可以是头文件,也可以是普通的文件)#include是预处理指令,不是一个语句,不需要加封号 2)#include<>   包含一个系统(编译器自带)的头文件 2.文件包含的实质: 把指定文件内容插入该命令行位置取代该命令行, include不一定非要写在第一行: 3.includ

Javascript模块化编程

(一):模块的写法 网页越来越像桌面程序,需要一个团队分工协作.进度管理.单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑. Javascript模块化编程,已经成为一个迫切的需求.理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块. 但是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了.(正在制定中的ECMAScript标准第六版,将正式支持"

C语言模块化编译介绍

C语言模块化编译介绍 模块化编程的概念 所谓模块化变成(多文件开发),就是多文件(.c文件)编程,一个.c文件和一个.h文件可以被称为一个模块. 头文件开发的注意事项: 1)头文件中可以和C程序一样引用其它头文件,可以写预处理块,但不要写具体的语句. 2 可以声明函数,但不可以定义函数 3 可以声明常量,但不可以定义变量 4)可以“定义”一个宏函数.注意:宏函数很象函数,但却不是函数.其实还是一个申明. 5)结构的定义.自定义数据类型一般也放在头文件中. 6)多文件编程时,只能有一个文件包含 m

Javascript模块化编程(二):AMD规范(转)

这个系列的第一部分介绍了Javascript模块的基本写法,今天介绍如何规范地使用模块. (接上文) 七.模块的规范 先想一想,为什么模块很重要? 因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块. 但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了. 目前,通行的Javascript模块规范共有两种:CommonJS和AMD.我主要介绍AMD,但

初步理解require.js模块化编程

初步理解require.js模块化编程 一.Javascript模块化编程 目前,通行的Javascript模块规范共有两种:CommonJS和AMD. 1.commonjs 2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程,这标志”Javascript模块化编程”正式诞生. 在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限:但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程. node.j