【C++】4、Preprocessor 预处理:条件编译、源文件包含、宏替换、重定义行号、错误信息、编译器预留指令

news2024/11/17 22:21:49

文章目录

  • 一、概述
  • 二、格式
    • 2.1 条件编译
    • 2.2 源文件包含
    • 2.3 宏替换
      • 2.3.1 语法
      • 2.3.2 C++标准内置的预定义宏
    • 2.4 重定义行号和文件名
    • 2.5 错误信息
    • 2.6 编译器预留指令
  • 三、应用场景

C++的 Build 可分为4个步骤:预处理、编译、汇编、链接。

  • 预处理就是本文要详细说的宏替换、头文件包含等
  • 编译是指对预处理后的代码进行语法和语义分析,最终得到汇编代码或接近汇编的其他中间代码
  • 汇编是指将上一步得到的汇编或中间代码转换为目标机器的二进制指令,一般是每个源文件生成一个二进制文件(VS是.obj,GCC是.o)
  • 链接是对上一步得到的多个二进制文件“链接”成可执行文件或库文件等。

一、概述

Preprocessor(预处理)包含 4 个阶段:

  • Trigraph replacement(字符映射):将系统相关的字符映射到C++标准定义的相应字符,但语义不变,如对不同操作系统上的不同的换行符统一换成规定字符(设为newline);
  • Line splicing(续行符处理):对于“\”紧跟newline的,删去“\”和newline,该过程只进行1遍(如果是“\”后有两个换行只会删去一个“\”);
  • Tokenization(字串分割):源代码作为一个串被分为如下串(Token)的连接:注释、whitespace、preprocessing tokens(标示符等这时都是preprocessing tokens,因为此时不知道谁是标示符,经过下一步之后,真正的预处理符会被处理);
  • 执行Preprocessor:对#include指令做递归进行该1-4步,此步骤时候源代码中不再含有任何预处理语句(#开头的那些)。

预处理之后的效果是:条件编译的测试不通过部分被删去、宏被替换、头文件被插入等。

预处理是以 translation unit 为单位进行的:一个 translation unit 就是一个源文件连同由#include包含(或间接包含)的所有文本文件的全体。一般编译器对一个 translation unit 生成一个二进制文件(VS是.obj,GCC是.o)。

二、格式

Preprocessor指令一般格式如下:

# preprocessing_instruction [arguments] newline

指令如下:(除了以上所列的Preprocessor指令外,其他指令是不被C++标准支持的,尽管有些编译器实现了自己的预处理指令。很据“可移植性比效率更重要”的原则,应该尽量仅适用C++标准的Preprocessor)

  • Null,一个 # 后跟 newline ,不产生任何影响,类似于空语句;
  • 条件编译,由 #if, #ifdef, #ifndef, #else, #elif, #endif 定义;
  • 源文件包含,由 #include 定义;
  • 宏替换,由 #define, #undef, #, ## 定义;
  • 重定义行号和文件名,由 #line 定义;
  • 错误信息,由 #error 定义;
  • 编译器预留指令,由 #pragma 定义。

2.1 条件编译

条件编译由 #if, #ifdef, #ifndef 开始,后跟 0-n 个 #elif ,后跟 0-1 个 #else ,后跟 #endif 。

#include <iostream>

#define ABCD 2

int main() {
#ifdef ABCD
    std::cout << "1: yes\n";
#else
    std::cout<<"2:no\n";
#endif

#ifndef ABCD
    std::cout << "2: no1\n";
#elif ABCD == 2
    std::cout << "2: yes\n";
#else
    std::cout << "2: no2\n";
#endif

#if !defined(DCBA) && (ABCD < 2 * 4 - 3) // todo: 不知道为什么ABCD < 2 * 4 - 3成立
    std::cout << "3: yes\n";
# endif
    std::cin.get();
    return 0;
}

// code result:
1: yes
2: yes
3: yes

条件编译被大量用于依赖于系统又需要跨平台的代码,这些代码一般会通过检测某些宏定义来识别操作系统、处理器架构、编译器,进而条件编译不同代码,以和系统兼容。

PS:但话又说回来,C++标准的最大价值就是让所有版本的C++实现都一致,所以除非调用系统功能,否则不应该对系统做出任何假设。

2.2 源文件包含

源文件包含指示将某个文件的内容插入到该#include处,这里“某个文件”将被递归预处理(1-4步,见第1节)。文件包含的3种格式为:

  • #include<filename>,在标准包含目录查找filename(一般C++标准库头文件在此)
  • #include"filename",先查找被处理源文件所在目录,如果没找到再找标准包含目录
  • #include pp-tokens其,其中pp-tokens须是定义为或"filename"的宏,否则结果未知。注意filename可以是任何文本文件,而不必是.h、.hpp等后缀文件,例如可以是.c或.cpp文本文件(所以标题是“源文件包含”而非“头文件包含”)。
#ifndef B_CPP
#define B_CPP
int b = 999;
#endif // B_CPP
// file: a.cpp
#include<iostream> // 在标准库目录找
#include"b.cpp"// 先在源文件所在目录找, 再再标准库找

#define CMATH <cmath> // 如下两行效果即为 #include<cmath>, 这是一个标准库
#include CMATH // 同上

int main() {
    std::cout << b << '\n'; // 是在b.cpp定义的
    std::cout << std::log10(10.0) << '\n';
    std::cin.get();
    return 0;
}

// code result: 注意将a.cpp和b.cpp放在同一文件夹,只编译a.cpp(命令为g++ a.cpp && ./a.out)
999
1

2.3 宏替换

2.3.1 语法

#define 定义宏替换,#define 之后的宏都将被替换为宏的定义,直到用 #undef 解除该宏的定义。

宏定义分为不带参数的常量宏(Object-like macros)和带参数的函数宏(Function-like macros)。其格式如下:

#define identifier replacement-list
#define identifier( parameters ) replacement-list
#define identifier( parameters, ... ) replacement-list
#define identifier( ... ) replacement-list
#undef identifier

对于有参数的函数宏,在replacement-list中,“#”置于identifier面前表示将identifier变成字符串字面值,“##”连接,下面的例子来自cppreference.com:

#include<iostream>

// make function factory
#define FUNCTION(name, a) int fun_##name() {return a;} // “#”置于identifier面前表示将identifier变成字符串字面值,“##”连接
FUNCTION(abcd, 12); // 定义func_abc()函数其无参数, 返回 12
FUNCTION(fff, 2);// 定义func_fff()函数其无参数, 返回 2
FUNCTION(kkk, 23);// 定义func_kkk()函数其无参数, 返回 23
#undef FUNCTION

#define FUNCTION 34 // 之前已定义过的 fun_abcd()、fun_fff()、fun_kkk() 已定义好, 现在可以重新宏定义了

#define OUTPUT(a) std::cout << #a << '\n'

int main() {
    std::cout << "abcd: " << fun_abcd() << std::endl; // use function factory
    std::cout << "fff: " << fun_fff() << std::endl;
    std::cout << "kkk: " << fun_kkk() << std::endl;
    std::cout << FUNCTION << std::endl; // 新的宏定义是34
    OUTPUT(million);
    std::cin.get();
    return 0;
}

// code result:
abcd: 12
fff: 2
kkk: 23
34
million

可变参数宏是C++11新增部分(来自C99),使用时用__VA_ARGS__指代参数“…”,一个摘自C++标准2011的例子如下(标准举的例子就是不一样啊):

#include<iostream>
#define debug(...) fprintf(stderr, __VA_ARGS__) // __VA_ARGS__指代参数“...”
#define showlist(...) puts(#__VA_ARGS__)
#define report(test, ...) ((test) ? puts(#test) : printf(__VA_ARGS__))
int main() {
    int x = 1;
    int y = 2;
    debug("Flag");
    debug("X = %d\n", x);
    showlist(The first, second, and third items.);
    report(x>y, "x is %d but y is %d", x, y);
}

// 这段代码在预处理后产生如下代码:
fprintf(stderr, "Flag");
fprintf(stderr, "X = %d\n", x);
puts("The first, second, and third items.");
((x>y) ? puts("x>y") : printf("x is %d but y is %d", x, y));

// code result:
FlagX = 1
The first, second, and third items.
x is 1 but y is 2

2.3.2 C++标准内置的预定义宏

// 其中上面5个宏一定会被定义,下面从__STDC__开始的宏不一定被定义,这些预定义宏不能被 #undef
// 这些宏经常用于输出调试信息。预定义宏一般以“__”作为前缀,所以用户自定义宏应该避开“__”开头
// 现代的C++程序设计原则不推荐适用宏定义常量或函数宏,应该尽量少的使用 #define ,如果可能,用 const 变量或 inline 函数代替
__cplusplus: 在C++98中定义为199711L,C++11中定义为201103L
__LINE__: 指示所在的源代码行数(从1开始),十进制常数
__FILE__: 指示源文件名,字符串字面值
__DATE__: 处理时的日期,字符串字面值,格式“Mmm dd yyyy”
__TIME__: 处理时的时刻,字符串字面值,格式“hh:mm:ss”

__STDC__: 指示是否符合Standard C,可能不被定义
__STDC_HOSTED__: 若是Hosted Implementation,定义为1,否则为0
__STDC_MB_MIGHT_NEQ_WC__: 见ISO/IEC 14882:2011
__STDC_VERSION__: 见ISO/IEC 14882:2011
__STDC_ISO_10646__: 见ISO/IEC 14882:2011
__STDCPP_STRICT_POINTER_SAFETY__: 见ISO/IEC 14882:2011
__STDCPP_THREADS__: 见ISO/IEC 14882:2011

// 示例如下:
#include <iostream>
int main() {
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
    PRINT(__cplusplus);
    PRINT(__LINE__);
    PRINT(__FILE__);
    PRINT(__DATE__);
    PRINT(__TIME__);
#ifdef __STDC__
    PRINT(__STDC__);
#endif
    std::cin.get();
    return 0;
}
// code result:
__cplusplus: 201703
__LINE__: 6
__FILE__: /cppcodes/run/a.cpp
__DATE__: Aug 26 2023
__TIME__: 21:11:36
__STDC__: 1

2.4 重定义行号和文件名

#line number ["filename"] 的下一行源代码开始, __LINE__ 被重定义为从 number 开始,__FILE__ 被重定义 "filename"(可选),一个例子如下:

#include <iostream>
int main() {
#define PRINT(arg) std::cout << #arg": " << arg << '\n'
#line 999 "WO"
    PRINT(__LINE__);
    PRINT(__FILE__);
    std::cin.get();
    return 0;
}

// code result:
__LINE__: 999
__FILE__: WO

2.5 错误信息

#error [message] 指示编译器报告错误,一般用于系统相关代码,例如检测操作系统类型,用条件编译里 #error 报告错误。例子如下:

int main(){
#error "w"
    return 0;
#error
}

// code result:
/cppcodes/run/a.cpp:2:2: error: "w"
#error "w"
 ^
/cppcodes/run/a.cpp:4:2: error:
#error
 ^
2 errors generated.

2.6 编译器预留指令

#pragma预处理指令是C++标准给特定C++实现预留的标准,所以在不同的编译器上 #pragma 的参数及意义可能不同,如:

  • VC++2010 提供 #pragma once 来指示源文件只被处理一遍,参见MSDN相关条目
  • OpenMP 作为一个共享内存并行编程模型,使用 #pragma omp 指导语句,详见:OpenMP共享内存并行编程详解。
  • GCC 的 #pragma 指令参见GCC文档相关条目。

三、应用场景

预处理的常见使用有:

  • Include guard,见wikipedia条目,该技术用来保证头文件仅被同一文件包含一次(准确地说,头文件内容在一个 translation unit 中仅出现一次),以防止违反C++的“一次定义”原则;
  • 用 #ifdef 和特殊宏识别操作系统、处理器架构、编译器,条件编译,进而实现针对特定平台的功能,多用于可移植性代码;
  • 定义函数宏,以简化代码,或是方便修改某些配置;
  • 用 #pragma 设定和实现相关的配置(见上一节最后给出的链接)。
    sourceforge.net上有一个项目,是关于用宏检测操作系统、处理器架构、编译器(请点链接或见参考文献)。下面是一个例子(来自这里):

关于用宏检测的定义如下链接,代码示例如下:

  • 操作系统宏
  • 处理器架构宏
  • 编译器宏
#ifdef _WIN64
   //define something for Windows (64-bit)
#elif _WIN32
   //define something for Windows (32-bit)
#elif __APPLE__
    #include "TargetConditionals.h"
    #if TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR
        // define something for simulator   
    #elif TARGET_OS_IPHONE
        // define something for iphone  
    #else
        #define TARGET_OS_OSX 1
        // define something for OSX
    #endif
#elif __linux
    // linux
#elif __unix // all unices not caught above
    // Unix
#elif __posix
    // POSIX
#endif

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

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

相关文章

跳跃游戏【贪心算法】

跳跃游戏 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。 判断你是否能够到达最后一个下标&#xff0c;如果可以&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。在这里插入图片…

Windows 10【压缩卷】操作报错【无法将卷压缩到超出任何不可移动的文件所在的点】的解决方法

目录 一、背景 二、原因 三、解决方法 3.1 Windows自带的碎片清理工具 3.1.1 操作步骤 3.1.2 操作结果 3.2 MyDefrag工具清理磁盘碎片 3.2.1 操作步骤 3.2.2 操作结果 3.3 Windows自带的事件查看器 3.3.1 操作步骤 3.3.2 操作结果 3.4 关闭虚拟内存并删除虚拟内存…

docker harbor私有库

目录 一.Harbor介绍 二.Harbor的特性 三.Harbor的构成 四.Harbor构建Docker私有仓库 4.2在Server主机上部署Harbor服务&#xff08;192.168.158.25&#xff09; 4.2.1 这时候这边就可以去查看192.168.158.25网页 4.3此时可真机访问serverIP 4.4通过127.0.0.1来登陆和推送镜…

一分钟学会用pygame制作棋盘背景

一分钟一个Pygame案例&#xff0c;这一集我们来学习一下如何生成一个视频中的棋盘背景效果&#xff0c;非常非常简单。 视频教程链接&#xff1a;https://www.bilibili.com/video/BV17G411d7Ah/ 当然我们这里是用来做页面的背景&#xff0c;你也可以拿来做别的效果&#xff0…

【随笔】- 程序员的40岁后健身计划

【随笔】- 40岁后程序员的健身计划 文章目录 【随笔】- 40岁后程序员的健身计划一、树立健身信心&#xff0c;制订坚持计划二、挑选让你舒适的方式三、调整速度&#xff0c;以间歇式训练为主四、刚开始锻炼&#xff0c;别求太快五、增加力量、柔韧性和平衡练习六、运动多样化七…

Nginx正向代理与反向代理及Minio反向代理实操(三)

本文是对: Nginx安装及Minio集群反向动态代理配置(二) 文的进一步完善: 多台服务器间免密登录|免密拷贝 Cenos7 搭建Minio集群部署服务器(一) Cenos7 搭建Minio集群Nginx统一访问入口|反向动态代理(二) Spring Boot 与Minio整合实现文件上传与下载(三) CentOS7的journa…

【80天学习完《深入理解计算机系统》】第十一天 3.4 跳转指令

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

清华大学龙明盛:人工智能工程化软件研发

今年以来&#xff0c;一系列通用大模型和领域大模型相继发布&#xff0c;将人工智能&#xff08;AI&#xff09;推向一个崭新的高度。AI开始得到人们开始普遍关注、并渗透至各行各业&#xff0c;引发各行各业发生巨大变化&#xff0c;软件研发行业自然也随之发生巨变。 基于对大…

6.1英寸屏幕?新款iPhone15系列在印度获得认证

据报道&#xff0c;A3904和A3090是最新的iPhone 15系列机型在印度获得监管机构BIS的认证。虽然目前BIS认证文件中没有包含相关的规格和参数信息&#xff0c;但可以确认这两款机型是iPhone 15系列中的一部分。 根据之前的报道&#xff0c;iPhone 15和iPhone 15 Pro将配备6.1英寸…

数据结构(Java实现)-java对象的比较

元素的比较 基本类型的比较 在Java中&#xff0c;基本类型的对象可以直接比较大小。 对象比较的问题 Java中引用类型的变量不能直接按照 > 或者 < 方式进行比较 默认情况下调用的就是equal方法&#xff0c;但是该方法的比较规则是&#xff1a;没有比较引用变量引用对象的…

深度学习7:生成对抗网络 – Generative Adversarial Networks | GAN

生成对抗网络 – GAN 是最近2年很热门的一种无监督算法&#xff0c;他能生成出非常逼真的照片&#xff0c;图像甚至视频。我们手机里的照片处理软件中就会使用到它。 目录 生成对抗网络 GAN 的基本原理 大白话版本 非大白话版本 第一阶段&#xff1a;固定「判别器D」&#x…

数据结构(Java实现)LinkedList与链表(下)

** ** 结论 让一个指针从链表起始位置开始遍历链表&#xff0c;同时让一个指针从判环时相遇点的位置开始绕环运行&#xff0c;两个指针都是每次均走一步&#xff0c;最终肯定会在入口点的位置相遇。 LinkedList的模拟实现 单个节点的实现 尾插 运行结果如下&#xff1a; 也…

React 使用 useRef() 获取循环中所有子组件实例

目录 背景思考实现完整代码&#xff1a;成功运行后的界面如下&#xff1a; 知识点总结uesRef() 作对象处理useImperativeHandle() 父组件操作引入子组件的内部方法最后 背景 之前项目中使用了antd pro 中的 可编辑表格 (EditableProTable)&#xff0c;在页面中表格要经过多层遍…

不用循环数组,js+html实现贪吃蛇

功能描述&#xff1a;每走10步随机改变一个方方向&#xff0c;当键盘按下方向键 w,s,a,d时&#xff0c;使用键盘方向控制蛇的移动&#xff0c;蛇头每撞到一次自身时改变屏幕颜色&#xff0c;蛇头碰到边界时从另一边回来。 实现思路&#xff1a;用个30大小的数组存放每个结点&a…

基于Java+SpringBoot+Vue前后端分离纺织品企业财务管理系统设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

nodejs+vue+JavaScript学科竞赛管理系统e41wj

本文拟采用nodejs技术和vue.js搭建系统框架&#xff0c;后台使用MySQL数据库进行信息管理&#xff0c;设计开发的学科竞赛全流程管理系统。通过调研和分析&#xff0c;系统拥有管理员、教师和学生三个角色&#xff0c;主要具备登录注册、个人信息修改、教师管理、学生管理、竞赛…

【Unity小技巧】unity2d平台制作一根三七调的鱼竿效果(附git源码)

文章目录 前言素材开始源码参考完结 前言 今天我们做一个2d鱼竿的效果&#xff0c;先看一下效果成品效果 素材 鱼竿 开始 首先创建一个2D URP项目 &#xff0c;创建一个空物体作为鱼竿&#xff0c;并创建两个子物体作为开始和结束点 配置层级如下 鱼竿和鱼线加Line Ren…

[Open-source tool] 可搭配PHP和SQL的表單開源工具_Form tools(1):簡介和建置

Form tools是一套可搭配PHP和SQL的表單開源工具&#xff0c;可讓開發者靈活運用&#xff0c;同時其有數個表單模板和應用模組供挑選&#xff0c;方便且彈性。Form tools已開發超過20年&#xff0c;為不同領域的需求者或開發者提供一個自由和開放的平台&#xff0c;使他們可建構…

SQL注入漏洞复现:探索不同类型的注入攻击方法

这篇文章旨在用于网络安全学习&#xff0c;请勿进行任何非法行为&#xff0c;否则后果自负。 准备环境 sqlilabs靶场 安装&#xff1a;详细安装sqlmap详细教程_sqlmap安装教程_mingzhi61的博客-CSDN博客 一、基于错误的注入 简介 基于错误的注入&#xff08;Error-based I…

Acrobat Pro DC软件安装包分享(附安装教程)

目录 一、软件简介 二、软件下载 一、软件简介 Acrobat Pro DC是Adobe公司开发的一款PDF编辑软件&#xff0c;简称为DC&#xff0c;是Acrobat系列软件中的一款&#xff0c;是行业内的标准工具&#xff0c;被广泛应用于文档处理、电子合同、PDF表单等领域。 Acrobat Pro DC软…