最佳化 C++/編寫高效程式碼/構造和析構
物件的構造和析構需要時間,尤其是當物件擁有其他物件時。
本節將提供有關減少物件構造次數及其對應析構次數的指南。
儘可能晚地宣告變數。
為此,程式設計師必須在最區域性的範圍內宣告所有變數。透過這樣做,如果該範圍從未到達,則不會構造或析構變數。在範圍內儘可能延遲宣告意味著如果在宣告之前存在提前退出(使用return或break或continue語句),則與變數關聯的物件不會被構造或析構。
通常情況下,在例程開始時,沒有可用於初始化變數的適當值。因此,變數使用預設值進行初始化,並在以後的賦值中設定正確的值,當它變得可用時。相反,如果變數僅在適當的值可用時定義,則物件使用該值進行初始化,並且不需要隨後的賦值。本節中的“初始化”指南建議這樣做。
使用初始化而不是賦值。特別是,在建構函式中,使用初始化列表。
例如,不要寫
string s;
...
s = "abc"
寫
string s("abc");
即使類例項(在上面的第一個示例中為 s)沒有明確初始化,它仍然會透過預設建構函式自動初始化。
呼叫預設建構函式後跟賦值操作可能不如僅呼叫具有相同值的建構函式效率高。
如果表示式值未使用,則使用字首增量(++)或減量(--)運算子,而不是相應的字尾運算子。
如果遞增的物件是基本型別,則字首運算子和字尾運算子之間沒有區別。但是,如果它是複合物件,則字尾運算子會導致建立臨時物件,而字首運算子不會。
因為每個基本型別的物件都可能在將來成為複合物件,所以最好儘可能使用字首運算子,尤其是在編寫對迭代器進行操作的泛型(模板化)程式碼時。
僅當變數位於更大的表示式中並且必須在表示式計算完成後才遞增時才使用字尾運算子。
使用賦值複合運算子(如a += b)而不是簡單運算子與賦值運算子的組合(如a = a + b)。
例如,不要寫以下程式碼
string s1("abc");
string s2 = s1 + " " + s1;
寫以下程式碼
string s1("abc");
string s2 = s1;
s2 += " ";
s2 += s1;
通常,簡單運算子會建立一個臨時物件。在示例中,運算子+建立臨時字串,它們的建立和析構需要很多時間。
相反,使用+=運算子的等效程式碼不會建立臨時物件。
當您將型別為T的物件x作為引數傳遞給函式時,請使用以下準則
- 如果
x是僅輸入引數,- 如果
x可能是空值,- 透過指向常量的指標傳遞它 (
const T* x),
- 透過指向常量的指標傳遞它 (
- 否則,如果
T是基本型別或迭代器或函式物件,- 透過值傳遞它 (
T x) 或透過常量值傳遞它 (const T x),
- 透過值傳遞它 (
- 否則,
- 透過指向常量的引用傳遞它 (
const T& x),
- 透過指向常量的引用傳遞它 (
- 如果
- 否則,即如果
x是僅輸出引數或輸入/輸出引數,- 如果
x可能是空值,- 透過指向非常量的指標傳遞它 (
T* x),
- 透過指向非常量的指標傳遞它 (
- 否則,
- 透過指向非常量的引用傳遞它 (
T& x)。
- 透過指向非常量的引用傳遞它 (
- 如果
透過引用傳遞比透過指標傳遞更有效率,因為它有助於編譯器消除變數,並且因為被呼叫者不需要檢查引用是否有效或為空。但是,如果引數可能缺失,那麼傳遞空指標比傳遞可能為虛擬物件的引用和一個布林值來指示引用是否有效更有效率。
對於可能包含在一個或兩個暫存器中的物件,透過值傳遞比透過引用傳遞更有效率(或效率相同)。這適用於微小的物件,如基本型別、迭代器和函式物件。對於更大的物件,透過引用傳遞比透過值傳遞更有效率,因為使用後者,物件必須複製到堆疊中。
當前複製速度快的複合物件可以透過值傳遞,從而提高效率。但是,除非物件是迭代器或函式物件(假設始終能夠有效複製),否則這種技術存在風險。物件將來的更改可能會增加其大小並使複製變得更昂貴。例如,如果Point類的物件只包含兩個float,那麼它可以透過值傳遞來高效地傳遞;但如果將來添加了第三個float,或者如果兩個float變成了兩個double,那麼透過引用傳遞可能會更高效。
將接收單個引數的所有建構函式宣告為explicit,除了具體類的複製建構函式。
當編譯器執行自動(隱式)型別轉換時,非explicit建構函式可能會被自動呼叫。執行此類建構函式可能需要很長時間。
如果此類轉換強制顯式,並且程式碼中未指定新的類名,則編譯器可以選擇另一個過載函式,避免呼叫代價高昂的建構函式,或者它可能會生成錯誤,從而迫使程式設計師選擇另一種方法來避免建構函式呼叫。
對於具體類的複製建構函式,必須做出區分以允許它們透過值傳遞。對於抽象類,即使複製建構函式也可以宣告為explicit,因為根據定義,抽象類不能被例項化,因此不應該透過值傳遞此類型別的物件。
僅為了與舊庫保持相容而宣告轉換運算子(在 C++11 中,將其宣告為explicit)。
轉換運算子允許隱式轉換,因此會遇到本節中“explicit 宣告”指南中描述的與隱式建構函式相同的問題。
如果需要此類轉換,請提供等效的成員函式,因為它只能被顯式呼叫。
轉換運算子唯一可接受的剩餘用法是在新庫必須與舊的類似庫共存時。在這種情況下,使用運算子自動將舊庫中的物件轉換為新庫中相應的型別,反之亦然可能會很方便。
僅當您希望使程式的其餘部分獨立於類的實現時,才使用 Pimpl 慣用法。
Pimpl 慣用法(表示指向 implementation 的指標)包含僅在物件中儲存指向資料結構的指標,該資料結構包含有關物件的所有有用資訊。
該慣用法的主要優點是透過降低少量原始碼更改導致需要重新編譯大量程式碼行的可能性,從而加快了程式碼的增量編譯。
此慣用法還會使某些操作更有效,例如兩個物件的 swap。但是,通常情況下,由於增加了間接級別,它會減慢對物件資料的每次訪問,並且在每次建立或複製此類物件時都會導致額外的記憶體分配。因此,不應將其用於公共成員函式被頻繁呼叫的類。
迭代器和函式物件
[edit | edit source]確保自定義迭代器和函式物件很小,並且不分配動態記憶體。
STL 演算法按值傳遞此類物件。因此,如果它們的副本效率不高,則 STL 演算法會變慢。
如果迭代器或函式物件出於某種原因需要一個詳細的內部狀態,則動態分配它並使用共享指標。例如,假設您想在 Linux/OpenBSD /dev/urandom 裝置之上實現一個符合 STL 的 32 位 隨機數生成器
#include <boost/shared_ptr.hpp>
#include <fstream>
class urandom32 {
boost::shared_ptr<std::ifstream> device;
public:
urandom32() : device(new std::ifstream("/dev/urandom")) { }
uint32_t operator()()
{
uint32_t r;
device->read(reinterpret_cast<char *>(&r), sizeof(uint32_t));
return r;
}
};
在這種情況下,使用指標實際上是必要的,因為 ifstream 類不可複製:它的複製建構函式被宣告為 private。此示例使用 boost::shared_ptr 智慧指標;為了獲得更高的速度,可以使用侵入式引用計數。