Way to C++: 1. Copy Constructor

·

2 min read

前言

這是這幾天的c++學習歷程的的整理,主要在於如何理解 c++11 的資源管理。

這是極為重要的,在 c ,資源的釋放都需要小心翼翼地寫:

Obj ReadObjFromFile(const char* filename) {  
    FILE* fp = fopen(filename, "r"); //簡化,沒有判斷 fopen 是否成功  
    Obj obj = ReadObjFromFp(fp);  
    fclose(fp);  
    return obj;  
}

return 的地方都必須釋放之前處理的資源。這樣的手動管理資源算是 c 容易被討厭的一點。

而在 c++ 裡面,利用了 class 的 destructor 會在物件被銷毀時呼叫,我們可以做到這件事情:

Obj ReadObjFromFile(const char* filename) {  
    File fp(filename);   
    return ReadObj(fp); // 先忽略 fp 複製問題  
}

在離開 ReadObjFromFile 時, fp 自然銷毀。

真的,那麼好寫?

事實上,我有很長一段時間都以為做到這件事情只需要 constructor 和 destructor,完全沒有注意到另外四個函數:

  • copy constructor

  • copy assignment

  • move constructor (c++11 開始有)

  • move assignment (c++11 開始有)

簡單來說,在一個 class 成員沒有指標時,這些基本上不太需要去注意,因為編譯器會自動生成合適的函數(一些人稱做 swallow copy,不過應該不太合適,因為假如 class 成員有一個好好定義的 class ,像是 std::vector,那 vector 裡面的東西也會按照 copy 的語意進行複製之類)。但是在有指標時,就會出問題。

舉個例子,以一個**簡單(?)**的 integer buffer 為例子,只寫了 constructor 和 destructor:

#include <cstdio>

class IntBuff {
public:
    IntBuff() = default;
    explicit IntBuff(size_t sz) : sz(sz) {
        if (sz) {
            arr = new int[sz];
        }
    };

    ~IntBuff() {
        printf("%p\n", arr);
        delete[] arr;
    }

private:
    int* arr = nullptr;
    size_t sz = 0;
};

int main() {
    {
        IntBuff a1(size_t(10)), b1;
        b1 = a1;
    }
    return 0;
}

可以發現,只要簡單的 b1 = a1 ,便複製了指標,並且讓指標被刪除兩次。但複製行為應該要是複製弄一個新的指標,並且把來源指標的內容全部複製過去。

甚麼是複製,怎麼處理?

在不考慮 r-value reference 的情況下,複製可以在初始化時或指定時觸發:

Object obj2("a");  
Object obj1(obj2); // 初始化時給另外一個 obj  
obj1 = obj2; // 或者指定

要讓這個行為在 IntBuff 上是好的,就需要好好定義這兩個函數:

IntBuff::IntBuff(const IntBuff& ib); // copy constructor  
IntBuff& IntBuff::operator=(const IntBuff& ib); // copy assignment
#include <algorithm>
#include <cstdio>
#include <memory>

class IntBuff {
public:
    IntBuff() = default;
    explicit IntBuff(size_t sz) : sz(sz) {
        if (sz) {
            arr = new int[sz];
        }
    };

    IntBuff(const IntBuff& ib) {
        sz = ib.sz;
        arr = new int[sz];
        for (int lx = 0; lx < sz; lx++) {
            arr[lx] = ib.arr[lx];
        }
    }

    IntBuff& operator=(const IntBuff& ib) {
        IntBuff tmp(ib);  // copy-and-swap
        std::swap(tmp.arr, arr);
        std::swap(tmp.sz, sz);
        return *this;
    }

    ~IntBuff() {
        printf("%p\n", arr);
        delete[] arr;
    }

private:
    int* arr = nullptr;
    size_t sz = 0;
};

int main() {
    {
        IntBuff a1(size_t(10)), b1;
        b1 = a1;
    }
    return 0;
}

這樣就好了?

不是,這篇也只講到 copy 這個 c++11之前就有的東西。而假如只有 copy 這個行為,會一個問題:就是效能,而這會需要用到 move 來解決。

然後是 copy-and-swap,眼尖的人會發現,在 copy assignment 函數,先觸發了 copy constructor ,然後再交換成員,比較完整的原因會在之後解釋。