详谈c++智能指针!!!


前言

C/C++ 语言最为人所诟病的特性之一就是存在内存泄露问题,因此后来的大多数语言都提供了内置内存分配与释放功能,有的甚至干脆对语言的使用者屏蔽了内存指针这一概念。这里不置贬褒,手动分配内存与手动释放内存有利也有弊,自动分配内存和自动释放内存亦如此,这是两种不同的设计哲学。有人认为,内存如此重要的东西怎么能放心交给用户去管理呢?而另外一些人则认为,内存如此重要的东西怎么能放心交给系统去管理呢?在 C/C++ 语言中,内存泄露的问题一直困扰着广大的开发者,因此各类库和工具的一直在努力尝试各种方法去检测和避免内存泄露,如 boost,智能指针技术应运而生。


一、智能指针的发展历史

1.C++ 98/03 的尝试——std::auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!

class Date {
private:
	int year_;
	int month_;
	int day_;
public:
	Date(int year = 2024, int month = 1, int day =1)
		:year_(year), month_(month), day_(day)
	{}
};

int main()
{
  	auto_ptr<Date> d1(new Date(2008, 1, 1));
   	return 0;
 }

通过上面的代码我们发现,智能指针实现了自动管理,在对象生命周期结束时,会自动释放内存,不需要程序员手动释放,减轻了程序员的负担。

但为什么auto_ptr会被抛弃呢?是因为它的实现原理有局限性:

  • 拷贝或赋值会改变资源的所有权
  • 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
  • 不支持对象数组的内存管理

我们用调试的监视窗口可以看到,auto_ptr在赋值和拷贝时,是用控制权转移实现的,所以它不能用在STL 容器中,因为容器内的元素必须支持赋值和拷贝。

在这里插入图片描述

另外,auto_ptr 不能支持数组对象的管理,所以c++用更严谨的unique_ptr代替了auto_ptr

在这里插入图片描述

2.std::unique_ptr

std::unique_ptr 对其持有的堆内存具有唯一拥有权,即引用计数永远是1,std::unique_ptr 对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个 std::unique_ptr 对象:

//初始化方式1
std::unique_ptr<int> sp1(new int(123));

//初始化方式2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));

//初始化方式3
std::unique_ptr<int> sp3 = std::make_unique<int>(123);

你应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因在其《Effective Modern C++》中已经解释过了,有兴趣的读者可以阅读此书相关章节。

令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&& ...params)
{
    return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

鉴于 std::auto_ptr 的前车之鉴,std::unique_ptr 禁止复制语义,为了达到这个效果,它的类的拷贝构造函数和赋值运算符被标记为 delete。

template <class T>
class unique_ptr
{
    //省略其他代码...

    //拷贝构造函数和赋值运算符被标记为delete
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;
};

因此,下列代码是无法通过编译的:

在这里插入图片描述

禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr

#include <memory>

std::unique_ptr<int> func(int val)
{
    std::unique_ptr<int> up(new int(val));
    return up;
}

int main()
{
    std::unique_ptr<int> sp1 = func(123);
    return 0;
}

上述代码从 func 函数中得到一个 std::unique_ptr 对象,然后返回给 sp1。

既然 std::unique_ptr 不能复制,那么如何将一个 std::unique_ptr 对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:

#include <memory>

int main()
{
    std::unique_ptr<int> sp1(std::make_unique<int>(123));

    std::unique_ptr<int> sp2(std::move(sp1));

    std::unique_ptr<int> sp3;
    sp3 = std::move(sp2);

    return 0;
}

以上代码利用 std::move 将 sp1 持有的堆内存(值为 123)转移给 sp2,再把 sp2 转移给 sp3。最后,sp1 和 sp2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 move 操作都有意义,只有实现了移动构造函数(或移动赋值运算符的类才行,而 std::unique_ptr 正好实现了这二者,以下是实现伪码:

template<typename T, typename Deletor>
class unique_ptr
{
    //其他函数省略...
public:
    unique_ptr(unique_ptr&& rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
    }

    unique_ptr& operator=(unique_ptr&& rhs)
    {
        this->m_pT = rhs.m_pT;
        //源对象释放
        rhs.m_pT = nullptr;
        return *this;
    }

private:
    T*    m_pT;
};

自定义智能指针对象持有的资源的释放函数

默认情况下,智能指针对象在析构时只会释放其持有的堆内存(调用 delete 或者 delete[]),但是假设这块堆内存代表的对象还对应一种需要回收的资源(如操作系统的套接字句柄、文件句柄等),我们可以通过自定义智能指针的资源释放函数。假设现在有一个 Socket 类,对应着操作系统的套接字句柄,在回收时需要关闭该对象,我们可以如下自定义智能指针对象的资源析构函数,这里以 std::unique_ptr 为例:

#include <iostream>
#include <memory>

class Socket
{
public:
    Socket() {}
    ~Socket() {}
    //关闭资源句柄
    void close() {}
};

int main()
{
    auto deletor = [](Socket* pSocket) {
        //关闭句柄
        pSocket->close();
        //TODO: 你甚至可以在这里打印一行日志...
        delete pSocket;
    };

    std::unique_ptr<Socket, void(*)(Socket * pSocket)> spSocket(new Socket(), deletor);
    return 0;
}

自定义 std::unique_ptr 的资源释放函数其规则是:

std::unique_ptr<T, DeletorFuncPtr>

其中 T 是你要释放的对象类型,DeletorPtr 是一个自定义函数指针。表示 DeletorPtr 有点复杂,我们可以使用 decltype(deletor) 让编译器自己推导 deletor 的类型:

std::unique_ptr<Socket, decltype(deletor)> spSocket(new Socket(), deletor);

在智能指针中,有一个接口reset(ptr),它的作用是将参数这个指针与托管的指针进行比较,如果地址不一样,那么就析构掉原来托管的指针,然后用参数这个指针来代替它,此时智能指针托管的就是参数这个指针。

那么在使用这种对资源有唯一管理权的智能指针时,就会引发一种内存陷阱:

在这里插入图片描述

所以当遇到这种有多个对象对一块资源进行管理时,我们就不能用排他性的智能指针进行管理,所以我们引入了std::shared_ptr

3.std::shared_ptr

std::unique_ptr 对其持有的资源具有独占性,而 std::shared_ptr 持有的资源可以在多个 std::shared_ptr 之间共享,每多一个 std::shared_ptr 对资源的引用,资源引用计数将增加 1,每一个指向该资源的 std::shared_ptr 对象析构时,资源引用计数减 1,最后一个对象析构时,发现资源计数为 0,将释放其持有的资源。多个线程之间,递增和减少资源的引用计数是安全的。(注意:这不意味着多个线程同时操作 std::shared_ptr 引用的对象是安全的)

std::shared_ptr 提供了一个 use_count() 方法来获取当前持有资源的引用计数。除了上面描述的,std::shared_ptr 用法和 std::unique_ptr 基本相同。

在这里插入图片描述

实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr 对象给外部使用,C++ 新标准考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this 模板对象即可。用法如下:

class A : public std::enable_shared_from_this<A>
{
public:
    std::shared_ptr<A> getSelf()
    {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<A> sp1(new A());
    std::shared_ptr<A> sp2 = sp1->getSelf();
    std::cout << "use count: " << sp1.use_count() << std::endl;
    return 0;
}

上述代码中,类 A 的继承 std::enable_shared_from_this 并提供一个 getSelf() 方法返回自身的 std::shared_ptr 对象,在 getSelf() 中调用 shared_from_this() 即可。

std::enable_shared_from_this 用起来比较方便,但是也存在很多不易察觉的陷阱。

陷阱一:不应该共享栈对象的 this 给智能指针对象

int main()
{
    A a;
    std::shared_ptr<A> sp2 = a.getSelf();
    std::cout << "use count: " << sp2.use_count() << std::endl;
    return 0;
}

运行修改后的代码会发现程序在 std::shared_ptr sp2 = a.getSelf(); 产生崩溃。这是因为,智能指针管理的是堆对象,栈对象会在函数调用结束后自行销毁,因此不能通过 shared_from_this() 将该对象交由智能指针对象管理。切记:智能指针最初设计的目的就是为了管理堆对象的(即那些不会自动释放的资源)。

陷阱二:避免 std::enable_shared_from_this 的循环引用问题

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比较好的做法是在构造函数里面调用shared_from_this()给m_SelfPtr赋值
        //但是很遗憾不能这么做,如果写在构造函数里面程序会直接崩溃
        std::cout << "A constructor" << std::endl;
    }

    ~A()
    {
        m_i = 0;
        std::cout << "A destructor" << std::endl;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int main()
{
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
    }
    return 0;
}

运行上面的代码,我们发现在程序的整个生命周期内,只有 A 类构造函数的调用输出,没有 A 类析构函数的调用输出,这意味着 new 出来的 A 对象产生了内存泄漏了!

在这里插入图片描述

我们来分析一下为什么 new 出来的 A 对象得不到释放。spa 出了其作用域准备析构,在析构时其发现仍然有另外的一个对象即 A::m_SelfPtr 引用了 A,因此 spa 只会将 A 的引用计数递减为 1,然后就销毁自身了。现在留下一个矛盾的处境:必须销毁 A 才能销毁其成员变量 m_SelfPtr,而销毁 m_SelfPtr 必须先销毁 A。这就是所谓的 std::enable_shared_from_this 的循环引用问题。我们在实际开发中应该避免做出这样的逻辑设计,这种情形下即使使用了智能指针也会造成内存泄漏。也就是说一个资源的生命周期可以交给一个智能指针对象,但是该智能指针的生命周期不可以再交给整个资源来管理。

4.std::weak_ptr

std::weak_ptr 是一个不控制资源生命周期的智能指针,是对对象的一种弱引用,只是提供了对其管理的资源的一个访问手段,引入它的目的为协助 std::shared_ptr 工作,同时,weak_ptr 没有重载*->但可以使用 lock() 获得一个可用的 shared_ptr 对象。

std::shared_ptr 可以直接赋值给 std::weak_ptr ,也可以通过 std::weak_ptrlock() 函数来获得 std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr 可用来解决 std::shared_ptr 相互引用时的死锁问题。

在这里插入图片描述

既然,std::weak_ptr 不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr 提供了一个 expired() 方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用 std::weak_ptrlock() 方法得到一个 std::shared_ptr 对象然后继续操作资源,以下代码演示了该用法:

//tmpConn_ 是一个 std::weak_ptr<TcpConnection> 对象
//tmpConn_引用的TcpConnection已经销毁,直接返回
if (tmpConn_.expired())
    return;

std::shared_ptr<TcpConnection> conn = tmpConn_.lock();
if (conn)
{
    //对conn进行操作,省略...
}

既然使用了 std::weak_ptrexpired() 方法判断了对象是否存在,为什么不直接使用 std::weak_ptr 对象对引用资源进行操作呢?实际上这是行不通的,std::weak_ptr 类没有重写 operator->operator* 方法,因此不能像 std::shared_ptrstd::unique_ptr 一样直接操作对象,同时 std::weak_ptr 类也没有重写 operator! 操作,因此也不能通过 std::weak_ptr 对象直接判断其引用的资源是否存在:

在这里插入图片描述

std::weak_ptr 的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。

5.智能指针的大小

在64位机器下,unique_ptr与普通指针大小一样,share_ptr / weak_ptr是普通指针的2倍:

在这里插入图片描述

6.智能指针使用注意事项

a. 一旦一个对象使用智能指针管理后,就不该再使用原始裸指针去操作;

b. 分清楚场合应该使用哪种类型的智能指针;
通常情况下,如果你的资源不需要在其他地方共享,那么应该优先使用 std::unique_ptr,反之使用 std::shared_ptr,当然这是在该智能指针需要管理资源的生命周期的情况下;如果不需要管理对象的生命周期,请使用 std::weak_ptr。

二、智能指针的模拟实现

我们只实现它最核心的部分:

  • 利用 RAII(一种利用对象生命周期来控制程序资源的简单技术)来管理指针
  • 像指针一样使用

auto_ptr:

template<class T>
class auto_ptr {
private:
	T* _ptr;
public:
	auto_ptr(T* ptr)
		:_ptr(ptr)
	{}

	~auto_ptr() { delete _ptr; }

	auto_ptr(auto_ptr<T>& sp)
		:_ptr(sp._ptr)
	{
		sp._ptr = nullptr;
	}

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

unique_ptr:

template<class T>
class unique_ptr {
private:
	T* _ptr;
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~unique_ptr() { delete _ptr; }

	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

shared_ptr:

template<class T>
class shared_ptr {
private:
	T* _ptr;
	int* _pcount;
	std::mutex* _pmtx;

public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
		,_pmtx(new std::mutex)
	{}
	~shared_ptr() { Release(); }

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
		,_pmtx(sp._pmtx)
	{
		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
	}
	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr == sp._ptr) return *this;
		Release();

		_pcount = sp._pcount;
		_ptr = sp._ptr;
		_pmtx = sp._pmtx;

		{
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		return *this;
	}

	void Release()
	{
		bool flag = false;
		{
			_pmtx->lock();
			if (--(*_pcount) == 0)
			{
				 delete _ptr;
				delete _pcount;
				flag = true;
			}
			_pmtx->unlock();
		}
		if (flag) delete _pmtx;
	}

	int use_count() const { return *_pcount; }
	T* get() const { return _ptr; }
	T& operator*() { return *_ptr; }
	T* operator->() { return _ptr; }
};

weak_ptr:

	template<class T>
	class weak_ptr
	{
	private:
		T* _ptr;
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}


		weak_ptr(const weak_ptr<T>& wp)
			:_ptr(wp._ptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

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

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

三、C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。
  • 44
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值