文章目录
前言
通过00【C++ 入门基础】前言得知,C++是为了解决C语言在面对大型项目的局限而诞生:
C语言面对的现实工程问题(复杂性、可维护性、可扩展性、安全性)
C语言面临的具体问题:
struct 数据公开暴露,函数数据分离,逻辑碎片化。(复杂性、安全性)
修改数据结构,如 struct 新增字段,可能导致大量相关函数需要修改。(可维护性)
添加新功能常需修改现有函数或结构体,易引入错误。(可扩展性)
资源(内存、文件句柄)需手动管理,易泄漏或重复释放。(安全性)
C++ 引入 new 和 delete,解决了C 语言动态内存分配机制对 面向对象特性(特别是构造/析构)缺乏支持以及类型不安全的问题。
但是即使如此,我们动态内存资源的申请和释放,还是需要手动,人脑没办法保证所new都有对应的delete;
且即使所有的new都有其对应的delete,在我们的上一节异常出来之后,delete语句也可能被跳过,而造成内存泄漏,所以智能指针的出现,是必要的。
智能指针主要是为了解决 手动管理动态内存带来的复杂性和潜在错误。
1.为什么需要智能指针?
手动使用 new和 delete虽然灵活,但极易导致以下问题:
- 内存泄漏
问题:忘记调用 delete释放不再使用的内存。
void test() {
int* ptr = new int(10);
// ...
// 忘记调用 delete释放不再使用的内存。
}
- 悬空指针
问题:释放内存后未将指针置空,后续误用已释放的内存。
int* ptr = new int(10);
delete ptr;
*ptr = 20; // 未定义行为!可能崩溃或数据损坏
- 重复释放
问题:同一块内存被多次释放。
int* ptr = new int(10);
int* ptr2 = ptr;
delete ptr;
delete ptr2; // 未定义行为!重复释放
- 异常栈展开跳过
问题:代码在 new和 delete之间抛出异常,导致资源泄漏。正如上一节所说,虽然我们可以通过捕获不同类型的异常去判断,但是一个局部域中可能有很多个动态内存的申请和函数的调用,它们都有可能抛出异常,这样的代码出来也太丑陋了。
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 抛异常会如何?
// 2、如果p2这里new 抛异常会如何?
// 3、如果div调用这里又会抛异常会如何?
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
2.智能指针原理
智能指针是类,但是它 模拟指针的行为(和迭代器的原理一致),**采用RAII管理资源 **,所以涉及到 资源管理权限的转移、释放、共享问题,也就是智能指针类对象的拷贝问题。
2.1 RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
2.2 简单模拟,理解原理
1. 我们将对资源的申请和释放,放在智能指针的构造和析构中,使用在栈上开辟的智能指针,去管理动态内存空间的申请和释放。
(栈上的智能指针对象,根据函数栈帧的申请和释放,会自动的调用其作用域中的临时智能指针对象的构造和析构函数,而构造和析构,又会去自动调用其函数体中对对应动态内存的申请和释放,这样就实现了自动对资源的控制。)
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{
}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void Func()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try {
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
这样,如果我们将我们所有的内存的申请和释放,都交给RAII的智能指针管理,我们上面遇到的问题就都可以解决了:
- 内存泄漏:忘记调用 delete释放不再使用的内存。
- 悬空指针: 释放内存后未将指针置空,后续误用已释放的内存。
- 重复释放:同一块内存被多次释放。
- 异常栈展开跳过:代码在 new和 delete之间抛出异常,导致资源泄漏。
异常是一种控制流转移机制,它的栈展开,在逻辑上是由内层向外层,在函数栈帧中查找对应的catch的异常解决语句;在 “物理上” 就是在虚拟进程地址空间的栈上,从低地址向高地址向上层层销毁函数调用链的函数栈帧的过程。
而一个函数的栈帧在被销毁之前,又会调用其局部域中所有局部对象的析构函数,这样,就实现了利用函数的栈帧,和RAII原理,自动的对资源进行申请和释放。
2. 智能指针模仿指针的行为,指针可以解引用,也可以通过->去访问所指空间中的内容,因此:智能指针类、模板中需要将* 、->重载,让其像指针一样去使用,让我们可以通过智能指针,访问其管理的资源。
//智能指针
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{
}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<Date> sparray(new Date);
// 需要注意的是这里应该是sparray.operator()->->_year = 2018;
// 本来应该是sparray->->_year这里语法上为了可读性,编译器省略了一个->
sparray->_year = 2018;
sparray->_month = 1;
sparray->_day = 1;
}
3. 智能指针是管理资源的方式,它又是类,所以又涉及到资源管理权限的转移、释放、共享问题,针对这个问题的方案,也是我们的C++标准模板库的来时路,我们在下文中逐渐揭晓大佬们的心路(设计)历程。
3.智能指针的使用
3.1 std::auto_ptr(管理权转移,失败设计)
C++98版本的库中就提供了auto_ptr的智能指针。下面演示的auto_ptr的使用及问题。
#include <memory> // 包含auto_ptr(需旧编译器支持)
struct Date
{
int _year;
int _month;
int _day;
};
int main() {
// 创建auto_ptr并分配资源
std::auto_ptr<Date> ptr1(new Date);
ptr1->_year; // 正常使用
// 所有权转移:ptr1将资源交给ptr2后自动置空
std::auto_ptr<Date> ptr2 = ptr1;
ptr2->_year; // 有效
//ptr1->_year; // 危险!ptr1已是空指针
return 0; // ptr2离开作用域,资源自动释放
}
auto_ptr,可以通过类的拷贝构造函数,将资源的管理权限转移给另一个指针的拷贝。
auto_ptr的实现原理:在模拟指针,RAII管理资源的基本智能指针前提下,实现对资源的管理权转移的思想,下面简化模拟实现了一份我们自己的auto_ptr来了解它的原理:
// C++98 管理权转移 auto_ptr
namespace myPtr
{
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;
};
}
// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
int main()
{
myPtr::auto_ptr<int> sp1(new int);
myPtr::auto_ptr<int> sp2(sp1); // 管理权转移
// sp1悬空
*sp2 = 10;
cout << *sp2 << endl;
//cout << *sp1 << endl; //这里会打印报错.
return 0;
}
auto_ptr可以实现资源的转移,这样会造成一个指针的悬空,这样的话又会造成我们上面分析过的一些问题:
“2. 悬空指针: 释放内存后未将指针置空,后续误用已释放的内存。”
“3. 重复释放:同一块内存被多次释放。”
我们智能指针本来就是为了解决这些问题而设计的,这样无疑是本末倒置的。所以auto_ptr没有意外的被大家诟病了。
指针本质是一个管理资源的地址,它本来就可以做管理资源的转移;我们的智能指针类是对指针的模仿,适当的模仿可以满足基本指针功能的使用前提下实现RAII,过度的模仿会适得其反,这和之前讨论的,面向对象编程对现实世界的过度描述,可能出现了副作用一样,多重验证了这句话:程序世界没有银弹。
结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr。
3.2 std::unique_ptr(禁止类拷贝,禁止管理权转移、共享)
C++11中开始提供更靠谱的unique_ptr(最早是在boost中出来的)。
#include <memory>
using namespace std;
struct Date
{
int _year;
int _month;
int _day;
};
// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
// C++11将boost库中智能指针精华部分吸收了过来
// C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
int main()
{
unique_ptr<Date> Dp1(new Date);
//unique_ptr<Date> Dp2(Dp1); // 尝试引用已删除的函数
//unique_ptr<Date> Dp3 = Dp1; // 尝试引用已删除的函数
return 0;
}
unique_ptr对于资源管理权限复制的解决方案更加暴力,直接不允许拷贝,如何让一个类不能拷贝呢?
我们知道: 对象的确定性生命周期框架——从诞生(构造)到复制传递(拷贝/移动),最终到消亡(析构),确保所有对象行为可预测、资源可管控,这是C++高效性与安全性的基石。
所以,我们只要将其复制和拷贝等函数禁止掉就可以了。
unique_ptr的实现原理:在模拟指针,RAII管理资源的基本智能指针前提下,实现对资源的管理权转移的直接禁止,这样直接避免了指针悬空等问题,下面简化模拟实现了一份我们自己的unique_ptr来了解它的原理:
namespace myPtr
{
template<class T>
class unique_ptr
{
public:
// RAII
// 像指针一样
unique_ptr(T* ptr)
:_ptr(ptr)
{
}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// ap3(ap1)
// 管理权转移
// 防拷贝
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
T* _ptr;
};
}
int main()
{
myPtr::unique_ptr<Date> Dp1(new Date);
//myPtr::unique_ptr<Date> Dp2(Dp1); // 尝试引用已删除的函数
//myPtr::unique_ptr<Date> Dp3 = Dp1; // 尝试引用已删除的函数
return 0;
}
C++98中我们实现让一个默认成员函数不被编译器自动生成、不被调用的方式:只声明,不实现,并且放到私有。
C++11的方式:用delete去声明该函数不可使用(删除)。
3.3 std::shared_ptr(管理权共享)
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
#include <memory>
using namespace std;
int main()
{
std::shared_ptr<int> sp1(new int);
std::shared_ptr<int> sp2 = sp1;
std::shared_ptr<int> sp3;
sp3 = sp2;
return 0;
}
shared_ptr允许管理权限的共享,允许多个智能指针管理同一个资源。
但是这样不就又会出现指针悬空或者重复释放的问题了吗?并不会,看下面原理。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如:
老师晚上在下班之前都会通知,让最后走的学生记得把门关上。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
shared_ptr的实现原理:在模拟指针,RAII管理资源的基本智能指针前提下,使用引用计数的方式,实现对资源的管理权共享,这样直接避免了指针悬空等问题,又可以实现权限共享,下面简化模拟实现了一份我们自己的shared_ptr来了解它的原理:
namespace myPtr
{
template<class T>
class shared_ptr
{
public:
// RAII
// 像指针一样
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// sp3(sp1)
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
// sp1 = sp5
// sp6 = sp6
// sp4 = sp5
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;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
}
实际上shared_ptr的在库中的实现是有一个控制块结构,它就像我们上面的_pcount的思想一样,类模板实例化出不同类,就有不同的控制块结构,一个类的不同对象就共享控制块:
// 控制块的基本结构
struct ControlBlock {
std::atomic<long> use_count; // 引用计数(原子操作)
std::atomic<long> weak_count; // 弱引用计数
void* managed_object; // 被管理对象的指针
Deleter deleter; // 删除器
Allocator allocator; // 分配器(可选)
};
它其实就是多搞了个类,其指针作为shared_ptr成员,将一些需要实现的功能细节(删除器、强计数、弱计数、内存分配)都放在这个类里面了。
- 这种方式将资源的生命周期管理(智能指针管理)与资源本身解耦,让被管理的资源专注于业务功能,shared_ptr类专注于生命周期管理。
- 和上面_pcount的思想一样,实现实例化出的类的不同对象共享访问。
3.3.1 shared_ptr循环引用问题
#include <memory>
#include <iostream>
using namespace std;
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; //1
cout << node2.use_count() << endl; //1
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl; //2
cout << node2.use_count() << endl; //2
//最后没有调用析构函数.
return 0;
}
循环引用问题分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于节点的成员,而节点又是智能指针node1管理的资源,只有node1释放了,_next才会析构,而节点又由_prev管理,_prev属于另一个节点的成员,所以这就叫循环引用,谁也不会释放。

shared_ptr,在模拟指针,RAII管理资源的基本智能指针前提下,使用引用计数的方式,实现对资源的管理权共享。
但是存在循环引用的问题,这可以认为是shared_ptr的一个缺陷 — 循环引用导致的内存泄漏。
为了补这个缺陷,又搞出了weak_ptr。
3.3.2 weak_ptr(不参与shared_ptr的引用计数,解决循环引用问题)
weak_ptr不是RAII的智能指针,它是专门用来解决shared_ptr循环引用问题的。
它甚至不可以使用指针参数构造,但是它却支持使用shared_ptr去构造:

#include <memory>
#include <iostream>
using namespace std;
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl; //1
cout << node2.use_count() << endl; //1
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl; //1
cout << node2.use_count() << endl; //1
//~ListNode()
//~ListNode()
return 0;
}
那么它是如何解决我们shared_ptr循环引用的问题的呢?
很简单,它不会增加我们shared_ptr的引用计数,它不是RAII智能指针,它不参与资源的管理,只是可以访问资源。
weak_ptr做成员分析:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的weak_ptr _next指向node2,node2的weak_ptr _prev指向node1,它们不参与引用计数,所以就算指向对方,shared_ptr中的引用计数还是1。
- 当我们函数结束,栈帧被销毁,我们的两个智能指针node1和node2相继销毁,并调用其析构函数,其中判断它们其中的shared_ptr的引用计数变为0,节点正常销毁,节点的成员_next、_prev也就正常销毁。
weak_ptr的实现原理:它并不是管理资源的RAII智能指针,它只是常量模拟指针,使得我们可以通过它正常访问其指向的资源,weak_ptr只能使用weak_ptr或者shared_ptr初始化,但是它并不参与shared_ptr的引用计数,下面简化模拟实现了一份我们自己的weak_ptr来了解它的原理:
namespace myPtr
{
//简化版weak_ptr
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;
};
}
3.3.3 shared_ptr的线程安全问题
shared_ptr的线程安全分为两方面:
- shared_ptr智能指针中引用计数,可能同时被多个线程操作,会有线程安全问题。
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会有线程安全问题。
1. shared_ptr智能指针中引用计数是否是线程安全的。
是的,引用计数的加减是加锁保护的,这由标准库自己维护,我们无需在意。
2. shared_ptr智能指针管理的资源是否是线程安全的。
引用计数的线程安全问题,是智能指针要处理的,而指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了。
通过下面的程序我们来测试shared_ptr管理的资源的线程安全问题:
#include <memory>
#include <iostream>
#include <mutex>
using namespace std;
struct Date
{
int _year = 0;
int _month = 0;
int _day = 0;
};
void SharePtrFunc(std::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
for (size_t i = 0; i < n; ++i)
{
// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
std::shared_ptr<Date> copy(sp);
// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
{
//unique_lock<mutex> lk(mtx); //当注释掉这行,我们智能指针管理的资源没有枷锁,将不具备原子性,最终的值有随机性:(_year输出: 198533 _month输出: 199769 _day输出: 199759)
copy->_year++;
copy->_month++;
copy->_day++;
}
}
}
int main()
{
std::shared_ptr<Date> p(new Date);
cout << p.get() << endl; //get是shared_ptr中的一个成员函数,作用是取到智能指针管理资源的地址。 输出:005646A0
const size_t n = 100000;
mutex mtx;
thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));
thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));
t1.join();
t2.join();
cout << p->_year << endl; //输出:200000
cout << p->_month << endl; //输出:200000
cout << p->_day << endl; //输出:200000
cout << p.use_count() << endl; //输出:1
return 0;
}
可见我们上面自己实现的shared_ptr还是有问题,因为我们并没有考虑多线程情况的线程安全问题。
考虑线程安全实现一下:
namespace myPtr
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new mutex)
{
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pmtx(sp._pmtx)
{
AddRef();
}
void Release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//if (this != &sp)
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count()
{
return *_pRefCount;
}
~shared_ptr()
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx;
};
}
4.删除器
这里还有一个问题,我们之前的代码(shared_ptr、unique_ptr),在其智能指针的析构函数中都是写死的直接delete对于的资源,那如果我们是new或者malloc出来一个数组给智能指针去管理呢?
struct Date
{
Date()
{
cout << "Date()" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
int _year = 0;
int _month = 0;
int _day = 0;
};
int main()
{
std::shared_ptr<Date> sp1(new Date[10]);
//输出:
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//~Date()
return 0;
}
new会自动调用类的构造,然后返回一个指针,被我们的智能指针管理起来,但是智能指针之中,析构函数中对管理的资源的释放,是写死的delete,所以它只会释放new出空间的头部的Date对象,其它的对象就出现了内存泄漏了。
库里面的解决办法就是:定制删除器
我们要控制一个模版类内释放资源的规则。
我们想在外部控制内部的比较/规则,那就很容易想到我们的老朋友:仿函数。
没错我们的定制删除器就是写一个仿函数传入shared_ptr,等到我们资源要释放的时候,shared_ptr智能指针会自动的调用这个仿函数并将管理的资源的指针传入。
其实不止仿函数,只要是可调用对象都可以作为智能指针的删除器(lambda、仿函数、函数指针)。
我们来看看shared_ptr构造函数的一个函数重载:
template <class U, class D> shared_ptr (U* p, D del);
我们来试试自己定制删除器然后传入库中的shared_ptr :
template<class T>
class DeleteType
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
template<class T>
void deleteFUNC(T* ptr)
{
delete[] ptr;
}
int main()
{
shared_ptr<Date> Dp1(new Date[10],DeleteType<Date>());
//shared_ptr<Date> Dp2((Date*)malloc(sizeof(Date)),[](Date* ptr){free ptr;});
//shared_ptr<Date> Dp3(new Date[10],deleteFUNC<Date>);
return 0;
}
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
//~Date()
这样就可以正常的删除我们的资源了。
智能指针管理的资源当然也不一定是动态内存,又可能是一个打开的文件或者是其他的资源,所以定制删除器是不可或缺的:
#define _CRT_SECURE_NO_WARNINGS
int main()
{
shared_ptr<FILE> Fp(fopen("test.txt", "r"), [](FILE* ptr) {fclose(ptr); });
cout << Fp.get() << endl;
char buffer[256];
while (fgets(buffer, sizeof(buffer), Fp.get()) != nullptr) {
std::cout << buffer;
}
return 0;
}
//输出:
//011EAC58
//你好
4.1 分析删除器

C++文档中对shared_ptr的构造函数的介绍可见,我们传入删除器类型,是传给其构造函数的,即class D类型参数,它是类的构造函数的参数模板,而不是整个类的参数模板。
那么既然如此,也是只有构造函数可以使用这个类型参数,我们的整个类应该如何使用传入的模板呢?
传入的是一个可调用对象,所以我们就可以用包装器
function<void*(U*)> del;去存它,然后整个类就可以获取其类型,在最后智能指针销毁析构的时候调用这个删除器可调用对象,并执行定制化的删除操作。
库中:实际shared_ptr是有一个控制块的,删除器的包装器实际存储在这个控制块中。
这里可以用包装器存的原因之一就是:
我们可以确定我们传入的仿函数的返回值一定为void,还能确定它的参数一定是U*的。
也正是因为我们传入删除器的底层原理是通过包装器去存的,所以其实所有的可调用类型都可以用作删除器。
注意:
我们的unique_ptr也有定制删除器,但是它通常是在类模版处给的:

这是因为unique_ptr和 shared_ptr的不同设计目标所决定的:
- unique_ptr追求极致的运行时效率和无额外开销。
-
零运行时开销(EBO - Empty Base Optimization):这是最关键的原因。当删除器是一个无状态的函数对象(例如 std::default_delete,它没有成员变量)时,通过将其作为模板参数,编译器可以利用空基类优化(EBO)。这意味着这个删除器不会占用 unique_ptr对象的任何额外内存。unique_ptr的大小通常就等于一个原始指针的大小。
// 自定义的无状态删除器 struct MyDeleter { void operator()(MyClass* ptr) { std::cout << "Deleting with MyDeleter\n"; delete ptr; } }; std::unique_ptr<MyClass, MyDeleter> p1(new MyClass); std::cout << "Size of unique_ptr with custom deleter: " << sizeof(p1) << std::endl; // 大概率输出 8 (64位系统下指针大小) // 对比:有状态的删除器(例如lambda捕获了变量) int some_capture = 42; auto lambda_deleter = [&](MyClass* ptr) { delete ptr; std::cout << some_capture; }; // 这样的删除器无法作为模板参数直接使用,除非包装成 std::function,但那会有开销。 -
类型安全与内联优化:由于删除器的类型在编译期就确定了,编译器可以毫无困难地进行内联优化。调用删除器的代码就像直接调用一个普通函数一样快,没有任何间接调用(如通过函数指针)的开销。
-
代价:类型刚性:这是这种设计的主要代价。删除器类型是 unique_ptr类型的一部分。这意味着两个拥有不同删除器类型的 unique_ptr是两种完全不同的类型,它们不能互相赋值,也不能放在同一个 vector<std::unique_ptr>中。
- shared_ptr追求更大的灵活性和动态分配能力。
-
类型擦除与灵活性:shared_ptr的设计目标之一是灵活性。无论你传入什么类型的删除器(函数指针、lambda表达式、函数对象等),这些类型信息都不会成为 shared_ptr类型的一部分。所有的 shared_ptr都是同一种类型,可以放在同一个容器里,互相赋值。
这是通过类型擦除技术实现的。shared_ptr在内部(通常是在控制块中)存储一个指向删除器的基础类指针,而具体的删除器信息被存储在一个派生类对象中。调用时通过虚函数机制来分派到正确的删除器。 -
动态分配开销:因为删除器是运行时绑定的,并且需要与引用计数等一起存储在控制块中(这个控制块通常是动态分配的),所以它必然带来一些额外开销:
- 内存开销:shared_ptr的大小通常是两个指针(一个指向对象,一个指向控制块)。
- 运行时开销:调用删除器需要一次间接调用(通过指针),可能无法像 unique_ptr那样容易被内联。
-
一致的生命周期管理:删除器与引用计数一起存储在控制块中,这意味着删除器的生命周期与它所管理的对象完全绑定。当引用计数降为0时,控制块会正确地调用存储的删除器来销毁对象,然后销毁自身(包括删除器)。这保证了安全性。
总结
三种智能指针都是模拟指针行为,采用RAII管理资源:
- auto_ptr:管理权转移(拷贝后原指针悬空,有安全隐患)
- unique_ptr:防拷贝(简单粗暴,适用于独占资源)
- shared_ptr:引用计数(支持拷贝共享资源,需注意循环引用问题)
- weak_ptr:配合shared_ptr解决循环引用问题
智能指针提高了内存管理的安全性,但仍需根据场景选择合适的类型。实际应用中,shared_ptr使用较多,但要注意线程安全和循环引用问题。
本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。

1064

被折叠的 条评论
为什么被折叠?



