目录
为什么需要智能指针?
RAII
智能指针的原理
C++智能指针的历史
智能指针
auto_ptr
unique_ptr
智能指针删除器
weak_ptr
为什么需要智能指针?
1. 我们在很多时候,使用new运算符在堆区申请空间,忘记释放则会带来内存泄漏,而智能指针可以自动释放堆区内存,自动执行delete。
2. 异常带来的内存泄漏隐患,可通过智能指针解决。
C++中某些函数或new运算符可能会抛异常,而如果我们在外层函数使用try catch捕获异常,则可能导致程序执行流跳转,即使new的后方有delete语句,也可能会因为捕获异常而不执行,从而造成内存泄漏,此时智能指针就是最好的解决方法。
int div()
{
int a = 0, b = 0;
cin >> a >> b;
if(b == 0)
{
throw invalid_argument("除0错误");
}
return a / b;
}
void func1()
{
// p1 p2 div三个地方抛异常会导致不同的内存泄漏
int* p1 = new int(0);
int* p2 = new int(1);
cout << div() << endl;
delete p1;
delete p2;
cout << "p1 and p2 were deleted" << endl;
}
void func2()
{
unique_ptr<int> up1(new int(1));
unique_ptr<int> up2(new int(2));
cout << div() << endl;
}
int main()
{
try
{
func1();
}
catch(const invalid_argument& ia)
{
cout << ia.what() << endl;
}
catch(...)
{
cout << "未知错误" << endl;
}
// test_auto_ptr();
// test_smart_ptr2();
// test_unique_ptr();
// test_circular_reference();
// test_weak_ptr();
return 0;
}
如上,外层函数使用try catch语句,则在new之后发生异常,会导致delete语句被跳过,其中的new 和 div方法都可能抛异常,会造成不同程序的内存泄漏。
使用func2方法中的智能指针即可解决此问题,因为在异常抛出并捕获之后,尽管程序执行流会从div方法或func1(new)中跳转到main,但是函数栈帧仍会从内向外依次销毁,此时作为局部变量的up1 up2生命周期结束,调用析构函数,即可避免内存泄漏。
这就是智能指针使用的RAII思想。
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的原理
1. RAII
有了上面的认识,其实RAII在智能指针这里就是,将堆区资源的创建和释放托管给一个对象,对象生命周期内,内存资源有效,对象生命周期结束时,自动调用析构函数,自动释放内存。
2. 像指针一样
类似于STL容器的迭代器,迭代器通过实现operator* operator->,将迭代器实现出指针的行为,使其使用起来像指针。
而智能指针,本身就是模拟指针,故智能指针类通过实现operator* operator->,使其具有指针一样的操作行为。
3. 智能指针的拷贝行为
我们知道, int* p = new int(1); int* p2 = p; 此时p和p2指针内存储的地址相同,即指向同样的内存空间。所以,在实现智能指针时,必须考虑智能指针类的拷贝构造 和 operator=的行为。这里不能实现深拷贝,因为原生指针的拷贝本身就是浅拷贝,浅拷贝就是我们的目的。但是若两个智能指针指向相同,一个调用析构之后,另一个智能指针就是野指针。
所以,我们必须考虑如何解决智能指针的拷贝问题。
在C++标准库中,auto_ptr shared_ptr unique_ptr 都有着RAII和像指针一样的特性,但是它们对于指针的拷贝行为解决方法不同。具体看下方...
C++智能指针的历史
C++98中出现了auto_ptr,这是最原始的智能指针,它实现了RAII和像指针一样的行为,所以,可以说是解决了异常带来的内存安全隐患。但是auto_ptr对于智能指针的拷贝的实现非常糟糕...
C++98 ~ C++11之间,boost库实现了scoped_ptr shared_ptr weak_ptr。 在C++11,C++标准库新增了unique_ptr shared_ptr weak_ptr对应boost库的三大智能指针。这三个智能指针都有不同的特性。并且这些智能指针的实现原理是参考boost中的实现的。
智能指针
auto_ptr
template <class T>
class auto_ptr
{
public:
explicit auto_ptr(T* p = nullptr)
:_ptr(p)
{}
// auto_ptr拷贝构造的特点:资源转移
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if(this != &ap)
{
if(_ptr != nullptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if(_ptr != nullptr)
delete _ptr;
}
// 其实这里的T可能是const int / const Node
// 明白吗?hhhhhh
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
};
auto_ptr的拷贝构造和operator=实现的是资源转移,也就是被拷贝的智能指针将被置为空。
auto_ptr同样采用了RAII技术以及opeator* operator->
unique_ptr
删除器
template <class T>
struct default_delete
{
void operator()(T* p)
{
delete p;
}
};
template <class T>
struct delete_array
{
void operator()(T* p)
{
delete[] p;
}
};
template <class T>
struct Free
{
void operator()(T* p)
{
free(p);
}
};
template <class T>
struct Close
{
void operator()(T* p)
{
close(p);
}
};
// 注意,如果是const int,则删除器就是default_delete<const int>
template <class T, class D = default_delete<T>>
class unique_ptr
{
public:
explicit unique_ptr(T* p = nullptr)
: _ptr(p)
{}
~unique_ptr()
{
if(_ptr != nullptr)
{
D()(_ptr);
}
}
// unique_ptr不允许拷贝,支持RAII和像指针一样
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
private:
T* _ptr;
};
unique_ptr,如名,就是独一无二的智能指针,此智能指针同样采用了RAII和operator* operator->
不同的是对指针拷贝问题的解决:unique_ptr不支持拷贝构造和operator=
shared_ptr
template <class T, class D = yzl::default_delete<T>>
class shared_ptr
{
public:
explicit shared_ptr(T* p)
:_ptr(p)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
// 有线程安全问题,之后学到了再解决。
++*_pCount;
}
shared_ptr& operator=(const shared_ptr<T>& sp)
{
// 防止自赋值
if(_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++*_pCount;
}
return *this;
}
~shared_ptr()
{
Release();
}
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
T* get() const
{
return _ptr;
}
int use_count() const
{
return *_pCount;
}
private:
void Release()
{
if(--*_pCount == 0)
{
if(_ptr != nullptr)
D()(_ptr);
delete _pCount;
}
}
private:
T* _ptr;
int* _pCount;
};
shared_ptr是功能最强大的智能指针,同样实现了RAII技术,operator*,operator->。
shared_ptr 引用计数解决智能指针拷贝问题
同时shared_ptr采用引用计数,实现了智能指针的拷贝。注意,这里的引用计数不能是int类型数据成员(很明显不可以),也不可以是静态int类型数据成员,因为这样的话所有的shared_ptr<int>智能指针使用的都是一个引用计数,不管它们的指向是否相同,这显然也是不行的。
解决方法就是一个int*类型数据成员,在shared_ptr的构造函数中,堆区开辟一个int,初始化为1。在每次拷贝构造 or operator=中++该引用计数。
shared_ptr 循环引用问题
// 循环引用问题
struct Node
{
int _val;
std::shared_ptr<Node> _next;
std::shared_ptr<Node> _prev;
// std::weak_ptr<Node> _next;
// std::weak_ptr<Node> _prev;
~Node()
{
cout << "~Node" << endl;
}
};
using yzl::Node;
// 循环引用
void test_circular_reference()
{
shared_ptr<yzl::Node> n1(new yzl::Node);
shared_ptr<Node> n2(new Node);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
// 解决循环引用:将内部的智能指针变为weak_ptr,而不是shared_ptr
// 调用 weak_ptr<T>& operator=(const shared_ptr<T>& sp);
n1->_next = n2;
n2->_prev = n1;
// n1->_next.lock()->_val = 10;
// cout << n2->_val << endl;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
};
形如上方场景,比如某类型(Node)中有shared_ptr类型数据成员,此时n1 和 n2本身也是两个智能指针。如上就会造成循环引用的问题:即当函数栈帧销毁,n1 n2 析构,此时引用计数--,因为_next _prev分别指向对方,引用计数减为1,不会释放资源。
此时左边Node想要释放,需要右边Node的_prev调用析构函数(引用计数减为0),而_prev作为Node的自定义类型数据成员,右边Node被析构时会自动调用_prev的析构函数,右边Node析构需要左边_next调用析构,左边_next调用析构需要左边Node释放。
此时的场景就会造成循环引用。
解决方法是,将Node的智能指针数据成员变为weak_ptr类型,weak_ptr的作用其实就是为了解决shared_ptr的循环引用问题。 解决方法是:weak_ptr不会增加引用计数,因此左右两边Node的引用计数分别为1,n1 n2调用析构,资源得以释放。
智能指针删除器
因为new/delete new[]/delete[] malloc/free fopen/fclose(FILE*) 要配对使用,因此智能指针的析构函数不能固定实现为delete。
方式就是使用仿函数(函数对象),仿函数实现了operator()
template <class T>
struct default_delete
{
void operator()(T* p)
{
delete p;
}
};
template <class T>
struct delete_array
{
void operator()(T* p)
{
delete[] p;
}
};
template <class T>
struct Free
{
void operator()(T* p)
{
free(p);
}
};
template <class T>
struct Close
{
void operator()(T* p)
{
close(p);
}
};
注意:STL中unique_ptr的删除器是类模板参数,
如 unique_ptr<int, delete_array<int>> up(new int[3]); 即必须在类模板实参处传递仿函数类型
而std::shared_ptr的删除器实现在了构造函数的模板参数中,因此shared_ptr可以通过构造函数时传函数对象/lambda的方式传递删除器。
如 shared_ptr<int> sp(new int[3], [](int* p)->void{delete[] p; });
但是因为技术问题,仅能简单模拟shared_ptr的类模板形式的删除器
template<class T>
struct DeleteArray
{
void operator()(T* p)
{
delete[] p;
}
};
template<>
struct DeleteArray<string>
{
void operator()(string* p)
{
cout << "Never Give up" << endl;
delete[] p;
}
};
template<class T>
struct Free
{
void operator()(T* p)
{
free(p);
}
};
struct Close
{
void operator()(FILE* fp)
{
fclose(fp);
}
};
void test()
{
shared_ptr<int> sp(new int[3], [](int* p)->void{delete []p;});
shared_ptr<string> sp2(new string[3], [](string* p){delete[] p;});
shared_ptr<FILE> sp3(fopen("./../hehe.txt", "w+"), [](FILE* p)->void{fclose(p);});
shared_ptr<int> sp4((int*)malloc(sizeof(int)*5), [](int* p){free(p);});
unique_ptr<string, DeleteArray<string>> up4(new string[3]);
unique_ptr<FILE, Close> up1(fopen("./../hhhh.txt", "w+"));
}
weak_ptr
weak_ptr就是为了解决shared_ptr的循环引用问题的。此智能指针没有RAII,没有operator* 和 operator->,不参与资源的创建和释放,不增加引用计数。
std::weak_ptr有一个lock成员函数,用于返回与此weak_ptr绑定的shared_ptr。可以借此访问资源。
weak_ptr没有以T*为参数的构造函数,仅能拷贝构造 or 以一个shared_ptr为参数的构造,所以说weak_ptr就是一个辅助型智能指针,不参与资源管理。
内存泄漏... 略了
shared_ptr还有线程安全问题,略了,之后学了再说