目录
智能指针
需要智能指针的场景
cpp
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;
}
问题:如果Division函数因除0错误抛出异常,程序会直接跳转到main()中的catch块,Func()中delete语句将永远不会执行,导致p1和p2指向的堆内存被永久占用(内存泄漏)。
解决办法:当前函数中没有匹配的catch会退出当前函数栈,会对函数先前定义的对象调用析构,那么我们可以根据对象的析构帮助我们自动释放资源
cpp
void Func()
{
SmartPtr<int> sp1(new int[10]);
SmartPtr<double> sp2(new double[10]);
cout << div() << endl;
}
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
cpp
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete[] _ptr;
cout << "delete[] " << _ptr << endl;
}
private:
T* _ptr;
};
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw invalid_argument("Division by zero condition!");
}
return (double)a / (double)b;
}
void Func()
{
// RAII
SmartPtr<int> sp1(new int[10]);
SmartPtr<double> sp2(new double[10]);
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针的原理
上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。
cpp
template<class T>
class SmartPtr
{
public:
// RAII
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "~SmartPtr()->"<<_ptr << endl;
delete _ptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void TestSmartPtr1()
{
SmartPtr<int> sp1(new int);
*sp1 = 1;
SmartPtr<pair<string, int>> sp2(new pair<string, int>("xxxx", 1));
sp2->first += 'y';
sp2->second += 1;
sp2.operator->()->second += 1;
}
总结智能指针的原理:
- RAII特性,利用对象生命周期来控制程序资源
- 重载operator*和opertaor->,具有像指针一样的行为。
std::auto_ptr
auto_ptr的实现原理:管理权转移的思想
cpp
// C++98
// 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
// 很多公司都明确规定了,不要用这个
template<class T>
class auto_ptr
{
public:
// RAII
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
cout << "delete->" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
// ap2(ap1)
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
缺点:管理权转移,导致对象悬空,使用者不知道ap1已经悬空,对ap1解引用就会出问题
主要是在auto_ptr的拷贝构造函数中
cpp
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr) // ap2._ptr 接收 ap1._ptr 的值
{
ap._ptr = nullptr; // ap1._ptr 被置空(管理权转移)
}
从代码层面看,dz::auto_ptr ap2 = ap1; 是一个非常自然的拷贝操作,使用者很可能误以为ap1和ap2会 "共享" 资源
但实际ap1已经被悄无声息地置空,后续对ap1的任何指针操作(解引用、调用operator->)都会触发错误。
cpp
void test_auto_ptr1()
{
dz::auto_ptr<int> ap1(new int);
dz::auto_ptr<int> ap2 = ap1;
// 管理权转移,导致对象悬空
(*ap1)++;
(*ap2)++;
}
std::unique_ptr
C++11中开始提供更靠谱的unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝
cpp
// C++11
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;
}
// C++11
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
// C++98
// 1、只声明不实现
// 2、限定为私有
//unique_ptr(const unique_ptr<T>& up);
//unique_ptr<T>& operator=(const unique_ptr<T>& up);
private:
T* _ptr;
};
std::shared_ptr
总有一些情况是需要我们拷贝的:
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
如果只是加一个int的成员变量计数:
两个对象有两个_count,sp1析构不影响sp2中_count,无法正确减去引用计数
如果选择用静态成员变量:
当有两块的资源需要管理时,本来静态成员是2,但sp3初始化时会把静态成员变成1
正确方法 :
初始化时new一块空间记录引用计数
拷贝时用pcount指针找到引用计数 - -
cpp
template<class T>
class shared_ptr
{
public:
// RAII
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
void release()
{
if (--(*_pcount) == 0)
{
//cout << "delete->" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//_ptr指向同一块空间不需要赋值,如果是自己给自己赋值会出问题
if (_ptr != sp._ptr)
{
//如果--引用计数后 == 0 手动释放空间
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
};
循环引用的问题
cpp
struct ListNode
{
int val;
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr3()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;//查看引用计数的变化
cout << n2.use_count() << endl;
// 循环引用
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
}
当只有n1->next=n2 时是可以正常析构的:
以下是正常析构流程
出了作用域n2先析构,对象是定义在函数中的,而函数调用会建立栈帧。在栈帧里面,c++规定数据后进先出,所以n2后定义,先析构
n2的引用计数减到1,然后到n1析构,引用计数减到0,delete _ptr时会去delete ListNode,ListNode的析构要释放它的next成员,释放next的时候会释放右边的ListNode
循环引用:
n1->next = n2;
n2->prev = n1;
这种情况就会出问题
引用计数无法减到0,也就无法释放资源
为了解决循环引用,出现了weak_ptr
weak_ptr
增加计数就会有问题,那么weak_ptr不参与shared_ptr计数的管理
weak_ptr不支持RAII
cpp
struct ListNode
{
int val;
weak_ptr<ListNode> next;
weak_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr3()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// 循环引用
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << endl;//引用计数不会因为next,prev变化
cout << n2.use_count() << endl;
}
用shared_ptr的_ptr初始化weak_ptr的_ptr
cpp
shared_ptr中的get:
返回 shared_ptr 内部管理的原始指针(raw pointer)。
它不会改变引用计数,仅仅是返回指向所管理对象的指针值。
T* get() const
{
return _ptr;
}
cpp
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;
};
原理图解:

当 n1->next = n2 时,不会增加 n2 的引用计数,因为next是一个weak_ptr,而weak_ptr没有引用计数
假设ListNode是一个 "人",next是这个人手里的 "望远镜"(weak_ptr),望远镜指向另一个人(另一个ListNode)。
- 望远镜(next)的存在依赖于它的主人(ListNode),主人消失了,望远镜也会被销毁(调用weak_ptr的析构)。
- 但望远镜(weak_ptr)的作用只是 "观察" 远处的人,不会决定远处的人何时消失(那是远处人的 "主人"------shared_ptr的责任)。
总结:
- next是weak_ptr的一个实例,它的生命周期由所在的ListNode对象决定(随ListNode的创建而创建,销毁而销毁)。
- weak_ptr的设计目的是 "弱引用其他资源",而非 "管理自身作为成员变量的存在"。
- 区分 "weak_ptr对象自身的生命周期" 和 "它所指向的资源的生命周期",就能理解为什么说它 "不管理资源"------ 它对前者的影响是被动的(随宿主销毁),对后者则完全无所有权(不影响其生死)。
如果share_ptr的生命周期到了,但weak_ptr的生命周期还没到,share_ptr把资源释放,weak_ptr就变成野指针了,为什么这么说
cpp
// 创建 shared_ptr,管理一块ListNode资源
shared_ptr<ListNode> sp(new ListNode);
// weak_ptr观察该资源
weak_ptr<ListNode> wp = sp;
wp为了观测,构造的时候拿到了shared_ptr里的_ptr,但是这个_ptr是shared管的,如果_ptr销毁了,wp还去使用_ptr就会出现野指针问题
为了解决这个问题
库中的weak_ptr不参与share_ptr引用计数的管理,但是可以用use_count()看引用计数,可以用expired()检测指向的资源有没有释放
定制删除器
我们之前写的shared_ptr都是针对new出来的,那如果不是new出来的,就不能直接delete
cpp
// 简化版shared_ptr实现,只演示调用逻辑
template<class T>
class shared_ptr {
public:
// 1. 构造函数:管理单个对象,使用默认删除器
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr),
_count(ptr ? new size_t(1) : nullptr),
_deleter(DefaultDelete<T>()) {} // 默认删除器
// 2. 构造函数:支持自定义删除器(如数组删除器)
template<class Deleter>
shared_ptr(T* ptr, Deleter deleter)
: _ptr(ptr),
_count(ptr ? new size_t(1) : nullptr),
_deleter(deleter) {} // 使用用户提供的删除器
// 3. 析构函数
~shared_ptr()
{
if (_count && --(*_count) == 0)
{
_deleter(_ptr); // 调用删除器释放资源
delete _count; // 释放引用计数
_ptr = nullptr;
_count = nullptr;
}
}
private:
T* _ptr; // 指向管理的资源
size_t* _count; // 引用计数
std::function<void(T*)> _deleter; // 存储删除器(可调用对象)
};
在1.构造函数中,_deleter(DefaultDelete()) {} // 默认删除器
就是设置删除器为默认删除器:delete删除
如果不想delete删除,想自定义删除,就要由用户自己提供(2.构造函数)
cpp
// 1. 构造函数:管理单个对象,使用默认删除器
explicit shared_ptr(T* ptr = nullptr)
: _ptr(ptr),
_count(ptr ? new size_t(1) : nullptr),
_deleter(DefaultDelete<T>()) {} // 默认删除器
// 2. 构造函数:支持自定义删除器(如数组删除器)
template<class Deleter>
shared_ptr(T* ptr, Deleter deleter)
: _ptr(ptr),
_count(ptr ? new size_t(1) : nullptr),
_deleter(deleter) {} // 使用用户提供的删除器
用户提供的删除器可以自由选择:仿函数形式,lambda表达式,以及包装器
cpp
// 仿函数的删除器
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
std::shared_ptr<int> sp1((int*)malloc(4), FreeFunc<int>());
std::shared_ptr<int> sp2(new int[10], DeleteArrayFunc<int>());
std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
return 0;
}
线程安全问题
学了C++线程库再看,就知道什么是原子操作,什么是锁,为什么会有线程安全问题了
可以移步我的另一篇博客:C++线程库的学习

引用计数不是原子操作,两个线程同时去拷贝的时候,引用计数原来是1,两个线程都拷贝++了两次之后,引用计数可能还是2
解决方法1:通过锁的方法控制
解决方法2:atomic把引用计数变为原子操作
int* _pcount;变为atomic< int >* _pcount;
就可以不用锁,保证++(*_pcount)不可被中断
cpp
template<class T>
class shared_ptr
{
public:
// RAII
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)
{}
// function<void(T*)> _del;
void release()
{
if (--(*_pcount) == 0)
{
//cout << "delete->" << _ptr << endl;
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
~shared_ptr()
{
release();
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}
// sp1 = sp3
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
//int* _pcount;
atomic<int>* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
当然,我们演示的是自己写的shared_ptr会出现的问题
库里的shared_ptr本身是线程安全的,里面引用计数的增减是原子操作
但是std::shared_ptr所指向的对象的访问不是线程安全的
发展历史
