我们知道,C++中是没有gc的,new/malloc出来的资源,是需要我们自己去手动释放的,此时便会出现一些问题,1.忘记释放,2.发生异常安全,这些问题就会导致资源的泄露,就发生了严重的问题,所以,我们的智能指针出现了
为什么需要智能指针?
内存泄漏
智能指针的使用及原理
C++11
和
boost
中智能指针的关系
RAII
扩展学习
?
为什么需要智能指针?
其实归根结底就一点,为了防止内存泄漏,防止我们因忘记释放资源或者在malloc与free之间抛出异常,出现异常安全时资源的流失
内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
我们先来看这样一段代码
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f1()
{
int* p1 = new int;
int* p2 = new int;
int* p3 = new int;
int* p = new int;
try
{
cout<<div()<<endl;
}
catch(...)
{
div();
delete p;
throw;
}
}
int main()
{
try
{
f1();
}
catch (exception& e)
{
cout << e.what() << endl;
}
system("pause");
return 0;
}
可以看到的是,尽管我们在主函数中设置了catch用来接收抛出的异常,但是我们new了多个对象,每个对象都有可能出现开辟失败从而出现异常,此时我们是无法判断是从哪个对象中抛出的一场的,没办法针对每个对象都设置catch,所以针对这个问题我们才有了智能指针
智能指针的使用及原理
RAII
RAII
(
Resource Acquisition Is Initialization
)是一种
利用对象生命周期来控制程序资源
(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源
,接着控制对资源的访问使之在对象的生命周期内始终保持有效,
最后在对象析构的
时候释放资源
。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。
我们来看这样一段代码
?这就是我们智能指针的原理,可以看到的是,智能指针其实是一个模板类,类中其实是将new出来的对象托管到了智能指针中,让其帮我们管理资源的释放,无论函数正常结束,还是抛出异常,都会导致sp对象的生命周期到了以后,调用析构函数
这就是我们RAII的思想
智能指针的原理
上述的
SmartPtr
还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过
->
去访问所指空间中的内容,因此:AutoPtr
模板类中还得需要将
*
、
->
重载下,才可让其像指针一样去使用
// RAII + 像指针一样
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们需要对智能指针的operator*与operator->进行重载,才能使其真的想指针一样使用
总结一下智能指针的原理:
1. RAII
特性
2.
重载
operator*
和
opertaor->
,具有像指针一样的行为
到这时其实出现了一个问题,也是C++坑的地方,我们来看看如果下面这段代码执行会发生什么
int* sp1 = new int;
int* sp2 = sp1
很简单的两个语句,其实就是拷贝构造除sp2,使sp1,sp2同时指向了new 出来的int,但是这时,因为其为智能指针,会自动地进行资源释放,这时就会出现多次释放资源的场景
?这是一定会出错的,那么我们该如何解决这个问题呢?
std::auto_ptr
C++98
版本的库中就提供了
auto_ptr
的智能指针,进行了管理权转移
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
// ap1 = ap2
auto_ptr<T>& operator=(const auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们可以看到,在C++98中,解决方案就是很简单的,当sp2拷贝构造sp1时,两个指针都指向int,此时将先前的sp1置空,仅剩sp2,这时也就变成了时sp2去管理int的资源了,管理权由sp1转移到了sp2,不过这个方法并不好,仅仅是允许了这种情况而已,其实是一种早期设计缺陷,一般公司都明令禁止这种使用方式
bit::auto_ptr<int> ap1(new int);
bit::auto_ptr<int> ap2 = ap1;
*ap1 = 1; 悬空崩溃
ap2 = ap1场景下ap1就悬空了,访问就会报错,如果不熟悉他的特性就会被坑
std::unique_ptr
C++11中开始提供更靠谱的unique_ptr,这种方式很简单粗暴,防拷贝,也就是防止拷贝与赋值
// C++11 unique_ptr
// 防拷贝。 简单粗暴,推荐使用
// 缺陷:如果有需要拷贝的场景,他就没法使用
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
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;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这种方式的缺陷也很明显,就是当我们需要拷贝的时候,这种就不行
std::shared_ptr
从名字我们就可以看出来,共享指针,可以共享指向一个对象,目的是可以进行拷贝,且不会出现上面auto指针悬空的问题,这里采用引用计数,那么现在问题来了,引用计数应该加在哪里呢?
首先,不能加到指针内部去,如果将其加到内部,那么每个智能指针都会有一个计数器,--时不会进行累计,也不能加static,这样会影响别的指针,所以我们应该怎么做呢?
其实我们的解决方案是将count置为指针,使得sp1,sp2同时指向这个指针,这样count就可以负责sp1与sp2共同的计数
但是我们这样操作其实并不是线程安全的,因为在我们对一个对象++计数时,是有可能在同时对另一个对象++的,此时因为加到一个指针里,并不安全
shared_ptr
的原理:是通过引用计数的方式来实现多个
shared_ptr
对象之间共享资源
。
1. shared_ptr
在其内部,
给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
。
2.
在
对象被销毁时
(
也就是析构函数调用
)
,就说明自己不使用该资源了,对象的引用计数减一。
3.
如果引用计数是
0
,就说明自己是最后一个使用该资源的对象,
必须释放该资源
;
4.
如果不是
0
,就说明除了自己还有其他对象在使用该份资源,
不能释放该资源
,否则其他对象就成野指针了。
// C++11 shared_ptr
// 引用计数,可以拷贝
// 缺陷:循环引用
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//初始化列表
:_ptr(ptr)
, _pcount(new int(1))//当其被指向一个对象时初始计数为1
, _pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造
:_ptr(sp._ptr)
, _pcount(sp._pcount)//引入计数器
, _pmtx(sp._pmtx)//引入互斥锁
{
add_ref_count();//计数+1
}
// sp1 = sp4
shared_ptr<T>& operator=(const shared_ptr<T>& sp)//重载operator=
{
if (this != &sp)
{
// 减减引用计数,如果我是最后一个管理资源的对象,则释放资源
release();
// 我开始跟你一起管理资源
_ptr = sp._ptr;
_pcount = sp._pcount;//共享计数器
_pmtx = sp._pmtx;//共享锁
add_ref_count();//计数+1
}
return *this;
}
void add_ref_count()//对++前后加锁
{
_pmtx->lock();
++(*_pcount);//对pcount指针+1,这里如果用整型无法满足我们想要的效果,所以需要搞成指针类型,使sp1sp2都指向它
_pmtx->unlock();
}
void release()//释放资源也不能同时释放
{
bool flag = false;//用来标记锁是否可以被释放
_pmtx->lock();
if (--(*_pcount) == 0)//当其为最后一个指向对象的智能指针时
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;//开始资源释放
delete _ptr;
_ptr = nullptr;
}
delete _pcount;//释放计数器
_pcount = nullptr;
flag = true;//因为计时器减到了0,所以需要释放锁
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;//释放锁
_pmtx = nullptr;
}
}
~shared_ptr()
{
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;
};
std::shared_ptr的线程安全问题
1.
智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时
++
或
--
,这个操作不是原子的,引用计数原来是1
,
++
了两次,可能还是
2.
这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++
、
--
是需要加锁的,也就是说引用计数的操作是线程安全的。
2.
智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
循环引用
其实我们定义了share_ptr,虽然解决了很多问题,但是其实还有一个比较明显的问题,我们先来看下这段代码
ListNode* n1 = new ListNode;
ListNode* n2 = new ListNode;
n1->_next = n2;
n2->_prev = n1;
delete n1;
delete n2;
这是我们一般指针的使用,运行也都会正常调用析构函数
wxy::shared_ptr<ListNode> spn1(new ListNode);
wxy::shared_ptr<ListNode> spn2(new ListNode);
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
我们再来看看这样,将指针改为了智能指针,当我们尝试去运行这个代码时,发现出问题了,spn1与spn2都没有进行析构,这是为什么呢?
其实这是因为引发了循环引用的问题,相互制约
?我们可以看到,当我们先创建两个智能指针分别指向对象时,计数器都是1,此时我们将这两个指针分别指向对方,计数器就都到了2,此时我们出作用域,开始析构,spn1析构,引用计数-1,spn2析构,引用计数-1,此时两个计数器都变为了1,他们都由对方管理,这就出问题了,我的释放由你管理,你的释放由我管理,我的生命周期需要结束,你的生命周期也需要结束,但是我们互相牵制,谁也不能结束,此时便无法对对象析构,这里是有问题的
那么我们是如何解决这样的问题的呢?
C++又引入了一个指针,弱智能指针?
weak_ptr
// 严格来说weak_ptr不是智能指针,因为他没有RAII资源管理机制
// 专门解决shared_ptr的循环引用问题
template<class T>
class weak_ptr
{
public:
weak_ptr() = default;//声明私有
weak_ptr(const shared_ptr<T>& sp)//参数为share_ptr
:_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;
};
}
我们发现,其实wake_ptr其实就不增加计数,去套壳share_ptr来解决的
// 循环引用
spn1->_spnext = spn2; // 解决方式:使用weak_ptr,不增加引用计数
spn2->_spprev = spn1;
cout << spn1.use_count() << endl;
cout << spn2.use_count() << endl;
C++11和boost中智能指针的关系
智能指针的历史 C++没有gc?(垃圾回收期),申请的资源需要释放是一个问题,尤其是碰到异常安全问题,特别难处理 稍不注意就会出现内存泄露。内存泄露到导致程序可用的内存越来越少,程序中很多操作都是需要内存的。那么会导致程序基本处于瘫痪状态,所以我们尽量要杜绝内存泄露问题。 所以就发展处了基于RAII思想的智能指针,但是由于没有gc的坑,引入智能指针 而智能指针经历了十几年发展的坑爹血泪史 第一阶段: C++98中首次推出了auto_?ptr,?但是auto_?ptr的设计存在重大缺陷,不建议使用。 第二阶段: C++官方在接下来的十几年中没有作为,有一帮牛人就生气了,觉得C++库太简陋了,所以自己搞一个非官方社区,写了一个库叫boost。boost库中就重新写了智能指针。注意boost库中其他很多其他实现的东西scoped_?ptr/scoped_?_array?防拷?贝版本 shared_?ptr/shared_?array?引?用计数版本. weak_?ptr?. 第三阶段: C++11中引入智能指针,参考boost的实现,微改了一下。其实C++11其?他类似右值引用移动语句等等也是参考boostunique_?ptr(参考的scoped_ptr搞的) shared_?ptr weak_?ptr
定制删除器(了解)
其实定制删除器就是针对智能指针无法识别的对象,比如数组,malloc出的对象,fopen文件等,而利用仿函数与重载的重新进行制定析构函数
// 定制删除器 -- (了解)
#include<memory>
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
private:
int _a1;
int _a2;
};
template<class T>
struct DeleteArry
{
void operator()(T* pa)
{
delete[] pa;
}
};
struct Free
{
void operator()(void* p)
{
cout << "free(p)" << endl;
free(p);
}
};
struct Fclose
{
void operator()(FILE* p)
{
cout << "fclose(p)" << endl;
fclose(p);
}
};
int x4()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2(new A[10], DeleteArry<A>());
std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());
std::shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());
return 0;
}
RAII扩展学习
RAII
思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题
// 使用RAII思想设计的锁管理守卫
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lock)//构造函数保存资源,加锁,加引用目的是保证为同一把锁
:_lk(lock)
{
_lk.lock();
}
~LockGuard()
{
cout << "解锁" << endl;
_lk.unlock();//析构函数解锁
}
LockGuard(LockGuard<Lock>&) = delete;
LockGuard<Lock>& operator=(LockGuard<Lock>&) = delete;
private:
Lock& _lk; // 注意这里是引用
};
//void f()
//{
// mutex mtx;
// mtx.lock();
//
// // func() // 假设func函数有可能抛异常,此时便是一个死锁
//
// mtx.unlock();
//}
void f()
{
mutex mtx;
LockGuard<mutex> lg(mtx);
cout << div() << endl; // 假设div函数有可能抛异常
}
将锁也同样利用RAII思想去设置一个守卫解决问题
补充:内存泄漏
内存泄漏:
1.什么是内存泄漏?
内存绣楼一般指我们申请了资源,这个资源不用了,但是忘记释放了,或者因为异常安全等问题没有释放
2.内存泄漏的危害是什么?
如果我们申请了内存没有释放,如果进程正常结束,那么这个内存也会释放
一般程序碰到内存泄漏,进行重启就可以了,但是长期运行,不能重启的程序碰到内存泄漏危害就很大了,比如操作系统,服务器上的服务,危害是:这些程序长期运行,不用的内存没有释放,内存泄露的越来越多,导致很多服务操作失败,因为容器存数据,打开文件,创建套接字,发送数据等等都是需要内存的
?3.如何解决内存泄漏问题
a.写代码时小心一点
b.不好处理的地方多用智能指针等等去管理,事前预防
c.如果怀疑有内存泄漏,或者已经出现,可以使用内存泄露工具去检测,事后解决,比如valgrind是一个Linux下的强大工具,还有其它工具,也可以试试
|