C语言预处理详解

前言

上篇博客我们总结了编译与链接,有说过编译里第一步是预处理,那本篇博客将对预处理进行进一步的详细的总结

个人主页:小张同学zkf

若有问题 评论区见

感兴趣就关注一下吧

 

目录

1. 预定义符号

2. #define 定义常量

3. #define定义宏

4. 带有副作用的宏参数

5. 宏替换的规则

6. 宏和函数的对比

 7. #和##

7.1 #运算符

7.2 ## 运算符

8. 命名约定

9. #undef

10. 命令行定义

11. 条件编译

12. 头文件的包含

12.1 头文件被包含的方式

12.1.1 本地文件包含

12.1.2 库文件包含

12.2 嵌套文件包含

13. 其他预处理指令


1. 预定义符号

C语言设置了一些预定义符号, 可以直接使用 ,预定义符号也是在 预处理期间 处理的。
__FILE__ // 进行编译的源文件
__LINE__ //文 件当前的行号
__DATE__ //文 件被编译的日期
__TIME__ //文 件被编译的时间
__STDC__ // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义

我们来看一下,在vs2022中是否遵循ANSI C(标准C)

 由此可见,vs2022不遵循ANSI C

注:预定义符号在预处理间就被替换了


2. #define 定义常量

基本语法:
# define name stuff

# define MAX 1000
# define reg register // register 这个关键字,创建⼀个简短的名字
# define do_forever for(;;) //用 更形象的符号来替换⼀种实现
# define CASE break;case // 在写 case 语句的时候⾃动把 break 写上。
// 如果定义的 stuff 过⻓,可以分成⼏行写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠 ( 续⾏
)
# define DEBUG_PRINT printf( "file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )

注:上面“\”是续行符

 我们想一下,在define定义标识符的时候,要不要在最后加上---“;”

比如:

# define MAX 1000;
# define MAX 1000

 建议不要加上    ,这样容易导致问题。

比如下面的场景:
if (condition)
max = MAX;
else
max = 0 ;
如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有一条语句。这里会出现语法错误。

3. #define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
# define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

 #define SQUARE( x ) x * x

这个宏接收一个参数 x .如果在上述声明之后,你把 SQUARE( 5 ); 置于程序中,预处理器就会用
下面这个表达式替换上面的表达式:5*5
警告:
这个宏存在一个问题:
观察下面的代码段:
int a = 5 ;
printf ( "%d\n" ,SQUARE( a + 1 ) );

乍一看,你可能觉得这段代码将打印36,事实上它将打印11,为什么呢?

 

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ( "%d\n" ,a + 1 * a + 1 );
这样就比较清晰了,由替换产生的表达式并没有按照预想的次序进行求值。
在宏定义上加上两个括号,这个问题便轻松的解决了:
# define SQUARE(x) (x) * (x)

这样预处理之后就产生了预期的效果:

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

 这里还有一个宏定义:

# define DOUBLE(x) (x) + (x)

 定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。

int a = 5 ;
printf ( "%d\n" , 10 * DOUBLE(a));
这将打印什么值呢?看上去,好像打印100,但事实上打印的是55
我们发现替换之后:
printf ( "%d\n" , 10 * ( 5 ) + ( 5 ));
乘法运算先于宏定义的加法,所以出现了 55 .
这个问题的解决办法是在宏定义表达式两边加上一对括号就可以了。
# define DOUBLE( x) ( ( x ) + ( x ) )
提示:
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
写宏一定要考虑优先级和结合性,这个很重要!!!!!!!!!!!!

该加括号就加括号 !!!!!!!


4. 带有副作用的宏参数

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

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

 MAX宏可以证明具有副作用的参数所引起的问题。

# 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++是5,第一个y++是8,5<8是假,此刻x是6,y是9,执行第二个y++,那返回的值是9,z就为9,第二个y++之后,y最后为10,

输出的结果是:x=6 y=10 z=9


5. 宏替换的规则

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

6. 宏和函数的对比

宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。
# define MAX(a, b) ((a)>(b)?(a):(b))

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

原因有二:
1. 于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。      所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使      用。反之这个宏可以适用于整形、长整型、浮点型等,甚至可以把类型当做参数传进去比  。宏的参数是类型无关的,无需进行类型比较。

 和函数相比宏的劣势:

1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度,如果不断调用这个宏,那程序长度不断增大,空间也会增大,反之函数永远调用的是那一块空间函数,在这个方面函数比较简便。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
# define MALLOC(num, type)\
 (type )malloc(num sizeof(type))
 ...
  // 使用
 MALLOC( 10 , int ); // 类型作为参数
  // 预处理器替换之后:
 ( int * ) malloc ( 10 sizeof ( int ));
宏和函数的一个对比

补充

这里我们补充一个奇怪的东西,在c++里面有个内联函数(inline)它具有宏的特点,也有函数的特点,我们先简单了解下,等到c++再详细总解


 7. #和##

7.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

7.2 ## 运算符

## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称
为记号粘合
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。

如:

int int_max ( int x, int y)
{
return x>y?x:y;
}
float float_max ( float x, float y)
{
return x>yx:y;
}

 但是这样写起来太繁琐了,现在我们这样写代码试试:

// 宏定义
# define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}

 使用宏,定义不同函数

GENERIC_MAX( int
// 替换到宏体内后 int##_max ⽣成了新的符号 int_max 做函数名
  GENERIC_MAX( float ) // 替换到宏体内后 float##_max ⽣成了新的符号 float_max 做函数名
  int main ()
  {
  // 调⽤函数
  int m = int_max( 2 , 3 );
  printf ( "%d\n" , m);
  float fm = float_max( 3.5f , 4.5f );
  printf ( "%f\n" , fm);
  return 0 ;
  }

 输出:

3
4.500000

 整体代码如上图,这个代码非常巧妙地用宏来函数定义,只需将类型传进去,这个##就是用来将左右两个标识符合并成一个标识符,type改变,那对应的type_max也发生改变,这样就能有不同的函数名字。

假如没有##,会发生什么

如上图可知,没有##,这个type_max本身就是一体的,函数名不会随着你穿进去类型的变化而变化,type_max就是简简单单的一个标识符。 

 在实际开发过程中##使用的很少,很难取出非常贴切的例子。


8. 命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
把宏名全部大写
函数名不要全部大写

当然也有小写的宏,例如: 


9. #undef

这条指令用于移除一个宏定义。
# undef NAME
  // 如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

比如:

这个MAX不是被定义了嘛,因为#undef出现所以取消了MAX定义,此刻MAX未定义


10. 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)
# include <stdio.h>
int main ()
{
int array [ARRAY_SIZE];
int i = 0 ;
for (i = 0 ; i< ARRAY_SIZE; i ++)
{
array [i] = i;
}
for (i = 0 ; i< ARRAY_SIZE; i ++)
{
printf ( "%d " , array [i]);
}
printf ( "\n" );
return 0 ;
}

假如我们不用#define定义,我们可以直接在命令行中进行如下操作

 编译指令:

//linux 环境演⽰
gcc -D ARRAY_SIZE= 10 programe.c

 用-D命令直接把ARRAY_SIZE赋值成10(这是在gcc编译器下)


11. 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
# include <stdio.h>
# define __DEBUG__
int main ()
{
int i = 0 ;
int arr[ 10 ] = { 0 };
for (i= 0 ; i< 10 ; i++)
{
arr[i] = i;
# ifdef __DEBUG__
printf ( "%d\n" , arr[i]); // 为了观察数组是否赋值成功。
# endif //__DEBUG__
}
return 0 ;
}

 常见的条件编译指令:

1.
# if 常量表达式
//...
# endif
// 常量表达式由预处理器求值。
如:
# define __DEBUG__ 1
# if __DEBUG__
//..
# endif
2. 多个分支的条件编译
# if 常量表达式
//...
# elif 常量表达式
//...
# else
//...
# endif
3. 判断是否被定义
# if defined(symbol)
# ifdef symbol
# if !defined(symbol)
# ifndef symbol
4. 嵌套指令
# 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

注意:只要是#if指令,那么一定要在用完之后加上#endif !!!


12. 头文件的包含

12.1 头文件被包含的方式

12.1.1 本地文件包含

# include "filename"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。
如果找不到就提示编译错误
Linux环境的标准头文件的路径:
/usr/include

 VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
  // 这是 VS2013 的默认路径

 注意按照自己的安装路径去找。

12.1.2 库文件包含

# include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用  “” 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

12.2 嵌套文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的
地方一样。
这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。
一个头文件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
test.c
# include "test.h"
# include "test.h"
# include "test.h"
# include "test.h"
# include "test.h"
int main ()
{
return 0 ;
}
test.h
void test ();
struct Stu
{
int id;
char name[ 20 ];
};
如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。
如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头文件被重复引入的问题?答案:条件编译。
每个头文件的开头写:
# ifndef __TEST_H__
# define __TEST_H__
// 头⽂件的内容
# endif //__TEST_H__

或者

 #pragma once

 就可以避免头文件的重复引入。


13. 其他预处理指令

# error
# pragma
# line
# pragma pack()
……

 结束语

本篇文章小编已经尽力在总结重点,但肯定有些地方挖的不够深,如果想更加详细的了解这方面的点点滴滴,我们可以参考《C语言深度解剖

OK感谢观看!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/505095.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

零失误微信支付商家转账到零钱功能开通教程

商家转账到零钱是什么&#xff1f; 使用商家转账到零钱这个功能&#xff0c;可以让商户同时向多个用户的零钱转账。商户可以使用这个功能用于费用报销、员工福利发放、合作伙伴货款或分销返佣等场景&#xff0c;提高效率。 商家转账到零钱的使用场景有哪些&#xff1f; 商家…

如何使用Axure RP制作网页原型并结合IIS服务实现公网访问本地HTML网页

文章目录 前言1.在AxureRP中生成HTML文件2.配置IIS服务3.添加防火墙安全策略4.使用cpolar内网穿透实现公网访问4.1 登录cpolar web ui管理界面4.2 启动website隧道4.3 获取公网URL地址4.4. 公网远程访问内网web站点4.5 配置固定二级子域名公网访问内网web站点4.5.1创建一条固定…

STM32CubeIDE基础学习-RS232通信

STM32CubeIDE基础学习-RS232通信 文章目录 STM32CubeIDE基础学习-RS232通信前言第1章 工程配置第2章 代码编写第3章 实验现象总结 前言 RS232也是串口的一种&#xff0c;RS-232是由电子工业协会(Electronic Industries Association, EIA)所制定的异步传输标准接口。在1962年发布…

sql之每日五题day01--多表联查/聚合函数

sql错题记录 含有聚合函数的不能用where升序排列order byleft join多表联查inner join不返回null三表联查 含有聚合函数的不能用where SQL19 分组过滤练习题 题目&#xff1a;现在运营想查看每个学校用户的平均发贴和回帖情况&#xff0c;寻找低活跃度学校进行重点运营&#x…

PHP远程命令执行与代码执行原理利用与常见绕过总结

PHP远程命令执行与代码执行原理利用与常见绕过总结 远程命令执行 相较于SQL注入漏洞&#xff0c;远程命令执行更加少见。由于是直接执行系统命令&#xff0c;所以相较于前者此漏洞会更加危险&#xff1a; 攻击者通过远程命令执行漏洞可以直接掌控服务器攻击者可以通过存在此…

C语言:动态内存管理(二)

目录 前言 1.3 realloc​编辑 3、常见动态内存管理错误 3.1 对空指针的解引用操作 3.2 对动态开辟的空间进行越界访问 3.3 对非动态开辟内存使用free释放 3.4 使用free释放一块动态内存开辟的一部分 3.5 对同一块空间的多次释放 3.6 动态内存开辟之后忘记释放 总结 前…

python用户管理系统(加密)

在用户管理系统中使用哈希算法对用户密码进行加密处理 import hashlibusers []# 用户类&#xff0c;包含基本信息 class User:def __init__(self, name, password, emailNone):self.name nameself.password self._encrypt_password(password) # 加密密码self.email email…

ViveNAS性能调试笔记(一)

ViveNAS是一个开源的NAS文件服务软件&#xff0c;有一套独立自创的架构&#xff0c;ViveNAS希望能做到下面的目标&#xff1a; - 能支持混合使用高性能的介质(NVMe SSD)和低性能介质&#xff08;HDD&#xff0c;甚至磁带&#xff09;。做到性能、成本动态均衡。因此ViveNAS使用…

力扣刷题Days28-第二题-11.盛水最多的容器(js)

目录 1&#xff0c;题目 2&#xff0c;代码 3&#xff0c;学习与总结 3.1思路回顾 1&#xff0c;如何遍历 2&#xff0c;算法流程 3.2剖析问题 1&#xff0c;题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线&#xff0c;第 i 条线的两个端点是 (i, 0) 和 (i, h…

WordPress AutomaticPlugin SSRF漏洞复现(CVE-2024-27954)

0x01 产品简介 WordPress是一款免费开源的内容管理系统(CMS),最初是一个博客平台,但后来发展成为一个功能强大的网站建设工具,适用于各种类型的网站,包括个人博客、企业网站、电子商务网站等,并逐步演化成一款内容管理系统软件。 0x02 漏洞概述 WordPress AutomaticPlu…

jsp中设置动态时间

第一步 在head中写入meta <head><meta charset"UTF-8" http-equiv"Refresh" content"1"> </head> 第二步在head中写入函数 <head><meta charset"UTF-8" http-equiv"Refresh" content"…

网站可扩展架构设计——领域驱动设计(上)

从公众号转载&#xff0c;关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、【DDD】领域驱动设计简介 1.什么是DDD——应对复杂性的利器 DDD不是架构&#xff0c;而是一种架构设计方法论&#xff0c;它通过划分领域边界…

HarmonyOS实战开发-一次开发,多端部署-音乐专辑

简介 基于自适应和响应式布局&#xff0c;实现一次开发、多端部署音乐专辑页面。 相关概念 一次开发&#xff0c;多端部署&#xff1a;一套代码工程&#xff0c;一次开发上架&#xff0c;多端按需部署。支撑开发者快速高效的开发支持多种终端设备形态的应用&#xff0c;实现对…

【算法】记忆化搜索

概念 举例 拿斐波那契数列举例 可以发现一般的求解过程中&#xff0c;有很多重复的子问题&#xff0c;递归的本质就是深度优先遍历&#xff0c;如果此时我们可以记录下此时的值然后记录在哈希表中&#xff0c;下一次递归前先去哈希表中查找如果有该值的答案直接返回即可。 这…

大数据 - Hadoop系列《五》- HDFS文件块大小及小文件问题

系列文章&#xff1a; 大数据- Hadoop入门-CSDN博客 大数据 - Hadoop系列《二》- Hadoop组成-CSDN博客 大数据 - Hadoop系列《三》- HDFS&#xff08;分布式文件系统&#xff09;概述_大量小文件的存储使用什么分布式文件系统-CSDN博客 大数据 - Hadoop系列《三》- MapRedu…

[flink 实时流基础] flink 源算子

学习笔记 Flink可以从各种来源获取数据&#xff0c;然后构建DataStream进行转换处理。一般将数据的输入来源称为数据源&#xff08;data source&#xff09;&#xff0c;而读取数据的算子就是源算子&#xff08;source operator&#xff09;。所以&#xff0c;source就是我们整…

PostCSS的安装及使用(1):安装

在不同操作系统上安装PostCSS的步骤大致相同&#xff0c;因为PostCSS是基于Node.js的一个JavaScript工具&#xff0c;是依赖于Node.js环境和npm&#xff08;Node包管理器&#xff09;。 PostCSS官网&#xff1a; GitHub地址&#xff1a; GitHub - postcss/postcss: Transformi…

WebScraper网页数据爬取可视化工具使用(无需编码)

前言 Web Scraper 是一个浏览器扩展&#xff0c;可以实现无需编码即可爬取网页上的数据。只需按照规则进行配置&#xff0c;即可实现一键爬取导出数据。 安装 进入Google应用商店安装此插件&#xff0c;安装步骤如下&#xff1a; 进入Google应用商店需要外网VPN才能访问&…

学生寝室恶性负载识别模块

学生寝室恶性负载识别模块系统是石家庄光大远通电气公司一种专门用于学生寝室电力管理的系统&#xff0c;旨在识别并限制使用违规或高风险的电器设备&#xff0c;以保障学生的用电安全和节约能源。该系统通过先进的电力检测技术和算法&#xff0c;能够实时监测寝室内的电力使用…

react ts 封装搜索条件

封装 import React, { ReactNode, useImperativeHandle, forwardRef } from react; import { Card, Form, Input, Button, Row, Col } from antd;interface Iprops {formItem: any,getParams: (params: any) > void,reset?: () > void | undefined,isButton?: boolean…