C语言之详解预处理

news2025/2/3 10:42:25

前言:

预处理也叫预编译,是编译代码时的第一步,经过预处理后生成一个.i文件,如果不明白编译与链接作用的小伙伴可以先看看博主的上一篇博客—— ,不然知识连贯性可能会显得很差哦。

正文目录:

  1. 预定义符号
  2. #define定义常量
  3. #define定义宏
  4. 带有副作用的宏参数
  5. 宏替换的规则
  6. 宏与函数的对比和命名约定
  7. #和##
  8. #undef
  9. 条件编译
  10. 头文件的包含
  11. 其他预处理指令......

1.预定义符号

如下为c语言中的预定义符号:

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

这些都是我们可以直接使用、在预处理阶段就已经处理了的。

我们举个例子:

2.#define定义常量

基本语法形式如下:

#define name stuff        //其中name表示名字, stuff表示(被替换的)内容

当我们用该语法后,stuff就被name给替换了。

示例如下:

此时100就被M替换了,因此a输出结果为100。

在预处理阶段,会将#define定义的“名字”替换为它所表示的常量,比如说上图中的a = M ,经预处理后实际的代码形式为a = 100

3.#define定义宏

如下为宏的申明方式:

#define name( parament-list ) stuff   //parament-list为参数列表,stuff为内容。

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

示例如下:

但是需要注意,#define定义宏可能会带来运算级优先级的问题。

就跟上面同样的题目,我们换种写法如下:

可以发现,我们M(a)中的a本来为10,但当我们写成9 + 1后结果与我们所期待的不符。

这是为什么呢?——上文讲过,“在预处理阶段,会将#define定义的“名字”替换为它所表示的常量”,此处的“名字”就相当于M(9 + 1)。当替换后,该行代码就变为了“a = 9 + 1 * 9 + 1”。

容易发现跟我们的要求不符,这就是运算符优先级的问题。

我们可以这样修改:

如上图所示,我们加个小括号就可以了。因此我们用#define定义宏时,一定不要吝啬括号

4.带有副作用的宏参数

简单来讲副作用就是说会导致参数发生改变

严谨点的就是下面的说法:

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

例如:

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

具体示例如下:

当我们后续再使用代码中的a、b时,就可能不是我们所期望的值了。

5.宏替换的规则

这个就是偏概念性的东西了,规则如下:

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

  1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。
  2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

我们给出一个示例,按照宏替换的规则进行替换,如下图所示:

然后我们就将第二个宏定义的内容替换到主函数中的相应宏中,

因此最终主函数的第一行代码被替换为了:int m = ((a) > (b) ? (a) : (b));

6.宏与函数的对比和命名约定

我们先讲二者的命名约定:

容易发现,函数的宏的使⽤语法很相似。所以语言本⾝没法帮我们区分⼆者。
因此我们平时的⼀个习惯是:
把宏名全部⼤写
函数名不要全部⼤写

宏与函数的对比

宏一般用于简单的运算,因为如果简单的运算就使用函数的话会加大我们的计算时间。

因此宏相对于函数:

宏在程序的规模和速度更胜一筹(规模更小,运算更快)

不仅如此,宏的参数与类型无关

而宏较于函数的劣势处:

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

但是宏却可以做到一些函数永远做不到的事——比如说参数中出现类型。

两者具体差异如下表所示:

7.#和##

#运算符

#可以理解为“字符串化

#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。

示例如下:

##运算符

##可以理解为“联结符号

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

示例如下:

但是这样就有点麻烦了 一次求值就要写一个函数 因此我们可以换种方式写👇:

//假设我们要求两个数中的较大值
#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 f = float_max(3.5f, 4.5f);
	printf("%f\n", f);
	return 0;
}

这样子 我们就可以直接通过宏来求 而不用每次求不同类型的数据时都要写不同的函数。

8.#undef

移除一条宏定义

示例如下:

9.条件编译

简单来讲就是“满足条件就编译,不满足条件就不编译”。


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

示例如下:

其中#if#endif就是条件编译语句。易知1 != 2 因此没有打印hehe。

10.头文件的包含

头文件的包含一般分为两种形式:

  1. <······>   (如#include <stdio.h>)
  2. "······"   (如#include "filename")

前者库文件包含,一般指标准库中头文件的包含。

查找头文件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤" "的形式包含呢?——
答案是肯定的,可以是可以,但是这样做查找的效率就低些,而且这样也不容易区分是库⽂件还是本地文件了。

后者本地文件包含,一般指自己创建的头文件的包含。

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

除了上述两种外,其实还有一种情况——嵌套文件包含

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

比如下面的代码:

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

其中的十条#include "test.h",在编译时都被test.h头文件中包含的内容替换了,导致编译压力较大。 这种情况就是嵌套文件包含。

那么我们该如何解决这些问题呢?——自然是用刚刚学的条件编译了。

1.每个头文件的开头写:

#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif 
//__TEST_H__

2.或者

 #pragma once

这样就可以有效避免头文件的重复引入了。

11.其他预处理指令

#error
#pragma
#line
...
...

#pragma pack()  

我们还有很多其他的预处理指令,本文自然不可能给大家一 一讲完,大家可以自行去了解哦~



创作不易,如果作者写的还行的话给个免费的三连吧亲😙😙

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

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

相关文章

Redis 入门篇

文章目录 Redis简介关系型数据库:非关系型数据库 Redis应用场景Redis下载和安装Redis 数据类型Redis 常用命令字符串 string 操作命令哈希 hash 操作命令列表 list 操作命令集合 set 操作命令有序集合 sorted set 操作命令通用命令 Jedis 快速入门配置依赖建立连接 / 操作 Jedi…

Android蓝牙开发(一)之打开蓝牙和设备搜索

private BluetoothManager bluetoothmanger; private​ BluetoothAdapter bluetoothadapter; /** 判断设备是否支持蓝牙 */ bluetoothmanger (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothadapter bluetoothmanger.getAdapter(); if (bl…

element-ui里message抖动问题

由于element默认屏蔽滚动条&#xff0c;导致取消时弹message时 侧边滚动栏突然回来后引起抖动问题 是由于打开弹窗时出现遮罩层dialog对话框 时引起了元素内容超出自身尺寸 对应的overflow样式内容为hidden&#xff0c;且新建了一个class类内容为增加17 内右边距&#xff0c;当…

一文详解分布式 ID

分布式系统中&#xff0c;我们经常需要对数据、消息等进行唯一标识&#xff0c;这个唯一标识就是分布式 ID&#xff0c;那么我们如何设计它呢&#xff1f;本文将详细讲述分布式 ID 及其生成方案。 一、为什么需要分布式 ID 目前大部分的系统都已是分布式系统&#xff0c;所以在…

python实训day2

1、 from ming import * # 有点像C语言中的头文件 """在Python开发环境中&#xff0c;封装一个函数&#xff0c;功能目标为&#xff1a;通过两个整数参数一次性获取和、差、积、商四个值 """ def calc(a, b):return a b, a - b, a * b, a / b…

使用SPI驱动数码管

代码&#xff1a; 7-seg.c /*《AVR专题精选》随书例程3.通信接口使用技巧项目&#xff1a;改进的延时法实现半双工软件串口文件&#xff1a;7seg.c说明&#xff1a;SPI控制数码管驱动文件作者&#xff1a;邵子扬时间&#xff1a;2012年12月15日*/#include <avr/io.h>ex…

AIGC时代的英语教育:人工智能会取代英语老师吗?

在当前AIGC&#xff08;Artificial Intelligence Generated Content&#xff09;时代&#xff0c;人工智能技术正在迅速发展并渗透到各个领域&#xff0c;其中包括英语教育。面对这一趋势&#xff0c;许多人担心人工智能会取代传统的英语教师。然而&#xff0c;本文将探讨人工智…

Android 天气APP(八)城市切换 之 自定义弹窗与使用

然后在模块的utils包中新建一个LiWindow类 代码如下&#xff1a; package com.llw.mvplibrary.utils; import android.app.Activity; import android.content.Context; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; im…

GitHub 标星 6

美国网友对这个大全给予了很高的评价&#xff1a;这份清单中列出的开源软件&#xff0c;不仅解决了硅谷大厂前员工的难处&#xff0c;也能为其他所有码农解除困惑。 在这套大全的指导下&#xff0c;任何一个工程师&#xff0c;都能获得类似在谷歌内部写代码的体验。xg2xg 上线…

【Flutter 专题】120 Flutter 腾讯移动通讯 TPNS~

1.2 方法使用 小菜按照官网的介绍尝试了一些常用的 API 方式&#xff0c;主要分为应用类&#xff0c;账号类和标签类三种 API&#xff0c;小菜业务中没有应用账号和标签模块&#xff0c;暂未深入研究&#xff1b; 应用接口 API a. 注册推送服务 对于服务的注册初始化&#x…

软件串口接收子程序

代码; stduart.c /*《AVR专题精选》随书例程3.通信接口使用技巧项目&#xff1a;使用延时法实现半双工软件串口文件&#xff1a;sfuart.c说明&#xff1a;软件串口驱动文件作者&#xff1a;邵子扬时间&#xff1a;2012年12月13日*/ #include "sfduart.h"// 循环中延…

数据结构历年考研真题对应知识点(栈)

目录 3.1栈 3.1.1栈的基本概念 【栈的特点&#xff08;2017&#xff09;】 【入栈序列和出栈序列之间的关系(2022)】 【特定条件下的出栈序列分析(2010、2011、2013、2018、2020)】 3.1.2栈的顺序存储结构 【出/入栈操作的模拟(2009)】 3.1栈 3.1.1栈的基本概念 【栈…

嵌入式linux系统中LCD屏驱动实现思路分析

在 Linux 下 LCD 的使用更加广泛,在搭配 QT 这样的 GUI 库下可以制作出非常精美的 UI 界面。接下来就来学习一下如何在 Linux 下驱动 LCD 屏幕。 第一:Framebuffer设备简介 先来回顾一下裸机的时候 LCD 驱动是怎么编写的,裸机 LCD 驱动编写流程如下: ①、初始化 I.MX6U 的…

NeRF从入门到放弃4: NeuRAD-针对自动驾驶场景的优化

NeuRAD: Neural Rendering for Autonomous Driving 非常值得学习的一篇文章&#xff0c;几乎把自动驾驶场景下所有的优化都加上了&#xff0c;并且也开源了。 和Unisim做了对比&#xff0c;指出Unisim使用lidar指导采样的问题是lidar的垂直FOV有限&#xff0c;高处的东西打不…

一年前 LLM AGI 碎片化思考与回顾系列⑦ · 在SystemⅡ未知之境之中徘徊

阅读提示&#xff1a; 本篇系列内容的是建立于自己过去一年在以LLM为代表的AIGC快速发展浪潮中结合学术界与产业界创新与进展的一些碎片化思考并记录最终沉淀完成&#xff0c;在内容上&#xff0c;与不久前刚刚完稿的那篇10万字文章「融合RL与LLM思想&#xff0c;探寻世界模型以…

02--MySQL数据库概述

目录 第10章 子查询 10.1 SELECT的SELECT中嵌套子查询 10.2 SELECT的WHERE或HAVING中嵌套子查询 10.3 SELECT中的EXISTS型子查询 10.4 SELECT的FROM中嵌套子查询 第11章 MySQL支持的数据类型 11.1 数值类型:包括整数和小数 1、整数类型 2、bit类型 3、小数类型 11.2…

1996年-2023年 全国298个地级市-外商直接投资FDI(数据收集)

外商直接投资&#xff08;FDI&#xff09;是一种跨国界的经济活动&#xff0c;它涉及外国投资者在中国境内进行的直接投资行为。这种投资行为不仅包括以货币、实物、技术等形式的资本投入&#xff0c;还可能包括开办独资企业、合资企业、合作企业&#xff0c;以及参与资源开发等…

FreeCAD中智能指针分析

实现原理 FreeCAD中有两套智能指针&#xff0c;一个是OCC的智能指针handle&#xff0c;另一个是自己定义的智能指针Reference&#xff0c;两种智能指针都是通过引用计数方式管理指针。 1.1 OCC智能指针handle OCC在基础类包中定义了一个模板类handle&#xff0c;该类包含一个私…

Github 2024-06-23开源项目日报 Top10

根据Github Trendings的统计,今日(2024-06-23统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量TypeScript项目3C++项目2JavaScript项目2非开发语言项目2Jupyter Notebook项目1Python项目1Vue项目1Java项目1HTML项目1从零开始构建你喜爱的技…

直流电机三级串电阻启动

直流电动机在工农业生产中拥有广泛的应用&#xff0c;这主要得益于其调速范围广、调速平稳、过载能力强以及启动和制动转矩大的优点。为了降低起动电流和起动转矩&#xff0c;研究者们探索了直流电动机串电阻起动方法。这种方法通过在直流电动机电枢绕组中串入电阻&#xff0c;…