拷貝構造函數:
如果一個構造函數的第一個參數是:自身類類型的引用,(且任何額外參數都有默認值)。
沒有拷貝構造函數,或者定義了其他構造函數,則編譯器,合成拷貝構造函數。
可使用合成拷貝構造函數,阻止拷貝某類類型的對象。
在合成拷貝構造函數中,類中每個成員的類型決定了它如何拷貝,該調用拷貝構造函數就調用。
直接初始化:要求編譯器使用普通的函數匹配。
拷貝初始化:要求編譯器,將右側運算對象,拷貝到正在創建的對象中。
可依靠:拷貝或移動構造函數來完成。
發生情況:
1、將對象傳遞給非引用形參;2、函數返回一個對象,函數返回類型爲非引用。3、用花括號列表初始化一個數組
vector接受單一大小參數的構造函數是,explicit:
雖然編譯器可略過,拷貝/移動構造函數,但他們必須是存在且可訪問的,也不能是private。
拷貝賦值運算符:
與處理拷貝構造函數一樣,如果一個類未定義,則編譯器自己合成拷貝賦值運算符。
合成的拷貝賦值運算符,可以用來禁止該類型對象的賦值。
它返回一個指向其左側運算對象的引用。
析構函數:
不接受參數,不能被重載,給定類,只有唯一一個析構函數。
作用:釋放對象在生存期分配的所有資源。在該函數中,首先執行函數體,然後銷燬成員,按初始化順序逆序銷燬。
與普通指針不同,智能指針成員,在析構階段會被自動銷燬。
調用時機:變量離開作用域;對象(容器)被銷燬,成員(元素)也被銷燬;應用delete;臨時對象;
析構函數自動運行,無須擔心何時釋放資源。
未定義,則編譯器回定義一個,合成析構函數(可用於,阻止該類型的對象被銷燬)。
成員是在,析構函數體之後,隱含的析構階段中,被銷燬的。
三/五法則:
1、若一個類需要析構函數,則幾乎肯定,也需要拷貝構造函數和拷貝賦值運算符。
若使用合成的拷貝構造函數和拷貝賦值運算符,有可能析構函數會釋放同個指針兩次:
2、若一個類需要拷貝構造函數,則幾乎,也需要一個拷貝賦值運算符。
使用=default:
顯示地要求,編譯器生成合成的版本。
若在類內使用=default,合成的函數,將隱式地聲明爲內聯的。
阻止拷貝:
iostream類阻止了拷貝,以避免多個對象寫入或讀取相同的IO緩衝。
可定義刪除的函數,來阻止拷貝。
=delete必須,出現在函數第一次聲明的時候。並可對任何函數指定=delete
析構函數不能是刪除的成員。
若析構函數是刪除了的,則不能定義該類型變量,但可動態分配這種類型的對象。
合成的拷貝控制成員,也可能是刪除的:
類的某個成員的 | 類中會被定義爲,刪除的函數 |
析構函數(刪除或不可訪問) | 合成的析構函數、合成的拷貝構造函數、默認構造函數 |
拷貝構造函數(刪除或不可訪問) | 合成的拷貝構造函數 |
拷貝賦值運算符(刪除)。或有一個const\引用成員 | 合成的拷貝賦值運算符 |
有引用\const成員(類內沒初始化器,且其類型未顯式定義) | 默認構造函數 |
拷貝控制和資源管理:
管理類外資源的類,必須定義拷貝控制成員。
拷貝一個像值的對象時,副本和原對象是完全獨立的。(如標準庫容器和string類)
拷貝一個像指針的類對象時,副本和原對象使用相同的底層數據。(如shared_ptr)
定義一個行爲像值的類:
一個對象賦予它自身,賦值運算符也能正確工作:
HasPtr& HasPtr::operator=(const HasPtr &rhs){
auto newp = new string(*rhs.ps);
delete ps;
ps = newp;
i = rhs.i;
return *this;
}
定義行爲像類指針的類:
可使用shared_ptr來管理類中的資源。
若想直接管理資源,則使用引用計數。可保存在動態內存中。
析構函數的定義:
賦值運算符定義:
HasPtr& HasPtr::operator=(const HasPtr &rhs){
++*ths.use;
if(--*use==0){
delete ps;
delete use;
}
ps = ths.ps;
i = ths.i;
use = rhs.use;
return *this;
}
交換操作:
void swap(Foo &lhs,Foo &rhs){
using std::swap;
swap(lhs.h,rhs.h);
}
如果存在類型特定的swap版本,swap調用會與之匹配。
自己編寫的swap函數:
ps:swap的存在就是爲了優化代碼,因此要將函數聲明爲inline函數。
ps:參數不是一個副本,因此傳參時會發生拷貝。拷貝HasPtr操作,會分配一個該對象的string副本。函數銷燬時,rhs會釋放掉交換出來的string的內存。這個函數也天然地確保,自賦值的安全。
拷貝控制示例:
資源管理,並不是一個類需要定義自己的,拷貝控制成員的唯一原因。
設計示例思路:
1、析構函數和拷貝賦值運算符,都必須從包含一條Message的所有Folder中刪除它。
2、拷貝構造函數和拷貝賦值運算符,都要將一個Message添加到給定的一組Folder中。
動態內存管理類:
某些類需要自己進行內存分配,也就是必須定義自己的拷貝控制成員來管理所分配的內存。
class StrVec {
public:
StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec &);
StrVec &operator=(const StrVec &);
~StrVec();
void push_back(const std::string &);//拷貝元素
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string *begin() const { return elements; }
std::string *end() const { return first_free; }
private:
static std::allocator<std::string> alloc;
void chk_n_alloc() {
if (size() == capacity())
reallocate();
}
std::pair<std::string *, std::string *> alloc_n_copy(const std::string *, const std::string *);
void free();//銷燬元素並釋放內存
void reallocate();//獲得更多內存,並拷貝已有內存
std::string *elements;//指向數組首元素的指針
std::string *first_free;//指向數組第一個空閒元素的指針
std::string *cap;//指向數組尾後位置的指針
};
void StrVec::push_back(const std::string &s) {
chk_n_alloc();
alloc.construct(first_free++, s);
}
//兩個指針分別指向新空間的,開始位置和拷貝的尾後的位置
std::pair<std::string *, std::string *> StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
auto data = alloc.allocate(e - b);
//first:指向分配的內存的開始位置; second:指向最後一個構造元素之後的位置
return {data, uninitialized_copy(b, e, data)};
}
void StrVec::free() {
if (elements) {
for (auto p = first_free; p != elements;)
alloc.destroy(--p);//運行string的析構函數
alloc.deallocate(elements, cap - elements);
}
}
StrVec::StrVec(const StrVec &s) {
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
StrVec::~StrVec() {
free();
}
StrVec &StrVec::operator=(const StrVec &rhs) {
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
//元素移動完後,string成員不再管理它們曾經指向的內存
void StrVec::reallocate() {
//將分配當前大小兩倍的內存空間
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;//指向新數組中,下一個空閒位置
auto elem = elements;//指向舊數組中,下一個元素
for (size_t i = 0; i != size(); ++i) {
//使用標準庫函數move,且提供一個using聲明
alloc.construct(dest++, std::move(*elem++));
}
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
ps:string具有類值行爲,每個string對構成它的所有字符都會保存自己的一份副本。
可以想象,string的移動構造函數,進行了指針的拷貝,而不是字符分配內存空間,然後拷貝字符。
對象移動:
IO類或unique_ptr,都不包含能被共享的資源。這些對象不能拷貝但可移動。
如果對象較大,進行不必要的拷貝代價非常高。
右值引用:必須綁定到右值的引用。
重要特性:只能綁定到一個將要銷燬的對象。
對於常規引用,則是左值引用,可將其綁定到,要求轉換的表達式、字面常量、返回右值的表達式。
標準庫move函數:
可以顯式地將一個左值轉換爲對應的右值引用類型。
move:獲得綁定到左值上的右值引用。
可以銷燬rr1或給它重新賦值,但不能再使用它的值。
移動構造函數和移動賦值運算符:
ps:移後原對象會被銷燬。
需顯式地,告訴標準庫,移動構造函數可以安全使用,將函數標記爲弄except來做到這一點。
移動賦值運算符:
StrVec &StrVec::operator=(StrVec &&rhs) noexcept{
//直接檢測自賦值
if(this != &rhs){
free();//釋放已有元素
elements = rhs.elements;//從rhs接管資源
first_free = rhs.first_free;
cap = rhs.cap;
//將rhs置於可析構狀態
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
ps:先釋放左側運算對象所使用的內存,並接管給定對象的內存。
需主要的細節:
1、在移動操作之後,移後源對象必須保持有效的、可析構的狀態,但用戶不能對其值進行任何假設。StrVec的移動操作,是通過,將移後源對象,的指針成員,置爲nullptr來實現。
2、只當類定義了自己的拷貝成員,且它的所有數據成員都有移動成員,編譯器纔會爲這個類,合成移動成員。
3、合成的移動操作,被定義爲刪除的函數:
一個Y類型的類成員,它定義了,自己的拷貝構造函數,但未定義自己的移動構造函數。某個類包含Y類型數據成員,則這個類的合成移動操作,被定義爲刪除的函數。
4、定義了移動操作的成員的類,必須要定義自己的拷貝操作,否則,合成的拷貝操作,將被定義爲刪除的。
5、如果一個類有,拷貝控制成員,而沒有移動控制成員,則其對象是,通過拷貝成員來"移動"的。
6、單一的賦值運算符,實現了拷貝和移動賦值運算符兩種功能:
上述,賦值運算符,有一個非引用參數,因此,會發生拷貝初始化。而拷貝初始化,要麼使用拷貝構造函數,要麼使用移動構造函數。
第一個賦值,拷貝初始化時,使用拷貝構造函數。第二個賦值,拷貝初始化時,則調用移動構造函數。
7、一個類要是定義了,任何一個拷貝操作,則就應該定義所有五個操作。
Message類的移動操作:
ps:從m的folders中,刪除掉m,然後在folders中加入,本msg。
移動構造函數:
ps:contents直接調用string的移動構造函數,folders就直接刪除m,然後加入,在新地址的Message
移動賦值構造函數:
ps:remove_from_Folders,要銷燬左側運算對象,就得從左側對象的folders中,移除掉指向本Message的指針。
移動迭代器:
通過改變,給定迭代器的解引用運算符,來適配此迭代器。解引用運算符,會生成一個右值引用。
可調用標準庫的make_move_iterator函數,將一個普通迭代器,轉換爲一個移動迭代器。
ps:對於轉換後的輸入序列,當解引用時,會生成一個右值引用,意味着,construct將使用,移動構造函數來構造元素。