以对象管理资源
- 为防止资源泄漏,使用RAII思想构建资源管理类,在构造函数中获取资源,在析构函数中释放资源
- 可以使用智能指针管理资源
手动管理资源容易造成泄漏
struct Payment {
Payment() {
printf("Construct Payment\n");
}
~Payment() {
printf("Deconstruct Payment\n");
}
};
static Payment *getPayment() {
static Payment *instance = new Payment;
return instance;
}
int main() {
Payment *p1 = getPayment();
Payment *p2 = getPayment();
cout << p1 << " " << p2 << endl;
}
/*
Construct Payment
0x1b40b081910 0x1b40b081910
*/
上述通过工厂函数获取到对象指针,但是存在问题
- 容易忘记delete
- 容易重复delete同一块地址(可见,这里delete了p1,那么p2呢?)
通过智能指针等RAII类来管理资源
智能指针对heap-based对象有奇效
- 获得资源后立即放进管理对象内。即RAII:Resource Acquisition Is Initialization(资源获取即初始化)。
- 管理对象使用析构函数来释放资源。即离开作用域后就释放资源
int main() {
shared_ptr<Payment> ptr(getPayment());
{
auto ptr2 = ptr;
cout << ptr.use_count() << endl;
}
cout << ptr.use_count() << endl;
}
/*
Construct Payment
2
1
Deconstruct Payment
*/
但是要注意避免循环引用,造成资源泄漏
class B;
struct A {
shared_ptr<B> ptr;
~A() {
printf("~A\n");
}
};
struct B {
shared_ptr<A> ptr;
~B() {
printf("~B\n");
}
};
int main() {
shared_ptr<A> pA(new A);
shared_ptr<B> pB(new B);
pA->ptr = pB;
pB->ptr = pA;
}
// 没有析构函数被调用
解决办法: 将类成员的shared_ptr
改用为弱引用指针weak_ptr
即可
注意智能指针的默认析构方法是delete
智能指针默认的析构方法是delete,所以以下函数可以正确编译,但是不能正确释放。
shared_ptr<int> p(new int[3]);
解决办法: 自定义释放器
struct A {
static int v;
int selfV;
A() : selfV(v++) {}
~A() {
printf("~A %d\n", selfV);
}
};
int A::v = 0;
void deleter(A *a) {
printf("call deleter B\n");
delete[]a;
}
int main() {
A *a = new A[2];
shared_ptr<A> ptr(a, [](A *toD) {
printf("call deleter A\n");
delete[]toD;
});
shared_ptr<A> p2(new A[2], deleter);
}
/*
call deleter B
~A 3
~A 2
call deleter A
~A 1
~A 0
*/
在资源管理类中小心copy行为
- 拷贝RAII对象必须考虑其管理的资源,针对其资源做出拷贝行为的实现
- 常见的RAII对象拷贝行为:拒绝拷贝、引用计数法、深拷贝、资源所有权转移
并非所有资源都是基于堆的(heap-based),对于这种对象不能直接使用智能指针,需要自定义其资源管理类。
例子:锁
为了说明锁的资源管理行为,我们这里给定义一个锁,来替代C++里的锁
struct MyMutex {
MyMutex() {
printf("Construct MyMutex\n");
}
~MyMutex() {
printf("Deconstruct MyMutex\n");
}
};
其上锁解锁行为:
void lock(MyMutex *) {
printf("lock\n");
}
void unlock(MyMutex *) {
printf("unlock\n");
}
锁的资源管理类,在构造函数获取资源(加锁),在析构函数释放资源(解锁):
struct Lock {
private:
MyMutex *myMutex;
public:
explicit Lock(MyMutex *mutex) : myMutex(mutex) {
lock(myMutex);
}
~Lock() {
unlock(myMutex);
}
};
使用:
int main() {
MyMutex myMutex;
{
printf("---------\n");
Lock lk(&myMutex);
printf("---------\n");
// 离开代码块将自动析构局部对象,因此会释放锁
}
}
/*
Construct MyMutex
---------
lock
---------
unlock
Deconstruct MyMutex
*/
潜在风险,如果发生了拷贝行为:
Lock l1(&mutex);
Lock l2(l1);
那么将立即死锁(Linux里一般是非递归锁,重复加锁会造成死锁)
禁止复制
继承nocopyable
,或者将拷贝相关函数设置为delete
对底层资源使用引用计数法
思想:维护一个计数器,当最后一个使用者被销毁时,才真正释放资源
- 维护计数器
- 使用智能指针管理资源
注意:智能指针释放资源的默认行为是delete,但是这里我们是想要解锁即可,不必将锁释放,这个时候就可以用到智能指针的自定义删除器Deleter
struct Lock {
private:
shared_ptr<MyMutex> mutexPtr;
public:
// 将unlock函数设置为删除器
explicit Lock(MyMutex *mutex) : mutexPtr(mutex, unlock) {
lock(mutexPtr.get());
}
// 不必声明析构函数,因为mutexPtr是栈上对象,所以会被默认释放,那么智能指针就会调用其释放器unlock
};
复制底部资源(深拷贝)或者转移资源管理权(移动语义)
在资源管理类中提供对原始资源的访问
- API常需要要求访问原始资源,所以RAII资源管理类应该提供访问原始资源的接口
- 对原始资源可以由显示转换或者隐式转换获得.其在安全性和方便性上各有取舍.
智能指针提供了get接口来访问原始资源
在其中要注意,不可以get一个智能指针去初始化另一个智能指针,否则会发生重复释放
int main() {
shared_ptr<MyMutex> p1 = make_shared<MyMutex>();
{
shared_ptr<MyMutex> p2(p1.get());
cout << p1.use_count() << " " << p2.use_count() << endl;
// 1 1
// p2离开代码块,释放其管理的资源,p1指针指向被释放的内存
}
}
程序将异常退出
显式/隐式资源转换
struct Resource {
string str;
Resource(string s) : str(s) {}
};
struct Manager {
private:
Resource *resource;
public:
Manager(Resource *r) : resource(r) {}
~Manager() {
delete resource;
}
// 显式转换,获得其资源
Resource *getResource() {
return resource;
}
// 隐式转换,获得其资源
operator Resource *() {
return resource;
}
};
void useResource(Resource *resource) {
cout << resource->str << endl;
}
int main() {
Manager manager(new Resource("Hello"));
useResource(manager.getResource());
Manager manager1(new Resource("World"));
useResource(manager1);
}
/*
Hello
World
*/
一般推荐使用显式转换(get),避免潜在错误
成对使用new和delete时采用相同的形式
- 如果在new中使用了[],那么同样需要delete []来释放。反之亦然
在使用new时,有两件事发生:
- 调用operator new函数来分配内存空间
- 针对此内存调用一个或多个构造函数
在使用delete时,发生: - 针对此内存调用一个或多个析构函数
- 调用operator delete函数来释放空间
如何知道delete []需要释放多少内存
通过new []构造的内存布局一般如下(不同编译器的实现方式可能不同):
n
−
O
b
j
e
c
t
1
−
O
b
j
e
c
t
2
−
.
.
.
−
O
b
j
e
c
t
n
n - Object_1 - Object_2 - ... - Object_n
n−Object1−Object2−...−Objectn,其中n是数组的长度。
对new调用delete[]或对new []调用delete都是未定义行为。
所以在不同的构造函数中,对于对应的指针应采用相同的new方式,不然的话怎么决定析构函数如何析构该资源呢?
特别是使用了自定义类型,容易造成错误释放
int main() {
typedef Base Base_Array_4[4];
Base *base = new Base_Array_4;
// 正确
delete[] base;
}
这里初始化的是一个数组,所以需要delete []
以独立语句将new出来的对象放入智能指针(对象管理类)
- 以独立语句将newed对象置入到智能指针去,否则异常不安全,存在潜在的资源泄露可能性
- 或者使用make_shared来初始化智能指针
func(shared_ptr<Resource>(new Resource), f());
是异常不安全的,因为f()
的执行次序不确定。
new Resource
一定先于智能指针的构造函数之前被调用。但是f()
的调用次序可能会在两者之前、之间、之后。
如果f()
在两者之间调用,然后发生了异常抛出后,new Resource
的资源就没有被接管,可能会造成资源泄露。
改写为:
shared_ptr<Resource> pr(new Resource);
func(pr, f());
或者
func(make_shared<Resource>(构造函数参数), f());