常见的预处理命令:
- #include 包含一个源代码文件
- #define 定义宏
- #undef 取消已定义的宏
- #if 如果给定条件为真,则编译下面代码
- #ifdef 如果宏已经定义,则编译下面代码
- #ifndef 如果宏没有定义,则编译下面代码
- #elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
- #endif 结束一个#if……#else条件编译块
- #error 停止编译并显示错误信息
10.编译C++程序时,编译器自动定义了一个预处理器名字__cplusplus(注意前面有两个下划线),因此可以根据这个来判断该程序是否是C++程序,以便有条件地包含一些代码,如:
#ifndef MYHEAD_H #define MYHEAD_H #ifdef __cplusplus //若是C++程序,则需要包含C的库 extern "C" { #endif int DMpostprocessing(); #ifdef __cplusplus } #endif #endif
11.在编译C程序时,编译器会自动定义预处理常量__STDC__。当然__cplusplus和__STDC__ 不会同时被定义;
12.另外两个比较有用的预定义常量是__LINE__(记录文件已经被编译的行数)和__FILE__(包含正在被编译的文件名称)。使用如下:
if(element_count==0) cerr<<"Error:"<<__FILE__ <<":line"<<__LINE__ <<"element_count must be non-zero.\n";
13. __DATE__:编译日期,当前被编译文件的编译日期
14. __TIME__:编译时间,当前被编译文件的编译时间
什么是预处理指令?
预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
以前没有在意的学者注意了,预处理指令是在编译器进行编译之前进行的操作.预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。在很多编程语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码(防止重复包含某些文件)。要完成这些工作,就需要使用预处理程序。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
#include包含一个源代码文件
这个预处理指令,我想是见得最多的一个,简单说一下,第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。采用两种不同包含格式的理由在于,编译器是安装在公共子目录下的,而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。
头文件中应该写什么
通过上面的讨论,我们可以了解到,头文件的作用就是被其他的 .cpp 包含进去的。它们本身并不参与编译,但实际上,它们的内容却在多个 .cpp 文件中得到了编译。通过"定义只能有一次"的规则,我们很容易可以得出,头文件中应该只放变量和函数的声明,而不能放它们的定义。因为一个头文件的内容实际上是会被引入到多个不同的 .cpp 文件中的,并且它们都会被编译。放声明当然没事,如果放了定义,那么也就相当于在多个文件中出现了对于一个符号(变量或函数)的定义,纵然这些定义都是相同的,但对于编译器来说,这样做不合法。
所以,应该记住的一点就是,.h头文件中,只能存在变量或者函数的声明,而不要放定义。即,只能在头文件中写形如:extern int a; 和 void f(); 的句子。这些才是声明。如果写上 int a;或者 void f() {} 这样的句子,那么一旦这个头文件被两个或两个以上的 .cpp 文件包含的话,编译器会立马报错。(关于 extern,前面有讨论过,这里不再讨论定义跟声明的区别了。)
但是,这个规则是有三个例外的:
- 一,头文件中可以写 const 对象的定义。因为全局的 const 对象默认是没有 extern 的声明的,所以它只在当前文件中有效。把这样的对象写进头文件中,即使它被包含到其他多个 .cpp 文件中,这个对象也都只在包含它的那个文件中有效,对其他文件来说是不可见的,所以便不会导致多重定义。同时,因为这些 .cpp 文件中的该对象都是从一个头文件中包含进去的,这样也就保证了这些 .cpp 文件中的这个 const 对象的值是相同的,可谓一举两得。同理,static 对象的定义也可以放进头文件。
- 二,头文件中可以写内联函数(inline)的定义。因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。如果内联函数像普通函数一样只能定义一次的话,这事儿就难办了。因为在一个文件中还好,我可以把内联函数的定义写在最开始,这样可以保证后面使用的时候都可以见到定义;但是,如果我在其他的文件中还使用到了这个函数那怎么办呢?这几乎没什么太好的解决办法,因此 C++ 规定,内联函数可以在程序中定义多次,只要内联函数在一个 .cpp 文件中只出现一次,并且在所有的 .cpp 文件中,这个内联函数的定义是一样的,就能通过编译。那么显然,把内联函数的定义放进一个头文件中是非常明智的做法。
- 三,头文件中可以写类(class)的定义。因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的定义的要求,跟内联函数是基本一样的。所以把类的定义放进头文件,在使用到这个类的 .cpp 文件中去包含这个头文件,是一个很好的做法。在这里,值得一提的是,类的定义中包含着数据成员和函数成员。数据成员是要等到具体的对象被创建时才会被定义(分配空间),但函数成员却是需要在一开始就被定义的,这也就是我们通常所说的类的实现。一般,我们的做法是,把类的定义放在头文件中,而把函数成员的实现代码放在一个 .cpp 文件中。这是可以的,也是很好的办法。不过,还有另一种办法。那就是直接把函数成员的实现代码也写进类定义里面。在 C++ 的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为内联的。因此,把函数成员的定义写进类定义体,一起放进头文件中,是合法的。注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中,这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的 .cpp 文件包含,这个函数成员就被重定义了。
头文件中的保护措施
考虑一下,如果头文件中只包含声明语句的话,它被同一个 .cpp 文件包含再多次都没问题——因为声明语句的出现是不受限制的。然而,上面讨论到的头文件中的三个例外也是头文件很常用的一个用处。那么,一旦一个头文件中出现了上面三个例外中的任何一个,它再被一个 .cpp 包含多次的话,问题就大了。因为这三个例外中的语法元素虽然"可以定义在多个源文件中",但是"在一个源文件中只能出现一次"。设想一下,如果 a.h 中含有类 A 的定义,b.h 中含有类 B 的定义,由于类B的定义依赖了??? A,所以 b.h 中也 #include了a.h。现在有一个源文件,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h 包含进来了,也把 b.h 包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。你也许会认为这是程序员的失误——他应该知道 b.h 包含了 a.h ——但事实上他不应该知道。
使用 "#define" 配合条件编译可以很好地解决这个问题。在一个头文件中,通过 #define 定义一个名字,并且通过条件编译 #ifndef...#endif 使得编译器可以根据这个名字是否被定义,再决定要不要继续编译该头文中后续的内容。这个方法虽然简单,但是写头文件时一定记得写进去。
#define定义宏
有关#define这个宏定义,在C语言中使用的很多,因为#define存在一些不足,C++强调使用const来定义常量。宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。记住仅仅是进行标识符的替换。下面列举一些#define的使用:
- 用#define实现求最大值和最小值的宏
#include <stdio.h> #define MAX(x,y) (((x)>(y))?(x):(y)) #define MIN(x,y) (((x)<(y))?(x):(y)) int main(void) { #ifdef MAX //判断这个宏是否被定义 printf("3 and 5 the max is:%d\n",MAX(3,5)); #endif #ifdef MIN printf("3 and 5 the min is:%d\n",MIN(3,5)); #endif return 0; } /* * (1)三元运算符要比if,else效率高 * (2)宏的使用一定要细心,需要把参数小心的用括号括起来, * 因为宏只是简单的文本替换,不注意,容易引起歧义错误。 */
2、宏定义的错误使用
#include <stdio.h> #define SQR(x) (x*x) int main(void) { int b=3; #ifdef SQR//只需要宏名就可以了,不需要参数,有参数的话会警告 printf("a = %d\n",SQR(b+2)); #endif return 0; } /* *首先说明,这个宏的定义是错误的。并没有实现程序中的B+2的平方 * 预处理的时候,替换成如下的结果:b+2*b+2 * 正确的宏定义应该是:#define SQR(x) ((x)*(x)) * 所以,尽量使用小括号,将参数括起来。 */
3、宏参数的连接
#include <stdio.h> #define STR(s) #s #define CONS(a,b) (int)(a##e##b) int main(void) { #ifdef STR printf(STR(VCK)); #endif #ifdef CONS printf("\n%d\n",CONS(2,3)); #endif return 0; } /* (绝大多数是使用不到这些的,使用到的话,查看手册就可以了) * 第一个宏,用#把参数转化为一个字符串 * 第二个宏,用##把2个宏参数粘合在一起,及aeb,2e3也就是2000 */
4、用宏得到一个字的高位或低位的字节
#include <stdio.h> #define WORD_LO(xxx) ((byte)((word)(xxx) & 255)) #define WORD_HI(xxx) ((byte)((word)(xxx) >> 8)) int main(void) { return 0; } /* * 一个字2个字节,获得低字节(低8位),与255(0000,0000,1111,1111)按位相与 * 获得高字节(高8位),右移8位即可。 */
5、用宏定义得到一个数组所含元素的个数
#include <stdio.h> #define ARR_SIZE(a) (sizeof((a))/sizeof((a[0]))) int main(void) { int array[100]; #ifdef ARR_SIZE printf("array has %d items.\n",ARR_SIZE(array)); #endif return 0; }
#ifdef,#ifndef,#endif...的使用
以上这些预编译指令,都是条件编译指令,也就是说,将决定那些代码被编译,而哪些不被编译。
- 示例1:
#include <stdio.h> #include <stdlib.h> #define DEBUG int main(void) { int i = 0; char c; while(1) { i++; c = getchar(); if('\n' != c) { getchar(); } if('q' == c || 'Q' == c) { #ifdef DEBUG//判断DEBUG是否被定义了 printf("We get:%c,about to exit.\n",c); #endif break; } else { printf("i = %d",i); #ifdef DEBUG printf(",we get:%c",c); #endif printf("\n"); } } printf("Hello World!\n"); return 0; } /*#endif用于终止#if预处理指令。*/
2、ifdef 和 #ifndef
#include <stdio.h> #define DEBUG main() { #ifdef DEBUG printf("yes "); #endif #ifndef DEBUG printf("no "); #endif } //#ifdefined等价于#ifdef; //#if!defined等价于#ifndef
3、#else指令
4、#elif指令
5、其他一些指令
#error指令将使编译器显示一条错误信息,然后停止编译。 #line指令可以改变编译器用来指出警告和错误信息的文件号和行号。 #pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。
工作中经常这样使用宏:
1. 常常使用宏来调试代码:
#if 0 ///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译) #else ///< 新的代码(或函数) #endif #ifndef JOE_DEBUG ///< 新的代码(或函数) #else ///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译) #endif #ifdef Q_DEBUG ///< 新的代码(或函数) #else ///< 旧的代码(或函数) (旧的代码, 将会被预处理的时候,屏蔽掉, 不进行编译) #endif<br><br>
通过以上类似的方法, 可以防止由于过多的修改代码, 而把代码修改的一塌糊涂. 建议修改代码的时候, 做到保护好以前的代码, 尽量不进行代码的删除操作. 切记, 能不删除, 就不删除...不要养成随手就删除的习惯. 要养成使用宏和注释代码的习惯.
2. 使用宏来根据不同的平台包含不同的文件. 很多时候, 我们的代码是需要跨系统平台编译和运行的. 比如: 一个小功能代码, 需要既可以在Win下面运行, 还要可以在Max, linux上面运行. 可是, 因为系统的不一样, 有些时候, 头文件的包含的名字是不一样的. 所以,这时候, 就是用到了宏. 因为我们使用编程工具分不同的系统平台, 编程工具自身的环境就会包含不同平台的系统宏, 假设OS_Win, OS_Mac, OS_Linux 分别代码三种系统不同的宏. 而且,Win版本的编程工具中已经定义了OS_Win, 类似的Mac下, 编程工具定义的是OS_Mac, Linux...
#ifdef OS_Win #include <windows.h> #endif #ifdef OS_Mac #include <mac.h> #endif #ifdef OS_Linux #include <linux.h> #endif /** 不仅使用在头文件的包含. 而且,对于不同的系统平台. 你也可以使用不同的代码结构. */