目录
智能指针意义
智能指针的使用及原理
RAII
智能指针的原理
std::auto_ptr
std::auto_ptr的模拟实现
std::unique_ptr
std::unique_ptr模拟实现
循环引用问题
智能指针意义
#问:为什么需要智能指针?
#include <iostream>
#include <stdexcept>
int div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("除0错误");
return a / b;
}
void Func()
{
// 1、如果p1这里new 可能会抛异常。
// 2、如果p2这里new 可能会抛异常。
// 3、如果div调用也可能会抛异常。
int *p1 = new int;
int *p2 = new int;
std::cout << div() << std::endl;
delete p1;
delete p2;
}
int main()
{
try
{
Func();
}
catch (std::exception &e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
在一异常中,因为异常会导致程序的执行流乱跳,所以很多个会出现异常的代码,放在一起就很容易其中一个抛异常,而导致其余的未执行 / 需要释放的空间未释放。如上:p2出问题,需要释放p1,div出问题,需要释放p1、p2,智能指针就是用来解决这个问题。
智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization - 获取资源即初始化)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。资源:需要手动释放的。(RAII:请求到志愿就初始化)
初始化指的是:
调用一个其他类的构造函数,利用其他类的生命周期来进行管理。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上:把管理一份资源的责任托管给了一个对象。
就是在获取到资源的时候,交给一个对象管理,于是在该对象的生命周期里这个资源始终有效。而这个对象无论如何是异常还是正常结束,都会调用这个对象的析构函数,于是利用这个对象的析构函数释放资源。
这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的原理
最基础的智能指针:
// 使用RAII思想设计的SmartPtr类
template <class T>
class SmartPtr
{
public:
SmartPtr(T *ptr = nullptr) // 将资源给智能指针
: _ptr(ptr) // 智能指针将资源保存
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
private:
T *_ptr;
};
#include <iostream>
#include <stdexcept>
// 1、利用RAII思想设计delete资源的类
// 2、像指针一样的行为
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr) // 将资源给智能指针
:_ptr(ptr) // 智能指针将资源保存
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
std::cin >> a >> b;
if (b == 0)
throw std::invalid_argument("除0错误");
return a / b;
}
void Func()
{
// sp1、sp2出作用域会调用析构函数,抛异常,栈帧会正常结束
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
*sp1 = 0;
*sp2 = 2;
std::cout << div() << std::endl;
}
int main()
{
try
{
Func();
}
catch (std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
- RAII特性。
- 重载operator*和opertaor->,具有像指针一样的行为。
Note:
智能指针看起来很完美,但是又一个致命的问题:智能指针的拷贝问题。默认的拷贝构造只会进行浅拷贝,就会导致一个地址被析构两次。主要原因就是:智能指针管理资源的释放。
解决方案:
问:深拷贝?
不能,违背了智能指针的功能需求,需要的就是浅拷贝,智能指针不知道该空间有多大,只是对与指针的保存。
(问题先保留看看C++库中的解决方式)
std::auto_ptr
http://www.cplusplus.com/reference/memory/auto_ptr/
#include <iostream>
#include <memory>
class A
{
public:
~A()
{
std::cout << "~A()" << std::endl;
}
int _a1 = 0;
int _a2 = 0;
};
int main()
{
std::auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
std::auto_ptr<A> ap2(ap1);
//ap1->_a1++;
//ap1->_a2++;
return 0;
}
总结:
std::auto_ptr是采用的资源管理权转移。但是,是不负责任的拷贝,会导致被拷贝对象悬空。所以多年以来被挂在耻辱柱上,很多公司明确要求不能使用它。
std::auto_ptr的模拟实现
注意:不是交换 —— 是管理权的转移,所以如果是:
std::auto_ptr<A> ap1(new A);
std::auto_ptr<A> ap2(new A);
ap2 = ap1;
ap2是获取ap1的资源(资源管理权的转移) ,ap2之前的资源自动调用析构释放,ap1置空(nullptr)。
namespace cr
{
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;
}
// 不是交换 —— 是管理权的转移
// ap1 = ap2;
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
std::unique_ptr
C++11中开始提供更靠谱的unique_ptr
https://cplusplus.com/reference/memory/unique_ptr/
std::unique_ptr模拟实现
namespace cr
{
template<class T>
class unique_ptr
{
private: // 防止有人跑到类外实现写一个浅拷贝
// 防拷贝 C++98 - 当时还是boost库中
// 只声明不实现
// unique_ptr(unique_ptr<T>& ap);
// unique_ptr<T>& operator=(unique_ptr<T>& ap);
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 防拷贝 C++11
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
std::shared_ptr
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr 。
#include <memory>
#include <iostream>
class A
{
public:
~A()
{
std::cout << "~A()" << std::endl;
}
int _a1 = 0;
int _a2 = 0;
};
void test_shared_ptr()
{
std::shared_ptr<A> sp1(new A);
std::shared_ptr<A> sp2(sp1);
sp1->_a1++;
sp1->_a2++;
std::cout << sp2->_a1 << ":" << sp2->_a2 << std::endl;
sp2->_a1++;
sp2->_a2++;
std::cout << sp1->_a1 << ":" << sp1->_a2 << std::endl;
}
int main()
{
test_shared_ptr();
return 0;
}
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
std::shared_ptr的模拟实现
不能用static修饰计数器,因为这样会让所有的模拟实现的shared_ptr的不同类型的实例化,同用一个计数器。需要的是一个资源,配一个计数器,多个智能指针对象共管静态计数对象,所以资源都只有一个计数,因为静态成员属于整个类,属于类的所有对象。
所以使用的是一个指针,这个时候这个指针指向的空间就是new出来的,使用构造函数new,这就保证了同一个资源,用一个计数器。并且这样这个指针就会指向需释放的资源,也指向了计数。
即:每个资源需要管理的时候,会给构造函数,构造new一个计数。
namespace cr
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(new int(1))
{}
// 判断是否释放
void Release()
{
// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
cout << "Delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
~shared_ptr()
{
Release();
}
// sp1(sp2)
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 防止自己给自己赋值,导致计数--,出现内存泄漏
if (_ptr == sp._ptr)
{
return *this;
}
// 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
Release();
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
// 引用计数
int* _pCount;
};
}
shared_ptr看起来很完美,但是也有问题:
- 在多线程使用shared_ptr的时候,shared_ptr不是线程安全的。
- shared_ptr存在一个循环引用的问题。
循环引用问题
(是一个非常特殊的情况下)
#include <iostream>
#include <memory>
class Node
{
public:
~Node()
{
std::cout << "~Node" << std::endl;
}
int val = 0;
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
};
void test_shared_ptr()
{
std::shared_ptr<Node> s1(new Node);
std::shared_ptr<Node> s2(new Node);
s1->_next = s2;
s2->_prev = s1;
}
int main()
{
test_shared_ptr();
return 0;
}
我们可以发现上面代码并没有像我们想象的一样,自动调用析构函数:
分析:
当下图的时候,是没有问题的,也就是对于shared_ptr智能指针的使用,shared_ptr的计数器分别都++。
但是当继续向下执行的时候,就是问题的关键所在。
此处:shared_ptr类型的s1,经过operator ->的重载,于是变为(Node*)->_next = s2,此处Note中的_next对象是shared_ptr类型的,所以会调用s2中的智能指针的赋值。
同理:会调用s1中的智能指针的赋值。
于是s1与s2的计数器分别都++到了2。
#问:通过以上为什么不会调用析构?
因为,根据函数的创建顺序,s2后构造所以先析构,s1先构造所以后析构。
于是s2与s1就分别调用它们的析构函数了,于是计数器分别都--到了1。但是由于:
- _next:管着右边的节点内存块。
- _prev:管着左边的节点内存块。
于是,便出现:理想状态下,_next释放那右边就释放,_prev释放那左边就释放。
但是会出现一个问题,_next与_prev分别数据不同的成员节点,对于对象中的成员是需要该对象调用析构函数才会释放其中的成员,于是对于左节点是需要计数器--到0,才能释放左节点,_next才会析构。同样的,右节点是需要计数器--到0,才会释放右节点,_prev才会析构。
这就是一个死循环,所以没有调用Node的析构函数,也就不可能看到Node类型对象的析构释放显示。
解决方式:
这个地方想使用shared_ptr进行Node类型的对象中类似的操作是把必定错的,没有办法,所以C++也为我们提供了一个新的方式:weak_ptr(弱指针)
weak_ptr与其他的智能指针都不一样,其并不是常规智能指针,没有RAII,不支持直接管理资源。weak_ptr主要用shared_ptr构造,用来解决shared_ptr循环引用问题。
https://legacy.cplusplus.com/reference/memory/weak_ptr/?kw=weak_ptr
主要用share_ptr来构造自己,并不支持用一个资源构造自己,所以其并不支持RAII。
- 特点:不++share_ptr的计数器。
#include <iostream>
#include <memory>
class Node
{
public:
~Node()
{
std::cout << "~Node" << std::endl;
}
int val = 0;
std::weak_ptr<Node> _next;
std::weak_ptr<Node> _prev;
};
void test_shared_ptr()
{
std::shared_ptr<Node> s1(new Node);
std::shared_ptr<Node> s2(new Node);
s1->_next = s2;
s2->_prev = s1;
}
int main()
{
test_shared_ptr();
return 0;
}
当 _next 与 _prev 是 weak_ptr 的时候,它们不参加资源的释放管理,但是可以访问和修改数据,且并不增加计数,所以不会存在循环引用的问题了。(这个方法可行,但是因为不计数,多以我们要识别到这个问题)
Note:
想看到计数的对应变化,可以使用shared_ptr的成员函数 use_count 。
weak_ptr的模拟实现
对于前面所模拟实现的shared_ptr同样的也具有循环引用的问题。
#include <iostream>
namespace cr
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr): _ptr(ptr), _pCount(new int(1)){}
// 判断是否释放
void Release()
{
// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
}
~shared_ptr(){Release();}
// sp1(sp2)
shared_ptr(const shared_ptr<T>& sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount)++;}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
// 防止自己给自己赋值,导致计数--,出现内存泄漏
if (_ptr == sp._ptr)
return *this;
// 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
Release();
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
private:
T* _ptr;
// 引用计数
int* _pCount;
};
}
class Node
{
public:
~Node(){std::cout << "~Node" << std::endl;}
int val = 0;
cr::shared_ptr<Node> _next;
cr::shared_ptr<Node> _prev;
};
void test_shared_ptr()
{
cr::shared_ptr<Node> s1(new Node);
cr::shared_ptr<Node> s2(new Node);
s1->_next = s2;
s2->_prev = s1;
}
int main()
{
test_shared_ptr();
return 0;
}
于是可以通过模拟实现weak_ptr进行避免我们所模拟实现的shared_ptr,出现循环引用的问题。
#include <iostream>
namespace cr
{
template <class T>
class shared_ptr
{
public:
shared_ptr(T *ptr = nullptr) : _ptr(ptr), _pCount(new int(1)) {}
// 判断是否释放
void Release()
{
// 减减被赋值对象的计数,如果是最后一个对象,要释放资源
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
}
~shared_ptr() { Release(); }
// sp1(sp2)
shared_ptr(const shared_ptr<T> &sp) : _ptr(sp._ptr), _pCount(sp._pCount) { (*_pCount)++; }
shared_ptr<T> &operator=(const shared_ptr<T> &sp)
{
// 防止自己给自己赋值,导致计数--,出现内存泄漏
if (_ptr == sp._ptr)
return *this;
// 判断是否释放 -- 防止_ptr原来的数据未计数--,导致内存泄漏
Release();
// 共管新资源,++计数
_ptr = sp._ptr;
_pCount = sp._pCount;
(*_pCount)++;
return *this;
}
T &operator*() { return *_ptr; }
T *operator->() { return _ptr; }
// 便于外部获取ptr,如weak_ptr
T *get()const
{
return _ptr;
}
private:
T *_ptr;
// 引用计数
int *_pCount;
};
// 辅助型智能指针,使命配合解决shared_ptr循环引用问题
template <class T>
class weak_ptr
{
public:
weak_ptr()
: _ptr(nullptr)
{}
weak_ptr(const shared_ptr<T> &sp)
: _ptr(sp.get())
{}
weak_ptr(const weak_ptr<T> &wp)
: _ptr(wp._ptr)
{}
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;
};
}
class Node
{
public:
~Node() { std::cout << "~Node" << std::endl; }
int val = 0;
cr::weak_ptr<Node> _next;
cr::weak_ptr<Node> _prev;
};
void test_shared_ptr()
{
cr::shared_ptr<Node> s1(new Node);
cr::shared_ptr<Node> s2(new Node);
s1->_next = s2;
s2->_prev = s1;
}
int main()
{
test_shared_ptr();
return 0;
}
需要掌握:
- 为什么需要智能指针?
主要的原因还是因为内存泄漏:忘记释放的问题,更重要还有异常安全的问题。
- RAII?
资源获得,资源请求机立即初始化。指的就是把资源交给一个对象,利用构造将其资源交给一个对象去管理。
- 发展历史
- auto_ptr / unique_ptr / shared_ptr / weak_ptr之间的区别与使用场景。
- 模拟实现简洁版智能指针。
- 什么是循环引用?如何解决循环引用?解决的原理是什么?