C++類和對象之類的6個(gè)默認(rèn)成員函數(shù)詳解
1.類的6個(gè)默認(rèn)成員函數(shù)
默認(rèn)成員函數(shù):用戶沒有顯示實(shí)現(xiàn),編譯器會(huì)生成的成員函數(shù)稱為默認(rèn)成員函數(shù)。
如果一個(gè)類中什么成員都沒有,簡稱為空類。
但空類中真的是什么都沒有嗎?并不是的,任何一個(gè)類在我們不寫的情況下,都會(huì)自動(dòng)生成下面6個(gè)默認(rèn)成員函數(shù)。
class Date{};
- 構(gòu)造函數(shù): 完成初始化工作
- 析構(gòu)函數(shù): 完成清理工作
- 拷貝構(gòu)造函數(shù): 使用同類對象初始化創(chuàng)建對象
- 賦值重載: 把一個(gè)對象賦值給另一個(gè)對象
- 取地址操作符重載: 對普通對象取地址
- const取地址操作符重載: 對const修飾的對象取地址
2.構(gòu)造函數(shù)
2.1概念
對于下面的Date類:
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date today1; today1.Init(2023,1,16); today1.Print(); Date today2; today2.Init(2023, 1, 17); today2.Print(); return 0; }
對于Date類,可以通過Init公有方法給對象設(shè)置日期,但如果每次創(chuàng)建對象時(shí)都調(diào)用該方法設(shè)置信息,未免有點(diǎn)麻煩,那能否在對象創(chuàng)建時(shí),就將信息設(shè)置進(jìn)去呢?
我們就需要一個(gè)函數(shù):保證對象被創(chuàng)造出來就被初始化了。
C++的構(gòu)造函數(shù)提供了這個(gè)功能:
構(gòu)造函數(shù)是一個(gè)特殊的成員函數(shù),名字與類名相同,創(chuàng)建類類型對象時(shí)由編譯器自動(dòng)調(diào)用,以保證每個(gè)數(shù)據(jù)成員都有一個(gè)合適的初始值,并且在對象整個(gè)生命周期內(nèi)只調(diào)用一次。
2.2特性
構(gòu)造函數(shù)是特殊的成員函數(shù),需要注意的是,構(gòu)造函數(shù)雖然名稱叫構(gòu)造,但是構(gòu)造函數(shù)的主要任務(wù)并不是開空間創(chuàng)建對象,而是初始化對象。
其特征如下:
- 函數(shù)名與類名相同。
- 無返回值(也不用寫void)
- 對象實(shí)例化時(shí)編譯器自動(dòng)調(diào)用對應(yīng)的構(gòu)造函數(shù)。
構(gòu)造函數(shù)可以重載。(一個(gè)類可以有多個(gè)構(gòu)造函數(shù))
class Date { public: Date() { cout << "自定義默認(rèn)構(gòu)造函數(shù)" << endl; } //Date(int year = 1, int month= 2, int day = 3) //{ // cout << "自定義全缺省默認(rèn)構(gòu)造函數(shù)" << endl; //} //Date(int year, int month, int day = 1) //{ // cout << "自定義半缺省構(gòu)造函數(shù)" << endl; //} Date(int year, int month, int day) { cout << "自定義構(gòu)造函數(shù)" << endl; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 6); return 0; }
無參的構(gòu)造函數(shù)和全缺省的構(gòu)造函數(shù)都稱為默認(rèn)構(gòu)造函數(shù),并且默認(rèn)構(gòu)造函數(shù)一個(gè)類中只能有一個(gè)。
原因: 這兩個(gè)構(gòu)造函數(shù)雖然滿足重載,但編譯器無法調(diào)用,存在歧義。如上面代碼的第一個(gè)無參構(gòu)造函數(shù)和第二個(gè)注釋的全缺省的構(gòu)造函數(shù),所以默認(rèn)構(gòu)造函數(shù)一個(gè)類只能有一個(gè)。(非默認(rèn)構(gòu)造函數(shù)也只能有一個(gè),如第三個(gè)半缺省構(gòu)造函數(shù)和第四個(gè)構(gòu)造函數(shù),需要傳參,同時(shí)存在會(huì)有歧義)
注意: 更具構(gòu)造函數(shù)需不需要傳參數(shù),我們將其分為兩種
- 默認(rèn)構(gòu)造函數(shù): 無參構(gòu)造函數(shù)、全缺省構(gòu)造函數(shù),我們沒寫編譯器默認(rèn)生成的構(gòu)造函數(shù)(下一條)(這些不需要傳參數(shù)的構(gòu)造函數(shù),都認(rèn)為是默認(rèn)構(gòu)造函數(shù))
- 傳參構(gòu)造函數(shù): 不缺省構(gòu)造函數(shù)、全缺省構(gòu)造函數(shù)
- 創(chuàng)建對象時(shí),調(diào)用默認(rèn)構(gòu)造函數(shù)不要在對象后加括號(hào)(加括號(hào)后編譯器會(huì)將其看作函數(shù)的聲明,而不是創(chuàng)建的對象)。調(diào)用傳參的構(gòu)造函數(shù)在對象后加括號(hào)加參數(shù)。
如果類沒有顯示定義構(gòu)造函數(shù),則C++編譯器會(huì)自動(dòng)生成一個(gè)無參的默認(rèn)構(gòu)造函數(shù),一旦用戶顯示定義編譯器將不再生成。(構(gòu)造函數(shù)可以重載,有很多種,只要我們寫一種,編譯器就不會(huì)默認(rèn)生成構(gòu)造函數(shù))
注意: 如下,創(chuàng)建對象時(shí)不帶括號(hào),調(diào)用的是默認(rèn)構(gòu)造函數(shù),帶括號(hào)后跟參數(shù),調(diào)用傳參構(gòu)造函數(shù)。
如下圖類中已經(jīng)定義了構(gòu)造函數(shù),編譯器不會(huì)在自動(dòng)生成默認(rèn)構(gòu)造函數(shù)。
在增加默認(rèn)構(gòu)造函數(shù)后,正常運(yùn)行
關(guān)于編譯器生成的默認(rèn)構(gòu)造函數(shù),很多人會(huì)疑惑:不實(shí)現(xiàn)構(gòu)造函數(shù)的情況下,編譯器會(huì)生成默認(rèn)的構(gòu)造函數(shù)。但是看起來默認(rèn)構(gòu)造函數(shù)又沒什么用?
如下面的代碼,today對象調(diào)用了編譯器生成的默認(rèn)構(gòu)造函數(shù),但是today對象的三個(gè)成員變量_day/_month/_year,依然是隨機(jī)數(shù),也就是說在這里編譯器生成的默認(rèn)構(gòu)造函數(shù)并沒有什么用?
先介紹一點(diǎn),C++將類型分為以下兩種:
- 內(nèi)置類型: 語言提供的數(shù)據(jù)類型,如:int、char…
- 自定義類型: 我們使用struct、class、union等自己定義的類型
如果一個(gè)類中存在自定義類型的成員變量,需要使用該成員變量對應(yīng)類的默認(rèn)構(gòu)造函數(shù)來初始化,否則無法通過。這也就是默認(rèn)構(gòu)造函數(shù)存在的意義。
自定義類型的成員變量對應(yīng)類存在默認(rèn)構(gòu)造函數(shù)
class A { public: A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: Date() { cout << "默認(rèn)構(gòu)造函數(shù)" << endl; } Date(int year, int month, int day) { cout << "傳參構(gòu)造函數(shù)" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
自定義類型的成員變量對應(yīng)類不存在默認(rèn)構(gòu)造函數(shù)
class A { public: A(int c) { cout << "A" << endl; } private: int a; int b; }; class Date { public: Date() { cout << "默認(rèn)構(gòu)造函數(shù)" << endl; } Date(int year, int month, int day) { cout << "傳參構(gòu)造函數(shù)" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
注意: 在C++11中針對內(nèi)置成員不初始化的缺陷,又打了補(bǔ)丁,即:內(nèi)置類型成員變量在類中聲明時(shí)可以給默認(rèn)值。
class A { public: void Print() { cout << _a << " " << _b << endl; } private: int _a = 10; int _b = 20; }; class Date { public: void Print() { a1.Print(); } private: int _year = 2000; int _month = 1; int _day = 1; A a1; }; int main() { Date today; today.Print(); return 0; }
此為缺省值,當(dāng)構(gòu)造函數(shù)沒有初始化成員變量時(shí),成員變量的值即為該缺省值,若初始化,以構(gòu)造函數(shù)為主(如下面代碼,初始化了一個(gè)變量,該變量就以構(gòu)造函數(shù)初始化為主,其他成員變量為缺省值)
class A { public: A() { _a = 40; } void Print() { cout << _a << " " << _b << endl; } private: int _a = 10; int _b = 20; }; class Date { public: void Print() { a1.Print(); } private: int _year = 2000; int _month = 1; int _day = 1; A a1; }; int main() { Date today; today.Print(); return 0; }
3.析構(gòu)函數(shù)
3.1概念
我們知道了對象創(chuàng)建時(shí)需要構(gòu)造函數(shù)來初始化,那對象銷毀時(shí)又需要什么呢?
析構(gòu)函數(shù): 與構(gòu)造函數(shù)相反,析構(gòu)函數(shù)不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時(shí)自動(dòng)調(diào)用析構(gòu)函數(shù),完成對象中資源的清理工作。
我們創(chuàng)建一個(gè)對象,它是在對象生命周期結(jié)束后,對應(yīng)函數(shù)的棧幀銷毀時(shí)一并銷毀,而析構(gòu)函數(shù)是在銷毀前函數(shù)自動(dòng)調(diào)用,對該對象的資源做清理清空對象的空間或?qū)⑸暾埖目臻g還給編譯器。
對于清理工作,我們必須要做,否則可能會(huì)造成內(nèi)存泄漏,而我們又常常忘記這一操作,于是C++增加了這么一個(gè)函數(shù)。
3.2特性
- 析構(gòu)函數(shù)名是在類名前加上字符**~**(取反符號(hào))
- 無參數(shù)也無返回值
- 一個(gè)類只能有一個(gè)析構(gòu)函數(shù)。若未顯示定義,系統(tǒng)會(huì)自動(dòng)生成默認(rèn)的析構(gòu)函數(shù)。注意:析構(gòu)函數(shù)不能重載
- 對象生命周期結(jié)束時(shí),C++編譯系統(tǒng)自動(dòng)調(diào)用析構(gòu)函數(shù)。
我們編寫如下代碼,向內(nèi)存申請空間,利用析構(gòu)函數(shù)釋放對應(yīng)的空間。
class Stack { public: Stack() { ArrStack = (int*)malloc(sizeof(int) * 4); if(!ArrStack)//下圖中未寫 { preeor("malloc fail!"); exit(-1); } _size = 4; _top = 0; } ~Stack() { if (ArrStack) { free(ArrStack); ArrStack = nullptr; _size = 0; _top = 0; } } private: int* ArrStack; int _size; int _top; }; int main() { Stack st; return 0; }
如果類中沒有申請資源時(shí),析構(gòu)函數(shù)可以不寫,直接使用編譯器生成默認(rèn)析構(gòu)函數(shù),比如Date類;有資源申請時(shí),一定要寫,否則會(huì)造成資源泄漏。
如下面的代碼,當(dāng)我們對同一個(gè)類創(chuàng)建兩個(gè)變量時(shí),構(gòu)造函數(shù)的執(zhí)行順序?yàn)椋簊1、s2,而函數(shù)是一種棧的形式,創(chuàng)建變量就是壓棧,s1先入棧,s2后入棧,銷毀時(shí),s2先出棧,s1后出棧,析構(gòu)函數(shù)的調(diào)用順序?yàn)椋簊2、s1
class Stack { public: Stack(int num) { ArrStack = (int*)malloc(sizeof(int) * num); if(!ArrStack)//下圖中未寫 { preeor("malloc fail!"); exit(-1); } _size = 4; _top = 0; } ~Stack() { if (ArrStack) { free(ArrStack); ArrStack = nullptr; _size = 0; _top = 0; } } private: int* ArrStack; int _size; int _top; }; int main() { Stack s1(10); Stack s1(40); return 0; }
觀察下圖this->_size
的變化
當(dāng)一個(gè)類中有自定義類型的成員變量,那再銷毀這個(gè)類創(chuàng)建的對象時(shí),會(huì)調(diào)用該類中自定義類型的成員變量的析構(gòu)函數(shù)
寫析構(gòu)函數(shù)
class A { public: ~A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: ~Date() { cout << "Date" << endl; } private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
不寫析構(gòu)函數(shù)
class A { public: ~A() { cout << "A" << endl; } private: int a; int b; }; class Date { public: private: int _year; int _month; int _day; A a1; }; int main() { Date today; return 0; }
注意:
- 默認(rèn)生成構(gòu)造函數(shù)和默認(rèn)生成析構(gòu)函數(shù),對內(nèi)置類型不處理,處理自定義類型。(有些編譯器會(huì),但那時(shí)編譯器的個(gè)人行為,和C++的語法無關(guān))
4.拷貝構(gòu)造函數(shù)
4.1概念
拷貝構(gòu)造函數(shù): 只有單個(gè)形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創(chuàng)建新對象時(shí)由編譯器自動(dòng)調(diào)用。
該函數(shù)功能為將一個(gè)對象的數(shù)據(jù)賦值給另一個(gè)對象,發(fā)生拷貝時(shí)編譯器就會(huì)調(diào)用該函數(shù),如下:
class Date { public: Date(int year,int month,int day) { _year = year; _month = month; _day = day; } Date(const Date& d)//拷貝構(gòu)造函數(shù) { _year = d._year; _month = d._month; _day = d._day; cout << "拷貝構(gòu)造函數(shù)" << endl; } private: int _year; int _month; int _day; }; void test(Date d)//調(diào)用拷貝構(gòu)造函數(shù) {} int main() { Date today1(2023,2,7); Date today2(today1);//調(diào)用拷貝構(gòu)造函數(shù) test(today1); return 0; }
4.2特征
拷貝構(gòu)造函數(shù)是構(gòu)造函數(shù)的一個(gè)重載形式。
拷貝構(gòu)造函數(shù)的參數(shù)只有一個(gè) 且 必須是類類型對象的引用 ,使用傳參方式編譯器會(huì)直接報(bào)錯(cuò) ,因?yàn)闀?huì)引發(fā)無窮遞歸調(diào)用。
如果不使用引用,代碼如下:
class Date { public: Date(const Date d)//拷貝構(gòu)造函數(shù) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; };
這樣的拷貝構(gòu)造函數(shù),我們在調(diào)用它時(shí)會(huì)發(fā)生拷貝,而需要拷貝我們就要調(diào)用拷貝構(gòu)造函數(shù),這就會(huì)形參死循環(huán),因?yàn)橐媚阄艺{(diào)用你,而想要調(diào)用你就要用你,編譯器不會(huì)允許這樣的事情發(fā)生。
如上圖,將對象d1的數(shù)據(jù)拷貝到d2,需要調(diào)用拷貝構(gòu)造函數(shù),而調(diào)用的過程形參發(fā)生拷貝又要調(diào)用拷貝構(gòu)造函數(shù),就這樣一直下去,很明顯這是不行的。
所以在這里我們要使用引用,如下:
Date(const Date& d)//拷貝構(gòu)造函數(shù) { _year = d._year; _month = d._month; _day = d._day; }
在第一次調(diào)用的時(shí)候,使用d給對象起別名,就不用再調(diào)用其他拷貝構(gòu)造函數(shù)。
對于這個(gè)函數(shù)建議使用const
修飾,防止我們在寫這個(gè)函數(shù)時(shí)不小心寫錯(cuò),使對象的成員變量發(fā)生改變。
**若未顯示定義,編譯器會(huì)生成默認(rèn)的拷貝構(gòu)造函數(shù)。**默認(rèn)的拷貝構(gòu)造函數(shù)對象按內(nèi)存存儲(chǔ)按字節(jié)序完成拷貝,這種拷貝叫淺拷貝,或值拷貝。
即上面的Date類對象,若發(fā)生淺拷貝只是將一個(gè)對象所占空間內(nèi)所有成員變量的值拷貝到另一個(gè)對象的成員變量,這么做看起來似乎很合理其實(shí)不然,對于內(nèi)置類型,這么做當(dāng)然沒有問題,但如棧這樣的數(shù)據(jù)結(jié)構(gòu),是萬萬不能的。如下面棧的代碼
class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(int* sizeof(int)); 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; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
這樣程序必定會(huì)發(fā)生錯(cuò)誤。
如果想要讓程序正確運(yùn)行我們,需要我們自己編寫拷貝構(gòu)造函數(shù),也就是深拷貝,讓他們每個(gè)對象的的成員變量在面對這種情況時(shí)都有自己獨(dú)立的空間,而不是共用一塊空間。
這也是拷貝構(gòu)造函數(shù)存在的意義,編譯器只能做淺拷貝的工作,若果一個(gè)對象的拷貝需要使用深拷貝,就需要程序員手動(dòng)來完成這個(gè)任務(wù),這也是C語言存在的缺陷,C++的很好的彌補(bǔ)了這一點(diǎn)。
修改后的棧代碼如下:
class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(capacity * sizeof(int)); if (nullptr == _array) { perror("malloc申請空間失敗"); return; } _size = 0; _capacity = capacity; } Stack(const Stack& st) { _array = (int*)malloc(sizeof(int) * st._capacity); if (_array == nullptr) { perror("malloc申請空間失敗"); return; } for (int i = 0; i < st._size; i++) { _array[i] = st._array[i]; } _size = st._size; } void Push(const int& 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; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2(s1); return 0; }
所以如果類中沒有涉及資源申請時(shí),拷貝構(gòu)造函數(shù)是否寫都可以,若涉及到資源申請時(shí),則拷貝構(gòu)造函數(shù)是一定要寫的,否則就是淺拷貝。
拷貝構(gòu)造函數(shù)調(diào)用頻率最多的三種場景場景如下
- 使用以存在的對象創(chuàng)建新對象
- 函數(shù)參數(shù)類型為類類型對象
- 函數(shù)返回值類型為類類型對象
通過這些我們也可以看出,拷貝在編寫代碼中是一個(gè)平常的事情,但其消耗的資源卻不少,所以在實(shí)際使用中,如果可以使用引用,盡量使用引用,減少計(jì)算機(jī)消耗,創(chuàng)出更優(yōu)得程序。
5.賦值運(yùn)算符重載
5.1運(yùn)算符重載
C++為了增強(qiáng)代碼的可讀性引入了運(yùn)算符重載,運(yùn)算符重載是具有特殊函數(shù)名的函數(shù), 也具有其返回值類型,函數(shù)名字以及參數(shù)列表,其返回值類型與參數(shù)列表與普通的函數(shù)類似。
在C++中類的封裝性是做的很好的,如果想在類和類之間進(jìn)行比較,拷貝等操作需要在類內(nèi)調(diào)用函數(shù),而對應(yīng)普通的內(nèi)置類型,只需要使用簡單的運(yùn)算符即可完成,C++規(guī)定可以將部分運(yùn)算符重載來完成這個(gè)功能,增強(qiáng)了代碼的可讀性。
函數(shù)名字為:關(guān)鍵字operator后面接需要重載的運(yùn)算符符號(hào)。
函數(shù)原型:返回值類型 operator操作符(參數(shù)列表)
注意:
- 不能通過連接其他符號(hào)來創(chuàng)建新的操作符:比如operator@
- 重載操作符必須有一個(gè)類類型參數(shù)
- 用于內(nèi)置類型的運(yùn)算符,其含義不能改變,例如:內(nèi)置的整形+,不能改變其含義
- 作為類成員函數(shù)重載時(shí),其形參看起來比操作數(shù)數(shù)目少1.因?yàn)槌蓡T函數(shù)的第一個(gè)參數(shù)為隱藏的this
- .* :: sizeof ?: .注意以上5個(gè)運(yùn)算符不能重載。在這個(gè)經(jīng)常在筆試選擇題中出現(xiàn)。
如下代碼,若運(yùn)算符重載函數(shù)作用域?yàn)槿?,那類的成員變量必須為公有的,這樣封裝性就無法保證
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //成員變量變?yōu)楣校拍苁诡愅庠L問 //private: int _year; int _month; int _day; }; bool operator==(const Date& d1, const Date& d2) { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; } bool test() { Date today1(2023, 2, 7); Date today2; return today1 == today2; }
這里我們可以使用友元解決,也可以將運(yùn)算符重載函數(shù)放入類中,我們一般將其放入類中。
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d1) { return _year == d1._year && _month == d1._month && _day == d1._day; } private: int _year; int _month; int _day; }; bool test() { Date today1(2023, 2, 7); Date today2; //today1.operator==(today2) return today1 == today2; }
在調(diào)用成員函數(shù)時(shí),編譯器會(huì)自動(dòng)將調(diào)用的對象作為this指針傳遞,我們只要寫入一個(gè)參數(shù)即可。
注意:
在使用時(shí)需要注意運(yùn)算符優(yōu)先級,如下面使用運(yùn)算符重載需使用括號(hào)
cout << (today1 == today2) << endl;
運(yùn)算符重載中,如果有多個(gè)參數(shù),第一參數(shù)為左操作數(shù),第二個(gè)參數(shù)為右操作數(shù),以此類推。如上面的代碼,第一個(gè)參數(shù)為today1,為左操作數(shù),由該對象調(diào)用運(yùn)算符重載函數(shù),第二參數(shù)today2即為參數(shù)。
5.2賦值運(yùn)算符重載
賦值運(yùn)算符如果不自己實(shí)現(xiàn),編譯器會(huì)默認(rèn)生成,只有賦值和取地址是這樣的,其它的自定義類型需要使用,就要我們自己寫。(取地址在下面)
賦值運(yùn)算符重載格式:
- 參數(shù)類型: const Typedef&,傳遞引用可以提高傳參效率
- 返回值類型: Typedef&,返回引用可以提高返回得效率,有返回值目的是為了支持連續(xù)賦值。
- 檢測是否自己給自己賦值
- **返回*this:**要符合連續(xù)賦值得含義
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator=(const Date& d) { if (this != &d)//檢測是否自己給自己賦值 { _year = d._year; _month = d._month; _day = d._day; } return *this;//返回*this } private: int _year; int _month; int _day; };
賦值運(yùn)算符只能重載成類得成員函數(shù)不能重載成全局函數(shù)
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private: int _year; int _month; int _day; }; //全局函數(shù)不能用`this`指針,需要給兩個(gè)參數(shù) Date& operator=(const Date& d1, const Date& d2) { if (d1 != &d2)//檢測是否自己給自己賦值 { d1._year = d2._year; d1._month = d2._month; d1._day = d2._day; } return d1;//返回*this }
其中為了訪問類中得成員變量,將其公有化,失去了封裝性。這樣得函數(shù)注定編譯失敗,其中賦值運(yùn)算符沒有實(shí)現(xiàn),則編譯器會(huì)在類中自己實(shí)現(xiàn)一個(gè)默認(rèn)的賦值運(yùn)算符,而在調(diào)用得時(shí)候,我們自己實(shí)現(xiàn)了一個(gè),編譯器又實(shí)現(xiàn)了一個(gè)這就產(chǎn)生沖突。
所以,賦值運(yùn)算符重載只能是類的成員函數(shù)。
上面已經(jīng)講了,如果我們沒有自己寫,編譯器會(huì)自己實(shí)現(xiàn)一個(gè)默認(rèn)的賦值運(yùn)算符重載,在運(yùn)行是是以值得方式逐字節(jié)拷貝。 上面得拷貝構(gòu)造函數(shù)中,編譯器自己默認(rèn)創(chuàng)建的拷貝構(gòu)造函數(shù)也是相同的,只能進(jìn)行淺拷貝,只能拷貝值無法為其分配內(nèi)存,但賦值運(yùn)算符重載還是有一點(diǎn)不同的,它初始化需要分配空間的時(shí)候,會(huì)先為創(chuàng)建的對象分配空間,之后在使用賦值運(yùn)算符,將分配好的空間舍棄,存入其他對象的空間地址。
如下代碼:
// 這里會(huì)發(fā)現(xiàn)下面的程序會(huì)崩潰掉?這里就需要我們以后講的深拷貝去解決。 class Stack { public: Stack(size_t capacity = 10) { _array = (int*)malloc(capacity * sizeof(int)); if (nullptr == _array) { perror("malloc申請空間失敗"); return; } _size = 0; _capacity = capacity; } void Push(const int& 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; }; int main() { Stack s1; s1.Push(1); s1.Push(2); s1.Push(3); s1.Push(4); Stack s2; s2 = s1; return 0; }
我們要注意,如果類中未涉及到資源管理,賦值運(yùn)算符是否實(shí)現(xiàn)都可以;一旦涉及到資源管理則必須要實(shí)現(xiàn)。
5.3前置++和后置++重載
對于前置++,我們按照正常的運(yùn)算符重載模式寫即可,但記得返回類型需要使用類類型&
,將修改后的對象返回。
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date& operator++() { _year += 1; return *this; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 7); Date d; d = ++today; //d:2024.2,7 today:2024,2,7 return 0; }
至于后置++,為了可以讓兩個(gè)函數(shù)實(shí)現(xiàn)重載,規(guī)定增加一個(gè)int類型的參數(shù),作為區(qū)分。
注意:前置++是先++后使用,所以可以直接返回其修改后的對象,對于后置++是先使用后++,所以返回的應(yīng)該是未修改的對象,我們可以在修改原對象前對其進(jìn)行拷貝,然后修改原對象,返回時(shí)直接返回之前拷貝的對象,這樣原對象即改變了,使用的也是未改變的對象,符合后置++
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date operator++() { Date& temp(*this); _year += 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date today(2023, 2, 7); Date d; d = today++; //d:2023,2,7 today:2024,2,7 return 0; }
5.4流插入和流提取運(yùn)算符重載
在C++中,我們輸出和輸入一個(gè)數(shù)據(jù)通常是通過cout、cin
,它們兩其實(shí)就是一個(gè)類對象,重載了<<、>>
兩個(gè)運(yùn)算符,所以輸入、輸出其實(shí)就是調(diào)用兩個(gè)運(yùn)算符重載函數(shù)。
如上圖,它們的類型分別為ostream、istream
,存放在iostream
這個(gè)頭文件中,而C++庫內(nèi)定義的東西都存放在std
這個(gè)命名空間內(nèi),所以我們每次開頭需要寫這兩行代碼。
對于內(nèi)置類型,如下:
int a = 10; double b = 10.0; cout << a; cout << b;
通過函數(shù)重載調(diào)用不同的運(yùn)算符函數(shù),將其打印。
下面我們一起來看一下這兩個(gè)運(yùn)算符是如何重載的。
流提取
在類中定義:
class Date { public: Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void operator<<(ostream& out) { //下面就是輸出內(nèi)置類型的值,流提取調(diào)用頭文件<iostream>內(nèi)的 out << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; int _month; int _day; }; int main() { Date today; //第一個(gè)參數(shù)為左操作數(shù),第二個(gè)參數(shù)為右操作數(shù),由創(chuàng)建的對象調(diào)用類內(nèi)的重載函數(shù) //today.operator<<(cout) today << cout; return 0; }
我們看到,函數(shù)的使用形式是today << cout;
,類對象搶占第一個(gè)參數(shù),一定在左邊,cout在右邊,這么寫肯定不符合我們平常的習(xí)慣,如果要將cout放在第一個(gè)位置,我們需要將函數(shù)在全局定義。
class Date { public: friend ostream& operator<<(ostream& out, const Date& d); Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; //不對對象的成員變量做修改,最好使用const修飾,防止寫錯(cuò),發(fā)生錯(cuò)誤 ostream& operator<<(ostream& out,const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } int main() { Date today; cout << today; return 0; }
如上面的代碼,我們函數(shù)變?yōu)槿趾?,很好的解決了位置的問題,但我們又無法訪問類中的成員變量,這里有三種方法,我們使用第一種
使該函數(shù)變?yōu)轭惖挠言瘮?shù),在類中public作用域下,使用friend修飾函數(shù)的聲明,即可在該函數(shù)內(nèi)使用對應(yīng)類的對象調(diào)用成員變量。增加接口,在類中創(chuàng)建輸出函數(shù),調(diào)用對應(yīng)函數(shù)即可得到對應(yīng)的成員變量值,對象在類外無法訪問成員變量,但可以訪問對外開發(fā)的函數(shù)。(java喜歡這么做)刪除private作用域,這樣成員變量就可以訪問。(不建議這么做,破壞封裝性)
為了防止出現(xiàn)下面的情況,以此要輸出多個(gè)對象的值,我們需要使重載的函數(shù)返回cout
,使函數(shù)可以正常運(yùn)行。
cout << d1 << d2 << d3 << endl; //cout << d1 //調(diào)用重載函數(shù),調(diào)用后返回cout繼續(xù)執(zhí)行 //cout << d2 //同時(shí),運(yùn)行后返回cout //.. //cout << endl; //與重載的類型不匹配,調(diào)用頭文件內(nèi)的函數(shù)
流插入
class Date { public: friend ostream& operator<<(ostream& out, const Date& d); friend istream& operator>>(istream& in, Date& d); Date(int year = 2000, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; ostream& operator<<(ostream& out, const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } //需要改變對象的成員變量,不能使用const修飾 istream& operator>>(istream& in, Date& d) { in >> d._year >> d._month >> d._day; return in; } int main() { Date today; cin >> today; cout << today; return 0; }
如上面的代碼與流提取相似。
6.const成員
如下面的代碼,是否可以正常運(yùn)行呢?
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << "Print" << endl; } private: int _year; int _month; int _day; }; int main() { const Date d; d.Print(); return 0; }
它不能正常運(yùn)行,因?yàn)閷ο骴使用const修飾了,它的值是無法改的(該對象的成員變量無法修改)。在調(diào)用成員函數(shù)時(shí),編譯器默認(rèn)傳過去的值為Date* const this
,this指針表示對象本身,意味著在此函數(shù)內(nèi)成員變量可以改變,這產(chǎn)生了沖突。更簡單的說,這就是將原本只能讀的對象變成可讀可寫,無視其權(quán)限。
想要解決這個(gè)問題,只要使用const修飾*this使其無法改變即可,而this又是編譯器默認(rèn)的,它是被隱藏著的不好修改,C++給出了如下方法,在成員函數(shù)的括號(hào)后直接加const即表示修飾*this
,如下
void Print() const { cout << "Print" << endl; }
如果我們使用為被修飾的const對象調(diào)用被const修飾的成員函數(shù),這時(shí)可以的,原本對象就可以通過調(diào)用成員函數(shù)修改和讀取,現(xiàn)在只是傳過去只能使成員函數(shù)讀取這沒有問題。
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() const { cout << "Print" << endl; } private: int _year; int _month; int _day; }; int main() { Date d; d.Print(); return 0; }
同理:在類中成員函數(shù)是可以相互調(diào)用的,但被const修飾的成員函數(shù)無法調(diào)用沒有被修飾的,因?yàn)楸恍揎椀某蓡T函數(shù)所函數(shù)*this指針是無法改變的,而沒有被修飾的是可以改變的,const失去了作用,這種寫法是錯(cuò)誤的。而沒有被修飾的成員函數(shù)是可以調(diào)用被修飾的,這屬于即可讀又可寫的情況向只可讀的情況發(fā)展,沒有改變語法。
注意:
類內(nèi)部不改變成員變量的成員函數(shù),最好加上const,防止數(shù)據(jù)被修改
一般會(huì)在下面的場景用到const成員
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } void Print() const { cout << _year << endl; } private: int _year; int _month; int _day; }; void test(const Date& d) { d.Print(); } int main() { Date td; test(td); return 0; }
我們在創(chuàng)建對象之初一般不為其修飾cosnt,但我們會(huì)經(jīng)常將對象作為實(shí)參傳遞給其他函數(shù),如果形參被const修飾,那在這個(gè)函數(shù)內(nèi)它只能被讀,無法修改意味著調(diào)用的成員函數(shù)也必須被const修飾。
const這種寫法只針對成員函數(shù)
若定義和聲明分離,需要修飾const時(shí),定義和聲明都要修飾
成員函數(shù)被const修飾和不被修飾構(gòu)成const重載
void Print() const { cout << _year << endl; } void Print() { cout << _year << endl; }
一個(gè)形參為Date* const this
,一個(gè)為const Date* const this
,形參不同滿足重載
若是成員函數(shù)被const修飾注意它的返回值類型,若返回的是成員變量,也需要修飾const,否則權(quán)限發(fā)生變化,編譯會(huì)出錯(cuò)
7.取地址重載和const取地址操作符重載
取地址重載和const取地址操作符重載是最后兩個(gè)編譯器默認(rèn)生成的成員函數(shù),我們一般不會(huì)去寫它,而是直接去使用編譯器默認(rèn)生成的。
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
我們?nèi)绻胍獙懗鰜硪部梢裕缦拢?/p>
class Date { public: Date(int year=2000,int month = 1,int day = 1) { _year = year; _month = month; _day = day; } Date* operator&()//取地址重載 { return this; } const Date* operator&() const //const取地址操作符重載 { return this; } private: int _year; int _month; int _day; }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
兩個(gè)使用的場景不同,取地址重載用在取一般的對象的地址,const取地址操作符重載用在取被const修飾的對象的地址。
總結(jié)
到此這篇關(guān)于C++類和對象之類的6個(gè)默認(rèn)成員函數(shù)的文章就介紹到這了,更多相關(guān)C++類的默認(rèn)成員函數(shù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
初識(shí)C++?Vector模板與實(shí)例化原理
這篇文章主要為大家介紹了初識(shí)C++?Vector模板與實(shí)例化原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12C++類型轉(zhuǎn)換運(yùn)算符的實(shí)例詳解
這篇文章主要介紹了C++類型轉(zhuǎn)換運(yùn)算符的實(shí)例詳解的相關(guān)資料,希望通過本文大家能夠掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09c++遞歸實(shí)現(xiàn)n皇后問題代碼(八皇后問題)
c++遞歸實(shí)現(xiàn)n皇后問題代碼分享,大家參考使用吧2013-12-12基于c++的中國象棋游戲設(shè)計(jì)與實(shí)現(xiàn)
這篇文章主要介紹了基于c++的中國象棋游戲設(shè)計(jì)與實(shí)現(xiàn),主要操作是possibleMove(int?x,?int?y),通過整個(gè)棋盤每個(gè)位置上的信息、中國象棋的規(guī)則來獲得位置(x,?y)這個(gè)棋子可以移動(dòng)到的位置,需要的朋友可以參考一下2022-02-02