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感谢观看!!!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实践笔记-harbor搭建(版本:2.9.0)

harbor搭建 1.下载安装包&#xff08;版本&#xff1a;2.9.0&#xff09;2.修改配置文件3.安装4.访问harbor5.可能用得上的命令: 环境&#xff1a;centos7 1.下载安装包&#xff08;版本&#xff1a;2.9.0&#xff09; 网盘资源&#xff1a;https://pan.baidu.com/s/1fcoJIa4x…

睿尔曼超轻量仿人机械臂之复合机器人底盘介绍及接口调用

机器人移动平台是一个包含完整成熟的感知、认知和定位导航能力的轮式机器人底盘产品级平台&#xff0c;产品致力于为各行业细分市场的商用轮式服务机器人提供一站式移动机器人解决方案&#xff0c;让合作伙伴专注在核心业务/人机交互的实现。以下是我司产品双臂机器人以及复合升…

安装部署MariaDB数据库管理系统

目录 一、初始化MariaDB服务 1、安装、启动数据库服务程序、将服务加入开机启动项中。 2、为保证数据库安全性和正常运转&#xff0c;需要对数据库程序进行初始化操作。 3、配置防火墙&#xff0c;放行对数据库服务程序的访问请求&#xff0c;允许管理员root能远程访问数据…

使用 Spring Email 和 Thymeleaf 技术,向新注册用户发送激活邮件(一)

这篇内容对应"2.1 发送邮件"小节 邮箱设置 需要去邮箱对应的官方客户端软件或网站开启IMAP/SMTP服务或POP3/SMTP服务器 如果不开启&#xff0c;就无法使用第三方用户代理&#xff0c;只能走第官方的电子邮件客户端软件或网站&#xff0c;用户代理就是电子邮件客户…

2024-03-26 Android8.1 px30 WI-FI 模块rtl8821cu调试记录

一、kernel 驱动&#xff0c;我这里使用v5.8.1.2_35530.20191025_COEX20191014-4141这个版本&#xff0c;下载这个版本的驱动可以参考下面的文章。 2021-04-12 RK3288 Android7.1 USB wifi bluetooth 模块RTL8821CU 调试记录_rk平台rtl8821cu蓝牙调试-CSDN博客 二、Makefile文…

C++从入门到精通——引用()

C的引用 前言一、C引用概念二、引用特性交换指针引用 三、常引用保证值不变权限的方法权限的放大权限的缩小权限的平移类型转换临时变量 四、引用的使用场景1. 做参数2. 做返回值 五、传值、传引用效率比较值和引用的作为返回值类型的性能比较 六、引用和指针的区别引用和指针的…

web 技术中前端和后端交互过程

1、客户端服务器交互过程 客户端:上网过程中,负责浏览资源的电脑,叫客户端服务器:在因特网中,负责存放和对外提供资源的电脑叫服务器 服务器的本质: 就是一台电脑,只不过相比个人电脑它的性能高很多,个人电脑中可以通过安装浏览器的形式,访问服务器对外提供的各种资源。 个人…

Electron 读取本地配置 增加缩放功能(ctrl+scroll)

最近&#xff0c;一个之前做的electron桌面应用&#xff0c;需要增加两个功能&#xff1b;第一是读取本地的配置文件&#xff0c;然后记载配置文件中的ip地址&#xff1b;第二就是增加缩放功能&#xff1b; 第一&#xff0c;配置本地文件 首先需要在vue工程根目录中&#xff0…

切换ip地址的app,简单易用,保护隐私

在数字化时代&#xff0c;IP地址作为网络设备的标识&#xff0c;不仅承载着数据在网络间的传输任务&#xff0c;还在一定程度上关联着用户的隐私和安全。因此&#xff0c;切换IP地址的App应运而生&#xff0c;为用户提供了一种便捷的方式来改变其网络身份&#xff0c;实现匿名浏…

【Spring MVC】快速学习使用Spring MVC的注解及三层架构

&#x1f493; 博客主页&#xff1a;从零开始的-CodeNinja之路 ⏩ 收录文章&#xff1a;【Spring MVC】快速学习使用Spring MVC的注解及三层架构 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 目录 Spring Web MVC一: 什么是Spring Web MVC&#xff1…

【应用笔记】LAT1413+快速开关蓝牙导致设备无广播

1. 问题背景 客户使用 BlueNRG-345MC 开发了一个 BLE 外设&#xff0c;和手机连接。在测试中发现&#xff0c;手机连接上外设之后&#xff0c;不断地在手机上点击蓝牙的开关按钮&#xff0c;造成设备不断地断开、重连&#xff1b;少则几次&#xff0c;多则几十次。点击之后&am…

【Entity Framework】创建并配置模型

【Entity Framework】创建并配置模型 文章目录 【Entity Framework】创建并配置模型一、概述二、使用fluent API配置模型三、分组配置四、对实体类型使用EntityTypeConfigurationAttribute四、使用数据注释来配置模型五、实体类型5.1 在模型中包含类型5.2 从模型中排除类型5.3 …

loadbalancer 引入与使用

在消费中pom中引入 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> 请求调用加 LoadBalanced 注解 进行服务调用 默认负载均衡是轮训模式 想要切换…

【数据结构与算法】二叉树的遍历及还原

树形结构 - 有向无环图 树是图的一种。 树形结构有一个根节点树形结构没有回路根节点&#xff1a;A叶子节点&#xff1a;下边没有其他节点了节点:既不是根节点,又不是叶子节点的普通节点树的度:这棵树最多叉的节点有多少叉&#xff0c;这棵树的度就为多少树的深度&#xff1a…

实例、构造函数、原型、原型对象、prototype、__proto__、原型链……

学习原型链和原型对象&#xff0c;不需要说太多话&#xff0c;只需要给你看看几张图&#xff0c;你自然就懂了。 prototype 表示原型对象__proto__ 表示原型 实例、构造函数和原型对象 以 error 举例 图中的 error 表示 axios 抛出的一个错误对象&#xff08;实例&#xff0…

WiFiSpoof for Mac wifi地址修改工具

WiFiSpoof for Mac&#xff0c;一款专为Mac用户打造的网络隐私守护神器&#xff0c;让您在畅游互联网的同时&#xff0c;轻松保护个人信息安全。 软件下载&#xff1a;WiFiSpoof for Mac下载 在这个信息爆炸的时代&#xff0c;网络安全问题日益凸显。WiFiSpoof通过伪装MAC地址&…

C++入门知识详细讲解

C入门知识详细讲解 1. C简介1.1 什么是C1.2 C的发展史1.3. C的重要性1.3.1 语言的使用广泛度1.3.2 在工作领域 2. C基本语法知识2.1. C关键字(C98)2.2. 命名空间2.2 命名空间使用2.2 命名空间使用 2.3. C输入&输出2.4. 缺省参数2.4.1 缺省参数概念2.4.2 缺省参数分类 2.5. …

GRE和MGRE综合实验

实际网段划分 分配IP 1.IP划分 [r1]int g0/0/0 [r1-GigabitEthernet0/0/0]ip add 192.168.1.254 24 Mar 29 2024 16:42:44-08:00 r1 %%01IFNET/4/LINK_STATE(l)[3]:The line protocol IP on the interface GigabitEthernet0/0/0 has entered the UP state. [r1-Gigabi…

飞天使-k8s知识点28-kubernetes散装知识点5-helm安装ingress

文章目录 安装helm添加仓库下载包配置创建命名空间安装 安装helm https://get.helm.sh/helm-v3.2.3-linux-amd64.tar.gztar -xf helm-v3.2.3-linux-amd64.tar.gzcd linux-amd64mv helm /usr/local/bin修改/etc/profile 文件&#xff0c;修改里面内容,然后重新启用export PATH$P…

动态规划-----背包类问题(0-1背包与完全背包)详解

目录 什么是背包问题&#xff1f; 动态规划问题的一般解决办法&#xff1a; 0-1背包问题&#xff1a; 0 - 1背包类问题 分割等和子集&#xff1a; 完全背包问题&#xff1a; 完全背包类问题 零钱兑换II: 什么是背包问题&#xff1f; 背包问题(Knapsack problem)是一种…