C++拷贝构造与拷贝赋值的深入探讨

 
Category: C_C++

写在前面

总结一下C++类的拷贝构造与拷贝赋值部分, 从基本的示例入手, 一点点介绍拷贝控制与资源管理的一些内容.

参考Cppprimer(第七章:类, 第十三章:拷贝控制), effective C++(55)

类的拷贝赋值运算符

一般流程

  1. 先将右侧运算对象拷贝到一个局部临时对象中
  2. 拷贝完成后, 销毁左侧运算对象的现有成员(保证内存安全)
  3. 将数据从临时对象拷贝到左侧运算对象的成员中.

要点

  1. 自赋值的正确性: 保证赋值运算符将对象赋予其自身时能够正确工作.

    要做到这一点, 一个好的方法是在销毁左侧运算对象资源之前, 先拷贝右侧运算对象.

  2. 大多数赋值运算符组合了析构函数和拷贝构造函数的工作.

实例: 行为像值的类

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

class HasPtr {
    friend void swap(HasPtr &, HasPtr &);

public:
    HasPtr(const string &s = string()) : ps(new string(s)), i(0) {}
    HasPtr(const HasPtr &p) : ps(new string(*p.ps)), i(p.i) {}
    HasPtr &operator=(const HasPtr &rhs) {
        auto newp = new string(*rhs.ps);
        delete ps;
        ps = newp;
        i = rhs.i;
        return *this;
    }
    string get() { return *ps; }

private:
    string *ps;
    int i;
};

inline void swap(HasPtr &lhs, HasPtr &rhs) {
    using std::swap;
    swap(lhs.ps, rhs.ps);
    swap(lhs.i, rhs.i);
    cout << "swap by myself\n";
}

void t1() {
    HasPtr a("hello");
    HasPtr b;
    b = a;
    HasPtr c = a;
    cout << a.get() << endl;
    cout << b.get() << endl;
    cout << c.get() << endl;
}
/* hello */
/* hello */
/* hello */

void t2() {
    HasPtr a("a"), b("b");
    cout << a.get() << endl;
    cout << b.get() << endl;
    swap(a, b);
    cout << a.get() << endl;
    cout << b.get() << endl;
}
int main(int argc, char *argv[]) {
    /* t1(); */
    t2();
    return 0;
}

实例: 行为像指针的类

针对这种情况(例如string就是一个例子), 需要为其定义拷贝构造函数和拷贝赋值运算符, 此时拷贝的是指针成员, 而不是其指向的string对象.

标准库的实现: shared_ptr.

引用计数: 直接管理资源

其实就是shared_ptr内部的一项内存资源管理技术, 工作流程:

  1. 除了初始化对象, 每个构造函数(除了拷贝构造函数)还要创建一个引用计数, 用于记录有多少对象与正在创建的对象共享状态. 第一次创建一个对象的时候, 只有一个对象共享状态, 所以引用计数器初始化为1.

  2. 拷贝构造函数不分配新的计数器, 而是拷贝给定对象的数据成员, 包括计数器. 调用拷贝构造函数递增共享的计数器, 表示智能指针所指对象的状态又被一个新用户所共享.

  3. 析构函数递减计数器, 指出共享状态的用户少了一个, 如果计数器变为零, 析构函数开始释放状态.

  4. 拷贝赋值运算符递增右侧运算对象的计数器, 递减左侧运算对象的计数器.

    如果左侧运算对象的计数器变为0, 意味着它的共享状态没有用户了, 拷贝赋值运算符就必须销毁状态了.

注意, 计数器的位置不能直接存储在行为像指针的类的实例化对象中, 成为对象的成员. 因为引用计数更新的不同步性不能在每一个所指对象中传递, 所以需要动态创建保存计数器的内存, 使得副本和原始对象都有相同的计数器.

示例代码

#include <cstddef>
#include <iostream>
#include <string>
using namespace std;
class HasPtr {
public:
    HasPtr(const string &s = string())
        : ps(new string(s)), i(0), use(new size_t(1)) {}
    HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) {
        ++*use;
        cout << "after copy ctor, use: " << *use << endl;
    }
    HasPtr &operator=(const HasPtr &);
    HasPtr &operator=(const string &);
    string &operator*() { return *ps; }
    ~HasPtr();
    int get_use_cnt() { return *use; }
    void print() {
        cout << "*ps: " << *ps << ", i: " << i << ", *use: " << *use << endl;
    }

private:
    string *ps;
    int i;
    size_t *use; // 记录有多少对象共享*ps的成员
};

HasPtr::~HasPtr() {
    cout << "before dtor, use: " << *this->use << endl;
    if (--*use == 0) {
        delete ps;
        delete use;
        use = nullptr;
    }
    if (use)
        cout << "after dtor, use: " << *this->use << endl;
    else
        cout << "use deleted..\n";
}

HasPtr &HasPtr::operator=(const HasPtr &rhs) {
    cout << "before copy assignment, this->use: " << *this->use << endl;
    cout << "before copy assignment, rhs.use: " << *rhs.use << endl;
    ++*rhs.use;
    if (--*use == 0) {
        delete ps;
        delete use;
        use = nullptr;
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use; // 这一步将右对象的引用计数赋值给左对象,
                   // 使得两个对象共享同一份引用计数
    cout << "after copy assignment, this->use: " << *this->use << endl;
    cout << "after copy assignment, rhs.use: " << *rhs.use << endl;
    return *this;
}

HasPtr &HasPtr::operator=(const string &rhs) {
    *ps = rhs;
    return *this;
}

void t1() {
    //
    cout << "HasPtr h(\"abc\")\n";
    HasPtr h("abc"), h2;
    cout << "h: ";
    h.print();
    cout << "h2: ";
    h2.print();

    /* cout << "\nHasPtr h2 = h;\n"; */
    /* HasPtr h2 = h; */
    /* cout << "h: "; */
    /* h.print(); */
    /* cout << "h2: "; */
    /* h2.print(); */

    /* cout << "\nh = \"cde\"\n"; */
    /* h = "cde"; */
    /* cout << "h: "; */
    /* h.print(); */
    /* cout << "h2: "; */
    /* h2.print(); */

    cout << "\nh = h2;\n";
    h2 = h;
    cout << "h: ";
    h.print();
    /* cout << h.get_use_cnt() << endl; */
    cout << "h2: ";
    h2.print();
    /* cout << h2.get_use_cnt() << endl; */
}

/* HasPtr h("abc") */
/* h: *ps: abc, i: 0, *use: 1 */
/*  */
/* HasPtr h2 = h; */
/* h: *ps: abc, i: 0, *use: 2 */
/* h2: *ps: abc, i: 0, *use: 2 */
/*  */
/* h = "cde" */
/* h: *ps: cde, i: 0, *use: 2 */
/* h2: *ps: cde, i: 0, *use: 2 */
int main(int argc, char *argv[]) {
    t1();
    return 0;
}