🔥個人主頁:@草莓熊Lotso
🎬作者簡介:C++研發方向學習者
📖個人專欄:?《C語言》?《數據結構與算法》《C語言刷題集》《Leetcode刷題指南》
??人生格言:生活是默默的堅持,毅力是永久的享受。
前言: 在上篇博客中我們學習了構造函數和析構函數這兩個類中的默認成員函數,今天這篇博客我想繼續為大家分享拷貝構造函數和賦值運算符重載。主要是先介紹特點再通過舉例說明,所以舉例中的代碼注釋是很重要的。
目錄
一.拷貝構造函數
拷貝構造的特點:
舉例說明:(注意看注釋)?
二.賦值運算符重載
運算符重載:
舉例說明:(注意注釋)
賦值運算符重載:
賦值運算符重載的特點:
舉例說明:(注意注釋)?
一.拷貝構造函數
拷貝構造的特點:
- 拷貝構造函數是構造函數的?個重載。
- 拷貝構造函數的第?個參數必須是類類型對象的引用,使用傳值方式編譯器直接報錯,因為語法邏輯上會引發無窮遞歸用。 拷貝構造函數也可以多個參數,但是第一個參數必須是類類型對象引用,后面的參數必須有缺省值。
- C++規定自定義類型對象進行拷貝行為必須調用拷貝構造,所以這里自定義類型傳值傳參和傳值返回都會調用拷貝構造完成。
- 若未顯式定義拷貝構造,編譯器會自動生成拷貝構造函數。自動生成的拷貝構造對內置類型成員變量會完成值拷貝/淺拷貝(?個字節?個字節的拷貝)(這里和構造與析構不一樣,它是會對內置類型有特定處理的),對自定義類型成員變量會調用他的拷貝構造。
- 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器自動生成的拷貝構造就可以完成需要的拷貝,所以不需要我們顯示實現拷貝構造。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器自動生成的拷貝構造完成的值拷貝/淺拷貝不符合我們的需求,所以需要我們自己實現深拷貝(對指向的資源也進行拷貝)。像MyQueue這樣的類型內部主要是自定義類型Stack成員,編譯器自動生成的拷貝構造會調用Stack的拷貝構造,也不需要我們顯示實現MyQueue的拷貝構造。這里還有一個小技巧,如果?個類顯示實現了析構并釋放資源(也可以說必須顯示實現析構),那么他就需要顯示寫拷貝構造,否則就不需要。
- 傳值返回會產生?個臨時對象調用拷貝構造,傳值引用返回,返回的是返回對象的別名(引用),沒有產生拷貝。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使用引用返回是有問題的,這時的引用相當于?個野引用,類似?個野指針?樣。傳引用返回可以減少拷貝,但是?定要確保返回對象,在當前函數結束后還在,才能用引用返回。
--上述特點在舉例說明中都會體現出來?
關于無窮遞歸圖示:
舉例說明:(注意看注釋)?
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷貝構造函數//Date(Date d),這種寫法不行,我們可以拿個func的例子看看,會發現傳值要先調用拷貝函數,下面有提到// 但是拷貝函數本身再一直調用拷貝函數本身的話,會出現無限遞歸的問題,所以要使用傳引用傳參//加個const可以讓下面傳實參的選擇更多,避免出現權限擴大等問題Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//其實這里用默認生成的也會處理內置類型,進行淺拷貝,在這里是沒問題的。這也是和構造和析構的一個區別//用指針實現拷貝,但是這里并不算拷貝函數/*Date(Date* d){_year = d->_year;_month = d->_month;_day = d->_day;}*/void Print(){cout << _year << "/" << _month << "/" << _day << '\n';}
private:int _year;int _month;int _day;
};//自定義類型傳值傳參和傳值返回都會調用拷貝構造完成
//傳值傳參會調用拷貝函數
void func(Date d)
{}//傳值返回也會調用拷貝函數,但是傳引用就不會(后續會用Stack繼續來講),這里就不過多介紹了
//Date& func2()
Date func2()//這里最好用傳值返回
{Date d(2025,7,31);return d;
}int main()
{Date d1(2025,8,1); Date d2(d1);//func(d1);//我們調試發現,他會先去調用Date里面的拷貝函數再去func//傳值返回可以,傳引用返回不行,這里不細講Date ret = func2();//Date ret = (func2());,這樣寫也是可以的//由這個我們還可以看出,那其實之前的拷貝也可以寫出這樣//Date d2 = d1;// 這里可以完成拷?,但是不是拷?構造,只是?個普通的構造//Date d2(&d1);d1.Print();d2.Print();return 0;
}
關于對內置類型處理和深淺拷貝的相關示例: (注意注釋,用的棧)
#include<iostream>
using namespace std;
typedef int STDataType;class Stack
{
public:Stack(int n=4){_a = (int*)malloc(n * sizeof(int));if (_a == nullptr){perror("malloc fail!");exit(1);}_top = 0;_capacity = n;}//這樣寫是錯的,因為這里的數組如果像這樣寫僅僅是淺拷貝,后續析構函數釋放空間會釋放同一塊空間// 畫圖理解,調試也會報錯/*Stack(const Stack& s){_a = s._a;_capacity = s._capacity;_top = s._top;}*///所以我們需要這樣寫Stack(const Stack& s){_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);//申請一塊同樣大小的空間if (_a == nullptr){perror("malloc fail!");exit(1);}//把值再拷貝過去memcpy(_a, s._a, s._top * sizeof(STDataType));//這兩個直接這樣就可以了_capacity = s._capacity;_top = s._top;}//補充一點,這里也不能不寫,用編譯器自動生成的默認的拷貝構造函數,因為這個函數雖然會處理內置類型//但是只會是淺拷貝/值拷貝,像Stack這樣需要有深拷貝的就不行了//可以這樣說,如果一個類必須顯示實現析構函數(需要釋放資源),那么他就一定也要顯示寫拷貝構造函數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");exit(1);}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){cout<<"~Stack()"<<'\n';if (_a){free(_a);_a = nullptr;}_top = 0;_capacity = 0;}
private://內置類型STDataType* _a;int _top;int _capacity;
};int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}
圖示如下:
理解技巧:可以這樣說,如果一個類必須顯示實現析構函數(需要釋放資源),那么他就一定也要顯示寫拷貝構造函數
借助上面的棧的類(這里就不再寫出來了),給大家對比看看傳引用返回在這里的弊端,同時也是在說第6個特點 :(注意注釋)
int& func1()
{int x = 1;return x;
}//自定義類型傳值返回是會調用拷貝函數的,但是傳引用返回不會,畫圖分析。
//它沒調拷貝函數的話,在后面函數棧幀銷毀,st析構掉了之后。你再通過別名來找,就出問題了,畫圖
//Stack func2()
Stack& func2()
{Stack st;return st;
}int main()
{int ret1 = func1();cout << ret1 << '\n';//可能是1也可能是隨機值,我們之前判斷過//但是這個棧就很明顯了,我們調試看看Stack ret2 = func2();//這里其實也是拷貝return 0;
}
?--會報realloc fail,但是上面的結構需要改一下(int改size_t),不然不會報這個錯誤
??再來看看如果是默認生成的拷貝構造函數對自定義類型的處理:(注意注釋)
#include<iostream>
using namespace std;
typedef int STDataType;class Stack
{
public:Stack(int n = 4){_a = (int*)malloc(n * sizeof(int));if (_a == nullptr){perror("malloc fail!");exit(1);}_top = 0;_capacity = n;}Stack(const Stack& s){_a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);//申請一塊同樣大小的空間if (_a == nullptr){perror("malloc fail!");exit(1);}//把值再拷貝過去memcpy(_a, s._a, s._top * sizeof(STDataType));//這兩個直接這樣就可以了_capacity = s._capacity;_top = s._top;}//補充一點,這里也不能不寫,用編譯器自動生成的默認的拷貝構造函數,因為這個函數雖然會處理內置類型//但是只會是淺拷貝/值拷貝,像Stack這樣需要有深拷貝的就不行了//可以這樣說,如果一個類必須顯示實現析構函數(需要釋放資源),那么他就一點也要顯示寫拷貝構造函數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");exit(1);}_a = tmp;_capacity = newcapacity;}_a[_top++] = x;}~Stack(){cout << "~Stack()" << '\n';if (_a){free(_a);_a = nullptr;}_top = 0;_capacity = 0;}
private://內置類型STDataType* _a;int _top;int _capacity;
};class MyQueue
{
public://編譯器默認生成MyQueue的構造函數調用了Stack的構造函數,完成了兩個成員的初始化//編譯器默認生成MyQueue的拷貝構造函數調用了Stack的拷貝構造函數,完成了拷貝//編譯器默認生成MyQueue的析構函數調用了Stack的析構函數,釋放的Stack內部的資源
private://自定義類型Stack _pushst;Stack _popst;//內置類型,但很奇怪,混在這里它卻能處理,這里大家可以自己去試試//int size = 0;
};
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;
}
二.賦值運算符重載
--在正式學習賦值運算符重載之前我們需要先了解一下運算符重載
運算符重載:
- 當運算符被用于類類型的對象時,C++語言允許我們通過運算符重載的形式指定新的含義。C++規定類類型對象使用運算符時,必須轉換成調用對應運算符重載,若沒有對應的運算符重載,則會編譯報錯。
- 運算符重載是具有特殊名字的函數,他的名字是由operator和后面要定義的運算符共同構成。和其他函數?樣,它也具有其返回類型和參數列表以及函數體。
- 重載運算符函數的參數個數和該運算符作用的運算對象數量?樣多。一元運算符有?個參數,二元運算符有兩個參數,?元運算符的左側運算對象傳給第?個參數,右側運算對象傳給第二個參數。
- 如果一個重載運算符函數是成員函數,則它的第?個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數比運算對象少一個。
- 運算符重載以后,其優先級和結合性與對應的內置類型運算符保持?致。
- 不能通過連接語法中沒有的符號來創建新的操作符:比如operator@。
- .* (點*),:: ,sizeof ,?:? ,. (點)注意以上5個運算符不能重載。(選擇題里面常考,大家要記一下)
- 重載操作符至少有一個類類型參數,不能通過運算符重載改變內置類型對象的含義,如: int?operator+(int x, int y)
- 一個類需要重載哪些運算符,是看哪些運算符重載后有意義,比如Date類重載operator-就有意義,但是重載operator+(當日期加日期時)就沒有意義(加天數還是可以的)。
- 重載++運算符時,有前置++和后置++,運算符重載函數名都是operator++,無法很好的區分。C++規定,后置++重載時,增加?個int形參,跟前置++構成函數重載,方便區分。
- 重載<<和>>時,需要重載為全局函數,因為重載為成員函數,this指針默認搶占了第?個形參位置,第?個形參位置是左側運算對象,調?時就變成了 對象<<cout,不符合使用習慣和可讀性。重載為全局函數把ostream/istream放到第?個形參位置就可以了,第?個形參位置當類類型對象。
--上述特點在舉例說明中大多都會提到,其中最后三個特點會在后續的博客中講解?
舉例說明:(注意注釋)
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//其實這里用默認生成的也會處理內置類型,進行淺拷貝,在這里是沒問題的。//這也是和構造和析構的一個區別,構造和析構不會處理內置類型int Getyear(){return _year;}//d1==d2,傳d2就行,d1有this指針,但是在實參和形參不能直接寫出來,函數體內可以bool operator==(const Date& d2){return this->_year == d2._year&& this->_month == d2._month&& this->_day == d2._day;}void Print(){cout << _year << "/" << _month << "/" << _day << '\n';}
private:int _year;int _month;int _day;
};// 重載為全局的面臨對象訪問私有成員變量的問題
// 有幾種方法可以解決:
// 1、成員放公有--這個最容易,但是不那么好
// 2、Date提供getxxx函數--上面有在類里面展現出來可以自己看看,然后在底下的函數體內需要修改一下
// 3、友元函數--這里先不講這個
// 4、重載為成員函數--這個我也在類里重載為成員函數了,但是有些需要注意的地方,我最后選取這種
//bool operator==(const Date& d1, const Date& d2)
//{
// return d1._year == d2._year//如果用了Get**就是這樣寫的:d1.Getyear() == d2.Getyear()
// && d1._month == d2._month
// && d1._day == d2._day;
//}int main()
{Date d1(2025,8,1);Date d2(2025,10,1);Date d3(2025, 8, 1);//我們在這里就需要實現運算符重載函數d1 == d2;//運算符重載函數可以顯示調用//operator==(d1, d2);//如果成成員函數了,顯示調用是這樣的//d1.operator==(d2);//只要傳一個參d2就行,d1通過this指針,但是不能在實參和形參顯示寫出來的//再加上運算符重載要求參數個數和運算符作用對象一樣多,所以只能傳一個//可以具體去看看上面類里面怎么實現的cout << (d1 == d2) << '\n';//這里需要打括號,優先級的問題,0cout << (d1 == d3) << '\n';//1return 0;
}
--0表示不相等,1表示相等?
給大家大概看一下 .* 這個符號:(注意注釋)
// .*符號普及,了解即可,剛好提到了這個運算符不能重載
#include<iostream>
using namespace std;void func1()
{cout << "void func()" << endl;
}class A
{
public:void func2(){cout << "A::func()" << endl;}
};int main()
{// 普通函數指針void(*pf1)() = func1;(*pf1)();// A類型成員函數的指針void(A::*pf2)() = &A::func2;A aa;(aa.*pf2)();//這里就是使用的.*return 0;
}
賦值運算符重載:
賦值運算符重載的特點:
- 賦值運算符重載是?個運算符重載,規定必須重載為成員函數。賦值運算重載的參數建議寫成const 當前類類型引用,否則會傳值傳參會有拷貝(const還可以有效防止權限擴大,讓能傳的參選擇更多)
- 有返回值,且建議寫成當前類類型引用,引用返回可以提高效率,有返回值目的是為了支持連續賦值的場景。
- 沒有顯式實現時,編譯器會自動生成?個默認賦值運算符重載,默認賦值運算符重載行為跟默認拷貝構造函數類似,對內置類型成員變量會完成值拷貝/淺拷貝(?個字節?個字節的拷貝),對自定義類型成員變量會調用他的賦值重載函數。
- 像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器自動生成的賦值運算符重載就可以完成需要的拷貝,所以不需要我們顯?實現賦值運算符重載。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器自動生成的賦值運算符重載完成的值拷貝/淺拷貝不符合我們的需求,所以需要我們自己實現深拷貝(對指向的資源也進行拷貝)。像MyQueue這樣的類型內部主要是自定義類型Stack成員,編譯器自動生成的賦值運算符重載會調用Stack的賦值運算符重載,也不需要我們顯示實現MyQueue的賦值運算符重載。這里還有?個小技巧,如果?個類顯示實現了析構并釋放資源,那么他就需要顯示寫賦值運算符重載,否則就不需要(跟拷貝構造函數類似)。
--上述特點大部分會在后續的舉例說明中解釋
舉例說明:(注意注釋)?
//賦值運算符重載
#include<iostream>
using namespace std;class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//傳引用返回可以減少拷貝(之前提到過在這里傳值返回是自動調用拷貝函數的)//這里能使用是傳引用返回是因為第一個參數用this來的,函數棧幀銷毀也不會找不到//函數要返回類型是為例更好處理連續賦值的情況(d3=d1=d2),用void不好處理Date& operator=(const Date& d)//const和傳引用傳參的作用就不再多說了{//自己等于自己就可以不用賦值了if (this != &d){_year = d._year;_month = d._month;_day = d._day;}//比如:d1=d2表達式的返回對象應該為d1,也就是*thisreturn *this;}//賦值運算符重載,但其實在Date類型里面不寫也沒影響,跟拷貝構造函數處理內置類型原理一樣//思考聯想方法也一樣,不再說了void Print(){cout << _year << "/" << _month << "/" << _day << '\n';}
private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(2025, 8, 1);d1 = d2;Date d3;d3 = d1 = d2;//從右往左d1.Print();d2.Print();d3.Print();// 需要注意這里是拷?構造,不是賦值重載// 要牢牢記住賦值重載完成兩個已經存在的對象直接的拷?賦值// 而拷?構造用于一個對象拷?初始化給另?個要創建的對象Date d4 = d1;//因為拷貝構造如果寫出這樣就有點容易混//Date d4(d1);//寫成這樣的時候不太容易混淆return 0;
}
完整源代碼:?
cpp-exclusive-warehouse: 【CPP知識學習倉庫】 - Gitee.com
往期回顧:
《吃透 C++ 類和對象(上):封裝、實例化與 this 指針詳解》
《吃透 C++ 類和對象(中):構造函數與析構函數的核心邏輯》
結語:本篇博客就到此結束了,在學完類和對象的這些知識后,雖然還沒學完,但博主后續會先更新實現一個完整的日期類的博客,這個還是有點難度的。大家可以看完之后自己去試一下,檢驗一下自己的學習成果,如果文章對你有幫助的話,歡迎評論,點贊,收藏加關注,感謝大家的支持。