2.3 WIN32开发
Visual C++5.0是一个全32位的软件开发工具,它完全支持32位的Win32平台开发。Win32平台包括32位的Windows操作系统和软件开发系统Win32 API。所谓API(应用程序接口)指的是一组由操作系统提供的函数。Win32 API是Windows平台上的一个32位的软件开发系统,它使应用程序可以充分利用32位Windows操作系统的能力。使用Win32 API写成的应用程序可以在Windows95或更高版本以及Windows NT上运行。
由于Microsoft在Windows 3.x及其Win16 API上取得巨大的成功,因此,在研制Win32 API时,首先考虑的就是保证Win32与Win16 API兼容,只有让软件开发者能将Win16代码很容易移植到Win32 API上,才有实际意义。Win32 API在语法上只作了极小的改动,API的命名与Windows 的Win16 API相同,语义也相同,消息序号也相同。事实上,完全可以保存独立的源代码,并选择编译成16位的Win16程序或32位的Win32程序。
其次,如其名所示,在设计Win32 API时考虑到了充分利用32位处理器的能力。随着硬件的发展,内存和CPU价格的降低和性能的提高,32位CPU的486、Pentium已成为主流。据有关数据显示,目前在我国家用计算机用户中,使用Pentium系列处理器的计算机已占80%以上。如何充分利用当前32位(和64位)处理器的能力,并预见将来处理器的发展,就成为Win32设计时考虑的重要因素之一。
再次,为了摆脱操作系统对Intel处理器的依赖,使应用程序可以运行于各种处理器平台上,Win32设计时增强了它的可移植性,提供了Microsoft Windows95和Windows NT之间的透明的移植能力。虽然Windows95只能运行于Intel平台上,但是Win32还支持Windows NT,而Windows NT已经被移植到许多非Intel的处理器上,如Alpha、RISC硬件平台等。
Win32可以应用于特定的操作系统,这种系统可以直接控制和处理PC硬件资源,而不必象Win16 API那样依赖于MS-DOS系统服务。然而,Win32不是简单的由Win16从16位到32位的升级,更重要的在于它支持:
- 高性能的抢先式多任务和多线程
- 连续的32位地址空间和先进的内存管理
- 对所有的可为进程共享的对象,解决了它的安全性问题
- 内存映射文件
2.3.1 抢先式多任务和多线程
我们知道Windows是一个多任务操作系统,它提供了一次运行多个应用程序的能力。但是,Windows 3.x和Windows95在多任务的实现上有所不同。
Windows 3.x的多任务是一种由协作、软件方式产生的有限的非抢先式的多任务。它是借助于每个应用程序的消息循环这种软件协议方式来实现多任务的。Windows 3.x管理所有的消息,并存放于系统的消息队列中。操作系统判断消息应归哪一个窗口去处理,再将消息发送给该窗口。每个应用程序窗口处于等待消息状态,直到有消息来,然后进行处理,处理完毕将控制权交给操作系统。在对消息进行处理时,对于用户用键盘或者鼠标输入的任何命令,Win16都不会理睬。比如,我们用WORD载入一个文件时,其他程序都得等待文件I/O操作完成才能获得响应。而且,一个应用程序切换到另一个应用程序时,需要较长的等待时间。各应用程序在取得消息、处理消息时是平等的,无优先级的,系统无法设置应用程序的优先级和时间片的大小。
Windows95的多任务是一种抢先式多任务。比如,我们在用资源管理器复制一个文件的同时,还可以启动另外一个应用程序,如纸牌游戏,而且随时都可以切换回资源管理器,察看文件复制进度,系统始终保持较好的响应和灵活性。Windows95的抢先式多任务机制不是用Windows 3.x下的软件调度来实现的。要了解抢先式多任务,我们需要首先了解一下进程和线程的概念。调入内存准备执行的应用程序叫做进程(process)。每个进程至少有一条线程,叫做主线程(primary thread)。一个进程包含代码、数据和其他属于应用程序的资源。一条线程包含一组指令,相关的CPU寄存器值和一个堆栈。
在抢先式多任务操作系统中,系统在所有运行的所有进程之间对CPU时间进行共享,从而保证每个进程都能频繁的访问处理器,并且实现指令的连续执行。这样,每个Win32进程都需要分配一个优先级,系统调度程序利用这种优先级来决定哪一时刻该运行哪一个进程。具有高优先级的进程(严格的说应当是线程)就是当前运行的哪一个。更高优先级的线程可以中断当前进程的执行。同一优先级的线程通过时间片来调度。一个线程处于以下三种状态之一:正在执行,挂起,准备运行。在单处理器环境下(如Windows 95),同一时刻只能运行一个线程。有关多线程,我们还将在后面的章节里作专门介绍。
为了在Win32中支持多线程进程结构,Win32在原来Win16基础上增加了:
对进程以及线程创建、操纵的支持
对一个进程内线程之间的同步和同步对象的支持
一个统一的共享机制。
2.3.2 连续的地址空间和先进的内存管理
对于各种操作系统和平台来说,内存管理都是一个非常重要的问题。在Windows3.1下,有两种形式的内存管理函数调用:局部的和全局的。全局内存管理函数从物理内存中分配一段,然后返回一个句柄值。该句柄可以转换为一个GlobalLock函数所使用的远指针。基本处理过程如下:
- 申请一块可移动的内存块
- 锁定该内存块。因为Windows引入了虚拟内存管理,可以把内存块移动到硬盘交换文件中,所以在使用内存块之前,必须将它锁定在真正的内存RAM之中,也就是告诉操作系统,现在这块内存暂时由应用程序来管理。
- 对该内存块进行各种操作:如复制数据到内存块。
- 解锁内存,应用程序将对该内存的控制交与Windows。
下面给出一个程序片段,来说明内存管理函数的用法。
HGLOBAL memHandle;//内存句柄
char far* lpMem;//假设长度为memLen
memHandle=GlobalAlloc(GHND,memLen+1);//申请内存块,此处未做返回结果检查,
//事实上,申请内存有时会失败
memcpy(lpMem,string,textLen);//拷贝数据,其中string为一字符串变量,textLen是这个
//字符串的长度
GlobalUnlock(memHandle);//解锁内存
...
GlobalFree((HGLOBAL) memHandle);//释放内存
全局内存对所有的应用程序都是可见的,不管是显式的还是隐式的请求。因为Windows 3.x的实现方式就是所有的进程在同一地址空间中运行。局部内存管理则是从64KB的段内分配对象并返回所分配内存的16位偏移量。
在Win32下,局部和全局内存管理函数基本相同,仍然可以使用可移动和可丢弃选项。但是它引入了连续(flat)的32位内存管理概念。
在Win32中,每个进程都有其特有的32位虚拟地址空间,该空间最大可达4GB。如图所示,低端内存的2GB是用户可用的,高端内存的2GB为内核(Kernel)保留。其中,最高的1GB用于VxD、内存管理和文件系统。下面的1GB用于共享的Win32 DLL、内存映射文件和共享内存区域。进程所使用的虚拟地址不代表一个对象在内存的实际的物理位置(事实上,我们大部分的PC还没有配置4GB内存)。操作系统为每个进程维护一个映射表,根据该表将虚拟地址映射到真正的物理位置处(RAM或者交换页文件中)。
图2.7 Windows95的内存映射
在Win32下局部内存对象有一个32位句柄而不是Windows 3.x下的16位句柄,而且这个句柄是一个实际指针而不是一个相对于段的偏移量。
Win32和16位Windows一个重要区别是:在Win32下,所有的进程都有自己独立的地址空间(在进程内部的线程仍然共享进程的内存变量),全局内存不再对所有的Windows应用程序都可见。由于每个应用程序都有自己的地址空间,一个进程分配的内存在该进程的地址之外就不再可见。DDE会话中使用的内存对接收者进程来说是透明的。这样,进程的安全性就得到大大提高,程序更加强壮。一个进程崩溃一般不会影响另外一个进程的执行。但是,这也给多个应用程序共享内存带来了困难。在许多情况下,需要在多个应用程序之间进行通讯和数据交换,这时,该怎么办呢?Win32引入了内存映射文件,很好的解决了这个问题。
2.3.3 内存映射文件
内存映射文件是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数(CreateFileMapping)。这样,文件内的数据就可以用内存读/写指令来访问,而不是用ReadFile和WriteFile这样的I/O系统函数,从而提高了文件存取速度。
这种函数最适用于需要读取文件并且对文件内包含的信息做语法分析的应用程序,如对输入文件进行语法分析的彩色语法编辑器,编译器等。把文件映射后进行读和分析,能让应用程序使用内存操作来操纵文件,而不必在文件里来回地读、写、移动文件指针。
有些操作,如放弃“读”一个字符,在以前是相当复杂的,用户需要处理缓冲区的刷新问题。在引入了映射文件之后,就简单的多了。应用程序要做的只是使指针减少一个值。
映射文件的另一个重要应用就是用来支持永久命名的共享内存。要在两个应用程序之间共享内存,可以在一个应用程序中创建一个文件并映射之,然后另一个应用程序可以通过打开和映射此文件把它作为共享的内存来使用。
2.3.4 Win32s:Windows 3.x对Win32 API的支持
我们经常会遇到Win32s这个词,它与Win32是有区别的。Win32s的s的含义是指子集(subset)。它指的是,在一个Win32程序中移入一些DLLs和一个VxD,使它运行于配置80386以上处理器的Windows 3.x系统之上,并且以一种增强模式运行(但有一定限制)。运行在Windows 3.x/Win32s系统上的Win32程序支持32位指针和32位寄存器,只需要在系统调用之前稍作形式替换。如果程序中使用大的数据结构或很多的计算时,Win32s性能明显优于16位Windows版本,根据Microsoft的测试,性能可以提高两倍左右;如果程序只是大量的调用Windows API,则16位版本的性能可能会强于32位版本,因为Win32s会对每一次API调用作一个从16位到32位的转换。
Win32s子集同Win32相比,不支持:多线程,高级图形API,异步文件I/O,Unicode和安全性;而且它是运行于16位的Windows系统上的。但是同Win16相比,有它的优越之处,目前在16位Windows程序开发方面有相当的潜力。
Visual C++4.1及以前版本支持Win32s,但Visual C++5.0不再支持Win32s。
2.3.5 Win32编程基础
Win32数据类型
这里的数据类型指的是一些关键字,这些关键字定义了Win32中的函数中的有关参数和返回值的大小和意义。Win32常用的数据类型有:
数据类型 描述 HANDLE 定义一个32位无符号的整数,用作句柄 HINSTANCE 定义一个32位的无符号整数,用作实例句柄 HWND 定义一个32位的无符号整数,用作窗口句柄 HDC 一个设备描述背景的句柄 LONG 说明一个32位带符号整数 LPSTR 定义一个线性的32位字符串指针 UINT 定义一个新的Win32数据类型,它会把一个参数强制转换成Windows3.x应用中的16位值或Win32应用中的32位 WCHAR 说明一个16位的UNICODE字符,用来表示世界上所有已知的书写语言的符号
这里需要解释一下的是句柄。句柄是Windows编程的一个关键性的概念,编写Windows应用程序总是要和各种句柄打交道。所谓句柄,就是一个唯一的数,用以标识许多不同的对象类型,如窗口、菜单、内存、画笔、画刷、电话线路等。在Win32里,句柄是指向一个“无类型对象”(void*)的指针,也就是一个4字节长的数据。无论它的本质是什么,句柄并不是一个真正意义上的指针。从构造上看,句柄是一个指针,尽管它没有指向用于存储某个对象的内存位置。事实上,句柄指向一个包含了对该对象进行的引用的位置。句柄的声明是这样的:
typedef void *HANDLE
由于Windows是一个多任务操作系统,它可以同时运行多个程序或一个程序的多个副本。这些运行的程序称为一个实例。为了对同一程序的多个副本进行管理,Windows引入了实例句柄。Windows为每个应用程序建立一张表,实例句柄就好象是这张表的一个索引。
Windows不仅使用句柄来管理实例,也用它来管理窗口、位图、字体、元文件、图标等系统资源。
标识符命名
在编程时,变量、函数的命名是一个极其重要的问题。好的命名方法使变量易于记忆且程序可读性大大提高。Microsoft采用匈牙利命名法来命名Windows API函数和变量。匈牙利命名法是由Microsoft的着名开发人员、Excel的主要设计者查尔斯·西蒙尼在他的博士论文中提出来的,由于西蒙尼的国籍是匈牙利,所以这种命名法叫匈牙利命名法。
匈牙利命名法为C标识符的命名定义了一种非常标准化的方式,这种命名方式是以两条规则为基础的:
1.标识符的名字以一个或者多个小写字母开头,用这些字母来指定数据类型。下表列出了常用的数据类型的标准前缀:
在Windows里定义数据类型的一些标准前缀
前缀
数据类型 c 字符(char) s 短整数(short) cb 用于定义对象(一般为一个结构)尺寸的整数 n 整数(integer) sz 以’\0’结尾的字符串 b 字节 i int(整数) x 短整数(坐标x) y 短整数(坐标y) f BOOL w 字(WORD,无符号短整数) l 长整数(long) h HANDLE(无符号int) m_ 类成员变量 fn 函数(function) dw 双字(DWORD,无符号长整数)
2.在标识符内,前缀以后就是一个或者多个第一个字母大写的单词,这些单词清楚地指出了源代码内那个对象的用途。比如,m_szStudentName表示一个学生名字的类成员变量,数据类型是字符串型。
从16位的Win16 API迁移到Win32 API注意点
1.数据类型字长的变化:
我们编写一个小程序来说明Win32下的常见数据类型的字长:
#include<windows.h>
#include<stdio.h>
void main(void)
{
printf("sizeof(int) is %d\n",sizeof(int));
printf("sizeof(BYTE) is %d\n",sizeof(BYTE));
printf("sizeof(WORD) is %d\n",sizeof(WORD));
printf("sizeof(DWORD) is %d\n",sizeof(DWORD));
printf("sizeof(LONG) is %d\n",sizeof(LONG));
printf("sizeof(PVOID) is %d\n",sizeof(PVOID));
printf("sizeof(LPVOID) is %d\n",sizeof(LPVOID));
}
使用Visual C++编译运行该程序,输出结果如下:
sizeof(int) is 4
sizeof(BYTE) is 1
sizeof(WORD) is 2
sizeof(DWORD) is 4
sizeof(LONG) is 4
sizeof(PVOID) is 4
sizeof(LPVOID) is 4
从上面的输出结果我们看到:整数类型字长已经同长整数相同,PVOID近指针和LPVOID远指针长度也相同。在编程过程中,我们要注意这些变化,凡是设计字长的问题最好还是采用可以移植的sizeof操作符来做。
2.内存模式变化:
在Win32平台下,不再有微模式、紧凑模式、中模式、大模式、巨模式、自定义内存模式之分,也不再有64KB代码段和数据段的限制。只有一种内存模式,Win32下的地址和代码均在线性寻址的2GB的32位内存空间中。当然,编程时还是要考虑到实际内存限制的。
3.类型修饰符:
在Win32下,不再有远指针、近指针、巨型指针之分,三种指针类型完全相同。32位的编译器会忽略所有的_near、_far、_huge关键字并一视同仁来处理。在Win32中,象LPSTR和PSTR这种类型是等价的。
4.函数的变化:
Win32API设计时尽可能保证与Win16API兼容,但是仍然对一些函数作了修改。比如在Win16下的MoveTo在Win32下为MoveToEx。如果在编译程序时某个API函数没找到,试着在这个函数名后面加上Ex,Ex表示它是Win16的扩展。