你經常要使用構造函數或者析構函數來定義某個類型的一個變量,當系統在運行至變量的定義時,就會引入一次構造的開銷;在變量達到自身作用域的邊界時,就會引入一次析構的開銷。未使用的變量也會帶來一定的開銷,所以你應該盡可能的避免這種浪費的出現。
你可能會想你永遠也不會定義變量而不去使用,但是你可能需要三思而后行。請觀察下邊的函數,它在所提供的密碼足夠長時,可以返回一個加密版本的密碼。如果密碼長度過短,函數就會拋出一個logic_error類型的異常(這個異常類型定義于標準C++庫中,參見條目54):
// 這個函數定義"encrypted"變量的時機過早
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
... // 對密碼加密
return encrypted;
}
本函數中,盡管對象encrypted并不是完全未使用的,但是在拋出異常的情況下,函數就不會使用它。也就是說,即使encryptPassword拋出一個異常,你也要為encrypted付出一次構造和一次析構的代價。因此,你最好推遲encrypted的定義,直到你確認你需要它時再進行:
// 這個函數推遲了encrypted的定義,直到真正需要它時再進行
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
string encrypted;
... // 對密碼加密
return encrypted;
}
上面的代碼還沒有那么嚴謹,這是因為在定義encrypted時沒有為它設置任何初始化參數。這就意味著編譯器將調用它的默認構造函數。通常情況下,你要對一個對象需要做的第一件事就是為它賦一個值,通常是通過一次賦值操作。條目4中解釋了為什么使用默認構造函數構造對象并為其賦值,要比使用需要的值對其進行初始化的效率低一些。那里的分析符合此處的情況。比如說,可以假設的較困難的部分是通過下面的函數來解決的:
void encrypt(std::string& s); // 適時為s加密
encryptPassword就應該以下面的方式來實現了,盡管它不是最優秀的:
// 這一函數推遲了enctypted定義的時機,直到需要時才進行。
// 但仍然會帶來不必要的效率問題。
std::string encryptPassword(const std::string& password)
{
... // 同上,檢查密碼長度
std::string encrypted; // encrypted的默認構造函數版本
encrypted = password; // 對encrypted賦值
encrypt(encrypted);
return encrypted;
}
更好的一種實現方式是,使用password來初始化encrypted,這樣就可以跳過默認構造過程所帶來的無謂的性能開銷:
// 最后給出定義和初始化encrypted的最佳方法
std::string encryptPassword(const std::string& password)
{
... // 檢查長度
std::string encrypted(password); // 通過拷貝構造函數定義和初始化
encrypt(encrypted);
return encrypted;
}
此時標題中的“越晚越好”的真正含義就十分明顯了。你不僅僅要推遲一個變量的定義時機,直到需要它時再進行;你還需要繼續推遲,直至你掌握了它的初始化參數為止。這樣做,你就可以避免去構造和析構不必要的對象,你也可以避免那些無關緊要的默認構造過程。還有,通過初始化這些變量,定義這些變量的目的一目了然,從而代碼也變得更加清晰。
“但是循環呢?”你可能會想。如果一個變量僅僅在循環題中使用,那么更好的選擇是:將它定義在循環題的外部,在每次循環迭代前對其進行賦值;還是:在循環體的內部定義變量?也就是說,哪種基本結構是更優秀的呢?
// 方法A:在循環體外部定義
Widget w;
for (int i = 0; i < n; ++i){
w = 取決于i的某個值;
...
}
// 方法B: 在循環體內部定義
for (int i = 0; i < n; ++i) {
Widget w(取決于i的某個值);
...
}
這里我使用了Widget類型的對象,而不是string類型的對象,從而避免了進行構造、析構、或者對象賦值等過程帶來的誤差。
對于Widget的操作而言,上面兩種方法所帶來的開銷如下:
l 方法A:1個構造函數 + 1個析構函數 + n次賦值。
l 方法B:n個構造函數 + n個析構函數。
對于那些一次賦值操作比一對構造-析構操作開銷更低的類而言,方法A是較高效的。尤其是在n較大的情況下。否則方法B就是更好的選擇。還有,方法A使得w位于一個比方法B更大的作用域中,這是違背程序的可讀性和可維護性原則的。因此,除非你確認: (1)賦值操作比一對構造-析構操作更高效,(2)當前代碼是對性能敏感的;其他任何情況下,你都應該使用方法B。
時刻牢記
l 定義變量的時機越晚越好。這可以提高程序的清晰度和工作效率。