6、带副作用的宏参数
什么是副作用?
-
定义:就是表达式求值是出现的永久性效果
-
理解:
-
x+1 这个代码重复多少次它输出的值都是相同的,因为它不改变x的值,进而也就没有副作用
-
x++ 这个代码就具有副作用,它会增加x的值,这个表达式下一次执行的时候就会产生一个不同的结果
-
示例:
#include "stdio.h"
-
#define MAX(a,b) ((a) > (b) ? (a) :(b))
int main(){
int x = 5;
int y = 8;
int z = MAX(x++,y++);
printf(“x = %d , y = %d , z = %d \n”,x,y,z);
}
```
![](https://whpbucket.oss-cn-beijing.aliyuncs.com/myImg/20201130091851.png)
-
分析:检查一下宏替换后产生的代码:
z = ((x++) > (y++) ? (x++) : (y++));
x 只增值一次,但是y增值了两次,而且a++会有先取值的特点运算
7、命名规定
一个常见的约定就是把宏的名字全部大写,如果写成小写,你就不得不去查看源文件以及它所包含的所有头文件来找出它的身份,而命名约定会使宏的身份很明显,如果宏使用可能具有副作用的参数时,这个约定尤为重要,它提醒程序员在使用宏之前把参数存储到临时变量中。
属性 | #define宏 | 函数 |
---|---|---|
代码长度 | 每次使用,宏的代码都会插入到程序中。除了非常小的宏之外,程序的长度将大幅度增长 | 函数代码只出现于一个地方,每次使用这个函数都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数调用/返回的额外开销 |
操作符 优先级 | 如果没有恰当的括号,上下文环境中邻近操作符的优先级很有可能会影响到运算操作 | 表达式求值更容易预测 |
参数求值 | 副作用的参数可能带来不可预料的影响 | 参数的副作用不会造成任何特殊问题 |
参数类型 | 宏与参数的类型无关,只要对参数的操作是合法的,它可以使用于任何参数类型 | 与类型有关,如果类型不同,就需要使用不同的函数,且函数是依托在参数类型上构建的 |
8、#undef
用于移除一个宏定义
#undef name
如果要对同一个名字的宏进行新定义,那么它的旧定义就一定要被移除
9、命令行定义
许多C编译器提供了一种能力,就是允许在命令行中定义符号,用于启动编译过程。当根据同一个编译文件启动编译一个程序时,可以根据这个特性调整对应的参数,进而让他适应不同的编译器甚至系统
- 示例:在UNIX编译器中,-D可以完成这项任务
- -Dname
- -Dname = stuff
- 那么这个程序的命令行可能就类似于
- cc -DARRAY_SIZE = 100 prog.c
- 如果要忽略一个定义的初始值在Unix 编译器中可以使用-U选项来执行这项任务
- 比如说:-Uname 就会让程序中符号name 的初始定义被忽略
四、条件编译
基本内容:
背景:在编译和调试的时候如果能选择一部分代码被完全编译或者完全忽略是一件非常方便的事,而条件编译就是用于实现这个目的,使用条件编译可以选择代码的一部分是被正常编译还是完全忽略。
定义:可用于选择代码的一部分被正常编译还是完全忽略。
结构:
#if constant-expression
statements
#endif
分析:
constant-expression
代表常量表达式,由预编译器进行求值,如果它的值是非零值(代表真)那么这块statements代码部分就会被正常编译,否则预编译器就会把它们删掉也就是不会编译
而这里的常量表达式,就是说它或者是一个字面量的常量值,或者是一个由**#define 定义的符号**,如果变量在执行之前无法获得它的值,那么它在常量表达式中就非法,这样的话在编译时产生的结果是不可预测的
示例:
#include "stdio.h"
#define DEBUG 1
int main(){
int x = 5;
int y = 8;
#if DEBUG
printf("x = %d \n",x);
#endif
printf("x = %d , y = %d , z = %d \n",x,y);
}
分析:
首先我定义了一个宏DEBUG是1,这里在条件编译语句中#if 后面DEBUG就是二进制的条件表达式
如果DEBUG是1则,#if和#endif中的代码段正常编译,如果DEBUG是0则不编译,在预编译的过程中会将这串代码删除
进而通过这样的形式就可以选择编译代码,而DEBUG在这里就是控制信号
结构二:
条件编译的另一个用途就是在编译时选择不同的代码部分,为了支持这个功能,#if指令还具有可选项#elif 和 #else语句
#if constant-expression
statements
#elif constant-expression
other statements
#else
other statements
#endif
#elif 子句出现的次数不限,每个constant-expression 只有当前面所有常量表达式都为假时才会被编译,#else 子句中的语句只有当前面的所有的常量表达式的值都是假时才会被编译,其他情况都会被忽略(也就是说和ifels语句的模式差不多,但是if后的条件表达式为0则不执行,但是会编译,而这里是选择性的不编译,在预编译的时候将不编译的内容删除。)
K&R C
补充:什么是K&R C和ANSI C
C语言是在1973年由Dennis M. Ritchie设计和实现完成的,后来1978年Ritchie和Bell实验室的另一个牛人Kernighan合写了著名的《The C Programming Language》从此C语言走向全世界,这本书再各种语言下都有译文,后来这本书定义的C语言就被称为 K&R C
C语言使用的越来越广泛,它出现了很多的新问题,人们要求对C进行标准化,后来这个标准化在美国国家标准局(ANSI)的框架中进行(1983-1988),最终1988年10月颁布ANSI标准,也就是后来的ANSI C标准,这个标准下定义的C语言被称为ANSI C
最初的K&R C并不具有#elif 指令。但是,在这类编译器中,可以使用嵌套的指令来获得相同的效果。
if (feature_selected = FEATURE1)
#if FEATURE_ENABLED_FULLY
feature1_function(arguments);
#elif FEATURE1_ENABLED_PARTALLY
feature1_partial_function(arguments);
#else
printf("To user this feature,send $39.35;allow ten weeks for delivery.\n")
#endif
这样,只需要编译一组源文件,当他们被编译时,每个当前版本所需的特性(或特性层次)符号被定义为1,其余的符号被定义为0
判断是否被定义:
测试一个符号是否已被定义是有可能的,在条件编译中完成这个任务往往更为方便,因为程序如果并不需要控制编译的符号所控制的特性,他就不需要被定义。这个测试可以通过下列任何一种方式进行:
#if defined(symbol)
#ifdef symbol
分析:这两个语句是同样的含义,如果symbol标识被定义了则编译下面的程序段
示例:
#include "stdio.h"
#define SYMBOL 1
int main(){
#ifdef SYMBOL
printf("SYMBOL has been defined\n");
#endif
#if defined(SYMBOL)
printf("SYMBOL HAS BEEN DEFINED\n");
#endif
}
分析:如果SYMBOL 这个符号已经被定义,则执行下面的print语句,#if defined 等价于 #ifdef 有着相同的作用
#if !defined(symbol)
#ifndef symbol
分析:这两个语句是相同的含义,如果symbol标识没有被定义则编制下面的程序段
#include "stdio.h"
int main(){
#ifndef SYMBOL
printf("SYMBOL has been defined\n");
#endif
#if !defined(SYMBOL)
printf("SYMBOL HAS BEEN DEFINED\n");
#endif
}
分析:如果SYMBOL 这个符号没有被定义,则执行下面的print语句,#ifndef 等价于 #if !defined 有着相同的作用
但是#if形式更强,因为常量表达式有时候可能包含额外的条件如:
#if X>0 || defined(ABC) && defined(BCD)
分析:如果定义了ABC和BCD 或者是X>0则执行下面的语句,通过多条件应用的方式实现
嵌套指令:
指令可以嵌套于另一个指令的内部
#include "stdio.h"
int main(){
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_of_option1();
#endif
#ifdef OPTION2
unix_version_of_option2();
#endif
#elif defined(OS_MASDOS)
#ifdef OPTION2
msdos_version_of_option2();
#endif
#endif
}
分析:可以看到在#if和#elif 之间还可以嵌套#ifdef,和其他的指令。这个例子中,操作系统的选择将决定不同的选项可以使用哪些方案,这个例子同时说明了预处理指令可以在他们前面添加空白,形成缩进,从而提高代码可读性
也可以在#if或者#elif后面添加注释从而更加清楚的区分嵌套指令
五、文件包含
#include 指令可以指定另一个文件的内容被编译,实现的效果就是另一个文件就好像实际出现于#include指令所在的位置一样,而预处理器的作用就是:删除这条指令,并用包含文件的内容取而代之。这样一个文件被包含在10个源文件中,那么这个文件就需要被编译10次
其实这样做的需要一定的开销,但是其实并不用担心,原因:
使用头文件模式的原因
- 这种额外开销并不大,通过使用#include 指令来指定头文件编译所需要的时间和把对应文件内容粘贴到源代码中编译所需要的时间编译起来差距微乎其微
- 这个开销只是在编译的时候才存在,所以对运行效率没有影响
- 放到头文件中,如果其他还需要这些声明,就没必要把代码拷贝到源文件中,相应的维护工作也变的简单了
- 使用头文件时,位于头文件中的内容都会被编译
- 程序设计和模块化思想的原则也支持这种方法
编译器支持两种不同类型的#include 文件包含:函数库文件和本地文件,而两者之间的区别其实很小
1)函数库文件包含
#include <filename>
filename并不存在任何限制,不过根据约定,标准库文件以一个.h后缀结尾(从技术上讲,函数库头文件并不需要以文件的形式存储,但对于程序员来说这样并非显而易见)
而编译器会通过观察由编译器定义的“一系列标准位置”来查找函数库头文件。
而典型的而言,UNIX系统上的C编译器在/user/include 目录查找函数头文件,这种编译器有一个命令行选项,允许把其他目录添加到这个列表中,这样就可以创建自己的头文件函数库。同样,查阅使用的编译器文档,就能看到系统在这个位置的规定
2)本地文件包含
#include "filename"
简单来说就是在包含函数库头文件的时候使用<>,同理在使用本地头文件的时候使用“”,从区分上讲,这样的使用很利于如何区分标准库函数还是一个本地头文件
而处理本地头文件基本策略就是在源文件所在的当前目录下查找,如果没有找到,编译器就会像查找函数库头文件一样在标准位置查找本地头文件。
理解:
对于预编译器查找头文件的行为:
一般情况下函数库头文件会以两种形式出现,一是尖括号,二是.h后缀,而预处理器查找函数库头文件的行为是在标准位置去查找
对于本地文件,则是用“”的形式出现,首先会通过在源文件的同一目录下查找,如果查找不到,就会以为是添加到了标准位置,就会去标准位置查找
可以看到对于本地头文件的文件形式并没有后缀要求,但是基本上就是要求为**.h 或者是 .c**,我也尝试了用其他的文件格式并不支持编译。
同时预编译添加头文件也支持通过相对路径和绝对路径导入头文件
示例:
#include <stdio.h>
#include "testW.c"
int main(){
lalaa();
}
示例:
#include <stdio.h>
#include "test/test.h"
int main(){
test();
}
示例:
#include <stdio.h>
#include "D:\Ccode\test2002\test\test.h"
int main(){
test();
}
思考:
这是在windows下有严格文件形式的情况下必须使用.h或者.c后缀,如果在linux下也许就不需要使用.c或者.h后缀
尝试:
我把test.c 改成了test,然后修改了main.c 为#include"test"
可以发现还是成功的运行了,说明确实在linux下没有严格文件要求的情况确实可以
3)嵌套文件包含
标准要求编译器必须支持至少八层的头文件嵌套,但它并没有限定嵌套深度的最大值。事实上,我们并没有很好的理由让#include指令的嵌套深度超过一层或者两层。
什么是嵌套文件包含:
因为嵌套#include文件的一个不利之处在于我们很难判断源文件之间真正的依赖关系,一个头文件可能被包含多次,比如说:
在a.h中:
#include "x.h"
.......
在b.h中:
#include "x.h"
........
在main.h中:
#include "a.h"
#include "b.h"
这样文件x.h其实就是被间接的包含了两次,这就是多重包含
多重包含一般出现在大型的系列程序中,因为它往往会需要使用很多头文件,因此发现这种情况并不容易,要解决这个问题,如果头文件都通过条件编译的形式这样去编写:
#ifdef _HEADERNAME_H
#define _HEADERNAME_H 1
/*
** All the stuff that you want in the header file
*/
#endif
那么这样就消除了多重包含的危险 。
理解:
当头文件第一次被包含时,它被正常的编译处理,符号_ HEADERNAME _被定义为1,如果头文件再次被包含,则通过条件编译,它的所有内容将被忽略,这里的 _ HEADERNAME _ 要按照被包含的文件名取名,进而避免与其它头文件使用相同的符号而引起冲突,这里 _ HEADERNAME _ 被定义成1 也好什么也好,空字符也好比如:
#define _HEADERNAME_H
这里就是定义了一个空字符串,但是使用和处理的目的是为了让这个符号被定义而不是值是多少
但是:即使这样预处理器还是会读入整个头文件,即使这个文件所有的内容将被忽略,由于这种处理将拖慢编译速度,所有尽可能的还是避免出现多重包含,不管它是不是由于嵌套的#include 文件导致
六、其他指令
#error 指令
用于生成错误信息
#erro text of error message
示例:
#include <stdio.h>
int main(){
#ifdef TESTMAX
printf("TESTMAX HAS BEEN DEFINED");
#elif TESTMIN
printf("TESTMIN HAS BEEN DEFINED");
#else
#error NO SYMBOL HAS BEEN DEFINED;
#endif
}
#line 指令
#line number "filename"
通知预处理器number是下一行输入的行号,修改_ _ LINE _ _符号的值,如果加上可选部分,预处理器将会把它作为当前文件的文件, 修改 _ _ FILE _ _符号的值
printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
#line 10
printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
#line 20 "hello.cpp"
printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
printf( "This code is on line %d, in file %s\n", __LINE__, __FILE__ );
简单来说就是修改文件运行的行数和文件名,修改运行状态的一个指令
# 无效指令
只被预处理器简单的删除,往往用于凸显某个指令的存在:
#
#include <stdio.h>
#
小结:
- #define 指令可以用于“重写”C语言,使他看上去像其他语言
- ##操作符用于把他两边的文本粘贴一起成为一个标识符
- 宏与类型无关,这是一个优点,宏的执行速度也快于函数,因为不存在函数调用/返回的开销,但宏会增加函数的长度,但函数却不会
- 宏的副作用会让宏在使用过程中出现不可预料的结构,而依托于数据类型的函数参数的行为更容易预测
- #undef 可以导致一个名字的原来定义被忽略
- #if 、#elif 、#else 、#endif 用于条件选择
- #ifdef #ifndef 可以用于判断一个标识是否被定义
- #include用于实现文本包含,文件包含有函数库头文件包含和本地文件包含,一种尖括号一种双引号,文件包含可以嵌套,但是很少需要进行超过一层或两层的文件包含嵌套,嵌套的包含文件将会增加多次包含同一个文件的为危险。
- #error 指令在编译时会产生一条错误信息,信息则是定义的文本
- #line 用于通知预编译器下一行输入的行号,加上可选内容可以修改参数和文件名
最后注意:
- 不要在宏定义的末尾加上分号,调用宏之后会自动加一个分号
- 宏定义参数积极的添加括号
- 记得在宏定义两边加上参数
- 避免用#define 指令定义可以用函数实现的很长序列的代码
- 避免使用#define 宏创建一种新的语言
- 积极采取宏全大写的命名约定,使程序员很容易看出某个标识符是否为#define 宏
- 合适就可以使用文件包含,不必要担心开销
- 自己的编写的头文件只应该包含一组函数或者数据的声明
- 嵌套的#include 文件使我们很难判断源文件之间的依赖关系