💐 🌸 🌷 🍀 🌹 🌻 🌺 🍁 🍃 🍂 🌿 🍄🍝 🍛 🍤
📃個人主頁 :阿然成長日記 👈點擊可跳轉
📆 個人專欄: 🔹數據結構與算法🔹C語言進階
🚩 不能則學,不知則問,恥于問人,決無長進
🍭 🍯 🍎 🍏 🍊 🍋 🍒 🍇 🍉 🍓 🍑 🍈 🍌 🍐 🍍
文章目錄
- 一、析構函數
- 1.概念
- 2.特點
- 3.案例
- 二、拷貝構造函數
- 1.拷貝構造函數的引入
- 2.格式
- 2.概念
- 3.特點
- 4.解決引入問題
- 5.總結
一、析構函數
1.概念
析構函數
:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由
編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。
析構函數是用于在對象被刪除之前的清理工作,它在對象生命周期即將結束時會被自動調用。(析構函數可以清理對象并且釋放內存)
2.特點
析構函數是特殊的成員函數,其特征如下:
- 析構函數名是在類名前加上字符
~
。 - 無參數無返回值類型。
- 一個類只能有一個析構函數。若未顯式定義,系統會自動生成
默認
的析構函數。注意:析構
函數不能重載
- 對象生命周期結束時,C++編譯系統系統自動調用析構函數
- 關于編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器
生成的默認析構函數,對自定類型成員調用它的析構函數
3.案例
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本類型(內置類型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定義類型
Time _t;
};
int main()
{
Date d;
return 0;
}
上述代碼定義了兩個類Data和Time。其中Data類中存在一個自定義類型變量Time,主方法中只創建了Data對象。那么在main方法中根本沒有直接創建Time類的對象,為什么最后會調用Time類的析構函數?
答:因為main方法中創建了Date對象d,而d中包含4個成員變量,其中_year
, _month
,_day
三個是內置類型成員,銷毀時不需要資源清理,最后系統直接將其內存回收即可;而_t
是Time類
對象,所以在d銷毀時,要將其內部包含的Time類的_t對象銷毀,需要調用Time類的析構函數。但是main函數中不能直接調用Time類的析構函數,實際要釋放的是Date類對象,所以編譯器只會調用Date類的析構函 數,而Date沒有顯式提供,則編譯器會給Date類生成一個默認的析構函數,目的是在其內部 調用Time類的析構函數,即當Date對象銷毀時,要保證其內部每個自定義對象都可以正確銷毀 ,main函數中并沒有直接調用Time類析構函數,而是顯式調用編譯器為Date類生成的默認析構函數
注意:創建哪個類的對象則調用該類的析構函數,銷毀那個類的對象則調用該類的析構函數
二、拷貝構造函數
1.拷貝構造函數的引入
如下代碼:
創建Stack類和Date類。
class Date
{
public:void print(){}
private:int _year =1;int _month =1;int _day =1;
};
class Stack
{public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}private:int* _array;size_t _size;size_t _capacity;
};
使用傳值的方式進行調用。
void fun1(Date d)
{//...
}
void fun2(Stack s)
{//...
}
主函數
int main()
{Date d1(1999, 1, 1);fun1(d1);Stack s1;fun2(s1);return 0;
}
可以發現運行報錯!!!為什么呢?
在C語言中我們遇到這種情況是沒有問題的,但是在C++中,由于引進了析構函數
,也就是說每次對象試用結束,系統都會自動調用析構函數進行對象的清除工作,正因如此我們在傳值的時候,會再次創建一個對象復制過去,當結束函數調用時,會自動調用析構函數對當前對象進行清除,【如果對象中都是變量的話是沒有問題的,因為值傳遞的改變不影響外部的變量,它只是一份拷貝。但是,如果對象中有指針類型的變量,它們指向的是同一位置,第一次函數調用結束后,析構函數已經對指向的位置進行了清除,第二次外部對象結束時,在調用析構函數,就會造成二次析構】。上面例子中Date類中全是普通變量,不會出現問題,造成二次析構。
為了解決上面的問題我們引入了拷貝構造函數。(通過深拷貝解決問題)
2.格式
類名(const 類名 &參數)
2.概念
只能有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存 在的類類型對象創建新對象時由編譯器自動調用。
3.特點
- 拷貝構造函數是構造函數的一個重載形式。
- 拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯,
因為會引發無窮遞歸調用。
例如:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{_year = year;_month = month;_day = day;
}
下面是拷貝構造!!
Date(const Date& ddd) // 正確寫法
Date(const Date ddd) // 錯誤寫法:編譯報錯,會引發無窮遞歸
{_year = d._year;_month = d._month;_day = d._day;
}
private:int _year;int _month;int _day;
};
主函數
int main()
{Date d1;Date d2(d1);return 0;
}
Date(const Date d)
// 錯誤寫法:編譯報錯,會引發無窮遞歸; 因為【Date d】參數會繼續生成一個拷貝構造,如此往復循環。- *
Date(const Date& ddd)
// 正確寫法 使用傳引用就可以解決這個問題。因為ddd就是對象d1的一個別名,this指針指向d2,ddd傳給了this,就相當于把d1給了d2,(傳遞指針也可以,但是很難用),規定必須使用引用。
4.解決引入問題
學到這里我們就可以很好地解決之前拋出的問題了。使用拷貝構造,將原來對象玩玩整整的拷貝一份出來,尤其是內部指針所指向的空間,拷貝時也會去開辟一塊新的空間。這樣在函數調用函數結束時,系統自動調用其析構造函數時,就不會造成一個地方多次析構了。各玩各的。
5.總結
內置類型系統會默認生成拷貝構造進行值拷貝,對自定義類型調用它的拷貝構造。
并不是所有的對象都需要拷貝構造:
- 類中如果沒有涉及資源申請時(例如 new申請空間),拷貝構造函數是否寫都可以;
- 一旦涉及到資源申請時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。
- 拷貝構造也是構造,是拷貝構造的一個重載。