【C++】预处理阶段技巧:宏定义和条件编译

宏定义和条件编译都是在预处理阶段做的事情。

只要是写C/C++程序,都会用到预处理,最常见的是“#include”,或者利用“#define”定义一些常数。但这些功能太简单了,没有真正发挥出预处理器的本领。

相信不少看过优秀的C++源码的程序员,都会被作者在某个文件中的预处理指令搞得云里雾里,看得头晕。他们到底用这里指令干嘛呢

首先要记住的是,预处理阶段编程的操作目标是“源码”,用各种指令控制预处理器,把源码改造成另一种形式,就像是捏橡皮泥一样。

1、预处理指令

预处理指令都是以“#”符号开头,其关键字只有十来个,常见的有#include、#define、#if。

预处理指令时常和C++代码在同一个源文件中,但它不属于C++语言,它走的是预处理器,不受C++语法规则的约束。所以,预处理编程也就不用太遵守C++ 代码的风格。一般来说,预处理指令不应该受 C++ 代码缩进层次的影响,不管是在函数、类里,还是在 if、for等语句里,永远是顶格写。

另外,单独的一个“#”也是一个预处理指令,叫“空指令”,可以当作特别的预处理空行。而“#”与后面的指令之间也可以有空格,从而实现缩进,方便排版。

#                              // 预处理空行
#if __linux__                  // 预处理检查宏是否存在
#   define HAS_LINUX    1      // 宏定义,有缩进
#endif                         // 预处理条件语句结束
#                              // 预处理空行

预处理程序也有它的特殊性,暂时没有办法调试,不过可以让 GCC 使用“-E”选项,略过后面的编译链接,只输出预处理后的源码,比如:

g++ test03.cpp -E -o a.cxx    #输出预处理后的源码
2、包含文件(#include)

这个可以说是我们日常编程用得最多的预处理指令,我以前只尝试过调用内置的或自己写的头文件和源文件,后来发现“#include”的作用是“包含文件”,这个文件可以是任意文件。换句话说,只要你愿意,把源码、普通文本,甚至是图片、音频、视频都引进来。当然,出现无法处理的错误就是另外一回事了。

#include "a.out"      // 完全合法的预处理包含指令,你可以试试

另一个我编程中常用的就是“Include Guard”,这是为了防止自己写的头文件出现重复包含的情况,导致编译错误。其指令形式如下:

#ifndef _XXX_H
#define _XXX_H
	/*
	头文件内容
	*/
#endif // _XXX_H

这个手法虽然比较“原始”,但在目前来说(C++11/14),是唯一有效的方法,而且也向下兼容 C 语言。所以,我建议你在所有头文件里强制使用。

除了最常用的包含头文件,你还可以利用“#include”的特点玩些“小花样”,编写一些代码片段,存进“*.inc”文件里,然后有选择地加载,用得好的话,可以实现“源码级别的抽象”。

比如说,有一个用于数值计算的大数组,里面有成百上千个数,放在文件里占了很多地方,特别“碍眼”:


static uint32_t  calc_table[] = {  // 非常大的一个数组,有几十行
    0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
    0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
    0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
    0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
    ...                          
};

这个时候,你就可以把它单独摘出来,另存为一个“*.inc”文件,然后再用“#include”替换原来的大批数字。这样就节省了大量的空间,让代码更加整洁。

static uint32_t  calc_table[] = {
#  include "calc_values.inc"        // 非常大的一个数组,细节被隐藏
};
3、宏定义(#define/#undef)

接下来要说的是预处理编程里最重要、最核心的指令“#define”,它用来定义一个源码级别的“文本替换”,也就是我们常说的“宏定义”。

“#define” 可谓“无所不能”,在预处理阶段可以无视 C++ 语法限制,替换任何文字,定义常量 / 变量,实现函数功能,为类型起别名(typedef),减少重复代码……

不过,也正是因为它太灵活,如果过于随意地去使用宏来写程序,就有可能把正常的 C++代码搞得“千疮百孔”,替换来替换去,都不知道真正有效的代码是什么样子了。所以,使用宏的时候一定要谨慎,时刻记着以简化代码、清晰易懂为目标,不要“滥用”,避免导致源码混乱不堪,降低可读性。

以下有几个帮你用好宏定义的小建议。

  • 首先,因为宏的展开、替换发生在预处理阶段,不涉及函数调用、参数传递、指针寻址,没有任何运行期的效率损失,所以对于一些调用频繁的小代码片段来说,用宏来封装的效果比 inline 关键字要更好,因为它真的是源码级别的无条件内联。参考如下:

    #define ngx_tolower(c)      ((c >= 'A' && c <= 'Z') ? (c | 0x20) : c)
    #define ngx_toupper(c)      ((c >= 'a' && c <= 'z') ? (c & ~0x20) : c)
    
    #define ngx_memzero(buf, n)       (void) memset(buf, 0, n)
    
  • 其次,你要知道,宏是没有作用域概念的,永远是全局生效。所以,对于一些用来简化代码、起临时作用的宏,最好是用完后尽快用“#undef”取消定义,避免冲突的风险。像下面这样:

    
    #define CUBE(a) (a) * (a) * (a)  // 定义一个简单的求立方的宏
    
    cout << CUBE(10) << endl;        // 使用宏简化代码
    cout << CUBE(15) << endl;        // 使用宏简化代码
    
    #undef CUBE                      // 使用完毕后立即取消定义
    

    另一种做法是宏定义前先检查,如果之前有定义就先 undef,然后再重新定义:

    
    #ifdef AUTH_PWD                  // 检查是否已经有宏定义
    #  undef AUTH_PWD                // 取消宏定义
    #endif                           // 宏定义检查结束
    #define AUTH_PWD "xxx"           // 重新宏定义
    
  • 再次,你可以适当使用宏来定义代码中的常量,消除“魔术数字”“魔术字符串”(magic number)。例子如下:

    #define MAX_BUF_LEN    65535
    #define VERSION        "1.0.18"
    
  • 除了上面说的三个,如果你开动脑筋,用好“文本替换”的功能,也能发掘出许多新颖的用法。我有一个比较实际的例子,用宏来代替直接定义名字空间(namespace):

    #define BEGIN_NAMESPACE(x)  namespace x {
    #define END_NAMESPACE(x)    }
    
    BEGIN_NAMESPACE(my_own)
    
    ...      // functions and classes
    
    END_NAMESPACE(my_own)
    
4、条件编译(#if/#else/#endif)

条件编译,也就是在预处理阶段实现分支处理,常见的是代码运行在不同的编译环境(wins or linux)。

条件编译有两个要点,一个是条件指令“#if”,另一个是后面的“判断依据”,也就是定义好的各种宏,而这个“判断依据”是条件编译里最关键的部分。

通常编译环境都会有一些预定义宏,比如 CPU 支持的特殊指令集、操作系统 / 编译器 /程序库的版本、语言特性等,使用它们就可以早于运行阶段,提前在预处理阶段做出各种优化,产生出最适合当前系统的源码。

你必须知道的一个宏是“__cplusplus”,它标记了 C++ 语言的版本号,使用它能够判断当前是 C 还是 C++,是 C++98 还是 C++11。你可以看下面这个例子。

#ifdef __cplusplus                      // 定义了这个宏就是在用C++编译
    extern "C" {                        // 函数按照C的方式去处理
#endif
    void a_c_function(int a);
#ifdef __cplusplus                      // 检查是否是C++编译
    }                                   // extern "C" 结束
#endif

#if __cplusplus >= 201402                // 检查C++标准的版本号
    cout << "c++14 or later" << endl;    // 201402就是C++14
#elif __cplusplus >= 201103              // 检查C++标准的版本号
    cout << "c++11 or before" << endl;   // 201103是C++11
#else   // __cplusplus < 201103          // 199711是C++98
#   error "c++ is too old"               // 太低则预处理报错
#endif  // __cplusplus >= 201402         // 预处理语句结束

除了“__cplusplus”,C++ 里还有很多其他预定义的宏,像源文件信息的“FILE”“ LINE”“ DATE”,以及一些语言特性测试宏,比如“__cpp_decltype” “__cpp_decltype_auto” “__cpp_lib_make_unique”等。

g++ -E -dM - < /dev/null

#define __GNUC__ 5
#define __unix__ 1
#define __x86_64__ 1
#define __UINT64_MAX__ 0xffffffffffffffffUL
...

基于它们,你就可以更精细地根据具体的语言、编译器、系统特性来改变源码,有,就用新特性;没有,就采用变通实现:

#if defined(__cpp_decltype_auto)        //检查是否支持decltype(auto)
    cout << "decltype(auto) enable" << endl;
#else
    cout << "decltype(auto) disable" << endl;
#endif  //__cpp_decltype_auto

#if __GNUC__ <= 4
    cout << "gcc is too old" << endl;
#else   // __GNUC__ > 4
    cout << "gcc is good enough" << endl;
#endif  // __GNUC__ <= 4

#if defined(__SSE4_2__) && defined(__x86_64)
    cout << "we can do more optimization" << endl;
#endif  // defined(__SSE4_2__) && defined(__x86_64)

条件编译还有一个特殊的用法,那就是,使用“#if 1”“#if 0”来显式启用或者禁用大段代码,要比“/* … */”的注释方式安全得多,也清楚得多,


#if 0          // 0即禁用下面的代码,1则是启用
  ...          // 任意的代码
#endif         // 预处理结束

#if 1          // 1启用代码,用来强调下面代码的必要性
  ...          // 任意的代码
#endif         // 预处理结束
5、问题

问题一:#include “”和#include <>的区别

​ “”一般用于我们自己写的一些文件搜索路径当前开始,而<>是从系统路径开始搜索。

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值