C/C++预处理指令

预处理指令是我们写在程序代码中的给预处理器(preprocessor)的命令,该指令将在编译器进行编译之前对源代码做某些转换。预处理指令以#号开头(# 号必须是该行除了任何空白字符外的第一个字符),#后是指令关键字,在关键字和# 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令。

使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。

常见的预处理指令

指令 说明
# 空指令,无任何效果
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个#if……#else条件编译块

#include

当预处理器找到一个#include 指令时,它用指定文件的全部内容替换这条语句。声明包含一个文件有两种方式:

1
#include "file"  or   #include <file>

两种表达的唯一区别是编译器应该在什么路经下寻找指定的文件。

  • 第一种情况下,文件名被写在双引号中,编译器首先在包含这条指令的文件所在的目录下进行寻找,如果找不到指定文件,编译器再到被配置的默认路径下(也就是标准头文件路径下)进行寻找。

  • 如果文件名是在尖括号 <> 中,编译器会直接到默认标准头文件路径下寻找。

#define

#define可以被用来生成宏定义常量(defined constantants 或 macros),它的形式是:

1
#define [MacroName] [MacroValue]

它的作用是定义一个叫做name 的宏定义,然后每当在程序中遇到这个名字的时候,它就会被value代替。

取消宏则是:

1
#undef [MacroName]

简单的define定义

1
#define MAXTIME 1000

一个简单的MAXTIME就定义好了,它代表1000,假设在程序里面写

1
if(i < MAXTIME){.........}

编译器在处理这个代码之前会对MAXTIME进行处理替换为1000。

这种定义看起来相似于普通的常量定义CONST,但也有着不同,由于define的定义更像是简单的文本替换,而不是作为一个量来使用,这个问题在以下反映的尤为突出。

define的“函数定义”

define能够像函数那样接受一些參数,例如以下

1
#define max(x,y) (x)>(y)?(x):(y);

这个定义就将返回两个数中较大的那个,看到了吗?由于这个“函数”没有类型检查,就好像一个函数模板似的,当然,它绝对没有模板那么安全就是了。能够作为一个简单的模板来使用而已。

可是这样做的话存在隐患,样例例如以下:

1
#define Add(a,b) a+b;

在一般使用的时候是没有问题的,可是假设遇到如:

1
c * Add(a,b) * d

的时候就会出现故障,代数式的本意是a+b然后去和c,d相乘,可是由于使用了define(它仅仅是一个简单的替换),所以式子实际上变成了:c*a + b*d

另外举一个样例:

1
2
#define pin (int*);
pin a,b;

本意是a和b都是int型指针,可是实际上变成int* a,b; a是int型指针,而b是int型变量。
这是应该使用typedef来取代define,这样a和b就都是int型指针了。
所以我们在定义的时候,养成一个良好的习惯,建议全部的层次都要加括号。

define的单行定义

1
2
3
#define A(x) T_##x
#define B(x) #@x
#define C(x) #x

我们如果:x=1,则有:

A(1)——> T_1 (T_##x,##符号是记号粘黏符号,将前后的字符粘黏起来。)
B(1)——> ‘1’ ( #@x , #@ 符号会将宏的参数进行字符串字面量化,并且加‘’号)
C(1)——> “1” ( #x ,#符号会将宏的参数进行字符串字面量化,并且加””号)

define的多行定义

define能够替代多行的代码,比如MFC中的宏定义

1
2
3
4
5
6
#define MACRO(arg1, arg2)  do{   \ 
\
stmt1; \
stmt2; \
\
} while(0)

注意:

  1. 使用#define声明多行宏函数与声明单行宏函数没有本质区别;
  2. 多行声明时,回车换行前要加上字符‘\’,即“[enter]”,注意字符‘\’后要紧跟回车键,中间不能有空格或其他字符。
  3. 另外,在Linux操作系统中 “[enter]”称为跳脱字符,意思是一行写不完的时候可以使用跳脱字符换行,但对于操作系统而言,它认为你并没有换行。

define和typedef的差别

typedef是一种用来声明自定义数据类型,配合各种原有数据类型达到简化编程目的的类型定义关键字,可以使用typedef为已有数据类型取别名。

两者的区别:

  1. 执行时间不同
  • 关键字 typedef 在编译阶段有效, 犹豫是在编译阶段, 因此 typededf 有类型检查的功能.
  • define 是宏定义, 发生在预处理阶段, 也就是编译之前, 它只是进行简单而机械的字符串替换, 而不进行任何检查.
  1. 功能不同
  • typedef 用来定义类型的别名, 这些类型不止包含内部类型(init,char等), 还包括自定义类型(如struct), 可以起到使类型易于记忆的功能.如:
    1
    2
    typedef int (*PF) (const char *, const char *); 
    //定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为const char *. 这个就和定义 Block 很像了
  • typedef 可定义机器无关的类型,如,你可以定义一个浮点类型,在目标机器上它可以获得最高的精度。如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef long double REAL; 

    //在不支持 long double 的机器上,该 typedef 看起来会是下面这样:

    typedef double REAL;

    //并且,在连 double 都不支持的机器上,该 typedef 看起来会是这样:

    typedef float REAL;
  • define 不止可以为类型取别名, 还可以定义常量, 变量, 编译开关等.
  1. 作用域不同
  • #define 没有作用域的限制, 只要是之前预定义过的宏, 在以后的程序中都可以使用。而 typedef 有自己的作用域.
    1
    2
    3
    4
    5
    6
    7
    void fun() {
    #define A int
    }
    void gun() {
    //在这里也可以使用A,因为宏替换没有作用域,
    //但如果上面用的是typedef,那这里就不能用A ,不过一般不在函数内使用typedef
    }
  1. 对指针的操作不同
  • 二者修饰指针类型时, 作用不同
    1
    2
    3
    4
    5
    6
    Typedef int * pint;  
    #define PINT int *
    Const pint p;//p不可更改,p指向的内容可以更改,相当于 int * const p;
    Const PINT p;//p可以更改,p指向的内容不能更改,相当于 const int *p;或 int const *p;
    pint s1, s2; //s1和s2都是int型指针
    PINT s3, s4; //相当于int * s3,s4;只有一个是指针。

条件编译——#ifdef, #ifndef, #if, #endif, #else and #elif

这些指令可以使程序的一部分在某种条件下被忽略。

#ifdef

#ifdef 可以使一段程序只有在某个指定常量已经被定义了的情况下才被编译,无论被定义的值是什么。它的操作是:

1
2
3
#ifdef name
// code here
#endif

例如:

1
2
3
#ifdef MAX_WIDTH
char str[MAX_WIDTH];
#endif

在这个例子中,语句char str[MAX_WIDTH]; 只有在宏定义常量MAX_WIDTH 已经被定义的情况下才被编译器考虑,不管它的值是什么。如果它还没有被定义,这一行代码则不会被包括在程序中。

#ifndef

#ifndef 起相反的作用:在指令#ifndef 和 #endif 之间的代码只有在某个常量没有被定义的情况下才被编译,例如:

1
2
3
4
#ifndef MAX_WIDTH
#define MAX_WIDTH 100
#endif
char str[MAX_WIDTH];

这个例子中,如果当处理到这段代码的时候MAX_WIDTH 还没有被定义,则它会被定义为值100。而如果它已经被定义了,那么它会保持原值 (因为#define 语句这一行不会被执行) 。

#if, #else 和 #elif

指令#if, #else 和 #elif (elif = else if) 用来使得其后面所跟的程序部分只有在特定条件下才被编译。这些条件只能够是常量表达式,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if MAX_WIDTH>200
#undef MAX_WIDTH
#define MAX_WIDTH 200

#elsif MAX_WIDTH<50
#undef MAX_WIDTH
#define MAX_WIDTH 50

#else
#undef MAX_WIDTH
#define MAX_WIDTH 100
#endif

char str[MAX_WIDTH];

注意看这一连串的指令 #if, #elsif 和 #else 是怎样以 #endif 结尾的。

常常使用宏来调试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#if 0
///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译)
#else
///< 新的代码(或函数)
#endif

#ifndef JOE_DEBUG
///< 新的代码(或函数)
#else
///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译)
#endif

#ifdef Q_DEBUG
///< 新的代码(或函数)
#else
///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译)
#endif

#line

当我们编译一段程序的时候,如果有错误发生,编译器会在错误前面显示出错文件的名称以及文件中的第几行发生的错误。

指令#line 可以使我们对这两点进行控制,也就是说当出错时显示文件中的行数以及我们希望显示的文件名。它的格式是:

1
#line number "filename"

这里number 是将会赋给下一行的新行数。它后面的行数从这一点逐个递增。

filename 是一个可选参数,用来替换自此行以后出错时显示的文件名,直到有另外一个#line指令替换它或直到文件的末尾。例如:

1
2
#line 1 "assigning variable"
int a?;

这段代码将会产生一个错误,显示为在文件”assigning variable”, line 1 。

#error

这个指令将中断编译过程并返回一个参数中定义的出错信息,例如:

1
2
3
4
#ifndef __cplusplus
#error A C++ compiler is required
#endif

这个例子中如果__cplusplus没有被定义就会中断编译过程。

#pragma

这个指令是用来对编译器进行配置的,针对你所使用的平台和编译器而有所不同。要了解更多信息,请参考你的编译器手册。
如果你的编译器不支持某个#pragma的特定参数,这个参数会被忽略,不会产生出错。

预定义宏 (Predefined macro names)

以下宏名称在任何时候都是定义好的:

macro value
LINE 整数值,表示当前正在编译的行在源文件中的行数。
FILE 字符串,表示被编译的源文件的文件名。
DATE 一个格式为 “Mmm dd yyyy” 的字符串,存储编译开始的日期。
TIME 一个格式为 “hh:mm:ss” 的字符串,存储编译开始的时间。
STDC 如果编译器接受标准C,那么值为1. (整型)
__cplusplus 整数值,所有C++编译器都定义了这个常量为某个值。如果这个编译器是完全遵守C++标准的,它的值应该等于或大于199711L,具体值取决于它遵守的是哪个版本的标准。

参考文献:

https://www.cnblogs.com/zi-xing/p/4550246.html
https://www.cnblogs.com/lcchuguo/p/4005360.html
https://www.kancloud.cn/kancloud/cplusplus/62276
http://blog.sina.com.cn/s/blog_4fc2fb600102yds4.html
https://www.jianshu.com/p/53ac91a23979