自学内容网 自学内容网

C++|智能指针

目录

引入

一、智能指针的使用及原理

1.1RAII

1.2智能指针原理

1.3智能指针发展

1.3.1std::auto_ptr

1.3.2std::unique_ptr

1.3.3std::shared_ptr 

二、循环引用问题及解决方法

2.1循环引用

2.2解决方法 

三、删除器

四、C++11和boost中智能指针的关系 


引入

回顾上一篇章,学习了异常机制,但面临了一种情况还没有解决,就是异常带来的内存泄漏,如下:

#include <iostream>
using namespace std;


double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";//抛出的异常是字符串
else
return ((double)a / (double)b);
}
void Func() 
{
    int* p = new int[1];
    int len, time;
    cin >> len >> time;

    cout << Division(len, time) << endl;

    delete[] p;

}

int main() {
    try 
    {
        Func();
    }
    catch (const char* errmsg) 
    {
        cout << "Caught in main: " << errmsg << endl;
    }
    return 0;
}

调用Division函数时,若抛出异常,最终是被main函数中的catch所捕获,直接进入到main函数中的catch中了,但是,delete[] p不会被执行了 ,导致的问题就是内存泄漏。那么可以通过智能指针来解决这个问题

一、智能指针的使用及原理

1.1RAII

RAII是一种利用对象生命周期来控制程序资源的简单技术。在对象构造时获取对象资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候由编译器顺带释放资源。实际上把控资源就是管理对象。

这种做法体现的优势是:

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

RAII是一种设计思想,而智能指针正是采用了这种思想。

RAII思想设计: 

#include <iostream>
using namespace std;

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}

~SmartPtr()
{
if (_ptr)
delete _ptr;
}

private:
T* _ptr;
};
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";//抛出的异常是字符串
else
return ((double)a / (double)b);
}
void Func()
{
int* p = new int[1];
int len, time;
cin >> len >> time;

cout << Division(len, time) << endl;

SmartPtr<int> sp(p);//当Division抛异常时,会被main函数中的catch捕获,那么程序会直接跳到main函数所在的catch
//对此了,Func函数的栈帧会自动销毁,对于内置类型会自动释放,对于自定义类型而言会去调用它的析构函数
//所以sp会去调用析构函数,同时释放了p的资源

}

int main() {
try
{
Func();
}
catch (const char* errmsg)
{
cout << "Caught in main: " << errmsg << endl;
}
return 0;
}

输出结果:

1.2智能指针原理

上述的SmartPtr虽然是通过RAII思想设计的类,但还不能真正称作为智能指针,因为它还不有指针的行为。指针可以解引用,也可以通过->去访问所指空间的内容,因此还需要重载*和->。

#include <iostream>
using namespace std;
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}

T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;

SmartPtr<Date> sparray(new Date);

sparray->_year = 2024;
sparray->_month = 7;
sparray->_day = 7;

cout << (*sparray)._year << ":" << (*sparray)._month << ":" << (*sparray)._day << endl;

return 0;
}

输出结果:

总结一下,智能指针是通过RAII思想设计出的类,它重载operator*和operator->,具有像指针一样的行为。 

1.3智能指针发展

对于上述代码,却有一个弊端,如下:

int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);

return 0;
}

 带来的问题是,sp1拷贝给sp2是一个浅拷贝,所以sp2._ptr和sp1._ptr指向同一块空间,在析构时候,sp2._ptr所指向空间先释放,随后释放sp1._ptr所指向空间,但是该空间已被释放,所以会报错。那么为了解决这个问题,在历史上,智能指针有了一定的发展,来看。

1.3.1std::auto_ptr

C++98版本库中提供了auto_ptr的智能指针。他上述问题的实现原理是:通过管理权转移思想。

// C++98 管理权转移 auto_ptr
#include <iostream>
using namespace std;
namespace bit
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
//ap2(ap1)
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;//相当于ap1._ptr不再管理资源了,被置空了
}


//ap2= ap1
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr; 
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}

int main()
{
 std::auto_ptr<int> sp1(new int);
 std::auto_ptr<int> sp2(sp1); // 管理权转移,sp1不再管理资源了,在析构的时候,确实没啥问题
 //但带来了另一个问题,sp1所管理资源,被置空了,即sp1._ptr1 ==  nullptr,但对于外人来说并不知道
 //若有人使用已被释放的资源,如下:
 *sp2 = 10;
 cout << *sp2 << endl;
 cout << *sp1 << endl;//对空指针解引用,报错
 return 0;
}

auto_ptr指针虽然解决了析构带来的问题,但是带来了新的问题,采用管理权转移思想,将资源由最后一个拷贝对象管理,而被拷贝对象都被置空了,导致有人使用被置空的拷贝对象时,会出现对空指针解引用,报错。所以该智能指针也可以算是一个"失败的man",很多公司也明确要求不能使用它。

1.3.2std::unique_ptr

随着C++的发展,C++11提供了更靠谱的智能指针unique_ptr,其实现原理:简单粗暴的防拷贝。

#include <iostream>
using namespace std;
namespace bit
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}

~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}

// 像指针一样使用
T& operator*()
{
return *_ptr;
}

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


//只声明不实现,但是不能防止有人在类外实现,所以此方法不行
//unique_ptr(unique_ptr<T>& sp);


//c++11仿拷贝
/*unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;*/
private:
T* _ptr;

//将拷贝私有化可以防拷贝,但这是c++98用法
//unique_ptr(unique_ptr<T>& sp);

};
}

//类外实现拷贝
//template<class T>
//bit::unique_ptr<T>::unique_ptr(bit:: unique_ptr<T>& sp)
//{
////...
//}

int main()
{
 bit::unique_ptr<int> sp1(new int);
 //bit::unique_ptr<int> sp2(sp1);//拷贝构造被删除了,编译器也不会默认生成了

     return 0;
}

 由于前面出现的问题都是因为拷贝带来的,那么unique_ptr了就直接把拷贝给禁了,不让你因为各种操作而导致额外的问题,这确实解决了。但是不能使用拷贝了,这也不是办法呀

1.3.3std::shared_ptr 

随着前面智能指针问题的暴露,C++11不断发展,最终也完善了该问题,提出了更靠谱且支持拷贝的shared_ptr。其原理:是通过引用计数方式来实现多个shared_ptr对象之间共享资源。

其满足以下规则:

1.每个shared_ptr对象,其都包含着一个指针,该指针所指向空间内容用来计数有多少个指针指向该空间,即有多少个对象管理同一份资源,当多个对象管理同一份资源,那么他们的指针就会指向同一份空间,空间内容记录着这些数量,每增加一个对象管理相同资源,则计数也会加1

2.当对象销毁时即调用析构函数,就说明该对象不再管理这份资源,其对应的指针不再指向计数的空间,引用计数就会减1

3.如果引用计数是0,就说明没有对象管理该资源了,则必须释放该资源,相反地,如果不是0,说明还有对象管理资源,则就不能释放该资源,否则,对于其他对象中的指针就成野指针了。

#include <iostream>
using namespace std;

namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}

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

_ptr = sp._ptr;

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

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

if (--(*sp._pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}

~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;

_ptr = nullptr;
_pcount = nullptr;
}
}

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

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

private:
T* _ptr;
int* _pcount;

};
}

调试结果: 

shared_ptr解决了拷贝带来的问题,不幸的是,又出来了新的问题,那就是在线程安全和循环引用问题,对于线程问题,先放放,那我们要讲的是循环引用问题。

二、循环引用问题及解决方法

2.1循环引用

根据上述shared_ptr的代码,在外头定义一个类,进行以下操作:

struct ListNode
{
bit::shared_ptr<ListNode> _next;
bit::shared_ptr<ListNode> _prev;

~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
bit::shared_ptr<ListNode> node1(new ListNode);
bit::shared_ptr<ListNode> node2(new ListNode);

node1->_next = node2;
node2->_prev = node1;


return 0;
}

输出结果:

没有任何结果,而当屏蔽node1->_next = node2;或者node2->_prev = node1;中的任何一句,

其输出结果:

这是为何?如图:

两边的节点各自受到_next,_prev牵连,造成了一个循环,且引用计数一直维持在1,并没有减到0,只要屏蔽其中一条语句就不会受到限制,正常析构。虽然这个智能指针是模拟的,就算是库里面的也有一样的问题。

2.2解决方法 

那么为了解决该问题,专门引入了一个智能指针来处理--weak_ptr

#include <iostream>
using namespace std;

namespace bit
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}

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

_ptr = sp._ptr;

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

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

if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}

~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;

_ptr = nullptr;
_pcount = nullptr;
}
}

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

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

T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;

};

// 简化版本的weak_ptr实现
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};

}


struct ListNode
{
bit::weak_ptr<ListNode> _next;
bit::weak_ptr<ListNode> _prev;

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


int main()
{
bit::shared_ptr<ListNode> node1(new ListNode);
bit::shared_ptr<ListNode> node2(new ListNode);



node1->_next = node2;
node2->_prev = node1;


return 0;
}

 输出结果:

那么weak_ptr是怎样解决问题的了,其实很简单,weak_ptr不在对引用计数进行管理了,所以shared_ptr对象初始化时的引用计数都是为1,在进行析构的时候,引用计数都会减到0,然后释放对应的_ptr,_pcount。

三、删除器

对于智能指针的析构还有一个问题,就是对于不是通过new出来的对象或者说new出来的对象是带有[]的如何通过智能指针进行管理,总不能每一种情况来一份代码。那么shared_ptr设计了一个删除器来解决这个问题。

// 仿函数的删除器

#include <iostream>
using namespace std;

template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4), freeFunc);


DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2(new int[4], deleteArrayFunc);


shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
{fclose(p); });

return 0;
}

输出结果:

如上代码,删除器可以通过自己定义的方式来释放空间,这只是展示了仿函数删除器,仅仅是九牛一毛。

四、C++11和boost中智能指针的关系 

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,是一个"准"标准库,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。

1.C++98 中产生了第一个智能指针auto ptr.

不好的设计,对象悬空(不建议使用)
2.C++ boost给出了更实用的scoped ptr和shared ptr和weak ptr.

scoped ptr 防拷贝-》简单粗暴,对于不需要拷贝的场景非常好

shared ptr 引用计数,最后一个释放的对象释放资源 -》复杂一些,但是支持拷贝,非常完美-》问题:循环引用

weak ptr 解决循环引用,不参与引用计数
3.C++ TR1,引入了shared ptr等。不过注意的是TR1并不是标准版,
4.C++ 11,引入了unique_ptr和shared ptr和weak_ptr。需要注意的是unique_ptr对应boost 的scoped ptr。并且这些智能指针的实现原理是参考boost中的实现的。


原文地址:https://blog.csdn.net/weixin_68201503/article/details/140247100

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