你好,我是史丰源
欢迎你的来访,希望我的博客能给你带来一些帮助。
我的Gitee: 代码仓库☀️
我的联系方式:
QQ:1756786195
邮箱:Marksky126@outlook.com🌐
程序环境和预处理
一. 程序的翻译环境和执行环境
在C语言中,有三个文件,源代码文件(.c),程序编译后的二进制文件(.obj),可执行文件(.exe)
翻译环境:源代码被转化成可执行的机器指令(二进制指令)。
执行环境:程序运行时的环境。
二.详解编译+链接
2.1 翻译环境
翻译环境就是源代码转换成二进制形式的过程。
下图可以清晰地指明他们之间的关系。
2.2 编译阶段
2.21 预编译(.i):
1.头文件的包含
⚡️举例子
#include<stdio.h>
#include"stdio.h"
在系统扫描头文件时,先从本地源文件所在目录进行扫描,若无法扫描到该头文件,则从库函数文件中扫描。如果再找不到,编译出错。
2.define 符号的替换
左侧是源代码文件,右侧是预编译文件,我们可以很清晰地看到,我们定义地MAX符号在预编译阶段被替换。
3.删除注释
左侧是源代码文件,右侧是预编译文件。
注释在预编译阶段被删除。
2.22 编译(.s):
- 语法分析:
语法分析就是根据高级语言的语法规则对程序的语法结构进行分析。 - 词法分析:将识别出的词转换成统一的机内表示。
- 语义分析:收集标识符的属性信息 和 对语义的检查(检查合法性)。
- 符号汇总:对全局变量符号和函数声明进行扫描提取,将扫描到的符号进行汇总,解决外部符号使用问题。
以上详情内容请参阅《编译原理》。
2.23 汇编(.o):
汇编实际上指把汇编语言代码翻译成目标机器指令并且形成符号表的过程。
对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:
🌻代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
🌻数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
2.3 运行环境
程序执行的过程:
🌻1. 程序必须载入内存中。在有操作系统的环境中,由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
🌻2. 程序运行开始。从main函数开始调用。
🌻3. 开始执行程序代码。此时程序使用一个运行堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值。
🌻4. 终止程序。正常终止or报错中止。
三.预处理详解
3.1 预定义符号
__FILE__//进行编译的源文件
__LINE__//文件当前的行号
__DATE__//文件被编译的日期
__TIME__//文件被编译的时间
__STDC__//若编译器遵循ANSI C,其值为1,否则未定义
3.2 #define
🌻1.#define 定义标识符
#define MAX 100
注意:不要在定义结束处加";"。
🌻2.#define 定义宏
#define name(paramen-list) stuff
paramen-list为一个由逗号隔开的符号表。
name 与paramen-list之间无空格。
🌻3.#define 替换规则
#define 替换只是单纯的替换,因此也会导致很多问题。
规则:
🌵1. 在调用宏时,首先对参数进行检查,查看是否包含由#define定义的符号,如果有,它们首先被替换。
🌵2. 替换文本随后被插入到程序原本文本的位置。
🌵3. 对结果文件再次扫描,查看是否还存在#define文件。
🌻4. #和##
使用#,可以把一个宏参数变成对应的字符串。
⚡️举例子:
#define PRINT(N,format) printf("the value of "#N" is "format"\n",N)
int main()
{
double pai = 3.14;
PRINT(pai,"%lf");
return 0;
}
使用##,可以将位于它两边的符号连接起来。
允许从分离的文本片段创建标识符。
⚡️举例子:
#define PRINT(name,num) name##num
int main()
{
int CC105 = 100;
printf("%d\n",PRINT(CC,105));
}
🌻5. 带有副作用的宏参数
⚡️举例子:
x+1;//不带有副作用,不改变x的值
x++;//带有副作用,改变x的值
🌻6. 宏和函数对比
宏其实主要是进行字符串的替换(只是进行字符串的替换,不涉及类型参数),
而函数是通过参数的传递,参数是有数据类型的。
编译器在预处理阶段就会进行宏的替换,
不会进行参数的检查,而函数调用是将值传递给形参(值传递、引用传递、指针传递),
在编译阶段之后,执行函数是会对参数进行检查的。
宏的参数不会占用内存空间,因为在预处理阶段时,宏替换已经完成。
而函数调用时的参数传递是作为函数的局部变量的,编译器会将函数的参数进行压栈,这样就会占用存储空间。
函数调用会花费一定的时空开销,系统在调用函数时,需要保留现场,然后转入被调用的函数中执行,执行完后,在返回到主调函数中继续执行。宏中不需要。
🌻7. 命名约定
宏定义一般全部使用大写字母。
函数名不用全部大写。
3.3 #undef
此指令用于移除一个宏定义。
💝写在最后:保持空杯心态,做有意义的事情。
希望我的博文能为你带来一些帮助。