周边编程一○五:歪打正着
/*
版权信息
作者:牛刀小试
首发论坛:http://0ginr.com/bbs/
首发时间:二○○八年五月八日
*/
上一讲与大伙讲到了模板,感觉似乎有点跑题,不过既然牛刀将其定义到宏的范围之内,肯定不会平白无故,如果大家仔细体会一下,那么我们的模板仅仅是一个特殊的宏,只不定义的手段不是用 #define 而是使用的 template 而已
template <class T>
但是不同的是什么呢?不同的是,这里的T不是我们传进参数或者直接用宏的办法去替换,而是由编译器自动分析下面程序的数据类型,从而用相应的数据类型去替换,仅此而已,我们下面看两种不同的定义:
代码:
定义一:
template <class T>
T max(T a,T b);
定义二:
#define T int
T max(T a,T b);
大家知道,如果我们只是使用 int 类型的话,那么两种定义是一模一样,可惜的是,下一种宏定义就特别地死板,只能产生一个整型的求最大值函数,而定义一就不同了,定义一中的 T 其实我们没有去定义,而是让编译器在编译的过程中去发现我们需要定义什么样的数据类型,再根据相应的数据类型去生成相应的最值函数,仅此而已。
那么大家动动脚趾头想想,定义一的替换是在编译之前还在编译之后呢?
显然编译器在没有编译的时候是不可能知道我们一个数据的数据类型的,那么第一种定义就肯定在编译之后喽,那么就怪了,编译器既然编译过了,那么这种替换还有意思吗?不是说编译过后就全变成二进制代码了吗?
非也非也,原来啊,我们编译器在编译程序时,不是一次成功的,而是要进行若干次,每编译一次称着一“遍”,一般的编译器的编译,少则两“遍”多则需要好几遍。
而我们这种识别类型的工作,部分空间分配的工作,就在第一遍完成,而需要编译器扫描后再替换的一些工作,就需要放在第一遍扫描过后去进行喽。
于是,我们看下面的程序:
代码:
#include <stdio.h>
template <class T>
T max(T a,T b)
{
if(a>b)return a;
return b;
}
void main()
{
printf("max(2,3)=%d\n",max(2,3));
}
程序在第一遍编译时,遇到了 T ,发现了这个T,编译器也不知道什么类型,但后来发现使用的 max 函数,使用到了这个 T ,那么好,编译器就会继续编译,去找这个函数,于是在 printf 函数中找到了 max(2,3) ,于是,根据其中的数据分析出其中数据为整型,于是,编译器就确定了 T 的类型为 int ,于是在第一遍结束之后,上面的 template<class T> 与 #define T int 就没有区别了。
第二遍开始之前,程序首先启动替换系统,将 max 函数中的 T 全部用 int 去替换掉,然后再重新编译,于是,程序顺利编译通过,过程就是这样的。
什么意思想必大家也明白了吧?计算机是不会回头的,所以一遇到这种不确定的东东,则必须进过多遍的编译,才能编译完成,仅此而已。
从这一点来说,大家应该明白,为什么模板的定义要用 template ,而不用 #define 了吧?因为 #define 是告诉编译器,我是一个普通的宏,编译器你只需要作相应替换就行了,而 template 则告诉编译器,编译器你行行好,你先编译一遍,确定我定义的数据类型,然后来来替换我,好吗?
于是,同样的宏,就有了两种不同的替换方案,歪打正着,都实现了同一个目的——替换。
对于类的替换也是一样,同样是通过多遍扫描才能编译通过的,于是,与正常程序不同的是,这种编译可能需要的时间要长一些,但对于编程者来说,无疑是一个福音。
我们编程的目的就是尽量让能够交给计算机去计算的工作去交给计算机去完成,最大化地做到我们人为的计算机减少到最少,从这一点上来说,如果不用宏,我们亏啊,呵呵,于是,我们下面就要专门地讨论讨论宏在我们编程中的作用——
作用一:提高通用性
首先来说,我们做程序不可能一成不变的,宏的最直接的一个例子就是:
代码:
#define PI 3.14
……
好处一:偷懒,这无疑是最大的好处,一个 PI 要少打好多的数字,呵呵。
好处二:整体的精度改变,我们一旦程序做好了,想要改变精度,我们只需要做一件事,那就是作如下改变:
#define PI 3.141592654
好处三:使得程序变得通用,例如我们需要在其他部分需要一个求圆面积的代码,正好这儿有一份,那么复制过去就行了,那么 PI 的精度一改,整个代码就可以满足我们的要求喽。
通用性好的最大的好处就是可能只知道程序的功能,就可以自由复制,并且只需要通过几个宏,就可以改变整个程序以满足我们的需要,从而使得我们编程变成了一个不是经常地敲字母的事喽,而变成了一个粘粘贴贴的东东喽,呵呵,需要改动的很少。
可以想象,只要宏定义得足够丰富,那么我们做程序可能就来得格外地方便。
作用二:跨平台性
例如有朋友跟牛刀讨论过,VC下, void main(); 可以使用,而其他编译环境下,通不过,那么我们就可以做如下的定义:
#define Main void main(){
#define EndMain }
如果在 GCC 的编译环境下,我们可以作如下的定义:
#define Main int main(){
#define EndMain return 0;}
那么,我们下面的程序:
代码:
Main
//程序代码
EndMain
这样的程序就很通用了,当然,你可能会觉得,那多费事?还要到相应的平台下面,改变相应的宏,真麻烦。
标准考虑到了我们的尴尬,于是,为我们的宏,做了专业的条件编译指令,这一讲先告诉两个,大家先用着,呵呵
代码:
#include <stdio.h>
#define VC 1
#ifdef VC
#define Main void main(){
#define EndMain }
#endif
#ifdef GCC
#define Main int main(){
#define EndMain return 0;}
#endif
Main
EndMain
大家看,#ifdef 就是:如果定义了后面的宏的话,就执行下面的宏定义,直到 #endif 结束
当然,如果你改变了平台,你只需要 #define GCC 2 就一切可以了。
对了,就到这儿提一句啊,我们一直在强调,宏不是我们的C语言编程部分,也就是说,不是语句,所以行未是不需要分号“;”作为结束标志的,这一点尤为重要,如果有分号的话,也会一并被替换掉,切记,切记。
对了,如果你改变了别人的 VC 变成了你的 GCC ,那么请你也要将别人的 VC 的定义给删掉哟,否则出问题的,呵呵,就是宏定义重复了。
其实啊,在现实的编程中,我们完全没有必要担心这种情况,因为平台的定义都是在专业文件中,并且随着专业的平台发布的,所以我们只要像这样定义:
代码:
#include <stdio.h>
#ifdef VC
#define Main void main(){
#define EndMain }
#endif
#ifdef GCC
#define Main int main(){
#define EndMain return 0;}
#endif
Main
EndMain
那么这个代码就拿到哪个平台下就都通用喽。
当然,我们这儿是为了与大家讲明白,可以通过这种宏的定义去使得我们的代码有平台通用性,其实这种通用性不需要我们自己去考虑,每个平台下都会有大段的代码,去专门处理这种事情,从而会使得我们的同样的 C 或 C++ 程序,拿到哪个平台上都能够顺利地编译通过。
当然,你如果有机会去看看相应的代码,你可能就会觉得那些代码怪怪的了,很可能是一头雾水,不过也没有必要,我们对待这些特殊的代码,我们唯一的方案就是——不要去看她。
说真的,牛刀原来为名人机做一些程序的时候,过后再看看这些程序,都会怀疑这些程序是不是我自己写的,也就是说,编程序是一个很享受的过程,而看(读别人的)程序则必然是一个很枯燥的过程。
所以牛刀一再建议大家,写程序时,一定要加注释,为了人类的编程事业,建议大家要舍得花一点时间。
作用三:方便性
经典的宏,往往会被传抄,拷贝,C语言不支持直接的二进制编程,于是,下面的代码就成了经典之举:
代码:
#define b00000000 0x00
#define b00000001 0x01
……
#define b11111110 0xfe
#define b11111111 0xff
如果你用到位运算,像这样的代码,有一份,那么编程真是太方便了,有时候牛刀在处理二进制的时候,就会想啊想(二进制到十六进制的表记不住啊,牛刀喜欢掰手指头,呵呵,别笑啊)。
就像上面这样的宏,可以说什么任何的技术含量,可惜这种宏是最珍贵的。
还有一种宏,是人们用计算机凭空生成一个图片用的,当然,牛刀以前也曾经在《点阵图片》那一章与大家做过一幅,可那真的是牛刀一个一个的十六进制算出来的,可牛刀在网上看过一个就相当经典了,人家这样定义:
代码:
#define X )*2+1
#define _ )*2
#define s ((((((((((((((((0
static unsigned short stopwatch[] =
{
s _ _ _ _ _ X X X X X _ _ _ X X _ ,
s _ _ _ X X X X X X X X X _ X X X ,
s _ _ X X X _ _ _ _ _ X X X _ X X ,
s _ X X _ _ _ _ _ _ _ _ _ X X _ _ ,
s _ X X _ _ _ _ _ _ _ _ _ X X _ _ ,
s X X _ _ _ _ _ _ _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ _ _ _ _ _ _ X X _ ,
s X X _ X X X X X _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ X _ _ _ _ _ X X _ ,
s X X _ _ _ _ _ X _ _ _ _ _ X X _ ,
s _ X X _ _ _ _ X _ _ _ _ X X _ _ ,
s _ X X _ _ _ _ X _ _ _ _ X X _ _ ,
s _ _ X X X _ _ _ _ _ X X X _ _ _ ,
s _ _ _ X X X X X X X X X _ _ _ _ ,
s _ _ _ _ _ X X X X X _ _ _ _ _ _ ,
s _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
};
上面的一段代码是牛刀复制过来的,至少牛刀觉得这段代码相当精彩。
作用四:空间效率高
宏与我们正常编程之间最大的不同就是,宏仅仅是一个替换,并且永远留在程序文件中,在编译完成的 exe 文件中,没有宏的概念,于是,当一段宏没有被替换的时候,那仅仅是一段宏,一段没有用的废代码而已,而一旦被替换,这种宏就可以变成数据,变成代码,变成函数,变成程序,甚至变成一些恐怖的恶魔。
曾经在于大家讲过的 MFC 中的消息响应机制的时候,就曾经与大家说过,我们的 MFC 类中,响应消息,没有使用消息响应机制,向父类中传递消息,也就是说,那不是虚函数,那是什么?
那就是宏:
代码:
IMPLEMENT_DYNCREATE(CMainFrame, CFrameWnd)
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
//{{AFX_MSG_MAP(CMainFrame)
ON_WM_CREATE()
ON_WM_NCLBUTTONDOWN()
ON_WM_NCMOUSEMOVE()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
大家看,其中的响应到的部分宏,就会变成程序,而没有响应到的部分,那段代码,那段宏就与我们没有关系,这样,微软就做到了,响应多少的消息,就做多大的程序,而不会使得程序因为我们的后台库的原因而变得很巨大。
在这一点上,VC做得就比VB要优秀的多,VB不管你做多么小的程序,都要将一整套的VB库带着,打包发行,才能够真正的使用VB做开发出来的一套软件。
提到这儿,牛刀再多说两句,我们的 API 编程是直接调用 API 函数产生的,没有库的概念,所以我们 API 编出的程序可以拿到任何一台装有 Windows 的电脑上直接运行,而 MFC 程序就不同了, MFC 有 DEBUG 模式 和 Release 模式,也就是调试模式与发行模式,为什么会有这两种模式呢?
这就在于,调试模式是没有带有 MFC 库的,所以我们的调试模式产生的 exe 文件如果想要发行的话,就要将 MFC 的相应的动态链接库带在一起去发行才行,然而,没有必须,一般我们在发行之前,使用 Release 模式,重新编译一下就行了,因为发行模式是将所有用到的 MFC 库都打包到了同一个程序中了,所以在任何机上都可以运行。
所以理论上说,Release 模式文件应该比 Debug 模式文件要大,然而事实不是,牛刀刚刚看了一下,发行模式的文件要比调试模式文件要小得多,这就矛盾了,为什么库越多,程序反而越小呢?
原来啊,调试模式中是有好多好多的废信息的,而发行模式将这些东东全都给删了,就是这个原因,简单吧?呵呵,也就是说,发行模式是将调试模式给简化喽。
那么是怎么简化的呢?我们调试模式大家还知道吗?我们可以将程序用调试方式一句一句地运行,这是怎么实现的呢?我们的断点是怎么设置的呢?这些信息,exe 文件中必须都有,让编译器知道运行到哪儿喽,因为 VC 和VB不同,VB是解释执行的程序,而VC是编译执行的程序,所以说,我们的VB是可以输入一行就执行一行的,从而,有用过 VB经历的人应该都知道,如果我们某一行语法如果有毛病,VB编译器立马就给报错了,而这一点,VC就做不到,所以说,VC的调试模式下,必须加入好多好多的冗余信息,去将我们在程序运行中的一些数据给导出来,甚至调试模式下会给一些特殊的宏,或者是函数,例如断言,反正牛刀也没用过那些,就不与大家说了,说也是空谈,你说是不,呵呵。
而这一系列的冗余信息,在编译成发行模式时,全部删去。
对了,大家知道,我们在调试模式下定义一个字符串,如果不赋值,就是会一串“烫”是吗?而发行模式就不会给我们的内存进行统一地设置了,于是,在那些模式中,我们的所有内存一下子就都暴露在了我们面前。做个程序试试:
代码:
#include <stdio.h>
void main()
{
char ch[20];
printf("%s\n",ch);
}
调试模式下,显然是:
代码:
烫烫烫烫烫烫烫烫烫烫?
那么我们按 编译/批构件.../全部重建
然后我们到相应的目录下,就会有一个 Release 目录,这个目录下,就是发行版的程序,我们进去当然需要用 DOS 形式进去,我们去试试:
代码:
D:\PVc\Test\Release>TEST
D:\PVc\Test\Release>
怎么样?什么都没有,牛刀不服,去 Debug 目录再执行一下看一看:
代码:
D:\PVc\Test\Release>cd ..\Debug
D:\PVc\Test\Debug>test
烫烫烫烫烫烫烫烫烫烫?
D:\PVc\Test\Debug>
看出区别来了吧?
严重跑题。
文章评论(0条评论)
登录后参与讨论