智能指针的基础知识

智能指针能做到保证资源的自动释放

利用栈上对象离开作用自动析构的特性保证自动释放。

//智能指针简单的实现
template<typename T>
class CSmartPtr{
    
public:
    CSmartPtr(T* ptr= nullptr):_mptr(ptr){
        
    }
    ~CSmartPtr(){
        delete _mptr;
    }
    
    T& operator*(){
        return *_mptr;
    }
    T* operator->(){
        return _mptr;
    }
private:
    T* _mptr;
};

不带引用计数的智能指针

在使用智能指针的使用要先引用memory库

stl中提供了三种不带引用计数的智能指针:auto_ptr、scoped_ptr和unique_ptr

auto_ptr在使用过程中,如果进行赋值和拷贝运算时,会将第一个指针置空,让拷贝或赋值的指针管理之前的内存。基本不使用这个

一个面试题:可以在vector中使用auto_ptr吗?

​ 答:最好不要,因为在使用容器的过程中,难免会发生拷贝(容器1复制到容器2),而auto_ptr的实现方式中为了避免浅拷贝,会把初始的指针置为空,让赋值的指针管理这块内存,之前的指针失去了堆内存的管理权,这样再访问容器1就会发现其中的都是空指针。

scoped_ptr中将拷贝构造函数和赋值重载函数都删除了,从源码中禁止了拷贝和赋值。这个指针用的也比较少

推荐使用unique_ptr,与scoped_ptr相同,也是删除了拷贝构造和赋值重载函数,但是定义了移动构造函数,在调用过程中需要使用move函数,可以对右值进行接管,好处是用户可以感知之前的指针不能再对内存进行管理了,心中有数。

带引用计数的智能指针

自己实现:

template<typename T>
class RefCnt {//计数类
public:
    RefCnt<T>(T *ptr = nullptr) : _mptr(ptr) {
        if (_mptr != nullptr) {
            _mcount = 1;
        }
    }

    void addRef() { _mcount++; }

    int delRef() { return --_mcount; }

private:
    T *_mptr;
    int _mcount;
};

template<typename T>
class CSmartPtr {
public:
    CSmartPtr(T *ptr = nullptr) : _mptr(ptr) {
        _mRefPtr = new RefCnt<T>(_mptr);
    }

    ~CSmartPtr() {
        if (0 == _mRefPtr->delRef()) {
            delete _mptr;
            _mptr = nullptr;
        }
    }

    CSmartPtr<T>(const CSmartPtr<T> &src)
            : _mptr(src._mptr), _mRefPtr(src._mRefPtr) {
        if (_mptr != nullptr)
            _mRefPtr->addRef();
    }

    CSmartPtr<T> &operator=(const CSmartPtr<T> &src) {
        if (this == &src) {
            return *this;
        }
        if (0 == _mRefPtr->delRef()) {
            delete _mptr;
        }

        _mptr = src._mptr;
        _mRefPtr = src._mRefPtr;
        _mRefPtr->addRef();
        return *this;
    }

    T &operator*() {
        return *_mptr;
    }

    T *operator->() {
        return _mptr;
    }

private:
    T *_mptr;
    RefCnt<T> *_mRefPtr;
};

shared_ptr交叉引用问题

stl中提供了两个带有引用计数的智能指针

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

weak_ptr:弱智能指针,不会改变资源的引用计数

有这样一例子,是shared_ptr的交叉引用:

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

class B; // 前置声明类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> ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
	ptra->_ptrb = ptrb;// A对象的成员变量_ptrb也指向B对象,B的引用计数为2
	ptrb->_ptra = ptra;// B对象的成员变量_ptra也指向A对象,A的引用计数为2

	cout << ptra.use_count() << endl; // 打印A的引用计数结果:2
	cout << ptrb.use_count() << endl; // 打印B的引用计数结果:2

	/*
	出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
	B对象的引用计数从2减到1,达不到释放A和B的条件(释放的条件是
	A和B的引用计数为0),因此造成两个new出来的A和B对象无法释放,
	导致内存泄露,这个问题就是“强智能指针的交叉引用(循环引用)问题”
	*/
	return 0;
}

代码打印结果:
A()
B()
2
2
可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr

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

  1. weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
  2. weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
  3. weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源

那么上面的代码怎么修改,也就是如何解决带引用计数的智能指针的交叉引用问题,代码如下:

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

class B; // 前置声明类B
class A
{
public:
	A() { cout << "A()" << endl; }
	~A() { cout << "~A()" << endl; }
	weak_ptr<B> _ptrb; // 指向B对象的弱智能指针。引用对象时,用弱智能指针
};
class B
{
public:
	B() { cout << "B()" << endl; }
	~B() { cout << "~B()" << endl; }
	weak_ptr<A> _ptra; // 指向A对象的弱智能指针。引用对象时,用弱智能指针
};
int main()
{
    // 定义对象时,用强智能指针
	shared_ptr<A> ptra(new A());// ptra指向A对象,A的引用计数为1
	shared_ptr<B> ptrb(new B());// ptrb指向B对象,B的引用计数为1
	// A对象的成员变量_ptrb也指向B对象,B的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptra->_ptrb = ptrb;
	// B对象的成员变量_ptra也指向A对象,A的引用计数为1,因为是弱智能指针,引用计数没有改变
	ptrb->_ptra = ptra;

	cout << ptra.use_count() << endl; // 打印结果:1
	cout << ptrb.use_count() << endl; // 打印结果:1

	/*
	出main函数作用域,ptra和ptrb两个局部对象析构,分别给A对象和
	B对象的引用计数从1减到0,达到释放A和B的条件,因此new出来的A和B对象
	被析构掉,解决了“强智能指针的交叉引用(循环引用)问题”
	*/
	return 0;
}

代码打印如下:
A()
B()
1
1
~B()
~A
可以看到,A和B对象正常析构,问题解决!

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

有一个用C++写的开源网络库,muduo库,作者陈硕,大家可以在网上下载到muduo的源代码,该源码中对于智能指针的应用非常优秀,其中借助shared_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。

先看如下代码:

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

class Test
{
public:
	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
	Test() :_ptr(new int(20)) 
	{
		cout << "Test()" << endl;
	}
	// 析构Test对象,释放_ptr指向的堆内存
	~Test()
	{
		delete _ptr;
		_ptr = nullptr;
		cout << "~Test()" << endl;
	}
	// 该show会在另外一个线程中被执行
	void show()
	{
		cout << *_ptr << endl;
	}
private:
	int *volatile _ptr;
};
void threadProc(Test *p)
{
	// 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了
	std::this_thread::sleep_for(std::chrono::seconds(2));
	/* 
	此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。
	此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象
	存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题!
	*/
	p->show();
}
int main()
{
	// 在堆上定义共享对象
	Test *p = new Test();
	// 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p
	std::thread t1(threadProc, p);
	// 在main线程中析构Test共享对象
	delete p;
	// 等待子线程运行结束
	t1.join();
	return 0;
}

运行上面的代码,发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show方法,无法打印出*_ptr的值20。可以用shared_ptr和weak_ptr来解决多线程访问共享对象的线程安全问题,上面代码修改如下:

#include <iostream>
#include <thread>
#include <memory>
using namespace std;

class Test
{
public:
	// 构造Test对象,_ptr指向一块int堆内存,初始值是20
	Test() :_ptr(new int(20)) 
	{
		cout << "Test()" << endl;
	}
	// 析构Test对象,释放_ptr指向的堆内存
	~Test()
	{
		delete _ptr;
		_ptr = nullptr;
		cout << "~Test()" << endl;
	}
	// 该show会在另外一个线程中被执行
	void show()
	{
		cout << *_ptr << endl;
	}
private:
	int *volatile _ptr;
};
void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针
{
	// 睡眠两秒
	std::this_thread::sleep_for(std::chrono::seconds(2));
	/* 
	如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升
	为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
	的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
	已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
	*/
	shared_ptr<Test> ps = pw.lock();
	if (ps != nullptr)
	{
		ps->show();
	}
}
int main()
{
	// 在堆上定义共享对象
	shared_ptr<Test> p(new Test);
	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
	std::thread t1(threadProc, weak_ptr<Test>(p));
	// 在main线程中析构Test共享对象
	// 等待子线程运行结束
	t1.join();
	return 0;
}

运行上面的代码,show方法可以打印出20,因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。

如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:

int main()
{
	// 在堆上定义共享对象
	shared_ptr<Test> p(new Test);
	// 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针
	std::thread t1(threadProc, weak_ptr<Test>(p));
	// 在main线程中析构Test共享对象
	// 设置子线程分离
	t1.detach();
	return 0;
}

该main函数运行后,最终的threadProc中,show方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用

自定义删除器

标准库中的智能指针中默认的析构方法是delete p,如果使用智能指针指向数组或者是文件就需要自定义删除器。传统的方式是通过自定义函数对象作为删除器,这种方式会构造大量无关的代码,所以在C++11中引入了function和lambda表达式,减少代码量。至于为什么要用函数对象:通过函数对象调用operator(),可以省略函数的调用开销,比通过函数指针调用函数(不能够inline内联调用)效率高。(见8.C++STL倒数第二段)

unique_ptr<int,function<void(int*)>> ptr1(new int[100],
                                          [](int *p)->void{//使用lambda表达式
    cout<<"call lambda release int[100]"<<endl;
    delete[] p;
});

unique_ptr<FILE,function<void(FILE*)>> ptr2(fopen("data.txt","w"),
                                            [](FILE* p)->void {
    cout<<"call lambda release FILE*"<<endl;
    fclose(p);
});
内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/woden3702/p/16298875.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!