【C进阶】详解预处理指令

news2025/2/24 10:18:04

文章目录

  • 预定义符号
  • #define
    • #define定义标识符
    • #define定义宏
    • #define替换规则
    • #和##
    • 带副作用的宏参数
    • 宏和函数对比
    • #undef
    • 命令行定义
  • 条件编译
  • 文件包含
    • 头文件被包含的方式
    • 嵌套文件包含
  • 其他预处理指令
  • 总结

在这里插入图片描述

预定义符号

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

在这里插入图片描述
MSVC编译器不是完全遵循ANSIC标准
在这里插入图片描述
gcc编译器严格遵循ANSIC标准
在这里插入图片描述


#define

#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__ )  

注意:转义字符是让计算机识别的,计算机看见\Enter不会认为这是分开的两行,因此不存在语法问题

在#define定义标识符时,不要加上;否则可能出问题,看下面这个例子
在这里插入图片描述

#define定义宏

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

定义宏的语法:#define macro_name(pramaent-list) stuff
pramament-list是一个由逗号隔开的参数列表,它们可能出现在stuff中
tips:参数列表和宏名字中间必须紧邻,不能有任何空白,否则宏会被解释为表示符

由于宏是先替换在进行运算,所以定义宏时stuff的每一部分最好用()括起来,防止因为优先级而产生问题请看下面示例
在这里插入图片描述
在这里插入图片描述

将上述num+num改成((num) + (num)),num * num 改成((num) * (num))即可

#define替换规则

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

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

#和##

C语言的字符串具有自动连接的特点

在这里插入图片描述
所以我们可以利用这个特点来实现一些特定的功能,比如将宏的参数插入到字符串中
在这里插入图片描述
总结:#操作符可以在宏中将宏参数转换为字符串

##操作符可以在宏体中完成拼接操作(拼接后的字符一定要是被定义过的)
在这里插入图片描述

将Class和C拼接在一起ClassC,ClassC是已经被定义过的标识符

注意:使用##拼接后的结果一定要是已定义的

带副作用的宏参数

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

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

MAX宏可以证明具有副作用的参数所引起的问题。
在这里插入图片描述
我们只看这个代码会认为x和y分别++了一次,但是由于这是宏,所以x和y其中有一个++了两次,所以这会导致意外的结果

tips:宏的命名往往是全字母大写,这是为了方便区分宏和函数

宏和函数对比

宏和函数的用法是一样的,都是名字紧跟参数列表,我们下面来看看它们的区别
用宏和函数实现求最大值
在这里插入图片描述

可以看见,两者都可以完成求最大值的功能,但是在这串代码中使用宏比函数要合适,原因有下:

  1. 宏是直接替换的,不需要开辟额外的空间,而函数调用需要开辟栈帧
  2. 宏比函数执行速度要快,函数调用时需要为形参压栈、为函数体开辟栈帧、执行函数体、销毁栈帧,执行速度小于宏
  3. 函数的定义必须要指名参数类型,而宏不用所以宏和以比所有类型的数据大小,而函数不行

那是不是所有情况下都应该用宏而不是函数呢?

答案是否定的,宏的缺点有如下几点:

  1. 每次使用宏时,宏的代码会插入到当前代码块中,除非宏的代码块很短,否则会增加代码的长度
  2. 使用宏是不会做类型检查,这有可能是不安全的
  3. 使用宏可能存在优先级的问题
  4. 宏没有办法调试的

宏和函数对比👇
在这里插入图片描述

#undef

这条指令用于移除一个宏定义

#undef NAME
//如果存在一个名字需要重新被定义,那么它应该先移除之前的定义

这是没有移除之前的定义
在这里插入图片描述
这是移除之前的定义
在这里插入图片描述


命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个数组能够大些。)

下面演示Linux环境下的命令行编译👇
在这里插入图片描述

命令行gcc -D ARRAY_SIZE=10 test.c表示在test.c文件中将ARRAY_SIZE定义为10


条件编译

在编译一个程序时,如果我们可以选择某条语句或某组语句进行翻译或者被忽略,常常会显得很方便。只用于调试程序的语句就是一个明显的例子。它们不应该出现在程序的产品版本中,但是你可能并不想把这些语句从源代码中物理删除,因为如果需要一些维护性修改时,你可能需要重新调试这个程序,还需要这些语句。

条件编译(conditionalcompilation)就是用于实现这个目的。使用条件编译,你可以选择代码的-部分是被正常编译还是完全忽略。用于支持条件编译的基本结构是#if指令和与其匹配的#endif指令。下面显示了它最简单的语法形式。

#if constant-expression
	statements
#endif

constant-expression(常量表达式)预处理器进行求值,如果它的值是非0,an那么statements会被编译,如果它的值是0,那么statements不会被编译

条件编译都是预处理指令,是在程序运行前进行的操作,如果此时出现了预处理时无法得知的数据(比如变量),那么程序会报错

注意:枚举常量和全局变量都是预处理之后、运行之前才知道的值

👇常见的条件编译指令👇

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

注意:每一条#if语句一定要有对应的#endif

演示预处理指令#if #elif #else
在这里插入图片描述
演示嵌套指令
在这里插入图片描述


文件包含

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

头文件被包含的方式

  • 本地文件包含
#include "filename"

查找策略:先在源文件目录底下找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件(标准位置是由编译器所决定的)如果找不到就会显示编译错误。

  • 库文件包含
#include <filename>

查找策略:只在标准位置查找该文件,找不到就会显示编译错误.

在这里插入图片描述

所以理论上来说使用本地文件包含既可以引入自定义文件,又可以引入包含库函数的头文件,只是在引入库函数头文件时速度会慢一点

嵌套文件包含

在编写多文件时可能会出现重复包含一个文件的情况,虽说这样的问题不是很大,但我们还是需要尽量避免嵌套文件包含
在这里插入图片描述

comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。

演示重复包含头文件
在这里插入图片描述

我们需要条件编译来避免这个问题
头文件中加上这句话

#ifndef __TEST_H__
#defince __TEST_H__
//头文件内容
...
#endif //__TEST_H__

演示结果
在这里插入图片描述

也可以使用预处理指令#pragma once避免头文件的重复引用
演示结果
在这里插入图片描述
可以看见预处理指令和条件编译的结果是一样的


其他预处理指令

  1. #error text of error message用于编译时生成错误信息
    在这里插入图片描述
  2. #line number "string""string"部分是可选的,将#line这一行的行数更改为number,该源文件名称更改为string
    没有使用#line指令
    在这里插入图片描述
    使用#line指令
    在这里插入图片描述

这条指令最常用于把其他语言的代码转换为 C代码的程序。C 编译器产生的错误信息可以引用源文件而不是翻译程序产生的 C 中间源文件的文件名和行号。

  1. #pragma
    #progma 指令是另一种机制,用于支持因编译器而异的特性。它的语法也是因编译器而异。有些环境可能提供一些#pragma 指令,允许一些编译选项或其他任何方式无法实现的一些处理方式。例如,有些编译器使用#pragma 指令在编译过程中打开或关闭清单显示,或者把汇编代码插入到C程序中。从本质上说,#pragma 是不可移植的。预处理器将忽略它不认识的#pragma 指令,两个不同的编译器可能以两种不同的方式解释同一条#pragma 指令。

总结

  • 程序编译时第一步就是预处理,预处理器支持5个预处理符号
  • #define指令将一个符号和任意的字符序列联系在一起,这个字符序列可以是字面量、也可以是一段程序,如果字符序列太长需换行,则需要在除开最一行的每一行加上\
  • 宏就是一个被定义的序列,它的参数将被替换。当一个宏被调用时,它的每个参数都会被具体的值替换,宏完整表达式的两边应该加上{}
  • #argument会被预处理器看作"argument"argumen1t##argument2会被预处理器会将argument1和argument2粘贴为一个文本
  • 宏的命名习惯是全字母大写
  • 宏与函数相比不会做类型检查,运行速度比函数快,带副作用的宏参数可能会产生意料之外的结果
  • 可以使用#undef name来取消对name的定义
  • 使用条件编译可以从单一的源文件创建不同程序的版本
  • #include指令可以实现头文件的包含。如果文件名位于尖括号之中,那么只会在编译器定义的标准路径下查找;如果文件名位于双引号之中,那么编译器优先到当前文件路径下查找,如果没找到再去标准路径下查找
  • 编写头文件时,记得加上防止重复包含的预处理指令#pragma once或者条件编译指令
  • #error在编译时产生一条错误信息,错误信息是#error后面的文本,#line告诉编译器当前行号,如果加上可选分"string"会告诉编译器源文件名字。#pragma允许编译器提供不标准的处理过程

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

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

相关文章

解码星地一体农机导航:无网作业,极致

星地一体系列农机导航最强大之处是在全国任何地方都拥有信号&#xff0c;即使是在偏远的戈壁滩也能作业&#xff0c;因为星地一体系列导航采用星地融合技术&#xff0c;彻底解决信号问题&#xff0c;通过卫星播发差分信号的服务&#xff0c;在中国&#xff0c;只要可见卫星&…

Windows下如何查看某个端口被谁占用被杀死占用进程

开发时经常遇到端口被占用的情况&#xff0c;这个时候我们就需要找出被占用端口的程序&#xff0c;然后结束它&#xff0c;本文为大家介绍如何查找被占用的端口。 1、打开命令窗口(以管理员身份运行) 开始—->运行—->cmd&#xff0c;或者是 windowR 组合键&#xff0c;…

Win10桌面我的电脑怎么调出来?最简单方法教学

Win10桌面我的电脑怎么调出来&#xff1f;有用户发现自己的电脑桌面没有我的电脑这个程序图标&#xff0c;每次要访问磁盘的时候&#xff0c;开启都非常的麻烦。那么怎么将这个图标设置到桌面显示呢&#xff1f;接下来我们一起来看看以下的解决方法吧。 方法一&#xff1a; 在开…

论文中参考文献的引用

论文中参考文献的引用 写在最前面删除特定格式的数字&#xff08;带小中大括号等等&#xff09;效果如下 设置参考文献格式设置编号格式设置段落格式效果 使用交叉引用去引用这些编号在需要插入参考文南引用的地方使用“交叉引用”快捷键进阶:为“交叉引用”设置快捷键 写在最前…

maven安装及配置IDEA

文章目录 下载下载完成后 解压可以得到如下的目录项 配置环境变量输入下列命令 出现 maven版本表示环境变量配置成功 配置本地仓库、镜像仓库、jdk版本配置本地仓库配置镜像仓库配置jdk版本 IDEA配置maven配置当前项目的maven配置其他项目的maven 下载 https://maven.apache.o…

威联通NAS文件共享 - 搭建SFTP服务并内网穿透实现在外远程访问

文章目录 前言1. 威联通NAS启用SFTP2. 测试局域网访问3. 内网穿透3.1 威联通安装cpolar内网穿透3.2 创建隧道3.3 测试公网远程访问 4. 配置固定公网TCP端口地址4.1 保留一个固定TCP端口地址4.2 配置固定TCP端口地址4.3 测试使用固定TCP端口地址远程连接威联通SFTP 转载自远程内…

Java web 项目 和 java 项目的区别

一、Java Web项目 和 java项目区别 1. Java Web项目是基于Java EE类的&#xff1b;而Java项目是基于Java应用程序的。 2. Java Web项目是网页的编码&#xff0c;像jsp,servlet,struts这类的&#xff0c;而java项目是AWT,SWING这类的编码。 3. Java Web项目中的JAVA文件是tomcat…

【CocosCreator入门】CocosCreator组件 | Layout(布局)组件

Cocos Creator 是一款流行的游戏开发引擎&#xff0c;具有丰富的组件和工具&#xff0c;其中的Layout组件是一种用于实现节点自适应布局的重要组件。它可以根据不同的布局方式&#xff0c;自动调整子节点的位置和大小&#xff0c;从而实现节点的自适应布局。 目录 一、组件介绍…

准备换工作的看过来~

大家好&#xff0c;最近有不少小伙伴在后台留言&#xff0c;得准备面试了&#xff0c;又不知道从何下手&#xff01;为了帮大家节约时间&#xff0c;特意准备了一份面试相关的资料&#xff0c;内容非常的全面&#xff0c;真的可以好好补一补&#xff0c;希望大家在都能拿到理想…

运行时内存数据区之方法区(二)

方法区的演进细节 首先明确&#xff1a;只有HotSpot才有永久代。BEA JRockit、IBMJ9等来说&#xff0c;是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节&#xff0c;不受《]Va虚拟机规范》管束&#xff0c;并不要求统一。Hotspot中方法区的变化&#xff1a; …

Spring核心-IoC控制反转详解 (典藏版)

文章目录 1.IoC容器和Bean介绍2.Spring 中的 IoC 容器2.1 BeanFactory和ApplicationContext概述2.2 BeanFactory2.3 ApplicationContext2.4 BeanFactory vs ApplicationContext2.5 容器的初始化2.6 配置元数据2.6.1 基于XML的容器配置2.6.2 基于注解的容器配置2.6.3 基于Java类…

Junit概述和快速入门

单元测试概述 在程序中&#xff0c;一个单元可以是一个完整的模块&#xff0c;但它通常是一个单独的方法或者程序 在面向对象的编程中&#xff0c;一个单元通常是整个界面&#xff0c;例如类&#xff0c;但可能是单个方法 JUnit是一个java编程语言的单元测试框架 通过先为最…

教育大数据总体解决方案(4)

组件配置 对组件中的项目配置项进行管理&#xff0c;包括节点内容、磁盘空间等等。每一次的配置都以一个配置版本的形式进行保存&#xff0c;用户可选择对应版本的查看对应的配置信息。 测度 对组件内的相关服务指标以图标形式进行状态呈现。可选择相应时间段&#xff0c;查看对…

scala闭包与柯里化

目录 通过闭包实现一个通用的两数相加函数简化柯里化 闭包&#xff1a;如果一个函数&#xff0c;访问到了它的外部&#xff08;局部&#xff09;变量的值&#xff0c;那么这个函数和他所处的环境&#xff0c;称为闭包 通过闭包实现一个通用的两数相加函数 这里内层函数访问了外…

【基础】Kafka -- 主题与分区

Kafka -- 主题与分区 主题的管理创建主题简单创建与查看指定分区副本分配创建指定参数创建 查看主题主题的简单查看带附加功能的查看 修改主题修改分区修改配置 删除主题 主题配置管理配置查看与变更配置查看配置变更 主题端参数 KafkaAdminClient 主题管理基本使用创建主题查看…

【创作赢红包】SQL Server之索引设计

SQL Server之索引设计 一、前言二、索引设计背景知识2.1、索引设计策略包括的任务 三、常规索引设计3.1、数据库注意事项3.2、查询注意事项3.3、列注意事项3.4、索引的特征3.5、索引排序顺序设计指南 总结 一、前言 索引设计不佳和缺少索引是提高数据库和应用程序性能的主要障…

【Spring Data Jpa】原生Jpa的使用

【Spring Data Jpa】原生Jpa的使用 1. Dependency2. Config1.1 persistence.xml1.2 Entity1.3 application.properties 3. Test4. 原生JdbcTemplate 5. Awakening 1. Dependency <dependency><groupId>org.hibernate</groupId><artifactId>hibernate-e…

本地连接github

本地连接github 想要通过github把本地代码同步一下&#xff0c;但是每次换一个电脑都要重新搜索如何配置连接github&#xff0c;趁着这次机会把电脑配置的时候记录一下&#xff0c;到时候找起来方便一点 一、git环境配置 1、首先安装git 找个安装包直接安装就行 2、配置用…

跨越行业壁垒:金融校对软件在跨国金融业务中的应用

随着全球金融市场的融合和跨国金融业务的快速发展&#xff0c;金融专业人士需要处理不同语言、文化和法规背景下的金融文档。金融校对软件在这一领域发挥着至关重要的作用&#xff0c;为跨国金融业务提供有力支持。本文将探讨金融校对软件在跨国金融业务中的应用。 一、跨语言支…

【故障诊断】基于最小熵反卷积、最大相关峰度反卷积和最大二阶环平稳盲反卷积等盲反卷积方法在机械故障诊断中的应用研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…