RAII(Resource Acquisition is Initialization),即【资源获取即初始化】,也就是说在构造函数中申请分配资源,在析构函数中释放资源。因为C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域的时候会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。
智能指针就是RAII最具代表的实现之一。使用智能指针,可以实现自动的内存管理,再也不需要担心忘记delete造成的内存泄漏。毫不夸张的来讲,有了智能指针,代码中几乎不需要再出现delete了。
在了解了RAII机制之后,我们可以尝试实现一个简单的智能指针。
#include
using namespace std;
/*RAII技术被认为是C++中管理资源的最佳方法,
进一步引申,使用RAII技术也可以实现安全、
简洁的状态管理,编写出优雅的异常安全的代码。*/
template<class T>
class Smart_ptr
{
private:
T* m_p;
public:
//调用构造函数时,申请资源
Smart_ptr(T* p = nullptr):m_p(p)
{
cout << "constructor ptr" << endl;
}
//调用析构函数时,自动释放资源
~Smart_ptr()
{
if(m_p)
{
cout << "delete ptr" << endl;
delete m_p;
}
}
//普通指针的功能
//通过重载运算符使其拥有和普通指针一样的功能
T& operator*()const {return *m_p;}
T* operator->()const {return m_p;}
};
在上述代码中,我们利用RAII机制,实现了智能指针自动释放资源,又通过重载了operator*和operator->,使其具有和指针一样的行为和功能。
我们可以通过以下的测试用例,观察RAII机制下的智能指针的工作情况:
struct AA
{
int m_a;
AA(int a = 0):m_a(a){cout << "constructor AA" << endl;}
~AA(){cout << "destructor AA" << endl;}
};
int main()
{
//语句块方便观察析构函数的调用
//指向自定义类型的对象
{
AA* pa = new AA;
Smart_ptr<AA> sp1(pa);
cout << sp1->m_a << endl;
}
//指向自定义类型的匿名对象
{
Smart_ptr<AA> sp2(new AA(5));
cout << sp2->m_a << endl;
}
//指向内置类型的对象
{
Smart_ptr<int> sp3(new int(5));
cout << (*sp3) << endl;
}
return 0;
}
运行结果:
constructor AA
constructor ptr
0
delete ptr
destructor AA
constructor AA
constructor ptr
5
delete ptr
destructor AA
constructor ptr
5
delete ptr
在C++11中,提供了三种智能指针,接下来我们来逐一了解。使用这些智能指针时需要引用头文件< memory>。
(在C++11之前还有auto_ptr,但由于它并非安全的所以本文不作介绍)
unique_ptr独享它指向的对象,也就是说,同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
以下是unique_ptr的声明:
template <typename T, typename D = default_delete<T>>//第二个模板参数D:指定删除器,缺省用delete释放资源
class unique_ptr
{
public:
explicit unique_ptr(pointer p) noexcept; // 不可用于转换函数。
~unique_ptr() noexcept;
T& operator*() const; // 重载*操作符。
T* operator->() const noexcept; // 重载->操作符。
unique_ptr(const unique_ptr &) = delete; // 禁用拷贝构造函数。
unique_ptr& operator=(const unique_ptr &) = delete; // 禁用赋值函数。
unique_ptr(unique_ptr &&) noexcept; // 右值引用。
unique_ptr& operator=(unique_ptr &&) noexcept; // 右值引用。
// ...
private:
pointer ptr; // 内置的指针。
};
可以看到,unique_ptr禁用了拷贝构造,也就是说不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
(1)初始化
unique_ptr的初始化方法和我们实现的智能指针的类似。事实上,我们可以使用以下的方法:
class AA{};
new* pa = new AA;
//通过构造函数
std::unique_ptr<AA> ptr1(new AA); // 分配内存并初始化
std::unique_ptr<AA> ptr1(pa); //将原始指针移交给智能指针管理
//通过移动函数
std::unique_ptr<AA> ptr2 = move(ptr1);
//通过reset初始化
ptr2.reset(new int);
//但以下这些方法是错误的:
// std::unique_ptr pu1 = p; // 错误,不能把普通指针直接赋给智能指针。
// std::unique_ptr pu2 = new AA("hello."); // 错误,不能把普通指针直接赋给智能指针。
// std::unique_ptr pu3 = pu2; // 错误,不能用其它unique_ptr拷贝构造。
// std::unique_ptr pu3;
// pu3 = pu1; // 错误,不能用=对unique_ptr进行赋值。
(2)一些技巧
注意:unique_ptr也不是绝对安全的,如果程序中调用exit()退出,全局的unique_ptr可以自动释放,但局部的unique_ptr无法释放。
unique_ptr还提供了支持数组的具体化版本,使用如下:
unique_ptr<int[]> parr1(new int[3]); // 不指定初始值
unique_ptr<int[]> parr1(new int[3]{ 33,22,11 }); // 指定初始值
shared_ptr共享它指向的对象,多个shared_ptr可以指向(关联)相同的对象。在其内部采用计数机制来实现,当新的shared_ptr与对象关联时,引用计数增加1,当shared_ptr超出作用域时,引用计数减1。当引用计数变为0时,则表示没有任何shared_ptr与对象关联,则释放该对象。
(1)初始化
和unique_ptr不同的是,shared_ptr没有删除拷贝构造和赋值,并且在C++11标准中可以通过std::make_shared初始化,效率更高(std::make_unique在C++14中才有)。
struct AA{};
//通过构造函数
shared_ptr<AA> pa0(new AA);
//通过移动函数
shared_ptr<AA> pa1 = move(pa0);
//通过拷贝函数
shared_ptr<AA> pa2 = pa1;
//通过std::make_shared(推荐)
shared_ptr<AA> pa3 = std::make_shared<AA>();
//通过reset初始化
pa0.reset(); //重置pa0, 使pa0的引用基数为0
pa0.reset(new int);
此外:
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
};
int main()
{
shared_ptr<AA> pa0(new AA("aa")); // 初始化资源aa
shared_ptr<AA> pa1 = pa0; // 用已存在的shared_ptr拷贝构造,计数加1
shared_ptr<AA> pa2 = pa0; // 用已存在的shared_ptr拷贝构造,计数加1
cout << "pa0.use_count()=" << pa0.use_count() << endl; // 值为3
cout << "pa0.get() = " << pa0.get() << endl;//pa0.get() = 0x1fb131d1420
cout << "pa1.get() = " << pa1.get() << endl;//pa1.get() = 0x1fb131d1420
cout << "pa2.get() = " << pa2.get() << endl;//pa2.get() = 0x1fb131d1420
shared_ptr<AA> pb0 = make_shared<AA>("bb"); // 初始化资源bb
shared_ptr<AA> pb1 = pb0; // 用已存在的shared_ptr拷贝构造,计数加1
cout << "pb0.use_count()=" << pb0.use_count() << endl; // 值为2
cout << "pb0.get() = " << pb0.get() << endl;//pb0.get() = 0x1fb131d17a0
cout << "pb1.get() = " << pb1.get() << endl;//pb1.get() = 0x1fb131d17a0
pb1 = pa1; // 资源aa的引用加1,资源bb的引用减1
pb0 = pa1; // 资源aa的引用加1,资源bb的引用成了0,将被释放
cout << "pa0.use_count()=" << pa0.use_count() << endl; // 值为5。
cout << "pb0.use_count()=" << pb0.use_count() << endl; // 值为5。
}
大家不要误解了共享的含义:shared_ptr指针指向的资源只有一个,它并不是被复制的,而shared_ptr可以有多个。
(2)一些细节
在默认情况下,智能指针过期的时候,用delete原始指针释放它管理的资源,程序员可以自定义删除器,改变智能指针释放资源的行为(旨在释放资源的同时能干点其他事情)。
删除器可以是全局函数、仿函数和Lambda表达式,形参为原始指针:
void deletefunc(AA* a) { // 删除器,普通函数。
cout << "自定义删除器(全局函数)。\n";
delete a;
}
struct deleteclass // 删除器,仿函数。
{
void operator()(AA* a) {
cout << "自定义删除器(仿函数)。\n";
delete a;
}
};
auto deleterlamb = [](AA* a) { // 删除器,Lambda表达式。
cout << "自定义删除器(Lambda)。\n";
delete a;
};
给shared_ptr指定删除器十分简单,写入函数名即可:
//普通函数版本
shared_ptr<AA> pa1(new AA("aa"), deletefunc);
//仿函数版本
shared_ptr<AA> pa1(new AA("bb"), deleteclass());
//lambda函数版本
shared_ptr<AA> pa1(new AA("cc"), deleterlamb);
而在unique_ptr中会复杂一些:
//普通函数版本
//模板参数用decltype推断会简单一些
unique_ptr<AA,decltype(deletefunc)*> pu1(new AA("aa"), deletefunc);
unique_ptr<AA,void(*)(AA*)> pu1(new AA("aa"), deletefunc);
//仿函数版本
unique_ptr<AA,deleteclass)> pu1(new AA("bb"), deleteclass());
//lambda函数版本
unique_ptr<AA, decltype(deleterlamb)> pu3(new AA("cc"), deleterlamb);
现在有如下一段代码:
#include
#include
using namespace std;
class BB;
class AA
{
public:
string m_name;
AA() { cout << m_name << "调用构造函数AA()。\n"; }
AA(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~AA() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
shared_ptr<BB> m_p;
};
class BB
{
public:
string m_name;
BB() { cout << m_name << "调用构造函数AA()。\n"; }
BB(const string & name) : m_name(name) { cout << "调用构造函数AA("<< m_name << ")。\n"; }
~BB() { cout << "调用了析构函数~AA(" << m_name << ")。\n"; }
shared_ptr<AA> m_p;
};
int main()
{
shared_ptr<AA> pa = make_shared<AA>("aa");
shared_ptr<BB> pb = make_shared<BB>("bb");
pa->m_p = pb;
pb->m_p = pa;
cout << "pa.use_count()=" << pa.use_count() << endl;// 结果为2
cout << "pb.use_count()=" << pb.use_count() << endl;// 结果为2
return 0;
}
我们会发现shared_ptr的计数器不灵了,因为上述的代码让pa和pb陷入了一个逻辑死区:我等你先死,你等我先死,结果谁都死不了。
为了解决这个问题,C++引入了weak_ptr。weak_ptr 是为了配合shared_ptr而引入的,它指向一个由shared_ptr管理的资源但不影响资源的生命周期。也就是说,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
不论是否有weak_ptr指向,如果最后一个指向资源的shared_ptr被销毁,资源就会被释放。
我们将上文代码中的类的成员变量指针替换成weak_ptr,结果就一切正常了,大家可以自己试一试。
使用weak_ptr:
对weak_ptr应用多在多线程中,对此我们总结weak_ptr的灵魂特性:
我们可以通过以下的代码应用这3点:
shared_ptr<AA> pa = make_shared<AA>("aa");
{
shared_ptr<BB> pb = make_shared<BB>("bb");
pa->m_p = pb;
pb->m_p = pa;
shared_ptr<BB> pp = pa->m_p.lock(); // 把weak_ptr提升为shared_ptr。
if (pp == nullptr)
cout << "语句块内部:pa->m_p已过期。\n";
else
cout << "语句块内部:pp->m_name=" << pp->m_name << endl;
}
shared_ptr<BB> pp = pa->m_p.lock(); // 把weak_ptr提升为shared_ptr。
if (pp == nullptr)
cout << "语句块外部:pa->m_p已过期。\n";
else
cout << "语句块外部:pp->m_name=" << pp->m_name << endl;