C++ 類的默認函數

2019-07-02     linux內核
來源:KeepHopeswww.cnblogs.com/yuwanxian/p/10924835.html


在C++中,一個類有八個默認函數:

1、默認構造函數;

2、默認拷貝構造函數;

3、默認析構函數;

4、默認重載賦值運算符函數;

5、默認重載取址運算符函數;

6、默認重載取址運算符const函數;

7、默認移動構造函數(C++11);

8、默認重載移動賦值操作符函數(C++11)。

只是聲明一個空類,不做任何事情的話,編譯器會自動為你生成一個默認構造函數、一個默認拷貝構造函數、一個默認重載賦值操作符函數和一個默認析構函數。這些函數只有在第一次被調用時,才會被編譯器創建,當然這幾個生成的默認函數的實現就是什麼都不做。所有這些函數都是inline和public的。

我們不希望對象被顯示構造(單列模式)或賦值,可以將對應函數聲明為private,或者寫一個基類,開放部分默認函數,子類去繼承就可以了。C++11新增標識符default和delete,控制這些默認函數是否使用。

default:被標識的默認函數將使用類的默認行為,如:A() = default;

delete:被標識的默認函數將禁用,如:A() = delete;

override:被標識的函數需要強制重寫基類虛函數;

final:被標識的函數禁止重寫基類虛函數;

class A
{
public:
// 默認構造函數;
A();
// 默認拷貝構造函數
A(const A&);
// 默認析構函數
~A();
// 默認重載賦值運算符函數
A& operator = (const A&);
// 默認重載取址運算符函數
A* operator & ();
// 默認重載取址運算符const函數
const A* operator & () const;
// 默認移動構造函數
A(A&&);
// 默認重載移動賦值操作符
A& operator = (const A&&);
};

下面就每個函數分別詳細討論學習,有不對之處,歡迎評論指正,您的批評指正就是在下前進的不竭動力!

一、構造函數(Constructor)

1、構造函數作用是對對象進行初始化,在堆上new一個對象或在棧上定義一個臨時對象時,會自動調用對象的構造函數。有初始化列表和構造函數體內賦值兩種方式,初始化列表在初始化對象時更高效(每個成員在初始化列表中只能出現一次),減少了一次賦值操作,推薦此方法;以下成員變量必須在初始化列表中初始化:常量成員變量、引用類型成員變量、沒有預設構造函數的成員變量(如果構造函數的參數列表中有一個類的對象,並且該對象的類里沒有預設參數的構造函數時,要是不使用初始化列表,參數中會調用無參或者全預設的構造函數,而那個類中又沒有);

2、函數名與類名相同,可以重載,不能為虛函數,不能有返回值,連void也不行;

3、如果沒有顯式定義,編譯器會自動生成一個默認的構造函數,默認的構造函什麼都不會做;

4、無參構造函數和帶有預設值的構造函數(全預設)都認為是預設的構造函數,並且預設的構造函數只能有一個;

5、函數體內可以使用this指針,但不可以用於初始化列表。因為構造函數只是初始化對象,初始化之前此對象已經存在了,所以可以有this,函數體裡面是進行賦值,初始化列表是對類中的各個成員變量進行初始化,初始化的位置對象不完整,所以不能使用this用於初始化列表;

6、對於出現單參數的構造函數需要注意,C++會默認將參數對應的類型轉換為該類類型,有時候這種隱式的轉換是我們不想要的,需要使用explicit關鍵字來限制這種轉換;

7、構造順序:虛擬基類的構造函數(如果有多個虛擬基類,按照它們被繼承的順序構造,而不是它們在成員初始化列表中的順序);

非虛擬基類的構造函函(如果有多個非虛擬基類,按照它們被繼承的順序構造,而不是它們在成員初始化列表中的順序);

成員對象的構造函數(如果有多個成員類對象,按照它們聲明的順序調用,而不是它們在成員初始化列表中的順序);

本類構造函數。構造的過程是遞歸的。

二、拷貝構造函數(Copy Constructor)

1、拷貝構造函數實際上是構造函數的重載,具有一般構造函數的所有特性,用此類已有的對象創建一個新的對象,一般在函數中會將已存在對象的數據成員的值複製一份到新創建的對象中。用類的一個已知的對象去初始化該類的另一個對象時,會自動調用對象的拷貝構造函數;

2、函數名與類名相同,第一個參數是對某個同類對象的引用,且沒有其他參數或其他參數都有默認值,返回值是類對象的引用,通過返回引用值可以實現連續構造,即類似A(B(C))這樣;

3、如果沒有顯式定義,編譯器會自動生成一個默認的拷貝構造函數,默認的拷貝構造函數會依次拷貝類的數據成員完成初始化;

4、淺拷貝和深拷貝:編譯器創建的默認拷貝構造函數只會執行"淺拷貝",也就是通過賦值完成,如果該類的數據成員中有指針成員,也只是地址的拷貝,會使得新的對象與拷貝對象該指針成員指向的地址相同,delete該指針時則會導致兩次重複delete而出錯,如果指針成員是new出來就是「深拷貝」。

三、析構函數(Destructor)

1、析構函數作用是做一些清理工作,delete一個對象或對象生命周期結束時,會自動調用對象的析構函數;

2、函數名在類名前加上字符~,沒有參數(可以有void類型的參數),也沒有返回值,可以為虛函數(通過基類的指針去析構子類對象時候),不能重載,故析構函數只有一個;

3、如果沒有顯式定義,編譯器會自動生成一個默認的析構函數,默認的析構函什麼都不會做;

4、析構順序:和構造函數順序相反。析構的過程也是遞歸的。

四、重載賦值運算符函數(Copy Assignment operator)

1、它是兩個已有對象,一個給另一個賦值的過程。當兩個對象之間進行賦值時,會自動調用重載賦值運算符函數,它不同於拷貝構造函數,拷貝構造函數是用已有對象給新生成的對象賦初值的過程;

2、賦值運算符重載函數參數中const和&沒有強制要求,返回值是類對象的引用,通過返回引用值可以實現連續賦值,即類似a=b=c這樣,返回值類型也不是強制的,可以返回void,使用時就不能連續賦值;

3、賦值運算符重載函只能定義為類的成員函數,不能是靜態成員函數,也不能是友元函數,賦值運算符重載函數不能被繼承,要避免自賦值;

4、如果沒有顯式定義,編譯器會自動生成一個默認的賦值運算符重載函數,默認的賦值運算符重載函數實現將數據成員逐一賦值的一種淺拷貝,會導致指針懸掛問題。

五、重載取址運算符(const)函數

1、重載取址運算符函數沒有參數;

2、如果沒有顯式定義,編譯器會自動生成默認的重載取址運算符函數,函數內部直接return this,一般使用默認即可。

六、移動構造函數和重載移動賦值操作符函數

1、C++11 新增move語義:源對象資源的控制權全部交給目標對象,可以將原對象移動到新對象, 用於a初始化b後,就將a析構的情況;

2、移動構造函數的參數和拷貝構造函數不同,拷貝構造函數的參數是一個左值引用,但是移動構造函數的初值是一個右值引用;

3、臨時對象即將消亡,並且它裡面的資源是需要被再利用的,這個時候就可以使用移動構造。移動構造可以減少不必要的複製,帶來性能上的提升。

七、討論

1、構造函數為什麼不能有返回值?

(1).C++語言規定構造函數沒有返回值;

(2).構造函數不作為右值使用,返回值也沒有用;

(3).就算有返回值,從基本語義角度來講,也應該返回的是所構造的對象,所以沒必要多此一舉來指定返回類型了;

(4).假如有返回值,討論下面代碼

class A
{
public:
A():m_iTest(0) { }
A(int i):m_iTest(i) { }
private:
int m_iTest;
};

按照C++的規定,A a = A();是用默認構造函數創建一個臨時對象,並用這個臨時對象初始化a,此時,a.m_iTest的值應該是0。現在如果A::A()有返回值,並且返回了1(表示構造成功),則C++會用1去初始化a,即調用有參數構造函數A::A(int i),得到的a.m_iTest便會是1。於是,語義產生了歧義,使得C++原本已經非常複雜的語法,進一步混亂不堪。

構造函數的調用之所以不設返回值,是因為構造函數的特殊性決定的。當然,上面的討論,也是基於C++語言規定,如果規定構造函數可以有返回值,上面用法也許就不一樣了。是先有雞還是先有蛋,這是一個神奇的問題。總之,現在C++語法體系是這樣的,如果設計構造函數可以有返回值,可能整個C++語言更難實現了。

2、對象創建和銷毀過程是怎樣的?

對象創建(new)過程:

(1).通過operator new申請內存;

(2).使用placement new調用構造函數(簡單類型忽略此步);

(3).返回內存指針。

new和malloc的比較:

(1).new失敗時會調用new_handler處理函數,malloc不會,失敗時返回NULL;

(2).new能通過placement new自動調用對象的構造函數,malloc不會;

(3).new出來的東西是帶類型的,malloc是void*,需要強制轉換;

(4).new是C++運算符,malloc是C標準庫函數。

new的三種形態:new operator,operator new,placement new

(1).new operator:上面所說的new就是new operator,共有三個步驟組成(申請內存,調用構造函數,返回內存指針),對於申請內存步驟是通過運算符new(operator new)完成的,對於調用什麼構造函數,可以由placement new決定;

(2).operator new:像普通運算符一樣可以被重載,operator new會去申請內存,申請失敗的時候會調用new_handler處理,這是一個循環的過程,如果new_handler不拋出異常,會一直循環申請內存,直到成功;

(3).placement new:用於定位構造函數,在指定的內存地址上用指定類型的構造函數構造對象。

對象銷毀(delete)過程:

(1).調用析構函數(簡單類型忽略此步);

(2).釋放內存。

delete和free比較

(1).delete能自動調用對象的析構函數,free不會;

(2).delete是C++運算符,free是C標準庫函數。

3、拷貝構造函數參數為什麼必須使用類類型對象引用傳遞?

傳參的位置如果一直調用拷貝構造函數,也就是會遞歸引用,導致棧溢出。

4、賦值運算符重載函數為什麼要避免自賦值?

(1).提高效率。自賦值無意義,如果自賦值,可以立即return *this;

(2).如果不避免,當類的數據成員中如果含有指針,自賦值時會造成內存泄漏。

文章來源: https://twgreatdaily.com/zh-cn/LOBjQWwB8g2yegNDUsbK.html