罗剑锋的 C++ 实战笔记
罗剑锋
前奇虎 360 技术专家,Nginx/OpenResty 开源项目贡献者
34779 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 32 讲
结束语 (1讲)
罗剑锋的 C++ 实战笔记
15
15
1.0x
00:00/00:00
登录|注册

03 | 预处理阶段能做什么:宏定义和条件编译

你好,我是 Chrono。
上一次我讲了在编码阶段要有好的 code style,尽量写出“人能够看懂的代码”。今天,我就继续讲讲编码后的预处理阶段,看看这个阶段我们能做哪些事情。

预处理编程

其实,只要写 C/C++ 程序,就会用到预处理,只是大多数时候,你只用到了它的一点点功能。比如,在文件开头写上“#include”这样的语句,或者用“#define”定义一些常数。只是这些功能都太简单了,没有真正发挥出预处理器的本领,所以你可能几乎感觉不到它的存在。
预处理只能用很少的几个指令,也没有特别严谨的“语法”,但它仍然是一套完整自洽的语言体系,使用预处理也能够实现复杂的编程,解决一些特别的问题——虽然代码可能会显得有些“丑陋”“怪异”。
那么,“预处理编程”到底能干什么呢?
你一定要记住:预处理阶段编程的操作目标是“源码”,用各种指令控制预处理器,把源码改造成另一种形式,就像是捏橡皮泥一样。
把上面的这句话多读几遍,仔细揣摩体会一下,理解了之后,你再去用那些预处理指令就会有不一样的感觉了。
C++ 语言有近百个关键字,预处理指令只有十来个,实在是少得可怜。而且,常用的也就是 #include、#define、#if,所以很容易掌握。不过,有几个小点我还是要特别说一下。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《罗剑锋的 C++ 实战笔记》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(41)

  • 最新
  • 精选
  • sugar
    老师,能否讲讲#include 各种头文件的细节,比如尖角和引号 在include时就是不同的。

    作者回复: 这个区别就是包含时的搜索路径不同,网上资料都有,所以就不重复了。好像“”是从当前开始搜索,<>是从系统路径开始搜索。

    26
  • 嵇斌
    1. #define PI (3.14) -> constexpr float PI = 3.14 其他貌似不能完全对等。比如内链的代码块,可以用lambda,但是效率如何得看编译器的优化了。比如有些条件编译可以想办法用enable_if来替换实现。到了后面这些替换就不单单是语言方面的实践了,可能涉及软件工程、设计模式。 2. 条件编译自己用还好,自己一般都还清楚自己的套路。条件编译最头疼的就是对着代码定位问题,这个实现到底是哪个代码块呢?尤其是那种跨平台的和OS相关的适配层代码。再聪明的IDE和编译器也还是有点区别的。当然现在可能Clion和VS等都已经比较完善了...

    作者回复: 说的很好。 1.constexpr是编译期的常量,和预处理期的常量效果等价,但生命周期不同。 2.C++对同一个问题有多种解法,虽然自由,但也有选择困难症,挑出合适自己的就比较累。 3.这个也是追求性能付出的代价吧,怕的就是四处散落的#if-#else-#endif,最好把平台相关的代码集中在一起,再用#include一次包含,可以参考Nginx,它做的很好。

    20
  • 人人搞科研
    老师 rust很安全,现在大公司很多项目都用rust改写了,cpp还需要学吗,是不是直接学rust呢

    作者回复: 可以看一下tobie榜单,C++还在前五,很多框架、系统都是用C++写的,学习它可以更好地理解系统架构。 当然最后你不一定在工作中用到C++,但它作为一个基础,会受用终身的。

    3
    14
  • 怪兽
    老师,我在看rapidjson源码时,看到如下代码,不知道有什么作用,为什么不直接使用assert? #ifndef RAPIDJSON_ASSERT #define RAPIDJSON_ASSERT(x) assert(x) #endif // RAPIDJSON_ASSERT

    作者回复: 这是开源库的通常做法,把调用的接口用宏或者函数统一做一个自己的封装,这样写代码的时候风格更一致。 在设计模式里这个叫包装外观wrapper facade。

    9
  • Wynter
    老师,我最近在阅读一个事件驱动框架代码libev,源码里就充斥着大量的宏,阅读起来很是费力。所以请教老师,阅读这种宏比较多的源码应该怎么办?

    作者回复: 如果有好的IDE,它可以帮你找出宏的定义,加快理解。 还有一种办法,就是用课程里的“gcc -E”,让预处理器给你展开宏。

    3
    7
  • Tedeer
    在读到这段话时:另存为一个“*.inc”文件,然后再用“#include”替换原来的大批数字。 想起以前开发过程中,曾经在头文件中定义了一个240*160的图片字节数组,现在看来有点蠢,又涨知识了。条件编译在Android系统源码见得比较多,区分不同平台之间代码块的实现。

    作者回复: 预处理就是面向源码编程,调整源码的形态,掌握了这一点就可以让代码更干净整齐。

    2
    6
  • Carlos
    先回答老师的问题🤔 1. 就中间那个算立方体的例子来说, 我觉得这个用法可以替换成一个 inline function. 储存常数可以直接用 const, 为了简化可以用 reference 2. 说实话, 我没用过条件编译 😂, 今天头一次学到, 但是我觉得这肯定会让代码变得更加复杂, 冒出很多意料之外的 bugs. 再提两个自己的小问题: 1. 使用宏的条件编译是不是可以用 Cmake 来做呢? (虽然我几乎不会用 Cmake 但是我预感它有这个功能) 2. 上文里面的 extern "C" { 部分我有些没看懂, 这个是预处理语言吗? 希望老师帮助理解.

    作者回复: 1.对,在C++里大部分函数宏都可以用inline函数代替。 2.条件编译多用在跨平台和高级优化方面,不用当然也可以,但要追求性能极致就必须考虑。 3.cmake不了解,其实预处理就是文本替换,你要是愿意,用shell、Python也可以处理C++源码来实现。 4.extern "C" { }是处理编译器的链接符号,保持与C兼容,通常是为了导出外部接口使用的。暂时不了解也没关系,以后用到的时候再学也来得及。

    5
    6
  • 企鹅君需要充电
    1、#include基本不会被替代,但为了避免#define滥用,少用它设置全局变量,const/static/enum处理常量声明还能自带额外信息(例如作用域) 2、debug期间用条件编译方便,但有时候会忽略另一种情况的判断测试,而且写死忘了改回来就gg了

    作者回复: 说的很对。 C++今后要引入import关键字和模块,以后#include等预处理指令可能就会越来越少了,不过有的时候还是很有用的。

    4
  • sugar
    另一个有关include头文件的问题。比如说我看很多例子都是 在单独一个h文件和一个cpp文件中定义了一个类,h里写了类的声明,类的具体实现在cpp里面。其他cpp文件里想引入这个类的时候,就直接include了h文件,并没include cpp文件,按照咱们这一讲 只有声明被预处理时放到实际执行的cpp代码里了,类的实现cpp是怎么引入进来的呢?

    作者回复: h里一般只是声明,cpp里是实现,在编译阶段会有个链接动作,把这些都链接起来。 具体的细节比较复杂,我也没细研究过,可以网上找点资料看看,不过我觉得不了解也没关系。

    4
  • 廖熊猫
    我的一些使用经历: 1. 使用#ifdef __cplusplus这个在用Emscripten编译wasm的时候会跟extern "c" { } 这个一起使用,防止编译后名字被修改掉。 2. 在C语言里直接使用const定义的长度在全局定义数组会报错,但是可以用预处理器来创建。 3. 快速定义一些内容相似的struct还有相关的操作函数,不过在C++里面应该可以使用template来解决这个问题了吧

    作者回复: 1.条件编译加__cplusplus经常用在与C配合工作方面,算是个惯用语了。 2.C语言对const的支持比较弱,在C++中好很多。 3.宏在减少重复代码方面有时候还是挺有用的,因为模板和泛型的语法检查很严格,要写好还是要费些力气的,如果是简单的工作用宏就会轻松一些。

    4
收起评论
显示
设置
留言
41
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部