一、为什么需要智能指针
先看一段很常见的代码:函数里 new 了两块内存,中间调用了一个可能抛异常的函数,然后手动 delete。看起来没问题,但只要中间抛了异常,后面的 delete 就永远不会执行,内存直接泄漏。
cpp
double Divide(int a, int b)
{
// 当 b == 0 时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 这里 new 也可能抛异常
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (...)
{
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
这段代码的问题在于:
Divide可能抛异常,导致后面的delete不执行;- 第二个
new本身也可能抛异常,此时array1已经泄漏; - 为了处理这些情况,需要层层嵌套
try-catch,代码可读性极差。
智能指针正是为了解决这类问题而出现的:把资源交给一个对象管理,让对象的生命周期自动完成释放。
二、RAII 与智能指针的设计思路
RAII 是 Resource Acquisition Is Initialization 的缩写,是一种管理资源的设计思想。它的本质是利用对象的生命周期来管理动态获取的资源,避免资源泄漏。这里的资源可以是内存、文件指针、网络连接、互斥锁等等。
RAII 的做法是:获取资源时把资源委托给一个对象,资源在对象的生命周期内始终保持有效,最后在对象析构时释放资源。这样无论函数正常返回还是异常退出,资源都能被正确释放。
智能指针除了满足 RAII,还要方便访问资源,所以它像迭代器一样重载了 operator*、operator->、operator[] 等运算符。
cpp
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << "delete[] " << _ptr << endl;
delete[] _ptr;
}
// 重载运算符,模拟指针的行为,方便访问资源
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
用这个类改写上面的 Func,代码立刻清爽了:
cpp
void Func()
{
// 使用 RAII 的智能指针类管理 new 出来的数组以后,程序简单多了
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
无论 Divide 是否抛异常,sp1 和 sp2 析构时都会自动释放内存。
三、C++ 标准库中的智能指针
C++ 标准库中的智能指针都在 <memory> 头文件下。除了 weak_ptr,它们都符合 RAII 和像指针一样访问的行为,原理上的主要区别在于拷贝时的处理思路不同。
| 智能指针 | 标准 | 拷贝 | 特点与建议 |
|---|---|---|---|
auto_ptr |
C++98 | 支持(转移管理权) | 拷贝后被拷贝对象悬空,设计糟糕,强烈建议不要使用 |
unique_ptr |
C++11 | 不支持,只支持移动 | 不需要拷贝的场景优先使用 |
shared_ptr |
C++11 | 支持拷贝和移动 | 需要共享资源的场景使用,底层是引用计数 |
weak_ptr |
C++11 | 不支持 RAII | 不能直接管理资源,用于解决 shared_ptr 循环引用 |
3.1 auto_ptr:一个糟糕的设计
auto_ptr 在拷贝时把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空,访问就会报错。C++11 之后很多公司明令禁止使用它。
3.2 unique_ptr:独占所有权
unique_ptr 不支持拷贝,只支持移动。移动之后原对象也会悬空,所以使用移动要谨慎。
3.3 shared_ptr:共享所有权
shared_ptr 支持拷贝和移动,底层通过引用计数实现。每多一个 shared_ptr 指向同一资源,计数加一;析构时计数减一,减到 0 才真正释放资源。
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象 ap1 悬空
auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1 对象已经悬空
// ap1->_year++;
unique_ptr<Date> up1(new Date);
// 不支持拷贝
// unique_ptr<Date> up2(up1);
// 支持移动,但是移动后 up1 也悬空,所以使用移动要谨慎
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
// 支持拷贝
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 3
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// 支持移动,但是移动后 sp1 也悬空,所以使用移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
3.4 删除器:管理非 new 资源
智能指针析构时默认执行 delete,如果资源不是 new 出来的,直接交给智能指针管理就会崩溃。为此智能指针支持在构造时传入删除器------本质是一个可调用对象,里面实现你想要的释放方式。
因为 new[] 经常使用,unique_ptr 和 shared_ptr 都特化了一份 [] 版本:
cpp
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
也可以用仿函数、函数指针或 lambda 作为删除器:
cpp
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
int main()
{
// 这样实现程序会崩溃
// unique_ptr<Date> up1(new Date[10]);
// shared_ptr<Date> sp1(new Date[10]);
// 解决方案 1:使用特化版本,析构时用 delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
// 解决方案 2:仿函数对象做删除器
// unique_ptr 在类模板参数中指定删除器,shared_ptr 在构造函数参数中传入
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda 表达式做删除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
注意:
unique_ptr的删除器通过类模板参数指定,shared_ptr的删除器通过构造函数参数传入,两者方式并不统一。
3.5 make_shared 与 operator bool
shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。shared_ptr 和 unique_ptr 都支持 operator bool 类型转换,可以直接放进 if 判断是否为空。
cpp
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp4 is nullptr" << endl;
// 报错:构造函数被 explicit 修饰,禁止普通指针隐式转换成智能指针
// shared_ptr<Date> sp5 = new Date(2024, 9, 11);
// unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
四、智能指针的原理与模拟实现
下面模拟实现 auto_ptr、unique_ptr 和 shared_ptr 的核心功能。重点看 shared_ptr 的引用计数设计。
一份资源需要一个引用计数,所以引用计数不能用静态成员实现,必须在堆上动态开辟:构造智能指针对象时 new 一个引用计数出来;多个 shared_ptr 指向同一资源时计数加一;析构时计数减一,减到 0 才释放资源。
cpp
namespace bit
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
: _ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移 ap 中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
template<class T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp)
: _ptr(sp._ptr)
{
sp._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& sp)
{
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
return *this;
}
private:
T* _ptr;
};
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
, _del(del)
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
++(*_pcount);
}
void release()
{
if (--(*_pcount) == 0)
{
// 最后一个管理的对象,释放资源
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
release();
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
// atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
// 这里的 shared_ptr 和 weak_ptr 都以最简洁的方式实现,只能满足基本功能,
// 这里的 weak_ptr lock 等功能无法实现。想要实现就要把 shared_ptr 和 weak_ptr
// 一起改,把引用计数拿出来放到一个单独类型,shared_ptr 和 weak_ptr 都要存储
// 指向这个类的对象才能实现。
template<class T>
class weak_ptr
{
public:
weak_ptr()
{}
weak_ptr(const shared_ptr<T>& sp)
: _ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr = nullptr;
};
}
使用自己实现的版本验证一下:
cpp
int main()
{
bit::auto_ptr<Date> ap1(new Date);
// 拷贝时,管理权限转移,被拷贝对象 ap1 悬空
bit::auto_ptr<Date> ap2(ap1);
// 空指针访问,ap1 对象已经悬空
// ap1->_year++;
bit::unique_ptr<Date> up1(new Date);
// 不支持拷贝
// bit::unique_ptr<Date> up2(up1);
// 支持移动,但是移动后 up1 也悬空,所以使用移动要谨慎
bit::unique_ptr<Date> up3(move(up1));
bit::shared_ptr<Date> sp1(new Date);
// 支持拷贝
bit::shared_ptr<Date> sp2(sp1);
bit::shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}
五、shared_ptr 与 weak_ptr
5.1 shared_ptr 的循环引用问题
shared_ptr 大多数情况下管理资源非常合适,既支持 RAII 也支持拷贝。但在循环引用的场景下会导致资源无法释放,造成内存泄漏。
考虑一个双向链表节点:
cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _next;
std::shared_ptr<ListNode> _prev;
// 这里改成 weak_ptr,当 n1->_next = n2; 绑定 shared_ptr 时
// 不增加 n2 的引用计数,不参与资源释放的管理,就不会形成循环引用了
/*std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;*/
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用 -- 内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl; // 1
cout << n2.use_count() << endl; // 1
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl; // 2
cout << n2.use_count() << endl; // 2
return 0;
}
为什么两个节点都释放不掉?
- 右边的节点什么时候释放?由左边节点的
_next管着,_next析构后右边节点才释放。 _next什么时候析构?_next是左边节点的成员,左边节点释放,_next才析构。- 左边节点什么时候释放?由右边节点的
_prev管着,_prev析构后左边节点才释放。 _prev什么时候析构?_prev是右边节点的成员,右边节点释放,_prev才析构。
逻辑上形成了一个回旋镖式的循环引用,谁都不会释放,最终导致内存泄漏。
把 ListNode 中的 _next 和 _prev 改成 weak_ptr 即可解决:weak_ptr 绑定到 shared_ptr 时不会增加引用计数,不参与资源释放管理,循环引用被成功打破。
5.2 weak_ptr 的用法
weak_ptr 不支持 RAII,也不支持访问资源。它构造时不支持绑定到资源,只支持绑定到 shared_ptr,绑定时不增加引用计数。
weak_ptr 也没有重载 operator* 和 operator->,因为它不参与资源管理,如果它绑定的 shared_ptr 已经释放了资源,直接访问就很危险。它提供了以下接口:
expired():检查指向的资源是否过期;use_count():获取shared_ptr的引用计数;lock():返回一个管理资源的shared_ptr。如果资源已释放,返回空对象;否则通过返回的shared_ptr访问资源是安全的。
cpp
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl; // 0
cout << wp.use_count() << endl; // 2
// sp1 和 sp2 都指向了其他资源,则 weak_ptr 就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl; // 0
cout << wp.use_count() << endl; // 1
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl; // 1
cout << wp.use_count() << endl; // 0
wp = sp1;
auto sp3 = wp.lock();
cout << wp.expired() << endl; // 0
cout << wp.use_count() << endl; // 2
*sp3 += "###";
cout << *sp1 << endl; // 222222###
return 0;
}
六、shared_ptr 的线程安全问题
shared_ptr 的线程安全问题要分两层看:
- 引用计数 :引用计数对象在堆上,多个线程同时拷贝、析构
shared_ptr时会修改引用计数,存在线程安全问题,需要加锁或用原子操作保证。 - 指向的对象 :对象本身的线程安全不归
shared_ptr管,也管不了,应该由外层使用shared_ptr的人进行控制。
下面的程序可能崩溃,也可能出现 A 资源没释放的情况。把 bit::shared_ptr 的引用计数从 int* 改成 atomic<int>* 就能保证引用计数的线程安全,或者使用互斥锁加锁也可以。
cpp
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
bit::shared_ptr<AA> p(new AA);
const size_t n = 100000;
mutex mtx;
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会 ++ 计数
bit::shared_ptr<AA> copy(p);
{
unique_lock<mutex> lk(mtx);
copy->_a1++;
copy->_a2++;
}
}
};
thread t1(func);
thread t2(func);
t1.join();
t2.join();
cout << p->_a1 << endl;
cout << p->_a2 << endl;
cout << p.use_count() << endl;
return 0;
}
七、C++11 与 boost 中智能指针的关系
Boost 库是为 C++ 标准库提供扩展的程序库总称,Boost 社区的初衷之一就是为 C++ 标准化工作提供可供参考的实现,C++11 及之后的新语法和库有很多都来自 Boost。智能指针的演进大致如下:
- C++98 中产生了第一个智能指针
auto_ptr; - Boost 给出了更实用的
scoped_ptr/scoped_array、shared_ptr/shared_array和weak_ptr等; - C++ TR1 引入了
shared_ptr等,但 TR1 并不是标准版; - C++11 引入了
unique_ptr、shared_ptr和weak_ptr,其中unique_ptr对应 Boost 的scoped_ptr,这些智能指针的实现原理参考了 Boost 中的实现。
八、内存泄漏
8.1 什么是内存泄漏,有什么危害
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放,或者发生异常导致释放程序未能执行。内存泄漏并不是指内存在物理上消失,而是应用程序分配某段内存后,因为设计错误失去了对该段内存的控制,从而造成内存浪费。
普通程序运行一会就结束了,出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。但长期运行的程序出现内存泄漏影响很大,比如操作系统、后台服务、长时间运行的客户端等,不断泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
cpp
int main()
{
// 申请一个 1G 未释放,这个程序多次运行也没啥危害
// 因为程序马上就结束,进程结束各种资源也就回收了
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
8.2 如何检测内存泄漏
- Linux 下:使用 Valgrind 等内存泄漏检测工具;
- Windows 下:使用第三方工具 VLD(Visual Leak Detector)等。
8.3 如何避免内存泄漏
- 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配释放。但碰上异常时,即使注意释放了还是可能出问题,需要智能指针来管理才有保证。
- 尽量使用智能指针管理资源,如果场景比较特殊,可以采用 RAII 思想自己造个轮子管理。
- 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者收费。
总结一下:内存泄漏非常常见,解决方案分为两种------事前预防型 (如智能指针)和事后查错型(如泄漏检测工具)。
九、总结
- 智能指针的核心是 RAII:用对象的生命周期管理资源,析构时自动释放。
auto_ptr拷贝转移管理权,已被废弃;unique_ptr独占所有权,不支持拷贝;shared_ptr共享所有权,基于引用计数。- 引用计数必须放在堆上,不能用静态成员,否则无法做到一份资源一个计数。
weak_ptr不增加引用计数,专门用来打破shared_ptr的循环引用。shared_ptr只保证引用计数的线程安全,指向对象的线程安全需要使用者自己控制。- 内存泄漏重在预防,智能指针是最有效的手段,配合检测工具做兜底。