预处理详解

一:预处理:


一:预定义符号:

C语⾔设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的

1__FILE__
//进⾏编译的源⽂件
2__LINE__
//⽂件当前的⾏号
3__DATE__
//⽂件被编译的⽇期
4__TIME__
//⽂件被编译的时间
5__STDC__
//如果编译器遵循ANSI C,其值为1,否则未定义 

举例:


 

二:#define定义常量 

举例:#define MAX 1000 

思考:语句后面要不要加分号?

#define MAX 1000;
#define MAX 1000

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

if(condition)
max = MAX;
else
max = 0;

如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀条语句。这⾥会出现语法错误。

 三:#define定义宏

#define机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏(macro)或定义宏
申明方式:#define name( parament-list ) stuff

其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。

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

举例

#define SQUARE( x ) x * x 

这个宏接收⼀个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 写于程序中,预处理器就会⽤下⾯这个表达式替换上⾯的表达式:5*5

但是!!!宏存在一个问题!观察下面代码

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

你觉得它可能打印36,但是实际上是11,因为宏只是做简单的文本替换,预处理后,这句话实际变为printf ("%d\n",a + 1 * a + 1 );

所以在使用宏时我们要小心运算符的优先级,为了避免这样的问题,我们在定义时候可以加上括号

#define SQUARE(x) ((x) * (x))

这样预处理后就达到了预期效果

printf ("%d\n",((a + 1) * (a + 1)) ); 


 

四:带有副作⽤的宏参数 

当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。副作⽤就是表达式求值的时候出现的永久性效果。

例如:

x+1;//不带副作⽤
x++;//带有副作⽤ 

举例:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z); 

预处理后的结果z = ( (x++) > (y++) ?(x++):(y++)); 

在这里面x++和y++因为宏出现了两次,所以输出的结果x=6,y=10,z等于9,

这并不是我们想要的值


 五:宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。


1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索


六:宏与函数的对比 

宏通常被应⽤于执⾏简单的运算。⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。

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

那为什么不⽤函数来完成这个任务?

原因有⼆:
1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐
函数在程序的规模和速度⽅⾯更胜⼀筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏的参数是类型⽆关的。 

和函数相⽐宏的劣势:

1每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
2. 宏是没法调试的。
3. 宏由于类型⽆关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

其中宏不能递归,不方便调试,函数可以递归,可以调试 


 

七:#和## 运算符

1:#运算符:

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。#运算符所执⾏的操作可以理解为”字符串化“。当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 .就可以写:

#define PRINT(n) printf("the value of "#n " is %d", n);

PRINT(a);//当我们把a替换到宏的体内时,就出现了#a,⽽#a就是转换为a,时⼀个字符串代码就会被预处理为:

printf("the value of ""a" " is %d", a); 

打印结果:the value of a is 10

2:##运算符 

在编程中,## 运算符通常被称为连接运算符,它是一种用于字符串连接的特殊运算符,特别是在C语言的预处理器中。这个运算符用于在编译时将两个标识符连接起来,形成一个新的标识符。

连接运算符的语法是两个 ## 符号,它们之间放置两个标识符:

identifier1 ## identifier2

连接运算符在预处理器中使用,它会在编译时替换代码中的 ## 符号,并将两个标识符连接起来。这通常用于生成文件名、路径名或者在宏定义中创建新的标识符。

例如,如果你有一个宏 MY_TYPE 和一个成员函数 SOME_FUNCTION,你可以使用 ## 运算符来创建一个完整的成员函数标识符:

#define MEMBER_FUNCTION(type, function) type##_##function

然后,你可以这样使用这个宏:

MEMBER_FUNCTION(MY_TYPE, SOME_FUNCTION);

这将生成一个名为 MY_TYPE_SOME_FUNCTION 的标识符,它被用作成员函数的名称。

连接运算符在预处理器中非常有用,因为它允许你创建与代码结构紧密相关的标识符,而不需要手动编写它们。然而,它也可能导致代码难以阅读和维护,因此在使用时应该谨慎

 八:#undef

这条指令⽤于移除⼀个宏定义。

#undef NAME//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除

九:条件编译

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。

调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译。

例如:

 

十: 常见条件编译指令

#if 常量表达式
//...
#endif//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif

 多个分⽀的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif

判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol 

嵌套指令
#if defined(OS_UNIX)
          #ifdef OPTION1
                  unix_version_option1();

                              #endif
           #ifdef OPTION2
                    unix_version_option2();
                             #endif
#elif defined(OS_MSDOS)
            #ifdef OPTION2
                   msdos_version_option2();
                               #endif
#endif 

十一:头文件的包含: 

1:本地⽂件包含

查找策略:先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。如果找不到就提⽰编译错误。

#include "filename"

例如Linux环境的标准头⽂件的路径:/usr/include 

2:库⽂件包含

查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。

#include <filename.h>


这样是不是可以说,对于库⽂件也可以使⽤ “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件了。 

十二:嵌套文件包含

我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的
地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。

在一个项目工程中,可能会包含很多头文件,然后头文件中又包含很大头文件,形成嵌套,但很大头文件是已经出现在工程中了,重复引入,会造成重复的编译

例如:

#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}

如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。
如果test.h⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。
如何解决头⽂件被重复引⼊的问题?答案:条件编译。 

对每个头文件这样写可以避免重复引入的问题:

#ifndef __TEST_H__
#define __TEST_H__

//头⽂件的内容
#endif 

解释:在第一次引入test.h文件时会把上面的代码都写入,第二次引入test.h文件时由于__TEST_H__已经被定义,所以接下来代码被忽略;

或者:每个文件开头这样写

#pragma once//文件只被编译一次

预处理整个过程:

将所有的 #define 删除,并展开所有的宏定义。
• 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
• 处理#include预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进
⾏的,也就是说被包含的头⽂件也可能包含其他⽂件。
• 删除所有的注释
• 添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等。
• 或保留所有的#pragma的编译器指令,编译器后续会使⽤

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值