C++ 類別定義表示、不變條件,以及建立和維持這些不變條件的操作。建構函式建立物件的不變條件,成員函式維持該不變條件,而解構函式釋放物件所擁有的資源。應先設計所有權策略,因為它決定編譯器產生的特殊成員是否正確。
建構與解構 #
建構函式具有類別名稱且沒有回傳型別。預設建構函式不接受參數,或其所有參數都有預設值。複製建構函式和移動建構函式分別從同一類別的左值或右值初始化新物件;複製指定和移動指定則更新現有物件。
#include <string>
#include <utility>
class Connection {
public:
explicit Connection(std::string endpoint) : endpoint_(std::move(endpoint)) {}
private:
std::string endpoint_;
};成員不會按照建構函式初始化器清單中的文字順序初始化。建構會依照以下順序進行:
- 建構最衍生物件時的虛擬基底。
- 按照基底指定子順序建構直接基底。
- 按照宣告順序建構非靜態資料成員。
- 建構函式本體。
因此,應按照宣告順序撰寫初始化器,並且不要使用後宣告的成員來初始化其他成員:
struct Valid {
int first;
int second;
Valid(int value) : first(value), second(first + 1) {}
};解構會逆序執行已完成的建構。應讓自動儲存期物件和RAII所有者執行正常解構;明確呼叫解構函式只適用於受控儲存管理,而且必須已確立物件生命週期以及之後對儲存空間的重複使用。
特殊成員函式與Rule of Zero #
特殊成員函式包括預設建構函式、複製建構函式、移動建構函式、複製指定運算子、移動指定運算子和解構函式。實作會隱含宣告類別尚未由使用者宣告的操作,並在需要時定義這些操作。函式可以被隱含宣告、明確預設或明確刪除;具有使用者撰寫定義的函式稱為使用者提供函式。
以下抑制規則需要刻意檢查:
| 操作 | 重要規則 |
|---|---|
| 預設建構函式 | 使用者宣告的建構函式或建構函式範本會抑制它。 |
| 移動建構函式和移動指定 | 使用者宣告的複製建構函式、複製指定、移動操作或解構函式會抑制兩個隱含移動操作。 |
| 複製建構函式和複製指定 | 未由使用者宣告時會被隱含宣告,但基底或成員無法執行所需操作時,可能會被定義為已刪除。 |
參照成員和不適用的const成員通常會導致隱含指定被刪除。宣告任一移動操作會使兩個隱含宣告的複製操作都被定義為已刪除。基底或成員中不可存取、已刪除或不可使用的操作,可能會刪除其他隱含特殊成員。當基底和成員符合標準的結構要求時,編譯器產生的函式也可能是trivial的;trivial性是型別屬性,而不是所有權策略。
優先採用Rule of Zero:組合具有正確所有權行為的型別,並且不要在領域型別中宣告特殊成員。
#include <string>
#include <utility>
#include <vector>
class Document {
public:
explicit Document(std::string title) : title_(std::move(title)) {}
private:
std::string title_;
std::vector<std::string> paragraphs_;
};自訂資源所有者需要明確且一致的策略。只能移動的所有者會刪除複製操作,只釋放一次資源;當轉移不會失敗時,應將移動操作標記為noexcept。定義傳統「Rule of Three」中的任一操作,或定義任一移動操作,都應觸發對所有複製、移動和解構行為的檢查,而不是自動實作五個函式。
繼承與虛擬分派 #
public繼承表達可替代性:對Base正確的程式碼,對Derived也必須繼續正確。對於不屬於「is-a」關係的實作重用,應使用組合。private繼承和protected繼承會改變繼承成員的存取權限,但不會公開public可替代性;基底的private成員仍無法由衍生類別透過名稱存取。
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle final : Shape {
double area() const override;
};沒有最終覆寫函式的純虛擬函式會使類別成為抽象類別。純虛擬解構函式仍需要定義,因為每個衍生解構函式都會呼叫它。虛擬分派會為物件的動態型別選擇最終覆寫函式,但建構和解構期間除外;在這兩個階段,呼叫只會在建構函式或解構函式所屬的類別階層中進行分派。避免在基底類別的建構函式或解構函式中進行依賴衍生類別狀態的虛擬呼叫。
對所有預期的覆寫使用override,因為它可以拒絕簽章不相符的情況。可能透過基底指標刪除的多型基底需要public虛擬解構函式;否則應禁止這種刪除路徑。
運算子與字面值後綴 #
只有在保留運算子既有含義時才重載運算子。operator=和operator->必須是非靜態成員函式。在C++23之前,operator[]和operator()也必須是非靜態成員函式;C++23允許這兩個運算子使用靜態重載。前置遞增慣例上回傳T&,後置遞增接受未使用的int區分參數,並慣例上回傳先前的T值。
class Flag {
public:
explicit operator bool() const noexcept;
Flag& operator++(); // prefix
Flag operator++(int); // postfix
};除非隱式轉換是介面刻意提供的一部分,否則應將轉換運算子標記為explicit。運算子不能改變優先順序、元數或短路行為,也不能引入新的運算子標記。
使用者定義字面值運算子在命名空間範圍宣告;成員函式不能定義字面值後綴。引數相依查找不會找到這些運算子。後綴必須以下底線開頭:不含下底線的後綴保留給標準函式庫,而以下底線後接大寫字母的識別字,以及以下底線開頭後再接另一個下底線的識別字,均屬於保留識別字。
#include <cstddef>
struct Bytes {
std::size_t value;
};
constexpr Bytes operator"" _bytes(unsigned long long value) {
return {static_cast<std::size_t>(value)};
}
constexpr Bytes cache = 64_bytes;經處理的整數和浮點字面值運算子分別接受unsigned long long和long double。字串字面值運算子接受相應的字元指標和std::size_t長度;字元字面值形式使用適用的字元型別。只能選擇字面值類別允許的參數形式,並明確說明後綴的單位和轉換規則。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)