我在閱讀 《Effective C++ (第三版本)》 書時作了很多筆記,從中收穫了很是多,也明白爲何會書中前言的第一句話會說:程序員
對於書中的「條款」這一詞,我更喜歡以「細節」替換,畢竟年輕的咱們在打 LOL 或 王者的時,總會說注意細節!細節!細節~ —— 細節也算伴隨咱們的青春的字眼編程
針對書中的前兩個章節,我篩選了 10 個 細節(條款)做爲了本文的內容,這些細節也相對基礎且重要。數組
針對這 10 細節我都用較簡潔的例子來加以闡述,同時也把本文所說起細節中的「小結」總結繪畫成了一副思惟導圖,便於你們的閱讀。安全
後續有時間也會繼續分享後面章節的筆記,喜歡的小夥伴「點擊左上角」關注我~服務器
舒適提示:本文較長,建議收藏閱讀。函數
#define 定義的常量有什麼不妥?性能
首先咱們要清楚程序的編譯重要的三個階段:預處理階段,編譯階段和連接階段。學習
#define
是不被視爲語言的一部分,它在程序編譯階段中的預處理階段的做用,就是作簡單的替換。ui
以下面的 PI
宏定義,在程序編譯時,編譯器在預處理階段時,會先將源碼中全部 PI
宏定義替換成 3.14
:this
#define PI 3.14
複製代碼
程序編譯在預處理階段後,才進行真正的編譯階段。在有的編譯器,運用了此 PI
常量,若是遇到了編譯錯誤,那麼這個錯誤信息也許會提到 3.14 而不是 PI,這就會讓人困惑哪裏來的3.14
,特別是在項目大的狀況下。
解決之道:以 const 定義一個常量替換上述的宏(#define)
做爲一個語言變量,下面的 const 定義的常量 Pi
確定會被編譯器看到,出錯的時候能夠很清楚知道,是這個變量致使的問題:
const doule Pi = 3.14;
複製代碼
若是是定義常量字符串,則必需要 const
兩次,目的是爲了防止指針所指內容和指針自身不能被改變:
const char* const myName = "小林coding";
複製代碼
若是是定義常量 string
,則只須要在最前面加一次 const
,形式以下:
const std::string myName("小林coding");
複製代碼
#define 不重視做用域,因此對於 calss 的專屬常量,應避免使用宏定義。
還有另一點宏沒法涉及的,就是咱們沒法利用 #define
建立一個 class
專屬常量,由於 #define
並不重視做用域。
對於類裏要定義專屬常量時,咱們依然使用 static
+ const
,形式以下:
class Student {
private:
static const int num = 10;
int scores[num];
};
const int Student::num; // static 成員變量,須要進行聲明
複製代碼
若是不想外部獲取到 class 專屬常量的內存地址,可使用 enum 的方式定義常量
enum
會幫你約束這個條件,由於取一個 enum
的地址是不合法的,形式以下:
class Student {
private:
enum { num = 10 };
int scores[num];
};
複製代碼
#define 實現的函數容易出錯,而且長相醜陋不易閱讀。
另一個常見的 #define
誤用狀況是以它實現宏函數,它不會招致函數調用帶來的開銷,可是用 #define
編寫宏函數容易出錯,以下用宏定義寫的求最大值的函數:
#define MAX(a, b) ( { (a) > (b) ? (a) : (b); } ) // 求最大值
複製代碼
這般長相的宏有着太的缺點,好比在下面調用例子:
int a = 6, b = 5;
int max = MAX(a++, b);
std::cout << max << std::endl;
std::cout << a << std::endl;
複製代碼
輸出結果(如下結果是錯誤的):
7 // 正確的答案是 max 輸出 6
8 // 正確的答案是 a 輸出 7
複製代碼
要解釋出錯的緣由很簡單,咱們把 MAX
宏作簡單替換:
int max = ( { (a++) > (b) ? (a++) : (b); } ); // a 被累加了2次!
複製代碼
在上述替換後,能夠發現 a
被累加了 2 次。咱們能夠經過改進 MAX
宏,來解決這個問題:
#define MAX(a, b) ({ \ __typeof(a) __a = (a), __b = (b); \ __a > __b ? __a : __b; \ })
複製代碼
簡單說明下,上述的 __typeof
能夠根據變量的類型來定義一個相同類型的變量,如 a
變量是 int 類型,那麼 __a
變量的類型也是 int 類型。改進後的 MAX
宏,輸出的是正確的結果,max 輸出 6,a 輸出 7。
雖然改進的後 MAX
宏,解決了問題,可是這種宏的長相就讓人困惑。
解決的方式:用 inline 替換 #define 定義的函數
用 inline
修飾的函數,也是能夠解決函數調用的帶來的開銷,同時閱讀性較高,不會讓人困惑。
下面用用 template inline 的方式,實現上述宏定義的函數::
template<typename T>
inline T max(const T& a, const T& b) {
return a > b? a : b;
}
複製代碼
max
是一個真正的函數,它遵循做用域和訪問規則,因此不會出現變量被屢次累加的現象。
模板的基礎知識內存,可移步到個人舊文進行學習 --> 泛型編程的第一步,掌握模板的特性!
細節 01 小結 - 請記住
const 的一件奇妙的事情是:它容許你告訴編譯器和其餘程序員某值應該保持不變。
1. 面對指針,你能夠指定指針自身、指針所指物,或二者都(或都不)是 const:
char myName[] = "小林coding";
char *p = myName; // non-const pointer, non-const data
const char* p = myName; // non-const pointer, const data
char* const p = myName; // const pointer, non-const data
const char* const p = myName; // const pointer, const data
複製代碼
*
)左邊,表示指針所指物是常量(不能改變 *p 的值);*
)右邊,表示指針自身是常量(不能改變 p 的值);*
)兩邊,表示指針所指物和指針自身都是常量;2. 面對迭代器,你也指定迭代器自身或自迭代器所指物不可被改變:
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin(); // iter 的做用像 T* const
*iter = 10; // 沒問題,能夠改變 iter 所指物
++iter; // 錯誤! 由於 iter 是 const
std::vector<int>::const_iterator cIter = vec.begin(); // cIter 的做用像 const T*
*cIter = 10; // 錯誤! 由於 *cIter 是 const
++cIter; // 沒問題,能夠改變 cIter
複製代碼
const
便可(即聲明一個 T* const
指針); —— 這個不經常使用const_iterator
(即聲明一個 const T*
指針)。—— 這個經常使用const 最具備威力的用法是面對函數聲明時的應用。在一個函數聲明式內,const 能夠和函數返回值、各參數、成員函數自身產生關聯。
1. 令函數返回一個常量值,每每能夠下降因程序員錯誤而形成的意外。舉個例子:
class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);
複製代碼
爲何要返回一個 const
對象呢?緣由是若是不這樣,程序員就能實現這一的暴力行爲:
Rational a, b, c;
if (a * b = c) ... // 作比較時,少了個等號
複製代碼
若是 operator*
返回的 const
對象,能夠預防這個沒意義的賦值動做。
2. 將 const
實施於成員函數的目的,是爲了確認該成員函數可做用於 const
對象。理由以下兩個:
理由 1 :
它們使得 class
接口比較容易理解,由於能夠得知哪一個函數能夠改動對象而哪些函數不行,見以下例子:
class MyString {
public:
const char& operator[](std::size_t position) const // operator[] for const 對象
{ return text[position]; }
char& operator[](std::size_t position) // operator[] for non-const 對象
{ return text[position]; }
private:
std::string text;
};
複製代碼
MyString 的 operator[]
能夠被這麼使用:
MyString ms("小林coding"); // non-const 對象
std::cout << ms[0]; // 調用 non-const MyString::operator[]
ms[0] = 'x'; // 沒問題,寫一個 non-const MyString
const MyString cms("小林coding"); // const 對象
std::cout << cms[0]; // 調用 const MyString::operator[]
cms[0] = 'x'; // 錯誤! 寫一個 const MyString
複製代碼
注意,上述第 7 行會出錯,緣由是 cms
是 const 對象,調用的是函數返回值爲 const 類型的 operator[]
,咱們是不能夠對 const 類型的變量或變量進行修改的。
理由 2 :
它們使操做 const 對象成爲可能,這對編寫高效代碼是個關鍵,由於改善 C++ 程序效率的一個根本的方法是以 pass by referenc-to-const(const T& a) 方式傳遞對象,見以下例子:
class MyString {
public:
MyString(const char* str) : text(str)
{
std::cout << "構造函數" << std::endl;
}
MyString(const MyString& myString)
{
std::cout << "複製構造函數" << std::endl;
(*this).text = myString.text;
}
~MyString()
{
std::cout << "析構函數" << std::endl;
}
bool operator==(MyString rhs) const // pass by value 按值傳遞
{
std::cout << "operator==(MyString rhs) pass by value" << std::endl;
return (*this).text == rhs.text;
}
private:
std::string text;
};
複製代碼
operator==
函數是 pass by value, 也就是按值傳遞,咱們使用它,看下會輸出什麼:
int main() {
std::cout << "main()" << std::endl;
MyString ms1("小林coding");
MyString ms2("小林coding");
std::cout << ( ms1 == ms2) << std::endl; ;
std::cout << "end!" << std::endl;
return 0;
}
複製代碼
輸出結果:
main()
構造函數
構造函數
複製構造函數
operator==(MyString rhs) pass by value
1
析構函數
end!
析構函數
析構函數
複製代碼
能夠發如今進入 operator==
函數時,發生了「複製構造函」,當離開該函數做用域後發生了「析構函數」。說明「按值傳遞」,在進入函數時,會產生一個副本,離開做用域後就會消耗,說明這裏是存在開銷的。
咱們把 operator==
函數改爲 pass by referenc-to-const 後,能夠減小上面的副本開銷:
bool operator==(const MyString& rhs)
{
std::cout << "operator==(const MyString& rhs)
pass by referenc-to-const" << std::endl;
return (*this).text == rhs.text;
}
複製代碼
再次輸出的結果:
main()
構造函數
構造函數
operator==(const MyString& rhs) pass by referenc-to-const
1
end!
析構函數
析構函數
複製代碼
沒有發生複製構造函數,說明 pass by referenc-to-const 比 pass by value 性能高。
在 const 和 non-const 成員函數中避免代碼重複
假設 MyString 內的 operator[] 在返回一個引用前,先執行邊界校驗、打印日誌、校驗數據完整性。把全部這些同時放進 const 和 non-const operator[]中,就會致使代碼存在必定的重複:
class MyString {
public:
const char& operator[](std::size_t position) const
{
... // 邊界檢查
... // 日誌記錄
... // 校驗數據完整性
return text[position];
}
char& operator[](std::size_t position)
{
... // 邊界檢查
... // 日誌記錄
... // 校驗數據完整性
return text[position];
}
private:
std::string text;
};
複製代碼
能夠有一種解決方法,避免代碼的重複:
class MyString {
public:
const char& operator[](std::size_t position) const // 一如既往
{
... // 邊界檢查
... // 日誌記錄
... // 校驗數據完整性
return text[position];
}
char& operator[](std::size_t position)
{
return const_cast<char&>(
static_cast<const MyString&>(*this)[position]
);
}
private:
std::string text;
};
複製代碼
這份代碼有兩個轉型動做:
static_cast<const MyString&>(*this)[position]
,表示將 MyString& 轉換成 const MyString&,可以讓其調用 const operator[] 兄弟;const_cast<char&>( ... )
,表示將 const char & 轉換爲 char &,讓其是 non-const operator[] 的返回類型。雖然語法有一點點奇特,但「運用 const 成員函數實現 non-const 孿生兄弟 」的技術是值得了解的。
須要注意的是:咱們能夠在 non-const 成員函數調用 const 成員函數,可是不能夠反過來,在 const 成員函數調用 non-const 成員函數調用,緣由是對象有可能所以改動,這會違背了 const 的本意。
細節 02 小結 - 請記住
內置類型初始化
若是你這麼寫:
int x;
複製代碼
在某些語境下 x 保證被初始化爲 0,但在其餘語境中卻不保證。那麼可能在讀取未初始化的值會致使不明確的行爲。
爲了不不肯定的問題,最佳的處理方法就是:永遠在使用對象以前將它初始化。 例如:
int x = 0; // 對 int 進行手工初始化
const char* text = "abc"; // 對指針進行手工初始化
複製代碼
構造函數初始化
對於內置類型之外的任何其餘東西,初始化責任落在構造函數。
規則很簡單:確保每個構造函數都將對象的每個成員初始化。可是別混淆了賦值和初始化。
考慮用一個表現學生的class,其構造函數以下:
class Student {
public:
Student(int id, const std::string& name, const std::vector<int>& score)
{
m_Id = id; // 這些都是賦值
m_Name = name; // 而非初始化
m_Score = score;
}
private:
int m_Id;
std::string m_Name;
std::vector<int> m_Score;
};
複製代碼
上面的作法並不是初始化,而是賦值,這不是最佳的作法。由於 C++ 規定,對象的成員變量的初始化動做發生在進入構造函數本體以前,在構造函數內,都不算是被初始化,而是被賦值。
初始化的寫法是使用成員初值列,以下:
Student(int id,
const std::string &name,
const std::vector<int> &score)
: m_Id(id),
m_Name(name), // 如今,這些都是初始化
m_Score(score)
{} // 如今,構造函數本體沒必要有任何動做
複製代碼
這個構造函數和上一個構造函數的最終結果是同樣的,可是效率較高,凸顯在:
m_Name
和 m_Score
對象的默認構造函數做爲初值,而後在構造函數體內馬上再對它們進行賦值操做,這期間經歷了兩個步驟。m_Name
以 name
爲初值進行復制構造,m_Score
以 score
爲初值進行復制構造。另一個注意的是初始化次序(順序),初始化次序(順序):
m_Id
先被初始化,再是 m_Name
,最後是 m_Score
,不然會出現編譯出錯。避免「跨編譯單元之初始化次序」的問題
如今,咱們關係的問題涉及至少兩個以上源碼文件,每個內含至少一個 non-local static 對象。
存在的問題是:若是有一個 non-local static 對象須要等另一個 non-local static 對象初始化後,纔可正常使用,那麼這裏就須要保證次序的問題。
下面提供一個例子來對此理解:
class FileSystem {
public:
...
std::size_t numDisk() const; // 衆多成員函數之一
...
};
extern FileSystem tfs; // 預備給其餘程序員使用對象
複製代碼
現假設另一個程序員創建一個class 用以處理文件系統內的目錄,很天然他們會用上 tfs 對象:
class Directory {
public:
Directory( params )
{
std::size_t disks = tfs.numDisk(); // 使用 tfs 對象
}
...
};
複製代碼
使用 Directory 對象:
Directory tempDir( params );
複製代碼
那麼如今,初始化次序的重要性凸顯出來了,除非 tfsd 對象在 tempDir 對象以前被初始化,不然 tempDir 的構造函數會用到還沒有初始化的 tfs, 就會出現未定義的現象。
因爲 C++ 對「定義於不一樣的編譯單元內的 non-local static 對象」的初始化相對次序並沒有明肯定義。但咱們能夠經過一個小小的設計,解決這個問題。
惟一須要作的是:將每一個 non-local static 對象搬到本身的專屬函數內(該對象在此函數內被聲明爲 static),這些函數返回一個引用指向它所含的對象。
沒錯也就是單例模式,代碼以下:
class FileSystem {
public:
...
static FileSystem& getTfs() // 該函數做用是獲取 tfs 對象, {
static FileSystem tfs; // 定義並初始化一個 local static 對象,
return tfs; // 返回一個引用指向上述對象。
}
...
};
class Directory {
public:
...
Directory( params )
{
std::size_t disks = FileSystem::getTfs().numDisk(); // 使用 tfs 對象
}
...
};
複製代碼
這麼修改後,Directory 構造函數就會先初始化 tfs 對象,就能夠避免次序問題了。雖然內含了 static 對象,可是在 C++11 以上是線程安全的。
細節 03 小結 - 請記住
當你寫了以下的空類:
class Student { };
複製代碼
編譯器就會它聲明,而且這些函數都是 public 且 inline:
就好像你寫下這樣的代碼:
class Student {
Student() { ... } // 默認構造函數
Student(const Student& rhs) { ... } // 複製構造函數
Student& operator=(const Student& rhs) { ... } // 賦值操做符函數
~Student() { ... } // 析構函數
};
複製代碼
惟有當這些函數被須要調用時,它們纔會被編譯器建立出來,下面代碼形成上述每個函數被編譯器產出:
Student stu1; // 默認構造函數
// 析構函數
Student stu2(stu1); // 複製構造函數
stu2 = stu1; // 賦值操做符函數
複製代碼
編譯器爲咱們寫的函數,來講說這些函數作了什麼?
編譯器拒絕爲 class 生出 operator= 的狀況
對於賦值操做符函數,只有當生出的代碼合法且有適當機會證實它有意義,纔會生出 operator=
,若萬一兩個條件有一個不符合,則編譯器會拒絕爲 class 生出 operator=
。
舉個例子:
template<class T> class Student {
public:
Student(std::string & name, const T& id); // 構造函數
... // 假設未聲明 operator=
priavte:
std::string& m_Name; // 引用
const T m_Id; // const
};
複製代碼
現考慮下面會發生什麼:
std::string name1("小美");
std::string name2("小林");
Student<int> p(name1, 1);
Student<int> s(name2, 2);
p = s; // 如今 p 的成員變量會發生什麼?
複製代碼
賦值以前, p.m_Name
和 s.m_Name
都指向 string
對象且不是同一個。賦值以後 p.m_Name
應該指向 s.m_Name
所指的那個 string
嗎?也就是說引用自身可被改動嗎?若是是,那就開闢了新天地,由於 C++ 並不容許「讓引用更改指向不一樣對象」。
面對這個難題,C++ 的響應是拒絕編譯那一行賦值動做,本例子拒絕生成的 operator=
緣由以下:
m_Name
)的class 內支持賦值操做,你必須本身定義賦值操做函數,這種狀況是編譯器不會爲你自動生成賦值操做函數的。const
成員」(如本例的 m_Id
)的class,編譯器也是會拒絕生成 operator=
,由於更改 const
成員是不合法的。最後還有一個狀況:若是某個基類將 operator=
函數聲明爲 private
,編譯器將拒絕爲其派生類生成 operator=
函數。
細節 04 小結 - 請記住
在不容許存在如出一轍的兩個對象的狀況下,能夠把複製構造函數和賦值操做符函數聲明爲 private,這樣既可防止編譯器自動生成這兩個函數。以下例子:
class Student {
public:
...
private:
...
Student(const Student&); // 只有聲明
Student& operator=(const Student&); // 只有聲明
};
複製代碼
這樣的話,Student 對象就沒法操做下面的狀況了:
Student stu1;
Student stu2(stu1); // 錯誤,禁用了 複製構造函數
stu2 = stu1; // 錯誤,禁用了 賦值操做符函數
複製代碼
更容易擴展的解決方式是,能夠專門寫一個爲阻止 copying 動做的基類:
class Uncopyale {
protect: // 容許派生類對象構造和析構
Uncopyale() {}
~Uncopyale() {}
private: // 禁止派生類對象copying
Uncopyale(const Uncopyale&);
Uncopyale& operater=(const Uncopyale&);
};
複製代碼
使用方式很簡單,只須要 private
形式的繼承:
class Student : private Uncopyale{
... // 派生類不用再聲明覆制構造函數和賦值操做符函數
};
複製代碼
那麼只要某個類須要禁止 copying 動做,則只須要 private
形式的繼承 Uncopyale
基類便可。
細節 05 小結 - 請記住
多態特性的基礎內容,可移步到個人舊文進行學習 --> 掌握了多態的特性,寫英雄聯盟的代碼更少啦!
多態性質基類需聲明 virtual 析構函數
若是在多態性質的基類,沒有聲明一個 virtual
析構函數,那麼在 delete
基類指針對象的時候,只會調用基類的析構函數,而不會調用派生類的析構函數,這就是存在了泄漏內存和其餘資源的狀況。
以下有多態性質基類,沒有聲明一個 virtual
析構函數的例子:
// 基類
class A {
public:
A() // 構造函數
{
cout << "construct A" << endl;
}
~A() // 析構函數
{
cout << "Destructor A" << endl;
}
};
// 派生類
class B : public A
{
public:
B() // 構造函數
{
cout << "construct B" << endl;
}
~B()// 析構函數
{
cout << "Destructor B" << endl;
}
};
int main() {
A *pa = new B();
delete pa; // 釋放資源
return 0;
}
複製代碼
輸出結果:
construct A
construct B
Destructor A
複製代碼
從上面的結果,是發現了在 delete
基本對象指針時,沒有調用派生類 B
的析構函數。問題出在 pa
指針指向派生類對象,而那個對象卻經由一個基類指針被刪除,而目前的基類沒有 virtual
析構函數。
消除這個問題的作法很簡單:爲了不泄漏內存和其餘資源,須要把基類的析構函數聲明爲 virtual
析構函數。改進以下:
// 基類
class A {
public:
.... // 如上
virtual ~A() // virtual 析構函數
{
cout << "Destructor A" << endl;
}
};
... // 如上
複製代碼
此後刪除派生類對象就會如你想要的那般,是的,它會銷燬整個對象,包括全部派生類成份。
非多態性質基類無需聲明 virtual 函數
當類的設計目的不是被當作基類,令其析構函數爲 virtual
每每是個餿主意。
若類裏聲明瞭 virtual
函數,對象必須攜帶某些信息。主要用來運行期間決定哪個 virtual
函數被調用。
這份信息一般是由一個所謂 vptr(virtual table pointer —— 虛函數表指針)指針指出。vptr 指向一個由函數指針構成的數組,稱爲 vtbl(virtual table —— 虛函數表);每個帶有 virtual 函數的類都有一個相應的 vtbl。當對象調用某一 virtual 函數,實際被調用的函數取決於該對象的 vptr 所指向的那個 vtbl,接着編譯器在其中尋找適當的函數指針,從而調用對應類的函數。
既然內含 virtual 函數的類的對象必須會攜帶信息,那麼必然其對象的體積是會增長的。
所以,無故地將全部類的析構函數聲明爲 virtual ,是錯誤的,緣由是會增長沒必要要的體積。
許多人的心得是:只有當 class 內含至少一個 virtual 函數,才爲它聲明 virtual 析構函數。
細節 06 小結 - 請記住
咱們不應在構造函數和析構函數體內調用 virtual
函數,由於這樣的調用不會帶來你預想的結果。
咱們看以下的代碼例子,來講明:
// 基類
class CFather {
public:
CFather()
{
hello();
}
virtual ~CFather()
{
bye();
}
virtual void hello() // 虛函數 {
cout<<"hello from father"<<endl;
}
virtual void bye() // 虛函數 {
cout<<"bye from father"<<endl;
}
};
// 派生類
class CSon : public CFather
{
public:
CSon() // 構造函數
{
hello();
}
~CSon() // 析構函數
{
bye();
}
virtual void hello() // 虛函數 {
cout<<"hello from son"<<endl;
}
virtual void bye() // 虛函數 {
cout<<"bye from son"<<endl;
}
};
複製代碼
如今,當如下這行被執行時,會發生什麼事情:
CSon son;
複製代碼
先列出輸出結果:
hello from father
hello from son
bye from son
bye from father
複製代碼
無疑地會有一個 CSon(派生類) 構造函數被調用,但首先 CFather(基類) 構造函數必定會更早被調用。 CFather(基類) 構造函數體力調用 virtual 函數 hello,這正是引起驚奇的起點。這時候被調用的 hello 是 CFather 內的版本,而不是 CSon 內的版本。
說明,基類構造期間 virtual 函數毫不會降低到派生類階層。取而代之的是,對象的做爲就像隸屬於基類類型同樣。
非正式的說法或許比較傳神:在基類構造期間,virtual 函數不是 virtual 函數。
相同的道理,也適用於析構函數。
細節 07 小結 - 請記住
關於賦值,又去的是你能夠把它們寫成連鎖形式:
int x, y, z;
x = y = z = 15; // 賦值連鎖形式
複製代碼
一樣有趣的是,賦值採用右結合律,因此上述連鎖賦值被解析爲:
x = (y = ( z = 15 ));
複製代碼
這裏 15 先被賦值給 z,而後其結果再被賦值給 y,而後其結果再賦值給 x 。
爲了實現「連鎖賦值」,賦值操做必須返回一個 reference (引用)指向操做符的左側實參。這是咱們爲 classes 實現賦值操做符時應該遵循的協議:
class A {
public:
...
A& operator=(const A& rhs) // 返回類型是一個引用,指向當前對象。
{
...
return *this; // 返回左側對象
}
...
};
複製代碼
這個協議不只適用於以上標準賦值形式,也適用於全部賦值相關運算(+=, -=, *=, 等等),例如:
class A {
public:
...
A& operator+=(const A& rhs) // 這個協議適用於 +=, -=, *=, 等等。
{
...
return *this;
}
...
};
複製代碼
注意,這只是個協議,並沒有強制性。若是不遵循它,代碼同樣能夠經過編譯,可是會破壞本來的編程習慣。
細節 08 小結 - 請記住
「自我賦值」發生在對象被賦值給本身時:
class A { ... };
A a;
...
a = a; // 賦值給本身
複製代碼
這看起來有點愚蠢,但它合法,因此不要認定咱們本身絕對不會那麼作。
此外賦值動做並不老是那麼一眼被識別出來,例如:
a[i] = a[j]; // 潛在的自我賦值
複製代碼
若是 i 和 j 有相同的值,這即是個自我賦值。再看:
*px = *py; // 潛在自我賦值
複製代碼
若是 px 和 py 恰好指向同一個東西,這也是自我賦值,這些都是並不明顯的自我賦值。
考慮到咱們的類內含指針成員變量:
class B { ... };
class A {
...
private:
B * pb; // 指針,指向一個從堆分配而得的對象
}
複製代碼
下面是operator = 實現代碼,表面上看起來合理,但自我賦值出現時並不安全:
A& A::operator=(const A& rhs) // 一份不安全的operator = 實現版本
{
delete pb; // 釋放舊的指針對象
pb = new B(*rhs.pb); // 生成新的地址
return *this;
}
複製代碼
這裏的自我賦值的問題是, operator=
函數內的 *this(賦值的目的端)和 rhs 有多是同一個對象。果然如此 delete 就不僅是銷燬當前對象的 pb,它也銷燬 rhs 的 pb。
至關於發生了自我銷燬(自爆/自滅)過程,那麼此時 A 類對象持有了一個指向一個被銷燬的 B 類對象。很是的危險,請勿模仿!
下面來講說如何規避這種問題的方式。
方式一:比較來源對象和目標對象的地址
要想阻止這種錯誤,傳統的作法是在 operator=
函數最前面加一個 if
判斷,判斷是不是本身,不是才進行賦值操做:
A& A::operator=(const A& rhs)
{
if(this == &rhs)
return *this; // 若是是自我賦值,則不作任何事情。
delete pb; // 釋放舊的指針對象
pb = new B(*rhs.pb); // 生成新的地址
return *this;
}
複製代碼
這樣錯雖然行得通,可是不具有自我賦值的安全性,也不具有異常安全性:
我舊文裏《C++ 賦值運算符'='的重載(淺拷貝、深拷貝)》在規避這個問題試,就採用的是方式 一,這個方式是不合適的。
方式二:精心周到的語句順序
把代碼的順序從新編排如下就能夠避免此問題,例如一下代碼,咱們只需之一在賦值 pb 所指東西以前別刪掉 pb :
A& A::operator=(const A& rhs)
{
A* pOrig = pb; // 記住原先的pb
pb = new B(*rhs.pb); // 令 pb 指向 *pb的一個副本
delete pOrig; // 刪除原先的pb
return *this;
}
複製代碼
如今,若是「 new B 」這句發生了異常,pb 依然保持原狀。即便沒有加 if 自我判斷,這段代碼仍是可以處理自我賦值,由於咱們對原 B 作了一份副本、刪除原 B 、而後返回引用指向新創造的那個副本。
它或許不是處理自我賦值的最高效的方法,但它行得通。
方式三:copy and swap
更高效的方式使用所謂的 copy and swap 技術,實現方法以下:
class A {
...
void swap(A& rhs) // 交換*this 和 rhs 的數據 {
using std::swap;
swap(pb, rhs.pb);
}
...
private:
B * pb; // 指針,指向一個從堆分配而得的對象
}
};
A& A::operator=(const A& rhs)
{
A temp(rhs); // 爲 rhs 製做一份復件(副本)
swap(tmp); // 將 *this 數據和上述復件的數據交換。
return *this;
}
複製代碼
當類裏 operator=
函數被聲明爲「以 by value 方式接受實參」,那麼因爲 by value 方式傳遞東西會形成一份復件(副本),則直接 swap 交換便可,以下:
A& A::operator=(A rhs) // rhs是被傳對象的一份復件
{
swap(rhs); // 將 *this 數據和復件的數據交換。
return *this;
}
複製代碼
細節 09 小結 - 請記住
在如下我把複製構造函數和賦值操做符函數,稱爲「copying 函數」。
若是你聲明本身的 copying 函數,那麼編譯器就不會建立默認的 copying 函數。可是,當你在實現 copying 函數,遺漏了某個成分沒被 copying,編譯器卻不會告訴你。
確保對象內的全部成員變量 copying
考慮用一個 class 用來表示學生,其中自實現 copying 函數,以下:
class Student {
public:
...
Student(const Student& rhs);
Student& operator=(const Student& rhs);
...
private:
std:: string name;
}
Student::Student(const Student& rhs)
: name(rhs.name) // 複製 rhs 的數據
{ }
Student& Student::operator=(const Student& rhs)
{
name = rhs.name; // 複製 rhs 的數據
return *this;
}
複製代碼
這裏的每一件事情看起來都很好,直到另外一個成員變量加入戰局:
class Student {
public:
... // 同前
private:
std:: string name;
int score;
}
複製代碼
這時候遺漏對新成員變量的 copying。大多數編譯器對此不作任何報錯。
結論很明顯:若是你爲 class 添加一個成員變量,你必須同時修改 copying 函數。
確保全部 base class (基類) 成分 copying
一旦存在繼承關係的類,可能會形成此一主題最黑暗肆意的一個潛在危機。試考慮:
class CollegeStudent : public Student // 繼承 Student
{
public:
...
CollegeStudent(const CollegeStudent& rhs);
CollegeStudent& operator=(const CollegeStudent& rhs);
...
private:
std::string major;
};
CollegeStudent::CollegeStudent(const CollegeStudent& rhs)
: major(rhs.major)
{ }
CollegeStudent& CollegeStudent::operator=(const CollegeStudent& rhs)
{
major = rhs.major;
return *this;
}
複製代碼
CollegeStudent
的 copying 函數看起來好像複製了 CollegeStudent
內的每同樣東西,可是請再看一眼。是的,它們複製了 CollegeStudent
聲明的成員變量,但每一個 CollegeStudent
還內含所繼承的 Student
成員變量復件(副本),而哪些成員變量卻未被複制。
因此任什麼時候候只要咱們承擔起「爲派生類撰寫 copying 函數」的重則大任,必須很當心地也複製其 base class 成分:
CollegeStudent::CollegeStudent(const CollegeStudent& rhs)
: Student(rhs), // 調用 base class 的 copy構造函數
major(rhs.major)
{ }
CollegeStudent& CollegeStudent::operator=(const CollegeStudent& rhs)
{
Student::operator=(rhs); // 對 base class 成分進行賦值動做
major = rhs.major;
return *this;
}
複製代碼
因此咱們不只要確保複製全部類裏的成員變量,還要調用全部 base classes 內的適當的 copying 函數。
消除 copying 函數之間的重複代碼
還要一點須要注意的:不要令複製「構造函數」調用「賦值操做符函數」,來減小代碼的重複。這麼作也是存在危險的,假設調用賦值操做符函數不是你指望的。—— 錯誤行爲。
一樣也不要令「賦值操做符函數」調用「構造函數」。
若是你發現你的「複製構造函數和賦值操做符函數」有近似的代碼,消除重複代碼的作法是:創建一個新的成員函數給二者調用。
細節 10 小結 - 請記住
思惟導圖:
關注公衆號,後臺回覆「我要學習」,便可免費獲取精心整理「服務器 Linux C/C++ 」成長路程(書籍資料 + 思惟導圖)