自学内容网 自学内容网

C++智能指针

目录

内存泄漏:

什么是内存泄漏:

内存泄漏的分类:

堆内存泄露:

系统资源泄露:

内存泄漏的危害:

内存泄漏的检测:

避免内存泄漏:

RAII:

智能指针:

智能指针的所有权:

独占资源所有权:

共享资源所有权:

C++中的智能指针: 

std::auto_ptr:

实现原理:

实现一个简单的auto_ptr:

std::unique_ptr:

实现原理:

实现一个简单的unique_ptr:

std::shared_ptr:

实现原理: 

定制删除器:

实现一个简单的shared_ptr:

线程安全问题:

加锁保证线程安全: 

使用atomic保证线程安全:

循环引用问题:

std::weak_ptr:

实现原理:

实现一个简单的weak_ptr:

注意:

内存泄漏:

什么是内存泄漏:

C/C++内存资源的分配与回收都是由开发人员在编写代码时主动完成的,好处是内存管理的开销较小,程序拥有更高的执行效率;弊端是依赖于开发者的水平,随着代码量的增加,极容易出现因为疏忽或错误造成程序未能释放已经不再使用的内存的情况

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void Func()
{

int* p = new int;
    
cout << div() << endl;// 代码变复杂过程中,可能因为程序设计缺陷,中间出了什么问题提前抛异常退出,导致没有释放掉p

delete p;//可能因为程序员的疏忽,忘了写了,导致没有释放p
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}

上述代码中,如果div函数中出现除零错误,就会直接抛异常,跳到main函数中的catch模块,Func函数中的 delete p 语句就不会执行,就会导致堆内存泄漏;也有可能因为程序员的疏忽,没写delete p 语句,即使程序正常执行也会导致堆内存泄漏。

内存泄漏的分类:

堆内存泄露:

指的是程序执行中通过malloc/calloc/realloc/new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete删掉。如果因为程序员的疏忽或者程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生队内存泄露。

系统资源泄露:

指程序使用系统分配的资源,如网络套接字、文件描述符、管道等,使用完后没有使用对应的函数关闭/释放掉,就会导致系统资源的浪费、系统效能减少、系统执行不稳定,严重时甚至会导致系统崩溃掉。

内存泄漏的危害:

一般而言内存泄露通常是内存问题里问题最小的,但也最难排查的问题。对于指针越界访问访问空指针重复释放等常见问题在测试时就可以找出来,虽然会让程序直接崩溃,但不至于后期维护时耗费大量精力查找问题。但是如果是长时间持续运行的服务器程序,每次就泄露一点(比如每次都new一个int,但是不释放),这种问题在测试时是不容易发现的,日积月累就会占用大量系统内存资源,导致系统响应越来越慢,甚至会导致系统运行崩溃

内存泄漏的检测:

Linux系统:Linux下几款C++程序中的内存泄露检查工具

Windows系统:针对windows平台上C++内存泄漏检测的软件和方法

其它:内存泄漏工具比较

需要注意的是,这些工具检测一些简单程序的内存泄漏还可以,对于复杂程序,效果并不是太理想。

避免内存泄漏:

  • 编写代码时养成良好的编码规范,申请的内存空间记着匹配的去释放。但是如果碰上异常时,就算注意释放了,还是可能会出问题。这就需要智能指针来管理才可以。
  • 采用RAII思想或者智能指针来管理资源。
  • 使用内存泄漏检测工具检测。不过很多工具都不够靠谱,或者收费昂贵。

为了在保证性能的前提下,又能使得开发者不需要关心内存的释放,进而使得开发者能够将更多的精力投入到业务上,自C++11开始,STL正式引入了智能指针。

RAII:

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

 基于RAII思想封装实现一个类:

//采用RAII的思想设计一个类
template<class T>
class RAIIPtr 
{
public:
RAIIPtr(T* ptr = nullptr)//构造对象时传入需要管理的资源指针
: _ptr(ptr)
{}
~RAIIPtr()//对象销毁时,负责释放资源
{
cout << "~RAIIPtr()" << endl;
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void Func()
{
RAIIPtr<int> rp(new int);
cout << div() << endl;
}

int main()
{
try 
{
Func();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}

上述代码中,利用RAII的思想设计了一个RAIIPtr类来帮助我们管理资源,rp对象是一个局部对象,生命周期只在Func函数内,当Func函数调用结束后,就会调用析构函数销毁对象,当div函数执行过程中出现除零错误,抛出异常直接跳到main函数时,rp对象会立即调用析构函数销毁对象,以保证自己管理的资源得到正确释放。

智能指针:

智能指针就是基于RAII的思想设计封装的一个类该类的对象可以像使用指针一样使用,即该类重载了“ * ”操作符和“ -> ”操作符

将上述RAIIPtr类进行完善,实现一个简单的智能指针:

template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)//构造对象时传入需要管理的资源指针
: _ptr(ptr)
{}
~SmartPtr()//对象销毁时,负责释放资源
{
cout << "~SmartPtr()" << endl;
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;//封装的原生指针
};
struct Date
{
Date()
:_year(0),_month(0),_day(0)
{}
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;

SmartPtr<Date> sp2(new Date);
cout << "sp2:" << sp2->_year << "-" << sp2->_month << "-" << sp2->_day << endl;
// 需要注意的是这里应该是sp2.operator->()->_year...
// 本来应该是sp2->->_year,语法上为了可读性,省略了一个->
cout << "sp2:" << sp2.operator->()->_year << "-" << sp2.operator->()->_month << "-" << sp2.operator->()->_day << endl;
sp2->_year = 2024;
sp2->_month = 10;
sp2->_day = 1;
cout << "sp2:" << (*sp2)._year << "-" << (*sp2)._month << "-" << (*sp2)._day << endl;
return 0;
}

上述代码实现了一个简单的智能指针SmartPtr类,该类在构造对象时传入需要管理的资源,当对象销毁时自动释放自己所管理的资源,同时SmartPtr类重载了“ * ”操作符和“ -> ”操作符,因此可以像使用指针一样使用SmartPtr类对象

代码运行结果:

上述只是一个极其简单的智能指针,仅仅是实现了智能指针的基本特性,但是还有资源如何管理、如何保证线程安全等的问题。 

智能指针的所有权:

由于智能指针的本质还是一个经过特殊封装的类,就不可避免的会有拷贝和赋值的问题出现,当用一个已经存在的智能指针对象去赋值或者拷贝构造另一个对象时,该如何处理?

因此对拷贝构造函数和赋值重载函数的处理就显得尤为重要。而拷贝构造和复制重载实际就是对自己管理的资源的所有权进行管理,根据管理方式,智能指针对资源所有权的管理主要有两种:独占资源所有权共享资源所有权

独占资源所有权:

独占资源所有权就是某一份资源的管理权只能由一个对象管理,所有权可以转移给其他对象,但是转移之后,所有权也是独自占有的。

资源释放时也只能由所有者释放。

共享资源所有权:

共享资源管理权就是某一份资源的管理权能同时由多个对象进行管理。

资源释放时由最后一个所有者释放。

C++中的智能指针: 

std::auto_ptr

其实早在C++98的标准库中就引入了一个智能指针std::auto_ptr。

实现原理:

std::auto_ptr的实现原理是对资源管理权的转移,即可以通过拷贝构造/赋值把资源管理权转移给另一个对象。同时也因为它的实现原理,要想使用它会有诸多限制,因此它在C++11之后就被废止了。

int main()
{
auto_ptr<int> a1(new int);
cout << *a1 << endl;
*a1 = 10;
cout << *a1 << endl;
return 0;
}

上边的代码是一个简单的使用auto_ptr的代码,先创建一个auto_ptr<int>对象a1,再通过“ * ”操作符解引用赋值,运行结果:

可见是没问题的,但是如果利用a1对象去构造新的对象,会如何呢?

int main()
{
auto_ptr<int> a1(new int);
cout << "a1:" << *a1 << endl;
*a1 = 10;
cout << "a1:" << *a1 << endl;

auto_ptr<int> a2(a1);
cout << "a2:" << *a2 << endl;
cout << "a1:" << *a1 << endl;


return 0;
}

上边的代码利用一个已存在的a1对象去构造了一个新对象a2,然后再访问a2、a1,看结果如何:

 

可以看到用a1构造完a2后,a1无法再次访问了,因为利用a1构造a2时,a1会把自己所管理的资源的所有权转移给a2,a1自己悬空了,当再次访问a1时,就会报错。

赋值也会出现同样的问题,就不多演示了。

因此如果想要使用auto_ptr,有许多限制:

  • 不能在STL容器中使用,因为复制将导致数据无效
  • 一些STL算法也可能导致auto_ptr失效,比如std::sort算法
  • 不能作为函数参数,因为这会导致复制,并且在调用后,导致原数据无效
  • 如果作为类的成员变量,需要注意在类拷贝时候导致的数据无效

正是因为auto_ptr的诸多限制,所以自C++11起,废弃了auto_ptr,引入了更靠谱的unique_ptr。

实现一个简单的auto_ptr:

namespace SmartPtr
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{ }

auto_ptr(auto_ptr<T>& ap)//拷贝构造直接转移对资源的管理权
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}

auto_ptr<T>& operator=(auto_ptr<T>& ap)//赋值直接转移对资源的管理权
{
if (this != &ap)//判断是否是自己给自己赋值
{
if (_ptr)//被赋值对象已经管理一块资源,要释放掉
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}

T* operator->()
{
return _ptr;
}

T& operator*()
{
return *_ptr;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;//封装的原生指针
};
}

使用效果:

int main()
{
SmartPtr::auto_ptr<int> a1(new int);
cout << "a1:" << *a1 << endl;
*a1 = 10;
cout << "a1:" << *a1 << endl;

SmartPtr::auto_ptr<int> a2(a1);
cout << "a2:" << *a2 << endl;
cout << "a1:" << *a1 << endl;
return 0;
}

std::unique_ptr

实现原理:

unique_ptr的原理是独占资源管理权,且禁止拷贝/赋值,即不允许使用一个已存在的unique_ptr对象去赋值/拷贝构造另一个unique_ptr对象但是可以通过C++11的移动语义转移unique_ptr的资源管理权(这会导致原对象悬空,出现和auto_ptr同样的问题,一般不建议使用)。

int main()
{
unique_ptr<int> u1(new int);
cout << "u1:" << *u1 << endl;
*u1 = 10;
cout << "u1:" << *u1 << endl;

return 0;
}

上边的代码是一个简单的使用unique_ptr的代码,先创建一个unique_ptr<int>对象u1,再通过“ * ”操作符解引用赋值,运行结果:

若是使用u1去构建另一个对象,会如何呢?

int main()
{
unique_ptr<int> u1(new int);
cout << "u1:" << *u1 << endl;
*u1 = 10;
cout << "u1:" << *u1 << endl;
unique_ptr<int> u2(u1);//不允许通过已存在的对象去构造另一个对象
unique_ptr<int> u3;
u3 = u2;
return 0;
}

上边的代码,试图通过一个已存在的对象u1去构造一个新对象u2,再用u2去给u3赋值,编译一下,看会出现什么结果:

可见unique_ptr的拷贝构造和赋值重载都是无法调用的,即无法通过一个已存在的unique_ptr对象去赋值/拷贝构造另一个unique_ptr对象。

再来试试C++11的移动语义:

int main()
{
unique_ptr<int> u1(new int);
cout << "u1:" << *u1 << endl;
*u1 = 10;
cout << "u1:" << *u1 << endl;

unique_ptr<int> u2(move(u1));//通过C++11的移动语义转移资源管理权,这会导致u1悬空,出现和auto_ptr同样的问题
*u2 = 20;
cout << "u2:" << *u2 << endl;
cout << "u1:" << *u1 << endl;

return 0;
}

上边的代码通过C++11的移动语义,使用u1构造了一个新的对象u2,再通过“ * ”解引用赋值,编译运行:

可见还可以通过C++11的移动语义使用u1去构造新对象u2的,但是这样会导致u1悬空出现了和auto_ptr同样的问题,因此不建议这样使用

同时也因为可以通过C++11的移动语义转移unique_ptr的资源管理权,所以unique_ptr也可以作为函数的返回值使用。

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};
unique_ptr<Date> test()
{
unique_ptr<Date> u(new Date);
return u;
}

int main()
{
unique_ptr<Date> u1 = test();
    return 0;
}

上边的代码定义了一个Date类,定义了一个返回值为unique_ptr<Date>的test函数,通过调用test构造u1,通过前边的测试,已经知道unique_ptr的拷贝构造函数和赋值重载函数都是已删除的函数,是无法调用的,如果上边这段代码调用的拷贝构造/赋值重载,编译运行必然会报错,那么编译运行一下试试:

可以发现是可以编译通过的,且只有一份Date资源,说明通过test的返回值构造u1是通过C++11的移动语义实现的

同理,unique_ptr也可以作为函数参数进行传递。

但是通过C++11的移动语义转移资源的管理权是非常危险的行为,一般不建议这样使用!!!

实现一个简单的unique_ptr:

namespace SmartPtr
{
    template<class T>
class unique_ptr//禁止拷贝和赋值
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{ }

unique_ptr(unique_ptr<T>&& up)
:_ptr(up._ptr)
{
            cout<<"unique_ptr(unique_ptr<T>&& up)"<<endl;
up._ptr = nullptr;
}
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
            cout<<"unique_ptr<T>& operator=(unique_ptr<T>&& up)"<<endl;
if (_ptr)//释放原本的资源,如果存在
{
delete _ptr;
}
_ptr = up._ptr;
up._ptr = nullptr;
return *this;
}

T* operator->()
{
return _ptr;
}

T& operator*()
{
return *_ptr;
}

~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}

//强制删除拷贝构造和赋值
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};

}

简单测试一下:

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};
SmartPtr::unique_ptr<Date> test()
{
SmartPtr::unique_ptr<Date> u(new Date);
return u;
}

int main()
{
SmartPtr::unique_ptr<Date> u = test();

SmartPtr::unique_ptr<int> u1(new int);
cout << "u1:" << *u1 << endl;
*u1 = 10;
cout << "u1:" << *u1 << endl;

SmartPtr::unique_ptr<int> u2(new int(1));
cout << "u2:" << *u2 << endl;
u2 = move(u1);
*u2 = 20;
cout << "u2:" << *u2 << endl;
cout << "u1:" << *u1 << endl;
    return 0;
}

编译运行:

上边红框框中的输出,进一步验证了通过函数返回值构造unique_ptr对象是借助C++11的移动语义实现的。

std::shared_ptr

unique_ptr因为其局限性(独享所有权),一般很少用于多线程操作。在多线程操作的时候,为了既可以共享资源,又可以自动释放资源,就引入了shared_ptr。 

实现原理: 

通过引用计数的方式来实现多个shared_ptr对象之间共享同一块资源。也就是说shared_ptr可以通过拷贝构造/赋值重载,用一个已存在的对象去构造/赋值另一个对象

  • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  • 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象会先把引用计数减1,在根据引用计数的值,确定自己该不该释放资源。
  • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
  • shared_ptr内部对引用计数的操作都是线程安全的
int main()
{
shared_ptr<int> sp1(new int);
cout << "sp1:" << *sp1 << endl;
cout << "sp1.use_count:" << sp1.use_count() << endl;
shared_ptr<int> sp2(sp1);
*sp2 = 10;
cout << "sp1:" << *sp1 << endl;
cout << "sp2:" << *sp2 << endl;
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
shared_ptr<int> sp3(new int);
sp3 = sp1;
*sp3 = 20;
cout << "sp1:" << *sp1 << endl;
cout << "sp2:" << *sp2 << endl;
cout << "sp3:" << *sp3 << endl;
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
cout << "sp3.use_count:" << sp3.use_count() << endl;

return 0;
}

上边的代码先定义了一个shared_ptr<int>对象sp1,接着输出sp1管理的资源的值,以及有多少对象管理这份资源(use_count函数可以获取对象所管理的资源的引用计数);然后通过sp1构造一个新的shared_ptr<int>对象sp2,解引用重新赋值后,再次输出;然后又定义了一个sp3,用sp1给sp3赋值,解引用重新赋值后,再次输出相关信息,编译运行:

可以发现只有一个sp1时,资源的引用计数为1,当通过sp1去构造了一个sp2时,资源的引用计数变为了2,然后又用sp1给sp3赋值后,资源的引用计数又变成的3。

有人可能会有疑问,用sp1给sp3重新赋值时,sp3原本所管理的资源该怎办?

答案是sp3会先把自己原本管理的资源释放掉,再管理sp1所管理的资源

上代码测试: 

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
shared_ptr<Date> sp1(new Date);
cout << "sp1.use_count:" << sp1.use_count() << endl;
shared_ptr<Date> sp2(sp1);
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
shared_ptr<Date> sp3(new Date);
sp3 = sp1;
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
cout << "sp3.use_count:" << sp3.use_count() << endl;

return 0;
}

上边的代码定义了一个Date类,每次调用Date类的析构函数时,都会打印出来,方便观察现象。先是定义了一个shared_ptr<Date>对象sp1,然后用sp1去构造sp2,接着又定义了一个shared_ptr<Date>对象sp3(sp3是这块资源的最后一个管理者),再用sp1给sp3重新赋值,观察运行起来后会出现什么现象:

可以发现,在用sp1给sp3赋值时,sp3会把自己所管理的资源释放掉(sp3是这块资源的最后一个管理者)。

定制删除器:

定制删除器,是一个根据实际要求定制的一个具有资源释放功能仿函数/lambda表达式

前边演示的都是管理单个对象的情况,如果是管理数组,会怎样呢?

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};

int main()
{
shared_ptr<Date> sp1(new Date[5]);
cout << "sp1.use_count:" << sp1.use_count() << endl;

return 0;
}

上边的代码定义了一个shared_ptr<Date>对象sp1,与前边不同的是,此时的sp1管理的不再是单个Date对象,而是一组Date对象,编译运行一下看看结果:

可以发现sp1在调用析构函数时出了问题。这是因为shared_ptr<Date>在调用析构函数时,默认调用的是delete,但是sp1此时管理的资源是一个Date数组,应该使用delete[]释放资源,因此就出了问题。先要解决这个问题可以在定义sp1时使用shared_ptr<Date[]>,之后再调用析构函数时就不会在出错了。

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};

int main()
{
    //shared_ptr<Date> sp1(new Date[5]);//错误的
shared_ptr<Date[]> sp1(new Date[5]);
cout << "sp1.use_count:" << sp1.use_count() << endl;

return 0;
}

运行结果:

除了使用shared_ptr<Date[]>定义对象,也可以在定义对象时传入一个自己实现的定制删除器,即一个根据自己要求实现的用于释放资源的仿函数/lambda表达式。

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
    //shared_ptr<Date[]> sp1(new Date[5]);
    shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());//仿函数
sp2.reset();//提前释放资源
cout << "------------------------------" << endl;
shared_ptr<Date> sp3(new Date[5], [](Date* ptr) {delete[] ptr; });//lambda表达式

return 0;
}

上边的代码定义了两个shared_ptr<Date>对象sp2、sp3,sp2、sp3都管理的是数组。定义sp2时,给它传入了一个仿函数对象,用于释放资源;定义sp3时,给他传入了一个lambda表达式,用于释放资源。编译运行:

可以发现,传入了定制删除器后,资源可以正常释放了。

其实unique_ptr也有管理数组的情况,它的处理和shared_ptr类似,要么使用unique_ptr<Date[]>定义对象,要么使用定制删除器但是unique_ptr的定制删除器的使用与shared_ptr有所不同

可以发现,unique_ptr的定制删除器是直接通过类模板来传递的,也正因为需要通过模板参数传递,lambda表达式将无法使用lambda表达式的本质是一个匿名仿函数对象不能作为模板参数传递,但是可以通过C++11的function函数包装器封装出一个类型,作为模板参数进行传递,再在构建对象时传入相应的lambda表达式。

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};
template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
    unique_ptr<Date[]> u1(new Date[5]);
u1.reset();
cout << "------------" << endl;
//unique_ptr<Date> u2(new Date[5], DeleteArray<Date>());//错误的用法
//unique_ptr<Date> u3(new Date[5], [](Date* ptr) {delete[] ptr; });//错误的用法
unique_ptr<Date,DeleteArray<Date>> u2(new Date[5]);//作为类模板参数传递
u2.reset();
cout << "------------" << endl;
//unique_ptr < Date, [](Date* ptr) {delete[] ptr; }> u3(new Date[5]);//错误的用法,lambda表达式无法作为模板参数传递
    unique_ptr < Date, function<void(Date*)>> u3(new Date[5], [](Date* ptr) {delete[] ptr; });
return 0;
}

编译运行:

实现一个简单的shared_ptr:

实现一个简单的shared_ptr的重点就在于定制删除器该如何处理。由于shared_ptr没有采用模板参数进行传递定制删除器,无法通过模板参数定义类成员来调用定制删除器,那该如何处理呢?

此时C++11的function函数包装器就派上用处了。function函数包装器只需要知道要包装函数的返回值和参数,就能包装出一个类型,再进行定义变量,调用时像函数一样调用即可。定制删除器的返回值都是void,参数都是T*,可以直接使用function包装一个用于调用定制删除器的类成员变量,并给一个默认的处理动作,在需要单独传递时再单独传递即可。

namespace Smart
{
template<class T>
class shared_ptr//通过引用计数,共享资源
{
public:
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
{ }

shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{ }

shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
release();//如果已经有管理的资源,先释放掉

_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}

return *this;
}

const int use_count()
{
return *_pcount;
}

T* get()const
{
return _ptr;
}

~shared_ptr()
{
release();
}

void release()
{
if (_ptr == nullptr)
{
delete _pcount;
_pcount = nullptr;
}
else
{
if (--(*_pcount) == 0)
{
_del(_ptr);//调用定制删除器清理数据
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
}

T* operator->()
{
return _ptr;
}

T& operator*()
{
return *_ptr;
}

private:
T* _ptr;
int* _pcount;//引用计数
function<void(T*)> _del = [](T* ptr) {delete ptr; };//包装器,包装一个定制删除器
};
}

简单测试一下:

struct Date
{
Date()
:_year(0), _month(0), _day(0)
{}
int _year;
int _month;
int _day;
~Date()
{
cout << "~Date()" << endl;
}
};

template<class T>
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};


int main()
{
//SmartPtr::shared_ptr<Date[]> sp1(new Date[5]);//需要做模板特化处理
SmartPtr::shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());//仿函数
SmartPtr::shared_ptr<Date> sp3(new Date[5], [](Date* ptr) {delete[] ptr; });//lambda表达式
return 0;
}

编译运行:

测试结果显示没有问题,数组资源得到了正确的释放。

需要注意的是使用shared_ptr<Date[]>这种方式定义对象时,需要进行模板特化处理,有兴趣的可以自己实现以下。

unique_ptr的定制删除器是通过类模板参数传递的,实现起来更加简单,有兴趣的可以自己试着实现一下。

线程安全问题:

由于shared_ptr是共享同一份资源的(包括引用计数),就不可避免的存在线程安全的问题。

std::shared_ptr本身是线程安全的,但是对自己所管理的资源并不是线程安全的

先来看看std::shared_ptr在多线程场景下的情况:

struct Node
{
int _val;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};

int main()
{
    shared_ptr<Node> sp(new Node);
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
shared_ptr<Node> p(sp);
sp->_val++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
shared_ptr<Node> p(sp);
sp->_val++;
}
});

t1.join();
t2.join();
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
    return 0;
}

上边的代码中,定义了一个Node类,其中有一个整型值_val,在main函数中,定义了一个shared_ptr<Node>对象sp,然后输出它所管理的Node资源的_val值和引用计数,又创建了两个线程对象t1、t2,同时通过lambda表达式分别给他们传入要执行的函数对象,t1、t2创建成功后会并行执行,分别对sp进行10000次拷贝并对sp的_val执行10000次加1擦做,等待线程退出后,再次输出sp所管理的Node资源的_val值和引用计数。

理论上如果sp所管理的资源是线程安全的话,sp->_val的值应该为20000;如果sp本身是线程安全的话,由于在线程中创建的对象会马上销毁,那么sp.use_count的值在创建线程对象前和等待线程退出后应该都为1。编译运行:

可以发现,std::shared_ptr本身的确是线程安全的,但是对自己所管理的资源并不是线程安全的。

再来测试我们自己实现的SmartPtr::shared_ptr:

struct Node
{
int _val;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
    SmartPtr::shared_ptr<Node> sp(new Node);
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});

t1.join();
t2.join();
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
    return 0;
}

编译运行:

可以发现程序直接崩溃退出了,明明只有一份Node资源,却调用了两次析构函数,并且还提前执行了析构函数,说明在多线程执行过程中Node资源的引用计数异常置0了,出现了严重的线程安全问题,可见上边实现的简单的shared_ptr还是存在问题的。

进行优化,以保证线程安全。Node资源的引用计数在多线程运行过程中异常置0,说明SmartPtr::shared_ptr对引用计数的操作是线程不安全的,需要加锁以保证线程安全

加锁保证线程安全: 
namespace SmartPtr
{
template<class T>
class shared_ptr//通过引用计数,共享资源
{
public:
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new int(1))
, _del(del)
, _mutex(new mutex)
{ }

shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _mutex(new mutex)
{ }

shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _mutex(sp._mutex)
{
unique_lock<mutex> lock(*_mutex);//加锁保证线程安全
(*_pcount)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
release();

_ptr = sp._ptr;
_pcount = sp._pcount;
unique_lock<mutex> lock(*_mutex);//加锁保证线程安全
(*_pcount)++;
}

return *this;
}

const int use_count()
{
return *_pcount;
}

T* get()const
{
return _ptr;
}

~shared_ptr()
{
release();
}

void release()
{
unique_lock<mutex> lock(*_mutex);//加锁保证线程安全
if (_ptr == nullptr)
{
delete _pcount;
_pcount = nullptr;
}
else
{
if (--(*_pcount) == 0)
{
_del(_ptr);//调用定制删除器清理数据
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
}

T* operator->()
{
return _ptr;
}

T& operator*()
{
return *_ptr;
}

private:
T* _ptr;
int* _pcount;//引用计数
function<void(T*)> _del = [](T* ptr) {delete ptr; };//包装器,包装一个定制删除器
mutex* _mutex;
};
}

再次测试:

struct Node
{
int _val;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
    SmartPtr::shared_ptr<Node> sp(new Node);
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});

t1.join();
t2.join();
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
    return 0;
}

编译运行:

除了使用锁来保证线程安全,也可以使用atomic来保证线程安全。 

使用atomic保证线程安全:

atomic是C++库提供的保证数据原子性(线程安全)的模板类,使用atomic就不用自己再进行加锁/解锁就能保证数据的原子性(线程安全)。

void test1()
{
cout << "test1()" << endl;
int temp = 0;
cout << "temp:" << temp << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
temp++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
temp++;
}
});

t1.join();
t2.join();
cout << "temp:" << temp << endl;
}
void test2()
{
cout << "test2()" << endl;
atomic<int> temp = 0;
cout << "temp:" << temp << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
temp++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
temp++;
}
});

t1.join();
t2.join();
cout << "temp:" << temp << endl;
}

int main()
{
test1();
test2();
return 0;
}

上边的代码分别展示了使用atomic和不使用atomic的情况下,对一个temp采用多线程加20000次,看一下效果:

可以发现,不使用atomic的情况下,采用多线程对同一个temp加20000次结果是错误的,而使用了atomic的情况下保证了结果的准确性。

使用atomic修改SmartPtr::shared_ptr:

namespace SmartPtr
{
template<class T>
class shared_ptr//通过引用计数,共享资源
{
public:
template<class D>
shared_ptr(T* ptr, D del)
:_ptr(ptr)
, _pcount(new atomic<int>(1))
, _del(del)
{ }

shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new atomic<int>(1))
{ }

shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
(*_pcount)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
release();

_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}

return *this;
}

const int use_count()
{
return *_pcount;
}

T* get()const
{
return _ptr;
}

~shared_ptr()
{
release();
}

void release()
{
if (_ptr == nullptr)
{
delete _pcount;
_pcount = nullptr;
}
else
{
if (--(*_pcount) == 0)
{
_del(_ptr);//调用定制删除器清理数据
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
}

T* operator->()
{
return _ptr;
}

T& operator*()
{
return *_ptr;
}

private:
T* _ptr;
atomic<int>* _pcount;//引用计数,使用atomic保证对引用计数的操作都是原子的
function<void(T*)> _del = [](T* ptr) {delete ptr; };//包装器,包装一个定制删除器
};
}

简单测试:

struct Node
{
int _val;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};
int main()
{
    SmartPtr::shared_ptr<Node> sp(new Node);
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
thread t1([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});
thread t2([&] {
for (int i = 0; i < 10000; i++)
{
SmartPtr::shared_ptr<Node> p1(sp);
sp->_val++;
}
});

t1.join();
t2.join();
cout << "sp->_val:" << sp->_val << endl;
cout << "sp.use_count:" << sp.use_count() << endl;
    return 0;
}

编译运行:

 

可见使用atomic确实保证了SmartPtr::shared_ptr的线程安全。

循环引用问题:

由于shared_ptr是通过引用计数管理资源的,那么就会存在循环引用的问题。循环引用是因为两个shared_ptr对象内部又存在指向对方的shared_ptr对象,这就会导致在释放资源时,互相等待对方先释放资源,一直循环等待,导致资源无法正常释放。

struct Node
{
int _val;
shared_ptr<Node> _ptr;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};

int main()
{
shared_ptr<Node> sp1(new Node(10));
shared_ptr<Node> sp2(new Node(20));
    cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;

sp1->_ptr = sp2;//sp1中有指向sp2的对象
sp2->_ptr = sp1;//sp2中有指向sp1的对象
    cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
return 0;
}

上边的代码定义了一个Node类,Node类中有一个整形数据和一个shared_ptr<Node>对象,main函数中定义了两个shared_ptr<Node>对象sp1、sp2,并输出其引用计数,然后又让sp1中的_ptr指向sp2,让sp2中_ptr指向sp1,再次输出sp1、sp2的引用计数,编译运行看一下结果:

可以发现经过赋值后,sp1、sp2的引用计数都变成了2,并且在程序结束后并没有调用Node类的析构函数,这说明sp1、sp2管理的资源并没有得到正确的释放。

分析一下原因:

定义sp1、sp2时,它们管理的资源的引用计数为1,执行sp1->_ptr = sp2、sp2->_ptr = sp1后,sp1、sp2管理的资源的引用计数变为了2,当sp1销毁时,它管理的资源引用计数减1,变为1;sp2销毁时,它管理的资源的引用计数减1,变为1。

由于引用计数都不为零,那么sp1、sp2销毁时,并不会释放各自所管理的资源。

而sp1所管理的资源想要释放,需要它引用计数减为0;而它的引用计数想要减为0,需要sp2所管理资源中的_ptr销毁;要想sp2所管理的_ptr销毁,需要sp2所管理的资源释放;想要sp2所管理的资源释放,需要它的引用计数减为0;而他的引用计数想要减为0,需要sp1所管理的资源中的_ptr销毁;想要sp1所管理的_ptr销毁,需要sp1所管理的资源释放......如此就形成了循环等待对方先销毁,最后导致资源无法合理释放。

为了解决这个问题,又引入了weak_ptr。

std::weak_ptr

weak_ptr比较特殊,它主要是为了配合shared_ptr,解决shared_ptr的循环引用问题而存在的。

实现原理:

通过shared_ptr或者weak_ptr构造新的weak_ptr对象,同时不增加资源的引用计数。 

它的特点: 

  • 不支持RAII不参与资源管理
  • 不具有普通指针的行为,没有重载operator*和operator->
  • 没有共享资源,它的构造不会引起引用计数增加
  • 用于协助shared_ptr来解决循环引用问题
  • 可以用一个shared_ptr或者另外一个weak_ptr对象构造,进而可以间接获取资源的弱共享权

因而严格来说,weak_ptr并不是一个智能指针,不可以像使用指针一样使用weak_ptr对象。

weak_ptr虽然不参与资源管理,但是可以通过use_count()函数查看自己所指向资源的引用计数

配合weak_ptr使用shared_ptr:

struct Node
{
int _val;
//shared_ptr<Node> _ptr;
weak_ptr<Node> _ptr;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};

int main()
{
shared_ptr<Node> sp1(new Node(10));
shared_ptr<Node> sp2(new Node(20));
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;

sp1->_ptr = sp2;//sp1中有指向sp2的对象
sp2->_ptr = sp1;//sp2中有指向sp1的对象
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
cout << "sp1->_ptr.use_count:" << sp1->_ptr.use_count() << endl;
cout << "sp2->_ptr.use_count:" << sp2->_ptr.use_count() << endl;

shared_ptr<Node> sp3 = sp1, sp4 = sp2;
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
cout << "sp1->_ptr.use_count:" << sp1->_ptr.use_count() << endl;
cout << "sp2->_ptr.use_count:" << sp2->_ptr.use_count() << endl;
return 0;
}

上边的代码把Node中的shared_ptr<Node>对象改为了weak_ptr<Node>对象,在main函数中,定义了shared_ptr<Node>类型的sp1,sp2,输出其引用计数,然后用sp2给sp1中的_ptr赋值,用sp1给sp2中的_ptr赋值,再次输出资源对应的引用计数,同时通过sp1、sp2各自的_ptr查看资源的引用计数,又定义了sp3、sp4,分别用sp1、sp2去构造,再次输出资源对应的引用计数:

可以发现,用sp2给sp1中的_ptr赋值,用sp1给sp2中的_ptr赋值后,并不会让资源的引用计数加1,同时通过weak_ptr的use_count函数确实可以查看自己所指向资源的引用计数,并且改用weak_ptr后,资源也得到了正确的释放。

实际上,weak_ptr内部也会有自己的引用计数以及还会有一个引用计数是weak_ptr和shared_ptr所共享的,以防止出现shared_ptr销毁,导致weak_ptr悬空的现象。

实现一个简单的weak_ptr:

namespace SmartPtr
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{ }

weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}

weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{ }

weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
        
        weak_ptr<T>& operator=(const weak_ptr<T>& wp)
{
_ptr = wp._ptr;
return *this;
}

private:
T* _ptr;
};
}

简单测试一下:

struct Node
{
int _val;
//shared_ptr<Node> _ptr;
//weak_ptr<Node> _ptr;
SmartPtr::weak_ptr<Node> _ptr;

Node(int val = 0)
:_val(val)
{}

~Node()
{
cout << "~Node()" << endl;
}
};

int main()
{
shared_ptr<Node> sp1(new Node(10));
shared_ptr<Node> sp2(new Node(20));
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;

sp1->_ptr = sp2;//sp1中有指向sp2的对象
sp2->_ptr = sp1;//sp2中有指向sp1的对象

cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
return 0;
}

上边的代码把Node中的std::weak_ptr替换为了SmartPtr::weak_ptr,编译运行:

注意:

前边所说的auto_ptr的转移资源管理权、unique_ptr的独占资源管理权(禁止拷贝/赋值)、shared_ptr的共享资源管理权,都是针对各自的对象而言的,如果用户主动把同一份资源交给多个auto_ptr、unique_ptr管理,编译器编译时不会报错,但是运行会崩溃,因为会导致资源重复释放

这是一种极其危险的行为,强烈不建议这样做!!!

int main()
{
int* n = new int;
unique_ptr<int> u1(n);
unique_ptr<int> u2(n);

return 0;
}

编译不会有问题,但是运行就会崩溃:


原文地址:https://blog.csdn.net/qq_74190188/article/details/142743940

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!