概念
在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。
所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。
在这里我们可以定义一个字面意思的自实现的智能指针
#include <iostream>
using namespace std;
template <class T>
class test
{
private:
T* ptr;
public:
test(T* ptr = nullptr):ptr(ptr){}
~test
{
if (ptr != nullptr)
{
delete ptr;
ptr = nullptr;
}
}
};
这样的设计也就是体现了我们前面提到的智能指针的特点——实际上是一个类模板可以适应泛型编程
智能指针和普通指针的使用方法类似,因此我们需要在设计自实现的智能指针中加上 * 和 ->的重载
#include <iostream>
using namespace std;
template <class T>
class test
{
private:
T* ptr;
public:
test(T* ptr = nullptr):ptr(ptr){}
~test
{
if (ptr != nullptr)
{
delete ptr;
ptr = nullptr;
}
}
T& operator*()
{
return *ptr;
}
T* operator->()
{
return ptr;
}
};
我们可以通过*和->去获得指向的对象
智能指针的特点就是当程序结束时,此时ptr1和ptr2指针被销毁时,对象ptr1和ptr2会自动调用析构函数去释放所指向的资源
int main()
{
test<int>ptr(new int);
test<int>ptr1(ptr);
return 0;
}
由于我们没有重写他的拷贝构造函数,因此在拷贝构造时就会出现问题,因为ptr和ptr1对象指向的是同一块内存空间,因此在程序结束调动析构函数析构时,就会对同一个资源对象进行析构,就会引发问题,因此我们就需要让拷贝构造和赋值只能释放一次内存资源。
由此我们引向C++提供的智能指针
C++智能指针库
1.auto_ptr
auto_ptr是c++98版本库中提供的智能指针,该指针解决上诉的问题采取的措施是管理权转移的思想,也就是原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
int main()
{
auto_ptr<int>ptr(new int);
auto_ptr<int>ptr1(ptr);//将ptr的资源给ptr1之后就会将ptr置为nullptr
}
那么这个时候问题就出现了,因为将ptr置为空,如果后续在使用了ptr,就对空指针进行了操作,这会使整个程序崩溃,因此auto_ptr逐渐被废弃
2.unique_ptr
unique_ptr是c++11版本库中提供的智能指针,它直接将拷贝构造函数和赋值重载函数给禁用掉,因此,不让其进行拷贝和赋值。
unique_ptr是C++11提供的用于防止内存泄漏的智能指针中的一种实现,独享被管理对象指针所有权的智能指针。unique_ptr对象包装一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。
unique_ptr对象始终是关联的原始指针的唯一所有者,实现了独享所有权的语义。一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权也从源指针转移给目标指针(源指针被置空)。拷贝一个unique_ptr将不被允许,因为如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,它们都认为自己拥有这块资源(所以都会企图释放)。因此unique_ptr是一个仅能移动的类型。当指针析构时,它所拥有的资源也被销毁。默认情况下,资源的析构是伴随着调用unique_ptr内部的原始指针的delete操作的。
unique_ptr具有->和*运算符重载符,因此它可以像普通指针一样使用。
3.shared_ptr
share_ptr是c++11版本库中的智能指针,shared_ptr允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,因此是程序不会崩溃
int main()
{
shared_ptr<int>ptr(new int);
shared_ptr<int>ptr1(ptr);
shared_ptr<int>ptr2;
ptr2 = ptr1;
cout << ptr.get() << endl;
}
那么shared_ptr实现多个智能指针指向一块资源并且能保证共享的资源只被释放一次呢;
这里我们就引入了一个引用计数,我们用画图来表述意思
shared_ptr采用的是引用计数原理来实现多个shared_ptr对象之间共享资源:
shared_ptr在内部会维护着一份引用计数,用来记录该份资源被几个对象共享。
当一个shared_ptr对象被销毁时(调用析构函数),析构函数内就会将该计数减1。
如果引用计数减为0后,则表示自己是最后一个使用该资源的shared_ptr对象,必须释放资源。
如果引用计数不是0,就说明自己还有其他对象在使用,则不能释放该资源,否则其他对象就成为野指针。
引用计数是用来记录资源对象中有多少个指针指向该资源对象。
实际上引用计数的内部实际上是存放在堆上的一个count,所有的线程都能对其访问,在多个线程修改该值时,就会出现线程安全问题,因此我们需要对在修改他的时候加锁保护
****
shared_ptr的缺点
1.shared_ptr固然好用,但是它也会有问题存在。假设我们要使用定义一个双向链表,如果我们想要让创建出来的链表的节点都定义成shared_ptr智能指针,那么也需要将节点内的_pre和_next都定义成shared_ptr的智能指针。如果定义成普通指针,那么就不能赋值给shared_ptr的智能指针。
2.当其中两个节点互相引用的时候,就会出现循环引用的现象。
当创建出node1和node2智能指针对象时,引用计数都是1.
当node1的next指向node2所指向的资源时,node2的引用计数就+1,变成2,node2的pre指向noede1所指向的资源时,node1的引用计数+1,变成2.
当这两个智能指针使用完后,调用析构函数,引用计数都-1,都变成1,由于引用计数不为0,所以node1和node2所指向的对象不会被释放。
当node1所指向的资源释放需要当node2中的_prev被销毁,就需要node2资源的释放,node2所指向的资源释放就需要当node1中的_next被销毁,就需要node1资源的释放。因此node1和node2都有对方的“把柄”,这两个就造成循环引用现象,最终这node1和node2资源就不会进行释放。
4.weak_ptr
weak_ptr这个指针天生一副“小弟”的模样,也是在C++11的时候引入的标准库,它的出现完全是为了弥补它老大shared_ptr天生有缺陷的问题,其实相比于上一代的智能指针auto_ptr来说,新进老大shared_ptr可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。
weak_ptr本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能配合shared_ptr来使用,可以将shared_ptr的对象赋值给weak_ptr,并且这样并不会改变引用计数的值。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快。表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
weak特点
weak_ptr虽然是一个模板类,但是不能用来直接定义指向原始指针的对象。
weak_ptr接受shared_ptr类型的变量赋值,但是反过来是行不通的,需要使用lock函数。
weak_ptr设计之初就是为了服务于shared_ptr的,所以不增加引用计数就是它的核心功能。
由于不知道什么之后weak_ptr所指向的对象就会被析构掉,所以使用之前请先使用expired函数检测一下。