智能指针
文章目录
基础–实现简单的智能指针
-
裸指针是什么?
裸指针(Raw Pointer) 是 C++ 中的一种基本指针类型,它直接存储内存地址,不附带任何额外的管理功能(如自动内存管理或生命周期管理)。裸指针是 C++ 中最原始的指针形式,通常用于直接操作内存。
没有自动内存管理功能,容易导致悬空指针、内存泄漏等问题。 需要手动释放 -
裸指针缺点:
1. 忘记释放资源,导致资源泄露(常发生内存泄漏问题) 2. 同一资源释放多次,导致释放野指针,程序崩溃 3. 明明代码的后面写了释放资源的代码,但是由于程序逻辑满足条件,从中间 return掉了,导致释放资源的代码未被执行到,懵 4. 代码运行过程中发生异常,随着异常栈展开,导致释放资源的代码未被执行到, 懵
-
智能指针,智能指针的智能二字,主要体现在用户可以不关注资源的释放,因为智能指针
会帮你完全管理资源的释放,它会保证无论程序逻辑怎么跑,正常执行或者产生异
常,资源在到期的情况下,一定会进行释放。 -
实现一个简单的 智能指针
#include <iostream> #include <cstring> using namespace std; template<typename T> class CSmartPtr { public: CSmartPtr(T* ptr = nullptr) : mptr(ptr) {} ~CSmartPtr() { delete mptr; } T& operator* () { return *mptr; } // 必须引用, 否则不能修改 T* operator->() { return mptr; } private: T* mptr; }; int main() { // int *p = new int; CSmartPtr<int> ptr1(new int); *ptr1 = 20; // 虽然 ptr1 本身是普通对象, 但是里面是指针,想要访问指针, 除了写访问函数, 还能用 *重载 class Test { public: void test() { cout << "test" << endl; } }; CSmartPtr<Test> ptr2(new Test()); (*ptr2).test(); // 好好理解,为什么要用*, ptr2->test(); return 0; }
利用栈上的 对象, 出作用域 自动析构的特征, 做到资源的 自动释放!
2.不带引用计数的智能指针
问题
-
上一节代码里, 加入做下面这个:
CSmartPtr<int> ptr1(new int); CSmartPtr<int> ptr2(ptr1);
默认拷贝构造是一个浅拷贝
- 浅拷贝:
- 仅复制指针的值(内存地址)。
- 多个对象共享同一块动态内存。
- 适用于没有动态内存或资源的对象。
- 深拷贝:
- 复制指针所指向的实际数据。
- 每个对象拥有独立的动态内存副本。
- 适用于有动态内存或资源的对象。
会导致 两次析构 释放同一个值, 造成野指针问题
野指针(Wild Pointer) 是指未初始化或指向已释放内存的指针。野指针的行为是未定义的,访问或修改野指针指向的内存可能导致程序崩溃、数据损坏或安全漏洞。
- 浅拷贝:
解决
- 使用 自定义的 深拷贝 构造函数, 可以解决这个问题
- 不带引用计数的智能指针 解决
不带引用计数的智能指针汇总
那些独占所有权的智能指针
禁止拷贝和赋值重载,auto_ptr例外
自动释放
auto_ptr – #include < memory > – 不推荐
它在 C++11 中被弃用,并在 C++17 中被移除。 why?
//c++库里
int main()
{
auto_ptr<int> ptr1(new int);
auto_ptr<int> ptr2(ptr1); // auto_ptr 源码显示, 在拷贝构造时, 会把原来的ptr1的指针置空
*ptr2 = 20;
cout << *ptr1 << endl; // 错误
return 0;
}
也就是说, 拷贝构造会使得 原来的 所有的 指针 都会 置空, 不能再访问这块内存
std::auto_ptr 采用独占所有权模型,但在拷贝或赋值时会发生所有权转移。
这意味着,当一个 auto_ptr 被拷贝或赋值给另一个 auto_ptr 时,原指针会失去所有权,变为 nullptr。
这种行为容易导致意外的所有权转移和悬空指针问题。
scoped_ptr — 不推荐
scoped_ptr
是 Boost 库中提供的一种智能指针,用于在特定作用域内管理动态分配的内存。
主要特点:禁止拷贝和赋值,从而避免所有权转移的问题
直接把 里面的 拷贝构造=delete; operator=() = delete
unique_ptr – 推荐 – 右值引用–move
直接把 里面的 左值引用拷贝构造=delete; operator=() = delete
但是, 提供了 右值 引用 拷贝构造 和 右值引用 operator=, 因此 需要移动语义
虽然同样是 所有权, 但是 好处是, 可以清楚地 看出来, 是在转移 资源
int main()
{
unique_ptr<int> ptr1(new int);
unique_ptr<int> ptr2(move(ptr1)); // 需要移动语义, 好处, 清晰可见
*ptr2 = 20;
cout << *ptr1 << endl; // 也是错误的,
return 0;
}
3.带引用计数的智能指针
好处: 多个指针 可以管理同一个资源
给每一个对象资源, 匹配一个引用计数
- 实现简单的 引用计数 智能指针----还是比较麻烦的
#include <iostream>
// 引用计数类
class RefCnt {
public:
RefCnt(void* ptr = nullptr) : ptr_(ptr), count_(1) {}
// 增加引用计数
void addRef() {
++count_;
}
// 减少引用计数,如果计数为 0 则释放资源
void release() {
--count_;
if (count_ == 0) {
delete static_cast<int*>(ptr_); // 假设资源是 int 类型
delete this; // 释放引用计数对象本身
}
}
int getCount() const {
return count_;
}
private:
void* ptr_; // 指向资源的指针
int count_; // 引用计数
};
// 智能指针类
template<typename T>
class CSmartPtr {
public:
// 构造函数
CSmartPtr(T* ptr = nullptr) : mptr(ptr) {
if (mptr) {
mpRefCnt = new RefCnt(mptr);
} else {
mpRefCnt = nullptr;
}
}
// 析构函数
~CSmartPtr() {
if (mpRefCnt) {
mpRefCnt->release();
}
}
// 拷贝构造函数
CSmartPtr(const CSmartPtr<T>& src) : mptr(src.mptr), mpRefCnt(src.mpRefCnt) {
if (mpRefCnt) {
mpRefCnt->addRef();
}
}
// 拷贝赋值运算符
CSmartPtr<T>& operator=(const CSmartPtr<T>& src) {
if (this != &src) {
// 释放当前资源
if (mpRefCnt) {
mpRefCnt->release();
}
// 复制新资源
mptr = src.mptr;
mpRefCnt = src.mpRefCnt;
// 增加引用计数
if (mpRefCnt) {
mpRefCnt->addRef();
}
}
return *this;
}
// 解引用运算符
T& operator*() const {
return *mptr;
}
// 箭头运算符
T* operator->() const {
return mptr;
}
// 获取引用计数
int use_count() const {
return mpRefCnt ? mpRefCnt->getCount() : 0;
}
private:
T* mptr; // 指向资源的指针
RefCnt* mpRefCnt; // 指向引用计数对象的指针
};
// 测试代码
int main() {
CSmartPtr<int> ptr1(new int(10));
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 1
{
CSmartPtr<int> ptr2 = ptr1; // 拷贝构造
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 2
std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 输出 2
} // ptr2 离开作用域,引用计数减 1
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 1
return 0;
}
-
多线程下 是不安全的, 因为 引用计数的修改 不是原子操作
std::atomic<int> count_; // 原子引用计数
4.shared_ptr 交叉(循环)引用问题
使用 .use_count() 获得 指针计数
代码示例
-
shared_ptr 是强智能指针, weak_ptr 是弱智能指针
强的 能改变资源的 引用计数, 弱的 不行
-
强智能指针, 交叉引用问题?
造成 new 出来的 资源 无法释放#include <iostream> #include <memory> using namespace std; class B; // 前向声明 class A { public: A() { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } shared_ptr<B> __ptrb; // 指向 B 的 shared_ptr }; class B { public: B() { cout << "B()" << endl; } ~B() { cout << "~B()" << endl; } // 如果 B 也持有 A 的 shared_ptr,会导致循环引用 shared_ptr<A> __ptra; }; int main() { shared_ptr<A> pa(new A()) ; shared_ptr<B> pb(new B()); pa->__ptrb = pb; // 出作用域之前, 是2 ,达不到析构 pb->__ptra = pa; cout << pa.use_count() << endl; cout << pb.use_count() << endl; return 0; }
整体过程
初始状态(创建对象)
- 创建
A
对象:shared_ptr<A> pa(new A());
A
对象的引用计数为 1(来自pa
)。B
对象尚未创建。
+-------------------+
| A |
|-------------------|
| shared_ptr<B> __ptrb | -> nullptr
| ref_count: 1 |
+-------------------+
- 创建
B
对象:shared_ptr<B> pb(new B());
B
对象的引用计数为 1(来自pb
)。A
对象的引用计数仍为 1。
+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr<B> __ptrb | -> nullptr | shared_ptr<A> __ptra | -> nullptr
| ref_count: 1 | | ref_count: 1 |
+-------------------+ +-------------------+
步骤 1:A
持有 B
(pa->__ptrb = pb
)
A
对象的__ptrb
指向B
对象。B
对象的引用计数从 1 变为 2(来自pb
和pa->__ptrb
)。A
对象的引用计数仍为 1。
+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr<B> __ptrb | ----> | shared_ptr<A> __ptra | -> nullptr
| ref_count: 1 | | ref_count: 2 |
+-------------------+ +-------------------+
步骤 2:B
持有 A
(pb->__ptra = pa
)
B
对象的__ptra
指向A
对象。A
对象的引用计数从 1 变为 2(来自pa
和pb->__ptra
)。B
对象的引用计数仍为 2。
+-------------------+ +-------------------+
| A | | B |
|-------------------| |-------------------|
| shared_ptr<B> __ptrb | ----> | shared_ptr<A> __ptra |
| ref_count: 2 | | ref_count: 2 |
+-------------------+ +-------------------+
^ |
| |
+-----------------------------+
最终状态(循环引用)
A
和B
对象的引用计数均为 2。- 由于互相持有
shared_ptr
,引用计数无法归零,导致内存泄漏。
解决办法–强弱混用
定义对象的地方 使用 强智能指针, 引用对象的 地方 使用 弱智能指针
#include <iostream>
#include <memory>
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
weak_ptr<B> __ptrb; // 修改
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
weak_ptr<A> __ptra; // 修改
};
int main() {
shared_ptr<A> pa(new A()) ;
shared_ptr<B> pb(new B());
pa->__ptrb = pb; // 出作用域之前, 是2 ,达不到析构
pb->__ptra = pa;
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
弱智能指针 - 不能使用资源
只观察资源, 不能使用资源
需要转换为 std::shared_ptr
才能访问对象
- 通过
weak_ptr::lock
方法,可以将std::weak_ptr
转换为std::shared_ptr
,从而安全地访问对象。shared_ptr<A> ps = __ptra.lock();
.lock() - 如果对象已被释放,
lock
返回一个空的std::shared_ptr
。
#include <iostream>
#include <memory>
using namespace std;
class B; // 前向声明
class A {
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
void test() { cout << "test" << endl; }
weak_ptr<B> __ptrb;
};
class B {
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
void func()
{
//__ptra->test(); // 弱智能指针不能 访问对象
shared_ptr<A> ps = __ptra.lock();
ps->test();
}
weak_ptr<A> __ptra;
};
int main() {
shared_ptr<A> pa(new A()) ;
shared_ptr<B> pb(new B());
pa->__ptrb = pb;
pb->__ptra = pa;
pb->func();
cout << pa.use_count() << endl;
cout << pb.use_count() << endl;
return 0;
}
5.多线程访问共享对象的线程安全问题
非常著名的 开源网络库 muduo 库
-
该源码中对于智能指针的应用非常优秀,其中借助shared_ptr和
weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题,解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又 要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问 该对象,就会发生不可预期的错误。
-
代码如下:
#include <iostream> #include <thread> using namespace std; class Test { public: // 构造Test对象,_ptr指向一块int堆内存,初始值是20 Test() :_ptr(new int(20)) { cout << "Test()" << endl; } // 析构Test对象,释放_ptr指向的堆内存 ~Test() { delete _ptr; _ptr = nullptr; cout << "~Test()" << endl; } // 该show会在另外一个线程中被执行 void show() { cout << *_ptr << endl; } private: int *volatile _ptr; }; void threadProc(Test *p) { // 睡眠两秒,此时main主线程已经把Test对象给delete析构掉了 std::this_thread::sleep_for(std::chrono::seconds(2)); /* 此时当前线程访问了main线程已经析构的共享对象,结果未知,隐含bug。 此时通过p指针想访问Test对象,需要判断Test对象是否存活,如果Test对象 存活,调用show方法没有问题;如果Test对象已经析构,调用show有问题! */ p->show(); } int main() { // 在堆上定义共享对象 Test *p = new Test(); // 使用C++11的线程类,开启一个新线程,并传入共享对象的地址p std::thread t1(threadProc, p); // 在main线程中析构Test共享对象 delete p; // 等待子线程运行结束 t1.join(); return 0; }
运行上面的代码,发现在main主线程已经delete析构Test对象以后,子线程threadProc再去访问Test对象的show方法,无法打印出*_ptr的值20。可以用shared_ptr和weak_ptr来解决多线程访问共享对象的线程安全问题,上面代码修改如下:
#include <iostream> #include <thread> #include <memory> using namespace std; class Test { public: // 构造Test对象,_ptr指向一块int堆内存,初始值是20 Test() :_ptr(new int(20)) { cout << "Test()" << endl; } // 析构Test对象,释放_ptr指向的堆内存 ~Test() { delete _ptr; _ptr = nullptr; cout << "~Test()" << endl; } // 该show会在另外一个线程中被执行 void show() { cout << *_ptr << endl; } private: int *volatile _ptr; }; void threadProc(weak_ptr<Test> pw) // 通过弱智能指针观察强智能指针 { // 睡眠两秒 std::this_thread::sleep_for(std::chrono::seconds(2)); /* 如果想访问对象的方法,先通过pw的lock方法进行提升操作,把weak_ptr提升 为shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存 的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象 已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。 */ shared_ptr<Test> ps = pw.lock(); if (ps != nullptr) { ps->show(); } } int main() { // 在堆上定义共享对象 , 智能释放 shared_ptr<Test> p(new Test); // 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针 std::thread t1(threadProc, weak_ptr<Test>(p)); // 在main线程中析构Test共享对象 // 阻塞等待子线程运行结束 t1.join(); return 0; }
运行上面的代码,show方法可以打印出20,因为main线程调用了t1.join()方法等待子线程结束,此时pw通过lock提升为ps成功,见上面代码示例。
如果设置t1为分离线程,让main主线程结束,p智能指针析构,进而把Test对象析构,此时show方法已经不会被调用,因为在threadProc方法中,pw提升到ps时,lock方法判定Test对象已经析构,提升失败!main函数代码可以如下修改测试:
int main() { // 在堆上定义共享对象 shared_ptr<Test> p(new Test); // 使用C++11的线程,开启一个新线程,并传入共享对象的弱智能指针 std::thread t1(threadProc, weak_ptr<Test>(p)); // 在main线程中析构Test共享对象 // 设置子线程分离 主线程不等待子线程 t1.detach(); return 0; }
该main函数运行后,最终的threadProc中,show方法不会被执行到。以上是在多线程中访问共享对象时,对shared_ptr和weak_ptr的一个典型应用。
6.自定义删除器
什么是自定义删除器
我们经常用智能指针管理的资源是堆内存,当智能指针出作用域的时候,在其析构函数中会delete释放堆内存资源,但是除了堆内存资源,智能指针还可以管理其它资源,比如打开的文件,此时对于文件指针的关闭,就不能用delete了,这时我们需要自定义智能指针释放资源的方式,先看看unique_ptr智能指针的析构函数代码,如下:
~unique_ptr() noexcept
{ // destroy the object
if (get() != pointer())
{
this->get_deleter()(get()); // 这里获取底层的删除器,进行函数对象的调用
}
}
从unique_ptr的析构函数可以看到,如果要实现一个自定义的删除器,实际上就是定义一个函数对象而已,示例代码如下:
_EXPORT_STD template <class _Ty, class _Dx /* = default_delete<_Ty> */>
class unique_ptr { // non-copyable pointer to an object
{}
这是unique的源码, 可以看到, 模板里的第二个参数, 使用 默认的 删除器, 这个是可选的, 因此 自定义删除类的 传入接口 就在这里
#include <iostream>
#include <thread>
#include <memory>
using namespace std;
template<typename T>
class MyDeletor
{
public:
void operator() (T* ptr) const
{
cout << "call MyDeletor.operator()" << endl;
delete[] ptr;
}
};
int main()
{
unique_ptr<int, MyDeletor<int>> ptr1(new int[100]);
return 0;
}
文件的自定义删除器 – 不推荐写类
class FileDeleter
{
public:
// 删除器负责删除资源的函数
void operator()(FILE *pf)
{
fclose(pf);
}
};
int main()
{
// 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
return 0;
}
当然这种方式需要定义额外的函数对象类型,不推荐,
function和lambda结合的删除器-----推荐
function下一节会讲到
自定义删除器 一般指出现在 指定语句中, 写类 就显得繁杂了
可以用C++11提供的函数对象function和lambda表达式更好的处理自定义删除器,代码如下:
#include <iostream>
#include <memory>
#include <cstdio> // 包含 fopen 和 fclose 的头文件
#include <functional>
using namespace std;
int main()
{
// 自定义智能指针删除器,关闭文件资源
unique_ptr<FILE, function<void(FILE*)>>
filePtr(fopen("data.txt", "w"), [](FILE* pf)->void {cout << "call lambda release file" << endl;fclose(pf); });
// 自定义智能指针删除器,释放数组资源
unique_ptr<int, function<void(int*)>>
arrayPtr(new int[100], [](int* ptr)->void {cout << "call lambda release int []" << endl;delete[]ptr; });
return 0;
}
如果想进一步了解智能指针,可以查看智能指针的源码实现,或者看muduo网络库的源码。