21:知識點:判斷一個類是否需要拷貝控制函數成員,首先判斷其是否需要自定義版本的析構函數,如果需要,則拷貝控制成員函數都需要。由于這兩個類中的指針為智能指針,可以自動控制內存的釋放,所以使用類的合成析構函數即可。另外類默認的拷貝控制成員對于智能指針的拷貝也不需要自定義版本來修改,所以全部定義為 =default 即可
22:知識點1:管理類外資源的類必須定義拷貝控制成員
知識點2:為了定義拷貝控制成員,我們可以定義拷貝操作,使得類的行為看起來像是一個值或者一個指針
知識點3:類的行為像一個值,拷貝發生時,副本和原對象是完全獨立的,改變副本不會對原對象產生影響
知識點4:類的行為像一個指針,拷貝發生時,副本和原對象共用底層數據,改變副本也會改變原對象
知識點5:標準庫容器和string類就是像值的類,shared_ptr類就是像指針的類,IO類和unique_ptr不允許拷貝和賦值,所以都不是
class Hasptr{public: Hasptr();//默認構造函數 //拷貝構造函數,完成string 指針指向內容的拷貝和i值的拷貝 Hasptr(const Hasptr& p):ps(new string(*p.ps)),i(p.i){} //拷貝賦值運算符 Hasptr& Operator= (const Hasptr& p) { auto new_ps = new string(p.ps); delete ps; ps = new_ps; return *this; } //析構函數 ~Hasptr(){delete ps;}PRivate: string *ps; int i;};23:知識點1:類值版本,類的構造函數需要可能需要動態分配其成員的副本
知識點2:類值版本,類的拷貝賦值運算符相當于結合了構造函數和析構函數的操作,首先銷毀左側運算對象的資源,再從右側運算符對象拷貝資源,注意順序
知識點3:由于有上述的順序存在,所以我們必須保證這樣的拷貝賦值運算符是正確的:首先將右側運算對象拷貝到一個臨時的對象中,再銷毀左側的運算對象的現有成員,之后將臨時對象中的數據成員拷貝至左側對象中(防范自賦值的情況發生—首先就銷毀了自身的成員,再進行拷貝自身則會訪問到已經釋放的內存中)
見22題,編寫時忘了一個析構函數,ps在構造函數中是動態分配的內存,所以需要進行delete
24:未定義析構函數,ps在使用結束后不會被合成版本的析構函數釋放,造成內存泄漏。未定義拷貝構造函數,使用自定義版本的拷貝構造函數,對于ps的拷貝就會是指針本身的拷貝。
25:動態分配的內存由shared_ptr管理,析構函數之后會自動判斷進行釋放,所以不需要自定義版本的析構函數。
26:知識點1:定義行為像指針的類,在不想使用shared_ptr的情況下我們可以使用引用計數來確定是否釋放內存
知識點2:每個構造函數(拷貝構造函數除外)都創建一個引用計數,記錄對象的共享狀態,第一次被新建時,計數為1
知識點3:析構函數遞減引用計數,拷貝賦值運算符遞增右側對象的引用計數,遞減左側的,當左側的引用計數為0時,拷貝賦值運算符就必須銷毀狀態
知識點4:計數器不能直接作為類對象的成員,否則在拷貝中,會出現歧義,我們可以將計數器保存在動態內存中,只定義一個指向計數器的指針,這樣拷貝或者賦值時,我們拷貝該指針,副本和原對象指向同樣的計數器
class Hasptr1{public: //構造函數,初始化相關成員 Hasptr1(const string& s = string()):ps(new string(s)),i(0),use(new size_t(1)){} //拷貝構造函數,將引用計數也拷貝過來,并且遞增引用計數 Hasptr1(const Hasptr1& p):ps(p.ps),i(p.i),use(p.use){++*use;} //拷貝賦值運算符 Hasptr1& operator= (const Hasptr1& p1) { ++*p1.use;//首先遞增右側運算符對象的引用計數 if (--*use == 0)//遞減本對象的引用計數,若沒有其他用戶,則釋放本對象的成員 { delete ps; delete use; } ps = p1.ps;//進行拷貝 use = p1.use; i = p1.i; return *this; } //析構函數 ~Hasptr1() { if (*use == 0)//引用計數變為0,說明已經沒有對象再需要這塊內存,進行釋放內存操作 { delete ps; delete use; } }private: //定義為指針,是我們想將該string對象保存在動態內存中 string *ps; size_t *use;//將計數器的引用保存 int i;};28:(a)類似于27題 (b)只有一個指針成員,參照27題
29:知識點1:如果一個類定義了自己的swap,那么算法將利用類自己的版本(重排順序等算法)
知識點2:自定義版本的swap存在的必要性:我們不希望進行新的內存分配,只希望將其指針進行拷貝賦值(交換的本質),省去不必要的內存分配,將函數定義為friend,以便訪問private成員
知識點3:相對于拷貝控制成員,swap并不是不要的,但是對于那些分配了資源的類,定義swap可能是一種很重要的優化手段
知識點4:swap函數自定義版本與std中版本的重合問題:對于swap函數,其調用應該都是不加限定的,若加std::swap則調用的是標準庫的版本,而標準庫的版本在一定程度上是為了那些內置類型沒有自定義版本的swap而準備的,若一個類有其自定義版本的swap函數,則我們就不應該使用std版本的。所以我們只要在前加上using std::swap聲明,即可,在使用中,若有類特定的swap,其匹配程度則會優于std中的版本(616頁有詳解)
知識點5:在賦值運算符中使用swap,以傳值的方式傳入新對象,再進行拷貝賦值,在一定程度上會比較安全
見知識點4,因為其調用到最后使用的是std中的swap,不存在循環
30:
class Hasptr{ friend void swap(Hasptr&,Hasptr&);public: Hasptr();//默認構造函數 //拷貝構造函數,完成string 指針指向內容的拷貝和i值的拷貝 Hasptr(const Hasptr& p):ps(new string(*p.ps)),i(p.i){} //拷貝賦值運算符 Hasptr& operator= (const Hasptr& p) { auto new_ps = new string(*p.ps); delete ps; ps = new_ps; return *this; } //析構函數 ~Hasptr(){delete ps;}private: string *ps; int i;};inline void swap(Hasptr& a,Hasptr& b){ using std::swap; swap(a.ps,b.ps); std::swap(a.i,b.i); cout<<"123";}
新聞熱點
疑難解答
圖片精選