Resource Acquisition Is Initialization,它是一种C++编程技术,它通过在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确获取和释放。
一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。
RAII 的用法是在构造函数中获取资源,在析构函数中释放资源,并使用栈上的对象或者智能指针来管理堆上的资源。C++ 标准库提供了许多 RAII 类型,如 std::fstream、std::unique_ptr、std::lock_guard 等,我们可以直接使用它们或者参考它们来实现自己的 RAII 类型。
遵循RAII设计原则的好处有以下几点:
智能指针的特点:
std::auto_ptr?的基本用法如下代码所示:
#include <memory>
int main()
{
//初始化方式1
std::auto_ptr<int> sp1(new int(8));
//初始化方式2
std::auto_ptr<int> sp2;
sp2.reset(new int(8));
return 0;
}
// sp 是 smart pointer(智能指针)的简写。
智能指针对象?sp1?和?sp2?均持有一个在堆上分配 int 对象,其值均是 8,这两块堆内存均可以在?sp1?和?sp2?释放时得到释放。这是?std::auto_ptr?的基本用法。
std::auto_ptr?真正让人容易误用的地方是其不常用的复制语义,即当复制一个?std::auto_ptr?对象时(拷贝复制或 operator = 复制),原对象所持有的堆内存对象也会转移给复制出来的对象。示例代码如下:
#include <iostream>
#include <memory>
int main()
{
//测试拷贝构造
std::auto_ptr<int> sp1(new int(8));
std::auto_ptr<int> sp2(sp1);
if (sp1.get() != NULL)
{
std::cout << "sp1 is not empty." << std::endl;
}
else
{
std::cout << "sp1 is empty." << std::endl;
}
if (sp2.get() != NULL)
{
std::cout << "sp2 is not empty." << std::endl;
}
else
{
std::cout << "sp2 is empty." << std::endl;
}
//测试赋值构造
std::auto_ptr<int> sp3(new int(8));
std::auto_ptr<int> sp4;
sp4 = sp3;
if (sp3.get() != NULL)
{
std::cout << "sp3 is not empty." << std::endl;
}
else
{
std::cout << "sp3 is empty." << std::endl;
}
if (sp4.get() != NULL)
{
std::cout << "sp4 is not empty." << std::endl;
}
else
{
std::cout << "sp4 is empty." << std::endl;
}
return 0;
}
// 结果如下:
sp1 is empty.
sp2 is not empty.
sp3 is empty.
sp4 is not empty.
上述代码中分别利用拷贝构造(sp1 => sp2)和 赋值构造(sp3 => sp4)来创建新的 std::auto_ptr 对象,因此 sp1 持有的堆对象被转移给 sp2,sp3 持有的堆对象被转移给 sp4。
导致的问题是:管理权的转移,比如将sp1拷贝给sp2,也就是将sp1对于资源的管理权转移给了sp2,即将sp1的指针置空,带来的问题是:后面的代码不能使用sp1对象。由于这个问题的存在auto_ptr指针被很多人诟病,并且许多公司明确要求不能使用auto_ptr指针,因为管理权转移导致了原指针不能使用,相比后续的智能指针,auto_ptr确实是个失败的设计。
之后c++11引入了三种指针:std::unique_ptr、std::shared_ptr?和?std::weak_ptr。
std::unique_ptr解决智能指针拷贝问题的方案是:禁止拷贝
std::unique_ptr?对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1。std::unique_ptr?对象销毁时会释放其持有的堆内存。可以使用以下方式初始化一个std::unique_ptr?对象:
//初始化方式1
std::unique_ptr<int> sp1(new int(123));
//初始化方式2
std::unique_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
std::unique_ptr<int> sp3 = std::make_unique<int>(123);
应该尽量使用初始化方式 3 的方式去创建一个 std::unique_ptr 而不是方式 1 和 2,因为形式 3 更安全,原因 Scott Meyers 在其《Effective Modern C++》中已经解释过了
令很多人对 C++11 规范不满的地方是,C++11 新增了 std::make_shared() 方法创建一个 std::shared_ptr 对象,却没有提供相应的 std::make_unique() 方法创建一个 std::unique_ptr 对象,这个方法直到 C++14 才被添加进来。当然,在 C++11 中你很容易实现出这样一个方法来:?
template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&& ...params)
{
return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}
鉴于?std::auto_ptr?的前车之鉴,std::unique_ptr?禁止复制语义,为了达到这个效果,std::unique_ptr?类的拷贝构造函数和赋值运算符(operator =)被标记为?delete。
template <class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr<T>& p) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& p) = delete;
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
在拷贝构造函数和赋值重载函数后加上delete,表示不能调用该函数,并且不能定义该函数。unique用这种简单粗暴的方式解决智能指针的拷贝问题,带来的缺陷是:unique独自占有资源,即不能使用拷贝构造和赋值,每一个unique都指向不同的资源。
禁止复制语义也存在特例,即可以通过一个函数返回一个 std::unique_ptr:
#include <memory>
std::unique_ptr<int> func(int val)
{
std::unique_ptr<int> up(new int(val));
return up;
}
int main()
{
std::unique_ptr<int> sp1 = func(123);
return 0;
}
上述代码从 func 函数中得到一个?std::unique_ptr?对象,然后返回给 sp1。
既然?std::unique_ptr?不能复制,那么如何将一个?std::unique_ptr?对象持有的堆内存转移给另外一个呢?答案是使用移动构造,示例代码如下:
#include <memory>
int main()
{
std::unique_ptr<int> sp1(std::make_unique<int>(123));
std::unique_ptr<int> sp2(std::move(sp1));
std::unique_ptr<int> sp3;
sp3 = std::move(sp2);
return 0;
}
以上代码利用 std::move 将 sp1 持有的堆内存(值为 123)转移给 sp2,再把 sp2 转移给 sp3。最后,sp1 和 sp2 不再持有堆内存的引用,变成一个空的智能指针对象。并不是所有的对象的 std::move 操作都有意义,只有实现了移动构造函数(Move Constructor)或移动赋值运算符(operator =)的类才行,而?std::unique_ptr?正好实现了这二者,以下是实现伪码:
template<typename T, typename Deletor>
class unique_ptr
{
//其他函数省略...
public:
unique_ptr(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
}
unique_ptr& operator=(unique_ptr&& rhs)
{
this->m_pT = rhs.m_pT;
//源对象释放
rhs.m_pT = nullptr;
return *this;
}
private:
T* m_pT;
};
std::unique_ptr?不仅可以持有一个堆对象,也可以持有一组堆对象,示例如下:
#include <iostream>
#include <memory>
int main()
{
//创建10个int类型的堆对象
//形式1
std::unique_ptr<int[]> sp1(new int[10]);
//形式2
std::unique_ptr<int[]> sp2;
sp2.reset(new int[10]);
//形式3
std::unique_ptr<int[]> sp3(std::make_unique<int[]>(10));
for (int i = 0; i < 10; ++i)
{
sp1[i] = i;
sp2[i] = i;
sp3[i] = i;
}
for (int i = 0; i < 10; ++i)
{
std::cout << sp1[i] << ", " << sp2[i] << ", " << sp3[i] << std::endl;
}
return 0;
}
// 结果如下:
0, 0, 0
1, 1, 1
2, 2, 2
3, 3, 3
4, 4, 4
5, 5, 5
6, 6, 6
7, 7, 7
8, 8, 8
9, 9, 9
std::shared_ptr?和?std::weak_ptr?也可以持有一组堆对象,用法与?std::unique_ptr?相同。
shared_ptr允许拷贝智能指针,其采用了计数的方式进行拷贝,只有一个指针指向资源时,计数为1,两个指针指向,计数为2,以此类推。对象析构时,只要将计数减一,如果减一后的计数为0,说明该对象是指向资源的最后一个对象,需要完成资源的释放。
注意:shared_ptr中又存储了一个指向_count的指针,在拷贝构造后,两对象的_ptr指向同一块空间,需要将_count的值+1,达到计数增加的效果。
?
template <class T>
class shared_ptr
{
public:
shared_ptr(T* ptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& p)
{
_ptr = p._ptr;
_pCount = p._pCount;
(*_pCount)++;
}
void Release()
{
if (--(*_pCount) == 0) // 当计数为0,需要释放pCount
{
if (_ptr) // 如果_ptr为空,只要释放pCount
{
delete _ptr;
_ptr = nullptr;
}
cout << "~shared_ptr()" << endl;
delete _pCount;
_pCount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& p)
{
// 指向资源不同时,使两指针指向同一资源,并且计数增加
if (_ptr != p._ptr) // 当指向资源相同时,没有必要进行赋值
{
Release(); // 先释放该指针之前指向的空间
_ptr = p._ptr;
_pCount = p._pCount;
* _pCount++;
}
}
~shared_ptr()
{
Release();
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
int* _pCount;
};
下面是一个初始化?std::shared_ptr?的示例:
//初始化方式1
std::shared_ptr<int> sp1(new int(123));
//初始化方式2
std::shared_ptr<int> sp2;
sp2.reset(new int(123));
//初始化方式3
std::shared_ptr<int> sp3;
sp3 = std::make_shared<int>(123);
例子:
#include <iostream>
#include <memory>
class A
{
public:
A()
{
std::cout << "A constructor" << std::endl;
}
~A()
{
std::cout << "A destructor" << std::endl;
}
};
int main()
{
{
//初始化方式1
std::shared_ptr<A> sp1(new A());
std::cout << "use count: " << sp1.use_count() << std::endl;
//初始化方式2
std::shared_ptr<A> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
sp2.reset();
std::cout << "use count: " << sp1.use_count() << std::endl;
{
std::shared_ptr<A> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
}
std::cout << "use count: " << sp1.use_count() << std::endl;
}
return 0;
}
// 结果如下:
A constructor
use count: 1
use count: 2
use count: 1
use count: 2
use count: 1
A destructor
执行过程如下:
虽然shared_ptr比其他智能指针完美(因为支持了拷贝构造和赋值),但其存在一个问题:循环引用。假设现在有Node这样一个类,有三个成员,两个分别指向前后节点的智能指针,一个保存数据的成员变量_val。当申请了Node类型的资源后,用shared_ptr托管,此时的智能指针可以正常的释放资源。
struct Node
{
Node()
: _prev(nullptr)
, _next(nullptr)
, _val(0)
{}
myPtr::shared_ptr<Node> _prev;
myPtr::shared_ptr<Node> _next;
int _val;
};
int main()
{
myPtr::shared_ptr<Node> p1(new Node);
myPtr::shared_ptr<Node> p2(new Node);
// p1->_next = p2;
// p2->_prev = p1;
return 0;
}
当两智能指针没有产生连接时,p1和p2的计数都为1,释放资源正常。
但是当p1指针的_next指向p2,p2的_prev指向p1后,资源就不能正常释放,产生了内存泄漏。
当它们产生连接后,p1的_next也是一个智能指针,将_next指向p2,相当于shared_ptr的一次赋值,此时的p2计数为2,p2的_prev指向p1后,p1的计数也为2。当main函数指向完,p1和p2的生命周期结束,自动调用其析构函数,但其析构函数是根据计数决定是否析构,两指针的计数都为2,析构后只是将计数减小为1,并没有释放资源。
此时要释放p1的资源,就要释放p2的_prev,_prev作为Node类的成员,只有Node类释放时才会被释放,所以现在要释放p2,而要释放p2就要释放p1的_next,要释放_next就要释放p1,这就回到了开头的问题。对于循环引用的问题,需要使用weak_ptr来解决。
weak_ptr是解决shared_ptr循环引用问题的关键,用shared_ptr作为Node的前后指针类型,使得Node的前后指针也参与了资源的管理,weak_ptr作为弱指针,即不参与资源的管理,只是用来指向资源,类似于容器里的迭代器。
template <class T>
class weak_ptr
{
public:
weak_ptr(T* ptr)
:_ptr(ptr)
{}
weak_ptr(weak_ptr<T>& p)
{
_ptr = p._ptr;
}
weak_ptr<T>& operator=(weak_ptr<T>& p)
{
_ptr = p._ptr;
return *this;
}
weak_ptr<T>& operator=(shared_ptr<T>& p)
{
_ptr = p.get();
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
// Node的前后指针类型改为weak_ptr
struct Node
{
Node()
: _prev(nullptr)
, _next(nullptr)
, _val(0)
{}
myPtr::weak_ptr<Node> _prev;
myPtr::weak_ptr<Node> _next;
int _val;
};
std::weak_ptr?可以从一个?std::shared_ptr?或另一个?std::weak_ptr?对象构造,std::shared_ptr?可以直接赋值给?std::weak_ptr?,也可以通过?std::weak_ptr?的?lock()?函数来获得?std::shared_ptr。它的构造和析构不会引起引用计数的增加或减少。std::weak_ptr?可用来解决?std::shared_ptr?相互引用时的死锁问题(即两个std::shared_ptr?相互引用,那么这两个指针的引用计数永远不可能下降为 0, 资源永远不会释放)。
#include <iostream>
#include <memory>
int main()
{
//创建一个std::shared_ptr对象
std::shared_ptr<int> sp1(new int(123));
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过构造函数得到一个std::weak_ptr对象
std::weak_ptr<int> sp2(sp1);
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过赋值运算符得到一个std::weak_ptr对象
std::weak_ptr<int> sp3 = sp1;
std::cout << "use count: " << sp1.use_count() << std::endl;
//通过一个std::weak_ptr对象得到另外一个std::weak_ptr对象
std::weak_ptr<int> sp4 = sp2;
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
// 结果如下:
use count: 1
use count: 1
use count: 1
use count: 1
无论通过何种方式创建?std::weak_ptr?都不会增加资源的引用计数,因此每次输出引用计数的值都是 1。
既然,std::weak_ptr?不管理对象的生命周期,那么其引用的对象可能在某个时刻被销毁了,如何得知呢?std::weak_ptr?提供了一个?expired()?方法来做这一项检测,返回 true,说明其引用的资源已经不存在了;返回 false,说明该资源仍然存在,这个时候可以使用?std::weak_ptr?的?lock()?方法得到一个?std::shared_ptr?对象然后继续操作资源。
参考: