VC 笔记

 Windows程序内部运行机制
1、API与SDK
Windows操作系统提供了各种各样的函数,以方便我们开发Windows应用程序,这些函数是Windows操作系统提供给应用程序编程的接口(Application Programming Interface),简称为API函数。我们在编写Windows程序时所说的API函数,就是指系统提供的函数,所有主要的Windows函数都在Window.h头文件中进行了说明
微软提供的API函数大多是有意义的单词组合,每个单词的首字母大写
MSDN是微软为开发人员提供的一套帮助系统,其中包含大量的开发文档、技术文章和示例代码
SDK的全称是Software Development Kit,中文译为软件开发包。SDK实际上就是开发所需资源的一个集合,包括API函数库、帮助文档、使用手册、辅助工具等资源
2、窗口与句柄
窗口是Windows应用程序中一个非常重要的元素,一个Windows应用程序至少要有一个窗口,称为主窗口。窗口是屏幕上的一块矩形区域,是Windows应用程序与用户进行交互的接口。利用窗口,可以接受用户的输入,以及显示输出
一个应用程序窗口通常包括标题栏、菜单栏、系统菜单、最小化框、最大化框、可调边框,有的还有滚动条
窗口可以分为客户区和非客户区。客户区是窗口的一部分,应用程序通常在客户区中显示文字或者绘制图形。标题栏、菜单栏、系统菜单、最小化框、最大化框、可调边框统称为窗口的非客户区,它们由Windows系统来管理,而应用程序则主要管理客户区的外观及操作
在Windows应用程序中,窗口是通过窗口句柄(HWND)来标识的。我们要对某个窗口进行操作,首先就要得到这个窗口的句柄。在Windows程序中,有各种各样的资源(窗口,图标、光标等),系统在创建这些资源时会为它们分配内存,并返回标识这些资源的标识号,即句柄。
3、消息与消息队列
在Windows中,不仅用户程序可以调用系统的API函数,反过来,系统也会调用用户程序,这个调用是通过消息队列来进行的
Windows程序设计是一种基于事件驱动方式的程序设计模式,主要是基于消息的。用户与应用程序交互时,操作系统感知事件,将事件包装成一个消息,投递到应用程序的消息队列中,然后应用程序从消息队列中取出消息并进行响应。在这个处理过程中,操作系统也会给应用程序“发送消息”。所谓“发送消息”,实际上是操作系统调用程序中一个专门负责处理消息的函数,这个函数称为窗口过程
1、消息
在Windows程序中,消息是由MSG结构体定义的。MSG结构体的定义如下:
typedef struct tagMSG {     // msg 
    HWND   hwnd;     
    UINT   message;
    WPARAM wParam;
    LPARAM lParam;
    DWORD  time;
    POINT  pt;
} MSG;
第一个成员变量hwnd表示消息所属的窗口。我们通常开发的程序都是窗口应用程序,一个消息一般都是与某个窗口相关联的。例如,在某个活动窗口中按下鼠标左键,产生的按键消息就是发给该窗口的。在Windows程序中,用HWND类型的变量来标识窗口
第二个成员变量message指定了消息的标识符。在Windows中,消息是由一个数值来表示的,不同的消息对应不同的数值。但是由于数值不便于记忆,所以Windows将消息对应的数值定义为WM_XXX宏的形式,WM是Windows Message的缩写,XXX对应某种消息的英文拼写的大写形式。在程序中,我们通常都是以WM_XXX宏的形式来使用消息的
第三、第四个成员变量wParam和lParam,用于指定消息的附加信息。wParam、lParam表示的信息随消息的不同而不同,如果想知道这两个成员变量具体表示的信息,可以在MSDN中关于某个具体消息的说明文档查看到。在VC++开发环境下通过goto definition查看WPARAM和LPARAM这两种类型的定义,可以发现这两种类型实际上就是unsigned int和long
最后两个变量分别表示消息投递到消息队列中的时间和鼠标的当前位置
2、消息队列
每一个Windows应用程序开始执行后,系统都会为该程序创建一个消息队列,这个消息队列用来存放该程序创建的窗口的消息。Windows将产生的消息依次放到消息队列中,而应用程序则通过一个消息循环不断地从消息队列中取出消息,并进行响应。这种消息机制,就是Windows程序运行的机制
3、进队消息和不进队消息
Windows程序中的消息可以分为“进队消息”和“不进队消息”。进队的消息将由系统放入到应用程序的消息队列中,然后由应用程序取出并发送。不进队的消息在系统调用窗口过程时,直接发送给窗口。不管是进队消息还是不进队消息,最终都由系统调用窗口过程函数对消息进行处理
4、WinMain函数
当Windows操作系统启动一个程序时,调用该程序的WinMain函数(实际是由插入到可执行文件中的启动代码调用的)。WinMain是Windows程序的入口点函数,与DOC程序的入口点函数main的作用相同,当WinMain函数结束或返回时,Windows应用程序结束
编写一个完整的Win32程序,该程序实现的功能是创建一个窗口,并在窗口中响应键盘及鼠标消息,程序实现的步骤为:
WinMain函数的定义
创建一个窗口
进行消息循环
编写窗口过程函数
4.1、WinMain函数的定义
WinMain函数的原形声明如下:
int WINAPI WinMain(
  HINSTANCE hInstance,  // handle to current instance
  HINSTANCE hPrevInstance,  // handle to previous instance
  LPSTR lpCmdLine,      // pointer to command line
  int nCmdShow          // show state of window
);
WinMain函数接收4个参数,这些参数都是在系统调用WinMain函数时,传递给应用程序的
第一个参数hInstance表示该程序当前运行的实例的句柄,这是一个数值。当程序在Windows下运行时,它唯一标识运行中的实例(注意,只有运行中的程序实例,才有实例句柄)。一个应用程序可以运行多个实例,每运行一个实例,系统都会给该实例分配一个句柄值,并通过hInstance参数传递给WinMain函数
第二个参数hPrevInstance表示当前实例的前一个实例的句柄,在Win32环境下,这个参数总是NULL,即在Win32环境下,这个参数不再起作用
第三个参数lpCmdLine是一个以空终止的字符串,指定传递给应用程序的命令行参数。要在VC++开发环境中向应用程序传递参数,可以单击菜单Project-Setting,选择“Debug”选项卡,在“Program arguments”编辑框中输入你想传递给应用程序的参数
第四个参数nCmdShow指定程序的窗口应该如何显示,例如最大化、隐藏等。这个参数的值由该程序的调用者所指定,应用程序通常不需要去理会这个参数的值
关于WinMain函数前的修饰符WINAPI,其实就是_stdcall
4.2、窗口的创建
创建一个完整的窗口,需要经过下面几个操作步骤:
设计一个窗口类
注册窗口类
创建窗口
显示及更新窗口
1、设计一个窗口类
一个完整的窗口具有许多特征,包括光标、图标、背景色等。在创建一个窗口前,必须对该类型的窗口进行设计,指定窗口的特征。Windows提供了WNDCLASS结构体来定义窗口特征,该结构体定义好一个窗口所具有的基本属性。WNDCLASS结构体的定义如下:
typedef struct _WNDCLASS {
    UINT    style;
    WNDPROC lpfnWndProc;
    int     cbClsExtra;
    int     cbWndExtra;
    HANDLE  hInstance;
    HICON   hIcon;
    HCURSOR hCursor;
    HBRUSH  hbrBackground;
    LPCTSTR lpszMenuName;
    LPCTSTR lpszClassName;
} WNDCLASS;
第一个成员变量style指定了这一类型窗口的样式,常用的样式:
CS_HREDRAW
当窗口水平方向上的宽度发生变化时,将重新绘制整个窗口。当窗口发生重绘时,窗口中的文字和图形将被擦除。如果没有指定这一样式,那么在水平方向上调整窗口的宽度时,将不会重绘窗口
CS_VREDRAW
当窗口垂直方向上的宽度发生变化时,将重新绘制整个窗口。如果没有指定这一样式,那么在垂直方向上调整窗口的宽度时,将不会重绘窗口
知识点:在Windows.h中,以CS_开头的类样式(Class Style)标识符被定义为16位的类常量,这些常量都只有某一位为1。用这种方式定义的标识符称为“位标志”,我们可以使用位运算操作符来组合使用这些样式
第二个成员变量lpfnWndProc是一个函数指针,指向窗口过程函数,窗口过程函数是一个回调函数,回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。回调函数的实现机制是:
1、定义一个回调函数
2、提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者
3、当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理
针对Windows的消息处理机制,窗口过程函数被调用的过程如下:
1、在设计窗口类的时候,将窗口过程函数的地址付给lpfnWndProc成员变量
2、调用RegisterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址
3、当应用程序接收到某一窗口的消息时,调用DispatchMessage(&msg)将消息回传给系统,系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理
一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一特定的窗口类相关联,基于该窗口类创建的窗口使用同一个窗口过程
lpfnWndProc成员变量的类型是WNDPROC,在VC++开发环境中使用goto definition功能,可以看到WNDPROC的定义:
typedef LRESULT (CALLBACK * WNDPROC)(HWND,UINT,WPARAM,LPARAM);
LRESULT和CALLBACK实际上是long和_stdcall
从WNDPROC的定义可以知道,WNDPROC实际上是函数指针类型
注意:WNDPROC被定义为指向窗口过程函数的指针类型,窗口过程函数的格式必须与WNDPROC相同
知识点:在函数调用过程中,会使用栈。_stdcall与_cdecl是两种不同的函数调用约定,定义了函数参数入栈的顺序,由调用函数还是被调用函数将参数弹出栈,以及产生函数修饰名的方法。
第三个成员变量cbClsExtra:Windows为系统中的每一窗口类管理一个WNDCLASS结构。在应用程序注册一个窗口类时,它可以让Windows系统为WNDCLASS结构分配和追加一定字节数的附加内存空间,这部分内存空间称为类附加内存,由属于这种窗口类的所有窗口所共享,类附加内存空间用于存储类的附加信息。Windows系统把这部分内存初始化为0。一般我们将这个参数设置为0
第四个成员变量cbWndExtra:Windows系统为每一个窗口管理一个内部数据结构,在注册一个窗口类时,应用程序能够指定一定字节数的附加内存空间,称为窗口附加内存。在创建这类窗口时,Windows系统就为窗口的结构分配和追加指定数目的附加内存空间,应用程序可用这部分内存存储窗口特有的数据。Windows系统把这部分内存初始化为0。一般我们将这个参数设置为0
第五个成员变量hInstance指定包含窗口过程的程序的实例句柄
第六个成员变量hIcon指定窗口类的图标句柄,这个成员变量必须是一个图标资源的句柄,如果这个成员为NULL,那么系统将提供一个默认的图标
在为hIcon变量赋值时,可以调用LoadIcon函数加载一个图标资源,返回系统分配给该图标的句柄。
HICON LoadIcon(
  HINSTANCE hInstance, // handle to application instance
  LPCTSTR lpIconName   // icon-name string or icon resource
                       // identifier
);
LoadIcon的第二个参数是LPCTSTR类型,实际被定义为CONST CHAR *,即指向字符常量的指针,而图标的ID是一个整数。对于这种情况,我们需要MAKEINTERSOURCE宏把资源ID标识符转换为需要的LPCTSTR类型
知识点:在VC++中,对于自定义的菜单、图标、光标、对话框等资源,都保存在资源脚本(通常扩展名为.rc)文件中。在VC++开发环境中,要访问资源文件,可以单击左边项目视图窗口底部的ResourceView选项卡,你将看到以树状列表形式显示的资源项目。在任何一种资源上双击鼠标左键,将打开资源编辑器。在资源编辑器中,以“所见即所得”的方式对资源进行编辑。资源文件本身是文本文件格式,如果了解资源文件的编写格式,也可以直接使用文本编辑器对资源进行编辑
在VC++中,资源是通过标识符(ID)来标识的,同一个ID可以标识多个不同的资源。资源的ID实质上是一个整数,在“resource.h”中定义为一个宏。我们在为资源指定ID的时候,应该养成一个良好的习惯,即在“ID”后附加特定资源英文名称的首字母,例如,菜单资源为IDM_XXX,图标资源为:IDI_XXX。采用这种方式时,我们在程序中使用资源ID时,可以一目了然
WNDCLASS结构体第七个成员变量hCursor指定窗口类的光标句柄,这个成员变量必须是一个光标资源的句柄
第八个成员变量hbrBackground指定窗口类的背景画刷句柄。当窗口发生重绘时,系统使用这里指定的画刷来擦除窗口的背景。我们可以调用GetStockObject函数来得到系统的标准画刷,GetStockObject函数不仅可以用于获取画刷句柄,还可以用于获取画笔、字体和调色板的句柄
第九个成员变量lpszMenuName是一个以空终止的字符串,制定菜单资源的名字。如果使用菜单资源的ID号,那么需要用MAKEINTERSOURCE宏来进行转换。
要注意,菜单并不是一个窗口
第十个成员变量lpszClassName是一个以空终止的字符串,指定窗口类的名字
2、注册窗口类
设计完窗口类(WNDCLASS)后,需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。注册函数的原型声明如下:
ATOM RegisterClass(
  CONST WNDCLASS *lpWndClass   // address of structure with class
                               // data
);
3、创建窗口
设计好窗口类并且将其注册成功之后,就可以用CreateWindow函数产生这种类型的窗口了。CreateWindow函数的原型声明如下:
HWND CreateWindow(
  LPCTSTR lpClassName,  // pointer to registered class name
  LPCTSTR lpWindowName, // pointer to window name
  DWORD dwStyle,        // window style
  int x,                // horizontal position of window
  int y,                // vertical position of window
  int nWidth,           // window width
  int nHeight,          // window height
  HWND hWndParent,      // handle to parent or owner window
  HMENU hMenu,          // handle to menu or child-window identifier
  HANDLE hInstance,     // handle to application instance
  LPVOID lpParam        // pointer to window-creation data
);
参数lpClassName指定窗口类的名称。产生窗口的过程是由操作系统完成的,如果再调用CreateWindow函数之前,没有用RegisterClass函数注册过窗口类,操作系统将无法得知这一类型窗口的相关信息,从而导致创建窗口失败
参数lpWindowName指定窗口的名字。如果窗口样式指定了标题栏,那么这里指定的窗口名字将显示在标题栏上
参数dwStyle指定创建的窗口的样式,要注意区分WNDCLASS中的style成员和CreateWindow函数的dwStyle参数,前者指定窗口类的样式,基于该窗口类创建的窗口都有这些格式,后者是指定某个具体的窗口的样式。下面是几种常用的窗口类型的说明:
WS_OVERLAPPED:产生一个层叠的窗口,一个层叠的窗口有一个标题栏和一个边框
WS_CAPTION:创建一个有标题栏的窗口
WS_SYSMENU:创建一个在标题栏上带有系统菜单的窗口
WS_THICKFRAME:创建一个具有可调边框的窗口
CreateWindow函数的参数x,y,nWidthn,Hight分别指定窗口左上角的x,y坐标,窗口的宽度,高度
参数hWndParent指定被创建窗口的父窗口句柄。窗口之间有父子关系,子窗口必须具有WS_CHILD样式
参数hMenu指定窗口菜单的句柄
参数hInstance:指定窗口所属的应用程序实例的句柄
参数lpParam:作为WM_CREATE消息的附加参数lParam传入的数据指针。在创建多文档界面的客户窗口时,lpParam必须指向CLIENTCREATESTRUCT结构体
如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄,否则返回NULL。注意,在创建窗口之前应先定义一个窗口句柄变量来接受创建窗口之后返回的句柄值
4、显示及更新窗口
显示窗口
窗口创建之后,调用函数ShowWindow来显示窗口,该函数的原型声明如下:
BOOL ShowWindow(
  HWND hWnd,     // handle to window
  int nCmdShow   // show state of window
);
更新窗口
在调用ShowWindow函数之后,紧接着调用UpdateWindow来刷新窗口,该函数的原型声明如下:
BOOL UpdateWindow(
  HWND hWnd   // handle of window
);
UpdateWindow函数通过发送一个WM_PAINT消息来刷新窗口,UpdateWindow将WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到消息队列中,请注意这一点
4.3、消息循环
在创建窗口、显示窗口、更新窗口后,需要编写一个消息循环,不断地从消息队列中取出消息,并进行响应。要从消息队列中取出消息,需要调用GetMessage()函数,该函数的原型声明如下:
BOOL GetMessage(
  LPMSG lpMsg,         // address of structure with message
  HWND hWnd,           // handle of window
  UINT wMsgFilterMin,  // first message
  UINT wMsgFilterMax   // last message
);
参数lpMsg指向一个消息结构体,GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中
参数hWnd指定接收属于哪一个窗口的消息。通常将其设置为NULL,用于接收属于调用线程的所有窗口的窗口消息
参数wMsgFilterMin指定要获取的消息的最小值,通常设置为0
参数wMsgFilterMax指定要获取的消息的最大值。如果wMsgFilterMin和wMsgFilterMax都设置为0,则接受所有消息
GetMessage函数接收到除了WM_QIUT外的消息均返回非零值。对于WM_QIUT消息,该函数返回零。如果出现了错误,该函数返回-1
编写的消息循环代码如下:
MSG msg;
 while(GetMessage(&msg,NULL,0,0))
 {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
GetMessage函数只有在接收到WM_QIUT消息时,才返回0。此时,while语句判断的条件为假,循环退出,程序才有可能结束运行。在没有接收到WM_QIUT消息时,Windows应用程序就通过这个while循环来保证程序始终处于运行状态
TranslateMessage函数用于将虚拟键消息转换为字符消息。字符消息被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出。当我们敲击键盘上的某个字符键时,系统将产生WM_KEYDOWN和WM_KEYUP消息。这两个消息的附加参数包含的是虚拟键代码和扫描码等信息,而我们在程序中需要得到某个字符的ACSII码,TranslateMessage 函数就可以将WM_KEYDOWN和WM_KEYUP消息的组合转换为一条WM_CHAR消息(该消息的wParam附加参数包含了字符的ASCII码),并将转换后的新消息投递到线程的消息队列中。注意,TranslateMessage函数并不会修改原有的消息,它只是产生新的消息并投递到消息队列中
DispatchMessage函数分派一个消息到窗口过程,由窗口过程对消息进行处理。DispatchMessage实际上是将消息回传给操作系统,由操作系统调用窗口过程对消息进行处理
Windows应用程序的消息处理机制:
    1、操作系统接收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中
2、应用程序在消息循环中调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理
3、应用程序调用DispatchMessage,将消息回传给操作系统。消息由MSG结构体对象来表示,其中包含了接收消息的窗口的句柄。因此,DispatchMessage函数总能进行正确的传递
4、系统利用WNDCLASS结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)
提示:从消息队列中获取消息还可以调用PeekMessage函数;发送消息可以使用SendMessage和PostMessage函数。SendMessage函数将消息直接发送给窗口,并调用该窗口的窗口过程进行处理。在窗口过程对消息处理完毕后,该函数才返回。PostMessage函数将消息放入到与创建窗口的线程相关的消息队列后立即返回
4.4、编写窗口过程函数
一个Windows应用程序的主要代码部分就集中在窗口过程函数中,该函数的声明形式如下:
LRESULT CALLBACK WindowProc(
  HWND hwnd,      // handle to window
  UINT uMsg,      // message identifier
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);
窗口过程函数的名字可以任意取,但函数定义的形式必须和上述声明的形式一致
WindowProc函数的4个参数分别对应消息的窗口句柄、消息代码、消息代码的两个附加参数。一个程序可以有多个窗口,窗口过程函数的第1个参数hwnd就标识了接收消息的特定窗口
在窗口过程函数内部使用switch/case语句来确定窗口过程接收的是什么消息,以及如何对这个消息进行处理
5、 小结
创建一个Win32应用程序的步骤:
编写WinMain函数,可以在MSDN上查找并复制
设计窗口类
注册窗口类
显示并更新窗口
编写消息循环
编写窗口过程函数
MFC框架程序剖析
MFC(微软基础类库)是微软为了简化程序员的开发工作所开发的一套C++类的集合,是一套面向对象的函数库,以类的方式提供给用户使用。利用这些类,可以有效地帮助程序员完成Windows应用程序的开发
1、MFC AppWizard
MFC AppWizard是一个辅助生成源代码的向导工具,可以自动生成基于MFC框架的源代码。
2、基于MFC的程序框架剖析
MFC库是开发Windows应用程序的C++接口。MFC提供了面向对象的框架,程序开发人员可以基于这一框架开发Windows应用程序。MFC采用面向对象设计,将大部分Windows API封装到C++类中,以类成员函数的形式提供给程序开发人员调用
在MFC中,类的命名都以字母“C”开头。对于一个单文档应用程序,都有一个CMainFrame类,和一个“C+工程名+App”为名字的类,一个以“C+工程名+Doc”为名字的类,一个以“C+工程名+View”为名字的类。
CFrameWnd和CView类有一个共同的基类:CWnd。Cwnd类是MFC中一个非常重要的类,它封装了与窗口相关的操作
2.1、MFC程序中的WinMain函数
基于MFC的Windows应用程序也有一个WinMain函数,这个WinMain函数是在程序编译链接时,由链接器将该函数链接到MFC程序中的
实际上,WinMain函数在APPMODUL.CPP这个文件中,以下是该文件中的代码:
extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpCmdLine, int nCmdShow)
{
 // call shared/exported WinMain
 return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
_tWinMain实际上是一个宏,展开之后就是WinMain函数
#define _tWinMain   WinMain
1、theApp全局对象
看一下Test.cpp的源文件,可以发现程序中定义了一个CTestApp类型的全局对象:theApp,代码如下:
// The one and only CTestApp object
CTestApp theApp;
theApp的构造函数会在WinMain函数之前执行
提示:MFC程序的全局变量都放置在ClassView标签页的Globals分支下,展开该分支即可看到程序当前所有的全局变量。
无论是全局变量,还是全局对象,程序在运行时,在加载main函数之前,就已经为全局变量或全局对象分配了内存空间。对一个全局对象来说,此时就会先调用该对象的构造函数,构造该对象,并进行初始化操作
对于Win32 SDK应用程序来说,应用程序的实例是由实例句柄(WinMain函数的参数hInstance)来标识的。而对MFC程序来说,通过产生一个应用程序类的对象来惟一标识应用程序的实例。每一个MFC程序有且仅有一个从应用程序类(CWinApp)派生的类。每一个MFC程序实例有且仅有一个该派生类的实例化对象,也就是theApp全局对象。该对象就表示了应用程序本身
当一个子类在构造之前会先调用其父类的构造函数。因此theApp对象的构造函数CTestApp在调用之前,会调用其父类CWinApp的构造函数,从而把我们程序自己创建的类与微软提供的基类关联起来。CWinApp的构造函数完成程序运行时的一些初始化工作
定义CWinApp类的源文件:appcore.cpp,其中CWinApp构造函数的代码如下:
CWinApp::CWinApp(LPCTSTR lpszAppName)
{
 if (lpszAppName != NULL)
  m_pszAppName = _tcsdup(lpszAppName);
 else
  m_pszAppName = NULL;
 
 // initialize CWinThread state
 AFX_MODULE_STATE* pModuleState = _AFX_CMDTARGET_GETSTATE();
 AFX_MODULE_THREAD_STATE* pThreadState = pModuleState->m_thread;
 ASSERT(AfxGetThread() == NULL);
 pThreadState->m_pCurrentWinThread = this;
 ASSERT(AfxGetThread() == this);
 m_hThread = ::GetCurrentThread();
 m_nThreadID = ::GetCurrentThreadId();
 
 // initialize CWinApp state
 ASSERT(afxCurrentWinApp == NULL); // only one CWinApp object please
 pModuleState->m_pCurrentWinApp = this;
 ASSERT(AfxGetApp() == this);
 
 // in non-running state until WinMain
 m_hInstance = NULL;
 m_pszHelpFilePath = NULL;
 m_pszProfileName = NULL;
 m_pszRegistryKey = NULL;
 m_pszExeName = NULL;
 m_pRecentFileList = NULL;
 m_pDocManager = NULL;
 m_atomApp = m_atomSystemTopic = NULL;
 m_lpCmdLine = NULL;
 m_pCmdInfo = NULL;
 
 // initialize wait cursor state
 m_nWaitCursorCount = 0;
 m_hcurWaitCursorRestore = NULL;
 
 // initialize current printer state
 m_hDevMode = NULL;
 m_hDevNames = NULL;
 m_nNumPreviewPages = 0;     // not specified (defaults to 1)
 
 // initialize DAO state
 m_lpfnDaoTerm = NULL;   // will be set if AfxDaoInit called
 
 // other initialization
 m_bHelpMode = FALSE;
 m_nSafetyPoolSize = 512;        // default size
}
上述CWinApp的构造函数中有这样一句代码:
pModuleState->m_pCurrentWinApp = this;
根据C++继承性原理,这个this对象代表的是子类CTestApp的对象,即theApp
2、AfxWinMain函数
当程序调用了CWinApp类的构造函数,并执行了CTestApp类的构造函数,且产生了theApp对象之后,接下来就进入WinMain函数。WinMain函数实际上是通过调用AfxWinMain函数来完成它的功能的
知识点: Afx前缀的函数代表应用程序框架函数。在MFC中,以Afx为前缀的函数都是全局函数,可以在程序的任何地方调用它们
定义AfxWinMain函数的源文件为:WINMAIN.CPP,AfxWinMain函数的定义代码如下:
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 LPTSTR lpCmdLine, int nCmdShow)
{
 ASSERT(hPrevInstance == NULL);
 
 int nReturnCode = -1;
 CWinThread* pThread = AfxGetThread();
 CWinApp* pApp = AfxGetApp();
 
 // AFX internal initialization
 if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
  goto InitFailure;
 
 // App global initializations (rare)
 if (pApp != NULL && !pApp->InitApplication())
  goto InitFailure;
 
 // Perform specific initializations
 if (!pThread->InitInstance())//设计窗口类、注册窗口类、创建窗口、显示及更新窗口
 {
  if (pThread->m_pMainWnd != NULL)
  {
   TRACE0("Warning: Destroying non-NULL m_pMainWnd/n");
   pThread->m_pMainWnd->DestroyWindow();
  }
  nReturnCode = pThread->ExitInstance();
  goto InitFailure;
 }
 nReturnCode = pThread->Run();//消息循环
 
InitFailure:
#ifdef _DEBUG
 // Check for missing AfxLockTempMap calls
 if (AfxGetModuleThreadState()->m_nTempMapLock != 0)
 {
  TRACE1("Warning: Temp map lock count non-zero (%ld)./n",
   AfxGetModuleThreadState()->m_nTempMapLock);
 }
 AfxLockTempMaps();
 AfxUnlockTempMaps(-1);
#endif
 
 AfxWinTerm();
 return nReturnCode;
}
AfxWinMain首先调用AfxGetThread函数获得一个CWinThread类型的指针,接着调用AfxGetApp函数获得一个CWinApp类型的指针。CWinApp派生于CWinThread
下面是AfxGetThread函数的源代码,位于THRDCORE.CPP文件中
CWinThread* AFXAPI AfxGetThread()
{
 // check for current thread in module thread state
 AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
 CWinThread* pThread = pState->m_pCurrentWinThread;
 
 // if no CWinThread for the module, then use the global app
 if (pThread == NULL)
  pThread = AfxGetApp();
 
 return pThread;
}
AfxGetThread函数返回的就是AfxGetApp函数的结果,因此,AfxWinMain函数中的pThread和pApp这两个指针是一致的
AfxGetApp是一个全局函数,定义于AFXWIN1.INL中:
_AFXWIN_INLINE CWinApp* AFXAPI AfxGetApp()
  { return afxCurrentWinApp;}
而afxCurrentWinApp的定义位于AFXWIN.H文件中,代码如下:
#define afxCurrentWinApp  AfxGetModuleState()->m_pCurrentWinApp
通过CWinApp构造函数的代码可以知道,AfxGetApp函数返回的是CWinApp构造函数中保存的this指针。即:theApp全局对象
也就是说,对Test程序来说,pThread和pApp所指向的都是CTestApp类的对象,即theApp全局对象
3、InitInstance函数
回到AfxWinMain函数,可以看到接下来的代码中,pThread和pApp调用了三个函数,这三个函数就完成了Win32程序所需要的几个步骤:设计窗口类、注册窗口类、创建窗口、显示窗口、更新窗口、消息循环,以及窗口过程函数。
pApp首先调用InitApplication函数,该函数完成MFC内部管理方面的工作,接着调用pThread的InitInstance函数。InitInstance函数是一个虚函数,根据类的多态性原理,可以知道AfxWinMain函数这里实际上调用的是子类CTestApp的InitInstance函数
CTestApp类的InitInstance函数定义代码如下:
BOOL CTestApp::InitInstance()
{
 AfxEnableControlContainer();
 
 // Standard initialization
 // If you are not using these features and wish to reduce the size
 //  of your final executable, you should remove from the following
 //  the specific initialization routines you do not need.
 
#ifdef _AFXDLL
 Enable3dControls();   // Call this when using MFC in a shared DLL
#else
 Enable3dControlsStatic(); // Call this when linking to MFC statically
#endif
 
 // Change the registry key under which our settings are stored.
 // TODO: You should modify this string to be something appropriate
 // such as the name of your company or organization.
 SetRegistryKey(_T("Local AppWizard-Generated Applications"));
 
 LoadStdProfileSettings();  // Load standard INI file options (including MRU)
 
 // Register the application's document templates.  Document templates
 //  serve as the connection between documents, frame windows and views.
 
 CSingleDocTemplate* pDocTemplate;
 pDocTemplate = new CSingleDocTemplate(
  IDR_MAINFRAME,
  RUNTIME_CLASS(CTestDoc),
  RUNTIME_CLASS(CMainFrame),       // main SDI frame window
  RUNTIME_CLASS(CTestView));
 AddDocTemplate(pDocTemplate);
 
 // Parse command line for standard shell commands, DDE, file open
 CCommandLineInfo cmdInfo;
 ParseCommandLine(cmdInfo);
 
 // Dispatch commands specified on the command line
 if (!ProcessShellCommand(cmdInfo))
  return FALSE;
 
 // The one and only window has been initialized, so show and update it.
 m_pMainWnd->ShowWindow(SW_SHOW);
 m_pMainWnd->UpdateWindow();
 
 return TRUE;
}
2.2、MFC框架窗口
1、设计和注册窗口
有了WinMain函数,接下来应该是设计窗口类和注册窗口类了。MFC已经为我们预定义了一些默认的标准窗口类,只需要选择所需的窗口类,然后注册就可以了。窗口类的注册是由AfxEndDeferRegisterClass函数完成的,该函数的定义位于WINCORE.CPP文件中。
BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister)
{
 // mask off all classes that are already registered
 AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
 fToRegister &= ~pModuleState->m_fRegisteredClasses;
 if (fToRegister == 0)
  return TRUE;
 
 LONG fRegisteredClasses = 0;
 
 // common initialization
 WNDCLASS wndcls;
 memset(&wndcls, 0, sizeof(WNDCLASS));   // start with NULL defaults
 wndcls.lpfnWndProc = DefWindowProc;
 wndcls.hInstance = AfxGetInstanceHandle();
 wndcls.hCursor = afxData.hcurArrow;
 
 INITCOMMONCONTROLSEX init;
 init.dwSize = sizeof(init);
 
 // work to register classes as specified by fToRegister, populate fRegisteredClasses as we go
 if (fToRegister & AFX_WND_REG)
 {
  // Child windows - no brush, no icon, safest default class styles
  wndcls.style = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
  wndcls.lpszClassName = _afxWnd;
  if (AfxRegisterClass(&wndcls))
   fRegisteredClasses |= AFX_WND_REG;
 }
 if (fToRegister & AFX_WNDOLECONTROL_REG)
 {
  // OLE Control windows - use parent DC for speed
  wndcls.style |= CS_PARENTDC | CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW;
  wndcls.lpszClassName = _afxWndOleControl;
  if (AfxRegisterClass(&wndcls))
   fRegisteredClasses |= AFX_WNDOLECONTROL_REG;
 }
 if (fToRegister & AFX_WNDCONTROLBAR_REG)
 {
  // Control bar windows
  wndcls.style = 0;   // control bars don't handle double click
  wndcls.lpszClassName = _afxWndControlBar;
  wndcls.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
  if (AfxRegisterClass(&wndcls))
   fRegisteredClasses |= AFX_WNDCONTROLBAR_REG;
 }
 
 // must have registered at least as mamy classes as requested
 return (fToRegister & fRegisteredClasses) == fToRegister;
}
AfxEndDeferRegisterClass()函数首先判断窗口类的类型,然后赋予其相应的类名(wndcls.lpszClassName变量),这些类名都是MFC预定义的。之后就调用AfxRegisterClass函数注册窗口类
AfxRegisterClass函数的定义也位于WINCORE.CPP文件中,代码如下:
BOOL AFXAPI AfxRegisterClass(WNDCLASS* lpWndClass)
{
 WNDCLASS wndcls;
 if (GetClassInfo(lpWndClass->hInstance, lpWndClass->lpszClassName,
  &wndcls))
 {
  // class already registered
  return TRUE;
 }
 
 if (!::RegisterClass(lpWndClass))
 {
  TRACE1("Can't register window class named %s/n",
   lpWndClass->lpszClassName);
  return FALSE;
 }
 
 if (afxContextIsDLL)
 {
  AfxLockGlobals(CRIT_REGCLASSLIST);
  TRY
  {
   // class registered successfully, add to registered list
   AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
   LPTSTR lpszUnregisterList = pModuleState->m_szUnregisterList;
   // the buffer is of fixed size -- ensure that it does not overflow
   ASSERT(lstrlen(lpszUnregisterList) + 1 +
    lstrlen(lpWndClass->lpszClassName) + 1 <
    _countof(pModuleState->m_szUnregisterList));
   // append classname + newline to m_szUnregisterList
   lstrcat(lpszUnregisterList, lpWndClass->lpszClassName);
   TCHAR szTemp[2];
   szTemp[0] = '/n';
   szTemp[1] = '/0';
   lstrcat(lpszUnregisterList, szTemp);
  }
  CATCH_ALL(e)
  {
   AfxUnlockGlobals(CRIT_REGCLASSLIST);
   THROW_LAST();
   // Note: DELETE_EXCEPTION not required.
  }
  END_CATCH_ALL
  AfxUnlockGlobals(CRIT_REGCLASSLIST);
 }
 
 return TRUE;
}
AfxRegisterClass函数首先获得窗口类信息,如果该窗口类已经注册,则直接返回一个真值;如果尚未注册,就调用RegiserClass函数注册该窗口类。
小技巧:如果在当前工程文件中查找某个函数或字符串,可以利用工具栏上的“Find in Files”工具按钮或Edit菜单下的Find in Files命令
应用程序框架窗口类CMainFrame类有一个PreCreateWindow函数,这是窗口产生之前被调用的。该函数的默认实现代码如下:
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
 if( !CFrameWnd::PreCreateWindow(cs) )
  return FALSE;
 // TODO: Modify the Window class or styles here by modifying
 //  the CREATESTRUCT cs
 
 return TRUE;
}
该函数首先调用CFrameWnd的PreCreateWindow函数,后者的定义位于源文件WINFRM.CPP中,代码如下:
BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs)
{
 if (cs.lpszClass == NULL)
 {
  VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
  cs.lpszClass = _afxWndFrameOrView;  // COLOR_WINDOW background
 }
 
 if ((cs.style & FWS_ADDTOTITLE) && afxData.bWin4)
  cs.style |= FWS_PREFIXTITLE;
 
 if (afxData.bWin4)
  cs.dwExStyle |= WS_EX_CLIENTEDGE;
 
 return TRUE;
}
该函数中调用了AfxDeferRegisterClass函数,可以在AFXIMPL.H文件中找到后者的定义,定义代码如下:
#define AfxDeferRegisterClass(fClass) AfxEndDeferRegisterClass(fClass)
AfxDeferRegisterClass实际上是一个宏,真正指向的是AfxEndDeferRegisterClass函数,这个函数完成的功能就是注册窗口类
MFC程序在调用theApp全局对象和WinMain函数之后,到达CMainFrame类的PreCreateWindow函数处。由此,我们知道MFC程序执行的脉络也是在WinMain函数之后,窗口产生之前注册窗口类的
2、创建窗口
在MFC程序中,窗口的创建功能是由CWnd类的CreateEx函数实现的,该函数的实现代码位于WINCORE.CPP,部分代码如下:
BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
 LPCTSTR lpszWindowName, DWORD dwStyle,
 int x, int y, int nWidth, int nHeight,
 HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam)
{
 // allow modification of several common create parameters
 CREATESTRUCT cs;
 cs.dwExStyle = dwExStyle;
 cs.lpszClass = lpszClassName;
 cs.lpszName = lpszWindowName;
 cs.style = dwStyle;
 cs.x = x;
 cs.y = y;
 cs.cx = nWidth;
 cs.cy = nHeight;
 cs.hwndParent = hWndParent;
 cs.hMenu = nIDorHMenu;
 cs.hInstance = AfxGetInstanceHandle();
 cs.lpCreateParams = lpParam;
 
 if (!PreCreateWindow(cs))
 {
  PostNcDestroy();
  return FALSE;
 }
 
 AfxHookWindowCreate(this);
 HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
   cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
   cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);
}
在MFC底层代码中,CFrameWnd类的Create函数内部调用了上述CreateEx函数,而前者又由CFrameWnd类的LoadFrame函数调用
CFrameWnd类的Create函数的定义位于WINFRM.CPP,部分代码如下:
BOOL CFrameWnd::Create(LPCTSTR lpszClassName,
 LPCTSTR lpszWindowName,
 DWORD dwStyle,
 const RECT& rect,
 CWnd* pParentWnd,
 LPCTSTR lpszMenuName,
 DWORD dwExStyle,
 CCreateContext* pContext)
{
if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,
  rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
  pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext))
 {
  TRACE0("Warning: failed to create CFrameWnd./n");
  if (hMenu != NULL)
   DestroyMenu(hMenu);
  return FALSE;
 }
}
CFrameWnd类派生于CWnd类。Cwnd类的CreateEx函数不是虚函数。另外,CFrameWnd类中也没有重写这个函数。根据类的继承性原理,CFrameWnd类就继承了CWnd类的CreateEx函数。因此CFrameWnd类的Create函数内调用的实际上就是CWnd类的CreateEx函数
CWnd类的CreateEx函数实现代码中,该函数又调用了PreCreateWindow函数,后者是一个虚函数。因此,这里实际上调用的是子类,即CMainFrame类的PreCreateWindow函数。之所以这里再次调用这个函数,主要是为了在产生窗口之前让程序员有机会修改窗口外观。PreCreateWindow函数的参数就是为了实现这个功能而提供的。该参数的类型是CREATESTRUCT结构,将这个结构体与CreateWindowEx函数的参数进行对比如下:
typedef struct tagCREATESTRUCT { // cs
    LPVOID    lpCreateParams;
    HINSTANCE hInstance;
    HMENU     hMenu;
    HWND      hwndParent;
    int       cy;
    int       cx;
    int       y;
    int       x;
    LONG      style;
    LPCTSTR   lpszName;
    LPCTSTR   lpszClass;
    DWORD     dwExStyle;
} CREATESTRUCT;
 
HWND CreateWindowEx(
  DWORD dwExStyle,      // extended window style
  LPCTSTR lpClassName,  // pointer to registered class name
  LPCTSTR lpWindowName, // pointer to window name
  DWORD dwStyle,        // window style
  int x,                // horizontal position of window
  int y,                // vertical position of window
  int nWidth,           // window width
  int nHeight,          // window height
  HWND hWndParent,      // handle to parent or owner window
  HMENU hMenu,          // handle to menu, or child-window identifier
  HINSTANCE hInstance,  // handle to application instance
  LPVOID lpParam        // pointer to window-creation data
);
可以发现,CREATESTRUCT结构体中的字段与CreateWindowEx函数的参数是一致的,只是先后顺序相反而已。
PreCreateWindow函数的这个参数是引用类型。在子类中对此参数所做的修改,在其基类中可以体现出来。再看CWnd类的CreateEx函数代码,如果在子类的PreCreateWindow函数中修改了CREATESTRUCT结构体的值,那么,接下来调用CreateWindowEx函数时,其参数就会发生相应的改变,从而就会创建一个符合我们要求的窗口
知识点:MFC中后缀名为Ex的函数都是扩展函数
3、显示窗口和更新窗口
在Test程序的应用程序类(CTest)中有一个名为m_pMainWnd的成员变量。该变量是一个CWnd类型的指针,它保存了应用程序框架窗口对象的指针,也就是指向CMainFrame类型的指针。在CTestApp类的InitInstance函数实现内部有如下代码:
// The one and only window has been initialized, so show and update it.
 m_pMainWnd->ShowWindow(SW_SHOW);
 m_pMainWnd->UpdateWindow();
这两行代码的功能就是显示应用程序框架窗口和更新窗口
2.3、消息循环
至此,注册窗口类,创建窗口、显示和更新窗口的工作都已完成,就该进入消息循环了。CWinThread类的Run函数就是完成消息循环这一任务的,该函数是在AfxWinMain函数中调用的,调用形式如下:
pThread->Run();
CWinThread类的Run函数的定义位于THRDCORE.CPP文件中,代码如下:
// main running routine until thread exits
int CWinThread::Run()
{
 ASSERT_VALID(this);
 
 // for tracking the idle time state
 BOOL bIdle = TRUE;
 LONG lIdleCount = 0;
 
 // acquire and dispatch messages until a WM_QUIT message is received.
 for (;;)
 {
  // phase1: check to see if we can do idle work
  while (bIdle &&
   !::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
  {
   // call OnIdle while in bIdle state
   if (!OnIdle(lIdleCount++))
    bIdle = FALSE; // assume "no idle" state
  }
 
  // phase2: pump messages while available
  do
  {
   // pump message, but quit on WM_QUIT
   if (!PumpMessage())
    return ExitInstance();
 
   // reset "no idle" state after pumping "normal" message
   if (IsIdleMessage(&m_msgCur))
   {
    bIdle = TRUE;
    lIdleCount = 0;
   }
 
  } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
 }
 
 ASSERT(FALSE);  // not reachable
}
该函数的主要结构是一个for循环,该循环在接收到WM_QUIT消息时退出,在此循环中调用了一个PumpMessage函数,该函数的部分定义代码如下:
BOOL CWinThread::PumpMessage()
{
 ASSERT_VALID(this);
 
 if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))
 {
……
  return FALSE;
 }
……
 // process this message
 
 if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
 {
  ::TranslateMessage(&m_msgCur);
  ::DispatchMessage(&m_msgCur);
 }
 return TRUE;
}
可以发现这与SDK编程的消息处理代码是一致的
2.4、窗口过程函数
现在已经进入消息循环,MFC程序也把消息路由给一个窗口过程函数去处理,AfxEndDeferRigisterClass函数的源代码中有这样一句代码:
wndcls.lpfnWndProc = DefWindowProc;
这行代码的作用就是设置窗口过程函数,这里指定的是一个默认的窗口过程:DefWindowProc。但实际上,MFC程序并不是把所有消息都交给DefWindowProc这一默认窗口过程来处理的,而是采用了一种称之为消息映射的机制来处理各种消息的
至此,我们就了解了MFC程序的整个运行机制,实际上与Win32 SDK程序是一致的。它同样也要经过:设计窗口类、注册窗口类,创建窗口,显示并更新窗口,消息循环
2.5、文档/视类结构
我们创建的MFC程序出了主框架窗口以外,还有一个窗口是视类窗口,对应的类是CView类,CView类也派生于CWnd类。框架窗口是视类窗口的一个父窗口,它们之间的关系如图:
Test程序中还有一个CTestDoc类,它派生于CDocument类,其基类是CCmdTarget,而后者又派生于CObject类,从而指导CTestDoc类不是一个窗口类,实际上它是一个文档类
MFC提供了一个文档/视 结构,其中文档就是指CDocument类,视就是指CView类。微软在设计基础类库时,考虑到要把数据本身与它的显示分离开,于是就采用文档类和视类结构来实现这一想法。数据的存储和加载由文档类来完成,数据的显示和修改则由视类来完成,从而把数据管理和显示方法分离开来。文档/视结构是MFC程序的一个重点
在CTestApp类的InitInstance函数实现代码中,定义了一个单文档模板对象指针。该对象把文档对象、框架对象、视类对象有机地组织在一起,程序接着利用AddDocTemplate函数把这个单文档模板添加到文档模板中,从而把这三个类组织成为一个整体
2.6、帮助对话框类
Test程序还有一个CAboutDlg类,其基类是CDialog类,,后者又派生于CWnd类。因此,CAboutDlg类也是一个窗口类,其主要作用是为用户提供一些与程序有关的帮助信息,例如版本号等
3、窗口类、窗口类对象与窗口
3.1、三者之间的关系
C++窗口类对象与窗口并不是一回事,它们之间惟一的关系是C++窗口类对象内部定义了一个窗口句柄变量,保存了与这个C++窗口类对象相关的那个窗口的句柄。窗口销毁时,与之对应的C++窗口类对象销毁与否,要看其生命周期是否结束。但C++窗口类对象销毁时,与之相关的窗口也将销毁
MFC提供的CWnd类定义了一个数据成员:m_hwnd,用来保存与之相关的窗口的句柄。因为MFC中所有窗口类都是由CWnd类派生的,于是,所有的窗口类内部都有这样的一个成员用来保存与之相关的窗口的句柄
MFC消息映射机制
1、ClassWizard
ClassWizard是VC++ 6.0中一个很重要的组成部分,它可以帮助我们创建一个新类,为已有的类添加成员变量,添加消息和命令的响应函数,以及虚函数的重写。在VC++开发环境界面中,打开View菜单,选择ClassWizard菜单命令就会打开MFC ClassWizard对话框
视类窗口始终覆盖在框架窗口之上,所有操作,包括鼠标单击、鼠标移动等操作都只能由视类窗口捕获
删除消息响应函数:在Class View选项卡上用鼠标右键单击要删除的函数名,在弹出的快捷菜单中选择delete菜单命令
2、消息映射机制
为视类增加了一个鼠标左键按下这一消息响应函数之后,在源文件中会增加三处代码:
1、消息响应函数原型
在CDrawView类的头文件中,有如下代码:
// Generated message map functions
protected:
       //{{AFX_MSG(CDrawView)
       afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
       //}}AFX_MSG
       DECLARE_MESSAGE_MAP()
在DECLARE_MESSAGE_MAP()宏之上有两个AFX_MSG注释宏,这两个注释宏之间有一个函数原型OnLButtonDown。该函数的前部有一个afx_msg限定符,这也是一个宏。该宏表明这个函数是一个消息响应函数的声明
2、ON_WM_LBUTTONDOWN消息映射宏
在CDrawView类的源文件中,有如下代码:
BEGIN_MESSAGE_MAP(CDrawView, CView)
       //{{AFX_MSG_MAP(CDrawView)
       ON_WM_LBUTTONDOWN()
       //}}AFX_MSG_MAP
       // Standard printing commands
       ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
       ON_COMMAND(ID_FILE_PRINT_DIRECT, CView::OnFilePrint)
       ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
END_MESSAGE_MAP()
BEGIN_MESSAGE_MAP和END_MESSAGE_MAP这两个宏之间定义了CDrawView类的消息映射表,其中有一个ON_WM_LBUTTONDOWN这消息映射宏,这个宏的作用就是把鼠标左键按下消息WM_LBUTTONDOWN与一个消息响应函数关联起来。通过这种机制,一旦有消息产生,程序就会调用响应的消息响应函数来进行处理
3、消息响应函数的定义
在CDrawView类的源文件中,可以看到OnLButtonDown函数的定义
经过以上分析,一个MFC消息响应函数在程序中有三处相关信息:函数原型、函数实现,以及用来关联消息和消息响应函数的宏。头文件中在两个AFX_MSG注释宏之间是消息响应函数原型的声明。源文件中有两处:一处是在两个AFX_MSG_MAP注释宏之间的消息映射宏,通过这个宏把消息与消息响应函数关联起来;另一处是源文件中消息响应函数的实现代码
MFC消息映射机制的具体实现方法是:在每个能接收和处理消息的类中,定义一个消息和消息函数静态对照表,即消息映射表。在消息映射表中,消息与对应的消息处理函数指针是成对出现的,某个类能处理的所有消息及其对应的消息处理函数的地址都列在这个类所对应的静态表中。当有消息需要处理时,程序只要搜索该消息静态表,查看表中是否含有该消息,就可以知道该类能否处理此消息,如果能处理此消息,则同样依照静态表能很容易找到并调用相应的消息处理函数
MFC消息映射机制的实际实现过程:
MFC在后台维护了一个窗口句柄与对应的C++对象指针的对照表,当收到某一消息时,消息的第一个参数就指明该消息与哪个窗口句柄相关,通过对照表,就可以找到与之相关的C++对象指针。然后把这个指针传递给应用程序框架窗口类的基类,后者会调用一个名为WindowProc的函数,该函数的定义位于WinCore.cpp文件,代码如下:
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
       // OnWndMsg does most of the work, except for DefWindowProc call
       LRESULT lResult = 0;
       if (!OnWndMsg(message, wParam, lParam, &lResult))
              lResult = DefWindowProc(message, wParam, lParam);
       return lResult;
}
根据这个WindowProc函数的定义,可以发现它是一个虚函数。同时,也可以发现,CWnd::WindowProc函数内部调用了一个OnWndMsg函数,真正的消息路由,也就是消息映射就是由此函数完成的。OnWndMsg函数的定义也位于WinCore.cpp文件中,部分代码如下:
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
       LRESULT lResult = 0;
 
       // special case for commands
       if (message == WM_COMMAND)
       {
              if (OnCommand(wParam, lParam))
              {
                     lResult = 1;
                     goto LReturnTrue;
              }
              return FALSE;
       }
 
       // special case for notifies
       if (message == WM_NOTIFY)
       {
              NMHDR* pNMHDR = (NMHDR*)lParam;
              if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
                     goto LReturnTrue;
              return FALSE;
       }
 
       // special case for activation
       if (message == WM_ACTIVATE)
              _AfxHandleActivate(this, wParam, CWnd::FromHandle((HWND)lParam));
 
       // special case for set cursor HTERROR
       if (message == WM_SETCURSOR &&
              _AfxHandleSetCursor(this, (short)LOWORD(lParam), HIWORD(lParam)))
       {
              lResult = 1;
              goto LReturnTrue;
       }
}
OnWndMsg函数的处理过程是:
首先判断消息是否有消息响应函数
判断方法是在相应窗口类中查找所需的消息响应函数。因为传递给WindowProc函数的是窗口子类指针,所以,OnWndMsg函数会到相应的子类头文件中查找,看看DECLARE_MESSAGE_MAP()宏之上,两个AFX_MSG注释宏之间是否有相应的消息响应函数原型的声明;再到子类源文件中,看看BEGIN_MESSAGE_MAP和END_MESSAGE_MAP这两个宏之间是否有相应的消息映射宏
如果通过以上步骤,找到了消息响应函数,那么接着就会调用该响应函数,对消息进行处理
如果在子类中没有找到消息响应函数,那么就交由基类进行处理
1、动态链接库概述
动态链接库一直就是Windows操作系统的基础。动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某项工作的函数。只有在其他模块调用动态链接库中的函数时,它才发挥作用。在实际编程时,我们可以把完成某种功能的函数放在一个动态链接库中,然后供给其他程序调用
Windows API中的所有函数都包含在DLL中,其中有3个最重要的DLL
Kernel32.dll:包含那些用于管理内存、进程和线程的函数
User32.dll:包含那些用于执行用户界面任务的函数
GDI32.dll:包含那些用于画图和显示文本的函数
1.1、静态库与动态库
静态库:函数和数据被编译进一个二进制文件(扩展名.lib)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.exe文件)。当发布产品时,只需要发布这个可执行文件,并不需要发布使用的静态库
动态库:在使用动态库时,往往提供两个文件:一个引入库(.lib)文件和一个DLL(.dll)文件。虽然引入库文件的后缀名也是“lib”,但是,动态链接库的引入库文件和静态库文件有着本质的区别,对一个DLL来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。这时,在发布产品时,除了发布可执行文件意外,同时还要发布该程序将要调用的动态链接库
1.2、使用动态链接库的好处
1、可以采用多种语言来编写
2、增强产品的功能
3、提供二次开发的平台
4、简化项目管理
5、可以节省磁盘空间和内存
当进程被加载时,系统为它分配一个4GB的地址空间,接着分析该可执行模块,找到该程序要调用哪些DLL,然后系统搜索这些DLL,找到后就加载它们,并为它们分配虚拟的内存空间,然后将DLL的页面映射到调用进程的地址空间。在内存中,只需要存在一份DLL的代码和数据。多个进程可以共享DLL的同一份代码,这样就可以节省内存空间
6、有助于资源的共享
7、有助于实现应用程序的本地化
1.3、动态链接库的加载
在程序中,有以下两种加载动态链接库的方式:
隐式加载
显式加载
2、Win32 DLL的创建和使用
工程:Win32 Dynamic-Link Library
2.1、Dumpbin命令
应用程序如果要访问某个DLL中的函数,那么该函数必须是已经导出的函数。为了查看一个DLL中有哪些导出函数,可以利用Visual Stidio提供的命令行工具:Dumpbin命令来实现
如果想要查看一个DLL提供的导出函数,可以使用/EXPORTS选项来运行Dumpbin命令。
2.2、从DLL中导出函数
为了让DLL导出一些函数,需要在每一个将要被导出的函数前面添加标识符:_declspec(dllexport)
Dumpbin命令的输出信息:
oradinal:导出函数的序号
hint:提示码
RVA:导出函数在DLL模块中的位置,通过该地址值,可以在DLL中找到它们
name:导出函数的名称
C++支持函数重载,对于重载的多个函数来说,其函数名都是一样的,为了加以区分,在编译链接时,C++会按照自己的规则纂改函数的名称,这一过程称为“名字改编”。不同的编译器会采用不同的规则进行名字改编,这样的话,利用不同C++编译器生成的程序在调用对方提供的函数时,可能会出现问题
3、隐式链接方式加载DLL
3.1、利用extern声明外部函数
在调用DLL中的函数之前,为了让编译器知道这两个函数,需要对这两个函数作一个声明,前面加上extern关键字表明函数是在外部定义的
为了让链接器找到定义在DLL中的函数是在哪个地方实现的,需要利用动态链接库的引入库文件。把.lib文件复制到工程所在目录下,该目录包含了.dll中导出函数的符号名。然后在工程中,选择Project/Setting菜单命令,打开工程设置对话框,选择link选项卡,在“Object/library modules”选项编辑框中输入.lib文件的文件名
当应用程序需要调用某个动态链接库提供的函数时,在程序链接时只需要包含该动态链接库提供的输入库文件就可以了。在引入库文件中并没有包含实际的代码,它只是用来为链接程序提供必要的信息,以便在可执行文件中建立动态链接时需要用到的重定位表。
我们也可以查看可执行程序的输入信息,以及其加载的DLL信息,这同样要利用dumpbin命令的imports选项来实现。
动态链接库的搜索顺序:
程序的执行目录
当前目录
系统目录
path环境变量所列出的目录
为了让可执行文件能够正常执行,就必须让加载模块能够找到该应用程序所需的所有动态链接库,如果加载模块未能找到其中的一个动态链接库,可执行程序将中止运行
3.2、使用_declspec(dllimport)声明外部函数
除了使用extern关键字表明函数是外部定义的之外,还可以使用标识符:_declspec(dllimport)来表明函数是从动态链接库引入的
4、完善Win32 DLL
一个DLL实现之后,通常都会交给客户程序,以便后者能访问该DLL。通常在编写动态链接库时,都会提供一个头文件,在此文件中提供DLL导出函数的声明,以及函数有关注释文档
5、从DLL中导出C++类
在一个动态链接库中可以导出C++类。为了从动态链接库中导出一个类,需要在class关键字和类名之间加入导出标识符,这样就可以导出整个类了。在访问该类的函数时,仍受限于函数自身的访问权限
在实现动态链接库时,可以不导出整个类,而只导出该类中的某些函数。这时,要把导出标识符放在要导出的成员函数的声明处。
动态链接库导出整个类和仅导出该类的某些成员函数在实现方式上的区别:如果在声明类时,指定了导出标志,那么该类中的所有函数都将被导出,否则,只有那些声明时指定了导出标志的类成员函数才被导出。但对这两种情况生成的DLL,客户端程序在访问上是没有区别的,都是先构造该类的一个对象,然后利用该对象访问该类的成员函数。
6、解决名字改编问题
C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改编规则不一样。这样,如果利用不同的编译器分别生成DLL和访问该DLL的客户端程序的话,后者在访问该DLL的导出函数时就会出现问题
为了实现动态链接库文件在编译时,导出函数的名称不发生改变,在定义导出函数时,需要加上限定符:extern “C”
利用限定符extern “C”可以解决C++和C语言之间相互调用时函数命名的问题。但是这种方法有一个缺陷,就是不能用于导出一个类的成员函数,只能用于导出全局函数这种情况
另外,如果导出函数的调用约定发生了改变,那么即使使用了限定符extern “C”,该函数的名字仍会发生改编
调用约定:_stdcall标准调用约定,C调用约定
可以通过模块定义文件(DEF)的方式来解决名字改编问题:
为工程添加模块定义文件,其后缀名为:def。添加方法有多种:
创建一个空的文本文件,并将文件命名为:DLL工程名.def。然后回到VC开发界面,将该文件添加到DLL工程中,添加方法是:选择project/add to project/files菜单项,把模块定义文件添加到工程中
模块定义文件的内容:
LIBRARY DLL2
 
EXPORTS
add
其中LIBRARY语句用来指定动态链接库的内部名称,该名称与生成的动态链接库的工程名一致,这句代码不是必须的。EXPORTS语句的作用是表明将要导出的函数,以及为这些导出函数指定的符号名。当链接器在链接时,会分析这个DEF文件,当发现在EXPORTS语句下面有add这个符号名,并且与源文件中定义的函数的名字是一样的时候,它就会以add这个符号名导出相应的函数。如果将要导出的符号名和源文件中定义的符号名不一样,则采用下述语法指定导出函数
entryname=internalname
其中entername项是导出的符号名,internalname项是DLL中将要导出的函数的名字
7、显式加载方式加载DLL
7.1、LoadLibrary
使用动态方式加载动态链接库时,需要用到LoadLibrary函数。该函数的作用是将指定的可执行模块映射到调用进程的地址空间。LoadLibrary函数的原型声明如下:
HMODUAL LoadLibrary(LPCTSTR lpFileName)
LoadLibrary函数不仅能够加载DLL(.dll)还可以加载可执行模块(.exe)
LoadLibrary函数有一个字符串类型的参数,该参数指定了可执行模块的名称,既可以是一个.dll文件,也可以是一个.exe文件。如果调用成功,LoadLibrary函数将返回所加载的那个模块的句柄
当获取到动态链接库模块的句柄后,接下来就该获取动态链接库中导出函数的地址,这可以通过GetProcAddress函数来实现。该函数用来获取DLL导出函数的地址,其原型声明如下:
FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName)
hModule:指定动态链接库模块的句柄
lpProcName:指定DLL导出函数的名字或函数的序号
如果调用成功,GetProcAddress函数返回指定导出函数的地址,否则返回NULL
 函数调用完毕后,使用FreeLibrary()卸载DLL文件。
FreeLibrary(hDll);
动态加载DLL时,客户端程序不再需要包含导出函数声明的头文件和引入库文件,只需要.dll文件既可
动态加载和隐式链接这两种加载DLL的方式各有优点:如果采用动态加载方式,那么可以在需要时才加载DLL,而隐式链接方式实现起来比较简单,在编写客户端代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。但是如果程序需要访问很多个DLL,如果都采用隐式链接方式加载的话,那么在该程序启动时,这些DLL都需要被加载到内存中,并映射到调用进程的地址空间,这样将加大程序的启动时间。而且,一般来说,在程序运行过程中只是在某些条件满足时才需要访问某个DLL中的函数,资源浪费情况比较严重。在这种情况下,就可以采用动态加载的方式访问DLL,在需要时才加载所需的DLL。实际上,采用隐式链接方式访问DLL时,在程序启动时也是通过调用LoadLibrary函数加载该进程需要的动态链接库的
另外,当采用动态方式加载DLL时,在客户端程序中将不能看到调用该DLL的输入信息了
7.2、调用约定
当DLL中导出函数采用的是标准调用约定时,访问该DLL的客户端程序也应该采用该约定类型来访问相应的导出函数
8、DllMain函数
对可执行模块来说,其入口函数是WinMain,而对DLL来说,其入口函数是DllMain,但该函数是可选的。如果提供了DllMain函数,那么当系统加载该DLL时,就会调用该函数
9、DLL的分类
VC++支持三种DLL,它们分别是Non-MFC Dll、Regular Dll、Extension Dll
Non-MFC Dll(非MFC动态库):这种动态链接库没有采用MFC的类结构,而是直接用C++语言编写的DLL。它的导出函数是标准的C接口,能被MFC和非MFC编写的应用程序调用。相比MFC类库编写的DLL,它占用很少的磁盘和内存空间
Regular DLL(常规DLL):用MFC类库编写的,它的一个明显的特征是,在源文件里有一个继承自CWinApp的类,导出的对象可以是C函数、C++类或者C++成员函数。只要应用程序可以调用C函数,它就可以调用常规DLL。
常规DLL可分为两种:静态链接到MFC的常规DLL和动态链接到MFC的常规DLL。二者的区别是:前者使用的是MFC的静态链接库,生成的DLL文件长度大;后者采用MFC的动态链接库,生成的DLL文件长度小
Extension DLL(扩展DLL):由MFC的动态链接库版本创建,且只能被使用MFC类库编写的应用程序调用。它没有一个从WinApp继承而来的类的对象。开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码
多线程
1、基本概念
1.1、进程
1、程序和进程
程序是计算机指令的集合,它以文件的形式存储在磁盘上,而进程通常被定义为一个正在运行的程序的实例,它是一个程序在其自身的地址空间的一次执行活动。
进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,它不占用系统的运行资源
2、进程组成
进程由两个部分组成:
(1)操作系统用来管理进程的内核对象
内核对象也是系统用来存放关于进程的统计信息的地方。内核对象是操作系统内部分配的一个内存块,该内存块是一中数据结构,其成员负责维护该对象的各种信息。由于内核对象的数据结构只能被内核访问使用,因此应用程序在内存中无法找到该数据结构,并直接改变其内容,只能通过Windows提供的一些函数来对内核对象进行操作
(2)地址空间
它包含所有可执行模块或DDL模块的代码和数据。另外,它也包含动态内存分配的空间,例如线程的栈和堆分配空间
进程从来不执行任何东西,它只是线程的容器。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程的地址空间中的代码。也就是说,真正完成代码执行的是线程,而进程只是线程的容器,或者说是线程的执行环境
单个进程可能包含若干个线程,这些线程都“同时”执行进程空间中的代码。每个进程至少拥有一个线程,用来执行进程的地址空间中的代码。当创建一个进程时,操作系统会自动创建这个进程的第一个线程,称为主线程,也就是执行main函数或WinMain函数的线程,可以把main函数或者WinMain函数看作是主线程的进入点函数。此后,主线程可以创建其他线程
3、进程地址空间
系统赋予每个进程独立的虚拟地址空间,对于32位进程来说,这个地址空间是4GB。因为对于32位指针来说,它能寻址的范围是2的32次幂,即4GB
1.2、线程
1、线程组成
线程由两个部分组成:
(1)线程的内核对象:操作系统用它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方
(2)线程栈:它用于维护线程在执行代码时需要的所有函数参数和局部变量
当创建线程时,系统创建一个线程内核对象。该线程内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。可以将线程内核对象视为由关于线程的统计信息组成的一个小型数据结构
线程只有一个内核对象和一个栈,保留的记录很少,因此所需要的内存也很少。由于线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程
2、线程运行
操作系统为每一个运行线程安排一定的CPU时间——时间片。系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此给用户的感觉就是多个线程是同时运行的一样
3、单线程程序与多线程程序
对单线程程序来说,在进程的地址空间中只有一个线程在运行。多线程程序,在进程地址空间中有多个线程,其中有一个是主线程。
编写多线程程序,每一个线程可以独立地完成一个任务,当该程序移植到多CPU的平台上时,其中的多个线程就可以真正意义上并发地同时运行了
2、线程创建函数
创建线程可以使用系统提供的API函数:CreateThread来完成,该函数将创建一个线程,函数原型如下:
HANDLE CreateThread(
 LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to security attributes
 DWORD dwStackSize,                         // initial thread stack size
 LPTHREAD_START_ROUTINE lpStartAddress,     // pointer to thread function
 LPVOID lpParameter,                        // argument for new thread
 DWORD dwCreationFlags,                     // creation flags
 LPDWORD lpThreadId                         // pointer to receive thread ID
);
lpStartAddress参数是一个函数指针,这个函数将由新线程执行,表明新线程的起始地址。main函数是主线程的入口函数,同样地,新创建的线程也需要有一个入口函数,这个函数的地址就由此参数指定。这就要求在程序中定义一个函数作为新线程的入口函数,该函数的名称任意,但函数类型必须遵照下述声明形式:
DWORD WINAPI ThreadProc(
 LPVOID lpParameter   // thread data
);
lpParameter参数提供了一种将初始值传递给线程函数的手段,这个参数的值即可以是一个数值,也可以是一个指向其他信息的指针
在创建线程完成之后,调用CloseHandle函数关闭新线程的句柄。实际上调用CloseHandle函数并没有终止新创建的线程,只是表示在主线程中对新创建的线程的引用不感兴趣,因此将它关闭。另一方面,当关闭该句柄时,系统会递减该线程对象的使用计数。当创建的这个新线程执行完毕之后,系统也会递减该线程内核对象的使用计数。当使用计数为0时,系统就会释放该线程内核对象。如果没有关闭线程句柄,系统就会一直保持着对线程内核对象的引用,这样,即使该线程执行完毕,它的引用计数仍不为0,这样该线程内核对象也就不会被释放,只有等到进程终止时,系统才会清理这些残留的对象。因此,在程序中,当不再需要线程句柄时,应将其关闭,让这个线程内核对象的引用计数减1
当主线程执行完毕后,进程也就退出了,这是进程中所有的资源,包括还没有执行的线程都要退出。为了让新创建的线程能够得到执行的机会,就需要使主线程暂停执行,即放弃执行权力,操作系统就会从等待运行的线程队列中选择一个线程来执行
在程序中,如果想让某个线程暂停运行,可以调用Sleep函数,该函数可以使调用线程暂停自己的运行,直到指定的时间间隔过去为止。该函数的原型声明如下:
void Sleep(DWORD dwMlilliseconds);
3、线程同步
一般来说,对多线程程序,如果这些线程需要访问共享资源,就需要进行线程间的同步处理
3.1、利用互斥对象实现线程同步
互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID用于表示系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数
为了创建互斥对象,需要调用函数:CreateMutex,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步。该函数的原型声明如下:
HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes,
                       // pointer to security attributes
 BOOL bInitialOwner, // flag for initial ownership
 LPCTSTR lpName       // pointer to mutex-object name
);
如果调用成功,该函数将返回所创建的互斥对象的句柄。如果创建的是命名的互斥对象,并且在CreateMutex函数调用之前,该命名的互斥对象存在,那么该函数将返回这个已经存在的这个互斥对象的句柄
另外,当线程对共享资源访问结束后,应释放该对象的所有权,也就是让该对象处于已通知状态,这时需要调用ReleaseMutex函数,该函数将释放指定对象的所有权,该函数的原型声明如下所示:
BOOL ReleaseMutex(
 HANDLE hMutex   // handle to mutex object
);
另外,线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过调用WaitForSingleObject函数来实现,该函数的原型声明如下:
DWORD WaitForSingleObject(
 HANDLE hHandle,        // handle to object to wait for
 DWORD dwMilliseconds   // time-out interval in milliseconds
);
对于互斥对象来说,它是惟一与线程相关的内核对象,当主线程拥有互斥对象时,操作系统系统会将互斥对象的线程ID设置为主线程的ID。当在线程1中调用ReleaseMutex函数释放互斥对象的所有权时,操作系统会判断线程1的线程ID与互斥对象内部所维护的线程ID是否相等,只有相等才能完成释放操作。
也就说对互斥对象来说,谁拥有谁释放
当调用WaitForSingleObject函数请求互斥对象时,操作系统需要判断当前请求互斥对象的线程ID是否与互斥对象当前拥有者的线程ID相同,如果相等,即使该互斥对象处于未通知状态,调用线程仍然能够获得其所有权,然后WaitForSingleObject函数返回。对于同一个线程多次拥有的互斥对象来说,该互斥对象内部的计数器记录了该线程拥有的次数。当接下来调用ReleaseMutex函数释放该互斥对象的所有权时,实际上就是递减这个计数器,只有当计数器的值变为0时,操作系统才会将互斥对象变为已通知状态
在程序运行时,操作系统维护了线程的信息以及与该线程相关的互斥对象的信息,因此它知道哪个线程终止了。如果某个线程得到其所需互斥对象的所有权,完成其线程代码的运行,但没有释放该互斥对象的所有权就退出之后,操作系统一旦发现该线程已经终止,它就会自动将该线程所拥有的互斥对象的线程ID设置为0,并将其计数器归0。
5、保证应用程序只有一个实例运行
通过命名的互斥对象可以实现同时只有应用程序的一个实例运行的功能。在调用CreateMutex函数创建一个命名的互斥对象后,如果其返回值是一个有效的句柄,那么接着调用GetLastError函数,如果该函数返回的是ERROR_ALREADY_EXISTS,就表明先前已经创建了这个命名的互斥对象,因此就可以知道先前已经有该应用程序的一个实例在运行了。当然如果GetLastError函数返回的不是ERROR_ALREADY_EXISTS,就说明这个互斥对象是创建的,从而也就知道当前启动的这个进程是应用程序的第一个实例
 
线程同步
1、事件对象
事件对象也属于内核对象,它包含以下三个成员:
使用计数
用于指明该事件是一个自动重置事件还是一个人工重置事件的布尔值
用于指明该事件处于已通知状态还是未通知状态的布尔值
事件对象有两种不同的类型:人工重置的事件对象和自动重置的事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程
1.1、创建事件对象
在程序中可以通过CreateEvent函数创建或打开一个命名的或匿名的事件对象,该函数的原型声明如下:
HANDLE CreateEvent(
 LPSECURITY_ATTRIBUTES lpEventAttributes,
                      // pointer to security attributes
 BOOL bManualReset, // flag for manual-reset event
 BOOL bInitialState, // flag for initial state
 LPCTSTR lpName      // pointer to event-object name
);
1.2、设置事件对象状态
SetEvent函数将把指定的事件对象设置为有信号状态,该函数的原型声明如下:
BOOL SetEvent(
HANDLE hEvent   // handle to event object
);
1.3、重置事件对象状态
ResetEvent函数将把指定的事件对象设置为无信号状态,该函数的原型声明如下所示:
BOOL ResetEvent(
 HANDLE hEvent   // handle to event object
);
1.4、利用事件对象实现线程同步
当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程
当一个线程等待到一个事件对象之后,这个事件对象仍然处于有信号状态,所以其他线程可以得到该事件对象,从而进入所保护的代码并执行。
为了实现线程间的同步,不应该使用人工重置事件对象,而应该使用自动重置的事件对象
当一个自动重置对象得到通知时,等待该事件的线程中只有一个线程变为可调度线程。当某个线程请求到事件对象之后,操作系统会将该事件对象设置为无信号状态,所以为了让程序能够正常运行,在线程对保护的代码访问完成之后应该立即调用SetEvent函数,将该事件对象设置为有信号状态,允许其他等待该对象的线程变成可调度状态
在使用线程同步时,要注意区分人工重置事件对象和自动重置事件对象。当人工重置事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程,当一个自动重置事件对象得到通知时,等待该事件对象的线程中只有一个变为可调度线程,同时操作系统会将该事件对象设置为无信号状态,这样,当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的时间对象,在一个线程得到该事件对象之后,操作系统并不会将该事件对象设置为无信号状态,除非显示地调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态
1.5、保证应用程序只有一个实例运行
通过创建一个命名的事件对象,也可以实现应用程序只有一个实例运行这一功能。对CreateEvent函数来说,如果创建的是命名的事件对象,并且在此函数调用之前此事件对象已经存在,那么该函数将返回已存在的这个事件对象的句柄,并且之后的GetLastError调用将返回ERROR_ALREADY_EXISTS。因此,与利用命名互斥对象的方法一样,利用CreateEvent函数创建命名事件对象,并根据其返回值判断应用程序是否已经有一个实例在运行,如果有,则应用程序退出,从而实现应用程序只有一个实例运行这一功能
2、关键代码段
利用关键代码段可以实现线程同步。关键代码段,也称为临界区,工作在用户方式下,它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当作是关键代码段
2.1、相关的API函数
在进入关键代码段之前,首先需要初始化一个这样的关键代码段,这可以调用InitializeCriticalSection函数实现,该函数的原型声明如下所示:
VOID InitializeCriticalSection(
 LPCRITICAL_SECTION lpCriticalSection   // address of critical
                                         // section object
);
该函数有一个参数,是一个指向CRITICAL_SECTION结构体的指针。该参数是out类型,即作为返回值使用的。因此,在使用时,需要构造一个LPCRITICAL_SECTION结构体类型的对象,然后将该对象的地址传递给InitialCriticalSection函数,系统会自动维护该对象,我们不需要了解或访问该结构体内部的成员
如果想要进入关键代码段,首先需要调用EnterCriticalSection函数,以获得指定的临界区对象的所有权。该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待
当调用线程获得了指定的临界区对象的所有权后,该线程就进入关键代码段,对所保护的资源进行访问。线程使用完所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权,之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源
对临界区对象来说,当不再需要时,需要调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源
2.2、利用关键代码段实现线程同步
在使用临界区对象编程时,有一点需要注意,线程1获得了临界区对象的所有权,忘记释放该所有权,虽然线程1执行完成之后就退出了,但是因为该线程一直未释放临界区对象的所有权,导致线程2始终无法得到该所有权,只能一直等待,无法执行下面的关键代码段,直到进程退出时,该线程也就退出了
3、线程死锁
    哲学家进餐的问题
线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,就造成了死锁。
4、互斥对象、事件对象与关键代码段的比较
    互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
在编写多线程程序并需要实现线程同步时,首选关键代码段

 

你可能感兴趣的:(C++/MFC)