Effective C++ 学习笔记 《十四》

探讨了资源管理类中的拷贝行为,包括禁止拷贝、引用计数及深拷贝等策略,强调了在RAII对象拷贝时需考虑的任务与资源管理的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Item 14: Think carefully about copying behavior in resource-managing classes.

这一节是承接Item13来接着讲关于资源管理类中的拷贝行为。说实话这一节还有很多地方我没想的很清楚,这里先做个记录。

回顾前面,item13介绍了RAII的资源管理法则,同时提到了智能指针对于堆资源的应用。
但是很多情况智能指针就不太合适,需要自己来写资源管理类。

接着作者给出了互斥锁mutex的例子,互斥锁有lock和unlock两个函数可以使用。为了保证正确对锁加锁解锁,一般会建立一个类来管理锁的机制,管理锁的类遵循RAII的思想,在构造函数中对mutex加锁,析构函数中解锁 代码如下

class Lock {
public:
	explicit Lock(Mutex *pm)
	: mutexPtr(pm)
	{ lock(mutexPtr); } // acquire resource
	~Lock() { unlock(mutexPtr); } // release resource
private:
	Mutex *mutexPtr;  
};

这些内容都是上一节里讲的,现在的问题是如果对RAII对象进行复制,会怎么样。比如对这个Lock类的对象用如下代码:

Lock m11(&m);
Lock ml2(ml1);

想一下,lock类是用于对mutex管理的,而mutex是一个互斥锁,复制一个对互斥锁进行管理的对象,好像是没有什么意义的。那既然没有意义,就应该对这个类禁止拷贝,这也是本节对资源管理类的拷贝操作处理的第一种情况。至于禁止拷贝的方法,在item6里讲的很详细。

另外一种情况,就是拷贝有意义的情况,这时我们的处理办法就是对管理对象所管理的资源进行引用计数,即shared_ptr的做法。比如还是上面的lock类,可以把它的mutexPtr装进智能指针,即shared_ptr< Mutex >。
但是问题就是智能指针会在引用计数为0的时候释放资源,然而这里的本意只是希望在析构的时候解锁。
解决这个问题的办法就是shared_ptr提供了删除器,可以在对mutexPtr初始化的时候,把它的删除器初始化为解锁函数,这样当引用为0的时候,智能指针就不是释放资源而是调用解锁函数,代码如下:

class Lock {
public:
	explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
	: mutexPtr(pm, unlock ) // to point to and the unlock func as the deleter
	{ 
		lock(mutexPtr.get() ); // see Item 15 for info on "get"
	}
private:
	std::tr1::shared_ptr<Mutex> mutexPtr; // use shared_ptr
}; // instead of raw pointe

而且值得注意的是,这个类是无需再声明析构函数的。原因就是没有意义,它只有一个智能指针成员,这个成员前面说到,在引用数为0的时候自动解锁便完成了它的任务,所以不需要额外声明析构函数。


讲到这里,好像本节的内容也应该是说完了。但是作者继续提到了两个跟管理资源的类的拷贝有关系的点。这里也记录一下:

- Copy the underlying resource(我理解成深拷贝)

作者讲到,对一份资源,我们是可以拷贝任意份的,但是资源管理的意义,是在不需要资源的时候保证释放。这就意味着我们在拷贝资源管理的对象的时候,必须同时拷贝它管理的资源即深拷贝
这个点我认为还是很重要的,来看一个例子就明白了。

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

class MyStr
{
private:
    char *name;
    int id;
public:
};

int main()
{
    MyStr str2(1, "hhxx");
    MyStr str3;
    str3 = str2;
    return 0;
}

上面这个类中,我们没有显式重载拷贝赋值运算符,编译器会默认构造一个,然而执行的是浅拷贝,也就意味着str2的name成员和str1的name成员指向同一个字符串 就像这样
在这里插入图片描述
这就会带来致命的错误,比如我们更改了str3的name指针的值,那么同时就修改了str2的name,再比如对str2和str3都进行析构,就会对一块内存释放两次,显然这都是错误。
回到书本,这就是作者强调的除了复制资源管理对象,还应该同时复制它管理的资源,也就是深拷贝。部分代码如下:

	MyStr(const MyStr& str)
    {
        id = str.id;
        if (name != NULL)
            delete[] name;
        name = new char[strlen(str.name) + 1];
        strcpy_s(name, strlen(str.name) + 1, str.name);
    }
    MyStr& operator =(const MyStr& str)
    {
        if (this != &str)
        {
            if (name != NULL)
                delete[] name;
            this->id = str.id;
            int len = strlen(str.name);
            name = new char[len + 1];
            strcpy_s(name, strlen(str.name) + 1, str.name);
        }
        return *this;
    }

这样才能实现对对象的正确拷贝。


-Transfer ownership of the underlying resource (转移资源的拥有权)

某些情况下,我们可能只希望有一个RAII对象指向底层资源,那么解决办法可以参考auto_pty的处理方法,也就是转移它的占有者

作者在这一节末尾,作者强调了一下有些时候必须要自己来写拷贝函数,除非编译器生成的版本能解决问题。写到这里,关于这一节的内容就写完了。我的感受其实是讲的看上去很简单的话题,但实际包含的东西很多,也有很多地方我还没有彻底想清楚,需要以后多加实践来理解。
由于比较杂,总结一下这一节:

- 我认为这一节的核心就是在考虑RAII对象的拷贝行为的时候,需要想清楚这个拷贝所要完成的任务。前面讲到了两点:如果没有意义就禁止拷贝,否则使用计数法(实际就是智能指针)。

- 对于深拷贝的问题,需要重视。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值