在〈類別模版〉中的 LinkedList
範例,每個元素都由內部類別 Node
實例保存,而 Node
是以 new
動態配置,若不再使用 LinkedList
實例,應該清除這些 new
出來的 Node
實例,這可以藉由定義解析式(destructor)來實作,例如:
#include <iostream>
using namespace std;
template <typename T>
class LinkedList {
class Node {
public:
Node(T value, Node *next) : value(value), next(next) {}
T value;
Node *next;
};
Node *first = nullptr;
public:
~LinkedList(); // 解構式
...略
};
...略
template <typename T>
LinkedList<T>::~LinkedList() {
if(this->first == nullptr) {
return;
}
Node *last = this->first;
do {
Node *next = last->next;
delete last;
last = next;
} while(last != nullptr);
}
...略
解析式是由 ~
開頭,不用指定傳回型態,與類別名稱空間的成員函式,當實例被清除時,就會執行解構式,可以在解構式中實作清除資源的動作,在這邊用來 delete
每個 new
出來的 Node
實例。
如果沒有定義解構式,那麼編譯器會自行建立一個本體為空的解構式。
如果 LinkedList
實例被建構出來之後,不會被用來建構另一個 LinkedList
實例,那麼以上的實作是不會有什麼問題,然而若是如下就會出問題:
...略
int main() {
LinkedList<int> *lt1 = new LinkedList<int>();
(*lt1).append(1).append(2).append(3);
LinkedList<int> lt2 = *lt1; // 複製初始化
delete lt1;
cout << lt2.get(2) << endl; // 不可預期的結果
return 0;
}
若使用一個類別實例來建構另一類別實例,預設會發生值域的複製,複製的行為視型態而定,以指標類型來說,會是複製位址,也就是淺複製(shallow copy),就上例來說,*lt1
實例的 first
位址值會複製給 lt2
的 first
,在 delete lt1
後,*lt1
實例的 first
位址處之物件被 delete
,因此透過 lt2
的 first
存取的位址值就無效了。
若使用一個類別實例來建構另一類別實例,可以定義複製建構式(copy constructor)來實現自定義的複製行為。例如:
#include <iostream>
using namespace std;
template <typename T>
class LinkedList {
class Node {
public:
Node(T value, Node *next) : value(value), next(next) {}
T value;
Node *next;
};
Node *first = nullptr;
public:
LinkedList() = default; // 預設建構式
LinkedList(const LinkedList<T> <); // 複製建構式
~LinkedList();
...略
};
template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
// 逐一複製 Node 實例(而不是複製位址值)
if(lt.first != nullptr) {
this->first = new Node(lt.first->value, nullptr);
}
Node *thisLast = this->first;
Node *srcNext = lt.first->next;
while(srcNext != nullptr) {
thisLast->next = new Node(srcNext->value, nullptr);
thisLast = thisLast->next;
srcNext = srcNext->next;
}
}
...
template <typename T>
LinkedList<T>::~LinkedList() {
if(this->first == nullptr) {
return;
}
Node *last = this->first;
do {
Node *next = last->next;
delete last;
last = next;
} while(last != nullptr);
}
...
跟預設建構式不同的是,無論有沒有定義其他建構式,若沒有定義複製建構式,那編譯器一定會生成一個複製建構式,預設會發生值域的複製,複製的行為視型態而定,基本型態的話就是複製值,指標的話是複製位址值,陣列的話,會逐一複製每個元素,類別型態的話,視各類別定義的複製建構式而定。
也就是說,在沒有自定義 LinkedList
的複製建構式前,編譯器產生的預設建構式,相當於有以下的內容:
template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) : first(lt.first) {}
在定義了複製建構式,方才的 main
執行上沒問題了,然而以下還是會有問題:
...略
int main() {
LinkedList<int> *lt1 = new LinkedList<int>();
LinkedList<int> lt2;
(*lt1).append(1).append(2).append(3);
lt2 = *lt1; // 指定時會發生複製
delete lt1;
cout << lt2.get(2) << endl; // 不可預期的結果
return 0;
}
在指定時預設也是會發生複製行為,指定時預設的行為類似預設的複製建構式,若要避免問題發生,得自定義複製指定運算子(copy assignment operator):
#include <iostream>
using namespace std;
template <typename T>
class LinkedList {
class Node {
public:
Node(T value, Node *next) : value(value), next(next) {}
T value;
Node *next;
};
Node *first = nullptr;
void copy(const LinkedList<T> <);
public:
LinkedList() = default;
LinkedList(const LinkedList<T> <);
~LinkedList();
LinkedList<T>& operator=(const LinkedList<T> <); // 定義複製指定運算子
...略
};
template <typename T>
void LinkedList<T>::copy(const LinkedList<T> <) {
// 逐一複製 Node 實例(而不是複製位址值)
if(lt.first != nullptr) {
this->first = new Node(lt.first->value, nullptr);
}
Node *thisLast = this->first;
Node *srcNext = lt.first->next;
while(srcNext != nullptr) {
thisLast->next = new Node(srcNext->value, nullptr);
thisLast = thisLast->next;
srcNext = srcNext->next;
}
}
template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
this->copy(lt);
}
template <typename T>
LinkedList<T>& LinkedList<T>::operator=(const LinkedList<T> <) {
this->copy(lt);
return *this;
}
...略
template <typename T>
LinkedList<T>::~LinkedList() {
if(this->first == nullptr) {
return;
}
Node *last = this->first;
do {
Node *next = last->next;
delete last;
last = next;
} while(last != nullptr);
}
...略
如果定義類別時,需要考慮到要不要自定義解構式、複製建構式、複製指定運算子其中之一,幾乎就是三個都要定義了,這就是 Rule of three。
如果某個類別不希望被複製、指定等,C++ 11 以後可以如下:
struct Foo {
Foo() = default; // 採用預設建構式行為
Foo(const Foo&) = delete; // 刪除此函式(不定義此函式)
~Foo() = default; // 採用預設解構式行為
Foo& operator=(const Foo&) = delete; // 刪除此函式(不定義此函式)
};
在過去的話,會將複製建構式、複製指定運算子設為 private
:
class Foo {
Foo(const Foo&);
Foo& operator=(const Foo&);
public:
Foo() = default;
~Foo();
};
另外,在〈rvalue 參考〉中看過 std::move
用來實現移動語義,而建構式、指定運算子也可以實現移動語義,也就是移動建構式(move constructor)、移動指定運算子(move assignment operator),如果考慮要在類別上實現移動語義,解構式、複製/移動建構式、複製/移動指定運算子幾乎就都要全部出現,這就是 Rule of Five。
例如,可以為 LinkedList
加上移動建構式、移動指定運算子:
#include <iostream>
#include <utility>
using namespace std;
template <typename T>
class LinkedList {
class Node {
public:
Node(T value, Node *next) : value(value), next(next) {}
T value;
Node *next;
};
Node *first = nullptr;
void copy(const LinkedList<T> <);
void move(LinkedList<T> <);
public:
LinkedList() = default;
LinkedList(const LinkedList<T> <); // 複製建構式
LinkedList(LinkedList<T> &<); // 移動建構式
~LinkedList(); // 解構式
LinkedList<T>& operator=(const LinkedList<T> <); // 複製指定運算子
LinkedList<T>& operator=(LinkedList<T> &<); // 移動指定運算子
LinkedList<T>& append(T value);
T get(int i);
};
template <typename T>
void LinkedList<T>::copy(const LinkedList<T> <) {
// 逐一複製 Node 實例(而不是複製位址值)
if(lt.first != nullptr) {
this->first = new Node(lt.first->value, nullptr);
}
Node *thisLast = this->first;
Node *srcNext = lt.first->next;
while(srcNext != nullptr) {
thisLast->next = new Node(srcNext->value, nullptr);
thisLast = thisLast->next;
srcNext = srcNext->next;
}
}
template <typename T>
void LinkedList<T>::move(LinkedList<T> <) {
if(lt.first != nullptr) {
this->first = lt.first;
lt.first = nullptr;
}
}
template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
this->copy(lt);
}
template <typename T>
LinkedList<T>::LinkedList(LinkedList<T> &<) {
this->move(lt);
}
template <typename T>
LinkedList<T>& LinkedList<T>::operator=(const LinkedList<T> <) {
this->copy(lt);
return *this;
}
template <typename T>
LinkedList<T>& LinkedList<T>::operator=(LinkedList<T> &<) {
this->move(lt);
return *this;
}
template <typename T>
LinkedList<T>& LinkedList<T>::append(T value) {
Node *node = new Node(value, nullptr);
if(first == nullptr) {
this->first = node;
}
else {
Node *last = this->first;
while(last->next != nullptr) {
last = last->next;
}
last->next = node;
}
return *this;
}
template <typename T>
T LinkedList<T>::get(int i) {
Node *last = this->first;
int count = 0;
while(true) {
if(count == i) {
return last->value;
}
last = last->next;
count++;
}
}
template <typename T>
LinkedList<T>::~LinkedList() {
if(this->first == nullptr) {
return;
}
Node *last = this->first;
do {
Node *next = last->next;
delete last;
last = next;
} while(last != nullptr);
}
int main() {
LinkedList<int> lt1;
lt1.append(1).append(2).append(3);
LinkedList<int> lt2 = std::move(lt1); // 將 lt1 的資料移動給 lt2
cout << lt2.get(2) << endl;
return 0;
}
記得移動之後,因為資料轉移出去了,對目前 lt1
的狀態不能有任何的假設,只能銷毀 lt1
,或者重新指定實例給 lt1
。
具有解構式、複製/移動建構式、複製/移動指定運算子的類別,要全權負責管理自身資源;至於其他類別,就完全不需要其中之一,這就是 Rule of zero。
有關 Rule of three、Rule of Five、Rule of zero,可進一步參考〈The rule of three/five/zero〉。