C_C语言宏的定义与使用范例

来源:互联网 发布:linux find 文件 编辑:程序博客网 时间:2024/05/29 18:53

下面我们来共同学习C语言宏的语法及其使用:
就我个人的使用经验而言,C语言的宏在语法和含义上市比较简单的,但是如果你想能够使用好他却着实是一件很不容易的事情,他会在使用它的过程中出现各
种各样的问题,让你头痛不已,好下面就让我们一起进入C语言的宏开发之旅:
C语言的预编译:
如果我们想学习C语言的宏,就不得C(包括C++)语言的一个概念--->预编译。那什么是预编译呢?
预编译又称为预处理,是做些代码文本的替换工作。处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等就是为编译做的预
备工作的阶段主要处理#开始的预编译指令。那么C语言都有哪些宏命令呢?
#include、#define、#if、#ifndef、#else和#endif基本上就有这些宏指令,我们下面就一一介绍这些宏指令。

1、#include<xxxx.xxx>
该指令指示编译器将xxx.xxx文件的全部内容插入此处。若用<>括起文件则在系统的INCLUDE目录中寻找文件,若用""括起文件则在当前目录中寻找文件。一般来说,该文件是后缀名为"h"或"cpp"的头文件。
也就是说include不仅能够引入我们常见的头文件(系统头文件和用户自定义头文件)还能在文件的定内插入一个文件我们可以看到示例:


上面的代码示例我们可以看到,#include不仅可以用在文件的开头处同样可以用在.c文件的函数或代码定义区,其实正如我们一开始讲的那样#include就是将它引用
的那个文件拷贝到我们的引用处而已,我们可以看到上面的代码经过我们的预编译后的样子:



通过上面的预编译结果也证明了我们的结论。

2、#ifndef #else #endif用于检查和定义一些宏
这些指令一般这样配合使用:
#if defined(标识) //如果定义了标识
要执行的指令
#else
要执行的指令
#endif
在头文件中为了避免重复调用(比如说两个头文件互相包含对方),常采用这样的结构:
#if !(defined XXX) //XXX为一个在你的程序中唯一的标识符,
//每个头文件的标识符都不应相同。
//起标识符的常见方法是若头文件名为"abc.h"
//则标识为"abc_h"
#define XXX
真正的内容,如函数声明之类
#endif
PS:我们之所以可以使用宏来判断一个文件有没有被引用,主要源于宏的一个特性:首先宏是没有作用域的而一旦创建那么就全局通用,这样我们就可以使用宏来限定每个头文件仅能被引用一次。

3、#define
下面我们再介绍#define三种最常用的方法:
第一种是定义常数,如#define max 100,则max代表100;
我们说这是一种定义常量的方法就是为一个标识符赋一个值而这个标识符是没有类型的实际上他就是一个替换标识符在程序的预编译时会将标识符的值原样替换到引用
此标识符的地方,我们可以看到代码:

我们来看预编译后的代码:

那么使用这种方法来定义一个“常量”好不好呢?当然是欠佳的。大家知道我为什么要给前面的常量一词加双引号吗?其实他并不是真正意义上的常量,而就是一个替换
标识符,不仅没有类型,更不会参与程序的运行。听了上面的解释不知各位有没有明白。我们简单的说,就是在预编译是这个标识符就会被他所定义的那个值替换掉,在
接下来的编译与运行的过程中,那个标识符是不存在的(也就是说一旦程序由于这个出现了错误,将没有一个标识符可以来提示是这个常量出现了问题,那此时程序就会
出现很多令人难以理解的错误信息,给我们程序的调试带来极大的麻烦)。而且#define定义的常量是没有作用域限制的,也就是说无论你在哪里定义了这个常量,这个常
量在程序的任何一个地方都是可以使用的(即使你在一个函数中定义了一个常量,那么你在函数外面也是可以直接使用的)。这就会造成很多不确定性的因素,而对我们
的程序的安全性产生严重的影响。那么我们应该怎么办?使用const,所以除非你已经找不到更加简便的方法了不要轻易使用#define来定义和使用一个常量。

第二种是定义"函数",如#define get_max(a, b) ((a)>(b)?(a):(b)) 则以后使用get_max(x,y)就可以得到x和y中较大的数(这种方法存在一些弊病,见注2)。
我们可以使用此种方法来定义一个函数,不过事实上这并不是一个函数其实他不过只是一个代码块而已,既没有函数名名也没有返回值同样不具备参数列表。更不会在我
们的文件中产生函数,他只是在我们调用此宏时会替换出一个代码块,而这个代码块会配合我们好好工作,但同样也会产生很多意想不到的错误,下面我们就来通过几个
代码示例来演示采用宏来替换代码块的用法及常见错误:
示例一、模拟foreach:
如果使用过面向对象语言(Java、C#都支持foreach,特别是C++2011标准也开始对foreach进行支持)的读者我想应该不会对foreach(或类似)陌生,他就是一个迭代器,
不断从一个序列中取出对象然后对取出的这个对进行处理。显然C语言对此是不支持的,那么我们就看看使用C语言的宏是如何来实现此类应用的,我们看代码:

我们看到实际上他还是使用了for循环,只不过一些复杂的定义都被我们使用宏进行了预定义,在我们使用时无需再进行复杂的定义只需要拿过来直接使用,然后再在其
后的一对大括号内定义对取出对象的操作即可。
下面我们就来看看这个宏预编译后的代码:

下面我们将来看看一些宏定义引发的逻辑错误,这些逻辑错误并不会引起程序异常或崩溃,只是会造成程序的结果错误,其实这种逻辑错误是最让我们头疼的错误:
我们来看第一种引发的错误:


从上面的代码中我们看到第一次运行的结果是正确的,但第二次的运行结果却与我们预料中的结果大不相同了。那么为什么第二次引发了错误呢?我们看到我们的代码再
给第二次传参时使用了一个表达式而非变量,宏就会将我们的表达式进行直接替换结果就是5+5*5+5而非我们想要的(5+5)*(5+5)这就提醒我们在进行宏定义的代码块
时最好能为每个变量添加括号包裹,以防不测事件的发生。但是如果我们为每个变量都添加了括号包裹就一定安全了吗?我们来看下面一个更头疼的逻辑错误,他将证明
即使你为每个变量都添加了括号包裹依旧不能完全避免逻辑错误,而且本示例引发的错了会造成我们的代码执行过程混乱不确定,这在程序设计上是绝对不能忍受的问题。
好我们看代码示例:


不知各位有没有看出什么问题。如果我们使用一个正常的数字当然不会出现问题,如果我们使用表达式由于我们使用了括号包裹,上一个例子中已经展示我们也不再单独
讨论,这次我们在代码中使用了三目运算符,而在参数给出的是带自增运算符的参数,我们看到当a大时a自增两次,而b自增一次,如果b更大的话,结果恰恰相反。我们
发现这段代码的执行过程是不确定的,所以他不是一段健壮的代码,还请各位慎用。
同样的我们还是要看看他的预编译代码:


第三种是定义"宏函数",如#define GEN_FUN(type) type max_##type(type a,type b){return a>b?a:b;} ,使用时,用GEN_FUN(int),则此处预编译后就变成
了 max_int(int a,int b){return a>b?a:b;},以后就可以使用max_int(x,y)就可以得到x和y中较大的数.比第三种,增加了类型的说明。
这种方式使用宏主要是他可以再我们的源文件中产生一个函数,然后我们就可以抛弃这个宏了,然后我们就可以直接使用他给我们生成的函数就可以了,而且这个函数就
和我们正常定义的函数没什么不同。如果我们要定义此类的“宏函数”我们在这还想先介绍两个在“宏函数”内部使用的运算符"##"、"/"首先他们可不是我们在代码中使用的
'#'和'/',"##"配合我们定义宏时,宏内部的参数就变成了字符串连接符,具体用法:例如我们定义了一个宏,#define getMax(type) get##typeMax 如果我们使用时
传入的type是"Age"而后面的get##typeMax就会变成getAgeMax,所以##起到一个连接字符串的作用,好我们再看'\'其实他也是一个连接符,我们回忆,当我们定义一个函
数时大多时候应该都有数行,而我们我们在定义“宏函数”时为了代码的清晰也应该有数行,但是#define仅能作用于一行所以我们可以使用'/'来连接多行定义。
好,下面就让我们一睹“宏函数”的风采:

通过上述代码我们见识了宏函数的定义与使用,那么下面我们还是来看看预编译之后的效果吧:

最后我们再来讨论,我们是不是能够有什么办法来更好的处理这个问题呢?其实在C语言里没有什么太好的方法不过在C++中有一种叫做模板的机制可以实现类似的功能例如
下面的代码:
template<T>
void fun(T t){
 .............
}
这样T就可以是任意一种类型了,他会根据你在调用这个函数的时候传进来的参数来判断他应该是什么类型,然后自动根据此类型做出相应的操作。

我们从以上讨论中可以认识到,使用宏有很多好处,但是他的问题也是不少,所以我们使用宏时,应该谨慎使用,首先想想好有没有什么其他较简单的方法能够实现相同的
功能,只有在没有其他办法不得已的情况下才使用宏,而且在使用时一定要认真考虑其代码的各种情况,以确保我们代码的健壮性和安全性。
好了我们今天关于宏的介绍就完毕了,还请各位认真思考。

0 0
原创粉丝点击