• C++ Primer : 第十三章 : 拷贝控制之拷贝控制和资源管理


    定义行为像值的类


    行为像值的类,例如标准库容器和std::string这样的类一样,类似这样的类我们可以简单的实现一个这样的类HasPtr。
    在实现之前,我们需要:
    定义一个拷贝构造函数,完成string的拷贝,而不是指向string的指针的拷贝;
    定义一个拷贝赋值运算符,释放当前的string,并从右侧拷贝新的string。
    定义一个析构函数来释放string

    class HasPtr {
    
    public:
    	HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0) {}
    
    	HasPtr(const HasPtr& has) : ps(new std::string(*has.ps)), i(has.i) {}
    	
    	HasPtr& operator = (const HasPtr& has);
    	
    	~HasPtr() { delete ps; }
    private:
    	std::string* ps;
    	int	i;
    };
    
    HasPtr::operatpr = (const HasPtr& has) {
    	// 先定义一个局部变量保存右侧对象,如果右侧对象和左侧对象是同一个对象,那么先删除右侧对象就会发生错误
    	auto newPtr = new std::string(*has.ps);
    	// 此时再释放左侧对象资源
    	delete ps;
    	ps 	= newPtr;
    	i 	= has.i;
    }

    拷贝赋值运算符的实现中,为了安全起见,必须先将右侧对象拷贝到一个局部变量里,然后再删除左侧对象,然后将局部变量赋值给左侧对象。如果先删除左侧对象,此时右侧对象和左侧对象是同一个对象时,就会发生严重错误。
    对于一个赋值运算符来说,一个好的方法是在左侧对象销毁之前拷贝右侧对象,因为左侧对象和右侧对象可能是同一个对象。



    定义行为像指针的类

    行为像指针的类,例如智能指针shared_ptr。既然要让类的行为像一个指针,那么这个类就不能在执行析构函数时简单的释放相关联的string,如果只有一个对象指向这个string时,我们才可以在这个类的析构函数中释放资源,如果有多个类指向这个string,那么其中一个类释放了这个string,其他的类就不能再复用。
    因此就要用到引用计数的方法来解决这个问题。


    引用计数的工作方式如下:
    • 除了初始化对象之外,每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象共享正在创建的对象共享状态,当创建一个对象时,引用计数为1,因为此时只有一个对象共享。
    • 拷贝构造函数不分配新得引用计数器,拷贝给定对象的数据成员,包括引用计数器,拷贝构造函数递增共享的计数器,表示给定对象更的状态又被一个新用户所共享
    • 拷贝赋值运算符递减左侧运算对象的引用计数器,递增右侧对象的引用计数器,如果左侧对象的引用计数器为0,则销毁左侧对象。
    • 析构函数判断引用计数是否为0,如果为0,则销毁左侧对象。

    引用计数的实现:我们假设有下面的情况:
    HasPtr h1;
    HasPtr h2(h1);
    HasPtr h3(h1);

    HasPtr是一个行为像指针的类,新创建的h1的引用计数为1,创建h2,用h1初始化h2,会递增h1的引用计数值,此时h2保存了h1中的引用计数,在创建h3的时候,递增了h1的引用计数值,而且我们必须做的是要更新h2中的引用计数值,此时无法更新h2中的引用计数值。因此,我们需要将引用计数保存在动态内存中,这样原对象和其他副本对象都会指向相同的计数器,这样就可以自动更新引用计数在每个共享对象中的状态。


    class HasPtr {
    
    public:
    	HasPtr(const std::string& s = std::string()) : ps(new std::string(s)), i(0), use(new size_t(1)) {}
    
    	HasPtr(const HasPtr& has) :  ps(has.ps), i(has.i), use(has.use) { ++ *use;}
    	
    	HasPtr& operator = (const HasPtr& has);
    	
    	~HasPtr();
    private:
    	std::strin g* ps;
    	int	i;
    	size_t* use; // 引用计数
    };
    
    HasPtr::HasPtr& operator = (const HasPtr& has) {
    	
    	++ *has.use;
    	if (0 == *use) {
    		delete ps;
    		delete use;
    	}
    	
    	ps 	=	has.ps;
    	i	=	has.i;
    	use	=	has.use;
    	return *this;
    }
    
    HasPtr::~HasPtr() {
    	
    	if (--*use == 0) {
    		delete ps;
    		delete use;
    	}
    }


    注意的是,我们在拷贝一个HasPtr时,拷贝的是ps本身而不是ps指向的string。




    交换操作

    通常,管理资源的类除了定义拷贝控制成员之外,还会定义交换操作的函数swap。理论上来说,我们的swap函数应该是这样的:

    HasPtr temp = 1;
    v1 = v2;
    v2 = temp;

    这样的代码将v1中string拷贝了两次,但是这样做是没有必要的,我们希望swap交换指针,而不是分配string的副本:
    std::string* temp = v1.ps;
    v1.ps = v2.ps;
    v2.ps = temp;
    v1.i  =	v2.i;

    我们将swap函数声明为HasPtr类的友元函数,这样swap就能访问HasPtr的ps和i成员:
    class HasPtr {
    
    	friend void swap(HasPtr&, HasPtr&);
    	// 其他定义
    };
    
    inline
    void swap(HasPtr& lhs, HasPtr& rhs) {
    	using std::swap;
    	swap(lhs.ps, rhs.ps);
    	swap(lhs.i, rhs.i);
    }

    在swap函数中:
    使用了using std::swap,如果这个类有自己的swap函数,匹配程度会高于标准库swap,会优先使用类自己的swap,如果没有,则使用标准库的swap。
    swap里交换类的指针和int成员,并不会发生递归循环,HasPtr的数据成员是内置类型的,这时候会调用标准库版本的swap。


    在赋值运算符中使用swap

    通常,一个定义了swap的类用它swap函数来定义拷贝赋值运算符,这种运算符使用了一种拷贝并交换的技术。

    HasPtr& operator = (HasPtr has) {
    
    	swap(*this, has);
    	return *this;
    }

    在进行HasPtr类的赋值运算中,先将右侧对象拷贝到拷贝赋值运算符函数里,然后交换左侧对象的指针和右侧对象的指针,交换后,右侧对象赋值给了左侧对象,左侧对象相应的string指针也指向了右侧对象副本的对应成员,而右侧对象的string指针则指向了左侧对象的相应成员。在这个函数结束后,右侧对象的副本被销毁,于是原来左侧对象的资源被释放,而左侧对象现在保存的是右侧对象的成员。
    拷贝并交换的操作,和之前的拷贝赋值运算符的实现原理是相同的,在改变左侧对象之前拷贝右侧对象。保证了这样的操作异常的安全。



  • 相关阅读:
    带有“全选”的combotree
    combotree(组合树)的使用
    根据权限显示accordion
    accordion(折叠面板)的使用
    js中substr、substring、indexOf、lastIndexOf的用法
    EasyUI中使用自定义的icon图标
    EasyUI tree的三种选中状态
    C# List集合去重使用lambda表达式
    C# DataTable去重,根据列名去重保留其他列
    win10下部署.Net Web项目到IIS10
  • 原文地址:https://www.cnblogs.com/averson/p/5150012.html
Copyright © 2020-2023  润新知