创建和销毁过程几个函数的执行顺序

本文详细解析了对话框窗口对象的创建、销毁过程及其生命周期内关键函数的作用,包括构造函数、预子类窗口函数、创建函数、初始化对话框函数、销毁窗口函数、关闭事件处理函数等。重点讨论了不同关闭方式(如点击确定、取消或关闭按钮)下窗口对象的关闭路径,并阐述了程序退出时的操作顺序。同时,文章对比了OnClose()和OnDestroy()函数的区别,强调了在进行程序结束时的清理工作时应在OnDestroy()中进行,避免内存泄漏。此外,文章还区分了对话框类和CWnd基类中消息响应函数的不同作用,并探讨了窗口对象清除流程,特别是如何正确地处理系统资源和内存资源的释放。

From: http://blog.youkuaiyun.com/csw_100/article/details/4806957


1:对象生成后 函数执行顺序

首先执行 构造函数-> Create->PreSubclassWindow()->OnCreate()->OnInitDialog()

Create 创建窗口,发出WM_CREARE消息。  
OnCreate  响应WM_CREARE消息。

Create是创建对话框,OnCreate是响应Create操作的事件处理函数,OnInitDialog是对话框初始化函数

 

销毁对象时,函数的执行顺序

DestroyWindow()->OnDestroy()->析构函数

1. 点“确定”、“取消”时的关闭路由为 
OnOK()或OnCancel() ---> EndDialog() ---> DestroyWindow() ---> OnDestroy() ---> PostNcDestroy() 
2. 点“关闭”标题栏按钮的关闭路由为 
OnClose()---> DestroyWindow() ---> OnDestroy() ---> PostNcDestroy()

 

OnClose():消息响应函数,响应WM_CLOSE消息,当"关闭"按钮被单击的时候(而不是我们眼睛所见的对话框从屏幕上消失时),该函数被调用,用户在响应OnOk()或者OnCancel()函数之后,不会发送WM_CLOSE消息

OnDestroy():消息响应函数,响应WM_DESTROY消息,当一个窗口即将被销毁时,被发送

在单视图程序中,根据<<深入浅出MFC>>所讲,程序退出时执行的操作顺序为
(1)用户点击退出按钮,发送了WM_CLOSE消息
(2)在WM_CLOSE消息的处理函数中,调用DestroyWindow()
(3)在DestroyWindow()中发送了WM_DESTROY消息
(4)在WM_DESTROY消息中调用PostQuitMessage(),发送WM_QUIT消息,结束消息循环

综上,程序先调用OnClose()(也可能不调用),然后调用OnDestroy()(必调用),所以,如果要进行程序结束时的清理工作,应该在OnDestroy()中,而不是在OnClose(),否则就有可能会出现内存泄漏的危险了!

 

 

第一,OnOK()和OnCancel()是CDialog基类的成员函数,而OnClose()和OnDestroy()是CWnd基类的成员函数,即WM消息响应函数。从应用程序结构的角度,拿对话框来说,红色的X对应的是CWnd,而处于对话框中的“确定”、“取消”按钮则对应了CDialog。

第二,OnClose()和OnDestroy()

在单视图程序中,根据<<深入浅出MFC>>所讲,程序退出时执行的操作顺序为(从点X按钮开始)
(1)用户点击X退出按钮,发送了WM_CLOSE消息----->响应OnClose()
(2)在WM_CLOSE消息的处理函数中,调用DestroyWindow()----->销毁与指定CWnd窗口对象关联的窗口,
但未销毁CWnd对象

(3)在DestroyWindow()中发送了WM_DESTROY消息----->窗口销毁后响应OnDestroy()
(4)在WM_DESTROY消息中调用PostQuitMessage(),发送WM_QUIT消息,结束消息循环

可以看到,程序的退出过程,是先响应OnClose(),然后响应OnDestroy(),在响应OnDestroy()之前,窗口对象已经被销毁。OnDestroy()到底干了什么呢?它就像一个teller,先通知CWnd对象告诉它即将被销毁,尔后OnDestroy的真正运行是在CWnd对象已经从屏幕上清除以后被调用的。

第三,OnOK()、OnCancel()()、OnClose()、OnDestroy()

CDialog::OnOK首先调用UpdateData(TRUE)将数据传给对话框成员变量,然后调用CDialog::EndDialog关闭对话框;   
CDialog::OnCancel只调用CDialog::EndDialog关闭对话框;   
OnClose()是响应   WM_CLOSE   的.一定程度上可以说CDialog::EndDialog()和OnClose()完成类似的工作,但处理的机制不一样,前者是CDialog的对象机制,后者是WM的消息映射机制。

CDialog::EndDialog()-------->OnDestroy()

                 OnClose()-------->OnDestroy()

EndDialog()和OnClose()属于“同级别”的,所以我们在按下OK按钮的时候,程序是不会执行OnClose()的,但两种机制都必须经过OnDestroy()。

 

要消除窗口对象,必须清楚窗口对象的构成.在一个通常的程序中,先创建c++窗口对象,然后由Windows创建实际的窗口结构,并返回句柄与c++对象连接.也就是说,窗口对象包含c++窗口对象和Windows窗口对象,两者通过句柄HWND联系. 

现在,让我们看看"正规"的窗口对象清除流程.所谓对象的清除是指释放对象所占的资源,窗口对象中Windows窗口对象占有的是系统资源,c++对象占 有的是内存资源.释放系统资源相对要简单一些:调用虚函数DestroyWindow删除Windows窗口对象.如果DestroyWindow删除的 是父窗口,Windows会自动为子窗口调用DestroyWindow.一般来说,程序不必调用DestroyWindow.因为当用户关闭窗口时, Windows便发送WM_CLOSE消息,WM_CLOSE的缺省消息处理函数CWnd::OnClose调用DestroyWindow. 

到这时,清除工作已经完成了一半,屏幕上的窗口已经不见了!但是别忘了,在内存中还有一个c++窗口对象.让我们再看看c++对象清除的过程:当 窗口被取消时,窗口最后发送的一个消息是WM_NCDESTROY.它缺省的消息处理函数CWnd::OnNcDestroy把c++窗口对象与句柄 HWND分离,并调用一个很重要的虚函数PostNcDestroy.这个函数是搞清窗口对象清除的关键.Cwnd中的PostNcDestroy什么都 不做.有些MFC窗口类会重载它,并加入delete this代码删除c++对象.这些窗口类常常是以new操作符建立在堆中的.由于重载了PostNcDestroy,使窗口有自动清除功能.因此,我们不 用关心清除问题了.另外的一些MFC窗口类一般是以变量形式创建的,MFC没有为也没必要为它们重载PostNcDestroy函数.

不具备自动清除功能的窗口类,一般在堆栈中创建或嵌入于其它c++对象中: 

所有标准的Windows控件类(如CStatic, CEdit, CListBox等等) 

由CWnd类直接派生出来的子窗口对象(如用户定制的控件) 

拆分窗口类(CSplitterWnd) 

缺省的控制条类(CControlBar的派生类) 

对话框类(CDialog)在堆栈上创建的模态对话框类 

所有的Windows通用对话框(除CFindReplaceDialog) 

由ClassWizard创建的对话框 

具有自动清除功能的窗口类,一般在堆中创建: 

主框架窗口类(直接或间接从CFrameWnd类派生) 

视图类(直接或间接从CView类派生) 

从某种程度上来说,MFC的"服务到家"使初学者有些找不着北.不过,不得不承认:MFC干的很漂亮! 

谈到这里,我们应该明白c++里一条重要的准则:用DestroyWindow清除窗口对象,不要用"delete". 

对于不具备自动清除功能的窗口类使用"delete"时,"delete"先调用析构函数里的DestroyWindow,由于在析构函数中,虚 机制不起作用,这里只能调用本地版本(Cwnd类)DestroyWindow函数,显然这不是我们想要的.对于有自动清除功能的窗口类,好象问题更严重 一点,前面提到了重载的PostNcDestroy已经含有了"delete this",这样c++对象就被释放了两次. 


<think>好的,用户现在想了解C++中构造函数执行顺序、赋值初始化、默认构造函数、临时变量析构函数的相关内容。我需要结合之前的回答提供的引用资料来组织这个回答。 首,用户之前的问题是关于列表初始化为什么更快,现在这个问题扩展到了构造函数执行顺序其他相关概念。需要确保回答的结构清晰,逐步解释每个部分,同时引用之前的资料中的内容。 构造函数执行顺序是一个基础但重要的点。需要分点说明,比如基类、成员变量、派生类的顺序。引用中的内容提到析构函数执行顺序,可能也需要关联起来。比如,在引用[1]中,析构函数函数结束后执行,而构造函数顺序则相反。 赋值初始化与列表初始化的区别之前已经详细讨论过,这里需要再次强调,尤其是在构造函数体内赋值的过程,可能涉及到默认构造赋值操作,引用[2]提到默认构造函数explicit关键字,可以结合这点说明默认构造函数的作用。 临时变量部分可能需要说明临时对象的构造析构时机,比如在函数传参或返回值时生成的临时变量,它们的构造函数析构函数何时被调用。引用中提到的析构函数函数结束后执行,可能与此相关。 关于默认构造函数,引用[2]提到如果没有定义,编译器会自动生成,这也是需要强调的。特别是在成员变量有默认构造函数的情况下,如何影响整个类的构造流程。 析构函数执行顺序通常与构造函数相反,需要明确说明,比如成员变量、基类等的析构顺序。引用[1]中的示例代码展示了析构函数的调用时机,可以引用这个例子来说明。 需要确保回答中有代码示例,比如展示构造函数执行顺序的示例,以及临时变量的生成析构过程。同时,注意LaTeX格式的应用,比如变量名使用$...$,独立公式用$$...$$,但在这个问题中可能数学公式较少,更多的是代码示例。 可能用户还会关心构造函数中的异常处理,或者如何手动控制构造顺序,但根据当前问题,需要集中在提到的几个关键词上。此外,用户提到临时变量,需要解释临时变量在何时被创建销毁,可能涉及拷贝构造函数移动构造函数的影响。 最后,生成的相关问题需要覆盖构造顺序、临时对象、默认构造的影响等,确保问题具有扩展性,帮助用户进一步学习。</think>### C++构造函数执行顺序与赋值初始化详解 --- #### 一、构造函数执行顺序 C++中构造函数执行顺序遵循**严格的层级规则**,直接影响对象初始化的正确性: 1. **基类构造函数** - 若存在继承关系,**基类构造函数**优执行(按继承声明顺序)。 ```cpp class Base1 { public: Base1() { cout << "Base1构造\n"; } }; class Base2 { public: Base2() { cout << "Base2构造\n"; } }; class Derived : public Base1, public Base2 { public: Derived() { cout << "Derived构造\n"; } }; // 输出顺序:Base1构造 → Base2构造 → Derived构造 ``` 2. **成员变量初始化** - 所有**成员变量**的构造函数按**类内声明顺序**执行(与初始化列表顺序无关)。 ```cpp class Data { string s1; // 声明顺序:s1在前 string s2; public: Data() : s2("test"), s1("hello") {} // 实际执行顺序:s1 → s2 }; // s1构造 → s2构造 ``` 3. **派生类自身构造函数** - 最后执行派生类的构造函数体。 --- #### 二、赋值初始化与列表初始化对比 ##### 1. 赋值初始化的本质 若在构造函数体内赋值,成员变量会经历以下步骤: ```cpp class Example { std::string str; public: Example(const std::string& s) { str = s; // 等效于:str默认构造 → 调用operator= } }; ``` - **性能问题**:对非平凡类型(如`std::string`),默认构造可能分配内存,后续赋值需释放旧内存并复制数据[^1]。 ##### 2. 列表初始化的优势 直接调用成员的**拷贝/移动构造函数**,避免冗余操作: ```cpp class Example { std::string str; public: Example(const std::string& s) : str(s) {} // 直接调用拷贝构造函数 }; ``` --- #### 三、默认构造函数的行为 1. **隐式生成的默认构造函数** - 若未定义任何构造函数,编译器自动生成一个**空实现的默认构造函数**。 - **限制**:若类包含`const`成员或引用成员,必须手动定义构造函数[^2]。 2. **默认构造的触发场景** - 对象声明不带参数:`MyClass obj;` - 成员变量未在初始化列表中显式初始化时,调用其默认构造函数。 --- #### 四、临时变量的生命周期 临时变量在表达式结束时立即析构,遵循**构造与析构的对称性**: ```cpp class Temp { public: Temp() { cout << "临时构造\n"; } ~Temp() { cout << "临时析构\n"; } }; void process(Temp t) {} // 传参时生成临时变量 int main() { process(Temp()); // 输出:临时构造 → 临时析构(函数返回前) cout << "main结束\n"; return 0; } ``` **输出顺序**: ``` 临时构造 → 临时析构 → main结束 ``` --- #### 五、析构函数执行顺序 析构函数执行顺序与构造函数**严格相反**: 1. **派生类析构函数** 2. **成员变量析构函数**(按声明逆序) 3. **基类析构函数**(按继承逆序) **示例**: ```cpp class Member { public: ~Member() { cout << "Member析构\n"; } }; class Base { public: ~Base() { cout << "Base析构\n"; } }; class Derived : public Base { Member m; public: ~Derived() { cout << "Derived析构\n"; } }; int main() { Derived d; // 构造顺序:Base → Member → Derived return 0; } // 析构顺序:Derived → Member → Base ``` **输出**: ``` Derived析构 Member析构 Base析构 ``` --- #### 六、关键问题扩展 1. **如何强制调用特定构造函数?** ```cpp class MyClass { int x; public: MyClass() : x(0) {} // 默认构造 MyClass(int val) : x(val) {}// 参数化构造 }; MyClass obj1; // 调用默认构造 MyClass obj2(10); // 调用参数化构造 MyClass obj3 = MyClass(); // 显式调用默认构造 ``` 2. **临时变量优化(RVO/NRVO)** - 编译器可能优化返回临时对象的拷贝操作,直接构造在目标位置: ```cpp MyClass create() { return MyClass(); // 编译器优化为直接构造在调用处 } ``` 3. **explicit关键字的作用** - 禁止隐式转换,避免意外的临时变量生成: ```cpp class Strict { public: explicit Strict(int x) {} }; Strict s1 = 5; // 错误:explicit禁止隐式构造 Strict s2(5); // 合法 ``` --- ### 总结 - **构造顺序**:基类 → 成员 → 自身 - **析构顺序**:自身 → 成员 → 基类 - **列表初始化**直接调用构造函数,性能优于赋值初始化[^1] - **临时变量**的析构在表达式结束时立即触发 - **默认构造函数**在未定义时由编译器生成,但受成员类型限制[^2] --- ### 相关问题 1. 如何避免隐式调用默认构造函数? 2. 临时变量在C++17后有哪些生命周期变化? 3. 多继承场景下构造函数执行顺序如何确定? 4. 移动构造函数如何影响临时变量的性能? [^1]: 列表初始化减少冗余操作,提升非平凡类型初始化效率。 [^2]: 默认构造函数对const/引用成员的限制要求显式初始化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值