c++智能指针

1、智能指针的模拟实现和使用

        c++中new或者malloc出来的资源,是需要程序员手动去释放的,在这个过程中会发生两种问题,(1)忘记释放,(2)发生异常安全问题,new出资源后,还没delete掉就发生异常,跳出到外层捕获异常,像下面这样。最终都会导致资源的泄露。

void func()
{
	int* p = new int(3);
	{
		if (*p == 3)
			throw (string)"cuowu";
	}
	delete p;
}

int main()
{
	try 
	{
		func();
	}
	catch (string str)
	{
		cout << str << endl;
	}
	return 0;
}

        这样衍生出了一种新的写法,叫做智能指针,在动态开辟(new/malloc)空间时使用智能指针对象来接收指针,智能指针生命周期到了之后会自动调用析构函数,自动delete空间,一旦发生上面所说的异常问题,在跳出到外层栈空间时,new出的资源就会自动释放。有了这种智能指针,我们就可以在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,我们实际上把管理一份资源的责任托管给了一个对象,这样我们就不需要显式地释放资源。通过重载operator*和operator->可以赋予智能指针正常指针的功能。

template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
    T& operator*()
    {
	    return *_ptr;
    }
    T* operator->()
    {
	    return _ptr;
    }
private:
	T* _ptr;
};

        这种利用对象生命周期来控制程序资源的思想我们称为RAII(Resource Acquisition Is Initialization),lock_guard也应用了这种思想。 

        上面是最简单最早期的智能指针实现版本,有很多缺陷,最主要的是没有解决智能指针之间的赋值问题。赋值操作使得两个智能指针能对同一块资源做管理,但是在析构时,两个智能指针先后析构同一块空间,会导致下面这段程序崩溃。

int main()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2 = sp1;
	return 0;
}

        在c++的发展过程中,先后有三种对上述问题的解决方案。c++98的auto_ptr,解决思路是管理权转移。c++11的unique_ptr,思路是防拷贝。c++11的shared_ptr思路是引用计数共享拷贝。

        auto_ptr采用的管理权转移,其实就是在发生拷贝构造或者赋值时,将原来的智能指针置空,这样对特定空间的管理权就由原对象转移到了新对象,这样原来的智能指针就不能进行解引用等指针操作了,这种管理权转移的做法是早期的设计缺陷,一般公司都明令禁止使用它。

template<class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	auto_ptr<T>& operator=(const auto_ptr<T>& ap)
	{
		if (this != &ap)
		{
			if (_ptr)
				delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
	~auto_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

        unique_ptr的策略也比较简单粗暴,直接将拷贝和赋值设置为delete,没有生成对应的拷贝和赋值,推荐使用。但是缺陷是,在某些特定的需要拷贝或者赋值的场景下无法使用。

class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	unique_ptr(unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
	~unique_ptr()
	{
		if (_ptr)
		{
			delete _ptr;
			_ptr = nullptr;
		}
	}
private:
	T* _ptr;
};

        shared_ptr的做法是在智能指针类中添加一个计数器,用于标定在发生赋值或者拷贝操作之后,一块空间由多少个智能指针来管理。发生一次拷贝或者赋值,计数器就+1,计数器不为1时,析构函数的操作是将计数器-1,如果为1则直接将所管理的空间直接析构。

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr), _pcount(new int(1))
	{}
	shared_ptr(shared_ptr<T>& sp)
		:_ptr(sp._ptr), _pcount(sp._pcount)
	{
		++(*_pcount);
	}
	shared_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		if (this != &sp)
		{
			if (--(*_pcount) == 0)
			{
				delete _pcount;
				delete _ptr;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*pcount);
		}
		return *this;
	}
	~shared_ptr()
	{
		if (--(*_pcount) == 0 && _ptr)
		{
			delete _ptr;
			_ptr = nullptr;
			delete _pcount;
			_pcount = nullptr;
		}
	}
private:
	T* _ptr;
	int* _pcount;
};

        在拷贝构造和赋值时有对计数器的++操作,在析构时有对计数器的--操作,那么shared_ptr,是否是线程安全的呢?答案是的,在库中实现的shared_ptr还内置了一个互斥量mutex,用于保证只有一个线程访问shared_ptr的计数器。

        shared_ptr也有其本身的缺陷,它无法应对循环引用的问题,观察下面的代码,ListNode节点内包含两个智能指针,当spn1的尾指向spn2,spn2的头指向spn1,这时两个节点的计数器都为2,因为”spn1->spnext = spn2;“以及后续的”spn2->spprev = spn1;“操作本质上就是赋值,发生了计数器的++,这样一来在spn1和spn2在生命周期结束之后都不能进行析构。解决方法是设计一个新的类叫做weak_ptr(弱指针),这个类没有默认构造函数,且需要用shared_ptr进行初始化。在struct ListNode内部使用weak_ptr<ListNode>就可以防止被多重引用的问题,节点链接时,计数器也不会++。

struct ListNode
{
	int val;
	shared_ptr<ListNode> _spnext;
	shared_ptr<ListNode> _spprev;
};

int main()
{
	shared_ptr<ListNode> spn1(new ListNode);
	shared_ptr<ListNode> spn2(new ListNode);
	spn1->spnext = spn2;
	spn2->spprev = spn1;
	return 0;
}
template<class T>
class weak_ptr
{
public:
	weak_ptr() = default;
	weak_ptr(const shared_ptr<T>& sp)
		:_ptr(sp.get_ptr())
	{}

	weak_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		_ptr = sp.get_ptr();
		return *this;
	}

	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

        库中实现的shared_ptr更加复杂,构造对象的参数还有删除器对象(缺省参数是默认析构函数),作为仿函数存在,在shared_ptr析构时定义一个删除器对象,调用其operator()函数对指向的空间进行析构。如果shared_ptr的模板不是一个普通的指针,而是一个文件FILE*,这时候就不能使用缺省参数,需要定制删除器对象并传参。具体如下。传入删除器相当于通过仿函数进行定制的析构,如果使用默认的delete这段代码会报错,因为FILE*不能通过delete来释放。

struct Fclose
{
	void operator()(FILE* p)
	{
		fclose(p);
	}
};

int main()
{
	std::shared_ptr<FILE> sp(fopen("test.txt", "w"), Fclose());
	return 0;
}

        使用RAII思想还可以设计锁的管理守卫,使用互斥量来构造LockGuard,这样在访问临界资源的时候假如发生了异常,也可以利用LockGuard的生命周期来进行解锁,不用担心多线程的死锁问题。注意LockGuard里面的成员变量是引用,锁是不支持拷贝的,因此只能用引用。

template<class Lock>
class LockGuard
{
public:
	LockGuard(lock& lock)
		:_lk(lock)
	{
		_lk.lock();
	}
	~LockGuard()
	{
		_lk.unlock();
	}
private:
	Lock& _lk;
};

2、内存泄漏

        一般我们申请了资源,这个资源不使用了,但是忘记释放,或者因为异常安全等问题没有释放,这时就造成了内存泄漏。如果我们申请的内存没有释放,但是进程正常结束,那么这个内存也会释放。一般程序碰到内存泄漏,重启后就可以,但是长期运行,不能随便重启的程序,碰到内存泄漏危害非常大,比如操作系统。

        由于栈上的内存的分配和回收都是由编译器控制的,所以在栈上是不会发生内存泄露的,只会发生栈溢出(Stack Overflow),也就是分配的空间超过了规定的栈大小。而当在堆上申请内存后忘记使用 free/delete 来回收,就发生了内存泄露。开辟空间本质上是建立虚拟地空间和物理地址的映射关系,如果使用完资源不释放,物理地址的映射关系就会一直存在,那么别的进程就无法使用这块资源。内存泄漏会造成可用的物理内存越来越小。

        在实际编程时,为了避免内存泄漏问题,我们需要小心谨慎一些,不好处理的地方多用智能指针去管理。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值