1. 程式人生 > >C++11(12):拷貝控制

C++11(12):拷貝控制

拷貝建構函式的第一個引數必須是引用型別,此引數幾乎總是const的引用。拷貝建構函式在幾種情況下會隱式地使用。因此,拷貝建構函式不應該是explicit的 即使我們定義了其他建構函式,在沒有拷貝建構函式時,編輯器也會為我們合成的。編輯器從給定物件中依次將每個非static成員拷貝到建立的物件中。每個成員決定了它使用何種方式進行拷貝。類呼叫拷貝建構函式,陣列逐個拷貝,內建型別直接拷貝 string dots(10,'.')  //直接初始化 string noll_book="999999";   //拷貝初始化 當我們使用拷貝初始化時,我們要去編譯器將右側運算物件拷貝到正在建立的物件中,如果需要可能進行型別轉化 拷貝初始化通常使用的是拷貝建構函式。但是,如果一個類有一個移動建構函式,則拷貝初始化有時會使用移動建構函式。 拷貝初始化不僅在我們用=定義變數時發生,在以下情況也會發生: 將一個物件作為實參傳遞給一個非引用型別的形參 從一個返回型別為非引用型別的函式返回一個物件 用花括號列表初始化一個數組中的元素或一個聚合類中的成員 某些類型別還會對他們所分配的物件使用拷貝初始化。如,當我們初始化標準庫容器或是呼叫insert或push成員時,容器會對其元素進行拷貝初始化。與之相對的,emplace用的是直接初始化。 如果拷貝建構函式的引數不是引用,那麼我們將會再次呼叫拷貝建構函式。如此迴圈 如果拷貝建構函式宣告為explicit那麼,不能進行隱式轉換 vector<int> v1(10); vector<int> v2 = 10 ; //// 錯誤, void f(vector<int>);// 宣告一個函式 f(10); //錯誤: f(vector<int>(10));///正確,從int直接構造一個臨時vector 編輯器可以繞過拷貝建構函式,但不是必須。而且拷貝/移動建構函式必須是可訪問的 string null_book = "9999"; 編輯器可改寫為: string null_book("9999"); 某些運算子,包括賦值運算子,必須定義為成員函式。如果一個運算子是一個成員函式,其左側運算物件就繫結到隱式的this引數。對於二元運算子,例如賦值運算子,其右側運算物件作為顯示引數傳遞。 為了與內建型別保持一致,賦值運算子通常返回一個指向其左側運算物件的引用。另外值得注意的是,標準庫通常要求儲存在容器中的型別要具有賦值運算子,且返回值是左側物件的引用。 如果未定義自己的拷貝賦值運算子,編譯器會為它自動生成一個合成賦值運算子,會將右側物件的每個非static成員賦予左側。 //等價於合成拷貝賦值運算子 Sales_data& Sales_data::operator = (const Sales_data &rhs) {     bookNo = rhs.bookNo;     units_sold = rhs.units_sold;     revenue = rhs.revenue;     return *this; } 解構函式釋放物件使用的資源,並銷燬物件的非static資料成員。由於解構函式不接有引數也沒有返回值,對一個給定的類,只有唯一一個。 在一個解構函式中,首先執行函式體,然後銷燬成員。成員按初始化順序的逆序銷燬。 在物件最後一次使用之後,解構函式的函式體可執行類設計者希望執行的任何收尾工作。通常,會釋放物件在生存期分配的所有資源 析構部分是隱式的。成員銷燬時依賴成員的型別。銷燬類會呼叫自己的解構函式。內建型別沒有,什麼也不做。 隱式銷燬一個內建指標型別的成員不會delete掉它所指的物件。但是智慧指標會呼叫自己的解構函式,在析構階段會自動銷燬 無論何時一個物件被銷燬,就會自動呼叫其解構函式:     變數離開其作用域時被銷燬     當一個物件被銷燬時,其成員被銷燬        容器(無論是標準庫容器還是陣列)被銷燬時,其元素被銷燬     對於動態分配的物件,當對指向它的指標引用delete運算子時被銷燬     對於臨時物件,當建立它的完整表示式結束時別銷燬。 解構函式時自動執行的,我們無須擔心資源何時被釋放。 當指向一個物件的引用或指標離開作用域時,解構函式不會被執行。 當類未定義自己的解構函式時,編輯器會自己定義一個合成解構函式。 類似拷貝建構函式和拷貝賦值運算子,對於某些類,合成解構函式被用來阻止該型別的物件被銷燬。如果不是這種情況,課程解構函式的函式體就為空。在(空)解構函式執行完畢後,成員會被自動銷燬。 解構函式自身並不直接銷燬成員。成員實在解構函式體之後隱含的析構階段中被銷燬的。解構函式體是作為成語銷燬之外的另一部分而進行的 我們可以從解構函式開始判斷:如果一個類需要一個解構函式,我們幾乎可以肯定它也需要一個拷貝建構函式和一個 拷貝賦值運算子。 如果一個雷需要一個拷貝建構函式,幾乎可以肯定它也需要一個拷貝賦值運算子。反之亦然,如果一個類需要一個拷貝賦值運算子,幾乎可以肯定它也需要一個拷貝建構函式。然而,無論是需要拷貝建構函式還是拷貝賦值運算子都不必然意味著也需要解構函式。 ~Sales_data() = default;//顯示的要求編輯器生成合成的版本; 當我們在雷內使用= default時,合成的函式將隱式的宣告為行內函數,如果我們不需要內聯應該只在定義是使用。 大多數類應該定義預設建構函式、拷貝建構函式和拷貝賦值運算子,無論是顯示的還是隱式的。 有時候類必須採取某種機制阻止拷貝和賦值,如,iostream類就阻止了拷貝,以避免多個物件寫入或讀取相同IO緩衝。 在新標準下,我們可以通過將拷貝建構函式和拷貝運算子定義成刪除的函式來阻止拷貝。 刪除函式是這樣的,我們雖然聲明瞭它們,但不能以任何方式使用它們。 NoCopy(const NoCapy& ) = delete;//阻止拷貝 與=default不同,=delete必須是在函式第一次宣告出現的時候。我們可以對任何函式指定delete,我們只能對編輯器可以合成的預設建構函式或拷貝控制成員使用=default。雖然刪除函式主要是阻止拷貝,擔當我們希望引導函式匹配過程時,刪除函式有時也是很有用的。 值得注意的是我們不能定義解構函式時刪除函式。 如果將解構函式定義成刪除函式,我們不能定義該類的變數或臨時物件。可以定義動態分配這種型別的物件。但是,不能釋放這些物件。 對某些類來說,編輯器將合成的成員定義為刪除的函式:     如果類的某個成員的解構函式時刪除的或不可訪問的(如,private),則類的合成解構函式被定義為刪除的。     如果類的某個成員的拷貝建構函式時刪除的或不可訪問的,則合成的拷貝建構函式被定義為刪除的。如果類的某個成員的解構函式是刪除的或不可訪問的,則類合成的拷貝建構函式也被定義為刪除的。     如果類的某個成員的拷貝賦值運算是刪除的或不可訪問的,或類有一個const或引用成員,則類的合成拷貝賦值運算子被定義為刪除的     如果類的某個成員的解構函式是刪除的或不可訪問的,或一個類有一個引用成員,它麼有類內初始化器,或是類有一個const成員,它沒有類內初始化器且其未顯示定義預設建構函式,則該類的預設建構函式被定義為刪除的。 這些規則的含義是:如果一個類有資料成員不能預設構造,拷貝,複製或銷燬,則對應的成員函式被定義為刪除的。 一個成員有刪除的或不可訪問的解構函式會導致合成預設和拷貝建構函式被定義為刪除的。 雖然我們可以將一個新值賦予一個引用成員,但這樣做改變的是引用指向的物件的值,而不是引用本身。 在新標準釋出之前,類是通過將其拷貝建構函式和拷貝賦值運算子宣告為private來阻止拷貝的、為了阻止友元和成員函式進行拷貝,我們將這些拷貝控制成員宣告為private的,但不定義他們。如果訪問就會在編譯階段或連結階段報錯。 宣告但不定義一個成員函式是合法的,對此只有一個例外。 應使用新版的=delete。 拷貝控制和資源管理有兩種方式,令類的行為像一個值,令類的行為像一個指標。 //行為像值得類 class HasPtr{ public:     HasPtr(const std::string &s = std::string()) : ps(new std::string(s),i(0) { }     HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)) , i(p.i) { }     HasPtr& operator = (const HasPtr &);     ~HasPtr() { delete ps;} private:     std::string *ps;     int                i; }; 賦值運算子通常組合了解構函式和建構函式的操作。要保證正確的順序執行,即使將一個物件賦予它自身,也保證正確。而且,如果有可能,我們編寫賦值運算還應該是異常安全的,當異常發生時能將左側物件置於一個有意義的狀態。 //類值拷貝賦值運算子 HasPtr& HasPtr::operator=(const HasPtr &rhs) {     auto newp = new string(*rhs.ps);   //在銷燬左側運算物件資源之前拷貝右側運算物件     delete ps;     ps=newp;        i=rhs.i;     return *this; } 如果寫成delete ps;        ps = new string(*(rhs.ps));  當是同一個物件時將報錯。 引用計數的工作方式:     除了初始化物件外,每個建構函式(拷貝建構函式除外)還要建立一個引用計數,用來記錄有多少物件正在建立的物件共享狀態。當我們建立一個物件時,只有一個物件共享狀態,因此將技術器初始化為1.     拷貝建構函式不分配新的計數器,而是拷貝給定物件的資料成員,包括計數器。拷貝建構函式遞增共享的計數器,指出給定物件的狀態又被一個新的使用者所共享。     解構函式遞減計數器,指出共享狀態的使用者少了一個。如果計數器變為0,則解構函式釋放狀態。     拷貝賦值運算子遞增右側運算物件的計數器,遞減左側運算物件的計數器。如果左側運算物件的計數器變為0,意味著它的共享狀態沒有使用者了,拷貝賦值運算子就必須銷燬狀態。 引用計數應該儲存在哪裡,一種方法是儲存在動態記憶體中。 //定義行為像指標的類,我們也可以用shared_ptr實現資料的共享。 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(p.ps) , i(p.i) , use(p.use) { ++*use;}     HasPtr& operator = (const HasPtr&);     ~HasPtr(); private:     std::string *ps;     int    i;     std::size_t   *use; }; HasPtr::~HasPtr() {     if(--*use == 0)     {         delete ps;         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。我們的版本應該是通過改變指標來實現資料交換。 class HasPtr{     friend void swap(HasPtr& , HasPtr& ); }; inline swap(HsaPtr &lhs , HasPtr &rhs) {     using std::swap;     swap(lhs.ps , rhs.ps);     swap(lhs.i , rhs.i); } 與拷貝控制成員不同,swap並不是必要的。但是,對於分配了資源的類,定義swap可能是一種重要的優化手段。 對於swap,如果存在型別中特定的swap版本,swap呼叫會與之匹配。如果不存在,則會呼叫std中的版本(假定作用域中有using宣告) 在定義了swap的類中,通常使用swap實現賦值運算子。使用的是一種名為拷貝交換的技術。 HasPtr& HasPtr::operator = (HasPtr rhs) {     swap(*this , rhs);     return  *this;   //rhs被銷燬 } 使用拷貝和交換的賦值運算子自動就是異常安全的。且能正確處理自賦值。它保證異常安全的方法與原來的賦值運算子實現一樣。程式碼中唯一可能丟擲異常的是拷貝建構函式中的new表示式。如果真的發生了異常,它也會在我們改變左側運算物件之前發生。 每個message物件可以出現在多個Folder中。解構函式和拷貝運算都必須包含一條Message的所有Folder 中刪除它。 拷貝賦值運算子通常執行拷貝建構函式和解構函式中也要只有的工作。這種情況下,公共的工作應該放在private的工具函式中完成。 我們假定Folder類包含名為addMsg和remMsg,分別完成在給定Folder物件的結合中新增和刪除Message的工作。 //拷貝控制示例 class Message{  friend class Folder; public:     explicit Message(const std::string &str = " ") : contents(str) { }     Message(const Message&);     Message& operator = (const Message& );     ~Message();     void save(Folder&);     void remove(Folder&); private:     std::string contents;     std::set<Folder*> folders;     void add_to_Folders(const Message&);     void remove_from_Folders(); }; void Message::save(Folder &f) {     folders.insert(&f);     f.addMsg(this); } void Message::remove(Folder &f) {     folders.erase(&f);     f.remMsg(this); } void Message::add_to_Floders(const Message &m) {     for(auto f : m.folders)         f->addMsg(this); } Message::Message(const Message &m) : contents(m.constents) , foders(m.folders) {     add_to_Folders(m); } void Message::remove_from_Folders() {     for (auto f : folders)         f->remMsg(this); } Message::~Message() {     remove_from_Folders(); } Message& Message::operator = (const Message &rhs) {     //通過先刪除指標在插入它們來處理自賦值情況     remove_from_Folder();     constents = rhs.contents;     folders = rhs.folders;     add_to_Folders(rhs);     return *this; } void swap(Message &lhs, Message &rhs) {     using std::swap;     for(auto f : lhs.folders)         f->remMsg(&lhs);     for(auto f : rhs.folders)         f->remMsg(&rhs);     swap(lhs.folders,rhs.folders);     swap(lhs.contents,rhs.constents);     for( auto f : lhs.folders)         f->addMsg(&lhs);     for(auto f : rhs.folders)         f->addMsg(&rhs); }  //動態記憶體管理類 class StrVec{ public:     StrVec() : elements(nullptr) , first_free(nullptr) , cap(nullptr) { }     StrVec(const StrVec&);     StrVec &operator = (const StrVec&);     ~Strvec();     void push_back(const std::stirng& );     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 string& s) {     chk_n_alloc();     alloc.construct(first_free++,s); } pair<string*,string*> StrVec::alloc_n_copy(const string *b, const string *e) {     auto data = alloc.allocate(e-b);     return {data,uninitialized_coyp(b,e,data)}; } void StrVec::free() {     if(elements)     {         for(auto p = first_free ; p!=elements ;)             alloc.destroy(--p);         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; } 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)         alloc.construct(dest++ , std::move(*elem++));     free();     elements = newdata;     first_free = dest;     cap = elements + newcapacity; } 上述使用了避免拷貝的兩種機制,使用移動建構函式,通常是將資源從給定物件“移動”而不是拷貝到正在建立的物件。而且標準庫保證“移後源”string仍然保持一個有效的、可析構的狀態。另一種機制,使用move,如果遺漏將使用拷貝建構函式,其次,我們通常不為move提供一個using宣告,而是直接呼叫std::move; io類或unique_ptr。這些類都包含不能被共享的資源。因此,這些型別的物件不能拷貝但可移動 右值引用:必須繫結到右值的引用。通過&&來獲得右值的引用。而且只能繫結到一個將要銷燬的物件。因此,我們可以 自由地將一個右值引用的資源“移動”到另一個物件中。 一般而言,一個左值表示式表示的是一個物件的身份,而一個右值表示式表示的是物件的值。 對常規引用(左值引用)我們不能將其繫結到要求轉換的表示式、字面常量或是返回右值的表示式。右值引用有著完全相反的繫結特性:我們可以將一個右值引用繫結到這類表示式上,但不恩能夠將一個右值引用直接繫結到一個左值上: int i = 42;     int &r = i;                        //正確 int &&rr = i ;                  //錯誤 int &r2 = i * 42 ;            //錯誤 const int &r3 = i*42;    //正確,我們可以講一個const的引用繫結到一個右值上 int &&rr2 = i * 42        //正確, 返回左值引用的函式, 連同賦值、下標、解引用和前置遞增/遞減運算子,都是返回左值的表示式的列子。我們可以講一個左值引用繫結到這類表示式的結果上。 返回非引用型別的函式,連同算術、關係、位以及後置遞增/遞減運算子,都生成右值。我們不能將一個左值引用繫結到這類表示式上,但我們可以將一個const的左值引用或者一個右值引用繫結到這類表示式上。 左值有持久的狀態,而右值要麼是字面常量,要麼是在表示式求值過程中建立的臨時物件。 由於右值引用只能繫結到臨時物件,我們知道,所引用的物件將要被銷燬,該物件沒有其他使用者。這說明,使用右值引用的程式碼可以自由地接管所引用的物件的資源。 右值引用指向將被銷燬的物件。因此,我們可以從繫結到右值引用的物件“竊取”狀態。 變數是左值,因此我們不能將一個右值引用直接繫結到一個變數上,即使這個變是右值引用型別也不行。 我們可以顯式的將一個左值轉換為對應的右值引用型別,呼叫move,在標頭檔案utility中 int &&rr3  = std::move(rr1);  ///ok,把一個左值當成右值來處理。 在呼叫move之後,我們不能對移後源物件的值做任何假設。 我們可以銷燬一個移後源物件,也可以賦予它新值,但不能使用一個移後源物件的值。 使用move應該使用的是std::move而不是move。可以避免潛在的名字衝突。 移動建構函式和移動賦值運算子從給定的物件“竊取”資源而不是拷貝資源。 移動建構函式的第一個引數是該類型別的以個引用。與拷貝建構函式一樣,任何額外的引數都必須有預設實參。 除了完成資源移動,移動建構函式還必須確保移後源物件處於這樣一個狀態,銷燬它是無害的。特別,一旦資源完成移動,源物件必須不在指向被移動的資源,這些資源的所有權已經歸屬新建立的物件了 StrVec:StrVec(StrVec &&s)noexcept    :: elements(s.elements) , first_free(s.first_free) , cap(s.cap) {     s.elements = s.first_free = s.cap =nullptr; } noexcept,它通知標準庫我們的建構函式不丟擲任何異常。如果我們忘記了改變s.first_free,則銷燬移後源物件就會釋放我們剛剛移動的記憶體 由於移動資源,通常是不分配任何資源的。因此通常不會丟擲任何異常。我們應該告訴標準庫,來阻止標準庫為了處理丟擲異常的可能性而做的一些額外的工作。一種通知方法就是使用noexcept,我們必須在宣告和定義(如果定義在類外)都指定noexcept。 雖然移動操作通常不丟擲異常,但丟擲異常也是允許的。其次,標準庫容器能對異常發生時,其自身的行為的到保障。如,vector保證,如果我們呼叫push_back時發生異常,vector自身不會發生改變。 為了避免物件移動到一半發生異常導致的問題。除非vector知道元素在移動建構函式不會丟擲異常,否則在重新分配記憶體過程中他就必須使用拷貝建構函式,如果希望使用,那麼就顯示的告訴標準庫我們的移動建構函式可以安全的使用。通過標記為noexcept來做到。 移動賦值運算子執行與解構函式和移動建構函式相同的工作。 StrVec &Strvec::operator = (StrVec &&rhs) noexcept {     if (this !=&rhs)     {         free();         elements = rhs.elements;         frist_free = rhs.first_free;         cap = rhs.cap;         rhs.elements = rhs.first_free = rhs.cap = nullptr;     }     return *this; } 在移動操作之後,移後源物件必須保持有效的、可析構的狀態,但是使用者不能對其值進行任何假設; 如果一個類定義了自己的拷貝建構函式、拷貝賦值運算子或解構函式,編譯器就不會為它合成移動建構函式和移動賦值運算子。 只有當一個類沒有定義任何自己版本的拷貝控制成員,且類的每個非static資料成員都可以移動時,編輯器才會為他們合成移動建構函式或移動賦值運算子。編輯器可以移動內建型別成員。還能移動定義了對應移動操作的類。 與拷貝操作不同,移動操作永遠不會隱式定義為刪除的函式。但是,如果我們呢顯示的要求編譯器正常=default的移動操作,且編輯器不能移動所有成員,則編輯器會將移動操作定義為刪除的函式。除了一個重要的例外,遵循與合成拷貝操作類似的原則:     與拷貝建構函式不同,移動建構函式被定義為刪除的函式的條件是:有類成員定義了自己的拷貝建構函式且未定義移動建構函式,或是有成員為定義自己的拷貝建構函式且編輯器不能為其合成移動建構函式。移動賦值運算子的情況類似     如果有類成員的移動建構函式或移動賦值運算子被定義為刪除的或是不可訪問的,則類的移動建構函式或移動賦值運算子被定義為刪除的     類似拷貝建構函式,如果類的解構函式被定義為刪除的或不可訪問的,則類的移動建構函式被定義為刪除的。     類似拷貝賦值運算子,如果有類成員是const或是引用,則類的移動賦值運算子被定義為刪除的。 一個類是否定義了自己的移動操作對拷貝操作如何合成有影響。如果類定義了一個移動建構函式和/或一個移動賦值運算子,則該類的合成拷貝建構函式和拷貝賦值運算子被定義為刪除的。 換句話,定義了一個移動建構函式,或移動賦值運算子的類必須也定義自己的拷貝操作,否則,這些成員預設為刪除的。 移動右值,拷貝左值,但是如果沒有移動建構函式,右值也被拷貝。 值得注意的是,用拷貝建構函式代替移動建構函式幾乎肯定是安全的,拷貝建構函式甚至都不改變源物件的值。 class HasPtr{ public:     HasPtr(HasPtr &&p)noexcept : ps(p.ps),i(p.i) { p.ps = 0;}     HasPtr& operator = (HasPtr rhs) { swap (*this , rhs); return *this; }     //其他成員 }; 不管使用的是拷貝建構函式還是移動建構函式,賦值運算子的函式體都swap兩個運算物件的狀態。 移動操作的例子: void Message::move_Folder(Message *m) {     folders = std::move(m->folders);     for (auto f : folders)     {         f->remMsg(m);         f->addMsg(this);     }     m->folders.clear(); } 通過move,我們使用了set的移動賦值運算子。值得注意的是,向set插入元素可能會丟擲一個異常,bad_alloc。因此我們沒有將他們標記成noexcept。 Message :: Message(Message &&m) {     move_Folders(&m); } Message& Message::operator = (Message &&rhs) {     if(this != &rhs)     {         remove_from_Folders();         contents = std::move(rhs.contents);         move_Foder(&rhs);     }     return *this; } 一般來說一個迭代器解引用返回的是一個指向元素的左值。與其他迭代器不同,移動迭代器的解引用生成的是一個右值引用。我們通過標準庫的make_move_iterator函式將一個普通迭代器轉換為一個移動迭代器。 void StrVec::reallocate() {     auto newcapacity = size( )? 2*size() :1 ;     auto first = alloc.allocate(newcapacity);     auto last = uninitialized_copy(make_move_iterator(begin()),make_move_iterator(end()),first);     free( );     elements = first;     first_free = last;     cap = elements + newcapacity; } uninitialized_copy對輸入序列中的每個元素呼叫construct來將元素“拷貝”到目的位置。由於我們傳遞的是移動迭代器,因此解引用運算子生成的是一個右值引用,這意味著construct將使用移動建構函式來構造元素。 值得注意的是,標準庫不保證那些演算法使用移動迭代器,哪些不適用,由於移動一個物件可能銷燬掉原物件,因此你只有在確信演算法在為一個元素賦值或將其傳遞給一個使用者定義的函式後不再訪問它時,才能將移動迭代器傳遞給演算法。 不要隨意適用move,由於一個移後源物件具有不確定的狀態,對其呼叫std::move是危險的,當我們呼叫move時,必須確定移後源物件麼有其他使用者。 在移動建構函式和移動賦值運算子這些類實現程式碼之外的地方,只有當你確信需要進行移動操作且移動操作是安全的,才可以使用std::move 對於一般的成員函式也可以只用於建構函式賦值函式相同的移動版,有兩個版本,一個版本接受一個指向const的左值引用,第二個版本接受一個指向非const的右值引用。通常我們不會定義const X&& 和一個X&引數的版本。 class StrVec{ public:     void push_back(const std::string& );     void push_back(std::string&& ); }; void StrVec::push_back(const string& s) {     chk_n_alloc();     alloc.construct(first_free++, s); } void StrVec::push_back(string &&s) {     chk_n_alloc();     alloc.construct(first_free++, std::move(s));//使用的是string的移動建構函式來構造元素 } 在舊版本,可以向右值賦值:s2+s1 =  "wow!";   無法阻止 為了向後相容新標準仍然支援,但是我們可能洗完再自己的類中阻止這種用法。 我們通過在引數列表後放置一個引用限定符,來指出this的左值/右值屬性的方式。宣告和定義中都要指出 引用限定符可以是&或&&,分別指出this可以指向一個左值或右值。 在有const的情