智能指针 (RAII)
- 前言
-
- [1. 引入智能指针](#1. 引入智能指针)
- [2. 智能指针的设计和使用](#2. 智能指针的设计和使用)
- 一、库中的智能指针
-
- [1. std::auto_ptr(C++98了解即可)](#1. std::auto_ptr(C++98了解即可))
- [2. std::unique_ptr(C++11)](#2. std::unique_ptr(C++11))
- [3. std::shared_ptr(C++11)](#3. std::shared_ptr(C++11))
- [4. 定制删除器](#4. 定制删除器)
前言
1. 引入智能指针
我在之前的博客介绍了异常(见到认识就行),用于处理一些函数内无法处理的问题,但是它会导致执行流乱跳。所以也就可能会导致内存泄漏等内存管理问题。
eg:抛出异常之后,无法delete,导致内存泄漏
cpp
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void Func()
{
int* p1 = new int;
int* p2 = new int;
cout << div() << endl; // 如果这里抛出异常(如除0错误)
delete p1; // 这些代码可能无法执行
delete p2;
}
为了解决上述问题,避免手动管理内存的缺陷,C++引入了 RAII(Resource Acquisition Is Initialization) 技术。
- 核心原理: 利用对象的生命周期来控制资源。构造时获取资源,析构时释放资源。实际上把一份资源的责任托管给了一个对象
- 优势: 无需显式释放,对象离开作用域(无论是正常结束还是异常退出)都会自动调用析构函数,保证资源不泄漏
2. 智能指针的设计和使用
指针指针的原理:用RAII思想,封装一个简单的SmartPtr类,且要像指针一样使用。
cpp
template<class T>
class SmartPtr
{
public:
// 获取资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 释放资源
~SmartPtr()
{
delete _ptr;
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
eg: 使用智能指针
cpp
void f()
{
SmartPtr<pair<string, string>> sp1(new pair<string, string>("1111", "22222"));
SmartPtr<string> sp2(new string("xxxxx"));
// 正常像指针一样使用
cout << sp1->first << endl;
cout << sp1->second << endl;
cout << *sp2 << endl;
div(); // 这里异常了也会正常释放资源
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
一、库中的智能指针
1. std::auto_ptr(C++98了解即可)
- auto_ptr的实现原理:管理权转移的思想。
- 问题: 当发生拷贝构造或赋值时,会把被拷贝对象的资源管理权转移给拷贝对象(赋值同理),原指针会变为 nullptr,导致原指针悬空(Dangling Pointer)
实现一个简易版本的auto_ptr:
cpp
namespace kpl
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// ap2(ap1) 赋值同理
// 管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
private:
T* _ptr;
};
}
使用:
cpp
// 用自定义类型测试
class A
{
public:
A(int a = 0)
:_a(a)
{}
~A()
{}
int _a;
};
int main()
{
kpl::auto_ptr<A> ap1(new A(1));
kpl::auto_ptr<A> ap2(ap1);
// 崩溃
// ap1->_a++; // 此时的ap1是nullptr 悬空
ap2->_a++;
return 0;
}
2. std::unique_ptr(C++11)
- std::unique_ptr原理: 简单粗暴的防拷贝。它明确禁止了拷贝构造和赋值操作(通过 = delete)。
- 特点: 独占所指对象的内存,同一时刻只能有一个unique_ptr指向该资源,用于不需要共享所有权的场景。
一个简易版本的unique_ptr:
cpp
namespace kpl
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 防拷贝
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
T* _ptr;
};
}
使用:
cpp
int main()
{
// A是自定义类型
kpl::unique_ptr<A> up1(new A(1));
kpl::unique_ptr<A> up2(new A(2));
// 都被禁止
//kpl:unique_ptr<A> up3(up1);
//up1 = up2;
return 0;
}
3. std::shared_ptr(C++11)
①std::shared_ptr
std::shared_ptr实现原理:关键就是引用计数(Reference Counting),在构造时创建
- 内部维护一个计数器,记录有多少个 shared_ptr 共享同一个对象。
- 每次拷贝构造或赋值,计数器 +1。
- 每次析构,计数器 -1。
- 当计数器变为 0 时,真正释放内存。
一个简易版本的shared_ptr:
cpp
namespace kpl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
// 这个引用计数要保证多个对象共享,且指向同一份资源的时候共同使用
// 使用static就不能保证多个对象都有这个引用计数
// 使用成员变量则不能保证指向同一个资源时共享
, _pcount(new int(1))
{}
~shared_ptr()
{
// 引用计数为0时,才释放资源
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 拷贝构造,多个对象共享资源
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
// 引用计数+1
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 要禁掉自己给自己赋值,否则它就会满足下一个if条件把自己的资源释放掉
// 再执行赋值操作,但是此时_ptr已经被释放
if (_ptr == sp._ptr)
return *this;
// 赋值是两个已经创建的对象,此时左侧对象指向右侧对象的资源
// 但是右侧的资源则少一个对象指向所以要减引用计数
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
// 返回引用计数
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
使用:
cpp
int main()
{
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp2(new A(2));
// 拷贝构造
shared_ptr<A> sp3(sp1);
sp1->_a++; // 都可以使用
sp3->_a++; // 都可以使用
shared_ptr<A> sp4(sp2);
shared_ptr<A> sp5(sp4);
sp1 = sp5;
sp3 = sp5;
sp1 = sp1;
return 0;
}
②线程安全问题
shared_ptr的线程安全问题:两个方面
- 引用计数的线程安全问题
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题(这不是库的问题是程序员写代码时需要注意的问题,不是这里要解决的)
解决引用计数的线程安全问题,把new出来的_pcount都使用atomic<int>
cpp
namespace kpl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new atomic<int>(1)) // 改
{}
~shared_ptr()
{
// 这里就会调用函数操作(原子操作),保证了线程安全,++或--是一次执行完成
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
private:
T* _ptr;
atomic<int>* _pcount; // 换成原子操作
};
}
test:线程安全的测试
cpp
namespace kpl
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new atomic<int>(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 拷贝构造,多个对象共享资源
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
private:
T* _ptr;
atomic<int>* _pcount; // 换成原子操作
};
}
int main()
{
size_t n1 = 10000;
size_t n2 = 10000;
mutex mtx;
kpl::shared_ptr<double> sp(new double(1.1));
atomic<size_t> x = 0;
thread t1([&]() {
for (size_t i = 0; i < n1; i++)
{
kpl::shared_ptr<double> copy1(sp);
{
// 外面这个大括号的作用,就是给这个unique_lock用的,这是一个域
unique_lock<mutex> lock(mtx);
++(*copy1);
}
}
});
thread t2([&]() {
for (size_t i = 0; i < n2; i++)
{
kpl::shared_ptr<double> copy1(sp);
{
unique_lock<mutex> lock(mtx);
++(*copy1);
}
}
});
t1.join();
t2.join();
cout << *sp << endl;
}
③weak_ptr
- weak_ptr不是RAII智能指针,专门用来解决shared_ptr循环引用问题
- 作用:weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
一个简易版本的weak_ptr:
cpp
namespace kpl
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
使用:
cpp
struct Node
{
A _val;
// Node*类型不满足智能指针的使用
// Node* _next;
// 会调用拷贝构造,所以会导致引用计数增加
// kpl::shared_ptr<Node> _next;
// 不会增加引用计数
kpl::weak_ptr<Node> _next;
kpl::weak_ptr<Node> _prev;
};
int main()
{
kpl::shared_ptr<Node> sp1(new Node);
kpl::shared_ptr<Node> sp2(new Node);
// 循环引用
sp1->_next = sp2;
sp2->_prev = sp1;
return 0;
}
4. 定制删除器
在C++11中的智能指针有个参数是del,即接下来要介绍的定制删除器。在上面我们自主实现的析构都是用delete进行释放资源,可是如果不是new一个数据而是一堆呢?是不是就要使用delete[],如果是打开一个文件的指针给智能指针管理是不是还得用fclose释放。定制删除器可以有效的释放资源
eg:shared_ptr库中的构造函数就包括定制删除器
cpp
template <class U, class D>
shared_ptr (U* p, D del); // 这里的D就是可调用对象类型,del就是可调用对象(只是其中一个例子)
使用:
cpp
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
// 用什么方法创建/打开的资源就要使用对应的方式去释放
shared_ptr<A> sp1(new A[10], DeleteArray<A>());
shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
fclose(ptr);
});
return 0;
}
再在上面实现的shared_ptr中增加一个构造和一个成员变量,用来实现这个定制删除器的使用
cpp
template<class T>
class shared_ptr
{
public:
// function<void(T*)> _del; 所添加的构造
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new atomic<int>(1)) // 解决线程安全问题
, _del(del) // 添加:定制器
{}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
delete _pcount;
// 添加:调整释放方式
_del(_ptr);
}
}
private:
// 添加的成员变量 借用包装器返回值是void,参数是T*。给一个默认值一个lambda对象
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};