在學C語言時,實現棧和隊列時容易忘記初始化和銷毀,就會造成內存泄漏。而在C++的類中我們忘記寫初始化和銷毀函數時,編譯器會自動生成構造函數和析構函數,對應的初始化和在對象生命周期結束時清理資源。那是什么是默認構造函數呢?例如在一個空類中,編譯器會自動生成六個默認函數。默認成員函數:用戶沒有顯式實現,編譯器會生成的成員函數稱為默認成員函數。
目錄
構造函數
構造函數的特性:
析構函數
拷貝構造函數
構造函數
概念:是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證 每個數據成員都有 一個合適的初始值,并且在對象整個生命周期內只調用一次。
構造函數的特性:
1. 函數名與類名相同。
2. 無返回值。
3. 對象實例化時編譯器自動調用對應的構造函數。
4. 構造函數可以重載。
5. 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦 用戶顯式定義編譯器將不再生成。
7、無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。 注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為 是默認構造函數。
示例:
class Student {
public:Student(){cout << "Student()" << endl;}
private:int age;
};int main()
{Student s1;
}
運行結果:
?創建一個Student對象,我們并未調用初始化函數,編譯器自動調用了默認構造函數Student(),這個雖然是我們自己寫的,但是也是默認構造函數。
注意:無參的構造函數和全缺省函數都可以稱為默認構造函數,并且默認構造函數只能有一個。編譯器自動生成的也是一個默認構造函數,所以有三個默認構造函數。
那么在沒有寫構造函數下,編譯器自動生成的默認構造函數會對內置類型以及自定義類型初始化嗎?
示例:
class Stack {
private:int* _a;int _top;int _capacity;
};class Student {
public:Student(){cout << "Student()" << endl;}
private:int age;Stack s;
};int main()
{Student st1;return 0;
}
通過調試看一下編譯器自動生成的默認構造函數是否對?age和s初始化:
從這里可以看出都進行了初始化,其實不然,如果只有內置類型的話就不初始化了,小編用的是VS2019,編譯器不同是否初始化也不同,所以在寫代碼時不能依賴編譯器自動生成的默認構造函數,還是自己手動寫更好。
這是只用age變量時的結果?:
總結:編譯器自動生成的默認構造函數,對于內置類型不做處理 (但是我們可以在聲明時給值),而對于自定義類型會去調用它自己的默認構造函數。
所以一般情況下構造函數都需要我們自己寫,除非以下兩種情況:
1、 內置類型成員都有缺省值,且符合預期值。
2、全是自定義類型成員,且自定義類型成員都有構造函數。
析構函數
概念:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由 編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作
析構函數特征:
1. 析構函數名是在類名前加上字符 ~。
2. 無參數無返回值類型。
3. 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:析構 函數不能重載。
4. 對象生命周期結束時,C++編譯系統系統自動調用析構函數。
示例:
class Stack {
public:Stack(){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * 4);if (_a == nullptr){perror("malloc fail");return;}_capacity = 4;_top = 0;}~Stack(){cout << "~Stack" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack s1;return 0;
}
運行結果:
?這里都會自動調用析構函數來清理資源,那么問題來了,是每次都需要寫析構函數嗎?
析構函數就和之前對動態空間釋放一樣,對于內置類型變量并沒有處理,因為它們在棧上開辟的空間,在程序生命周期結束會自動銷毀,而向系統申請的空間不一樣,是開辟在堆上的,不能自動釋放,只能手動釋放空間,所以有以下三種情況:
1、一般情況下,有動態資源申請,就需要顯示寫析構函數。
2、沒有動態資源申請,不需要寫析構函數。
3、需要資源釋放的都是自定義成員,不要寫析構函數。
拷貝構造函數
概念:只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存 在的類類型對象創建新對象時由編譯器自動調用。
如果形參是值傳遞,那么會產生什么結果?
示例:
class Student {
public:Student(int age = 18){cout << "Student()" << endl;_age = age;}Student(const Student s)//報錯{_age = s._age;}Student(const Student& s)//正確寫法{_age = s._age;}
private:int _age;
};int main()
{Student s1(20);Student s2(s1);return 0;
}
?這里會報錯,形成無窮遞歸,傳值調用時會先調用Student(Student s),因為傳值所以會再一次調用,循環往復,所以報錯了,可以通過一下示例來加深理解:
?這里進行調試,在準備進入test函數時觀察是否進入了test函數:
這里可以觀察到在準備進入test函數時卻進入Student函數,所以如果在Student函數的形參的傳遞是值傳遞,那么就會無窮遞歸。
?C++規定:內置類型可以直接拷貝,自定義類型必須調用拷貝構造完成拷貝。
拷貝構造函數特征:
1. 拷貝構造函數是構造函數的一個重載形式。 ?
2. 拷貝構造函數的參數只有一個且必須是類類型對象的引用,使用傳值方式編譯器直接報錯, 因為會引發無窮遞歸調用。
3. 若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按 字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
對于內置類型的拷貝可以不用寫拷貝構造函數,但是對于自定義類型需要自己寫拷貝構造函數?
示例:
class Stack {
public:Stack(){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * 4);if (_a == nullptr){perror("malloc fail");return;}_capacity = 4;_top = 0;}~Stack(){cout << "~Stack" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack st1;Stack st2(st1);return 0;
}
這段代碼沒有寫拷貝構造函數,看看會出現什么結果:
?這是什么問題呢?看不出可以調試看一下:
通過調試可以看出,兩個對象的_a的地址相同,也就是說指向了同一塊空間,當指向同一塊空間時,會對該空間進行兩次析構,所以程序崩潰了。
這時就需要我們自己來寫拷貝構造函數,不可以依賴編譯器自動生成的拷貝構造函數。
正確代碼:
class Stack {
public:Stack(){cout << "Stack()" << endl;_a = (int*)malloc(sizeof(int) * 4);if (_a == nullptr){perror("malloc fail");return;}_capacity = 4;_top = 0;}~Stack(){cout << "~Stack" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}Stack(const Stack& st){_a = (int*)malloc(sizeof(int) * st._top);if (_a == nullptr){perror("malloc fail");return;}_capacity = st._capacity;_top = st._top;}
private:int* _a;int _top;int _capacity;
};int main()
{Stack st1;Stack st2(st1);return 0;
}
運行結果:
?這就是自定義類型的深拷貝,當然深拷貝的知識不止這一點點,關注博主后續給你帶來C++更多知識。