C/C++预处理指令

C/C++常见的预处理指令如下:

C/C++编译系统编译程序的过程为预处理、编译、链接。预处理器是在程序源文件被编译之前根据预处理指令对程序源文件进行处理的程序。预处理器指令以#号开头标识,末尾不包含分号。预处理命令不是C/C++语言本身的组成部分,不能直接对它们进行编译和链接。C/C++语言的一个重要功能是可以使用预处理指令和具有预处理的功能。C/C++提供的预处理功能主要有文件包含、宏替换、条件编译等。

  1. #空指令,无任何效果
  2. #include包含一个源代码文件
  3. #define定义宏
  4. #undef取消已定义的宏
  5. #if如果给定条件为真,则编译下面代码
  6. #ifdef如果宏已经定义,则编译下面代码#ifdef如果宏已经定义,则编译下面代码
  7. #ifndef如果宏没有定义,则编译下面代码#ifndef如果宏没有定义,则编译下面代码
  8. #elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
  9. #endif结束一个#if……#else条件编译块#endif结束一个#if…#else条件编译块
  10. #error停止编译并显示错误信息 #error停止编译并显示错误信息

什么是预处理指令?

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

预处理指令是在编译器进行编译之前进行的操作.预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。可见预处理过程先于编译器对源代码进行处理。在很多编程语言中,并没有任何内在的机制来完成如下一些功能:在编译时包含其他源文件、定义宏、根据条件决定编译时是否包含某些代码(防止重复包含某些文件)。要完成这些工作,就需要使用预处理程序。 尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立于编译器的。预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。

#include包含一个源代码文件

这个预处理指令,是见得最多的一个,简单说一下,第一种方法是用尖括号把头文件括起来。这种格式告诉预处理程序在编译器自带的或外部库的头文件中搜索被包含的头文件。第二种方法是用双引号把头文件括起来。这种格式告诉预处理程序在当前被编译的应用程序的源代码文件中搜索被包含的头文件,如果找不到,再搜索编译器自带的头文件。采用两种不同包含格式的理由在于,编译器是安装在公共子目录下的,而被编译的应用程序是在它们自己的私有子目录下的。一个应用程序既包含编译器提供的公共头文件,也包含自定义的私有头文件。采用两种不同的包含格式使得编译器能够在很多头文件中区别出一组公共的头文件。

#define定义宏

有关#define这个宏定义,在C语言中使用的很多,因为#define存在一些不足,C++强调使用const来定义常量。宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。记住仅仅是进行标识符的替换。下面列举一些#define的使用:

  1. 用#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)宏的使用一定要细心,需要把参数小心的用括号括起来,
 * 因为宏只是简单的文本替换,不注意,容易引起歧义错误。
*/
  1. 宏定义的错误使用
#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))
 * 所以,尽量使用小括号,将参数括起来。
*/
  1. 宏参数的连接
#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
*/
  1. 用宏得到一个字的高位或低位的字节
#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位即可。
*/
  1. 用宏定义得到一个数组所含元素的个数
#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;
}
/*
 *总的大小除以每个类型的大小
 */

关于#define宏的使用,应该特别小心,尤其是含有参数计算的时候如小2示例,最保险的做法将参数用括号括起来。

#ifdef,#ifndef,#endif…的使用

以上这些预编译指令,都是条件编译指令,也就是说,将决定那些代码被编译,而哪些不被编译。

  1. 示例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预处理指令。*/
  1. ifdef 和 #ifndef
#include <stdio.h>
#define DEBUG
main()
{
#ifdef DEBUG
    printf("yes ");
#endif
#ifndef DEBUG
    printf("no ");
#endif
}
//#ifdefined等价于#ifdef;
//#if!defined等价于#ifndef
  1. #else指令
    在这里插入图片描述
  2. #elif指令
    在这里插入图片描述
  3. 预定义宏

DATE,字符串常量类型,表示当前所在源文件的编译日期,输出格式为Mmm dd yyyy(如May 27 2006)。

TIME,字符串常量类型,表示当前所在源文件的编译日期,输出格式为hh:mm:ss(如09:11:10)。

FILE,字符串常量类型,表示当前所在源文件名,且包含文件路径。

LINE,整数常量类型,表示当前所在源文件中的行号。

FUNCTION,字符串常量类型,表示当前所在函数名。

这些预定义宏在调试程序时是很有用的,因为你可以很容易的知道程序运行到了那个文件的那一行,是那个函数。

除了可以在源文件的开头使用#define定义宏外,还可在编译器项目属性“预处理器”属性页定义宏。这种宏定义方式支持数字和字符串,一般形式为:标识符=数字或字符串常量,如果省略=以及后面的内容,则宏名标识符默认为整数1。定义宏的方法是在“预处理器定义”属性输入宏定义内容,多个宏定义之间用分号隔开。“预处理器定义”中的宏定义要先于源文件中的宏定义被处理,其有效范围为整个项目,除非在源文件中遇到重定义或用 #undef 指定取消宏定义名,否则该宏定义名在源文件中一直保持有效。

  1. 其他一些指令

#error

一个项目的模块儿之多,源文件之大,代码之多,那么其中的宏, 也会很多. 免不了冲突定义.这时候, 我们就需要编译器能及早的告诉我们.那就是在编译的时候.#error就可以这么实现:

/** 如果JOE宏没有定义,那么编译就此结束, 编译器就会显示红色的错误 */
#ifndef JOE
#error "JOE is not exits"
#endif

#line

#line指令用于重新设定当前由__FILE__和__LINE__宏指定的源文件名字和行号。
#line一般形式为#line number “filename”,其中行号number为任何正整数,文件名filename可选。#line主要用于调试及其它特殊应用,注意在#line后面指定的行号数字是表示从下一行开始的行号。

#pragma

#pragma指令可能是最复杂的预处理指令,它的作用是设定编译器的状态或指示编译器完成一些特定的动作。
#pragma一般形式为#pragma para,其中para为参数,下面介绍一些常用的参数。
#pragma once,只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。
#pragma message(“info”),在编译信息输出窗口中输出相应的信息,例如#pragma message(“Hello”)。
#pragma warning,设置编译器处理编译警告信息的方式,例如#pragma warning(disable:4507 34;once : 4385;error:164)等价于#pragma warning(disable:4507 34)(不显示4507和34号警告信息)、#pragma warning(once:4385)(4385号警告信息仅报告一次)、#pragma warning(error:164)(把164号警告信息作为一个错误)。
#pragma comment(…),设置一个注释记录到对象文件或者可执行文件中。常用lib注释类型,用来将一个库文件链接到目标文件中,一般形式为#pragma comment(lib,"*.lib"),其作用与在项目属性链接器“附加依赖项”中输入库文件的效果相同。

工作中使用的宏:

  1. 常常使用宏来调试代码:

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

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

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

通过以上类似的方法, 可以防止由于过多的修改代码, 而把代码修改的一塌糊涂. 建议修改代码的时候, 做到保护好以前的代码, 尽量不进行代码的删除操作. 切记, 能不删除, 就不删除…不要养成随手就删除的习惯. 要养成使用宏和注释代码的习惯.

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
 
/** 不仅使用在头文件的包含. 而且,对于不同的系统平台. 你也可以使用不同的代码结构. */

小结

预处理就是在进行编译的第一遍词法扫描和语法分析之前所作的工作。说白了,就是对源文件进行编译前,先对预处理部分进行处理,然后对处理后的代码进行编译。这样做的好处是,经过处理后的代码,将会变的很精短。