構(gòu)造管“生”對象?析構(gòu)管“埋”對象?C++中構(gòu)造析構(gòu)還沒整明白?
在談類的構(gòu)造前,先聊聊面向?qū)ο缶幊膛c面向過程的個人體會。
面向過程策略
要談這個問題,先來對比一下傳統(tǒng)面向過程的編程策略:
比較典型的–C, Pascal, Basic, Fortran語言,傳統(tǒng)的做法是整了很多函數(shù),整合時主要是整合各函數(shù)的調(diào)用,main函數(shù)協(xié)調(diào)對過程的調(diào)用,并將適當(dāng)?shù)臄?shù)據(jù)作為參數(shù)在調(diào)用間進(jìn)行傳遞流轉(zhuǎn)而實現(xiàn)業(yè)務(wù)需求。信息流轉(zhuǎn)主要靠:
-
函數(shù)傳遞參數(shù) -
函數(shù)返回值
如上圖,傳統(tǒng)面向過程編程語言,比如C語言其編程的主要元為函數(shù),這種語言有這些弊端:
-
程序主要由函數(shù)組成,重用性較差。比如直接將從一個程序源文件中復(fù)制一個函數(shù)并在另一個程序中重用是非常困難的,因為該函數(shù)可能需要包含頭文件,引用其他全局變量或者其他函數(shù)。換句話說,函數(shù)沒有很好地封裝為獨立的可重用單元。相互牽扯、相互耦合比較復(fù)雜。 -
過程編程語言不適合用于解決現(xiàn)實生活中問題的高級抽象。例如,C程序使用諸如if-else,for循環(huán),array,function,pointer之類的結(jié)構(gòu),這些結(jié)構(gòu)是低級的并且很難抽象出諸如客戶關(guān)系管理(CRM)系統(tǒng)或足球?qū)崨r游戲之類的實際問題。(想象一下,使用匯編代碼來實現(xiàn)足球?qū)崨r游戲,酸爽否?顯然C語言比匯編好,但還不是足夠好)
當(dāng)然如C語言開發(fā),現(xiàn)在的編程策略已經(jīng)大量引入了面向?qū)ο缶幊滩呗粤?,但是要實現(xiàn)這些對象編程策略,則開發(fā)人員本身需要去實現(xiàn)這些面向?qū)ο蟊旧淼牟呗?,需要自己手撕實現(xiàn)這些基礎(chǔ)的對象思想。所以這里僅僅就語言本身特征做為說明,不必糾結(jié)。而這些策略如果由編程語言本身去實現(xiàn),則顯然是一個更優(yōu)異的解決方案。但是比如嵌入式領(lǐng)域為嘛不直接用C++呢,而往往更多采用C的方式采用對象策略來擼代碼呢? 嵌入式領(lǐng)域更多需要與底層硬件打交道,而C語言本身抽象層級相對更適合這種場景,結(jié)合對象策略編程則可以兼顧重用封裝等考量。
回到技術(shù)發(fā)展歷史來看,1970年代初期,美國國防部(DoD)成立了一個工作隊,調(diào)查其IT預(yù)算為何總是失控的原因,其調(diào)查結(jié)果是:
-
預(yù)算的80%用于軟件(其余20%用于硬件)。 -
超過80%的軟件預(yù)算用于維護(hù)(僅剩余的20%用于新軟件開發(fā))。 -
硬件組件可以應(yīng)用于各種產(chǎn)品,其完整性通常不會影響其他產(chǎn)品。(硬件可以共享和重用!硬件故障是隔離的!) -
軟件過程通常是不可共享且不可重用的。軟件故障可能會影響計算機(jī)中運行的其他程序。
而面向?qū)ο缶幊陶Z言則很好的解決了這些弊端:
-
OOP的基本單元是一個類,該類將靜態(tài)屬性和動態(tài)行為封裝在一個“黑盒”里,并開放使用這些黑盒的公共接口。由于類相對函數(shù)具有更好的封裝,因此重用這些類更加容易。換句話說,OOP將同一黑盒中的軟件實體的數(shù)據(jù)結(jié)構(gòu)和算法組合在一起。 良好的類封裝不需要去讀實現(xiàn)代碼,只要知道接口怎么用,實現(xiàn)真正意義上的造磚,哪里需要哪里搬! -
OOP語言允許更高級別的抽象來解決現(xiàn)實生活中的問題。傳統(tǒng)的過程語言例如C需要程序猿根據(jù)計算機(jī)的結(jié)構(gòu)(例如內(nèi)存位和字節(jié),數(shù)組,決策,循環(huán))進(jìn)行思考,而不是根據(jù)您要解決的問題進(jìn)行思考。OOP語言(例如Java,C ++,C#)使開發(fā)人員可以在問題空間中進(jìn)行思考,并使用軟件對象來表示和抽象問題空間的實體進(jìn)行建模從而解決問題。 因此OOP會更聚焦于問題域!
面向?qū)ο蟛呗?/span>
而現(xiàn)代面向?qū)ο缶幊陶Z言(OOP: Object-Oriented Programming) ,從語言本身角度,其編程的場景則變成下面一種截然不一樣的畫風(fēng):
程序的運行態(tài):是不同的實例化對象一起協(xié)作干活的場景
應(yīng)用程序通過對象間消息傳遞,對象執(zhí)行自身的行為進(jìn)而實現(xiàn)業(yè)務(wù)需求。編寫代碼,設(shè)計類,撰寫類的代碼,然而應(yīng)用程序運行時,卻是以相應(yīng)的類實例化的對象協(xié)作完成邏輯,這就是所謂面向?qū)ο缶幊痰暮x。那么對于對象而言,具有哪些屬性呢?
-
對象是一種抽象,所謂抽象就是類。比如MFC中的Window -
代表現(xiàn)實世界的實體 -
類是定義共享公共屬性或?qū)傩缘臄?shù)據(jù)類型 -
對象是類的實例存在,類本身在程序的運行態(tài)并不存在,以對象存在。 -
對象具有狀態(tài),或者稱為屬性,是運行時值。比如MFC窗體大小,title等 -
對象具有行為,亦稱為方法或者操作。比如常見MFC中窗體具有show方法 -
對象具有消息,通過發(fā)送消息來請求對象執(zhí)行其操作之一,消息是對象之間交換數(shù)據(jù)的方式。
從上面的描述,應(yīng)用程序本質(zhì)是很多對象相互協(xié)作一起在干活,就好比一個車間,有機(jī)器、工人、工具等一起相互在一起產(chǎn)生相互作用,從而完成某項產(chǎn)品的制造。那么,這些對象從哪里來呢?
對象來自于類的實例化,誰負(fù)責(zé)實例化對象呢?這就是類中構(gòu)造函數(shù)干的活,那么析構(gòu)函數(shù)就是銷毀對象的。所以構(gòu)造函數(shù)管生,析構(gòu)函數(shù)管埋。
構(gòu)造管?“生”
構(gòu)造函數(shù)按照類的樣式生成對象,也稱為實例化對象,那么C++中有哪幾種構(gòu)造函數(shù)呢?
構(gòu)造函數(shù)的相同點:
-
函數(shù)名都與類的名字一樣; -
構(gòu)造函數(shù)都沒有返回類型; -
創(chuàng)建對象時會自動調(diào)用構(gòu)造函數(shù);
那為嘛又要整這么幾個不同的構(gòu)造函數(shù)呢?舉個生活中你或許遇到過的栗子:
-
Case 1: 打比方你去商店對售貨員說買個燈泡,沒有什么附加信息(比如品牌、功率、發(fā)光類型、尺寸等),那么售貨員把常見的燈泡給你一個,這就相當(dāng)于C++語言中創(chuàng)建對象時,按照默認(rèn)方式創(chuàng)建,故調(diào)用默認(rèn)構(gòu)造函數(shù)。 -
Case 2: 拿到燈之后回家一裝,擦,太大了裝不上!這回你聰明了,量了下安裝尺寸,跑去給售貨員說你要XX大小的燈,此時就相當(dāng)于C++利用參數(shù)化構(gòu)造函數(shù)實例化對象。 -
Case 3: 擦,用了很久燈又掛了,這回你更聰明了,把壞燈卸下來帶到商店照著買一個,這場景就有點像C++中的拷貝構(gòu)造函數(shù)了~。
那么,到底不同的構(gòu)造函數(shù)有些什么不同呢?為嘛C++語言設(shè)計這么多種不同的構(gòu)造函數(shù)呢?
-
默認(rèn)構(gòu)造函數(shù):默認(rèn)構(gòu)造函數(shù)不帶任何參數(shù)。 如果不指定構(gòu)造函數(shù),則C ++編譯器會為我們生成一個默認(rèn)構(gòu)造函數(shù)(不帶參數(shù),并且具有空主體)。 -
參數(shù)化構(gòu)造函數(shù):參數(shù)傳遞給構(gòu)造函數(shù),這些參數(shù)用于創(chuàng)建對象時初始化對象。要實現(xiàn)參數(shù)化構(gòu)造函數(shù),只需像向其他函數(shù)一樣向其添加參數(shù)即可。定義構(gòu)造函數(shù)的主體時,使用參數(shù)初始化對象的數(shù)據(jù)成員。 -
拷貝構(gòu)造函數(shù):拷貝構(gòu)造函數(shù),顧名思義,就是按照現(xiàn)有對象一模一樣克隆出一個新的對象。其參數(shù)一般為現(xiàn)有對象的引用,一般具有如下函數(shù)原型:
//函數(shù)名為類名,參數(shù)為原對象const引用
ClassName(const?ClassName?&old_object);?
析構(gòu)管“埋”
析構(gòu)函數(shù)通常用于釋放內(nèi)存,并在銷毀對象時對類對象及其類成員進(jìn)行其他清理操作。當(dāng)類對象超出生命周期范圍或被顯式刪除時,將為該類對象調(diào)用析構(gòu)函數(shù)。
那么析構(gòu)函數(shù)具有哪些特點呢?
銷毀對象時,將自動調(diào)用析構(gòu)函數(shù)。 不能將其聲明為static或const。 析構(gòu)函數(shù)沒有參數(shù),也沒有返回類型。 具有析構(gòu)函數(shù)的類的對象不能成為聯(lián)合的成員。 析構(gòu)函數(shù)應(yīng)在該類的public部中聲明。 程序員無法訪問析構(gòu)函數(shù)的地址。 一個類有且僅有一個析構(gòu)函數(shù)。 如果沒有顯式定義析構(gòu)函數(shù),編譯器會自動生成一個默認(rèn)的析構(gòu)函數(shù)。
既然析構(gòu)函數(shù)是構(gòu)造函數(shù)的反向操作,對于對象管"埋",那么什么時候“埋”呢?
-
函數(shù)返回退出 -
程序被關(guān)掉,比如一個應(yīng)用程序被kill -
包含局部對象的塊結(jié)尾 -
主動調(diào)用刪除符delete
前面說如果程序猿沒有顯式定義析構(gòu)函數(shù),編譯器會自動生成一個默認(rèn)的析構(gòu)函數(shù)。言下之意是有的時候需要顯式定義析構(gòu)函數(shù),那么什么時候需要呢?
當(dāng)類中動態(tài)分配了內(nèi)存時,或當(dāng)一個類包含指向在該類中分配的內(nèi)存的指針時,應(yīng)該編寫一個析構(gòu)函數(shù)以釋放該類實例之前的內(nèi)存。否則會造成內(nèi)存泄漏。
“生”與“埋”舉例
前面說構(gòu)造管“生”,析構(gòu)管“埋”,那么到底怎么“生”的呢?怎么“埋”呢?,看看栗子:
#include?
using?namespace?std;
class?Rectangle
{
public:?
?Rectangle();?
?Rectangle(int?w,?int?l);
? Rectangle(const?Rectangle?&rct)?{width?=?rct.width;?length?=?rct.length;?}
?~Rectangle();
public:
? int?width,?length;
};
Rectangle::Rectangle()
{
? cout?<"默認(rèn)矩形誕生了!"?<endl;
}
Rectangle::Rectangle(int?w,?int?l)
{
?width ?=?w;
? length?=?l;
?cout?<"指定矩形誕生了!"?<endl;
}
Rectangle::~Rectangle()
{
? cout?<"矩形埋掉了!"?<endl;
}
int?main()
{?
? Rectangle?rct1;
? Rectangle?*pRct?=?new?Rectangle(2,3);
?? Rectangle?rct2??=?rct1;
??
?return?0;
}
這個簡單的代碼,實際運行的輸出結(jié)果:
默認(rèn)矩形誕生了!
指定矩形誕生了!
矩形埋掉了!
矩形埋掉了!
技術(shù)人總是喜歡眼見為實:因為看見,所以相信!,看看其對應(yīng)的匯編代碼(VC++ 2010匯編結(jié)果,這里僅貼出main函數(shù),僅為理解原理,對于匯編指令不做描述,其中#為對匯編注釋):
31: int main()
32: {
012C1660 55 push ebp
012C1661 8B EC mov ebp,esp
012C1663 6A FF push 0FFFFFFFFh
012C1665 68 76 53 2C 01 push offset __ehhandler$_main (12C5376h)
012C166A 64 A1 00 00 00 00 mov eax,dword ptr fs:[00000000h]
012C1670 50 push eax
012C1671 81 EC 14 01 00 00 sub esp,114h
012C1677 53 push ebx
012C1678 56 push esi
012C1679 57 push edi
012C167A 8D BD E0 FE FF FF lea edi,[ebp-120h]
012C1680 B9 45 00 00 00 mov ecx,45h
012C1685 B8 CC CC CC CC mov eax,0CCCCCCCCh
012C168A F3 AB rep stos dword ptr es:[edi]
012C168C A1 00 90 2C 01 mov eax,dword ptr [___security_cookie (12C9000h)]
012C1691 33 C5 xor eax,ebp
012C1693 50 push eax
012C1694 8D 45 F4 lea eax,[ebp-0Ch]
012C1697 64 A3 00 00 00 00 mov dword ptr fs:[00000000h],eax
33: Rectangle rct1;
012C169D 8D 4D E8 lea ecx,[ebp-18h]
#調(diào)用默認(rèn)構(gòu)造函數(shù)管“生”
012C16A0 E8 32 FA FF FF call Rectangle::Rectangle (12C10D7h)
012C16A5 C7 45 FC 00 00 00 00 mov dword ptr [ebp-4],0
34: Rectangle *pRct = new Rectangle(2,3);
012C16AC 6A 08 push 8
012C16AE E8 41 FB FF FF call operator new (12C11F4h)
012C16B3 83 C4 04 add esp,4
012C16B6 89 85 F4 FE FF FF mov dword ptr [ebp-10Ch],eax
012C16BC C6 45 FC 01 mov byte ptr [ebp-4],1
012C16C0 83 BD F4 FE FF FF 00 cmp dword ptr [ebp-10Ch],0
012C16C7 74 17 je main+80h (12C16E0h)
012C16C9 6A 03 push 3 #傳參
012C16CB 6A 02 push 2 #傳參
012C16CD 8B 8D F4 FE FF FF mov ecx,dword ptr [ebp-10Ch]
#調(diào)用參數(shù)化構(gòu)造函數(shù)
012C16D3 E8 B8 FA FF FF call Rectangle::Rectangle (12C1190h)
012C16D8 89 85 E0 FE FF FF mov dword ptr [ebp-120h],eax
012C16DE EB 0A jmp main+8Ah (12C16EAh)
012C16E0 C7 85 E0 FE FF FF 00 00 00 00 mov dword ptr [ebp-120h],0
012C16EA 8B 85 E0 FE FF FF mov eax,dword ptr [ebp-120h]
012C16F0 89 85 E8 FE FF FF mov dword ptr [ebp-118h],eax
012C16F6 C6 45 FC 00 mov byte ptr [ebp-4],0
012C16FA 8B 8D E8 FE FF FF mov ecx,dword ptr [ebp-118h]
012C1700 89 4D DC mov dword ptr [ebp-24h],ecx
35: Rectangle rct2 = rct1;
012C1703 8D 45 E8 lea eax,[ebp-18h]
012C1706 50 push eax
012C1707 8D 4D CC lea ecx,[ebp-34h]
#調(diào)用拷貝構(gòu)造函數(shù)
012C170A E8 3C F9 FF FF call Rectangle::Rectangle (12C104Bh)
36:
37: return 0;
012C170F C7 85 00 FF FF FF 00 00 00 00 mov dword ptr [ebp-100h],0
012C1719 8D 4D CC lea ecx,[ebp-34h]
#調(diào)用析構(gòu)函數(shù),銷毀rct2
012C171C E8 15 FA FF FF call Rectangle::~Rectangle (12C1136h)
012C1721 C7 45 FC FF FF FF FF mov dword ptr [ebp-4],0FFFFFFFFh
012C1728 8D 4D E8 lea ecx,[ebp-18h]
#調(diào)用析構(gòu)函數(shù),銷毀rct1
012C172B E8 06 FA FF FF call Rectangle::~Rectangle (12C1136h)
012C1730 8B 85 00 FF FF FF mov eax,dword ptr [ebp-100h]
38: }
這里引發(fā)幾個問題:
問題1:為什么先析構(gòu)rct2,后析構(gòu)rct1呢?
這是由于這兩個對象在棧上分配內(nèi)存,所以基于棧的特性,顯然rct2位于C++運行時棧的頂部,而rct1位于棧底。
你如不信,將上述代碼修改一下,測測:
Rectangle::~Rectangle()
{
????cout?<<"當(dāng)前寬為:"?<"矩形埋掉了!"?<}
int?main()
{?
?Rectangle?rct1;
? rct1.width?=?1;
? Rectangle?*pRct?=?new?Rectangle(2,3);
? ?Rectangle?rct2??=?rct1;
? ?rct2.width?=?2;
?return?0;
}
其輸出結(jié)果為:
默認(rèn)矩形誕生了!
指定矩形誕生了!
當(dāng)前寬為:2矩形埋掉了!
當(dāng)前寬為:1矩形埋掉了!
問題2:請問上述代碼,構(gòu)造函數(shù)被調(diào)用了幾次?析構(gòu)函數(shù)又被調(diào)用了幾次?這是經(jīng)常面試會考察的基礎(chǔ)知識。顯然前面的打印以及給出了答案。
問題3:該代碼有啥隱患?
答:調(diào)用了new,而沒有調(diào)用delete,為啥這是隱患,new屬于動態(tài)申請內(nèi)存,是在堆上為對象申請內(nèi)存,這屬于典型的管“生”不管“埋”,造成內(nèi)存泄漏,如果整的多了,**必然尸體埋 “堆”!**造成程序引發(fā)不可預(yù)料的崩潰!
所以應(yīng)該修正一下:
Rectangle::~Rectangle()
{
????cout?<<"當(dāng)前寬為:"?<"矩形埋掉了!"?<endl;
}
int?main()
{?
?Rectangle?rct1;
??rct1.width?=?1;
?Rectangle?*pRct?=?new?Rectangle(2,3);
? ?Rectangle?rct2??=?rct1;
? ?rct2.width?=?3;
?delete?pRct;
?cout?<"手動埋掉!"?<endl;
?return?0;
}
看看輸出結(jié)果:
默認(rèn)矩形誕生了!
指定矩形誕生了!
當(dāng)前寬為:2矩形埋掉了!
手動埋掉!
當(dāng)前寬為:3矩形埋掉了!
當(dāng)前寬為:1矩形埋掉了!
總結(jié)一下
-
OOP的基本單元是類,該類將靜態(tài)屬性和動態(tài)行為封裝在一個黑盒中,并指定使用這些框的公共接口。由于該類具有很好的封裝(與功能相比),因此重用這些類會更加容易。換句話說,OOP將同一黑盒中軟件實體的數(shù)據(jù)結(jié)構(gòu)和算法較好結(jié)合在一起。 -
OOP語言允許更高級別的抽象來解決現(xiàn)實生活中的問題。傳統(tǒng)的過程語言例如C迫使您根據(jù)計算機(jī)的結(jié)構(gòu)(例如內(nèi)存位和字節(jié),數(shù)組,決策,循環(huán))進(jìn)行思考,而不是根據(jù)您要解決的問題進(jìn)行思考。OOP語言(例如Java,C ++,C#)使您可以在問題空間中進(jìn)行思考,并使用軟件對象來表示和抽象問題空間的實體以解決問題。 -
對于C++語言,構(gòu)造函數(shù)與析構(gòu)函數(shù)是基礎(chǔ)中的基礎(chǔ),類在運行態(tài)并不存在,類以對象形式在運行態(tài)實現(xiàn)業(yè)務(wù)需求。對象如何按照類黑盒樣式如何在運行態(tài)誕生,利用類的構(gòu)造函數(shù)而誕生,對象生存期結(jié)束,析構(gòu)函數(shù)管“埋”,銷毀對象。 -
對于局部對象,非new產(chǎn)生的對象,誕生地為棧,在棧中誕生,編譯器會插入析構(gòu)函數(shù)使得程序運行態(tài)在對象生命周期結(jié)束時自動管“埋”,而如果利用new動態(tài)創(chuàng)建的對象,則需要手動管“埋”,如手動管“生”(new),不手動管“埋”(delete),對象必成孤魂野鬼,嚴(yán)重時,對象尸體滿“堆”。
對于拷貝構(gòu)造函數(shù),還有一個所謂深拷貝、淺拷貝的要點沒有涉及,下次學(xué)習(xí)總結(jié)分享一下,敬請關(guān)注期待~,如發(fā)現(xiàn)文中有錯誤,敬請留言指正,不勝感激~
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個人觀點,不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!