首页 / 资讯中心 / 文章详情

预处理、编译、汇编、链接

预处理、编译、汇编、链接 ★ FEATURED ARTICLE
1.翻译环境和运行环境在 ANSI C 的任何⼀种实现中存在两个不同的环境。第一种是翻译环境在这个环境中源代码被转换为可执行的机器指令二进制指令第二种是执行环境它用于实际执行代码。1.1翻译环境翻译环境下源代码被转换为可执行的机器指令二进制指令。翻译环境由编译和链接俩个大的过程组成。编译又可以分解成1,预处理预编译、2,编译、3,汇编三个过程。编译.c文件经过编译器的处理生成.obj(.o)的目标文件。链接目标文件加上链接库在链接器的处理下生成可执行程序.exe。1.2执行环境用于实际执行代码。一般指操作系统。以GCC为例子拆解编译链接的过程:源代码.c和.h为后缀的文件经过预处理.c文件会生成.i为后缀的中间文件.i为后缀的文件经过编译会生成.s为后缀的中间文件.s文件经过汇编会生成.o为后缀的目标文件然后多个目标文件加上链接库经过链接器的处理生成可执行程序.exe。也就是说有如下过程C代码--预处理--编译--汇编--链接--可执行程序____________________________________________________________________以GCC编译器为例子所用到的指令预处理gcc test.c -E -o test.i //-o是output生成的意思编译: gcc test.i -S -o test.s 将c代码转换成汇编代码要进行语义分析...汇编gcc test.s -c -o test.o //将汇编代码转换成机器可执行的指令(二进制指令)2.预处理详解2.1预定义符号__FILE__//进⾏编译的源⽂件__LINE__//⽂件当前的⾏号__DATE__//⽂件被编译的⽇期__TIME__//⽂件被编译的时间__STDC__//如果编译器遵循ANSI C其值为1否则未定义printf(file:%s line:%d\n, __FILE__, __LINE__);2.2#define 定义常量//语法 #define name stuff //例子 #define MAX 1000 #define reg register //为 register这个关键字创建⼀个简短的名字 #define do_forever for(;;) //⽤更形象的符号来替换⼀种实现 #define CASE break;case //在写case语句的时候⾃动把 break写上。 // 如果定义的 stuff过⻓可以分成⼏⾏写除了最后⼀⾏外每⾏的后⾯都加⼀个反斜杠(续⾏ 符)。 #define DEBUG_PRINT printf(file:%s\tline:%d\t \ date:%s\ttime:%s\n ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )// \为续航符,一行写不完可以用续航符多行2.3#define定义宏#define 机制包括了⼀个规定允许把参数替换到文本中这种实现通常称为宏macro或定义宏define macro。宏是纯粹的文本替换。#define name( parament-list ) stuff /* 其中的 parament-list 是⼀个由逗号隔开的符号表它们可能出现在stuff中。 注意 参数列表的左括号必须与name紧邻如果两者之间有任何空⽩存在参数列表就会被解释为stuff的 ⼀部分。 */ //例子,宏是纯粹的文本替换 //宏实现数的平方 #define SQUARE(x) x * x int main() { int a 5; int ret SQUARE(a); printf(%d\n,ret); return 0; }2.4带有副作用的宏参数当宏参数在宏的定义中出现超过⼀次的时候如果参数带有副作⽤那么你在使⽤这个宏的时候就可能出现危险导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。x1;//不带副作⽤ x;//带有副作⽤(改变了x的值) //写一个宏求俩个数的较大值 #define MAX(x,y) (x) (y) ? (x) : (y) int main() { int a 3; int b 5; int ret MAX(a, b); //a中的为后置在三目运算符中先进行判断再自增。显然35,判断完后a,b各自自增一次为4、6 //又yx则执行:后表达式 b,先将b的值此时为6赋给ret 再自增为7 printf(ret %d\n, ret);//6 printf(a %d\n, a);//4 printf(b %d\n, b);//7 return 0; }2.5宏替换的规则在程序中扩展#define定义符号和宏时需要涉及几个步骤。在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先被替换。替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上述处理过程。注意1宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏不能出现递归。2当预处理器搜索#define定义的符号的时候字符串常量的内容并不被搜索。2.6宏和函数的对比宏通常被应⽤于执行简单的运算。//⽐如在两个数中找出较⼤的⼀个时写成下⾯的宏更有优势⼀些。 #define MAX(a, b) ((a)(b)?(a):(b))那为什么不选择函数完成呢1用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜⼀筹。2更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于 来比较的类型。宏的参数是类型无关的。和函数相比宏的劣势1每次使用宏的时候⼀份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。2宏是没法调试的。3宏由于类型无关也就不够严谨。4宏可能会带来运算符优先级的问题导致程容易出现错。宏有时候可以做函数做不到的事情。比如宏的参数可以出现类型但是函数做不到。#define MALLOC(num, type)\ (type )malloc(num sizeof(type)) ... //使⽤ MALLOC(10, int);//类型作为参数 //预处理器替换之后 (int *)malloc(10 sizeof(int));3.#和###运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。#运算符所执行的操作可以理解为字符串化。#define Print(n,format) printf(the value of #n is format\n,n) //#运算符 int main() { int a 1; Print(a,%d);//the value of a is 1 return 0; }##运算符可以把位于它两边的符号合成⼀个符号它允许宏定义从分离的文本片段创建标识符。 ##被称为记号粘合。这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。在实际开发过程中##使⽤的很少很难举出恰当例子。#define GENERIC_MAX(type) \ type type##_max(type x,type y) \ { \ return (xy?x:y); \ } GENERIC_MAX(int) GENERIC_MAX(float) int main() { int m int_max(2,3); printf(%d\n,m); float fm float_max(3.5f, 4.5f); printf(%.2f\n,fm); return 0; }4.命名约定⼀般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分⼆者。那我们平时的⼀个习惯是:把宏名全部大写函数名不要全部大写这只是大部分情况也有宏是全小写如offestof用来计算结构体成员相较于结构体起始位置的偏移量5.#undef这条指令用于移除⼀个宏定义。#undef NAME //如果现存的⼀个名字需要被重新定义那么它的旧名字⾸先要被移除。6.条件编译在编译⼀个程序的时候我们如果要将⼀条语句⼀组语句编译或者放弃是很方便的。因为我们有条件编译指令。//比如调试性的代码删除可惜保留⼜碍事所以我们可以选择性的编译。 #include stdio.h #define __DEBUG__ int main() { int i 0; int arr[10] {0}; for(i0; i10; i) { arr[i] i; #ifdef __DEBUG__ printf(%d\n, arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__ } return 0; }常见条件编译指令1. #if 常量表达式 //若if后常量表达式为假则不执行 //... #endif //常量表达式由预处理器求值。 如 #define __DEBUG__ 1 #if __DEBUG__ //.. #endif #if 0 //##运算符 #define GENERIC_MAX(type) \ type type##_max(type x,type y) \ { \ return (xy?x:y); \ } GENERIC_MAX(int) GENERIC_MAX(float) int main() { int m int_max(2,3); printf(%d\n,m); float fm float_max(3.5f, 4.5f); printf(%.2f\n,fm); return 0; } #endif 2.多个分⽀的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif 3.判断是否被定义 #if defined(symbol) #ifdef symbol #if !defined(symbol) #ifndef symbol 4.嵌套指令 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif7.头文件的包含7.1头文件被包含的方式本地文件包含 #include filename查找策略先在源文件所在目录下查找如果该头文件未找到编译器就像查找库函数头文件⼀样在标准位置查找头文件。如果找不到就提示编译错误。库文件包含 #include filename.h查找头文件直接去标准路径下查找若找不到就提示编译错误。7.2嵌套文件包含我们已经知道 #include指令可以使另外⼀个文件被编译。就像它实际出现于#include指令的地方⼀样。这种替换的方式很简单预处理器先删除这条指令并用包含文件的内容替换。⼀个头文件被包含10次那就实际被编译10次如果重复包含对编译的压力就比较大。为了解决这个问题在头文件中写入下行代码就可避免头文件被重复编译。#pragma once
阅读完成 · 觉得有帮助?
咨询建站