在实际的C++开发过程中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用的内存越来越多最终不得不重启等问题,这些问题往往都是内存管理资源不当造成的。比如:
①有些内存资源已经释放,但指向它的指针并没有改变指向,最终成为了野指针,并且后续还在使用;
②有些内存资源已经被释放,后期又试图再释放一次,最终导致重复释放同一块内存会使程序运行崩溃;
③没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。
针对以上情况,C++提供了更友好的内存管理机制,让程序员更专注于开发项目的各个功能上,而不是自己进行内存管理。事实上,显示内存管理的替代方案很早就有了,早在1959年前后,就有人提出“垃圾自动回收”机制,“垃圾”指的是那些不再使用或者没有任何指针指向的内存空间,而“回收”指的是将这些“垃圾”收集起来以便再次利用。
在C++98/03标准中,支持使用anto_ptr
智能指针来实现堆内存的自动回收,C++11新标准在废弃auto_ptr
的同时,增加了nique_ptr
、shared_ptr
以及 weak_ptr
这 3 个智能指针来实现堆内存的自动回收。
智能指针和普通指针用法相似,智能指针的本质是一个模板类,对普通指针进行了封装,通过在构造函数中初始化分配内存,在析构函数中释放内存,达到自己管理内存,不需要手动管理内存的效果,因此智能指针可以在适当时机自动释放分配的内存,即使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题,由此可见,C++开始支持了垃圾回收机制,但是目前支持的程度有限。
接下来对shared_ptr
的原理和用法做详细的解释说明。
shared_ptr
是以类模板的方式实现的,shared_ptr
(其中 T 表示指针指向的具体数据类型)的定义位于
头文件,并位于 std
命名空间中,因此在使用该类型指针时,程序中应包含如下 2 行代码:
#include
using namespace std; //这一行代码不是必须的,如果不添加则在后续使用shared_ptr 智能指针时,就需要明确指明std::
shared_ptr的简单实现如下:
#include
#include
#include
using namespace std;
template<class T> //模板类
class Shared_Ptr{
public:
//以普通指针进行构造
Shared_Ptr(T* ptr = nullptr)
:_pPtr(ptr)
, _pRefCount(new int(1))
, _pMutex(new mutex)
{}
//析构函数
~Shared_Ptr()
{
Release();
}
//拷贝构造函数
Shared_Ptr(const Shared_Ptr<T>& sp)
:_pPtr(sp._pPtr)
, _pRefCount(sp._pRefCount)
, _pMutex(sp._pMutex)
{
AddRefCount();
}
//重载赋值号,使得同一类型的shared_ptr智能指针可以相互赋值
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp)
{
if (_pPtr != sp._pPtr)
{
// 释放管理的旧资源
Release();
// 共享管理新对象的资源,并增加引用计数
_pPtr = sp._pPtr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
// 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据
T& operator*(){
return *_pPtr;
}
//重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员
T* operator->(){
return _pPtr;
}
//返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量
int UseCount() { return *_pRefCount; }
//返回 shared_ptr 对象内部包含的普通指针
T* Get() { return _pPtr; }
void AddRefCount()
{
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release()
{
bool deleteflag = false;
_pMutex->lock();
if (--(*_pRefCount) == 0)
{
delete _pRefCount;
delete _pPtr;
deleteflag = true;
}
_pMutex->unlock();
if (deleteflag == true)
delete _pMutex;
}
private:
int *_pRefCount; //定义一个引用计数指针
T* _pPtr; //定义一个存储指针
mutex* _pMutex; //定义一个锁指针,为了保证线程安全,防止资源未释放或程序崩溃
};
从shared_ptr
源码可以看出模板类Shared_Ptr
有一个存储指针( _pPtr),一个锁指针( _pMutex)和一个引用计数指针( _pRefCount),共三个成员。为了方便用户使用 shared_ptr
智能指针,shared_ptr
模板类还提供有一些实用的成员方法,它们各自的功能如下:
成员方法名 | 功 能 |
---|---|
operator=() | 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。 |
operator*() | 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据。 |
operator->() | 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。 |
swap() | 交换 2 个相同类型 shared_ptr 智能指针的内容。 |
reset() | 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针;当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1 |
get() | 获得 shared_ptr 对象内部包含的普通指针。 |
use_count() | 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量。 |
unique() | 判断当前shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。 |
operator bool() | 判断当前 shared_ptr 对象是否为空智能指针,如果是空指针,返回 false;反之,返回 true。 |
make_shared(args) | 返回一个shared_ptr ,指向一个动态分配的类型为T的对象。使用args初始化次对 |
想系统的了解share_ptr的详细功能,可见shared_ptr
官网:share_ptr官网
shared_ptr
的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
①shared_ptr
的存储指针和引用计数指针是一一对应的,即shared_ptr
里存的是存储指针,对应的引用计数指针就是对stored pointer
的加一,因此shared_ptr
在其内部,给每个资源都维护着一份计数,用来记录该份资源被几个对象共享;
②在对象被销毁时,即调用了析构函数,说明自己不使用该资源了,对象的引用计数减一;
③如果引用计数是0,说明自己是最后一个使用该资源的对象,必须释放该对象;
④如果不是0,说明除了自己还有其他对象在使用该份资源,不能释放资源,否则其他对象就成了野指针。
⑤ 锁指针是为了保证线程安全。 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++
或--
,这个操作不是原子的,引用计数原来是1,如果两个线程同时访问,++
了两次可能还是2,这样的引用计数都是错乱的,会导致资源未释放或程序崩溃的问题。因此智能指针引用计数++
和--
是需要加锁的,这样能够保证引用计数的操作是线程安全的。
可以通过构造函数、std::make_shared
和reset
初始化三种初始化方式
#include "stdafx.h"
#include
#include
#include
using namespace std;
class Person
{
public:
Person(int v) {
value = v;
std::cout << "Cons" <<value<< std::endl;
}
~Person() {
std::cout << "Des" <<value<< std::endl;
}
int value;
};
int main()
{
//构造函数初始化
std::shared_ptr<Person> p1(new Person(1));// Person(1)的引用计数为1
//std::make_shared初始化
std::shared_ptr<Person> p2 = std::make_shared<Person>(2);
//reset初始化
p1.reset(new Person(3));// 首先生成新对象,然后引用计数减1,引用计数为0,故析构Person(1)
// 最后将新对象的指针交给智能指针
std::shared_ptr<Person> p3 = p1;//现在p1和p3同时指向Person(3),Person(3)的引用计数为2
p1.reset();//Person(3)的引用计数为1
p3.reset();//Person(3)的引用计数为0,析构Person(3)
return 0;
}
初始化方式有很多,下面具体讲下每个初始化方式的用法
构造函数初始化方式
//1.通过如下两种方式,可以构造出 shared_ptr 类型的空智能指针,对于空的指针,其初始引用计数方式为0,不是1
std::shared_ptr<int> p1; //不传入任何实参
std::shared_ptr<int> p2(nullptr); //传入空指针 nullptr
//2.创建指针时,可以明确指向
std::shared_ptr<int> p3(new int(10)); //指向一个存有10这个int类型数据的堆内存空间
//3.调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr p4 = p3; 如果P3为空,则P4也为空,其引用计数初始值为0,反之,则表明P4和P3指向同一块堆内存,同时堆内存的引用次数会加1.
//4.调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr p5 = std::move(p4); 即P5拥有了P4的堆内存,而P4则变成了空智能指针
std::make_shared
初始化方式,C++11
标准中提供了 std::make_shared
模板函数,其可以用于初始化 shared_ptr
智能指针
//1.定义一个空的智能指针
std::shared_ptr<int> p6 = std::make_shared<int>();
//2.创建指针,并明确指向
std::shared_ptr<int> p7 = std::make_shared<int>(10);
//3.auto关键字代替std::shared_ptr,p8指向一个动态分配的空vector
auto p8 = make_shared<vector<int>>();
reset
初始化
//创建了一个指针,并明确指向
//调用reset(new xxx())重新赋值时,智能指针首先是生成新对象,然后将旧对象的引用计数减1(当然,如果发现引用计数为0时,则析构旧对象),然后将新对象的指针交给智能指针保管。
std::shared_ptr<int> p8 = nullptr;
p8.reset(new int(1));
//当智能指针中有值的时候,调用reset()会使引用计数减1,如果引用计数为0时,则析构旧对象
p8.reset();
智能指针一般都提供了get()
成员函数,用来执行显示转换,即返回智能指针内部的原始指针。
std::shared_ptr<int> p9(new int(5));
int *pInt = p9.get();
如果需要调用成员函数,由于几乎所有的智能指针都重载了 *
,->
操作符,所以直接使用把智能指针当做一般的指针变量来使用就可以了。但是有时候需要传递参数,如果参数是 T*
,那么传递一个智能指针类是无法识别的,因此需要使用原始指针。
在c++17
前std::shared_ptr
是不支持动态数组的,如下
#include
std::shared_ptr<int[]> sp1(new int[10]()); // 错误,c++17前不能传递数组类型作为shared_ptr的模板参数
std::unique_ptr<int[]> up1(new int[10]()); // ok, unique_ptr对此做了特化
std::shared_ptr<int> sp2(new int[10]()); // 错误,可以编译,但会产生未定义行为,请不要这么做
sp1
错误的原因很明显,然而sp2
的就没有那么好找了,究其原因,是因为std::shared_ptr
对非数组类型都使用delete p
释放资源,显然这对于new int[10]
来说是不对的,对它应该使用delete [] p
。
其实c++17
前的解决方案并不复杂,我们可以借助std::default_delete
,它用于提供对应类型的正确的delete
操作,即 指定删除器 。
智能指针可以指定删除器,当智能指针的引用计数为0时,自动调用指定的删除器来释放内存。std::shared_ptr
可以指定删除器的一个原因是在C++11
标准中其默认删除器不支持数组对象,比如,对于申请的动态数组来说,shared_ptr
指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存,这一点需要注意。对于申请的动态数组,释放规则可以使用C++11
标准中提供的 default_delete
模板类,我们也可以自定义释放规则:
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
以上,我们可以用默认的删除器,也可以用制定的删除器进行正确的delete
操作,但是用默认删除器的缺点是明显的:
int[]
类型的,然而事实上传给模板参数的是int
delete functor
std::make_shared
,无法保证异常安全c++17
前shared_ptr
未提供opreator[]
,所以当需要类似操作时不得不使用p7.get()[index]
的形式c++17
增加了opreator[]
和使用int[]
类的数组类型做模板参数以上的代码可以简化为:
std::shared_ptr<int[]> p7(new int[10]());
对于访问分配的空间,可以将p7.get()[index]
替换为p7[index]
。看个具体的例子:
#include
#include
int main()
{
std::shared_ptr<int[]> p7(new int[5]());
for (int i = 0; i < 5; ++i) {
p7[i] = (i+1) * (i+1);
}
for (int i = 0; i < 5; ++i) {
std::cout << p7[i] << std::endl;
}
}
c++17
缺点:无法使用std::make_shared
,而我们除非指定自己的delete functor
,否则我们应该尽量使用std::make_shared
。
c++20
支持std::make_shared
auto up2 = std::make_unique<int[]>(10); // 从c++14开始,分配一个管理有10个int元素的动态数组的unique_ptr
// c++20中你可以这样写,与上一句相似,只不过返回的是shared_ptr
auto sp3 = std::make_shared<int[]>(10);
shared_ptr
需要注意的问题①不要用一个原始指针初始化多个shared_ptr
,原因在于,会造成二次销毁,如下所示:
int *p5 = new int;
std::shared_ptr<int> p6(p5);
std::shared_ptr<int> p7(p5);// logic error
②不要在函数实参中创建shared_ptr
。因为C++
的函数参数的计算顺序在不同的编译器下是不同的。正确的做法是先创建好,然后再传入。
function(shared_ptr<int>(new int), g());
③看了很多书籍或博客,都说shared_ptr
不支持动态数组,但是本地编译器由通过了,这是为啥?
随着标准越来越新,C++17
及以后是支持动态数组的,C++11/14
是不支持的,只要是最新的编译器是没问题的。shared_pt
r动态数组定义如下:std::shared_ptr
;C++11
中shared_ptr
默认调用的析构函数是default_delete()
,而非default_delete<_Ty[]>
,很显然,如果分配数组,当然应该使用delete[]
, 所以直到C++17
才被支持。
④避免循环引用。智能指针最大的一个陷阱是循环引用,循环引用会导致内存泄漏。
⑤线程安全问题。
参考线程安全
假设一个简单的场景,有 3 个 shared_ptr
对象 x、g、n:
shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr<Foo> x; // 线程 A 的局部变量
shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
因此,shared_ptr
的线程不安全性在于它需要操作两个成员,即new
出来的对象和计数器。在多线程下,不能保证new
出来一个对象一定能被放入shared_ptr
中,也不能保证智能指针管理的引用计数的正确性,这是因为shared_ptr
操作不是一气呵成的。即存在以下情况:同一个shared_ptr
对象可以被多线程同时读取。不同的shared_ptr对象可以被多线程同时修改。同一个shared_ptr
对象不能被多线程直接修改,但可以通过原子函数完成。因此在创建一个shared_ptr
时,需要使用C++11
提供的make_shared
模板,make_shared
创建shared_ptr
只申请一次内存,避免了上述错误,也提高了性能,同时在读写操作时,需要加锁。