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

文章目录

  • 一、概述
  • 二、格式
    • 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.mfbz.cn/a/90895.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

服务注册中心 Eureka

服务注册中心 Eureka Spring Cloud Eureka 是 Netflix 公司开发的注册发现组件&#xff0c;本身是一个基于 REST 的服务。提供注册与发现&#xff0c;同时还提供了负载均衡、故障转移等能力。 Eureka 有 3 个角色 服务中心&#xff08;Eureka Server&#xff09;&#xff1a;…

Java学数据结构(3)——树Tree B树 红黑树 Java标准库中的集合Set与映射Map 使用多个映射Map的案例

目录 引出B树插入insert删除remove 红黑树(red black tree)自底向上的插入自顶向下红黑树自顶向下的删除 标准库中的集合Set与映射Map关于Set接口关于Map接口TreeSet类和TreeMap类的实现使用多个映射Map&#xff1a;一个词典的案例方案一&#xff1a;使用一个Map对象方案二&…

[管理与领导-49]:IT基层管理者 - 8项核心技能 - 4 - 团队激励

目录 前言&#xff1a; 一、什么是团队激励 二、为什么需要激励 三、激励的误区 3.1 常见误区 3.2 以下是一些常见的激励错误做法&#xff1a; 四、如何正确地激励 五、关于激励的一些理念 六、常见障碍 前言&#xff1a; 管理者存在的价值就是制定目标&#xff0c;即…

CV:边缘检测的算法包含 Prewitt、Sobel、Laplacian 和 Canny。

目录 1. 边缘检测&#xff08;Prewitt&#xff09; 2. 边缘检测&#xff08;Sobel&#xff09; 3. 边缘检测&#xff08;Laplacian&#xff09; 3. 边缘检测&#xff08;Canny&#xff09; 边缘检测的算法包含 Prewitt、Sobel、Laplacian 和 Canny。 人在图像识别上具有难…

IDEA 配置注释模板

目录 一、配置类模板注释 二、配置方法注释 一、配置类模板注释 打开IDEA&#xff0c;打开settings(快捷键&#xff1a;Ctrl Alt s)&#xff0c;选择Editor&#xff0c;找到File and Code Templates&#xff0c;设置需要配置注释的文件类型&#xff0c;如下图所示&#xf…

Redis 为什么这么快?

前言 作为一名后端软件工程师&#xff0c;工作中你肯定和 Redis 打过交道。但是Redis 为什么快呢&#xff1f;很多人只能答出Redis 因为它是基于内存实现的&#xff0c;但是对于其它原因都是模棱两可。 那么今天就一起来看看是Redis 为什么快吧&#xff1a; Redis 为什么这么快…

Ubuntu释放VMware虚拟磁盘未使用空间

By: Ailson Jack Date: 2023.08.26 个人博客&#xff1a;http://www.only2fire.com/ 本文在我博客的地址是&#xff1a;http://www.only2fire.com/archives/152.html&#xff0c;排版更好&#xff0c;便于学习&#xff0c;也可以去我博客逛逛&#xff0c;兴许有你想要的内容呢。…

C++通过JNI调用JAVA方法返回ArrayList对象

运行效果: JAVA实现: 获取系统已安装应用列表并返回List<String>对象 //使用系统API获取安装包列表public List<String> getAppList(MainActivity act) {List<String> packages = new ArrayList<String>();try {//取包信息列表List<PackageInf…

Unity项目如何上传Gitee仓库

前言 最近Unity项目比较多&#xff0c;我都是把Unity项目上传到Gitee中去&#xff0c;GitHub的话我用的少&#xff0c;可能我还是更喜欢Gitee吧&#xff0c;毕竟Gitee仓库用起来更加方便&#xff0c;注意Unity项目上传时最佳的方式是把 Asste ProjectSetting 两个文件夹上传上…

蓝牙耳机语音信号处理之ENC算法

+他V hezkz17进数字音频系统研究开发交流答疑群(课题组) 1 什么是单麦谱减法降噪? 单麦谱减法降噪是一种音频信号处理技术,用于在单声道录音中减少噪音的方法。它基于频域分析和减法混叠原理。首先,通过将音频信号转换为频域表示(如快速傅立叶变换),可以将音频信号分解…

二叉树、红黑树、B树、B+树

二叉树 一棵二叉树是结点的一个有限集合&#xff0c;该集合或者为空&#xff0c;或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。 二叉树的特点&#xff1a; 每个结点最多有两棵子树&#xff0c;即二叉树不存在度大于2的结点。二叉树的子树有左右之分&#xf…

ant design自定义展开折叠查看子项和点击行查看详情

实现思路&#xff1a;通过配置rowSelection&#xff0c;列表项是否可选择来实现。 页面内容&#xff1a; <a-table :dataSource"integrationBonds" :columns"columns" :customRow"customintegrationBondsRow":pagination"{hideOnSingle…

使用Pytorch和OpenCV实现视频人脸替换

“DeepFaceLab”项目已经发布了很长时间了&#xff0c;作为研究的目的&#xff0c;本文将介绍他的原理&#xff0c;并使用Pytorch和OpenCV创建一个简化版本。 本文将分成3个部分&#xff0c;第一部分从两个视频中提取人脸并构建标准人脸数据集。第二部分使用数据集与神经网络一…

【C++心愿便利店】No.3---内联函数、auto、范围for、nullptr

文章目录 前言&#x1f31f;一、内联函数&#x1f30f;1.1.面试题&#x1f30f;1.2.内联函数概念&#x1f30f;1.3.内联函数特性 &#x1f31f;二、auto关键字&#x1f30f;2.1.类型别名思考&#x1f30f;2.2.auto简介&#x1f30f;2.3.auto的使用细节&#x1f30f;2.4.auto不能…

【matlab利用shp文件制作mask白化文件】

matlab白化文件 mask文件的作用matlab制作mask文件mask结果 mask文件的作用 地理信息绘图中的 “mask” 通常指的是遮罩或掩膜&#xff0c;用于在地图或图像上隐藏、高亮或标记特定区域。 数据可视化&#xff1a; 地理信息绘图 mask 可以用于突出显示特定地理区域&#xff0c;使…

【水平垂直居中布局】CSS实现水平垂直居中的5种方法(附源码)

文章目录 写在前面涉及知识点1、子绝对定位父相对定位&#xff0c;子节点设置位移实现1.1效果1.2实现源码 2、子绝对定位父相对定位&#xff0c;子节点设置上下边距2.1 效果2.2 实现源码 3、利用flex布局实现3.1 效果3.2 实现源码 4、利用行高和文本水平居中设置4.1 效果4.2 实…

中国移动秋招攻略,网申测评和面试

中国移动秋招简介 按照往年的惯例来看&#xff0c;移动会在每年的8月份发布相关秋招信息&#xff0c;紧接着考生并进行网申&#xff0c;面试的时间跨度也非常的长&#xff0c;大概是9~12月份。整个招聘流程&#xff0c;包括投递简历网申&#xff0c;笔试测评&#xff0c;面试录…

[Linux]进程概念

[Linux]进程概念 文章目录 [Linux]进程概念进程的定义进程和程序的关系Linux下查看进程Linux下通过系统调用获取进程标示符Linux下通过系统调用创建进程-fork函数使用 进程的定义 进程是程序的一个执行实例&#xff0c;是担当分配系统资源&#xff08;CPU时间&#xff0c;内存…

HAProxy+nginx搭建负载均衡群集

目录 一、常见的Web集群调度器 二、HAProxy群集介绍 1、Haproxy的特性 : 2、Haproxy常用的调度算法 ① 轮询调度&#xff08;Round Robin&#xff09; ② 最小连接数&#xff08;Least Connections&#xff09; ③ 基于来源访问调度算法&#xff08;Source Hashing&am…

Java“牵手”天猫店铺所有商品API接口数据,通过店铺ID获取整店商品详情数据,天猫API申请指南

天猫商城是一个网上购物平台&#xff0c;售卖各类商品&#xff0c;包括服装、鞋类、家居用品、美妆产品、电子产品等。天猫商品详情可以帮助消费者更好的了解宝贝信息&#xff0c;从而做出购买决策。同时&#xff0c;消费者也可以通过商品详情了解其他买家对宝贝的评价&#xf…