C++智能指针/RAII/资源管理

本文介绍C++中智能指针的概念及其在资源管理中的应用,包括shared_ptr、unique_ptr、weak_ptr的特点及使用场景,并探讨如何利用资源管理类实现RAII原则。

0. 概述:

所谓资源就是,一旦用了它,将来必须还给系统。

C++程序中最常用的资源就是动态内存(如果分配内存后不归还,将会导致内存泄露),另外会经常用到的资源还包括:文件描述符、互斥锁、socket套接字、数据库连接 等。

不论是哪种资源,当你不再使用它时,必须将它还给系统。

通过将资源管理建立在C++的构造函数、析构函数、拷贝控制函数的基础之上,可以几乎消除资源管理问题。

1. 以对象管理资源:

以对象管理资源,是指“以RAII对象管理动态内存资源”(RAII = Resource Acquisition Is Initialization,资源获取即初始化)。 在应用程序获取到系统资源后(堆内存、套接字、互斥锁等),立即使用其初始化一个类对象,从而实现在资源获取时初始化类对象、在类对象析构时释放资源。

典型的RAII类型例如shared_ptr智能指针,在构造函数中获得资源(堆内存),并在析构函数中释放资源。

1.1 关于 智能指针:

C++11中主要提供三种智能指针:
shared_ptr 、 unique_ptr 、 weak_ptr。

智能指针在退出作用域释放对象时,不仅将其归还系统,还会将内部保存的指针置为空,所以如果再对其经行操作将会因访问空指针而导致段错误。(相比之下,delete只是将堆内存资源归还系统,而没有置指针为空)

1.1.1 shared_ptr :

shared_ptr 允许多个指针指向同一对象。

shared_ptr 和 unique_ptr 都支持的操作:

shared_ptr<T> sp		空智能指针,可以指向类型为T的对象
unique_ptr<T> up	
p						可以用作一个条件判断,若p指向一个对象,则true
*p						解引用p,获得它指向的对象
p->mem					等价于 (*p).mem
p.get()					返回p中保存的指针。要小心使用:若智能指针释放了其对象,之前返回的指针所指向的对象也就消失了
swap(p, q)				交换p和q中的指针
p.swap(q)

shared_ptr 独有的操作:

make_shared<T>(args)
shared_ptr<T> p(q)
p = q

p.unique()				若p.use_count()1,返回true;否则返回false
p.use_count()			返回与p共享对象的智能指针数量:可能很慢,主要用于调试!!

shared_ptr使用举例:

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

int main() {
	shared_ptr<int> ptr = make_shared<int>(42);
	//或者使用new初始化:shared_ptr<int> ptr(new int(42));

	shared_ptr<int> p(ptr);
	cout << ptr.use_count() << endl;	//=2;

	return 0;
}

1.1.2 make_shared 与 new 的区别:

new会导致内存碎片化,make_shared则不会。
① new: 先new后赋值的方式,是先在堆上分配一块内存,然后在堆上再建一个智能指针控制块,这两个东西是不连续的,会造成内存碎片化;
② make_shared: make_shared的方式是直接在堆上新建一块足够大的内存,其中包含两部分,上面是内存(用来使用),下面是控制块(包含引用计数),然后用T的构造函数去初始化分配的内存。

1.2 unique_ptr :

unique_ptr 独占其所指对象。
unique_ptr 必须采用直接初始化,尝试对其进行拷贝或赋值将会在编译阶段报错;
没有一个类似于make_shared的函数用于初始化unique_ptr,unique_ptr只能绑定到一个new返回的指针上。

unique_ptr 使用举例:

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

int main() {
	unique_ptr<int> ptr(new int(42));
	unique_ptr<int> p(ptr);			//编译时将会报错
	unique_ptr<int> q = ptr;		//编译时将会报错

	return 0;
}

1.2.1 auto_ptr :

在C++11之前,auto_ptr 的作用与 unique_ptr 类似:独占所指对象。
在C++11之后,auto_ptr已经被弃用。

auto_ptr与unique_ptr的区别在于:
如果尝试对 auto_ptr/unique_ptr 进行拷贝或赋值,unique_ptr 会直接在编译阶段报错;
而auto_ptr却可以编译通过,只有在运行阶段,且访问到由于被拷贝而导致失去了对象控制权后变成空指针的unique_ptr后,程序才会报错:段错误,访问空指针。

auto_ptr 使用举例:

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

int main() {
	auto_ptr<int> ptr(new int(42));
	auto_ptr<int> p(ptr);
	
	cout << *p << endl; 	// 程序运行到这一步都不会有任何问题,如果没有后面的访问 *ptr,则程序甚至可以正常运行
	
	cout << *ptr << endl;	// 这一句将会导致程序段错误:访问空指针
	
	return 0;
}

运行结果:

segmentation fault

1.3 weak_ptr :

weak_ptr 支持的操作:

weak_ptr<T> w		空weak_ptr可以指向类型为T的对象
weak_ptr<T> w(sp)	与shared_ptr sp指向相同对象的weak_ptr,T必须能转换为sp指向的类型
w = p				p可以是一个shared_ptr或一个weak_ptr,赋值后w与p共享对象
w.reset()			将w置空(相当于在退出作用域自动释放前提前释放)

//由于weak_ptr是弱绑定,不能独立判断所指对象是否已经被释放,所以需要借助其绑定的shared_ptr进行判断对象是否仍然存在
//以下3个操作用于判断weak_ptr绑定的shared_ptr所指向的对象是否仍存在:
w.use_count()		与w共享对象的shared_ptr的数量
					# weak_ptr所绑定的shared_ptr的引用计数
w.expired()			如果w.use_count()0,返回true,否则返回false
					# 判断weak_ptr所绑定的shared_ptr是否存在,返回bool值
w.lock()			如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
					# 判断weak_ptr所绑定的shared_ptr是否存在,如果不存在则返回一个空的shared_ptr;如果存在则返回该对象的shared_ptr

weak_ptr 使用举例:

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

int main() {
	shared_ptr<int> p3 = make_shared<int>(4);
	weak_ptr<int> p4(p3);

	// cout << *p4 << endl;	//错误!不能直接访问weak_ptr,因为无法确定其所指对象是否已经被释放掉
	
	if(shared_ptr<int> np = p4.lock()) {	//在访问weak_ptr指向的对象之前,必须通过 .lock() 取得其绑定的shared_ptr
		cout << *np << endl;
	}
	else {
		cout << "null" << endl;
	}

	return 0;
}

weak_ptr 使用要点:

weak_ptr<int> wp(ptr);

if( shared_ptr<int> np = wp.lock() ) {
	//weak_ptr.lock();返回的shared_ptr 非空,才能继续访问
}

注意 make_shared 仅 支持对 shared_ptr类型进行初始化,unique_ptr需要使用new初始化(且unique_ptr必须采用直接初始化,尝试对其拷贝或赋值将会在编译阶段报错),weak_ptr需要使用一个shared_ptr初始化。

2. 使用智能指针管理资源(RAII):

使用new、delete手动管理动态内存可能导致的问题:
delete的时机可能出错:如果忘记了对申请的动态内存进行释放,则会导致内存泄露;如果delete过早,则导致程序访问已被释放的内存(踩内存)。

而即使delete的时机正确,也可能因为某些以外而导致内存无法得到释放,例如:

void func() {
	Investment *pInv = createInvestment();
	...
	delete pInv;
}

如果 “…” 区域内一个过早的return、或者goto等跳过了delete,则会导致内存泄露(即使现在不会,后续的代码维护、优化、修改,也能引入意料之外的问题)。

解决new、delete可能引入的问题的方法是 使用 “类指针对象”(即智能指针)对动态内存进行管理:

void func() {
	shared_ptr<Investment*> pInv(createInvestment());
}

2.1 智能指针中只有share_ptr可以用来实现RAII:

“以对象(类指针对象)管理资源” 的观念便被称为 RAII(Resource Acquisition Is Initialization),“资源获得时机即是初始化时机”

注意这里不能使用unique_ptr或者auto_ptr,因为它们无法实现多个指针指向同一对象。

3. 使用资源管理类实现RAII:

对于简单的堆内存来说,使用智能指针对其进行资源管理即可,例如:

void func() {
	shared_ptr<Investment> pInv(createInvestment());
}

然而对于有些资源来说,它们并不是堆内存,例如互斥锁mutex,属于系统资源。
这种资源显然不能通过依靠智能指针的 “退出作用域时自动调用对象的析构函数delete释放内存” 的方式来实现资源管理,因为mutex的释放并不是 delete + ptr,而是 pthread_mutex_destroy(&pthread_mutex_t) 函数,此时则需要建立自己的资源管理类。

这样的class的基本结构由 RAII 守则支配,也就是:“资源在构造期间获得,在析构期间释放”。 以保证不会在使用后忘记对mutex解锁。

实现示例:

class Mutex;

class Lock {
public:
	explicit Lock(Mutex *pm) : mutexPtr(pm) {
		pthread_mutex_lock(mutexPtr);		//在构造函数中 上锁
	}
	~Lock() {
		pthread_mutex_unlock(mutexPtr);		//在析构函数中 解锁
	}
private:
	Mutex	*mutexPtr;
};

class Mutex {
public:
	Mutex() {
		pthread_mutex_init(&mutex, NULL);	//在构造函数中 初始化锁
	}
	~Mutex() {
		pthread_mutex_destroy(&mutex);		//在析构函数中 释放锁
	}
private:
	pthread_mutex_t		mutex;
};

客户对Lock的使用方式:

void func() {
	Mutex m;	
	{
		Lock ml(&m);	
	}
}

有一个特殊情况需要考虑:当 Lock 对象被被复制时,会发生什么?

Lock ml2(ml1);

当一个RAII对象被复制,大多数情况下可以有两种供选择的处理方式:
① 禁止复制。
② 对底层资源进行“引用计数”。

对于互斥锁这样的资源,显然我们不希望其被复制,所以应该将用于管理资源的类的拷贝控制函数声明为 “=delete;” 或者 声明为 private。

3.1 小结:

RAII 意味着用对象来管理资源,以达到对象退出作用域时自动释放资源的目的。

而对于系统资源,除了常用的 动态内存之外,还有 互斥锁、信号量这一类型的资源。

管理动态内存可以借助 shared_ptr 智能指针:用申请到的动态内存去初始化一个shared_ptr,当shared_ptr退出作用域时释放内存;
管理互斥锁、信号量这类资源则需要自行实现一个类,因为这类资源的释放并不是“delete释放”,而是需要在管理类的析构函数用调用释放锁的API函数。

4. 成对使用new 和 delete时要采取形同形式:

如果在new表达式中使用 [ ] ,那么必须在相应的delete表达式中也使用 [ ] 。如果你在new表达式中不使用 [ ] ,一定不要再相应的delete表达式中使用 [ ] 。

当调用 new 时,实际上有两件事发生:
① 第一,内存被分配出来;
② 第二,针对此内存有一个(或多个)构造函数被调用。

当调用 delete 时,实际上也是有两件事发生:
① 第一,针对此内存会有一个(或多个)析构函数被调用;
② 第二,内存被释放。

因此,当分配/释放一个数组形式的内存时,要注意调用new和delete的形式:

string *stringArray = new string[100];
delete[] stringArray;		//表明是要删除一个由对象组成的数组

Base *bptr = new Base[4];	//构造4个Base类型对象
delete[] bptr;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值