【C语言】--- 预处理详解

news2025/5/31 11:25:42

预处理详解

    • 1. 预定义符号
    • 2. define定义常量
    • 2. 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 嵌套文件包含

1. 预定义符号

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

int main()
{
	printf("%s\n", __FILE__);//当前被编译的源文件的路径
	printf("%d\n", __LINE__);//文件当前的行号
	printf("%s\n", __DATE__);//文件被编译的日期
	printf("%s\n", __TIME__);//文件被编译的时间
	//printf("%s\n", __STDC__); //如果编译器支持 ANSI c,其值为1,否则未定义,可惜的是vs中不能使用这个符号
}

在这里插入图片描述
GCC可以支持_STDC,下面我们在预处理后的code1.i这个文件中查看上述预处理符号的替换情况。
在这里插入图片描述

2. define定义常量

  1. 基本语法

#define name stuff

例如

  1. define MAX 100 定义整形常量
  2. define ll long long 给关键字创建一个简短的名字
  3. define STR “hehe\n” 定义字符串常量

下面在Linux的GCC环境下演示一下
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们这里也可以看到,#define 定义都是在预处理阶段做替换处理的。

下面给出几种比较奇怪的#define 定义

正常switch语句的语法如下

	int n = 0;
	switch (n)
	{
	case 1:
		break;
	case 2:
		break;
	case 3:
		break;
	case 4:
		break;
	}

#define CASE break;case

当使用使用以上的定义后switch语句就可以变成这种形式

switch (n)
{
	case 1:
	CASE 2 :
	CASE 3 :
	CASE 4 :
	break;
}

经过预处理后为如下:这本质就是替换

	switch(n)
	{
	case 1:
	break; case 2:
	break; case 3:
	break; case 4:
	break;
	}

如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后⾯都加⼀个反斜杠
(续行符

#define DEBUG_PRINT printf("hehe %d"\
,\
M);

这里提出一个问题:#define 定义的语句后面是否可以加;

# define N 100;
printf("%d", 100 + N); 

这样预处理后的语句为:

printf("%d", 100 + 100;);

所以#define后面不建议加;,因为这样在部分情况下替换后会出现语法错误。

2. define 定义宏

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

申明方式 : #define name( parament-list ) stuff

  1. parament-list 是一个由逗号隔开的符号表,它们可能会出现在stuff中
  2. 参数列表的左括号必须和那么紧邻,不然会被解释为stuff的一部分。

举个例子

#define SQUARE(N) N*N
int main()
{
	int a = 5;
	int ret = SQUARE(a);
	printf("%d\n",ret);
	return 0;
}

在这里插入图片描述
这就是宏,在预处理阶段,宏会被替换,int ret = a*a,所以输出结果为25。
但是这样的宏定义是不够健壮的,请下面的例子:

int main()
{
	int a = 5;
	int ret = SQUARE(a+1);
	printf("%d\n",ret);
	return 0;
}

在这里插入图片描述
这里的结果为什么不是36而是11呢?抓住宏的本质是替换,int ret = SQUARE(a+1)—> int ret =a+1*a+1
在宏定义中加上两个括号,这个问题就轻松解决了。

#define SQUARE(N) (N)*(N)

这样替换替换后的结果为:

int ret = (a+1)*(a+1);

再来一个例子:

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

这个宏定义是否健壮呢?

10 * DOUBLE(a)   ---- >   10*(x)+(x)

这需要在宏定义中再加一个括号

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

从上面的举例可以看出,宏的定义还是有很多的坑的,所以用于对数值表达式进行求值的宏定义都应该加上括号,避免在使用宏时由于参数中的操作符优先级导致非预期的计算顺序。

4. 带有副作用的宏参数

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

int b = a+1; 没有副作用
int b =++a; 存在副作用

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

 #define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
int main()
{
	x = 5;
 	y = 8;
	z = MAX(x++, y++);
 	printf("x=%d y=%d z=%d\n", x, y, z);
	return 0;
}

这个程序的输出结果时什么呢?

z=((x++)>(y++) ? (x++):(y++));  

在这里插入图片描述

5.宏替换的规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#define M1 10
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int a = 5;
MAX(a,M1) -- > MAX(a,10) -- > ((a) >(10)?(a):(10))

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#define M 10
printf("M = %d",M) --- > printf("M = %d",10);

6. 宏和函数的对比

宏通常由于简单的运算,比如在两个数中找出较大的⼀个时,写成下面的宏,更有优势⼀些。

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

原因如下:

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

和函数相比宏的劣势:

  1. 每次适用宏的时候,⼀份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的,因为宏的本质是替换,不像函数建立了函数栈帧。
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错,需要加上很多括号。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

# define MALLOC(a,b) (b*)malloc(a,sizeof(b))
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	//分配内存的时候代码太长了,写的一点也不爽,可以通过宏改写一下
	int* p2 = MALLOC(10, int);
	return 0;
}

在这里插入图片描述

7. # 和 ##

7.1#运算符

#运算符将宏的⼀个参数转换为字符串字面量它仅允许出现在带参数的宏的替换列表中
#运算符所执行的操作可以理解为”字符串化“。

int main()
{
	int a = 3;
	printf("the value of a is %d\n", a);
	int b = 20;
	printf("the value of b is %d\n", b);
	float f = 3.14f;
	printf("the value of f is %f\n", f);
	return 0;
}

代码类似了如何化简呢?

#define PRINT(v,format) printf("the value of "#v" is "format"\n",v)
//#v == "v"
int main()
{
	int a = 3;
	PRINT(a, "%d");
	int b = 20;
	PRINT(b, "%d");;
	float f = 3.14f;
	PRINT(f, "%f");
	return 0;
}

在这里插入图片描述

7.2 ##运算符

## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。
##被称为记号粘合,这样的连接必须产生⼀个合法的标识符。否则其结果就是未定义的。

#define CAT(x,y) x##y
int main()
{
	int classx = 100;
	printf("%d\n", CAT(class, x));
	return 0;
}

在这里插入图片描述

我们想想,写⼀个函数求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 main()
{
	int m = int_max(3, 4);
	printf("%d\n", m);
	return 0;
}

在这里插入图片描述
我们看到这个宏在预处理后被处理为函数的定义了。
在这里插入图片描述
在实际开发过程中##使用的很少。

8. 命名约定

⼀般来讲函数和宏的使用语法很相似。所以语言本身没法帮我们区分二者。

那我们平时的⼀个习惯是:
宏名全部大写
函数名不要全部大写,可以写为小驼峰 helloWorld 、大驼峰 HelloWorld 、下划线 hello_world。

9.#undef

这条指令⽤于移除⼀个宏定义。
在这里插入图片描述

10.命令行定义

许多C的编译器提供了⼀种能力,允许在命令行中定义符号。用于启动编译过程。当我们根据同⼀个源文件要编译出⼀个程序的不同版本的时候,这个特性有点用处。
在这里插入图片描述
在这里插入图片描述
这样就可以灵活的调整数组的大小了。

11.条件编译

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

#define __DEBUG__
int main()
{
	int arr[10] = { 0 };
	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 1;
#ifdef __DEBUG__
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

在这里插入图片描述
下面来看一下常见的条件编译指令
<1>单分支的条件编译

#if (常量表达式)   //不可以是变量,变量在程序运行的时候才会分配空间。

#endif
int main()
{
#if 3==3
	printf("hehe\n");
#endif
	return 0;
}

在这里插入图片描述
<2>多分支的条件编译

#if 常量表达式

 #elif 常量表达式

 #else
 #endif
#define M 3
int main()
{
#if M ==1
	printf("hehe\n");
#elif M ==2
	printf("hha\n");
#elif M ==3
	printf("heihei\n");
#else
	printf("哈哈\n");
#endif
}

在这里插入图片描述
<3>判断是否被定义

#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

仅仅关系是否被定义过,而不关心其值为多少。

#define M 
#define N 
int main()
{
#if defined M
	printf("呵呵\n");
#endif
#ifdef N
	printf("哈哈\n");
#endif
#if !defined N2
	printf("桀桀\n");
#endif
#ifndef N3
	printf("嘻嘻\n");
#endif
	return 0;
}

在这里插入图片描述
<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
#define M 3
#define N 2
int main()
{
#if defined M
  #if M ==3
	printf("呵呵!");
  #elif
	printf("haha");
  #endif
#elif !defined M
  #if N == 2
	printf("嘿嘿");
  #else 
	printf("11");
  #endif
#endif
	return 0;
}

12. 头文件的包含

12.1 头文件被包含的方式

12.1.1 头文件的本地包含
#include "filename"

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

  1. linux下标准头文件的查找路径:/usr/include
12.1.2 库文件的包含
#include<filename>

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

12.2 嵌套文件包含

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如何头文件比较大,这样预处理后代码量会剧增。那么解决头文件重复包含的问题呢?

  1. 条件编译
    在这里插入图片描述
    这样头文件就会仅包含一次了
    在这里插入图片描述
  2. #program once
    只需要在头文件中添加一句#program once就可以解决头文件被重复包含的问题。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Axure视频教程】标准金额格式转换

今天教大家在Axure制作标准金额格式转换的原型模板&#xff0c;具体效果可以参考下方视频&#xff0c;该教程从0开始制作&#xff0c;手把手教学&#xff0c;无论是新手小白还是有一定基础的同学&#xff0c;都可以学习的哦。 【视频教程——试看版】 【Axure教程】标准金额格…

每日算法(双指针算法)(Day 1)

双指针算法 1.算法题目&#xff08;移动零&#xff09;2.讲解算法原理3.编写代码 1.算法题目&#xff08;移动零&#xff09; 2.讲解算法原理 数组划分&#xff0c;数组分块&#xff08;快排里面最核心的一步&#xff09;只需把0改为tmp 双指针算法&#xff1a;利用数组下标来…

微服务多模块构建feign项目过程与一些报错(2025详细版)

目录 1.eureka-server的注意事项 2.eureka-feign的注意事项 3.多模块构建feign项目过程 3.1创建父项目 3.2创建子项目eureka-server 3.3创建子项目eureka-provider 3.4创建子项目eureka-feign 3.5运行 给个点赞谢谢 1.eureka-server的注意事项 eureka-server的yml文件…

性能测试-tomcat连接数

Tomcat 处理请求时,是需要 Connector 进行调度和控制的,Connector是Tomcat 处理请求的主干。 Connector 中有一个 accepf队列,当客户端向服务器发送http请求时,如果客户端与操作系统完成三次握手建立了连接,就将该连接放入accept队列,poller从队列中获取到链接后,从链接…

C# 运行web项目

1、web项目直接点击顶部运行

全网通emotn ui桌面免费吗?如何开机自启动

在智能设备的使用中&#xff0c;一款优秀的桌面系统能带来截然不同的体验。全网通Emotn UI桌面便是其中的佼佼者&#xff0c;它以完全免费的特性与卓越性能&#xff0c;成为众多用户的心头好。 其简洁美观的界面设计如同为设备换上"清新外衣"&#xff0c;常用功能一…

【AI模型学习】MAE——CV界的无监督预训练

文章目录 一、诞生背景1.1 自监督学习的趋势2.2 ViT 的出现 二、模型2.1 模型架构2.1.1 数据shape变化2.1.2 模型架构流程图2.1.3 PyTorch 代码示例&#xff08;核心部分&#xff09; 2.2 位置信息2.3 非对称的编码器-解码器结构2.4图片重构 三、实验3.1 主实验3.2 消融实验3.3…

远方游子的归家记:模仿美食网页的制作与实现

前言 2023年的夏天&#xff0c;闲得无聊学了一个礼拜前端知识点。并根据所学知识点模仿制作了一篇网络上公开发布的关于家乡美食的文章。今天才想到有这个不错的案例可以分享出来&#xff0c;以供大家学习参考。 知识点简介 运用的知识点比较简单&#xff0c;常规的div盒子&…

element-ui colorPicker 组件源码分享

简单分享 colorPicker 颜色选择器组件源码&#xff0c;主要从以下三个方面&#xff1a; 1、colorPicker 组件页面结构。 2、colorPicker 组件属性。 3、colorPicker 组件事件。 一、组件页面结构。 二、组件属性。 2.1 value/v-model 绑定值属性&#xff0c;类型为 string…

Git 学习笔记

这篇笔记记录了我在git学习中常常用到的指令&#xff0c;方便在未来进行查阅。此篇文章也会根据笔者的学习进度持续更新。 网站分享 Git 常用命令大全 Learn Git Branching 基础 $ git init //在当前位置配置一个git版本库 $ git add <file> //将文件添加至…

安防监控视频管理平台EasyCVR助力建筑工地施工4G/5G远程视频监管方案

一、项目背景 随着城市建设的快速发展&#xff0c;房地产建筑工地的数量、规模与施工复杂性都在增加&#xff0c;高空作业、机械操作频繁&#xff0c;人员流动大&#xff0c;交叉作业多&#xff0c;安全风险剧增。施工企业和政府管理部门在施工现场管理上都面临难题。政府部门…

Cursor Talk To Figma MCP 安装与配置指南

Cursor Talk To Figma MCP 安装与配置指南 1.项目基础介绍 Cursor Talk To Figma MCP 是一个开源项目&#xff0c;它实现了 Cursor AI 与 Figma 之间的 Model Context Protocol&#xff08;MCP&#xff09;集成。通过这个集成&#xff0c;Cursor 能够与 Figma 进行通信&#…

高性能内存kv数据库Redis

目录 引言 一.Redis相关命令详解及其原理 1.redis是什么&#xff1f; 2.redis中存储数据的数据结构都有哪些&#xff1f; 3.redis的存储结构&#xff08;KV&#xff09; 4.reidis中value编码 5.string的基本原理和相关命令 5.1基本原理 5.2基础命令 5.3string存储结构 …

性能优化实践

4.1 大规模量子态处理的性能优化 背景与问题分析 量子计算中的大规模量子态处理(如量子模拟、量子态可视化)需要高效计算和实时渲染能力。传统图形API(如WebGL)在处理高维度量子态时可能面临性能瓶颈,甚至崩溃(如表格中14量子比特时WebGL的崩溃)。而现代API(如WebGPU…

使用wpa_cli和wpa_supplicant配置Liunx开发板的wlan0无线网

目录 1 简单介绍下wpa_cli和wpa_supplicant 1.1 wpa_supplicant 简介 1.2 wpa_cli 简介 1.3 它们之间的关系 2 启动wpa_supplicant 3 使用rz工具把wpa_cli命令上传到开发板 4 用wpa_cli配置网络 参考文献&#xff1a; 1 简单介绍下wpa_cli和wpa_supplicant 1.1 wpa_su…

C++Cherno 学习笔记day19 [76]-[80] std::optional、variant、any、如何让C++及字符串运行得更快

b站Cherno的课[76]-[80] 一、如何处理OPTIONAL数据 std::optional二、单一变量存放多类型的数据 std::variant三、如何存储任意类型的数据 std::any四、如何让C运行得更快五、如何让C字符串更快 一、如何处理OPTIONAL数据 std::optional std::optional C17 数据是否存在是可选…

【项目日记(一)】-仿mudou库one thread oneloop式并发服务器实现

1、模型框架 客户端处理思想&#xff1a;事件驱动模式 事件驱动处理模式&#xff1a;谁触发了我就去处理谁。 &#xff08; 如何知道触发了&#xff09;技术支撑点&#xff1a;I/O的多路复用 &#xff08;多路转接技术&#xff09; 1、单Reactor单线程&#xff1a;在单个线程…

OpenCV 图形API(35)图像滤波-----中值模糊函数medianBlur()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 使用中值滤波器模糊图像。 该函数使用带有 ksizeksize 开口的中值滤波器来平滑图像。多通道图像的每个通道都是独立处理的。输出图像必须与输入…

视觉slam框架从理论到实践-第一节绪论

从opencv的基础实现学习完毕后&#xff0c;接下来依照视觉slam框架从理论到实践&#xff08;第二版&#xff09;的路线进行学习&#xff0c;主要以学习笔记的形式进行要点记录。 目录 1.数据里程计 2.后端优化 3.回环检测 4.建图 在视觉SLAM 中整体作业流程可分为&#xff1…

图论--DFS搜索图/树

目录 一、图的存储结构 二、题目练习 846. 树的重心 - AcWing题 dfs&#xff0c;之前学习的回溯算法好多都是用dfs实现搜索的&#xff08;把题目抽象成树形结构来搜索&#xff09;&#xff0c;其实 回溯算法就是 深搜&#xff0c;只不过针对某一搜索场景 我们给他一个更细分…