網(wǎng)站建設(shè)業(yè)務(wù)拓展思路北海seo快速排名
💖作者:小樹苗渴望變成參天大樹
???🩹作者宣言:認(rèn)真寫好每一篇博客
💨作者gitee:gitee
💞作者專欄:C語言,數(shù)據(jù)結(jié)構(gòu)初階,Linux,C++
如 果 你 喜 歡 作 者 的 文 章 ,就 給 作 者 點(diǎn) 點(diǎn) 關(guān) 注 吧!
文章目錄
- 前言
- 一、案例引入
- 二、拷貝構(gòu)造
- 三、案例引入
- 四、運(yùn)算符重載
- 五、總結(jié)
前言
今天博主又來更新新的文章了,今天我們接著上面的內(nèi)容就下兩個默認(rèn)成員函數(shù),講完這兩個,剩下來的兩個就簡單了,因?yàn)橛玫揭膊欢?#xff0c;今天講的這個兩個也特別的關(guān)鍵,尤其是第一個也不好理解,我盡量使用易懂的語言給大家講解,而且要用到之前的棧類,日期類,myQueue類,話不多說,我們開始進(jìn)入正文。
一、案例引入
在我們之前學(xué)習(xí)的內(nèi)置類型我定義一個整型變量
int a=10;
此時我想定義一個和a是一樣的變量怎么做:
int b=a;
內(nèi)置類型是這樣就可以解決問題了。
對于自定義類型我們?nèi)绻策@樣呢??
Date d1(2023,5,1);
Date d2=d1;
在C++里面是不允許這么賦值的,在傳營參的時候也不是直接把對象1直接賦值給對象2,必須要通過調(diào)用拷貝構(gòu)造函數(shù)去實(shí)現(xiàn)。
拷貝構(gòu)造函數(shù)其實(shí)是特殊的構(gòu)造函數(shù),也是完成初始化操作的,所以有些特性和構(gòu)造函數(shù)一樣,無返回值,函數(shù)名和類名相同,形參是固定的
拷貝構(gòu)造函數(shù): 只有單個形參,該形參是對本類類型對象的引用(一般常用const修飾),在用已存在的類類型對象創(chuàng)建新對象時由編譯器自動調(diào)用。
我們來看具體寫法:
大家可以看到完成我們想要的效果。
解決困惑:
1.為什么要加引用
我給大家舉一個例子:
class Date
{
public:void print(){cout << _year << " " << _month << " " << _day << endl;}Date()//無參構(gòu)造函數(shù){_year = 1;_month = 1;_day = 1;}Date(const Date& d)//拷貝構(gòu)造函數(shù){_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};void func(int m){}
void func(Date d){}
int main()
{Date d1;//調(diào)用無參構(gòu)造器初始化d1;func(10);//對于傳內(nèi)置類型是直接把值賦給形參,在一開始就講過了func(d1);//這個傳給形參需要調(diào)用拷貝構(gòu)造,然后拷貝構(gòu)造里面的形參也是通過實(shí)參傳過去有會形成新的拷貝構(gòu)造,我們一起來看調(diào)用會出現(xiàn)什么情況//Date d2(d1);//使用d1對象初始化d2,是兩個不同的對象,但是里面的內(nèi)容是一樣的//d1.print();/*d2.print();*/return 0;
}
大家看到我們箭頭指向第二個func的時候不是直接跳到函數(shù)體里面,而是直接跳到拷貝構(gòu)造那里了,因?yàn)楝F(xiàn)在的編譯器都強(qiáng)制檢查,不使用引用就會報錯,如果不加能調(diào)試你就會看到箭頭一致在拷貝構(gòu)造哪一行,永遠(yuǎn)進(jìn)不去拷貝構(gòu)造體里面,無線遞歸下去,就像下面的情況一樣:
這時候就需要使用到引用,形參就是實(shí)參的別名,不需要像傳值一樣,還要創(chuàng)建歷史變量在賦值過去,所以之前的傳值的拷貝構(gòu)造不行,傳引用就直接使用也不需要創(chuàng)建臨時變量,所以可以直接進(jìn)入函數(shù)體里面,這一定是最不好理解的,所以大家一定對于調(diào)用函數(shù)的傳參機(jī)制要弄明白,而且要之前自定義類型和內(nèi)置類型的賦值方式是不一樣的(傳參就是一種賦值)
2.為什么要加const
加const是怕有人寫反,例如:
這樣不僅僅沒有給d2進(jìn)行初始化,反而讓自己的值也改變了,所以加一個const就剛好的解決了這個問題
通過上面的例子,我們大致知道了拷貝構(gòu)造函數(shù)的用法,以及拷貝構(gòu)造函數(shù)的特征有那些,也解釋了拷貝構(gòu)造使用時候該注意的細(xì)節(jié)是什么,算是入門了,但拷貝構(gòu)造的細(xì)節(jié)往往不止這些,讓我們正式進(jìn)入拷貝構(gòu)造的講解
二、拷貝構(gòu)造
在案例引用那一塊我已經(jīng)介紹了拷貝拷貝構(gòu)造的一部分細(xì)節(jié),在文章的開頭,我提到過,拷貝構(gòu)造函數(shù)也是默認(rèn)成員函數(shù),不寫,編譯器會默認(rèn)生成的,那我們來看看不寫會出現(xiàn)什么情況:
大家發(fā)現(xiàn)這寫不寫拷貝構(gòu)造效果都一樣啊
大家在來看一下棧類如果這樣會出現(xiàn)什么情況:
我們發(fā)現(xiàn)我們不寫構(gòu)造函數(shù)就會出現(xiàn)問題,我們通過調(diào)試來看看什么時候出現(xiàn)這樣的錯誤:
我們來看一下圖解:
報錯的原因是對同一塊地址析構(gòu)了兩次,在st1結(jié)束時調(diào)用析構(gòu)釋放了那塊空間,在st2結(jié)束時有調(diào)用了析構(gòu)函數(shù),對已經(jīng)釋放的空間在次釋放,在動態(tài)內(nèi)存管理那一節(jié)明確說過這樣是不可以的。
那為什么會出現(xiàn)這樣的情況呢??
拷貝構(gòu)造在不顯示的時候,并不會像構(gòu)造函數(shù)一樣對內(nèi)置類型不做任何處理,而是會將內(nèi)置類型按照字節(jié)拷貝去進(jìn)行拷貝的,也就是值拷貝或者叫淺拷貝,跟memcpy類似,剛才那種情況是,那三個成員變量都是內(nèi)置類型,指針也屬于內(nèi)置類型,所以才會出現(xiàn)剛才的問題,那么怎么解決這個問題,顯然默認(rèn)生成的肯定不行,就需要自己寫一個拷貝構(gòu)造函數(shù),采取深拷貝,這里就提一下,后面的博客會重點(diǎn)介紹,我們來看一下深拷貝是怎么解決這個問題的:
Stack(const Stack& st){_array = (int*)malloc(sizeof(int) * st._capacity);if (_array == NULL){perror("malloc:");exit(-1);}memcpy(_array, st._array, sizeof(int) * st._size);_size = st._size;_capacity = st._capacity;}
相信大家應(yīng)該知道怎么處理了吧,并且一開始那種不止是析構(gòu)兩次的問題,在操作的是另一個會影響另一個,因?yàn)楣靡粔K空間
可以簡單的理解,拷貝構(gòu)造函數(shù)就是為深拷貝而生的,也體會到了創(chuàng)造者的厲害之處
但是剛才對于默認(rèn)生成的拷貝構(gòu)造函數(shù),對內(nèi)置類型會做處理,對自定義類型呢?我們來看效果:
對于自定義類型,編譯器默認(rèn)生成的拷貝構(gòu)造會自動調(diào)用自定義類中的拷貝構(gòu)造,這一點(diǎn)和構(gòu)造函數(shù),析構(gòu)函數(shù)類似希望大家可以更好的理解
什么時候需要自己寫拷貝構(gòu)造呢??
- 都是內(nèi)置類型并且沒有動態(tài)申請資源的,就可以不用自己寫拷貝構(gòu)造。
- 全部都是自定義類型的時候也不需要寫,典型就是兩個棧實(shí)現(xiàn)隊列的時候
具體問題具體分析,希望大家可以理解。
到這幾乎把拷貝構(gòu)造講解清楚了,大家一定要好好消化,接下來我將講解運(yùn)算符重載函數(shù)
三、案例引入
我們來看一下整型怎么比較大小的:
int a=10;
int b=20;
a<b;
那我們來看一下自定義類型:
Date d1(2023,5,1);
Date d2(2022,4,30);
d1>d2;
我們看到顯然這樣是不行的,我們需要寫一個函數(shù)來進(jìn)行比較大小,我直接將函數(shù)體內(nèi)容寫出來:
bool Less(const Date& x1, const Date& x2)
{if (x1._year < x2._year)//年小就小{return true;}if (x1._year == x2._year && x1._month < x2._month)//年不小,月小就小{return true;}if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)//年月不小,天小就小{return true;}return false;//所以小的都找到,剩下的就是大的
}
大家看我們上面都是報錯,原因就是,我在類外寫的函數(shù)體,恰好成員變量是私有的,只能在類里面使用,所以這個時候就會報錯,就將成員變量的權(quán)限改成共有的就可以了,也可以將函數(shù)放到類里面但是還有好多細(xì)節(jié),一會在說
大家可能認(rèn)為這么解決問題很簡單,但是如果有人的函數(shù)名寫的千奇百怪的怎么辦,又不寫注釋,那么我們使用起來就非常的難受,我們希望的是一開始那種,直接使用運(yùn)算符來進(jìn)行比較,清晰明了,這時候就要使用運(yùn)算符重載來做,
- 我先將成員變量的權(quán)限變成共有的,方便運(yùn)行,平時都是私有的,安全性高
- 我們在運(yùn)算符前面加一個operator就可以重載運(yùn)算符
- 因?yàn)檩敵隽鞑迦氲膬?yōu)先級高于運(yùn)算符所以要加括號
cout << (d1<d2)<<endl;
cout << operator<(d1, d2) << endl;//這兩種寫的效果一樣
在底層的匯編兩者是一樣的指令。
到現(xiàn)在大家應(yīng)該已經(jīng)算是初步了解的運(yùn)算符重載,他的目的就是將原來的運(yùn)算符進(jìn)行一個新的定義,因?yàn)樽远x類型的大小比較只有設(shè)計者自己知道,但為了讓代碼看的顯而易懂,創(chuàng)造者就引出了運(yùn)算符重載。接下來正式開始介紹運(yùn)算符重載,也是給一個新的知識做鋪墊
四、運(yùn)算符重載
為什么要講運(yùn)算符重載,一是他非常重要,二是方便講賦值運(yùn)算符重載,他是默認(rèn)的成員函數(shù)。在案例引用的時候,我們發(fā)現(xiàn)將函數(shù)體寫在外面,成員函數(shù)出現(xiàn)了無法訪問的情況,那我們將函數(shù)寫在類中,看看需要注意什么:
出現(xiàn)參數(shù)太多的情況,原因是成員函數(shù)都會有一個隱含的this,所以這里面報參數(shù)過多,==我們大部分的運(yùn)算符都是二元運(yùn)算符,所以在運(yùn)算符重載幾乎都是只有一個參數(shù)
這樣就可以了,上面那種方式就不可以這么寫了
cout << (d1<d2)<<endl;
cout << d1.operator<(d2) << endl;//必須寫成這樣的形式,d1.才能將d1的地址傳給this指針
大家應(yīng)該知道元素在類中是怎么使用的吧,接下來講解一個重要的知識,賦值運(yùn)算符重載,上面是小于運(yùn)算符重載,這也是默認(rèn)成員函數(shù)只有,不寫就會自動生成,讓我們這個函數(shù)是怎么使用的,和注意的細(xì)節(jié)
賦值運(yùn)算符重載:
對于賦值運(yùn)算符重載,我們只能卸載類里面,不能寫成全局的
用一個已經(jīng)存在的對象去初始化另一個的對象,這是拷貝構(gòu)造
已經(jīng)存在的兩個對象之間的賦值拷貝,這是賦值運(yùn)算符重載
大家一定要理解這兩個,不然一開始很容易將拷貝構(gòu)造和賦值運(yùn)算符搞混,覺得是同一個東西,實(shí)際上還是有本質(zhì)的差別
d1成功被d2賦值了,這是目前寫的一個最簡單版本的賦值運(yùn)算符重載,相比較拷貝構(gòu)造,他又返回值,而拷貝構(gòu)造沒有,但是這種寫的不太完美,萬一我想連續(xù)賦值呢??
d4=d3=d2=d1;
我們以整型為例:
int i,j,k=0;
i=j=k=0;
在整型的時候可以這樣寫,原因是k=0,返回的是k,j=k返回的是j,i=j返回的是i,每個運(yùn)算符返回的都是對應(yīng)類型的,那么我們賦值運(yùn)算符重載是不也要有類型返回值:
這么寫還是不太完美,用值返回,我們在函數(shù)那一將說過,返回的值,需要創(chuàng)建臨時變量,先將值拷貝到臨時變量上,在返回,而我們上面說過,對于自定義類型的拷貝,需要調(diào)用拷貝構(gòu)造,我們來看看效果:
有四次返回就要四次調(diào)用拷貝構(gòu)造,怎么解決這個問題呢??我們就需要使用到引用返回,對于引用返回我們需要主要幾個點(diǎn),局部變量不能喲個引用很危險,靜態(tài)的可以用引用,對于這里,我們的this是局部變量出了我們的賦值運(yùn)算符函數(shù)就會被銷毀,但是我們返回的是*this,*this就是對象了,他的生命周期是main函數(shù),所以不會隨著賦值運(yùn)算符的結(jié)束而銷毀,所以可以使用引用返回
這里就不用調(diào)用拷貝構(gòu)造減少消耗,提高效率,但是我們還需要完善,有的人會這么寫:
d1=d1;
這樣沒有什么意義,避免這樣我們需要加一個if判斷:
相同對象的地址肯定能夠是一樣的,有的人會這么寫:
if(*this!=d)
這樣寫的前提是重載了!=運(yùn)算符,那這樣成本太高,不如直接用地址來判斷。
說到這里,大家應(yīng)該可以體會到我之前寫的C++入門那篇博客的主要性了吧,前后知識都是連貫的
賦值運(yùn)算符重載也是默認(rèn)的成員函數(shù),不寫會默認(rèn)生成,我們看看默認(rèn)生成的會干那些事:
大家看到我們沒有寫,居然達(dá)到了同樣的效果,那我們寫他為了干什么,對于內(nèi)置類型我們會完成淺拷貝,但是又自定義類型,我們就會去調(diào)用他的賦值運(yùn)算符重載,我們來看效果:
我們來看看效果,會去調(diào)用棧里面的賦值運(yùn)算符重載
所以賦值運(yùn)算符重載的操作行為和構(gòu)造函數(shù)的行為一樣,對內(nèi)置類型完成淺拷貝,對于自定義類型去調(diào)用他的賦值運(yùn)算符。
什么時候需要寫賦值運(yùn)算符重載??
- 全部都是內(nèi)置類型的時候不需要寫(日期類)
- 有動態(tài)開辟的空間需要寫(棧類)
- 都是自定義類型的不需要寫(MyQueue類)
注意:
- 不能通過連接其他符號來創(chuàng)建新的操作符:比如operator@
- 重載操作符必須有一個類類型參數(shù)
- 用于內(nèi)置類型的運(yùn)算符,其含義不能改變,例如:內(nèi)置的整型+,不 能改變其含義
- 作為類成員函數(shù)重載時,其形參看起來比操作數(shù)數(shù)目少1,因?yàn)槌蓡T函數(shù)的第一個參數(shù)為隱藏的this
- .* :: sizeof ?: . 注意以上5個運(yùn)算符不能重載。這個經(jīng)常在筆試選擇題中出
現(xiàn)。
五、總結(jié)
今天重點(diǎn)講解了拷貝構(gòu)造和運(yùn)算符重載,這都是默認(rèn)成員函數(shù)比較重要的知識點(diǎn),大家一定要學(xué)號,后面的學(xué)習(xí)都是圍繞這些基礎(chǔ)展開講解的,接下來我會寫一篇博客,來完善我們的日期類,把我們這兩篇總結(jié)的知識點(diǎn)運(yùn)用一下,我們一個六個默認(rèn)成員函數(shù),目前已經(jīng)講解了四個,剩下來的兩個不是重點(diǎn),后面我在單獨(dú)寫一篇博客,給大家簡單介紹一下即可,現(xiàn)在我們的任務(wù)就是完成這兩篇博客的學(xué)習(xí)和練習(xí)。希望大家都來學(xué)到知識。我們下篇再見