智能指针

目录

auto_ptr

scope_ptr(unique_ptr)

scoped_array

share_ptr

weak_ptr

多线程访问共享对象问题


智能指针是为了方便管理动态内存,防止产生内存泄漏,而对指针进行的一层封装,可以通过析构函数,构造函数,赋值运算符重载的处理,而实现的可以自动管理指针所指向的动态内存,防止产生内存泄漏。定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。智能指针:主要有Boost库中的智能指针和STL标准库中的智能指针构成。

所有智能指针编写代码时都需要注意的事:

  • 智能指针编写的时候,因为不知道指向的是何种类型的堆内存,所以都写成模板形式
  • 注意智能指针的构造以及析构函数的编写
  • 注意拷贝构造函数以及赋值运算符的重载函数的编写,这里可能导致多个智能指针指向同一块堆内存,防止同一块内存被多次释放导致系统崩溃。
  • 普通指针含有的功能,我们一般也要给智能指针加上,比如*解引用功能以及->指向功能。

auto_ptr

特点:管理权唯一,释放权也唯一(拷贝构造和赋值的方式,都会导致原有的指针失效)auto_ptr智能指针不带引用计数,那么它处理浅拷贝的问题,是直接把前面的auto_ptr都置为nullptr,只让最后一个auto_ptr持有资源。

缺点:

  1. 不能多个对象同时访问这个内存,所以我们可以给里面添加一个标志位flag进行优化
  2. 类似于链表中,由于所有权的转移,导致某些指针在预期失效前就失效了
  3. auto_ptr不能管理对象数组,因为auto_ptr的析构函数中是delete而不是delete[],当然这里我们可以进行模板的特例化对其进行优化。
template<class T>
class Auto_ptr
{
public:
    //构造函数
    Auto_ptr(T* p)
        :_ptr(p)
    {}

    //拷贝构造函数--管理权的转移--只改变指针的指向(浅拷贝)
    Auto_ptr(Auto_ptr<T>& ap){
        _ptr = ap._ptr;
        ap._ptr = NULL;
    }

    //赋值运算符重载
    Auto_ptr<T>& operator=(const Auto_ptr<T>& ap){
        //防止自己给自己赋值
        if (*this != *ap){
            //释放旧空间
            if (_ptr){
                delete _ptr;
            }
            _ptr = ap._ptr;
            ap._ptr = NULL;
        }
        return *this;
    }

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

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

    //析构函数
    ~Auto_ptr<T>(){
        if (_ptr)
            delete _ptr;
    }


private:
    T* _ptr;

};

scope_ptr(unique_ptr)

scoped_ptr其实是对auto_ptr的改进,禁止拷贝,即一个空间只能被一个对象管理:所以这时就引入了Boost库中的scope_ptr,这个智能指针很简单,它不允许权限进行转移,屏蔽了拷贝构造函数和赋值运算符的重载函数,将其在私有下写成声明即可。当然scope_ptr是boost库中的叫法,引入到STL标准库中后就叫做unique_ptr,当然进行了简单的修改。

区别:

  • scope_ptr:不允许复制,也不允许转移所有权。
  • unique_ptr:不允许复制,但是允许转移所有权,比如通过return返回值的方式或者通过STL标准库中的move函数进行转移所有权。
template <typename T>
class Scope_ptr
{
public:
	Scope_ptr(T* ptr) :mptr(ptr){}
	~Scope_ptr()
	{
		delete mptr;
		mptr = NULL;
	}
	T& operator* ()
	{
		return *mptr;
	}
	T* operator-> ()
	{
		return mptr;
	}
private:
	Scope_ptr(const Scope_ptr<T>& rhs);
	Scope_ptr<T>& operator= (const Scope_ptr<T>& rhs);
	T* mptr;
};

scoped_array

scoped_array 和 scoped_ptr的功能是一样的,只是scoped_array管理的对象是数组,需要重载[]的形式。

template<class T>
class Scoped_array{
public:
    //构造函数
    Scoped_array(T* p){
        _ptr = p;
    }

    T& operator[](size_t i){
        return _ptr[i];
    }

    //析构函数
    ~Scoped_array<T>(){
        if (_ptr)
            delete[] _ptr;
    }


private:
    //拷贝构造
    Scoped_array(const Scoped_array<int>& s);
    //赋值运算符的重载
    Scoped_array& operator=(const Scoped_array<int>& s);
    T* _ptr;
};

share_ptr

shared_ptr:带有引用计数的智能指针,shared_ptr智能指针的资源引用计数器在内存的heap堆上

引用计数:有多少个智能指针对象管理这个堆内存。

这个引用计数不能写成普通的成员变量,因为成员变量是每个对象私有的,而这里需要让所有智能指针对引用计数进行共享。

每创建一个share_ptr对象,给该对象模型的引用计数加一,用来标记此时这个空间有多少个指针指向。

在每次构造函数里,对前四个字节里的值,加一;在析构函数里,先最前四个字节里的值减一,再判断前四个字节里的值是否为0,若为0,则释放内存空间。

template<class T>
class Share_ptr
{
public:
    //构造函数
    Share_ptr(const T* p)
        :_ptr(p)
        , _pCount(new int(1))
    {}

    Share_ptr(Share_ptr<T>* sp, int* spCount)
        :_ptr(sp)
        , _pCount(spCount)
    {}

    //拷贝构造函数
    Share_ptr(const Share_ptr<T>& sp){
        _ptr = sp._ptr;
        _pCount = sp._pCount;
        ++(*_pCount);
    }

    //赋值运算符重载
    Share_ptr& operator=(Share_ptr<T>& sp){
        if (this != &sp){
            if (--(*_pCount) == 0){
                delete _ptr;
                delete _pCount;
            }
            _ptr = sp._ptr;
            _pCount = sp._pCount;
            ++(*_pCount);
        }
        return *this;
    }

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

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

    ~Share_ptr(){
        if (--(*_pCount) == 0){
            delete _ptr;
            delete _pCount;
        }
    }

private:
    T* _ptr;
    int* _pCount;

};

接下来我们看一下强智能指针会存在的问题。强智能指针的交叉引用(循环引用)!!!

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对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。

weak_ptr

弱指针是指当指针指向原来空间时,引用计数不在进行加1,不是管理和释放当前的指针,而是避免了一次引用计数。

template <typename T>
class Weak_ptr
{
public:
	Weak_ptr(T* ptr = NULL) :mptr(ptr)
	{
		std::cout << "Weak_ptr()" << std::endl;
	}
	Weak_ptr(const Weak_ptr<T>& rhs)
	{
		mptr = ptr;
	}
	Weak_ptr<T>& operator= (const Weak_ptr<T>& rhs)
	{
		std::cout << "operator= (Weak_ptr)" << std::endl;
		if(this != &rhs)
		{
			mptr = rhs.mptr;
		}
		return *this;
	}
	Weak_ptr<T>& operator= (const Shared_ptr<T>& rhs)
	{
		std::cout << "operator= (Shared_ptr)" << std::endl;
		//mptr = rhs.mptr;//错误 在Weak_ptr类中看不到Shared_ptr<T>& rhs这个类中的mptr  所以得通过接口GetPtr()来获取其mptr值
		//而上面的Weak_ptr<T>& operator= (const Weak_ptr<T>& rhs)函数中,由于this和rhs一样是Weak_ptr类,所以不存在访问私有成员的事
		mptr = rhs.GetPtr();//这块肯定不用判断自赋值
		return *this;
	}
	~Weak_ptr()
	{
		std::cout << "~Weak_ptr()" << std::endl;
		//弱智能指针析构函数什么都不做,释放操作交给强智能指针
	}
 
	T* operator-> ()
	{
		return mptr;
	}
	T& operator* ()
	{
		return *mptr;
	}
private:
	T* mptr;
};

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

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

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

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;
}

多线程访问共享对象问题

借助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来解决多线程访问共享对象的线程安全问题,上面代码修改如下:


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的一个典型应用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值