高质量C++小白教程:2.10-预处理器简介
当你在编译项目时,你可能希望编译器完全按照你编写的方式编译每一个代码文件,当事实并非如此。
相反,在编译之前,每一个.cpp文件都会经历一个预处理的阶段,在此阶段中,称为预处理器的程序对代码文件的文本进行各种更改.
预处理器实际上不会以任何方式修改原始代码文件,预处理器所做的更改要么临时发生在内存中,要么使用临时文件。
从历史角度看,预处理器是与编译器分开的程序,但在现代编译器中,预处理器可以直接构建到编译器本身中。
预处理器所做的大部分工作都是相当无趣的,例如,它删除注释,并确保每个代码文件以换行符结束。然而,预处理器确实有一个非常重要的作用:处理#include
指令;
但预处理器完成对代码文件的处理时,结果称为 翻译单元 该翻译单元随后由编译器编译。
预处理、编译、链接的整个过程称为翻译;
预处理器指令
当预处理器运行时,它会从上到下的扫描代码文件,插在预处理指令。
预处理指令 通常称为指令,是以#符号开头并以换行符结尾的指令。这些指令告诉预处理器执行某些文本创作任务。
请注意,预处理器不理解c++语法,相反,它有自己的指令语法,只能说类似c++语法。
#Include
想必你已经看到了*#include指令的实际应用(通常是 #include )。当您#include*文件时,预处理器会将 #include 指令替换为所包含文件的内容。然后对包含的内容进行预处理(这可能会导致递归地预处理额外的 #include),然后对文件的其余部分进行预处理。
考虑下面的程序:
#include<iostream>int main()
{std::cout << "Hello,cpp\n";return 0;
}
当预处理器在此程序上运行时,预处理器将用名为“iostream”文件的v内容替换#include<iostream>
然后预处理包含的内容和文件的其余部分。
每个翻译单元通常由单个代码 (.cpp) 文件和它 #include 的所有头文件组成(递归应用,因为头文件可以 #include 其他头文件)。
宏定义
#define
指令可用于创建宏,在C++中,宏是定义如何将输入文本替换为输出文本的规则。
宏有两种基本类型:
- 类对象宏
- 类函数宏
类函数宏的行为类似于函数,并具相似的用途,他们的使用通常被认为是不安全的,几乎他们能做的任何事情都可以通过普通的函数完成。
类对象宏的定义可以通过下面两种方式之一:
#define IDENTIFIER
#define IDENTIFIER substitution_text
顶部定义没有替换文本,而底部定义有。因为这些是预处理器指令(而不是语句),所以请注意,这两种形式都不以分号结尾。
宏的标识符使用与普通标识符相同的命名规则:可以使用字母、数字和下划线,不能以数字开头,也不应该以下划线开头。按照约定,宏名称通常全部大写,并用下划线分隔。记住,无须加上分号。
带有替换文本的类对象宏
当预处理器遇到此指令时,宏标识符和替换文本之间会建立关联。所有进一步出现的宏标识符都将会被替换文本替换。
考虑下面的程序:
#include <iostream>
#define MY_NAME "Alex"
int main()
{std::cout << "My name is:" << MY_NAME << '\n';return 0;
}
预处理器将上面的内容转换为一下内容:
// The contents of iostream are inserted here
int main()
{std::cout << "My name is: " << "Alex" << '\n';return 0;
}
运行时,会打印输出My name is: Alex
。
使用带有替换文本的类对象宏(在 C 中)作为将名称分配给文字的方法。这不再是必要的,因为 C++ 中有更好的方法。现在大多数带有替换文本的类似对象的宏只能在遗留代码中看到,我们建议尽可能避免它们。
除非不存在可行的替代方案,否则请避免使用带有替换文本的宏。
没有替换文本的类对象宏
类似对象的宏也可以在没有替换文本的情况下定义。
#define USE_YEN
这种形式的宏的工作方式与你期望的一样:这种形式的宏的工作方式与您可能期望的一样:标识符的大多数后续出现都被删除并被替换为空!
#include <iostream>
#define MY_NAME
int main()
{int x = MY_NAME + 5;std::cout << x << "\n";return 0;
}
上面的宏在代码执行之后会使用空来替换掉对应的位置,导致最终的输出结果为5;这看起来并没有什么作用,确实,这也不是该宏的通常用途,具体会在后面的内容中进行讨论。
条件编译
条件编译预处理器指令允许你指定在什么条件下不去编译某些代码块。有很多不同的条件编译指令,但我们这里只会介绍一些最常用的:
- #ifdef
- #ifndef
- #endif
#ifdef
预处理器指令允许预处理器检查是否已存在指定的标识符,如果是,则会编译#ifdef
和#endif
之间的代码,否则忽略该代码。
考虑下面的程序:
#include <iostream>
#define PRINT_JOE
int main()
{
#ifdef PRINT_JOEstd::cout << "Joe\n";
#endif#ifdef PRINT_BOBstd::cout <<"Bob\n";
#endifreturn 0;
}
因为 PRINT_JOE 已被 #define, std::cout << "Joe\n"
行将被编译。由于 PRINT_BOB 尚未#define,因此std::cout << "Bob\n"
行将被忽略。所以程序将打印Joe
#ifndef与*#ifdef相反,它允许你检查标识符是否尚未*#define 。
#include <iostream>int main()
{
#ifndef PRINT_BOBstd::cout << "Bob\n";
#endifreturn 0;
}
该程序打印“Bob”,因为 PRINT_BOB 从未被*#define*过。
除了#ifdef PRINT_BOB
和#ifndef PRINT_BOB
之外,您还将看到#if defined(PRINT_BOB)
和#if !defined(PRINT_BOB)
。它们的作用相同,但使用稍微更 C++ 风格的语法。
if 0
条件编译的一种更常见的用法是使用#if0来排除代码块的编译:
#include <iostream>int main()
{std::cout << "Joe\n";#if 0 // 此处开始的内容不会被编译std::cout << "Bob\n";std::cout << "Steve\n";
#endif // 编译排除结束点return 0;
}
上面的代码只打印“Joe”,因为“Bob”和“Steve”被*#if 0*预处理器指令排除在编译之外。这提供了一种便捷的方式来“注释掉”包含多行注释的代码(由于多行注释是不可嵌套的,因此无法使用另一个多行注释来注释掉):
#include <iostream>int main()
{std::cout << "Joe\n";#if 0 std::cout << "Bob\n";/* Some* multi-line* comment here*/std::cout << "Steve\n";
#endif return 0;
}
要暂时重新启用#if 0
中包含的代码,您可以将#if 0
更改为#if 1
:
#include <iostream>int main()
{std::cout << "Joe\n";#if 1 // always true, so the following code will be compiledstd::cout << "Bob\n";/* Some* multi-line* comment here*/std::cout << "Steve\n";
#endifreturn 0;
}
其他预处理器中的宏替换
现在你可能想知道,前面提到的没有替换文本的类对象宏的用法:
#define PRINT_JOEint main()
{
#ifdef PRINT_JOEstd::cout << "Joe\n"; // will be compiled since PRINT_JOE is defined
#endifreturn 0;
}
既然我们将PRINT_JOE定义为空,那么预处理器为什么不将*#ifdef PRINT_JOE中的PRINT_JOE*替换为空,并从编译中排除输出语句呢?
在大多数情况下,当你在某些预处理器命令中使用宏标识符时,不会进行宏替换。这意味着预处理器不会将这些宏标识符替换为它们的定义内容。这是因为这些命令本身就是用于控制代码的编译过程,而不是直接进行代码替换。
#ifdef
只是检查宏是否存在: #ifdef
(即“如果已定义”)并不会关心宏的值是什么。它仅仅检查宏是否已经定义。只要你写了 #define PRINT_JOE
(无论是空的还是有值的),宏 PRINT_JOE
就被认为是已定义的,预处理器就会编译 #ifdef
和 #endif
之间的代码。
这就是为什么,即使 PRINT_JOE
是空的,它依然会导致 std::cout << "Joe\n";
被编译的原因。
这一规则至少有一个例外:大多数形式的
#if
和#elif
在预处理器命令中会进行宏替换。
#include <iostream>
#define MAX 100
#if MAX > 50int x = MAX;
#endif
int main()
{std::cout << x << "\n";return 0;
}
指令在编译之前被解析,从上到下逐个文件地解析。
考虑下面程序:
#include <iostream>void foo()
{
#define MY_NAME "Alex"
}int main()
{std::cout << "My name is: " << MY_NAME << '\n';return 0;
}
尽管看起来*#define MY_NAME “Alex”是在函数foo内定义的,但预处理器并不理解函数等 C++ 概念。因此,该程序的行为与在函数foo之前或之后定义#define MY_NAME “Alex” 的程序相同。为了避免混淆,你通常需要在函数外部#define 标识符。
由于 #include 指令将 #include 指令替换为包含文件的内容,因此 #include 可以将指令从包含文件复制到当前文件中。然后将按顺序处理这些指令。
例如,以下示例的行为也与前面的示例相同:
- Alex.h
#define MY_NAME "Alex"
- main.cpp
#include <iostream>
#include "Alex.h"int main()
{std::cout << "My name is: " << MY_NAME << '\n';return 0;
}
在一个文件中定义的指令不会对其他文件产生任何影响(除非它们被 #included 到另一个文件中)。例如:
- function.cpp
#include <iostream>void doSomething()
{
#ifdef PRINTstd::cout << "Printing!\n";
#endif
#ifndef PRINTstd::cout << "Not printing!\n";
#endif
}
- Main.cpp
#include <iostream>void doSomething();#define PRINT
int main()
{doSomething();return 0;
}
尽管 PRINT 是在main.cpp中定义的,但这不会对function.cpp中的任何代码产生任何影响(PRINT 仅是从定义点到 main.cpp 末尾的#define)。
下篇预告:头文件
相关文章:
高质量C++小白教程:2.10-预处理器简介
当你在编译项目时,你可能希望编译器完全按照你编写的方式编译每一个代码文件,当事实并非如此。 相反,在编译之前,每一个.cpp文件都会经历一个预处理的阶段,在此阶段中,称为预处理器的程序对代码文件的文本进行各种更改. 预处理器实际上不会以任何方式修改原始代码文件,预处理…...

一、二极管(模电理论篇)
导论:PN结(结电容)是构成二极管,三极管,场效应管的原理基础 1.二极管特性(单向导电性) 1.1 P型半导体与N型半导体 在单晶体硅(原子核为正四价电子,可以形成四条共价键&…...

JAVA学习笔记_JVM
文章目录 初识jvm内存结构程序计数器(寄存器) 栈问题辨析内存溢出 线程诊断本地方法栈Heap堆内存溢出内存诊断 方法区内存溢出常量池 stringTable直接内存垃圾回收 初识jvm JRE JVM 基础类库 JDK JRE 编译工具 JavaSE JDK IDE工具 JavaEE JDK 应用服务器 IDE工具 jvm是…...
SQL 中复杂 CASE WHEN 嵌套逻辑优化
目标:优化复杂的 CASE WHEN 逻辑,提升 SQL 语句的可读性与执行效率,减少多层嵌套带来的复杂性。 1. CASE WHEN 的常见问题 嵌套过深:多个条件判断嵌套,难以阅读和维护。重复逻辑:相似逻辑在多个分支中重复…...

STM32-笔记34-4G遥控灯
4G接线 一、项目需求 服务器通过4G模块远程遥控开关灯。 二、项目实现 复制项目文件夹38-wifi控制风扇项目 重命名为39-4G遥控点灯 打开项目文件 加载文件 main.c #include "sys.h" #include "delay.h" #include "led.h" #include "ua…...

被催更了,2025元旦源码继续免费送
“时间从来不会停下,它只会匆匆流逝。抓住每一刻,我们才不会辜负自己。” 联系作者免费领💖源💖码。 三联支持:点赞👍收藏⭐️留言📝欢迎留言讨论 更多内容敬请期待。如有需要源码可以联系作者免…...

Java(day1)
注释 在Java中注释分为单行注释、多行注释还有文档注释 //我是单行注释/*我 是多行 注释 *//** 我是文档注释*/ 关键字 关键字:是被Java赋予了特定含义的英文单词 特点:关键字的字母都是c 在常用的代码编辑器中关键字都有特殊的高亮标记 在这个里…...

PDF文件提示-文档无法打印-的解决办法
背景信息 下载了几个签名的PDF文件,想要打印纸质版,结果打印时 Adobe Acrobat Reader 提示【文档无法打印】: 解决办法 网上的方案是使用老版本的PDF阅读器, 因为无法打印只是一个标识而已。 PDF文件不能打印的五种解决方案-zhihu 这些方…...

ubuntu操作系统安装SSH服务
1、更新仓库 sudo apt-get update 2、安装SSH服务 #安装SSH服务 apt-get install openssh-server#启用SSH服务 service ssh start#查看SSH服务运行状态 service ssh status 3、修改SSH配置文件 sudo vi /etc/ssh/sshd_config 4、开启ssh端口 sudo ufw allow ssh 5、重启SSH…...

Beamer-LaTeX学习(教程批注版)【1】
该文档总体由beamer-latex的教程而来,由耳东小白以自身学习路径整理。因其中要点基本按照教程的顺序和结构整理,故而不能称之为完全原创,但也不是翻译,更不是抄袭,是个人自学笔记和批注,其中添加了小白个人…...
IIS设置IP+端口号外网无法访问的解决方案
在IIS将站点设置为IP端口访问,假设端口为8080,设好后,服务器上可以访问,外网无法访问。 通常是端口8080没有加入【入站规则】的缘故,将8080端口加入【入站规则】即可,操作如下: 一、ctrlr 输入 …...
Markdown段落的空行缩进用法
Markdown段落的空行缩进用法。 前言语法详解●正文●段落●不分段换行●缩进 使用场景及实例小结其他文章快来试试吧🥰 Markdown段落,分割线的用法 👈点击这里也可查看 前言 段落由一句或多句连续的文本组成,通过一个或多个空行…...

[paddle] 非线性拟合问题的训练
利用paddlepaddle建立神经网络,模拟有限个数据的非线性拟合 本文仍然考虑 f ( x ) sin ( x ) x f(x)\frac{\sin(x)}{x} f(x)xsin(x) 函数在区间 [-10,10] 上固定数据的拟合。 import paddle import paddle.nn as nn import numpy as np import matplotlib.…...
每日一学——监控工具(Grafana)
2.2 Grafana 2.2.1 数据源配置 嘿,各位小伙伴们!既然我们已经有了Prometheus这位超级英雄来帮我们收集数据,那么接下来我们就需要一位艺术家来把这些枯燥的数据变成美丽的图画。这就是Grafana出场的时候了!Grafana是一款非常流行…...

FreshTomato 路由器固件常见配置以及踩坑记录
本文首发于只抄博客,欢迎点击原文链接了解更多内容。 前言 在上一篇文章《网件 R6400 梅林固件恢复官方固件后刷入 FreshTomato》中,我将网件 R6400 刷入了 FreshTomato 固件,目前已经使用了大半个月了,稳定性比起 380.70_0-X7.9…...

ubuntu 使用samba与windows共享文件[注意权限配置]
在Ubuntu上使用Samba服务与Windows系统共享文件,需要正确配置Samba服务以及相应的权限。以下是详细的步骤: 安装Samba 首先,确保你的Ubuntu系统上安装了Samba服务。 sudo apt update sudo apt install samba配置Samba 安装完成后,…...
使用 httputils + chronicle-bytes 实现金融级 java rpc
1、认识 chronicle-bytes Chronicle-Bytes 是一个类似于 Java NIO 的 ByteBuffer 的高效字节操作库,但它提供了许多扩展功能。这个项目由 OpenHFT 开发并维护,旨在提供高性能、低延迟的数据处理解决方案。Chronicle-Bytes 设计用于高频率交易和其他对性…...
软件工程期末复习(一)
题目复习 单选题 软件产品的核心特性是什么? A. 物质性 B. 逻辑性 C. 可复制性 D. 消耗性 正确答案:B 单选题 在软件开发过程中,哪个环节最接近于传统制造业中的“生产”过程? A. 需求分析 B. 编码 C. 测试 D. 研制(…...

element ui 组件 时间选择器出现转换问题的解决办法
在使用时间选择器添加数据是会出现转换失败问题 vue中代码为: 解决办法: 在el-date-picker中添加 format 和 value-format...

报错:websocket注入为null,已解决!
错误截图 原因分析: WebSocket 在 Spring 框架中的注入问题是由其生命周期与 Spring 容器的作用域不一致引起的。spring管理的都是单例(singleton),和 websocket (多对象)相冲突。如果你的WebSocket 处理类…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...
如何为服务器生成TLS证书
TLS(Transport Layer Security)证书是确保网络通信安全的重要手段,它通过加密技术保护传输的数据不被窃听和篡改。在服务器上配置TLS证书,可以使用户通过HTTPS协议安全地访问您的网站。本文将详细介绍如何在服务器上生成一个TLS证…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...

零基础设计模式——行为型模式 - 责任链模式
第四部分:行为型模式 - 责任链模式 (Chain of Responsibility Pattern) 欢迎来到行为型模式的学习!行为型模式关注对象之间的职责分配、算法封装和对象间的交互。我们将学习的第一个行为型模式是责任链模式。 核心思想:使多个对象都有机会处…...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...

LLMs 系列实操科普(1)
写在前面: 本期内容我们继续 Andrej Karpathy 的《How I use LLMs》讲座内容,原视频时长 ~130 分钟,以实操演示主流的一些 LLMs 的使用,由于涉及到实操,实际上并不适合以文字整理,但还是决定尽量整理一份笔…...