c++之類和對象詳解 拷貝構造,指派運算符重載
拷貝構造
那在建立對象時,可否建立一個與已存在對象一某一樣的新對象呢?
==拷貝構造函數:隻有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象建立新對象時由編譯器自動調用==
構造——初始化
拷貝構造——拷貝初始化
class Date { //.... }; int main() { Date d1(2000, 1, 1); Date d2(d1);//以d1的資料來初始化d2 return 0; }
拷貝構造特征
拷貝構造函數也是特殊的成員函數,其特征如下:
- 拷貝構造函數是構造函數的一個重載形式。
- 拷貝構造函數的參數隻有一個且必須是==類類型對象的引用==,使用傳值方式編譯器直接報錯, 因為會引發無窮遞歸調用。
為什麼會引發無窮遞歸引用呢?class Date { public: Date(const Date& d) { cout << "拷貝構造成功!" << endl; _year = d._day; _month = d._month; _day = d._day; } Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year = 0; int _month = 0; int _day = 0; }; void func1(Date d)//形參是實參的拷貝 { cout << "func1" << endl; } void func2(Date& d)//形參是實參的别名 { cout << "func2" << endl; } int main() { Date d1(2000, 1, 1); func1(d1); func2(d1); return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 我們可以看到==傳值引用導緻産生的臨時變量進行拷貝時候會引發拷貝構造==!
假如我們使用的是傳值引用來當拷貝構造的參數:
拷貝構造函數接收值需要建立臨時變量!建立臨時變量引發拷貝構造,然後拷貝構造又需要建立臨時變量去接收值,那麼臨時變量的臨時變量又引發拷貝構造......以此不斷的遞歸下去!
是以隻能使用引用,因為引用是取别名實際用的仍然是那個變量!![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 那能不呢使用指針呢?答案是可以的!==因為指針的類型是date 産生的臨時變量不會引發拷貝構造,但是這時候叫做構造函數,不是拷貝構造函數!==,==因為拷貝構造的定義就是參數類型要是類的引用!==但是那樣子很不友善還要解引用等操作,不如使用引用!*為什麼要使用拷貝構造這個東西呢?
因為自定義類型可能很複雜!編譯器不清楚應該按什麼方式拷貝!
像是内置類型編譯器可以直接按位元組一個個拷貝!
但是像是連結清單或者樹呢?
像是連結清單我們就要一個個拷貝!
像是樹我們就得遞歸拷貝!
先是拷貝根再拷貝左子樹,然後拷貝右子樹
然後一直遞歸下去!
拷貝構造的注意
為了防止反向拷貝的情況發生我們一般都會加上const縮小權限!Date(Date& d) { _year = d._day; _month = d._month; d._day = _day; }//沒有加上const會導緻反向拷貝使原先資料丢失! Date(const Date& d) { _year = d._day; _month = d._month; d._day = _day; }//這樣的話一旦發生反向拷貝就會直接報錯!
- 若未顯式定義,編譯器會生成==預設的拷貝構造函數==。 預設的拷貝構造函數對象按記憶體存儲按位元組序完成拷貝,這種拷貝叫做==淺拷貝==,或者值拷貝。
注意:在編譯器生成的預設拷貝構造函數中,内置類型是按照位元組方式直接拷貝的,而自定 義類型是調用其拷貝構造函數完成拷貝的。class Date { public: //Date(Date& d) //{ // _year = d._day; // _month = d._month; // _day = d._day; //} Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } private: int _year = 0; int _month = 0; int _day = 0; }; int main() { Date d1(2000, 1, 1); Date d2(d1); return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 我們可以看到對于日期類這種隻含内置類型的,編譯器自動生成的預設拷貝構造函數已經夠用了!
但是像是更複雜就會出問題
class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack()//這就是棧的析構函數! { free(_a); _a = nullptr; _top = 0; _capacity = 0; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2(st1); return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 看上去好像是完成了拷貝!但是有沒有發現st1和st2的指針指向了==同一塊記憶體空間==,也就是意味着我 改變st1就會改變st2!
這個程式會導緻崩潰!因為當st2的指針被析構函數釋放掉之後,st1的析構函數就會導緻通路野指針!
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 是以此時我們需要使用到深拷貝!![]()
拷貝構造,指派運算符重載(六千字長文詳解!) class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack() { free(_a); _a = nullptr; _top = 0; _capacity = 0; } stack(stack& st)//深拷貝 { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2(st1); st2.Push(3); return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 這下就成功完成了深拷貝
需要寫析構函數的類的都要寫拷貝構造!
不需要寫析構的類都不需要自己寫!
像是棧,連結清單,順序表,二叉樹....這些要動态開辟的都需要寫析構函數,也都需要寫拷貝構造!
但是像是日期類的這些就不需要!
class stack { //.... }; class MyQuene { public: void Push(int x) { _PushST.Push(x); } private: stack _PopST; stack _PushST; };
像是myqueue這種也都不需要寫拷貝構造!因為它也不需要寫析構函數!![]()
拷貝構造,指派運算符重載(六千字長文詳解!) - 拷貝構造函數典型調用場景:
- 使用已存在對象建立新對象
- 函數參數類型為類類型對象
- 函數傳回值類型為類類型對象
指派運算符重載
class Date
{
public:
Date(Date& d)
{
_year = d._day;
_month = d._month;
_day = d._day;
}
Date(int year, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(2000, 2, 28);
d1 > d2;
d1 == d2;
d1 + 100;
d1 - d2;
return 0;
}
像是上面的d1與d2我們如何簡潔的進行比較呢?如何像是以前一樣使用運算符?這時候就引入了運算符重載!
運算符重載
C++為了增強代碼的可讀性引入了運算符重載,運算符重載是具有特殊函數名的函數,也具有其傳回值類型,函數名字以及參數清單,其傳回值類型與參數清單與普通的函數類似。為了讓自定義類型可以使用運算符!函數名字為:關鍵字operator後面接需要重載的運算符符号。 函數原型:傳回值類型 operator操作符(參數清單)class Date { public: Date(Date& d) { _year = d._day; _month = d._month; _day = d._day; } Date(int year, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } bool operator==(const Date& d2) { return _year == d2._year && _day == d2._day && _month == d2._month; } private: int _year = 0; int _month = 0; int _day = 0; }; int main() { Date d1(2000, 1, 1); Date d2(2000, 2, 28); d1 == d2;//此時編譯器會轉換成調用d1.operator==(d2); d1.operator==(d2);//直接這樣調用也行! //編譯器很聰明如果是放在全局的那麼會優先調用全局,如果全局沒有就回去類裡面找! //如果是全局就會被轉換成operator==(d1,d2) cout << (d1 == d2) << endl;//要加括号因為運算符優先級問題,因為我們并沒有重載<< 是以<< 沒喲辦法輸出自定義類型! return 0; }
為什麼運算符重載在要放在類裡面呢?
因為如果我們放在外面會導緻一個問題,那就是因為類裡面的資料都是私有的!
我們無法通路,如果想要通路我們隻能再寫一個函數用于擷取類裡面的資料!
是以不如直接放在裡面!
bool operator==(const Date& d1,const Date& d2) { return d1._year == d2._year && d1._day == d2._day && d1._month == d2._month; }//這樣寫會顯示參數哦多為什麼? //因為類裡面的非靜态成員函數都會加上一個this指針! //也就是說是實際上的樣子為 bool operator==(cosnt* Date this,const Date& d1,const Date& d2) { return d1._year == d2._year && d1._day == d2._day && d1._month == d2._month; }//是以實際上我們隻需要自己寫一個參數就足夠了! bool operator==(const Date& d2) { return _year == d2._year && _day == d2._day && _month == d2._month; }
指派重載
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
};//這就是指派重載
指派運算符的寫法注意
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
如果怎麼寫的話會出現一個問題就是在無法進行鍊式通路
d1 = d2 = d3;
但是也最好不要使用Date作為傳回類型因為這樣子會産生一個臨時變量!![]()
拷貝構造,指派運算符重載(六千字長文詳解!) Date operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; };
因為d在出了作用域後不會被銷毀可以使用引用來作為傳回值!
使用引用作為傳參的類型 也是為了防止産生臨時變量!const是為了防止對傳參對象進行修改!
以引用作為傳回值的時候要注意不可以傳參對象作為傳回值
Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 這樣就會出現經典的權限放大!
解決方法
cosnt Date& operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; } // or Date& operator=(Date& d) { _year = d._year; _month = d._month; _day = d._day; return d; }
但是這樣也要求我們得使用const的類型去接收,我們一般要求變量都是可以修改的!
是以還是使用Date&
如果使用第二種修改方式這樣也會導緻我們無法對傳參對象進行保護!
Date& operator=(Date& d) { d._year = _year;//萬一寫反了! _month = d._month; _day = d._day; return d; }
指派重載的預設性
如果我們不寫一個指派重載,類中會自己生成一個指派重載!class Date { public: Date(int year = 10,int month = 10,int day = 10) { _year = year; _month = month; _day = day; } //Date& operator=(const Date& d) //{ // _year = d._year; // _month = d._month; // _day = d._day; // return *this; //} private: int _year; int _month; int _day; }; int main() { Date d1(100,10,100); Date d2; d2 = d1; return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 預設的指派重載會完成一次直拷貝!按位元組一個個的拷貝過去!
==預設指派重載和預設拷貝構造很相似==
- 對于内置類型都是進行值拷貝!
- 對于自定義類型都是調用自定義類型的預設成員函數!
預設指派重載調用自定義類型的預設指派重載
預設拷貝構造調用自定義類型的預設拷貝構造!
是以對于預設拷貝構造的問題也會出現在預設指派重載上面
像是對于要動态開辟的類型 例如stack/queue/list.....
class stack { public: stack(int newcapcacity = 4) { int* temp = (int*)malloc(sizeof(int) * newcapcacity); if (temp == nullptr) { perror("malloc fail"); exit(-1); } _a = temp; _top = 0; _capacity = newcapcacity; } ~stack()//這就是棧的析構函數! { free(_a); _a = nullptr; _top = 0; _capacity = 0; } stack(stack& st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } void Push(int x) { if (_top == _capacity) { int newcapacity = 2 * _capacity; int* temp = (int*)realloc(_a, sizeof(int) * newcapacity); if (temp == nullptr) { perror("realloc fail"); exit(-1); } _a = temp; _capacity = newcapacity; } _a[_top++] = x; } private: int* _a; int _top; int _capacity; }; int main() { stack st1; st1.Push(1); st1.Push(2); stack st2; st2.Push(3); st2.Push(4); st1 = st2; return 0; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) ==然後這個程式會發生崩潰!==
因為兩個指針指向了同一塊的記憶體位址!
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) st2的析構函數釋放了這一塊記憶體空間
然後st1的析構函數又一次的釋放了這一塊記憶體空間!
釋放野指針導緻了崩潰!
而且還會導緻==記憶體洩漏!==
因為st的指針指向了st2的空間,但是卻沒有釋放自己原有的空間!
是以要自己去寫指派重載!
stack& operator=(const stack& st) { free(_a); _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit; } memcpy(_a, st._a, sizeof(int) * _top); _top = st._top; _capacity = st._capacity; return *this; }
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 這樣子就可以完成對于棧這種類型的指派
但是這樣寫還是有缺陷!比如遇到自己指派給自己的時候!
![]()
拷貝構造,指派運算符重載(六千字長文詳解!) 我們會發現原本的值竟然變成的随機值!
因為我們一開始就free掉了原來的空間!是以導緻了一旦自己指派給自己的時候,一開始的free就會導緻資料的丢失!
是以我們要進一步的修改
stack& operator=(stack& st) { if (this != &st) { _a = (int*)malloc(sizeof(int) * st._capacity); if (_a == nullptr) { perror("malloc fail"); exit(-1); } memcpy(_a, st._a, st._top * sizeof(int)); _capacity = st._capacity; _top = st._top; } return *this; }
為什麼不使用realloc去改變原來的數組大小,反倒是使用先free再malloc的形式呢?
答:==因為要考慮的情況太多了,數組比原來大,數組比原來小,數組和原來相同==
數組比原來的大我們可以正常使用realloc。
數組比原來小,我們不使用realloc(realloc一般不用于縮小數組!)
但是如果不對數組進行縮小而是正常的進行拷貝,萬一出現原先數組有100000個 指派數組隻有10個這種情況的話就會導緻極大的空間浪費!
是以基于以上的幾種情況我們選擇采用先free在malloc的方式!
我們從上面的特征可以看出來拷貝構造和指派重載具有很多的相似之處!
我們也可以得出一個相似的結論:
==需要寫析構函數的就要寫顯性指派重載,否則就不需要!==
class stack { //.... } class MyQuene { public: void Push(int x) { _PushST.Push(x); } private: stack _PopST; stack _PushST; }; int main() { MyQuene q1; q1.Push(1); q1.Push(2); MyQuene q2; q2.Push(3); q2.Push(4); q1 = q2; return 0; }
指派重載和拷貝指派的差別在哪裡?
class Date
{
public:
Date(int year = 10,int month = 10,int day = 10)
{
_year = year;
_month = month;
_day = day;
}
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(100,10,100);
Date d2(d1);//拷貝構造 是初始化另一個要馬上建立的對象!
d2 = d1;//指派重載(指派拷貝!)已經存在的兩個對象之間的拷貝!
Date d3 = d1;//這看上去好像是指派重載!
//但是其實拷貝構造!因為從定義上看它更符合 初始化一個要建立的對象!
return 0;
}