前言:在上一篇類和對象(上)的文章中我們已經帶領大家認識了類的概念,定義以及對類和對象的一些基本操作,接下來我們要逐步進入到類和對象(中)的學習。我們將逐步的介紹類和對象的核心——類和對象的六個默認成員函數。(注意:這六個默認成員函數是類和對象的核心,學好了它我們才能更好的去理解類和對象!)
文章目錄
- 一,什么是成員函數?
- 二,默認成員函數的種類
- 三,六個成員函數
- 3.1構造函數
- 3.2構造函數的種類和使用
- 四,析構函數
- 4.1析構函數的作用
- 4.2析構函數的用法
- 五,拷貝構造
- 5.1拷貝構造的作用
- 5.2拷貝構造的使用
- 六,總結
一,什么是成員函數?
要學習類和對象中的六個成員函數,那我們就要先了解什么是成員函數?
- 成員函數就是在類里面定義的函數,一般定義在類里面的都稱為成員如果是變量就稱為成員變量,如果是函數就稱為成員函數。
#include<iostream>
using namespace std;
class A
{
public://成員函數void func(){cout<<"void func()"<<endl;}
private://成員變量int _a;
}
二,默認成員函數的種類
C++的默認成員函數就是說我們沒有顯式的寫該函數編譯器會自動生成該函數就稱為默認成員函數。C++有六個默認的成員函數也就是說這六個成員函數如果我們自己不寫編譯器就會自動生成。至于為什么要搞這些默認成員函數待學完這些默認成員函數你自然就會明白!
六個默認成員函數如下:
六個默認成員函數有三種,分別是執行初始化,拷貝,以及重載功能的函數。
- 執行初始化:構造函數,析構函數
- 執行拷貝:拷貝構造,賦值重載
- 取地址重載:兩個重載函數
注意:這六個成員函數中比較重要的是前4個,后兩個可以作為了解!
下面我們依次介紹這幾個函數。
三,六個成員函數
3.1構造函數
構造函數的概念:構造函數是一種特殊的成員函數,用于在創建對象時初始化對象的狀態。通常與類名相同,無返回類型,支持重載。
3.2構造函數的種類和使用
在上面的構造函數中我們看到了幾種構造函數的類型分別是:
- 默認構造函數:無參數,如果我們沒有顯式的寫出來,編譯器就會自動生成并進行默認初始化。
- 帶參的構造函數:帶參的構造函數又分為有缺省值和沒有缺省值。注意全缺省的構造函數不能與無參的構造函數同時存在!因為這兩個函數在調用時會引發沖突!
- 拷貝構造函數,參數是類名這個我們后面介紹。
class Date
{
public:
//默認構造函數與類名相同,無返回值,支持重載//不帶參數Date(){_year = 1;_month = 1;_day = 1;}//Date(int year=1, int month=1, int day=1) 全缺省的構造函數//一般的構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << this->_year << "/" << this->_month << "/" << _day << endl;}
private:int _year;int _month;int _day;
};
什么都不寫自動調用編譯生成的默認構造,對成員變量進行默認初始化(值是隨機值).
通過上面的代碼我們能了解到構造函數的主要特點:
自動調用:不顯式寫構造函數的情況下,對象創建時由編譯器隱式調用,無需手動觸發。
無返回值:即使語法上不寫 void,也不實際返回任何值。
支持重載:一個類可以定義多個參數列表不同的構造函數。
注意:對于自定義類型成員變量,要求調用這個成員變量的默認構造函數初始化。如果這個成員變量,沒有默認構造函數,那么就會報錯!這就要使用初始化列表了,初始化列表后面介紹。
class A
{
};
class Date
{
private:int _year;int _month;int _day;A a; //自定義類a 對于a的初始化就要去調用A這個類的默認構造!
}
四,析構函數
4.1析構函數的作用
析構函數與構造函數相反,構造函數是執行初始化功能的那么析構函數就是釋放資源的。析構函數發揮作用的場景是在有資源需要釋放的時候使用,如果像日期類沒有資源就沒有必要使用析構。當然編譯器也會自動調用但不影響使用。
4.2析構函數的用法
析構函數相較于構造函數不同的是構造函數名與類名相同,而析構函數是
~類名()
。
注意:若未顯式定義析構函數,編譯器會生成默認析構函數。
默認析構函數:
1. 對基本類型(如 int、float)無操作。
2. 對類成員調用其析構函數(按成員聲明順序逆序調用)。
class Date
{
public:
//構造函數Date(int year = 1, int month = 1, int day = 1){//在初始化之前先打印一下這個函數 這樣便于我們直觀的看到它在被調用cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;_year = year;_month = month;_day = day;}
//析構函數~Date(){//在釋放資源之前打印一下 方便直觀看到被調用cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;return 0;
}
通過上面的代碼我們可以認識到析構函數:
- 析構函數跟構造函數類似,無參數,無返回值。
- 自動調用析構函數,一個類只有一個析構若為顯式定義則調用編譯器生成的析構。
- 在同一個局部域中含有多個對象那么后定義的對象會先析構!
在上面的代碼中我們看到析構函數并沒有做什么事只是單純的在函數內部打印方便我們直觀看到調用,那這不就跟默認生成的析構一樣嗎?
像日期類這樣沒有資源的類編譯器默認生成的就夠用它不hi對內置類型如(int,float)做處理,但是如果是像棧這樣的類那么編譯器生成的析構就不能滿足我們的需求了。下面我們來看棧這個類:
class Stack
{
public:Stack(int n = 4){cout << "Stack(int n = 4)" << endl;_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}//編譯器默認生成的析構函數不能滿足我們的要求它不會去自動的釋放資源 所以需要我們顯式寫析構函數~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_capacity = _top = 0;}
private:int* _a;size_t _capacity;size_t _top;
};
總結一下就是:在對象生命周期結束時系統就會自動調用析構。析構函數與構造函數類似對內置類型不做處理,對于自定義類型的成員就調用它的析構,且無論我們是否顯式寫析構只要類里面包含自定義類型的成員那么就會去調用它自己那個類的析構!
五,拷貝構造
5.1拷貝構造的作用
拷貝構造的作用就是使用同類型的對象去初始化創建另外一個對象,其中這兩個對象都具有相同的成員變量值。可以將拷貝構造理解為給對象創建副本。
5.2拷貝構造的使用
- 拷貝構造與構造函數類似,就是參數上略有不同,所以拷貝構造是構造函數的一個重載。
- 如果沒有顯示定義拷貝構造,編譯器就會自動生成一個拷貝構造。編譯器生成的拷貝構造對內置類型會進行淺拷貝,對于自定義類型就去調用它的拷貝構造。
- C++規定了只要是自定義類型的對象進行拷貝行為就要去調用拷貝構造,無論是自定義類型的傳值傳參還是傳值返回都要去調用拷貝構造。對于引用返回可能引發的野引用問題可以去看我之前的文章:傳送門:引用返回的坑
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;_year = year;_month = month;_day = day;}//d2(d1) 拷貝構造函數生成對象的拷貝 參數類型與被拷貝對象的類型相同為DateDate(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//這里拷貝構造其實也可以傳指針 引用的本質其實就是指針 但是指針還需要開空間 而引用不需要開空間//所以還是優先使用引用做為函數參數/*Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}*/~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1(2025, 4, 24); // 構造Date d2(d1); // 拷貝構造 構成 副本(原對象)//直接賦值也是拷貝的意思Date d4 = d1; // 拷貝構造//使用指針 不推薦//Date d3(&d1); // 構造//Date d5 = &d1; // 構造return 0;
}
在上面的拷貝構造中參數一定要是傳引用傳參而不是傳值傳參,傳值傳參就會引發無窮遞歸!下面我們畫圖來解釋:
這里要注意:像日期類內部沒有資源我們可以使用編譯器默認生成的拷貝構造就可以滿足我們的要求;但是像棧這樣的類就不一樣了,棧這樣的類內部有資源如果直接淺拷貝就會有問題,所以要自己寫拷貝構造下面我們就來了解一下深淺拷貝問題:
class Stack
{
public://構造函數Stack(int n = 4){_a = (int*)malloc(sizeof(int) * n);if (nullptr == _a){perror("malloc申請空間失敗");return;}_capacity = n;_top = 0;}// st2(st1) 拷貝構造函數生成對象的拷貝 參數類型與被拷貝對象的類型相同為stckStack(const Stack& s){cout << "Stack(Stack& s)" << endl;_a = (int*)malloc(sizeof(int) * s._capacity);if (nullptr == _a){perror("malloc申請空間失敗");return;}memcpy(_a, s._a, sizeof(int) * s._top);_top = s._top;_capacity = s._capacity;}
private:int* _a;size_t _capacity;size_t _top;
};
這時可能有人問那以后是不是看到成員變量帶有指針的就要去寫拷貝構造呢?其實不是寫不寫拷貝構造其實跟寫析構函數一樣看類里面有沒有資源,一般來說有資源就要寫析構和拷貝構造,所以我們也可以看一個類如果顯式寫了析構函數那么就要寫拷貝構造。
六,總結
學完了上面的內容回答下面兩個問題:
第?:我們不寫時,編譯器默認生成的函數行為是什么,是否滿足我們的需求?
- 當我們不寫時編譯器會自動生成調用對應的構造,析構,拷貝構造等函數。如果像日期類這樣沒有資源的類可以使用編譯器自己生成的構造,析構,拷貝構造函數;相反如果有資源則需要我們自己顯式寫對應的函數!
第?:編譯器默認生成的函數不滿足我們的需求,我們需要自己實現,那么如何自己實現?
- 像有資源的類編譯器生成的函數不能滿足時比如像構造函數就要自己去創造空間,析構函數就要手動去釋放空間,拷貝構造就要完成深拷貝等。
以上就是本篇文章的所有內容了,感謝各位大佬觀看,制作不易還望各位大佬點贊支持一下! |