簡介:
本文將介紹 C++11 標準的兩個新特性:defaulted 和 deleted 函數。對於 defaulted 函數,編譯器會爲其自動生成默認的函數定義體,從而獲得更高的代碼執行效率,也可免除程序員手動定義該函數的工作量。對於 deleted 函數, 編譯器會對其禁用,從而避免某些非法的函數調用或者類型轉換,從而提高代碼的安全性。本文將通過代碼示例詳細闡述 defaulted 和 deleted 函數的用法及益處。
Defaulted 函數
背景問題
C++ 的類有四類特殊成員函數,它們分別是:默認構造函數、析構函數、拷貝構造函數以及拷貝賦值運算符。這些類的特殊成員函數負責創建、初始化、銷燬,或者拷貝 類的對象。如果程序員沒有顯式地爲一個類定義某個特殊成員函數,而又需要用到該特殊成員函數時,則編譯器會隱式的爲這個類生成一個默認的特殊成員函數。例 如:
清單 1class X{ private: int a; }; X x;
在清單 1 中,程序員並沒有定義類X的默認構造函數,但是在創建類X的對象x的時候,又需要用到類X的默認構造函數,此時,編譯器會隱式的爲類X生成一個默認構造函數。該自動生成的默認構造函數沒有參數,包含一個空的函數體,即X::X(){ }。雖然自動生成的默認構造函數僅有一個空函數體,但是它仍可用來成功創建類X的對象x,清單 1 也可以編譯通過。
但是,如果程序員爲類 X 顯式的自定義了非默認構造函數,卻沒有定義默認構造函數的時候,清單 2 將會出現編譯錯誤:
清單 2
class X{ public: X(int i){ a = i; } private: int a; }; X x; // 錯誤 , 默認構造函數 X::X() 不存在 |
清單 2 編譯出錯的原因在於類X已經有了用戶自定義的構造函數,所以編譯器將不再會爲它隱式的生成默認構造函數。如果需要用到默認構造函數來創建類的對象時,程序員必須自己顯式的定義默認構造函數。例如:
清單 3
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
class X{ public: X()= default; X(int i){ a = i; } private: int a; }; X x; |
在清單 4 中,編譯器會自動生成默認構造函數X::X(){},該函數可以比用戶自己定義的默認構造函數獲得更高的代碼效率。
Defaulted 函數定義語法
Defaulted 函數是 C++11 標準引入的函數定義新語法
Defaulted 函數的用法及示例
Defaulted 函數特性僅適用於類的特殊成員函數,且該特殊成員函數沒有默認參數。例如:
清單 5
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
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
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
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
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
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
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 函數的用法及示例
Deleted 函數特性還可用於禁用類的某些轉換構造函數,從而避免不期望的類型轉換。在清單 12 中,假設類X只支持參數爲雙精度浮點數 double 類型的轉換構造函數,而不支持參數爲整數 int 類型的轉換構造函數,則可以將參數爲 int 類型的轉換構造函數聲明爲 deleted 函數。
清單 12
class X{ public: X(double); X(int) = delete; }; int main(){ X x1(1.2); X x2(2); // 錯誤,參數爲整數 int 類型的轉換構造函數被禁用 } |
Deleted 函數特性還可以用來禁用某些用戶自定義的類的new操作符,從而避免在自由存儲區創建類的對象。例如:
清單 13
#include <cstddef> 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
class X { public: X(const X&); }; X::X(const X&) = delete; // 錯誤,deleted 函數必須在函數第一次聲明處聲明 |
雖然 defaulted 函數特性規定了只有類的特殊成員函數才能被聲明爲 defaulted 函數,但是 deleted 函數特性並沒有此限制。非類的成員函數,即普通函數也可以被聲明爲 deleted 函數。例如:
清單 15
int add (int,int)=delete; int main(){ int a, b; add(a,b); // 錯誤,函數 add(int, int) 被禁用 } |
值得一提的是,在清單 15 中,雖然add(int, int)函數被禁用了,但是禁用的僅是函數的定義,即該函數不能被調用。但是函數標示符add仍是有效的,在名字查找和函數重載解析時仍會查找到該函數標示符。如果編譯器在解析重載函數時,解析結果爲 deleted 函數,則會出現編譯錯誤。例如:
清單 16
#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++ 標準的一個非常有價值的擴充。