C++的预处理(Preprocess),是指在C++程式原始码被编译之前,由预处理器(Preprocessor)对C++程式原始码进行的处理。这个过程并不对程式的原始码进行解析,但它把原始码分割或处理成为特定的符号用来支持宏调用。
基本介绍
- 中文名:C++预处理
- 外文名:Preprocess
- 包含头档案:#include
- 否则:#else
- 条件:#if
常用预处理
1)常用的预处理:
#include 包含头档案
#if 条件
#else 否则
#elif 否则如果
#endif 结束条件
#ifdef 或 #if defined 如果定义了一个符号, 就执行操作
#ifndef 或 #if !defined 如果没有定义一个符号,就指执行操作
#define 定义一个符号
#undef 删除一个符号
#line 重新定义当前行号和档案名称
#error 输出编译错误 讯息, 停止编译
#pragma 提供 机器专用的特性,同时保证与C++的完全兼容
2)#include 在 程式中包含头档案
头档案通常以.h结尾,其 内容可使用#include预处理器指令包含到 程式中
头档案中一般包含: 函式原型与全局变数
形式常有下面两种
#include <iostream>
#include "myheader.h"
前者<>用来引用标準库头档案,后者""常用来引用自定义的头档案
前者<>编译器只搜寻包含标準库头档案的默认 目录,后者首先搜寻正在编译的源档案所在的 目录,找不到时再搜寻包含标準库头档案的默认 目录.
如果把头档案放在其他 目录下,为了查找到它,必须在双引号中指定从源档案到头档案的完整路径
3)#define 定义符号、宏
1>符号
#define PI 3.1415925 //定义符号PI为3.1415925
#undef PI //取消PI的值
这里PI看起来像一个变数,但它与变数没有任何关係,它只是一个符号或标誌,在 程式代码编译前,此符号会用一组指定的字元来代替
3.14159265 不是一个数值,只是一个字元串,不会进行检查
在编译前,预处理器会遍历代码,在它认为置换有意义的地方,用字元串PI的定义值(3.14159265)来代替
在注释或字元串中的PI不进行替换
在C中常以#define来定义符号常量,但在C++中最好使用const 来定义常量
#define PI 3.14159265
const long double PI=3.14159265;
两者比较下,前者没有类型的指定容易引起不必须的麻烦,而后者定义清楚,所以在C++中推荐使用const来定义常量
#define的缺点:
1)不支持类型检查
2)不考虑作用域
3)符号名不能限制在一个命名 空间中
2>#undef 删除#define定义的符号
#define PI 3.14159265
... //之间所有的PI都可以被替换为3.14159265
#undef PI
之后不再有PI这个标识符
3>定义宏
#define Print(Var) count<<(Var)<<endl
用宏名中的参数带入语句中的参数
宏后面没有;号
Print(Var)中的Print和(之间不能有空格,否则(就会被解释为置换字元串的一部分
#define Print(Var, digits) cout << setw(digits) << (Var) << endl
调用
Print(ival, 15)
预处理器就会把它换成
cout << setw(15) << (ival) << endl;
所有的情况下都可以使用内联函式来代替宏,这样可以增强类型的检查
template<class T> inline void Print (const T& var, const int& digits)
{
cout<<setw(digits)<<var<<endl;
}
调用
Print(ival, 15);
使用宏时应注意的易引起的错误:
#define max(x,y) x>y?x:y;
调用 result = max(myval, 99); 则换成 result = myval>99?myval:99; 这个没有问题是正确的
调用 result = max(myval++, 99); 则换成 result = myval++>99?myval++:99; 这样如果myval>99那幺myval就会递增两次,这种情况下()是没什幺用的如result=max((x),y)则 result = (myval++)>99?(myval++):99;
再如
#define product(m,n) m*n
调用
result = product(5+1,6);则替换为result = 5+1*6; 所以产生了错误的结果,此时应使用()把参数括起
#define product(m,n) (m)*(n)
则result = product(5+1,6);则替换为result = (5+1)*(6); 所以产生了错误的结果,此时应使用()把参数括起
结论: 一般用内联函式来代替预处理器宏
技巧:
1)给替换变数加引号
#define MYSTR "I love you"
cout << MYSTR ; //I love you而不是"I love you"
如果
cout << "MYSTR" ; //则会输出"MYSTR"而不是"I love you"
可以这样做
cout << #MYSTR ; //则会输出 "I love you"即cout << "\"I love you\"";
2)在宏表达式中连线几个参数
如
#define join(a,b) ab 这样不会理解为参数a的值与参数b的值的连线,即如join(10,999)不会理解为10999而是把ab理解为字元串,即输出ab
这时可以
#define join(a,b) a##b
则join(10,999)就会输出10999
3)逻辑预处理器指令
#if defined CALCAVERAGE 或 #ifdef CALCAVERAGE
int count=sizeof(data)/sizeof(data[0]);
for(int i=0; i<count; i++)
average += data;
average /= count;
#endif
如果已经定义符号CALCAVERAGE则把#if与#endif间的语句放在要编译的原始码内
防止重複引入某些头档案
#ifndef COMPARE_H
#define COMPARE_H 注意: 这里只是定义一个没有值的符号COMPARE_H, 下面的namespace compare不是COMPARE_H的 内容,这里的定义不像是定义一个常量或宏,仅仅定义一个符号,指出此符号已定义,则就会有下面的 内容namespace compare{...
namespace compare{
double max(const double* data, int size);
double min(const double* data, int size);
}
#endif
比较
#define VERSION \
3
因为有换行符\ 所以上句等价于 #define VERSION 3
由此可以看出#define COMPARE_H与namespace compare是独立没有关係的两个行
也可以这样用
#if defined block1 && defined block2
...
#endif
#if CPU==PENTIUM4
...
#endif
#if LANGUAGE == ENGLISH
#define Greeting "Good Morning."
#elif LANGUAGE == GERMAN
#define Greeting "Guten Tag."
#elif LANGUAGE == FRENCH
#define Greeting "Bonjour."
#else
#define Greeting "Hi."
#endif
std::cout<<Greeting << std::endl;
#if VERSION == 3
...
#elif VERSION == 4
...
#else
...
#endif
5)标準的预处理器宏
__LINE__ 当前源档案中的代码行号,十进制整数
__FILE__ 源档案的名称,字元串字面量
__DATE__ 源档案的处理日期,字元串字面量,格式mmm dd yyyy其中mmm是月份如Jan、Feb等 dd是01-31 yyyy是四位的年份
__TIME__ 源档案的编译 时间,也是字元串字面量格式是hh:mm:ss
__STDC__ 这取决于实现方式,如果编译器选项设定为编译标準的C代码,通常就定义它,否则就不定义它
__cplusplus 在编译C++ 程式时,它就定义为199711L
使用#line可以修改__FILE__返回的字元串
如
#line 1000 把当前行号设定为1000
#line 1000 "the program file" 修改__FILE__返回的字元串列号改为了1000,档案名称改为了"the program file"
#line __LINE__ "the program file" 修改__FILE__返回的字元串列号没变,档案名称改为了"the program file"
cout << "program last complied at "<<__TIME__
<< " on " << __DATE__
<< endl;
6)#error
在预处理阶段,如果出现了错误,则#error指令可以生成一个诊断 讯息,并显示为一个编译错误,同时中止编译
#ifndef __cplusplus
#error "Error - Should be C++"
#endif
7)#pragma
专门用于实现预先定义好的选项,其结果在编译器说明文档中进行了详细的解释。编译器未识别出来的#pragma指令都会被忽略
8)assert()宏
在标準库头档案<cassert>中声明
用于在 程式中 测试一个逻辑表达式,如果逻辑表达式为false, 则assert()会终止 程式,并显示诊断 讯息
用于在条件不满足就会出现重大错误,所以应确保后面的语句不应再继续执行,所以它的套用非常灵活
注意: assert不是错误处理 机制,逻辑表达式的结果不应产生负面效果,也不应超出 程式设计师的控制(如找开一个档案是否成功), 程式应提供适当的代码来处理这种情况
assert(expression);
assert(expression) && assert(expression2);
可以使用#define NDEBUG来关闭断言 机制
#include <iostream>
#include <cassert>
using std::cout;
using std::endl;
int main()
{
int x=0;
int y=0;
cout<<endl;
for(x=0; x<20; x++)
{
cout<<"x= "<<x <<" y= "<<y<<endl;
assert(x<y); //当x>=y与x==y时,就报错,并终止 程式的执行
}
return 0;
}
预处理的由来
在C++的历史发展中,有很多的语言特徵(特别是语言的晦涩之处)来自于C语言,预
处理就是其中的一个。C++从C语言那里把C语言预处理器继承过来(C语言预处理器,被Bj
arne博士简称为Cpp,不知道是不是C Program Preprocessor的简称)。
预处理功能
预处理器的主要作用就是把通过预处理的内建功能对一个资源进行等价替换,最常见
的预处理有:档案包含,条件编译、布局控制和宏替换4种。
档案包含:#include 是一种最为常见的预处理,主要是做为档案的引用组合源程式正
文。
条件编译:#if,#ifndef,#ifdef,#endif,#undef等也是比较常见的预处理,主要是进
行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对档案重複
包含的功能。
布局控制:#pragma,这也是我们套用预处理的一个重要方面,主要功能是为编译程式
提供非常规的控制流信息。
宏替换: #define,这是最常见的用法,它可以定义符号常量、函式功能、重新命名
、字元串的拼接等各种功能。
预处理指令
预处理指令的格式如下:
# directive tokens
#符号应该是这一行的第一个非空字元,一般我们把它放在起始位置。如果指令一行放
不下,可以通过\进行控制,例如:
#define Error if(error) exit(1) 等价于
#define Error \
if(error) exit(1)
不过我们为了美化起见,一般都不怎幺这幺用,更常见的方式如下:
# ifdef __BORLANDC__
if_true<(is_convertible<Value,named_template_param_base>::value)>:
:
template then<make_named_arg, make_key_value>::type Make;
# else
enum { is_named = is_named_parameter<Value>::value };
typedef typename if_true<(is_named)>::template
then<make_named_arg, make_key_value>::type Make;
# endif
下面我们看一下常见的预处理指令:
#define 宏定义
#undef 未定义宏
#include 文本包含
#ifdef 如果宏被定义就进行编译
#ifndef 如果宏未被定义就进行编译
#endif 结束编译块的控制
#if 表达式非零就对代码进行编译
#else 作为其他预处理的剩余选项进行编译
#elif 这是一种#else和#if的组合选项
#line 改变当前的行数和档案名称称
#error 输出一个错误信息
#pragma 为编译程式提供非常规的控制流信息
下面我们对这些预处理进行一一的说明,考虑到宏的重要性和繁琐性,我们把它放到
最后讲。
四、档案包含指令:
这种预处理使用方式是最为常见的,平时我们编写程式都会用到,最常见的用法是:
#include <iostream> //标準库头档案
#include <iostream.h> //旧式的标準库头档案
#include "IO.h" //用户自定义的头档案
#include "../file.h" //UNIX下的父目录下的头档案
#include "/usr/local/file.h" //UNIX下的完整路径
#include "..\file.h" //Dos下的父目录下的头档案
#include "\usr\local\file.h" //Dos下的完整路径
这里面有2个地方要注意:
1、我们用<iostream>还是<iostream.h>?
我们主张使用<iostream>,而不是<iostream.h>,为什幺呢?我想你可能还记得我
曾经给出过几点理由,这里我大致的说一下:
首先,.h格式的头档案早在98年9月份就被标準委员会抛弃了,我们应该紧跟标準
,以适合时代的发展。
其次,iostream.h只支持窄字元集,iostream则支持窄/宽字元集。
还有,标準对iostream作了很多的改动,接口和实现都有了变化。
最后,iostream组件全部放入namespace std中,防止了名字污染。
2、<io.h>和"io.h"的区别?
其实他们唯一的区别就是搜寻路径不同:
对于#include <io.h> ,编译器从标準库路径开始搜寻
对于#include "io.h" ,编译器从用户的工作路径开始搜寻
五、编译控制指令:
这些指令的主要目的是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达
到版本控制、防止对档案重複包含的功能。
使用格式,如下:
1、
#ifdef identifier
your code
#endif
如果identifier为一个定义了的符号,your code就会被编译,否则剔除
2、
#ifndef identifier
your code
#endif
如果identifier为一个未定义的符号,your code就会被编译,否则剔除
3、
#if expression
your code
#endif
如果expression非零,your code就会被编译,否则剔除
4、
#ifdef identifier
your code1
#else
your code2
#endif
如果identifier为一个定义了的符号,your code1就会被编译,否则your code2就
会被编译
5、
#if expressin1
your code1
#elif expression2
your code2
#else
your code3
#enif
如果epression1非零,就编译your code1,否则,如果expression2非零,就编译y
our code2,否则,就编译your code3