程序环境和预处理
大家好,我是晓星航。今天为大家带来的是文件操作相关知识点的讲解!😀
1. 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境:
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
2. 详解编译+链接
2.1 翻译环境
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2 编译本身也分为几个阶段:
1.预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i**(1.头文件的包含 2.注释删除)**文件中。
2.编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s**(把c语言代码转换成汇编代码)**中。
3.汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o**(把汇编代码转化成二进制指令 形成符号表)**中。
2.3 运行环境
程序执行的过程:
1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2.程序的执行便开始。接着便调用main函数。
3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序。正常终止main函数;也有可能是意外终止。
3. 预处理详解
3.1 预定义符号
3.2 #define
3.2.1 #define 定义标识符
即reg和register是相同含义
3.2.2 #define 定义宏
如:
#define SQUARE( x ) x * x
这个宏接收一个参数 x .
如果在上述声明之后,你把
SQUARE( 5 );
置于程序中,预处理器就会用下面这个表达式替换上面的表达式:
5 * 5
警告:
这个宏存在一个问题:
观察下面的代码段:
这个问题,的解决办法是在宏定义表达式两边加上一对括号就可以了。
#define DOUBLE( x) ( ( x ) + ( x ) )
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
3.2.3 #define 替换规则
3.2.4 #和##
#的作用:把一个宏参数变成对应的字符串。
如何把参数插入到字符串中?
我们发现字符串是有自动连接的特点的。
## 的作用:##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
此时的CAT即为一个宏 它的作用是连接两个字符串(通过##实现)
3.2.5 带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。
eg:
原本应该a为3,b为5,但是因为宏的副作用导致a原本值变为5,b原本值变为7。
3.2.6 宏和函数对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
为什么不用函数来完成这个任务呢?
宏的缺点:当然和函数相比宏也有劣势的地方:
1.每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2.宏是没法调试的。
3.宏由于类型无关,也就不够严谨。
4.宏可能会带来运算符优先级的问题,导致程容易出现错。
宏和函数对比表:
3.2.7 命名约定(尽量按要求来 不一定是百分百要按他来)
把宏名全部大写
函数名不要全部大写
3.3 #undef
这条指令用于移除一个宏定义。
用法:#undef空格+要移除的宏名字
3.4 命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ADom0eU-1664363904281)(https://7078909xxh.oss-cn-shanghai.aliyuncs.com/csdn%E5%9B%BE%E5%BA%8A%E4%B8%93%E7%94%A8%E5%8C%BA/202209262023435.png)]
即在函数中不定义在编译函数时用编译指令来定义它。
3.5 条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
常见的条件编译指令:
3.6 文件包含
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
3.6.1 头文件被包含的方式:
本地文件包含(双引号):
#include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。(查两次)
库函数包含(花括号):
#include <stdio.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。(查一次)
Linux环境的标准头文件的路径:
/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
注意按照自己的安装路径去找。
库文件包含
#include
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用 “” 的形式包含?
答案是肯定的,可以。
但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了**。**
3.6.2 嵌套文件包含
如果出现这样的场景:
笔试题:
1.头文件中的 ifndef/define/endif是干什么用的?
答:查找头文件的包含有没有重复的
2.#include 和 #include "filename.h"有什么区别?
答:<>是先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。(查两次) 而""是查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。(查一次)
感谢各位读者的阅读,本文章有任何错误都可以在评论区发表你们的意见,我会对文章进行改正的。如果本文章对你有帮助请动一动你们敏捷的小手三连一波,你的每一次鼓励都是作者创作的动力哦!😘