欢迎各位看官!如果您觉得这篇文章对您有帮助的话
欢迎您分享给更多人哦 感谢大家的点赞收藏评论
感谢各位看官的支持!!!
一:翻译环境和运行环境
在ANSIIC的任何一种实现中,存在两个不同的环境 1,翻译环境:源代码 被转换成 可执行的机器指令(二进制指令) (电脑只能读懂这个) 2,执行环境:实际执行代码 其实翻译环境是由编译和链接两个⼤的过程组成的,⽽编译⼜可以分解成:预处理(有些书也叫预编译)、编译、汇编三个过程
二.预处理(预编译),编译和汇编
2.1:预处理(预编译)
在预处理阶段。**源文件和头文件**都会被处理成后缀为(.i)的文件```c命令:gcc -E test.c -o test.i **(-E到预处理结束,-o,生成test.i文件)**
> 1.预处理阶段主要处理那些源⽂件中#开始的预编译指令。⽐如:#include,#define,处理的规则如下: > • 将所有的 #define 删除,并展开所有的宏定义。**(将#define定义的内容展开) > • 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。> • 处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头⽂件也可能包含其他⽂件。**> • 删除所有的注释(变成空格) • 添加行号和文件名标识,方便后续编译器生成调试信息等。> • 或保留所有的#pragma的编译器指令,编译器后续会使用。
经过预处理后的 .i 文件中不再包含宏定义,因为宏已经被展开。并且包含的头文件呢都被插入到 .i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的 .i 文件来确认。
2.2编译
词法分析,语法分析,语义分析。将C语言代码转换成汇编代码
gcc -S test.i -o test.s //生成test.s例如: arr[index]=(index+4)*(2+6)
2.3 汇编
汇编器是将汇编代码转转变成机器可执行的指令(二进制),每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进⾏翻译,也不做指令优化。汇编的命令如下:
gcc -c test.s -o test.o对test.c处理成test.o(二进制文件)
链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执行程序。
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调用的问题
三.运行环境
程序必须载入内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独立的环境中(单片机,里面无操作系统),程序的载入必须手工安排,也可能是通过可执行代码置入只读内存来完成。3. 程序的执行便开始。接着便调用main函数。
4. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(函数栈帧)(stack),存储函数的局部变量和返回地址 (程序是由一个个函数组成的,每创建一个函数就会创建一个运行时堆栈,运行时维护,结束时销毁)程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
5. 终止程序。正常终止main函数;也有可能是意外终止。
四.预处理详解
1.1define 定义常量
__FILE__ //进⾏编译的源⽂件__LINE__ //⽂件当前的⾏号__DATE__ //⽂件被编译的⽇期__TIME__ //⽂件被编译的时间__STDC__ //如果编译器遵循ANSI C(C语言标准),其值为1,否则未定义//vs并未完全遵守
C语言设置的预定义符号,可以直接使用,预定义符号也是在预处理期间处理的
并且预处理后代码中**预定义符号**(你猜为什么叫预定义符号)就被替换了printf("%s\n",C:\code\c-language-411\test_9_5\test_9_5\test.c)以下类推63Sep 5 202419:58:36
#include <stdio.h>#define M 100#define STR "hehe"#define reg registerint main(){int arr[M] = { 0 }; int a = M;printf("%d\n", M); 100 大家都是预处理的时候就已经换到printf("%d\n",100)这种了printf(STR); hehereg int b= 10;//预处理后是register int b=10return 0;}
1.2:关于define的一些规则
#define不要加;
#define M 100;if(a)max=M;//这里的;让if else语句分开了本来是if(a)else max=Mmax=0; else max=0;
但是有一种用法
#define CASE breakcaseint main(){int a = 5;switch (a){case 1:CASE 2 :CASE 3 :CASE 4 :CASE 5 :CASE 6 :break;}return 0;}
3.1define 定义宏 (宏里面的参数是整体替换的,并不会算出一个结果)
#define 机制包括了⼀个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏
(define macro)。
#define name(参数)替换的内容 (空格隔开)#define ADD(n) ((n)*(n)) 这里的括号尽量不要省,不然容易错误
#define SQUARE(n) n+nint main(){int ret = 10 * SQUARE(5); **结果是55不是100,变成了10*5+5**printf("%d", ret);return 0;
3.2 带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可
能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。
#define MAX(a,b) (a)>(b)?(a):(b)int main(){int a = 10; int b = 20; int ret = MAX(a++, b++); printf("ret=%d,b=%d", ret, b);21,22return 0;}
4# 和##
4.1#(只能出现在宏体)
4.2##(记号粘合)
5.1#undef
#undef NAME (取消定义)
5.2 条件编译
调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。
6.头文件包含
(1):本地文件包含(一般这指自己创建的头文件的包含) #include “test.h”
> 查找策略:先在源文件所在目录下查找,如果该头⽂件未找到,编译器就像查找库函数头文件⼀样在标准位置查找头文件。> (先在自己这个文件目录下找,找不到去库函数里面找)> 如果找不到就提示编译错误。 > Linux环境的标准头⽂件的路径: /usr/include > VS环境的标准头文件的路径: > C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include > //这是VS2013的默认路径 注意按照⾃⼰的安装路径去找
(2):库文件包含 #include <stdio.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
7.如何避免重复包含同一个头文件:
每次编译都要删除#include,然后用包含头文件的内容替换,头文件包含10次,替换10次,test.h文件的内容被包含在test(如果test.h文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件、被都能使用,又不做任何的处理,**那么后果真的不堪设想**。
如何解决头文件被重复引入的问题?
答案:条件编译。
每个头文件的开头写:
方法一:这种方法通过检查一个特定的宏是否已经被定义来决定是否包含头文件的内容.如果宏已经定义(意味着头文件已经被包含过一次),则跳过头文件的内容。
#ifndef __TEST_H__#define __TEST_H__#endif //__TEST_H__这种方法通过检查一个特定的宏是否已经被定义来决定是否包含头文件的内容.如果宏已经定义(意味着头文件已经被包含过一次),则跳过头文件的内容。
方法二:
#pragma once提供了另一种更简洁的方法来实现同样的功能。使用#pragma once,编译器在遇到这个指令时会确保当前头文件在同一个编译单元中只被包含一次,无论它是否被多次显式包含。
#pragma once
与宏定义保护相比,#pragma once的优点是:
更简洁,不需要定义和检查宏。减少了命名冲突的风险,因为不需要为每个头文件创建一个唯一的宏名。在某些情况下,可以稍微提高编译速度,因为编译器可能能够更有效地优化包含关系但是:需要注意的是,#pragma
once是非标准的,这意味着它的行为可能不是所有编译器都一致。但幸运的是,几乎所有现代主流编译器都支持这个指令,并且其行为也基本一致。因此,在大多数情况下,#pragma once是一个安全且方便的选择。
上述就是编译(包括预处理详解)和链接的全部内容了
能看到这里相信您一定对小编的文章有了一定的认可,有什么问题欢迎各位大佬指出
欢迎各位大佬评论区留言修正
您的支持就是我最大的动力