RAII
Resource Acquisition Is Initialization,它是一种C++编程技术,它通过在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确获取和释放。
一般情况下,C++申请资源后都需要手动释放资源,一旦忘记资源的释放就会造成内存泄漏,为了解决内存泄漏问题,C++引入了RAII机制。
RAII 的用法是在构造函数中获取资源,在析构函数中释放资源,并使用栈上的对象或者智能指针来管理堆上的资源。C++ 标准库提供了许多 RAII 类型,如 std::fstream、std::unique_ptr、std::lock_guard 等,我们可以直接使用它们或者参考它们来实现自己的 RAII 类型。
遵循RAII设计原则的好处有以下几点:
- 简化资源管理:RAII可以将资源的获取和释放封装在对象中,从而简化了资源管理的代码。使用RAII可以避免手动管理资源的复杂性和错误,减少了代码的出错率。
- 避免资源泄漏:RAII可以确保资源在对象生命周期结束时被正确释放,从而避免了资源泄漏的问题。如果不使用RAII,可能会因为忘记释放资源或者异常情况导致资源泄漏。
- 提高代码可读性:RAII可以将资源的获取和释放封装在对象中,从而使代码更加清晰和易于理解。使用RAII可以使代码更加模块化,从而提高代码的可读性和可维护性。
- 支持异常安全:RAII可以确保资源在异常情况下也能够被正确释放,从而支持异常安全。如果不使用RAII,可能会因为异常情况导致资源没有被正确释放,从而导致程序出现错误。
智能指针
智能指针的特点:
- 不用显式地写出析构函数。
- 资源在智能指针的生命周期中始终有效。
- 可以像指针一样的使用。
- 最重要的是:具有RAII特性。
c++ 98提供的auto_prt
std::auto_ptr 的基本用法如下代码所示:
#include <memory>int main()
{//初始化方式1std::auto_ptr<int> sp1(new int(8));//初始化方式2std::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确实是个失败的设计。
std::unique_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类型的堆对象//形式1std::unique_ptr<int[]> sp1(new int[10]);//形式2std::unique_ptr<int[]> sp2;sp2.reset(new int[10]);//形式3std::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 相同。
std::shared_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()
{{//初始化方式1std::shared_ptr<A> sp1(new A());std::cout << "use count: " << sp1.use_count() << std::endl;//初始化方式2std::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
执行过程如下:
- 上述代码 22 行 sp1 构造时,同时触发对象 A 的构造,因此 A 的构造函数会执行;
- 此时只有一个 sp1 对象引用 22 行 new 出来的 A 对象(为了叙述方便,下文统一称之为资源对象 A),因此代码 24 行打印出来的引用计数值为 1;
- 代码 27 行,利用 sp1 拷贝一份 sp2,导致代码 28 行打印出来的引用计数为 2;
- 代码 30 行调用 sp2 的 reset() 方法,sp2 释放对资源对象 A 的引用,因此代码 31 行打印的引用计数值再次变为 1;
- 代码 34 行 利用 sp1 再次 创建 sp3,因此代码 35 行打印的引用计数变为 2;
- 程序执行到 36 行以后,sp3 出了其作用域被析构,资源 A 的引用计数递减 1,因此 代码 38 行打印的引用计数为 1;
- 程序执行到 39 行以后,sp1 出了其作用域被析构,在其析构时递减资源 A 的引用计数至 0,并析构资源 A 对象,因此类 A 的析构函数被调用。
问题
虽然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来解决。
std::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 对象然后继续操作资源。
参考:
https://github.com/balloonwj/CppGuide/blob/master/articles/C%2B%2B%E5%BF%85%E7%9F%A5%E5%BF%85%E4%BC%9A%E7%9A%84%E7%9F%A5%E8%AF%86%E7%82%B9/%E8%AF%A6%E8%A7%A3C%2B%2B11%E4%B8%AD%E7%9A%84%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.md
C++的RAII思想以及在智能指针上的应用-CSDN博客