声明:经过长时间的学习,对宏定义尤其是条件编译这块存在盲区,特此整理笔记:此次内容参考《c++ 程序设计教程(第三版)》:清华大学出版社
一、编译预处理
编译预处理是在编译源程序之前,由预处理器对源程序进行加工处理工作,所谓预处理器,是包含在编译器中的预处理程序。如图所示:
源程序的中的编译预处理命令一律以#开头,回车键结束,每条命令占一行,并且通常放在源程序的开始部分。
编译预处理的作用是将源程序文件中的预处理命令进行处理,生成一个中间文件,编译系统再对此中间文件进行编译并生成目标代码,最后生成的目标代码中不含有预处理命令;
c++提供的预处理功能主要有宏定义、文件包含、条件编译,前两种比较常见,主要介绍最后一种
二、宏定义
2.1不带参数的宏定义
用一个指定的标识符来代表一个字符串,这个指定的标识符称为宏名,格式:
#define <宏名> <字符串>
#define PI 3.1415926
/*
*1.在进行预编译后,将程序中的所有宏名PI替换成3.1415926
*2.#define 定义的宏有效范围为定义处至本源文件结束,可以使用undef 命令终止宏定义的作用域
*3.宏定义语句的行末一般不加分号,因为他仅具有替换功能,并不是代码语句
*/
#define PI 3.1415926
/*
//do something;
*/
#undef PI //终止宏定义的作用域
2.2带参数的宏定义
宏名也可以带参数定义,支持参数替换,其格式为:
#define <宏名(参数列表)> <字符串>
#define S(a,b) a*b
/*
*参数表中的参数a,b称为宏名的形式参数,源程序语句中的宏名仍使用右端字符串替换,只不过替换时将字符串中的
*形式参数用语句中宏名所带的实际参数取代而已;
*/
double area;
area=S(3,2);
/*此时,3为a的实际参数,2为b的实际参数,则宏名S(3,2)经处理后,用字符串3*2替换再进行编译。即S(a,b)等同于a*b*/
/*带参数的宏替换过程可描述过程如下:*/
按#define命令行中指定的字符串从左至右置换宏名,字符串中的形参以相应的实参代替,字符串中非形参字符保持不变。
2.3带参数的宏定义与函数调用的区别
2.3.1.宏定义与函数调用计算机处理的时间不同
宏定义是编译预处理,计算机还未正式编译程序,就先处理预处理命令,因此,宏定义的实参对形参的代入是“机械”替换,同时宏定义的形参也不需要标注数据类型,因为系统这时候还根本不“认识”这些参数的类型,不进行语法检查,跟谈不上为其分配内存单元。
函数调用是在程序已经编译连接成为二进制码可执行程序后,在执行这个函数时发生的,此时需要分配形参的存储单元,完成实参到形参的具体值的传递,所以函数的形参要定义数据类型,且有时函数需要返回一个具体的值,函数也需要有明确的类型说明。
2.3.2.宏定义与函数调用的侧重点不同
宏定义在程序运行期间时已经不存在了,所以其目的仅仅是为了书写时使程序清晰、简洁。宏展开只占用编译时间,不占用运行时间,宏展开后使源程序增长,占用程序的“空间”
被调函数无论被调用了多少次,其生成的机器代码长度是不变的。但当程序运行时,每次调用该函数时,系统都要完成调用前的准备和收尾工作,如分配形参单元,保存调用时刻现场地址、参数、值传递、函数值返回、恢复现场,这些工作都要占用一定的运行时间,因此函数调用占用程序的“时间”
三、文件包含
文件包含用#include命令,预处理后将指令中指明的源程序文件嵌入到当前源文件的指令位置处,格式为:
#include <文件名>
//or
#include "文件名"
含有文件包含命令的源程序,在编译预处理时将所包含的文件全部内容复制到该命令行处,生成一个中间文件,然后编译连接这个文件,得到实际上包含两个(或多个)源程序的可执行程序。
注意:
1.一个#include命令只能包含一个文件。
2.文件包含命令所包含的文件必须是文本文件(ASCII)文件,一般是c++源文件或系统库文件,不可以是可执行程序(.exe)或者是目标程序(.obj)
3.1 include包含格式;
文件包含有两种格式,用#include<文件名>的格式时,预处理编译器就直接到存放c++系统的文件目录中查找要包含的文件,若找不到,编译系统给出出错信息
。一般来说系统的标准头文件和库文件都是用这种方式包含的,如#include。
用#include"文件名"的格式时,预编译处理器首先在当前编译文件所在目录进行搜索,如果找不到,再去c++系统目录中进行搜索;也可以在包含命令中指定首先搜索的目录,如#include"/home/.../file2.cpp",可指定在"/home/.../"目录下搜索file2.cpp,若找不到,再去c++系统目录下搜索;如果还找不到,就给出出错信息。
<一般这种方式用来包含用户自己建立的文件>
1.用#include”文件名”的格式时,预编译处理器首先在当前编译文件所在目录进行,或给定目录进行搜索,如果找不到,再去c++系统目录**中进行搜索,如果还找不到,就给出出错信息。
2.用#include<文件名>的格式时,预处理编译器就直接到存放c++系统的文件目录中查找要包含的文件,若找不到,编译系统给出出错信息`。
四、条件编译
在通常情况下,源程序中所有的语句都要被编译,生成机器码可执行程序。但有时希望源程序的某些语句在满足一定条件下才被编译,或者说对源程序的部分语句有选择进行编译。
条件编译指令有两类:
一是根据宏名是否定义来确定是否编译某些程序段;另一类是根据表达式的值来确定被编译的程序段;
4.1宏名作为编译的条件
#ifdef 宏名
程序段1
<#else
程序段2>
#endif
/*#ifdef 与ifndef 作用一样,只是选择的条件相反*/
其中<>中的内容可以省略。
如果源程序前面的定义了宏名,则编译程序段1,否则编译程序段2;
例如,在调试程序时,常常需要输出一些变量在程序运行过程中的值,以此来检查程序运行中的错误,而调试完成后不需要输出这些信息,就可以将输出这些值的语句段作为条件语句;
#include<iostream>
using namespace std;
int main()
#define DEBUG //定义了宏名DEBUG
{
int n = 5, s = 1;
for (int i = 0; i < n; i++)
{
s = s * i;
#ifdef DEBUG //如果有定义则编译
cout << "i=" << i << '\t' << "s=" << s << endl; //条件编译语句
#endif //条件编译语句结束
}
cout << "n=" << s << endl;
return 0;
}
输出:
i=0 s=0
i=1 s=0
i=2 s=0
i=3 s=0
i=4 s=0
n=0
#include<iostream>
using namespace std;
int main()
//#define DEBUG //注释该行,即该行不成立
{
int n = 5, s = 1;
for (int i = 0; i < n; i++)
{
s = s * i;
#ifdef DEBUG //条件编译的条件不成立
cout << "i=" << i << '\t' << "s=" << s << endl; //该行不被编译,不生成机器码
#endif
}
cout << "n=" << s << endl;
return 0;
}
输出:
n=0
2.表达式的值作为编译条件
#ifdef 表达式
程序段1
<#else
程序段2>
#endif
其中<>中的内容可以省略。
如果表达式的值非0(表达式应该是一些常量的计算),则编译程序段1,否则编译程序段2;
#include<iostream>
using namespace std;
int main()
{
int n = 5, s = 1;
for (int i = 0; i < n; i++)
{
s = s * i;
#if 1 //为真,执行
cout << "i=" << i << '\t' << "s=" << s << endl;
#endif
}
cout << "n=" << s << endl;
return 0;
}