1.为什么需要智能指针
C++无gc new/malloc出来的资源 是需要我们去手动释放
1.忘记释放
2.发生异常安全问题
new/malloc
fun()://throw 异常
delete/free
最终都导致资源的泄漏
利用智能指针更好的去解决此类问题
2.智能指针
1°RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
2°智能指针的原理
SmartPtr.h
//RAII + 像指针一样
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
//制空解决不了
//sp2还是正常指向
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
#include "SmartPtr.h"
//-------------------
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f1()
{
//智能指针
//法1:
//int* p = new int;
//SmartPtr<int> sp(p);
//--------------------
//无论是函数正常结束 还是抛异常 都会导致sp对象的
//生命周期到了以后 调用析构函数
//相当于类帮我们管理资源的释放
//RAII是一种利用对象生命周期来控制程序资源
//RAII与智能指针的关系
//RAII是一种托管资源的思考 智能指针是依靠这种RAII实现的
//unique_lock/lock_guard也是
//法2:
//需要重载operator*和operator-> 像指针一样
SmartPtr<int> sp1(new int);
*sp1 = 10;
SmartPtr<pair<int, int>> sp2(new pair<int, int>);
sp2->first = 20;
sp2->second = 30;
cout << div() << endl;
}
//抛异常
int main()
{
try
{
f1();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
智能指针有坑
#include "SmartPtr.h"
int main()
{
//指向同一块资源就被释放两次
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2 = sp1;
return 0;
}
同一块空间被释放两次
有三种解决方法:
- 管理权转移
- 防拷贝
- 引用计数
3°auto_ptr
管理权转移 早期设计缺陷 一般公司都明令禁止使用它
缺陷:ap2 = ap1场景下ap1就制空了 访问就会报错 如果不熟悉它的特性就会被坑
说白了就是ap2指向了ap1的空间 然后ap1制空
namespace szh
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
//给你了 我不管了 直接制空
}
//赋值 sp1 = sp2
auto_ptr<T>& operator=(const auto_ptr <T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
//赋给你后 我就制空
_ptr = ap._ptr;
ap._ptr = nullptr;
}
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
//制空解决不了
//sp2还是正常指向
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
#include "SmartPtr.h"
int main()
{
szh::auto_ptr<int> ap1(new int);
szh::auto_ptr<int> ap2 = ap1;
}
但是 如果访问ap1的话就会崩溃
#include "SmartPtr.h"
int main()
{
szh::auto_ptr<int> ap1(new int);
szh::auto_ptr<int> ap2 = ap1;
*ap1 = 1; //悬空崩溃
return 0;
}
闪光标
auto_ptr的缺陷挺大的 基本不会使用
4°unique_ptr
C++11 unique_ptr
防拷贝 推荐使用 简单粗暴
缺陷:如果有需要拷贝的场景 就没法使用
namespace szh
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造和operator=直接搞成删除函数
unique_ptr(unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
//制空解决不了
//sp2还是正常指向
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
#include "SmartPtr.h"
int main()
{
szh::unique_ptr<int> up1(new int);
szh::unique_ptr<int> up2(up1);//拷贝的时候直接编译不过
return 0;
}
5°shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
shared_ptr在其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享。
在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1。
如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
计数:
-
如果使用普通count 不会释放 sp1 sp2都会减到1 希望的是引用一个计数
-
如果使用静态的 static count可以正常释放 但遇到下面情况还是有问题
szh::shared_ptr sp1(new int);
szh::shared_ptr sp2(sp1);
szh::shared_ptr sp3(new int);
应该释放两次 但只释放一次
此时s3自己管理一块资源空间 希望有两个计数
因为静态只有一个计数
-
最后使用指针可以解决问题
namespace szh
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
//++(*_pcount);
add_ref_count();
}
//sp1 = sp4
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (this != &sp)
{
//--引用计数 如果我是最后一个管理资源的对象 则释放资源
//if (--(*_pcount) == 0)
//{
// delete _pcount;
// delete _ptr;
//}
release();
//共同管理后++
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
//++(*_pcount);
add_ref_count();
}
return *this;
}
void add_ref_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
//制空解决不了
//sp2还是正常指向
_ptr = nullptr;
}
//注意释放
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
_pmtx = nullptr;
}
}
~shared_ptr()
{
//if (--(*_pcount) == 0 && _ptr)
//{
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// //制空解决不了
// //sp2还是正常指向
// _ptr = nullptr;
// //注意释放
// delete _pcount;
// _pcount = nullptr;
//}
release();
}
int use_count()
{
return *_pcount;
}
T* get_ptr() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;//记录有多少个对象一起共享管理资源 最后一个析构释放资源
mutex* _pmtx;//解决线程安全问题
};
}
#include "SmartPtr.h"
int main()
{
szh::shared_ptr<int> sp1(new int);
szh::shared_ptr<int> sp2(sp1);
szh::shared_ptr<int> sp3(new int);
szh::shared_ptr<int> sp4(sp3);
szh::shared_ptr<int> sp5(sp3);
sp1 = sp4;
//sp1不再管理原来的资源 与sp4共同管理一个资源
//sp1管理资源的计数-1 sp4管理资源的计数+1
return 0;
}
shared_ptr有线程安全问题
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
需要加锁
shared+str是否是线程安全的
答:注意这里问题的shared_ptr对象拷贝和析构++/--引用计数是否是安全的
库的实现中是安全的
#include "SmartPtr.h"
#include <thread>
int main()
{
szh::shared_ptr<int> sp(new int);
cout << sp.use_count() << endl;
int n = 10000;
//一次拷贝构造问题不大 多了就有问题了
std::thread t1([&]() {
//szh::shared_ptr<int> sp1(sp);
for (int i = 0; i < n; ++i)
{
szh::shared_ptr<int> sp1(sp);
}
});
std::thread t2([&]() {
//szh::shared_ptr<int> sp2(sp);
for (int i = 0; i < n; ++i)
{
szh::shared_ptr<int> sp2(sp);
}
});
t1.join();
t2.join();
//析构后回到1
cout << sp.use_count() << endl;
return 0;
}
shared_ptr会有循环引用问题 使用弱指针来解决
循环引用:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。
此时引入弱指针
当调用拷贝构造的时候 直接返回 不进行++
namespace szh
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
//++(*_pcount);
add_ref_count();
}
//sp1 = sp4
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if (this != &sp)
{
//--引用计数 如果我是最后一个管理资源的对象 则释放资源
//if (--(*_pcount) == 0)
//{
// delete _pcount;
// delete _ptr;
//}
release();
//共同管理后++
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
//++(*_pcount);
add_ref_count();
}
return *this;
}
void add_ref_count()
{
_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}
void release()
{
_pmtx->lock();
bool flag = false;
if (--(*_pcount) == 0)
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
//制空解决不了
//sp2还是正常指向
_ptr = nullptr;
}
//注意释放
delete _pcount;
_pcount = nullptr;
flag = true;
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
_pmtx = nullptr;
}
}
~shared_ptr()
{
//if (--(*_pcount) == 0 && _ptr)
//{
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// //制空解决不了
// //sp2还是正常指向
// _ptr = nullptr;
// //注意释放
// delete _pcount;
// _pcount = nullptr;
//}
release();
}
int use_count()
{
return *_pcount;
}
T* get_ptr() const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;//记录有多少个对象一起共享管理资源 最后一个析构释放资源
mutex* _pmtx;//解决线程安全问题
};
//弱指针
//严格来说weak_ptr不是智能指针 因为他没有RAII资源管理机制
//专门解决shared_ptr的循环引用问题
template<class T>
class weak_ptr
{
public:
weak_ptr() = default;
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get_ptr())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get_ptr();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
#include "SmartPtr.h"
struct ListNode
{
int val;
//ListNode* _prev;
//ListNode* _next;
//szh::shared_ptr<ListNode> _spnext;
//szh::shared_ptr<ListNode> _spprev;
//弱指针
szh::weak_ptr<ListNode> _spprev;
szh::weak_ptr<ListNode> _spnext;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
//可正常释放
szh::shared_ptr<ListNode> spn1(new ListNode);
szh::shared_ptr<ListNode> spn2(new ListNode);
//为什么编译不过
//spn1->_next = spn2;
//spn2->_prev = spn1;
//这里next prev是原生指针
//next和prev改成智能指针即可
//-------------------
//改了之后 加上这两句 无法正常释放
//spn1->_spnext = spn2;
//spn2->_spprev = spn1;
//此时发生了循环引用问题
//引用计数都变成了2
//spn1 spn2析构后 引用计数都变成1
//我的释放由你管理着 你的释放由我管理着
//互相管理对方的空间
//就像我抓着你的头发 你抓着我的头发 两人互相抓着
//谁也不愿意放手 最后就互相伤害
//那么如何解决?
//使用weak_ptr 不增加引用计数
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
spn1->_spnext = spn2;
spn2->_spprev = spn1;
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
return 0;
}
6°智能指针的发展历史
智能指针的历史
C++没有gc(垃圾回收器) 申请的资源需要释放是一个问题 尤其是碰到异常安全问题 特别难处理
稍不注意就会出现内存泄漏 内存泄漏导致程序可用的内存越来越少 程序中很多操作都是需要内存的
那么会导致程序基本处于瘫痪状态 所以我们尽量要杜绝内存泄漏问题
所以就发展处于基于RAII思想的智能指针 但是由于没有gc的坑 引入智能指针
而智能指针经历了十几年发展的坑爹血泪史
第一阶段:
C++98中首次退出了auto_ptr 但是auto_ptr的设计存在重大缺陷 不建议使用
第二阶段:
C++官方在接下来的十几年中没有作为 有一帮牛人就生气了 觉得C++的库太简陋了 所以自己搞一个非官方社区 写了一个库叫boost boost库中就重新写了智能指针(注意boost库中其他很多其他实现的东西)_
scoped_ptr/scoped_array 防拷贝版本
scoped_ptr/scoped_array 引用计数版本
scoped/shared->new 析构delete
scoped_array/shared_array-> new[] 析构 delete[] 重载的是operator[]
weak_ptr
第三阶段:
C++11中引入智能指针 参考了boost的实现 微改了一下
其实C++11其他类似右值引用移动语句等等也是参考boost
unique_ptr(参考的scoped_ptr)
shared_ptr
weak_ptr
7°定制删除器
分析:C++11中没有xxx_array版本
那么如果是new[]出来的对象 怎么办?(了解)
定制删除器--(了解)
传一个实现释放方式的仿函数对象进去给智能指针
#include <iostream>
using namespace std;
#include <memory>
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
int _a2;
};
template<class T>
struct DeleteArray
{
void operator()(T* pa)
{
cout << "delete[] pa" << endl;
delete[] pa;
}
};
struct Free
{
void operator()(void* pa)
{
cout << "free(p)" << endl;
free(pa);
}
};
struct Fclose
{
void operator()(FILE* p)
{
cout << "fclose(p)" << endl;
fclose(p);
}
};
int main()
{
std::shared_ptr<A> sp1(new A);
//默认delete就是delete 无delete[]
//此时需要传仿函数调delete[] 仿函数拿到对象 对象调用operator()
std::shared_ptr<A> sp2(new A[10], DeleteArray<A>());//直接给一个匿名对象 释放10次
std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
std::shared_ptr<FILE> sp4(fopen("test.txt","w"),Fclose());
return 0;
}
8°智能指针与RAII
智能指针是RAII思想的一种应用的体现
本质RAII就是借助构造函数和析构函数来搞事情
因为析构函数和构造函数的特点都是自动调用
使用RAII思想设计的锁管理守卫 防止死锁出现
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lock)
:_lk(lock)//锁不支持拷贝
{
_lk.lock();
}
//解锁也是解拷贝后的锁 但变成引用后 你解锁的锁就是原来的锁
~LockGuard()
{
cout << "解锁" << endl;
_lk.unlock();
}
private:
//Lock _lk;
Lock& _lk;//解决锁不能拷贝
//注意这里是引用
};
void f()
{
mutex mtx;
mtx.lock();
//func() //假设func函数有可能抛异常
mtx.unlock();
//t1线程先进来 t2线程锁住
//此时t1抛异常 t1没有往下走 t2成了死锁
}
int main()
{
try
{
f();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
如果t1抛异常 t2就会成死锁 因此加守卫锁
f换成下面
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
mutex mtx;
LockGuard<mutex> lg(mtx);
cout << div() << endl; //假设div可能抛异常
}
3°内存泄漏
- 什么是内存泄漏?
内存泄漏一般是我们申请了资源 忘记释放 或者因为异常安全等问题没有释放
虚拟进程地址空间
mm_struct { void* _heap_start; void* _heap_end; ... }
物理地址
用页表按页为单位进行映射
多个进程的虚拟地址空间按页为单位跟物理地址建立映射关系
- 内存泄漏的危害是什么?
如果我们申请了内存没有释放 如果进程正常结束 那么这个内存也会释放
一般程序碰到内存泄漏 重启后就OK了 长期运行 不能随便重启的程序 碰到内存泄漏危害非常大
比如操作系统 服务器上的服务 危害是:这些程序长期运行 不用的内存没有释放 内存泄漏
可用内存越来越少 导致服务很多操作失败(因为容器存数据 打开文件 创建套接字 发送数据等等都是需要内存的)
ps:一般后台服务器开发 如果出现内存泄漏 都是事故
- 如何解决内存泄漏相关问题?
a.写C/C++代码时小心谨慎一点
b.不好处理的地方多用智能指针等等去管理(事前预防)
c.如果怀疑存在内存泄漏 或者已经出现 可以使用内存泄漏工具去检测(事后解决)
比如valgrind是一个linux下的强大工具
4°特殊类设计
- 这个类只能在堆上创建对象
创建出的类对象只能在堆上
class HeapOnly
{
public:
static HeapOnly* GetObj()
{
return new HeapOnly;
}
//静态的不需要对象就可以调用
private:
//...
HeapOnly()
{}
//C++98 声明成私有
HeapOnly(const HeapOnly& );
//C++11 delete 公有私有都ok
HeapOnly(const HeapOnly&) = delete;
};
int main()
{
//HeapOnly hp;
//HeapOnly* p = new HeapOnly;
//调不动private
//HeapOnly* p = HeapOnly::GetObj();//都是new出来的 在堆上
//内存泄漏
//改为shared_ptr
std::shared_ptr<HeapOnly> sp1(HeapOnly::GetObj());
//HeapOnly copy(*sp1);
//拷贝构造 还是在栈上
//因此拷贝构造也要封掉
//三步
//1:构造函数封死
//2.写一个GetObj只new出对象
//3.拷贝构造也封死
return 0;
}
- 只在栈上创建出对象的类
class StackOnly
{
public:
static StackOnly GetObj()
{
return StackOnly();
}
/*void* operator new(size_t size) = delete;*/
//禁掉operator new
private:
StackOnly()
{}
};
int main()
{
StackOnly so = StackOnly::GetObj();
//StackOnly* p = new StackOnly;//new不出来
//或者禁掉operator new
//但有缺陷
//static StackOnly sso;在静态区
return 0;
}
- 设计一个类 不能被拷贝
只在私有里声明 不定义
- 设计一个类 不能被继承
a.构造函数私有化 父类的私有在子类中不可见
b.C++11 final
【C++】21.智能指针 完