详解C/C++预处理器 .-阿里云开发者社区

开发者社区> 开发与运维> 正文

详解C/C++预处理器 .

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

       1、文件包含
       预处理指令#include用于包含头文件,有两种形式:#include <xxx.h>,#include "xxx.h"。
尖括号形式表示被包含的文件在系统目录中。如果被包含的文件不一定在系统目录中,应该用双引号形式。
在双引号形式中可以指出文件路径和文件名。如果在双引号中没有给出绝对路径,则默认为用户当前目录中的文件,此时系统首先在用户当前目录中寻找要包含的文件,若找不到再在系统目录中查找。
对于用户自己编写的头文件,宜用双引号形式。对于系统提供的头文件,既可以用尖括号形式,也可以用双引号形式,都能找到被包含的文件,但显然用尖括号形式更直截了当,效率更高。
./表示当前目录,../表示当前目录的父目录。

       2、宏替换
       ① 宏定义
       宏定义的作用一般是用一个短的名字代表一个长的代码序列。宏定义包括无参数宏定义和带参数宏定义两类。宏名和宏参数所代表的代码序列可以是任何意义的内容,如类型、常量、变量、操作符、表达式、语句、函数、代码块等。但要尤其注意的是宏名和宏参数必须是合法的标识符,其所代表的内容及意义在宏展开前后必须一直是独立且保持不变的,不能分开解释和执行。
       无参数宏定义。用一个用户指定的称为宏名的标识符来代表一个代码序列,这种定义的一般形式为#define 标识符 代码序列。其中#define之后的标识符称为宏定义名(简称宏名),在宏定义#define之前可以有若干个空格、制表符,但不允许有其它字符,宏名与代码序列之间用空格符分隔。
带参数宏定义。带参数宏定义进一步扩充了无参数宏定义的能力,这时的宏展开既进行宏名的替换又进行宏参数的替换。带参数的宏定义的一般形式为#define 标识符(参数表) 代码序列,其中参数表中的参数之间用逗号分隔,在代码序列中必须要包含参数表中的的参数。在定义带参数的宏时,宏名与左圆括号之间不允许有空白符,应紧接在一起,否则变成了无参数的宏定义。带参数宏调用提供的实在参数个数必须与宏定义中的形式参数个数相同。
宏定义的有效范围称为宏名的作用域,宏名的作用域从宏定义的结束处开始到其所在的源代码文件末尾。宏名的作用域不受分程序结构的影响。如果需要终止宏名的作用域,可以用预处理指令#undef加上宏名。
宏名一般用大写字母,以便与变量名区别。如有必要,宏名可被重复定义,被重复定义后,宏名原先的意义被新意义所代替。
       宏定义代码序列中必须把""配对,不能把字符串""拆开。例如#define NAME "vrmozart不合法,应为#define NAME "vrmozart"。
       宏定义代码序列中可以引用已经定义的宏名,即宏定义可以嵌套。
       ② 多行宏
       宏定义在源文件中必须单独另起一行,换行符是宏定义的结束标志,因此宏定义以换行结束,不需要分号等符号作分隔符。如果一个宏定义中代码序列太长,一行不够时,可采用续行的方法。续行是在键入回车符之前先键入符号\,注意回车要紧接在符号\之后,中间不能插入其它符号,当然代码序列最后一行结束时不能有\注意多行宏在调用时只能单独一行调用,不能用在表达式中或作为函数参数
       ③ 宏展开
       预处理器在处理宏定义时,会对宏进行展开(即宏替换)。宏替换首先将源文件中在宏定义随后所有出现的宏名均用其所代表的代码序列替换之,如果是带参数宏则接着将代码序列中的宏形参名替换为宏实参名。宏替换只作代码字符序列的替换工作,不作任何语法的检查,也不作任何的中间计算,一切其它操作都要在替换完后才能进行。如果宏定义不当,错误要到预处理之后的编译阶段才能发现
源代码中的宏名和宏定义代码序列中的宏形参名必须是标识符才会被替换,即只替换标识符,不替换别的东西,像注释、字符串常量以及标识符内出现的宏名或宏形参名则不会被替换。例如:
       #define NAME vrmozart,源代码//NAME、/*NAME*/、"NAME"、my_NAME_blog中的宏名NAME都不会被替换。
       #define BLOG(name) my_name_blog="name",宏定义代码序列中的宏形参名name也都不会被替换。
       如果希望宏定义代码序列中标识符内出现的宏形参名能够被替换,可以在宏形参名与标识符之间添加连接符##,在宏替换过程中宏形参名和连接符##一起将被替换为宏实参名##用于把宏参数名与宏定义代码序列中的标识符连接在一起,形成一个新的标识符。例如:
       #define BLOG(name) my_##name,BLOG(vrmozart)表示my_vrmozart
       #define BLOG(name) name##_ blog,BLOG(vrmozart)表示vrmozart_ blog
       #define BLOG(name) my_##name##_blog,BLOG(vrmozart)表示my_vrmozart_ blog
       如果希望宏定义代码序列中的宏形参名被替换为宏实参名的字符串形式(即在宏实参名两端加双引号"),而不是替换为宏实参名,可以在宏定义代码序列中的宏形参名前面添加符号#。#用于把宏参数名变为一个字符串形式。例如:
       #define STR(name) #vrmozart,STR(vrmozart)表示"vrmozart"
       当宏参数是另一个宏的时候,需要注意的是宏定义代码序列中有用#或##的宏参数是不会再展开
       ④ 宏的独立性
       在宏定义中说过,宏名和宏形参名所代表的内容及意义在宏展开前后必须一直是独立且保持不变的,不能分开解释和执行。其原因如下,在宏调用时,用宏定义的代码序列替换宏名,用宏实参名替换宏形参名。替换后,宏定义的代码序列就与源文件中相邻的代码自然连接,宏实参名也与代码序列中相邻的代码自然连接,宏定义的代码序列和宏实参名的独立性就不一定依旧存在。例如:
       #define SQR(x) x*x,希望实现表达式的平方计算。
       对于宏调用p=SQR(y),能得到希望的宏展开p=y*y。但对于宏调用q=SQR(u+v),得到的宏展开是q=u+v*u+v。显然,后者的展开结果不是程序设计者所希望的。为能保持宏实参名替换后的独立性,应在宏定义中给形式参数加上括号。进一步,为了保证宏名调用的独立性,作为算式的宏定义代码序列也应加括号。SQR宏定义改写成#define SQR(x) ((x)*(x))才是正确的宏定义。
       ⑤ 宏调用与函数调用的区别
       函数调用在程序运行时实行,而宏展开是在编译的预处理阶段进行;函数调用占用程序运行时间,宏调用只占编译时间;函数调用对实参有类型要求,而宏调用实在参数与宏定义形式参数之间没有类型的概念,只有字符序列的对应关系;函数调用可返回一个值,宏调用获得希望的代码序列。另外,函数调用时,实参表达式分别独立求值在前,执行函数体在后。宏调用是实在参数字符序列替换形式参数。
       ⑥ 预定义宏
       __DATE__,字符串常量类型,表示当前所在源文件的编译日期,输出格式为Mmm dd yyyy(如May 27 2006)。
       __TIME__,字符串常量类型,表示当前所在源文件的编译日期,输出格式为hh:mm:ss(如09:11:10)。
       __FILE__,字符串常量类型,表示当前所在源文件名,且包含文件路径。
       __LINE__,整数常量类型,表示当前所在源文件中的行号。
       __FUNCTION__,字符串常量类型,表示当前所在函数名。
       这些预定义宏在调试程序时是很有用的,因为你可以很容易的知道程序运行到了那个文件的那一行,是那个函数。
       用户除了可以在源文件的开头使用#define定义宏外,还可在编译器项目属性“预处理器”属性页定义宏。这种宏定义方式支持数字和字符串,一般形式为:标识符=数字或字符串常量,如果省略=以及后面的内容,则宏名标识符默认为整数1。定义宏的方法是在“预处理器定义”属性输入宏定义内容,多个宏定义之间用分号隔开。“预处理器定义”中的宏定义要先于源文件中的宏定义被处理,其有效范围为整个项目,除非在源文件中遇到重定义或用 #undef 指定取消宏定义名,否则该宏定义名在源文件中一直保持有效。

       3、条件编译指令
       一般情况下,在进行编译时对源程序中的每一行都要编译,但是有时希望程序中某一部分内容只在满足一定条件时才进行编译,如果不满足这个条件,就不编译这部分内容,这就是条件编译。条件编译主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到多个版本控制、防止对文件重复包含的功能。#if,#ifndef,#ifdef,#else,#elif,#endif是比较常见条件编译预处理指令,可根据表达式的值或某个特定宏是否被定义来确定编译条件。
       ① 指令意义
       #if 表达式非零就对代码进行编译;
       #ifdef 如果宏被定义就进行编译;
       #ifndef 如果宏未被定义就进行编译;
       #else 作为其它预处理的剩余选项进行编译;
       #elif 这是一种#else和#if的组合选项;
       #endif 结束编译块的控制。
       ② 常用形式
       #if_#endif形式:
       #if 常数表达式 或 #ifdef 宏名 或 #ifndef 宏名
          程序段
       #endif
       如果常数表达式为真或者该宏名已定义或者该宏名未定义,则编译后面的程序段;否则就不编译,跳过这段程序。
       #if_#else_#endif形式:
       #if 常量表达式 或 #ifdef 宏名 或 #ifndef 宏名
          程序段1
       #else
           程序段2
       #endif
       如果常数表达式为真或者该宏名已定义或者该宏名未定义,则编译后面的程序段1;否则编译后面的程序段2。
       #if_#elif_#endif形式:
       #if 常量表达式1
         程序段1
       #elif 常量表达式2
          程序段2
         .......
       #elif 常量表达式n
          程序段n
       #endif
       注意这种形式#elif不可以用于#ifdef和#ifndef中,但#else可以
       ③ 表达式
       预处理器表达式包括的操作符主要涉及到单个数的操作(+、-、~、<<、>>)、多个数的运算(*、/、%、+、-、&、^、|)、关系比较(<、<=、>、>=、==、!=)、宏定义判断(defined)、逻辑操作(!、&&、||),其优先级和行为方式与C++表达式操作符相同。对于预处理器表达式,一定要记住它们是在编译器预处理器上执行的,是在编译前进行的
       下表列出了操作符的优先级顺序,从上到下,优先级从高到低。可以用圆括号改变优先级顺序。

操作符的优先级

       例子:#ifndef 与#if !defined意义相同,#ifdef 与#if defined意义相同。

       4、其它预处理指令
       除了上面讨论的常用预处理指令外,还有三个不太常见的预处理指令:#line、#error、#pragma,下面分别介绍。
       ① #line
       #line指令用于重新设定当前由__FILE__和__LINE__宏指定的源文件名字和行号。
       #line一般形式为#line number "filename",其中行号number为任何正整数,文件名filename可选。#line主要用于调试及其它特殊应用,注意在#line后面指定的行号数字是表示从下一行开始的行号
       ② #error
       #error指令使预处理器发出一条错误消息,然后停止执行预处理。
       #error 一般形式为#error info,如#error MFC requires C++ compilation。
       ③ #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"),其作用与在项目属性链接器“附加依赖项”中输入库文件的效果相同。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章