1. 智能指针的使用场景分析
虽然有了析构,但C++程序员仍然无法避免手动管理内存,麻烦且容易导致内存泄漏。尤其是有了异常之后,过于跳跃,很不可控。C++是一门对性能要求比较高的语言,相比其他语言选择的垃圾回收器方案,C++选择了智能指针。
下⾯程序中我们可以看到,new 了以后,我们也 delete 了,但是因为抛异常,正常 Func 函数逻辑中的 delete 没有得到执⾏,内存泄漏了。所以 new 以后需要立马捕捉 catch(…),如果捕获到异常就 delete 释放,再把异常重新抛出。但是 new 本⾝也可能抛异常,连续的两个 new 和下⾯的 Divide 都可能会抛异常,处理起来很⿇烦,每一个可能失败抛异常的都需要 catch 捕捉走一遍冗余的代码。
代码示例 1.1
#include <iostream>
#include <exception>
using namespace std;
// 基础除法函数
double Divide(int a, int b)
{
// 当除数为0时,主动抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
// 核心业务函数
void Func()
{
// 使用裸指针(Raw Pointers)管理动态内存
int* array1 = new int[10];
// 危险点1:如果这里 new 失败(抛出 std::bad_alloc),
// array1 已经分配但还没来得及被任何机制接管,直接导致内存泄漏!
int* array2 = new int[10];
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (…)
{
// 危险点2:如果 Divide 抛出异常,程序会直接跳转到 catch 块。
// 为了保证不泄漏,我们必须在 catch 中手动释放内存。
cout << "捕获异常,正在清理资源…" << endl;
delete[] array1;
delete[] array2;
// 重新抛出异常,交给外层处理
throw;
}
// 危险点3:代码冗余与维护
// 如果 try 块正常执行完毕,我们需要在函数末尾再次手动释放内存。
// 如果函数中有多个 return 语句,或者嵌套了更多的 try-catch,
// 每一个出口都必须记得 delete,极易遗漏!
delete[] array1;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
// 捕获 Divide 中抛出的字符串异常
cout << "字符串异常: " << errmsg << endl;
}
catch (const exception& e)
{
// 捕获标准库异常(如 new 失败抛出的 std::bad_alloc)
cout << "标准异常: " << e.what() << endl;
}
catch (…)
{
// 兜底捕获所有未知异常
cout << "未知异常" << endl;
}
return 0;
}
2. RAII和智能指针的设计思路
RAII是 Resource Acquisition Is Initialization 的缩写,即资源获取即初始化。意思是,申请到资源后,不应该由程序员自己管理,而是交给一个类对象进行管理,相当于交给这个类的构造函数来初始化该对象,这个对象就与这个资源绑定了,对象的生命周期结束,资源随之释放。
- RAII是⼀种管理资源的类的设计思想,本质是利⽤对象的⽣命周期来管理获取到的动态资源,避免资源泄漏。这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。
- RAII在获取资源时把资源委托给⼀个对象,借对象控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源。这种方式保障了资源的正常释放,避免了资源泄漏问题。
- 智能指针这个类除了要满⾜RAII的设计思路,还要⽅便资源的访问。所以,智能指针类类型还会像迭代器类型⼀样,模拟指针的行为,重载 operator*/operator->/operator[] 等运算符。
代码示例 2.1
#include <iostream>
#include <exception>
using namespace std;
// 利用RAII思想设计的初级智能指针
// 模板类,支持任意类型的智能指针
template<class T>
class SmartPtr
{
public:
// 资源,或者说空间就是资源,通常都是用一个指针,存储该空间的地址
// 即资源总是由指针指向
// 构造函数:接收一个原始指针,由智能指针接管其生命周期(RAII 核心)
SmartPtr(T* ptr)
:_ptr(ptr)
{}
// 析构函数:当智能指针SmartPtr对象离开作用域被销毁时,自动释放其管理的内存
~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; // 被对象内部管理持有的原始指针
};
// 除法函数
double Divide(int a, int b)
{
if (b == 0)
{
throw "Divide by zero condition!"; // 除数为0时抛出异常
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 使用智能指针管理 new 出来的数组,无需手动 delete
// new返回的就是开辟出来的空间的地址,直接拿来构造初始化智能指针对象
SmartPtr<int> sp1 = new int[10]; // new失败可能抛异常
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;
// 如果这里抛出异常,sp1 和 sp2 的析构函数依然会被调用,内存自动释放
cout << Divide(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (…)
{
cout << "未知异常" << endl;
}
return 0;
}
在异常章节说过,即使抛出异常,也要经历栈展开的过程,沿着函数调用链,直到找到类型匹配的 catch 子句之前,依次释放函数栈帧,各函数内部的局部对象都会随之销毁释放,自然也会调用析构函数。因此即使抛出异常,也会沿着调用链,完成各个智能指针对象的析构。
- 无论是正常结束,还是抛异常,智能指针管理的资源都是有保障的,都可以正常释放。
3. C++标准库智能指针的使用
智能指针真正的问题是拷贝。智能指针是代替管理资源,模拟原生指针的行为。因此,两个智能指针指向同一份资源,应该使用浅拷贝,而不是深拷贝。但两个智能指针对象销毁时,又会调用两次析构释放同一份资源。这就是智能指针必须解决的问题。智能指针的设计思想早在C++98之前已经出现,但C++98推出的智能指针并没有很好的解决上述问题,直到C++11才有了不错的解决方案。
- C++标准库中的智能指针相关都在 <memory> 头⽂件:
前两个是C++98支持的智能指针,后面都是C++11新增的。最关键的是C++11的两个智能指针 unique_ptr 和 shared_ptr,它们同样符合RAII设计思想,并且模拟指针⾏为。auto_ptr/unique_ptr/shared_ptr 是解决智能指针拷⻉问题的3种不同方案,其他几个不算智能指针,更多算是智能指针在实现或使用上的辅助工具。
。特点是拷⻉时会转移资源管理权给新智能指针对象,始终只有一个智能指针管理资源,自然只析构一次。但这是⼀个糟糕的设计,它会让原本的被拷⻉对象悬空,即“空指针”问题,导致越界访问报错。强烈建议不要使⽤ auto_ptr。即使C++11出来之前很多公司也是明令禁⽌使⽤ auto_ptr 的,例如谷歌之前的C++规范有一条就是明确不要使用C++98的 auto_ptr。
//unique_ptr<Date> up1(new Date[5]);
unique_ptr<Date[]> up1(new Date[5]);
//shared_ptr<Date> sp1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
// 使用删除器
// lambda函数对象
shared_ptr<int> up2(new int[5], [](int* ptr) {
delete[] ptr;
});
// 仿函数对象
class Fclose
{
public:
void operator()(FILE* ptr)
{
fclose(ptr);
}
};
shared_ptr<FILE> sp2(fopen("Test.cpp", "r"), Fclose());
// 函数指针
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// 注意,unique_ptr的删除器在unique_ptr类模板处声明,即类型处声明
// 实例化时需要声明删除器的类型,而不是将删除器对象传参给构造函数
// 函数指针/仿函数等好用,lambda函数对象就不好用,拿不到它的类型
unique_ptr<FILE, Fclose> up3(fopen("Test.cpp", "r"));
// lambda必须用function包装器或者bind一下,或者用auto + decltype
auto delArr = [](Date* ptr) { delete[] ptr; };
std::unique_ptr<Date[], decltype(delArr)> up4(new Date[5], delArr);
// unique_ptr建议用仿函数作为删除器,shared_ptr都可以,相对建议lambda
//shared_ptr<Date> sp(new Date(2024, 9, 11));
// 可直接用初始化Date对象的值2024、9、11初始化sp
shared_ptr<Date> sp = make_shared<Date>(2024, 9, 11);
// make_shared的好处就是减少内存碎片问题
// 它不是直接new一个Date对象再交给sp的构造,再单独开辟一个引用计数
// make_shared拿到的是构造Date对象的值,Date对象还未创建
// make_shared同时为Date对象和引用计数申请空间,将空间一起开辟,避免碎片化
// 而且两个空间一起开辟,只需要开辟一次,效率也高一点点
// 内存碎片化问题另一种解决方案就是内存池
// 我们可以单独为引用计数开辟一个内存池,传给shared_ptr的构造
// 标准库有支持该构造函数重载,引用计数全部从这个内存池中申请 
shared_ptr<Date> sp(new Date);// if (sp) 等价于 if (sp.operator bool())
if (sp) cout << "非空对象,sp有管理资源" << endl;
else cout << "空对象,sp没有管理资源" << endl;
// new Date(2024, 1, 1)构造Date匿名对象,返回Date对象的原始指针
// 再用Date对象的原始指针隐式类型转换成shared_ptr<Date>或unique_ptr<Date>
// 这种隐式类型转换是不可以的,包括其他类型的普通指针,都不可以转换成智能指针
shared_ptr<Date> sp = new Date(2024, 1, 1);
unique_ptr<Date> up = new Date(2022, 1, 2);
// 只可以直接调用智能指针的构造函数,构造智能指针对象
shared_ptr<Date> sp(new Date(2024, 1, 1));
unique_ptr<Date> up(new Date(2022, 1, 2));
代码示例 3.1
#include <iostream>
#include <memory>
using namespace std;
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;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// ⽀持移动,但是移动后sp1也悬空,所以使⽤移动要谨慎
shared_ptr<Date> sp4(move(sp1));
return 0;
}
运行结果:
4. 智能指针的原理
下⾯模拟实现了 auto_ptr 和 unique_ptr 的核⼼功能,这两个智能指针的实现⽐较简单,了解原理即可。auto_ptr 的思路是拷⻉时转移资源管理权给被拷⻉对象,这种思路是不被认可的,也不建议使⽤ auto_ptr。unique_ptr 不⽀持拷⻉,只支持移动。
代码示例 4.1
// 抢占式:管理权转移
template<class T>
class auto_ptr
{
public:
// 构造函数
auto_ptr(T* ptr)
:_ptr(ptr)
{ }
// 拷贝构造:转移ap的资源给当前对象*this
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
{
// 抢占式,原对象不再持有资源,避免共同管理导致的两次析构
ap._ptr = nullptr;
}
// 拷贝赋值:将ap的资源全部掠夺过来
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 避免自己给自己赋值
if (this != &ap)
{
// 释放当前智能指针对象*this自己的资源
if (_ptr) delete _ptr;
// 转移ap中的资源到当前对象*this
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
// 析构:delete释放当前对象*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禁止隐式类型转换,避免普通指针转换成智能指针
explicit unique_ptr(T* ptr)
:_ptr(ptr)
{ }
// 析构:delete释放当前对象*this的资源
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 模拟指针行为,重载运算符
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// 禁止拷贝构造和拷贝赋值,全部设为删除函数
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
// 支持移动构造:掠夺up的资源
unique_ptr(unique_ptr<T>&& up)
: _ptr(up._ptr)
{
// 掠夺后up置空,避免共同管理析构两次
up._ptr = nullptr;
}
// 支持移动赋值:掠夺up的资源
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
// 先释放当前对象*this自己的资源
delete _ptr;
// 掠夺
_ptr = up._ptr;
up._ptr = nullptr;
}
private:
T* _ptr; // 指向被管理的资源
};
重点是 shared_ptr 的设计,尤其是引⽤计数的设计。
- 多个 shared_ptr 对象共同管理同一份资源,⼀份资源代表⼀个引⽤计数,每多一个管理者,引用计数加1。智能指针对象生命周期到了,调用析构,让引用计数减1,除非引用计数减到0才释放资源。
- 强调:不可使用静态成员变量作为引用计数。静态成员变量属于这个类的所有对象,只要智能指针类型相同,就会共有静态成员变量。同样类型的资源可以有多份,管理这些资源的智能指针类型都相同,只有一个静态成员变量,却需要多份引用计数。因此,引⽤计数要使⽤堆上动态开辟的⽅式。构造智能指针对象时,根据资源的数量 new 引⽤计数,而不是资源的类型。
代码示例 4.2
// 共享式:管理权共有
template<class T>
class shared_ptr
{
public:
// 构造函数:多一份资源就多一个引用计数
// explicit禁止普通指针通过该构造隐式类型转换成智能指针
explicit shared_ptr(T* ptr == nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{
}
// 拷贝构造:多一个管理者,引用计数++
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}
// 拷贝赋值:当前管理者资源的引用计数–,转而管理sp的资源,对应引用计数++
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 避免同一份资源的管理者之间互相赋值,包括自己给自己赋值
if (_ptr != sp._ptr)
{
if (–(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
// 析构:引用计数大于0则–,等于0则释放资源
~shared_ptr()
{
if (–(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; // 引用计数通过动态内存申请
};
定制删除器
#include <iostream>
#include <functional>
using namespace std;
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;
}
};
namespace david
{
template<class T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new int(1))
{
}
// 带定制删除器的构造
// 难点1:需要保持删除器,D是删除器的类型,但只属于当前函数模板,
// 类里面用不了,定义不了函数对象
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)
{
(*_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)
{
// 释放当前对象*this的资源
release();
// 拷贝sp共同管理资源
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
// 析构释放资源
release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
private:
T* _ptr;
int* _pcount; // 引用计数通过动态内存申请
// 用function包装器解决删除器的类型问题
// void是可调用对象的返回类型,(T*)是可调用对象的参数列表
// function的默认构造是禁用的,不可调用
// 必须给缺省值,默认的lambda执行delete删除
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
}
int main()
{
david::shared_ptr<Date> sp1(new Date);
david::shared_ptr<Date> sp2(new Date[10], [](Date* ptr) { delete[] ptr; });
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
5. shared_ptr 和 weak_ptr
5.1 shared_ptr 的循环引用问题
大部分情况下,shared_ptr 非常合适用于管理资源,⽀持RAII,也⽀持拷⻉。但是在极端场景下,即循环引⽤的场景下,会导致内存泄漏。
下面我们来看看:
代码示例 5.1.1
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
// 双向循环链表的结点
// 有前驱结点指针_next,同时也有后继结点指针_prev
// 都通过智能指针管理前驱结点和后继结点
int _data;
shared_ptr<ListNode> _next = nullptr;
shared_ptr<ListNode> _prev = nullptr;
// 不可以用原生指针ListNode*管理,这样在外面赋值的话
// 例如n1->_next = n2就编译报错了
// 因为n1->_next 是ListNode*,但n2是shared_ptr<ListNode>
//ListNode* _next = nullptr;
//ListNode* _prev = nullptr;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 智能指针n1和n2分别管理两个链表结点
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// n1的后继结点是n2
n1->_next = n2;
// n2的前驱结点是n1
n2->_prev = n1;
// 构成了互相指向的情况
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
我们只定义了两个智能指针对象,分别管理结点。通过 n1 管理结点1,即资源1;通过 n2 管理结点2,即资源2。智能指针对象 n1 析构销毁则释放结点1,智能指针对象 n2 析构销毁则释放结点2。
但实际上:

结点1想释放,还得销毁结点2的智能指针对象 _prev,想销毁 _prev 就得释放结点2。反过来,想释放结点2,就得释放结点1,陷入了无法破解的循环。
究其原因,本质是结点本身就是资源,但资源内部还管理着其他资源,并且参与影响了引用计数。_next 和 _prev 可以指向结点/资源,但不应该参与管理,更不应该参与引用计数。
- 因此,可以将 _next 和 _prev 改成 std::weak_ptr。weak_ptr 是弱引用,不是RAII设计,不参与管理资源,用 shared_ptr 智能指针构造或赋值 weak_ptr 智能指针不会增加它的引⽤计数。_next 和 _prev 不参与资源释放管理逻辑,就成功打破了循环引⽤,解决了这⾥的问题。



weak_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()
{
}
// shared_ptr构造
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{
}
// shared_ptr拷贝赋值
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
// 弱引用机制:只进行安全观测与访问,只是帮shared_ptr保存资源的工具
T* _ptr = nullptr;
};
5.2 weak_ptr
- weak_ptr 不⽀持RAII,也不⽀持访问资源,所以我们看⽂档发现 weak_ptr 构造不⽀持绑定到资源,只⽀持绑定到 shared_ptr。
- weak_ptr 也没有重载 operator* 和 operator-> 等,不模拟指针的访问行为。因为不参与资源管理就不能随意访问。如果绑定的 shared_ptr 已经释放了资源,再去访问资源就是很危险的。
- 标准库中通常会让 weak_ptr 保存一份 shared_ptr 的引用计数,避免悬空风险,但这也让它必须参与到引用计数的管理。引用计数本身也是一种资源,堆上动态开辟自然也要释放。如果 shared_ptr 释放资源的同时,也无脑释放引用计数。weak_ptr 既无法访问资源,也无法通过引用计数判断资源能不能访问。因此,引用计数必须被共同管理。通常会将引用计数提取出来单独封装成一个类,通过对象的生命周期进行管理。weak_ptr 和 shared_ptr 各自保存一份引用计数对象,所有 weak_ptr 对象和 shared_ptr 对象都销毁了,没有引用计数对象被保存了,才会调用引用计数的析构释放引用计数。
- 因此,weak_ptr 也有 use_count,可以获取绑定的 shared_ptr 当前的引用计数,因为 shared_ptr 的引用计数不会跟着资源一起释放,所以还在,但会减到0,表示 weak_ptr 为空或者绑定的 shared_ptr 的资源释放了。weak_ptr 也支持 expired 接口检查指向的资源是否过期。确实过期了,就返回true;没有过期,就返回false,底层其实也是通过判断引用计数实现的。


- weak_ptr 虽然不参与资源的管理,但可能有访问资源的需求。我们可以调⽤ weak_ptr::lock 接口返回⼀个管理资源的 shared_ptr。如果资源已经被释放,返回的 shared_ptr 就是⼀个空对象;如果资源没有释放,就可以直接访问资源。因此,通过返回的 shared_ptr 访问资源是绝对安全的。我们可以根据返回对象判断是否能安全访问。

6. shared_ptr 的线程安全问题
shared_ptr 的引⽤计数对象在堆上。如果多个 shared_ptr 对象在多个线程中,拷⻉或析构时访问修改引⽤计数就会出现线程安全问题,因为引用计数是共享资源。例如在两个线程中,可能同时有两个对象析构,同时获取到当前的引用计数为5,各自执行引用计数减1,结果为4,但实际少了两个对象,应该减到3。这就是 shared_ptr 的线程安全问题。
- 解决引用计数的线程安全问题,必须保证 shared_ptr 的引用计数操作是原子性的。可通过加锁或者其他原⼦操作来保证线程安全。例如在 shared_ptr 中加一把锁,将引用计数变为临界资源,每次访问或修改操作前先尝试获取锁,持有锁才可以进行操作,操作结束必须释放锁,确保各个线程执行这部分代码时都是互斥的 (锁,或称互斥量,也相当于一种引用计数,但只有0或1,同样通过对象的生命周期管理,操作完出了作用域对象就销毁析构进行解锁)。
- 因此,标准库中的 std::shared_ptr 本身是线程安全的,但管理的资源就不一定了。不过这个对象的线程安全问题也不归 shared_ptr 管,应该由外层的使⽤者进⾏线程安全的控制。
可以使用互斥锁,也可以直接将 shared_ptr 中的引⽤计数从 int* 改成 atomic<int>*,保证对该类型的变量的操作都是原子的,也可以保证线程安全。
代码示例 6.1
#include <iostream>
#include <memory>
#include <functional>
#include <atomic>
#include <thread>
#include <mutex>
using namespace std;
namespace david
{
template<class T>
class shared_ptr
{
public:
// 构造函数:_pcount改为atomic<int>*类型
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
{ }
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new atomic<int>(1))
, _del(del)
{ }
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_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& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
private:
T* _ptr;
//int* _pcount;
// 改为atomic<int>*类型,该变量的操作就是原子的,保证线程安全
atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
}
// 一个自定义类型AA,有两个变量_a1和_a2
struct AA
{
int _a1 = 0;
int _a2 = 0;
~AA()
{
cout << "~AA()" << endl;
}
};
int main()
{
// 通过shared_ptr智能指针管理AA对象
david::shared_ptr<AA> p(new AA);
// n为10万
const size_t n = 100000;
// 对资源的操作不是线程安全的,外部自己管理
// 这里用互斥锁管理,定义一个锁对象mtx
mutex mtx;
// 定义一个lambda函数对象
// 进行10万次循环,每次都拷贝一份shared_ptr智能指针
// 该操作多一个copy管理资源,增加引用计数,但循环很快结束,引用计数又减回去
// 关键在于多线程,可能同时有多个线程中在执行这段代码
// 但引用计数是atomic类型,保证线程互斥,同时只有一个线程修改引用计数
auto func = [&]()
{
for (size_t i = 0; i < n; ++i)
{
// 拷贝智能指针,shared_ptr内对引用计数的操作都是线程安全的
david::shared_ptr<AA> copy(p);
// 对资源进行操作,修改智能指针管理的对象中的_a1和_a2
{
// 线程不安全,必须自己确保线程安全,可以通过加锁
// 尝试获取mtx锁,并用unique_lock唯一锁进行加锁与管理
// 同一时间只有一个线程可以拿到锁
unique_lock<mutex> lk(mtx);
// 拿到锁以后,执行对资源的操作
copy->_a1++;
copy->_a2++;
// {}花括号围起来就算一个作用域,lk出去就析构释放锁/解锁
}
// 拷贝完毕,先加锁,再进行资源操作,操作完锁也释放了,进行下一个循环
}
};
// 创建两个线程一起执行func
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;
}
运行结果:
结果为20万。因为两个线程分别执行 func,各自创建函数栈帧,各自创建自己的 i 变量,都是10万,各自进行了10万次循环,两个线程共执行20万次循环,但被智能指针管理的 AA 对象是共享的,AA 对象中的 _a1 和 _a2 相当于加了20万次。
7. C++11和boost中智能指针的关系
Boost库,也被称为C++的准标准库,是为C++标准库提供扩展的⼀些C++程序库的总称。Boost社区建⽴的初衷之⼀就是为C++的标准化⼯作提供可供参考的实现,社区发起⼈Dawes本⼈就是C++标准委员会的成员之⼀。在Boost库的开发中,Boost社区确实在这个⽅向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的,例如右值引用、移动语义、智能指针等。
- C++98产⽣了第⼀个智能指针 auto_ptr。
- C++boost库给出了更实⽤的 scoped_ptr/scoped_array、shared_ptr/shared_array 和 weak_ptr 等。
- C++TR1引⼊了 shared_ptr 等,但C++TR1并不是标准版,而是作为标准库的扩展。
- C++11,引⼊了 unique_ptr、shared_ptr 和 weak_ptr。unique_ptr 即对应boost的 scoped_ptr。这些智能指针的实现原理都是参考boost库实现的。
8. 内存泄漏
- 内存泄漏:程序运行过程中,申请了资源,但资源不再使用了却没有释放,即因为疏忽或错误造成程序未能释放已经不再使⽤的内存。
⼀般是忘记释放或者发⽣异常没能执行释放代码。内存泄漏并不是内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费。
- 内存泄漏的危害:运行一会就结束的普通程序,出现内存泄漏基本没什么影响,进程正常结束,⻚表的映射关系解除,物理内存一样释放。但更多的是⻓期运⾏的程序,如果出现内存泄漏影响很⼤,如操作系统、后台服务、⻓时间运⾏的客户端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死。
代码示例 8.1
int main()
{
// 申请1G内存未释放
// 程序⻢上就结束,进程结束各种资源就回收
// 毫无影响
char* ptr = new char[1024 * 1024 * 1024];
cout << (void*)ptr << endl;
return 0;
}
避免内存泄漏要做到:
- ⼯程前期有良好的设计规范,养成良好的编码规范,申请了内存空间就匹配释放。但大多是出现异常等,就算注意释放了,还是可能出问题。通过智能指针来管理才是最有保证的。
- 尽量使⽤智能指针管理资源,且保证正确使用智能指针,如果场景⽐较特殊,也可以采⽤RAII思想⾃⼰造个轮⼦管理。
- 可以定期使⽤内存泄漏⼯具帮助检测程序,尤其是每次项⽬快上线前,但需要找到靠谱的⼯具,还要考虑收费情况和成本。
总结:内存泄漏⾮常常⻅,解决⽅案分两种:1、事前预防型。如智能指针等;2、事后查错型。如泄漏检测⼯具。一般还是倾向方案1,提前设计hao




