在使用RegisterClass注册窗口类成功之后,即可以使用该窗口类创建并显示应用程序的窗口。
这个过程如下面的代码所示:
// 创建应用程序主窗口
hWnd=CreateWindow ("SdkDemo1", // 窗口类名
"第一个Win32 SDK应用程序", // 窗口标题
WS_OVERLAPPEDWINDOW, // 窗口样式
CW_USEDEFAULT, // 初始化 x 坐标
CW_USEDEFAULT, // 初始化 y 坐标
CW_USEDEFAULT, // 初始化窗口宽度
CW_USEDEFAULT, // 初始化窗口高度
NULL, // 父窗口句柄
NULL, // 窗口菜单句柄
hInstance, // 程序实例句柄
NULL); // 创建参数
// 显示窗口
ShowWindow(hWnd,SW_SHOW);
// 更新主窗口客户区
UpdateWindow(hWnd);
CreateWindow函数的原型是这样的:
HWND CreateWindow(LPCTSTR lpClassName, // 指向已注册的类名
LPCTSTR lpWindowName, // 指向窗口名称
DWORD dwStyle, // 窗口样式
int x, // 窗口的水平位置
int y, // 窗口的垂直位置
int nWidth, // 窗口宽度
int nHeight, // 窗口高度
HWND hWndParent, // 父窗口或所有者窗口句柄
HMENU hMenu, // 菜单句柄或子窗口标识符
HANDLE hInstance, // 应用程序实例句柄
LPVOID lpParam, // 指向窗口创建数据的指针
);
在前面的示例中,我们对x、y、nWidth和nHeight参数都传递了同一个值CW_USEDEFAULT,表示使用系统默认的窗口位置和大小,该常量仅对于重叠式窗口(即在dwStype样式中指定了WS_OVERLAPPEDWINDOW,另一个常量WS_TILEDWINDOW有着相同的值)有效。
● 注意:
● 尽管Windows 95是一个32位的操作系统,但是,其中也保留了很多16位的特征,比如说,在Windows 95环境下,系统最多只可以有16384个窗口句柄。而在Windows NT下则无此限。然而,事实上,对于一般的桌面个人机系统来说,我们几乎不可能超过这个限制。
创建窗口完成之后,ShowWindows显示该窗口,第二个参数SW_SHOW表示在当前位置以当前大小激活并显示由第一个参数标识的窗口。然后,函数UpdateWindows向窗口发送一条WM_PAINT消息,以通知窗口更新其客户区。需要注意的是,由UpdateWindows发送的WM_PAINT消息将直接发送到窗口过程(在上面的例子中是WndProc函数),而不是发送到进程的消息队列,因此,尽管这时应用程序的主消息循环尚未启动,但是窗口过程仍可接收到该WM_PAINT消息并更新其用户区。
在完成上面的步骤之后,进入应用程序的主消息循环。
一般情况下,主消息循环具有下面的格式:
while (GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
主消息循环由对三个API函数的调用和一个while结构组成。其中GetMessage从调用线程的消息队列中获取消息,并将消息放到由第一个参数指定的消息结构中。如果指定了第二个参数,则GetMessage获取属于该参数指定的窗口句柄所标识的窗口的消息,如果该参数为NULL,则GetMessage获取属于调用线程及属于该线程的所有窗口的消息。最后两个参数指定了GetMessage所获取消息的范围,如果两个参数均为0,则GetMessage检索并获取所有可以得到的消息。
在上面的代码中,变量msg是一个类型为MSG的结构对象,该结构体的定义如下:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt; } MSG;
下面解释各成员的含义:
hwnd: 标识获得该消息的窗口进程的窗口句柄。
message: 指定消息值。
wParam: 其含义特定于具体的消息类型。
lParam: 其含义特定于具体的消息类型。
time: 指定消息发送时的时间。
pt: 以屏幕坐标表示的消息发送时的鼠标指针的位置。
在while循环体中的TranslateMessage函数将虚拟按键消息翻译为字符消息,然后将消息发送到调用线程的消息队列,在下一次调用GetMessage函数或PeekMessage函数时,该字符消息将被获取。
TranslateMessage函数将WM_KEYDOWN和WM_KEYUP虚拟按键组合翻译为WM_CHAR和WM_DEADCHAR,将WM_SYSKEYDOWN和WM_SYSKEYUP虚拟按键组合翻译为WM_SYSCHAR和WM_SYSREADCHAR。需要注意的一点是,仅当相应的虚拟按键组合能够被翻译为所对应的ASCII字符时,TranslateMessage才发送相应的WM_CHAR消息。
如果一个字符消息被发送到调用线程的消息队列,则TranlateMessage返回非零值,否则返回零值。
● 注意:
● 与在Windows 95操作系统下不同,在Windows NT下,TranslateMessage对于功能键和光标箭头键也返回一个非零值。
然后,函数DispatchMessage将属于某一窗口的消息发送该窗口的窗口过程。这个窗口由MSG结构中的hwnd成员所标识的。函数的返回值为窗口过程的返回值,但是,我们一般不使用这个返回值。这里要注意的是,并不一定是所有属于某一个窗口的消息都发送给窗口的窗口过程,比如对于WM_TIMER消息,如果其lParam参数不为NULL的话,由该参数所指定的函数将被调用,而不是窗口过程。
如果GetMessage从消息队列中得到一个WM_QUIT消息,则它将返回一个假值,从而退出消息循环,WM_QUIT消息的wParam参数指定了由PostQuitMessage函数给出的退出码,一般情况下,WinMain函数返回同一值。
下面我们来看一下程序主窗口的窗口过程WndProc。
窗口过程名是可以由用户自行定义,然后在注册窗口类时在WNDCLASS结构中指定。但是,一般来说,程序都把窗口过程命令为WndProc来类似的名称,如MainWndProc等,并不是一定要这样做,但是这样明显的有利于阅读,因此也是我们推荐的做法。
窗口过程具有如下的原型:
LRESULT WINAPI WndProc(HWND,UINT,WPARAM,LPARAM);
或
LRESULT CALLBACK WndProc(HWND,UINT,WPARAM,LPARAM);
对于编译器而言,两种书写形式都是一样的,它们都等价于
long __stdcall WndProc(void *,unsigned int,unsigned int,long)
窗口过程使用了四个参数,在它被调用时(再强调一点,一般情况下,窗口过程是由操作系统调用,而不是由应用程序调用的,这就是我们为什么将它们称为回调函数的道理),这四个参数对应于所发送消息结构的前四个成员。
下面给出了一个窗口过程的例子:
// WndProc 主窗口过程
LRESULT WINAPI WndProc (HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam)
{
HDC hdc;
RECT rc;
HPEN hPen,hPenOld;
HBRUSH hBrush,hBrushOld;
switch (msg)
{
case WM_PAINT:
hdc=GetDC(hWnd);
GetClientRect(hWnd,&rc);
hPen=CreatePen(PS_SOLID,0,RGB(0,0,0));
hBrush=CreateHatchBrush(HS_DIAGCROSS,RGB(0,0,0));
hPenOld=SelectObject(hdc,hPen);
hBrushOld=SelectObject(hdc,hBrush);
Ellipse(hdc,rc.left,rc.top,rc.right,rc.bottom);
SelectObject(hdc,hPenOld);
SelectObject(hdc,hBrushOld);
ReleaseDC(hWnd,hdc);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProc(hWnd,msg,wParam,lParam);
}
在该窗口过程中,我们处理了最基本两条消息。
第一条消息是WM_PAINT,当窗口客户区的全部或一部分需要重绘时,系统向该窗口发送该消息。在前面的过程中我们已经提到过,在使用ShowWindow函数显示窗口之后,通常随即调用函数UpdateWindow,该函数直接向窗口过程发送一个WM_PAINT消息,以通知窗口绘制其客户区。在该消息的处理函数中,我们先使用GetDC获得窗口的设备句柄,它是用来调用各种绘图方法的。然后调用GetClientRect获得当前窗口的客户区矩形。接着调用CreatePen创建一个黑色画笔,调用CreateHatchBrush创建一个45度交叉线的填充画刷,并且SelectObject函数将它们选入设备描述表中,原有的画笔和画刷被保存到hPenOld和hBrushOld中,以便以后恢复。完成以上步骤之后,调用Ellipse函数以当前客户区大小绘制一个椭圆。最后,再一次调用SelectObject函数恢复原有的画笔和画刷,并调用ReleaseDC释放设备描述表句柄。
对于窗口来说,除了客户区以外的其它内容将由系统进行重绘,这些内容包括窗口标题条、边框、菜单条、工具条以及其它控件,如果包含了它们的话。这种重绘往往发生在覆盖于窗口上方的其它窗口被移走,或者是窗口被移动或改变大小时。因此,对于大多数窗口过程来说,WM_PAINT消息是必须处理的。
另一个对于绝大多数窗口过程都必须处理的消息是WM_DESTROY,当窗口被撤消时(比如用户从窗口的系统菜单中选择了“关闭”,或者单击了右边的小叉,对于这些事件,Windows的默认处理是调用DestroyWindow函数撤销相应的窗口),将会接收到该消息。由于本程序仅在一个窗口,因此在这种情况下应该终止应用程序的执行,因此我们调用了PostQuitMessage函数,该函数向线程的消息队列中放入一个WM_QUIT消息,传递给PostQuitMessage函数的参数将成为WM_QUIT消息的wParam参数,在上面的例子中,该值为0。
对于其它情况, Windows专门为此提供了一个默认的窗口过程,称为DefWindowProc,我们只需要以WndProc的参数原封不动的调用默认窗口过程DefWindowProc,并将其返回值作为WndProc的返回值即可。
将上面讲述的所有内容综合起来,我们就已经使用Win32 SDK完成了一个功能简单,但是结构完整的Win32应用程序了。
对于使用Win32 SDK编写的实用的Win32应用程序,它们的结构与此相比要复杂得多,在这些情况下,应用程序也许不仅仅包括一个窗口,而对应的窗口过程中的switch
结构一般也会是一个异常膨胀的嵌套式switch结构。如此庞大的消息处理过程大大增加了程序调试和维护的难度,使用MFC则有可能在很多程度上减轻这种负担,这便是MFC为广大程序员所乐于接受,以至今天成为实际上的工业标准的原因。但是,不管它如何复杂,归根到底,一般情况下,它仍然具有和我们的这个功能简单的Win32应用程序一样或类似的结构。
为了读者阅读和分析方便,我们把这个程序的完整代码给出如下:
#include <windows.h>
// 函数原型
int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int);
LRESULT WINAPI WndProc(HWND,UINT,WPARAM,LPARAM);
// WinMain 函数
int WINAPI WinMain (HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd; // 主窗口句柄
MSG msg; // 窗口消息
WNDCLASS wc; // 窗口类
if (!hPrevInstance)
{
// 填充窗口类信息
wc.style=CS_HREDRAW|CS_VREDRAW;
wc.lpfnWndProc=WndProc;
wc.cbClsExtra=0;
wc.cbWndExtra=0;
wc.hInstance=hInstance;
wc.hIcon=LoadIcon(NULL,IDI_APPLICATION);
wc.hCursor=LoadCursor(NULL,IDC_ARROW);
wc.hbrBackground=GetStockObject(WHITE_BRUSH);
wc.lpszMenuName=NULL;
wc.lpszClassName="SdkDemo1";
// 注册窗口类
RegisterClass(&wc);
}
// 创建应用程序主窗口
hWnd=CreateWindow ("SdkDemo1", // 窗口类名
"第一个Win32 SDK应用程序", // 窗口标题
WS_OVERLAPPEDWINDOW, // 窗口样式
CW_USEDEFAULT, // 初始化 x 坐标
CW_USEDEFAULT, // 初始化 y 坐标
CW_USEDEFAULT, // 初始化窗口宽度
CW_USEDEFAULT, // 初始化窗口高度
NULL, // 父窗口句柄
NULL, // 窗口菜单句柄
hInstance, // 程序实例句柄
NULL); // 创建参数
// 显示窗口
ShowWindow(hWnd,SW_SHOW);
// 更新主窗口客户区
UpdateWindow(hWnd);
// 开始消息循环
while (GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
// WndProc 主窗口过程
LRESULT WINAPI WndProc (HWND hWnd,
UINT msg,
WPARAM wParam,
LPARAM lParam)
{
HDC hdc;
RECT rc;
HPEN hPen,hPenOld;
HBRUSH hBrush,hBrushOld;
switch (msg)
{
case WM_PAINT:
hdc=GetDC(hWnd);
GetClientRect(hWnd,&rc);
hPen=CreatePen(PS_SOLID,0,RGB(0,0,0));
hBrush=CreateHatchBrush(HS_DIAGCROSS,RGB(0,0,0));
hPenOld=SelectObject(hdc,hPen);
hBrushOld=SelectObject(hdc,hBrush);
Ellipse(hdc,rc.left,rc.top,rc.right,rc.bottom);
SelectObject(hdc,hPenOld);
SelectObject(hdc,hBrushOld);
ReleaseDC(hWnd,hdc);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
break;
}
return DefWindowProc(hWnd,msg,wParam,lParam);
}
这里我们简单的说一下如何在Microsoft Developer Studio中编译该示例程序。请按下面的步骤进行:
1. 选择File菜单下的New命令,新建一个Win32 Application工程,这里我们假设对该工程命名为SdkDemo1。
2. 选择Project菜单下的Add To Project|New...命令,向工程中添加一个C++ Source File (C++源文件),可以将该文件命名为winmain.cpp,不需要键入扩展名,Microsoft Developer Studio在创建文件时会自动加上.cpp的后缀名。
然后在Wordspace窗口的FileView中双击文件名winmain.cpp (在依赖于你在前面过程中的设定),输入下面的源代码即可。
如果已将源代码输入为C++源文件(以.cpp为后缀名的文件),则可以使用Project|Add To Project|Files...将其添加到工程中。
图3.2 示例程序SdkDemo1的运行结果
3. 单击Build菜单下的Build SdkDemo1.exe或Build All或按下快捷键F7或单击Build或Build Minibar工具条上的按钮,编译并创建可执行文件SdkDemo1.exe,运行该可执行文件(从Developer Studio中或资源管理器均可),将得到如图3.2所示的结果。
32位编程的特点
本节假定用户是刚接触32位Windows编程的新手,那么,有必要将一些相关的概念术语弄清楚,
同时,也要把Windows 95、Windows NT和16位的Windows 3.x相区别开来。
这些最重要的概念包括进程和线程的管理以及新的32位平坦内存模式。
进程是装入内存中正在执行的应用程序,进程包括私有的虚拟地址空间、代码、数据及其它操作系统资源,如文件、管道以及对该进程可见的同步对象等。
进程包括了一个或多个在进程上下文内运行的线程。
线程是操作系统分配CPU时间的基本实体。线程可以执行应用程序代码的任何部分,包括当前正在被其它线程执行的那些。同一进程的所有线程共享同样的虚拟地址空间、全局变量和操作系统资源。
在一个应用程序中,可以包括一个或多个进程,每个进程由一个或多个线程构成。
线程通过“休眠”(sleeping,暂停所有执行并等待)的方法,来做到与进程中的其它线程所同步。
在线程休眠前,必须告诉Windows,该线程将等待某一事件的发生。当该事件发生时,Windows发给线程一个唤醒调用,线程继续执行。也就是说,线程与事件一起被同步,除此之外,也可以由特殊的同步对象来进行线程的同步。这些同步对象包括:
● 互斥 不受控制的线程访问在多线程应用程序中可能会引起很大的问题。这里所说的互斥是一小段代码,它时刻采取对共享数据的独占控制以执行代码。
互斥常被应用于多进程的同步数据存取。
● 信号量 信号量与互斥相似,但是互斥只允许在同一时刻一个线程访问它的数据,而信号量允许多个线程在同一时刻访问它的数据。Win32不知道哪一个线程拥有信号量,它只保证信号量使用的资源量。
● 临界区 和互斥相似,但它仅被属于单个进程的线程使用。临界区对象提供非常有效的同步模式,每次在同一时间内只有一个线程可以访问临界区对象。
● 事件 事件对象用于许多实例中去通知休眠的线程所等待的事件已经发生,事件告诉线程何时去执行某一个给定的任务,并可以使多线程流平滑。
将所有的这些同步对象应用于控制数据访问使得线程同步成为可能,否则,如果一个线程改变了另一个线程正在读的数据,将有可能导致很大的麻烦。
在Win32环境下,每个运行的在进程内的线程还可以为它自己的特定线程数据分配内存,通过Win32提供的线程本地存储(TLS)API,应用程序可以建立动态的特定线程数据,在运行时这些数据联系在一起。
下面我们来看在32位应用程序地址空间中的内存分配和内存管理。
常见的内存分配可以划分为两类:帧分配(frame allocation)和堆分配(heap allocation)。
两者的主要区别在于帧分配通常和实际的内存块打交道,而堆分配在一般情况下则使用指向内存块的指针,并且,帧对象在超过其作用域时会被自动的删除,而程序员必须显式的删除在堆上分配的对象。
在帧上分配内存的这种说法来源于“堆栈帧”(stack frame)这个名词,堆栈帧在每当函数被调用时创建,它是一块用来暂时保存函数参数以及在函数中定义的局部变量的内存区域。
帧变量通常被称作自动变量,这是因为编译器自动为它们分配所需的内存。
帧分配有两个主要特征,首先,当我们定义一个局部变量时,编译器将在堆栈帧上分配足够的空间来保存整个变量,对于很大的数组和其它数据结构也是这样;其次,当超过其作用域时,帧变量将被自动的删除。
下面举一个帧分配的例子:
int Func(int Argu1,int Argu2) // 编译器将在堆栈帧上为函数参数变量分配空间
{
// 在堆栈上创建局部对象
char szDatum[256][256];
...
// 超过作用域时将自动删除在堆栈上分配的对象
}
对于局部函数变量,其作用域转变在函数退出时发生,但如果使用了嵌套的花括号,则帧变量的作用域将有可能比函数作用域小。自动删除这些帧变量非常之重要。对于简单的基本数据类型 (如整型或字节变量)、数组或数据结构,自动删除只是简单的回收被这些这是所占用的内存。由于这些变量已超出其作用域,它们将再也不可以被访问。对于C++对象,自动删除的过程要稍稍复杂一些。当一个对象被定义为一个帧变量时,其构造函数在定义对象变量时被自动的调用,当对象超出其作用域时,在对象所占用的内存被释放前,其析构函数先被自动的调用。这种对构造函数和析构函数的调用看起来非常的简便,但我们必须对它们倍加小心,尤其是对析构函数,不正确的构造和释放对象将可能对内存管理带来严重的问题。
在帧上分配对象的最大的优越性在于这些对象将会被自动的删除,也就是说,当你在帧上分配对象之后,不必担心它们会导致内存漏损(memory leak)。
但是,帧分配也有其不方便之处,首先,在帧上分配的变量不可以超出其作用域,其中,帧空间往往是有限的,因此,在很多情况下,我们更倾向于使用堆分配来代替这里所讲述的帧分配来为那些庞大的数据结构或对象分配内存。
堆是为程序所保留的用于内存分配的区域,它与程序代码和堆栈相隔离。在通常情况下,C程序使用函数malloc和free来分配和释放堆内存。调试版本(Debug version)的MFC提供了改良版本的C++内建运算符new和delete用于在堆内存中分配和释放对象。
与帧分配不同,在堆上可分配的对象所占用的内存的总量只受限于系统可有的所有虚拟内存空间。
以下的示例代码对比了上面讨论的内存分配方法在为数组、数据结构和对象分配内存时的用法:
使用帧分配为数组分配内存:
{
const int BUFF_SIZE = 128;
// 在帧上分配数组空间
char myCharArray[BUFF_SIZE];
int myIntArray[BUFF_SIZE];
// 所分配的空间在超出作用域时自动回收
}
使用堆分配为数组分配内存:
const int BUFF_SIZE = 128;
// 在堆上分配数组空间
char* myCharArray = new char[BUFF_SIZE];
int* myIntArray = new int[BUFF_SIZE];
...
delete [] myCharArray;
delete [] myIntArray;
使用帧分配为结构分配内存:
struct MyStructType { int topScore;};
void SomeFunc(void)
{
// 帧分配
MyStructType myStruct;
// 使用该结构
myStruct.topScore = 297;
// 在超出作用域时结构所占用的内存被自动回收
}
使用堆分配为结构分配内存:
// 堆分配
MyStructType* myStruct = new MyStructType;
// 通过指针使用该结构
myStruct->topScore = 297;
delete myStruct;
使用帧分配为对象分配内存:
{
CMyClass myClass; // 构造函数被自动调用
myClass.SomeMemberFunction(); // 使用该对象
}
使用堆分配为对象分配内存:
// 自动调用构造函数
CMyClass *myClass=new CMyClass;
myClass->SomeMemberFunction(); // 使用该对象
delete myClass; // 在使用delete的过程中调用析构函数
● 注意:
●在堆上分配的内存一定要记得释放,对于使用运算符new分配的内存,应当使用delete运算符来释放;而使用malloc函数分配的内存应当使用free函数来释放。
不应当对同一内存块交叉使用运算符new、delete和函数malloc、free (即使用delete运算符释放由malloc函数分配的内存,或使用free函数释放由new运算符根本的内存),否则在MFC的调试版本下将会导致内存冲突。
对固定大小的内存块,使用运算符new和delete要比使用标准C库函数malloc和free方便。
但有时候我们需要使用可变大小的内存块,这时,我们必须使用标准的C库函数malloc、realloc和free。
最后重新强调一点,即不要使用realloc改变由new运算符分配的内存块的大小。
下面我们简单的介绍Win32内存管理模式。
在Microsoft Win32应用程序编程接口中,每一个进程都有自己多达4GB的虚拟地址空间。内存中低位的2GB (从0x00到0x7FFFFFFF)可以为用户所用,高位的2GB (从0x80000000到0xFFFFFFFFF)为内核所保留。进程所使用的虚拟地址并不代码对象在内存中实际的物理地址。事实上,内核为每一个进程维护了一个页映射,页映射是一个用于将虚拟地址转换为对应的物理地址的内部数据结构。
每一个进程的虚拟地址空间都要比所有进程可用的物理内存RAM (随机存取存储器)的总和大得多。为了增加物理存储的大小,内核使用磁盘作为额外的存储空间。对于所有正在执行的进程来说,总的存储空间的量是物理内存RAM和磁盘上可以为页面文件所用的自由空间的总合,这里页面文件指用来增加物理存储空间的磁盘文件。每个进程物理存储空间和虚拟地址(也称为逻辑地址)空间以页的形式来组织,页是一种内存单元,其大小依赖于宿主计算机的类型。对于x86计算机来说,宿主页大小为4KB,但我们不能假定对有所有运行Windows操作系统的计算机,其页大小均为4KB。
为了使内存管理具有最大的灵活性,内核可以将物理内存中的页移入或移出磁盘上的页面文件。
当一个页被移入物理内存时,内核更新受到影响的进程的页映射。将内核需要物理内存空间时,它将物理内存中最近最少使用的页移入页面文件。对于应用程序来说,内核对物理内存的管理是完全透明的,应用程序只对它自己的虚拟地址空间进行操作。
在进程虚拟地址空间中的页可以具在表所列的状态之一:
状态说明
空闲(Free) 空闲页是当前不可用,但可以被占用或保留的页。
保留(Reserved) 保留页是进程的虚拟地址空间中为将来使用所保留的页。进程不可以存取保留页,
并且也没有为保留页分配物理存储。保留页保留虚拟地址中的一段以使得它们不可以被随后的其它分配操作(如malloc和LocalAlloc之类的函数等)所使用。一个进程可以使用VirtualAlloc函数在其地址空间中保留页面,然后再占用这些保留页。最后使用VirtualFree函数释放它们。
占用(Committed) 已被占用的页是那些已分配了物理存储(在内存中或磁盘上)的页。占用页可以被禁
止进行存取,或允许只读存取,或允许读写存取。进程可以使用VirtualAlloc函数分
配占用页。GlobalAlloc和LocalAlloc函数分配允许读写存取的占用页。占用页可以使
用VirtualFree函数进行释放,函数VirtualFree释放页的存储空间,并将其状态改变为
保留。
进程可以使用函数GlobalAlloc和LocalAlloc来分配内存。在Win32 API的32位线性环境中,本地堆和
全局堆并没有区别,因此,使用这两个函数来分配内存对象也没有任何区别。
由GlobalAlloc和LocalAlloc函数分配的内存对象位于私有的占用页中,这些页允许进行读写存取。
私有内存不可以为其它进程所访问。与在Windows 3.x中不同,使用带有GMEM_DDESHARE标志
的GlobalAlloc函数分配的内存事实上并没有被全局共享。保留该标志位仅是为了向前兼容和为一
些应用程序增强动态数据交换(DDE,dynamic data exchange)的性能而使用。应用程序如果因其它
目的需要共享内存,那么必须使用文件映射对象。多个进程可以通过映射同一个文件映射对象的
视来提供命名共享内存。我们在这里将不讨论文件映射和共享内存的问题。
通过使用函数GlobalAlloc和LocalAlloc,可以分配能够表示为32位的任意大小的内存块,所受的唯
一限制是可用的物理内存,包括在磁盘上的页面文件中的存储空间。这些函数,和其它操作全局
和本局内存对象的全局和本地函数一起被包含在Win32 API中,以和Windows的16位版本相兼容。
但是,从16位分段内存模式到32位虚拟内存模式的转变将使得一些函数和一些选项变得不必要甚
至没有意义。比如说,现在不再和近指针和远指针的区别,因为无论在本地还是在全局进行分配
都将返回32位虚拟地址。
函数GlobalAlloc和LocalAlloc都可以分配固定或可移动的内存对象。可移动对象也可以被标记为可
丢弃的(discardable)。在早期的Windows版本中,可移动的内存对象对于内存管理非常之重要,它
们允许系统在必要时压缩堆以为其它内存分配提供可用空间。通过使用虚拟内存,系统能够通过
移动物理内存页来管理内存,而不影响使用这些页的进程的虚拟地址。当系统移动一个物理内存
页时,它简单的将进程的虚拟页映射到新的物理页的位置。可移动内存在分配可丢弃内存仍然有
用。当系统需要额外的物理存储时,它使用一种称作“最近最少使用”的算法来释放非锁定的可
丢弃内存。可丢弃内存可以用于那些不是经常需要和易于重新创建的数据。
当分配固定内存对象时,GlobalAlloc和LocalAlloc返回32位指针,调用线程可以立即使用该指针来
进行内存存取。对于可移动内存,返回值为一个句柄。为了得到一个指向可移动内存的指针,调
用线程可以使用GlobalLock和LocalLock函数。这些函数锁定内存使得它不能够被移动或丢弃,除
非使用函数GlobalReAlloc或LocalReAlloc对内存对象进行重新分配。已锁定内存对象的内存块保持
锁定状态,直至锁定计数减到0,这时该内存块可以被移动或丢弃。
由GlobalAlloc和LocalAlloc所分配的内存的实际大小可能大于所要求的大小。为了得到已分配的实
际内存数,可以使用函数GlobalSize和LocalSize。如果总分配量大于所要求的量,进程则可以使用
所有的这些量。]
函数GlobalReAlloc和LocalReAlloc以字节为单位改变由GlobalAlloc和LocalAlloc函数分配内存对象的
大小或其属性。内存对象的大小可以增大,也可以减小。
函数GlobalFree和LocalFree用于释放由GlobalAlloc、LocalAlloc、GlobalReAlloc或LocalReAlloc分配的
内存。
其它的全局和本地函数包括GlobalDiscard、LocalDiscard、GlobalFlags、LocalFlags、GlobalHandle和
LocalHandle。GlobalDiscard和LocalDiscard用于丢弃指定的可丢弃内存对象,但不使其句柄无效。
该句柄可能通过函数GlobalReAlloc或LocalReAlloc与新分配的内存块相关联。函数GlobalFlags或
LocalFlags返回关于指定内存对象的信息。这些住处包括对象的锁定计数以及对象是否可丢弃或是
否已被丢弃。函数GlobalHandle或LocalHandle返回与指定指针相关联的内存对象的句柄。
Win32进程可以完全的使用标准的C库函数malloc、free等来操作内存。在Windows的早期版本中使
用这些函数,将可能带来问题隐患,但是使用Win32 API的应用程序中则不会。举例来说,使用
malloc分配固定指针将不能使用可移动内存的优点。由于系统可以通过移动物理内存页来自由的
管理内存,而不影响虚拟地址,因此内存管理将不再成为问题。类似的,远指针和近指针之间不
再有差别。因此,除非你希望使用可丢弃内存,否则完全可以将标准的C库函数用于内存管理。
Win32 API提供了一系列的虚拟内存函数来操作或决定虚拟地址空间中的页的状态。许多应用程
序使用标准的分配函数GlobalAlloc、LocalAlloc、malloc等就可以满足其需要。然而,虚拟内存函
数提供了一些这些标准分配函数所不具有的功能,它们可以进行下面的这些操作:
● 保留进程虚拟地址空间中的一段 保留地址空间并不为它们分配物理存储,而只是防止其它
分配操作使用这段空间。它并不影响其它进程的虚拟地址空间。保留页防止了对物理存储
的不必要的浪费,然而它允许进程为可能增长的动态数据结构保留一段地址空间,进程可
以在需要的时候为这些空间分配物理存储。
● 占用进程虚拟地址空间中的保留页的一部分,以使得物理存储(无论是RAM还是磁盘空间)
只对正在进行分配的进程可用。
● 指定允许读写存取、只读存取或不允许存取的占用页区域。这和标准的分配函数总是分配
允许读写存取的页不同。
● 释放一段保留页,使得调用线程在随后的分配操作中可以使用这段虚拟地址。
● 取消对一段页的占用,释放它们的物理存储,使它们可以其它进程在随后的分配中使用。
● 在物理内存RAM中锁定一个或多个占用页,以免系统将这些页交换到页面文件中。
● 获得关于调用线程或指定线程的虚拟地址空间中的一段页的信息。
● 改变调用线程或指定线程的虚拟地址空间中指定占用页段的存取保护。
虚拟内存函数对内存页进行操作。函数使用当前计算机的页大小来对指定的大小和地址进行舍
入。
可以使用函数GetSystemInfo来获得当前计算机的页大小。
函数VirtualAlloc完成以下操作:
● 保留一个或多个自由页。
● 占用一个或多个保留页。
● 保留并占用一个或多个自由页。
你可以指针所保留或占用的页的起始地址,或者让系统来决定。函数将指定的地址舍入到合适的
页边界。保留页是不可访问的,但占用页可以使用标志位PAGE_READWRITE、
PAGE_READONLY和PAGE_NOACCESS来分配。当页被占用时,从页面文件中分配存储空间,
每个页仅在第一次试图对其进行读写操作时被初始化并加载到物理内存中。可以用一般的指针引
用来访问由VirtualAlloc函数占用的页。
函数VirtualFree完成下面的操作:
● 解除对一个或多个页的占用,改变其状态为保留。解除对页的战胜释放与之相关的物理存
储,使其为其它进程可用。任何占用页块都可以被解除占用。
● 释放一个或多个保留页块,改变其状态为自由。释放页块使这段保留空间可以为进程分配
空间使用。保留页只能通过释放由函数VirtualAlloc最初保留的整个块来释放。
● 同时解除对一个或多个占用页的占用并释放它们,将其状态改变为自由。指定的块必须包
括由VirtualAlloc最初保留的整个块,而且这些页的当前状态必须为占用。
函数VirtualLock允许进程将一个或多个占用页锁定在物理内存RAM中,防止系统将它们交换到页
面文件中。这保证了一些要求苛刻的数据可以不通过磁盘访问来存取。将一个页锁定入内存是很
危险的,因为它限制了系统管理内存的能力。由于可能会将可执行代码交换到页面文件中,可执
行程序使用VirtualLock将有可能降低系统性能。函数VirtualUnlock解除VirtualLock对内存的锁
定。
函数VirtualQuery和VirtualQueryEx返回关于以进程地址空间中某一指定地址开始的一段连续内存
区域的信息。VirtualQuery返回关于调用线程内存的信息。VirtualQueryEx返回指定进程内存的信
息,这通常用来支持调试程序,这些程序常常需要知道关于被调试进程的信息。页区域以相对于
指定地址最接近的页边界为界。一般来说,它通过具有下述属性的后续页来进行扩展:
所有页具有相同的状态,或为占用,或为保留,或为自由。
如果初始页不为自由,区域中的所有页都属于通过调用VirtualAlloc保留的同一个最初页分配。
所有页的存取保护相同,或为PAGE_READONLY,或为PAGE_READWRITE,或为
PAGE_NOACCESS。
函数VirtualProtect允许进程修改进程地址空间中任意占用页的存取保护。举例来说,一个进程可
以分配读写页来保存易受影响的数据,然后将存取改变为只读或禁止访问,以避免无意中被重
写。典型的,VirtualProtect用于使用VirtualAlloc分配的页,但事实下,它也可以用于通过其它分
配函数占用的页。然而,VirtualProtect改变整个页的保护状态,而由其它函数返回的指针并非总
是指向页边界。函数VirtualProtectEx类似于VirtualProtect,但函数VirtualProtectEx可以改变指定进
程的内存的保护状态。改变这些内存的保护状态在调试程序访问被调试进程的内存的非常有用。__