C++系列总结——构造与析构
前言
在使用資源前,我們需要做一些準(zhǔn)備工作保證資源能正常使用,在使用完資源后,我們需要做一些掃尾工作保證資源沒有泄露,這就是構(gòu)造與析構(gòu)了,這和編程語(yǔ)言是無(wú)關(guān)的,而是使用資源的一種方式。C++只不過是把這個(gè)過程內(nèi)置到了語(yǔ)言本身,規(guī)定了構(gòu)造函數(shù)和析構(gòu)函數(shù)的形式以及執(zhí)行時(shí)機(jī)。
編譯器的無(wú)私奉獻(xiàn)
下面這段代碼很好理解
#include <iostream> class A { public:A(){std::cout << "A\n";}~A(){std::cout << "~A\n";} }; int main() {A local;return 0; }如果執(zhí)行的話,會(huì)輸出
A ~A對(duì)于一個(gè)從C轉(zhuǎn)到C++的人,我就很糾結(jié)為什么我沒有調(diào)用A::A()和A::~A(),它們卻執(zhí)行了。
在GDB面前,程序是沒有秘密的,因此就讓我們開始GDB,看看剝?nèi)ジ呒?jí)語(yǔ)言的外衣后的程序是什么樣子。
用GDB的disassemble命令查看匯編代碼,可以看到實(shí)際上調(diào)用了A::A()(callq 0x400888 <A::A()>)和A::~A()(callq 0x4008a6 <A::~A()>)。果然沒有任何神奇的地方,函數(shù)都是需要被調(diào)用才會(huì)執(zhí)行的,只不過我沒有做的時(shí)候,編譯器幫我做了。
編譯器除了幫我們調(diào)用構(gòu)造函數(shù)和析構(gòu)函數(shù)外,如果我們沒有寫構(gòu)造函數(shù)和析構(gòu)函數(shù),編譯器會(huì)幫我們補(bǔ)上默認(rèn)的構(gòu)造函數(shù)和析構(gòu)函數(shù)嗎?
在下面的情況下,編譯器會(huì)幫我們補(bǔ)上默認(rèn)的構(gòu)造函數(shù)
- 類成員變量有構(gòu)造函數(shù):默認(rèn)的構(gòu)造函數(shù)里就是為了調(diào)用一下類成員變量的構(gòu)造函數(shù)
- 類的父類有構(gòu)造函數(shù):默認(rèn)的構(gòu)造函數(shù)就是為了調(diào)用一下父類的構(gòu)造函數(shù)。父類是否有默認(rèn)構(gòu)造函數(shù),同樣取決于上一種情況。
- 類的父類有虛函數(shù):默認(rèn)的構(gòu)造函數(shù)就是為了設(shè)置一下虛函數(shù)表
在下面的情況下,編譯器會(huì)幫我們補(bǔ)上默認(rèn)的析構(gòu)函數(shù)
- 類成員變量有自己的析構(gòu)函數(shù):默認(rèn)的析構(gòu)函數(shù)里就只是為了調(diào)用一下類成員變量的析構(gòu)函數(shù)
- 類的父類有自己的析構(gòu)函數(shù):默認(rèn)的析構(gòu)函數(shù)為了調(diào)用父類的析構(gòu)函數(shù)
從上面我們也可以看出編譯器不做無(wú)用之事。當(dāng)不需要構(gòu)造函數(shù)或析構(gòu)函數(shù)時(shí),編譯器就不會(huì)補(bǔ)上默認(rèn)的構(gòu)造函數(shù)和析構(gòu)函數(shù)。我們知道C語(yǔ)言中是沒有構(gòu)造函數(shù)和析構(gòu)函數(shù)的,可以簡(jiǎn)單的認(rèn)為符合C語(yǔ)言語(yǔ)法的自定義類型,編譯器都不會(huì)補(bǔ)上默認(rèn)的構(gòu)造函數(shù)和析構(gòu)函數(shù)。大家可以了解下POD類型。
”符合C語(yǔ)言語(yǔ)法的自定義類型“的描述是不準(zhǔn)確的,這是在將class視為struct,忽略權(quán)限關(guān)鍵字public、protected、private的基礎(chǔ)上說的,畢竟C中沒有這些關(guān)鍵字。
構(gòu)造和析構(gòu)的時(shí)機(jī)
下面描述的前提是存在構(gòu)造函數(shù)和析構(gòu)函數(shù)
當(dāng)實(shí)例化對(duì)象時(shí),會(huì)執(zhí)行構(gòu)造函數(shù),而實(shí)例化對(duì)象分為兩種情況
- 定義變量,如A a;。需要特別注意的是通過thread_local修飾定義的變量,在首次在線程中使用時(shí)才會(huì)執(zhí)行構(gòu)造函數(shù)。
new實(shí)例化,如new A;
構(gòu)造函數(shù)是無(wú)法主動(dòng)調(diào)用的
當(dāng)對(duì)象存儲(chǔ)期結(jié)束時(shí),就會(huì)執(zhí)行析構(gòu)函數(shù),存儲(chǔ)期分為
- 靜態(tài)存儲(chǔ)期:進(jìn)程退出時(shí)執(zhí)行析構(gòu)函數(shù),如全局變量和靜態(tài)局部變量
- 自動(dòng)存儲(chǔ)期:離開變量的作用域時(shí)執(zhí)行析構(gòu)函數(shù),如普通局部變量
- 動(dòng)態(tài)存儲(chǔ)期:new實(shí)例化的對(duì)象,在delete時(shí)會(huì)執(zhí)行析構(gòu)函數(shù)。
線程存儲(chǔ)期:線程退出時(shí)執(zhí)行析構(gòu)函數(shù),如thread_local修飾的變量
因?yàn)槲鰳?gòu)函數(shù)是可以主動(dòng)調(diào)用的,所以delete也可以只釋放內(nèi)存而不調(diào)用析構(gòu)函數(shù)。
構(gòu)造和析構(gòu)的順序
順序就一句話:先構(gòu)造后析構(gòu)。分兩部分來理解
- 為什么需要先構(gòu)造后析構(gòu)
如何實(shí)現(xiàn)先構(gòu)造后析構(gòu)
先構(gòu)造意味著先定義,但這只在同一文件中生效,不同文件之間的全局變量構(gòu)造順序是不確定的。
為什么需要先構(gòu)造后析構(gòu)
原因很樸素:先構(gòu)造的對(duì)象說明其可能會(huì)被后續(xù)的對(duì)象使用,因此為了程序運(yùn)行安全,必須等到其使用者結(jié)束使用后,才能析構(gòu)該對(duì)象即在那些之后構(gòu)造的對(duì)象析構(gòu)后才能析構(gòu)。
先構(gòu)造后析構(gòu)也是保證我們安全使用資源的一個(gè)原則。假設(shè)我們把一個(gè)功能的初始化封裝為init(),把功能的銷毀封裝為destroy(),一般destroy()中資源銷毀的順序是init()中資源申請(qǐng)的逆序。
基于以上,我們就能很容易的理解
- 為什么父類的構(gòu)造函數(shù)先執(zhí)行:因?yàn)楸绢惖臉?gòu)造函數(shù)可能要用到父類的東西
- 為什么類成員變量的構(gòu)造函數(shù)先執(zhí)行:因?yàn)楸绢惖臉?gòu)造函數(shù)內(nèi)可能要用到類成員變量
如何實(shí)現(xiàn)先構(gòu)造后析構(gòu)
普通局部變量的先構(gòu)造后析構(gòu),就是編譯器按照定義順序插入對(duì)應(yīng)的構(gòu)造函數(shù),然后再逆序插入析構(gòu)函數(shù)。
int main() {A local_1;A local_2;return 0; }其匯編如下
0x000000000040088f <+9>: lea -0x12(%rbp),%rax 0x0000000000400893 <+13>: mov %rax,%rdi # local_1的地址 0x0000000000400896 <+16>: callq 0x40093c <A::A()> 0x000000000040089b <+21>: lea -0x11(%rbp),%rax 0x000000000040089f <+25>: mov %rax,%rdi # local_2的地址 0x00000000004008a2 <+28>: callq 0x40093c <A::A()> 0x00000000004008a7 <+33>: mov $0x0,%ebx 0x00000000004008ac <+38>: lea -0x11(%rbp),%rax 0x00000000004008b0 <+42>: mov %rax,%rdi # local_2的地址 0x00000000004008bf <+57>: callq 0x40095a <A::~A()> 0x00000000004008c4 <+62>: mov %ebx,%eax 0x00000000004008c6 <+64>: jmp 0x4008e2 <main()+92> 0x00000000004008c8 <+66>: mov %rax,%rbx 0x00000000004008cb <+69>: lea -0x12(%rbp),%rax 0x00000000004008cf <+73>: mov %rax,%rdi # local_1的地址 0x00000000004008d2 <+76>: callq 0x40095a <A::~A()>全局變量和靜態(tài)局部變量在析構(gòu)函數(shù)的設(shè)置上稍有差別。
A global; int main() {return 0; }因?yàn)槿肿兞康臉?gòu)造在main()之前,所以在A::A()上設(shè)置斷點(diǎn)。執(zhí)行后,打印調(diào)用棧如下
(gdb) bt #0 A::A (this=0x601171 <gloabl>) at main.cpp:15 #1 0x0000000000400856 in __static_initialization_and_destruction_0 ( __initialize_p=1, __priority=65535) at main.cpp:23 #2 0x0000000000400880 in _GLOBAL__sub_I_gloabl () at main.cpp:27 #3 0x000000000040090d in __libc_csu_init () #4 0x00007ffff771fe55 in __libc_start_main (main=0x400806 <main()>, argc=1, argv=0x7fffffffec48, init=0x4008c0 <__libc_csu_init>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffec38) at libc-start.c:246 #5 0x0000000000400739 in _start ()讓我們回到frame 1截取一小段它的匯編代碼
0x0000000000400851 <+64>: callq 0x400882 <A::A()> => 0x0000000000400856 <+69>: mov $0x601058,%edx 0x000000000040085b <+74>: mov $0x601171,%esi 0x0000000000400860 <+79>: mov $0x4008a0,%edi 0x0000000000400865 <+84>: callq 0x4006d0 <__cxa_atexit@plt>我們發(fā)現(xiàn)在執(zhí)行完A::A()后,還調(diào)用了__cxa_atexit@plt。看到__cxa_atexit@plt,有沒有覺得很熟悉,是不是立即就想到int atexit( void (*function)(void))。我們知道atexit()用于注冊(cè)一個(gè)在進(jìn)程退出時(shí)執(zhí)行的函數(shù),那么這里是不是注冊(cè)了全局變量的析構(gòu)函數(shù)?
mov $0x4008a0,%edi就是給`__cxa_atexit@plt傳遞參數(shù),可知注冊(cè)的函數(shù)地址是0x4008a0。我們可以輕易地發(fā)現(xiàn)A::~A()的地址就是0x4008a0。
類成員函數(shù)的第一個(gè)參數(shù)是this,mov $0x601171,%esi的0x601171就是變量global的地址。
由此我們知道全局變量的析構(gòu)函數(shù)是在執(zhí)行構(gòu)造函數(shù)后,注冊(cè)為進(jìn)程退出時(shí)的執(zhí)行函數(shù)。我們知道通過atexit()注冊(cè)的函數(shù)的執(zhí)行順序是先注冊(cè)的后執(zhí)行即FILO,__cxa_atexit也是一樣,也就實(shí)現(xiàn)了先構(gòu)造后析構(gòu)。靜態(tài)局部變量的析構(gòu)函數(shù)也是一樣的設(shè)置方式。
假設(shè)一個(gè)類繼承自一個(gè)有構(gòu)造函數(shù)的類,且其成員變量也擁有構(gòu)造函數(shù)。我們知道會(huì)先執(zhí)行父類和成員變量的構(gòu)造函數(shù),然后再執(zhí)行本類的構(gòu)造函數(shù)。按這樣的描述,豈不是要在每個(gè)實(shí)例化該類的地方加上很多額外的代碼了,編譯器會(huì)這么蠢么?讓我們來看一下
class A { public:A(){}~A(){} }; class B { public:B(){}~B(){} }; class C : public A { public:C(){ std::cout << "C\n" };~C(){}B a; }; int main() {C local;return 0; }查看匯編,可知實(shí)際調(diào)用的仍是C::C(),并非在C::C()之前插入A和B的構(gòu)造函數(shù),
0x00000000004006e6 <+16>: callq 0x400788 <C::C()> 0x00000000004006eb <+21>: mov $0x0,%ebx 0x00000000004006f0 <+26>: lea -0x11(%rbp),%rax 0x00000000004006f4 <+30>: mov %rax,%rdi 0x00000000004006f7 <+33>: callq 0x4007b0 <C::~C()>而是在C::C()的第一行代碼前,插入了A和B的構(gòu)造函數(shù)。
Dump of assembler code for function C::C(): 0x0000000000400788 <+0>: push %rbp => 0x0000000000400789 <+1>: mov %rsp,%rbp 0x000000000040078c <+4>: sub $0x10,%rsp 0x0000000000400790 <+8>: mov %rdi,-0x8(%rbp) 0x0000000000400794 <+12>: mov -0x8(%rbp),%rax 0x0000000000400798 <+16>: mov %rax,%rdi 0x000000000040079b <+19>: callq 0x400758 <A::A()> 0x00000000004007a0 <+24>: mov -0x8(%rbp),%rax 0x00000000004007a4 <+28>: mov %rax,%rdi 0x00000000004007a7 <+31>: callq 0x400770 <B::B()> 0x00000000004007ac <+36>: nop 0x00000000004007ad <+37>: leaveq 0x00000000004007ae <+38>: retq當(dāng)然C::~C()的最后一行代碼之后,也會(huì)插入A和B的析構(gòu)函數(shù)。
常見問題
問題通常都來自于錯(cuò)誤的構(gòu)造順序。
一種情況是a.cpp中定義的全局變量A使用了b.cpp中定義的全局變量B,實(shí)際A先構(gòu)造,此時(shí)A使用到了還未構(gòu)造的B,程序會(huì)出現(xiàn)異常。建議是保證不同全局變量之間是獨(dú)立的。如果存在使用關(guān)系,則定義為指針類型,延遲到main()中再按預(yù)期的順序依次實(shí)例化。
還有一種更常見的情況是使用了靜態(tài)局部變量。因?yàn)殪o態(tài)局部變量包含在函數(shù)內(nèi)部,更隱晦,所以更容易出現(xiàn)問題。問題通常是在進(jìn)程退出時(shí)出現(xiàn)的,靜態(tài)局部變量先析構(gòu)了,導(dǎo)致程序異常。
后話
構(gòu)造與析構(gòu)是一種資源使用機(jī)制,我們常用C++的構(gòu)造函數(shù)和析構(gòu)函數(shù)來實(shí)現(xiàn)RAII(Resource Acquisition Is Initialization),保證諸如鎖、內(nèi)存等資源的正確使用和釋放。
以上的代碼都在www.onlinegdb.com上運(yùn)行調(diào)試的。不同平臺(tái),不同編譯器,其底層實(shí)現(xiàn)會(huì)存在差異,高級(jí)語(yǔ)言本就是為了隱藏這些底層差異,因此不必糾結(jié)于具體實(shí)現(xiàn),而是要關(guān)注思維方式。
轉(zhuǎn)載于:https://www.cnblogs.com/yizui/p/10590840.html
總結(jié)
以上是生活随笔為你收集整理的C++系列总结——构造与析构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [八省联考2018]劈配
- 下一篇: (JS基础)DOM:节点类型