拷贝控制
當定義一個類時,我們顯式地或隱式地指定在此類型的對象拷貝、移動、賦值和銷毀時做什么。一個類通過定義五種特殊的成員函數來控制這些操作,包括:拷貝構造函數(copy constructor)、拷貝賦值運算符(copy-assignment operator)、移動構造函數(move constructor)、移動賦值運算符(move-assignment operator)和析構函數(destructor)。拷貝和移動構造函數定義了當用同類型的另一個對象初始化本對象時做什么。拷貝和移動賦值運算符定義了將一個對象賦予同類型的另一個對象時做什么。析構函數定義了當此類型對象銷毀時做什么。我們稱這些操作為拷貝控制操作(copy control)。
1,拷貝構造函數——如果沒有為一個類定義拷貝構造函數,編譯器會為我們自動定義一個,稱之為合成拷貝構造函數
class Foo { public:Foo(); //默認構造函數Foo(const Foo&); //拷貝構造函數 } string dots(10,'.'); //直接初始化 string s(dots); //直接初始化 string s2=dots; //拷貝初始化 string null_book="9-999-99999-9"; //拷貝初始化 string nines=string(100,'9'); //拷貝初始化當使用直接初始化時,編譯器使用普通函數匹配來選擇與我們提供的參數最匹配的構造函數,當我們使用拷貝初始化時,編譯器將右側運算對象拷貝到正在創建的對象中,如果需要的話還要進行類型轉換。
2,拷貝賦值運算符——合成拷貝
class Foo{ public: Foo& operator=(const Foo&); //賦值運算符 }3,析構函數——合成析構函數不會delete一個指針數據成員
class Foo{ public:~Foo(); //析構函數 }三/五法則
1) 需要析構函數的類也需要拷貝和賦值操作
2) 需要拷貝操作的類也需要賦值操作,反之亦然。
使用default:當我們在類內用=default修飾成員的聲明時,合成的函數將隱式地聲明為內聯的,而對成員的類外定義使用=default,就像對拷貝賦值運算符所做的那樣。
使用delete阻止拷貝和賦值,不能刪除析構函數。
private拷貝控制,將其拷貝構造函數和拷貝賦值運算符聲明為private來阻止拷貝。將拷貝控制成員聲明為private,但不定義它們。
拷貝控制和資源管理:通常,管理類外資源的類必須定義拷貝控制成員。拷貝語義:1,類的行為像一個值;2,類的行為像一個指針,共享狀態。
1,類的行為像一個值
Hasptr需要:
1,定義一個拷貝構造函數,完成string的拷貝,而不是拷貝指針
2,定義一個析構函數來釋放string
3,定義一個拷貝賦值運算符來釋放對象當前的string,并從右側運算對象拷貝string
2,定義行為像指針的類
對于行為類似指針的類,我們需要為其定義拷貝構造函數和拷貝賦值運算符,來拷貝指針成員本身而不是它指向的string。我們的類仍然需要自己的析構函數來釋放接受string參數的構造函數的內存。但是,析構函數不能單方面地釋放關聯的string。只有當最后一個指向string的Hasptr銷毀時,它才可以釋放string。
定義一個使用引用計數的類
class Hasptr{ public:Hasptr(const std::string &s = std::string()):ps(new std::string(s)), i(0),use(new std::size_t(1)){}Hasptr(const Hasptr &p) :ps(new std::string(*p.ps)), i(p.i), use(p.use){ ++*use; }Hasptr& operator=(const Hasptr &);~Hasptr(){ } private:std::string *ps;int i;std::size_t *use; //用來記錄有多少個對象共享*ps的成員 };類指針的拷貝成員“篡改”引用計數
Hasptr::~Hasptr() {if (--*use == 0){delete ps; //釋放string內存delete use; //釋放計數器內存} }Hasptr& Hasptr::operator=(const Hasptr &rhs) {++*rhs.use; //遞增右側運算對象的引用計數if (--*use == 0){delete ps; //如果沒有其他用戶,釋放本對象分配的成員delete use;}ps = rhs.ps;i = rhs.i;use = rhs.use;return *this; }交換操作——swap,交換兩個元素,swap函數應該調用swap,而不是std::swap
class Hasptr{friend void swap(HasPtr&, HasPtr&); }_inline void swap(Hasptr &lhs, Hasptr &rhs) {using std::swap;swap(lhs.ps, rhs.ps); //交換指針,而不是string數據swap(lhs.i, rhs.i); //交換int成員 }每個swap調用應該都是未加限定的。即,每個調用都應該是swap,而不是std::swap。如果存在類型特定的swap版本,其匹配程度會優于std中定義的版本。同樣,在賦值運算符中使用swap
//注意rhs是按值傳遞的,意味著HasPtr的拷貝構造函數
//將右側運算對象中的string拷貝到rhs
對象移動:1,在重新分配內存的過程中,從舊內存將元素拷貝到新內存是不必要的,更好的方式是移動元素。2,使用移動而不是拷貝的另一個原因源于IO類或unique_ptr這樣的類,這些類都包含不能被共享的資源(如指針或IO緩沖)。因此,這些類型的對象不能拷貝但可以移動。
一,右值引用(rvalue reference)。通過&&而不是&來獲得右值引用,右值只能綁定到一個將要銷毀的對象。左值有持久的狀態,而右值要么是字面常量,要么是在表達式求值過程中創建的臨時對象。右值所引用的對象將要被銷毀,該對象沒有其他用戶。使用右值引用的代碼可以自由地接管所引用的對象的資源。
int i=42; int &r=i; //正確:r引用i int &&rr=i; //錯誤:不能將一個右值引用綁定到一個左值上 int &r2=i*42; //錯誤:i*42是一個右值 const int &r3=i*42; //正確:我們可以將一個const的引用綁定到一個右值上 int &&rr2=i*42; //正確;將rr2綁定到乘法結果上標準庫move函數,定義在頭文件utility。
int &&rr3=std::move(rr1); //okmove告訴編譯器:我們有一個左值,但我們希望像一個右值一樣處理它。我們必須認識到,調用move就意味著承諾:除了對rr1賦值或銷毀它之外,我們將不再使用它。在調用move之后,我們不能對移后源對象的值做任何假設。
移動構造函數和移動賦值運算符,我們必須在類頭文件的聲明中和定義中(如果定義在類外的話)都指定noexcept。
class StrVec{ public:StrVec(StrVec&&) noexcept; //移動構造函數 }StrVec::StrVec(SteVec &&s) noexcept : elements(s.elements),first_free(s.first_free),cap(s.cap) {//令s進入這樣的狀態—對其運行析構函數是安全的s.elements=s.first_free=s.cap=nullptr; }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; }只有當一個類沒有定義任何版本的拷貝控制成員,且類的每個非static數據成員都可以移動時,編譯器才會為它合成移動構造函數或移動賦值運算符。編譯器可以移動內置類型的成員。如果一個成員是類類型,且該類有對應的移動操作,編譯器才能移動這個成員:
//編譯器會為X和hasX合成移動操作 struct X{int i; // 內置類型可以移動std::string s; //string定義了自己的移動操作 }struct hasX{X mem; //X有合成的移動操作 }X x,x2=std::move(x); //使用合成的移動構造函數 hasX hx,hx2=std::move(hx); //使用合成的移動構造函數另外,
hasY(hasY&&)=default; //hasY將有一個刪除的移動構造函數如果一個類既有移動構造函數,也有拷貝構造函數,編譯器使用普通的函數匹配規則來確定使用哪個構造函數,賦值操作的情況類似。
1,移動右值,拷貝左值
2,如果沒有移動構造函數,右值也被拷貝
總結

- 上一篇: 动态内存
- 下一篇: Caffe下自己的数据训练和测试