文档库 最新最全的文档下载
当前位置:文档库 › 第十五章 C语言模块化编程,C语言多文件编译

第十五章 C语言模块化编程,C语言多文件编译

第十五章  C语言模块化编程,C语言多文件编译
第十五章  C语言模块化编程,C语言多文件编译

第十五章C语言模块化编程,C语言多文件编译

所谓模块化编程,就是多文件(.c文件)编程,一个 .c 文件和一个 .h 文件可以被称为一个模块。

如果你学习C语言很久了,还只会在一个 .c 文件里写代码,那么本章将会让你突破,带你了解多文件编程,教你学会如果有效的组织各个文件,如果将各个文件联系起来。

15.1 C语言头文件深入理解

C语言程序中,源文件通常分为两种:一种用于保存程序的声明(declaration),称为头文件;另一种用于保存程序的实现(implementation),称为定义(definition)文件。 C程序的头文件以“.h”为后缀,C 程序的定义文件以“.c”为后缀。

可以将 .h 文件的内容写在 .c 文件中,也可以将 .c 文件的内容写在 .h 中,但这是很不好的习惯。许多初学者用了头文件,却不明其理。在此略作说明。在以下场景中会使用头文件:

?通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。

?多文件编译。将稍大的项目分成几个文件实现,通过头文件将其他文件的函数声明引入到当前文件。

?头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。

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

编译产生的 .obj 文件(Linux下为 .o 文件)已经是二进制文件,与 .exe 的组织形式类似,只是有些函数的入口地址还未找到,程序不能执行。链接的作用就是找到函数入口地址,将所有的源文件组织成一个可以执行的二进制文件。关于头文件的内容,初学者还必须注意:

?头文件中可以和C程序一样引用其它头文件,可以写预处理块,但不要写具体的语句。

?可以申明函数,但不可以定义函数。

?可以申明常量,但不可以定义变量。

?可以“定义”一个宏函数。注意:宏函数很象函数,但却不是函数。其实还是一个申明。

?结构的定义、自定义数据类型一般也放在头文件中。

?#include ,编译系统会到环境指定的目录去引用。#include "filename.h",系统一般首先在当前目录查找,然后再去环境指定目录查找。

好的风格是成功的关键,版本申明、函数功能说明、注释等是C语言程序的一部分。不养成很好的习惯则不能成为C语言高手(专业人员)。

C标准库中,每一个库函数都在一个头文件中声明,可以通过 #include 预处理命令导入。

头文件只是声明,不占内存空间,编译时会被合并到源文件;要想知道它的具体实现,要看头文件所声明的函数是在哪个 .c 文件里定义的,然后查看源代码。C标准库共包含 15 个头文件,可以分为 3 组,如何正确并熟练的使用它们,可以相应的可区分出 3 个层次的程序员:

?合格程序员:

?熟练程序员:

?优秀程序员:

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

C语言头文件具有以下几个特性:

?幂等性。可以多次包含相同的标准头文件,但效果与只包含一次相同。

?相互独立。任何标准头文件的正常工作都不需要以包含其他标准头文件为前提。也

没有任何标准头文件包含了其他标准头文件。

?和文件级别的声明等同。必须先把某标准头文件包含到你的程序中,然后才能使用该头文件已定义或声明的东西。不能在声明中包含标准头文件。并且,也不能在包含标准头文件之前用宏定义去代替关键字。

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

复制纯文本新窗口

1#ifndef_STDIO_H

2#define_STDIO_H

3/* 主要实现部分*/

4#endif

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

如果我们自己编写的头文件可能会用到标准头文件中的定义或者声明,最好把标准头文件包含在自定义头文件的开头。这样,就不会在程序中忘记引入该标准头文件,也不会有顺序问题。这正是利用了头文件的等幂性。

注意一个约定,引入标准头文件用尖括号,引入自定义头文件用双引号,例如:

复制纯文本新窗口

01 #include

02 #include"myFile.h"

C语言库函数是头文件的最佳实践,仔细阅读各个头文件的内容,尤其是stdio.h,能够学到很多东西。

在VC6.0 中找到头文件

C标准头文件,例如 stdio.h、string.h 等在 VC6.0 的安装目录中是可以找到的。我的 VC6.0 安装在 C:\Program Files\Microsoft Visual Studio\ 目录,那么 VC6.0 附带的所有头文件(包括但不限于标准头文件)都在 C:\Program Files\Microsoft Visual Studio\VC98\Include\ 目录下。

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

到:

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

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

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

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

15.2 第一个C语言多文件编译的例子

这一节通过一个简单的例子,向大家展示如何有效地将各个文件联系在一起。在 VC6.0 中新建一个工程,添加 fun.c、main.c 两个源文件和 fun.h 一个头文件,内容如下:

fun.c

复制纯文本新窗口

01 #include

02 int fun1(){

03 printf("The first function!\n");

04 return0;

05 }

06 int fun2(){

07 printf("The second function!\n");

08 return0;

09 }

10 int fun3(){

11 printf("The third function!\n");

12 return0;

13 }

fun.h

复制纯文本新窗口

01 #ifndef_FUN_H

02 #define_FUN_H

03

04 extern int fun1(void);

05 extern int fun2(void);

06 extern int fun3(void);

07

08 #endif

main.c

复制纯文本新窗口

?#include

?#include

?#include"fun.h"

?

?int main(){

?fun1();

?fun2();

?fun3();

?

?system("pause");

?return0;

?}

对上面的每个 .c 文件都进行编译,然后链接并运行:The first function!

The second function!

The third function!

上面的例子,函数定义放在 fun.c 文件中,在 fun.h 头文件中对函数进行声明,暴露接口,然后在主文件 main.c 中引入 fun.h。

注意:编译是针对单个 .c 文件的,如果项目中有多个 .c 文件,需要逐一编译,然后链接,或者使用“组建 -> 全部重建”选项,一次性编译并链接所有文件。

多文件编程时,只能有一个文件包含 main() 函数,因为一个工程只能有一个入口函数。我们把包含 main() 函数的文件称为主文件。

可以在其他 .c 文件中对函数进行定义,在 .h 中对函数进行声明,只要主文件包含进相应的头文件,就能使用这些函数。实际开发中,很少有简单到只有几十行代码的C语言项目,合理的组织代码和文件,是开发大中型项目的必备技能。为了更好的组织各个文件,一般情况下一个 .c 文件对应一个 .h 文件,并且文件名要相同,例如 fun.c 和 fun.h。如果 fun.c 使用到了 fun.h 的宏定义、类型定义等,还需要在 fun.c 中 #include "fun.c"。

.c 文件主要包含各个函数的定义,.h 文件声明函数原型,向外暴露接口,供主文件调用。另外也可以在 .h 中包含宏定义、类型定义。

注意:.h 文件头文件中不能有可执行代码,也不能有变量定义,只能有宏、类型( typedef,struct,union,menu )定义和变量、函数的声明。

这倒不是说在 .h 中定义变量或函数会有语法错误,实际上#icnlude机制很简单,就是把#include所包含的文件中的内容直接复制到#include所在的位置并替换#include语句。但是这样做不符合模块化编程的惯例,也不利于文件的组织,不利于二次开发,不利于团队协作。

头文件要遵守幂等性原则,即可以多次包含相同的头文件,但效果与只包含一次相同。

可以使用下面的宏防止一个头文件被重复包含。

复制纯文本新窗口

?#ifndef MY_INCLUDE_H

?#define MY_INCLUDE_H

?//头文件内容

?#endif

如果该头文件已被包含,那么会定义宏 MY_INCLUDE_H,再次包含时,就不会对头文件内容进行编译了。

15.3 动态链接库(dll)简介

DLL 是 Dynamic Link Library 的缩写,译为“动态链接库”。DLL也是一个被编译过的二进制程序,可以被其他程序调用,但与 exe 不同,DLL不能独立运行,必须由其他程序调用载入内存。

DLL 中封装了很多函数,只要知道函数的入口地址,就可以被其他程序调用。Windows API中所有的函数都包含在DLL中,其中有3个最重要的DLL:?Kemel32.dll:它包含那些用于管理内存、进程和线程的函数,例如CreateThread

函数;

?User32.dll:它包含那些用于执行用户界面任务(如窗口的创建和消息的传送)的

函数,例如CreateWindow 函数;

?GDI32.dll:它包含那些用于画图和显示文本的函数。

静态链接库和动态链接库

1) 静态库

函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.EXE文件)。当发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。

2) 动态库

在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件和一个DLL (.dll) 文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质上的区别,对一个DLL来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。这时,在发布产品时,除了发布可执行文件以外,同时还要发布该程序将要调用的动态链接库。

使用动态链接库的好处

1) 可以采用多种编程语言来编写

我们可以采用自己熟悉的开发语言编写DLL,然后由其他语言编写的可执行程序

来调用这些DLL。例如,可以利用VB来编写程序的界面,然后调用利用VC++或Delphi编写的完成程序业务逻辑的DLL。

2) 增强产品的功能

在发布产品时,可以发布产品功能实现的动态链接库规范,让其他公司或个人遵照这个规范开发自己的DLL,以取代产品原有的DLL,让产品调用新的DLL,从而实现功能的增强。在实际工作中,我们看到许多产品都提供了界面插件功能,允许用户动态地更换程序的界面,这就可以通过更换界面DLL来实现。

3) 提供二次开发的平台

在销售产品的同时,可以采用DLL的形式提供一个二次开发的平台,让用户可以利用该DLL调用其中实现的功能,编写符合自己业务需要的产品,从而实现二次开发。

4) 简化项目管理

在一个大型项目开发中,通常都是由多个项目小组同时开发,如果采用串行开发,则效率是非常低的。我们可以将项目细分,将不同功能交由各项目小组以多个DLL的方式实现,这样,各个项目小组就可以同时进行开发了。

5) 可以节省磁盘空间和内存

如果多个应用程序需要访问同样的功能,那么可以将该功能以DLL的形式提供,这样在机器上只需要存在一份该DLL文件就可以了,从而节省了磁盘空间。另外,如果多个应用程序使用同一个DLL,该DLL只需要放入内存一次,所有的应用程序就都可以共亨它了。这样,内存的使用将更加有效。

我们知道,当进程被加载时,系统会为它分配内存,接着分析该可执行模块,找到该程序将要调用哪些DLL,然后系统搜索这些DLL,找到后就加载它们,并为它们分配内存空间。DLL的内存空间只有一份,如果有第二个程序也需要加载该DLL,那么它们共享内存空间,相同的DLL不会再次加载。

6) 有助于资源的共享

DLL可以包含对话框模板、字符串、图标和位图等多种资源,多个应用程序可以使用DLL来共享这些资源。在实际工作中,可以编写一个纯资源的动态链接库,供其他应用程序访问。

7) 有助于实现应用程序的本地化

如果产品需要提供多语言版本,那么就可以使用DLL来支持多语言。可以为每种语言创建一个只支持这种语言的动态链接库。

15.4 第一个DLL程序:动态链接库DLL教程,30分钟快速上手

DLL 程序的入口函数是 DllMain(),就像 DOS 程序的入口函数是 main()、Win32 程序的入口函数是 WinMain() 一样。前面我们一直在讲的就是DOS程序。

DllMain() 函数的原型为:

复制纯文本新窗口

?BOOL APIENTRY DllMain(

?HANDLEhModule,

?DWORD ul_reason_for_call,

?LPVOID lpReserved

?);

其中:

5hModule 表示本DLL程序的句柄。

6ul_reason_for_call 表示DLL当前所处的状态,例如DLL_PROCESS_ATTACH表示DLL刚刚被加载到一个进程中,DLL_PROCESS_DETACH表示DLL刚刚从一个进程中卸载。

7lpReserved 表示一个保留参数,目前已经很少使用。

一个简单的DLL程序并不比 "Hello World" 程序难,下面就开始介绍如何利用VC6.0创建DLL及其调用方式。

首先利用VC6.0新建一个 Win32 Dynamic-Link Library 类型的工程,工程取名为 dllDemo,并选择“An empty Dll project"选项,即创建一个空的动态链接库工程。然后,为该工程添加一个C源文件 main.c,并在其中编写完成加法运算和减法运算的函数,代码如下所示:

复制纯文本新窗口

?#include// 也可以#include

?#include

?

?_declspec(dllexport)int add(int a,int b){

?return a+b;

?}

?_declspec(dllexport)int sub(int a,int b){

?return a-b;

?}

?

?BOOL APIENTRY DllMain(

?HANDLEhModule,

?DWORD ul_reason_for_call,

?LPVOID lpReserved

?){

?if(ul_reason_for_call == DLL_PROCESS_ATTACH){

?printf("Congratulations! DLL is loaded!");

?}

?}

然后利用Build命令生成dllDemo这一动态链接库程序。之后,在该工程的Debug 目录下,可以看到有一个dllDemo.dll文件,这就是生成的动态链接库文件。

读者要记住,应用程序如果想要访问某个DLL中的函数,那么该函数必须是已经被导出的函数。为了导出一些函数,需要在函数前面添加标识符_declspec(dllexport)。

为了查看一个DLL中有哪些导出函数,可以利用VC6.0提供的命令行工具Dumpbin来实现。

Dumpbin.exe文件位于VC6.0安装目录下的VC98\bin目录下。在该目录下还有一个批处理文件VCVARS32.bat,该文件的作用是用来建立VC6.0使用的环境信息。如果读者在其他目录下无法执行Dumpbin命令,原因可能就是你的VC6.0安装的环境信息被破坏了,那么可以运行VCVARS32.bat这个批处理文件,之后在其他目录下,就可以执行Dumpbin命令了。

注意:当在命令行界面下执行VCVARS32.bat文件后,该文件所设置的环境信息只是在当前命令行窗口生效。如果关闭该窗口,并再次启动一个新的命令行窗口后,仍需要运行VCVARS32.bat文件。

在命令行界面下,cd 到工程目录下的debug目录,输入dumpbin -exports dllDemo.dll 命令,然后回车,即可查看DLL中的导出函数,如下图:

注意红色方框标出的信息:

ordinal hint RVA name

1 0 00001005 add

2 1 0000100A sub

在这段信息中,"ordinal" 列列出的信息 '1' 和 '2' 是导出函数的序号;"hint" 列列出的数字是提示码,该信息不重要;"RVA" 列列出的地址值是导出函数在DLL模块中的位置,也就是说,通过该地址值,可以在DLL中找到它们;最后一列 "name" 列出的是导出函数的名称。

将 add 函数前面的 _declspec(dllexport) 标识符去掉,再次编译 dllDemo 工程,然后执行 dumpbin -exports dllDemo.dll 命令,输出如下图所示:

可以看到,add 函数已经不是导出函数了。

打开项目目录下的Debug目录,发现有 dllDemo.dll 和 dllDemo.lib 两个文件。上节已经说过,.lib 文件包含DLL导出的函数和变量的符号名,.dll 文件才包含实际的函数和数据。主程序调用 DLL 需要这两个文件,下节会讲解如何使用。

注意:DllMain() 函数在DLL程序载入和卸载时执行,可以用来做一些初始化和清理的工作,如果仅仅是向外暴露函数,就可以省略 DllMain() 函数。但是如果有DllMain() 函数,就一定要#include 或#include

例如,上面DLL如果只想暴露 add() 和 sub() 函数,而不想进行其他操作,那么可以这样写:

复制纯文本新窗口

?_declspec(dllexport)int add(int a,int b){

?return a+b;

?}

?_declspec(dllexport)int sub(int a,int b){

?return a-b;

?}

15.5 动态链接库DLL的加载:隐式加载(载入时加载)和显式加载(运行时加载)

静态链接库在链接时,编译器会将 .obj 文件和 .LIB 文件组织成一个 .exe 文件,程序运行时,将全部数据加载到内存。

如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端。

动态链接库有两种加载方式:隐式加载和显示加载。

?隐式加载又叫载入时加载,指在主程序载入内存时搜索DLL,并将DLL载入内存。

隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。

?显式加载又叫运行时加载,指主程序在运行过程中需要DLL中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。

隐式加载

首先创建一个工程,命名为 cDemo,添加源文件 main.c,内容如下:

复制纯文本新窗口

8#include

9

10extern int add(int,int);// 也可以是_declspec(dllimport) int add(int, int);

11extern int sub(int,int);// 也可以是_declspec(dllimport) int sub(int, int);

12

13int main(){

14int a=10, b=5;

15printf("a+b=%d\n",add(a, b));

16printf("a-b=%d\n",sub(a, b));

17return0;

18}

找到上节创建的 dllDemo 工程,将 debug 目录下的 dllDemo.lib 和 dllDemo.dll 复制到当前工程目录下。

前面已经说过:.lib 文件包含DLL导出的函数和变量的符号名,只是用来为链接程序提供必要的信息,以便在链接时找到函数或变量的入口地址;.dll 文件才包含实际的函数和数据。所以首先需要将 dllDemo.lib 引入到当前项目。

选择”工程(Project) -> 设置(Settings)“菜单,打开工程设置对话框,选择”链接(link)“选项卡,在”对象/库模块(Object/library modules)“编辑框中输入dllDemo.lib,如下图所示:

但是这样引入 .lib 文件有一个缺点,就是将源码提供给其他用户编译时,也必须手动引入 .lib 文件,麻烦而且容易出错,所以最好是在源码中引入 .lib 文件,如下所示:

#pragma comment(lib, "dllDemo.lib")

更改上面的代码:

复制纯文本新窗口

19#include

20#pragma comment(lib,"dllDemo.lib")

21

22_declspec(dllimport)int add(int,int);

23_declspec(dllimport)int sub(int,int);

24

25int main(){

26int a=10, b=5;

27printf("a+b=%d\n",add(a, b));

28printf("a-b=%d\n",sub(a, b));

29return0;

30}

点击确定回到项目,编译、链接并运行,输出结果如下:

Congratulations! DLL is loaded!

a+b=15

a-b=5

在 main.c 中除了用 extern 关键字声明 add() 和 sub() 函数来自外部文件,还可以用 _declspec(dllimport) 标识符声明函数来自动态链接库。

为了更好的进行模块化设计,最好将 add() 和 sub() 函数的声明放在头文件中,整理后的代码如下:

dllDemo.h

复制纯文本新窗口

31#ifndef_DLLDEMO_H

32#define_DLLDEMO_H

33

34#pragma comment(lib,"dllDemo.lib") 35_declspec(dllexport)int add(int,int);

36_declspec(dllexport)int sub(int,int);

37

38#endif

main.c

复制纯文本新窗口

39#include

40#include"dllDemo.h"

41

42int main(){

43int a=10, b=5;

44printf("a+b=%d\n",add(a, b));

45printf("a-b=%d\n",sub(a, b));

46return0;

47}

显式加载

显式加载动态链接库时,需要用到 LoadLibrary() 函数,该函数的作用是将指定的可执行模块映射到调用进程的地址空间。LoadLibrary() 函数的原型声明如下所示:

HMODULE LoadLibrary(LPCTSTR 1pFileName);

LoadLibrary() 函数不仅能够加载DLL(.dll),还可以加载可执行模块(.exe)。一般来说,当加载可执行模块时,主要是为了访问该模块内的一些资源,例如位图资源或图标资源等。LoadLibrary() 函数有一个字符串类型(LPCTSTR)的参数,该参数指定了可执行模块的名称,既可以是一个.dll文件,也可以是一个.exe 文件。如果调用成功, LoadLibrary() 函数将返回所加载的那个模块的句柄。该函数的返回类型是HMODULE。 HMODULE类型和HINSTANCE类型可以通用。

当获取到动态链接库模块的句柄后,接下来就要想办法获取该动态链接库中导出函数的地址,这可以通过调用 GetProcAddress() 函数来实现。该函数用来获取DLL导出函数的地址,其原型声明如下所示:

FARPROC GetProcAddress(HMODULE hModule, LPCSTR 1pProcName);

可以看到,GetProcAddress函数有两个参数,其含义分别如下所述:?hModule:指定动态链接库模块的句柄,即LoadLibrary() 函数的返回值。

?1pProcName:字符串指针,表示DLL中函数的名字。

首先创建一个工程,命名为 cDemo,添加源文件 main.c,内容如下:

复制纯文本新窗口

01 #include

02 #include

03 #include// 必须包含windows.h

04

05 typedef int(*FUNADDR)();// 指向函数的指针

06

07 int main(){

08 int a=10, b=5;

09 HINSTANCE dllDemo =LoadLibrary("dllDemo.dll");

10 FUNADDR add, sub;

11 if(dllDemo){

12 add =(FUNADDR)GetProcAddress(dllDemo,"add");

13 sub =(FUNADDR)GetProcAddress(dllDemo,"sub");

14 }else{

15 printf("Fail to load DLL!\n");

16 system("pause");

17 exit(1);

18 }

19

20 printf("a+b=%d\n",add(a, b));

21 printf("a-b=%d\n",sub(a, b));

22

23 system("pause");

24 return0;

25 }

找到上节创建的 dllDemo 工程,将 debug 目录下的 dllDemo.dll 复制到当前工程目录下。注意,只需要 dllDemo.dll,不需要 dllDemo.lib。

运行程序,输出结果与上面相同。

HMODULE 类型、HINSTANCE 类型在 windows.h 中定义;LoadLibrary() 函数、GetProcAddress() 函数是Win32 API,也在 windows.h 中定义。

通过以上的例子,我们可以看到,隐式加载和显式加载这两种加载DLL的方式各有优点,如果采用动态加载方式,那么可以在需要时才加载DLL,而隐式链接方式实现起来比较简单,在编写程序代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。但是,如果程序需要访问十多个DLL,如果都采用隐式链接方式加载它们的话,那么在该程序启动时,这些DLL都需要被加载到内存中,并映射到调用进程的地址空间,这样将加大程序的启动时间。而且,一般来说,在程序运行过程中只是在某个条件满足时才需要访问某个DLL中的某个函数,其他情况下都不需要访问这些DLL中的函数。但是这时所有的DLL都已经被加载到内存中,资源浪费是比较严重的。在这种情况下,就可以采用显式加载的方式访问DLL,在需要时才加载所需的DLL,也就是说,在需要时DLL才会被加载到内存中,并被映射到调用进程的地址空间中。有一点需要说明的是,实际上,采用隐式链接方式访问DLL时,在程序启动时也是通过调用LoadLibrary() 函数加载该进程需要的动态链接库的。

C语言编译过程中的错误分析

C语言编译过程中的错误分析 语言的最大特点是:功能强、使用方便灵活。C编译的程序对语法检查并不象其它高级语言那么严格,这就给编程人员留下“灵活的余地”,但还是由于这个灵活给程序的调试带来了许多不便,尤其对初学C语言的人来说,经常会出一些连自己都不知道错在哪里的错误。看着有错的程序,不知该如何改起,本人通过对C的学习,积累了一些C编程时常犯的错误,写给各位学员以供参考。 1.书写标识符时,忽略了大小写字母的区别。 main() { int a=5; printf("%d",A); } 编译程序把a和A认为是两个不同的变量名,而显示出错信息。C认为大写字母和小写字母是两个不同的字符。习惯上,符号常量名用大写,变量名用小写表示,以增加可读性。 2.忽略了变量的类型,进行了不合法的运算。 main() { float a,b; printf("%d",a%b); } %是求余运算,得到a/b的整余数。整型变量a和b可以进行求余运算,而实型变量则不允许进行“求余”运算。 3.将字符常量与字符串常量混淆。 char c; c="a"; 在这里就混淆了字符常量与字符串常量,字符常量是由一对单引号括起来的单个字符,字符串常量是一对双引号括起来的字符序列。C规定以“”作字符串结束标志,它是由系统自动加上的,所以字符串“a”实际上包含两个字符:‘a'和‘',而把它赋给一个字符变量是不行的。 4.忽略了“=”与“==”的区别。 在许多高级语言中,用“=”符号作为关系运算符“等于”。如在BASIC程序中可以写 if (a=3) then … 但C语言中,“=”是赋值运算符,“==”是关系运算符。如: if (a==3) a=b; 前者是进行比较,a是否和3相等,后者表示如果a和3相等,把b值赋给a。由于习惯问题,初学者往往会犯这样的错误。 5.忘记加分号。 分号是C语句中不可缺少的一部分,语句末尾必须有分号。 a=1 b=2 编译时,编译程序在“a=1”后面没发现分号,就把下一行“b=2”也作为上一行语句的一部分,这就会出现语法错误。改错时,有时在被指出有错的一行中未发

C语言编译器的设计与实现.

C语言编译器的设计与实现 01计算机4班18号任春妍2号陈俊我们设计的编译程序涉及到编译五个阶段中的三个,即词法分析器、语法分析器和中间代码生成器。编译程序的输出结果包括词法分析后的二元式序列、变量名表、状态栈分析过程显示及四元式序列程序,整个编译程序分为三部分: (1) 词法分析部分 (2) 语法分析处理及四元式生成部分 (3) 输出显示部分 一.词法分析器设计 由于我们规定的程序语句中涉及单词较少,故在词法分析阶段忽略了单词输入错误的检查,而将编译程序的重点放在中间代码生成阶段。词法分析器的功能是输入源程序,输出单词符号。我们规定输出的单词符号格式为如下的二元式:(单词种别,单词自身的值) #define ACC -2 #define syl_if 0 #define syl_else 1 #define syl_while 2 #define syl_begin 3 #define syl_end 4 #define a 5 #define semicolon 6 #define e 7 #define jinghao 8 #define s 9 #define L 10 #define tempsy 11 #define EA 12 #define EO 13 #define plus 14 #define times 15 #define becomes 16 #define op_and 17 #define op_or 18 #define op_not 19 #define rop 20 #define lparent 21 #define rparent 22 #define ident 23 #define intconst 24

C语言的编译链接过程的介绍

C语言的编译链接过程的介绍 发布时间:2012-10-2600:00:00来源:中国IT实验室作者:佚名 关键字:C语言 C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码的过程。过程图解如下:

从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。 编译过程 编译过程又可以分成两个阶段:编译和会汇编。 编译 编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段: 第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。如 #include指令就是一个预处理指令,它把头文件的内容添加到.cpp文件中。这个在编译之前修改源文件的方式提供了很大的灵活性,以适应不同的计算机和操作系统环境的限制。一个环境需要的代码跟另一个环境所需的代码可能有所不同,因为可用的硬件或操作系统是不同的。在许多情况下,可以把用于不同环境的代码放在同一个文件中,再在预处理阶段修改代码,使之适应当前的环境。 主要是以下几方面的处理:

(1)宏定义指令,如#define a b 对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的a则不被替换。还有#undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。 (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif 等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译 程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。 (3)头文件包含指令,如#include"FileName"或者#include等。 在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在 /usr/include目录下。在程序中#include它们要使用尖括号(<>)。

C语言编程要点程序的编写和编译

C语言编程要点程序的 编写和编译 Document serial number【LGGKGB-LGG98YT-LGGT8CB-LGUT-

C语言编程要点---第18章程序的编写和编译 第18章程序的编写和编译 本章讲述在编译程序时可以使用的一些技术。在本章中,你将学到专业C程序员在日常编程中所使用的一些技巧。你将会发现,无论是对小项目还是大项目,把源代码分解成几个文件都是很有益处的。在生成函数库时,这一点更为重要。你还将学到可以使用的各种存储模式以及怎样为不同的项目选择不同的存储模式。如果你的程序是由几个源文件组成的,那么你可以通过一个叫MAKE的工具来管理你的项目(project)。你还将学到“.COM"文件和".EXE"文件的区别以及使用“.COM”文件的一个好处。 此外,你还将学到用来解决一个典型的DOS问题的一些技巧,这个问题就是“没有足够的内存来运行DOS程序”。本章还讨论了扩展内存、扩充内存、磁盘交换区、覆盖管理程序和DOS扩展程序的用法,提出了解决"RAM阻塞”这一问题的多种方法,你可以从中选择一种最合适的方法 . 程序是应该写成一个源文件还是多个源文件? 如果你的程序确实很小又很紧凑,那么当然应该把所有的源代码写在一个“.C”文件中。然而,如果你发现自己编写了许多函数(特别是通用函数),那么你就应该把程序分解成几个源文件(也叫做模块)。 把一个程序分解成几个源文件的过程叫做模块化程序设计(modular programming)。模块化程序设计技术提倡用几个不同的结构紧凑的模块一起组成一个完整的程序。例如,如果一个程序中有几种实用函数、屏幕函数和数据库函数,你就可以把这些函数分别放在三个源文件中,分别组成实用模块、屏幕模块和数据库模块。 把函数放在不同的文件中后,你就可以很方便地在其它程序中重复使用那些通用函数。如果你有一些函数还要供其它程序员使用,那么你可以生成一个与别人共享的函数库(见18.9)。 你永远不必担心模块数目“太多”——只要你认为合适,你可以生成很多个模块。一条好的原则就是保持模块的紧凑性.即在同一个源文件中只包含那些在逻辑上与其相关的函数。如果你发现自己把几个没有关系的函数放在了同一个源文件中,那么最好停下来检查一下程序的源代码结构,并且对模块做一下逻辑上的分解。例如,如果要建立一个通信管理数据库,你可能需要有这样一个模块结构: --------------------------------------------------------- 模块名内容 --------------------------------------------------------- Main.c maln()函数 Screen.c 屏幕管理函数 Menus.c 菜单管理函数 Database.c 数据库管理函数 Utility.c 通用功能函数 Contact.c 通信处理函数 Import.c 记录输入函数 Export.c 记录输出函数 Help.c 联机帮助支持函数 ---------------------------------------------------------- 请参见: 18.10 如果一个程序包含多个源文件,怎样使它们都能正常工作? . 各种存储模式之间有什么区别? DOS用一种段地址结构来编址计算机的内存,每一个物理内存位置都有一个可通过段地址一偏移量的方式来访问的相关地址。为了支持这种段地址结构,大多数C编译程序都允许你用以下6种存储模式来创建程序: ----------------------------------------------------------------------- 存储模式限制所用指针 ----------------------------------------------------------------------- Tiny(微) 代码、数据和栈一64KB Near

编译原理C语言词法分析器

编译原理 C语言词法分析器 一、实验题目 编制并调试C词法分析程序。 a.txt源代码: ?main() { int sum=0 ,it=1;/* Variable declaration*/ if (sum==1) it++; else it=it+2; }? 设计其词法分析程序,能识别出所有的关键字、标识符、常数、运算符(包括复合运算符,如++)、界符;能过滤掉源程序中的注释、空格、制表符、换行符;并且能够对一些词法规则的错误进行必要的处理,如:标识符只能由字母、数字和下划线组成,且第一个字符必须为字母或下划线。实验要求:要给出所分析语言的词法说明,相应的状态转换图,单词的种别编码方案,词法分析程序的主要算法思想等。 二、实验目的 1、理解词法分析在编译程序中的作用; 2、掌握词法分析程序的实现方法和技术; 3、加深对有穷自动机模型的理解。 三、主要函数 四、设计 1. 主函数 void main ( )

2. 初始化函数 void load ( ) 3. 保留字及标识符判断函数 void char_search(char *word) 4. 整数类型判断函数 void inta_search(char *word) 5. 浮点类型判断函数 void intb_search(char *word)

6. 字符串常量判断函数 void cc_search(char *word) 7. 字符常量判断函数 void c_search(char *word) 同4、5函数图 8.主扫描函数 void scan ( ) 五、关键代码 #include <> #include <> #include <> char *key0[]={"

C语言编译过程总结详解

C语言的编译链接过程要把我们编写的一个c程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码的过程。过程图解如下: 从图上可以看到,整个代码的编译过程分为编译和链接两个过程,编译对应图中的大括号括起的部分,其余则为链接过程。 编译过程 编译过程又可以分成两个阶段:编译和会汇编。 编译 编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含两个主要阶段: 第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。如#include指令就是一个预处理指令,它把头文件的内容添加到.cpp文件中。这个在编译之前修改源文件的方式提供了很大的灵活性,以适应不同的计算机和操作系统环境的限制。一个环境需要的代码跟另一个环境所需的代码可能有所不同,因为可用的硬件或操作系统是不同的。在许多情况下,可以把用于不同环境的代码放在同一个文件中,再在预处理阶段修改代码,使之适应当前的环境。 主要是以下几方面的处理: (1)宏定义指令,如 #define a? b 对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a 则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。 (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。 (3)头文件包含指令,如#include "FileName"或者#include 等。 在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在 /usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。 (4)特殊符号,预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。 第二个阶段编译、优化阶段,经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。

C语言作业题

能将高级语言编写的源程序转换成目标程序的是______。 A) 编辑程序B) 编译程序C) 解释程序D) 链接程序 [A] [B] [C] [D] 以下选项中合法的用户标识符是______。 A) long B) _2Test C) 3Dmax D) A.dat [A] [B] [C] [D] 以下叙述正确的是 A) 可以把define和if定义为用户标识符 B) 可以把define定义为用户标识符,但不能把if定义为用户标识符 C) 可以把if定义为用户标识符,但不能把define定义为用户标识符 D) define和if都不能定义为用户标识符 [A] [B] [C] [D] 以下叙述正确的是 A) C语言比其他语言高级 B) C语言可以不用编译就能被计算机识别执行 C) C语言以接近英语国家的自然语言和数学语言作为语言的表达形式 D) C语言出现的最晚、具有其他语言的一切优点 [A] [B] [C] [D] 在一个C语言程序中 A) main函数必须出现在所有函数之前 B) main函数必须出现在所有函数之后 C) main函数可以在任何地方出现 D) main函数必须出现在固定位置 [A] [B] [C] [D] 一个C语言程序是由_______组成的。 A) 一个主程序和若干子程序 B) 若干子程序 C) 函数 D) 若干过程 [A] [B] [C] [D] 以下叙述中正确的是() A) C语言的源程序不必通过编译就可以直接运行 B) C语言中的每条可执行语句最终都将被转换成二进制的机器指令

C) C源程序经编译形成的二进制代码可以直接运行 D) C语言中的函数不可以单独进行编译 [A] [B] [C] [D] 下列关于C语言用户标识符的叙述中正确的是 A) 用户标识符中可以出现下划线和数字,它们都可以放在用户标识符的开头 B) 用户标识符中可以出现下划线,但不可以放在用户标识符的开头 C) 用户标识符中不可以出现中划线,但可以出现下划线 D) 用户标识符中可以出现下划线和中划线(减号) [A] [B] [C] [D] 以下说法中正确的是 A)C语言程序总是从第一个定义的函数开始执行 B)C语言程序中,要调用的函数必须在main()函数中定义 C)C语言程序总是从main()函数开始执行 D)C语言程序中的main()函数必须放在程序的开始部分 [A] [B] [C] [D] 默认情况下,一个C程序的执行是从 A)本程序的main函数开始,到main函数结束 B)本程序文件的第一个函数开始,到本程序文件的最后一个函数结束 C)本程序的main函数开始,到本程序文件的最后一个函数结束 D)本程序文件的第一个函数开始,到本程序main函数结束 [A] [B] [C] [D] C语言编程整个操作的过程是 A)编辑、编译、链接、执行 B)编译、编辑、链接、执行 C)链接、编译、编辑、执行 D)编辑、链接、编译、执行 [A] [B] [C] [D] 下列说法不正确的是 A)链接操作将生成扩展名为.lnk的文件 B)编辑过程将生成扩展名为.cpp或.c的文件 C)编译过程将生成扩展名为.obj的文件 C)C编程最终要生成扩展名为.exe的文件

C语言条件编译及编译预处理阶段

C语言条件编译及编译预处理阶段 一、C语言由源代码生成的各阶段如下: C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件其中编译预处理阶段,读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。或者说是扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程先于编译器对源代码进行处理。 在C 语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。 二、伪指令(或预处理指令)定义 预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令: 指令用途 # 空指令,无任何效果 #include 包含一个源代码文件 #define定义宏 #undef取消已定义的宏 #if如果给定条件为真,则编译下面代码 #ifdef 如果宏已经定义,则编译下面代码 #ifndef 如果宏没有定义,则编译下面代码 #elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码, 其实就是elseif的简写 #endif结束一个#if……#else条件编译块 #error停止编译并显示错误信息 三、预处理指令主要包括以下四个方面: 1、宏定义指令 宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏(宏函数),这样的宏可以象函数一样被调用,但它是在调用语句处展开宏,并

编译原理课程设计____C语言编译器的实现

南华大学 编译原理课程设计名:编译代生 成器设计 专业计算机科学与技术 学生姓名熊浩斌 班级计算机01班 学号 20109440114 指导老师陈星 实验地点 8栋 2-209 完成日期:2013.6.2

一、课程设计的目的 编译原理课程兼有很强的理论性和实践性,是计算机专业的一门非常重要的专业基础课程,它在系统软件中占有十分重要的地位,是计算机专业学生的一门主修课。为了让学生能够更好地掌握编译原理的基本理论和编译程序构造的基本方法和技巧,融会贯通本课程所学专业理论知识,提高他们的软件设计能力,特设定该课程的课程设计,通过设计一个简单的PASCAL语言(EL语言)的编译程序,提高学生设计程序的能力,加深对编译理论知识的理解与应用。 二、课程设计的要求 1、明确课程设计任务,复习编译理论知识,查阅复印相关的编译资料。 2、按要求完成课程设计内容,课程设计报告要求文字和图表工整、思路清晰、算法正 确。 3、写出完整的算法框架。 4、编写完整的编译程序。 三、课程设计的内容 课程设计是一项综合性实践环节,是对平时实验的一个补充,课程设计内容包括课程的主要理论知识,但由于编译的知识量较复杂而且综合性较强,因而对一个完整的编译程序不适合平时实验。通过课程设计可以达到综合设计编译程序的目的。本课程的课程设计要求学生编写一个完整的编译程序,包括词法分析器、语法分析器以及实现对简单程序设计语言中的逻辑运算表达式、算术运算表达式、赋值语句、IF语句、While语句以及do…while语句进行编译,并生成中间代码和直接生汇编指令的代码生成器。 四、总体设计方案及详细设计 总体设计方案: 1.总体模块

C语言基础知识(详细版)

C语言程序的结构认识 用一个简单的c 程序例子,介绍c 语言的基本构成、格式、以及良好的书写风格,使小伙伴对 c 语言有个 初步认识。 例1:计算两个整数之和的c 程序: #include main() { int a,b,sum; /* 定义变量a,b ,sum 为整型变量*/ a=20; /* 把整数20 赋值给整型变量a*/ b=15; /* 把整数15 赋值给整型变量b*/ sum=a+b; /* 把两个数之和赋值给整型变量sum*/ printf( “ a=%d,b=%d,sum=%d\n” ,a,b,sum); /* 把计算结果输出到显示屏上*/ } 重点说明: 1、任何一个c 语言程序都必须包括以下格式: main() { } 这是c 语言的基本结构,任何一个程序都必须包含这个结构。括号内可以不写任何内容,那么该程序将不执行任何结果。 2、main() - 在c 语言中称之为“主函数” ,一个c 程序有且仅有一个main 函数,任何一个c 程序总是从 main 函数开始执行,main 函数后面的一对圆括号不能省略。 3、被大括号{ }括起来的内容称为main 函数的函数体,这部分内容就是计算机要执行的内容。 4、在{ }里面每一句话后面都有一个分号(; ),在c 语言中,我们把以一个分号结尾的一句话叫做一个 c 语 言的语句,分号是语句结束的标志。 5、printf( “ a=%d,b=%d,sum=%d\n” ,a,b,sum); 通过执行这条c 语言系统提供给我们直接使用的屏幕输出 函数,用户即可看到运行结果,本程序运行后,将在显示器上显示如下结果: a=20,b=15,sum=35 6、#include 注意:(1)以#号开头 (2)不以分号结尾这一行没有分号,所以不是语句,在c 语言中称之为命令行,或者叫做“预编译处理命令” 。 7、程序中以/* 开头并且以*/ 结尾的部分表示程序的注释部分,注释可以添加在程序的任何位置,为了提高程序的可读性而添加,但计算机在执行主函数内容时完全忽略注释部分,换而言之就是计算机当做注释部分不存在于主函数中。 C程序的生成过程 C程序是先由源文件经编译生成目标文件,然后经过连接生成可执行文件。 源程序的扩展名为.c ,目标程序的扩展名为.obj , 可执行程序的扩展名为.exe 。

编译原理课程设计----C语言编译器的实现

$ 编译原理课程设计报告 设计题目编译代码生成器设计 、 学生姓名 班级 学号 指导老师 成绩 `

一、课程设计的目的 编译原理课程兼有很强的理论性和实践性,是计算机专业的一门非常重要的专业基础课程,它在系统软件中占有十分重要的地位,是计算机专业学生的一门主修课。为了让学生能够更好地掌握编译原理的基本理论和编译程序构造的基本方法和技巧,融会贯通本课程所学专业理论知识,提高他们的软件设计能力,特设定该课程的课程设计,通过设计一个简单的PASCAL语言(EL语言)的编译程序,提高学生设计程序的能力,加深对编译理论知识的理解与应用。 二、课程设计的要求 1、明确课程设计任务,复习编译理论知识,查阅复印相关的编译资料。 2、按要求完成课程设计内容,课程设计报告要求文字和图表工整、思路清晰、算法正 确。 3、@ 4、写出完整的算法框架。 5、编写完整的编译程序。 三、课程设计的内容 课程设计是一项综合性实践环节,是对平时实验的一个补充,课程设计内容包括课程的主要理论知识,但由于编译的知识量较复杂而且综合性较强,因而对一个完整的编译程序不适合平时实验。通过课程设计可以达到综合设计编译程序的目的。本课程的课程设计要求学生编写一个完整的编译程序,包括词法分析器、语法分析器以及实现对简单程序设计语言中的逻辑运算表达式、算术运算表达式、赋值语句、IF语句、While语句以及do…while语句进行编译,并生成中间代码和直接生汇编指令的代码生成器。 四、总体设计方案及详细设计 总体设计方案: 1.总体模块

【 2. \ 详细设计: 界面导入设计 (1)一共三个选项: ①choice 1--------cifafenxi ②choice 2--------yufafenxi ③choice 3--------zhongjiandaima (2)界面演示 } 图一

C语言编译和连接

一、编译 编译(compilation,compile) 1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。 编译就是把高级语言变成计算机可以识别的2进制语言,计算机只认识1和0,编译程序把人们熟悉的语言换成2进制的。 编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。 (1)词法分析 词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。执行词法分析的程序称为词法分析程序或扫描器。 源程序中的单词符号经扫描器分析,一般产生二元式:单词种别;单词自身的值。单词种别通常用整数编码,如果一个种别只含一个单词符号,那么对这个单词符号,种别编码就完全代表它自身的值了。若一个种别含有许多个单词符号,那么,对于它的每个单词符号,除了给出种别编码以外,还应给出自身的值。 词法分析器一般来说有两种方法构造:手工构造和自动生成。手工构造可使用状态图进行工作,自动生成使用确定的有限自动机来实现。 (2)语法分析 编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位。编译程序的语法规则可用上下文无关文法来刻画。 语法分析的方法分为两种:自上而下分析法和自下而上分析法。自上而下就是从文法的开始符号出发,向下推导,推出句子。而自下而上分析法采用的是移进归约法,基本思想是:用一个寄存符号的先进后出栈,把输入符号一个一个地移进栈里,当栈顶形成某个产生式的一个候选式时,即把栈顶的这一部分归约成该产生式的左邻符号。 (3)中间代码生成 中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现。中间代码即为中间语言程序,中间语言的复杂性介于源程序语言和机器语言之间。中间语言有多种形式,常见的有逆波兰记号、四元式、三元式和树。 (4)代码优化 代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。所谓等价,是指不改变程序的运行结果。所谓有效,主要指目标代码运行时间较短,以及占用的存储空间较小。这种变换称为优化。 有两类优化:一类是对语法分析后的中间代码进行优化,它不依赖于具体的计算机;另一类是在生成目标代码时进行的,它在很大程度上依赖于具体的计算机。对于前一类优化,根据它所涉及的程序范围可分为局部优化、循环优化和全局优化三个不同的级别。 (5)目标代码生成

C语言中的条件编译

C语言中的条件编译 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件下才进行编译,即对一部分内容指定编译条件,这就是“条件编译”(conditional compile 预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。 在C语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。 预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。下面是部分预处理指令: 指令用途 #空指令,无任何效果 #include包含一个源代码文件 #define定义宏 #undef取消已定义的宏 #if如果给定条件为真,则编译下面代码 #ifdef如果宏已经定义,则编译下面代码 #ifndef如果宏没有定义,则编译下面代码 #elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 #endif结束一个#if……#else条件编译块 #error停止编译并显示错误信息 一、文件包含 #include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。 预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含。这样就可以在多次包含同一个头文件时,通过给定编译时的条件来达到不同的效果。例如: #define AAA #include"t.c" #undef AAA #include"t.c" 为了避免那些只能包含一次的头文件被多次包含,可以在头文件中用编译时条件来进行控制。例如: /*my.h*/ #ifndef MY_H #define MY_H

C语言exe文件编译过程

用简单C程序分析DOS下的EXE文件 DOS下的EXE文件格式比较简单,所以咱们先把Windows下的那个复杂的EXE文件放一边,挑个软柿子捏捏(以下EXE如不特殊说明均指DOS下的EXE文件格式)。 其实网上关于EXE格式的说明很多,大都是哗啦列出大批格式说明,看得人是头晕脑胀的。等自己搞懂了,总觉的其中个别说明不太精确导致自己误解浪费了不少时间。所以,咱们要自己动手去实践一下,边动手边理解就容易多了。至于本次分析为什么用C,这个嘛,咱们随便分析一下C语言与汇编的联系,尤其是子程序的调用,这跟什么什么标准有关。好,废话少说,切入正题! 1.软件准备 ①既然跟DOS有关,得有个DOS系统吧。现在盛行虚拟机,安装简单并且系统崩溃的话跟自己电脑的硬件没关联,安全方便。至于怎么安装,请参阅本人拙文: VMware上安装MS-DOS 6.22之一:基本系统的安装 VMware上安装MS-DOS 6.22之二:光驱驱动及其他的安装 ②另外还要安装上Turbo C。初学C的大概都用过这个东东吧。安装在DOS上吧。我用的是Turbo C++ 3。 ③还得有个能查看文件16进制的软件。例如UltraEdit。 2.生成EXE文件 我们要从最简单的分析起,所以C程序尽量简单,只包含一个子程序调用。如下 怎么样,够简单的吧。编译链接生成EXE文件。

3.小样,来吧,让我分析分析你 ①用UltraEdit打开生成的EXE文件如下图(只截取了开头一部分) 下面着重说几个重要的偏移。 ②上图的被红框圈的两个字母看见了没,这个就是EXE文件的标示,它占用了文件最开头的俩字节。可能你纳闷了为什么用MZ呢?哈哈,自己上网查查吧。 ③再往下看,在偏移02-03h的地方存放了000C(不要告诉我存放的是0C00),在偏移04-04h的地方存放了0009(请不要替我纠“错”)。通过这两个数据可以计算出文件大小,在这里0009指出该文件用了9个块(1个块是512B),000C指出最后一个块(第9个块)没有用完只用了000C个字节。明白了吧。来,实际计算一下(9﹣1)×512B=4096,再加上12(000Ch)等于4108B。跟DOS里显示的一样。 ④偏移06-07h处为重定向项目的个数。什么叫重定向呢,简单的说就是:EXE文件必须要加载到内存中才能执行,但是文件中数据的偏移地址跟内存中偏移是不一样的,重定向就是达到重新修改偏移的目的。可以看出我们这个文件中重定向项的个数为1。在这里我们也应该看一下偏移18-19h处的数据,它指出了第一个重定向项目在本文件中的偏移,在本文件中为003Eh。即,在本文件003Eh偏移处存放了第一个重定向项目的内容,它的结构体声明为: struct EXE_RELOC { unsigned short offset; unsigned short segment; }; ⑤偏移08-09h处:该处数据指出了EXE头部大小,一般EXE头部后面紧跟着的就是程序数据了。本文件中为0020h,注意它的单位是节,一个节为16个字节,也即程序数据开始于文件偏移200h处。 ⑥偏移0A-0Bh处:该处数据指出了运行该程序所需的最小内存,如果小于这个内存,程序将不会被加载执行。 ⑦偏移0C-0Dh处:该处数据指出了运行该程序所需的最大内存,一般为FFFFh。 ⑧偏移0E-0Fh处:堆栈段在装入模块中的偏移,本文件中为:00E5h 偏移10-11h处:SP初始值,本文件中为:0080h 即SS:SP=00E5:0080 ⑨偏移14-15h处:IP初始值,本文件中为:0 偏移16-17h处:CS在装入模块中的偏移,本文件中为:0 我们看看实际加载到内存中是SS、SP和CS、IP是如何分配的

C语言编译全过程

编译的概念:编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。 编译的完整过程:C源程序-->预编译处理(.c)-->编译、优化程序(.s、.asm)-->汇编程序(.obj、.o、.a、.ko)-->链接程序(.exe、.elf、.axf等) 1. 编译预处理 读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理 伪指令主要包括以下四个方面: (1)宏定义指令,如#define Name TokenString,#undef等。 对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。 (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。 这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉 (3)头文件包含指令,如#include "FileName"或者#include 等。 在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。 采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。 包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。 (4)特殊符号,预编译程序可以识别一些特殊的符号。 例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。 2. 编译、优化阶段 经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语

C程序编译过程详解

C程序编译过程详解 概述: C语言编译的整个过程是非常复杂的,里面涉及到的编译器知识、硬件知识、工具链知识都是非常多的,深入了解整个编译过程对工程师理解应用程序的编写是有很大帮助的,希望大家可以多了解一些,在遇到问题时多思考、多实践。一般情况下,我们只需要知道分成编译和连接两个阶段,编译阶段将源程序(*.c)转换成为目标代码(,一般是obj文件,至于具体过程就是上面说的那些阶段),连接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件(exe文件)就可以了,其他的都需要在实践中多多体会才能有更深的理解。 一. 简单解释: 程序的编译过程如上图所示,分为预处理、编译、汇编、链接等几个阶段。 预处理:预处理相当于根据预处理命令组装成新的C程序,不过常以i为扩展名。 编译:将得到的i文件翻译成汇编代码。s文件。 汇编:将汇编文件翻译成机器指令,并打包成可重定位目标程序的O文件。该文件是二进制文件,字节编码是机器指令。 链接:将引用的其他O文件并入到我们程序所在的o文件中,处理得到最终的可执行文件. 二.详细解释 编译的概念:编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。 编译的完整过程:C源程序-->预编译处理(.c)-->编译、优化程序(.s、.asm)-->汇编程序(.obj、.o、.a、.ko)-->链接程序(.exe、.elf、.axf等) 编译,编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。 C源程序头文件-->预编译处理(cpp)-->编译程序本身-->优化程序-->汇编程序-->链接程序-->可执行文件 1.编译预处理 读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理

C程序的编译过程

C程序的编译过程 编译,编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。 C源程序头文件-->预编译处理(cpp)-->编译程序本身-->优化程序-->汇编程序-->链接程序-->可执行文件elf 1.编译预处理 读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理 [析] 伪指令主要包括以下四个方面 (1)宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。 (2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉 (3)头文件包含指令,如#include "FileName"或者#include 等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。 包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。 (4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。 预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处

相关文档
相关文档 最新文档