关键词挖掘工具爱站网,网页出现网站维护,免费网络推广,深圳大浪网站建设Faye#xff1a;孤独让我们与我们所爱的人相处的每个瞬间都无比珍贵#xff0c;让我们的回忆价值千金。它还驱使你去寻找那些你在我身边找不到的东西。 ---------《寻找天堂》 目录
一、编译和链接的介绍
1.1 程序的翻译环境和执行环境
1.1.1 翻译环境
1.1.2 运行环境
… Faye孤独让我们与我们所爱的人相处的每个瞬间都无比珍贵让我们的回忆价值千金。它还驱使你去寻找那些你在我身边找不到的东西。 ---------《寻找天堂》 目录
一、编译和链接的介绍
1.1 程序的翻译环境和执行环境
1.1.1 翻译环境
1.1.2 运行环境
1.2 预处理
1.2.1 预定义符号 1.2.2 #define #define的语法 #define 替换规则 #和##
带副作用的宏参数
编辑 宏和函数对比 #undef
1.3 编译 1.3.1 词法分析 1.3.2 语法分析
1.3.3 语义分析
1.4 汇编 1.5 链接 一、编译和链接的介绍
1.1 程序的翻译环境和执行环境 在ANSI C的任何一种实现中存在两个不同的环境。 第1种是翻译环境在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境它用于实际执行代码。 注ANSI C是由美国国家标准协会ANSI及国际标准化组织ISO推出的关于C语言的标准。ANSI C 主要标准化了现存的实现 同时增加了一些来自 C 的内容 主要是函数原型 并支持多国字符集 包括备受争议的三字符序列。 ANSI C 标准同时规定了 C 运行期库例程的标准。 1.1.1 翻译环境 组成一个程序的每个源文件通过编译过程分别转换成目标代码object code。 每个目标文件由链接器linker捆绑在一起形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数而且它可以搜索程序员个人的程序库将其需要的函数也链接到程序中 翻译的几个环节通过下面的图进行初步的了解 1.1.2 运行环境 程序执行的过程 程序必须载入内存中。在有操作系统的环境中一般这个由操作系统完成。在独立的环境中程序的载入必须由手工安排也可能是通过可执行代码置入只读内存来完成。 程序的执行便开始。接着便调用main函数。开始执行程序代码。这个时候程序将使用一个运行时堆栈stack存储函数的局部变量和返回地址。程序同时也可以使用静态static内存存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。 终止程序。正常终止main函数也有可能是意外终止。
1.2 预处理 在预处理阶段源⽂件和头⽂件会被处理成为 .i 为后缀的⽂件。在 gcc 环境下想观察⼀下对 test.c ⽂件预处理后的.i⽂件命令以下所有的命令在Linux下的指令如下 gcc -E test.c -o test.i 在Linux下执行这条指令后生成.i文件查看里面内容大多都是宏 预处理阶段主要处理那些源⽂件中#开始的预编译指令。 ⽐如#include,#define处理的规则如下
将所有的 #define 删除并展开所有的宏定义。处理所有的条件编译指令如 #if、#ifdef、#elif、#else、#endif 。处理#include 预编译指令将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进行的也就是说被包含的头⽂件也可能包含其他⽂件。删除所有的注释添加⾏号和⽂件名标识⽅便后续编译器⽣成调试信息等。或保留所有的#pragma的编译器指令编译器后续会使⽤。
经过预处理后的 .i ⽂件中不再包含宏定义因为宏已经被展开。并且包含的头⽂件都被插⼊到 .i⽂件中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候可以查看预处理后的 .i ⽂件来确认。
1.2.1 预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C其值为1否则未定义 这些预定义的符号都是语言内置的。下面使用部分宏
#includestdio.hint main() {//__FILE__进行编译的源文件 __LINE__文件当前的行号printf( file:%s \n line:%d\n, __FILE__, __LINE__); //__DATE__ 文件被编译的日期 __TIME__ 文件被编译的时间printf( date:%s \n time:%lld\n, __DATE__, __TIME__);return 0;
} 运行结果如下 1.2.2 #define #define是一种定义标识符用来定义宏下面是#define的功能介绍 #define 机制包括了一个规定允许把参数替换到文本中这种实现通常称为宏 macro 或定义宏define macro 。 举个梨子
#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__ ) 在define定义标识符的时候建议不要加上 ; 这样容易导致问题。比如下面的场景
#includestdio.h
#define MAX 1000;
int main() {int condition 0, max 0;if (condition) max MAX; elsemax 0;return 0;
} 在vs下进行编译这里会出现语法错误。 #define的语法 语法 #define name stuff 下面是宏的申明方式 #define name( parament- list ) stuff 其中的 parament- list 是一个由逗号隔开的符号表它们可能出现在 stuff 中 宏的命名约定 把宏名全部大写 函数名不要全部大写 注意 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分 举个小梨子定义一个数的平方的宏 #define MUL(x) x*x //参数列表的左括号必须与name紧邻 这个宏接收一个参数 x 如果在上述声明之后MUL(5)。置于程序中预处理器就会用下面这个表达式替换上面的表达式5*5
#includestdio.h#define MUL(x) x*x //参数列表的左括号必须与name紧邻。int main() {printf(%d\n, MUL(5)); // printf(%d\n, 5*5)return 0;
} 那么如果输入的参数是一个表达式呢MUL宏输出的结果是否还是符合预期呢此时将MUL5替换为 MUL(23)它的预期结果应该也是25运行一下看看 结果是11为什么呢这时候把(23)参数带入MUL宏中看看 23* 23 263,所以输出的结果变为了11。这样就比较清晰了由替换产生的表达式并没有按照预想的次序进行求值。这里涉及到了运算符优先级的问题。在宏定义上加上两个括号这个问题便轻松的解决了 #define MUL(x) (x)*(x) 举另外一个小梨子定义一个数跟自己加和的宏
#includestdio.h#define SADD(x) (x)(x) //参数列表的左括号必须与name紧邻。int main() {printf(%d\n, 10*SADD(2) SADD(2)); //预期结果 10*4444return 0;
} 欸这又是怎么回事参数我也加上了小括号不应该呀。依旧是上面的分析方法将宏在表达式中进行展开10*SADD(2) SADD(2) 10 * (2) (2 (2) (2 20626。乘法运算先于宏定义的加法所以出现了26。这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了 #define SADD(x) ((x)(x)) 这样运行结果便符合预期啦 通过上面两个小梨子得出以下的经验 用于对数值表达式进行求值的宏定义都应该用这种方式加上括号避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。 #define 替换规则 在程序中扩展 #define 定义符号和宏时需要涉及几个步骤。 1. 在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先 被替换。 2. 替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。 3. 最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上述处理过程。 注意 1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏不能出现递归。 2. 当预处理器搜索#define定义的符号的时候字符串常量的内容并不被搜索。 #和## 字符串是有自动连接的特点的将两个或者多个字符串紧挨着它们会自动连接形成一个组合后的字符串通过下面的小梨子看看
#includestdio.h
int main() {char* p hello world!!!\n;printf(hello world!!!\n);printf(%s, p);return 0;
} 如果把这些写到宏里是不是实现同样的效果呢 使用 # 把一个宏参数变成对应的字符串。还可以添加部分参数进行打印
#includestdio.h//使用 # 把一个宏参数变成对应的字符串
//FORMAT 数据输出格式,VALUE 数据
#define PRINT(FORMAT,VALUE) printf(the value of #VALUE is FORMAT\n, VALUE);int main() {int i 10;PRINT(% d , i 5)return 0;
} 代码中的 #VALUE 会预处理器处理为 VALUE ## 的作用 ## 可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。 通过一个小梨子看看
#includestdio.h
#define ADD_TO_SUM(num, value) s##num value;int main() {int s1 0, s2 0, s3 0;ADD_TO_SUM(1, 10) // 作用是给s1增加10.ADD_TO_SUM(2, 20) //给s2增加20.ADD_TO_SUM(3, 30) //给s3增加30.printf(s1: %d s2: %d s3: %d, s1, s2, s3);return 0;
} 带副作用的宏参数 当宏参数在宏的定义中出现超过一次的时候如果参数带有副作用那么你在使用这个宏的时候就可能出现危险导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如 x 1 ; // 不带副作用 不会改变参数的数值 x ; // 带有副作用 参数的数值被永久修改 借用上面MUL的宏运行下列代码
#define MUL(x) (x)* (x)int main() {int i 2;printf(MUL: %d i: %d\n, MUL(i),i);return 0;
} 发现宏替换后MUL(i) (i)* (i) 。此后i被加加两次产生了副作用 宏和函数对比 宏通常被应用于执行简单的运算。 那为什么不用函数来完成这个任务 原因有二 1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。 2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于来比较的类型。 宏是类型无关的。 宏的缺点 当然和函数相比宏也有劣势的地方 1. 每次使用宏的时候一份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。 2. 宏是没法调试的。 3. 宏由于类型无关也就不够严谨。 4. 宏可能会带来运算符优先级的问题导致程容易出现错。 宏有时候可以做函数做不到的事情。比如宏的参数可以出现类型但是函数做不到。 下面表格是将宏与函数进行对比 属性 #define 定义宏 函数 代码长度 每次使用时宏代码都会被插入到程序中。除了非常 小的宏之外程序的长度会大幅度增长 函数代码只出现于一个地方每次使用这个函数时都调用那个地方的同一份代码 执行速度 更快 存在函数的调用和返回的额外开销所以相对慢一些 操作符优 先级 宏参数的求值是在所有周围表达式的上下文环境里 除非加上括号否则邻近操作符的优先级可能会产生 不可预料的后果所以建议宏在书写的时候多些括号。 函数参数只在函数调用的时候求 值一次它的结果值传递给函 数。表达式的求值结果更容易预 测 带有 副作 用的 参数 参数可能被替换到宏体中的多个位置所以带有副作用的参数求值可能会产生不可预料的结果。 函数参数只在传参的时候求值一 次结果更容易控制。 参数 类型 宏的参数与类型无关只要对参数的操作是合法的 它就可以使用于任何参数类型 函数的参数是与类型有关的如 果参数的类型不同就需要不同 的函数即使他们执行的任务是 相同的。 调试 宏是不方便调试的 函数是可以逐语句调试的 递 归 宏是不能递归的 函数是可以递归的 #undef 这条指令用于移除一个宏定义。 #undef NAME // 如果现存的一个名字需要被重新定义那么它的旧名字首先要被移除。 1.3 编译 编译过程就是将预处理后的文件进行⼀系列的词法分析、语法分析、语义分析及优化⽣成相应的汇编代码文件。编译过程的命令如下 gcc -S test.i -o test.s 在Linux下执行这条指令后生成.s文件查看.s文件里面内容是相应的汇编代码 对下面的代码进行编译的时候流程会是怎么样的呢 num(z6)*(9/3) 1.3.1 词法分析 将源代码程序被输⼊扫描器扫描器的任务就是简单的进⾏词法分析把代码中的字符分割成⼀系列的记号关键字、标识符、字⾯量、特殊字符等。
上⾯程序进行词法分析后得到了13个记号
记号类型num标识符赋值(左圆括号z标识符加号6数字)右圆括号*乘号(左圆括号9数字加号3数字)右圆括号 1.3.2 语法分析 接下来语法分析器将对扫描产⽣的记号进行语法分析从⽽产⽣语法树。这些语法树是以表达式为节点的树 1.3.3 语义分析 由语义分析器来完成语义分析即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配类型的转换等。这个阶段会报告错误的语法信息。 1.4 汇编 汇编器是将汇编代码转转变成机器可执行的指令每⼀个汇编语句几乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进行翻译也不做指令优化。汇编的命令如下 gcc -c test.s -o test.o 在Linux下执行这条指令后生成.o文件查看里面内容为机器语言 1.5 链接 链接是一个复杂的过程链接的时候需要把一堆文件链接在⼀起才生成可执行程序。链接过程的命令如下 gcc test.o -o test 链接过程主要包括地址和空间分配符号决议和重定位等这些步骤。链接解决的是一个项目中多文件、多模块之间互相调用的问题。比如 在一个C的项目中有2个.c文件 test.c 和 add.c 代码如下
#include stdio.h
//test.c
//声明外部函数
extern int Add(int x, int y);
//声明外部的全局变量
extern int g_val;
int main()
{int a 10;int b 20;int sum Add(a, b);printf(%d\n, sum);return 0;
}
int g_val 2022;
int Add(int x, int y)
{return xy;
} 我们已经知道每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
test.c 经过编译器处理⽣成 test.oadd.c 经过编译器处理⽣成 add.o 我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。 我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地址但是由于每个⽂件是单独编译的在编译器编译 test.c 的时候并不知道 Add 函数和 g_val 变量的地址所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址然后将 test.c 中所有引⽤到Add 的指令重新修正让他们的⽬标地址为真正的 Add 函数的地址对于全局变量 g_val 也是类似的⽅法来修正地址。这个地址修正的过程也被叫做重定位