我不知道各位,一提起C++,第一感觉是什么?而据俺的观察,许多人几乎成了“谈C色变”。不管是C还是C++,一直以来都被很多人视为相当难学的玩意儿,幸好只是一个C++,没有C--,C**和C//,不然,那还得了?曾记得,某年某月某日,在某论坛上看到有牛人说“C++++”,当时我猜想这是啥玩意儿,后来经过一番顺虅摸瓜,深入调查发现,原来有人作了这么个有趣的等式:C# == C++++。
显然,这个等式也不太正确,C#不仅继承了C++一些特性,也继承了Delphi中的和VB中的一些优点。
好了,这个等式意义不大,咱们不扯它了。前面我写了许多和移动开发的文章,估计现在移动市场泡沫也差不多膨胀起来了,你说这泡沫,泡到什么程度呢?据说连压根连程序都没写过的人,也嚷着说:移动开发,我要(幸好不是官人,不然动机不纯)。
这很容易让人联想到“全民炒股”的创世纪大笑话,中国人貌似很喜欢这样,一曰跟风,二曰盲从。这二者合并起来,正好为市场本质上的“自发性,盲目性”等特征作了相当有力的诠释,难怪罗斯福总统说必要时还得宏观调控。在1932年如果还不调控的话,估计到了1945年,在太平洋战场上完蛋的不是零式战斗机了,该是地狱猫战斗机了,呵呵。
不管是移动互联网,还是云计算,各位还是理性地考虑一下吧,认为有需要才进行投资,目前来说,移动市场绝大部分还是在娱乐上,要说真要和商业模式融合,估计现在的手机和平板电脑还达不到这个指标,未来几年有可能开始和商业平台对接,今年的话,不太可能,如果你计划把你的商业应用向移动平台扩展(我这里用扩展,千万不要转移,不然会丢失原来的市场),那么,你现在可能要考虑你现有的应用程序框架到底有多少可以进行扩展了。
这扩展一事说起来容易,做起来可不轻松,记得去年我在F公司工作,尝把ERP的功能,从小的模块开始,向Web/电子商务平台整合,技术上是没问题的,但业务逻辑上有可能会一败涂地。所以,有时候,咱们做开发的,学一学市场营销、财务会计、企业管理、HR,甚至是文学艺术,对我们的成长还是有好处的,你只会写程序,有时候很容易“当局者迷”,金庸老先生在小说里常常把这个称为“走火入X”,不知道欧阳锋大哥算不算。
很多人学C语言,就是败在她的“石榴裙”下的,指针(Pointer),这里我为什么要把英文原名写出来了,我目的想让你思考一下,我们常叫它指针,但是,这个翻译到底合不合理?
在学习C和C++时,很多人会被指针给弄得“六神无主”,虽然大家在许多书上都看到,指针就是用来保存地址的,是吧。然很多人就是无法理解。我这里告诉大家一个技巧,凡是遇到抽象的东西,你就不妨尝试着在客观存在的事物中去寻找与其相似的东西,例如从我们日常生活中入手。我们要明白一个道理,所有抽象的东西,追根到底,都是从客观事物中提取出来的,也就是说,任何抽象的东西,都会在客观世界中找到它的原形。
那么,在我们的日常生活中,有没有与C/C++中指针对应的东西呢?有,多得是:
指南针。
手表。
电流表/电压表。
汽车上用来显示剩余汽油的表。
……
看看,这些物体都有什么共同特点?是不是都有一根或者多根指针?比如,下面图片中的山寨手表。
现在,我要你从山寨手表中读出其指示的时间,那么,你想一想,你会怎么看出来的
这个应该会看吧,小学生都会用了。我们会先看一下时针所指的方向在哪个时刻范围内,如10-11之间,所以我们确定了是10点钟;然后,我们看到分针所指的是第二个刻度,我们读出2,所以一组合,就是10点2。
是不是这样读,没读错吧?
像电流表也是这样,我们就是通过指针所指的方向找到对应的刻度,就知道当前电流是多少安/毫安,家用的应该是以毫安为单位。
我们知道,程序的运行是存在内存中的,因此,我们在代码中声明的所有变量都是存放在内存中的某块区域中,所以,在C语言中,指针用来告诉我们,某个变量在内存中的首地址在哪。注意,是首地址,因为变量的长度不一定就是一个字节,有可能N多个字节,它在内存中是排列在一段连续的区域块中。
比如,某中学的教学楼,每个年级使用一栋楼,初一年级在A栋,初二在B栋,初三在C栋。某学生在校其间,由于多次非礼女同学,老师说要见家长,于是,家长K君来到了某学校,但学校那么大,怎么找到K君的儿子所在的教室呢?这时候,保安人员告诉K君,初二年级在B栋,并用手指着东北方向(指针指向的内存地址块的位置)。
于是,K君顺着保安人员手指的方向找到了B栋,他知道他儿子在3班,而楼上的教室都是按顺序的,1班在第一个教室,2班在第二个教室,以此类推。所以,K君很快就找到他儿子,然后把他教育了两顿(访问或处理指针所指向内存中的数据),然后,K君很郁闷地下楼,离开了学校(发生析构,清理内存)。
因此,我们可以对指针这样宝义:
通过指针中存放的首地址,应用程序顺利地找到某个变量。就好像我最近认识了一位朋友,他叫我有空去他家坐坐,然后,他留下了地址。某个周末我正闲着,忽然想起这位朋友,于是,我就根据他留的地址去找他,结果,当我来到傻B街230号出租房时,里面走出一个我不认识的人,于是,我问他我这位朋友去哪了,陌生人说,我刚租了这房子,你找的可能是前一位租户吧。
所以,指针所指向的地址,有可能是变量B,也有可能是变量F,或者变量S,指针是房东,可以把房子租给B,C,或F,它可以动态为变量分配内存,也可以把变量销毁(delete),交不起房租就滚蛋(析构函数)。
从上面的故事中,我们看到指针的两个用途:索引内存和分配内存。
看看下面这个例子。
你猜猜,它运行后会出现什么?
我们看到了,pint里面存的就是整型100的首地址,因为它是int*,是指向int的指针,所以指针知道,找到首地址后,我只关注从首地址开始,连续的4个字节,后面的我不管了,因为我只知道int有四个字节。上面的例子,我们看到pint的值就是0x10f1968,这就是整型100在内存中的首地址,所以,100所拥有的内存块可能是:
0x10f1968 , 0x10f1969, 0x10f196A, 0x10f196b
总之是连续的内存块来保存这4个字节。
new int(100),表示指针pint在首地址为0x10f1968的内存区域创建了一个4个字节的区域,里面保存的值就是整型100,所以,pint取得的就是100的首地址,而加上*号就不同了,看看上面的例子,*pint的值就是100了。这样一来,我们又得到一个技巧:
利用指针标识符 * 放在指针变量前即可获得指针所指地址中存储的实际值。
我都大家一个很简单的技巧。看看下面两行代码。
int *p = new int(200);
int p = 200;
因为 * 放在类型后或放在变量名前面都是可以的,即int* pint和int *pint是一个道理。这样一来,我们不妨把int *pint 看作int (*pint),将整个*pint看作一个整体,这样看上去是不是和下面的声明很像?
int a = 30;
所以,int* p = new int(30)中,*p返回的值就是int的本值30,而p则只是返回30的首地址。
再看看下面的代码:
现在你可以猜猜它的运行结果是什么。
从上面的代码我们又看到了指针的第三个功能:创建数组。
上例中,我创建了有三个元素的数组。在使用完成后,要使用delete来删除已分配的内存,所以,我们的第一个例子中,其实不完善,我们没有做内存清理。
int* pint = new int(100);
/****/
delete pint;
为什么指针可以创建数组?前面我提到过,指针是指向首地址的,那么你想想,我们的数组如果在堆上分配了内存,它们是不是也按一定次序存放在一块连续的内存地址中,整个数组同样构成了一段内存块。
很多书和教程都把这个符号叫引用,但我不喜欢翻译为引用,因为引用不好理解,如果叫取地址符,那我估计你就会明白了,它就是返回一个变量的首地址。
看看例子:
我们不能直接对指针变量赋值,要把变量的地址传给指针,就要用取地址符&。上面的代码中我们声明了int类型的变量a,值为50,通过&符号把变量a的地址存到p指针中,这样,p指向的就是变量a的首地址了,故:a的值的50,而p的值就应该是a的地址。
那么,这样做有啥好处呢?我们把上面的例子再扩展一下,变成这样:
先预览一下结果。
我们定义了变量a,值为50,然后指针p指向了a的首地址,但注意,后面我只是改变了p所指向的那块内存中的值,我并没有修改a的值,但是,你看看最后a的值也变为了250,想一想,这是为什么?
很多书,包括一些计算机二级的考试内容,那些傻S砖家只是想出一大堆与指针相关的莫名其妙的考题,但很让人找不到指针在实际应用到底能干什么,我估计那些砖家自己也不知识吧。所以,我们的考试最大的失败,就是让学生不知识学了有什么用。
上面介绍了指针可以存首地址,可以分配内存,可以创建数组,还说了取地址符&,那么,这些东西有什么用呢?你肯定会问,我直接声明一个变量也是要占用内存的,那我为什么要吃饱了没事干还要用指针来存放首地址呢?
好,我先不回答,我们再说说函数的参数传递。看看下面这样的例子。
我们希望,在调用函数fn后,变量a的值会加上100,现在我们运行一下,看看结果:
我们可能会很失望,为什么会这样?我明明是把20传进了fn函数的,为什么a的值还是不变呢?不用急,我们再把代码改一下:
运行结果如下:
看到了吗?变量a和fn函数的参数x的地址是不一样的,这意味着什么呢?这说明,变量a的值虽然传给了参数x,但实际上是声明了一个新变量x,而x的值为20罢了,最后加上100,x的中的值是120,但a的值没有变,因为在函数内被+100的根本不是变量a,而是变量x(参数)。
这样,就解释了为什么么函数调用后a的值仍然不变的原因。
那么,如何让函数调用后对变量a作修改,让它变成120呢?这里有两个方法:
(1)指针法。把参数改为指针类型。
这里要注意,把变量传给指针类型的参数,要使用取地址符&。
那么,这次运行正确吗?
好了,终于看到想要的结果了。
(2)引用法,就是把参数改为&传递的。
可以看到,这样的运行结果也是正确的。
不管是类还是结构(其实结构是一种特殊的类),它们在创建时还是要创建内存的,但是,创建类的对象也有两种方式,直接声明和用指针来分配新实例。
我们来看看这个例子,首先定义了一个类Test,在类的构造函数中输出对象被创建的个息,在发生析构时输出对象被销毁。
接着, 我们分别在两个函数中创建Test类的对象,因为对象是在函数内部定义的,根据其生命周期原理,在函数返回时,对象会释放,在内存中的数据会被销毁。理论上是这样的,那么,程序实际运行后会如何呢?
这时候我们发现一个有趣的现象,在第一个函数直接以变量形式创建的对象在函数执行完后被销毁,因为析构函数被调用;可是,我们看到第二个函数中并没有发生这样的事,用指针创建的对象,在函数完成时居然没有调用析构函数。
直接创建对象,变量直接与类实例关联,这样一来,当变量的生命周期结束时,自然会被处理掉,而用指针创建的实例,指针变量本身并不存储该实例的数据,它仅仅是存了对象实例的首地址罢了,指针并没有与实例直接有联系,所以,在第二个函数执行完后,被销毁的是Test*,而不是Test的对象,仅仅是保存首地址的指针被释放了而已,而Test对象依然存在于内存中,因此,在第二个函数完成后,Test的析构函数不会调用,因为它还没死呢。
那么,如何让第二个函数在返回时也销毁对象实例呢?还记得吗,我前文中提过。对,用delete.。
现在看看,是不是在两个函数返回时,都能够销毁对象。
现在你明白了吧?
由此,可以得出一个结论:指针只负责创建和清理内存,并不与内存中的对象实例有直接关系。
上一篇中我给各位说了一般人认为C++中较为难的东西——指针。其实对于C++,难点当然不局限在指针这玩意儿上,还有一些有趣的概念,如模板类、虚基类、纯虚函数等,这些都是概念性的东西,几乎每一本C++书上都会介绍,而平时我们除了会接触到纯虚函数外,其他的不多用。纯虚函数,你可以认为与C#中的抽象方法或接口中的方法类似,即只定义,不实现。好处就是多态,发何处理,由派生类来决定。
在开始吹牛之前,我先推荐一套视频教程,孙鑫老师的C++教程,共20课,我不是帮他老人家打广告,而是因为孙老师讲的课是我听过的最好的课,我都看过4次了,我完全可以用他的视频教程来复习C++的。
好了,F话说完了,下面我就扯一下编写一个Win32应用程序的大致流程,不管你的程序有多么复杂,多么变态,其基本思路和流程是不变的。这就好比你写书法的时候,特别是写楷书,我不管你用的是欧体、颜体,还是柳体,你都得遵守“永字八法”基本规则。
那么,我们要编写一个Win32应用程序,要经过哪几个步骤呢?
你不妨想一想,你有一家工厂是生产女性服装的,如果你要生产一批新式服装(例如某种冬装),你会有哪些流程?
首先,如果我们确定要做这么一款服式,我们要请设计师来把服装设计好,然后打版,打版就是生成基本样本,以后工人就按照这个样本做就行了。
其次,注册产品,向上级主管申报,登记后就转入车间或下游加工企业开工。
再次,为了展示你的新产品的特色,你要举办一场服装表演。
接着、持续更新,发现产品存在的问题,不断改进修正。
最后,推向市场。
我们开发Win32应用程序也是遵守这样的规范。不过,我想现在很少人用Win32在实际开发中,毕竟它的开发效率是相当地低下,所以,曾被某些人误认为只适用于开发木马程序。其实,也不一定的,不要太邪恶了。
MFC对Win API函数的封装,后来出现了托管C++,你可以用于写WinForm程序,这样可以提高开发效率。
如果你有足够的时间,如果你还在学习编程,如果你是刚进入大学的年轻有为者,你不用急,因为你会有更多的时间磨炼,你应当考虑多学一点C类语言,C++的学习你会发现你能学到很多其他语言中学不到的知识,特别是接触到不少原理性的东西,能加深你对编程哲学的认知。
我们在学习标准C++的时候,都知道每个应用程序运行时都会先进入入口点函数main,而当从main函数跳出时程序就结束了。在Windows编程里面,也是一样的,只是我们的入口点函数不叫main,叫WinMain,这个函数不同于main,我们不能乱来,它的定义必须与声明保持一致。
我建议各位安装VS的时候,都顺便更新帮助文档到本地硬盘,这样我们可以方便查找。有一点要注意,目前DestTop Develop的文档基本上是英文的,做好心理准备。
WinMain函数怎么写呢,不用记的,到MSDN文档一搜,直接复制就行了。
这个函数带了一个CALLBACK,说明它是一个回调函数,那么这个CALLBACK是啥呢。我们先不管,我们先动写一个Windows,让大家有一个更直观的认识。
1、启动你的开发工具,版本任意。
2、从菜单栏中依次【文件】【新建】【项目】,在新建项目窗口中,选择Win32-Win32应用程序。
2、点击确定后,会弹出一个向导,单击【下一步】。项目类型选择Windows应用程序,附加选项选择空项目,我们要自己编写实现代码。
3、单击完成,项目创建成功。打开【解决方案资源管理器】,在“源文件”文件夹上右击,从菜单中找到【添加】【新建项】,注意,是源文件,不要搞到头文件去了。
在新建项窗口中选C++代码文件,.cpp后缀的,不要选错了,选成头文件,不然无法编译,因为头文件是不参与编译的。文件名随便。
包含Windows.h头文件,这个是最基本的。
然后是入口点,这个我们直接把MSDN的声明Ctrl + C,然后Ctrl + V上去就行了。
WinMain返回整型,返回0就行了,其实是进程的退出码,一定要0,不要写其他,因为0表示正常退出,其他值表示非正常退出。
刚才我们提到这个函数带了CALLBACK,那么,它是什么?很简单,你回到IDE,在CALLBACK上右击,选【转到定义】,看看吧。
我们看到它其实是一个宏,原型如下:
这时候我们发现了,它其实就是__stdcall,那么这个__stdcall是什么呢?它是和__cdecl关键字对应的,这些资料,你网上搜一下就有了,如果你觉得不好理解,你不妨这样认为,__stdcall是专门用来调用Win API 的,反正MSDN上也是这样说的,它其实是遵循Pascal的语法调用标准,相对应地,__cdecl是C语言的调用风格,这个也是编译器选项。
打开项目属性,找到节点C/C++\高级,然后查看一下调用约定,我们看到默认是选择C风格调用的,所以,WIN API 函数才用上关键字__stdcall,如果你实在不懂,也没关系,这个东西一般不影响我们写代码,但属性窗口中的编译器选项不要乱改,改掉了可能会导致一些问题。
那么CALLBACK有什么特别呢?一句话:函数不是我们调用的,但函数只定义了模型没有具体处理,而代码处理权在被调用者手里。怎么说呢,我们完全把它理解为.NET中的委托,我想这样就好理解了,委托只声明了方法的参数和返回值,并没有具体处理代码。
WinMain是由系统调用的,而WinMain中的代码如何写,那操作系统就不管了。就好像我告诉你明天有聚会,一起去爬山,反正我是通知你了,至于去不去那是你决定了。
接下来看看入口点函数的参数。
注意,我们平时看到很多如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串数据类型,也许我们会说,怎么Windows开发有那么多数据类型。其实你错了,人总是被眼睛所看到的东西欺骗,Win API 中根本没有什么新的数据类型,全都是标准C++中的类型,说白了,这些东西全是数字来的。如果你不信,自己可以研究一下。
它定义这些名字,只是方便使用罢了,比如下面这样:
第一个变量指的是窗口的句柄,第二个指的是一个图标的句柄,第三个是当前应用程序的实例句柄,你看看,如果我们所有的句柄都是int,我们就无法判断那些类型是专门用来表示光标资源,不知道哪些类型是专用来表示位图的句柄了,但是,如果我们这样:
这样就很直观,我一看这名就知道是Brush Handlers,哦,我就明白它是专门用来管理内存中的画刷资源的,看,这就很明了,所以,通常这些新定义的类型或者宏,都是取有意义的名字。比如消息,它也是一个数字,如果我说115代表叫你去滚,但光是一个115谁知道你什么意思,但是,如果我们为它定义一个宏:
这样,只要我SendMessage(hwnd, WM_GET_OUT, NULL, NULL),你就会收到一条消息,滚到一边去。
WinMain的第一个参数是当前应用程序的实例句柄,第二个参数是前一个实例,比如我把kill.exe运行了两个实例,进程列表中会有两个kill.exe,这时候第一次运行的实例号假设为0001,就传递第一个参数hInstance,第二次运行的假设实例号为0002,就传给了hPrevInstance参数。
lpCmdLine参数从名字上就猜到了,就是命令行参数,那LPSTR是啥呢,它其实就是一个字符串,你可以跟入定义就知道了,它其实就是char*,指向char的指针,记得我上一篇文章中说的指针有创建数组的功能吗?对,其实这里传入的命令行参数应该是char[ ],这就是我在第一篇文章中要说指针的原因。
这里告诉大家一个技巧,我们怎么知道哪些参数是指针类型呢,因为不是所有参数都有 * 标识。技巧还是在命名上,以后,只要我们看到P开头的,或者LP开头的,都是指针类型。
比如LPWSTR,LPCTSTR,LPRECT等等。
最后一个参数nCmdShow是主窗口的显示方式。它定义了以下宏。
Value | Meaning |
---|---|
|
Hides the window and activates another window. |
|
Maximizes the specified window. |
|
Minimizes the specified window and activates the next top-level window in the Z order. |
|
Activates and displays the window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when restoring a minimized window. |
|
Activates the window and displays it in its current size and position. |
|
Activates the window and displays it as a maximized window. |
|
Activates the window and displays it as a minimized window. |
|
Displays the window as a minimized window. This value is similar to SW_SHOWMINIMIZED, except the window is not activated. |
|
Displays the window in its current size and position. This value is similar toSW_SHOW, except the window is not activated. |
|
Displays a window in its most recent size and position. This value is similar toSW_SHOWNORMAL, except the window is not activated. |
|
Activates and displays a window. If the window is minimized or maximized, the system restores it to its original size and position. An application should specify this flag when displaying the window for the first time. |
这个参数是操作系统传入的,我们无法修改它。那么,应用程序在运行时,是如何决定这个参数的呢?看看这个,不用我介绍了吧,你一定很熟悉。
我们写了WinMain,但我们还要在WinMain前面预先定义一个WindowProc函数。C++与C#,Java这些语言不同,你只需记住,C++编译器的解析是从左到右,从上到下的,如果某函数要放到代码后面来实现,但在此之前要使用,那么你必须先声明一下,不然编译时会找不到。这里因为我们通常会把WindowProc实现放在WinMain之后,但是在WinMain中设计窗口类时要用到它的指针,这时候,我们必须在WinMain之前声明WindowProc。
同样地,WindowProc的定义我们不用记,到MSDN直接抄就行了。
前导声明与后面实现的函数的签名必须一致,编译才会认为它们是同一个函数。在WindowProc中返回DefWindowProc是把我们不感兴趣或者没有处理的消息交回给操作系统来处理。也许你会问,函数的名字一定要叫WindowProc吗?当然不是了,你可以改为其他名字,如MyProc,但前提是返回值和参数的类型以及个数必须一致。
这个函数带了CALLBACK,说明不是我们调用的,也是由操作系统调用的,我们在这个函数里面对需要处理的消息进行响应。至于,为什么可以改函数的名字而系统为什么能找到这个函数呢,后面你就知道了。
设计窗口类,其实就是设计我们程序的主窗口,如有没有标题栏,背景什么颜色,有没有边框,可不可以调整大小等。要设计窗口类,我们用到一个结构——
通常情况下,我们用WNDCLASS就可以了,当然还有一个WNDCLASSEX的扩展结构,在API里面,凡是看到EX结尾的都是扩展的意思,比如CreateWindowEx就是CreateWindow的扩展函数。
第一个成员是窗口的类样式,注意,不要和窗口样式(WS_xxxxx)混淆了,这里指的是这个窗口类的特征,不是窗口的外观特征,这两个style是不一样的。
它的值可以参考MSDN,通常我们只需要两个就可以了——CS_HREDRAW | CS_VREDRAW,从名字就看出来了,就是同时具备水平重画和垂直重画。因为当我们的窗口显示的时候,被其他窗口挡住后重新显示,或者大小调整后,窗口都要发生绘制,就像我们在纸上涂鸦一样,每次窗口的变化都会“粉刷”一遍,并发送WM_PAINT消息。
lpfnWndProc参数就是用来设置你用哪个WindowProc来处理消息,前面我说过,我们只要不更改回调函数的返回值和参数的类型和顺序,就可以随意设置函数的名字,那为什么系统可以找到我们用的回调函数呢,对的,就是通过lpfnWndProc传进去的,它是一个函数指针,也就是它里面保存的是我们定义的WindowProc的入口地址,使用很简单,我们只需要把函数的名字传给它就可以了。
cbClsExtra和cbWndExtra通常不需要,设为0就OK。hInstance是当前应用程序的实例句柄,从WinMain的hInstance参数中可以得到。hIcon和hCursor就不用我说了,看名字就知道了。
hbrBackground是窗口的背景色,你也可以不设置,但在处理WM_PAINT消息时必须绘制窗口背景。也可以直接用系统定义的颜色,MSDN为我们列出这些值,大家不用记,直接到MSDN拿来用就行了,这些都比较好理解,看名字就知道了。
lpszMenuName指的是菜单的ID,没有菜单就NULL,lpszClassName就是我们要向系统注册的类名,字符,不能与系统已存在的类名冲突,如“BUTTON”类。
所以,在WinMain中设计窗口类。
窗口类设计完成后,不要忘了向系统注册,这样系统才能知道有这个窗口类的存在。向操作系统注册窗口类,使用RegisterClass函数,它的参数就是一个指向WNDCLASS结构体的指针,所以我们传递的时候,要加上&符号。
窗口类注册完成后,就应该创建窗口,然后显示窗口,调用CreateWindow创建窗口,如果成功,会返回一个窗口的句柄,我们对这个窗口的操作都要用到这个句柄。什么是句柄呢?其实它就是一串数字,只是一个标识而已,内存中会存在各种资源,如图标、文本等,为了可以有效标识这些资源,每一个资源都有其唯一的标识符,这样,通过查找标识符,就可以知道某个资源存在于内存中哪一块地址中,就好比你出身的时候,长辈都要为你取个名字,你说名字用来干吗?名字就是用来标识你的,不然,你见到A叫小明,遇到B又叫小明,那谁知道哪个才是小明啊?就好像你上大学去报到号,会为你分配一个可以在本校学生中唯一标识你的学号,所有学生的学号都是不同的,这样,只要通过索引学号,就可以找到你的资料。
CreateWindow函数返回一个HWND类型,它就是窗口类的句柄。
窗外观的样式都是WS_打头的,是Window Style的缩写,这个我就不说了,MSDN上全有了。
窗口创建后,就要显示它,就像我们的产品做了,要向客户展示。显示窗口调用ShowWindow函数。
既然要显示窗口了,那么ShowWindow的第一个参数就是刚才创建的窗口的句柄,第二个参数控制窗口如何显示,你可以从SW_XXXX中选一个,也可以用WinMain传进来的参数,还记得WinMain的最后一个参数吗?
为什么更新窗口这一步可有可无呢?因为只要程序在运行着,只要不是最小化,只要窗口是可见的,那么,我们的应用程序会不断接收到WM_PAINT通知。这里先不说,后面你会明白的。好了,更新窗口,当然是调用UpdateWindow函数。
Windows操作系统是基于消息控制机制的,用户与系统之间的交互,程序与系统之间的交互,都是通过发送和接收消息来完成的。就好像军队一样,命令一旦传达,就要执行,当然,我们的应用程序和军队不一样,我们收到指令不一要执行,我们是可以选择性地执行。
我们知道,代码是不断往前执行的,像我们刚才写的WinMain函数一样,如果你现在运行程序,你会发现什么都没有,是不是程序不能运行呢,不是,其实程序是运行了,只是它马上结束了,只要程序执行跳出了WinMain的右大括号,程序就会结束了。那么,要如何让程序不结束了,可能大家注意到我们在C程序中可以用一个getchar()函数来等到用户输入,这样程序就人停在那里,直到用户输入内容。但我们的窗口应用不能这样做,因为用户有可能进行其他操作,如最小化窗口,移动窗口,改变窗口大小,或者点击窗口上的按钮等。因此,我们不能简地弄一个getchar在那里,这样就无法响应用户的其他操作了。
可以让程序留在某处不结束的另一个方法就是使用循环,而且是死循环,这样程序才会永久地停在某个地方,但这个死循环必须具有跳出的条件,不然你的程序会永久执行,直达停电或者把电脑砸了。
这样消息循环就出现了,只要有与用户交互,系统人不断地向应用程序发送消息通知,因为这些消息是不定时不断发送的,必须有一个绶冲区来存放,就好像你去银行办理手续要排队一样,我们从最前端取出一条一条消息处理,后面新发送的消息会一直在排队,直到把所有消息处理完,这就是消息队列。
要取出一条消息,调用GetMessage函数。函数会传入一个MSG结构体的指针,当收到消息,会填充MSG结构体中的成员变量,这样我们就知道我们的应用程序收到什么消息了,直到GetMessage函数取不到消息,条件不成立,循环跳出,这时应用程序就退出。MSG的定义如下:
hwnd不用说了,就是窗口句柄,哪个窗口的句柄?还记得WindowProc回调函数吗?你把这个函数交给了谁来处理,hwnd就是谁的句柄,比如我们上面的代码,我们是把WindowProc赋给了新注册的窗口类,并创建了主窗口,返回一个表示主窗口的句柄,所以,这里MSG中的hwnd指的就是我们的主窗口。
message就是我们接收到的消息,看到,它是一个数字,无符号整型,所以我们操作的所有消息都是数字来的。wParam和lParam是消息的附加参数,其实也是数值来的。通常,lParam指示消息的处理结果,不同消息的结果(返回值)不同,具体可参阅MSDN。
有了一个整型的值来表示消息,我们为什么还需要附加参数呢?你不妨想一下,如果接收一条WM_LBUTTONDOWN消息,即鼠标左键按下时发送的通知消息,那么,我们不仅知道左键按下这件事,我们更感趣的是,鼠标在屏幕上的哪个坐标处按下左键,按了几下,这时候,你公凭一条WM_LBUTTONDOWN消息是无法传递这么多消息的。可能我们需要把按下左键时的坐标放入wParam参数中;最典型的就是WM_COMMAND消息,因为只要你使用菜单,点击按钮都会发送这样一条消息,那么我怎么知道用户点了哪个按钮呢?如果窗口中只有一个按钮,那好办,用户肯定单击了它,但是,如果窗口上有10个按钮呢?而每一个按钮被单击都会发送WM_COMMAND消息,你能知道用户点击了哪个按钮吗?所以,我们要把用户点击了的那个按钮的句柄存到lParam参数中,这样一来,我们就可以判断出用户到底点击了哪个按钮了。
GetMessage函数声明如下:
这个函数在定义时带了一个WINAPI,现在,按照前面我说的方法,你应该猜到,它就是一个宏,而真实的值是__stdcall,前文中说过了。
第一个参数是以LP开头,还记得吗,我说过的,你应该想到它就是 MSG* ,一个指向MSG结构的指针。第二个参数是句柄,通常我们用NULL,因为我们会捕捉整个应用程序的消息。后面两个参数是用来过滤消息的,指定哪个范围内的消息我接收,在此范围之外的消息我拒收,如果不过滤就全设为0.。返回值就不说了,自己看。
TranslateMessage是用于转换按键信息的,因为键盘按下和弹起会发送WM_KEYDOWN和WM_KEYUP消息,但如果我们只想知道用户输了哪些字符,这个函数可以把这些消息转换为WM_CHAR消息,它表示的就是键盘按下的那个键的字符,如“A”,这样我们处理起来就更方便了。
DispatchMessage函数是必须调用的,它的功能就相当于一根传送带,每收到一条消息,DispatchMessage函数负责把消息传到WindowProc让我们的代码来处理,如果不调用这个函数,我们定义的WindowProc就永远接收不到消息,你就不能做消息响应了,你的程序就只能从运行就开始死掉了,没有响应。
其实现在我们的应用程序是可以运行了,因为在WindowProc中我们调用了DefWindowProc,函数,消息我们不作任何处理,又把控制权路由回到操作系统来默认处理,所以,整个过程中,我们现在的消息循环是成立的,只不过我们不做任何响应罢了。
好的,现在我把完整的代码贴一下,方便你把前面我们说的内容串联起来。
所有代码看上去貌似很正常,也遵守了流程,设计窗口类,注册窗口类,创建窗口,显示窗口,更新窗口,消息循环。是吧,这段代码看上去毫无破绽,运行应该没问题吧。好,如果你如此自信,那就试试吧。
按下F5试试运行。
哈哈,结果会让很多人失望,很多初学者就是这样,一切看起来好像正常,于是有人开始骂VC是垃圾,是编译器有bug,也有人开始想放弃了,妈的,这么难,不学了。人啊,总是这样,老指责别人的问题,从不在自己身上找问题,是真的VC的bug吗?
我前面说了,这段代码貌似很正常,呵呵,你看到问题在哪吗?给你两分钟来找错。我提示一下,这个程序没有运行是因为主窗口根本就没有创建,因为我在代码里面做了判断,如果窗口顺柄hwnd为NULL,就退出,现在程序一运行就退出了,明显是窗口创建失败。
…………
好了,不用找了,很多人找不出来,尤其是许多初学者,不少人找了一遍又一遍,都说没有错误,至少代码提示没说有错,编译运行也没报错,所以不少人自信地说,代码没错。
其实你是对的,代码确实没有错,而问题就出在WNDCLASS结构上,认真看一下MSDN上有关RegisterClass函数说明中的一句话,这句话很多人没注意到,但它很关键。
You must fill the structure with the appropriate class attributes before passing it to the function.
现在你明白了吧,还不清楚?没关系,看看我把代码这样改一下你就知道了。
现在,你运行一下,你一定能看到窗口。
但现在你对窗口无法进行操作,因为后续的代码还没完成。
为什么现在又可以了呢?MSDN那句话的意思就是说我们在注册窗口类之前必须填充WNDCLASS结构体,何为填充,就是要为结构的所有成员赋值,就算不需要你也要为它赋一个NULL或0,因为结构在创建时没有对成员进行初始化,这就导致变量无法正确的分配内存,最后注册失败。
那么,如果一个结构体成员很多,而我只需要用到其中三个,其他的也要初始化,是不是很麻烦,是的,除了为每个成员赋值,还有一种较简单的方法,就是在声明变量时给它赋一对大括号,里面放置结构体的应该分配内存的大小,如:
这样一来,我们也发现,窗口也可以成功创建。
我们还可以更简单,直接把sizeof也去掉,在声明变量时,直接赋一对空的大括号就行了,就如这样。
这样写更简单,窗口类同样可以正常注册。大括号代表的是代码块,这样,结构体有了一个初值,因此它会按照结构体的大小分配了相应的内存。
为什么会这样呢?这里涉及到一个关于结构体的一个很有趣的赋值方式。我们先放下我们这个例子,下面我写一个简单的例子,你就明白了。
在本例中,我们定义了一个表示矩形的结构体 RECT ,它有四个成员,分别横坐标,纵坐标,宽度,高度,但是,我们在声明和赋值中,我们只用了一对大括号,把每个成员的值,按照定义的顺序依次写到大括号中,即{ 0, 0, 20, 30 },x的值为0,y的值为0,width为20,height的值为30。
也就是说,我们可以通过这种简单的方法向结构变量赋值,注意值的顺序要和成员变量定义的顺序相同。
现在,回到我们的Windows程序来,我们明白了这种赋值方式,对于 WNDCLASS wc = { } 就不难理解了,这样虽然大括号里面是空的,其实它已经把变量初始化了,都赋了默认值,这样一来,就可以正确分配内存了。
通常情况下,当我们的主窗口关闭后,应用程序应该退出(木马程序除外),但是,我们刚才运行后发现,为什么我的窗口关了,但程序不退出呢?前面我说了,要退出程序,就要先跳出消息循环,和关闭哪个窗口无关。因此,我们要解决两个问题:
1、如果跳出消息循环;
2、什么时候退出程序。
其实两个问题是可以合并到一起解决。
首先要知道,当窗口被关闭,为窗口所分配的内存会被销毁,同时,我们会收到一条WM_DESTROY消息,因而,我们只要在收到这条消息时调用PostQuitMessage函数,这个函数提交一条WM_QUIT消息,而在消息循环中,GetMessage函数是不接收WM_QUIT消息的,这样一来,GetMessage返回FALSE,就可以跳出消息循环了,这样应用程序就可以退出了。
所以,我们要做的就是捕捉WM_DESTROY消息,然后PostQuitMessage.
我们会收到很多消息,所以用switch判断一下是不是WM_DESTROY消息,如果是,退出应用程序。
好了,这样,我们一个完整的Windows应用程序就做好了。
下面是完整的代码清单。