前言

在编写C/C++程序,会用到预处理,但是大多数时候,我们只用到了它的一点点功能。

C++语言有近百个关键字,但预处理指令只有十来个。我们常用的也就是#include#define#if,所以很容易掌握。

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

下面是一个示例,#号都在行首,而且if里面的define有缩进,看起来还是比较清楚的。以后你在写预处理代码的时候,可以参考这个格式。

  1. # // 预处理空行
  2. #if __GNUC__ // 预处理检查宏是否存在
  3. # define __packed 1 // 宏定义,有缩进
  4. #endif // 预处理条件语句结束
  5. # // 预处理空行

包含文件(#include)

正如标题所述,这个最常用的预处理指令“#include”,它的作用是“包含文件”。注意,不是“包含头文件”,而是可以包含任意的文件

也就是说,使用“#include”可以把任意文件都引进来。但错误使用会出现无法处理的报错等。因此在使用“#include”时,为了防止重复包含。通常会加上“Include Guard”也就是用“#ifndef/#define/#endif”来保护整个头文件

  1. #ifndef _XXX_H_INCLUDED_
  2. #define _XXX_H_INCLUDED_
  3. ... // 头文件内容
  4. #endif // XXX_H_INCLUDED

当然,巧妙地使用“#include”可以使得代码更简洁,更易于阅读。比如,编写一些代码片段,存进“**.inc”文件里,然后有选择地加载。

  • 处理大数组
  1. static uint32_t table[] = { // 非常大的一个数组,有几十行
  2. 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
  3. 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
  4. 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
  5. 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
  6. ...
  7. };

这个时候,可以把数组整理出来,另存为一个“.inc”文件,然后再用“#include**”替换原来的大数组,使得代码更加整洁,后期维护更容易。

处理后的代码示例。

  1. static uint32_t table[] = {
  2. # include "table_values.inc" // 非常大的一个数组,细节被隐藏
  3. };

table_values.inc文件

  1. 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
  2. 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
  3. 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
  4. 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
  5. ...

宏定义(#define/#undef)

宏定义是预处理编程里最重要、最核心的指令。

首先,我们应该知道,宏的展开、替换发生在预处理阶段,不涉及函数调用、参数传递、指针寻址,没有任何运行期的效率损失。

其次,宏是没有作用域概念的,永远是全局生效。所以,对于一些用来简化代码、起临时作用的宏,最好是用完后尽快用“#undef”取消定义。

  1. //示例一
  2. #define CUBE(a) (a) * (a) * (a) // 定义一个简单的求立方的宏
  3. cout << CUBE(10) << endl; // 使用宏简化代码
  4. cout << CUBE(15) << endl; // 使用宏简化代码
  5. #undef CUBE // 使用完毕后立即取消定义
  6. //示例二
  7. #ifdef AUTH_PWD // 检查是否已经有宏定义
  8. # undef AUTH_PWD // 取消宏定义
  9. #endif // 宏定义检查结束
  10. #define AUTH_PWD "xxx" // 重新宏定义

最后,利用宏来定义代码中的常量,可以在后期维护时直接修改,方便高效。

  1. #define MAX_BUF_LEN 65535

条件编译(#if/#else/#endif)

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

  1. #ifdef __cplusplus // 定义了这个宏就是在用C++编译
  2. extern "C" { // 函数按照C的方式去处理
  3. #endif
  4. void a_c_function(int a); // 此部分编译器会按照C的方式去处理,但仍可使用C++调用
  5. #ifdef __cplusplus // 检查是否是C++编译
  6. } // extern "C" 结束
  7. #endif
  8. #if __cplusplus >= 201402 // 检查C++标准的版本号
  9. cout << "c++14 or later" << endl; // 201402就是C++14
  10. #elif __cplusplus >= 201103 // 检查C++标准的版本号
  11. cout << "c++11 or before" << endl; // 201103是C++11
  12. #else // __cplusplus < 201103 // 199711是C++98
  13. # error "c++ is too old" // 太低则预处理报错
  14. #endif // __cplusplus >= 201402 // 预处理语句结束

以上示例要注意,extern “C”的主要作用就是为了能够正确实现C代码调用其他C语言代码。加上extern “C”后,会指示编译器这部分代码按C语言的进行编译,而不是C的。因为由于,C++的函数重载,编译器会将函数的参数类型也加到编译后的代码中。但C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。因此,但需要使用C ++语言调用C语言代码时,需要用extern “C”来强制编译器不要重载函数。

  1. void a_c_function(int a);
  2. //C编译器
  3. _a_c_function
  4. //C++编译器
  5. _a_c_function_int

条件编译还有一个特殊的用法,那就是,使用“#if 1”“#if 0”来显式启用或者禁用大段代码。

  1. #if 0 // 0即禁用下面的代码,1则是启用
  2. ... // 任意的代码
  3. #endif // 预处理结束
  4. #if 1 // 1启用代码,用来强调下面代码的必要性
  5. ... // 任意的代码
  6. #endif // 预处理结束