【C语言:编译、预处理详解】
文章目录
- 1.编译
- 2.预处理
- 2.1宏定义
- 2.1.1预定义符号
- 2.1.2#define定义常量
- 2.1.3#define定义宏
- 2.1.4do-while-zero
- 2.1.5宏的注意事项
- 2.1.6宏与函数的对比
- 2.2条件编译
- 2.3文件包含
- 3.offsetoff
- 4.#与##
- 4.1. #号
- 4.2 ##号
1.编译
我们都知道,一个程序如果想运行起来要经过编译、链接然后才能生成.exe的文件。
编译⼜可以分解为三个过程:
- 预处理(有些书也叫预编译)、
- 编译
- 汇编
预处理阶段主要处理那些源文件中以#开始的预编译指令。比如:#include,#define,处理的规则如下:
- 删除所有的注释
(该步骤在宏替换之前)
- 将所有的 #define 删除,并替换所有的宏定义。
- 处理所有的条件编译指令,如: #if、#elif、#else、#ifdef、#ifndef、#endif 。
- 处理#include 预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
- 添加行号和文件名标识,方便后续编译器生成调试信息等。
- 保留所有的#pragma的编译器指令,编译器后续会使⽤。
经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候,可以查看预处理后的.i文件来确认。
编译阶段:就是将预处理后的⽂件进行⼀系列的:词法分析、语法分析、语义分析及优化 ,⽣成相应的汇编代码⽂件。
汇编阶段:汇编器将汇编代码转转变成机器可执⾏的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根据汇编指令和机器指令的对照表⼀⼀的进⾏翻译,也不做指令优化。
2.预处理
C语言提供的预处理功能主要有以下三种:
- 宏定义
- 条件编译
- 文件包含
2.1宏定义
2.1.1预定义符号
C语言设置了⼀些预定义符号,可以直接使⽤,预定义符号也是在预处理期间处理的。
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
2.1.2#define定义常量
- 基本使用
- 巧妙使用
- 续航符
当需要替换的内容过长的时候,可以使用 \ 实现续航。
思考:在#define定义标识符的时候,要不要在最后加上 ;
建议不要加上 ; 这样容易导致问题
如果是加了分号的情况,等替换后,if和else之间就是2条语句,⽽没有⼤括号的时候,if后边只能有⼀条语句 。这⾥会出现语法错误。
2.1.3#define定义宏
#define机制包括了⼀个规定,允许把参数替换到⽂本中,这种实现通常称为宏。
宏的声明方式:
#define name( parament-list ) stuff
其中的 parament-list 是⼀个由逗号隔开的符号表,它们可能出现在stuff中。参数列表的左括号必须与name紧邻
,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。
对于上述案例,我们可以清楚的看到括号在宏中起着至关重要的作用,为了让表达式的结果是我们想要的结果,要尽量给宏带上括号。避免在使⽤宏时由于参数中的操作符或邻近操作符之间不可预料的相互作⽤。
要点:宏在使用的时候,只是原样替换,不进行任何的改变。
下面代码的输出结果是什么呢?
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main()
{int x = 5;int y = 8;int z = MAX(x++, y++);printf("x=%d y=%d z=%d\n", x, y, z);return 0;
}
z = ( (x++) > (y++) ? (x++) : (y++));
x-->5-->6
y-->8-->9-->10
z-->9
所以输出的结果是:x=6 y=10 z=9
2.1.4do-while-zero
假如现在我有一个代码,我就想在if后面跟一条用宏定义多条语句的一条语句,我可以怎么做呢?
我们发现这样是可以实现目的,但是有点别扭,而且我们习惯在一条语句后面加分号,所以在预处理后的代码中,就会对应有两个分号。
那到底能怎么处理呢?
这样是不是就可以满足既是一条语句,还能写分号(没有空语句)的目的了。
2.1.5宏的注意事项
- 源文件的任何地方,宏都可以定义,与是否在函数内外,无关
- 宏替换的规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
- 宏的作用范围
从定义处开始,往后都有效(没有出现
#undef
)
出现#undef
思考下面的代码:
2.1.6宏与函数的对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不⽤函数来完成这个任务?
原因有二:
- 用于调用函数和从函数返回的代码可能⽐实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度⽅⾯更胜⼀筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之这个宏怎可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型⽆关的。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
。
#include<stdio.h>
#include<stdlib.h>
#define MALLOC(num, type) (type*)malloc((num) * sizeof(type))
int main()
{int* p = (int*)malloc(40);int* q = MALLOC(10, int);//(int*)malloc((10) * sizeof(int))return 0;
}
宏和函数的⼀个对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数的代码只出现在一个地方,每次使用函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一点 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号 | 函数的参数只在调用时求值一次,结果传递给函数。表达式的求值更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数与类型有关,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
2.2条件编译
一般情况下,源程序中所有行都参加编译。但有时候希望程序中的一部分内容只在满足一定条件时才进行编译
,也就是对这一部分内容指定编译的条件,这就是“条件编译”。
条件编译的指令有以下几种形式:
- #ifdef #else # endif
它的作用是:若指定的标识符以定义(不管真假)
,则让程序段1参与编译,否则让程序段2参与编译。
#endif必须要有,#else可以没有
- #ifndef #else # endif
它的作用是:若指定的标识符没有定义
,则让程序段1参与编译,否则让程序段2参与编译。这种形式与第一种恰恰相反。
- #if #else # endif
它的作用是:判断指定表达式的结果是否为真,为真就执行程序段1;否则执行程序段2.
多分支的条件编译
利用 #if 实现 #indef / #ifndef
2.3文件包含
所谓文件包含,是指一个源文件可以将另一个源文件的全部内容包含进来,即将另外的文件内容包含到本文件之中,插入到当前位置。
C语言有两种文件包含的方式:
- #include< >
- #include" "
这两种方式有什么区别呢?- - - - -二者的查找策略不同
- #include< > :查找头文件直接
去标准路径下查找
,如果找不到就提⽰编译错误。 - #include" ":
先在源⽂件所在⽬录下查找
,如果该头文件未找到,编译器就像查找库函数头⽂件⼀样,再去标准路径
下查找头⽂件。如果找不到就提示编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ " " 的形式包含?
是的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地⽂文件了。
文件包含就是将指定文件内容包含到本文件之中,插入到当前位置。那如果我将一个头文件重复包含会怎么样呢?
包含几次,就会将所包含文件的代码复制几次,如果工程比较大,有公共使用的头文件,被⼤家都能使用,又不做任何的处理,那么后果真的不堪设想。
如何解决头⽂件被重复引⼊的问题?
答案:条件编译
#ifndef __TEST_H__
#define __TEST_H__
//头⽂件的内容
#endif //__TEST_H__
或
#pragma once
3.offsetoff
这个其实是一个宏,它的作用就是返回结构体/共用体成员,在结构体/共用体中的偏移量。
既然我们已经学习了宏,那就来模拟实现以下吧!
我们都知道,偏移量是相较于起始位置的地址,那么是不是就可以将这个起始位置想象为0。
将0强转为一个结构体类型的指针,然后去访问结构体的成员,
取出该成员的地址,这时候该成员的地址是不是就是相较于0地址处的偏移量了呢?
代码如下:
#define MY_OFFSETOF(type,member_name) (int)&(((type*)0)->member_name)
struct stu
{char c1;int i;char c2;
};
int main()
{struct stu s = { 0 };printf("%d\n", MY_OFFSETOF(struct stu, c1));printf("%d\n", MY_OFFSETOF(struct stu, i));printf("%d\n", MY_OFFSETOF(struct stu, c2));return 0;
}
4.#与##
4.1. #号
平常当我们有⼀个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 是不是会这样写:
这样写是不是不能是字符串中的变量名联动起来,一个变量就得写一条printf,挺麻烦的。
下面了解一下#:
#运算符是将宏的⼀个参数转换为字符串字⾯量
。它仅允许出现在带参数的宏的替换列表中。#运算符所执⾏的操作可以理解为”字符串化“。
有了#就可以这样写:
4.2 ##号
##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符,## 被称为记号粘合。
这样的连接必须产生⼀个合法的标识符,否则其结果就是未定义的。
右侧为预处理后的结果:
相关文章:

【C语言:编译、预处理详解】
文章目录 1.编译2.预处理2.1宏定义2.1.1预定义符号2.1.2#define定义常量2.1.3#define定义宏2.1.4do-while-zero2.1.5宏的注意事项2.1.6宏与函数的对比 2.2条件编译2.3文件包含 3.offsetoff4.#与##4.1. #号4.2 ##号 1.编译 我们都知道,一个程序如果想运行起来要经过…...

【宇宙猜想】AR文创入驻今日美术馆、北京天文馆等众多展馆,在AR互动中感受科技魅力!
近日,由「宇宙猜想」推出的AR系列文创产品先后入驻今日美术馆、北京天文馆、国家自然博物馆、上海天文馆、国家海洋馆、中华手工展馆等各大馆场并与其展开相关合作。 「宇宙猜想」致力于创造虚拟空间价值,用AR技术与文创产品碰撞出新的火花,为…...
前端面试题html
HTML DOCTYPE有什么作用? DOCTYPE是一种指示浏览器以何种HTML或XHTML规范来解析文档的声明。它能够告知浏览器网页文档使用的标记语言的类型以及版本,从而确保浏览器能够正确地展示网页内容。DOCTYPE声明通常位于HTML文档的开头,是HTML文档…...

AOSP源码下载方法,解决repo sync错误:android-13.0.0_r82
篇头 最近写文章,反复多次折腾AOSP代码,因通过网络repo sync aosp代码,能一次顺利下载的概率很低,以前就经常遇到,但从未总结,导致自己也要回头检索方法,所以觉得可以总结一下,涉及…...

TCP:IP原理
TCP/IP 原理 TCP/IP 协议不是 TCP 和 IP 这两个协议的合称,而是指因特网整个 TCP/IP 协议族。从协议分层模型方面来讲,TCP/IP 由四个层次组成:网络接口层、网络层、传输层、应用层。 网络访问层(Network Access Layer) 网络访问层(Network …...
Java 中 Lambda 表达式的使用
目录 一、Lambda 表达式的概念 二、Lambda 表达式的语法格式 三、Lambda 表达式的案例使用 1、使用 Lambda 来实现启动线程 2、使用 Lambda 表达式实现集合的排序 3、使用 Lambda 表达式实现文件夹下的文件查找 一、Lambda 表达式的概念 Lambda 表达式是特殊的匿名内部类…...

【IO】IO模型与零拷贝
前言: 正在运行的程序其实就是系统中的一个进程,操作系统会为每一个进程分配内存空间,而内存空间分为两部分,一部分是用户空间,这是用户进程访问的内存区域;另一部分是内核空间,是操作系统内核访…...
鸿蒙 - arkTs:状态管理
状态 State: 在声明式UI中,以状态驱动视图更新 状态(State):指驱动视图更新的数据(被装饰器标记的变量)视图(View):基于UI描述渲染得到的用户界面 使用示例…...

YOLOv5-Lite 树莓派4B 15帧教程
【前言】 由于v5Lite仓库遗漏了不少历史问题,最大的问题是毕业后卷起来了,找不到时间更新。 上面是这篇博客的背景,那么先说下结论,使用 v5lite-e 模型,在 树莓派4B(4G内存) 上,有三…...

2014年第三届数学建模国际赛小美赛A题吹口哨解题全过程文档及程序
2014年第三届数学建模国际赛小美赛 A题 吹口哨 原题再现: 哨子是一种小装置,当空气被迫通过开口时会发出声音。哨声的巨大而引人注目,使其对警察和体育裁判来说至关重要。当救生员、迷路的露营者或犯罪受害者使用它们时,它们可以…...

设计模式-注册模式
设计模式专栏 模式介绍模式特点应用场景注册模式和单例模式的区别代码示例Java实现注册模式Python实现注册模式 注册模式在spring中的应用 模式介绍 注册模式是一种设计模式,也称为注册树或注册器模式。这种模式将类的实例化和创建分离开来,避免在应用程…...
css 美化滚动条样式
ChatgGPT4.0国内站点: 海鲸AI-支持GPT(3.5/4.0),文件分析,AI绘图 在CSS中,你可以使用伪元素::-webkit-scrollbar以及相关的伪元素来为Webkit浏览器(如Chrome和Safari)自定义滚动条的样式。以下是一些基本的CSS规则&am…...

视频压缩不影响画质简单方法,一分钟搞定!
很多朋友在处理视频的时候都会遇到视频过大的问题,想要压缩视频的同时不影响画质,简单的方法有两种。一种是用专业的压缩软件,在压缩的时候设置一个合适的压缩比例,压缩大小的同时保持清晰度,也能提高压缩率࿰…...

Zookeeper的使用场景
统一命名服务 利用ZooKeeper节点的树形分层结构和子节点的顺序维护能力,来为分布式系统中的资源命名。 例:分布式节点命名 分布式消息队列 1.在Zookeeper中创建一个持久节点,用作队列的根节点。队列元素的节点放在这个根节点下。 2.入队:…...
Java 面试题集锦记录
Java 面试题集锦记录 一1. SpringBoot、SpringCloud区别2. SpringCloud怎么保证服务间通信?3. Spring怎么保持高可用性、稳定性?4. 负载均衡5. [Rabbitmq](https://blog.csdn.net/qq_40985985/article/details/128013229) 怎么避免重复消费,[…...

【自然语言处理】第2部分:识别文本中的个人身份信息
自我介绍 做一个简单介绍,酒架年近48 ,有20多年IT工作经历,目前在一家500强做企业架构.因为工作需要,另外也因为兴趣涉猎比较广,为了自己学习建立了三个博客,分别是【全球IT瞭望】,【…...
C#中的.NET与.NET Framework区别
C#是一种编程语言,而.NET是一个开发平台。在.NET生态系统中,有两个相关但不同的概念:.NET和.NET Framework。 .NET Framework 发布时间: .NET Framework是最早引入的,它于2002年首次发布。它是一个用于构建Windows应…...

详解Keras3.0 Layer API: LSTM layer
LSTM layer 用于实现长短时记忆网络,它的主要作用是对序列数据进行建模和预测。 遗忘门(Forget Gate):根据当前输入和上一个时间步的隐藏状态,计算遗忘门的值。遗忘门的作用是控制哪些信息应该被遗忘,哪些…...

Vue和React的运行时,校验引入包的上下文差异
背景 系统使用 webpack 5 模块联邦实现微前端,有关如何实现跨应用的代码共享,可参考 如何优雅的实现跨应用的代码共享 里的第三大点。 总之,这里是其他应用使用了某个应用共享出来的reg文件,引入方式为: import REG …...

C语言中函数调用和嵌套
函数是C语言的基本组成元素 函数调用 根据函数在程序中出现的位置有下列三种函数调用方式: 将函数作为表达式调用 将函数作为表达式调用时,函数的返回值参与表达式的运算,此时要求函数必须有返回值 int retmax(100,150); 将函数作为语句…...
Python爬虫实战:研究MechanicalSoup库相关技术
一、MechanicalSoup 库概述 1.1 库简介 MechanicalSoup 是一个 Python 库,专为自动化交互网站而设计。它结合了 requests 的 HTTP 请求能力和 BeautifulSoup 的 HTML 解析能力,提供了直观的 API,让我们可以像人类用户一样浏览网页、填写表单和提交请求。 1.2 主要功能特点…...
挑战杯推荐项目
“人工智能”创意赛 - 智能艺术创作助手:借助大模型技术,开发能根据用户输入的主题、风格等要求,生成绘画、音乐、文学作品等多种形式艺术创作灵感或初稿的应用,帮助艺术家和创意爱好者激发创意、提高创作效率。 - 个性化梦境…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
深入浅出:JavaScript 中的 `window.crypto.getRandomValues()` 方法
深入浅出:JavaScript 中的 window.crypto.getRandomValues() 方法 在现代 Web 开发中,随机数的生成看似简单,却隐藏着许多玄机。无论是生成密码、加密密钥,还是创建安全令牌,随机数的质量直接关系到系统的安全性。Jav…...

【JVM】- 内存结构
引言 JVM:Java Virtual Machine 定义:Java虚拟机,Java二进制字节码的运行环境好处: 一次编写,到处运行自动内存管理,垃圾回收的功能数组下标越界检查(会抛异常,不会覆盖到其他代码…...
linux 错误码总结
1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
python爬虫:Newspaper3k 的详细使用(好用的新闻网站文章抓取和解析的Python库)
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 一、Newspaper3k 概述1.1 Newspaper3k 介绍1.2 主要功能1.3 典型应用场景1.4 安装二、基本用法2.2 提取单篇文章的内容2.2 处理多篇文档三、高级选项3.1 自定义配置3.2 分析文章情感四、实战案例4.1 构建新闻摘要聚合器…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...