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

被折叠的 条评论
为什么被折叠?



