文章目录
- [C++ 智能指针](#C++ 智能指针)
C++ 智能指针
为什么需要智能指针?
我们看下面这个代码有什么问题,提示请看Func函数接口:
cpp
#include <iostream>
#include <stdexcept>
#include <exception>
int div()
{
int a,b;
std::cin >> a >> b;
if(b == 0)
{
throw std::invalid_argument("除0错误");
}
return a / b;
}
void Func()
{
int *p1 = new int(1);
int *p2 = new int(2);
std::cout << div() << std::endl;
delete p1;
delete p2;
}
int main()
{
try{
Func();
}catch(std::exception& e){
std::cout << e.what() << std::endl;
}
return 0;
}
通过我们对C++的学习和了解,我们分析一下上面代码的问题:
p1 new的时候抛异常,没造成内存泄漏。因为p1申请空间未成功;p2 new的时候抛异常,造成p1的内存泄漏。因为抛异常后p1的delete操作无法执行;div抛异常,造成p1、p2的内存泄漏。因为抛异常后p1、p2的delete操作都无法执行。
内存泄漏
什么是内存泄漏,内存泄漏的危害
- 内存泄漏:程序申请了内存,但因为设计错误或异常路径,失去了对该内存的控制,导致无法释放。不是内存物理消失,而是这部分内存无法再被使用。
- 危害:长期运行的程序,如操作系统、后台服务,内存泄漏会导致响应越来越慢,最终卡死。
cpp
void Func()
{
int *p1 = new int(1);
int *p2 = new int(2);
std::cout << div() << std::endl;
delete p1;
delete p2;
}
抛异常之后就会导致申请的内存无法释放。
内存泄漏分类
- 堆内存泄漏:
malloc/calloc/realloc/new申请,未用free/delete释放。 - 系统资源泄漏:套接字、文件描述符、管道、锁等未释放,可能导致系统性能下降或不稳定。
如何检测内存泄漏
在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
如何避免内存泄漏
- 良好编码规范,申请与释放匹配。但异常场景仍可能出问题。
- 采用 RAII 思想或智能指针管理资源。
- 公司内部私有内存管理库。
- 出问题后用泄漏检测工具事后查错。
总结一下:
内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源 (如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源 ,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
使用RAII思想我们重新设计一些刚才那个代码:
cpp
template <class T>
class SmartPtr{
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
~SmartPtr() {
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
// 简化一下,禁止拷贝
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
private:
T* _ptr;
};
int div()
{
int a,b;
std::cin >> a >> b;
if(b == 0)
{
throw std::invalid_argument("除0错误");
}
return a / b;
}
void Func()
{
// int *p1 = new int(1);
// int *p2 = new int(2);
SmartPtr<int> p1(new int(1));
SmartPtr<int> p2(new int(2));
std::cout << div() << std::endl;
// delete p1;
// delete p2;
}
int main()
{
try{
Func();
}catch(std::exception& e){
std::cout << e.what() << std::endl;
}
return 0;
}
这样当b输入0时就不会导致内存泄漏了,因为抛异常的时候p2、p1 依次析构,资源自动释放。
智能指针的原理
但是仅有 RAII 还不够,上述的SmartPtr 不能称为智能指针。因为智能指针还需要重载:
operator*:解引用;operator->:访问成员。
因此AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
cpp
template <class T>
class SmartPtr{
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
~SmartPtr() {
if (_ptr) {
std::cout << "delete: " << _ptr << std::endl;
delete _ptr;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
// 简化一下,禁止拷贝
SmartPtr(const SmartPtr&) = delete;
SmartPtr& operator=(const SmartPtr&) = delete;
private:
T* _ptr;
};
struct Date {
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
SmartPtr<int> sp1(new int(0));
*sp1 = 10;
std::cout << *sp1 << std::endl;
SmartPtr<Date> sparray(new Date);
// 等价于 sparray.operator->()->_year = 2018;
sparray->_year = 1949;
sparray->_month = 10;
sparray->_day = 1;
std::cout << sparray->_year << "-" << sparray->_month << "-" << sparray->_day << std::endl;
return 0;
}
智能指针我们可以简单总结为:
- RAII 特性:构造接管资源,析构释放资源;
- 重载
operator*和operator->:具有像指针一样的行为。
std::auto_ptr
C++98 提供了 auto_ptr。它的实现思想是管理权转移:
- 拷贝构造时,把原对象的指针转移给新对象,原对象置空;
- 赋值时,先释放当前对象资源,再转移对方资源,对方置空。
缺陷:
- 拷贝后原对象变成空指针,继续使用会空指针解引用;
- 语义反直觉:拷贝本应不改变源对象,但
auto_ptr会改变; - 不能很好用于 STL 容器,容器拷贝可能导致资源所有权被转移,源对象悬空/为空;
- C++11 弃用,C++17 移除。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份xhs::auto_ptr来了解它的原理
cpp
namespace xhs{
template<class T>
class auto_ptr{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if(this != &ap)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if(_ptr)
{
std::cout << "delete:" << _ptr << std::endl;
delete _ptr;
}
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
}
int main()
{
xhs::auto_ptr<int> ap1(new int(10));
xhs::auto_ptr<int> ap2(ap1);
std::cout << (ap1.get() == nullptr) << std::endl; // 1,ap1 已被置空
std::cout << *ap2 << std::endl; // 10
xhs::auto_ptr<int> ap3(new int(20));
ap3 = ap2; // 释放 ap3 原来的 20,转移 ap2 的资源
std::cout << (ap2.get() == nullptr) << std::endl; // 1
std::cout << *ap3 << std::endl; // 10
return 0;
}
std::unique_ptr
C++11中开始提供更靠谱的unique_ptr
- 同一时刻只能有一个
unique_ptr管理某资源; - 禁止拷贝构造和拷贝赋值;
- 支持移动构造和移动赋值,转移所有权后源对象为空;
- 对应 boost 的
scoped_ptr思路,但unique_ptr支持移动语义。
为什么禁止拷贝?如果两个 unique_ptr 指向同一资源,析构时会 delete 两次,导致 double free。独占所有权从类型系统上避免这个问题。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份xhs::unique_ptr来了解它的原理
cpp
namespace xhs
{
template <class T>
class unique_ptr
{
public:
unique_ptr(T *ptr = nullptr)
: _ptr(ptr)
{
}
~unique_ptr()
{
delete _ptr;
}
unique_ptr(const unique_ptr<T> &) = delete;
unique_ptr<T> &operator=(const unique_ptr<T> &) = delete;
T &operator*() const
{
return *_ptr;
}
T *operator->() const
{
return _ptr;
}
private:
T *_ptr;
};
}
int main() {
xhs::unique_ptr<int> up1(new int(10));
// xhs::unique_ptr<int> up2(up1); // 编译错误:禁止拷贝
std::unique_ptr<int> up3(new int(30));
// std::unique_ptr<int> up4(up3); // 编译错误:禁止拷贝
return 0;
}
std::shared_ptr
shared_ptr 支持多个智能指针共享同一资源,原理是引用计数:
- 每个资源维护一份计数,记录被几个
shared_ptr共享; - 拷贝
shared_ptr时计数加一; - 析构时计数减一;
- 计数为 0 时释放资源;
- 计数不为 0 时不能释放,否则其他对象变野指针。
shared_ptr就像最后离开教室的人负责锁门。当最后一个 shared_ptr 析构时才释放资源。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。下面我们简单实现一下xhs::shared_ptr。
cpp
namespace xhs
{
template <class T>
class shared_ptr
{
struct ControlBlock
{
std::atomic<int> count;
ControlBlock() : count(1) {}
};
mutable std::mutex _mtx; // 保护 _ptr / _ctrl 的读写
T *_ptr;
ControlBlock *_ctrl;
// 调用者必须持有 _mtx(当前对象的锁)
void add_ref_locked()
{
if (_ctrl)
_ctrl->count.fetch_add(1, std::memory_order_relaxed);
}
// 不持锁的静态释放函数,便于在锁外调用
static void release(T *ptr, ControlBlock *ctrl)
{
if (ctrl && ctrl->count.fetch_sub(1, std::memory_order_acq_rel) == 1)
{
delete ptr;
delete ctrl;
}
}
public:
explicit shared_ptr(T *ptr = nullptr)
: _ptr(ptr), _ctrl(ptr ? new ControlBlock() : nullptr) {}
shared_ptr(const shared_ptr<T> &sp)
{
// 锁住源对象,读取它当前的 _ptr / _ctrl
std::lock_guard<std::mutex> lk(sp._mtx);
_ptr = sp._ptr;
_ctrl = sp._ctrl;
add_ref_locked();
}
shared_ptr<T> &operator=(const shared_ptr<T> &sp)
{
if (this == &sp)
return *this;
T *old_ptr = nullptr;
ControlBlock *old_ctrl = nullptr;
{
// 同时锁住两边,scoped_lock 内部用 std::lock 避免死锁
std::scoped_lock lk(_mtx, sp._mtx);
// 摘下旧资源
old_ptr = _ptr;
old_ctrl = _ctrl;
_ptr = nullptr;
_ctrl = nullptr;
// 挂上新资源
_ptr = sp._ptr;
_ctrl = sp._ctrl;
add_ref_locked();
}
// 锁外释放旧资源,避免持锁期间 delete 引发死锁
release(old_ptr, old_ctrl);
return *this;
}
~shared_ptr()
{
T *ptr = nullptr;
ControlBlock *ctrl = nullptr;
{
std::lock_guard<std::mutex> lk(_mtx);
ptr = _ptr;
ctrl = _ctrl;
_ptr = nullptr;
_ctrl = nullptr;
}
// 锁外释放
release(ptr, ctrl);
}
T &operator*() const
{
std::lock_guard<std::mutex> lk(_mtx);
return *_ptr;
}
T *operator->() const
{
std::lock_guard<std::mutex> lk(_mtx);
return _ptr;
}
T *get() const
{
std::lock_guard<std::mutex> lk(_mtx);
return _ptr;
}
int use_count() const
{
std::lock_guard<std::mutex> lk(_mtx);
return _ctrl ? _ctrl->count.load(std::memory_order_relaxed) : 0;
}
};
}
// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
int main()
{
xhs::shared_ptr<int> sp1(new int(1));
xhs::shared_ptr<int> sp2(sp1);
xhs::shared_ptr<int> sp3(sp1);
std::cout << sp1.use_count() << std::endl; // 3
xhs::shared_ptr<int> sp4(new int(4));
sp1 = sp4;
std::cout << sp1.use_count() << " "
<< sp2.use_count() << " "
<< sp3.use_count() << std::endl; // 2 2 2
*sp2 = 3;
std::cout << *sp2 << std::endl; // 3
return 0;
}
std::shared_ptr的线程安全问题
通过下面的程序我们来测试shared_ptr的线程安全问题。需要注意的是shared_ptr的线程安全分为两方面:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
cpp
struct Date
{
int year = 0;
int month = 0;
int day = 0;
};
void SharedPtrFunc(xhs::shared_ptr<Date> &sp, std::size_t n, std::mutex &mtx)
{
std::cout << sp.get() << std::endl; // get() 内部已加锁
for (std::size_t i = 0; i < n; ++i)
{
xhs::shared_ptr<Date> copy(sp); // 拷贝构造内部锁住 sp
{
std::lock_guard<std::mutex> lk(mtx);
sp->year++;
sp->month++;
sp->day++;
}
}
}
int main()
{
xhs::shared_ptr<Date> p(new Date);
const std::size_t n = 100000;
std::mutex mtx;
std::thread t1(SharedPtrFunc, std::ref(p), n, std::ref(mtx));
std::thread t2(SharedPtrFunc, std::ref(p), n, std::ref(mtx));
t1.join();
t2.join();
std::cout << std::dec; // 打印前强制十进制
std::cout << p->year << " " << p->month << " " << p->day << std::endl;
std::cout << p.use_count() << std::endl;
return 0;
}
这里锁的写法很精妙,因为lock_guard它是自动解锁的我们这里避免忘记解锁就给它单独搞一个域这样lock_guard运行完出域的时候就自动解锁了不需要我们手动来解锁
std::shared_ptr的循环引用
cpp
struct ListNode
{
int _data;
std::shared_ptr<ListNode> _prev;
std::shared_ptr<ListNode> _next;
~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
node1->_next = node2;
node2->_prev = node1;
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
return 0;
}
我们分析一下上述代码:
node1、node2分别指向两个节点,引用计数为 1;node1->_next = node2,node2->_prev = node1,引用计数变成 2;- 作用域结束,
node1、node2析构,引用计数减到 1; - 但
node1的_next还指向node2,node2的_prev还指向node1; node2要释放,需要node1->_next析构;node1要释放,需要node2->_prev析构;- 谁也不会释放,形成循环引用,资源泄漏。

那么如何解决上面这个问题呢?
只需要我们将shared_ptr换成weak_ptr就可以解决上述问题了,weak_ptr就是C++用来解决shared_ptr的循环引用问题的。
cpp
struct ListNode
{
int _data;
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;
~ListNode() { std::cout << "~ListNode()" << std::endl; }
};
int main()
{
std::shared_ptr<ListNode> node1(new ListNode);
std::shared_ptr<ListNode> node2(new ListNode);
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
node1->_next = node2;
node2->_prev = node1;
std::cout << node1.use_count() << std::endl;
std::cout << node2.use_count() << std::endl;
return 0;
}
为什么weak_ptr能解决循环引用计数问题?
- 因为
weak_ptr是弱引用,不增加强引用计数,不参与所有权。
那么shared_ptr会造成资源泄漏,为什么不把所有地方都换成weak_ptr呢?
- 因为weak_ptr 不拥有资源,不能保证对象一直存活,只适合打破循环或观察场景。实际上weak_ptr底层上是通过调用shread_ptr来访问对象的。
- 那么为什么weak_ptr能解决shared_ptr上的问题呢?明明都是用的同一套代码,因为weak_ptr它不动shared_ptr的引用计数而是底层自己管理这自己的那一套,它的那个计数只是说明weak_ptr能够访问的对象,并不是持有对象。
这里的weak_ptr实现起来比较复杂,所以这里就不提供模拟实现的代码,感兴趣的可以去问ai,让ai来生成对应的代码。
如果不是new出来的对象如何通过智能指针管理呢?其实shared_ptr设计了一个删除器来解决这个问题(只做了解)
cpp
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
std::cout << "free:" << ptr << std::endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
std::cout << "delete[]" << ptr << std::endl;
delete[] ptr;
}
};
// 原图中 sp4 使用了 A,但未定义,这里补上一个空结构体保证编译通过
struct A {};
int main()
{
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p){fclose(p); });
return 0;
}
完