MFC总结

淺唱寂寞╮ 提交于 2019-12-07 10:47:19

   
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)主线程终止
    这种情况需要搞清。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!