《C++Primer》學習筆記(13、拷貝控制)

拷貝構造函數:

如果一個構造函數的第一個參數是:自身類類型的引用,(且任何額外參數都有默認值)。

沒有拷貝構造函數,或者定義了其他構造函數,則編譯器,合成拷貝構造函數。

可使用合成拷貝構造函數,阻止拷貝某類類型的對象。

在合成拷貝構造函數中,類中每個成員的類型決定了它如何拷貝,該調用拷貝構造函數就調用。

直接初始化:要求編譯器使用普通的函數匹配。

拷貝初始化:要求編譯器,將右側運算對象,拷貝到正在創建的對象中。

可依靠:拷貝或移動構造函數來完成。

發生情況:

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將使用,移動構造函數來構造元素。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章