1.MFC 消息
只有继承CCmdTarget的类才能实现消息映射。
消息映射应该首先在类里面使用 DECLARE_MESSAGE_MAP(),
然后再类的实现文件里使用
BEGIN_MESSAGE_MAP(theClass, baseClass)
END_MESSAGE_MAP()
现在看看怎样实现消息映射的 (注意不同版本的vs实现的上面的宏可能会有微小的差异) :
DECLARE_MESSAGE_MAP()为这个类引入了函数 GetMessageMap()的声明;
BEGIN_MESSAGE_MAP 给这个类引入了 _messageEntries 数组(也被称为消息映射数组),也实现了GetMessageMap() .
END_MESSAGE_MAP() 作为 数组_messageEntries 的结束标志。
_messageEntries数组的每一项存储的内容是 消息ID和这个消息的处理函数 。
GetMessageMap() 得到的是一个struct,它的内容是 基类的GetMessageMap()函数的地址 和 自己的_messageEntries数组的地址。 这样当一个消息来到的时候,如果在自己的 消息映射数组(既是_messageEntries) 里面没有找到这个消息处理函数,那么就会调用GetMessageMap() 间接得到父类的消息映射数组(_messageEntries), 就这样不断的向上找消息处理函数,最后如果还是没有找到的话就调用默认的处理方法了 。
要注意的是,MFC实现消息映射机制没有使用虚函数,是为了效率的考虑。
在BEGIN_MESSAGE_MAP 里面可以使用的宏有很多,但是大概可以分为下面的三种。这些宏本质上还是 消息映射数组(_messageEntries)的一个元素,比如用 ON_MESSAGE 这个宏可以实现ON_COMMAND 这个宏,但是肯定需要很多代码。
ON_COMMAND 是菜单和工具栏处理消息的宏
ON_MESSAGE 用户自定义消息的宏
ON_NOTIFY 控件向其父窗口发送消息的宏。它里面有一个NMHDR指针,可以有大量的信息。
当然还会有鼠标和键盘处理的宏。等等。
MFC程序的执行都是从 theApp开始的。CWinApp继承于CWinThread,所以消息循环在CWinThread::Run()里面。Run() 里面有传统的消息循环,当取到消息后会由DispatchMessage() 把消息发送到到对应的窗口函数。由于MFC中所有的窗口函数注册的都是 AfxWndProc, 所以在这个函数里面之后就开启了MFC的消息机制。 然后再继续跟踪AfxWndProc 就能找到处理消息映射的地方。
控制消息的方法:
(1). PreTranslateMessage 是CWnd里面的虚函数,在CWinThread里面有一段代码(pMainWnd->PreTranslateMessage), 我们可以利用虚函数的特性让自己的窗口类重载这个函数,实现某些功能。比如让char都是大写的。
(2). 实现自己窗口类的消息映射函数
(3). DefWindowProc我们也可以重载,在这上面想办法。
实际上上面三种方法都能达到同样的功能。
和消息想的函数:
PeekMessage如果没有消息那么就立即返回0,GetMessage如果没有消息就会被阻塞直到有消息。GetMessage不同于PeekMessage,PeekMessage还可以不移走消息只是查看有没有消息。
2.由CString 写入文件时乱码想到的
(1)下面的将会产生乱码:
CString tStr = _T("蒋德伟");
WriteFile(hFile,tStr.GetBuffer(),tStr.GetLength()*2,&m,NULL);
产生乱码的原因:由于txt文件不是unicode编码, 所以把unicode写入的话肯定有问题。
Txt可能是utf-8编码: 汉字占两个字节,英文字母占1个字节。由于汉字的最高位是1,英文字母的最高位是0,所以可以得到最简单的识别方法。识别的方式是,一次取两个字节,与0x8000进行与操作,不等于零的就是汉字,否则就是英文字母。
(2)我们解决的方法:
WideCharToMultiByte(CP_OEMCP,NULL,tStr.GetBuffer(),-1,sttt,6,NULL,false);
WriteFile(hFile,sttt,tStr.GetLength()*2,&m,NULL);
(3)总结wchar和char在二进制的表示上是一样的???
Char tem = "蒋德伟"
CString tStr = _T("蒋德伟");
其中char是以二进制的形式编码,可以用ultraEdit查看字符串 "蒋德伟" 的二进制值和char是一样的。
Wchar是以unicode编码,所有这里的wchar和char虽然都是一样的,但是tem[0] != tStr[0]
所以:wchar和char在二进制的表示上是不一样的
我们需要注意的是char也能显示中文,wchar也能显示中文,但是如果有成千上万的 中文字符和 英文字符用 wchar显示的话,就会浪费内存,因为 因为字符的unicode的一个byte是空的。
于是我们就可以考虑使用 MultiByteToWideChar 和WideCharToMultiByte 函数,在char和wchar之间转换。
对共享内存函数的具体说明:http://baike.baidu.com/view/3171002.htm
二、多线程
1.线程的基本函数
1)CreateThread 创建一个线程,可以设定这个线程创建的时候立即启动,或者创建的时候不启动,而是用ResumeThread() 启动。
CreateThread 有两个返回值:返回thread的HANDLE, 和 得到 thread 的 ID。
HANDLE CreateThread(
lpThreadAttributes, //线程的属性,一般是NULL,表示使用默认属性
dwStackSize, //线程的堆栈,一般是0,表示使用默认值
lpStartAddress, //线程函数
lpParameter, //线程函数的参数
dwCreationFlags, //如果为0,表示线程立即执行。如果为CREATE_SUSPENDED,线程不立即执行。只有使用
ResumeThread时才启动线程
lpThreadId //得到线程的ID
);
线程在创建之后由两部分组成:
1.线程的内核对象 线程的内核对象可以用来对线程进行操纵,并且存储一些线程信息。
2.线程本身 线程最重要的是线程堆栈,存放有线程执行时的参数和线程局部变量。
对于返回的线程HANDLE:
HANDLE是"线程的内核对象",是一块占用内存的数据结构,这块内存只能由内核访问。
内核对象里面有一个引用计数,只有在引用计数为0的时候,系统才会删除这个内核对象。
对于线程的这个HANDLE,它的初始引用计数为2。一个是返回的HANDLE占用一个引用计数,一个是线程本身占用一个引用计数。
所以如果我们在创建这个线程之后,对这个HANDLE没有使用的话,最好调用一个 CloseHandle(hThread) .因为如果在线程执行结束完了之后,引用计数不为0的话,就会造成句柄泄漏。 句柄泄漏不同于"内存泄漏",因为进程在结束之后会把所有的内核对象删除,但是如果一个不使用的句柄未被及时删除的话就会浪费资源。
线程堆栈虽然是属于这个线程的,但是由于其它线程也和这个线程在同一地址空间中,所以其它线程也可以访问这个线程的堆栈。
我们怎样知道线程的状态:
(1)用GetExitCodeThread来查询线程的状态
(2)用WaitForSingleObject来判断线程内核对象是否是已触发状态
2)DOWRD SuspendThread(hThread)
用于挂起指定的线程
3)DWORD ResumeThread(hThread)
用于结束一个线程的挂起状态
4)VOID ExitThread(exitCode)
线程用于自身的终止
5)BOOL TerminateThread(hThread, exitCode)
用于强行终止一个线程的执行。
这种方法是不安全的,因为虽然线程被终止了,但是并不释放线程的资源。
6)BOOL PostThreadMessage( hThread,….)
把一个消息放入线程消息队列中,并不等待消息的执行,而是立即返回。
3.线程的局部存储
什么是线程的局部存储: 虽然有相同的访问index或者相同的变量名,但是在不同线程中访问不相互影响。只有同一个线程内访问才会发生影响。
实现的原理:在进程内有一个 线程局部存储标识位 的全局存储结构。表示任何一个线程的TLS槽(slot)是否被使用。如被使用了,里面存储的是一个 VOID 的指针,可以得到对应的值。如下图示:
实现的方法1:
DWORD TlsAlloc(VOID);
BOOL TlsFree(DWORD dwTlsIndex );
BOOL TlsSetValue(DWORD dwTlsIndex,LPVOID lpTlsValue // value to store);
LPVOID TlsGetValue(DWORD dwTlsIndex );
方法二:直接声明一个变量为如下,虽然在不同的线程中使用相同的变量名访问,但是在不同的线程中是不同的拷贝,所以不会相互影响。
__declspec( thread ) int tls_i = 1;
下面是例程1:
__declspec( thread ) int tlsEx = 1;
unsigned int WINAPI threadFun(LPVOID)
{
tlsEx = 2;
cout<<"thread: "<< tlsEx << endl;
return 0;
}
void main()
{
unsigned int threadID;
HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,threadFun,0,0,&threadID);
Sleep(3000);
cout<<"main: "<< tlsEx<<endl;
}
得到的结果是:
thread:2
main:1
例程2:
DWORD dwIndex;
unsigned int WINAPI threadFun(LPVOID)
{
TlsSetValue(dwIndex, (LPVOID)2);
lpvoid TlsValue = TlsGetValue(dwIndex);
cout<<"thread: "<< TlsValue << endl;
return 0;
}
void main()
{
dwIndex = TlsAlloc();
TlsSetValue(dwIndex,(LPVOID)1);
unsigned int threadID;
HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,threadFun,0,0,&threadID);
Sleep(3000);
lpvoid TlsValue = TlsGetValue(dwIndex);
cout<<"main: "<< TlsValue<<endl;
}
得到的结果:
thread:2
main:1
4.怎样使得一个线程正确的开始和正确的结束
方法1:直接调用windows提供的函数CreateThread创建一个线程。在这个线程正常执行结束后,系统会自动调用ExitThread告诉操作系统这个线程结束了,应该释放资源了,比如引用计数减1.
方法2:调用C/C++的CRT(运行时)函数,_beginthreadex创建一个线程。这个线程正常执行结束之后,系统会自动调用_endthreadex函数来结束线程。
我们如果要结束一个线程,
我们可以用return,直接从线程返回,这样不会有任何问题;
或者用和创建函数配对的线程结束函数(CreateThread和ExitThread,_beginthreadex和_endthreadex 配对)来结束线程,但是这样会产生一个问题,就是class对象的析构函数不会被调用。所以使用这种方法要特别注意。
这里要说明的是:_beginthreadex 和CreateThread 有相同的返回值和参赛
为什么要使用配对的创建和结束函数(不过使用线程结束函数要特别注意):
由于c/c++的CRT函数(可以理解为就是c/c++函数)是为单线程设计的。所以在多线程使用的时候就会出问题,比如c/c++的全局变量 errno ,有的函数在出错时会设置该变量。如果在多线程情况下使用errno的话,就会把errno搞混。 解决办法就是,在线程启动的时候给每个线程加入一个_tiddata 内存块,用来存储线程的局部信息。所以每个c/c++的CRT函数在执行的时候都会去读这个内存块。如果发现这个内存块是空的的话,那么这些CRT函数会自己创建一个_tiddata 内存块给这个线程。以后调用的CRT函数都会使用这个内存块。但是只有_beginthreadex函数在启动一个线程的时候创建了这个内存块,而CreateThread没有创建。而且也只有_endthreadex函数最后会把这个内存块delete掉。
由于_beginthreadex函数创建的线程在结束之后会有一个调用_endthreadex的动作,所以_tiddata内存会被delete。 所以由CreateThread创建的线程如果调用了CRT函数,最后就会造成内存泄漏。
CRT(c runtime)函数会有一个_tiddata的数据结构。如果使用CreateThread创建的线程调用CRT函数,CRT函数在发现 _tiddata为空的话也会创建一个_tiddata,但是如果没有用_endthreadex结束的话,就会造成 _tiddata 申请的内存没有被释放,就会造成内存泄漏。 所以最好的方法就是配对使用。
5.CreateThread和c++不合拍
首先要知道线程终止应该干的事:
(1)释放windows资源。 比如线程引用计数减1, 释放线程堆栈等
(2)释放c++对象 就是调用c++对象的析构函数。
线程终止的四种方式:
(1)线程运行结束或者return 。
这种方式是最好的方式,会调用声明对象的析构函数,会释放系统资源(比如线程引用计数减1)。
(2)调用ExitThread函数
这种方式下面讨论
(3)调用Terminate函数
这种方法是绝对禁止的。
Ex:如果线程正在new 一片内存,但是new还没有完成,线程就被终止了的话。由于new是用了锁的,在new没有完成的时候,那么锁就不会被解开。所以以后调用new或者delete都会被阻塞。
(4)主线程终止
这种情况需要搞清。
来源:CSDN
作者:jiangdewei2012
链接:https://blog.csdn.net/jiangdewei2012/article/details/19151917