?? 訂閱量破千的火熱 C++ 教程
?? 火速訂閱《C++要笑著學(xué)》?
??? CSDN 累計訂閱量破千的火爆 C/C++ 教程的 2023 重制版,C 語言入門到實(shí)踐的精品級趣味教程。 了解更多: ???"不太正經(jīng)" 的專欄介紹?← 試讀第一章 訂閱鏈接: ??《C語言趣味教程》?← 猛戳訂閱! |
? 本篇博客全站綜合熱榜最高排名:7
?? 本篇博客是本專欄最受歡迎的一篇 ??
- ?? 寫在斜面:朋友們好啊,我是亦優(yōu)葉子,今天終于更新了。本章將繼續(xù)講解C++中的面向?qū)ο蟮闹R點(diǎn),本篇主要講解默認(rèn)成員函數(shù)中的構(gòu)造函數(shù)、析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)。還是和以前一樣,我們將由淺入深地去講解,以 "初學(xué)者" 的角度去探索式地學(xué)習(xí)。會一步步地推進(jìn)講解,而不是直接把枯燥的知識點(diǎn)倒出來,應(yīng)該會有不錯的閱讀體驗。保證文章非常有意思!不信你可以讀一讀。本章內(nèi)容全是干貨,是 C++ 面向?qū)ο蟮闹匾鹿?jié)!?如果覺得不錯,可以 "一鍵三連" 支持一下博主!你們的關(guān)注就是我更新的最大動力!
- 【2023.9.27 更新】評論區(qū)對于此問題也有不少提問,這里我再做一個補(bǔ)充說明:這兩個問題實(shí)際上都是在問析構(gòu)順序的,只要把這塊知識點(diǎn)搞明白就會很好理解(講解在本章的 Ⅲ 0x03)
?? 本章目錄:
目錄
Ⅰ.? 默認(rèn)成員函數(shù)(Default member function)
Ⅱ. 構(gòu)造函數(shù)(Constructor)
0x00 引入:為什么要有構(gòu)造函數(shù)?
0x01 構(gòu)造函數(shù)的概念
0x02 構(gòu)造函數(shù)的特性
0x03 默認(rèn)構(gòu)造函數(shù)
0x04 構(gòu)造函數(shù)的特性的測試
Ⅲ. 析構(gòu)函數(shù)(Destructor)
0x00 引入:專門 “擦屁股” 的析構(gòu)函數(shù)
0x01 析構(gòu)函數(shù)的概念
0x02 析構(gòu)函數(shù)的特性
0x03 析構(gòu)順序問題
0x04?析構(gòu)函數(shù)的特性的測試
Ⅳ.? 拷貝構(gòu)造函數(shù)(Copy Constructor)
0x00 引入:可以幫我拷貝嗎?可以!
0x01 拷貝構(gòu)造函數(shù)的概念
0x02 拷貝構(gòu)造函數(shù)的特性
0x03 關(guān)于默認(rèn)生成的拷貝構(gòu)造
Ⅴ.? 總結(jié)(構(gòu)造&析構(gòu)&拷貝構(gòu)造)
0x00 構(gòu)造函數(shù)?
0x01 析構(gòu)函數(shù)
0x02 拷貝構(gòu)造
Ⅰ.? 默認(rèn)成員函數(shù)(Default member function)
如果一個類中什么成員都沒有,我們稱之為 "空類" 。
但是空類中真的什么都沒有嗎?答案是否定的!
?類有六個默認(rèn)成員函數(shù),特殊的點(diǎn)非常多,我們本章就來好好學(xué)學(xué)。
?對于 默認(rèn)成員函數(shù),如果我們不主動實(shí)現(xiàn),編譯器會自己生成一份。
? 它們有什么用呢?舉個例子:比如我們在上一章里舉過的一個 Stack 的例子。
如果需要初始化和清理,"構(gòu)造函數(shù)" 和 "析構(gòu)函數(shù)" 就可以幫助我們完成。
構(gòu)造函數(shù)就類似于 Init,而析構(gòu)函數(shù)就類似于 Destroy。
? 沒錯,就是這么的爽!
還是和以前一樣,我們將先由淺入深地進(jìn)行學(xué)習(xí),我們先從最簡單的?"構(gòu)造函數(shù)" 開始講起。
Ⅱ. 構(gòu)造函數(shù)(Constructor)
0x00 引入:為什么要有構(gòu)造函數(shù)?
打開宇宙第一編輯器,一起敲一敲看看 ~
為了能夠更好地講解,我們來寫一個簡單的日期類,通過日期類來講解!
?? 代碼演示:Date.cpp
#include <iostream>
class Date {
public:
void SetDate(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
d1.SetDate(2022, 3, 8);
d1.Print();
Date d2;
d2.SetDate(2022, 3, 12);
d2.Print();
return 0;
}
?對于 Date 類,我們可以通過我們寫的成員函數(shù)?SetDate 給對象設(shè)置內(nèi)容。
但是每次創(chuàng)建對象都要調(diào)用這個 SetDate ,是不是太雞兒煩了?
? 那有沒有什么辦法能在創(chuàng)建對象時,自動將我們要傳遞的內(nèi)容放置進(jìn)去呢?
?有!下面我們來隆重介紹一下 構(gòu)造函數(shù)!
0x01 構(gòu)造函數(shù)的概念
?構(gòu)造函數(shù)是一個特殊的成員函數(shù):
- 名字與類名相同(寫起來簡單又草率)
- 創(chuàng)建類類型對象時由編譯器自動調(diào)用(全自動工具人)
- 能夠保證每個數(shù)據(jù)成員都有一個合適的初始值,并且在對象的生命周期內(nèi)只調(diào)用一次。
?構(gòu)造函數(shù)的意義:能夠保證對象被 初始化 (init) 。
構(gòu)造函數(shù)是特殊的成員函數(shù),主要任務(wù)是初始化,而不是開空間(雖然構(gòu)造函數(shù)的名字叫構(gòu)造)。
0x02 構(gòu)造函數(shù)的特性
?構(gòu)造函數(shù)是特殊的成員函數(shù),主要特征如下:
- 構(gòu)造函數(shù)的函數(shù)名和類名是相同的 (比如類名是 Date,構(gòu)造函數(shù)名就是 Date)
- 構(gòu)造函數(shù)無返回值 (它不具有返回類型,因此不能直接返回值)
- 構(gòu)造函數(shù)支持重載 (仔細(xì)看下面的例子)
- 會在對象實(shí)例化時自動調(diào)用對象定義出來
比如下面的代碼只需要 Date d1,就會自動調(diào)用構(gòu)造,保證了對象一定是被初始化過的!
"還有這種好事?!"
這也太好了吧?我們直接來看看它是怎么用的!?
?? 代碼演示:構(gòu)造函數(shù)的用法 (無參構(gòu)造函數(shù) 和 帶參構(gòu)造函數(shù))
#include <iostream>
class Date {
public:
/* 無參構(gòu)造函數(shù) */
Date() {
_year = 0;
_month = 1;
_day = 1;
}
/* 帶參構(gòu)造函數(shù) */
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; // 對象實(shí)例化,此時觸發(fā)構(gòu)造,調(diào)用無參構(gòu)造函數(shù)
d1.Print();
Date d2(2022, 3, 9); // 對象實(shí)例化,此時觸發(fā)構(gòu)造,調(diào)用帶參構(gòu)造函數(shù)
d2.Print();
return 0;
}
?? 運(yùn)行結(jié)果如下:
??? 解讀:不給參數(shù)時就會調(diào)用 無參構(gòu)造函數(shù),給參數(shù)則會調(diào)用 帶參構(gòu)造函數(shù)。
?? 注意事項:
① 構(gòu)造函數(shù)是特殊的,不是常規(guī)的成員函數(shù),不能直接調(diào)??。
#include <iostream>
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
d1.Date(); // 不能這么去調(diào),構(gòu)造函數(shù)是特殊的,不是常規(guī)的成員函數(shù)!
return 0;
}
?? 運(yùn)行結(jié)果:(報錯)
② 如果通過無參構(gòu)造函數(shù)創(chuàng)建對象,對象后面不用跟括號,否則就成了函數(shù)聲明。
#include <iostream>
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
//帶參這么調(diào):加括號(),在括號中加參數(shù)列表
Date d2(2022, 3, 9);
Date d3(); // 這樣可以嗎?
// 既然代參的調(diào)用加括號,在括號中加參數(shù)列表。
// 那我不帶參,可不可以加括號呢?
? 仍然不可以。
// 這個對象實(shí)際上沒有被定義出來,這里會報錯。
// 編譯器不會識別,所以不傳參數(shù)就老老實(shí)實(shí)地
// Date d3; 不要 Date d3();
// 主要是編譯器沒法識別,所以這里記住不能這么干就行了。
return 0;
}
③ 這里如果調(diào)用帶參構(gòu)造函數(shù),我們需要傳遞三個參數(shù)(這里我們沒設(shè)缺?。?。
④?如果你沒有自己定義構(gòu)造函數(shù)(類中未顯式定義),C++ 編譯器會自動生成一個無參的默認(rèn)構(gòu)造函數(shù)。當(dāng)然,如果你自己定義了,編譯器就不會幫你生成了。
#include <iostream>
class Date {
public:
/* 如果用戶顯式定義了構(gòu)造函數(shù),編譯器將不再生成
Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
*/
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; // 這里調(diào)用的是默認(rèn)生成的無參的構(gòu)造函數(shù)
d1.Print();
return 0;
}
?? 運(yùn)行結(jié)果如下:
?? 沒有定義構(gòu)造函數(shù),對象也可以創(chuàng)建成功,因此此處調(diào)用的是 編譯器默認(rèn)生成的構(gòu)造函數(shù)。
0x03 默認(rèn)構(gòu)造函數(shù)
?無參構(gòu)造函數(shù)、全缺省構(gòu)造函數(shù)、自動生成的構(gòu)造函數(shù)都被稱為 默認(rèn)構(gòu)造函數(shù)。
并且 默認(rèn)構(gòu)造函數(shù)只能有一個!
class Date {
public:
/* 全缺省構(gòu)造函數(shù) - 默認(rèn)構(gòu)造函數(shù) */
Date(int year = 1970, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
?? 注意事項:
語法上無參和全缺省可以同時存在,但如果同時存在會引發(fā)二義性:
#include <iostream>
class Date {
public:
Date() {
_year = 1970;
_month = 1;
_day = 1;
}
Date(int year = 1970, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; ?
return 0;
}
?? 運(yùn)行結(jié)果如下:(報錯)
?? 解讀:無參的構(gòu)造函數(shù)和全缺省的構(gòu)造函數(shù)都成為默認(rèn)構(gòu)造函數(shù),并且默認(rèn)構(gòu)造參數(shù)只能有一個,雖然語法上允許它們們兩個可以同時存在,但是如果有對象定義去調(diào)用就會報錯。
??強(qiáng)烈推薦實(shí)現(xiàn)全缺省或者半缺省,因為真的很好用:
#include <iostream>
class Date {
public:
/* 全缺省 */
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; // 如果不傳,就是缺省值
Date d2(2022, 1, 15);
Date d3(2009);
Date d4(2012, 4);
d1.Print(); // 0-1-1
d2.Print(); // 2022-1-15
d3.Print(); // 2009-1-1
d4.Print(); // 2012-4-1
return 0;
}
?? 運(yùn)行結(jié)果如下:
0x04 構(gòu)造函數(shù)的特性的測試
通過剛才的講解我們知道了任何一個類的默認(rèn)構(gòu)造函數(shù),只有三種:
- 無參的構(gòu)造函數(shù)
- 全缺省的構(gòu)造函數(shù)
- 我們不寫,編譯器自己生成的構(gòu)造函數(shù)
如果你沒有自己定義構(gòu)造函數(shù)(類中未顯式定義),C++ 編譯器會自動生成一個無參的默認(rèn)構(gòu)造函數(shù)。當(dāng)然,如果你自己定義了,編譯器就不會幫你生成了。
?? 代碼演示:讓編譯器自己生成一個
#include <iostream>
class Date {
public:
// 讓編譯器自己生成一個
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1; // 這里調(diào)用的是默認(rèn)生成的無參的構(gòu)造函數(shù)
d1.Print();
return 0;
}
?? 運(yùn)行結(jié)果如下:
??好了,我們來好好探討探討這個問題!
在我們不是先構(gòu)造函數(shù)的情況下,編譯器生成的默認(rèn)構(gòu)造函數(shù)。
"似乎這看起來沒有什么鳥用啊,這不就是一堆隨機(jī)值嘛……"
d1 對象調(diào)用了編譯器生成的默認(rèn)函數(shù),但 d1 對象 year / month / day 依舊是隨機(jī)值,
也就是說這里編譯器生成的默認(rèn)構(gòu)造函數(shù)好像并沒有什么卵用。
?? 解答:C++ 把類型分成內(nèi)置類型(基本類型)和自定義類型。
- 內(nèi)置類型就是語法已經(jīng)定義好的類型:如 int / char...
- 自定義類型就是我們使用 class / struct / union / 自己定義的類型。
觀察下面的程序,你就會發(fā)現(xiàn)編譯器生成默認(rèn)的構(gòu)造函數(shù),會對自定類型成員?_t?調(diào)用的它的默認(rèn)成員函數(shù):
#include <iostream>
using namespace std;
class Time {
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date {
private:
// 基本類型(內(nèi)置類型)
int _year;
int _month;
int _day;
// 自定義類型
Time _t;
};
int main()
{
Date d;
return 0;
}
?? 運(yùn)行結(jié)果如下:?
?? 測試:對自定義類型處理,會調(diào)用默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)的函數(shù))
#include<iostream>
using namespace std;
class A {
public:
// 默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)的)
A() {
cout << " A() " << endl;
_a = 0;
}
private:
int _a;
};
class Date {
public:
private:
int _year;
int _month;
int _day;
A _aa; // 對自定義類型處理,此時會調(diào)用默認(rèn)構(gòu)造函數(shù) A() {...}
};
int main(void)
{
Date d1;
return 0;
}
?? 運(yùn)行結(jié)果如下:
C++?里面把類型分為兩類:內(nèi)置類型(基本類型)和 自定義類型。
C++ 規(guī)定:我們不寫?編譯器默認(rèn)生成構(gòu)造函數(shù)對于內(nèi)置類型的成員變量不做初始化處理。
但是對于自定義類型的成員變量會去調(diào)用它的默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)的)初始化。
如果沒有默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)用的構(gòu)造函數(shù))就會報錯!
"你要寫就寫好了,要么就別寫,不寫我默認(rèn)生成的能保底"
?? 為了驗證,這里我們故意寫個帶參的默認(rèn)構(gòu)造函數(shù),讓編譯器不默認(rèn)生成:
#include <iostream>
using namespace std;
class A {
public:
// 如果沒有默認(rèn)的構(gòu)造函數(shù),會報錯。
A(int a) { // 故意給個參
cout << " A() " << endl;
_a = 0;
}
private:
int _a;
};
class Date {
public:
private:
// 如果沒有默認(rèn)構(gòu)造函數(shù)就會報錯
int _year;
int _month;
int _day;
A _aa;
};
int main(void)
{
Date d1;
return 0;
}
?? 運(yùn)行結(jié)果如下(報錯)
?? 我們不寫,讓編譯器默認(rèn)生成一個:
#include<iostream>
using namespace std;
class A {
public:
// 讓編譯器默認(rèn)生成
private:
int _a;
};
class Date {
public:
private:
int _year;
int _month;
int _day;
A _aa;
};
int main(void)
{
Date d1;
return 0;
}
?是隨機(jī)值沒錯,但是這是一種對自定義類型的 "處理",這就是隨機(jī)值的原因?。
?這里說個題外話,個人認(rèn)為 C++里,我們不寫構(gòu)造函數(shù)編譯器會默認(rèn)生成的這個特性設(shè)計得不好(狗頭保命)……因為沒有對內(nèi)置類型和自定義類型統(tǒng)一處理,不處理內(nèi)置類型成員變量,只處理自定義類型成員變量。
Ⅲ. 析構(gòu)函數(shù)(Destructor)
0x00 引入:專門 “擦屁股” 的析構(gòu)函數(shù)
?通過前面構(gòu)造函數(shù)的學(xué)習(xí),我們知道了一個對象是怎么來的了,
? 那一個對象又是怎么沒的呢?既然構(gòu)造函數(shù)的本質(zhì)是初始化,那清理的工作交給誰來干呢?
當(dāng)然是交給專門擦屁股的 —— 析構(gòu)函數(shù) (destructor)?!
以前我們玩數(shù)據(jù)結(jié)構(gòu)的時候經(jīng)常忘記調(diào)用 destroy 函數(shù),但是現(xiàn)在我們有析構(gòu)函數(shù)了?。。?/p>
(非常炸裂…… 多么振奮人心啊!話不多說讓我們開始講解?。。。?/span>
0x01 析構(gòu)函數(shù)的概念
?析構(gòu)函數(shù)與構(gòu)造函數(shù)的功能相反。
構(gòu)造函數(shù)是特殊的成員函數(shù),主要任務(wù)是初始化,而不是開空間;
析構(gòu)函數(shù)也一樣,主要任務(wù)是清理,而不是做對象銷毀的工作。
(局部對象銷毀工作是由編譯器完成的)
?? 概念:對象在銷毀時會自動調(diào)用析構(gòu)函數(shù),完成對象的一些資源清理工作。
構(gòu)造函數(shù)和析構(gòu)函數(shù)的名詞翻譯趣談
首先,構(gòu)造函數(shù)的英文是?Constructor,意思為建造者/制造商。析構(gòu)函數(shù)是 Destructor,意思是破壞者/垃圾焚燒爐,誒,這里的垃圾焚燒爐,完成對象的資源清理工作,非常的合理啊。下面我們來看看?"構(gòu)造函數(shù)" 和 "析構(gòu)函數(shù)"?的其他翻譯方式!
[繁] 中國臺灣地區(qū)將 Constructor 翻譯為 "建構(gòu)函式",表示建造和構(gòu)成,Destructor 翻譯為 "解構(gòu)函式",意思應(yīng)該是 "解除構(gòu)造" 的意思。
[韓] 韓語翻譯的就有意思了,本人在韓國留學(xué),這邊的 Constructor 叫? ??? (生成者) Destructor 叫???? (消滅者),非常的簡單直白,但一開始在教材上讀到 "消滅者" 的時候一直沒想到說的是析構(gòu)函數(shù),感覺非常中二……? 怎么不叫 ??? (終結(jié)者)?呢?
0x02 析構(gòu)函數(shù)的特性
?構(gòu)造函數(shù)是特殊的成員函數(shù),主要特征如下:
- 析構(gòu)函數(shù)名是在類名前面加上字符?
?
- 析構(gòu)函數(shù)既沒有參數(shù)也沒有返回值(因為沒有參數(shù),所以也不會構(gòu)成重載問題)
- 一個類的析構(gòu)函數(shù)有且僅有一個(如果不寫系統(tǒng)會默認(rèn)生成一個析構(gòu)函數(shù))
- 析構(gòu)函數(shù)在對象生命周期結(jié)束后,會自動調(diào)用。
(和構(gòu)造函數(shù)是對應(yīng)的構(gòu)造函數(shù)是在對象實(shí)例化的時候自動調(diào)用)
?? 代碼演示:為了演示自動調(diào)用,我們來讓析構(gòu)函數(shù)被調(diào)用時 "吱" 一聲:
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
~Date() {
// Date 類沒有資源需要清理,所以Date不實(shí)現(xiàn)析構(gòu)函都是可以的
cout << "~Date() 吱~ " << endl; // 測試一下,讓他吱一聲
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
Date d2(2022, 3, 9);
return 0;
}
?? 運(yùn)行結(jié)果如下:
(這里的析構(gòu)順序問題我們下面會講,不要著急)
額,之前舉得日期類的例子沒法很好地展示析構(gòu)函數(shù)的 "魅力" ……
就像本段開頭說情景,我們拿 Stack 來舉個例子,這就很貼切了。
我們知道,棧是需要 destroy 清理開辟的內(nèi)存空間的。
?這里我們讓析構(gòu)函數(shù)來干這個活,簡直美滋滋!
?? 代碼演示:析構(gòu)函數(shù)的用法
#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int StackDataType;
class Stack {
public:
/* 構(gòu)造函數(shù) - StackInit */
Stack(int capacity = 4) { // 這里只需要一個capacity就夠了,默認(rèn)給4(利用缺省參數(shù))
_array = (StackDataType*)malloc(sizeof(StackDateType) * capacity);
if (_array == NULL) {
cout << "Malloc Failed!" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
/* 析構(gòu)函數(shù) - StackDestroy */
~Stack() { // 這里就用的上析構(gòu)函數(shù)了,我們需要清理開辟的內(nèi)存空間(防止內(nèi)存泄漏)
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
private:
int* _array;
size_t _top;
size_t _capacity;
};
int main(void)
{
Stack s1;
Stack s2(20); // s2 棧 初始capacity給的是20(可以理解為"客制化")
return 0;
}
?? 解讀:我們在設(shè)置棧的構(gòu)造函數(shù)時,定義容量 capacity 時利用缺省參數(shù)默認(rèn)給個4的容量,這樣用的時候默認(rèn)就是4,如果不想要4可以自己傳。
如此一來,就可以保證了棧被定義出來就一定被初始化,用完后會自動銷毀。以后就不會有忘記調(diào)用 destroy 而導(dǎo)致內(nèi)存泄露的慘案了,這里的析構(gòu)函數(shù)就可以充當(dāng)銷毀的作用。
0x03 析構(gòu)順序問題
?? 先看代碼:這是我們剛才舉的析構(gòu)函數(shù)的例子
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {...}
void Print() {...}
~Date() {
cout << "~Date() 吱~ " << endl; // 測試一下,讓他吱一聲
}
private:
{...}
};
int main(void)
{
Date d1; // 誰先析構(gòu)?
Date d2(2022, 3, 9);
return 0;
}
? 問一個比較有意思的問題:這個例子中是先析構(gòu) s1 還是先析構(gòu) s2?
?
或者這么問:打印結(jié)果的兩個吱分別都是誰叫的?
?? 答案:先析構(gòu) s2,再析構(gòu) s1 ;第一個吱是 s2 叫的,第二個吱是 s1 叫的。
?剛才例子的解析圖上,我就寫著第一個 吱是 d2 的了:
因為析構(gòu)的順序在局部的棧中是相反的,棧幀銷毀清理資源時 s2 先清理,然后再清理 s1!
所以 先構(gòu)造的后析構(gòu),后構(gòu)造的先析構(gòu),所以是 d2 先吱。
(不信的話可以去監(jiān)視一下 this 觀察下成員變量)
【2023.9.27 更新】評論區(qū)對于此問題也有不少提問,這里我再做一個補(bǔ)充說明:
這兩個問題實(shí)際上都是在問析構(gòu)順序的,只要把這塊知識點(diǎn)搞明白就會很好理解。
0x04?析構(gòu)函數(shù)的特性的測試
又到了測試環(huán)節(jié),上號!
我們知道了,如果沒寫析構(gòu)函數(shù)編譯器會自動生成一個。
那默認(rèn)生成的析構(gòu)函數(shù)會做什么事情呢?它會幫我們 destroy 嘛?
?hhh,哪有這種好事,不能什么都幫你做啊!
我們剛才講了(我們回顧下,串聯(lián)一下知識點(diǎn)):
?? 如果不自己寫構(gòu)造函數(shù),讓編譯器自動生成,那么這個自動生成的 默認(rèn)構(gòu)造函數(shù):
- 對于 "內(nèi)置類型" 的成員變量:不會做初始化處理。
- 對于 "自定義類型" 的成員變量:會調(diào)用它的默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)的)初始化,如果沒有默認(rèn)構(gòu)造函數(shù)(不用參數(shù)就可以調(diào)用的構(gòu)造函數(shù))就會報錯!
?而我們的析構(gòu)函數(shù)也是這樣的,一個德行!
?? 如果我們不自己寫析構(gòu)函數(shù),讓編譯器自動生成,那么這個 默認(rèn)析構(gòu)函數(shù):
- 對于 "內(nèi)置類型" 的成員變量:不作處理 (不會幫你清理的.)
- 對于 "自定義類型" 的成員變量:會調(diào)用它對應(yīng)的析構(gòu)函數(shù) (已經(jīng)仁至義盡了) 。
" 編譯器:哈哈哈,給你默認(rèn)生成個用用就不錯了,你都懶得寫了,不要挑三揀四滴!"
?? 代碼演示:
#include<iostream>
#include<stdlib.h>
using namespace std;
typedef int StackDataType;
class Stack {
public:
Stack(int capacity = 4) {
_array = (StackDataType*)malloc(sizeof(int*) * capacity);
if (_array == NULL) {
cout << "Malloc Failed!" << endl;
exit(-1);
}
_top = 0;
_capacity = capacity;
}
// ~Stack() {
// free(_array);
// _array = nullptr;
// _top = _capacity = 0;
// }
private:
int* _array;
size_t _top;
size_t _capacity;
};
int main(void)
{
Stack s1;
Stack s2(20);
return 0;
}
難道就不能幫我把這些事都干了嗎?幫我都銷毀掉不就好了?
不不不,舉個最簡單的例子,迭代器,析構(gòu)的時候是不釋放的,因為不需要他來管,
所以默認(rèn)不對內(nèi)置類型處理是正常的,萬一誤殺了怎么辦,對吧。?
有人可能又要說了,這么一來默認(rèn)生成的析構(gòu)函數(shù)不就沒有用了嗎?
有用!他對內(nèi)置類型的成員類型不作處理,會在一些情況下非常的有用!
比如說:?兩個棧實(shí)現(xiàn)一個隊列(LeetCode232) ,用 C++ 可以非常的爽。
?? 代碼演示:自定義類型的成員變量調(diào)用它的析構(gòu)函數(shù)
#include <iostream>
using namespace std;
class String {
public:
String(const char* str = "jack") {
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String() {
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person {
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
?? 運(yùn)行結(jié)果如下:
Ⅳ.? 拷貝構(gòu)造函數(shù)(Copy Constructor)
0x00 引入:可以幫我拷貝嗎?可以!
我們在創(chuàng)建對象的時候,能不能創(chuàng)建一個與某一個對象一模一樣的新對象呢?
Date d1(2022, 3, 9);
d1.Print();
Date d2(d1); // 照著d1的模子做一個d2
d2.Print();
當(dāng)然可以,這時我們就可以用拷貝構(gòu)造函數(shù)。
(但是要警惕無窮遞歸,一定要加引用&,這個我們下面重點(diǎn)講解?。?/span>
0x01 拷貝構(gòu)造函數(shù)的概念
?? 拷貝構(gòu)造函數(shù):只有單個形參,該形參是對本類類型對象的引用(一般常用 const 修飾),
在用已存在的類類型對象創(chuàng)建新對象時由編譯器自動調(diào)用。
0x02 拷貝構(gòu)造函數(shù)的特性
它也是一個特殊的成員函數(shù),所以他符合構(gòu)造函數(shù)的一些特性:
① 拷貝構(gòu)造函數(shù)是構(gòu)造函數(shù)的一個重載形式。函數(shù)名和類名相同,沒有返回值。
② 拷貝構(gòu)造函數(shù)的參數(shù)只有一個,并且 必須要使用引用傳參!
? ? ??使用傳值方式會引發(fā)無窮遞歸調(diào)用!
"拷貝構(gòu)造函數(shù)的引用是必不可少的!"
類名(const 類名& 形參);
?? 代碼演示:拷貝構(gòu)造函數(shù)的用法
#include <iostream>
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
/* Date d2(d1); */
Date(Date& d) { // 這里要用引用,否則就會無窮遞歸下去
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2022, 3, 9);
Date d2(d1); // 拷貝復(fù)制
// 看看拷貝成功沒
d1.Print();
d2.Print();
return 0;
}
?? 運(yùn)行結(jié)果如下:
? 為什么必須使用引用傳參呢?
調(diào)用拷貝構(gòu)造,需要先穿參數(shù),傳值傳參又是一個拷貝構(gòu)造。
調(diào)用拷貝構(gòu)造,需要先穿參數(shù),傳值傳參又是一個拷貝構(gòu)造。
調(diào)用拷貝構(gòu)造,需要先穿參數(shù),傳值傳參又是一個拷貝構(gòu)造。
……
一直在傳參這里出不去了,所以這個遞歸是一個無窮無盡的。
?? 我們來驗證一下:
error:?invalid?constructor;?you?probably?meant?'Date?(const?Date&)'
這里不是加不加 const 的問題,而是沒有用引用導(dǎo)致的問題。
不用引用,他就會在傳參那無線套娃遞歸,至于為什么我們繼續(xù)往下看。
?? 拷貝構(gòu)造函數(shù)加 const:如果函數(shù)內(nèi)不需要改變,建議把 const 也給它加上!
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
/* Date d2(d1); */
Date(const Date& d) { // 如果內(nèi)部不需要改變,建議加上const
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
第一個原因:怕出錯,萬一你一不小心寫反了怎么辦?
/* Date d2(d1); */
Date(Date& d) {
d._year = _year;
d._month = _month;
d._day = _day;
}
這樣會產(chǎn)生一個很詭異的問題,這一個可以被編譯出來的 BUG ,結(jié)果會變?yōu)殡S機(jī)值。
所以,這里加一個 const 就安全多了,這些錯誤就會被檢查出來了。
第二個原因:以后再講,因為涉及一些臨時對象的概念。
???反正,不想深究的話就記?。?span style="background-color:#f9eda6;">如果函數(shù)體內(nèi)不需要改變,建議把 const 加上?就完事了。
"社會上的事情少打聽,拷貝構(gòu)造直接? X(const& x)? 就行了"
0x03 關(guān)于默認(rèn)生成的拷貝構(gòu)造
?這里比較特殊,我們單獨(dú)領(lǐng)出來講。?
?? 默認(rèn)生成拷貝構(gòu)造:
① 內(nèi)置類型的成員,會完成按字節(jié)序的拷貝(把每個字節(jié)依次拷貝過去)。
② 自定義類型成員,會再調(diào)用它的拷貝構(gòu)造。
?? 拷貝構(gòu)造我們不寫生成的默認(rèn)拷貝構(gòu)造函數(shù),對于內(nèi)置類型和自定義類型都會拷貝處理。但是處理的細(xì)節(jié)是不一樣的,這個跟構(gòu)造和析構(gòu)是不一樣的!
#include<iostream>
using namespace std;
class Date {
public:
Date(int year = 0, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
// Date(Date& d) {
// _year = d._year;
// _month = d._month;
// _day = d._day;
// }
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1(2002, 4, 8);
// 拷貝復(fù)制
Date d2(d1);
// 沒有寫拷貝構(gòu)造,但是也拷貝成功了
d1.Print();
d2.Print();
return 0;
}
?? 運(yùn)行結(jié)果如下:
?? 他這和之前幾個不同了,這個他還真給我解決了。
所以為什么要寫拷貝構(gòu)造?寫他有什么意義?沒有什么意義。
?默認(rèn)生成的一般就夠用了!
?當(dāng)然,這并不意味著我們都不用寫了,有些情況還是不可避免要寫的
比如實(shí)現(xiàn)棧的時候,棧的結(jié)構(gòu)問題,導(dǎo)致這里如果用默認(rèn)的?拷貝構(gòu)造,會翻車。
按字節(jié)把所有東西都拷過來會產(chǎn)生問題,如果?Stack?st1?拷貝出另一個?Stack?st2(st1)?
會導(dǎo)致他們都指向那塊開辟的內(nèi)存空間,導(dǎo)致他們指向的空間被析構(gòu)兩次,導(dǎo)致程序崩潰
然而問題不止這些……
?其實(shí)這里的字節(jié)序拷貝是淺拷貝,下面幾章我會詳細(xì)講一下深淺拷貝,這里的深拷貝和淺拷貝先做一個大概的了解。
?? 總結(jié):對于常見的類,比如日期類,默認(rèn)生成的拷貝構(gòu)造能用。但是對于棧這樣的類,默認(rèn)生成的拷貝構(gòu)造不能用。
Ⅴ.? 總結(jié)(構(gòu)造&析構(gòu)&拷貝構(gòu)造)
默認(rèn)成員函數(shù)有六只,本篇只介紹了三只,剩下的我們后面講。
類和對象部分知識很重要,所以我們來做一個簡單的總結(jié) ~
0x00 構(gòu)造函數(shù)?
初始化,在對象實(shí)例化時候自動調(diào)用,保證實(shí)例化對象一定被初始化。
構(gòu)造函數(shù)是默認(rèn)成員函數(shù),我們不寫編譯器會自己生成一份,我們寫了編譯器就不會生成。
我們不寫內(nèi)置類型成員變量不處理。
對于內(nèi)置類型成員變量不處理。
對于自定義類型的成員變量會調(diào)用它的默認(rèn)構(gòu)造函數(shù)。
// 我們需要自己實(shí)現(xiàn)構(gòu)造函數(shù)
class Date {
int _year;
int _month;
int _day;
};
// 我們不需要自己實(shí)現(xiàn)構(gòu)造函數(shù),默認(rèn)生成的就可以
class MyQueue {
Stack _pushST;
Stack _popST;
};
0x01 析構(gòu)函數(shù)
?完成對象中自愿的清理。如果類對象需要資源清理,才需要自己實(shí)現(xiàn)析構(gòu)函數(shù)。
析構(gòu)函數(shù)在對象生命周期到了以后自動調(diào)用,如果你正確實(shí)現(xiàn)了析構(gòu)函數(shù),保證了類對象中的資源被清理。
什么時候生命周期到了?如果是局部變量,出了作用域。全局和靜態(tài)變量,整個程序結(jié)束。
我們不寫編譯器會默認(rèn)生成析構(gòu)函數(shù),我們實(shí)現(xiàn)了,編譯器就不會實(shí)現(xiàn)了。
對于內(nèi)置類型成員變量不處理。
對于自定義類型的成員變量會調(diào)用它的析構(gòu)函數(shù)。
// 沒有資源需要清理,不徐需要自己實(shí)現(xiàn)析構(gòu)函數(shù)
class Date {
int _year;
int _month;
int _day;
};
// 需要自己實(shí)現(xiàn)析構(gòu)函數(shù),清理資源。
class Stack {
int* _a;
int _top;
int _capacity;
};
0x02 拷貝構(gòu)造
使用同類型的對象去初始化實(shí)例對象。
參數(shù)必須是引用!不然會導(dǎo)致無窮遞歸。
如果我們不實(shí)現(xiàn),編譯器會默認(rèn)生成一份默認(rèn)的拷貝構(gòu)造函數(shù)。
默認(rèn)生成的拷貝構(gòu)造:
① 內(nèi)置類型完成按子繼續(xù)的值拷貝。 —— 淺拷貝
② 自定義類型的成員變量,會去調(diào)用它的拷貝構(gòu)造。
// 不需要自己實(shí)現(xiàn),默認(rèn)生成的拷貝構(gòu)造,完成淺拷貝就能滿足需求
class Date {
int _year;
int _month;
int _day;
};
// 需要自己實(shí)現(xiàn),因為默認(rèn)生成的淺拷貝不能滿足需求。
// 我們需要自己實(shí)現(xiàn)深拷貝的拷貝構(gòu)造,深拷貝我們后面會用專門的章節(jié)去講解。
class Stack {
int* _a;
int _top;
int _capacity;
};
#include <iostream>
using namespace std;
class Date {
public:
Date(int year = 1, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
void Print() {
printf("%d-%d-%d\n", _year, _month, _day);
}
~Date() {
cout << "&Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main(void)
{
Date d1;
d1.Print();
Date d2(2002);
d2.Print();
Date d3(2022, 3);
d3.Print();
Date d4(2022, 3, 9);
d4.Print();
return 0;
}
??? 運(yùn)行結(jié)果如下:
????
?? [ 筆者 ]? ?王亦優(yōu)
?? [ 更新 ]? ?2022.3.15 | 2023.9.27(重制)
? [ 勘誤 ]?? Star丶北辰:拿棧舉例時malloc空間 sizeof 有誤(已修正)
?? [ 聲明 ]? ?由于作者水平有限,本文有錯誤和不準(zhǔn)確之處在所難免,
本人也很想知道這些錯誤,懇望讀者批評指正!
?? 參考資料? Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/.文章來源:http://www.zghlxwxcb.cn/news/detail-724322.html 比特科技. C++[EB/OL]. 2021[2021.8.31]文章來源地址http://www.zghlxwxcb.cn/news/detail-724322.html |
到了這里,關(guān)于?【C++要笑著學(xué)】(7) 默認(rèn)成員函數(shù):構(gòu)造函數(shù) | 析構(gòu)函數(shù) | 拷貝構(gòu)造函數(shù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!