一.前言
程序运行需要经过编译链接的过程如图下,各部分概述上篇已经介绍完毕,而在编译器编译代码之前需要由预处理器对程序源代码进行处理,本文主要介绍的预处理详细过程,希望大家有所收获🌹🌹
以linux环境下的gcc编译器为例:
二.预处理
(1) 概述
- .c 文件>> .i文件
- 主要处理源文件中
#
开始的预编译指令,e.g. #include, #define,规则如下
① 将所有的#define
删除,并展开所有宏定义
② 处理所有的条件编译指令,e.g. #if, #ifdef, #elif, #endif
③ 处理#include
预编译指令,将包含的头文件内容插入到该预编译指令的位置,这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件
④ 删除所有的注释
⑤ 添加行号和文件名标识,方便后续编译器生成调试信息等
⑥ 保留所有的#pragma
的编译器指令,编译器后续会使用 - 当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的
.i
文件来确认 - 命令为:
gcc -E test.c -o test.i
,其中, -E:让完成预处理停下,-o用于调试:自定义输出文件名为test.i
(2) 预处理符号
- C语言设置了一些预定义符号,可以直接使用,此符号也是在预处理期间处理的
__FILE__
:进行编译的源文件,__LINE__
:文件当前的行号,__DATE__
:文件被编译的日期,__TIME__
:文件被编译的时间,__STDC__
:如果编译器遵循ANSI C(由美国国家标准协会ANSI及标准化组织ISO推出的关于C语言的标准),其值为1,否则未定义,示例如下:
c
printf("file:%s line:%d\n", __FILE__,__LINE__);
(3) #define定义常量
- 基本语法:
#define name stuff
- 若想定义多行,可在每行末尾加上续行符
\
后直接回车开启下一行,可理解为转义回车,让它不再是回车从而连上,示例如下:
c
#include <stdio.h>
#define PRINT_INT(x)\
do{\
printf("%d\n", x);\
}while(0)
int main()
{
int num = 10;
PRINT_INT(num);//10
return 0;
}
(4)#define定义宏
- #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏
- 声明方式:
#define name(parament-list) stuff
,其中,parament-list为参数列表,是一个逗号隔开的符号表,他们可能出现在stuff(内容)中。
<注意>:参数列表的左括号必须和name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分 - 用于数值表达式进行求值的宏定义不要吝啬括号,避免在使用宏时由于参数中操作符或邻近操作符之间不可预料的相互作用
(5)带有副作用的宏参数
- 当宏参数在宏定义中出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果,副作用就是表达式求值的时候出现的永久性后果,示例如下:
c
#define MAX(x,Y) ((x)>(y)?(x):(y))//求出两个数之间的较大值
······
int m = MAX(a++,b++);//此时a++,b++为带有副作用的宏参数,最后打印出来的值是变的
- 宏比函数在程序的规模和速度方面更胜一筹
- 宏的参数是类型无关的
(6)和函数相比宏的劣势
① 每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
② 宏是没法调试的
③ 宏由于类型无关,也就不够严谨
④ 宏可能会带来运算符优先级的问题,导致程序容易出现错误
- 宏的参数可以出现类型,函数不可以,示例如下:
c
#define MALLOC(n, type) (type*)malloc(n*sizeof(type))
int main()
{
int* p = MALLOC(10, int);
//int* p = (int*)malloc(10 * sizeof(int));
return 0;
}
- 实现的逻辑比较简单的情况下使用宏
- 平时习惯是:宏名全部大写,函数名不要全部大写
(7)#和##
① #运算符 :将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏替换列表中,所执行的操作可以理解为"字符串化",示例如下:当我们有一个变量int a = 10;
的时候,我们想打印出the value of a is 10
,就可以写成
② ##运算符:可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,##被称为记号粘合,这样的链接必须产生一个合法的标识符,否则结果就是未定义的。示例如下:
(8)#undef
- 用于移除一个宏定义,如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
- 基本语法:
#undef NAME
(9)条件编译
一般情况下,C语言源程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译,条件编译由预处理程序提供。
C语言中,条件编译主要有如下几种格式:
if格式
c
#if 表达式
语句序列①
[#else
语句序列②]
#endif
功能:当表达式的值为真时,编译语句序列①,否则编译语句序列②。
其中,#else和语句序列②可有可无。[1]
ifdef格式
c
#ifdef 标识符
语句序列①
[#else
语句序列②]
#endif
功能:当标识符已被定义时(用#define定义),编译语句序列①,否则编译语句序列②。
其中#else和语句序列②可有可无。[1]
ifndef格式
c
#ifndef 标识符
语句序列①
[#else
语句序列②]
#endif
功能:该格式功能与ifdef相反。
- 判断是否被定义(在头文件中非常普遍)
c
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
(10)头文件的包含
- 头文件的包含有两种形式:
①#include <xxx.h>
:库文件包含,一般指标准库中头文件的包含
②#include "xxx.h"
:本地文件包含,一般指自己创建的头文件的包含 - ②的查找策略:现在源文件所在目录下查找,如果该文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
--- Linux环境的标准头文件路径:/usr/include
--- VS2013的默认标准头文件路径:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
(11)解决头文件被重复多次的包含问题(两种方法)
法(一)
c
#ifdef xxxx
#define xxxx
//头文件的内容
#endif
法(二)
c
#pragma once
//头文件的内容
三.总结
C语言预处理讲解告一段落,创作不易,希望大家多多支持,有什么想法欢迎讨论🌹🌹