1.什么是只能指针?为什么要有智能指针?
#include
int main()
{
int *p1 = new int();
//其他操作...
return 0;
}
以上面这段代码为例,如果我们向内存中new了一块空间,等同于C语言中的malloc函数,内存给我们返回了一块空间的地址,我们用指针保存,在进行了一系列操作后,并没有进行释放,而是直接退出程序,那么这样会产生什么问题?
存在内存泄漏的问题,我们都知道如果内存泄漏问题并不是小问题,很多高级程序设计语言都加了很多“保险”来预防内存问题的出现,因为一个计算机的内存空间是有限的,如果我们只用不释放,相当于这块内存始终被我们使用,当进程中出现许多执行流,对这段代码进行频繁的调用,因为一块的错误会导致整个计算机性能大大下降,所以这个问题是我们一定要避免的。
内存泄漏问题并不是失去了这块内存,并不是指物理层面的消息,而是应用程序分配某段内存后,因为设计错误,失去了对某段内存的控制权,从而导致了内存的浪费。
所以,我们在使用new关键字开辟对象后,一定要养成使用delete关键字对其进行释放,对应C语言的关键字是malloc/free。
2.内存泄露的分类
C/C++程序中一般我们只关心两种方面的泄漏:
①堆内存泄漏:
堆内存指的是程序执行中依据须要分配通过malloc/calloc/realloc/new等从堆中分配一块
内存,用完后必须通过调用响应的free和delete删掉。假设程序设计错误导致这部分内存没有
被释放,那么以后这部分空间将无法再使用,就会产生堆内存泄漏(Heap Leak)。
②系统资源泄漏:
指程序使用系统分配的资源,比如套接字,文件描述符,管道等没有使用对应的函数释放,导致系统资源的浪费,严重可导致系统效能减少,系统执行严重不稳定。
3.RAII
RAII(Resource Acquisition Is Initialization) - 资源获取即初始化。这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入;
为了避免程序员在写完代码后忘记释放资源这个习惯,C++引入这项技术,运用了C++语言局部对象出了作用域自动销毁的特性来控制资源的生命周期。
Class Person
{
public:
Person(const string& name = "", int age = 0)
:_name(name)
,_age(age)
{
std::cout << "Person()" << std::endl;
}
const std::string& getname() const
{
return this->_name;
}
int getage()
{
return this->_age;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
private:
const std::string _name;
int _age;
}
int main()
{
Person p;
reutrn 0;
}
在运行这段代码后,结果为:Person();~Person();所以我们能看到Person对象是先创建了p对象,然后我们在没有手动释放时自动调用了Person的析构函数进行了删除。
也可以使用指针来实现:
template
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
}
上述代码还不足以成为智能指针,作为指针它必须要有指针的功能,而作为指针,最重要的功能就是解引用去访问空间中的内容,所以我们还需要重载* 和->两个操作符。
T& operator*() {return *this->_ptr};
T* operator->() {return this->_ptr};
namespace zq
{
template
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr& sp)
:_ptr(sp._ptr)
{
//管理权转移
sp._ptr = nullptr;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
auto_ptr& operator=(auto_ptr& ap)
{
if(this != &ap)
{
if(this->_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return ptr;
}
private:
T* _ptr;
};
}
我们可以看到,auto_ptr在使用时使用了管理权转移的方法,在使用auto_ptr管理资源时,auto_ptr会先将传进来的new出来的对象,然后auto_ptr会用传进来的地址赋值给自己的指针,然后再释放掉传进来的指针所指向的空间,将资源转移给了auto_ptr管理,也就是说ap对象现在是空的,但是如果下文中使用ap对象去访问了其成员,就会出现错误,针对这个特性,auto_ptr也是被建议尽量不要使用的。
unique_ptr的设计思路非常的粗暴 - 防拷贝,正如其名,“唯一的指针”,也就是不让拷贝和赋值。
template>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
unique_ptr(const unique_ptr& sp) = delete;
unique_ptr& operator=(const unique_ptr& sp) = delete;
~unique_ptr()
{
if (_ptr)
{
//cout << "delete:" << _ptr << endl;
//delete _ptr;
D del;
del(_ptr);
}
}
//像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们可以看到,在unique_ptr的模拟实现中,拷贝赋值和赋值运算符的重载都被delete掉,
private:
UniquePtr(UniquePtr const &);
UniquePtr & operator=(UniquePtr const &);
在C++98中,unique_ptr的防拷贝方法是只声明不实现,而且声明也要用私有限定符。
那么unique_ptr在析构时,释放方式由一个额外的删除器来完成//TODO
综上所述,unique_ptr相对于auto_ptr更加安全,因为它增添了防拷贝机制,但是我们都知道在实际情况中,拷贝情况肯定是不可避免的,那么如果当我们遇到了拷贝情况,unique_ptr显然解决不了问题了,那么该如何做呢?
从C++11中开始增加了更加靠谱并支持拷贝的shared_ptr,shared_ptr通过一个引用计数的属性来支持指针对象的拷贝。那么何为引用计数呢?引用计数的原理是记录有多少个对象管理着这块资源,每个对象析构的时候对减计数,当有对象进行拷贝时加计数,由最后一个指向的对象来负责释放资源,所以当拷贝的对象增加时,此属性也需要增加,来记录现在有几个对象拷贝了自己。在对象被销毁时,也就是调用析构函数时,就说明自己不适用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源。反之就不能释放该资源,只能进行引用计数的减一,否则会造成野指针问题。
注意:这里的引用计数并不能使用静态成员,因为静态的成员函数被全局所有对象共享,所以当使用引用计数使用静态成员时,所有的对象都可以对其进行更改,会造成混乱,所以我们应该一个资源配备一个引用计数,所以不能使用静态成员。
template
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pRefCount(new int(1))
,_pmtx(new mutex)
{}
shared_ptr(const shared_ptr& 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;
//delete _pmtx; //err
}
_pmtx->unlock();
if (flag == true)
{
delete _pmtx;
}
}
void AddRef()
{
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr& operator=(const shared_ptr& 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;
};
那么在多线程的环境下,当同时有多个执行流来访问shared_ptr并对其引用计数进行操作时,此时引用计数就变成了“临界资源”,也就是说在同一时间只能被一个执行流访问,在此期间如果有第二个执行流想访问它,只能排队,如果发生了同时更改,会引发线程安全的问题,所以我们还要在shared_ptr中加入锁,并且在shared_ptr对引用计数进行操作时候进行lock()和unlock(),以此来保证引用计数操作是原子的。
但是shared_ptr也有弱点:
struct ListNode
{
int val;
//std::shared_ptr _next;
//std::shared_ptr _prev;
std::weak_ptr _next;
std::weak_ptr _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr n1(new ListNode);
std::shared_ptr n2(new ListNode);
//循环引用,死结
//n1->_next = n2;
//n2->_prev = n1;
return 0;
}
当用shared_ptr定义了两个链表节点,并且用链表的前驱和后继分别指向对方,在逻辑层面就会产生循环引用的场景,所谓循环引用就是两个智能指针互相指向的对方,也就是说这两个智能指针中每个引用计数都有对方,只有一方先释放后才会释放另一方,但是这个概念是相对的,究竟要谁先释放呢?这个问题shared_ptr无法处理,如果要解决这个问题,要引出一个新的智能指针weak_ptr。
这个智能指针严格意义上说并不算是智能指针,可以理解为它是专门为了解决shared_ptr循环引用问题而引出的。
那么它是如何解决的呢?weak_ptr在使用时会写两个拷贝构造函数,参数都是shared_ptr,专门负责来管理shared_ptr中的资源,而且weak_ptr针对引用计数并不操作,
template
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr& sp)
:_ptr(sp.get())
{}
weak_ptr& operator=(const shared_ptr& sp)
{
_ptr = sp.get();
return *this;
}
~weak_ptr()
{}
private:
T* _ptr;
};
struct ListNode
{
int val;
//std::shared_ptr _next;
//std::shared_ptr _prev;
std::weak_ptr _next;
std::weak_ptr _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr n1(new ListNode);
std::shared_ptr n2(new ListNode);
//循环引用,死结
//n1->_next = n2;
//n2->_prev = n1;
//weak_ptr 不是常规意义上的智能指针,没有接收一个原生指针的构造函数,也不符合RAII
//weak_ptr的next和prev对象可以访问指向节点资源,但是不参与节点资源释放,其实就是不增加计数
return 0;
}