MFC如何销毁窗口资源

在做MFC界面时,有一个问题曾经困扰我很长时间,窗口资源到底该如何销毁?这个问题网上也很多,我结合自己的体会,以及MSDN,尝试给出一个解答。如有错误,也请不吝赐教。
我从三个方面分别论述。

一、子类对象在析构时修改了VTBL地址

这是理解销毁窗口的一个重要问题:子类对象在析构时,首先析构自身,再去析构父类对象;并且,在析构自身时,改变了对象本身的VTBL地址。

上面这句话什么意思呢?我们知道,C++虚函数的实现原理是:
    1.对每个包含虚函数的类,编译器都创建一个虚函数地址表(数组)。这个数组称为虚函数表(virtual function table, VTBL),虚函数表中存储了类声明的虚函数的地址。
    2.对每个包含虚函数的类对象中,编译器添加一个隐藏成员,这个成员中保存了一个指向函数地址数组(VTBL)的指针。
    3.对于每个虚函数的调用,首先找到这个类对象的VTBL,然后在VTBL中找到对应函数的地址。

说了一堆,简单概括就是:每个包含虚函数的类的对象,都有一个隐藏成员;根据这个隐藏成员指向的地址,确定到底调用子类还是父类的成员函数。因此,指向子类对象的指针调用虚函数时,找到子类的VTBL,根据这个表找到子类的方法;而指向父类对象的指针调用虚函数时,找到父类的VTBL,根据这个表找到父类的方法。 那么这样就会有个微妙的问题:
如果在父类的析构函数中调用一个虚函数,它会调用子类的方法吗?

答案是不会。因此在子类析构时,已经把对象中这个隐藏的成员指向的地址改为了父类的VTBL地址。
请看下面代码

#include <iostream>

//基类
class CBase
{
public:
	CBase(int i) {
		m_base = i;
	}

	~CBase() {
		std::cout << "父类开始析构了,此时调用Print,结果为:" << std::endl;
		Print();
	}

	virtual void Print() const {
		std::cout << "	这是在父类中:m_base=" << m_base << std::endl;
	}
private:
	int m_base;
};

//派生类
class CDerived1 :public CBase
{
public:
	CDerived1(int i)
		:CBase(6 / 2) {
		m_Derive = i;
	}

	~CDerived1() {
		std::cout << "子类开始析构了,此时调用Print,结果为:" << std::endl;
		Print();
	}

	virtual void Print() const {
		std::cout << "\t这是在子类中:m_Derive=" << m_Derive << std::endl;
	}

private:
	int m_Derive;
};


int main()
{
	CDerived1 cSon(6);
	CBase* pBase = &cSon;
	std::cout << "第一次调用指向子类的基类方法Print,结果为:" << std::endl;
	pBase->Print();

	std::cout << "\n让我们看看析构时发生了什么" << std::endl;
}

在上面程序中,父类和子类都有Print()方法,程序最终的结果是如下。
在这里插入图片描述

至此,可以验证我们刚刚说的,在父类的析构函数中,调用虚函数不会调用子类的方法,而只是自身的方法。
先记住结论,我们看MFC的窗口资源销毁。

二 、MSDN中关于MFC的窗口对象销毁的建议

这个问题,在MSDN中 TN017: Destroying Window Objects 有详尽的叙述。我们开始咬文嚼字。
(翻译参考:http://blog.chinaunix.net/uid-16444831-id-76752.html

TN017: 销毁窗口对象

This note describes the use of the CWnd::PostNcDestroy method. Use this method if you want to do customized allocation of CWnd-derived objects. This note also explains why you should use CWnd::DestroyWindow to destroy a C++ Windows object instead of the delete operator.
This is important. If you follow the guidelines below, you will have few cleanup problems (such as forgetting to delete/free C++ memory, forgetting to free system resources like HWNDs, or freeing objects too many times).

本文描述了成员函数CWnd::PostNcDestroy的使用方法。使用这个方法来定制CWnd对象的(内存)分配行为。同时本文也说明了使用DestoryWindow而非”delete”来销毁C++ Windows对象(C++ Windows object,这是个专有名词,待会介绍)的原因。
这一点很重要。跟随以下的指引,清除工作将不会有什么问题(诸如忘记删除或释放C++内存,忘记释放系统资源,例如HWNDs,或者多次释放同一个对象)。

问题的提出
The Problem
Windows objects (objects of classes derived from CWnd) represent both a C++ object (allocated in the application's heap) and an HWND (allocated in system resources by the window manager). Since there are several ways to destroy a window object, we must provide a set of rules that prevent system resource or application memory leaks and that prevent objects and Windows handles from being destroyed more than once.
This is more than a memory management problem. The presence of a Windows window has user-interface impact: a window drawn on the screen; once it is destroyed, there is also an impact on system resources. Leaking C++ memory in your application address space is not as bad as leaking system resources.
Windows对象(Windows objects :特指CWnd派生类的对象)即可以看作是一个C++对象(分配在应用程序堆里),又可以看作是HWND(由窗口管理器在系统资源里分配)。由于有多种方法来销毁一个窗口对象,我们必须提供一组规则来防止系统资源或者应用程序内存泄露,保证对象和Windows句柄不被多次释放。
(作者注:特别注意MSDN指出:Windows objects特指从CWnd派生的类对象,这个对象代表一个C++的对象和一个窗口)
这不仅仅是内存管理的问题。首先,Windows窗口的出现会对用户看到的界面产生影响:窗口是被绘制到屏幕上的;其次,一旦窗口被销毁,这将对系统资源产生影响。系统资源泄露的危害要比应用程序地址空间发生内存泄露的危害大得多。

Destroying Windows
销毁窗口
The two permitted ways to destroy a Windows object are:
1. Calling CWnd::DestroyWindow or the Windows API ::DestroyWindow.
2. Explicitly deleting with the delete operator.
The first case is by far the most common. This case applies even if is not called directly by your code. This is the case when the user directly closes a frame window (the default WM_CLOSE behavior is to call DestroyWindow), and when a parent window is destroyed, Windows calls DestroyWindow for all the children.
The second case, the use of the delete operator on Windows objects, should be very rare and only in the cases outlined below.

销毁一个窗口对象有两种方法:
    1.调用CWnd::DestroyWindow或者Windows API ::DestroyWindow。
    2.使用delete操作符显式删除。
第一种情况比较常见,即使代码里没有直接调用DestroyWindow函数,DestroyWindow也会被间接调用。例如,当用户直接关闭框架窗口(WM_CLOSE的默认实现是调用DestroyWindow)或者当一个父窗口被销毁时,Windows调用DestroyWindow函数来销毁它所有的子窗口。
第二种情况,即使用delete操作符销毁窗口对象,应尽量少用,并且仅在以下情况下使用。

Auto Cleanup with CWnd::PostNcDestroy
使用CWnd::PostNcDestroy自动销毁
When destroying a Windows window, the last Windows message sent to the window is WM_NCDESTROY. The default CWnd handler for that message (CWnd::OnNcDestroy) will detach the HWND from the C++ object and call the virtual function PostNcDestroy. Some classes override this function to delete the C++ object.
The default implementation of CWnd::PostNcDestroy does nothing which is appropriate for window objects allocated on the stack frame or embedded in other objects. This is not appropriate for window objects that are designed to be allocated by themselves on the heap (not embedded in other C++ object).
Those classes that are designed to be allocated by themselves on the heap override the PostNcDestroy member function to perform a . This statement will free any C++ memory associated with the C++ object. Even though the default CWnd destructor calls DestroyWindow if m_hWnd is non-NULL, this does not lead to infinite recursion since the handle will be detached and NULL during the cleanup phase.
当销毁一个Windows窗口时,最后发送给此窗口的Windows消息是WM_NCDESTROY。CWnd类处理该消息的函数(CWnd::OnNcDestroy)默认地会将HWND和C++对象解绑,然后调用虚函数PostNcDestroy,有些类会重载该函数来释放(delete)此C++对象。默认的CWnd::PostNcDestroy实现并不做任何处理,这适用于在栈上创建的或者嵌入其它对象中的窗口对象。但对于在堆上创建(没有嵌入其它C++对象中)的窗口对象不适用。
如果需要在堆上创建窗口对象,需要在类中重载PostNcDestroy函数,并执行"delete this"操作,这将释放掉该C++对象所占用的C++内存。 尽管缺省的CWnd析构函数会在m_hWnd不为空的情况下调用DestoryWindow,但这不会导致无穷递归,因为此句柄在清除阶段将会处于分离状态并置为空。

Note
CWnd::PostNcDestroy is normally called after the Windows WM_NCDESTROY message is processed, as part of window destruction, and the HWND and the C++ window object are no longer attached.
CWnd::PostNcDestroy will also be called in the implementation of most Create calls if failure occurs (see below for auto cleanup rules).
注意
CWnd::PostNcDestroy正常情况下会在WM_NCDESTROY消息被处理之后,作为窗口销毁的一部分被调用,此时,HWND和C++窗口对象已不再关联。在大多数的Create实现中,如果发生错误CWnd::PostNcDestroy也会被调用(参见以下自动清理规则)。

Auto Cleanup Classes
自动清理类
The following classes are not designed for auto-cleanup. They are normally embedded in other C++ object or on the stack:
    1. All standard Windows controls (CStatic, CEdit, CListBox, and so on).
    2. Any child windows derived directly from CWnd (for example, custom controls).
    3. Splitter windows (CSplitterWnd).
    4. Default control bars (classes derived from CControlBar, see Technical Note 31 for enabling auto-delete for control bar objects).
    5. Dialogs (CDialog) designed for modal dialogs on the stack frame.
    6. All the standard dialogs except CFindReplaceDialog.
    7. The default dialogs created by ClassWizard.
The following classes are designed for auto-cleanup. They are typically allocated by themselves on the heap:
    1. Main frame windows (derived directly or indirectly from CFrameWnd).
    2. View windows (derived directly or indirectly from CView).
If you want to break these rules, you must override the PostNcDestroy method in your derived class. To add auto-cleanup to your class, call your base class and then do a delete this. To remove auto-cleanup from your class, call CWnd::PostNcDestroy directly instead of the PostNcDestroy method of your direct base class.
The most common use of changing auto cleanup behavior is to create a modeless dialog that can be allocated on the heap.
以下的类并不会自动清理,它们通常在栈中创建或者嵌入其它的C++对象中:
    1. 所有Windows标准控件(CStatic,CEdit,CListBox等)。
    2. 所有从CWnd直接派生来的子窗口(例如,自定义控件)。
    3. 拆分窗口(CSplitterWnd)。
    4. 默认的控件栏(从CControlBar派生的类,请参照Technical Note 32来实现控件栏对象的自动删除)。
    5. 在栈上创建的模态对话框(CDialog)。
    6. 所有的标准对话框,除了CFindReplaceDialog。
    7. 由ClassWizard创建的缺省对话框。
下面的类可以自动清理,它们一般在堆上创建:
    1. 主框架窗口(直接或者间接派生至CFrameWnd)。
    2. 视图窗口(直接或者间接派生至CView)。
如果你想打破以上任何一条规则,你必须在你的派生类中重载PostNcDestroy成员函数。 若要为你的类添加自动清理功能,需要调用基类的PostNcDestroy,然后执行delete this操作。若要去除自动清理功能,直接调用CWnd::PostNcDestroy来代替你直接基类的成员函数PostNcDestory。这些规则,主要用于在堆上创建非模态对话框。

When to Call 'delete'
何时调用'delete'
We recommend that you call DestroyWindow to destroy a Windows object, either the C++ method or the global DestroyWindow API.
Do not call the global DestroyWindow API to destroy a MDI Child window. You should use the virtual method CWnd::DestroyWindow instead.
For C++ Window objects that do not perform auto-cleanup, using the delete operator can cause a memory leak when you try to call DestroyWindow in the CWnd::~CWnd destructor if the VTBL does not point to the correctly derived class. This occurs because the system cannot find the appropriate destroy method to call. Using DestroyWindow instead of delete avoids these problems. Because this can be a subtle error, compiling in debug mode will generate the following warning if you are at risk.

在这里插入图片描述

我们推荐使用DestroyWindow函数(C++成员函数或者全局的::DestroyWindow API)来销毁窗口对象。 销毁MDI子窗口,要调用虚拟成员函数CWnd::DestroyWindow,而不能调用全局的::DestroyWindow API。
对于那些不执行自动清理的C++窗口对象,使用delete操作符可能会产生内存泄漏,因为调用基类CWnd::~CWnd析构函数中的DestroyWindow时,VTBL没有指向正确的派生类地址,进而系统找不到要调用的适当销毁方法。 (作者注:还记得第一节讲的内容吗?delete首先析构了子类,然后到基类析构函数中(CWnd::~CWnd中)调用DestoryWindow时,VTBL的地址已经改变,DestoryWindow不会调用子类的方法,而仅仅是基类的方法。另外,更重要的是,CWnd::DestroyWindow方法会调用全局的::DestroyWindow函数,而这个函数会发出WM_DESTROY消息,很多情况下这个消息会调用子类的OnDestroy 响应函数,但是现在子类对象已经被析构了,自然也就找不到子类的OnDestroy方法了。这样子类对象中的一些资源就不会销毁,自然会出错了). 使用 DestroyWindow 而不是 delete 可以避免这些问题。这可能会导致一些不明显的bugs,所以MFC诊断版本(调试)中会警告你:

在这里插入图片描述

In the case of C++ Windows objects that do perform auto-cleanup, you must call DestroyWindow. If you use operator delete directly, the MFC diagnostic memory allocator will alert you that you are freeing memory twice (the first call to delete as well as the indirect call to “delete this” in the auto-cleanup implementation of PostNcDestroy).

After calling DestroyWindow on a non-auto-cleanup object, the C++ object will still be around, but m_hWnd will be NULL. After calling DestroyWindow on an auto-cleanup object, the C++ object will be gone, freed by the C++ delete operator in the auto-cleanup implementation of PostNcDestroy.



对于执行自动清理工作的C++ Windows对象,你必须调用DestroyWindow。如果你直接使用操作符delete,MFC的诊断内存分配算符将会警告你:你正在第二次释放内存(第一次调用delete,还有在PostNcDestroy的自动清理执行过程中调用delete this)。

对于一个不执行自动清理的对象,在调用DestroyWindow之后,这个C++对象仍然存在,但是m_hWnd会为空。对一个执行自动清理的对象,在调用DestroyWindow之后,此C++对象就不存在了,它被PostNcDestroy的自动清理执行过程里的delete操作符释放。

三、总结

根据以上分析,微软建议的窗口销毁方式是:
    1.推荐使用DestroyWindow而不是delete销毁对话框;
    2.如果是分配在堆上的对话框,并且其不属于自动清理的类,应当在PostNcDestroy执行delete this。
    3.对模态对话框而言,单击对话框【OK】【CANCEL】时会自动调用DestroyWindow,也就销毁窗口资源。但是对非模态对话框,单击对话框【OK】【CANCEL】仅仅是隐藏这个对话框,并没有调用DestroyWindow销毁资源;因此,非模态对话框必须重写OnOk和OnCancel函数,并在函数中调用DestroyWindow来销毁窗口资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Santiago

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值