構造管“生”對象?析構管“埋”對象?C++中構造析構還沒整明白?

關注、星標嵌入式客棧,精彩及時送達

[導讀] C++語言有時候也拿來寫寫應用代碼,可是居然發現連構造、析構都還沒弄明白,把這糟心的概念整理分享一下。

在談類的構造前,先聊聊面向對象編程與面向過程的個人體會。

面向過程策略

要談這個問題,先來對比一下傳統面向過程的編程策略:

比較典型的–C, Pascal, Basic, Fortran語言,傳統的做法是整了很多函數,整合時主要是整合各函數的調用,main函數協調對過程的調用,並將適當的數據作爲參數在調用間進行傳遞流轉而實現業務需求。信息流轉主要靠:

  • 函數傳遞參數

  • 函數返回值

如上圖,傳統面向過程編程語言,比如C語言其編程的主要元爲函數,這種語言有這些弊端:

  • 程序主要由函數組成,重用性較差。比如直接將從一個程序源文件中複製一個函數並在另一個程序中重用是非常困難的,因爲該函數可能需要包含頭文件,引用其他全局變量或者其他函數。換句話說,函數沒有很好地封裝爲獨立的可重用單元。相互牽扯、相互耦合比較複雜。

  • 過程編程語言不適合用於解決現實生活中問題的高級抽象。例如,C程序使用諸如if-else,for循環,array,function,pointer之類的結構,這些結構是低級的並且很難抽象出諸如客戶關係管理(CRM)系統或足球實況遊戲之類的實際問題。(想象一下,使用匯編代碼來實現足球實況遊戲,酸爽否?顯然C語言比彙編好,但還不是足夠好)

當然如C語言開發,現在的編程策略已經大量引入了面向對象編程策略了,但是要實現這些對象編程策略,則開發人員本身需要去實現這些面向對象本身的策略,需要自己手撕實現這些基礎的對象思想。所以這裏僅僅就語言本身特徵做爲說明,不必糾結。而這些策略如果由編程語言本身去實現,則顯然是一個更優異的解決方案。但是比如嵌入式領域爲嘛不直接用C++呢,而往往更多采用C的方式採用對象策略來擼代碼呢? 嵌入式領域更多需要與底層硬件打交道,而C語言本身抽象層級相對更適合這種場景,結合對象策略編程則可以兼顧重用封裝等考量。

回到技術發展歷史來看,1970年代初期,美國國防部(DoD)成立了一個工作隊,調查其IT預算爲何總是失控的原因,其調查結果是:

  • 預算的80%用於軟件(其餘20%用於硬件)。

  • 超過80%的軟件預算用於維護(僅剩餘的20%用於新軟件開發)。

  • 硬件組件可以應用於各種產品,其完整性通常不會影響其他產品。(硬件可以共享和重用!硬件故障是隔離的!)

  • 軟件過程通常是不可共享且不可重用的。軟件故障可能會影響計算機中運行的其他程序。

而面向對象編程語言則很好的解決了這些弊端:

  • OOP的基本單元是一個類,該類將靜態屬性和動態行爲封裝在一個“黑盒”裏,並開放使用這些黑盒的公共接口。由於類相對函數具有更好的封裝,因此重用這些類更加容易。換句話說,OOP將同一黑盒中的軟件實體的數據結構和算法組合在一起。良好的類封裝不需要去讀實現代碼,只要知道接口怎麼用,實現真正意義上的造磚,哪裏需要哪裏搬!

  • OOP語言允許更高級別的抽象來解決現實生活中的問題。傳統的過程語言例如C需要程序猿根據計算機的結構(例如內存位和字節,數組,決策,循環)進行思考,而不是根據您要解決的問題進行思考。OOP語言(例如Java,C ++,C#)使開發人員可以在問題空間中進行思考,並使用軟件對象來表示和抽象問題空間的實體進行建模從而解決問題。因此OOP會更聚焦於問題域!

面向對象策略

而現代面向對象編程語言(OOP: Object-Oriented Programming) ,從語言本身角度,其編程的場景則變成下面一種截然不一樣的畫風:

程序的運行態:是不同的實例化對象一起協作幹活的場景

應用程序通過對象間消息傳遞,對象執行自身的行爲進而實現業務需求。編寫代碼,設計類,撰寫類的代碼,然而應用程序運行時,卻是以相應的類實例化的對象協作完成邏輯,這就是所謂面向對象編程的含義。那麼對於對象而言,具有哪些屬性呢?

  • 對象是一種抽象,所謂抽象就是類。比如MFC中的Window

    • 代表現實世界的實體

    • 類是定義共享公共屬性或屬性的數據類型

    • 對象是類的實例存在,類本身在程序的運行態並不存在,以對象存在。

  • 對象具有狀態,或者稱爲屬性,是運行時值。比如MFC窗體大小,title等

  • 對象具有行爲,亦稱爲方法或者操作。比如常見MFC中窗體具有show方法

  • 對象具有消息,通過發送消息來請求對象執行其操作之一,消息是對象之間交換數據的方式。

從上面的描述,應用程序本質是很多對象相互協作一起在幹活,就好比一個車間,有機器、工人、工具等一起相互在一起產生相互作用,從而完成某項產品的製造。那麼,這些對象從哪裏來呢?

對象來自於類的實例化,誰負責實例化對象呢?這就是類中構造函數乾的活,那麼析構函數就是銷燬對象的。所以構造函數管生,析構函數管埋

構造管 “生”

構造函數按照類的樣式生成對象,也稱爲實例化對象,那麼C++中有哪幾種構造函數呢?

構造函數的相同點:

  • 函數名都與類的名字一樣;

  • 構造函數都沒有返回類型;

  • 創建對象時會自動調用構造函數;

那爲嘛又要整這麼幾個不同的構造函數呢?舉個生活中你或許遇到過的栗子:

  • Case 1: 打比方你去商店對售貨員說買個燈泡,沒有什麼附加信息(比如品牌、功率、發光類型、尺寸等),那麼售貨員把常見的燈泡給你一個,這就相當於C++語言中創建對象時,按照默認方式創建,故調用默認構造函數。

  • Case 2: 拿到燈之後回家一裝,擦,太大了裝不上!這回你聰明瞭,量了下安裝尺寸,跑去給售貨員說你要XX大小的燈,此時就相當於C++利用參數化構造函數實例化對象。

  • Case 3: 擦,用了很久燈又掛了,這回你更聰明瞭,把壞燈卸下來帶到商店照着買一個,這場景就有點像C++中的拷貝構造函數了~。

那麼,到底不同的構造函數有些什麼不同呢?爲嘛C++語言設計這麼多種不同的構造函數呢?

  • 默認構造函數:默認構造函數不帶任何參數。如果不指定構造函數,則C ++編譯器會爲我們生成一個默認構造函數(不帶參數,並且具有空主體)。

  • 參數化構造函數:參數傳遞給構造函數,這些參數用於創建對象時初始化對象。要實現參數化構造函數,只需像向其他函數一樣向其添加參數即可。定義構造函數的主體時,使用參數初始化對象的數據成員。

  • 拷貝構造函數:拷貝構造函數,顧名思義,就是按照現有對象一模一樣克隆出一個新的對象。其參數一般爲現有對象的引用,一般具有如下函數原型:

//函數名爲類名,參數爲原對象const引用
ClassName(const ClassName &old_object); 

析構管“埋”

析構函數通常用於釋放內存,並在銷燬對象時對類對象及其類成員進行其他清理操作。當類對象超出生命週期範圍或被顯式刪除時,將爲該類對象調用析構函數。

那麼析構函數具有哪些特點呢?

  • 銷燬對象時,將自動調用析構函數。

  • 不能將其聲明爲static或const。

  • 析構函數沒有參數,也沒有返回類型。

  • 具有析構函數的類的對象不能成爲聯合的成員。

  • 析構函數應在該類的public部中聲明。

  • 程序員無法訪問析構函數的地址。

  • 一個類有且僅有一個析構函數。

  • 如果沒有顯式定義析構函數,編譯器會自動生成一個默認的析構函數。

既然析構函數是構造函數的反向操作,對於對象管"埋",那麼什麼時候“埋”呢?

  1. 函數返回退出

  2. 程序被關掉,比如一個應用程序被kill

  3. 包含局部對象的塊結尾

  4. 主動調用刪除符delete

前面說如果程序猿沒有顯式定義析構函數,編譯器會自動生成一個默認的析構函數。言下之意是有的時候需要顯式定義析構函數,那麼什麼時候需要呢?

當類中動態分配了內存時,或當一個類包含指向在該類中分配的內存的指針時,應該編寫一個析構函數以釋放該類實例之前的內存。否則會造成內存泄漏。

“生”與“埋”舉例

前面說構造管“生”,析構管“埋”,那麼到底怎麼“生”的呢?怎麼“埋”呢?,看看栗子:

#include <iostream>
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 << "默認矩形誕生了!" << 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;
}

這個簡單的代碼,實際運行的輸出結果:

默認矩形誕生了!
指定矩形誕生了!
矩形埋掉了!
矩形埋掉了!

技術人總是喜歡眼見爲實:因爲看見,所以相信!,看看其對應的彙編代碼(VC++ 2010彙編結果,這裏僅貼出main函數,僅爲理解原理,對於彙編指令不做描述,其中#爲對彙編註釋):

    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]  
#調用默認構造函數管“生”
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] 
#調用參數化構造函數
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]  
#調用拷貝構造函數
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]  
#調用析構函數,銷燬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] 
#調用析構函數,銷燬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: }

這裏引發幾個問題:

問題1:爲什麼先析構rct2,後析構rct1呢?

這是由於這兩個對象在棧上分配內存,所以基於棧的特性,顯然rct2位於C++運行時棧的頂部,而rct1位於棧底。

你如不信,將上述代碼修改一下,測測:

Rectangle::~Rectangle()
{
    cout <<"當前寬爲:" << width << "矩形埋掉了!" << endl;
}
int main()
{ 
   Rectangle rct1;
   rct1.width = 1;
   Rectangle *pRct = new Rectangle(2,3);
   Rectangle rct2  = rct1;
   rct2.width = 2;
   return 0;
}

其輸出結果爲:

默認矩形誕生了!
指定矩形誕生了!
當前寬爲:2矩形埋掉了!
當前寬爲:1矩形埋掉了!

問題2:請問上述代碼,構造函數被調用了幾次?析構函數又被調用了幾次?這是經常面試會考察的基礎知識。顯然前面的打印以及給出了答案。

問題3:該代碼有啥隱患?

答:調用了new,而沒有調用delete,爲啥這是隱患,new屬於動態申請內存,是在堆上爲對象申請內存,這屬於典型的管“生”不管“埋”,造成內存泄漏,如果整的多了,必然屍體滿 “堆”!造成程序引發不可預料的崩潰!

所以應該修正一下:

Rectangle::~Rectangle()
{
    cout <<"當前寬爲:" << width << "矩形埋掉了!" << 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;
}

看看輸出結果:

默認矩形誕生了!
指定矩形誕生了!
當前寬爲:2矩形埋掉了!
手動埋掉!
當前寬爲:3矩形埋掉了!
當前寬爲:1矩形埋掉了!

總結一下

  • OOP的基本單元是類,該類將靜態屬性和動態行爲封裝在一個黑盒中,並指定使用這些黑盒的公共接口。由於該類具有很好的封裝(與函數相比),因此重用這些類會更加容易。換句話說,OOP將同一黑盒中軟件實體的數據結構和算法較好結合在一起。

  • OOP語言允許更高級別的抽象來解決現實生活中的問題。傳統的過程語言例如C迫使您根據計算機的結構(例如內存位和字節,數組,決策,循環)進行思考,而不是根據您要解決的問題進行思考。OOP語言(例如Java,C ++,C#)使您可以在問題空間中進行思考,並使用軟件對象來表示和抽象問題空間的實體建模以解決問題。

  • 對於C++語言,構造函數與析構函數是基礎中的基礎,類在運行態並不存在,類以對象形式在運行態實現業務需求。對象如何按照類黑盒樣式如何在運行態誕生,利用類的構造函數而誕生,對象生存期結束,析構函數管“埋”,銷燬對象。

  • 對於局部對象,非new產生的對象,誕生地爲棧,在棧中誕生,編譯器會插入析構函數使得程序運行態在對象生命週期結束時自動管“埋”,而如果利用new動態創建的對象,則需要手動管“埋”,如手動管“生”(new),不手動管“埋”(delete),對象必成孤魂野鬼,嚴重時,對象屍體滿“堆”。

對於拷貝構造函數,還有一個所謂深拷貝、淺拷貝的要點沒有涉及,下次學習總結分享一下,敬請關注期待~,如發現文中有錯誤,敬請留言指正,不勝感激~

END

往期精彩推薦,點擊即可閱讀

▲Linux驅動相關專輯 

手把手教信號處理專輯

片機相關專輯

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章