第八章 编译预处理(一)

这是是比较独特的一章,在这一章中详细介绍了C语言项目的组成以及一个可执行文件的生成步骤,以这些知识点为开始,先后提出了宏定义、文件包含、条件编译三个C语言所提供的编译预处理功能。

宏定义为程序编写的灵活性带来了极大的便利,文件包含为减少重复代码的编写提供了必要的支持。条件编译所支持的跨平台特性,使编写的程序不需要改动任何代码就能够在不同的操作系统平台上直接编译运行。

一、宏定义

回顾一下前面讲过的内容,针对一个项目,有两种说法:

1)一个项目,由一个或者多个源程序文件组成。

2)一个项目,可以通过编译、链接(Xcode等负责做这件事)最终生成一个可执行文件。

这里谈到的编译,是以一个个的源程序文件(.cpp文件)为单位进行的,每个源程序文件都会编译成一个目标文件,(目标文件扩展名可能是.O,也可能是.obj等,这与操作系统类型有关)。如果源程序文件有多个,则会编译生成多个目标文件,然后将这些目标文件进行链接,最终生成一个可执行文件。

那么,编译这个动作或者称为阶段,都干了什么事呢?一般来说,编译阶段会做如下几件事:

1)预处理

2)编译,包括词法分析、语法分析、目标代码生成、优化等

3)汇编,产生.O(.obj)目标文件

假设一个项目中包含a.cpp,b.cpp,c.cpp三个源文件,下图展示了最终生成一个可执行文件的过程。

预处理:

在软件开发过程中,根据实际需要会在源程序文件中写入一些特殊代码(特殊命令),这些特殊代码有一些特殊能力,提供一些特殊功能,编译系统(Xcode中内置)会先对这些特殊代码做预先的处理,这就叫"预处理",处理的结果,再和源程序代码一起进行上面步骤中提到的编译、汇编等系列动作。

C语言一般提供三种预处理功能,宏定义、文件包含、条件编译,这三种预处理功能也是通过在源程序文件写人代码来实现的,只不过这些代码比较特殊,都是以"#"开头。本节先要讲解“宏定义”。

1、不带参数的宏定义

不带参数的宏定义是用来做什么的呢?用一句话描述:用一个指定的宏名来代表内容。宏名,其实就是一个标识符,其一般形式为:

# define 宏名 被替换的内容

# define PI3.1415926               //末尾没有分号

作用:用PI来代替3.1415926,那么在程序源码中写的是PI(也就是上述般形式中的宏名),在预处理阶段,所有在该#define语句行之后的代码中出现的PI都会被替换成3.415926。有几点说明:

1)#define就是宏定义命令,实现了用一个简单名字(宏名)代替一个很长内容的效果,在预编译时将宏名替换成指定内容的过程称为"宏展开",也可以称为"宏替换”,上面用PI替换成3.1415926的过程就是宏展开。

2)利用#define,增加了代码修改的方便性,为代码修改提供极大便利,这能力在开发中被频繁地使用。例如,如果将来PI不等于3.145926,而是等于2.58,那么只需要修改一行代码,整个程序中出现PI的地方就都被替换成了2.58,这也叫作提高了程序的可移植性。

例:

针对上面的代码,在进行宏展开时,PI会被直接替换成3.1415926,并不做语法检查,所以替换完后直接参与乘法运算,即"ftmp=2*PI;",相当于”ftmp=2*3.1415926;"。有几点说明:

1)宏名一般大写字母表示,这是一种习惯,建议遵照这个习惯。

2)宏定义其实并不是C语言语句(虽然有时候会称其为语句)。不必在行末加分号,如果加分号则连分号一起被替换了。

3)#define命令出现在程序中函数的外面,宏名的有效范围是#define之后到本源程序文件结束,不能跨文件使用,如果在另外一个源程序文件中使用,则需要在另外一个源程序文件中也做相同定义,或者把这些#define定义统一放到一个公共文件(如.h头文件)里,并用#include(后面会讲)把这个公共文件包含到每个源程序文件中去。一般来说#define命令都写在源程序文件开头部分,函数之前。

4)可以用#undef命令终宏定义的作用域,不过#undef命令用得比较少。

例:

5)用#define进行宏定义,还可以引用已定义的宏,可以层层置换。

例:

6)字符串内的字符即使与宏名相同,也不进行替换。

2、带参数的宏定义

前面讲的是不带参数的宏定义,只是进行简单的内容替换,那这里要讲的带参数的宏定义,就不仅是进行简单的内容替换,还要进行参数替换,一般形式为:

# define 宏名(参数表) 被替换的内容

作用:用右边的"被替换的内容"代替宏名(参数表)",但具体怎样替换,后面会详细讲,和不带参数的宏定义相比,这里多了个参数表,在被替换的内容中,一般都会包含参数表中所指定的参数。

# define S(a,b) a*b

例:int Area=S(3,2);

在上面的例子中,用了宏S(3,2),系统是怎样替换的呢?把分别代替宏定义中的形参a、b,最终用3*2替换了S(3,2),所以程序代码"int Area=S(3,2);"就等价于"int Area=3*2;"。

刚刚说过,一般"被替换的内容"中都会包含参数表中所指定的参数,但不包含也是可以的,但若不包含,那么通过参数表传进去这个数就没什么意义了。

# define S(a,b) a

例:int Area=S(3,2);    ///宏展开后相当"int Area=3;",显然2这个数字就毫无意义了

带参数的宏定义展开置换的总结:

一般形式中提到的"被替换的内容",要从左到右处理。如果"被替换的内容"中有"宏名"后列出的形参,如a、b则将程序代码中相应的实参(可以是常量、变量或者表达式)代替形参,如果"被替换的内容"的项并不是"宏名"后列出的形参,则保留,如上面a*b中的“*”就会被保留。

例:

有几点说明:

1)如果代码中出现area=S(1+5),替换后会变成3.1415926*1+5*1+5,这肯定不对,程序代码的原意是替换后变成3.1415926*(1+5)*(1+5),为了解决这个问题,要在形参外面加一个括号。

例:

#define S(r) PI*(r)*(r)

2)宏定义时,宏名和带参数的括号之不能加空格,否则,空格之后的内容都作为被替代内容的一部分。

例:

#define S (r) PI*(r)*(r)

这样,S成为不带参数的宏定义,代表被替换内容(r) PI*(r)*(r),显然是不对的。

带参数的宏和函数挺像的,宏也有实参和形参,所以两者不好区分?实际上两者之间还是有很不相同的地方。

总结一下:

1)函数调用是先求出实参表达式的值,然后传递给形参,带参数的宏只进行简单的内容替换,宏展开时并不求值,如上面S(1+5),宏展开时并不求1+5的值,只是原样用实参替换掉形参。

2)函数调用是在程序运行阶段执行到该函数时才执行其中的代码,这涉及比如为所调用的函数分配临时内存等一系列工作。但宏展开是在编译阶段进行的,而且展开时也并不分配内存,当然也不存在”值传递”"返回值"等只有在函数调用中才存在的说法。

3)宏的参数没有类型这个说法,只是一个符号,展开时用指定内容替换。例如#define S (r) PI*(r)*(r),其中r是没有类型这种概念和说法的。

4)宏展开每进行一次,源程序代码都会有所增多,如area=S(1+5),在宏展开时会被替换成"area=3.1415926*(1+5)*(1+5);",显然代码变多了,所以使用宏的次数如果增多,源程序代码就会增多,但函数调用不会使源程序代码增多。

5)宏展开只占用编译时间,不占用运行时间,而函数调用占用运行时间(分配内存、传递参数、执行函数体、返回值等)。

有时会用宏来代替一些较复杂的语句,看看如下相对复杂一点的范例,求x和y的最大值:

还有能代替多行语句的宏定义写法。

例:注意末尾的"\",用来表示下一行代码和本行代码本是同一行,这种用法在一定程度上能简化程序书写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值