析构函数、复制与移动


在〈类模版〉中的LinkedList范例,每个元素都由内部类Node实例保存,而Node是以new动态配置,若不再使用LinkedList实例,应该清除这些new出来的Node实例,这可以借由定义解析式(destructor)来实现,例如:

#include <iostream>
using namespace std;

template <typename T>
class LinkedList {
    class Node {
    public:
        Node(T value, Node *next) : value(value), next(next) {}
        T value;
        Node *next;
    };

    Node *first = nullptr;

public:
    ~LinkedList(); // 析构函数
    ...略
};

...略

template <typename T>
LinkedList<T>::~LinkedList() {
    if(this->first == nullptr) {
        return;
    }

    Node *last = this->first;
    do {
        Node *next = last->next;
        delete last;
        last = next;
    } while(last != nullptr);
}

...略

解析式是由~开头,不用指定返回类型,与类命名空间的成员函数,当实例被清除时,就会执行析构函数,可以在析构函数中实现清除资源的动作,在这边用来delete每个new出来的Node实例。

如果没有定义析构函数,那么编译器会自行创建一个本体为空的析构函数。

如果LinkedList实例被构造出来之后,不会被用来构造另一个LinkedList实例,那么以上的实现是不会有什么问题,然而若是如下就会出问题:

...略

int main() {
    LinkedList<int> *lt1 = new LinkedList<int>();
    (*lt1).append(1).append(2).append(3);

    LinkedList<int> lt2 = *lt1;   // 复制初始化化

    delete lt1;

    cout << lt2.get(2) << endl;   // 不可预期的结果

    return 0;
}

若使用一个类实例来构造另一类实例,默认会发生值域的复制,复制的行为视类型而定,以指针类型来说,会是复制地址,也就是浅复制(shallow copy),就上例来说,*lt1实例的first地址值会复制给lt2first,在delete lt1后,*lt1实例的first地址处之对象被delete,因此透过lt2first访问的地址值就无效了。

若使用一个类实例来构造另一类实例,可以定义复制构造函数(copy constructor)来实现自定义的复制行为。例如:

#include <iostream>
using namespace std;

template <typename T>
class LinkedList {
    class Node {
    public:
        Node(T value, Node *next) : value(value), next(next) {}
        T value;
        Node *next;
    };

    Node *first = nullptr;

public:
    LinkedList() = default;               // 默认构造函数
    LinkedList(const LinkedList<T> <);  // 复制构造函数
    ~LinkedList();
    ...略
};

template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
    // 逐一复制 Node 实例(而不是复制地址值)
    if(lt.first != nullptr) {
        this->first = new Node(lt.first->value, nullptr);
    }

    Node *thisLast = this->first;
    Node *srcNext = lt.first->next;
    while(srcNext != nullptr) {
        thisLast->next = new Node(srcNext->value, nullptr);
        thisLast = thisLast->next;
        srcNext = srcNext->next;
    }
}

...

template <typename T>
LinkedList<T>::~LinkedList() {
    if(this->first == nullptr) {
        return;
    }

    Node *last = this->first;
    do {
        Node *next = last->next;
        delete last;
        last = next;
    } while(last != nullptr);
}

...

跟默认构造函数不同的是,无论有没有定义其他构造函数,若没有定义复制构造函数,那编译器一定会生成一个复制构造函数,默认会发生值域的复制,复制的行为视类型而定,基本类型的话就是复制值,指针的话是复制地址值,数组的话,会逐一复制每个元素,类类型的话,视各类定义的复制构造函数而定。

也就是说,在没有自定义LinkedList的复制构造函数前,编译器产生的默认构造函数,相当于有以下的内容:

template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) : first(lt.first) {}

在定义了复制构造函数,方才的main执行上没问题了,然而以下还是会有问题:

...略

int main() {
    LinkedList<int> *lt1 = new LinkedList<int>();
    LinkedList<int> lt2;
    (*lt1).append(1).append(2).append(3);

    lt2 = *lt1;                 // 指定时会发生复制

    delete lt1;

    cout << lt2.get(2) << endl; // 不可预期的结果

    return 0;
}

在指定时默认也是会发生复制行为,指定时默认的行为类似默认的复制构造函数,若要避免问题发生,得自定义复制指定运算符(copy assignment operator):

#include <iostream>
using namespace std;

template <typename T>
class LinkedList {
    class Node {
    public:
        Node(T value, Node *next) : value(value), next(next) {}
        T value;
        Node *next;
    };

    Node *first = nullptr;

    void copy(const LinkedList<T> <);

public:
    LinkedList() = default;
    LinkedList(const LinkedList<T> <);
    ~LinkedList();
    LinkedList<T>& operator=(const LinkedList<T> <);  // 定义复制指定运算符
    ...略
};

template <typename T>
void LinkedList<T>::copy(const LinkedList<T> <) {
    // 逐一复制 Node 实例(而不是复制地址值)
    if(lt.first != nullptr) {
        this->first = new Node(lt.first->value, nullptr);
    }

    Node *thisLast = this->first;
    Node *srcNext = lt.first->next;
    while(srcNext != nullptr) {
        thisLast->next = new Node(srcNext->value, nullptr);
        thisLast = thisLast->next;
        srcNext = srcNext->next;
    }
}

template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
    this->copy(lt);
}

template <typename T>
LinkedList<T>& LinkedList<T>::operator=(const LinkedList<T> <) {
    this->copy(lt);
    return *this;
}

...略

template <typename T>
LinkedList<T>::~LinkedList() {
    if(this->first == nullptr) {
        return;
    }

    Node *last = this->first;
    do {
        Node *next = last->next;
        delete last;
        last = next;
    } while(last != nullptr);
}

...略

如果定义类时,需要考虑到要不要自定义析构函数、复制构造函数、复制指定运算符其中之一,几乎就是三个都要定义了,这就是 Rule of three。

如果某个类不希望被复制、指定等,C++ 11 以后可以如下:

struct Foo {
    Foo() = default;                     // 采用默认构造函数行为
    Foo(const Foo&) = delete;            // 删除此函数(不定义此函数)
    ~Foo() = default;                    // 采用默认析构函数行为
    Foo& operator=(const Foo&) = delete; // 删除此函数(不定义此函数)
};

在过去的话,会将复制构造函数、复制指定运算符设为private

class Foo {
    Foo(const Foo&);
    Foo& operator=(const Foo&);
public:
    Foo() = default;                  
    ~Foo();           
};

另外,在〈rvalue 参考〉中看过std::move用来实现移动语义,而构造函数、指定运算符也可以实现移动语义,也就是移动构造函数(move constructor)、移动指定运算符(move assignment operator),如果考虑要在类上实现移动语义,析构函数、复制/移动构造函数、复制/移动指定运算符几乎就都要全部出现,这就是 Rule of Five。

例如,可以为LinkedList加上移动构造函数、移动指定运算符:

#include <iostream>
#include <utility>
using namespace std;

template <typename T>
class LinkedList {
    class Node {
    public:
        Node(T value, Node *next) : value(value), next(next) {}
        T value;
        Node *next;
    };

    Node *first = nullptr;

    void copy(const LinkedList<T> <);
    void move(LinkedList<T> <);

public:
    LinkedList() = default;
    LinkedList(const LinkedList<T> <);                // 复制构造函数
    LinkedList(LinkedList<T> &<);                     // 移动构造函数
    ~LinkedList();                                      // 析构函数

    LinkedList<T>& operator=(const LinkedList<T> <);  // 复制指定运算符
    LinkedList<T>& operator=(LinkedList<T> &<);       // 移动指定运算符

    LinkedList<T>& append(T value);
    T get(int i);
};

template <typename T>
void LinkedList<T>::copy(const LinkedList<T> <) {
    // 逐一复制 Node 实例(而不是复制地址值)
    if(lt.first != nullptr) {
        this->first = new Node(lt.first->value, nullptr);
    }

    Node *thisLast = this->first;
    Node *srcNext = lt.first->next;
    while(srcNext != nullptr) {
        thisLast->next = new Node(srcNext->value, nullptr);
        thisLast = thisLast->next;
        srcNext = srcNext->next;
    }
}

template <typename T>
void LinkedList<T>::move(LinkedList<T> <) {
    if(lt.first != nullptr) {
        this->first = lt.first;
        lt.first = nullptr;
    }
}

template <typename T>
LinkedList<T>::LinkedList(const LinkedList<T> <) {
    this->copy(lt);
}

template <typename T>
LinkedList<T>::LinkedList(LinkedList<T> &<) {
    this->move(lt);
}

template <typename T>
LinkedList<T>& LinkedList<T>::operator=(const LinkedList<T> <) {
    this->copy(lt);
    return *this;
}

template <typename T>
LinkedList<T>& LinkedList<T>::operator=(LinkedList<T> &<) {
    this->move(lt);
    return *this;
}

template <typename T>
LinkedList<T>& LinkedList<T>::append(T value) {
    Node *node = new Node(value, nullptr);
    if(first == nullptr) {
        this->first = node; 
    }
    else {
        Node *last = this->first;
        while(last->next != nullptr) {
            last = last->next;
        }
        last->next = node;
    }
    return *this;
}

template <typename T>
T LinkedList<T>::get(int i) {
    Node *last = this->first;
    int count = 0;
    while(true) {
        if(count == i) {
            return last->value;
        }
        last = last->next;
        count++;
    }
}

template <typename T>
LinkedList<T>::~LinkedList() {
    if(this->first == nullptr) {
        return;
    }

    Node *last = this->first;
    do {
        Node *next = last->next;
        delete last;
        last = next;
    } while(last != nullptr);
}

int main() {
    LinkedList<int> lt1;
    lt1.append(1).append(2).append(3);

    LinkedList<int> lt2 = std::move(lt1); // 将 lt1 的数据移动给 lt2
    cout << lt2.get(2) << endl;

    return 0;
}

记得移动之后,因为数据转移出去了,对目前lt1的状态不能有任何的假设,只能销毁lt1,或者重新指定实例给lt1

具有析构函数、复制/移动构造函数、复制/移动指定运算符的类,要全权负责管理自身资源;至于其他类,就完全不需要其中之一,这就是 Rule of zero。

有关 Rule of three、Rule of Five、Rule of zero,可进一步参考〈The rule of three/five/zero〉。


展开阅读全文