首页 > 其他分享 >浅析C语言预处理

浅析C语言预处理

时间:2022-12-17 22:33:22浏览次数:35  
标签:__ 文件 符号 int 浅析 编译 C语言 预处理 define

计算机操作系统属于计算机基础,了解C语言从源文件到可执行文件的被处理过程,有助于认识操作系统,帮助我们理解C语言的某些程序现象。


第一部分


程序的翻译环境和执行环境

翻译环境

源文件被处理成可执行文件(源代码到机器指令)所依赖的环境。翻译环境又分为编译和链接

执行环境

可执行文件执行所依赖的环境

浅析C语言预处理_文件包含


翻译过程

翻译是.c文件到.exe文件的过程。翻译包括编译链接两个步骤。在编译阶段,每个源文件都会被编译器单独处理,分别生成各自的目标文件;在链接阶段,各个目标文件和链接库链接器的处理下生成一个可执行文件

浅析C语言预处理_预编译_02

编译

编译阶段又分为预编译编译汇编三个阶段,下面逐一对其解释。

预编译

预编译将.c文件转化为.i文件。

在Linux中用gcc编译器对源文件进行预编译

浅析C语言预处理_预编译_03

预编译进行的是文本操作,主要包括三个部分:

1.头文件的包含,即将#include包含的头文件的内容复制到文件中;

2.注释的删除,将源文件和头文件中的注释用空格替换

3.#define定义的符号的替换


编译

编译.i文件转化为.s文件,即将c代码转化为汇编代码

该步骤包括语法分析、词法分析、语义分析符号汇总(全局变量、函数名等)。该部分原理较为复杂,详细请参考书籍:《程序员的自我修养》

.i文件进行编译后:

浅析C语言预处理_宏和函数_04

汇编

预编译将.s文件转化为.o文件。包括两个动作:

1.将汇编代码转化为二进制指令

2.形成符号表,其中不同的.c文件分别各自形成不同的符号表。

符号表:将符号及其对应的符号信息制表得到。

将汇编转化为二进制指令,打开文件呈现出的是一堆乱码

浅析C语言预处理_文件包含_05

链接

数个.o文件在链接器的作用下,生成一个.exe文件。包括两个动作:

1.合并段表.o文件本身会将自己分段,当各个.o文件链接在一起时,各个段表会合并

2.符号表的合并和重定位:此举是为了找到函数位置;当符号名冲突时,用符号的有效地址

链接并运行程序:

浅析C语言预处理_预处理指令_06


.exe文件的执行

程序的运行必须载入到内存中。在有操作系统的环境下,一般由操作系统来完成;在独立环境下,程序的载入必须手动完成

程序运行时,首先main函数被调用,接着开始执行程序代码。此时程序将使用一个运行时栈帧,存储函数的局部变量和返回地址。程序也可以使用静态内存,使用静态内存的变量在程序运行的过程中一直保留他们的值

最后程序会终止。这时可能是程序运行完毕正常终止,也可能是意外终止


第二部分


预编译(预处理)详解


预定义符号

预定义符号是ANSIC规定的宏定义符号,这些符号在预处理阶段会被替换成实际的数据。常见的预定义符号有6个。

__FILE__ //代码所在的文件名
__LINE__ //代码所在的行号
__DATE__ //运行时刻的日期
__TIME__ //运行时刻的时间
__FUNCTION__ //代码所在的函数
__STDC__ //检测是否严格遵循C标准

这些预定义符号常见于运行日志的生成。需要注意的是,预定义是预先存在的符号,自己​​#define​​定义的符号不属于预定义符号。

int main()
{
FILE* pf = fopen("test.log", "w");
if (pf == NULL)
{
perror("OpenFile:");
exit(-1);
}

for (int i = 0; i < 10; i++)
{
printf("%d ", i + 1);
fprintf(pf, "Date:%s Time:%s File:%s Function:%s Time:%d\n",
__DATE__, __TIME__, __FILE__, __FUNCTION__, __LINE__);
}

fclose(pf);
pf = NULL;
return 0;
}

运行后的文件内容:

浅析C语言预处理_宏和函数_07


预处理指令


一.#define

1.#define定义标识符

可以用#define赋予符号特定的意义,这些符号在预编译阶段会被替换成代码被编译

2.#define定义宏

​#define name(parament-list) stuff​

其中parament-list是一个用逗号隔开的参数表,约定将宏名大写

需要注意以下几点:

  1. 参数表的左括号必须与宏名紧邻,否则会被视为​​stuff​​的内容
  2. 宏完成的是替换,不是传参。为了避免不可预料的相互作用,尽量在定义宏时给每个独立部分加上括号
  3. 宏不能出现递归
  4. 当预处理器搜索​​#define​​定义的符号时,字符串常量的内容不被搜索

#define MAX(X, y) ((x) > (y) ? (x) : (y)) //定义一个宏

使用自定义符号和宏的步骤如下:调用宏时,首先对参数进行检查,看是否包含​​#define​​定义的符号,若有,则它们首先被替换;替换文本随后被插入到原来文本的位置,对于宏,宏名和参数被替换为指定的运算;最后再对文件进行扫描,直至文本中​​#define​​定义的符号被完全替换

在宏的实际使用过程中,有时会由于操作不慎而产生带有副作用的宏参数,造成不可预料的结果。

输出结果是什么?

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = 8;
int z = MAX(x++, y++); //带有副作用的宏参数
printf( "x = %d, y = %d, z = %d\n", x, y, z);

这段程序的输出为:​​x = 6, y = 10, z = 9​​产生这个问题的本质原因是宏完成的是直接替换而不是传参,参与编译的实际上是如下代码:

z = ((x++) > (y++) ? (x++) : (y++))

看到这里相信你已经发现了问题所在,程序并没有如你所想般把 5 和 8 进行比较,而是被动了些手脚:x 和 y 进行比较后各自增1,然后 y 再次自增1,造成了现在的结果。


宏和函数的对比

既然宏和函数都能帮我们实现一些功能,那某者的存在是否是多余的呢?事实上两者各有优劣。

1.当宏和函数都能实现一个功能时,尽量使用宏。

宏的使用更加灵活:宏的参数是直接进行替换的,不进行类型检查,因此可以灵活对各种类型的数据进行操作;

宏的使用节省资源:宏不需要进行压栈等操作,没有函数函数调用和返回的开销。

#define MAX(x, y) ((x) > (y) ? (x) : (y))

int main()
{
double max1 = MAX(3,14, 3);
double max2 = MAX(5, 8); //宏的使用较灵活
printf("max1 = %f max2 = %f\n", max1, max2);
return 0;
}

2.宏的劣势

1.每使用一次宏,替换后程序中都会插入一行代码,有时会大大增加代码量

2.宏不能进行调试。进行调试时,已经完成了预编译,此时宏已经被替换;

3.宏可能带来运算符优先级的问题

4.宏是类型无关的,不够严谨


二.#和##

在宏中,​​#​​ 的使用可以实现将参数插入到字符串中

#define PRINT(X) printf("the value of " #X " is %d\n",X)
//在宏中 # 可以实现将参数插入到字符串中

int main()
{
int a = 10;
int b = 20;
PRINT(a);
PRINT(b);
return 0;
}

输出浅析C语言预处理_预编译_08


在宏中,​​##​​ 可以把位于其两边的符号合成一个符号,它允许从分离的文本片段创建已存在的标识符

#define CONS(a, b) (int)(a##e##b)

int main()
{
int x = CONS(1, 3);
printf("%d\n", x);
return 0;
}

这段程序的 ​​x​​ 在编译阶段实际上是下面的代码:

​int x = (int)(1e3);​

输出:浅析C语言预处理_文件包含_09


三.#undef

​#undef​​用于移除一个宏定义

#define MAX 100

int main()
{
printf("%d\n", MAX);//输出 100
#undef MAX//移除MAX的定义
int MAX = 10;
printf("%d\n", MAX);//输出 10
return 0;
}


四.条件编译语句

使用条件编译语句可以选择性地对某些语句进行编译或删除。条件编译语句支持单分支、多分支和嵌套使用

//单分支:
#ifdef 常量表达式
语句; //若常量表达式被定义则执行此语句,否则不执行
#endif

#ifndef 常量表达式
语句; //若常量表达式未被定义,则执行此语句,否则不执行
#denif

//多分支:
#if 常量表达式1
语句1; //若常量表达式1被定义则执行该语句
#elif 常量表达式2
语句2; //若常量表达式2被定义则执行该语句
#else 常量表达式3
语句3; //若常量表达式3被定义则执行该语句
#endif

条件编译语句常用于防止头文件被重复包含:

#ifndef DE_BUG
#define DE_BUG
#endif

在头文件开头使用​​#pragma once​​也可以达到同样效果


五.文件包含

当程序需要使用其他文件中的函数或功能时,需要包含该文件。被包含的文件在预编译阶段会被拷贝到程序中,并在其中查找对应的函数定义。​​#include​使另外一个文件中的程序被编译。

在C语言中文件包含有两种方式:​​#include"filename"​​​和​​#include<filename>​​,二者主要的不同在于查找方式

本地文件包含:​​#include"filename"​​。查找策略:先在工程目录下查找,找不到则在标准库中查找,再找不到则报错;

库文件包含:​​#include<filename>​​。查找策略:直接在标准库中查找

理论上库文件也可以被​​" "​​包含,但是考虑到效率问题,尽量选择合适的包含方式。有时会不可避免地重复引用同一个头文件,造成代码冗余,使用条件编译语句可以避免此问题。

标签:__,文件,符号,int,浅析,编译,C语言,预处理,define
From: https://blog.51cto.com/u_15752114/5950172

相关文章