计算机编程C语言第1讲7.1—7.1.12编译与预处理
- 格式:doc
- 大小:2.05 MB
- 文档页数:14
C语言编译过程详解C语言是一种广泛应用于软件开发和系统编程的高级编程语言。
为了将C语言源代码转换为计算机可以执行的机器码,需要经过一系列的编译过程。
在本文中,我们将详细介绍C语言编译的几个阶段,并解释每个阶段的作用和过程。
一、预处理阶段预处理阶段是编译过程的第一步,其目的是处理源代码中的宏定义、条件编译指令和头文件引用等。
在这一阶段,编译器会根据预处理指令将源代码进行修改和替换。
预处理器还可以将源文件中包含的其他文件一同合并,生成一个拓展名为".i"的中间文件。
二、编译阶段编译阶段是将预处理后的源代码转换为汇编语言的阶段。
编译器会将C语言源文件翻译成汇编语言,生成一个拓展名为".s"的汇编代码文件。
这个文件包含了与机器相关的汇编指令,但是还不是最终可以在机器上执行的形式。
三、汇编阶段汇编阶段是将汇编语言代码翻译为机器语言指令的过程。
在这一阶段,汇编器将汇编代码转换为二进制的机器指令,并将其保存在一个拓展名为".o"的目标文件中。
这个目标文件包含了机器代码和一些与目标机器相关的信息。
四、链接阶段链接阶段是将编译生成的目标文件和库文件进行整合,生成最终的可执行文件。
链接器会解析目标文件中的符号引用,并将其与其他对象文件中定义的符号进行连接。
此外,还会进行地址重定位、符号决议和库函数的链接等操作。
最终生成的可执行文件可以在目标机器上运行。
C语言编译过程总结综上所述,C语言的编译过程可以分为预处理、编译、汇编和链接四个阶段。
在预处理阶段,预处理器会处理源代码中的宏定义和头文件引用等。
在编译阶段,编译器将C语言源文件翻译成汇编语言。
在汇编阶段,汇编器将汇编代码转换为机器指令。
在链接阶段,链接器将目标文件和库文件进行整合,生成最终的可执行文件。
C语言的编译过程不仅有助于我们理解程序的执行原理,还可以帮助我们排除程序中的错误和优化代码。
通过深入了解编译过程,我们可以更好地掌握C语言的使用和开发。
第七章2本章导读:C 语言的个重要特征,可以改进程序设计预处理是C语言的一个重要特征,可以环境,提高编译效率。
C 语言在正式编译(语法分析)系统先对这些命令进行“预处理”,进行宏替换之前系统先对这些命令进行预处理,进行宏替换和将包含的函数定义包含进源程序,然后整个源程序再进行通常的编译处理。
①本章学习重点:宏定义和宏替换②文件包含③条件编译7.1 概述概在前面各章中,已多次使用过以“# ”号开头的预处理命令。
如包含命令#include,宏定义命令#define 等。
在源程序中这些命令都放在函数之外,而且一般都放在源文件的前面,它们称为预处理部分。
所谓预处理是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所做的工作。
预处理是C语言的一个重要功能,它由预处理程序负责完成。
当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预部分作完毕自动进对程序的编的预处理部分作处理,处理完毕自动进入对源程序的编译。
概7.1 概述C 语言提供了多种预处理功能,如宏定义、文件包含、条件编译等。
合理地使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
本章将介绍常用的也有利于模块化程序设计本章将介绍常用的几种预处理功能。
7.2 宏定义在C 语言源程序中允许用一个标识符来表示个字符串,称为宏。
被定义为宏的标一个字符串“宏”。
被定义为“宏”的标识符称为“宏名”。
在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去所有出现的“宏名”代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令完成的。
是由源程序中的宏定义命令完成的宏替换是由预处理程序自动完成的。
在C语言中,“宏”分为有参数和无参数两种。
下面分别讨论宏分为有参数参数种面分别讨论这两种“宏”的定义和调用。
7.2.1 无参宏定义无参宏的宏名后不带参数。
其定义的一般形式为:# define 标识符字符串#define其中的“# ”表示这是一条预处理命令。
C语言中的预处理详解目录一.预处理的工作方式 (3)1.1.预处理的功能 (3)1.2预处理的工作方式 (3)二.预处理指令 (4)2.1.预处理指令 (4)2.2.指令规则 (4)三.宏定义命令----#define. 43.1.无参数的宏 (4)3.2带参数的宏 (5)3.3.预处理操作符#和##. 63.3.1.操作符#. 63.3.2.操作符##. 6四.文件包含------include. 6五.条件编译 (7)5.1使用#if 75.2使用#ifdef和#ifndef 95.3使用#defined和#undef 10六.其他预处理命令 (11)6.1.预定义的宏名 (11)6.2.重置行号和文件名命令------------#line. 11 6.3.修改编译器设置命令 ------------#pragma. 12 6.4.产生错误信息命令 ------------#error 12 七.内联函数 (13)在嵌入式系统编程中不管是内核的驱动程序还是应用程序的编写,涉及到大量的预处理与条件编译,这样做的好处主要体现在代码的移植性强以及代码的修改方便等方面。
因此引入了预处理与条件编译的概念。
在C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。
预处理命令属于C语言编译器,而不是C语言的组成部分。
通过预处理命令可扩展C语言程序设计的环境。
一.预处理的工作方式1.1.预处理的功能在集成开发环境中,编译,链接是同时完成的。
其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。
预编译的主要作用如下:●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。
1.2预处理的工作方式预处理的行为是由指令控制的。
这些指令是由#字符开头的一些命令。
#define指令定义了一个宏---用来代表其他东西的一个命令,通常是某一个类型的常量。
C语言编译过程通常分为预处理、编译、汇编和链接四个步骤。
以下是C语言编译过程的详细解释:
1. 预处理:在编译之前,预处理器会对源代码进行预处理。
预处理包括以下步骤:
-删除源代码中的注释
-展开宏定义
-处理文件中的预定义符号
2. 编译:编译器将预处理后的代码转换成中间代码(即汇编语言)。
编译器会对源代码进行词法分析、语法分析和优化,生成目标代码(即汇编语言)。
3. 汇编:汇编器将汇编代码转换成机器指令。
汇编器将汇编指令转换成机器指令,并将它们组合成可执行的程序。
4. 链接:链接器将多个目标文件组合成一个可执行文件或共享库文件。
链接器会解决符号引用问题,并将它们链接到相应的代码段和数据段。
在C语言编译过程中,编译器和链接器通常使用标准库和用户定义的库。
标准库提供了一些常用的函数和数据类型,如printf()和malloc()。
用户定义的库可以包含自定义的函数和数据类型,以便更好地满足应用程序的需求。
总之,C语言编译过程是一个复杂的过程,需要多个步骤和工具的协同工作。
正确的编译过程可以确保生成的可执行程序具有良好的性能和可靠性。
C语言的编译预处理发布:2009-11-11 13:56 | 作者:hnrain | 来源:本站| 查看:44次| 字号: 小中大在将一个C源程序转换为可执行程序的过程中, 编译预处理是最初的步骤. 这一步骤是由预处理器(preprocessor)来完成的. 在源流程序被编译器处理之前, 预处理器首先对源程序中的"宏(macro)"进行处理.C初学者可能对预处理器没什么概念, 这是情有可原的: 一般的C编译器都将预处理, 汇编, 编译, 连接过程集成到一起了. 编译预处理往往在后台运行. 在有的C编译器中, 这些过程统统由一个单独的程序来完成, 编译的不同阶段实现这些不同的功能. 可以指定相应的命令选项来执行这些功能. 有的C编译器使用分别的程序来完成这些步骤. 可单独调用这些程序来完成. 在gcc中, 进行编译预处理的程序被称为CPP, 它的可执行文件名为cpp.编译预处理命令的语法与C语言的语法是完全独立的. 比如: 你可以将一个宏扩展为与C语法格格不入的内容, 但该内容与后面的语句结合在一个若能生成合法的C语句, 也是可以正确编译的.(一) 预处理命令简介--------------------------------------------------------------------------------预处理命令由#(hash字符)开头, 它独占一行, #之前只能是空白符. 以#开头的语句就是预处理命令, 不以#开头的语句为C中的代码行. 常用的预处理命令如下:#define 定义一个预处理宏#undef 取消宏的定义#include 包含文件命令#include_next 与#include相似, 但它有着特殊的用途#if 编译预处理中的条件命令, 相当于C语法中的if语句#ifdef 判断某个宏是否被定义, 若已定义, 执行随后的语句#ifndef 与#ifdef相反, 判断某个宏是否未被定义#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else#endif #if, #ifdef, #ifndef这些条件命令的结束标志.defined 与#if, #elif配合使用, 判断某个宏是否被定义#line 标志该语句所在的行号# 将宏参数替代为以参数值为内容的字符窜常量## 将两个相邻的标记(token)连接为一个单独的标记#pragma 说明编译器信息#warning 显示编译警告信息#error 显示编译错误信息(二) 预处理的文法--------------------------------------------------------------------------------预处理并不分析整个源代码文件, 它只是将源代码分割成一些标记(token), 识别语句中哪些是C语句, 哪些是预处理语句. 预处理器能够识别C标记, 文件名, 空白符, 文件结尾标志.预处理语句格式: #command name(...) token(s)1, command预处理命令的名称, 它之前以#开头, #之后紧随预处理命令, 标准C允许#两边可以有空白符, 但比较老的编译器可能不允许这样. 若某行中只包含#(以及空白符), 那么在标准C中该行被理解为空白. 整个预处理语句之后只能有空白符或者注释, 不能有其它内容.2, name代表宏名称, 它可带参数. 参数可以是可变参数列表(C99).3, 语句中可以利用"\"来换行.e.g.# define ONE 1 /* ONE == 1 */等价于: #define ONE1#define err(flag, msg) if(flag) \printf(msg)等价于: #define err(flag, msg) if(flag) printf(msg)(三) 预处理命令详述--------------------------------------------------------------------------------1, #define#define命令定义一个宏:#define MACRO_NAME(args) tokens(opt)之后出现的MACRO_NAME将被替代为所定义的标记(tokens). 宏可带参数, 而后面的标记也是可选的. 对象宏不带参数的宏被称为"对象宏(objectlike macro)"#define经常用来定义常量, 此时的宏名称一般为大写的字符串. 这样利于修改这些常量.e.g.#define MAX 100int a[MAX];#ifndef __FILE_H__#define __FILE_H__#include "file.h"#endif#define __FILE_H__ 中的宏就不带任何参数, 也不扩展为任何标记. 这经常用于包含头文件.要调用该宏, 只需在代码中指定宏名称, 该宏将被替代为它被定义的内容.函数宏带参数的宏也被称为"函数宏". 利用宏可以提高代码的运行效率: 子程序的调用需要压栈出栈, 这一过程如果过于频繁会耗费掉大量的CPU运算资源. 所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率.函数宏的参数是固定的情况函数宏的定义采用这样的方式: #define name( args ) tokens其中的args和tokens都是可选的. 它和对象宏定义上的区别在于宏名称之后不带括号.注意, name之后的左括号(必须紧跟nam e, 之间不能有空格, 否则这就定义了一个对象宏, 它将被替换为以(开始的字符串. 但在调用函数宏时, name与(之间可以有空格.e.g.#define mul(x,y) ((x)*(y))注意, 函数宏之后的参数要用括号括起来, 看看这个例子:e.g.#define mul(x,y) x*y"mul(1, 2+2);" 将被扩展为: 1*2 + 2同样, 整个标记串也应该用括号引用起来:e.g.#define mul(x,y) (x)*(y)sizeof mul(1,2.0) 将被扩展为sizeof 1 * 2.0调用函数宏时候, 传递给它的参数可以是函数的返回值, 也可以是任何有意义的语句:e.g.mul (f(a,b), g(c,d));e.g.#define insert(stmt) stmtinsert ( a="1"; b="2";) 相当于在代码中加入a="1"; b="2" .insert ( a="1", b="2";) 就有问题了: 预处理器会提示出错: 函数宏的参数个数不匹配. 预处理器把","视为参数间的分隔符.insert ((a=1, b="2";)) 可解决上述问题.在定义和调用函数宏时候, 要注意一些问题:1, 我们经常用{}来引用函数宏被定义的内容, 这就要注意调用这个函数宏时的";"问题.example_3.7:#define swap(x,y) { unsigned long _temp=x; x="y"; y=_tmp}如果这样调用它: "swap(1,2);" 将被扩展为: { unsigned long _temp=1; 1=2; 2=_tmp};明显后面的;是多余的, 我们应该这样调用: swap(1,2)虽然这样的调用是正确的, 但它和C语法相悖, 可采用下面的方法来处理被{}括起来的内容:#define swap(x,y) \do { unsigned long _temp=x; x="y"; y=_tmp} while (0)swap(1,2); 将被替换为:do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);在Linux内核源代码中对这种do-while(0)语句有这广泛的应用.2, 有的函数宏是无法用do-while(0)来实现的, 所以在调用时不能带上";", 最好在调用后添加注释说明. eg_3.8:#define incr(v, low, high) \for ((v) = (low),; (v) <= (high); (v)++)只能以这样的形式被调用: incr(a, 1, 10) /* increase a form 1 to 10 */函数宏中的参数包括可变参数列表的情况C99标准中新增了可变参数列表的内容. 不光是函数, 函数宏中也可以使用可变参数列表.#define name(args, ...) tokens#define name(...) tokens"..."代表可变参数列表, 如果它不是仅有的参数, 那么它只能出现在参数列表的最后. 调用这样的函数宏时, 传递给它的参数个数要不少于参数列表中参数的个数(多余的参数被丢弃).通过__VA_ARGS__来替换函数宏中的可变参数列表. 注意__VA_ARGS__只能用于函数宏中参数中包含有"..."的情况.e.g.#ifdef DEBUG#define my_printf(...) fprintf(stderr, __VA_ARGS__)#else#define my_printf(...) printf(__VA_ARGS__)#endiftokens中的__VA_ARGS__被替换为函数宏定义中的"..."可变参数列表.注意在使用#define时候的一些常见错误:#define MAX = 100#define MAX 100;=, ; 的使用要值得注意. 再就是调用函数宏是要注意, 不要多给出";".注意: 函数宏对参数类型是不敏感的, 你不必考虑将何种数据类型传递给宏. 那么, 如何构建对参数类型敏感的宏呢? 参考本章的第九部分, 关于"##"的介绍.关于定义宏的另外一些问题(1) 宏可以被多次定义, 前提是这些定义必须是相同的. 这里的"相同"要求先后定义中空白符出现的位置相同, 但具体的空白符类型或数量可不同, 比如原先的空格可替换为多个其他类型的空白符: 可为tab, 注释...e.g.#define NULL 0#define NULL/* null pointer */ 0上面的重定义是相同的, 但下面的重定义不同:#define fun(x) x+1#define fun(x) x + 1 或: #define fun(y) y+1如果多次定义时, 再次定义的宏内容是不同的, gcc会给出"NAME redefined"警告信息.应该避免重新定义函数宏, 不管是在预处理命令中还是C语句中, 最好对某个对象只有单一的定义. 在gcc中, 若宏出现了重定义, gcc会给出警告.(2) 在gcc中, 可在命令行中指定对象宏的定义:e.g.$ gcc -Wall -DMAX=100 -o tmp tmp.c相当于在tmp.c中添加" #define MAX 100".那么, 如果原先tmp.c中含有MAX宏的定义, 那么再在gcc调用命令中使用-DMAX, 会出现什么情况呢? ---若-DMAX=1, 则正确编译.---若-DMAX的值被指定为不为1的值, 那么gcc会给出MAX宏被重定义的警告, MAX的值仍为1.注意: 若在调用gcc的命令行中不显示地给出对象宏的值, 那么gcc赋予该宏默认值(1), 如: -DVAL == -DVAL=1(3) #define所定义的宏的作用域宏在定义之后才生效, 若宏定义被#undef取消, 则#undef之后该宏无效. 并且字符串中的宏不会被识别。
7.1编译与预处理
1、预处理:是指在进行编译之前所作的处理,由预处理程序负责完成。
2、宏:用一个标识符来表示一个字符串,称为“宏”,被定义为“宏”的标识符称为“宏名”。
3、宏替换:在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”;
4、Vs2013运行预处理与编译代码:
7.1.2HelloWorld案例拓展
Hello,World这样简单的示例程序,都要经过编辑、预处理、编译、链接4个步骤,才能变成可执行程序,鼠标双击就弹出命令窗口,显示“Hello,World”。
这也是一般C语言程序的编译流程,如所示。
7.1.3编辑
编辑可能就是通常所说的“写代码”,用集成开发工具也好,用记事本也好,按C语言的语法规则组织一系列的源文件,主要有两种形式,一种是.c文件,另一种是.h文件,也称头文件。
7.1.4预处理
“#include”和“#define”都属于编译预处理,C语言允许在程序中用预处理指令写一些命令行。
预处理器在编译器之前根据指令更改程序文本。
编译器看到的是预处理器修改过的代码文本,C语言的编译预处理功能主要包括宏定义、文件包含和条件编译3种。
预处理器对宏进行替换,并将所包含的头文件整体插入源文件中,为后面要进行的编译做好准备。
下面是一个包含头文件的例子,请看
(1)举例:下面是一个头文件,p.txt
(2)包含该头文件,并执行代码如下:
(3)执行后的结果如下:
7.1.5编译
编译器处理的对象是由单个c文件和其中递归包含的头文件组成的编译单元,一般来说,头文件是不直接参加编译的。
编译器会将每个编译单元翻译成同名的二进制代码文件,在DOS 和Windows环境下,二进制代码文件的后缀名为.obj,在Unix环境下,其后缀名为.o,此时,二进制代码文件是零散的,还不是可执行二进制文件。
了解编译过程:
编译:
在文件资源管理中查看:
编译过程示例如下:
在linux下编译链接过程:(1)源代码如下
(2)
生成run.o文件
(3)生成可执行文件
生成run.out
(4)执行run.out。
结果如下
编译后不能直接执行。
上述问题解决办法是实现go()函数
可执行,其结果如下
7.1.5编译
编译器处理的对象是由单个c文件和其中递归包含的头文件组成的编译单元,一般来说,头文件是不直接参加编译的。
编译器会将每个编译单元翻译成同名的二进制代码文件,在DOS 和Windows环境下,二进制代码文件的后缀名为.obj,在Unix环境下,其后缀名为.o,此时,二进制代码文件是零散的,还不是可执行二进制文件。
错误检查大多是在编译阶段进行的,编译器主要进行语法分析,词法分析,产生目标代码并进行代码优化等处理。
为全局变量和静态变量等分配内存,并检查函数是否已定义,如没有定义,是否有函数声明。
函数声明通知编译器:该函数在本文件晚些时候定义,或者是在其他文件中定义。
在vs2013工程中如何查看编译器:
总结:
7.1.6链接
链接器将编译得到的零散的二进制代码文件组合成二进制可执行文件,主要完成下述两个工作,一是解析其他文件中函数引用或其他引用,二是解析库函数。
7.1.7程序错误
编译链接,一大堆的错误提示,没有完美的程序,不存在没有缺陷的程序,如果一个程序运
行很完美,那是因为它的缺陷到现在还没有被发现。
同样,软件测试是为了发现程序中可能存在的问题,而不是证明程序没有错误。
7.1.8错误分类
错误可分两大类,一是程序书写形式在某些方面不合C语言要求,称为语法错误,这种错误将会由编译器指明,是种比较容易修改的错误,二是程序书写本身没错,编译链接能够完成,但输出结果与预期不符,或着执行着便崩溃掉,称为逻辑错误。
语法错误又可分为编译错误和链接错误,很明显,编译错误就是在程序编译阶段出的错误,而链接错误就是在程序链接阶段出的问题。
示例:排错过程
少写了分号时如下:
逻辑错误,运行(读取错误)示例:
7.1.9编译错误
如果文件中出现编译错误,编译器将给出错误信息,并指明错误所在的行,提示用户修改代码,编译错误主要有两类:
(1)语法问题,缺少符号,如缺分号,缺括号等,符号拼写不正确,一般来说,编译器都会指明错误所在行,但由于代码是彼此联系的,有时编译器给出的信息未必正确。
(2)上下文关系有误,程序设计中有很多彼此关联的东西,比如变量要先创建再使用,有时编译器会发现某个变量尚未定义,便会提示出错。
除了错误外,编译器还会对程序中一些不合理的用法进行警告(warning),尽管警告不耽误程序编译链接,但对警告信息不能掉以轻心,警告常常预示着隐藏很深的错误,特别是逻辑错误,应当仔细排查。
错误示例:(分号写成逗号)
上下文联系错误(未声明的标识符):
运行错误(访问野指针)
7.1.10链接错误
当一个编译单元中调用了库函数或定义在其他编译单元中的函数时,在链接阶段就需要从库文件或其他目标文件中抽取该函数的二进制代码,以便进行组合等一系列工作,找不到函数定义时,链接器无法找到该函数对应的代码,便会提示出错,指出名字未解析。
一般来说,链接器给出的错误提示信息是关乎函数的链接。
代码示例链接错误:
出现错误是由于缺少z的变量定义,加上下面这个文件就可以了
7.1.11逻辑错误
程序顺利通过了编译链接,可要检查生成的可执行程序,看其是否实现了所需的功能。
实际上,运行阶段出现的逻辑错误更难排查,编译错误和链接错误好歹有提示信息,但面对逻辑错误,就像浑水摸鱼。
可能出现的逻辑错误有以下情况:
(1)、与操作系统有关的操作,是否进行了非法操作,如非法内存访问等。
错误示例:
可以编译连接运行错误,但是执行结果与想象的不一样,这是因为多了一个分号,逻辑错误。
错误示例:
把1234作为地址,当读取*p时实际读取的1234地址的数据,此时非法访问,出现错误。
(2)、是否出现了死循环,表现为长时间无反应,假死,注意,长时间无反应并不一定都是死循环,有的程序确实需要很长时间,这种情况要仔细分析。
例:
(3)、程序执行期间发生了一些异常,比如除数为0等,操作无法继续进行。
错误提示:
(4)、程序能正确执行,但结果不对,此时应检查代码的编写是否合乎问题规范。
7.1.12排错
排除错误,有两层含义,找到出错的代码,修改该代码。
排错也有两种形式,一是静态排错,另一种是动态排错。
如果还是不行,就要使用动态检查机制,最基本的方法是“分而治之”,检查程序执行的中间状态,最常用的方法是在可能出错的地方插入一些输出语句,让程序输出一些中间变量的值,确定可能出错的区域。
此外,还可利用编译环境提供的DEBUG工具,对程序进行跟踪、监视和设断点等,定位并排错。
例题代码如下:
可编译不可执行,说明是链接器错误。
需要静态排错。