2010年3月15日
2009-10-7
=======================
《深入解析MFC》笔记 12. 进程与线程
=======================
核心对象
核心对象 产生方法
event CreateEvent
mutex CreateMutex
semaphore CreateSemaphore
file CreateFile
file-mapping CreateFileMapping
process CreateProcess
thread CreateThread
进程的生命周期: 《深入浅出MFC》P39
1、shell调用 CreateProcess 激活 App.exe
2、系统产生一个进程核心对象,计数值为1.
3、系统在此进程建立一个4GB地址空间。
4、加载器将必要的代码加载到上述地址空间中,包括App.exe的程序、数据,以及所需的动态链接函数库(DLLs)。
5、系统为此进程建立一个线程,成为主线程,现成才是CPU时间的分配对象。
6、系统调用 C runtime 函数库的 Startup code。
7、startup code 调用 App程序的 WinMain函数。
8、App开始运行。
9、使用者关闭App主窗口,是WinMain消息循环结束,WinMain结束
10、回到Startup code。
11、回到系统,系统调用 ExitProcess结束进程
产生子进程:
CreateProcess(
LPCSTR lpApplicationName, //指定可执行文件名
LPSTR lpCommandLine, //指定欲传给新进程的命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, //指定3、4的安全属性是否需要被继承
DWORD dwCreationFlags, //标识组合
LPVOID lpEnvironment, //指定进程所使用的环境变量区,Null或父进程环境变量
LPCSTR lpCurrentDirectory, //设定子进程的工作目录与工作驱动器,若为NULL,继承父进程
LPSTARTUPINFO lpStartupInfo, //指向一个 STARTUPINFO结构的指针,
LPPROCESS_INFORMATION lpProcessInformation
);
线程的生命周期: 《深入浅出MFC》P41
调用CreateThread产生额外的线程时,系统完成以下工作
1、配置“线程对象”,其handle将成为 CreateThread 的返回值
2、设定计数器为 1.
3、配置线程 context。
4、保留线程的堆栈。
5、将 context 中的堆栈指针缓存器(SS)和指令指针缓存器(IP)设定妥当。
CreateThread ( LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性的设定及继承
DWORD dwStackSize, //堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, //设定“线程函数”的名称
LPVOID lpParameter,
DWORD dwCreationFlags, //为0,则线程立即开始执行。
LPDWORD lpThreadId //纺织线程的ID
);
unsigned long _beginthreadex ( 《深入浅出MFC》P42
void* security,
unsigned stack_size,
unsigned (__stdcall *start_address) (void *),
void *arglist,
unsigned initflag,
unsigned* thrdaddr
);
posted @
2010-03-15 23:29 Euan 阅读(896) |
评论 (0) |
编辑 收藏
2009-10-2
======================
《深入解析MFC》笔记 11. MFC 实现 COM
======================
COM ( Component Object Model,组件对象模型)
OLE 是微软的对象技术的名字,是一种整合软件的统一方法,使得软件可以随时间而演变,是一种整合技术。
从时间的角度讲,OLE是最早出现的,然后是COM和 ActiveX;
从体系结构角度讲,OLE和ActiveX是建立在COM之上的,所以COM是基础;
OLE的主要目标是使得应用程序被外界所认识和理解。
使用COM编程的目的是使得软件从源代码以及的重用上升到二进制一级的重用。
组件是一个可重用的模块,由一组处理过程、数据封装和用户接口组成的业务对象(Rules Object)。组件看起来像对象。它们的主要区别是:
1)组件可以在另一个称为容器(有时也称为承载者或宿主)的应用程序中使用,也可以作为独立过程使用;
2)组件可以由一个类构成,也可以由多个类组成,或者是一个完整的应用程序;
3)组件为模块重用,而对象为代码重用。
组件模型有COM(Component Object Model,对象组件模型)/DCOM(Distributed COM,分布式对象组件模型)和CORBA(Common Object Request Broker Architecture,公共对象请求代理体系结构)。
=================
COM
------------------------
COM 类
COM 所做的全部工作就是在二进制一级共享软件。
C++ 类 和 COM 类
1、C++使用操作符new来实例化一个对象,而COM对象通过API的函数来创建。
2、C++使用delete来删除一个对象,COM使用引用计数来控制对象的生存期,引用计数为0,自动删除。
3、C++使用特殊的机制来控制运行时类型信息(run-time type info)和类型转化(typecasting),而COM类通过成员函数来支持
4、C++类提供成员函数和数据以供外部访问,COM中,类只提供成员函数。
5、COM类没有具体规定的布局。
6、COM引入了一套接口的规范来形式化对象指针的概念。
使用COM对象:
①、给操作系统发送实例化一个COM的请求;
②、得到对象的一个接口指针
③、使用接口; ④、释放接口
------------------------
COM 接口
一个接口就是一组等待被实现的纯虚函数
GUID
Globally Unique Identifier 128位数字
COM 类的ID用 "CLSID"作为前缀,
接口 ID 用"IID"作为前缀
=================
IUnknown接口
struct IUnknown{
virtual HRESULT QueryInterface ( IID& iid, void** ppvObj ) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
}
-------
对象生存期管理
COM对象自己控制生存期。
COM 对象的引用计数由 IUnknown 接口的 AddRef() 和Release() 这两个函数来管理。
--------
接口协商(negotiation,运行时发现功能)
当一个客户拥有了一个COM对象的一个接口指针,就能够通过 QueryInterface() 发现这个对象所支持的别的接口
HRESULT QueryInterface ( REFIID riid, LPVOID far* ppvObj );
riid用来标识所要得到的接口的 GUID。
ppvObj 用来存放接口指针
实现 IUnknown::QueryInterface的一个方法:
HRESULT CoSomeObject::QueryInterface ( IID& riid, void FAR** ppvObj ){
HRESULT hr = ResultFromSCode ( E_NOINTERFACE );
*ppvObj = NULL;
if ( riid == IID_IUnknown ){
*ppvObj = ( IPersist * ) this;
hr = NOERROR;
} else if (riid == IID_IPersist ) {
*ppvObj = ( IPersist * ) this;
hr = NOERROR;
}
return hr;
}
-------------
调用 、 使用 、释放。
1、调用某个方法得到一个接口(通过初始化一个对象或者通过一个存在的接口调用 QueryInterface())。
2、使用接口内的函数。
3、调用Release() 对对象引用计数减1.
========================
COM 对象服务器
COM类存在于服务器之中。COM服务器分为两种: 进程内服务器(in-proc)和进程外服务器(out-proc)。
in-proc server 是 DLL形式的,他们与客户在同一个进程空间内。
out-proc server 是作为 EXE文件形式存在,和客户在不同的进程空间内(甚至在不同机器).
进程内服务器(DLL)
进程外服务器(EXE)
有更强的鲁棒性,进程外服务器需要付出额外的代价,列集(marshaling)。
一个进程空间内的地址(数据地址、函数指针)在另一个进程空间内不起作用,需要用远程过程调用(RPC)和列集(marshaling)。
marshaling:
每一个接口,在客户代码中有一个代理(proxy),一个代理是一个接口的进程内实现。
服务端中,每一个特定的接口都建立了一份存根(stub)代码(在服务器进程空间内)。存根是RPC传输的接收端,
它把代理传过来的函数调用和参数传递映射为映射为服务器进程内的函数调用和参数传递。
客户通过接口进行函数调用时,代理将参数打包为一个32位可移植的数据结构。打包完后,对服务器进程产生一个远程过程调用。
服务器端,存根保存真正的接口指针、函数和相关的数据结构。存根将参数从包中解开以在本进程空间内调用。
一个函数调用返回时,存根将返回值和其他返回信息打包,发送回代理。代理解包,取出返回值供客户使用。
类厂(class factory)
COM通过类厂来创建对象,进程空间内的每一个COM类都有一个类厂与之相对应。类厂产生的是对象,不是类。
类厂是一个特殊COM类。
一个重要的接口是 IClassFactory。它包含两个函数:CreateInstance()和LockServer()。
客户通过调用CreateInstance来创建OLE类的实例,LockServer来增加整个服务器的引用计数。
每一个COM类在源代码一级都有自己的类厂。
进程内服务器通过一个预先定义的函数将类厂提供给客户,
进程外服务器将类厂注册到 Windows的注册表中。
在进程内服务器提供类厂
通过导出函数(exported function)来提供类厂,函数名为:DLLGetClassObject()。
客户可以通过 CoGetClassObject() 或 CoCreateInstance() 来创建一个 COM 对象。它们都调用了DLLGetClassObject来得到相应COM类的类厂接口指针。
然后可以调用类厂的CreateInstance() 来创建一个类的实例并且得到一个接口指针。
STDAPI DllGetClassObject ( REFCLSID rclsid, REFIID riid, LPVOID FAR* ppv)
要得到的类厂的COM类的GUID值,第三个参数 是一个指向接口指针的指针,用来存放请求的接口指针的地方。
在进程外服务器中提供类厂:
在运行时在注册表里注册类厂,CoRegisterClassObject():
HRESULT CoRegisterClassObject ( REFCLSID clsid, // 要注册的类的GUID
IUnknown *pUnk, //对象的 IUnknown 指针
DWORD grfContext, //参数用来标识服务器的环境——可执行的服务器代码是一个DLL、一个本地服务器还是一个远程服务器
DWORD grfFlags, //用来表示客户使用何种方式跟服务器建立连接
LPDWORD pdwRegister ); //指向DWORD的指针,所指向的值在类厂注册的时候由COM来填写。该值标识了类厂的注册信息,可以通过
它然后调用 CoRevokeClassObject() 来取消类厂的注册。
卸载进程内服务器
调用COM中的 CoFreeUnusedLibraries() 来实现这个功能,该函数通过调用DLL的DLLCanUnloadNow()来询问是否能被卸载。
卸载进程外服务器
进程外服务器是自己卸载自己,
· 当客户减少服务器的引用计数(通过调用IClassFactory::LockServer ( FALSE ),而且外部没有对象了。
· 当客户释放最后一个对象的最后一个接口指针,并且服务器的锁计数是0.
posted @
2010-03-15 23:28 Euan 阅读(941) |
评论 (0) |
编辑 收藏
2009-10-1
========================================================
《深入解析MFC》笔记 10. MFC的DLL与线程
========================================================
---------------------
概念:
模块:
一段可执行的程序(包括EXE和DLL),其程序代码、数据、资源被加载到内存中,由系统建置一个数据结构来管理它,这就是一个模块。
进程:
进程是一堆拥有权(ownership)的集合,进场那个拥有地址空间,动态配置而来的内存、文件、线程和一序列的模块。
概要:
· DLL与线程的实现依赖于内部 MFC 状态类,MFC的状态将数据分到不同的逻辑范围中,从而使得线程和 DLL 不会破会对方的数据。
· 扩展 DLL 仅仅用来扩展已存在的 MFC 应用程序。
· MFC中,扩展 DLL 被创建时要使用 _AFXDLL标志。
· 扩展 DLL 有一些资源和其他信息需要在运行时被检索。CDynLinkLibrary是它的辅助类。
· 辅助线程,UI线程 都是用 _beginthreadex()创建、以_endthread()来结束。
· CWinThread::CreateThread创建线程,并且使用_AfxThreadEntry()来为线程提供执行路径。
· 核心:CWinThread::Run()
--------------------
MFC状态
3中MFC状态信息类型:
模块状态、进程状态、线程状态
win32中,一个模块就是独立于应用程序其他部分而操作的可执行代码。
模块状态, 既可以包含真正的全局状态,也可以包含进程局部或者线程局部的状态,且它可以被快速地切换。
可把MFC状态理解成应用程序不同部分的局部数据。
进程状态包含局部于进程和某个模块的数据。模块的状态信息包含局部于该模块的数据,线程的状态信息包含局部于该线程的数据。
------------------------------
MFC进程状态
AFX_MODULE_PROCESS_STATE的定义, AFXSTAT_.H 《深入解析MFC》P301
· m_pfnFilterToolTipMessage —— 一个函数指针,指向用于过滤工具提示消息的函数。
· CTypedSimpleList<CDynLinkLibrary*> m_libraryList —— 附加的MFC扩展DLL链表
· HINSTANCE m_appLangDLL —— 当前被局部化的资源的实例句柄。
· COccManager* m_pOccManager —— 指向OLE控件管理对象的指针
· CTypedSimpleList<COleControlLock*> m_lockList —— 被锁定的 OLE 控件链表
------------------------------
MFC模块状态
AFX_MODULE_STATE 定义, AFXSTAT_.H 《深入解析MFC》 P302
· CWinApp* m_pCurrentWinApp —— 指向该模块的CWinApp对象的指针
· HINSTANCE m_hCurrentInstanceHandle —— 模块的实例句柄
· HINSTANCE m_hCurrentResourceHandle —— 资源的实例句柄
· LPCTSTR m_lpszCurrentAppName —— 当前应用程序的名称
· BYTE m_bDLL —— 指明该模块是否是 DLL 的一个标志。
· BYTE m_bSystem —— 指明该模块是否是系统模块的一个标志
· short m_fRegisteredClasses —— 用于模块的延迟注册类的位标识。
· CRuntimeClass* m_pClassInit —— 指向第一个类的 CRuntimeClass 信息的指针(通常是 m_classList的头)
· CTypedSimpleList<CRuntimeClass*> m_classList —— 模块里各个对象的 CRuntimeClass 信息链表
· COleObjectFactory* m_pFactoryInit —— 指向第一个 COleObjectFactory对象的指针(通常是 m_factoryList 的头)
· CTypedSimpleList<COleObjectFactory*> m_factoryList —— 模块里各个对象的 COleObjectFactory 对象的链表
· long m_nObjectCount —— 被锁住的OLE对象的数目。
· BOOl m_bUserCtrl —— 如果用户有控制权,则设置为 TRUE,否则为FALSE
· TCHAR m_szUnregisterList —— 未被注册的类链表
· WNDPROC m_pfnAfxWndProc —— 指向模块所有的 AfxWndProc 的指针
· DWORD m_dwVesion —— 模块连接所使用的 MFC 版本号
· m_process —— 进程状态信息 PROCESS_LOCAL( AFX_MODULE_PROCESS_STATE, m_process )
· m_thread —— 线程状态信息 THTEAD_LOCAL( AFX_MODULE_THREAD_STATE, m_thread )
-------------------------------
MFC 线程状态信息 《深入解析MFC》P305
_AFX_THREAD_STATE
· AFX_MODULE_STATE* m_pModuleState —— 指向当前模块状态的指针
· AFX_MODULE_STATE* m_pPrevModuleState —— 指向下一个模块状态的指针
· void* m_pSafetyPoolBuffer —— 指向安全缓冲区的指针,它支持强壮的临时对象内存分配
· AFX_EXCEPTION_CONTEXT m_exceptionContext —— 当前的异常环境。
· CWnd* m_pWndInit —— 一个窗口指针,指向最近hook的窗口
· CWnd* m_pAlternateWndInit —— 指向最近被hook的公用对话框窗口的指针
· DWORD m_dwPropStyle ——属性页的风格
· DWORD m_dwPropExStyle —— 属性页的扩展风格
· HWND m_hWndInit —— m_pWndInit 的匹配句柄
· BOOL m_bDlgCreate —— 表明某个对话框被创建了,MFC为对话框绘制与普通窗口不同的背景颜色
· HHOOK m_hHookOldSendMsg —— 由::SetWindowsHookEx ( WH_CALLWNDPROC ) 返回的前一个句柄的句柄
· HHOOK m_hHookOldCbtFilter —— 由::SetWindowsHookEx ( WH_CBT )返回的前一句并的句柄。
· HHOOK m_hHookOldMsgFilter —— 由::SetWindowsHookEx ( WH_MSGFILTER )返回的前一个句柄的句柄
· MSG m_lastSentMsg —— 发送的最后一个消息
· HWND m_hTrackingWindow —— 当前跟踪窗口的句柄
· HMENU m_hTrackingMenu —— 当前跟踪菜单的句柄
· TCHAR m_szTempClassName —— 在 AfxRegisterWndClass() 中使用的缓冲区
· HWND m_hLockoutNotifyWindow —— 在锁定(没有OLE控件)的窗口句柄,如果存在一个的话。
· BOOL m_bInMsgFilter —— 表明该线程在一个消息过滤器中的标志。
· CView* m_pRoutingView —— 在将消息发送给文档之前,视图所先将自己保存到该变量中。
· CFrameWnd* m_bWaitForDataSource —— 之名 ODBC 正在等待的数据
· CToolTipCtrl* m_pToolTip —— 指向当前CToolTipCtrl的指针
· CWnd* m_pLastHit —— 指向拥有工具提示控件的最后一个窗口的指针
· int m_nLastHit —— 最后的点击测试代码(用于工具提示点击测试)
· TOOLINFO m_lastInfo —— 最后的工具提示 TOOLINFO结构
· int m_nLastStatus —— 最后的浮动状态代码
· CControlBar* m_pLastStatus —— 指向最后的浮动状态控制条的指针
----------------------------------------------
MFC状态之间的联系 《深入解析MFC》P305
当MFC需要到达当前的 _AFX_THREAD_STATE时,调用 AfxGetThreadState()
THREAD_LOCAL( _AFX_THREAD_STATE, _afxThreadState )
这行代码为每个线程的 TLS 创建一个名为 _afxThreadState 的 _AFX_THREAD_STATE 类,可通过调用 AfxGetThreadState()来访问
_AFX_THREAD_STATE 记录了指向当前模块状态的指针,名为 m_pModuleState。可通过调用 AfxGetModuleState()得到。
· 多数情况会得到 _afxThreadState.m_pModuleState 的 AFX_MODULE_STATE
· 如果为NULL,将有一个全局的进程局部模块状态,PROCESS_LOCAL( _AFX_BASE_MODULE_STATE, _afxBaseModuleState )
当MFC是一个DLL时,会通过调用 AfxSetModuleState() 来改变模块的状态。
=================
MFC的DLL
---------------------------《深入解析MFC》P307
USRDLL 与 AFXDLL
USRDLL:
可被静态“粘贴”到DLL上,可以再DLL使用MFC,而不必要求使用该DLL的应用程序也是用MFC写的。
AFXDLL :
是使用了MFC的DLL,只能用于MFC程序。
---------------------------
DLL 的资源问题
DLL 里有 3 种类型的信息:
资源、 静态的CRuntimeClass指针、 OLE对象厂(object factory)
MFC 的 DLL 版本程序起点在 DllMain()(当DLL被装载时该函数被调用)
AfxInitExtensionModule( coreDLL, hInstance );
CDynLinkLibrary* pDLL = new CDynLinkLibrary ( coreDLL, TRUE );
装载资源时,通过调用 AfxFindResourceHandle() 来实现
AfxInitExtensionModule( AFX_EXTENSION_MODULE& state, HMODULE hModule )
{
//only initialize once
if ( state.bInitialized ){ //若该模块已经初始化,AfxInitLocalData()被调用来更新 TLS 使用的模块句柄。
AfxInitLocalData( hModule );
return TRUE;
}
state.bInitialized = TRUE;
// save the current HMODULE info for resource loading
state.hModule = hModule;
state.hResource = hModule;
// save the start of the runtime class list
AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
state.pFirstSharedClass = pModuleState->m_classList.GetHead();
pModuleState->m_classList.m_pHead = pModuleState->m_pClassInit;
//save the start of the class factory list
state.pFirstSharedFactory = pModuleState->m_factoryList.GetHead();
pModuleState->m_factoryList.m_pHead = pModuleState->m_pFactoryInit;
return TRUE;
}
--------------------------------
剖析 CDynLinkLibrary 《深入解析MFC》P310
class CDynLinkLibrary : public CCmdTarget
{
HMODULE m_hModule; //
HMODULE m_hResource; //for shared resources
CTypedSimpleList<CRuntimeClass* > m_classList;
CTypedSimpleList<COleObjectFactory*> m_factoryList;
BOOL m_bSystem; // TRUE only for MFC DLLs
CDynLinkLibrary* m_pNextDLL; //simple singly linked list
//implementation
CDynLinkLibrary::CDynLinkLibrary ( AFX_EXTENSION_MODULE& state, BOOL bSystem ){
m_factoryList.Construct( offsetof ( COleObjectFactory, m_pNextFactory ) );
m_classList.Construct( offsetof ( CRuntimeClass, m_pNextClass ) );
//copy info from AFX_EXTENSION_MODULE
m_hResource = state.hResource;
m_classList.m_pHead = state.pFirstSharedClass;
m_factoryList.m_pHead = state.pFirstSharedFactory;
m_bSystem = bSystem;
//insert at the end of the list ( extensions will go in front of core DLL )
AFX_MODULE_PROCESS_STATE* pState = AfxGetModuleProcessState();
AfxLockGlobals ( CRIT_DYNLINKLIST );
pState->m_libraryList.AddHead ( this );
AfxUnlockGlobals ( CRIT_DYNLINKLIST );
}
}
--------------------------------
剖析 AfxFindResourceHandle() 《深入解析MFC》P311
AfxFindResourceHandle() "DLLINIT.CPP"
首先,如果当前模块不是系统模块,在当前模块的资源句柄(由AfxGetResourceHandle() 函数返回)里查找资源。
接着,在进程状态 m_libraryList 里遍历 CDynLinkLibrary 链表。若扩展DLL不是系统 DLL,FindResource() 被调用。
然后在被进程状态指向的与特定语言相关的DLL里寻找该信息。
接着,检查当前的模块是否是系统模块,调用FindResource() 搜索资源,最后再次遍历 CDynLinkLibrary链表,在系统扩展DLL里查找该资源
若没有发现任何东西,返回 AfxGetResourceHandle(),即返回了进程的当前资源句柄。
--------------------------------
扩展 DLL 初始化与清除
AFXDLL 与 宏
DECLARE_DYNAMIC( DLL版 和 非DLL版本) (AFX.H) 《深入解析MFC》P312
IMPLEMENT_DYNAMIC (AFX.H)
DLL 和 非DLL之间的差别:
MFC必须调用能返回静态 CRuntimeClass成员地址的函数,而不是直接存储、访问静态CRuntimeClass数据成员的地址。
=========================
MFC 线程
辅助线程 UI 线程
AfxBeginThread()
辅助线程:将一个 CWinThread对象和一个指针传递给控制函数(控制函数负责工作的完成)
UI 线程: 创建一个 CWinThread,将他的 CRuntimeClass 信息传递给 AfxBeginThread()。
--------------------------
MFC 的辅助线程
AfxBeginThread(){
CWinThread* pThread = new CWinThread ( pfnThreadProc, pParam );
if ( !pThread->CreateThread ( dwCreateFlags | CREATE_SUSPENDED, nStackSize, lpSecurityAttrs ) ){
pThread->Delete(); return NULL;
}
VERIFY ( pThread->SetThreadPriority ( nPriority ) );
if( !(dwCreateFlags & CREATE_SUSPENDED )
VERIFY ( pThread->ResumeThread() != (DWORD) - 1);
return pThread;
}
=========================
CWinThread 《深入解析MFC》P316
数据成员:
· CWnd* m_pMainWnd —— 指向应用程序主窗口的指针, CWinThread需要用该指针来正确地用程序的主窗口。
· CWnd* m_bActiveWnd —— 当OLE 服务器在某个地方处于活动状态时,该指针指向包含应用程序的主窗口。
· BOOL m_bAutoDelete —— 当该变量为 TRUE时, CWinThread会在线程结束时删除自己。
· HANDLE m_hThread —— 被 CWinThread封装的Win32线程的句柄
· DWORD m_hThreadID —— 被CWinThread 封装的线程的 Win32 线程 ID。
· MSG m_msgCur —— 缓冲当前正被 CWinThread 消息发送其所处理的消息。
· LPVOID m_pThreadParams —— 保存pParam参数,他会继续传递给辅助函数(worker function)。
· AFX_THREADPROC m_pfnThreadProc —— 指向辅助函数的指针
· CPoint m_ptCursorLast —— 最后鼠标移动消息中的 CPoint。被用来在 CWinThread 的空闲时刻过滤掉多与的鼠标移动信息。
· UINT m_nMsgLast —— 用来探测两个连续的消息。
· CommonConstruct() —— 供构造函数调用,仅仅将数据成员设置成正确的默认值
· Delete() —— 如果 m_bAutoDelete为TRUE,则调用删除自身。
----------------------. 《深入解析MFC》P317
线程的创建—— “THRDCORE.CPP”
CWinThread::CreateThread() 第一部分:
BOOL CWinThread::CreateThread ( DWORD dwCreateFlags, UINT nStackSize, LPSECURITY_ATTRIBUTES lpSecurityAttrs ){
//setup startup structure for thread initialization
_AFX_THREAD_STARTUP startup;
memset ( &startup, 0, sizeof( startup) );
startup.pThreadState = AfxGetThreadState(); //_AFX_THREAD_STATE
startup.pThread = this; //指向 CWinThread 的后向指针 (back pointer)
startup.hEvent = ::CreateEvent ( NULL, TRUE, FALSE, NULL); //线程被成功创建后,hEvent被触发
startup.hEvent2 = ::CreateEvent ( NULL, TRUE, FALSE, NULL); //线程被重新启动,hEvent2 被触发
startup.dwCreateFlags = dwCreateFlags; //指定线程是否应该被挂起以及其他信息的创建标志。
//**some event checking and cleanup omitted for brevity
//create the thread (it may or may not start to run)
m_hThread = (HANDLE) _beginthreadex ( lpSecurityAttrs, nStackSize, &_AfxThreadEntry, &startup,
dwCreateFlags | CREATE_SUSPENDED, (UINT* ) &m_nThreadID );
if( m_hThread ==NULL)
return FALSE;
}
线程创建之后:
//CWinThread::CreateThread() continued...
① ResumeThread();
::WaitForSingleObject ( startup.hEvent, INFINITE );
② ::CloseHandle ( startup.hEvent );
//if created suspended , suspend it until resum thread wakes it up
if( dwCreateFlags & CREATE_SUSPENDED )
::SuspendThread ( m_hThread );
if( startup.bError )
//**Error cleanup omitted - lots of frees and closes <g>
// allow thread to continue, once resumed (it may already be resumed)
::SetEvent ( startup.hEvent2 );
return TRUE;
一个最初调用CreateThread() 得到的线程(父线程),另一个处在幼稚期的线程。
到①时调用::ResumeThread( m_hThread ),这是父进程暂停,它等待_AFX_THREAD_START::hEvent。
幼稚期线程开始运行,开始的位置在 _beginthreadex()调用告诉的位置:_AfxThreadEntry()
⊙﹏⊙b _AfxThreadEntry 简称为 “_ATE” 《深入解析MFC》P319
1、_ATE将它的 _AFX_THREAD_STARTUP 参数的 pThread 域保存到真正的 CWinThread 指针里。
2、然后调用 AfxGetThreadState(),它会返回父线程的状态,复制大部分父线程的状态信息,用_AFX_THREAD_STARTUP的参数来修改模块状态。
新的子线程从父线程那里”继承“到了模块状态信息。
3、再调用 AfxInitThread(),该函数为该线程创建了一个消息队列以及一些消息过滤器。
4、在AfxInitThread()执行完成后,_ATE创建一个局部的 CWnd对象,并将它贴附到当前的主窗口里。
即将它贴附到 CWinApp::m_pMainWnd::m_hWnd,然后使得 CWinThread::m_pMainWnd指针指向它的地址。
5、hEvent2句柄被从 _AFX_STARTUP_THREAD指针里拷贝出来。
6、准备通知父线程子线程已准备好,信号被转换成时间,使得父线程里的 ::WaitForSingleObject() 被启动。真正的函数调用是 ::SetEvent(startup->hEvent)
7、_ATE调用 ::WaitForSingleObject( hEvent2, INFINITE ) 将新的 成人线程休眠,直到父线程发出最后的信号
这时,在调用完::WaitForSingleObject()后,回到父线程,即回到②。
父线程调用::CloseHandle() 为它的子女清除 hEventHandle,若子线程以暂停方式被调用,则在子线程调用::SuspendThread()。
8、WaitForSingleObject()调用完成后,子线程向它的父线程说再见,并关闭了 hEvent2 记录的句柄
9、如果CWinThread::m_pfnThreadProc非空,子线程会意识到自己是 辅助线程,并且通过控制传递给 m_pfnThreadProc来开始工作
10、若m_pfnthreadproc为空,则为一个UI 线程。
_AfxThreadEntry() 中与 UI 有关的部分 "THRDCORE.CPP"
//Initialization and synchronization with mom before here.
if ( pThread->m_pfnThreadProc != NULL )
nResult = pThread->ExitInstance();
else if ( ! pThread->InitInstance() ) // InitInstance() 被CWinThread的派生类重载
nResult = pThread->ExitInsance();
else
nResult = pThread->Run(); // 通常情况调用 CWinThread::Run()
//cleanup and shutdown the thread
threadWnd.Detach(); //将局部的 CWnd 主窗口对象分离出来
AfxEndThread( nResult ); //最终调用 _endthreadex().
return 0; //not reached
----------------
MFC UI 线程 《深入解析MFC》P320
辅助线程仅需向 CWinThread 提供一个指向某个函数的指针,
UI 线程 需要从CWinThread 派生。
· ExitInstance() —— 当线程结束时执行一些清理工作。
· InitInstance() —— 执行线程实例的初始化工作。
· OnIdle() —— 执行与线程有关的空闲时的处理工作。
· PreTranslateMessage() —— 消息过滤器
· Run() —— 消息泵(非MFC消息的循环处理)
CWinApp 是一个UI线程
-----------------------------
CWinThread::Run() “THRDCORE.CPP”
int CWinThread::Run(){
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
for( ; ; ){
//phase1: check to see if we can do idle work
while (bIdle && ! ::PeekMessage ( &m_msgCur, NULL, NULL, NULL, PM_NOREMOVE ) ){
if ( ! OnIdle ( lIdleCount ++ ) )
bIdle = FALSE; // assume "no idle " state
}
//phase 2: pump messages while available
do {
if ( !PumpMessage() )
return ExitInstance();
if ( IsIdleMessage ( &m_msgCur ) ){
bIdle = FALSE;
lIdleCount = 0;
}
}while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE ) );
}
}
通过bIdle标志 和 LONG变量 lIdleCount 连接两个阶段。
· 阶段一 —— while循环,当线程的消息队列里没有任何消息时,它处在闲置状态。
· 阶段二 —— do-while 循环,当有消息需要被分发时,它进行消息的分发。
● 阶段一: 空闲处理
通过调用OnIdle() 来告诉用户没有执行任何操作,线程可以做一些清理工作或其他。闲置操作完成后,返回 0 ;
OnIdle()的参数用来指示线程已经有多长时间处在非活动状态了,这样,可以对空闲处理做一些优先排序工作。
CWinThread::OnIdle() “THRDCORE.CPP”
BOOL CWinThread::OnIdle ( LONG lCount )
{
if ( lCount <= 0 ) {
//send WM_DILEUPDATECMDUI to the main window
//send WM_IDLEUPDATECMDUI to all frame windows
}
else if ( lCount >= 0 ){
//Call AfxLockTempMaps / AfxUnlockTempMaps to free maps, DLLS, etc...
}
return lCount < 0; //nothing more to do if lCount >= 0
}
若调用 OnIdle( -1 ),强制执行一次 UI 更新,
● 阶段二 : 消息的分发
do-while 循环中,调用 PumpMessage() 分发消息,然后调用 IsIdleMessage() 来确认当前正在被处理的消息是否是空闲消息。
posted @
2010-03-15 23:27 Euan 阅读(2618) |
评论 (0) |
编辑 收藏
2009-9-8
========================================================
《深入解析MFC》笔记 9.MFC的增强型用户界面类
========================================================
----------------------------
CSplitterWnd: MFC分割窗口 《深入解析MFC》P249
使用 CSplitterWnd
创建动态分割窗口
① 在子框架的派生类中加上一个 CSplitterWnd 数据成员。
② 在CMyChildFrame::OnCreateClient() 处理程序,添加对 CSplitterWnd::Create() 的调用。
Create() 的第一个参数指向父框架的指针,第二个和第三个分别指定了最大行数和最大列数。
第四个参数指定了所允许的最小窗格大小,第五个参数指向 CCreateContext 的指针。
③ 将框架窗口的 RUNTIME_CLASS 消息传递给 CDocTemplate 构造函数:
CMultiDocTemplate* pDocTemplate =
new CMultiDocTemplate( IDR_MYAPPTYPE, RUNTIME_CLASS(CMyDocClass ), RUNTIME_CLASS( CMyChildFrameClass ),
RUNTIME_CLASS(CMyViewClass));
AddDocTemplate(pDocTemplate);
创建静态分割窗口,用 CreateStatic() 代替 Create()。调用CSplitterWnd::CreateView() 来创建新的窗格。
*************************
CSplitterWnd 内部实现
封装的数据类型
· ESplitType —— 定义要画出的分割器的类型,属于枚举类型。类型有 分割框、分割条、分割焦点以及分割边界。
· CRowColInfo —— 记录行或列的最小尺寸、理想尺寸和当前尺寸。
创建/布局数据成员
· m_pDynamicViewClass —— 指向由 CSplitterWnd 动态创建的视图(窗格)的CRuntimeClass 信息的指针。Create() 或CreateView()中定义
· m_nMaxRows / m_nMaxCols —— 调用 Create() 和 CreateStatic() 时制定的最大行数和列数。
· m_nRows / m_nCols —— 当前在 CSplitterWnd 里显示的行数和列数。
· m_bHasHScroll / m_bHasVScroll —— 表明行滚动条或列滚动条 是否已经创建的标记。
· m_pColInfo —— 是CRowInfo的数组,每个元素对应 CSplitterWnd 的一列。静态分割中,这个值固定。
· m_pRowInfo —— 是 CRowColInfo 的数组,每个元素对应CSplitterWnd 的一行。
修饰的数据成员
在构造函数中初始化,
· m_cxSplitte / m_cySplitter —— 分割框和分割器的宽度和高度。
· m_cxBorderShare / m_cyBorderShare —— 如果分割窗口正在画分割窗口的边界,值为1.
· m_cxSplitterGap / m_cySplitterGap —— 分割框/分割条和滚动条/边界之间的距离。值为6.
· m_cxBorder / m_cyBorder —— 分割边界的边界宽度。值为0.
跟踪数据成员
用于点击测试和跟踪
· m_bTracking —— 如果为真,则用户正在拖动一个分割条。
· m_bTracking2 —— 如果为真,用户正在拖动两个分割条。
· m_ptTrackOffset —— 点击测试中的“选取”尺寸。允许用户有所偏差。
· m_rectLimit —— 跟踪时窗格的大小,用来确定被跟踪的分割条的高度。
· m_rectTracker —— 跟踪时用来画分割条的矩形。
· m_rectTracker2 —— 跟踪时用来画第二个分割条的矩形
· m_htTrack —— 被 CSplitterWnd 的点击跟踪机制设置成一个枚举值,用来描述分割窗口的哪个部分被点击了。
通用成员函数
· CreateCommon() —— 当Create() 和 CreateStatic() 初始化完 CSplitterWnd 的动态成员或静态成员时会调用这个函数。
· CreateScrollBarCtrl() —— 创建带有指定风格和标示符的滚动条。
· DoScroll() —— 对滚动条消息作出反应。 DoScroll() 能够同步适当的窗格。
· DoScrollBy() —— 以制定的数量滚动相应的窗格。
· DoKeyboardSplit() —— 在程序里调用该函数会使得窗口被分割。
· CanActivateNext() —— 用来确定下一个窗格是否能被激活。即是否能得到焦点,被CView类调用
· ActivateNext() —— 激活下一个窗格。通常在一个窗格被删除时调用
布局成员函数
· RecalcLayout() —— 维护所有分割窗口的位置,当一个窗格被创建/删除时,被调用。
· TrackRowSize() —— 更新指定行的 m_pRowInfo 数组信息。同时确定是否有足够的空间来存储该行。
· TrackColumnSize() ——
· GetSizingParent() —— 搜索大小可变的父窗口
绘画成员函数
· DrawAllSplitBars() —— “驱动”分割窗口的绘画进程,为每个需要绘画的组件调用 OnDrawSplitter.
· OnDrawSplitter() —— 为分割窗口的每个组件进行绘画,为虚函数。
· OnPaint() —— 对WM_PAINT消息作出响应
点击测试成员函数
· HitTest() —— 选取某个点,返回这个点的点击测试值。
· GetInsideRect() —— 类同GetClientRect(),考虑了共享的滚动条。
· GetHitRect() —— 为某一指定的分割窗口组件检索点击矩形。
· SetSplitCursor() —— 使用点击测试来确定要显示哪一种光标。
跟踪成员函数
· OnNcCreate() —— CSplitterWnd 处理WM_NCCREATE 消息,所以它可以移走 WS_EX_CLIENTEDGE 的扩展风格位。
· OnPaint() —— 画分割窗口的各个组件。
· OnDisplayChange() —— 当用户改变显示器的分辨率时背调用,调用RecalcLayout() 来更新分割窗口。
· OnSize() —— 当用户改变窗口大小时 调用 RecalcLayout()。
· OnMouseMove() —— 在分割窗口组件上执行点击测试。
**************************
CSplitteWnd 的初始化
CSPlitterWnd::CreateCommon()
① 调整好风格标记。
② 调用 AfxDeferRegisterClass(),CWnd::CreateEx()。
③ 为 m_pColInfo 和 m_pRowInfo 数组分配空间,并进行初始化,将 m_nMaxCols/Rows 作为数组的大小。
CreateCommon() 在循环里一次访问CRowColInfo数组,做如下操作
1. nMinSize 和 nIdealSize 都被设置成参数 sizeMin的值。
2. nCurSize 被初始化为 -1,说明当窗格的尺寸被初始化(RecalcLayout)时,该值应该被设置。
④ 初始化完CrowColInfo 的行列数组后,调用 SetScrollStyle() 将 m_bHasH/VScroll 进行初始化,然后返回TRUE
CSplitterWnd::CreateView()
① 将sizeInit参数存储在 CRowColInfo相应的数组下标里,设置一个局部标记 bSendInitialUpdate 值为FALSE。
② 创建一个局部的CCreateContext(),尽量将每个元素初始化为比较完整的值。调用 GetActivePane() 来确定 m_pLastViewCView指针
一旦CreateView() 有了m_pLastView,就可以通过调用 GetDocument() 来确定 CCreateContext 其他域的值,
然后调用 CDocument::GetDocTemplate()。在找到所有这些元素后,pContext指向她们,将bSendInitialUpadte设为 TRUE。
③ 调用 CreateObject() 为 CRuntimeClass 信息创建一个窗格对象。设置要传递到 Create() 的参数 风格和定位矩形。
posted @
2010-03-15 23:26 Euan 阅读(1585) |
评论 (0) |
编辑 收藏
2009-9-7
========================================================
《深入解析MFC》笔记 8.MFC的文档/视图结构 拓展
========================================================
----------------------------
CMirrorFile 《深入解析MFC》 P221
CMirrorFile::Open() (DOCCORE.CPP)
第一部分:先检查 modeCreate,看调用者是想创建一个新文件还是想连接在已有文件后面。
然后调用CFile::GetStatus(),若文件非空,返回一个非零的数;
若文件存在,Open()接着会调用GetDiskFreeSpace(),确定驱动器有多少字节可以用。将结果和已有文件大小的两倍进行比较。
若大于后者,创建一个临时文件,文件名存储在 m_strMirrorName 中。
第二部分当 m_strMirrorName 非空时执行。
调用 CFile::Open() 打开镜像文件,然后将文件的时间和文件权限从源文件拷贝给镜像文件。
若执行了第二部分,Open() 返回TRUE,若没有被执行,会调用 CFile::Open(), 返回调用结果。
(若有写操作正在执行或者有文件被覆盖,CMirrorFile::Open() 就打开一个和指定文件不同的文件。
CMirrorFile::Close()
先在 m_strName 存储文件的名字,在调用CFile::Close() 之后,检查是否使用了镜像文件,若使用了,Open()会删除指定 文件,将文件拷贝为指定文件。
即CMirrorFile 通过保存原有文件和对临时文件进行写操作来保护你的文档。
这样,若写操作出现问题,源文件也是安全的 。
CMirrorFile 还会确保源文件的安全性以 及文件创建信息是否能被正确地拷贝
---------------------------
CView 打印 《深入解析MFC》 P223 - 235
---------------------------
CView的派生类: CScrollView
调用SetScrollSizes() 让 CScrollView 知道你的“逻辑视图”的大小,获得大小信息后,操纵传递给 OnDraw() 的DC,从而支持滚动
CScrollView 如何运作:
CScrollView ( VIEWSCRL.CPP )
· m_nMapMode —— 在 SetScrollSizes() 中,可以为应用程序指定一个映射模式。默认为MM_NONE,CScrollView定义为(MM_SCALETOFIT)。
· m_totalLog —— 逻辑坐标中视图的大小,这个值通过 SetScrollSizes() 成员函数传递给 CScrollView。
· m_totalDev —— 设备坐标中视图的大小。
· m_pageDev —— 设备坐标中一个页的大小。
· m_lineDev —— 设备中一条线的大小。
· m_bCenter —— CPreviewView 使用这个数据成员进入窗口中的视图。
· CenterOnPoint() —— 将视图集中于一点,由 CPreviewView 调用
· ScrollToDevicePosition() —— 负责滚动视图。通过调用::SetScrollPos() 和 ::CSrollWindow() 更新滚动条,来完成视图的滚动条。
· UpdateBars() —— CScrollView 在初始化的时候和窗口大小发生变化的时候调用。责任是根据 GetScrollBarState() 的返回信息隐藏、显示、初始化滚动条。
· GetTrueClientSize() —— 用来确定用户视图是否足够大(是否需要滚动条),只有 UpdateBars 才会调用这个函数。
· GetScrollBarSizes() —— 确定滚动条的宽度和高度,考虑了窗口的风格和边框的宽度。
· GetScrollBarState() —— 获取 CScrollView 所需要的关于视图的状态。
· CalcWindowRect() —— 计算窗口矩形的大小,考虑滚动条和其他窗口饰物的大小。
· OnPrepareDC() —— CScrollView 和 ONVScroll() 交互的关键。
· OnScroll() —— OnHScroll() 和 OnVScroll两个消息处理函数都调用了这个函数。根据页面大小和行大小确定要滚动的量。再调用 OnScrollBy()
· OnScrollBy() —— 检查需要滚动的量是否超出了滚动条的范围和视图的逻辑大小。若没有,调用 ::SetScrollPos() 移动滚动条,在调用 ::ScrollWindow().
· OnSize() —— 若 CScrollView 不是处于“scale-to-fit”模式下,OnSize() 调用 UpdateBars();否则,调用 SetScaleToFitSize()。
· OnHScroll() —— 调用 OnScroll() 的消息处理函数。
· OnVScroll() —— 调用 OnScroll() 的消息处理函数。
CScrollView::SetScrollSizes() (VIEWSCRL.cpp) 《深入解析MFC》P239
①. 将 m_nMapMode 初始化为提供的新映射模式, m_totalLog 的初始化值是sizeTotal 参数。
②. 在栈上创建一个 CWindowDC,并且在设置了映射模式之后,使用DC 来计算视图大小、页面大小和行大小的设备坐标。
③. 将逻辑坐标转化为设备坐标后,检查确保用户提供了一个非默认的值,若为默认值0,页面大小设为视图大小的1/10,行为页的1/10或视图的1/100。
④. 若需要,调用 UpdateBars() 和 Invalidate()。设置滚动条,若修改了映射模式,重画。
CScrollView::OnPrepareDC() 在调用 OnDraw()之前调用
①. 设置映射模式,若在“scale-to-fit”模式下,模式设置为 ANISOTROPIC,然后对窗口和视图端口操作,知道视图的大小和客户窗口匹配。
若不是,调用 SetMapMode(), 参数是用户通过 SetScrollSizes() 指定的映射模式。
②. 在栈上创建一个 CPoint ptVpOrg(assume no shift for printing),初始化为 0,0。 若视图不在打印, ptVpOrg = -GetDeviceScrollPosition()
③. 调用 CDC::SetViewportOrg( ptVpOrg )。然后调用 CView::OnPrepareDC()。
-------------------------------------
CFormView
先用资源编辑器创建一个对话框模板,然后创建一个CFormView 的派生类,然后将它和对话框模板绑定(通过将模板的资源ID 传递给 CFormView 构造函数)
(VIEWFORM.CPP) 《深入解析MFC》 P242
· m_lpszTemplateName —— 对话框模板资源的名字
· m_pCreateContext —— 指向 CCreateContext 的指针。CFormView不直接使用,会再 OnCreate() 函数中将它传送给框架。
· m_hWndFocus —— 在 OnSetFocus() 中,CFormView将焦点设置为窗口句柄, OnActivateView(),OnActivateFrame()。
· OnDraw() —— 什么都不做,Windows控件都会重画自己。
· PreTranslateMessage() —— 完成一些路由的任务,将控件、视图和窗口的消息送往正确的目的地
· SaveFocusControl() —— 将 m_hWndFocus 设置为 ::GetFocus() 的返回结果,即当前处于焦点的控件。
· OnActivateFrame() —— 当视图被禁用时,调用 SaveFocusControl() 确保当焦点回来是,焦点的控件仍有控制。
· OnActivateView —— 设置 m_hWndFocus 变量
· OnCreate() —— 将m_pCreateContext 传递给 LPCREATESTRUCT 结构指针的 lpCreateParams 域。
· OnSetFocus() —— 若 m_hWndFocus 指向一个合法窗口,OnSetFocus()会将焦点设为这个控件。否则将焦点设为源窗口。
CFormView::Create()
①. 在m_pCreateContext 中存储 CCreateContext 参数,这样就可以再调用 OnCreate() 时传入。然后确保 Windows通用控件都是注册过的。
②. 调用PreCreateWindow 来确定用户指定的扩展风格,在调用 CWnd::CreateDlg()。这个函数从应用程序的资源中装载对话框模板,
并调用CWnd::CreateDlgIndirect()。CreateDlg() 创建一个非模态的对话框。
③. 将m_pCreateContext 设置为 NULL,再修改窗口的标准风格和扩展风格。将窗口控件的标示符设置为 nID 参数。
再调用 SetScrollSizes() 来初始化 ScrollView,采用的映射模式为 MM_TEXT。就创建了一个和对话框模板一样大的逻辑视图。
④. 将视图的大小和 rect 参数的值匹配,再在 PreCreateWindow() 中调用指定了 WS_VISIBLE,就调用 ShowWindow()。
CFormView::OnInitialUpdate()调用了 UpdateData(FALSE) 来初始化窗体中的控件。 DDX/DDV就认为CFormView是一个对话框。
--------------------------------
CCtrlView 《深入解析MFC》 P244
CCtrlView 派生以下类: CEditView、CListView、CTreeView、CRichEditView
· m_strClass —— 包含了控件的窗口类的名字,通常是“viewized”。在构造函数中设置。
· m_dwDefaultStyle —— 视图类的默认风格。通过构造函数传入。
· OnDraw() —— 从不被调用,调用了 ASSERT(FALSE)来确保不被调用。
· PreCreateWindow() —— 设置CREATESTRUCT 的 lpszClass 域,设置成 m_strClass 成员数据,这样就创建了控件。
还对 CREATESTRUCT 的风格域操作,以反映存储在 m_dwDefaultStyle 中的值。
· OnPaint() —— 调用 Default()。Default() 会将前一个消息(WM_PAINT)发送给DefWindowProc()。
CTreeView (VIEWCMN.CPP,AFXCVIEW.INL)
· GetTreeCtrl() —— 返回一个 CTreeCtrl 引用,类的使用者可以用这个引用直接调用 CTreeCtrl,对树控件操作。
· RemoveImageList() —— 一个内部辅助函数,将控件的图像链表清空。这个辅助函数会再 OnDestroy() 中被调用
· OnDestroy() —— 清空 LSVIL_NORMAL 和 LVSIL_STATE 两个图像链表,调用 RemoveImageList()。
posted @
2010-03-15 23:25 Euan 阅读(2185) |
评论 (0) |
编辑 收藏
2009-9-3
========================================================
《深入解析MFC》笔记 7.MFC的文档/视图结构
========================================================
文档/视图相互依赖关系
· CWinApp包含一个 CDocManager 指针。
· CDocManager 维护一个文档模板链表,须在 CWinApp::IniInstance()中 创建并加入这些模板。
· 文档模板将这 3 个类绑定在一起: CView / CDocument / CFrameWnd。这些类都根据传递给文档模板构造函数的CRuntimeClass信息创建
· MDI 专有的文档模板维护一个打开文档链表,而 SDI 专有的文档模板只有一个指向打开文档的指针。
· 文档维护一个打开视图的链表。使用这个链表与视图通信、更新视图。
· 文档有一个指针指向它们的文档模板。用这个指针设置标题,进行命令路由选择,在被删除时通知文档模板
· 框架有一个指针指向当前的活跃视图
· 被创建时,框架等到一个 CCreateContext 结构,包含了在文档模板中可以找到的CRuntimeClass信息和一个指向文档的指针。
· 视图有一个指针指向它的文档。当该视图被删除时,用该指针通知文档。
· 视图可以通过调用 CView::GetParentFrame() 获得它的框架窗口。
· 如果文档要访问它的所有视图的框架,可以遍历它的视图链表,并调用 CView::GetParentFrame().
· CDocTemplate —— 在 CWinApp 中创建,保存在 CWinApp::m_pDocManage中
· CDocManager —— 在CWinApp::InitInstance() 中创建
· CFrameWnd —— 在 CDocTemplate::CreateNewFrame() 中创建
· CDocument —— 在 CDocTemplate::CreateNewDocument() 中创建
· CView —— 通过 CFrameWnd::OnCreate() 创建
ID_FILE_OPEN → CWinApp::OnFileOpen() CWinApp::OnFileNew() ← ID_FILE_NEW
命令 ↓ ↓
获取文件名 一个文档模板? →否→ 从用户获取文档类型
↓ ↓ ↓
使用文件拓展名,选择文档模板 是 ↓
↓ ↓ ↓
------------------------------选择的模板-------------------------------
选择模板
↓ → →→打开? →→Yes→→ CMyDoc::OnOpenDocument() →
Construct document object: CMyDoc ↑ ↓ ↓ ↓
↓ ↑ ↓ ↓ ↓
Construct frame window object : CMainFrame ↑ →→→→→→→CMyDoc::OnNewDocument() ↓
↓ ↑ ↓ ↓
CframeWnd::Create() ↑ 文档准备好 ←←←←←←
CFrameWnd::OnCreateClient() ↑
↓ ↑
Create CMyView →→→→→→→→→→→→ 选择模板之后的控制流程
CWinApp::OnFileOpen() CWinApp::OnFileNew()
↓ ↓
CDocManager::OnFileOpen() CDocManager::OnFileNew()
↓ ↓
CDocTemplate::OpenDocumentFile()
CDocTemplate::CreateNewDocument()
CDocTemplate::CreateNewFrame()
WM_CREATE
CFrameWnd::OnCreate()
CFrameWnd::OnCreateHelper()
CFrameWnd::OnCreateClient()
CFrameWnd::CreateView()
CMyDocument::OnOpenDocument()
[if file was specified to OpenDocumentFile()]
CMyDocument::OnNewDocument()
创建文档/视图体系中所有对象过程中调用的函数
-----------
体系结构
文档与视图
1)文档; 2)视图; 3)文档/视图框架; 4)文档模版
文档
由CDocument类体现,派生自CCmdTarget,具有CObject提供的所有支持,且可以接受命令消息,支持组件对象模型(COM)接口和 OLE 自动化。
视图
文档/视图框架
文档模板 《深入解析MFC》 P 198
将整个机制绑定在一起。这3个组件由 CDocTemplate 的类管理。
CDocTemplate是一个抽象基类,定义了处理文档、框架和视图的基本操作。
CSingleDocTemplate: 4个参数①一个资源ID,②运行时的CDocument派生类;③ 运行时的试图框架; ④ 运行时的文档视图。
资源ID:①窗口标题;②文档名称;③当创建一个新文档时使用的文档类型描述符;④对标准的打开文件对话框中文件类型的描述符
⑤文档扩展名过滤器; ⑥ 文件管理器使用的文件类型描述符; ⑦ 在Windows注册表中注册的 ProgID。
SMultiDocTemplage
可以支持一组文档的链表,而CSingleDocTemplate只能支持一种文档
CWinApp的角色
文档模板由 CWinApp 对象管理。
---------------
文档/视图结构内幕 《深入解析MFC》P 199
------------------------------------
CWinApp 管理文档模板,文档模板管理 框架/视图/文档。
CWinApp / CDocTemplate 接口: CDocManager
· CPtrList m_templateList —— 维护一个文档模板列表的指针。 通过GetFirstDocTemplatePosition()和GetNextDocTemplate()来遍历模板列表。
CDocManager::OnFileNew()
负责创建一个新的文档模板
· ①OnFileNew() 检查 m_templateList 中是是否有多于一个文档模板,若是,则创建一个 CNewTypeDlg 对话框,列出一个文档链表以供用户选择。
· ② 调用CDocTemplate::OpenDocumentFile(),
tag:CNewTypeDlg的构造函数读入一个指向文档模板指针链表的指针,即 m_templateList。在DoModal() 之后,OnFileNew()从
CNewTypeDlg::m_pSelectdeTemplate 中得到选中的对话框模板。
CNewTypeDlg (DOCMGR.CPP)
CNewTypeDlg::OnInitDialog()
· ①, 调用GetDlgItem() 得到一个指向 CListBox 指针,并将之类型转换成 CListBox 指针。
· ②,得到指针后,遍历它的文档模板链表,遍历时,通过调用CDocTemplate::GetDocString()得到文档类型的字符串版本,
OnInitDialog获取每个文档的这个字符串并加入链表,再调用 CListBox::SetItemDataPtr() 将这个文档模板指针附加到这项上。
· ③,返回到CDialog::OnInitDialog()。
tag:在CNewType::OnOK()中,当用户选择从链表中选择了一项并按下OK按钮时, CNewType::OnOK() 从量表中得到被选中的项,并调用
CListBox::SetItemDataPtr() 获得指向该文档模板的一个指针。 OnOK() 将这个被选中的模板指针放在 pSelectedTemplate 中,这样,
DoModal() 返回后就可以获取这个指针
----------------------------------------
CDocTemplate: CDocument、CView 和 CFrameWnd 管理器
· m_nIDResource —— 存储传给 CDocTemplate 构造函数的第一个参数,是一个资源ID,
· m_pDocClass —— 指向该文档模板的CDocument(或派生类)的CRuntimeClass结构,在构造函数中设定
· m_pFrameClass —— 指向该文档模板的 CFrameWnd(或派生类)的CRuntimeClass 结构。在构造函数中设定
· m_pViewClass —— 指向该文档模板的CView (或派生类)的CRuntimeClass结构,在构造函数中设定。
· m_strDocStrings —— 一个CString,包含用来定义与每个文档模板联系的7个字符串的字符串表资源。在LoadTemplate()中初始化。
创建新的 文档/视图/框架
CDocTemplate::CreateNewDocument() (DOCTEMPL.CPP) 《深入解析MFC》P205
· 首先,通过CRuntimeClass 指针的数据成员 m_pDocClass 调用 CRuntimeClass::CreateObject() 创建一个新的文档
· 在pDocument 中保存了 新的 CDocument(或派生类 ) 指针之后,验证 CreateObject 是否已经工作,若失败,返回NULL。
若成功,调用 AddDocument() 将该文档加到打开文档链表中。 CreateNewDocument() 返回指向新文档的指针。
CDocTemplate::CreateNewFrame()
· 首先,填充一个CCreateContext(存放创建各种文档/视图结构元素所需的关键信息)。
1>. m_pNewViewClass —— 一个 CRuntimeClass 指针,用来创建视图
2>. m_pCurrentDoc —— 指向当前文档对象的指针
3>. m_pCurrentFrame —— 指向当前框架对象的指针
4>. m_pNewDocTemplate —— 如果有多个文档,指向最后一个;
5>. m_pLastView —— 如果有多个视图,指向最后一个视图。
· 通过m_pFrameClass 中的 CRuntimeClass 结构调用 CRuntimeClass::CreateObject() 创建一个框架,再调用 LoadFrame()装入
这个框架的资源,根据这些值创建该框架。 最后,CreateNewFrame()返回一个指向新框架的指针。
CSingleDocTemplate 《深入解析MFC》P 207
: CDocument* m_pOnlyDoc;
AddDocument() 将 m_pOnlyDoc 设置成参数;
CMultiDocTemplate
: AddDocument() 通过调用 m_docList.AddTail( pDocument ) 将新文档加入自己的链表中。
CPtrList m_docList;
int m_nUntitledCount; //记录没有标题的窗口数
CMultiDocTemplate::OpenDocumentFile()
· 调用CreateNewDocument() 创建一个新的空文档(一个CDocument派生对象),然后通过 CreateNewFrame() 创建一个新的框架,
· 若参数 lpszPathName 为NULL, 则OpenDocumentFile() 知道要创建的事一个空的新文档,调用SetDefaultTitle()完成这个功能。
这个函数为新文档确认名称[使用Untitled与m_nUntitledCount中未命名计数的组合],然后通过新文档指针调用CDocument::SetTitle()
设置新文档名字,组后,OpenDocumentFile()调用 OnOpenDocumentFile(),并将未命名计数的数据成员加1.
· 若lpszPathName 不等于NULL,则 OpenDocumentFile() 尝试打开指定的文档,首先,OpenDocumentFile() 显示一个等待光标,然后
调用CDocument::OnOpenDocumentFile(),将文档名传入作为参数。若调用失败,则销毁新创建的对话框,返回一个NULL。若调用成功,
则OpenDocumentFile() 调用 CDocument::SetPathName() 更新CDocument 的路径。
·不管lpszPathName的状态时说明,OpenDocumentFile() 最后调用 CDocTemplate::InitializeUpdateFrame(),再调用
CFrameWnd::InitialUpdateFrame(), 并传递一个指向新文档的指针。
****///---------------------------------------------
CFrameWnd (WINFRM.CPP)
CFrameWnd 和 CView 的创建 《深入解析MFC》P208
视图的创建:CFrameWnd::CreateView
在文档模板在CreateNewFrame() 中创建框架时,Windows发出一个 WM_CREATE,进行如下调用
OnCreate -> OnCreateHelper() -> OnCreateClient() -> CreateView()
CreateView() 使用 CCreateContext::m_pNewViewClass 中存储的 CRuntimeClass 创建一个视图,创建完成后,调用 CWnd::Create()完成创建工作。
CFrameWnd::InitialUpdateFrame()
使框架中的所有视图都被更新。
· 首先,InitialUpdateFrame() 获得一个活跃视图的指针,并存放在 pView中,
· 若 bMakeVisible 参数为TRUE, 则InitialUpdateFrame() 发出 WM_INITIALUPDATE 给他所有后代,这个消息被映射到 CView::OnInitialUpadte()。
发送消息后,通过CView::OnActiveView() 激活视图。
****///---------------------------------------------
CDocument (AFXWIN.H)
· m_strTitle —— 文档名,通过 SetTitle()设置,保存时用它来设定框架窗口的标题和文件的名字
· m_strPathName —— 当前文档的路径(包括文件名)。用 SetPathname() 设置,是对MRU菜单的补充,保存文件时用到。
· m_pDocTemplate —— 指向文档的文档模板的指针。 CDocTemplate::AddDocument()中设置,通过CDocTemplate::GetDocTemplate()访问
· m_viewList —— 记录所有在该文档下打开的所有视图的指针。 文档改变时,通过这个指针通知视图。
视图通过AddView()加入到m_viewList中,RemoveView()删除。
GetFirstViewPosition() 和 GetNextView() 来遍历
· m_bModified —— “修改”标志位,被修改就设为 1。为1是框架在用户关闭文档时提示。 IsModified() SetModifiedFlag()
· m_bAutoDelete —— 被框架设置为 TRUE时,所有视图关闭时该文档对象要被删除。可设为 FALSE,文档在没有任何打开视图时仍会保留
· DisconnectViews() —— 该成员函数遍历 m_viewlist 中保存的视图链表,并将 CView::m_pDocument 指针设置为 NULL,从而将所有视图从文档上断开。
· DoSave() —— CDocument成员 OnFileSaveAs() 和 OnFileSave() 在提示输入文件名或验证文件名后,最终都调用 DoSave(),DoSave调用了 OnSaveDocument()
· DoFileSave() —— 先检查文档要被存储到的文件的一些性质,在调用 DoSave()。
· UpdateFrameCounts() —— 计数为该文档的所有视图打开的框架,并根据新的技术通知更新它们的窗口标题。
· SendInitialUpdate() —— 遍历视图链表并调用 CView::OnInitialUpdate()。
创建文档: 《深入解析MFC》P213
-> 创建一个空的文档,会调用 CDocument::OnNewDocument()。
首先调用DeleteContents() 清空文档(默认什么都不做),然后设置修改标志位为 FALSE,并确保 m_strPathName为空
-> CDocument::OnOpenDocument() 从一个文件创建文档
①. 首先调用 CDocument::GetFile() 打开一个文件,该函数用给出的文件名调用 CFile::Open()。若打开成功,OnOpenDocument()调用DeleteContents()
为文件的发序列化(de-serialization)作准备(从永久存储设备读入一个新的文档)。调用SetModified()。 若序列化失败,文档被标记为“已修改”。
②. 根据pFile 创建一个 CArchive,并将这个 CArchive 传递给 CDocument::Serialize()。 若发序列化成功,关闭档案并释放文件
③. 最后,关闭修改标志位并返回TRUE
保存文档:
CDocument::OnSaveDocument()
通过GetFile()打开一个文件,使用标志指定文件是打开用来保存的(CFile:: mode ReadWrite|CFile::modeCreate)。
创建CArchive,并将之床底给 Serialize() 成员函数。
若成功序列化,关闭CArchive,标志位设置成 FALSE,返回TRUE。
与视图通信:
m_viewlist 是 CDocument 与正在“视”这个文档的视图进行交互的中介。
(需要交互的状况)
1. 通知视图文档被销毁。 CDocument析构函数调用 DisconnectView()
2. 从视图出发,通过调用CView::GetParentFrame() 访问框架,
3. 通知视图文档被修改、需要更新, UpdateAllViews()完成这个功能
CDocument::UpdateAllViews()
首先调用 GetFirstViewPosition() 获得 m_viewlist的第一个视图;
然后,遍历整个视图链表,除发送者的视图外调用 CView::OnUpdate()。
****///---------------------------------------------
CView (AFXWIN.H)
CView 绘图
CView::OnPaint()
· 首先创建一个CPaintDC,包含要重画的区域,
· 将这个 paint DC 传递给 OnPrepareDC(),最后将 paint DC 传给 OnDraw()。
tag:默认的 OnPrepareDC() 和 OnDraw() 实现不做任何事,
posted @
2010-03-15 23:24 Euan 阅读(2574) |
评论 (0) |
编辑 收藏
2009-9-2
======================
《深入解析MFC》笔记 6.MFC消息处理、消息映射
======================
CCmdTarget和消息映射表
-窗口消息
3个部分:1)一个无符号整数,包含了消息的实际内容; 2)WPARAM—— 一个4字节的参数;3)LPARAM—— 一个4字节的参数
MFC消息映射
CCmdTarget类
消息映射表数据结构:
AFX_MSGMAP_ENTRY,表示消息映射表的实际入口:
struct AFX_MSGMAP_ENTRY{
UINT nMessage; //表示Windows消息。
UINT nCode; //表示控件代码或WM_NOTIFY代码
UINT nID; //产生消息的控件ID
UINT nLastID; //
UINT nSig; //表示用于处理消息的函数的签名(signature)
AFX_PMSG pfn; //指向处理消息的函数
};
typedef void (CCmdTarget::*AFX_PMSG) (void);
struct AFX_MSGMAP{
const AFX_MSGMAP* pBaseMap; //指向另一个AFX_MSGMAP结构的指针(基类的消息映射表)
const AFX_MSGMAP_ENTRY* lpEntries; //
};
消息映射表基本上就是AFX_MSGMAP_ENTRY结构数组。
消息映射宏
DECLARE_MESSAGE_MAP、 BEGIN_MESSAGE_ MAP 和 END_MESSAGE_MAP.
在类定义中使用DECLARE_MESSAGE_MAP意味着在类中定义了3种东西:
1)一个名为_messageEntries 的 AFX_MSGMAP_ENTRY 结构数组; (静态成员)
2)一个名为messageMap 的 AFX_MSGMAP; (静态成员)
3)一个获得类的消息映射表的函数(GetMessageMap())。
#define BEGIN_MESSAGE_MAP(theClass, baseClass) \
PTM_WARNING_DISABLE \
const AFX_MSGMAP* theClass::GetMessageMap() const \
{ return GetThisMessageMap(); } \
const AFX_MSGMAP* PASCAL theClass::GetThisMessageMap() \
{ \
typedef theClass ThisClass; \
typedef baseClass TheBaseClass; \
static const AFX_MSGMAP_ENTRY _messageEntries[] = \
{
宏产生一个GetMessageMap()函数,该函数返回类的消息映射表指针。框架程序用GetMessageMap()来获得类的消息映射表。
然后宏产生填充AFX_MSGMAP结构的代码。第一个域指向基类的消息映射表,同时创建一条到根对象的链表。
第二个域是指向类自己的第一个消息映射表的入口。
最后,宏产生实际的消息表,消息表实际上就是AFX_MSGMAP_ENTRY结构的表。
#define END_MESSAGE_MAP() \
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } \
;
MFC如何使用消息映射表
---------------------------------------
Command Routing(命令传递)
■ 若是一般的 Windows消息(WM_xxx),一定由派生类流向基类
■ 若是命令消息 WM_COMMAND
Frame窗口: View → Frame窗口本身 → CWinApp对象
↓
View: View本身 → Document
↓
Document: Document本身→ DocumentTemplate
posted @
2010-03-15 23:14 Euan 阅读(875) |
评论 (0) |
编辑 收藏
2009-9-2
===========================
《深入解析MFC》笔记 5.对话框和控件类
===========================
CDialog: 模态MFC对话框 和 非模态MFC对话框
--------------------------------------------------------------------------
模态对话框
从CDialog派生一个本地对象,调用 DoModal() 初始化、创建、显示并销毁对话框
非模态对话框
从堆上利用new 操作符创建,显示调用Create() 成员函数来显示对话框, 销毁 DestroyWindow()
对话框操作
NextDlgCtrl() 、PrevDlgCtrl() 或 GotoDlgCtrl() ,遍历对话框中的控件
--------------------------------------------------------------------------
Win32 API
· CreateDialog() —— 从模板资源创建一个非模态的对话框。
· CreateDialogIndirect() —— 从模板指针创建一个非模态的对话框。
· DialogBox() —— 从模板资源创建一个模态的对话框。
· DialogBoxIndirect() —— 从模板指针创建一个模态的对话框。
CDialog只抵用CreateDialogIndirect(),自己实现了模态化。
--------------------------------------------------------------------------
CDialog的一些数据成员
· m_nIDHelp —— 按钮的辅助ID,由模板ID确定
· m_lpszTemplateName —— 资源模板的名字。若指定了ID,m_lpszTemplateName就是MAKELONG(0,ID).
· m_hDialogTemplate —— 一旦装载之后,是资源模板句柄。
· m_lpDialogInit —— 一个指向用来初始化对话框的数据的指针。用来初始化对话框的数据是从资源文件中装载的,通过一个资源ID与对话框相联系。
· m_pParentWnd —— 一个指向父窗口或主窗口的CWnd指针。
· m_hWndTop —— 最顶层的父窗口
· m_pOccDialogInfo —— 存储OLE控件所需要的信息。
CDialog的一些成员函数
· virtual PreTranslateMessage() —— 为特殊情况过滤消息,如工具提示和SHIFT-F1环境帮助
· virtual OnCmdMsg() —— 将命令消息路由给所有者和当前的 CWinThread 对象。忽略控件通知
· virtual CheckAutoCenter() —— 检查用户是否已经指定了对话框自动位于中心
· virtual SetOccDialogInfo —— 用于设置 m_pOccDialogInfo。
· virtual PreInitDialog() —— 在WM_INITDIALOG(OnInitDialog() )之前被调用。
· PreModal() —— 通过准备将自己粘贴到已创建的对话框窗口上,为DoModal()逻辑准备CDialog
· PostModal —— 在DoModal()逻辑之后将CDialog分离。
-------------
CDialog的构造函数
DLGCORE.CPP
DoModal() 《深入解析MFC》 P155
处理对华框的创建、显示和销毁
· 首先载入对话框资源: 从资源文件中查找、载入并锁定对话框模版。无法定位资源时,返回-1.
· 准备创建对话框: 先调用PreModal() 来执行安全检查并未对话框查找所需的父句柄,调用CWnd::GetSageOwner()来完成这些,并将结果存入m_hWnd中。
然后调用EnableWindow(hWndParent,FALSE)来讲对话框的父窗口禁用,强制实现了模态化。
· 创建并显示对话框:
先通过调用AfxHookWindowCreate() 将一个MFC对象粘贴到对话框中,
然后调用 CWnd::CreateDlgIndirect(),该函数会做一些错误检查,然后设置对话框字体,再用 WF_CONTINUEMODAL
对 CWnd::m_nFlags成员做 OR 操作,然后调用 ::CreateDialogIndirect(),参数是对话框模版、父窗口句柄 和 AfxDlgProc()
(作为对话框过程)。返回TRUE或FALSE。
调用CWnd::RunModalLoop() 处理消息直至用户按下 ”OK“或”Cancel“,OnOK() 和 OnCancel() 调用 EndDialog(),EndDialog() 调用
CWnd::EndModalLoop(),这样就结束了loop处理过程,控制流转给DoModal()。
调用 SetWindowPos() 隐藏目前已经死掉的对话框,再调用 EnableWindow(hWndParent,TRUE)来激活父窗口。
· 收尾:
调用DestroyWindow()来销毁Windows对话框对象,然后调用PostModal() 将第一步中粘贴到Windows对象的C++对象分离,设置m_hWndTop为NULL
CDialog控件的初始化
WM_INITDIALOG
WM_INITDIALOG映射到CDIalog::HandleInitDialog().HandleInitDialog()执行OLE控件的一些初始化,从而调用CDialog::OnInitDialog()。
OnInitDialog()调用CWnd::ExecuteDlgInit(),由后者完成最终对话框的初始化。
----------------------------------------------------------------------------------------------------------------
DDX/DDV: CDialog数据交换与验证 P160
CDataExchange 成员函数
· 构造函数 —— 传入一个对话框指针和 bSaveAndValidate的初始化设置。
· PrepareCtrl() —— 为DDX/DDV准备一个非编辑控件。通常这个函数会调用内部对话框指针 m_pDlgWnd 的 GetDlgItem() 方法
· PrepareEditCtrl() —— 为DDX/DDV 准备一个编辑控件。首先调用PrepareCtrl(),然后将m_bEditLastControl设置为TRUE
. Fail() —— 如果验证失败就调用这个函数。将焦点设置到前一个控件,并抛出CUserException异常。
· PrepareOleCtrl() —— 为DDX/DDV 准备一个OLE控件。
· m_bSaveAndValidate —— 指定了DDX的方向,以及是否应该做 DDV ,FALSE时,数据从成员变量流到控件。
· m_pDlgWnd —— 一个指向对话框的CWnd指针,它包含了需要交换和验证的控件。这个值通过CDataExchange构造函数传入。
· m_hWndLastControl —— 记录了前一个控件的句柄。
· m_bEditLastControl —— 一个Boolean变量,指定了前一个控件是否是编辑器。
DDX_Text()
① 先调用 PrepareEditCtrl() 或 PrepareCtrl()。
② 然后DDX_Text() 检查 Boolean 变量 m_bSaveAndValidate的值,
③ 若标志为TRUE(数据从控件流向成员变量),调用 Win32API(如 ::GetWindowText()),从编辑控件获得字符串,将字符串放在"value"中
若为FALSE,调用 AfxSetWindowText(),使用CString参数的内容更新编辑控件。
DDV_MaxChars()
检查 CDataExchange::m_bSaveAndValidate 标志,确保为 TRUE(即现在正在进行验证)。然后根据指定的大小检查字符串的长度。
------------------------------------------------------------------------------------------------
CWnd::UpdateData()
①· 首先创建一个CDataExchange实例,将 this 作为CDialog指针传入,并继续传入bSaveAndValidate 标志(OnDialogInit()调用时为FALSE…)。
②· UpadteData() 会阻塞当前线程,使线程不能接收控件的消息。
③· 本地Boolean变量 bOK用来存储 DDX/DDV 代码的返回值。一旦UpdateData() 将 bOK设置为 FALSE,它会调用你的CDialog派生类的DoDataExchange方法,
同时将CDataExchange实例作为参数传入。 bOK被设置为TRUE, 这个值最终返回给 OnUpdate(),表示当前交换和验证没有错误。
④· 出现CUserException异常时,调用这块代码,bOK被设置为FALSE,最终UpdateData会返回0;
⑤· 如果DDX/DDV处理过程中没有任何异常抛出,会报告已经发生的一系列错误,也有可能导致返回FALSE。
⑥· 重置通知窗口,从而按常规方法处理控件通知,最终返回FALSE 或 TRUE(依赖于DoDataExchange()的返回值 )。
========================================================================
MFC公用对话框
CColorDialog 让用户选择一种颜色
CFileDialog 用于打开和保存文件
CFindReplaceDialog 允许用户查找和替代项
CFontDialog 提供了选择字体的对话框
CPrintDialog 让用户选择打印机、页数、打印方向
----------------------------------------------------------------------------------
属性页
CPropertySheet CPropertyPage
1、创建多个对话框模板,使模板描述你想要的属性页中布置的控件和布局。
2、为每个对话框模板创建CPropertyPage的一个派生类,在类中增加成员变量来存储属性页的状态。
3a、对于一个模态的属性页,只要创建它的CPropertySheet实例,然后调用CPropertySheet::AddPage()将前两步创建的单个属性页加入到属性页中。
再调用CDialog::DoModal()来显示属性页。
3b、如果创建一个非模态的属性页,则创建的CPropertySheet派生类须增加一些东西一边能够关闭对话框,MFC不会自动增加OK/Apply/Cancel
然后创建你的CPropertySheet派生类的一个实例,并调用它的Create()来显示非模态属性页。
使用属性页公用控件
首先,填写PROPSHEETHEADER结构的一些域,然后调用 ::PropertyPage(),以该结构为参数,负责创建属性页。
创建完成后,属性页发送 WM_NOTIFY 消息:
· PSN_APPLY —— Apply 按钮被按下
· PSN_WIXZBACK —— 在向导模式下,用户按下了 Previous 按钮
· PSN_WIZNEXT —— 在向导模式下,用户按下了 Next 按钮
· PSN_WIZFINISH —— 在向导模式下,用户按下了 Finish 按钮
若想响应刚提到的通知,可想属性页控件发送消息。
· PSM_SETTILTLE —— 设置属性页的标题
· PSM_ADDPAGE —— 在属性页中增加一个单页。消息的 lParam参数是一个指向已创建的属性单页(由CreatePropertySheetPage()创建)的句柄
· PSM_REMOVEPAGE —— 在属性页中删除一个属性单页。
CPropertySheet 内幕
· CommonConstruct() —— 不同的CPropertySheet构造函数接受的参数传递给 CommonConstruct()。 首先设置了m_psh所指向的 PSH_PROPSHEETPAGE
结构的 dwFlags 域,然后初始化了 m_psh 所指向结构的几个其他域。 并将 m_bStacked 设置为TRUE,m_bModeless设为FALSE。
· EnableStackedTabs() —— 将CPropertySheet设置为“stacked”模式而不是“scrolled”模式。只修改了 m_bStacked 成员变量。
· BuildPropPageArray() —— 负责将 m_pages 数组转化成 PROPSHEETPAGE 结构。
· OnInitDialog() —— 1、若不是出于 向导 模式,将整个属性页传递出去,以免控件绑在一起。
2、根据 m_bStacked 的值修改标签的风格。通过调用 EnableStackedTabs() 完成。
3、若属性页为非模态,也不处于 向导 状态,OnInitDialog()就删除标准按钮。
4、调用 CWnd::CenterWindow(),是属性页出于窗口正中央。
· m_pages —— 一个指针数组,是 CPtrArray 的实例,保存了指向属性页的所有 CPropertyPage 类的指针。
· m_strCaption —— 属性页的说明
· m_pParentWnd —— 指向父窗口的指针
· m_bStacked —— 一个Boolean 变量,记录了用户指定的标签风格是“stacked”还是“scrolled”。
· m_bModeless —— 一个Boolean变量, 记录用户希望属性页是模态还是非模态。
CPropertySheet::DoModal() (DLGPROP.CPP)
首先,调用 AfxDeferRegisterClass(),导致MFC调用 InitCommonControls()函数,该函数初始化了Windows 通用控件 DLL。
然后调用 BuildPropPageArray(),
然后禁用主窗口,强制实现模态化,调用 ::PropertySheet() 来创建并显示公用属性页控件。
再调用 CWnd::RunModalLoop() 在模态化窗口中处理消息泵。完成后,销毁窗口,激活父窗口,然后返回 RunModalLoop() 的返回值。
tag: 直到 DoModal()(对于模态属性页)或Create() (对于非模态属性页)被调用,类都不能映射到一个Windows窗口句柄。
在::PropertySheet() 被调用后,Windows窗口句柄才被创建,并和 MFC CPropertySheet 对象绑定在一起。
CPropertySheet::AddPage()
首先,在内部变量 m_pages(属性单页指针数组)中保持了CPropertyPage指针。
检查 m_hWnd非空后,调用 ::CreatePropertySheetPage(),并发送 PSM_ADDPAGE 消息给属性页控件,从而让它将创建的属性单页加入。
CPropertySheet::BuildPropPageArray()
首先,删除了 m_psh.ppsp 域所指向的数组,并将它设置为NULL,为新数组保留控件,新的数组来自内部 CPtrArray 数组 m_pages。
然后,为本地变量 ppsp分配了一个新数组(数组基类型是 PROPSHEETPAGES),并将 m_psh.ppsp 指向同一块内存。
遍历 m_pages数组中的所有 CPropertyPage。对于每个夜,它将CPropertyPage 的内部 PROPSHEETPAGE 结构拷贝到 PROPSHEETHEADER 数组
ppsp中。拷贝结束后,调用_ChangePropPageFont() 来修改所有控件的字体,参数是一个指向对话框模版的指针,这样控件和属性页使用相同的字体。
所有页都放入了 m_psh.ppsp,就更新 m_psh.nPages 这个单页计数,以便和 m_pages 的大小相匹配。
CProperSheet 通知
CPropertySheet 将几个属性页控件通知映射为可覆盖的回调函数
================================================================
MFC控件类
· AFXWIN2.INL —— 所有控件类的内嵌函数
· WINCTRL1.CPP —— CStatic 、 CButton 、 CListBox 、CComboBox、CEdit和 CScrollBar
· WINCTRL2.CPP —— CDragListBox。
· WINCTRL3.CPP —— CCheckListBox
· WINBTN.CPP —— CBitmapButton
posted @
2010-03-15 23:09 Euan 阅读(3742) |
评论 (0) |
编辑 收藏
2009-9-2
===========================
《深入解析MFC》笔记 4. CObject
===========================
运行时类的信息
CObject的RTCI(run-time class information,运行时类的信息)特性使开发人员在运行时可以确定关于类的信息
DECLARE_DYNAMIC / IMPLEMENT_DYNAMIC
DECLARE_DYNCREATE / IMPLEMENT_DYNCREATE
DECLARE_SERIAL / IMPLEMENT_SERIAL
将这些宏加入到CObject的派生类中,就可以调用IsKindOf()来测试这个类的类型,IsKindOf有一个参数,该参数有另一个宏RUNTIME_CLASS创建。
--------------------------------------------------------------------------
RTCI如何运作 《深入解析MFC》 P114
#define _DECLARE_DYNAMIC(class_name) \
public: \
static CRuntimeClass class##class_name; \ //创建一个静态成员变量
virtual CRuntimeClass* GetRuntimeClass() const; \ //返回正确的运行时类信息
“##”告诉预处理器将操作符右侧的部分和左侧的部分连接在一起。
#define IMPLEMENT_DYNAMIC(class_name, base_class_name) \
IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, 0xFFFF, NULL, NULL)
#define _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \
class_name::CreateObject, &_init_##class_name) \
AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); \
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \
{ pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \
return ar; }
"#"字符串化的宏,将右侧的部分转化成用引号包括的字符串。
1. 初始化了静态CRuntimeClass数据成员变量 class##class_name
2. 创建了一个静态AFX_CLASSINIT结构。
struct AFX_CLASSINIT{ AFX_CLASSINIT ( CRuntimeClass* pNewClass ); };
3. IMPLEMENT_DYNAMIC宏会生成这个覆盖成员函数GetRuntimeClass()。
GetRuntimeClass90会返回静态CRuntimeClass成员变量class##class_name的地址
--------------------------------------------------------------------------
CRuntimeClass类
· LPCSTR m_lpszClassName —— 类的名字。在序列化过程中,作为类状态的一部分被写入读出。
· UINT m_wSchema —— 也是累的一个状态。 给出类的版本信息
· void Store( CArchive& ar) const —— CArchive::WriteClass()调用这个函数来写出关于类的CRuntimeClass结构信息
· static CRuntimeClass* Load ( CArchive& ar, UINT* pwSchemaNum); —— CArchive::ReadClass() 调用Load()来读入由Store()写入的CRuntimeClass信息。
· int m_nObjectSize;
· CRuntimeClass* m_pNextClass; ——维护一个已创建的对象的简单链表
· m_pBaseClass —— 保存一个指向基类的CRuntimeClass结构的指针。IsKindOf和IsDerivedFrom()用它来确定对象的“多态类型".
· m_pfnCreateObject —— 用于动态创建。DECLARE/IMPLEMENT_DYNCREATE 宏会将这个成员函数的指针指向CMyClass::CreateObject()成员函数。
调用CRuntimeClass::CreateObject()时,它会通过m_pfnCreateObject调用CMyClass::CreateObject()来返回一个新的CMyClass。
--------------------------------------
#define RUNTIME_CLASS(class_name) (&class_name::class##class_name)
RUNTIME_CLASS宏辉返回静态CRuntimeClass成员数据,DECLARE_DYNAMIC会将CRuntimeClass成员变量放置在你的类中(类似于GetRuntimeClass())
--------------------------------------------------------------------------
动态创建
在CObject派生类中增加对动态创建的支持,要使用DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏,这些宏在DECLARE_DYNAMIC 宏和
IMPLEMENT_DYNAMIC 宏的基础上创建,不用在类中同时使用这两种宏。
加入这些宏后,调用CRuntimeClass 的成员函数 CreateObject() 创建对象
CRuntimeClass::CreateObject()
首先进行检查,确定用户是否指定了正确的宏,然后调用 CRuntimeClass::m_pfnCreateObject 成员变量指向的函数
--------------------------------------------------------------------------
MFC中的持续性(serialization) 《深入解析MFC》 P119
持续性指存储对象,并在之后的某个时刻恢复对象的能力。
CArchive
· m_nMode —— 制订了文档是否在读和写。还可以用它在删除的时候关闭刷新
· IsLoading()、IsStoring()、IsByteSwapping() 和 IsBufferEmpty() —— 用来确定CArchive对象的状态
· 操作符 —— CArchive为 CObject派生类、Windows数据类型和C++类型定义了插入和提取操作符。
· Map成员 —— 进行写操作时,CArchive维护了一个映射表,以便能迅速访问到类似对象的类信息。还利用这个映射表来确保对一个特定的类只写一次
CRuntimeClass信息,然后在序列化流中引用这个信息。读操作时,CArchive会维护已经创建的对象的数组,并在数组中存储那些已经读出的
CRuntimeClass结构信息。这样,每当CArchive查找一个写入到序列化流的引用时,都可以查找该数组。
· 类成员函数 —— WriteClass()、ReadClass() 和 SerializeClass()等成员函数用来序列化CRuntimeClass结构信息。
CArchive 操作符实现
_AFX_INLINE CArchive& CArchive::operator<<(WORD w){
if ( m_lpBufCur + sizeof( WORD ) > m_lpBufMax )
Flush();
*(WORD* ) m_lpBufCur = w;
m_lpBufCur += sizeof(WORD);
return *this;
}
_AFX_INLINE CArchive& CArchive::operator>>(WORD& w){
if( m_lpBufCur + sizeof( WORD ) > m_lpBufMax)
FillBuffer( sizeof(WORD) - (UINT) ( m_lpBufMax - m_lpBufCur) );
w = *( WORD* ) m_lpBufCur;
m_lpBufCur += sizeof( WORD);
return *this;
}
用户选择文档要写入的文件的名字:
CDocument::OnSaveDocument() //以存储模式创建了一个CArchive对象,然后将它贴到由对话框(pFile)打开的文件后面。然后,OnSaveDocument() 调用文 [doccore.cpp] 档的Serialize() 方法,最后关闭文档
CArchive::CArchive() //2-5,检查文档是否是通过IsStoring() 方法保存,为写操作调用插入符操作
CMyDocument::Serialize() //
CArchive::IsStoring()
operator<<(CArchive&, CObject* )
CArchive::WriteObject() //6-8, 插入操作符调用CArchive::WriteObject() 函数,
CArchive::WriteClass() //该函数又调用WriteClass()
CMyClass::Serialize() //,然后序列化CMyDocument对象
CArchive::IsStoring()
CArchive::operator<<(WORD)
CArchive::operator<<(DWORD)
operator<<(CArchive& , point)
CArchive::Write()
CArchive::Close()
对同一对象读操作:
CDocument::OnOpenDocument()
[doccore.cpp]
CArchive::CArchive()
CMyDocument::Serialize() //
CArchive::IsStoring() (FALSE)
operator >> (CArchive&, CMyClass* )
CArchive::ReadObject()
CMyClass::Serialize()
CArchive::IsStoring()
CArchive::operator>>(WORD)
CArchive::operator>>(DWORD)
operator>>(CArchive& , point)
CArchive::Read()
CArchive::Close()
若数据大于16KB,SetLoadParams() 和SetStoreParams() 可帮助提高序列化性能。
Serializable必要条件 《深入浅出MFC》P394
1、DECLARE_SERIAL 、IMPLEMENT_SERIAL。
2、派生自 CObject,改写 Serialize虚函数
3、加上一个default构造函数
--------------------------------------------------------------------------
CObject对诊断的支持
TRACE宏
类似于printf的输出。
(调用了全局函数::AfxTrace()(DUMPOUT.CPP))《深入解析MFC》P133-136
对象转储
Dump() 函数,调用打印类的状态。
(OBJCORE.CPP,CObject::Dump() 只是转出了类名(存储在CRuntimeClass类中)和this指针的地址)
运行时检查
断言
ASSERT宏
AFX.H
#define ASSERT(f)\
do\
{\
if(!(f)&AfxAssertFailedLine(THIS_FILE, __LINE__))\
AfxDebugBreak();\
}while(0)\
对象合法性检查
AssertValid() (CBJCORE.CPP){ASSERT(this!=NULL);}
#define ASSERT_VALID(pOb) (::AfxAssertValidObject(pOb, THIS_FILE, __LINE__)
内存诊断
检测内存泄露
使用CMemoryState 类的 Checkpoint()将要检测的区域包装起来,来检测内存泄露的状况。
然后使用Difference()来比较两个断点,查看两个调用Checkpoint()之间之间是否有泄漏。
CMemoryState记录的每个已分配的内存块是以下几个之一
· freeBlock —— 由于使用 delayFreeMemDF 而导致释放被延迟的块
· objectBlock —— 保留的或者已经泄漏的 CObject 对象的数量。
· bitBlock —— 已分配内存的非 CObject 对象的个数。
· crtBlock —— 已分配的 C 运行时块的个数。
· ignoredBlock —— MFC检查所忽略的块的个数。
CMemoryState将分配的次数保存在m_lCounts数组中,已分配的各种类型的大小存放在m_lSizes成员数组中。
m_lHighWaterCount 成员函数存储了最大分配数,
m_lTotalCount 记录了分配的总次数。
m_memState 和 m_pBlockHeader 成员变量都指向了一个内存块链表的头节点。
《深入解析MFC》P140-143
检测最大内存使用
通过在afxMemDF全局变量中郑家按位或(OR)的枚举值delayFreeMemDF,可以停止使用内存释放函数。
如 afxMemDF |= delayFreeMemDF; 统计程序使用了多大内存。
检查内存
若想检测所有的内存分配情况和释放情况,只要与checkAlwaysMemDF的值进行或操作。
如 afxMemDF |= checkAlwaysMemDF.
框架就会在分配和释放内存是检测内存。
内存统计
DumpStatistics()
转储所有对象
DumpAllObjectsSince(),转储所有泄露对象的行、地址、大小。
AFX辅助函数
AfxDoForAllObjects() 和 AfxDoForAllClasses()
AfxDoForAllObjects()
两个参数,一个纸箱一个函数的指针,另一个是void* 指针。
遍历内存对象链表,将他们作为CObject指针传递给用户提供的函数
AfxDoForAllClasses()
只遍历CRuntimeClass结构
posted @
2010-03-15 23:05 Euan 阅读(1051) |
评论 (0) |
编辑 收藏
2009-9-2
===========================
《深入解析MFC》笔记 3. MFC实用类
===========================
类CString
· Find() —— 查找子串或字符。
· FindOneOf( char * ) —— 查找参数字符串中出现的第一个字符
· ReverseFind() —— 从字符串的右端开始向左查找。
· Format() —— 参数类同printf。
char name[20] = "Bob"; int age = 21;
printf( "Hello %s, you are %d years old\n", name, age );
Hello Bob, you are 21 years old
%s 表示, "在这里插入首个参数,一个字符串." %d 表示第二个参数(一个整数)应该放置在那里. 不同的"%-codes"表示不同的变量类型, 也可以限制变量的长度.
%c 字符 %d 带符号整数 %i 带符号整数 %e 科学计数法, 使用小写"e"
%E 科学计数法, 使用大写"E" %f 浮点数 %g 使用%e或%f中较短的一个
%G 使用%E或%f中较短的一个 %o 八进制 %s 一串字符
%u 无符号整数 %x 无符号十六进制数, 用小写字母 %X 无符号十六进制数, 用大写字母
%p 一个指针 %n 参数应该是一个指向一个整数的指针
指向的是字符数放置的位置
一个位于一个%和格式化命令间的整数担当着一个最小字段宽度说明符,并且加上足够多的空格或0使输出足够长.
如果你想填充0,在最小字段宽度说明符前放置0. 你可以使用一个精度修饰符,它可以根据使用的格式代码而有不同的含义.
用%e, %E和 %f,精度修饰符让你指定想要的小数位数. 例如,
%12.6f
将会至少显示12位数字,并带有6位小数的浮点数.
用%g和 %G, 精度修饰符决定显示的有效数的位数最大值.
用%s,精度修饰符简单的表示一个最大的最大长度, 以补充句点前的最小字段长度.
一些简单类:
值的类型 结构 源文件
CPoint POINT(struct tagPoint) afxwin1.inl
CRect RECT(struct tagRECT) afxwin1.inl, wingdix.cpp
CSize SIZE(struct tagSIZE) afxwin1.inl
CTime time_t operations afx.inl, timecore.cpp
CTimeSpan time_t math afx.inl, timecore.cpp
MFC集合类
数组array、 链表list 、 映射表map
C*Array *= Byte /DWord /Uint(unsigned int) /Ob(CObject) /String /Word
C*List *= Ob(OCObject* ) /Ptr(void*) / String
CMap* *=PtrToPtr /StringToOb /StringToPtr /StringToString /WordToOb /WordToPtr
CFile家族: MFC对文件的访问
CFile
· AfxFullPath —— 将一个文件路径转化成绝对路径。
· AfxGetRoot —— 解析一个UNC(Uniform Nameing Convention)路径或一个旧式路径,得到卷标名。
· AfxComparePath —— 比较两个路径是否一样。
· AfxGetFileTitle —— 从路径中解析出文件名。
CStdio File (支持文件缓冲)
ReadString() WriteString()
CMemFile
CException
· CArchiveException —— 序列化异常。
· CDaoException —— DAO(数据访问对象)异常。
· CDBException —— 数据库异常
· CFileException —— 文件异常
· CMemoryException —— 内存异常
· CNotSupportedException —— 某些内容不支持
· COleDispatchException —— OLE分发(自动化)异常
· COleException —— OLE异常
· CResourceException —— Windows资源问题
· CUserException —— 用户产生的异常
GetErrorMessage() 返回一个缓冲区,缓冲区内存存放着描述异常的字符串;
ReportError() 用一个Windows消息框显示异常信息字符串
posted @
2010-03-15 23:04 Euan 阅读(1165) |
评论 (0) |
编辑 收藏
2009-9-2
===========================
《深入解析MFC》笔记 2. MFC基础
===========================
基本的MFC应用程序组件:
CWinApp:应用程序对象
· 保存了一些传递给WinMain()的命令行参数,包括当前实例的句柄(m_hInstance)、前一个实例的句柄(m_hPrevInstance)、命令行参数(m_lpCmdLine)以及显示窗口标志(m_nCmdShow)。
· CWinApp在m_pszAppName中保存了应用程序名字的拷贝。
· m_pszExeName(指向可执行文件名字的指针)、m_pszHelpFilePath(指向应用程序帮助文件路径的指针)、以及指向应用程序配置文件(profile)名字的指针
· CCommandLineInfo结构用来保存命令行参数
· MFC中,实例的初始化有CWinApp::InitInstance()完成。
· 关闭程序和清除资源由ExitInstance()完成。
· 调用CWinApp::Run()会启动标准的GetMessage()...DispatchMessage()循环
· 每当消息队列为空时,CWinApp::Run()会调用OnIdle()。
CWnd:窗口基类
包装Windows API
m_hWnd成员变量表示API级的窗口句柄(HWND)。
将窗口句柄转化成窗口对象
CHandleMap:没有文档说明的窗口句柄映射表类
将窗口句柄映射成MFC的Windows对象。
CMapPtrToPtr m_permanentMap: 表示永久映射表,保存程序运行过程中句柄/对象映射表。
每当创建一个CWnd的派生类,MFC都会再永久目录下插入一条映射记录。调用CWnd::OnNcDestroy()时就会从永久目录下删除一条映射记录。
CMapPtrToPtr m_temporaryMap:表示临时映射表,仅在消息存在的过程中存在。
除CWnd派生对象与HWND之间的映射外,还有4个从MFC类到本地窗口句柄的映射。
AFX_MODULE_THREAD_STATE结构
· m_pmapHWND: 窗口句柄与CWnd对象之间的映射表
· m_pmapHMENU: 菜单句柄到CMenu对象的映射表。
· m_pmapHDC: 设备环境句柄到CDC对象的映射表。
· m_pmapHGDIOBJ: GDI对象句柄到CGDI对象的映射表。
· m_pmapHIMAGELIST: 图象链表句柄到CImageList对象的映射表
每个参与了句柄映射方案的MFC类都包括一个FromHandle函数,该函数将CHandleMap::FromHandle()包装了起来,CHandleMap::FromHandle()完成了查找,并将一个本地句柄与一个C++对象相关联。CWnd::FromHandle()会返回一个适当类型的对象。
关联窗口句柄和分离窗口句柄
CWnd::Attach() 将CWnd::m_hWnd赋值为已有的窗口句柄,并将这队关系存放在MFC的永久窗口句柄映射表中。
CWnd::Detach() 将窗口关系和CWnd派生对象之间的关系从窗口句柄映射表汇总删除,将CWnd::m_hWnd的值设为NULL.
MFC状态信息
AFX_MODULE_STATE:没有文档说明的状态信息
· m_pCurrentWinApp—— 一个纸箱CWinApp的指针
· m_hCurrentInstanceHandle —— 该模块的实例句柄
· m_hCurrentResourceHandel —— 代表保存了模块资源的实例句柄
· m_lpszCurrentAppName —— 指向应用程序名称的指针
· m_bDLL —— 表示模块是一个动态链接库还是一个可执行文件
· m_classList—— 指向应用程序的CRuntimeClass结构链表中的第一个运行时类的指针。
· m_factoryList —— 纸箱应用程序的 COleObjectFactory结构链表中的第一个运行时类的指针
· m_nObjectCount—— OLE服务器的引用计数,表示服务器是否有未完成的COM对象
· m_bUserCtrl —— 表示拥护是否在使用OLE服务器的标志。
· m_szUnregisterList[4096]——维护一个注册了的窗口类的链表,以便结束时的注销
· m_pfnAfxWndProc —— 指向MFC的标准窗口过程
· m_fRegisteredClasses——表示哪些MFC窗口类已经注册了。
多数成员可以通过类似AfxGetInstanceHandle()来访问。
MFC对GDI的支持
CDC类内部
HDC成员变量,表示一个实际设备环境的句柄。
m_hAttribDC,
CDC析构函数调用DeleteDC()。
CDC派生类
· CPaintDC —— CPaintDC的构造函数需要一个指向CWnd的指针,他会调用BeginPaint()而创建一个设备环境,然后写入设备环境映射表。CPaintDC的析构函数是EndPaint()。
· CWindowDC —— 表示窗口内的整个屏幕区域(包括用户区和框)。CWindowDC的构造函数需要一个指向CWnd的指针,调用GetWindowDC()来为整个窗口创建一个设备环境。CWindowDC的析构函数是ReleaseDC()。
· CClientDC——用户区。构造函数也需要一个指向CWnd的指针。GetClientDC()、ReleaseDC().
· CMetaFileDC —— Windows的元文件有一个GDI命令序列,利用这个命令序列可以重新画出图像。创建一个元文件DC:构造CMetaFileDC(在栈中分配一个或声明一个),然后调用CMetaFile::Create()来创建一个元文件并初始化CMetaFile的成员变量,析构函数:DeleteMetaFile()。
图形对象
Device context 保存了在Windows绘画所需要的所有信息,包括映射模式等等。还保存了关于在设备上绘画所使用的工具的信息。
CGDIObject
所有GDI对象的基类。m_hObject是GDI对象的句柄。
CPen
CBrush
CFont
CBitmap
Cpalette
CRgn
posted @
2010-03-15 23:00 Euan 阅读(740) |
评论 (0) |
编辑 收藏
2009-9-1
=======================
《深入解析MFC》笔记 1. 概念总结
=======================
缩写:
API: Application Programming Interface
DLL: Dynamic Link Library
GUI: Graphics User Interface
MDI: Multiple Document Interface
MFC: Microsoft Function Class
OLE: Object Linking&Embedded
OWL: Object Windows Library
SDK: Software Development Kit
SDI: Single Document Interface
UI: User Interface
WinApp: Windows Application
control 控件
notification 通知信息(发生于控件)
preemptive 强制性、抢占式、优先级
process 进程
Constructor Destructor Encapsulation封装 Inheritance继承 Polymorphism多态
数据类型:
BSTR 32-bit 字符指针
BYTE 8-bit 整数,未带正负号
COLORREF 32-bit 数值,代表一个颜色值
DWORD 32-bit 整数,未带正负号
LONG 32-bit 整数,带正负号
LPARAM 32-bit 数值,作为窗口函数或 callback 函数的一个参数。
LPCSTR 32-bit 指针,指向一个常数字符串
LPSTR 32-bit 指针,指向一个字符串
LPCTSTR 32-bit 指针,指向一个常数字符串。此字符串可移植到 Unicode 和DBCS(双字节字集)
LPTSTR 32-bit 指针,指向一个字符串。此字符串可移植到Unicode和DBCS
LPVOID 32-bit 指针,指向一个未指定类型的数据
LPRESULT 32-bit 数值,作为窗口函数或者 callback 函数的返回值
WNDPROC 32-bit 指针,指向一个窗口函数
WORD 16-bit 整数,未带正负号。
WPARAM 窗口函数的callback函数的一个参数,win32中是32bits
WINAPI类
任何从CCmdTarget派生的类都有一个与之相关联的消息映射表,它将命令传递给从CCmdTarget派生的类。
CCmdUI类提供了更新用户界面对象(如菜单或复选框控件)状态的函数。在单击菜单之后以及菜单项显示之前,MFC会给应用程序中的命令目标发送一个命令更新消息。如果在命令目标对象的消息映射表中有这个更新消息的内容,MFC会给CCmdUI对象传递一个代表菜单项的指针,也就是命令目标对象所更新的内容。
CWinThread代表在MFC程序内执行的线程。
同步对象类
CSyncObject ——同步对象类的基类。
CCriticalSection——一个同步类,它只允许单个进程中的一个线程访问一个对象。
CSemaphore——一个同步类,它只允许一个对象有一个到某个指定的之间个数的同步访问。
CMutex——一个同步类,它只允许任何数目进程中的一个线程访问对象。
CEvent——一个同步类,当某个时间发生时,它会通知某个应用程序。
CSingleLock——线程安全的类的成员函数中用来锁住一个同步对象的对象。
CMultiLock——线程安全的类的成员函数中用来锁住一个或更多个同步对象的对象,锁住的对象来自一个同步对象数组。
框架窗口
CFrameWnd就是SDI应用程序的主窗口的基类。
CMDIFrameWnd为MDI应用程序提供了主框架窗口,CMDIChildWnd为MDI应用程序提供了子窗口。
对话框
CFileDialog——从某个目录下选定一个文件
CColorDialog——选择一个指定的颜色
CFontDialog——选择一种字体。
CPrintDialog——处理打印机的安装和打印
CFindReplaceDialog——为查找和替换选择文本
对话框数据的交换和验证(DDX/DDV)通过CDataExchange类实现。
属性页:CPropertySheet 和 CPropertyPage
CAnimateCtrl——播放动画控件
CDragListBox——CListBox的派生类,你可以在这个列表框中拖动和去掉选项。
CHeaderCtrl——和CListCtrl一起来显示柱状信息
CHotKeyCtrl——为从用户获得键序列提供接口(Alt-Backspace-Delete).
CImageList——一个CObject的派生类,它为你维护图像集合。
CListCtrl——显示一个链表项的图形链表(类似Explorer)。
CProgressCtrl——显示一个进度条
CRichEditCtrl——一个丰富的编辑控件,它理解一些RTF格式的概念,而且允许使用多字体,多颜色等。
CSliderCtrl——一个在某个值范围内进行选择的滚动条。
CSpinButtonCtrl——微调控制项。
CStatusBarCtrl——状态栏
CTabCtrl——属性页控件
CToolBarCtrl——实现一个工具栏
CToolTipCtrl——提供工具提示
CTreeCtrl——一个类似Explorer的树控件
GDI支持和绘画对象
CDC类表示设备环境。
· CPaintDC——封装了处理WM_PAINT消息时所要使用的BeginPaint()和EndPaint()两个调用。
· CWindowDC——封装了与整个窗口相关的设备环境
· CClientDC——封装了与窗口中客户区有关的设备环境
· CMetaFileDC——为元文件(metafile)封装了设备环境。
· CFont、CPen、CBrush、CBitmap、CPalette和CRgn均从CGdiObject类中派生。
应用程序框架类
文档视图结构
· CDocTemplage、CSingleDocTemplate和CMultiDocTemplage——文档模板是将文档和其视图粘合在一起的粘合剂
· CDocument——处理应用程序汇总数据的类。
· CView——代表在屏幕上看到的窗口的客户区。
控件视图
· CEditView
· CListView
· CRichEditView
· CTreeView
分割窗口(Splitter Window)
两种分割窗口:静态、动态
静态:预定义窗格数目,数目和排列不能修改,每个窗格可以显示不同类型的视图。
动态:每个窗格必须显示同一类型的视图。
操作系统扩展
OLE支持:OLE文档
创建一个支持OLE复合文档的文档时需要使用的类。
· CDocItem——MFC的COleClientItem和COleServerItem类的基类。
· COleServerItem——表示与嵌入或链接的OLE项的链接的服务器端。
· COleClientItem——表示与嵌入或链接的OLE项的连接的容器(container)端。
· COleDocument——是MFC对复合文档支持的核心。除维护应用程序的本地数据之外,还维护了一个CDocItem对象链表
· COleLinkingDoc——包含一些链接,这些链接指向嵌入在其他地方的项。
· COleServerDoc——由符合文档中合体的服务端应用程序使用。
· COleIPFrameWnd——为成为复合文档服务器,应用程序有两种不同的框架窗口 1、通常的框架窗口 2、应用程序在恰当位置显示时所使用的框架窗口(用户调用一个复合文档内部的可视化编辑操作时)。COleIPFrameWnd封装了符合文档服务器的部分功能。
OLE支持:类厂(class factory)
每个要对外暴露借口的OLE对象都要有一个类厂。类厂位于OLE服务器中,会创建一个OLE对象的实例来代表服务器。
· COleObjectFactory——为需要类厂,但又不是面向文档的MFC应用程序实现类厂。
· COleTemplateServer——从COleObjectFactory直接派生出来的类,为面向文档的、能使用OLE的MFC应用程序实现类厂。
OLE支持:自动化
OLE支持:统一数据传输
OLE数据传输由任何实现了IDataObject接口的对象完成。
· COleDataSource——完成初始化,可以用于剪贴板(clip-board)传输,也可以用于拖放(drag-and-drop)传输。
· COleDataObject——数据传输的另一端,目的地,通常使用COleDataObject表示。
· COleDropSource——定制“拖放”操作时有用。
· COleDropTarget——每当创建一个接受拖放数据的窗口感兴趣时,
OLE支持:OLE控件
· COleControl——从CWnd派生,是OLE控件的基类。
· COlePropertyPage——从Dialog派生,用于修改控件的属性。
· COleControlModule——从CWinApp派生,是保持OLE空间的动态链接库(dynamic link library)的基类。负责执行初始化和OLE控件特有的各种任务。
· COleObjectFactoryEx——扩展了COleClassFactory。
· COleConnectionPoint——从CCmdTarget派生,代表到其他OLE对象的输出接口,用于事件触发和向容器发出修改通知。
· CPropExchange——同用于标准的DDX/DDV的CDataExchange类似,为属性交换建立环境,并在控件与容器之间帮助交换属性。
· CFontHolder——封装了Windows的字体类。实现了OLE的IFont接口,用于Font的常备属性 。
· CPictureHolder——实现了“图像属性”。以多态的方式封装了一个位图、图标或元文件。
ODBC支持
· CDatabase——封装了对数据源的连接诶,通过它可以对数据源进行操作。
· CRecordset——封装从数据源中选出的记录。记录集允许从记录到记录的滚动,更新记录,使用过滤器选择记录,排序。
· CFieldExchange——提供环境信息来支持RFX(Record Field Exchange,记录域交换)。RFX会再记录集对象的域数据成员以及参数数据成员与数据源的响应的链表之间交换数据。
· CLongBinary——封装了一个句柄,以便存储大的二进制对象。主要用于管理存储于数据库表中的大数据对象。
· CRecordView——提供一个连接到记录集对象的窗体视图。DDX机制负责在记录集合记录视图的控件之间交换数据。
DAO支持(Data Access Object,数据访问对象)
· CDaoWorkspace——管理命名的、有密码保护的数据库会话。
· CDaoDatabase——连接到某个数据库上,可通过它访问数据库。
· CDaoRecordset——从数据源中选出的记录集。
· CDaoRecordView——在控件中显示数据库记录。
· CDaoQueryDef——一个查询定义,通常存放在数据库里。
· CDaoTableDef——一个基表(base table)或附加表(attached table)的存储定义。
· CDaoException——DAO类产生的异常情况。
· CDaoFieldExchange——支持由DAO数据库类使用的DAO记录域交换例程。
posted @
2010-03-15 22:59 Euan 阅读(1207) |
评论 (0) |
编辑 收藏
========================
Effective C++ 继承与面向对象设计
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 32 : public 继承意味着 is-a 关系
--------------------------------------------------
tag: public inheritance 公有继承 is-a
每一个类型为 Derived 的对象同时也是一个类型为 Base 的对象,反之不成立。
实际情况中很多“是一个”的体现并不那么纯粹:大师说“鸟”都会飞,但是实际上是有不会飞的“鸟”的。
在公有继承中,有两种办法来解决这种“不纯粹”:
- 多重继承。对于“鸟”的例子,设计一个“鸟”类,然后从中派生出一个“不会飞的鸟”类和一个“会飞的鸟”类,然后再在它们之中分别派生其他具体的“鸟”们。
- 允许运行时出错。还是“鸟”的例子,对于每一个“鸟”类的派生类,不管它是不是能飞,都会有个“飞”的函数。不同的是,能飞的“鸟”就直接飞了,不能飞的“鸟”则会在“飞”函数里说:”对不起,我不能飞,找别人去吧……”(所谓的运行时错误,runtime error)
Item 33 : 避免遮掩继承而来的名称
--------------------------------------------------
tag: scopes
·derived classes 内的名称会遮掩 base classes 内的名称。
·可以使用 Using 声明式或转角函数 (forwarding functions)。
先在本地域中查找(local scope,比如说函数内部)是否有该名字的定义,如果没有找到
往外一层名字域(比如说函数所在的类)中查找,如果没有找到
再往外一层名字域(比如说函数所在类的父类)中查找,如果没有找到
继续忘外一层名字域中查找(比如说函数所在类的父类的父类,等等),一直找到全局名字域(global scope)还是没找到的话,就报告错误。
在“洋葱”的内部某层定义了和外部某层一样名字的函数:使得位于内部的函数“屏蔽”了外部的同名函数(哪怕两个函数拥有不同的参数表)。
第一,在公有继承中,上述的情况是不允许存在的,因为从定义上来说,公有继承中的子类应该具备父类所有的特征和功能,应该“是一个”父类。
第二,如果在上述情况中需要调用/访问被“屏蔽”的函数/对象,有两个方法可以采用:
using。用using“声明”过完整的名字后,就可以“看见”并使用这个函数/对象了。
踢皮球函数(forwarding functions)。编写一个函数,把真正的活踢给别人……
两种方法示例见下,Derived_0是有“屏蔽”问题的类,Derived_1和Derived_2分别是采用了第一种和第二种方法的类。
class Base {
public :
virtual void func_1();
virtual void func_1( int param);
}
class Derived_0: public Base {
public :
virtual void func_1();
}
class Derived_1: public Base {
public :
using Base::func_1;
virtual void func_1();
}
class Derived_2: private Base {
public :
virtual void func_1();
virtual void func_1( int param)
{ Base::func_1(param);}
}
Item 34 : 区分接口继承和实现继承
--------------------------------------------------
tag: function interfaces, function implementations.
·声明一个 pure virtual 函数以让 derived classes 只继承函数接口
·声明 普通virtual 函数以让 derived classes 继承该函数的接口和缺省实现
·声明 non-virtual 函数以令 derived classes 继承函数的接口及一份强制性实现。
class AClass {
public :
virtual void interface_1() = 0 ;
virtual void interface_2()
{ /* the default implementation..*/ }
void interface_3()
{ /* the compulsory implementation..*/ }
// ..
} ;
class AClassDerived {
public :
virtual void interface_1()
{ /* OK you have to implement this..*/ }
virtual void interface_2()
{ /* you can, but don't have to implement this..*/ }
// void interface_3()
// {you can't implement your own..}
} ;
class AClass {
public :
virtual void interface_1. 5 () = 0 ;
protected :
void default_interface_1. 5 ()
{ /* ..*/ }
} ;
class AClassDerived {
public :
virtual void interface_1. 5 ()
{
// you can either do this
default_interface_1. 5 ();
// or implement in your own way..
}
} ;
Item 35 : 考虑 virtual 函数意外的其他选择
--------------------------------------------------
tag: tr1::function tr1::bind (祥书上实例),
Template Method, Strategy ,
·使用 non-virtual interface(NVI)手法,即 Template Method 设计模式的一种特殊形式。以public non-virtual成员函数包裹较低访问性的virtual函数
·将virtual函数替换为“函数指针成员变量”,即Strategy设计模式的一种分解表现形式
·以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entiry)搭配一个兼容于需求的签名式。也是Strategy设计模式的某种形式。
·将继承体系内的 virtual函数替换为另一个继承体系内的 virtual函数。即Strategy设计模式的传统实现手法。
----------------------------------
一: Non-Virtual Interface 手法实现 Template Method 模式
:让客户通过 public non-virtual 成员函数间接调用 private virtual 函数,此即NVI手法,Template Method 设计模式的一种表现形式。
这个 public non-virtual 函数成为 virtual 函数的外覆器(wrapper)。
优点在于可在 wrapper 中做一些事前工作和事后工作。
NVI 手法中virtual function 不一定得是 private.
----------------------------------
二: Function Pointers 实现 Strategy 模式
:这种方法的实质,就是把接口函数的实现拿到了类之外。类之中只声明接口的形式,只定义一个函数指针。真正干活的函数(实现)都不是类的成员。
这样做带来了一定的灵活性,具体采用哪种实现与类的继承关系是独立无关联的;同时,非类成员函数也有局限性:无法访问类的非公有成员。如果把函数定义为友元或利用公有函数输出私有成员,又会破坏原设计的 封装。如下代码所示:
class AClass
{
public :
typedef void *(Interface)( /* param.. */ );
explicit AClass( Interface pint = defaultInterface) : pInterface(pint)
{}
private :
Interface pInterface;
} ;
在构造AClass对象的时候即可指定Interface的真身,虽然,它无法直接访问AClass的非公有成员。
指针在C++里简单一些,更推崇用对象(如智能指针tr1)来管理接口函数。(是不是想到item13?:))
原理和函数指针是一样的,只不过因为用了对象来管理资源,使得应用更加灵活。当然,要付出更多一点的代码体积和运行时间代价。
class AClass
{
// all are the same with the funtion pointer version
// except for:
typedef std::tr1::function < void ( /* param.. */ ) > Interface;
} ;
---------------------------------
三: 古典策略模式实现,也是我觉得比较漂亮且容易理解的实现方式。
用两个类搞定:
class AInterface
{
public:
virtual void DoInterface(/* param.. */);
};
AInterface defaultInterface;
class AClass
{
public:
explicit AClass(AInterface * pinter = &defaultInterface) : pInter(pinter)
{}
void TryInterface()
{
pInter->DoInterface();
}
private:
pInterface * pInter;
};
Item 36 :绝不重新定义继承而来的non-virtual函数
--------------------------------------------------
tag:
任何情况下都不该重新定义一个继承而来的non-virtual函数,
Item 37 :绝不重新定义继承而来的 缺省参数值
--------------------------------------------------
tag: 静态类型(static type) 动态绑定 静态绑定 前期绑定 后期绑定 bound bind
virtual 函数为动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically bound)。
静态类型 ( static type ):
在程序中被声明时所采用的类型。
Shape* ps; //静态类型为 Shape*, ps没有动态类型,因为为指向任何对象。
Shape* pc = new Circle; //静态类型为 Shape*, pc的动态类型为 Circle*
Shape* pr = new Rectangle; //静态类型为 Shape*, pr的动态类型为 Rectangle*
动态类型 ( dynamic type ) :
目前所指对象的类型,
virtual 函数系动态绑定而来,调用一个virtual函数时,调用哪一份实现代码,取决于发出调用的那个对象的动态类型。
class AClass
{
public :
virtual void func( int param = 123 )
{
// ..
}
} ;
class AClassDerived : public AClass
{
public :
// problematic overwriting the default parameter..
virtual void func( int param = 456 )
{
// ..
}
} ;
int main()
{
AClass * pA = new AClassDerived;
pA -> func();
}
由于函数默认参数的静态绑定特性,pA->func()执行时param事实上被赋予了123,而非子类中期望的456,虽然接下来执行的是子类的函数实现……
C++考虑到执行效率和复杂性方面的代价,规定了只能是静态绑定的。
解决方式:
可以用非虚函数接口(NVI)来解决这个问题,看代码
class AClass
{
public:
void func(int param = 123)
{
funcImpl(param);
}
private:
virtual void funcImpl( int real_param ) = 0;
//..
};
class AClassDerived : public AClass
{
private:
virtual void funcImpl( int real_param )
{
//do whatever you feel like to do here..
}
//..
};
Item 38 :通过 composition 塑模出 has-a 或 is-implemented-in-terms-of
--------------------------------------------------
tag: has-a composition is-implemented-in-terms-of 、根据list实现set(祥书上例)
·复合(composition)的意义同public继承完全不同。
·在应用域(application domain), 复合意味着 has-a,
在实现域(implementation domain),复合意味着 is-implemented-in-terms-of
composition是类型之间的一种关系,即某种类型的对象内含它种类型的对象。
composition、 layering分层、 containment内含、aggregation聚合、 embedding内嵌
composition意味着has-a 或 is-implemented-in-terms-of。
Item 39 :明智而审慎地使用 private 继承
--------------------------------------------------
tag: private inheritance
Private inheritance意味着 implemented-in-terms-of.只有实现部分被继承,接口部分被略去。
若 D 以 private 形式继承 B, 意思是 D 对象根据 B 对象实现而得。
公有继承中的子类对象是可以被转换为它的父类对象的(“是一个”的关系),而私有继承中这种转换是不成立的。
另外一点,私有继承中父类的所有公有和保护成员(public和protected)到了子类中,都变成了私有成员。
因为上面的特性,私有继承并不满足“是一个”模型的需要。更可怜的是,私有继承并不能代表一种设计思路(公有继承代表了“是一个”的模型设计),而仅仅是“有一个”模型的一种实现手段(私有继承过来的所有成员都是私有的,从这个角度来说它就只是“实现”)。
另一种手段大师在Item38中有提过,就是用类成员的方式来构造,名曰composition。
既然两者都能实现“有一个”模型,那么如何选择呢?能用composition就用composition,必需私有继承的时候方才私有继承。
比如我们有个AClass:class AClass{
public:
virtual void Interface_1(/*..*/);
};
以下为私有继承:class BClass : private AClass{
private:
virtual void Interface_1(/*..*/);
//..
};
而下面的composition可以达到一样甚至更好的效果:class AnotherAClass: public AClass{
public:
virtual void Interface_1(/*..*/);
//..
};
class DClass{
private:
AnotherAClass* a;
//..
};
BClass和DClass都实现了“有一个”,但相比之下还是能分辨出长短:
DClass中的AnotherAClass是私有成员,除了它自己没有人能够访问修改;而私有继承的BClass不能保证其“拥有”的AClass实现部分不会被第三者修改,即使是私有继承来的。(为什么这么说?看下去……)
BClass私有继承了AClass,相当于它“有了一个”AClass可以用,可以玩。AClass中的公有/保护成员都变成了BClass的人,但是在享受使用这些成员的同时,BClass还要承担提供这些成员给别人服务的义务。
ITEM35中曾经提到:虚拟函数机制和公有/私有/保护体制是没有任何关系的。因此在例子中的Interface_1有可能在以下的情况中被替代然后“调用”:
一个CClass公有继承了BClass
CClass定义了自己的Interface_1版本
有一个BClass的指针,指向一个CClass的对象,某个操作中调用了Interface_1(CClass的实现版本)
这时候BClass可能要有意见了:它的作者并没有打算让它的继承者修改BClass版本的Interface_1,但事实是CClass违背了它的意志!
很曲折哈?希望我下次读的时候还能看懂@#¥%
DClass由于只是定义了一个指向AnotherAClass的指针,那么在定义DClass的文件中就不需要include AClass或AnotherAClass的头文件。于是就避免了编译依赖(compilation dependecies)
而BClass因为是继承了AClass,在BClass的文件中就需要加上AClass的头文件,也就不可避免的产生了编译时的依赖。
由此看来,绝大部分情况下,组合方式(composition)是要优于私有继承的。之所以说“绝大部分”,是因为大师说了:
对于EBO(Empty Base Optimization)的情况,私有继承就显现出了它的优势。
所谓EBO就是这样的一种情况,有一种特殊的类,它没有非静态数据成员(non-static data member),也没有虚函数(于是不会需要空间存储虚表)。
所以这样的一种类其实不占用任何空间,不过因为C++不允许0字节的对象存在,而且很多编译器都会添加额外的空间来实现字节对齐,于是这种特殊的类的实际大小应该是1个char对象的大小。
在这种类中,往往会有很多typedef,enum,静态数据成员或者是非虚函数。所以他们还是有价值的。
需要在“有一个”关系中利用这种类的时候,如果采用composition,那么根据上面的结论,就需要付出额外的空间来“存放”这个本来不占空间的类。
然而如果是私有继承呢,就可以避免这种情况。
Item 40 :明智而审慎地使用多重继承
--------------------------------------------------
tag: Multiple Inheritance, MI
MI 的优与劣。
MI 的第一个问题就是名字冲突, 最经典的例子就是钻石问题 (diamond problem)。
设想 A 中有一个函数叫做 GetName(), B 和 C 中都将有这一函数成员,这个时候 D::GetName() 的真正实现是来自 B 的还是 C 的呢?二义性出现了 (ambiguity) 。
不过如果真的发生了这种情况,要解决的方法也不是没有,可以这样做:
D d;
d.B::GetName(); //Calling B's implementation
另外一个高阶一点的方法叫做虚继承 (virtual inheritance) 。对于在虚拟继承中的父类,其中的成员都保证不会在后面的子类中出现二义现象 (ambiguity) 。
class A
{
public:
void GetName();
};
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }
D d;
d.GetName(); //there is no ambiguity here.
但是虚继承不是没有代价的,大师说这种技术会使得最终代码变得更大,访问虚拟继承中的父类成员也会变得更慢一些。
这个也不难理解。和空间换时间一样,和不给牛吃草牛就不干活一样。 ( 另外的一个代价我还没能完全理解透彻:书上说因为虚继承中基类的初始化是由继承关系中最底层的子类负责的,因此对于这些最底下的 “ 嫡孙 ” 类来说,就不是那么方便了 )
于是大师建议只有在必要的时候才使用虚继承,而在虚继承中的基类里也不要放置数据成员,这样就不用担心初始化的问题了。
不过存在就是合理,还是有需要用到 MI 的时候。一个在书中提到的使用 MI 的情形是:当需要从一个类 AClass 中继承接口,又需要从另外一个类 BClass 中继承实现细节时,就可以考虑在公有继承 AClass 的同时又私有继承 BClass 。道理大致就是这样,就不编造程序画蛇添足了。
总结一下: MI 比 SI(Single Inheritance) 要复杂容易出错 ( 比如说钻石问题 ) ,即使可以用虚继承来解决钻石问题,但是其带来的代码体积增大,访问效率下降以及初始化问题还是不能忽视的。最后话说回来,需要用到 MI 的时候,小心点用便是
posted @
2010-03-15 22:54 Euan 阅读(401) |
评论 (0) |
编辑 收藏
========================
Effective C++ 实现
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 26 : 尽可能延后变量定义式的出现时间
--------------------------------------------------
tag:
·尽可能延后变量定义式的出现,以增加程序的清晰度并改善程序效率。
定义变量包含了该变量对象的构造操作,如果因为某个原因(如抛出异常,条件语句未执行等)而没有真正用到这个变量,那么构造该变量所耗费的时间和资源就白费了。
在即将使用变量前再定义它对理解代码也有好处:要想知道某个变量时做什么用的?读接下来的代码便是。
思考题,以及答案:
//方法A:循环外定义
Widget w;
for (int i = 0; i < n; ++i){
w = some_value_dependent_on_i;
//..
}
//方法B:循环内定义
for (int i = 0; i < n; ++i) {
Widget w(some_value_dependent_on_i);
//..
}
方法A调用了1次构造函数、1次析构函数、n次拷贝函数;
方法B调用了n次析构函数、n次析构函数。
当 拷贝操作的开销 比 构造-析构操作 要廉价的时候,一般来说A方法是上选。
但是A方法中对象的作用域比B方法中更大,也就违背了代码的集中性和可维护性原则。
因此,除非
拷贝操作比构造-析构操作开销小,并且此部分代码对性能(performance)要求很高,(此时选择为A)
否则B方法还是更合理。
Item 27 : 少做转型动作
--------------------------------------------------
tag: cast 转型 const_cast dynamic_cast reinterpret_cast static_cast
·尽量避免转型,特别是注重效率的代码中避免 dynamic_casts.尽量将要转型的设计转化为无需转型。
·若转型必须,试着将它隐藏于某个函数背后。客户随后可以随时调用该函数,而不需将转型放进他们自己的代码中。
·另可使用 C++ style转型,也不要使用旧式转型。前者更容易分辨。
类型转换的三种形式(前面两种都是C风格的旧式类型转换):
(T)expression
T(expression)
C++ Style:
const_cast:设置或是去除对象的const属性。
dynamic_cast:主要用于继承关系层次中的向上、向下转换,以及类之间的交叉转换。会进行转换安全性检查。
static_cast:可用于内置类型的转换,以及继承关系层次中的向上转换。没有转换安全性检查。
reinterpret_cast:简单的强制将一个指针转换为另外一种指针或整数类型,不做任何检查。
类型转换还可能引发额外的代码运行。比如说dynamic_cast就会通过调用strcmp来比较类的名称,从而完成继承关系中不同类对象的转换,这个时候就不仅仅是简单的变变类型了。因此,说“类型转换仅是告诉编译器把一种类型的数据当成另外一种来参与计算”其实是一个理解上的误区。
类型转换也有可能带来额外开销:比如书中用static_cast进行的继承关系的向上转换,就会自作主张地生成一个临时的对象。
dynamic_cast 通常是在一个认定为 derived class 对象上执行 derived class操作函数,但手上只有一个“指向base”的pointer或reference。
可以用以下两种方法来避免这个问题:
1. 使用容器并在其中存储直接指向 derived class 对象的指针(通常为智能指针),以消除通过base class接口处理对象的需要。
2. 在base class内提供virtual函数做你想对各个derived classes做的事。
要避免所谓的“连串(cascading)dynamic_casts”
在C++中,两个指向同一个对象的不同指针可能拥有不同的地址值。
因此,不仅要尽可能的避免转换类型,而且在不得不使用类型转换的时候,也应该考虑将转换的代码用函数封装起来。
Item 28 : 避免返回 handles 指向对象内部成分
--------------------------------------------------
tag: 返回值
避免返回 handles(包括reference、pointer、iterator)指向对象内部。
增加封装性,帮助const成员函数的行为像个const,并将发生“悬垂handles”的可能性降至最低。
如果只需要读访问,就使用const的返回值,不要开放写的权限。
有可能产生悬垂指针(dangling pointer)也是暴露对象内部成员handles的后果之一。
一个返回对象内部成员的函数,在用户不正确使用的情况下,就有可能产生悬垂指针。
class AClass{//..};
class BClass{
//..
const AClass& FuncReturningARef();
//..
}
//a possible user's code
BClass AnObjectOfB;
const AClass *pAClass = &(AnObjectOfB.FunReturningARef());
//After the call pAClass becomes a dangeling pointer..
Item 29 : 编写对异常免疫(exception-safe)的代码
--------------------------------------------------
tag: Exception safety
·Exception safety functions即使发生异常也不回泄露字元或允许任何数据结构败坏。区分为三种可能的保证:基本型、强类型、不抛出异常型
·强烈保证型通常能以 copy-and-swap 实现,但强烈保证兵匪对所有函数都可实现或具备现实意义。
·函数提供的“Exception-safe保证”通常只等于其所调用之各个函数的“Exception-safe保证"中的最弱者。
对异常免疫的函数在异常发生的时候应该具备两个特征:
不泄漏任何资源(内存、锁等等)
不造成任何数据结构的损坏
并能够提供至少以下保证中的一项:
Exception-safe functions 提供以下三个保证之一:
基本的保证:当异常抛出时,程序中的对象、数据免遭破坏。
较强的保证:当异常抛出时,程序的状态不会被改变。若成功调用函数,则系统进入成功后的状态;如果函数中因异常而出错,系统应该留在调用函数前的状态:
最强的保证:不会有异常抛出。例如对内置类型的操作就不会抛出异常。这是最理想的,但也很难做到。更多的函数只能在前两者中做一选择。
为了能够提供较强的保证,也即系统的状态不因异常抛出与否而变化,大师又重新提出了“先拷贝后交换”(copy-and-swap)这一方法论来。
用不那么严谨的说法:为了避免在操作对象时触发异常影响系统状态,“先拷贝后交换”先是创建了一个临时对象,将所有的操作都施加在该临时对象上。如果没有出错,把这个处理过的临时对象和真正需要处理的对象交换一通,算是顺利完成任务;如果有错并抛出了异常,原系统状态也不会被影响,因为真正需要处理的对象根本没有被动过。
当然,天下没有免费的午餐。
“先拷贝后交换”不仅耗费了一个临时对象的存储代价,同时支出的还有后面交换对象时的时间和资源开销。因此,对异常免疫的较强保证是很好很强大,但是实际中并不是任何时候都需要做到那么高的保证。杀鸡岂需用牛刀?
最后要提醒的是,对异常免疫的函数也符合“短板理论”:木桶能装的水与其最短的那块木板有关,函数对异常免疫的程度也由函数中程度最低的代码(包括其调用的函数)决定。某个函数如果调用了另外一个一出现异常就崩溃的函数,那么这个函数就不能提供基本的异常免疫保证。
Item 30 : 透彻了解 inlining
--------------------------------------------------
tag: inline
·将大多数 inlining 限制在小型、被频繁调用的函数身上。可使日后的调用过程和二进制升级更容易,也使程序的速度提升机会更大。
·不要只因为 function templates 出现在头文件,就将他们声明为 inline.
使用内联函数(inline function)可以省去一般函数调用的入栈操作开销,比宏(macro)要好用。从编译器的角度来看,没有函数调用的代码要更容易优化。
但是天下没有免费的午餐,以空间换时间的内联函数同时也带来了更大的程序占用空间,更甚者还会因为这变大的代码空间导致额外的内存换页操作,降低指令缓存(instruction cache)的命中率……这些都是使用内联函数需要考虑到的负面影响。(Scott还是辩证地提醒了一点:当内联函数非常短小时,相比一般意义上的函数调用,它能够帮助编译器生成更小的最终代码和运行时更高的指令缓存命中率)
内联函数的声明可以是显式的:使用inline关键字;也可以是隐式的:在类的定义中定义函数。
内联函数一般而言都是定义在头文件中,这是因为大多数编译器的内联动作都是发生在编译过程中。(也有着链接甚至是运行中才进行内联的,但是俺们这里随大流,讲主要矛盾)
虽然内联函数和函数模板有一点相似:它们都几乎定义在头文件中,但是这两者之间没有必然联系,而非有的程序员想的那样“函数模板一定是内联的”)
内联函数的定义仅仅是对编译器提出内联的请求。编译器完全有可能忽视这个请求,于是某“内联函数”有可能在最后还是生成了一般函数的代码:
请求内联的函数有可能太过于复杂
请求内联的函数有可能是虚函数(虚函数的真正实体要在运行时才能得知,让编译器编译阶段去做内联实在有点强人所难)
请求内联的函数没什么问题,但是在代码中有用函数指针的方式调用该函数(这样编译器也没办法,如果不生成一般函数哪来的函数指针?)
这些情况下确实不应该把函数作为内联函数。一个“内联函数”是否最终生成了内联函数,还得编译器说了算。
然而编译器并不是总能帮助我们做出正确的决定,还有一些情况是需要我们自己做出判断的:
请求内联的函数是构造/析构函数(表面上看起来某个构造/析构函数很短小甚至是空的,但是为了构造/析构类中的其他成员,编译器有可能会“自觉”地写入必要的代码,这样的构造/析构函数就有可能不适合再做内联了)这一点原文中有更详细的说明。
当编写支持库时(library)也不建议使用内联函数,因为一旦用户使用了这些含有内联函数的库并编译了自己的程序,这些内联函数就已经“写死”在他们的程序中了。当日后对原先的库做了更新修改,用户就必须重新编译整个程序才能用上新的补丁。而一般的函数就不会有这个问题:他们是动态链接的,用户根本感觉不到任何改动。
考虑到很多调试器(debugger)无法调试内联函数(本来就没有这么一个“函数”,叫人家怎么设断点?),在调试版本中也不建议使用内联函数。
有那么多需要注意的地方,大师最后总结了一下:用好内联函数的第一步就是:不用内联函数。并没有那么多的函数真正需要内联,因为80%的程序运行时间都是花在了20%的代码中。第二步是把内联函数当成是手工优化的手段,仅仅在非常需要效率和优化的代码中使用内联。
Item 31 : 将文件间的编译依存关系降至最低
--------------------------------------------------
tag: Handle class,Interface classes , 接口类 实现类,
·相依于声明式,不要相依于定义式。两个手段来实现:Handle classes 和 Interface classes.
·程序库头文件应该以 “ 完全且仅有声明式 ”(full and declaration-only forms)的形式存在,不论是否涉及 templates.
大师说了,C++的设计还是有缺陷的:它无法把接口(interface)的设计和实现(implementation)的设计完全划分开来。
比如说在一个类的(接口)声明当中,总是或多或少的会泄漏一些实现上的细节,虽然这样做与接口的设计并没有太多联系。
class AClass {
public :
void interface_1();
std::string interface_2();
private :
// implementation details are leaking as below..
std::string internalData_1;
BClass internalData_2;
}
往往还需要引用其他头文件中相关对象的定义(如下面的代码),从而产生了对这些头文件的(在编译时的)依赖。因此每次这些文件中的某个有变化时,依赖它的所有文件都需要重新编译。
#include < string >
#include " BClass.h " //
【注意】这里貌似逻辑不是很顺:就算没有那些私有成员的声明,接口函数的返回值如果是string或是BClass等类型,不还是一样需要依赖引用其他头文件吗?
这是两种不一样的情况,实现和接口。
前面说的实现细节的泄漏是会导致编译依赖的,因为编译器需要了解这些类型对象的大小进而为其分配内存空间;
但是接口,比如说函数的返回值或是参数表中的参数,就不需要编译器去考虑分配内存的问题,因此也就没有所谓的编译依赖了。
将类分割为两个classes,一个只提供接口,另一个负责实现该接口。
class AClassImpl
{
private :
// implementation details are moved here..
std::string internalData_1;
BClass internalData_2;
}
class AClass
{
public :
void interface_1();
std::string interface_2();
private :
// there is only a pointer to implementation
std::tr1::shared_ptr < AClassImpl > pImpl;
}
// a constructor: instantiations of AClass and AClassImpl should always be bound together.
AClass::AClass( // ..) : pImpl(new AClassImpl( // ..))
{ }
分离的关键在于以“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,如果不行,就让它与其他文件内的声明式(而非定义式)相依:
·若使用 object references 或 object pointers 可以完成任务,就不要使用 objects.但如果要定义某类型的object,就需要用到定义式,。
·尽量以class声明式替换class定义式。
·为声明式和定义式提供不同的头文件。
第二种方法中,抽象类/接口类提供了所有接口的纯虚函数形式:会有该类的子类去实现这些接口。
在抽象类/接口类中还会有一个静态(static)的工厂函数(比如create()/produce()/factory()……),这个函数实际上起到了构造函数的作用,它“制造”出子类对象来完成真正的任务,同时返回这个对象的指针(通常是智能指针如shared_ptr)。凭借这个返回的指针就可以进行正常的操作,同时不会有编译依赖的担心。一个简陋的代码见下:
class AClass: public AClassFactory {
public :
AClass() {}
void interface_1();
std:: string interface_2();
virtual ~ AClass();
}
class AClassFactory {
public :
virtual void interface_1() = 0 ;
virtual std::string interface_2() = 0 ;
virtual ~AClassFactory() { /* .. */ }
static std::tr1::shared_ptr < AClassFactory > Produce( /* .. */ )
{
// this factory function could be more complicated in practice..
return std::tr1::shared_ptr < AClassFactory > ( new AClass);
}
}
// AClassFactory could be used in this way..
std::tr1::shared_ptr < AClassFactory > pAClassObject;
pAClassObject = AClassFactory::Produce( /* .. */ );
// pAClassObject->..
posted @
2010-03-15 22:53 Euan 阅读(469) |
评论 (0) |
编辑 收藏
========================
Effective C++ 设计与声明
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 18 : 接口应该容易被正确使用,不易被误用
--------------------------------------------------
tag:消除客户的资源管理责任 tr1::shared_ptr cross-DLL problem
用户使用接口时却没有获得预期的行为,这个代码不应该通过编译。
用错的可能有:
·调用接口时输入了错误的参数。
如(一个接受年、月、日为参数的接口函数,用户可以轻易给出各种错误的输入),
解决办法:用对象来约束参数输入的范围(不接受简单的整数作为输入,而是Date、Mon、Year对象)
struct Day{ explicit Day(int d):val(d){} int val;}
class Month{
public:
static Month May(){ return Month(5); }
private:
explicit Month(int m);
}
Date d(Month::Mar(),Day(20),Year(1995));
·用常规的用法调用“特别”设计的接口。所以需要尽可能的把自己的设计往常规上靠:数据对象的行为要尽可能符合内建对象(比如int)的行为;接口的名字和意义要尽可能一致(比如STL中的容器基本都有一个叫做size的返回容器大小的接口)……这样做鼓励用户去正确的看待和使用你的接口。
·忘了处理调用接口后的遗留问题。因此不要让用户去“记得”做一些事情。
如设计一个接口返回一个指向某新建对象的指针,该接口的用户需要“记得”去释放这个指针所指的对象:如果用户忘了释放或释放了好几次,后果就是@#¥%
解决的办法之一是让该接口返回一个智能指针(嗯……印象模糊了?去看Item14),这样用户用完了就可以“忘记”这个指针:它自己会处理后事。
·所谓的“跨DLL问题”(cross DLL problem):在一个DLL中new一个对象,然后对象被传到另外一个DLL里被delete。大师推荐用shared_ptr因为它解决了这个问题。
代价:额外对象的创建和销毁需要时间空间。比如boost的shared_ptr就是普通指针的两倍大小,还有额外的对象操作时间+过程动态内存分配等。
实际上有些底层代码根本没这个资本提供这样的“豪华装备”,不过有这样的思想还是很重要D……
Item 19 :设计class犹如设计type
----------------------------------------
tag:class design
·小心设计类的创建和销毁方式。比如说Item8和Item16
·认真考虑如何区分类的构造函数和赋值(assignment)操作符。即初始化与赋值的差别。
·注意实现类的传值(passed by value)。这个实际上是在说要注意拷贝构造函数的实现。
·切勿忽略类对非法输入的处理。其实是要注意各种出错情况,是否需要抛出异常以及如何实现异常处理。
·需要审视类所在的继承体系。
如果该类有父类,那么必定要受到父类的一些限制,特别是函数是否为虚构;如果该类有子类,那么就要考虑是不是一些函数需要定义为虚函数,比如说析构函数。
·谨慎实现类对象与其他类型对象的转换。这一点稍有些复杂:如果有将T1转换为T2的需求,就有隐式转换和显式转换两种方式。
对于前者,可以编写一个(隐式的)转换函数(参考Item15里面的隐式转换咯~),或者是通过额外编写一个T2的构造函数来实现T1向T2的转换。
对于后者,Scott说写一个(显式的)转换函数就可以了。(同样,在Item15里也有显式转换函数的例子)
·需要考虑该类需要参与哪些运算。很明显,如果需要参与A运算就要相应定义类的A运算符函数。大师在这里提的另外一点是,这些运算符号函数有些应该是成员函数,有些不应该。原因在Item23、24、26【555我还没看到,留空】
·不要提供不应该暴露的标准函数。这里的标准函数指的是构造/析构/拷贝等等可能由编译器“自愿”为你生成的函数,如果不希望它们中的一些被外界调用,就声明为私有(private)。没印象了?降级到Item6重新学习~
·注意设计类成员的访问权限。公有(public)、保护(protected)、私有(private)应该用哪一种?有没有需要定义友元?或者是干脆来一个类中类?都需要考虑。
·认真审查类的隐性限制。性能上的要求、使用资源的限制或是出错时的处理都有可能影响到类的具体设计和实现。
·谨慎考虑类的适用范围。也就是说如果某个设计会可能用在很多方面,适用于许多不同的实际对象。也许这个时候你需要设计的不是一个类,而是一个类模板。
最后一点其实应该放在第一位:你真的需要定义一个类吗?如果仅仅是在继承某类的基础上加一两个成员,是不是非成员函数或模板就已经够了捏?
第19招其实更像是个check list,在准备动手设计之前,一一比对打勾划叉,应该可以提前避免很多人间惨剧……
Item 20 : 用 传const引用 替换 传值
-------------------------------------------
tag: const引用 值传递
C++传递对象的时候默认是传值的(pass-by-value),而这样的传递自然是昂贵的:这当中包含了临时对象的构造/析构,以及临时对象中的对象的构造/析构,运气背点还可能有对象中的对象中的对象的构造/析构……(有好的不学,去学C@#¥%)
·相对于传“值”,一个更好的替代方法是传“const引用”(pass-by-reference-to-const)。
·传值与传指针的一个区别是,通过传值传递的对象并不是原来的对象,而是一个复制品,所以随便你打它骂它,真身都不会受到影响。
·而通过传指针的对象和原来的对象就是同一家伙,改动一个另外一个也受到相同的影响。而这有时候并不是我们想要的结果。
·考虑到传值代价太高,传“const引用”就成了一个很好的替代品。
·传“const引用”的另外一个好处在于避免了“剥皮问题”(slicing problem,侯捷大师的版本是“对象切割问题”,我用这个中文名字是为了更容易记住:))
用传值方式传参的函数,如果某参数的类型是一个父类对象,而实际传递的参数是一个子类对象,只有该对象的父类部分会被构造并传递到函数中,子类部分的成员,作为父类对象的“皮”,就被血淋淋的剥掉了……
而如果用传“const引用”方式,就没有这种惨无人道的状况:本来父类的指针就可以用来指向一个子类对象,天经地义。
例外:对于内置类型(bulit-in type)对象以及STL中的迭代器、函数对象,Scott还是建议使用传值方式传递,原因是他们本来就是被设计成适合传值传递的。
(个人观点:大师说:“……it's not unreasonable to choose pass-by-value。”,注意这里有句潜台词:其实对以上类型用传“const引用”方式传递也是可以的。)
如果你认为上面两种情况可以用传值传递是因为它们,比如说内置类型对象,的大小本来就小,进而得出小数据类型就可以用传值传递,就打错特错了。小对象的构造/析构过程完全可能很恐怖。
再退一步,哪怕某个类型很小,它的构造/析构函数也简单到可以忽略不计,我们还是不能以此断定可以用传值传递这种类型的对象:因为编译器往往会做出一些蠢事。书中的一个例子是,对于一些编译器可以接受把一个double类型对象存入寄存器,但是如果你给它一个只有一个double成员的对象交给它,它却拒绝将该对象存入寄存器。(什么事让编译器插一手,不是问题也有了问题……)
最后还有个理由,虽然某对象现在很小,可是随着社会的发展人类的进步,有可能两年后它就会变成一个庞然大物,到时候用传值也会变得不合适。
·除了内置类型和STL的迭代器、函数对象外,其他的对象传递时,用传“const引用”代替传值吧。
Item 21: 该换回对象时别返回它的reference
---------------------------------------------------
tag:
如果一个函数可能返回一个对原来不存在的对象的引用,那么函数就要自己去创建这个对象:要么在栈上(stack)要么在堆上(heap)。
第一种情况中,函数中定义了局部对象,然后返回对该对象的引用。对象在函数结束后自动销毁,引用指向无效的地址。
第二种情况,函数使用new动态创建了一个对象,然后返回对该对象的引用。粗看没有问题,因为这个返回的引用还是有效的。
但是细想就会发现:我们能确保这个对象被正确的收回(delete)吗?
书中举了一个很好的例子:一个*运算符函数,接受两个乘法运算数,返回一个积。
如果在函数中动态创建一个对象来存储乘积,并返回对这个新对象的引用,那么下面的计算就会带来内存泄漏:
Y=A*B*C
因为在这个“连续”乘法中,有两个“乘积对象”被创建,但是我们丢失了第一次乘法创建的对象的指针。
所以这样的做法是不妥的。
也许大师有被问过:那么对于第一种情况我们可不可以返回一个静态(static)对象的引用?书中用了同样的例子来回答:NO。
if (A*B == C*D) {//..}
如果返回静态对象的引用,上面的判断语句永远得到true值,因为“==”号两边的运算结果是指向同一块数据的引用。
不知道是不是后面又有刨根问题的学生追问:那我能不能用一个静态对象的数组来存放不同此运算的结果?大师懒得举例子了,我猜想也没必要:这样的方案带来的副作用及其开销本身就已经大于原来要解决的问题了吧?
不要尝试在函数中返回对局部对象(存储于栈)的引用,也不要对动态创建的对象(存储于堆)做同样的蠢事,而如果真有打算、非常渴望、十分想要返回对静态对象的引用,在这么做之前也要考虑清楚会不会有上面例子中的情况出现。
至少,返回对象不会有上面列出的种种错误危险,仅仅是有可能带来创建额外对象的开销而已,而这个开销的可能还有可能被排除,如果你用的编译器足够聪明的话。
Item 22: 将成员变量声明为private
-----------------------------------------------------
如果数据成员都是私有的,那么访问这些成员就只能通过函数进行。于是用户就不需要费心考虑到底要用什么方式去访问数据成员:因为只有定义了的函数可以用。
通过定义数据成员为私有,可以实现函数来设计、约束或禁止对这些成员的各种访问(读/写等)。而如果将其设为公有(public),你将无法得知你的成员会被谁改动,也不知道会是怎样的改动。
而更重要的好处是封装(encapsulation):可以方便的通过修改函数来改变成员的访问方式;在成员被访问时通知其他对象;实现多线程中的同步等等。
封装的好处究其本质,是通过对用户隐藏数据成员来保证类行为的一致性(class invariant)。因为接口被成员访问函数限制了,类的作者也为自己日后修改类的实现留了后路:如果所有的成员都是公有的,对任何代码的修改都有可能影响到外界的使用。(因此Scott说“Public means unencapsulated, and practically speaking, unencapsulated means unchangeable, especially for classes that are widely used.”)
那么可不可以声明为保护(protected)呢?其实道理和前面的公有是一样的。公有的成员对类的外部完全开放,而保护的成员对类的继承者完全开放。这个就像两个区间:(-infinity, +infinity) 和 (0, +infinity),两者的大小是一样的。
从分装的角度,只有两种访问级别:私有,及其他。
Item 23: 用 non-member、non-friend 替换 member 函数
------------------------------------------------------
从面向对象的角度来看,非成员函数更有利于数据的封装。
一个数据成员被越少的代码访问到,该成员的封装程度就越高。越少函数可以直接访问一个数据成员,该成员的封装程度就越高。
类的数据成员应该定义为私有。如果这个前提成立,那么能够访问一个数据成员的函数便只能是该类的成员函数,或是友元函数。
于是为了更好的封装数据,在可以完成相同功能的前提下,应该优先考虑使用非成员并且非友元函数。
这里的“非成员并且非友元函数”是针对数据成员所在的类而言的,也就是说这个函数完全可以是其他类的成员,只要是不能直接访问那个数据成员就可以。
从灵活性上来说,非成员函数更少编译依赖(compilation dependency),也就更利于类的扩展。
例:一个类可能有多个成员函数,可能有一个函数需要A.h,另外一个函数要包含B.h,那么在编译这个类时就需要同时包含A.h和B.h,也就是说该类同时依赖两个头文件。
如果使用非成员函数,这个时候就可以把这些依赖关系不同的函数分别写在不同的头文件中,有可能这个类在编译时就不需要再依赖A.h或是B.h了。
把这些非成员函数分散定义在不同头文件中的同时,需要用namespace关键字把它们和需要访问的类放在一起。
// code in class_a.h
namespace AllAboutClassA {
class ClassA { // ..};
// ..
}
// code in utility_1.h
// ..
namespace AllAboutClassA {
void WrapperFunction_1() { // ..};
// ..
}
// ..
// code in utility_2.h
// ..
namespace AllAboutClassA {
void WrapperFunction_2() { // ..};
// ..
}
// ..
这样一来,虽然这些非成员和类不“住在”一个头文件里,它们的“心”还是在一起的(在同一个名字空间, namespace, 中)。
如果有需要添加新的非成员函数,我们要做的只是在相同的名字空间中定义这些函数就可以,那个类丝毫不会被影响,也即所谓的易扩展性吧。
对于类的用户来说,这样的实现方式(指用非成员函数)就更加合理:
因为作为类的用户,需要扩展类的时候又不能去修改别人的类(版权?安全性?或者根本就没有源码?),就算是通过继承该类的方式也不能访问父类的私有数据。
Item 24: 若所有参数皆需类型转换,为此函数采用 non-member 函数
------------------------------------------------------
tag: operator
若需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,这个函数应该设为 non-member.
---------------------
class Rational{
public:
Rational(int num = 0, int denominator = 1); //构造函数刻意不为 explicit; 允许 int-to-Rational 隐式转换
...
const Rational operator* (const Rational& rhs) const;
}
Rational oneHalf(1, 2);
result = oneHalf * 2; //right → result = oneHalf.operator*(2);
result = 2 * oneHalf; //wrong! → result = 2.operator*(oneHalf); → result = operator*(2, oneHalf);
---------------------
const Rational operator*(const Rational& lhs, const Rational& rhs){..} //non-member function.
若函数不该成为member,不一定要成为friend.
Item 25: 考虑写出一个不抛异常的 swap 函数
------------------------------------------------------
tag: 异常处理
·当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定函数不抛出异常。
·如果你提供一个 member swap,也该提供一个 non-member swap 用来调用前者,
对于 classes(非 templates),请特化 std::swap;
·调用 swap 时应针对 std::swap 使用using 声明式,然后调用 swap 并不带任何“命名空间资格修饰”。
·为“用户定义类型”进行std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西。
std::swap的缺省实现:用了一个中间临时对象,然后两两交换。
缺省的方法很简单,而在一些情况下却也很耗资源:比如需要交换的是一个很复杂庞大的对象时,创建/拷贝大量数据会使得这种swap的效率显得非常低下。
--------------------------
更加适应的实现思路:
在类/模板类(class/class template)中定义一个公有的swap成员函数,这个函数负责实现真正的交换操作。同时,这个函数不能抛出异常。
用成员是因为交换操作中可能会需要访问/交换类的私有成员;用公有(public)来限制是为了给下面的辅助函数(wrapper function)提供接口。
至于不能抛出异常,有两个原因:
一是Item29中所提到的异常安全性(exception-safety)需要不抛出异常的swap来提供保障(更多细节就到拜读29条的时候再记录吧。)
二是一般而言高效的swap函数几乎都是对内置类型的交换,而对内置类型的操作是不会抛出异常的。
1. 如果需要使用swap的是一个类(而不是模板类),就为这个类全特化std::swap,然后在这个特化版本中调用第一步中实现的swap函数。
class AClass{
public :
void swap(AClass & theOtherA){
using std::swap; // 这一句会在稍后的第3点提到
// 通过调用swap来完成该类的特有交换动作
}
// ..
}
namespace std{
// 在std名字域内定义一个全特化的swap
template <> // 这样的定义说明是全特化
void swap < AClass > ( AClass & a, AClass & b){
a.swap(b);
}
}
如此一来,用户可以直接应用同样的swap函数进行交换操作,而当交换对象是需要特殊对待的AClass对象时,也可以无差别的使用并得到预期的交换结果。
2. 如果我们需要交换的是模板类,那么就不能用全特化std::swap的方法了,偏特化的std::swap也行不通,因为:
C++中不允许对函数进行偏特化(只能对类偏特化),也 就是说不能写出下面的程序:
namespace std{
// illegal code as C++ doesn't allow partial specialization for function templates
template<typename T>
void swap< AClassTemplate<T> >(AClassTemplate<T>& a, AClassTemplate<T>& b)
{
a.swap(b);
}
}
std名字空间中的内容都是C++标准委员会的老大们定义的,为了保证std内部代码的正常运作,不允许往里头添加任何新的模板、类、方程,重载也不可以。
虽然可以像1.那样写出全特化的模板函数,但是企图在std的名字空间添加以下重载的swap(这种重载变相实现了函数的偏特化)(虽然你可以通过编译,但是会埋下隐患):
namespace std{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
给自己的一个小提醒:因为函数名swap后没有<>,所以不是偏特化,而是对
namespace std{
template<class _Ty> inline
void swap(_Ty& _X, _Ty& _Y)
{/*..*/}
}
的重载而已。
基于上面两个原因,一个变通的方法是在该模板类所在的名字空间中编写一个 非成员的函数模板来调用这个公有的接口:
namespace AClassSpace{
template <typename T>
void swap (AClass<T>& a, AClass<T>& b)
{ a.swap(b);}
}
在限定的名字空间中实现函数,是为了避免“污染”全局的名字空间。而且,不同的名字空间都可以使用一样的函数名字而不会有冲突。
基于前面第23课所学,使用非成员函数也是应该的了。
至于为什么要函数模板,那就匪常D简单:因为要交换的是模板@#¥%
pimpl也即pointer to implementation,当需要交换两个复杂且臃肿的对象时,可以先用两个指针分别指向着两个对象
之后对这些对象的操作,包括交换,就只需要通过这两个指针来进行(交换两个指针的值便实现了对象的交换)。
posted @
2010-03-15 22:50 Euan 阅读(463) |
评论 (0) |
编辑 收藏
========================
Effective C++ 资源管理类(resource-managing classes)
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 13 :以对象管理资源
---------------------------------------
tag:auto_ptr shared_ptr RAII RSCP 智能指针
要利用对象来管理资源(可以是分配出来的内存、互斥量等)。这样可以避免因为写代码时的疏忽而导致的资源流失(可以是内存的泄漏,也可能是忘了解锁)。
将释放资源的代码放在对象的析构函数中,只要对象被析构,就可以保证资源被完整释放。
C++的析构函数自动调用机制(C++’s automatic destructor invocation)能够保证当对象脱离控制时该对象的析构函数被调用(比如一个对象在某函数内部定义,那么在函数执行结束后,这个对象的析构函数会被自动调用以销毁此对象)。
是个好办法,至少对我这样时常丢三落四的人来说是个福音。何况这样的对象大多数情况下不用自己写,auto_ptr(智能指针, smart pointer的一种)就可以胜任。
std::auto_ptr<void *> pMem(AllocAndInitMemory());
高亮的部分即说明了auto_ptr的用法,也附带示范了所谓的RAII(Resource Acquisition Is Initialization)资源取得时机便是初始化时期。
这里的auto_ptr就像是个陪女友逛街的可怜家伙,女朋友(用户)说要什么,auto_ptr就乖乖的去买来(申请),想要用的时候就赶紧拿出来给女友用,哪怕她忘了曾经买了某件衣服,auto_ptr还是会老老实实地送回家里,不用担心会弄丢或是搞脏。嗯,有点做上帝的感觉吧?
不过,没有什么是十全十美的。大师马上就说到了用对象管理资源的问题,好吧,应该说是需要注意的地方:
首先不能用一个以上的对象,比如说auto_ptr,管理同一个资源。这个很好理解,就像一个漂亮的女孩子脚踏多条船(同时定义了多个auto_ptr),但是她必须要小心,不能让多个对象参加同一个约会:今天逛街的时候喊上了小张就不能叫小王,明天一起吃饭如果约了小李就别再和小赵定时间:一个资源一次只能让一个对象来管理。
这样的规则用在auto_ptr上就是书中奇怪的“=”行为:两个auto_ptr对象A和B,A=B运算的结果是A接管了B管理的资源,B指向的是null。(如果记不起来了,具体代码见书)这样的行为很像是传递:小李陪女朋友逛完街后,送她到人民广场,在那里小陈约好了和她一起看电影。到了人民广场之后小陈搂着女人开心的走了,只剩下小李一个孤单的背影,杯具啊杯具……
为了避开这种尴尬且极易被误解的“=”操作,出现了以shared_ptr为代表的reference-counting smart pointer(RCSP)(引用计数型智能指针?)。它们可以“共事一夫”,由一个公用的reference counter来计数,某个shared_ptr在准备抛弃一个资源时先参考reference counter的值,如果非零,说明还有其他的“姐妹”在罩着资源,不需要也不可以释放该资源;如果值为零,说明当前只有她一个人了,于是真正的担当起smart pointer的责任:释放资源。
std::shared_ptr<void *> pMem(AllocAndInitMemory());
重点不在于是用auto_ptr还是shared_ptr还是其他的什么东东,而是要领会精神——使用对象来管理资源。比如说用string对象来管理一个字符串而不是手工申请一片内存然后用个指针“吧唧”粘上去就开始用了。
不能再动态分配而得的 array 上使用 auto_ptr 或 tr1::shared_ptr,因为两者在析构的时候调用的是delete而不是delete[].
要拥有针对数组而设计的,可用 boost::scoped_array 和 boost::shared_array classes.
Item 14: 在管理类中小心 coping 行为
--------------------------------------------
tag: RAII
RAII守则:资源在构造期间获得,在析构期间释放。
RAII对象被复制:
·不允许拷贝。当资源本身不能复制时,对象可以说“不”。怎么做?回到Item6……
·使用Reference-Count(引用计数),可以用上节说到的shared_ptr来干这个事,这里顺带介绍了shared_ptr提供的一个接口:一个可以在构造对象时定义的delete操作:如果对象是内存就是释放,如果对象是锁就是解锁。
·直接复制。别人有什么,你就直接原封不动也复制一份。如果是内存的话说得过去,如果是锁,我想还是不能这样乱用哈。
·移交所有权。这个不算是真正意义的复制,移交手续而已。最典型的例子就是auto_ptr的复制行为。
Item 15: 在资源管理类中提供对原始资源的访问
-------------------------------------------------
tag:返回原始资源
·在使用对象管理资源的同时也要留出接口给那些需要绕过对象而直接访问资源的人。
写个函数暴露出指向资源的指针就可以。书里讲得更多的是用怎样的函数:
显式转换(explicit conversion)
可以实现一个get函数,或是*、->运算,返回指向资源的指针。
隐式的转换函数(implicit conversion),
但是个人觉得实际工作中应该是不提倡这样做的,因为隐式的转换极有可能发生在编程者没有意识的情况下,导致后面的代码出错。
class Font {
public:
// ..
// implicit conversion function
operator FontHandle() const { return f; }
// ..
};
上面代码的应用如下,f本身为Font类型,(changeFontSize第一个参数为FontHandle),但是由于隐式转换,类型变成了FontHandle。
Font f(getFont());
int newFontSize;
//..
// implicitly convert Font to FontHandle
changeFontSize(f, newFontSize);
为了能兼容更多的API,需要留出接口提供对资源的直接访问。
隐式或显示主要取决于RAII class 被设计执行的特定工作,以及它被使用的状况。
Item 16 : 成对使用new和 delete时采用相同形式
---------------------------------------------------
tag: new delete
·用new分配一个内存对象时,语法格式是new a;用delete释放一个内存对象时,语法格式是delete a;
·用new分配一组内存对象时,语法格式是new a [num_of_elem]; 用delete释放一组内存对象时,语法格式是delete [] a;
new或是delete包含了两个阶段:
new:申请并分配内存空间;调用构造函数构造即将使用空间的对象
delete:调用析构函数析构使用空间的对象;释放内存
分配内存给一组对象的时候,编译器一般会在这一片内存前端(或是其他什么地方)插入一小段信息,用来标明这片内存是给多少个对象的,然后反复调用构造函数来创建这一组对象。当用delete []的时候,释放内存的操作就会以该信息为依据,反复调用对象的析构函数对这组对象进行释放。(下面的[n]就是这段信息)
[n][MEM]
而如果只是分配内存给一个对象,这段信息就不存在了。直接在这片内存上应用析构函数。
于是用delete []去释放new的内存,或是用delete去释放new []的内存,都会造成不可预计的后果。
Item 17 : 将new语句单独写
----------------------------------
tag: newed
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());这行语句有问题,这个复杂的参数表包含了三个动作:
·new 一个 Widget
·用new的Widget做为参数执行share_ptr的构造函数
·执行priority
C++的某个编译器可能为了效率而自作主张,导致这三个动作的执行顺序是不确定的!因此上面的动作执行顺序可能是这样的:
·new 一个 Widget
·执行priority
·用new的Widget做为参数执行share_ptr的构造函数
这个时候如果priority的执行出错而引发异常,就会发生内存泄漏(Memory Leak),因为new出来的Widget再也无法跟踪了。
而解决方法也很简单,不要妄图一行写完所有程序,分开来老老实实写就是了:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
VC++下不会。
posted @
2010-03-15 22:48 Euan 阅读(496) |
评论 (0) |
编辑 收藏
========================
Effective C++ 构造、析构、赋值运算
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 5、6 :C++默认编写的函数
-----------------------------------------
tag: 默认拷贝函数 copy assignment
如果没有定义类的构造函数/拷贝函数/拷贝构造函数/析构函数,编译器会自作主张帮你定义这些函数,而且还是public外加inline的。(估计这是最冷的冷饭了)
帮写拷贝函数时仅限于帮你处理一下诸如int的内嵌类型(build-in type)以及自身就有拷贝函数的类对象,再复杂一点的就搞不定了。
有时候需要禁止某个类对象的拷贝操作,就要避免以上函数的自动生成,需要自己定义这些函数,并且有以下特性
private
只是声明,不给予实现(give no implementation)
当然要是还是觉得麻烦,干脆直接继承Boost的noncopyable(自己写一个类似的也可以),就可以不费吹灰之力的获得不能拷贝的特性。
Item 7 :为多态基类声明virtual析构函数
--------------------------------------------
tag: 多态(polymorphism) 虚函数 virtual function 析构函数
·polymorphic base classes 应将析构函数定义为虚函数(virtual function)
·当class内至少含一个virtual函数时,才声明virtual析构函数。
否则在通过基类指针析构一个子类对象的时候,因为没有虚表(V Table)对析构函数的指引,对象的基类部分是被毁了,对象的子类部分却没办法得到析构(没有了虚表中的说明它根本不知道有子类的析构函数来析构子类部分!),从而导致内存泄漏。
virtual函数实现:
要实现virtual,vptr(virtual table pointer)指针指向一个向函数指针构成的数组,称为vtbl(virtual table);
带有virtual函数的class都有一个对应的vtbl,对象调用virtual函数时,实际被调用的函数取决于该对象的vptr所指的那个vtbl(在其中寻找适当的函数指针)
假设基类指针在析构时要做的事情是奉命救人:指针啊指针,XX地发大水了,你赶紧去把张三一家人(子类对象)救出来。
指针拿着指令(析构函数)就找到了张三(张三家家长,子类对象的基类部分):张三是吧,你一家人都在这里了吧,跟我走吧。(析构吧,回空余内存里去吧@#¥%)
如果张三已经没有其他亲戚了,他就是张三家的全部人(这个对象其实就是基类对象)。很简单,直接跟着指针走就完事了。
如果张三还有个小弟叫张三疯(张三家除张三外的一部分,即子类对象的子类部分),张三就必须拿着族谱(V Table)跟指针说哎呀这是我弟啊,把他也析构了吧,张三家对您感恩不尽……也很简单,一并带走就完成任务了(完整析构/释放了整个对象)
如果,张三没了族谱,记不得有个张三疯弟弟,傻乎乎的跟了指针走,大家也以为张三一家都被救出来了。灾区里就只剩下张三疯一个人在疯跑了(memory leak)
另一方面,如果不准备把某个类用作多态中的基类,就没必要定义析构函数为虚函数。这样除了增加代码大小开销外(因为需要空间存放虚表),没有任何好处。(这个倒是很多书上没说过的)
如果张三家本来就只有张三一个人,他就没必要还要带着个空白的族谱了。逃命的时候能扔就扔嘛。
最后提到的一点和抽象类有关。抽象类是有纯虚函数的类,只能作为接口,不能有自己的对象。在设计软件框架的时候会有需要用到抽象类的时候。可是如果有这么一个类,需要设计为抽象类,但是却找不到一个成员函数可以拉去做纯虚函数,那么这个时候就把析构函数定义为纯虚函数好了。只是同时还要给它一个空的实现。
class::~class() {}
Item 8 :别让异常逃离析构函数
-------------------------------------------
tag:异常 try catch 析构函数
·在析构函数中不能抛出异常。 若被析构函数调用的函数可能会抛出异常,析构函数应该捕捉任何异常,并吞下(不传播)或结束程序。
·若一个操作可能出现异常,客户需要对这个异常作出反应。class应提供一个普通函数来执行该函数,而不是在析构函数内调用。
异常不像函数调用,一旦抛出(throw),就回不来了。
如果在析构函数中抛出了异常,有一部分释放/摧毁对象成员的操作可能就无法进行。因为某个异常,导致了所在的析构函数无法完成全部析构任务。
可是要是真的需要对析构动作中出现的错误/异常进行处理咋办?书中自有解决方案:从差的到好的。
在析构函数内布置catch,一旦发生异常就会被捕获,然后简单调用std::abort自杀
点评:干脆是干脆了,但是这样猝死会不会有点太突然?
也是在函数内布置catch,但是遇到异常就把它直接吃到肚子里(大师语:Swallow the exception)。
点评:一般不该这样处理,毕竟发生了不好的事。但如果真的想要程序继续带伤上阵,也确定这样不会有问题,那也不会有人有意见。
除了在函数里布置catch,并采用以上任一方法,另外实现一个可供用户调用的函数接口,用来处理这些有可能出错的析构工作。
点评:大师给予这个方案高度的评价,因为这样不但有以上两种方法的效果,还给用户一个参与处理异常的机会(调用接口函数)。如果用户没有(或者忘记)用该函数处理析构的动作,那么析构函数也会自觉挑起这个任务。而这个时候如果还出问题,用户也没有什么理由来责怪了:是你自己不想要管的!
Item 9 :不在构造和析构过程中调用 virtual 函数
-----------------------------------------------------
tag:
拥有虚函数的类就有虚表,虚表可能会引发子类相应虚函数的调用,在这些调用中有可能对某些子类对象成员的访问。
在构造一个子类对象时: 开始构造父类部分 -> 完成父类部分并开始构造子类部分 -> 完成子类部分 (完成整个构造工作)
析构一个子类对象的时: 开始析构子类部分 -> 子类析构完毕并开始析构父类部分 -> 完成析构父类部分(完成整个析构工作)
在构造函数的第一步,子类对象成员还不存在,调用虚函数有可能会访问不存在的子类成员;
哪怕到了第二步,因为子类对象还没有完全构造完毕,此时调用虚函数也是危险的。事实上在整个构造过程中,该对象都被视作一个父类对象。
反过来也是同样道理,在析构函数的第一步,子类成员已经开始销毁,不能调用虚函数;到了第二步,整个子类部分都没有了,更不能用虚函数了。
而在整个析构过程中,该对象也是被看作是父类对象的。
确保虚函数不会在对象构造/析构过程中被调用:
方法之一就是用参数传递代替虚函数机制。
把可能被构造函数调用的虚函数改成非虚函数,然后让父类的构造函数将需要的信息/数据通过参数传递给子类构造函数。
Class Parent
{
public :
Parent();
Parent( const std:: string & WordsFromChild){
DoStuff(WordsFromChild);
// ..
};
void DoStuff( const std:: string & WordsFromChild);
}
Class Child : public Parent
{
public :
Child( /**/ /* some parameters here */ ) : Parent(TellParent( /**/ /* some parameters here */ )) {
// ..
};
private :
static std:: string & TellParent( /**/ /* some parameters here */ );
}
也许看到这里会想:要是TellParent()中访问了未被初始化的子类成员呢?那不也是一样有问题么?
注意,这就是为什么前面有个static限定的原因。因为静态函数可以在对象的初始化阶段就开始工作,更详细的描述看这里。
子类的虚表在子类的构造函数时生成,所以在父类的构造函数中调用虚函数使用的是父类的版本。
子类和父类对象都会有自己的虚表,里面安置的是自己版本的虚函数实现。
Item 10-12 : 拷贝运算符
--------------------------
tag:assignment operator(赋值运算符) 自我赋值 copying函数
·令赋值(assignment)操作符返回一个reference to *this。 可实现(a=b=c)。
·处理自我赋值
·构造函数用来初始化新对象,而 assignment操作符只施行于已初始化对象身上。
·copying函数应该确保复制“对象内所有成员变量”和“所有 base class成分”
1、
在函数入口检查是否属于自拷贝(例如:检查指针是否指向同一片内存),如果是,啥也不干直接返回。否则属于正常情况的拷贝。
这样解决了self-assignment-unsafe的问题,但是没能避免exception-unsafe。
2、
第二种方法比较简单,只是整理一下指令的顺序。但是却同时解决了自赋值和抛出异常带来的问题。继续无耻的抄写代码一段:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // remember original pb
pb = new Bitmap(*rhs.pb); // make pb point to a copy of *pb
delete pOrig; // delete the original pb
return *this;
}
这样的做法在解决以上两个问题的同时却也降低了执行的效率:不论什么情况,这个赋值函数都要创建一个新的Bitmap对象。
第一种方法的额外支出:判断语句必然地引入了程序的分支(branch),于是指令的预取(prefetch)、缓冲(caching)、流水线处理(pipelining)的效率就会被降低。
3、
Copy And Swap。改赋值为交换。
void swap(Widget& rhs); //交换*this 和rhs的数据;
Widget& Widget::operator=(const Widget& ths)
{
Widget temp(ths); //为rhs数据制作一份副本
swap(temp); //将*this数据和上述副本的数据交换。
return *this;
}
Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
{ // passed in — note pass by val
swap(rhs); // swap *this's data with the copy's
return *this;
}利用参数传值,隐性的构造了一个Widget对象。然后将新对象和本对象中的数据成员交换,达到为本对象赋值的效果。
新的临时对象在跳出函数后自动销毁。刚才说的两个unsafe,都不会存在。
这样开销较大了,无论什么时候都要构造新的对象。用swap来完成赋值的做法有点逻辑混淆。但这样做很有可能让编译器生成更有效率的代码。
---------------------------
如何保证在赋值/拷贝的时候能够将所有的成员完整拷贝过去?
对于简单的数据成员,编译器自动生成的拷贝函数可以保证一个不漏都帮你拷贝;
如果是比较复杂的成员(比如说指向一片内存空间的指针),编译器就没有足够的智商把这些成员拷贝到另外一个对象中去了。
在增加类成员以后记得更新拷贝函数,以免拷贝不完全。
子类的拷贝函数把自己的成员都拷贝了,但是却漏了把父类对象的成员拷贝到新的对象中。 在子类的拷贝函数中调用父类的拷贝函数
Widget& Widget:: operator = (Widget src)
{
swap(src); // copy-and-swap
WidgetParent:: operator = (src); // invoking the parent's copy assignment operator
return * this ;
}
最后的最后,通常来说在拷贝函数和拷贝构造函数中的实现大多相同,
不要在拷贝函数中调用拷贝构造函数或者反之。如果真的需要避免代码的重复,大可定义一个私有的函数来负责前面两者相同的部分。
posted @
2010-03-15 22:44 Euan 阅读(526) |
评论 (0) |
编辑 收藏
毕业设计中途重读了几本以前看过的书,需要做些笔记做日后查阅时使用。
偶然看到 Justin 写的《Effective C++》笔记,为方便就结章转载在此。
========================
Effective C++ C++
书作者:Scott Meyers
原笔记作者:Justin : http://www.cppblog.com/note-of-justin/
========================
Item 1 :C++是一个语言联邦
--------------------------
tag: c c++组成
C
区块(blocks)、语句(statements)、预处理器(preprocessor)、内置数据类型(built-in data types)、数组(arrays)、指针(pointers)。
Object-Oriented C++
classes(包括构造、析构)、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数(动态绑定)
Template C++
泛型编程(generic programming) Template metaprogramming(TMP,模版元编程)
STL
容器(containers)、迭代器(iterators)、算法(algorithms)、函数对象(function objects)
每个语言都有自己的次规则
Item 2 :用const、enum和模板inline推翻#define的统治
---------------------------------------------------
tag: const enum inline #define
需要定义常量时,不要用#define,改用const修饰的变量或是用enum吧
要想写一些简短小函数时,别考虑#define啦,改用template+inline吧
原因是若为浮点变量,用const减小了代码大小,同时还使得封装(encapsulation)变得可行(宏被定义后,在之后的编译过程都有效,除非undef ),而且,在调试的时候,因为const定义的变量是会加在符号表(Symbol Table)的,就比define常量要方便跟踪了(在预处理阶段,常量的名字就已经被替换掉了)
在一些特定的情况下(编译器不允许“static整数型class常量”完成“in class 初值设定” ),如果不能用const取代#define,就用enum。除了不能获取一个enum的地址之外,这种方法和const的用法其实差不多。
可以取一个const的地址,不能取一个enum的地址,通常不能取一个#define的地址。
inline函数和宏有个共同的地方,他们都不会有函数调用的栈的开销。再喊上模板(template)来帮忙,就不用去考虑实际调用时的参数类型。
Item 3 :尽可能使用const
--------------------------------------------------
tag: const non-const conceptual constness mutable
·首先要知道const可以通用在对象上,函数参数和返回值上,甚至是用在限制函数本身。
·const 出现在星号左边,表式被指物是常量;出现在星号右边,指针自身是常量;
·两个成员函数如果只是常量性(constness)不同,可以被重载。
·const和non-const成员函数的实现等价时,可以用non-const版本调用const版本避免代码重复。
Compilers enforce bitwise constness, but you should program using conceptual constness.
这里有提到constness(常量性)的两个门派: bitwise学院派和conceptual实用派。
bitwise constness阵营应该都是很学究的,这里认为如果一个函数被声明是const,你就绝对不能修改对象里的任何成员(static成员除外)。
主张conceptual constness流的当然都比较好说话,虽然你是const党,但需要的时候,还是应该有例外的嘛。正所谓人无完人,const也没有绝对的const~
conceptual constness可以这样解释:具备conceptual constness的对象/函数,其行为对于该对象/函数以外的数据是const的,不会篡改别人的东东。但是不保证它 不会修改对象/函数内部的成员:当这些成员用mutable修饰的时候,我们可以在一个const函数中修改这些mutable成员的值。
所以说这样的constness是概念上的,实际上在这样的函数中有可能改变了一些变量的值,只不过没有与它声称的constness矛盾而已。
用mutable限定的对象,哪怕是在const函数里,一样可以修改!
和const有关的还有在const和非const对象间的转换问题(用const_cast和static_cast完成两个方向的转换),不过层次太高,我还没能看到有需要用的地方
const char& operator[](size_t position) const {
...
}
char& operator[](size_t position) //调用已经实现的const op[]
{
return const_cast<char&> ( //将const op[] 的返回值中移除 const
static_cast<const CClass&>(*this) //将*this转型为 const,指明调用的是const版本的op[]
[position] );
}
Item 4 :对象初始化
--------------------------------------------
tag:local static , 初始化列表, 赋值(assignment)
·对于内建的对象类型,手工初始化。
·对于对象自身的成员,推荐的方法是在构造函数的初始化列表。
·以logcal static 对象替换 non-local static对象,以避免“跨编译单元之初始化次序”问题。
赋值(assignment)的效率要比初始化(initialization)低,因为前者先构造了对象再对他们赋值,在构造的同时就也把值赋了。这里还没加上拷贝构造函数的可能开销,还有一些类型如const变量、引用(reference)是不能用赋值的形式“初始化”的……
如果在初始化某个对象的时候,有对其他对象是否有初始化的依赖(对不起,这里有点拗口),一定要确保其中所依赖的对象已经初始化完毕。
当不同的对象的初始化存在相互依赖时,某个对象没有初始化有可能导致另外一个对象初始化的失败。
当初始化涉及到非局部静态对象(non-local static object)时,问题更加明显:非局部静态对象如果定义在不同的文件中,他们就有可能位于不同的编译单元(translation unit),因为这些对象到底谁先被初始化是不可预知的。
编译单元(translation unit):产出单一目标文件(single object file)的那些源码,通常为单一源码文件加上所包含的头文件。
解决此类问题的一个方法是:把非局部静态对象转换为局部静态对象(local static object),也就是把它的定义放在一个函数里。然后紧接着在这个函数返回该对象的引用。C++语言规定在调用一个含有局部静态对象的函数时,其中的所有局部静态对象都必须初始化。这个方法就是利用这一特性,将原本对一个非局部静态对象的访问,转换为对一个函数的调用,这个函数会返回该静态对象的引用,并且保证这个对象已经被初始化了。
如果需要初始化一个非局部静态对象,就把它放到一个函数里,让这个函数简单的返回这个对象的引用。
posted @
2010-03-15 22:43 Euan 阅读(570) |
评论 (0) |
编辑 收藏