【C语言进阶】预处理那些事儿(一)

简介: 【C语言进阶】预处理那些事儿(一)

ddd2cc12d2214a6c920ef26088033809.gif前言

 上一次分享了与程序有关的两种环境,分别是 翻译环境和 执行环境,在执行环境中又细分出了 预处理(预编译)、 编译、 汇编、 链接几个过程,今天就让我们来深入了解一下 预处理过程都干了些什么,话不多说,让我们开启今天的学习吧!

📖预定义符号

__FILE__----当前源文件所在的路径

__LINE__----文件当前的行号

__DATE__----文件被编译的日期

__TIME__----文件被编译的时间

__STDC__----如果编译器遵循ANSI C ,其值为1,否则未定义

__FUNCTION__----__FUNCTION__所在函数的函数名

int main()
{
  printf("%s\n", __FILE__);
  printf("%d\n", __LINE__);
  printf("%s\n", __DATE__);
  printf("%s\n", __TIME__);
  return 0;
}
//上面这段代码是在VS2019这个环境下运行的,__STDC__显示未定义
//说明VS2019不支持ANSI C标准

bebeef08802349a18143b789b46615f1.png

这些预处理符号在预处理阶段就会被具体的值替换,如下图所示:

1b9126b18cb9425b8ca1c2eaf17023e8.png


📖预处理指令

 我们常见的下面这些符号都被叫做预处理指令:

  • #define----定义宏和标识符常量
  • #include----头文件的包含
  • #pragma
  • 对这些预处理指令都是在预处理阶段执行的。

📖#define

🔖#define定义标识符

 语法:

#define name stuff
//表示代码中所有name的地方都用stuff来进行替换

实例:

#define MAX 1000//把代码中所有的MAX用1000来替换
#define reg register//为register这个关键字,创建一个简短的名称
#define do_forever for(;;)//用更形象的符号来替换一种实现
#define CASE break;case//在写case语句的时候自动把 break写上。
//如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续航符)

#define定义标识符的时候,要不要在最后加;?

比如:

#define MAX 1000
#define MAX 1000;

建议不要加上;,这样容易导致问题,比如下面的场景:

  • 情景一:(加上;没有影响)
#define MAX 100;
int main()
{
    int min = MAX;
    return 0;
}

bdc5fb9706ef424f81f7dc4ae03c5ed2.png

上面的代码在预处理阶段,用100;去替换程序中的MAX,这就导致在text.i文件中100的后面有两个分号,其中一个时我们在源代码(text.c)中写的,另一个是在对MAX替换后得到的。这两个分号并不会对程序造成什么影响,第二个分号会被当成一条空语句去执行。

情景二:(加上;导致异常)

#define MAX 100;
int main()
{
    int i = 1;
    int n = 0;
    if(i > 0)
        n = MAX;
    else
        n = 0;
    return 0;
}

9334c70776624048a566d05847004691.png

可以看出,此时程序报错了。为什么呢?因为根据语法规定:if语句后面如果没有大括号的话只能有一条语句,但是从预编译的得到的text.i文件中可以看出if语句后面跟了两条语句,分别是赋值语句n = 100;和空语句;,这显然不符合语法规定,报错也是理所当然。当然针对上面的错误也有以下几种修改手段:

  1. 在#define的时候,后面不加;,这是一本万利的方法
  2. 在写源代码的时候MAX后面不写;,这种方法虽然可行,但是不符合我们的日常使用习惯,一条语句结束没有;给我们的第一感觉就是代码写的有问题
  3. 在写源代码的时候对if else子句加上大括号,当然这种方法也仅仅是针对当前的情况有效,如果是其他情况还是需要另寻它路。

 通过分析我们得出结论:在用#define定义标识符的时候不要加;。

🔖#define定义宏

 #define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro) 或定义宏(#define macro)。

 语法:

#define name(parament-list) stuff

name是宏的名字

parament-list是一个用逗号隔开的符号表,它们可能会出现在stuff中(类似于参数,没有类型)

stuff会用parament-list来实现一定的功能

 注意: 参数列表必须的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

 实例:

#define SQUARE(x) x*x
int main()
{
  printf("%d\n", SQUARE(2));
  printf("%f\n", SQUARE(5.0));
  return 0;
}

工作原理:

d0be56b6b7b44152bcb76141d56e3604.png


 可以看出在预处理阶段对源程序中的SQUARE(2)和SQUARE(5.0)进行了替换。以SQUARE(2)为例在预处理阶段宏的参数x就是2,然后用x*x就变成了2*2再用2*2去替换SQUARE(2)最终就得到了text.i文件中的结果。

 存在的陷阱一:

#define SQUARE(x) x*x
int main()
{
  printf("%d\n", SQUARE(5 + 1));
  return 0;
}

174b68884d3649a7be146d09cf5783d0.png

 按照一般思维去看上面的代码,首先5+1=6我们会以为是把6传给x,然后用6*6来替换SQUARE(5 + 1)最终得到36,可结果并不像我们想的那样,正确结果是11,为什么会这样呢?去看看预处理后得到的文件我们就会恍然大悟

e27aac91a297409e8cf363139e16615d.png

可以看出真实的替换结果并不是我们想的用6*6去替换SQUARE(5 + 1),而是用5+1*5+1去替换的。这说明在预处理的时候并没有执行5+1,而是直接把5+1传了过去,最终得到的结果就是11。

 总结: 宏的参数是不加运算直接进行替换的

 如何得到我们想要的36呢?有以下两种方法供大家参考:

对宏调用进行修改:SQUARE((5 + 1)),给5+1再加一层括号,此时在替换的时候,x就是(5+1),SQUARE((5 + 1))就会被替换成(5+1)*(5+1)最终得到的结果就是36。

对定义的宏进行修改:#define SQUARE(x) (x)*(x),此时在替换的时候,x是5+1,SQUARE(5+1)会被替换成(5+1)*(5+1)最终得到的结果也是36。

 对比上面的两种方案,第一种方案带两个括号看起来比较别扭,所以更推荐第二种方案,也就是在定义宏的时候给参数带上括号。

31b65bc9b3424f89ae316955ea07a794.png

存在的陷阱二:

//我们希望计算一个数的二倍再乘10
#define DOUBLE(x) (x)+(x)//这里定义了一个宏来计算一个数的二倍
int main()
{
  printf("%d\n", 10 * DOUBLE(3));
  return 0;
}

806f08462ade4910908cd65cead1bffd.png

 根据需求描述,我们希望得到的应该是60,但实际却得到的是33,本质原因就是宏只会进行替换,先进行参数的替换,再进行宏体的替换。让我们来看看上面的代码经过预处理会得到什么

7920ea352c8f4ff4bad1bbf0df78fb84.png

 不难看出,为了得到我们希望的结果,应该对宏替换后得到的内容加上括号,也就是在定义宏的时候对处理结果加上括号:#define DOUBLE(x) ((x)+(x))

1803a88cb0e64dd19f088e81658a4a97.png

 通过上面介绍的两种陷阱,我们可以得出一个结论:在宏定义的时候,千万不要吝啬括号!!,先给每个参数带上括号,再给stuff整体带上括号。

 对于#define定义宏的注意事项总结如下:

  • 参数列表必须的左括号必须与宏的名字name紧邻
  • 宏的参数都是不加计算直接替换的
  • 不要吝啬括号

目录
相关文章
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
148 4
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
201 1
|
自然语言处理 编译器 Linux
【C语言篇】编译和链接以及预处理介绍(上篇)1
【C语言篇】编译和链接以及预处理介绍(上篇)
131 1
指针进阶(C语言终)
指针进阶(C语言终)
|
编译器 Linux C语言
【C语言篇】编译和链接以及预处理介绍(下篇)
【C语言篇】编译和链接以及预处理介绍(下篇)
148 1
【C语言篇】编译和链接以及预处理介绍(下篇)
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
257 2
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
118 2
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
存储 C语言
【C语言篇】编译和链接以及预处理介绍(上篇)2
【C语言篇】编译和链接以及预处理介绍(上篇)
119 0
|
存储 自然语言处理 程序员
【C语言】文件的编译链接和预处理
【C语言】文件的编译链接和预处理