本文將介紹 C++11 標準的兩個新特性:defaulted 和 deleted 函式。對於 defaulted 函式,編譯器會為其自動生成預設的函式定義體,從而獲得更高的程式碼執行效率,也可免除程式設計師手動定義該函式的工作量。對於 deleted 函式, 編譯器會對其禁用,從而避免某些非法的函式呼叫或者型別轉換,從而提高程式碼的安全性。本文將通過程式碼示例詳細闡述 defaulted 和 deleted 函式的用法及益處。
Defaulted 函式
背景問題
C++ 的類有四類特殊成員函式,它們分別是:預設建構函式、解構函式、拷貝建構函式以及拷貝賦值運算子。這些類的特殊成員函式負責建立、初始化、銷燬,或者拷貝類的物件。如果程式設計師沒有顯式地為一個類定義某個特殊成員函式,而又需要用到該特殊成員函式時,則編譯器會隱式的為這個類生成一個預設的特殊成員函式。例如:
清單 1
1 2 3 4 5 6 |
class X{ private: int a; }; X x; |
在清單 1 中,程式設計師並沒有定義類 X
的預設建構函式,但是在建立類 X
的物件 x
的時候,又需要用到類 X
的預設建構函式,此時,編譯器會隱式的為類 X
生成一個預設建構函式。該自動生成的預設建構函式沒有引數,包含一個空的函式體,即 X::X(){ }
。雖然自動生成的預設建構函式僅有一個空函式體,但是它仍可用來成功建立類 X
的物件 x
,清單 1 也可以編譯通過。
但是,如果程式設計師為類 X 顯式的自定義了非預設建構函式,卻沒有定義預設建構函式的時候,清單 2 將會出現編譯錯誤:
清單 2
1 2 3 4 5 6 7 8 9 10 |
class X{ public: X(int i){ a = i; } private: int a; }; X x; // 錯誤 , 預設建構函式 X::X() 不存在 |
清單 2 編譯出錯的原因在於類 X
已經有了使用者自定義的建構函式,所以編譯器將不再會為它隱式的生成預設建構函式。如果需要用到預設建構函式來建立類的物件時,程式設計師必須自己顯式的定義預設建構函式。例如:
清單 3
1 2 3 4 5 6 7 8 9 10 11 |
class X{ public: X(){}; // 手動定義預設建構函式 X(int i){ a = i; } private: int a; }; X x; // 正確,預設建構函式 X::X() 存在 |
從清單 3 可以看出,原本期望編譯器自動生成的預設建構函式需要程式設計師手動編寫了,即程式設計師的工作量加大了。此外,手動編寫的預設建構函式的程式碼執行效率比編譯器自動生成的預設建構函式低。類的其它幾類特殊成員函式也和預設建構函式一樣,當存在使用者自定義的特殊成員函式時,編譯器將不會隱式的自動生成預設特殊成員函式,而需要程式設計師手動編寫,加大了程式設計師的工作量。類似的,手動編寫的特殊成員函式的程式碼執行效率比編譯器自動生成的特殊成員函式低。
Defaulted 函式的提出
為了解決如清單 3 所示的兩個問題:1. 減輕程式設計師的程式設計工作量;2. 獲得編譯器自動生成的預設特殊成員函式的高的程式碼執行效率,C++11 標準引入了一個新特性:defaulted 函式。程式設計師只需在函式宣告後加上“=default;
”,就可將該函式宣告為 defaulted 函式,編譯器將為顯式宣告的 defaulted 函式自動生成函式體。例如:
清單 4
1 2 3 4 5 6 7 8 9 10 11 |
class X{ public: X()= default; X(int i){ a = i; } private: int a; }; X x; |
在清單 4 中,編譯器會自動生成預設建構函式 X::X(){}
,該函式可以比使用者自己定義的預設建構函式獲得更高的程式碼效率。
Defaulted 函式定義語法
Defaulted 函式是 C++11 標準引入的函式定義新語法,defaulted 函式定義的語法如圖 1 所示:
圖 1. Defaulted 函式定義語法圖
Defaulted 函式的用法及示例
Defaulted 函式特性僅適用於類的特殊成員函式,且該特殊成員函式沒有預設引數。例如:
清單 5
1 2 3 4 5 6 |
class X { public: int f() = default; // 錯誤 , 函式 f() 非類 X 的特殊成員函式 X(int) = default; // 錯誤 , 建構函式 X(int, int) 非 X 的特殊成員函式 X(int = 1) = default; // 錯誤 , 預設建構函式 X(int=1) 含有預設引數 }; |
Defaulted 函式既可以在類體裡(inline)定義,也可以在類體外(out-of-line)定義。例如:
清單 6
1 2 3 4 5 6 7 8 9 10 11 |
class X{ public: X() = default; //Inline defaulted 預設建構函式 X(const X&); X& operator = (const X&); ~X() = default; //Inline defaulted 解構函式 }; X::X(const X&) = default; //Out-of-line defaulted 拷貝建構函式 X& X::operator = (const X&) = default; //Out-of-line defaulted // 拷貝賦值操作符 |
在 C++ 程式碼編譯過程中,如果程式設計師沒有為類 X
定義解構函式,但是在銷燬類 X
物件的時候又需要呼叫類 X
的解構函式時,編譯器會自動隱式的為該類生成一個解構函式。該自動生成的解構函式沒有引數,包含一個空的函式體,即 X::~X(){ }
。例如:
清單 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class X { private: int x; }; class Y: public X { private: int y; }; int main(){ X* x = new Y; delete x; } |
在清單 7 中,程式設計師沒有為基類 X 和派生類 Y 定義解構函式,當在主函式內 delete 基類指標 x 的時候,需要呼叫基類的解構函式。於是,編譯器會隱式自動的為類 X 生成一個解構函式,從而可以成功的銷燬 x 指向的派生類物件中的基類子物件(即 int 型成員變數 x)。
但是,這段程式碼存在記憶體洩露的問題,當利用 delete
語句刪除指向派生類物件的指標 x
時,系統呼叫的是基類的解構函式,而非派生類 Y
類的解構函式,因此,編譯器無法析構派生類的 int
型成員變數 y。
因此,一般情況下我們需要將基類的解構函式定義為虛擬函式,當利用 delete 語句刪除指向派生類物件的基類指標時,系統會呼叫相應的派生類的解構函式(實現多型性),從而避免記憶體洩露。但是編譯器隱式自動生成的解構函式都是非虛擬函式,這就需要由程式設計師手動的為基類 X
定義虛解構函式,例如:
清單 8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class X { public: virtual ~X(){}; // 手動定義虛解構函式 private: int x; }; class Y: public X { private: int y; }; int main(){ X* x = new Y; delete x; } |
在清單 8 中,由於程式設計師手動為基類 X
定義了虛解構函式,當利用 delete
語句刪除指向派生類物件的基類指標 x
時,系統會呼叫相應的派生類 Y
的解構函式(由編譯器隱式自動生成)以及基類 X
的解構函式,從而將派生類物件完整的銷燬,可以避免記憶體洩露。
但是,在清單 8 中,程式設計師需要手動的編寫基類的虛構函式的定義(哪怕函式體是空的),增加了程式設計師的程式設計工作量。更值得一提的是,手動定義的解構函式的程式碼執行效率要低於編譯器自動生成的解構函式。
為了解決上述問題,我們可以將基類的虛解構函式宣告為 defaulted 函式,這樣就可以顯式的指定編譯器為該函式自動生成函式體。例如:
清單 9
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class X { public: virtual ~X()= defaulted; // 編譯器自動生成 defaulted 函式定義體 private: int x; }; class Y: public X { private: int y; }; int main(){ X* x = new Y; delete x; } |
在清單 9 中,編譯器會自動生成虛解構函式 virtual X::X(){}
,該函式比使用者自己定義的虛解構函式具有更高的程式碼執行效率。
Deleted 函式
背景問題
對於 C++ 的類,如果程式設計師沒有為其定義特殊成員函式,那麼在需要用到某個特殊成員函式的時候,編譯器會隱式的自動生成一個預設的特殊成員函式,比如拷貝建構函式,或者拷貝賦值操作符。例如:
清單 10
1 2 3 4 5 6 7 8 9 10 11 |
class X{ public: X(); }; int main(){ X x1; X x2=x1; // 正確,呼叫編譯器隱式生成的預設拷貝建構函式 X x3; x3=x1; // 正確,呼叫編譯器隱式生成的預設拷貝賦值操作符 } |
在清單 10 中,程式設計師不需要自己手動編寫拷貝建構函式以及拷貝賦值操作符,依靠編譯器自動生成的預設拷貝建構函式以及拷貝賦值操作符就可以實現類物件的拷貝和賦值。這在某些情況下是非常方便省事的,但是在某些情況下,假設我們不允許發生類物件之間的拷貝和賦值,可是又無法阻止編譯器隱式自動生成預設的拷貝建構函式以及拷貝賦值操作符,那這就成為一個問題了。
Deleted 函式的提出
為了能夠讓程式設計師顯式的禁用某個函式,C++11 標準引入了一個新特性:deleted 函式。程式設計師只需在函式宣告後加上“=delete;
”,就可將該函式禁用。例如,我們可以將類 X
的拷貝建構函式以及拷貝賦值操作符宣告為 deleted 函式,就可以禁止類 X
物件之間的拷貝和賦值。
清單 11
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class X{ public: X(); X(const X&) = delete; // 宣告拷貝建構函式為 deleted 函式 X& operator = (const X &) = delete; // 宣告拷貝賦值操作符為 deleted 函式 }; int main(){ X x1; X x2=x1; // 錯誤,拷貝建構函式被禁用 X x3; x3=x1; // 錯誤,拷貝賦值操作符被禁用 } |
在清單 11 中,雖然只顯式的禁用了一個拷貝建構函式和一個拷貝賦值操作符,但是由於編譯器檢測到類 X
存在使用者自定義的拷貝建構函式和拷貝賦值操作符的宣告,所以不會再隱式的生成其它引數型別的拷貝建構函式或拷貝賦值操作符,也就相當於類 X
沒有任何拷貝建構函式和拷貝賦值操作符,所以物件間的拷貝和賦值被完全禁止了。
Deleted 函式定義語法
Deleted 函式是 C++11 標準引入的函式定義新語法,deleted 函式定義的語法如圖 2 所示:
圖 2. Deleted 函式定義語法圖
Deleted 函式的用法及示例
Deleted 函式特性還可用於禁用類的某些轉換建構函式,從而避免不期望的型別轉換。在清單 12 中,假設類 X
只支援引數為雙精度浮點數 double 型別的轉換建構函式,而不支援引數為整數 int 型別的轉換建構函式,則可以將引數為 int 型別的轉換建構函式宣告為 deleted 函式。
清單 12
1 2 3 4 5 6 7 8 9 10 |
class X{ public: X(double); X(int) = delete; }; int main(){ X x1(1.2); X x2(2); // 錯誤,引數為整數 int 型別的轉換建構函式被禁用 } |
Deleted 函式特性還可以用來禁用某些使用者自定義的類的 new
操作符,從而避免在自由儲存區建立類的物件。例如:
清單 13
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include using namespace std; class X{ public: void *operator new(size_t) = delete; void *operator new[](size_t) = delete; }; int main(){ X *pa = new X; // 錯誤,new 操作符被禁用 X *pb = new X[10]; // 錯誤,new[] 操作符被禁用 } |
必須在函式第一次宣告的時候將其宣告為 deleted 函式,否則編譯器會報錯。即對於類的成員函式而言,deleted 函式必須在類體裡(inline)定義,而不能在類體外(out-of-line)定義。例如:
清單 14
1 2 3 4 5 6 |
class X { public: X(const X&); }; X::X(const X&) = delete; // 錯誤,deleted 函式必須在函式第一次宣告處宣告 |
雖然 defaulted 函式特性規定了只有類的特殊成員函式才能被宣告為 defaulted 函式,但是 deleted 函式特性並沒有此限制。非類的成員函式,即普通函式也可以被宣告為 deleted 函式。例如:
清單 15
1 2 3 4 5 6 |
int add (int,int)=delete; int main(){ int a, b; add(a,b); // 錯誤,函式 add(int, int) 被禁用 } |
值得一提的是,在清單 15 中,雖然 add(int, int)
函式被禁用了,但是禁用的僅是函式的定義,即該函式不能被呼叫。但是函式標示符 add
仍是有效的,在名字查詢和函式過載解析時仍會查詢到該函式標示符。如果編譯器在解析過載函式時,解析結果為 deleted 函式,則會出現編譯錯誤。例如:
清單 16
1 2 3 4 5 6 7 8 9 10 11 |
#include <iostream> using namespace std; int add(int,int) = delete; double add(double a,double b){ return a+b; } int main(){ cout << add(1,3) << endl; // 錯誤,呼叫了 deleted 函式 add(int, int) cout << add(1.2,1.3) << endl; return 0; } |
結束語
本文詳細介紹了 C++11 新特性 defaulted 和 deleted 函式。該特性巧妙地對 C++ 已有的關鍵字 default 和 delete 的語法進行了擴充,引入了兩種新的函式定義方式:在函式宣告後加 =default 和 =delete。通過將類的特殊成員函式宣告為 defaulted 函式,可以顯式指定編譯器為該函式自動生成預設函式體。通過將函式宣告為 deleted 函式,可以禁用某些不期望的轉換或者操作符。Defaulted 和 deleted 函式特性語法簡單,功能實用,是對 C++ 標準的一個非常有價值的擴充。