C++拷贝控制:拷贝控制与资源管理

拷贝控制和资源管理

​ 通常,管理类外资源的类必须定义拷贝控制成员。为了定义这些成员,我们首先确定此类型对象的拷贝语义。一般来说,有两种选择:可以定义拷贝操作,使类的行为看起来像一个值或像一个指针。

类的行为像一个值,意味着它有自己的状态 (如 string 类)。当我们拷贝一个像值的对象时,副本和原对象完全独立。变副本不会影响原对象,反之亦然。

​ **行为像指针的类则共享状态 **(如 shared_ptr 类)。当我们拷贝一个这种类的对象时,副本和原对象使用相同的底层数据。改变副本会改变原对象,反之亦然。

​ 当然,也存在既不像值也不像指针的类。如 IO 类,unique_str 等。因为它们都不支持拷贝或赋值。

​ 这里,我们将实现一个 HasPtr 类,首先将令其行为像一个值。然后重新实现,使它的行为像一个指针。

​ HasPtr 类有两个成员,一个 int 和一个 string 指针。通常,类直接拷贝内置类型 (不包括指针) 成员;这些成员本身就是值,因此通常应该让它们的行为像值一样。我们如何拷贝指针成员决定了像 HasPtr 这样的类是具有类值行为还是类指针行为。

行为像值的类(如 string)

​ 这里将 HasPtr 作为类值行为来实现,对于类管理的资源,每个对象都应该拥有一份自己的拷贝。所以,类值版本的 HasPtr 如下:

class HasPtr {
public:
    HasPtr(const std::string &s = std::string()) :
            ps(new std::string(s)), i(0) {}
    // 对 ps 指向的 string,每个 HasPtr 对象都有自己的拷贝,所以我
    // 们拷贝构造函数拷贝的是 string,并不是拷贝指针
    HasPtr(const HasPtr &sour):
        ps(new std::string(*sour.ps)), i(sour.i) { }
    HasPtr &operator=(const HasPtr &sour);
    ~HasPtr() { delete(ps); }
private:
    std::string *ps;
    int i;
};

其次,析构函数肯定是必要的,因为我们要释放 ps 指向的动态内存。由于定义了析构函数,我们肯定会定义拷贝构造函数与拷贝赋值运算符。

类值拷贝赋值运算符

​ 我们知道,拷贝赋值运算符即 =,将右侧对象拷贝到左侧对象。而且,拷贝赋值运算符会销毁左侧运算对象的资源,所以对于对象中含动态内存时,我们需要手动释放。同时,我们应该支持当一个对象赋予它自身时,赋值运算符也应该能正常执行。这两点很重要,都是易忽略的点。

  • 赋值运算符重载 错误版本一

    HasPtr& HasPtr::operator=(const HasPtr &sour) {
        ps = new std::string(*(sour.ps));
        i = sour.i;
        return *this;
    }
    

    **分析:**很显然,这里的函数并没有销毁左侧运算对象所拥有的 string 类型指针指向的动态内存。会造成内存泄漏。

  • 赋值运算符重载 错误版本二:

    HasPtr& HasPtr::operator=(const HasPtr &sour) {
        delete(ps);     // 释放左侧对象指向的 string
        // 如果 sour 和 *this 是同一对象,我们就将从已释放的内存中拷贝数据!!!
        ps = new std::string(*sour.ps);
        i = sour.i;
        return *this;
    }
    

    **分析:**我们可以看见,代码中第一个便是 delete,解决了版本一的问题。但是当我们的右侧运算对象与左侧运算对象是同一个时,我们就会从已释放的内存中拷贝数据!!!这是显然错误的。

  • 正确的赋值运算符重载:

    HasPtr& HasPtr::operator=(const HasPtr &sour) {
        auto newp = new std::string(*(sour.ps));       // 拷贝底层 string
        delete(ps);     // 释放旧内存
        ps = newp;      // 从右侧运算对象拷贝到本对象
        i = sour.i;
        return *this;   // 返回本对象
    }
    

    **分析:**从代码中可以看出,我们首先拷贝了右侧运算对象,这样可以处理自赋值的情况,并能保证在异常发生时代码也是安全的。完成拷贝后,我们再释放左侧运算对象的资源,然后从右侧运算对象拷贝到左侧运算对象。

当重载赋值运算符时,需要注意两点:

  • 拷贝赋值运算符会销毁左侧运算对象的资源,所以对于对象中含动态内存时,我们需要手动释放
  • 我们应该支持当一个对象赋予它自身时,赋值运算符也应该能正常执行。

所以,好的方法是在销毁左侧对象之前先拷贝右侧运算对象。

所有代码见同目录HasPtr_likeValue.cc文件

定义行为像指针的类(如 shared_ptr)

​ 对于行为类似指针的类,我们需要为其定义拷贝构造函数和拷贝赋值运算符,来拷贝指针成员本身而不是它所指向的对象 string。

​ 我们的类仍然需要自己的析构函数来释放接受 string 参数的构造函数分配的内存。当然,我们不能简单的直接释放,因为有可能还有其他的对象也指向这个内存。所以这种时候,我们使用引用计数来解决这个问题。(可以发现,这个类的行为甚至有一点像 shared_ptr)

引用计数

​ 引用计数的工作方式如下:

  • 除了初始化对象外,每个构造函数(拷贝构造函数除外)都要创建一个引用计数。当我们创建一个对象时,引用计数初始化为1。
  • 拷贝构造函数不分配新的计数器。显然它应该是递增共享的计数器。
  • 析构函数递减计数器。如果计数器变为 0,则释放资源
  • 拷贝赋值运算符递增右侧对象计数器,递减左侧对象计数器。如果左侧对象计数器变为 0,则拷贝赋值运算符就必须销毁状态。

唯一的难题就是确定哪里存放引用计数。因为计数器不能直接作为 HasPtr 的成员。下面例子说明了原因:

HasPtr p1("hihi");
HasPtr p2(p1);
HasPtr p3(p1);

当我们创建 p3 时,可以很简单的更新 p1 与 p3 中的计数器,但是 p2 如何更新呢?

解决的唯一方法就是将计数器保存在动态内存中。当创建一个对象时,我们分配一个新的计数器。当拷贝或者赋值对象时,我们拷贝指向计数器的指针。

定义一个使用引用计数的类

​ 通过引用计数,我们可以编写类指针的 HasPtr 版本:

class HasPtr_Point {
private:
    std::string *ps;
    int i;
    std::size_t *use;       // 用来记录有多少个对象共享 *ps 成员
public:
    HasPtr_Point() : ps(new std::string()), i(0), use(new std::size_t(1)) {}
    HasPtr_Point(const HasPtr_Point &sour) :
            ps(sour.ps), i(sour.i), use(sour.use) { ++*use; }
    HasPtr_Point &operator=(const HasPtr_Point &);
    ~HasPtr_Point();
};
类指针的拷贝成员“篡改”引用计数

​ 当赋值或拷贝一个 HasPtr 对象时,我们希望副本和原对象都指向相同的 string。即,当拷贝一个 HasPtr 时,我们将拷贝 ps 本身,而不是 ps 指向的 string。当我们进行拷贝时,还会递增该 string 关联的计数器。

​ 同样的,析构函数不能无条件的 delete ps,因为可能还有其他对象指向这块内存。析构函数应该递减引用计数,指出共享 string 的对象有多少个。如果计数器变为 0,则析构函数释放 ps 和 use 指向的内存:

HasPtr_Point::~HasPtr_Point() {
    if (-- *use == 0) {
        delete (use);
        delete (ps);
    }
}

​ 对于拷贝赋值运算符,我们需要解决相同的问题。

​ 在这里,我们就不需要拷贝右侧对象了,我们可以先递增右侧对象的引用计数,再递减左侧对象的引用计数。这样做,即使左右两侧的运算对象是同一对象,也不会出现向已释放的内存拷贝数据的情况。同样,在递减左侧引用计数时,我们相当于是在做“销毁”左侧对象的事情。

HasPtr_Point &HasPtr_Point::operator=(const HasPtr_Point &rhs) {
    ++*rhs.use;				// 先增
    if (-- *use == 0) {		// 后减。避免左右两侧是同一个对象可能出现的问题。
        delete (use);
        delete (ps);
    }
    ps = rhs.ps;
    i = rhs.i;
    use = rhs.use;
    return *this;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值