文章目录
前言
智能指针是一个RAII(Resource Acquisition is initialization)类模型,是行为类似于指针的类对象,用来动态的分配内存。它提供所有普通指针提供的接口,却很少发生异常。在构造中它分配内存,当离开作用域时它会自动释放已分配的内存。这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了。常见的智能指针模板有四类:unique_ptr、shared_ptr、weak_ptr、auto_ptr等四类,其中auto_ptr已经在C++ 17中移除,所以重点讲解其余三个指针模板。
一、RAII机制
1、什么是RAII?
RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
2、为什么要使用RAII?
RAII是用来管理资源、避免资源泄漏的方法。在编程使用系统资源时,都必须遵循一个步骤:
1)申请资源
2)使用资源
3)释放资源
第一步和第二步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。但是往往在编程过程中,会遇到异常分支不释放的问题。因此、需要使用RAII来对资源进行自动化管理,避免出现资源泄漏问题。
3、应用实例
通过lock_guard模板来管理锁,直接使用mutex锁,如果在临界区抛出异常或者走到异常分支return,就会导致没有解锁就退出的问题,此时很可能会发生死锁。lock_guard采用"资源分配时初始化"(RAII)方法来加锁、解锁,极大地简化了程序员编写mutex相关的异常处理代码。C++11的标准库中提供了std::lock_guard类模板做mutex的RAII。lock_guard的优点是在构造时自动对mutex加锁,在作用域结束/析构时,自动对mutex进行解锁。
二、unique_ptr
1、定义
unique_ptr 不共享它的指针。 它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何 C++ 标准库算法。 只能移动 unique_ptr。 这意味着,内存资源所有权将转移到另一 unique_ptr,并且原始 unique_ptr 不再拥有此资源。
unique_ptr 的基本特征:可移动,但不可复制
2、创建
构造 unique_ptr 时,可使用
make_unique
函数。如下:
#include <iostream>
#include <memory>
using namespace std;
class Resource {
public:
int m_resNum;
Resource(int num) {
m_resNum = num;
cout << "object create, num = " << m_resNum << endl;
}
Resource() {
m_resNum = 0;
cout << "default object create, num = " << m_resNum << endl;
}
~Resource() {
cout << "object delete, num = " << m_resNum << endl;
}
};
int main()
{
// 创建unique_ptr<Resource>实例
unique_ptr<Resource> pRes1(new Resource(10)); // 使用裸指针创建Resource对象
unique_ptr<Resource> pRes2 = make_unique<Resource>(20); // 使用make_unique创建Resource对象
// 转移pRes1所有权到pRes3
unique_ptr<Resource> pRes3 = move(pRes1);
return 0;
}
3、使用
unique_ptr
对象作为函数参数或返回值时,需要调用std::move
转移资源所有权
// unique_ptr指针对象作为函数参数
void Display(unique_ptr<Resource> pRes)
{
// 智能指针判空
if (pRes == nullptr)
{
return;
}
cout << "Res count " << pRes->m_resNum;
}
// unique_ptr指针对象作为函数返回值,调用std::move转移对象所有权
unique_ptr<Resource> GetResource(int num)
{
return move(make_unique<Resource>(num));
}
int main()
{
unique_ptr<Resource> pRes = GetResource(10);
// 调用std::move转移资源所有权
Display(move(pRes));
return 0;
}
三、shared_ptr
1、定义
shared_ptr 类型是 C++ 标准库中的一个智能指针,是为多个所有者可能必须管理对象在内存中的生命周期的方案设计的。 在您初始化一个 shared_ptr 之后,您可复制它,按值将其传入函数参数,然后将其分配给其他 shared_ptr 实例。 所有实例均指向同一个对象,并共享对一个“控制块”(每当新的 shared_ptr 添加、超出范围或重置时增加和减少引用计数)的访问权限。 当引用计数达到零时,控制块将删除内存资源和自身。
2、创建
创建内存资源时,请使用 make_shared 函数创建 shared_ptr。如果不使用 make_shared,则必须先使用显式 new 表达式来创建对象,然后才能将其传递到 shared_ptr 构造函数,例如:
int main()
{
// 创建shared_ptr对象
shared_ptr<Resource> pRes1(new Resource(10));
shared_ptr<Resource> pRes2 = make_shared<Resource>(20);
shared_ptr<Resource> pRes3 = pRes2;
return 0;
}
3、使用
可以通过下列方式将 shared_ptr 传递给其他函数:
- 按值传递:这将调用复制构造函数,增加引用计数,并使被调用方成为所有者。
- 按引用或常量引用传递:在这种情况下,引用计数不会增加,并且只要调用方不超出范围,被调用方就可以访问指针
4、循环引用问题
4.1、问题背景
使用shared_ptr可能存在循环引用问题,如下:
class ClassB;
class ClassA {
public:
ClassA() {
cout << "ClassA()" << endl;
}
~ClassA() {
cout << "~ClassA()" << endl;
}
void set_ptr(shared_ptr<ClassB>& ptr) {
m_ptr_b = ptr;
}
private:
shared_ptr<ClassB> m_ptr_b;
};
class ClassB {
public:
ClassB() {
cout << "ClassB()" << endl;
}
~ClassB() {
cout << "~ClassB()" << endl;
}
void set_ptr(shared_ptr<ClassA>& ptr) {
m_ptr_a = ptr;
}
private:
shared_ptr<ClassA> m_ptr_a;
};
int main()
{
ClassA *pA = new ClassA;
ClassB *pB = new ClassB;
shared_ptr<ClassA> ptr_a(pA);
shared_ptr<ClassB> ptr_b(pB);
pA->set_ptr(ptr_b);
pB->set_ptr(ptr_a);
return 0;
}
输出结果
ClassA()
ClassB()
Process returned 0 (0x0) execution time : 0.008 s
Press any key to continue.
从输出结果来看,只调用了ClassA与ClassB的构造函数,析构函数没有被调用。析构函数没有调用,就说明ptr_a和ptr_b两个变量的引用计数都不是0。下面分析一下例子中的引用情况:
起初定义完ptr_a和ptr_b时,只有①、③两条引用,即ptr_a指向CA对象,ptr_b指向CB对象。然后调用函数set_ptr后又增加了②、④两条引用,即CB对象中的m_ptr_a成员变量指向CA对象,CA对象中的m_ptr_b成员变量指向CB对象。这个时候,指向CA对象的有两个,指向CB对象的也有两个。当main函数运行结束时,对象ptr_a和ptr_b被销毁,也就是①、③两条引用会被断开,但是②、④两条引用依然存在,每一个的引用计数都不为0,结果就导致其指向的内部对象无法析构,造成内存泄漏。
4.2、使用weak_ptr解决循环引用问题
weak_ptr的出现就是为了解决shared_ptr的循环引用的问题的。以上文的例子来说,解决办法就是将两个类中的一个成员变量改为weak_ptr对象,比如将ClassB中的成员变量改为weak_ptr对象,即ClassB类的代码如下:
class ClassB {
public:
ClassB() {
cout << "ClassB()" << endl;
}
~ClassB() {
cout << "~ClassB()" << endl;
}
void set_ptr(shared_ptr<ClassA>& ptr) {
m_ptr_a = ptr;
}
private:
weak_ptr<ClassA> m_ptr_a;
};
输出结果
ClassA()
ClassB()
~ClassA()
~ClassB()
Process returned 0 (0x0) execution time : 0.008 s
Press any key to continue.
从输出结果来看,ClassA和ClassB的对象都被正常的析构了。修改后例子中的引用关系如下图所示:
流程与上一例子大体相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数。也就是说,CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当main函数返回时,对象ptr_a和ptr_b被销毁,也就是①、③两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b成员变量也会被析构,导致CB对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题。
四、weak_ptr
1、定义
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。 weak_ptr指针可调用的成员方法,如下:
成员方法 | 功能 |
---|---|
operator=() | 重载 = 赋值运算符,是的 weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值 |
reset() | 将当前 weak_ptr 指针置为空指针 |
use_count() | 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量 |
expired() | 判断当前 weak_ptr 指针为否过期(指针为空,或者指向的堆内存已经被释放) |
lock() | 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针 |
2、使用
weak_ptr并没有重载operator ->和operator *操作符,因此不可直接通过weak_ptr使用对象,典型的用法是调用其lock函数来获得shared_ptr实例,进而访问原始对象。
int main()
{
int *p = new int(10);
shared_ptr<int> ptr1(p);
weak_ptr<int> ptr2 = ptr1;
cout << "use count " << ptr2.use_count() << endl;
cout << "value " << *(ptr2.lock()) << endl;
return 0;
}
输出结果
use count 1
value 10
Process returned 0 (0x0) execution time : 0.009 s
Press any key to continue.
五、使用智能指针实现defer
#define _MACRO_CONTACT_IMPL(x, y) x##y
#define _MACRO_CONTACT(x, y) _MACRO_CONTACT_IMPL(x, y)
#define defer(X) \
do { \
auto _MACRO_CONTACT(__FUNCTION__, __LINE__) = \
std::unique_ptr<void, std::function<void(void *)>>{reinterpret_cast<void *>(1), [&](void *) { X }}; \
} while (0)
六、使用智能指针要注意的问题
1、智能指针不能管理非堆内存
下面的代码使用智能指针管理非堆内存,导致智能指针对象生命周期结束时,会使用栈上的内存,代码运行时会崩溃
int main()
{
// 注意:不能使用智能指针管理非堆内存
int val = 10;
shared_ptr<int> pRes1(&val);
return 0;
}