Introduction

编译器对C/C++文件进行编译时, 第一步就是宏处理, 也即预处理.这步是由预处理器完成.

预处理就是对源文件的宏(macro)进行处理(如宏替换, 条件编译, 包含头文件信息).这个步骤属于文本处理,
还没有真正开始编译.

现在我们一般感受不到这个过程, 因为现在的编译器把预处理, 编译, 汇编, 链接的过程都集成起来了, programmer只需要一键即可生成可执行文件.

C/C++中宏处理命令是以#开头的语句, 不以#开头的则是代码语句.

预定义宏

下面是C标准中已经预定义好的宏:

  1. __LINE__
  2. __FILE__
  3. __DATE__
  4. __TIME__
  5. __STDC__

当然可以使用#undef取消或者自己重新定义.

此外编译器也有自己定义的宏, 称为编译器扩展宏.
但是不同的编译器使用的宏是不一样的.典型的是MS的VC++和GCC.
如VC++中的#pragma comment(lib, “xxx.lib”)链接静态链接库的宏, 改用gcc编译就不成功了.

gcc编译器可以通过下面命令查看编译器定义的宏:

1
2
3
gcc -dM -E main.cpp
or
cpp -dM main.cpp

cpp是GNU的预处理器(preprocessor).

而VC++编译器定义的宏, 到MSDN参看.

自定义宏

除了在代码中通过#define自定义宏外, 还可以在编译源文件的时候通过配置编译器实现.

编译器允许在编译源码时,用户自定义宏.

gcc编译器

gcc/g++在编译源代码的时候, 添加命令选项-D添加自定宏.

假设我要定义一个DEBUG的宏, 则可以使用下面命令编译:

1
2
3
g++ -o main -D DEBUG main.cpp
or
g++ -o main -DDEBUG main.cpp

-D参数后面跟的是自定义宏的名字, 需要定义多个宏的话, 使用多个-D.

MS VC++编译器

MS的VS中, 可以通过如下设置自定义宏:

右击项目-> properties -> C/C++ -> preprocessor -> preProcessor Definitions

在相应窗口中添加自己自定义的宏.

应用

在写ACM代码时,可以通过自定义宏实现输入/输出的重定向而不用每次手动把重定向语句给注释掉再提交.

在重定向输入/输出语句如下代码:

1
2
3
4
#ifndef ONLINE_JUDGE
fropen("data.in", "r", stdin);
fropen("data.out", "w", stdout);
#endif

一般来说OJ都会编译都会定义ONLINE_JUDGE宏(不同OJ有可能不同).在我们本机编译代码时, 我们一般的编译是没有指定这个ONLINE_JUDGE宏的,
因此, 在本机编译的时候, 就会包含上面的两个重定向代码.而在OJ编译时, 由于ONLINE_JUDGE定义了, 因此没有包含重定向语句, 而是从标准输入/输出.

gcc生成预处理后文件

gcc/g++可以通过命令参数输出预处理后的文件.

对于C预处理后的文件后缀是.i, c++处理后文件后缀是.ii

命令如下:

1
2
3
g++ -E main.cpp //对main.cpp预处理后结果输出到标准stdout
g++ -c -save-temps main.cpp //生成预处理后main.ii文件
gcc -c -save-temps main.c //生成预处理后main.i文件

生成的.i/.ii文件会原来的cpp文件大很多, 因为把头文件的信息都完整包含进来了.
并且源代码中的宏定义都被处理了, 在预处理后文件中都不见了.

避免头文件多次被引用

C/C++中的宏最常用的就是通过#ifdef, #endif来避免头文件多次被引用(redefinition).

C++ 03标准中有一条ODR(One Definition Rule)准则, 里面提到:

In any translation unit, a template, type, function, or object can have no more than one definition. Some of these can have any number of declarations. A definition provides an instance.

就是说C++中的模版,类型, 函数, 对象可以有多个声明, 但最多有1个定义.

如果违反ODR原则, 编译器会提示redefinition错误.

如果防止头文件中的定义被多次包含呢? 答案就是宏定义.

对每个头文件都定义一个独特唯一标识的宏,通过判断该宏是否被定义, 而决定是否包含该头文件.

标准模版如下:

1
2
3
4
5
6
7
//头文件name.h
#ifndef NAME_H
#def NAME_H
... //name.h文件定义的内存
#endif

一般来说, 对于头文件定义的宏, 命名规则通常为NAME_H, 其中NAME为头文件名字的大写.

下面是一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//file: obj.h
#ifndef _OBJ_H
#define _OBJ_H
class obj
{
public:
void out() { std::cout << "Hello World!"; };
};
#endif
//main.cpp
#include "obj.h"
#include "obj.h" //多次包含obj.h头文件
int main()
{
obj t;
t.out();
return 0;
}

上面main.cpp文件中多次包含obj.h, 但是obj.h使用#ifndef, 因此不会出现redefinition问题.

我们分别对上面两种情况看看g++预处理后的.i文件内容:

1. obj.h没有使用#ifndef, 预处理后的main.i文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 5 "obj.h" 2
class obj
{
public:
void out() { std::cout << "Hello World!"; };
};
# 2 "main.cpp" 2
# 1 "obj.h" 1
class obj
{
public:
void out() { std::cout << "Hello World!"; };
};
# 3 "main.cpp" 2
int main()
{
obj j;
j.out();
return 0;
}

2. obj.h使用#ifndef, 预处理后的main.i文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5 "obj.h" 2
class obj
{
public:
void out() { std::cout << "Hello World!"; };
};
# 2 "main.cpp" 2
int main()
{
obj j;
j.out();
return 0;
}

明显可以看出, 使用#ifndef后, obj的定义只包含1次.

除了使用#ifdef做法外, 还可以使用#pragma once宏, 很多编译器都支持这个宏.
但是#pragma once并不是C++标准的.

符号##,#, \

C的宏中有两个符号#和##是用于处理字符串的.
#是可以把传给宏的token转换成字符串类型
##则是把两个taokens拼接成1个, 然后替换.

用例子更好地说明, 先看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#define change_to_string(a) #a
#define combine(a, b) a##b
int main()
{
int a1 = 10;
printf("%s\n", change_to_string(abc));
printf("%d\n", combine(a, 1));
return 0;
}

上面代码的输出的结果是:

abc //输出字符串"abc"
10 //输出a1

第一个输出的是”abc”, #a把a转换字符串类型;
而第二个输出是a1的值, a##b就是拼接ab, combine(a, 1)在编译器预处理后就变成a1了.

我们看看gcc预处理后的.i文件中的main函数:

1
2
3
4
5
6
7
int main()
{
int a1 = 10;
printf("%s\n", "abc");
printf("%d\n", a1);
return 0;
}

可以看到, 和预想情况一样.

行连接符\

当宏定义一行代码无法写完时, 可以使用反斜杠\续行.即在每行宏后面添加\.

预处理器会把以\结束的宏指令的末尾的\和换行符删除掉,把若干宏指令行合并成一行.

Reference

  1. gcc预处理指令之#pragma once
  2. One Definition Rule
  3. GCC/G++编译器和VS IDE中的一些预定义宏和用户自定义宏
  4. An Introduction to GCC - for the GNU compilers gcc and g++
  5. gcc编译器使用简明指南
  6. gcc,g++-GNU工程的C和C++编译器(egcs-1.1.2)