1.類的6個默認成員函數?
如果一個類中什么成員都沒有,簡稱為空類。可是空類中真的什么都沒有嗎?
其實并不是的,任何類在什么都不寫時,編譯器會自動生成以下6個默認成員函數。 默認成員函數:用戶沒有顯式實現,編譯器會生成的成員函數稱為默認成員函數。
類的6個默認成員函數編譯器都會自己生成,如果編譯器生成的默認成員函數能夠滿足我們的需求,我們就無需再自己實現;
相反,如果編譯器生成的默認成員函數不能滿足我們的需求,我們就必須要自己實現了。
本篇博客正是介紹類的這6個默認成員函數都有哪些特性,講述什么情況下只需使用默認成員函數,什么情況下需要自己實現以及要怎樣實現的問題!
2. 構造函數
2.1 構造函數的概念
如下代碼,我們定義一個日期類并且調用成員函數:
#include <iostream>
using namespace std;class Data
{
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()
{Data d1;d1.Init(2023, 5, 23);d1.Print();Data d2;d2.Init(2022, 5, 23);d2.Print();return 0;
}
?
按照我們之前學過的,按部就班地先調用初始化成員函數,再調用打印成員函數,運行結果也中規中矩地跑出來了。
可是有一天,我需要很多個Data變量,寫代碼又太急躁,在創建某個Data變量時忘記調用Init成員函數了,如下所示:
#include <iostream>
using namespace std;class Data
{
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()
{Data d1;d1.Init(2023, 5, 23);d1.Print();Data d2;d2.Print();return 0;
}
結果d2出現了隨機值:
通過以上贅述:對于Data類,可以通過Init公有方法給對象設置日期,如果忘記一次初始化就會導致bug的產生,那就不得不每次創建對象時都調用該方法設置信息,可是這樣是不是有些太麻煩了呢?有沒有方法在對象創建時,就將對象設置進去呢?
答案當然是有的,這就引出了C++的1個默認成員函數——構造函數:
構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證每個數據成員都有一個合適的初始值,并且在對象整個生命周期內只調用一次。
2.2 構造函數的特性
構造函數是特殊的成員函數,需要注意的是,構造函數沒有用我們經常熟悉的Init來命名,雖然名稱叫構造,但是構造函數的主要任 務并不是開空間創建對象,而是初始化對象。
其特征如下:
1. 函數名與類名相同;
2. 無返回值;
3. 對象實例化時編譯器自動調用對應的構造函數;
4. 構造函數可以重載;
驗證如下:
#include <iostream>
using namespace std;class Data
{
public://退出歷史舞臺:/*void Init(int year, int month, int day){_year = year;_month = month;_day = day;}*///1. 函數名與類名相同;2. 無返回值;Data(int year, int month, int day){_year = year;_month = month;_day = day;}//4. 構造函數可以重載;Data(){_year = 8;_month = 8;_day = 8;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{//調用含參構造函數:Data d1(2023, 5, 23); //3. 對象實例化時編譯器自動調用對應的構造函數;d1.Print();//調用無參構造函數:Data d2; //注意這里不能用諸如:Data d2();不能加(),因為會與函數聲明產生歧義;d2.Print();return 0;
}
當然這里也完全可以用到缺省參數:
#include <iostream>
using namespace std;class Data
{
public:Data(int year = 8, int month = 8, int day = 8){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Data d1(2023, 5, 23); d1.Print();Data d2(2023, 5);d2.Print();Data d3;d3.Print();return 0;
}
5.?如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
代碼驗證如下:
#include <iostream>
using namespace std;class Data
{
public:如果顯示定義,編譯器將不再生成//Data(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()
{Data d1;d1.Print();return 0;
}
將Date類中構造函數屏蔽后,代碼可以通過編譯,因為編譯器生成了一個無參的默認構造函數:
將Date類中構造函數放開,代碼編譯失敗,因為一旦顯式定義任何構造函數,編譯器將不再生成無參構造函數,放開后報錯:error C2512: “Date”: 沒有合適的默認構造函數可用:
這時你可能要問了:
在不顯示定義構造函數的情況下,編譯器會生成默認的構造函數。但是看起來默認構造函數似乎并沒有什么用處呀!?
上面d1對象調用了編譯器生成的默認構造函數,但是d1的對象_year/_month/_day,結果顯示依舊是隨機值,上面的運行結果就是鐵錚錚的事實呀!這不是恰恰證明了這里編譯器生成的默認構造函數并沒有什么卵用嗎?
這就涉及到了構造函數的第6個特性:
6.C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語言提供的數據類型,如:int/char...,自定義類型就是我們使用class/struct/union等自己定義的類型,編譯器生成默認的構造函數會對自定義類型成員調用的它的默認成員函數,而內置類型則不做處理。
代碼驗證如下:
#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
{public:
void Print()
{cout << _year << "-" << _month << "-" << _day << endl;
}
private:// 基本類型(內置類型)int _year;int _month;int _day;// 自定義類型Time _t;
};
int main()
{Date d;d.Print();return 0;
}
說到這里,我又有些不解,同樣都是變量,為什么還要分自定義類型調用它的默認成員函數,內置類型卻不做處理呢?這難道不是一件畫蛇添足的事情嗎?
這次不否定了,說的確實有道理,所以在C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在類中聲明時可以給默認值。
代碼驗證如下:
#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
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// 基本類型(內置類型)// C++11支持,聲明時給缺省值int _year = 2023;int _month = 5;int _day = 23;// 自定義類型Time _t;
};
int main()
{Date d;d.Print();return 0;
}
思考如下代碼能否正常運行:
#include <iostream>
using namespace std;
class Date
{
public:Date(){_year = 2023;_month = 5;_day = 23;}Date(int year = 2023, int month = 5, int day = 23){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};
// 以下測試函數能正常運行嗎?
void Test()
{Date d1;
}
答案是否定的:
針對以上現象,可以引出構造函數的第7個特性:
7.無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。 注意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認構造函數。?
3. 析構函數
3.1 析構函數的概念
通過上面構造函數的學習,我們知道一個對象是怎么來的,那一個對象又是怎么沒的呢?這就需要我們學習析構函數了:
析構函數:與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。
3.2 析構函數的特性
析構函數也是特殊的成員函數,其特征如下:
1. 析構函數名是在類名前加上字符 ~;
2. 無參數無返回值類型;
3. 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。注意:與構造函數不同的是,析構函數不能重載;
4. 對象生命周期結束時,C++編譯系統自動調用析構函數;
#include <iostream>
using namespace std;class Data
{
public:Data(int year = 2023, int month = 5, int day = 23){_year = year;_month = month;_day = day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}//1. 析構函數名是在類名前加上字符 ~;2. 無參數無返回值類型;~Data(){cout << "~Data" << endl;}private:int _year;int _month;int _day;
};
int main()
{Data d;d.Ptint();//這里為調用~Data,4. 對象生命周期結束時,C++編譯系統自動調用析構函數;return 0;
}
當然,Data類并不需要析構函數,這里只是為了證明C++自動調用了析構函數。
我們將析構函數用到順序表中,可能會對析構函數有更深刻的理解:
#include <iostream>
using namespace std;typedef int DataType;class SeqList
{
public:SeqList(){cout << "已經調用了SeqList()構造函數;" << endl;_a = (DataType*)malloc(sizeof(DataType) * 4);if (_a == nullptr){perror("malloc failed");//如果擴容失敗,說明原因exit(-1);}_size = 0;//當size≥capacity時就動態開辟空間_capacity = 4;//初始化數組容量為4}~SeqList(){cout << "已經調用了~SeqList()析構函數;" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{SeqList sl;return 0;
}
對于第3條特性,系統自動生成默認的析構函數,會不會完成一些事情呢?
5. 答案與構造函數相似,編譯器生成的默認析構函數,對自定義類型成員調用它的析構函數,而內置類型則不做處理。
代碼驗證如下:
#include <iostream>
using namespace std;class Time
{
public:~Time(){cout << "已經調用了~Time()析構函數" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private:// 基本類型(內置類型)int _year = 2023;int _month = 5;int _day = 23;// 自定義類型Time _t;
};
int main()
{Date d;return 0;
}
運行結果為:
對以上結果和第3、第5條特性的詳細解釋:
程序運行結束后輸出:“已經調用了~Time()析構函數”,在main中根本沒有直接創建Time類的對象,為什么最后會調用Time類的析構函數?
因為:main中創建了Date對象d,而d中包含4個成員變量,其中_year, _month, _day三個是內置類型成員,銷毀時不需要資源清理,最后系統直接將其內存回收即可;
而_t是Time類對象,所以在d銷毀時,要將其內部包含的Time類的_t對象銷毀,所以要調用Time類的析構函數;
但是:main函數中不能直接調用Time類的析構函數,實際要釋放的是Date類對象,所以編譯器會調用Date類的析構函數,而Date沒有顯式提供,則編譯器會給Date類生成一個默認的析構函數,目的是在其內部調用Time類的析構函數;
即:當Date對象銷毀時,要保證其內部每個自定義對象都可以正確銷毀,main函數中并沒有直接調用Time類析構函數,而是顯式調用編譯器為Date類生成的默認析構函數;
注意:創建哪個類的對象則調用該類的構造函數,銷毀哪個類的對象則調用該類的析構函數
6. 如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數,比如 Date類;有資源申請時,一定要寫,否則會造成資源泄漏,比如SeqList類。?
4.拷貝構造函數
4.1 拷貝構造函數的概念
電視劇中以及現實中,雙胞胎的例子不在少數,我們甚至可以說簡直他們就是一個模子里刻出來的!那么,在創建對象時,可否創建一個與已存在對象一某一樣的新對象呢?答案的肯定的。
拷貝構造函數:只有單個形參,該形參是對本類 類型對象的引用(一般常用const修飾),在用已存在的類 類型對象創建新對象時由編譯器自動調用。
4.2 拷貝構造函數的特性
1. 拷貝構造函數是構造函數的一個重載形式。即:拷貝構造函數是一個特殊的構造函數;
2. 拷貝構造函數的參數只有一個且必須是類 類型對象的引用,使用傳值方式編譯器直接報錯,因為會引發無窮遞歸調用;
代碼驗證如下:
不考慮特性2,我們偏偏就要直接傳值調用:
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Date( Date d) {_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}
會發現程序報錯:
這是為什么呢?答案就像特征2中所說的,在此過程中引發了無窮遞歸調用:
當我們直接傳值調用時,會發生先傳值再調用拷貝構造函數的情況,即:
所以正確應該如特性2所說的那樣:
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// Date( Date d) // 錯誤寫法:編譯報錯,會引發無窮遞歸Date( Date& d) // 正確寫法{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}
那么在概念中又提到:(一般常用const修飾),這是為什么呢?
這是為了防止我們在定義拷貝構造函數時寫反了:
Date( Date& d) {d._year = _year;d._month = _month;d._day = _day;}
那么運行結果不但不會正確,反而會偷雞不成蝕把米:
所以加上const,即使出現了這樣的低級錯誤,編譯器就會報錯,我們也能及時發現:
Date( const Date& d) {d._year = _year;d._month = _month;d._day = _day;}
正確代碼:
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// Date( Date d) // 錯誤寫法:編譯報錯,會引發無窮遞歸Date( const Date& d) // 正確寫法{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Print();Date d2(d1);d2.Print();return 0;
}
3.與構造函數和析構函數相似,若未顯式定義,編譯器會生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。
在編譯器生成的默認拷貝構造函數中,內置類型是按照字節方式直接拷貝的,而自定義類型是調用其拷貝構造函數完成拷貝的。
代碼驗證如下:
#include <iostream>
using namespace std;
class Time
{
public:Time(){_hour = 8;_minute = 8;_second = 8;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "已經調用了!Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:// 基本類型(內置類型)int _year = 2023;int _month = 7;int _day = 7;// 自定義類型Time _t;
};
int main()
{Date d1;// 用已經存在的d1拷貝構造d2,此處會調用Date類的拷貝構造函數// 但Date類并沒有顯式定義拷貝構造函數,則編譯器會給Date類生成一個默認的拷貝構造函數Date d2(d1);d2.Print();return 0;
}
與前面的構造函數和析構函數相似的問題:編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝了,還需要自己顯式實現嗎?
當然像上面的Data類這樣的類是沒必要的。那么像順序表之類的類呢?驗證如下:
#include <iostream>
using namespace std;typedef int DataType;class SeqList
{
public:SeqList(){cout << "已經調用了SeqList()構造函數;" << endl;_a = (DataType*)malloc(sizeof(DataType) * 4);if (_a == nullptr){perror("malloc failed");//如果擴容失敗,說明原因exit(-1);}_size = 0;//當size≥capacity時就動態開辟空間_capacity = 4;//初始化數組容量為4}//打印void Print(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}//尾插void PushBack(const DataType& x){_a[_size] = x;_size++;}~SeqList(){cout << "已經調用了~SeqList()析構函數;" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;
};int main()
{SeqList sl1;sl1.PushBack(1);sl1.PushBack(2);sl1.PushBack(3);sl1.PushBack(4);sl1.Print();SeqList sl2(sl1);sl2.Print();return 0;
}
運行結果如圖:
我們可以看到,程序崩潰了!這是為什么呢?
打開監視窗口看一下sl1和sl2的內存地址:
發現二者的地址相同,所以我們就知道了:?
1.sl1對象調用構造函數創建,在構造函數中,申請了(_capacity)4個元素的空間,然后里面存儲了4個元素:1 2 3 4;
2. sl2對象使用sl1對象拷貝構造,而SeqList類沒有顯示定義拷貝構造函數,則編譯器會給SeqList類生成一份默認的拷貝構造函數,默認拷貝構造函數是按照值拷貝的,即將sl1中的內容原封不動地拷貝到sl2中。因此sl1與sl2指向了同一塊內存空間;
3. 當程序退出時,sl2和sl1要銷毀。sl2先銷毀,sl2銷毀時調用析構函數,已經將0x00b59580的空間釋放了,但是sl1并不知道,到sl1銷毀時,會將0x00b59580的空間再釋放一次(正如3.2的第5條特性說的那樣),一塊內存空間多次釋放,必然會導致bug的產生。
現在我已經知道原因了,那么正確的代碼應該怎么寫呢?這就需要用到深拷貝去解決(關于深拷貝后面會有詳解):
//自定義拷貝構造函數,不用編譯器默認生成的(深拷貝)SeqList( const SeqList& sl){_a = (DataType*)malloc(sizeof(DataType) * 4);//我也開辟一個空間if (_a == nullptr){perror("malloc failed");//如果擴容失敗,說明原因exit(-1);}memcpy(_a, sl._a, sizeof(int) * sl._capacity);_size = sl._size;_capacity = sl._capacity;}
所以,我們應該要明白:
4.類中如果沒有涉及資源申請時,拷貝構造函數是否寫都可以;一旦涉及到資源申請 時,則拷貝構造函數是一定要寫的,否則就是淺拷貝。
5.賦值運算符重載函數
5.1運算符重載函數
在學習賦值運算符重載之前,我們先來了解一下運算符重載:
通過上面的學習,我們已經知道了內置類型和自定義類型的區別,思考這樣一個問題:
顯而易見,內置類型對象可以直接用各種運算符,內置類型是語言自己定義的,編譯直接轉換成指令
舉個簡單的例子,內置類型的int類型2和1,編譯器可以輕松知道2>1;內置類型的double類型2.2和1.1,編譯器輕松知道2.2>1.1,諸如此類......
那么問題來了,我們通篇寫的Data類對象,我這時候需要判斷2012年7月7日與2013年7月7日哪個日期更大,編譯器能直接判斷出2012年7月7日<2013年7月7日嗎?顯然是不能的!因為自定義類型編譯器不支持直接轉換成指令。
那么這時候就需要我們自己寫一個函數來實現:
寫一個大于比較函數:
bool Greater(Data d1, Data d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}
再比如我寫一個等于比較的函數:
bool Equal(Data d1, Data d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
運行一下:
#include <iostream>
using namespace std;
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}//private:int _year;int _month;int _day;
};//布爾類型(bool)用于表示真(true)和假(false)的值。
//它只有兩個取值:true 和 false,分別對應 1 和 0 。
bool Greater(Data d1, Data d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}bool Equal(Data d1, Data d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; return 0;
}
說到這里,我們來說一個題外話,關于函數的命名,其實在C語言中我們就遇到過很多了,一個函數命名就如同給自己的孩子取名字一樣,比如上面的判斷大于和判斷相等函數,我能用Greater、Equal,為什么就不能用DaYu、DengYu,或者Compare1、Compare2,又或者func1、func2呢?
這些確實都是可以的呀!我創建的函數,我樂意怎樣取名我就怎樣取名!
可是話說回來,你的孩子在你口中叫狗蛋兒、在老師口中叫張三,有一天你去開家長會,老師問你是誰的家長,你說你是狗蛋兒的家長,你這樣說老師會知道張三就是狗蛋兒,狗蛋兒就是張三嗎?
話再說回來,你寫的函數叫DaYu、DengYu,而你的同事要用這個函數,你寫的DaYu、DengYu,同事能知道這是個什么函數嗎?
所以,為了規避這種情況,增強代碼的可讀性,C++引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有返回值類型,函數名字以及參數列表,其返回值類型與參數列表與普通的函數類似。
函數名字為:關鍵字operator后面接需要重載的運算符符號。
函數原型:返回值類型?operator操作符(參數列表)
因此上述兩個函數就可以寫為(更加規范,我們加上const和&):
//bool Greater(Data d1, Data d2)
bool operator>(const Data& d1, const Data& d2)
{if (d1._year > d2._year){return true;}else if (d1._year == d2._year && d1._month > d2._month){return true;}else if (d1._year == d2._year && d1._month ==d2._month && d1._day > d2._day){return true;}return false;
}//bool Equal(Data d1, Data d2)
bool operator == (const Data& d1, const Data& d2)
{return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);/*cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; */cout << operator > (d1, d2) << endl;cout << operator == (d1, d2) << endl;return 0;
}
以及為了令自定義類型更貼合與內置類型一樣讓編譯器自己計算,直接轉為指令:
int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);/*cout << Greater(d1, d2) << endl;cout << Equal(d1, d2) << endl; *//*cout << operator > (d1, d2) << endl;cout << operator == (d1, d2) << endl;*/bool ret1 = d1 > d2; //d1>d2嗎?是為1,否為0;bool ret2 = d1 == d2; //d1=d2嗎?是為1,否為0;int a = 3 > 2; //3>2嗎?是為1,否為0;int b = 3 == 2; //3=2嗎?是為1,否為0;cout << ret1 << endl << ret2 << endl;cout << a << endl << b << endl;return 0;
}
仔細觀察我們上面寫的Data類,可以發現我把private注釋掉了,那現在我把注釋關掉:
成員變量變私有了該怎么辦呢?其實C++常用的解決方法是直接將函數放到類里面,因為類里面可以隨便訪問private:
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}bool operator == (const Data& d1, const Data& d2){return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
可是這樣寫發現還是編譯不通過:
它說函數參數太多?!我放到類外面參數就不多,怎么放到類里面就多參數了呢?
對!因為存在一個隱含的this指針(詳細請看C++入門2——類與對象(1)中的3),所以這時就可以這樣修改:
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}bool operator>(const Data& d2){if (_year > d2._year){return true;}else if (_year == d2._year && _month > d2._month){return true;}else if (_year == d2._year && _month == d2._month && _day > d2._day){return true;}return false;}bool operator == ( const Data& d2){return _year == d2._year && _month == d2._month && _day == d2._day;}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};
那么在函數調用時,編譯器就會幫我們負重前行:
int main()
{Data d1(2013, 7, 7);Data d2(2012, 7, 7);bool ret1 = d1 > d2; //d1.operator>(d2)--->d1.operator>(&d1,d2)bool ret2 = d1 == d2; //d1.operator==(d2)--->d1.operator==(&d1,d2)cout << ret1 << endl << ret2 << endl;return 0;
}
所以運算符重載歸納有以下特點:
1. 不能通過連接其他符號來創建新的操作符:比如operator@;
2. 重載操作符必須有一個類類型參數;
3.?用于內置類型的運算符,其含義不能改變,例如:內置的整型+,不能改變其含義;
4.?作為類成員函數重載時,其形參看起來比操作數數目少1,因為成員函數的第一個參數為隱藏的this;
5.? .*? ? ::? ? ?sizeof? ? ??:? ? ?.? ??以上5個運算符不能重載。
5.2 賦值運算符重載函數
鋪了那么多前戲,終于來到我們要學習的賦值運算符重載函數了:
我們知道,拷貝構造就是將一個已經初始化的變量A拷貝到未初始化的變量B中,
那么如果存在兩個都已經初始化的變量A、B,我想把A的值拷貝到B,顯然就不能再用拷貝構造了,要用到我們就要開始講的賦值運算符重載:
1. 賦值運算符重載格式:
參數類型:const T&,傳遞引用可以提高傳參效率
返回值類型:T&,返回引用可以提高返回的效率,有返回值目的是為了支持連續賦值檢測是否自己給自己賦值
返回*this :要復合連續賦值的含義
(詳解精華都在代碼里):
#include <iostream>
using namespace std;
class Data
{
public:Data(int year = 2012, int month = 7, int day = 7){_year = year;_month = month;_day = day;}Data(const Data& d){_year = d._year;_month = d._month;_day = d._day;}//d1=d2;d1傳給this,d2傳給d//返回值應該是什么類型呢?當然是Data類型;應該返回d1的地址,所以用&引用返回Data& operator=(const Data& d){//判斷是否為自己給自己賦值,&放到這里不是引用,是取地址://判斷d2的地址是否與d1地址相等if (this != &d){_year = d._year;_month = d._month;_day = d._day;}return *this;//返回d1的地址}void Ptint(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Data d1(2023, 7, 7);Data d2(2022, 8, 8);//拷貝構造:一個已經存在的對象去拷貝初始化另一個對象Data d3(d2);d1.Ptint();d2.Ptint();d3.Ptint();cout << endl;//賦值運算符重載:兩個已經存在的對象拷貝d1 = d2;//運算自定義類型,就要用到運算符重載operator=d1.Ptint();d2.Ptint();d3.Ptint();
}
2. 賦值運算符只能重載成類的成員函數不能重載成全局函數
?原因:賦值運算符如果不顯式實現,編譯器會生成一個默認的。此時用戶再在類外自己實現 一個全局的賦值運算符重載,就和編譯器在類中生成的默認賦值運算符重載沖突了,故賦值 運算符重載只能是類的成員函數。
3. 用戶沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。注意:內置類型成員變量是直接賦值的,而自定義類型成員變量需要調用對應類的賦值運算符重載完成賦值。?
4.如果類中未涉及到資源管理,賦值運算符是否實現都可以;一旦涉及到資源管理則必 須要實現。
這些特性與拷貝構造函數有極大的相似性,這里不再過多贅述。
?5.3?前置++和后置++重載
在C語言中,我們已經知道了:前置++,先加后用;后置++,先用后加 這樣的基本常識
那么我們學了運算符重載,現在我要自定義類型Data前置++和后置++,要怎么定義和實現呢?
前置++:
前置++為先+1后使用;所以前置++的返回值應該是返回+1之后的結果;
故前置++重載函數的實現為:
// 前置++:返回+1之后的結果Date& operator++()//返回d1,所以返回值類型當然為Date類型{_day += 1;return *this;//this指向的對象函數結束后不會銷毀,故以&引用方式返回提高效率}
后置++:
后置++為先使用后+1;所以后置++的返回值應該為+1之前的舊值,故需在實現時需要先將this保存一份,然后給this+1;
那么問題來了,前置++和后置++都是一元運算符,實現起來兩個函數名字相同,都是operator++,怎么才能區分這兩個函數呢?
為了讓前置++與后置++形成能正確重載,C++規定:后置++重載時多增加一個int類型的參數,但調用函數時該參數不用傳遞,編譯器自動傳遞
故后置++重載函數的實現為:
Date operator++(int){Date temp(*this);// 先將this保存一份,然后給this + 1_day += 1;return temp; // temp是臨時對象,出了作用域就會被銷毀,//因此只能以值的方式返回,不能返回引用}
驗證:
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 2023, int month = 7, int day = 7){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的結果Date& operator++()//返回d1,所以返回值類型當然為Date類型{_day += 1;return *this;//this指向的對象函數結束后不會銷毀,故以&引用方式返回提高效率}Date operator++(int){Date temp(*this);// 先將this保存一份,然后給this + 1_day += 1;return temp; // temp是臨時對象,出了作用域就會被銷毀,//因此只能以值的方式返回,不能返回引用}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(2022, 7, 7);d1 = ++d2;//前置++,編譯器默認調用不帶(int)參數的operator++d1.Print();d1 = d2++;//后置++,編譯器默認調用帶(int)參數的operator++d1.Print();return 0;
}
小知識:在C語言中前置++和后置++二者的效率高低可能并不明顯,但是在C++中,一般來說前置++的效率要高于后置++
6.(&)取地址操作符重載函數和const取地址操作符重載函數
終于來到類的最后兩個默認成員成員函數了,二者的形式為:
class Date
{
public://(&)取地址操作符重載Date* operator&(){return this;}//const(&)取地址操作符重載const Date* operator&()const{return this;}
private:int _year; // 年int _month; // 月int _day; // 日
};
這兩個默認成員函數一般不用重新定義 ,編譯器默認會生成。
這兩個運算符一般不需要重載,使用編譯器生成的默認取地址的重載即可,只有特殊情況,才需 要重載,比如想讓別人獲取到指定的內容!所以這里就不再過多介紹這兩個默認成員函數。
(本篇完)?
?