详解C++智能指针

目录

裸指针有哪些不好的地方?

智能指针原理:

不带引用计数的智能指针:

1.auto_ptr : 

2.scoped_ptr:

3.unique_ptr:

自己实现一个简单的unique_ptr : 

带引用计数的智能指针 : shared_ptr和weak_ptr

自己实现一个简单的shared_ptr:

智能指针的交叉引用(循环引用)问题 : 

多线程访问共享对象的线程安全问题

自定义删除器:

当unique_ptr和shared_ptr管理动态分配的数组空间时候的区别:

效率与灵活性: 编译时绑定删除器和运行时绑定删除器:

shared_from_this类和enable_shared_from_this函数;

make_shared函数make_unique函数:


裸指针有哪些不好的地方?

  1. 忘记释放资源,导致资源泄漏(常发生内存泄漏);
  2. 同一资源释放多次,导致释放野指针(程序崩溃);
  3. 明明程序代码在后面写了释放资源的代码,但由于程序的逻辑,从中间return回去,导致释放资源的代码并未执行;
  4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未执行;

智能指针原理:

智能指针是利用栈上的对象出作用域自动析构的特征来做到资源的自动释放,把资源释放的代码放到这个对象的析构函数中;用户可以不关注资源如何释放,无论程序逻辑如何运行,正常执行或者异常执行,资源在到期的情况下,一定会释放;

面试题:可以把智能指针放到堆上吗?智能指针一般定义在上(利用栈中对象出作用域自动析构特点)

CSMartPtr *p = new CSMartPtr(new int);

这里还是需要手动delete p,如果发生异常了,那么就会内存泄漏所以上面的写法错误,不要把智能指针放到堆上;

不带引用计数的智能指针:

auto_ptr,scoped_ptr,unique_ptr

1.auto_ptr : 

/*这里是auto_ptr的拷贝构造函数,
	_Right.release()函数中,把_Right的_Myptr
	赋为nullptr,也就是换成当前auto_ptr持有资源地址
	*/
	auto_ptr(auto_ptr& _Right) noexcept
		: _Myptr(_Right.release())
		{	// construct by assuming pointer from _Right auto_ptr
		}

观察auto_ptr的拷贝构造函数我们知道,只有最后一个auto_ptr智能指针持有资源,原来的auto_ptr都被赋nullptr了;看下面代码:

auto_ptr<int> ptr1 (new int );
auto_ptr<int> ptr2(ptr1);

结果是:ptr2指向ptr1之前的动态分配的资源,ptr1此时为nullptr,如果再访问ptr1就是访问空指针,很危险; 即auto_ptr永远让最后一个智能指针管理资源,前面的指针都置空; 所以不推荐使用auto_ptr,容器中也不要使用,因为容器经常需要拷贝赋值,那么一个容器有效,另一个容器就成全是空指针了;

【总结】:auto_ptr智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的auto_ptr都置为nullptr,只让最后一个auto_ptr持有资源。

2.scoped_ptr:

private:
    T * px;
	/*
	私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
	对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
	*/
    scoped_ptr(scoped_ptr const &);
    scoped_ptr & operator=(scoped_ptr const &);

观察scoped_ptr的源码我们发现,它将拷贝构造函数和拷贝赋值运算符进行私有化了,那么外部就不能调用了,从根本上杜绝了智能指针浅拷贝的发生,即scoped_ptr不能在容器中使用,如果容器相互拷贝和赋值,那么就会出现调用私有的scoped_ptr的拷贝构造函数和拷贝赋值运算符,就会出现编译错误。

3.unique_ptr:

独占式智能指针只推荐使用unique_ptr,不要使用auto_ptr和scoped_ptr了;

unique_ptr是将左值引用的拷贝构造函数和拷贝赋值运算符删除了,外部不能调用(等同于私有化),防止智能指针浅拷贝问题的发生;但unique_ptr提供了带右值引用的拷贝构造函数和拷贝赋值运算符,这样临时对象就可以使用了,非常有用;

// 示例1
unique_ptr<int> ptr(new int);
unique_ptr<int> ptr2 = std::move(ptr); 
//move将左值强转为右值,那么这里调用unique_ptr的带右值引用的拷贝构造函数
ptr2 = std::move(ptr); // 同上,因为ptr2已经存在,所以调用带右值引用的拷贝赋值运算符
// 示例2
unique_ptr<int> test_uniqueptr()
{
	unique_ptr<int> ptr1(new int);
	return ptr1;
}
int main()
{

//调用test_uniqueptr函数,在return ptr1代码处,调用右值引用的拷贝构造函数,由ptr1拷贝构造ptr 
	unique_ptr<int> ptr = test_uniqueptr();
	return 0;
}

那么unique_ptr与scoped_ptr的区别:他们二个都删除了带左值引用的拷贝构造函数和拷贝赋值运算符,但是unique_ptr定义了带右值引用的拷贝构造函数和拷贝赋值运算符;所以可以显式通过std::move, 就可以通过一个unique_ptr指向另一个uniquet_ptr(资源转移),不是二个指向同一对象;而且unique_ptr也可以①:做为函数的形参,传递给它的实参是一个同类型的unique_ptr经过st::move调用右值引用的拷贝构造函数,将拥有权转移;②:当函数返回一个unique_ptr,因为函数内的uniuqe_ptr即将析构,属于临时对象,所以会调用右值引用的拷贝构造函数,所以拥有权会转移给调用端;(因为在函数内返回的是即将要销毁的对象,属于临时对象,所以调用右值引用的拷贝构造函数;)

自己实现一个简单的unique_ptr : 

template <typename  T>
class CSmartPtr{ //
private:
    T* mptr;
public:
    CSmartPtr(T* ptr = nullptr  ):mptr(ptr){}
    ~CSmartPtr(){delete mptr;mptr = nullptr;}
    CSmartPtr(const CSmartPtr&) = delete ; 
    //删除左值引用的拷贝构造函数
    CSmartPtr& operator = (const CSmartPtr&) = delete ;
    //删除右值引用的拷贝赋值运算符
    //需要定义带右值引用的拷贝构造函数和拷贝赋值运算符
    CSmartPtr(CSmartPtr&& rhs){
        if(this == &rhs) return ; //检测自我赋值
        mptr = rhs.mptr; rhs.mptr = nullptr;
    }
    void operator =(CSmartPtr&& rhs){
        if(this == &rhs) return ; //检测自我赋值
        mptr = rhs.mptr;rhs.mptr = nullptr;
    } 
    T& operator * (){return *get();} //返回引用,因为要改变指针指定内存上的值
    const T& operator *()const {return *get();}
    T* operator -> (){ return get() ;} 
    const T* operator ->()const {return get();}
    //return &(operator *());通常用operator*实现operator->
    T* get(){return mptr;} //获得指针指针的底层指针
};

上面这段代码就是一个简单的智能指针,主要用到了这二点:

  1. 智能指针体现在对裸指针进行了一层面向对象的封装,将指针定义为类的成员,在构造函数中初始化资源地址,在析构函数中释放资源;
  2. 利用栈上的对象出作用域会自动析构这个特点,那么对象生命周期结束后会自动调用析构函数释放资源,从而避免资源泄漏;

带引用计数的智能指针 : shared_ptr和weak_ptr

带引用计数的智能指针:多个指针可以管理同一个资源:shapred_ptr,weak_ptr;

        当允许多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针的析构函数来处理资源的释放问题,这就是引用计数的概念。这样能够保证在最后一个指向资源的shapred_ptr离开作用域后保证释放资源,而多个shapred_ptr指向同一个资源时候,一个shapred_ptr的析构不会将释放资源,即不会影响其他shared_ptr;通常情况下资源默认删除是delete,也可以自定义删除器来处理如果释放资源;

        要对资源的引用个数进行计数,那么大家知道,对于整数的++或者- -操作,它并不是线程安全的操作,因此shared_ptr和weak_ptr底层的引用计数已经通过CAS操作,保证了引用计数加减的原子特性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针。

        曾经有一道面试的问题这样问“shared_ptr智能指针的引用计数在哪里存放?资源引用计数存放在内存上

private:
/*下面这两个是shared_ptr的成员变量,_Ptr是指向内存资源的指针,
_Rep是指向new出来的计数器对象的指针,该计数器对象包含了资源的一个引用计数器count */
	element_type * _Ptr{nullptr}; 
	_Ref_count_base * _Rep{nullptr};

自己实现一个简单的shared_ptr:

template <typename  T>
class RefCnt {
public:
	RefCnt(T* ptr = nullptr) :mptr(ptr) {
		if (ptr) {
			mcount = 1;
		}
	}
    void delRefCnd(){--mcount;} //减少资源的引用计数
	void addRef() { ++mcount; } //增加资源的引用计数
//自己实现的与shared_ptr相比,没有线程安全特性;因为对mcount不是原子操作;
private:
	T* mptr;
	int mcount; // 改进 为 atomic_int CAS  
	//库中shared_ptr和weak_ptr都是线程安全的,可以在多线程环境下使用;
	
};
template <typename  T>
class CSmartPtr {
public:
	CSmartPtr(T* ptr = nullptr) :mptr(ptr) {
		mpRefCnt = new RefCnt<T>(mptr);

	}
	~CSmartPtr() {
		if (0 == mpRefCnt->delRefCnt()) {
			delete mptr;
			mptr = nullptr;
		}
	}
	CSmartPtr(const CSmartPtr<T>& src) :mptr(src.mptr), mpRefCnt(src.mpRefCnt) {
		if (mptr) {
			mpRefCnt->addRef();
		}
	}
	CSmartPtr<T>& operator=(const CSmartPtr<T>& src) {
		if (this == &src) {
			return *this;
		}
		if (mpRefCnt->delRefCnt() == 0) {
			delete mptr;
		}
		mptr = src.mptr;
		mpRefCnt = src.mpRefCnt;
		mpRefCnt->addRef();
		return *this;
	}
	T& operator*() { return *get(); }
	T* operator->() { return get(); }
	T* get() { return mptr; }

private:
	RefCnt<T>* mpRefCnt; //指向该资源引用计数对象的指针
	T* mptr; //指向资源的指针

};

智能指针的交叉引用(循环引用)问题 : 

shared_ptr:强智能指针,可以改变资源的引用计数;

weak_ptr:弱引用指针,不会改变资源的引用计数,辅助shared_ptr工作的,防止循环引用;

注意:弱智能指针只是资源的观察者,它是不能直接使用资源的,不会增加引用计数,它没有提供*,->运算符重载函数;要通过它访问资源必须通过lock方法,得到一个强智能指针,然后就可以使用资源了;

强指针指针循环引用(交叉引用)是什么问题?什么结果?怎么解决?

class B; //类的前置声明
class A 
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	shared_ptr<B> _ptrb;//指向B对象的智能指针
};
class B 
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	shared_ptr<A> _ptra; //指向A对象的智能指针
};
int main()
{
	shared_ptr<A> pa(new A());//pa指向A对象,A的引用计数为1
	shared_ptr<B> pb(new B());//pb指向B对象,B的引用计数为1
	pa->_ptrb = pb; //A对象的成员变量ptrb指向B对象,B的引用计数为2
	pb->_ptra = pa;//B对象的成员变量ptra指向A对象,A的引用计数为2
         cout << pa.use_count() << endl;//A对象的引用计数是2
         cout << pb.use_count() << endl;//B对象的引用计数是2
         return 0;
}
//出了main函数作用域后,ptra和ptrb二个局部对象析构,分别给A对象和B对象的引用计数从2减到1,
//此时A对象中的ptrb和B对象中的ptra互相引用了,所以二个new出来的对象无法释放,内存泄漏;

怎么解决循环引用问题呢?定义对象的时候使用强智能指针shared_ptr,使用对象的时候使用弱指针指针weak_ptr;

弱智能指针weak_ptr区别于强智能指针shared_ptr之处在于:

  • weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
  • weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  • weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源
class B;
class A 
{
public:
	A(int _a):a(_a) { cout << "A()   a =  "<<a << endl; }
	~A() { cout << "~A()" << endl; }
	void func() { cout << "A的函数" << endl; }
	weak_ptr<B> _ptrb;
	int a;
};
class B 
{
public:
	B(int _b):b(_b) { cout << "B()   b=   "<<b << endl; }
	~B() { cout << "~B()" << endl; }
	//现在b想调用a中的函数呢?
	void func() {
		shared_ptr<A> sp = _ptra.lock(); //weak_ptr通过lock方法发挥一个强指针指针
		if (sp) { //提升成功
			sp->func();
		}
	}
	weak_ptr<A> _ptra;
	int b;
};
int main()
{
	shared_ptr<A> pa(new A(1));
	shared_ptr<B> pb(new B(2));
	pa->_ptrb = pb;
	pb->_ptra = pa;
	//通过pb访问A中的方法
	pb->func();
	cout <<"用pa访问pb中的b:"<< pa->_ptrb.lock()->b << endl;
	cout << "用pb访问pa中的a:"<<pb->_ptra.lock()->a << endl;
	cout << pa.use_count() << endl;
	cout << pb.use_count() << endl;

	return 0;
}

多线程访问共享对象的线程安全问题

shared_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。

下面这个多线程程序子线程先睡眠,而主线程已经将p所指向资源释放了,那么子线程再访问那个对象的方法很明显就会出现问题;需要在子线程中先判断指针所指向对象是否有效!!

//有问题的代码,么有考虑多线程访问共享对象的线程安全问题
class A 
{
public:
    A() { cout << "A()" << endl; }
    ~A() { cout << "~A()" << endl; }
    void testA() { cout << "A::func()" << endl; }
};
void  handler01(A* q) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); //睡眠2秒
    q->testA();
}
//多线程访问共享对象的线程安全问题:
int main() {
    A* p = new A();
    thread t1(handler01, p);
    delete p;
    t1.join();
    return 0;
}

通过智能指针改进: 注意:为了防止循环引用问题,建议在指针定义的时候使用shared_ptr,在使用指针的时候使用weak_ptr;

class A 
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	void testA() { cout << "A::func()" << endl; }
};
void  handler01(weak_ptr<A> pw ) {
/* 如果想访问智能指针pw所指对象的方法,先通过lock将弱智能指针weak_ptr提升为强智能指针shared_ptr,
提升过程中,是通过检测它所观察的强智能指针的引用计数,来判断对象是否存活;
sp如果为nullptr,说明对象已经析构,不能访问;
sp不为nullptr,那么可以正常访问对象及其方法;
*/


	shared_ptr<A> sp = pw.lock();
	if (sp) {
		sp->testA();
	}
	else {
		cout << "对象已经析构,不能调用它的方法了" << endl;
	}
}
//多线程访问共享对象的线程安全问题:
int main() {
	{
		shared_ptr<A> p(new A());
		thread t1(handler01,weak_ptr<A>(p));
		t1.detach();//分离线程
		std::this_thread::sleep_for(std::chrono::seconds(10)); //主线程阻塞
	} //出了作用域后智能指针p自动释放它指向的资源

	std::this_thread::sleep_for(std::chrono::seconds(20)); //主线程阻塞


	return 0;
}

所以通过shared_ptr和weak_ptr可以解决多线程里面访问共享对象的线程安全问题;因为它可以在线程里面通过对象资源的引用计数来检测对象的生存状态;

自定义删除器:

        智能指针的删除器deleter : 智能指针默认释放资源的方式是delete,但并不是所有的资源都是申请的堆内存,所有有时候智能指针默认删除器释放资源就不合适了;比如说申请数组,文件资源,数据库连接,网络连接等;

        我们如果看了智能指针的源码,就会发现,在智能指针的析构函数里面有对一个函数对象的调用:deleter(ptr) :

        默认的deleter如下:

template <typename  T>
class Deleter{ //智能指针默认的删除器
public:
    void operator()(T* ptr){
        delete  ptr;
    }
};

当unique_ptr和shared_ptr管理动态分配的数组空间时候的区别:

unique_ptr提供了一个偏特化版本来指向动态分配的数组空间,这个版本在对象析构时候会自动调用delete [ ] ; 注意这个偏特化版本的unique_ptr不提供*和->操作符,而是提供操作符[ ] ,用以访问其指向动态分配数组中的某个对象;unique_ptr up(new string[10]);

而shared_ptr是不支持直接管理动态分配的数组空间,所以用shared_ptr管理动态分配的数组空间,必须提供自定义的删除器;shared_ptr sp(new int [10],[ ] (int *p){delete [ ] p ;});

sp.release(); //使用我们的自定义删除器lambda表达式释放数组,调用delete [ ]

而shapred_ptr未定义下标运算符,而且智能指针类型不支持指针算术操作,所以访问数组元素必须使用get获得内置指针,然后用它来访问数组元素;

for(size_t i = 0;i!=10;++i)
{
    *(sp.get()+i) = i ; //shared_ptr通过get获取内置指针然后访问数组元素
}

当unique_ptr管理的是文件资源,数据库连接,网络连接等,默认删除器是delete就不合适了,需要自定义删除器;

//下面用模板实现函数对象作为自定义删除器,但这种方式不好;
//定义一个模板类型仅仅使用在智能指针中,却一直出现在源码中;
template  <typename  T>
class MyFileDeleter{
public:
    void operator ()(T* ptr) const
    {
        cout<<"call MyFileDeleter.operator()"<<endl;
        fclose(ptr);
    }
};
unique_ptr<FILE,MyFileDeleter<FILE >> ptr(fopen("data.txt","w"));

可以使用function函数对象lambda表达式来完成:

unique_ptr<int,function<void(int*)>> ptr (new int [100],
        [](int *p)->void {
            cout<<"call labmda release new int [100]"<<endl;
            delete [] p;
        }
);
unique_ptr<FILE ,function<void(FILE*)>> ptr2(fopen("data.txt","w"),
        [](FILE* p)->void {
            cout<<"call lambda relsease new fopen" <<endl;
            fclose(p);
        }
);

效率与灵活性: 编译时绑定删除器和运行时绑定删除器

        shared_ptr和unique_ptr之间明显不同的就是他们管理所保存的指针的策略,shared_ptr是共享式智能指针,unique_ptr是独占式智能指针;另一个差异是它们允许用户重载默认删除器的方式;我们可以很容易重载一个shared_ptr的删除器,只要在创建或者reset指针时传递它一个可调用对象即可;与之相反,删除器的类型是一个unique_ptr对象的类型的一部分。用户必须在定义unique_ptr时以显式模板实参的形式提供删除器的类型,因此,对于unique_ptr的用户来说,提供自己的删除器就更为复杂;

运行时绑定删除器

在一个shared_ptr的生命周期内,可以随时改变删除器的类型;因为删除器类型是变化的,类成员的类型在运行时是不能改变的,所以不能直接保存删除器,只能间接保存删除器,所以调用shared_ptr的删除器会有运行时开销;

编译时绑定删除器

对unique_ptr而言,删除器的类型是类类型的一部分,即unique_ptr有二个模板参数,一个表示它所管理的指针,另一个表示删除器的类型;由于删除器的类型是unique_ptr类型的一部分,因此删除器成员的类型在编译器时是知道的,从而删除器可以直接保存在unique_ptr对象中;编译器就知道执行删除器的代码,实际上删除器调用有可能会编译为内联形式,那么效率更高;

总结

通过在编译时绑定删除器,unique_ptr的删除器调用效率很高,但删除器不可更改;

通过在运行时绑定删除器,shared_ptr使用户重载删除器更加方便,但是会有间接调用删除器的运行时开销;

shared_from_this类和enable_shared_from_this函数;

先给出两个智能指针的应用场景代码,都是有问题的,仔细思考一下问题的原因。

#include <iostream>
using namespace std;
// 智能指针测试类
class A
{
public:
	A():mptr(new int) 
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
		delete mptr; 
		mptr = nullptr;
	}
private:
	int *mptr;
};
int main()
{
	A *p = new A(); // 裸指针指向堆上的对象
	shared_ptr<A> ptr1(p);// 用shared_ptr智能指针管理指针p指向的对象
	shared_ptr<A> ptr2(p);// 用shared_ptr智能指针管理指针p指向的对象
	// 下面两次打印都是1
	cout << ptr1.use_count() << endl; 
	cout << ptr2.use_count() << endl;
	return 0;
}
//输出:因为二个智能指针的引用计数都是1,所以在析构会调用二次构造函数,逻辑错误;

成员函数错误的直接通过this返回智能指针:

#include <iostream>
using namespace std;
// 智能指针测试类
class A
{
public:
	A():mptr(new int) 
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
		delete mptr; 
		mptr = nullptr;
	}	 
//A类提供了一个成员方法,通过this返回指向自身对象的shared_ptr智能指针。
	shared_ptr<A> getSharedPtr() 
	{ 
/*注意:<不能直接返回this,在多线程环境下,根本无法获知this指针指向的对象的生存状态>
《通过shared_ptr和weak_ptr可以解决多线程访问共享对象的线程安全问题》 */
		return shared_ptr<A>(this); 
	}
private:
	int *mptr;
};
int main()
{
	shared_ptr<A> ptr1(new A());
	shared_ptr<A> ptr2 = ptr1->getSharedPtr();
//按原先的想法,上面两个智能指针管理的是同一个A对象资源,但是这里打印都是1
//导致出main函数A对象析构两次,析构逻辑有问题
	cout << ptr1.use_count() << endl; 
	cout << ptr2.use_count() << endl;
	return 0;
}
//代码同样有错误,A对象被析构了两次,而且看似两个shared_ptr指向了同一个A对象资源,
//但是资源计数并没有记录成2,还是1,不正确。

如何解决上面的问题呢?我们从底层源码分析:

shared_ptr原理分析:

源码上shared_ptr的定义如下:template<class _Ty> class shared_ptr: public _Ptr_base<_Ty>

shared_ptr是从_Ptr_base继承而来的,作为派生类,shared_ptr本身没有提供任何成员变量,但是它从基类_Ptr_base继承来了如下成员变量(只罗列部分源码):

template<class _Ty>
class _Ptr_base
{	// base class for shared_ptr and weak_ptr
protected:
	void _Decref()
		{	// decrement reference count
		if (_Rep)
			{
			_Rep->_Decref();
			}
		}
	void _Decwref()
		{	// decrement weak reference count
		if (_Rep)
			{
			_Rep->_Decwref();
			}
		}
private:
	// _Ptr_base的两个成员变量,这里只罗列了_Ptr_base的部分代码
	element_type * _Ptr{nullptr}; // 指向资源的指针
	_Ref_count_base * _Rep{nullptr}; // 指向资源引用计数的指针
};

_Ref_count_base 记录资源引用计数的类是怎么定义的呢,如下(只罗列部分源码):

class __declspec(novtable) _Ref_count_base
	{	// common code for reference counting
private:
	/* _Uses记录了资源的引用计数,也就是引用资源的shared_ptr
	的个数;_Weaks记录了weak_ptr的个数,相当于资源观察者的
	个数,都是定义成基于CAS操作的原子类型,增减引用计数时时
	线程安全的操作
	*/
	_Atomic_counter_t _Uses;
	_Atomic_counter_t _Weaks;
}

也就是说,当我们定义一个shared_ptr< int > ptr(new int)的智能指针对象时,该智能指针对象本身的内存是8个字节,如下图所示:

那么把智能指针管理的外部资源以及引用计数资源都画出来的话,就是如下图的展示:

当你做这样的代码操作时:

shared_ptr<int> ptr1(new int); //调用shared_ptr的构造函数
shared_ptr<int> ptr2(ptr1);  //调用shared_ptr的拷贝构造函数
cout<<ptr1.use_count()<<endl;
cout<<ptr2.use_count()<<endl;

        这段代码没有任何问题,ptr1和ptr2管理了同一个资源,引用计数打印出来的都是2,出函数作用域依次析构,最终new int资源只释放一次,逻辑正确!这是因为shared_ptr ptr2(ptr1)调用了shared_ptr的拷贝构造函数(源码可以自己查看下),只是做了资源的引用计数的改变,没有额外分配其它资源,如下图所示:

但是当你做如下代码操作时(错误代码,一定不要这么使用)

int *p = new int;
shared_ptr<int> ptr1(p); //调用shared_ptr的构造函数,会创建一份引用计数
shared_ptr<int> ptr2(p); //调用shared_ptr的构造函数,会创建一份引用计数
//所以这是二组不同的智能指针,有各自的引用计数,都是1,最后资源会错误的释放二次;
cout<<ptr1.use_count()<<endl;
cout<<ptr2.use_count()<<endl;

这段代码就有问题了,因为shared_ptr ptr1( p )和shared_ptr ptr2( p )都调用了shared_ptr的构造函数,在它的构造函数中,都重新开辟了引用计数的资源,导致ptr1和ptr2都记录了一次new int的引用计数,都是1,析构的时候它俩都去释放内存资源,导致释放逻辑错误,如下图所示:

        上面两个代码段,分别是shared_ptr的构造函数和拷贝构造函数做的事情,导致虽然都是指向同一个new int资源,但是对于引用计数对象的管理方式,这两个函数是不一样的,构造函数是新分配引用计数对象,拷贝构造函数只做引用计数增减

        相信说到这里,大家知道最开始的两个代码清单上的代码为什么出错了吧,因为每次调用的都是shared_ptr的构造函数,虽然大家管理的资源都是一样的,_Ptr都是指向同一个堆内存,但是_Rep却指向了不同的引用计数对象,并且都记录引用计数是1,出作用域都去析构,导致问题发生!

        代码1修改正确很简单,就是在产生同一资源的多个shared_ptr的时候,通过拷贝构造函数或者赋值operator=函数进行,不要重新构造,避免产生多个引用计数对象,代码修改如下:

     A *p = new A(); // 裸指针指向堆上的对象
    shared_ptr<A> ptr1(p);// 用shared_ptr智能指针管理指针p指向的对象
    shared_ptr<A> ptr2(ptr1);// 用ptr1拷贝构造ptr2
   // 下面两次打印都是2,最终随着ptr1和ptr2析构,资源只释放一次,正确!
    cout << ptr1.use_count() << endl; 
    cout << ptr2.use_count() << endl;

代码2修改:注意我们有时候想在类里面提供一些方法,返回当前对象的一个shared_ptr强智能指针,做参数传递使用(多线程编程中经常会用到)。

        首先肯定不能像之前那样写return shared_ptr< A > ( this ) ,这会调用shared_ptr智能指针的构造函数,对this指针指向的对象,又建立了一份引用计数对象,加上main函数中的shared_ptr< A > ptr1(new A());已经对这个A对象建立的引用计数对象,又成了两个引用计数对象,对同一个资源都记录了引用计数,为1,最终两次析构对象释放内存,错误!

重点:那如果一个类要提供一个函数接口,返回一个指向当前对象的shared_ptr智能指针怎么办?方法就是继承enable_shared_from_this类,然后通过调用从基类继承来的shared_from_this()方法返回指向同一个资源对象的智能指针shared_ptr。这样还是相同的引用计数对象,只是对引用计数增加,而不会创建新的引用计数对象;

//智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A>
{
public:
	A() :mptr(new int)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
		delete mptr;
		mptr = nullptr;
	}

 //A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
	shared_ptr<A> getSharedPtr()
	{
//通过调用基类的shared_from_this方法得到一个指向当前对象的智能指针
		return shared_from_this();
	}
private:
	int *mptr;
};

一个类继承enable_shared_from_this会怎么样?看看enable_shared_from_this基类的成员变量有什么,如下:

template<class _Ty>
	class enable_shared_from_this
	{	// provide member functions that create shared_ptr to this
public:
	using _Esft_type = enable_shared_from_this;
	_NODISCARD shared_ptr<_Ty> shared_from_this()
		{	// return shared_ptr
		return (shared_ptr<_Ty>(_Wptr));
		}
	// 成员变量是一个指向资源的弱智能指针
	mutable weak_ptr<_Ty> _Wptr;
};

        也就是说,如果一个类继承了enable_shared_from_this类,那么它产生的对象就会从基类enable_shared_from_this继承一个成员变量_Wptr,当定义第一个智能指针对象的时候shared_ptr< A > ptr1(new A()),调用shared_ptr的普通构造函数,就会初始化A对象的成员变量_Wptr,作为观察A对象资源的一个弱智能指针观察者(在shared_ptr的构造函数中实现,有兴趣可以自己调试跟踪源码实现)。

        然后代码如下调用shared_ptr< A > ptr2 = ptr1->getSharedPtr(),getSharedPtr函数内部调用shared_from_this()函数返回指向该对象的智能指针,这个函数怎么实现的呢,看源码:

shared_ptr<_Ty> shared_from_this()
{	// return shared_ptr
    return (shared_ptr<_Ty>(_Wptr)); 
//将当前对象继承自类enable_shared_from_this的弱智能指针提升为强智能指针返回
}

shared_ptr< _Ty >(_Wptr),说明通过当前A对象的成员变量_Wptr构造一个shared_ptr出来,看看shared_ptr相应的构造函数:

shared_ptr(const weak_ptr<_Ty2>& _Other)
{	// construct shared_ptr object that owns resource *_Other
    if (!this->_Construct_from_weak(_Other)) // 从弱智能指针提升一个强智能指针
    {
         _THROW(bad_weak_ptr{});
    }
}

接着看上面调用的_Construct_from_weak方法的实现如下:

template<class _Ty2>
bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other)
{	// implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
//if通过判断资源的引用计数是否还在,判定对象的存活状态,对象存活,提升成功;
//对象析构,提升失败!之前的博客内容讲过这些知识,可以去参考!
    if (_Other._Rep && _Other._Rep->_Incref_nz())
    {
    _Ptr = _Other._Ptr;
    _Rep = _Other._Rep;
    return (true);
    }
    return (false);
}

综上所说,所有过程都没有再使用shared_ptr的普通构造函数,没有在产生额外的引用计数对象,不会存在把 一个内存资源,进行多次计数的过程;更关键的是,通过weak_ptr到shared_ptr的提升,还可以在多线程环境中判断对象是否存活或者已经析构释放,在多线程环境中是很安全的;而通过this裸指针进行构造shared_ptr,不仅仅资源会多次释放,而且在多线程环境中也不确定this指向的对象是否还存活。

#include <iostream>
using namespace std;
// 智能指针测试类,继承enable_shared_from_this类
class A : public enable_shared_from_this<A>
{
public:
	A() :mptr(new int)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
		delete mptr;
		mptr = nullptr;
	}
	// A类提供了一个成员方法,返回指向自身对象的shared_ptr智能指针
	shared_ptr<A> getSharedPtr()
	{
		/*通过调用基类的shared_from_this方法得到一个指向当前对象的
		智能指针*/
		return shared_from_this();
	}
private:
	int *mptr;
};
int main()
{
	shared_ptr<A> ptr1(new A());
	shared_ptr<A> ptr2 = ptr1->getSharedPtr();

	// 引用计数打印为2
	cout << ptr1.use_count() << endl;
	cout << ptr2.use_count() << endl;

	return 0;
}
//输出:A对象构造一次,引用计数为2,A对象析构一次;

make_shared函数make_unique函数:

使用new创建shared_ptr的底层内存分配如下图所示:

使用make_shared函数创建shared_ptr的底层内存分配如下图所示:

        从前面的分析我们知道智能指针内部有二个指针,一个指针指向托管的资源,另一个指针指向引用计数;托管的资源和引用计数都是new出来的堆内存;而shared_ptr sp1(new int (10));这种方式创建智能指针是存在缺陷的,因为它包含了二次内存申请,一次是对托管资源的内存申请,一次是存放强-弱引用计数内存的申请;假设第一次对资源的内存申请成功,第二次对引用计数内存的申请失败了,那么shared_ptr对象创建失败了,那么不会调用shared_ptr的析构函数了,则第一次申请的资源的内存无法释放,导致内存泄漏了;这个风险是存在的;如果换成make_shared去创建智能指针,shared_ptr sp3 = make_shared(10); 情况就不同了; make_shared函数申请一块内存空间同时保存资源和引用计数,这样更加安全却效率更高,但也有缺点,不能自定义删除器了,而且会延迟托管的资源释放;

make_shared函数的优缺点:

优点:内存分配效率高且更安全;原始通过new去创建shared_ptr需要二次内存分配,且有内存泄漏风险,而通过make_shared函数创建智能指针只会进行一次内存分配,效率更高,且不会发生资源泄漏问题; 使用make_shared函数能够避免无意的智能指针初始化错误,比如让二个shared_ptr指向一个new出来的内存,存在二份引用计数,最后会发现错误;

缺点: 1. make_shared不能自定义删除器了; 2. 导致托管的资源延迟释放 :如果是通过new创建shared_ptr分配二块内存, 只要没有强智能指针指向资源了,那么资源内存会直接释放,弱智能指针是指向引用计数的那块内存; 如果是使用make_shared函数只会分配一块内存,保存托管的资源和引用计数, 即便强智能指针没有指向资源了,也不能释放那块内存,因为还有弱智能指针在观察引用计数,那块内存同时包含托管的资源和引用计数,只能同时释放;即即使没有强智能指针指向资源,只要还有弱智能指针,那么那个资源就不会得到释放;

同时C++14还提供了make_unique来代替new去创建unique_ptr智能指针;自己手写基本版的make

template <typename  T,typename ...Ts>
std::unique_ptr<T> make_unique<Ts&&  ... params >{
    return  std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
};
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值