日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

声明及赋值_重述《Effective C++》二——构造、析构、赋值运算

發(fā)布時間:2025/4/5 37 豆豆
生活随笔 收集整理的這篇文章主要介紹了 声明及赋值_重述《Effective C++》二——构造、析构、赋值运算 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

關(guān)于本專欄,請看為什么寫這個專欄。如果你想閱讀帶有條款目錄的文章,歡迎訪問我的主頁。

構(gòu)造和析構(gòu)一方面是對象的誕生和終結(jié);另一方面,它們也意味著資源的開辟和歸還。這些操作犯錯誤會導致深遠的后果——你需要產(chǎn)生和銷毀的每一個對象都面臨著風險。這些函數(shù)形成了一個自定義類的脊柱,所以如何確保這些函數(shù)的行為正確是“生死攸關(guān)”的大事。


條款05:了解C++默默編寫并調(diào)用了哪些函數(shù)

編譯器會主動為你編寫的任何類聲明一個拷貝構(gòu)造函數(shù)、拷貝復制操作符和一個析構(gòu)函數(shù),同時如果你沒有生命任何構(gòu)造函數(shù),編譯器也會為你聲明一個default版本的拷貝構(gòu)造函數(shù),這些函數(shù)都是public且inline的。注意,上邊說的是聲明哦,只有當這些函數(shù)有調(diào)用需求的時候,編譯器才會幫你去實現(xiàn)它們。但是編譯器替你實現(xiàn)的函數(shù)可能在類內(nèi)引用、類內(nèi)指針、有const成員以及類型有虛屬性的情形下會出問題。

  • 對于拷貝構(gòu)造函數(shù),你要考慮到類內(nèi)成員有沒有深拷貝的需求,如果有的話就需要自己編寫拷貝構(gòu)造函數(shù)/操作符,而不是把這件事情交給編譯器來做。
  • 對于拷貝構(gòu)造函數(shù),如果類內(nèi)有引用成員或const成員,你需要自己定義拷貝行為,因為在上述兩個場景中編譯器替你實現(xiàn)的拷貝行為很有可能是有問題的。
  • 對于析構(gòu)函數(shù),如果該類有多態(tài)需求,請主動將析構(gòu)函數(shù)聲明為virtual,具體請看條款07 。

除了這些特殊的場景以外,如果不是及其簡單的類型,請自己編寫好構(gòu)造、析構(gòu)、拷貝構(gòu)造和賦值操作符、移動構(gòu)造和賦值操作符(C++11、如有必要)這六個函數(shù)。


條款06:若不想使用編譯器自動生成的函數(shù),就該明確拒絕。

承接上一條款,如果你的類型在語義或功能上需要明確禁止某些函數(shù)的調(diào)用行為,比如禁止拷貝行為,那么你就應(yīng)該禁止編譯器去自動生成它。作者在這里給出了兩種方案來實現(xiàn)這一目標:

  • 將被禁止生成的函數(shù)聲明為private并省略實現(xiàn),這樣可以禁止來自類外的調(diào)用。但是如果類內(nèi)不小心調(diào)用了(成員函數(shù)、友元),那么會得到一個鏈接錯誤。
  • 將上述的可能的鏈接錯誤轉(zhuǎn)移到編譯期間。設(shè)計一不可拷貝的工具基類,將真正不可拷貝的基類私有繼承該基類型即可,但是這樣的做法過于復雜,對于已經(jīng)有繼承關(guān)系的類型會引入多繼承,同時讓代碼晦澀難懂。

但是有了C++11,我們可以直接使用= delete來聲明拷貝構(gòu)造函數(shù),顯示禁止編譯器生成該函數(shù)。


條款07:為多態(tài)基類聲明virtual

該條款的核心內(nèi)容為:帶有多態(tài)性質(zhì)的基類必須將析構(gòu)函數(shù)聲明為虛函數(shù),防止指向子類的基類指針在被釋放時只局部銷毀了該對象。如果一個類有多態(tài)的內(nèi)涵,那么幾乎不可避免的會有基類的指針(或引用)指向子類對象,因為非虛函數(shù)沒有動態(tài)類型,所以如果基類的析構(gòu)函數(shù)不是虛函數(shù),那么在基類指針析構(gòu)時會直接調(diào)用基類的析構(gòu)函數(shù),造成子類對象僅僅析構(gòu)了基類的那一部分,有內(nèi)存泄漏的風險。除此之外,還需注意:

  • 需要注意的是,普通的基類無需也不應(yīng)該有虛析構(gòu)函數(shù),因為虛函數(shù)無論在時間還是空間上都會有代價,詳情《More Effective C++》條款24。
  • 如果一個類型沒有被設(shè)計成基類,又有被誤繼承的風險,請在類中聲明為final(C++ 11),這樣禁止派生可以防止誤繼承造成上述問題。
  • 編譯器自動生成的析構(gòu)函數(shù)時非虛的,所以多態(tài)基類必須將析構(gòu)函數(shù)顯示聲明為virtual。

條款08:別讓異常逃離析構(gòu)函數(shù)

析構(gòu)函數(shù)一般情況下不應(yīng)拋出異常,因為很大可能發(fā)生各種未定義的問題,包括但不限于內(nèi)存泄露、程序異常崩潰、所有權(quán)被鎖死等。

一個直觀的解釋:析構(gòu)函數(shù)是一個對象生存期的最后一刻,負責許多重要的工作,如線程,連接和內(nèi)存等各種資源所有權(quán)的歸還。如果析構(gòu)函數(shù)執(zhí)行期間某個時刻拋出了異常,就說明拋出異常后的代碼無法再繼續(xù)執(zhí)行,這是一個非常危險的舉動——因為析構(gòu)函數(shù)往往是為類對象兜底的,甚至是在該對象其他地方出現(xiàn)任何異常的時候,析構(gòu)函數(shù)也有可能會被調(diào)用來給程序擦屁股。在上述場景中,如果在一個異常環(huán)境中執(zhí)行的析構(gòu)函數(shù)又拋出了異常,很有可能會讓程序直接崩潰,這是每一個程序員都不想看到的。

話說回來,如果某些操作真的很容易拋出異常,如歸還資源等,并且你又不想把異常吞掉,那么就請把這些操作移到析構(gòu)函數(shù)之外,提供一個普通函數(shù)做類似的清理工作,在析構(gòu)函數(shù)中只負責記錄日志,我們需要時刻保證析構(gòu)函數(shù)能夠執(zhí)行到底。


條款09:絕不在構(gòu)造和析構(gòu)過程中調(diào)用virtual函數(shù)。

結(jié)論正如該條款的名字:請不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用virtual函數(shù)。

在多態(tài)環(huán)境中,我們需要重新理解構(gòu)造函數(shù)和析構(gòu)函數(shù)的意義,這兩個函數(shù)在執(zhí)行過程中,涉及到了對象類型從基類到子類,再從子類到基類的轉(zhuǎn)變。

一個子類對象開始創(chuàng)建時,首先調(diào)用的是基類的構(gòu)造函數(shù),在調(diào)用子類構(gòu)造函數(shù)之前,該對象將一直保持著“基類對象”的身份而存在,自然在基類的構(gòu)造函數(shù)中調(diào)用的虛函數(shù)——將會是基類的虛函數(shù)版本,在子類的構(gòu)造函數(shù)中,原先的基類對象變成了子類對象,這時子類構(gòu)造函數(shù)里調(diào)用的是子類的虛函數(shù)版本。這是一件有意思的事情,這說明在構(gòu)造函數(shù)中虛函數(shù)并不是虛函數(shù),在不同的構(gòu)造函數(shù)中,調(diào)用的虛函數(shù)版本并不同,因為隨著不同層級的構(gòu)造函數(shù)調(diào)用時,對象的類型在實時變化。那么相似的,析構(gòu)函數(shù)在調(diào)用的過程中,子類對象的類型從子類退化到基類。

因此,如果你指望在基類的構(gòu)造函數(shù)中調(diào)用子類的虛函數(shù),那就趁早打消這個想法好了。但很遺憾的是,你可能并沒有意識到自己做出了這樣的設(shè)計,例如將構(gòu)造函數(shù)的主要工作抽象成一個init()函數(shù)以防止不同構(gòu)造函數(shù)的代碼重復是一個很常見的做法,但是在init()函數(shù)中是否調(diào)用了虛函數(shù),就有待考證了,同樣的情況在析構(gòu)函數(shù)中也是一樣。


條款10:令operator =返回一個reference to *this

簡單來說:這樣做可以讓你的賦值操作符實現(xiàn)“連等”的效果:

x = y = z = 10;

在設(shè)計接口時一個重要的原則是,讓自己的接口和內(nèi)置類型相同功能的接口盡可能相似,所以如果沒有特殊情況,就請讓你的賦值操作符的返回類型為ObjectClass&類型并在代碼中返回*this吧。


條款11:在operator=中處理“自我賦值”

自我賦值指的是將自己賦給自己。這是一種看似愚蠢無用但卻在代碼中出現(xiàn)次數(shù)比任何人想象得都多的操作,這種操作常常披著指針的外衣:

*pa = *pb; //pa和pb指向同一對象,便是自我賦值。arr[i] = arr[j]; //i和j相等,便是自我賦值

那么對于管理一定資源的對象重載的operator = 中,一定要對是不是自我賦值格外小心并且增加預判,因為無論是深拷貝還是資源所有權(quán)的轉(zhuǎn)移,原先的內(nèi)存或所有權(quán)一定會被清空才能被賦值,如果不加處理,這套邏輯被用在自我賦值上會發(fā)生——先把自己的資源給釋放掉了,然后又把以釋放掉的資源賦給了自己——出錯了。

第一種做法是在賦值前增加預判,但是這種做法沒有異常安全性,試想如果在刪除掉原指針指向的內(nèi)存后,在賦值之前任何一處跑出了異常,那么原指針就指向了一塊已經(jīng)被刪除的內(nèi)存。

SomeClass& SomeClass::operator=(const SomeClass& rhs) {if (this == &rhs) return *this;delete ptr; ptr = new DataBlock(*rhs.ptr); //如果此處拋出異常,ptr將指向一塊已經(jīng)被刪除的內(nèi)存。return *this;}

如果我們把異常安全性也考慮在內(nèi),那么我們就會得到如下方法,令人欣慰的是這個方法也解決了自我賦值的問題。

SomeClass& SomeClass::operator=(const SomeClass& rhs) {DataBlock* pOrg = ptr;ptr = new DataBlock(*rhs.ptr); //如果此處拋出異常,ptr仍然指向之前的內(nèi)存。delete pOrg;return *this;}

另一個使用copy and swap技術(shù)的替代方案將在條款29中作出詳細解釋。


條款12:復制對象時勿忘其每一個成分

所謂“每一個成分”,作者在這里其實想要提醒大家兩點:

  • 當你給類新增了成員變量時,請不要忘記在拷貝構(gòu)造函數(shù)和賦值操作符中對新加的成員變量進行處理。如果你忘記處理,編譯器也不會報錯。
  • 如果你的類有繼承,那么在你為子類編寫拷貝構(gòu)造函數(shù)時一定要格外小心復制基類的每一個成分,這些成分往往是private的,所以你無法訪問它們,你應(yīng)該讓子類使用子類的拷貝構(gòu)造函數(shù)去調(diào)用相應(yīng)基類的拷貝構(gòu)造函數(shù)。
//在成員初始化列表顯示調(diào)用基類的拷貝構(gòu)造函數(shù) ChildClass::ChildClass(const ChildClass& rhs) : BaseClass(rhs) { // ... }

除此之外,拷貝構(gòu)造函數(shù)和拷貝賦值操作符,他們兩個中任意一個不要去調(diào)用另一個,這雖然看上去是一個避免代碼重復好方法,但是是荒謬的。其根本原因在于拷貝構(gòu)造函數(shù)在構(gòu)造一個對象——這個對象在調(diào)用之前并不存在;而賦值操作符在改變一個對象——這個對象是已經(jīng)構(gòu)造好了的。因此前者調(diào)用后者是在給一個還未構(gòu)造好的對象賦值;而后者調(diào)用前者就像是在構(gòu)造一個已經(jīng)存在了的對象。不要這么做!

總結(jié)

以上是生活随笔為你收集整理的声明及赋值_重述《Effective C++》二——构造、析构、赋值运算的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。