C/C++ ② —— C++11智能指针

1. 为什么要使用智能指针?

  • 智能指针可以解决忘记释放内存导致内存泄漏的问题;
  • 智能指针可以解决异常安全问题。

2. 智能指针的原理

  • RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
  • 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
    • 不需要显示地释放资源。
    • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

例子1:不使用智能指针

#include <iostream>
using namespace std;

int div(){
	int a, b;
	cin >> a >> b;
	if(b == 0){
		throw invalid_argument("除0错误");
	}
	return a / b;
}

void Func(){
	int *p1 = new int;
	cout << div() << endl;
	delete p1;
}

int main(){
	try{
		Func();
	}
	catch(exception& e){
		cout << e.what() << endl;
	}
	return 0;
}
  • new空间也有可能会抛出异常,对于 p1 如果抛出异常:没有问题,可以不管,直接到最外面去了。
  • 如果用户输入的除数为0,那么div函数就会抛出异常,跳到主函数的catch块中执行,此时Func()中的申请的内存资源还没有释放,就会发生内存泄漏。

例子2:在例子1基础上对new进行异常捕获

void Func(){
	int* p1 = new int;
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	delete p1;
}
  • 如果还要申请的p2,p3…这时候就需要套很多。因此要根本解决这个问题,可以使用智能指针。

例子3:使用智能指针

#include <iostream>
using namespace std;

template<class T>
class smart_ptr{
public:
	smart_ptr(T *ptr = nullptr):_ptr(ptr){ }
	~smart_ptr(){
		if(_ptr){
			cout << "delete: " << _ptr << endl;
			delete _ptr;
		}
	}
	T& operator*(){
		return *_ptr;
	}
	T* operator->(){
		return _ptr;
	}
	T& operator[](size_t pos){
		return _ptr[pos];
	}
private:
	T *_ptr;
};

int div(){
	int a, b;
	cin >> a >> b;
	if(b == 0){
		throw invalid_argument("除0错误");
	}
	return a / b;
}

void Func(){
	smart_ptr<int> sp1(new int);
	int *p2 = new int;
	smart_ptr<int> sp2(p2);
	cout << div() << endl;
}

int main(){
	try{
		Func();
	}
	catch(exception& e){
		cout << e.what() << endl;
	}
	return 0;
}
  • 在构造smart_ptr对象时,自动调用构造函数,将传入的需要管理的内存保存起来;
  • 在析构smart_ptr对象时,自动调用析构函数,将管理的内存空间进行释放
  • smart_ptr还可以与普通指针一样使用,需对*和->以及[]进行运算符重载
  • 问题
    • 如果用一个smart_ptr对象来拷贝构造另一个smart_ptr对象,或者一个smart_ptr对象赋值给另一个smart_ptr对象,最终结果会导致程序崩溃
    • 原因:编译器默认生成的拷贝构造函数对内置类型完成浅拷贝(值拷贝),单纯的浅拷贝会导致空间多次释放。

3. C++11中的几种智能指针

3.1 shared_ptr

  • 通过引用计数的方式解决智能指针的拷贝问题
  • shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据
  • shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。
  • shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。
int main(){
	shared_ptr<int> sp1(new int(1));
	shared_ptr<int> sp2(sp1);
	*sp1 = 10;
	*sp2 = 20;
	cout << sp1.use_count() << endl;  // 2
	shared_ptr<int> sp3(new int(1));
	shared_ptr<int> sp4(new int(2));
	sp3 = sp4;
	cout << sp3.use_count() << endl;  // 2
	return 0;
}

shared_ptr常用函数和基本方法

  • 初始化和reset
    • sp.reset():重置shared_ptr,reset()不带参数时,若智能指针sp是唯一指向该对象的指针,则释放,并置空。若智能指针sp不是唯一指向该对象的指针,则引用计数减少1,同时将sp置空
    • sp.reset(new int(200)):reset()带参数时,若智能指针sp是唯一指向对象的指针,则释放并指向新的对象。若sp不是唯一的指针,则只减少引用计数,并指向新的对象
shared_ptr<int> p1(new int(1));
shared_ptr<int> p2 = p1;
shared_ptr<int> p3;
p3.reset(new int(1));
  • 使用make_shared来构造智能指针更高效
auto sp1 = make_shared<int>(100);
或
shared_ptr<int> sp1 = make_shared<int>(100);

// shared_ptr<int> p = new int(1);  // 这是错误的,不能通过直接用原始赋值来初始化
  • 获取原始指针
shared_ptr<int> ptr(new int(1));
int *p = ptr.get();  // 返回shared_ptr中保存的裸指针
  • 指定删除器
    • 如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。
    • 当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。
#include <iostream>
#include <memory>
using namespace std;

void DeleteIntPtr(int *p){
	cout << "call DeleteIntPtr" << endl;
	delete p;
}

int main(){
	shared_ptr<int> p(new int(1), DeleteIntPtr);
	return 0;
}

或者使用lambda表达式

shared_ptr<int> p(new int(1), [](int*p)){
		cout << "call DeleteIntPtr" << endl;
		delete p; });
  • 当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象
shared_ptr<int> p3(new int[10], [](int *p) { delete [] p; });

shared_ptr线程安全问题

  • 管理同一个资源的多个对象共享引用计数,多个线程可能会同时对同一个个引用计数进行加或减,而自增或自减都不是原子操作,所以需要通过加锁对引用计数进行保护。
  • 通过加锁让引用计数的++、-- 操作变成原子操作,对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数封装。
template<class T>
class shared_ptr{
public:
	shared_ptr(T* ptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new 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();
	}
	// flag作用:当引用计数减到0时需要释放互斥锁,但是不能在临界区直接进行释放,因为后面还要解锁
	// 所以可以通过flag去标记,判断解锁后是否释放互斥锁资源
	void Release(){
		bool flag = false;
		_pmtx->lock();
		if (--(*_pcount) == 0){
			delete _pcount;
			delete _ptr;
			flag = true;
		}
		_pmtx->unlock();
		if (flag == true) delete _pmtx;
	}
	shared_ptr<T>& operator=(const shared_ptr<T>& sp){
		if (_ptr != sp._ptr){
			Release();
			_pcount = sp._pcount;
			_ptr = sp._ptr;
			_pmtx = sp._pmtx;
			_pmtx->lock();
			++(*_pcount);
			_pmtx->unlock();
		}
		return *this;
	}
	T& operator*(){
		return *_ptr;
	}
	T* operator->(){
		return _ptr;
	}
	T& operator[](size_t pos){
		return _ptr[pos];
	}
	int use_count(){
		return *_pcount;
	}
private:
	T* _ptr;
	int* _pcount;
	mutex* _pmtx;
};
  • shared_ptr本身是线程安全的(拷贝和析构时,引用计数++、-- 都是线程安全的),不需要保证管理的资源的线程安全问题;而shared_ptr管理资源的访问不是线程安全的,需要用的地方自行保护。
struct Date{
	int _year = 0;
	int _month = 0;
	int _day = 0;
};
void test_shared_ptr(){
	int n = 100000;
	mutex mtx;
	shared_ptr<Date> sp1(new Date);
	
	thread t1([&](){
		for (int i = 0; i < n; i++){
			shared_ptr<Date> sp2(sp1);
			mtx.lock();
			sp2->_year++;
			sp2->_month++;
			sp2->_day++;
			mtx.unlock();
		}
	});
	
	thread t2([&](){
		for (int i = 0; i < n; i++){
			shared_ptr<Date> sp3(sp1);
			mtx.lock();
			sp3->_year++;
			sp3->_month++;
			sp3->_day++;
			mtx.unlock();
		}
	});

	t1.join();
	t2.join();
	cout << sp1.use_count() << endl;

	cout << sp1->_year << endl;
	cout << sp1->_month << endl;
	cout << sp1->_day << endl;
}

注意事项

    1. 不要用一个原始指针初始化多个shared_ptr
    1. 不要在函数实参中创建shared_ptr
function(shared_ptr<int>(new int), g());  //有缺陷

// 正确做法应该是先创建智能指针
shared_ptr<int> p(new int);
function(p, g());
    1. 不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,这样可能会导致重复析构
class A{
public:
	shared_ptr<A> GetSelf(){ return shared_ptr<A>(this); }
	~A(){ cout << "Destructor A" << endl; }
};
int main(){
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2 = sp1->GetSelf();
	return 0;
}
  • 由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。
  • 正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr
class A: public enable_shared_from_this<A>{
public:
	shared_ptr<A> GetSelf(){ return shared_from_this(); }
	~A(){ cout << "Destructor A" << endl; }
};
int main(){
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2 = sp1->GetSelf();
	return 0;
}
    1. 避免循环引用,循环引用会导致内存泄漏
class A;
class B;

class A{
public:
	shared_ptr<B> bptr;
	~A(){ cout << "A is deleted" << endl; }
};

class B{
public:
	shared_ptr<A> aptr;
	~B(){ cout << "B is deleted" << endl; }
};

int main(){
	{
		shared_ptr<A> ap(new A);
		shared_ptr<B> bp(new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}
	cout<< "main leave" << endl;  // 循环引用导致ap bp退出了作用域都没有析构
	return 0;
}
  • 循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。

3.2 weak_ptr

  • share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
  • weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。
  • weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

weak_ptr 使用方法

shared_ptr<int> p1(new int(10));
weak_ptr<int> p2(p1);

// 通过use_count()方法获取当前观察资源的引用计数
cout << p2.use_count() << endl;   // count = 1

// 通过expired()方法判断所观察资源是否已经释放
if(p2.exxpired()) cout << "weak_ptr无效,资源已释放" << endl;
else cout << "weak_ptr有效" << endl;

// 通过lock方法获取监视的shared_ptr
weak_ptr<int> wp;
void func(){
	auto spt = wp.lock();
	if(wp.expired()) cout "weak_ptr无效,资源已释放" << endl;
	else cout << "weak_ptr有效,*spt=" << *spt << endl;
}
int main(){
	{
		auto sp = make_shared<int>(42);
		wp = sp;
		func();
	}
	func();
	return 0;
}

weak_ptr返回this指针

  • shared_ptr中提到不能直接将this指针返回shared_ptr,需要通过派生enable_shared_from_this类,并通过其方法shared_from_this来返回指针
    • 原因是enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针
    • 调用shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回
#include <iostream>
#include <memory>
using namespace std;

class A: public enable_shared_from_this<A>{
public:
	share_ptr<A>GetSelf(){ return shared_from_this(); }
	~A(){ cout << "Destructor A" << endl; }
};
int main(){
	shared_ptr<A> sp1(new A);
	shared_ptr<A> sp2 = sp1->GetSelf();
	return 0;
}
  • 在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题。
  • 获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

weak_ptr 解决循环引用问题

  • 只要将A或B的任意一个成员变量改为weak_ptr即可解决智能指针的循环引用导致内存泄漏的问题
class A;
class B;

class A {
public:
	weak_ptr<B> bptr;
	~A(){ cout << "A is deleted" << endl; }
};

class B{
public:
	shared_ptr<A> aptr;
	~B(){ cout << "B is deleted" << endl; }
};

int main(){
	{
		shared_ptr<A> ap(new A);
		shared_ptr<B> bp(new B);
		ap->bptr = bp;
		bp->aptr = ap;
	}
	return 0;
}

3.3 unique_ptr

  • unique_ptr独占对象的所有权,没有引用计数。同⼀时刻只能有⼀个unique_ptr指向给定对象,离开作⽤域时,若其指向对象,则将其所指对象销毁(默认delete)
  • 它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr
  • 定义unique_ptr时,需要将其绑定到⼀个new返回的指针上
  • unique_ptr不⽀持普通的拷⻉和赋值(因为拥有指向的对象),但是可以拷⻉和赋值⼀个将要被销毁的unique_ptr;可以通过release或者reset将指针所有权从⼀个(⾮const) unique_ptr转移到另⼀个unique
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = move(my_ptr);  // 正确
unique_ptr<T> my_other_ptr = my_ptr;  // 报错,不能复制
  • unique_ptr可以指向一个数组
unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
shared_ptr<int []> ptr2(new int[10]);  // 这个是不合法的
  • unique_ptr指定删除器和shared_ptr有区别
shared_ptr<int> ptr1(new int(1), [](int *p){delete p;});  // 正确
unique_ptr<int> ptr2(new int(1), [](int *p){delete p;});  // 错误
unique_ptr<int, void(*)(int*)> ptr3(new int(1), [](int *p){delete p;});  // 正确
  • 如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值