這里寫目錄標題
- <font color="#FF00FF">1. 類和對象(中)
- <font color="#FF00FF">2. 構造函數
- <font color="#FF00FF">3. 析構函數
- <font color="#FF00FF">4. 拷?構造函數
1. 類和對象(中)
類的默認成員函數:類的默認成員函數就是用戶沒有寫,而編譯器自動生成的函數,?個類,我們不寫的情況下編譯器會默認?成6個默認成員函數,最后兩個之后再說。
2. 構造函數
構造函數是默認成員函數之一,需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開空間創建對象(我們常使?的局部對象是棧幀創建時,空間就開好了),而是對象實例化時初始化對象。構造函數就相當于之前數據結構中的Init函數。
構造函數的特點:
1. 函數名與類名相同。
2. 無返回值。
3. 對象實例化時系統會?動調?對應的構造函數。
4. 構造函數可以重載。
5. 如果類中沒有顯式定義構造函數,則C++編譯器會?動?成?個?參的默認構造函數,?旦?戶顯式定義編譯器將不再?成。
6. 無參構造函數、全缺省構造函數、我們不寫構造時編譯器默認?成的構造函數,都叫做默認構造函數。但是這三個函數有且只有?個存在,不能同時存在。?參構造函數和全缺省構造函數雖然構成函數重載,但是調?時會存在歧義。
7. 我們不寫,編譯器默認?成的構造,對內置類型成員變量的初始化沒有要求,也就是說是是否初始化是不確定的,看編譯器。對于?定義類型成員變量,要求調?這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那么就會報錯
說明:C++把類型分成內置類型(基本類型)和?定義類型。內置類型就是語?提供的原?數據類型,如:int/char/double/指針等,?定義類型就是我們使?class/struct等關鍵字??定義的類型。
這里能夠看出我們并沒有顯示調用構造函數,但成員變量還是被賦值了,說明對象實例化時系統會?動調?對應的構造函數,并且可以重載。
接下來我們看一下后三條:
我把前面手動生成的構造函數代碼注釋掉,會發現編譯器不會對內置類型成員變量進行處理,但是會生成調用,這里不太明顯是否完成了生成調用,后續再說,但是對應了第7條的特性,至于對于?定義類型成員變量,要求調?這個成員變量的默認構造函數初始化,這里后續跟第5條一起說。
這段代碼證實了第6條。
這里就調用了兩個?定義類型成員變量棧,也就說明了第七條:對于?定義類型成員變量,要求調?這個成員變量的默認構造函數初始化。
也就說明了第5條,如果類中沒有顯式定義構造函數,則C++編譯器會?動?成?個?參的默認構造函數,?旦?戶顯式定義編譯器將不再?成,這里就是調用了編譯器默認?成的MyQueue的構造函數。
3. 析構函數
析構函數與構造函數功能相反,析構函數不是完成對對象本?的銷毀,?如局部對象是存在棧幀的,函數結束棧幀銷毀,他就釋放了,不需要我們管,C++規定對象在銷毀時會?動調?析構函數,完成對象中資源的清理釋放?作。析構函數的功能類?我們之前Stack實現的Destroy功能,?像Date沒有Destroy,其實就是沒有資源需要釋放,所以嚴格說Date是不需要析構函數的。
析構函數的特點:
1. 析構函數名是在類名前加上字符 ~。
2. ?參數?返回值。(這?跟構造類似,也不需要加void)
3. ?個類只能有?個析構函數。若未顯式定義,系統會?動?成默認的析構函數。
4. 對象?命周期結束時,系統會?動調?析構函數。
5. 跟構造函數類似,我們不寫編譯器?動?成的析構函數對內置類型成員不做處理,?定義類型成員會調?他的析構函數。
6. 還需要注意的是我們顯?寫析構函數,對于?定義類型成員也會調?他的析構,也就是說?定義類型成員?論什么情況都會?動調?析構函數。
7. 如果類中沒有申請資源時,析構函數可以不寫,直接使?編譯器?成的默認析構函數,如Date;如果默認?成的析構就可以?,也就不需要顯?寫析構,如MyQueue;但是有資源申請時,?定要??寫析構,否則會造成資源泄漏,如Stack。
8. ?個局部域的多個對象,C++規定后定義的先析構。
這里顯示寫了析構函數還是會調用,證明了?定義類型成員?論什么情況都會?動調?析構函數。
4. 拷?構造函數
如果?個構造函數的第?個參數是??類類型的引?,且任何額外的參數都有默認值,則此構造函數也叫做拷?構造函數,也就是說拷?構造是?個特殊的構造函數。
拷貝構造的特點:
1. 拷貝構造函數是構造函數的?個重載。
2. 拷貝構造函數的第?個參數必須是類類型對象的引?,使?傳值?式編譯器直接報錯,因為語法邏輯上會引發?窮遞歸調?。拷?構造函數也可以多個參數,但是第?個參數必須是類類型對象的引?,后?的參數必須有缺省值。
3. C++規定?定義類型對象進?拷??為必須調?拷?構造,所以這??定義類型傳值傳參和傳值返回都會調?拷?構造完成。
4. 若未顯式定義拷?構造,編譯器會?成?動?成拷?構造函數。?動?成的拷?構造對內置類型成員變量會完成值拷?/淺拷?(?個字節?個字節的拷?),對?定義類型成員變量會調?他的拷貝構造。
5.像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器?動?成的拷?構造就可以完成需要的拷?,所以不需要我們顯?實現拷?構造。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器?動?成的拷?構造完成的值拷?/淺拷?不符合我們的需求,所以需要我們??實現深拷?(對指向的資源也進?拷?)。像MyQueue這樣的類型內部主要是?定義類型Stack成員,編譯器?動?成的拷?構造會調?Stack的拷?構造,也不需要我們顯?實現MyQueue的拷?構造。這?還有?個?技巧,如果?個類顯?實現了析構并釋放資源,那么他就需要顯?寫拷?構造,否則就不需要。
6. 傳值返回會產??個臨時對象調?拷?構造,傳值引?返回,返回的是返回對象的別名(引?),沒有產?拷貝。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使?引?返回是有問題的,這時的引?相當于?個野引?,類似?個野指針?樣。傳引?返回可以減少拷貝,但是?定要確保返回對象,在當前函數結束后還在,才能?引?返回。
我們先看前兩點,首先拷貝構造函數是構造函數的?個重載。
這里就是d1去拷貝構造d2,所以d2打印出了結果。
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:Stack(int n = 4){_a = (STDataType*)malloc(sizeof(STDataType) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}Stack(const Stack& st){// 需要對_a指向資源創建同樣?的資源再拷?值 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);if (nullptr == _a){perror("malloc申請空間失敗!!!");return;}memcpy(_a, st._a, sizeof(STDataType) * st._top);_top = st._top;_capacity = st._capacity;}void Push(STDataType x){if (_top == _capacity){int newcapacity = _capacity * 2;STDataType* tmp = (STDataType*)realloc(_a, newcapacity *sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_top = _capacity = 0;}
private:STDataType* _a;size_t _capacity;size_t _top;};
// 兩個Stack實現隊列
class MyQueue
{
public:
private:Stack pushst;Stack popst;
};
int main()
{Stack st1;st1.Push(1);st1.Push(2);// Stack不顯?實現拷?構造,??動?成的拷?構造完成淺拷? // 會導致st1和st2??的_a指針指向同?塊資源,析構時會析構兩次,程序崩潰 Stack st2 = st1;MyQueue mq1;// MyQueue?動?成的拷?構造,會?動調?Stack拷?構造完成pushst/popst //的拷?,只要Stack拷?構造??實現了深拷?,他就沒問題 MyQueue mq2 = mq1;return 0;
}
這也對應第二條。
去掉&這里有兩個拷貝構造,一是返回st時,2是拷貝構造ret時,但編譯器會優化成一次拷貝構造,如果傳引用返回就會報錯,野引用,這里傳值返回不會報錯的原因是臨時對象會調用stack的拷貝構造函數將st拷貝一份返回,等返回拷貝構造完了ret后再銷毀。這符合第三條。
第5條和第6條結合前面理解。