C++ 三五法则,看看你能不能理解
簡介:三五法則規定了什么時候需要 1、拷貝構造函數 2、拷貝賦值函數 3、析構函數
1、需要析構函數的類也需要拷貝構造函數和拷貝賦值函數。
通常,若一個類需要析構函數,則代表其合成的析構函數不足以釋放類所擁有的資源,其中最典型的就是指針成員(析構時需要手動去釋放指針指向的內存)。
所以,若存在自定義(且正確)的析構函數,但使用合成的拷貝構造函數,那么拷貝過去的也只是指針,此時兩個對象的指針變量同時指向同一塊內存,指向同一塊內存的后果很有可能是在兩個對象中的析構函數中先后被釋放兩次。所以需要額外的拷貝控制函數去控制相應資源的拷貝。
所以這類例子的共同點就是:一個對象擁有額外的資源(指針指向的內存),但另一個對象使用合成的拷貝構造函數也同時擁有這塊資源。當一方對象被銷毀后,析構函數釋放了資源,這時另一個對象便失去了這塊資源(但程序員還不知道)。
在上面的代碼中對象b使用合成的拷貝構造函數拷貝對象a的值,這個程序沒有什么實際意義。
在main函數返回時,a,b變量會分別被析構,它們的成員name指向同一塊內存,所以在程序結束時便會發生錯誤。
2、需要拷貝操作的類也需要賦值操作,反之亦然。
需要拷貝操作代表這個類在拷貝時需要進行一些額外的操作。 賦值操作 <<< = >>> 先析構+拷貝,所以拷貝需要的賦值也需要。反之亦然。
3、析構函數不能是刪除的
如果類的析構函數是刪除的,那么成員便無法銷毀。所以在程序中不能定義這個類的對象。可以動態分配該對象并獲得其指針,但無法銷毀這個動態分配的對象(delete 失效)。
若上面的類的定義是
則在main函數中定義變量a,b就會發生編譯錯誤,然而,這樣的定義卻可以通過編譯
person *p;
p = new person(“me”, 20)
但是,這樣動態分配的變量是不能被釋放的,在調用 delete p 會發生編譯錯誤, 內存泄露就這樣發生了。
如果類的某個成員的析構函數是刪除的或不可訪問的(例 private的)則類的合成析構函數被定義為刪除的 (小析構NO 大析構NO)
如果類的某個成員的拷貝構造函數是刪除的或不可訪問的(例 private的)則類的合成拷貝函數被定義為刪除的 (小拷貝NO 大拷貝NO)
如果類的某個成員的析構函數是刪除的或不可訪問的(例 private的)則類的合成拷貝函數被定義為刪除的 (小析構NO 大拷貝NO)
如果類的某個成員的析構函數是刪除的或不可訪問的(例 private的)則類的合成拷貝函數被定義為刪除的
4、如果一個類成員有刪除的或不可訪問的析構函數,那么其默認和拷貝構造函數會被定義為刪除的。
如果沒有這條規則,可能會創造出無法被刪除的對象。 理論上來說,當析構函數不能被訪問時,任何靜態定義的對象都不能通過編譯器的編譯,所以這種情況只會出現在與動態分配有關的拷貝/默認構造函數身上。
5、如果一個類有const或引用成員,則不能使用合成的拷貝賦值操作。(無法默認構造的const成員的類 則該類就無默認構造函數)
原因很簡單,const或引用成員只能在初始化時被賦值一次,而合成的拷貝賦值操作會對所有成員都進行賦值。顯然,它不能賦值const和引用成員,所以合成的拷貝構造函數不能被使用,即會被定義為刪除的。
本質上,當不可能拷貝、賦值、或銷毀類的所有成員時,類的合成拷貝控制函數就被定義成刪除的了。
以下這些內容 來自 c++ primer 第五版一書 歸納而來 詳細內容可以 看書 值和指針類型的類設計等
拷貝構造函數
如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有默認值,則此構造函數是拷貝構造函數。
拷貝構造函數第一個參數必須是一個引用類型。此參數幾乎總是一個const的引用。拷貝構造函數在幾種情況下都會被隱式地使用。因此,拷貝構造函數通常不應該是explicit的。
合成拷貝構造函數
與合成默認構造函數不同,即使我們定義了其他構造函數,編譯器也會為我們合成一個拷貝構造函數。
對某些類來說,合成拷貝構造函數用來阻止我們拷貝該類類型的對象。而一般情況,合成的拷貝構造函數會將其參數的成員逐個拷貝到正在創建的對象中。每個成員的類型決定了它如何拷貝。
拷貝初始化
直接初始化和拷貝初始化的差異。
string dots(10,’,’); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷貝初始化
當使用直接初始化時,我們實際上是要求編譯器使用普通的函數匹配來選擇與我們提供的參數最匹配的構造函數。當我們使用拷貝初始化時,我們要求編譯器將右側運算對象拷貝到正在創建的對象中,如果需要的話還要進行類型轉換。
拷貝初始化通常使用拷貝構造函數來完成。拷貝初始化是依靠拷貝構造函數或移動構造函數來完成的。
拷貝初始化不僅在我們用=定義變量時會發生,在下列情況下也會發生
將一個對象作為實參傳遞給一個非引用類型的形參。
從一個返回類型為非引用類型的函數返回一個對象。
用花括號列表初始化一個數組中的元素或一個聚合類中的成員。
參數和返回值
拷貝構造函數被用來初始化非引用類類型參數,這一特性解釋了為什么拷貝構造函數自己的參數必須是引用類型。如果其參數不是引用類型,則調用永遠也不會成功——為了調用拷貝構造函數,我們必須拷貝它的實參,但為了拷貝實參,我們又必須調用拷貝構造函數,如此無限循環。
拷貝初始化的限制
vector v1(10); //直接初始化
vector v1 = 10; //錯誤:接受大小參數的構造函數是explicit的
如果我們希望使用一個explicit構造函數,就必須顯式的使用:
void f(vector); //f的參數進行拷貝初始化
f(10); //錯誤:不能用一個explicit的構造函數拷貝一個實參
(vector(10)); //正確:從一個int直接構造一個臨時vector
如果我們希望使用一個explicit構造函數,就必須顯式的使用:
編譯器可以繞過拷貝構造函數
編譯器被允許將下面的代碼string null_book = “9-999-99999-9”;
給寫成string null_book(“9-999-99999-9”);//編譯器略過了拷貝構造函數。
拷貝賦值運算符
類通過拷貝賦值運算符控制其對象如何賦值。
重載賦值運算符
重載賦值運算符本質上是函數,其名字由operator關鍵字后接表示要定義的運算符的符號組成。因此,賦值運算符就是一個名為operator=的函數。類似于任何其他函數,運算符函數也有一個返回類型和一個參數列表。
值得注意的是,標準庫通常要求保存在容器中的類型要具有賦值運算符,且其返回值是左側運算對象的引用。
合成拷貝賦值運算符
與處理拷貝構造函數一樣,如果一個類未定義自己的拷貝賦值運算符,編譯器會為他它生成一個合成拷貝賦值運算符。類似拷貝構造函數,對于某些類,合成拷貝構造運算符用來禁止該類型對象的賦值。
析構函數
析構函數釋放對象使用的資源,并銷毀對象的非static數據成員。
析構函數是類的一個成員函數,名字由波浪號接類名構成。它沒有返回值,也不接受參數。
由于析構函數不接受參數,因此它不能被重載,對一個給定類,只會有唯一一個析構函數。
析構函數完成什么工作
如同構造函數有一個初始化部分和一個函數體,析構函數也有一個函數體和一個析構部分。在一個析構函數中,首先執行函數體,然后銷毀成員。成員按初始化順序的逆序銷毀。
在對象最后一次使用之后,析構函數的函數體可執行類設計者希望執行的任何收尾工作。通常,析構函數釋放對象在生存期分配的所有資源。
在一個析構函數中,不存在類似構造函數中初始化列表的東西來控制成員如何銷毀,析構部分是隱式的。成員銷毀時發生什么完全依賴于成員類型。銷毀類類型的成員需要執行成員自己的析構函數。內置類型沒有析構函數,因此銷毀內置類型成員什么也不需要做。
隱式的銷毀一個內置指針類型的成員不會delete它所指向的對象。
什么時候會調用析構函數
無論何時一個對象被銷毀,就會自動調用其析構函數:
變量在離開其作用域時被銷毀。
當一個對象被銷毀時,其成員被銷毀。
容器(無論是標準庫容器還是數組)被銷毀時,其元素被銷毀。
對于動態分配的對象,當對指向它的指針應用delete運算符時被銷毀。
對于臨時對象,當創建它的完整表達式結束時被銷毀。
由于析構函數自動運行,我們的程序可以按需要分配資源,而(通常)無須擔心何時釋放這些資源。
當指向一個對象的引用或指針離開作用域時,析構函數不會執行。
合成析構函數
下面的代碼片段等價于Sales_data的合成析構函數:
在(空)析構函數體執行完畢后,成員會被自動銷毀。認識到析構函數體自身并不直接銷毀成員是非常重要的。
(在學習C/C++或者想要學習C/C++可以加我們的學習交流QQ群:712263501群內有相關學習資料)
總結
以上是生活随笔為你收集整理的C++ 三五法则,看看你能不能理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《听歌六绝句·离别难》第四句是什么
- 下一篇: 求一个qq男生网名唯美!