天天看點

第13章 拷貝控制【C++】

第13章 拷貝控制

從此章我們即将開始第三部分的學習,之前我們已經學過了兩個部分,C++基礎和C++标準庫,第三部分為類設計者的工具

也就是我們即将開始傳說中的對象對象程式設計之旅,面向對象程式設計(Object Oriented Programming)

本章進行學習類如何操控該類型的拷貝,指派,移動或者銷毀,有:拷貝構造函數、移動構造函數、拷貝指派運算符、移動指派運算符以及析構函數等重要知識

拷貝構造函數

定義:如果一個構造函數的第一個參數是自身類類型的引用,且任何額外參數都有預設值,則此構造函數是構造拷貝函數

簡單上手

//example1.cpp
class Person
{
public:
    int age;
    Person() = default;
    Person(int age) : age(age) {}
    Person(const Person &person)
    {
        //内容拷貝
        this->age = person.age;
    }
};

int main(int argc, char **argv)
{
    Person person1(19);
    Person person2 = person1;
    cout << person2.age << endl; // 19
    return 0;
}      

合成拷貝構造函數

預設情況下,編譯器會定義一個拷貝構造函數,即使在我們提供拷貝構造函數的情況下也仍會自動生成,預設情況下會将每個非static成員拷貝到正在建立的對象中

//example2.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(const Person &);
    Person(const int age, const string name) : age(age), name(name)
    {
    }
};

//直接使用構造函數初始化清單
//此定義與預設合成拷貝函數相同
Person::Person(const Person &person) : age(person.age), name(person.name)
{
}

int main(int argc, char **argv)
{
    Person me(19, "gaowanlu");
    Person other = me;
    // 19 gaowanlu
    cout << other.age << " " << other.name << endl;
    return 0;
}      

嘗試測試一下編譯器預設提供的合成拷貝構造函數,可見存在預設合成拷貝構造函數

如果不想讓一個構造函數具有可以指派轉換的功能,則将其定義為explicit的

//example3.cpp
class Person
{
public:
    string name;
    int age;
    Person(const int age, const string name) : name(name), age(age) {}
};

int main(int argc, char **argv)
{
    Person me(19, "gaowanlu");
    Person other = me;
    // 19 gaowanlu
    cout << other.age << " " << other.name << endl;
    return 0;
}      

重載指派運算符

重載​

​operator=​

​方法進行自定義指派運算符使用時要做的事情

//example4.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(int age, string name) : age(age), name(name) {}
    Person &operator=(const Person &);
};

Person &Person::operator=(const Person &person)
{
    cout << "operator =" << endl;
    this->age = person.age;
    this->name = person.name;
    return *this;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2;
    person2 = person1;                                  // operator =
    cout << person2.age << " " << person2.name << endl; // 19 me
    return 0;
}      

合成拷貝指派運算符

與合成拷貝構造函數類似,如果沒有自定義拷貝指派運算符,編譯器會自動生成

//example5.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(int age, string name) : age(age), name(name) {}
};

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2;
    person2 = person1;                                  //使用預設合成拷貝指派運算符
    cout << person2.age << " " << person2.name << endl; // 19 me
    return 0;
}      

析構函數

析構函數與構造函數不同,構造函數初始化對象的非static資料成員,還可能做一些在對象建立時需要做的事情。析構函數通常釋放對象的資源,并銷毀對象的非static資料成員

​~TypeName();​

​析構函數沒有傳回值,沒有接收參數,是以其沒有重載形式

在構造函數中,初始化部分執行在函數體執行前,析構函數則是首先執行函數體,然後按照初始化順序的逆序銷毀。

構造函數被調用的時機

  • 變量在離開其作用域時被銷毀
  • 當一個對象被銷毀時,其成員被銷毀
  • 容器(無論标準容器還是數組)被銷毀時,其元素被銷毀
  • 動态記憶體配置設定,當對它的指針使用delete時被銷毀
  • 對于臨時對象,當建立它的完整表達式結束時被銷毀
//example6.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(int age, string name) : age(age), name(name) {}
    ~Person()
    {
        cout << "~Person" << endl;
    }
};

Person func(Person person)
{
    return person;
}

int main(int argc, char **argv)
{
    Person person(19, "me");
    Person person1 = func(person);
    //~Person被列印三次
    //首先将person拷貝給func的形參,然後形參person作為傳回值指派給person1
    //然後func傳回值person被銷毀
    //随着main執行完畢,main内的兩個Person被銷毀
    return 0;
}      

合成析構函數

當為自定義析構函數時,編譯器會自動提供一個合成析構函數,對于某些類作用為阻止該類型的對象被銷毀,如果不是則函數體為空

//example7.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(int age, string name) : age(age), name(name) {}
    ~Person() {} //等價于合成析構函數
};

int main(int argc, char **argv)
{
    Person person(19, "gaowanlu");
    cout << person.age << " " << person.name << endl; // 19 gaowanlu
    return 0;
}      

在合成析構函數體執行完畢之後,成員會被自動銷毀,對象中的string被銷毀時,将會調用string的析構函數,将name的記憶體釋放掉,​

​析構函數自身并不直接銷毀成員​

​,是在析構函數體之後隐含的析構階段中被銷毀的,整個銷毀過程,析構函數體是作為成員銷毀步驟之外的并一部分而進行的

如果對象的内部有普通指針記錄new動态記憶體,在對象析構過程預設隻進行指針變量指針本身的釋放,而不對申請的記憶體進行釋放,則就需要動态記憶體章節學習的在析構函數體内手動釋放他們,或者使用智能指針,随着智能指針的析構被執行,動态記憶體會被釋放

三/五法則

有三個基本操作可控制類的拷貝操作:拷貝構造函數、拷貝指派運算符、析構函數。在新标準下還可以通過定義一個移動構造函數、一個移動指派運算符

我們發現有時指派運算符與拷貝構造函數會執行相同的功能,通常情況下并不要求定義所有這些操作

使用合成拷貝函數和合成拷貝指派運算符時可能遇見的問題

//example8.cpp
class Person
{
public:
    int age;
    string *name;
    Person(const string &name = string()) : name(new string(name)), age(0) {}
    ~Person()
    {
        delete name;
    }
};

int main(int argc, char **argv)
{
    {
        Person person1("me");
        Person person2 = person1; //使用合成拷貝構造函數
        //此時的person1.name與person2.name指向相同的記憶體位址
        *person1.name = "he";
        cout << *person2.name << endl; // he
    }
    cout << "end" << endl; // end
    return 0;
}      

在合成拷貝構造函數和合成拷貝指派運算符,其中的拷貝操作都是簡單的指針位址指派,而不是重新開辟空間,再将原先的name指派到新的記憶體空間

使用=default

使用​

​=default​

​可以顯式要求編譯器生成合成拷貝構造函數和拷貝指派運算符

//example9.cpp
class Person
{
public:
    Person() = default;                //合成預設構造函數
    Person(const Person &) = default;  //合成拷貝構造函數
    Person &operator=(const Person &); //合成拷貝指派運算
    ~Person() = default;               //合成析構函數
};

//預設在類内使用=default的成員函數為内聯的
//如果不希望是内聯函數則應在類外部定義使用=default
Person &Person::operator=(const Person &person) = default;

int main(int argc, char **argv)
{
    Person person1;
    Person person2 = person1;
    cout << "end" << endl; // endl
    return 0;
}      

=delete阻止拷貝

​使用=delete​

​定義删除的函數

//example10.cpp
class Person
{
public:
    Person() = default;
    Person(const Person &) = delete;            //禁止拷貝構造函數
    Person &operator=(const Person &) = delete; //阻止拷貝指派
    ~Person() = default;
};

int main(int argc, char **argv)
{
    Person person1;
    // Person person2 = person1;//錯誤 不允許拷貝複制指派
    return 0;
}      

​析構函數不能是删除的成員​

​,否則就不能銷毀此類型,沒有析構函數的類型可以使用動态配置設定方式建立,但是不能被銷毀

//example11.cpp
class Person
{
public:
    int age;
    string name;
    Person(const int age, const string name) : age(age), name(name) {}
    ~Person() = delete;
};

int main(int argc, char **argv)
{
    Person *person = new Person(19, "me");
    // delete person;//錯誤 Person沒有析構函數
    return 0;
}      

編譯器将成員處理為删除的

對于某些情況,編譯器會将合成的成員定義為删除的函數

重點:如果一個類有資料成員不能預設構造、拷貝、複制、銷毀,則對應的成員函數将被定義為删除的

private拷貝控制

在新标準之前沒有,删除的成員,類是通過将其拷貝構造函數和拷貝指派運算符聲明為private的來阻止拷貝的

//example12.cpp
class Person
{
private:
    Person(const Person &person);
    Person &operator=(const Person &person);

public:
    int age;
    string name;
    Person(const int age, const string name) : age(age), name(name) {}
    ~Person() = default;
    Person() = default;
    void test();
};

Person::Person(const Person &person)
{
}
Person &Person::operator=(const Person &person)
{
    return *this;
}

void Person::test()
{
    Person *person = new Person(19, "me");
    Person person1 = *person; //函數成員或者友元函數可以使用
    delete person;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    // Person person2 = person1;
    // error: 'Person::Person(const Person&)' is private within this context
    person1.test();
    return 0;
}      

這種雖然類的外部不能使用拷貝構造和拷貝指派,但是類的友元和成員函數仍可使用二者,同時想要阻止友元函數或者成員函數的使用,則隻聲明private成員即可不進行定義

//example13.cpp
class Person
{
private:
    Person(const Person &person);            //隻聲明不定義
    Person &operator=(const Person &person); //隻聲明不定義

public:
    int age;
    string name;
    Person(const int age, const string name) : age(age), name(name) {}
    ~Person() = default;
    Person() = default;
    void test();
};

int main(int argc, char **argv)
{
    Person person1(19, "me");
    // Person person2 = person1;
    //  error: 'Person::Person(const Person&)' is private within this context
    // 如果函數成員或友元函數使用拷貝構造或者指派 也會報錯
    return 0;
}      

總之優先使用=delete這種新的規範,delete是從編譯階段直接解決問題

行為像值的類

有些類拷貝是值操作,是一份相同得副本

//example14.cpp
class Person
{
public:
    int *age;
    string *name;
    Person(const int &age, const string &name) : age(new int(age)), name(new string(name)) {}
    Person() : age(new int(0)), name(new string("")) {}
    Person &operator=(const Person &person);
    ~Person()
    {
        delete age, delete name;
    }
};

Person &Person::operator=(const Person &person)
{
    *age = *person.age;
    *name = *person.name;
    return *this;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2(20, "she");
    person1 = person2;
    cout << *person1.age << " " << *person1.name << endl; // 20 she
    cout << *person2.age << " " << *person2.name << endl; // 20 she
    *person1.name = "gaowanlu";
    cout << *person1.age << " " << *person1.name << endl; // 20 gaowanlu
    cout << *person2.age << " " << *person2.name << endl; // 20 she
    //可見之間此類對象像一種值類型
    return 0;
}      

行為像指針的類

有些類拷貝是指針指向的操作,也就是不同的類的成員會使用相同的記憶體

先來看一種簡單使用的情況

//example15.cpp
class Person
{
public:
    int *age;
    string *name;
    Person() : age(new int(0)), name(new string) {}
    Person(const int &age, const string &name) : age(new int(age)), name(new string(name)) {}
    Person &operator=(const Person &person);
};

Person &Person::operator=(const Person &person)
{
    if (age)
        delete age;
    if (name)
        delete name;
    age = person.age;
    name = person.name;
    return *this;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2 = person1;
    // person1 person2的内容的記憶體是相同的
    *person2.age = 20;
    *person2.name = "gaowanlu";
    cout << *person1.age << " " << *person1.name << endl; // 20 gaowanlu
    cout << *person2.age << " " << *person2.name << endl; // 20 gaowanlu
    return 0;
}      

實作引用計數

有意思的例子是我們也可以設計引用計數的機制,通過下面這個例子可以學到很多的程式設計思想

//example16.cpp
class Person
{
public:
    string *name;
    int *age;
    Person(const int &age = int(0), const string &name = string("")) : use(new size_t(1)), age(new int(age)), name(new string(name)) {}
    //拷貝構造時
    Person(const Person &person)
    {
        name = person.name;
        age = person.age;
        use = person.use;
        ++*use; //引用數加一 不能寫 *use++哦 因為那是*(use++)
    }
    //指派拷貝時
    Person &operator=(const Person &person);
    //析構時
    ~Person();
    size_t *use; //引用計數器
};

//拷貝指派
Person &Person::operator=(const Person &person)
{
    //遞增右邊對象的引用系數
    ++*person.use;
    //遞減本對象引用計數
    --*use;
    if (*use == 0)
    {
        delete age, delete name, delete use;
    }
    age = person.age;
    name = person.name;
    use = person.use;
    return *this;
}

//析構
Person::~Person()
{
    //将引用數減1
    --*use;
    //判斷引用數是否為0
    if (*use == 0)
    {
        delete age, delete name, delete use;
    }
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    cout << *person1.use << endl; // 1
    {
        Person person2(person1);
        cout << *person1.use << endl; // 2
        Person *ptr = new Person(person2);
        cout << *ptr->use << endl; // 3
        delete ptr;
        cout << *person1.use << endl; // 2
    }
    cout << *person1.use << endl; // 1
    //最後當person1銷毀時 析構函數内引用計數變為0 随之将記憶體釋放 達到記憶體管理的效果
    return 0;
}      

編寫swap函數

可以在類上定義一個自己的swap函數重載swap預設行為

//example17.cpp
class Person
{
    //聲明為友元函數可通路類私有成員
    friend void swap(Person &a, Person &b);

public:
    int age;
    string name;
    Person(const int &age, const string &name) : age(age), name(name) {}
};

//定義函數 void swap(Person &a, Person &b);
inline void swap(Person &a, Person &b)
{
    std::swap(a.age, b.age);
    std::swap(a.name, b.name);
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2(19, "she");
    swap(person1, person2);
    cout << person1.age << " " << person1.name << endl; // 19 she
    cout << person2.age << " " << person2.name << endl; // 19 me
    return 0;
}      

拷貝指派運算中使用swap

類的swap通常用來定義它們的指派運算符,是一種拷貝并交換的技術

//example18.cpp
class Person
{
    friend void swap(Person &a, Person &b);

public:
    int age;
    string name;
    Person &operator=(Person person);
    Person(const int &age, const string &name) : age(age), name(name) {}
};

// person為使用合成拷貝構造函數值複制
Person &Person::operator=(Person person)
{
    swap(*this, person); //二者内容交換
    return *this;
}

// Person的swap行為
inline void swap(Person &a, Person &b)
{
    a.age = b.age;
    a.name = b.name;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2 = person1;
    cout << person2.age << " " << person2.name << endl; // 19 me
    return 0;
}      

對象移動

什麼是對象移動,也就是将對象移動到某處,即複制,但複制後就将原來的進行對象銷毀了

标準庫函數 ​

​std::move​

​,标準庫容器、string、shared_ptr類即支援移動也支援拷貝,IO類和unique_ptr類可以移動但不能拷貝

//example19.cpp
#include <iostream>
#include <utility>
#include <string>
int main(int argc, char **argv)
{
    using namespace std;
    string a1 = "hello";
    string a2 = std::move(a1);
    cout << a1 << endl; // nothing
    cout << a2 << endl; // hello

    int b1 = 999;
    int b2 = std::move(b1); // int不是對象是基本資料類型不适用
    cout << b1 << endl;     // 999
    cout << b2 << endl;     // 999
    return 0;
}      

右值引用

什麼是右值引用,右值引用為支援移動操作而生,右值引用就是必須綁定到右值的引用,使用&&而不是&來獲得右值引用

左值與右值的聲明周期,左值有持久的狀态直到變量聲明到上下文切換記憶體釋放,右值要麼是字面量或者求值過程中的臨時對象

右值引用特性:

  • 所引用的對象将要被銷毀
  • 該對象沒有其他使用者
  • 無法将右值引用綁定到右值引用
//example20.cpp
int main(int argc, char **argv)
{
    int num = 666;
    int &ref = num; //引用
    // int &&refref = num; //錯誤:不能将右值引用綁定到左值上
    // int &ref1 = num * 42; //錯誤:i*42為右值
    const int &ref2 = num * 42; // const引用可綁定到右值上
    int &&refref1 = num * 10;   // 右值引用可以綁定在右值上
    cout << refref1 << endl;    // 6660
    refref1 = 999;
    cout << refref1 << endl; // 999 而且與使用普通變量沒什麼差別

    // int &&refref2 = refref1; //錯誤:無法将右值引用綁定到左值
    return 0;
}      

move與右值引用

雖然不能将右值引用綁定在左值上,但可以通過std::move來實作

//example21.cpp
int main(int argc, char **argv)
{
    int num = 999;
    // int &&rr1 = num; //錯誤 無法将右值引用綁定到左值
    string stra = "hello";
    string &&straRef = std::move(stra);
    cout << stra << endl;    // hello
    cout << straRef << endl; // hello

    stra = "world";
    cout << straRef << endl; // world
    //可見straRef綁定定在了stra上

    string a = "world";
    string b = std::move(a); 
    // move函數的表現根據等号左側的類型的不同随之行為也不同
    cout << a << endl;       // nothing
    cout << b << endl;       // world

    return 0;
}      

右值引用做參數

右值引用的最大貢獻就是将臨時變量的聲明周期延長,減少臨時變量的頻繁銷毀,記憶體的利用效率也會變高,當右值表達式被處理後結果存放在會塊臨時記憶體空間,右值引用指向它,則可以利用,直到指向它的右值引用全部被銷毀,記憶體才會被釋放

//example22.cpp
class Person
{
public:
    string name;
    Person(const string &name) : name(name)
    {
        cout << "string &name" << endl;
    }
    Person(string &&name) : name(name)
    {
        cout << "string &&name" << endl;
    }
};
// const引用與右值引用重載時 傳遞右值時 右值引用的優先級高

int main(int argc, char **argv)
{
    //建立臨時變量"hello"
    Person person1("hello"); // string&&name
    
    string s = "world";
    Person person2(s); // string &name
    return 0;
}      

右值和左值引用成員函數

在舊标準中,右值可以調用相關成員函數與被指派

//example23.cpp
int main(int argc, char **argv)
{
    string hello = "hello";
    string world = "world";
    cout << (hello + world = "nice") << endl; // nice 右值被指派
    return 0;
}      

&左值限定符

怎樣限定指派時右邊隻能是可修改的左值指派,引入了引用限定符(reference qualifier),使得方法隻有對象為左值時才能被使用

//example24.cpp
#define USE_LIMIT

class Person
{
public:
    string name;
#ifdef USE_LIMIT
    Person &operator=(const string &) &; //引用限定符 等号左側必須為可修改的左值
#else
    Person &operator=(const string &);
#endif
    Person(string &&name) : name(name)
    {
    }
    inline void print()
    {
        cout << this->name << endl;
    }
};

#ifdef USE_LIMIT
Person &Person::operator=(const string &name) & //引用限定
#else
Person &Person::operator=(const string &name)
#endif
{
    this->name = name;
    return *this;
}

Person func()
{
    return Person("me");
}

int main(int argc, char **argv)
{
    func() = "hello"; // func()傳回右值
    //當define USE_LIMIT時發生錯誤
    //沒有define USE_LIMIT時不會發生錯誤
    return 0;
}      

const與&左值限定符

一個方法可以同時用const和引用限定,引用限定必須在const之後

//example25.cpp
class Person
{
public:
    int age;
    string name;
    Person(const int &age = 19, const string &name = "me") : age(age), name(name)
    {
    }
    void print() const &
    {
        cout << age << " " << name << endl;
    }
};

//func傳回右值
Person func()
{
    return Person(19, "she");
}

int main(int argc, char **argv)
{
    func().print();
    //當print不是const&時報錯,例如隻有引用限定符&,隻有const不報錯
    //很雞肋沒什麼卵用
    //當有const時 &限定作用消失了
    return 0;
}      

&&右值引用限定符

可以使用​

​&&​

​​進行方法重載,使其為可改變的右值服務

當一個方法名字相同 函數參數清單相同時 有一個有引用限定,全部都應該有引用限定或者全部都沒有

//example26.cpp
class Foo
{
public:
    Foo sort() &&;
    Foo sort() const &;
    //當一個方法名字相同 函數參數清單相同時 有一個有引用限定
    //全部都應該有引用限定或者全部都沒有
};

Foo Foo::sort() &&
{
    cout << "&&" << endl;
    return *this;
}

Foo Foo::sort() const &
{
    cout << "const &" << endl;
    return *this;
}

// func傳回右值
Foo func()
{
    return Foo();
}

int main(int argc, char **argv)
{
    Foo foo1;
    foo1.sort();   // const &
    func().sort(); //&&
    //如果沒有定義Foo Foo::sort() && 二者都會調用 Foo Foo::sort() const &
    return 0;
}      

移動構造函數和移動指派運算符

資源移動執行個體,嫖竊其他對象的記憶體資源,與拷貝構造函數類似,移動構造函數第一個參數也是引用類型,但隻不過是右值引用,任何額外參數必須有預設實參

//example27.cpp
class Person
{
public:
    int *age;
    string *name;
    Person(const int &age, const string &name) : age(new int(age)), name(new string(name))
    {
    }
    //移動操作不應抛出任何異常
    Person(Person &&person) noexcept //”盜竊“資源 這是個移動構造函數不是 拷貝構造函數
    {
        delete age, delete name;
        age = person.age;
        name = person.name;
        person.age = nullptr;
        person.name = nullptr;
    }
    void print();
};

void Person::print()
{
    if (age && name)
    {
        cout << *age << " " << *name;
    }
    cout << endl;
}

int main(int argc, char **argv)
{
    Person person1(19, "me");
    Person person2 = std::move(person1);
    person1.print(); // nothing
    person2.print(); // 19 me
    return 0;
}      

移動指派運算符

與拷貝類似,可以也可以重載指派運算符來實作對象的移動功能

//example28.cpp
class Person
{
public:
    int *age;
    string *name;
    Person(const int &age, const string &name) : age(new int(age)), name(new string(name))
    {
        cout << "Person(const int &age, const string &name)" << endl;
    }
    Person &operator=(Person &&person) noexcept;//移動指派運算符
    Person(const Person &person) : age(person.age), name(person.name)
    {
        cout << "Person(const Person &person)" << endl;
    }
    void print();
};

Person &Person::operator=(Person &&person) noexcept
{
    cout << "Person &Person::operator=(Person &&person)" << endl;
    if (&person != this)
    {
        delete age, delete name;
        age = person.age;
        name = person.name;
        person.age = nullptr;
        person.name = nullptr;
    }
    return *this;
}

void Person::print()
{
    if (age && name)
    {
        cout << *age << " " << *name;
    }
    cout << endl;
}

//傳回右值
Person func()
{
    return Person(19, "she"); // Person(const int &age, const string &name) 2
}

int main(int argc, char **argv)
{
    Person person2(18, "oop"); // Person(const int &age, const string &name) 1
    person2 = func();          // Person &Person::operator=(Person &&person)
    person2.print();           // 19 she
    
    Person person1 = std::move(person2);//person2移動到person1
    cout << *person1.age << " " << *person2.name << endl; // 19 she
    return 0;
}      

合成的移動操作

隻有當一個類沒有定義任何自己版本的拷貝控制成員時,且所有資料成員都能進行移動構造或移動指派時,編譯器才會合成移動構造函數或移動指派運算符

當一定義了拷貝控制成員,如自定義了拷貝構造拷貝拷貝指派時,将不會提供合成的移動操作

//example29.cpp
class X
{
public:
    int i;    //内置類型可以移動
    string s; // string定義了自己的移動操作
};

class HasX
{
public:
    X member; // X有合成的移動操作
};

int main(int argc, char **argv)
{
    X x1;
    x1.i = 100;
    x1.s = "me";
    cout << x1.i << " " << x1.s << endl; // 100 me
    // X移動
    X x2 = std::move(x1);
    cout << x2.i << " " << x2.s << endl; // 100me
    cout << x1.i << " " << x1.s << endl; // 100 nothing
    // HasX移動
    HasX hasx;
    hasx.member.i = 99;
    hasx.member.s = "me";
    HasX hasx1 = std::move(hasx);
    cout << hasx1.member.i << " " << hasx1.member.s << endl; // 99 me
    return 0;
}      

本質上move的使用就是調用了拷貝構造函數,但拷貝構造是值拷貝還是指針拷貝有我們自己定義

//example30.cpp
class Y
{
public:
    int age;
    Y() = default;
    Y(const Y &y) //拷貝構造 則 Y沒有合成的移動操作
    {
        this->age = age;
    };
    // Y(Y &&y)
    // {
    //     age = y.age;
    //     y.age = 0;
    // }
};

class HasY
{
public:
    HasY() = default;
    Y member;
};

int main(int argc, char **argv)
{
    HasY hasy;
    hasy.member.age = 999;
    HasY hasy1 = std::move(hasy);     //因為Y沒有移動操作
    cout << hasy1.member.age << endl; //亂碼 hasy為一個新對象,為Y添加自定義移動構造函數則輸出999
    cout << "end" << endl;            // end
    return 0;
}      

拷貝構造與移動構造的比對

當一個類既有移動構造函數,也有拷貝構造函數,當我們使用哪一個,會根據函數比對規則來确定

//example31.cpp
class Person
{
public:
    Person() = default;
    Person(const Person &person)
    {
        cout << "Person(const Person &person)" << endl;
    }
    Person(Person &&person)
    {
        cout << "Person(Person &&person)" << endl;
    }
};

int main(int argc, char **argv)
{
    Person person1;
    Person person2(person1); // Person(const Person &person)

    const Person person3;
    Person person4(person3); // Person(const Person &person)

    //而移動構造隻接受右值
    Person person5 = std::move(person4); // Person(Person &&person)
    
    return 0;
}      

要注意的是,當有拷貝構造函數沒有移動構造函數時,右值也将被拷貝

//example32.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(const Person &person) : age(person.age), name(person.name)
    {
        cout << " Person(const Person &person)" << endl;
    }
};

int main(int argc, char **argv)
{
    Person person1;
    // std::move的作用就是将person1作為右值傳遞
    Person person2(std::move(person1)); // Person(const Person &person)
    //當存在移動構造時則會優先使用移動構造
    return 0;
}      

拷貝并交換指派運算符和移動操作

當定義了移動構造函數,且定義了指派運算符,但無定義移動指派方法,則将一個右值賦給左值時,将會先使用移動構造函數構造新對象,然後将新對象指派給原左值,類似地隐含了移動指派

//example33.cpp
class Person
{
public:
    int age;
    string name;
    Person() = default;
    Person(const int &age, const string &name) : age(age), name(name){};
    //移動構造函數
    Person(Person &&p) noexcept : age(p.age), name(p.name)
    {
        cout << "Person(Person &&p)" << endl;
        p.age = 0;
        p.name = "";
    }
    //拷貝構造
    Person(const Person &p) : age(p.age), name(p.name)
    {
        cout << "Person(const Person &p)" << endl;
    }
    //指派運算符 也是 移動指派運算符 也是拷貝指派運算符
    Person &operator=(Person p)
    {
        cout << "Person &operator=(Person p)" << endl;
        age = p.age;
        name = p.name;
        return *this;
    }
};

int main(int argc, char **argv)
{
    Person person1(19, "me"); //構造函數
    //顯式調用移動構造函數
    Person person2(std::move(person1));                 // Person(Person &&p)
    cout << person1.age << " " << person1.name << endl; // 0 nothing

    Person person3(19, "me");     //構造函數
    Person person4;               //預設構造函數
    person4 = std::move(person3); //先使用移動構造函數生成新對象 将新對象指派給person4
    // Person(Person &&p) Person &operator=(Person p)
    cout << person4.age << " " << person4.name << endl; // 19 me
    return 0;
}      

移動疊代器

移動疊代器解引用傳回一個指向元素的右值引用

//example34.cpp
int main(int argc, char **argv)
{
    vector<string> vec = {"aaa", "bbb"};
    auto iter = make_move_iterator(vec.begin());
    // auto std::move_iterator<std::vector<std::string>::iterator>
    allocator<string> allocat;
    string *ptr = allocat.allocate(10);
    uninitialized_copy(make_move_iterator(vec.begin()), make_move_iterator(vec.end()), ptr);
    cout << vec[0] << " " << vec[1] << endl; //空字元串
    cout << ptr[0] << " " << ptr[1] << endl; // aaa bbb
    //可見使用移動疊代器進行了移動操作
    return 0;
}      

繼續閱讀