注:这个是博主复习使用的专题,仅适用于自己以及学习过C++知识点的同学
文章目录
[1.1. 智能指针的使用](#1.1. 智能指针的使用)
[1.2. 智能指针的原理](#1.2. 智能指针的原理)
[二、C++ 中的智能指针](#二、C++ 中的智能指针)
[2.1. std::auto_ptr](#2.1. std::auto_ptr)
[介绍 auto_ptr](#介绍 auto_ptr)
[auto_ptr 的模拟实现](#auto_ptr 的模拟实现)
[2.2. std::unique_ptr](#2.2. std::unique_ptr)
[介绍 unique_ptr](#介绍 unique_ptr)
[2.3. std::shared_ptr](#2.3. std::shared_ptr)
[介绍 shared_ptr](#介绍 shared_ptr)
[shared_ptr 的线程安全问题](#shared_ptr 的线程安全问题)
[2.4. std::weak_ptr](#2.4. std::weak_ptr)
前言
注:这个是博主复习使用的专题,仅适用于自己以及学习过C++知识点的同学。
一、智能指针的使用及原理
1.1. 智能指针的使用
内存泄漏问题
内存泄露是指因为疏忽或错误,造成程序未能释放已经不再使用的内存的情况。比如:
cpp
#include<iostream>
using namespace std;
double Divide(int a, int b)
{
// 当b == 0 时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
int* array1 = new int[10];
int* array2 = new int[10]; // 抛异常呢
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch(...)
{
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw; // 异常重新抛出,捕获到什么抛出什么
}
cout << "delete []" << array1 << endl;
delete[] array1;
cout << "delete []" << array2 << endl;
delete[] array2;
}
int main()
{
try
{
Func();
}
catch(const char* errmsg)
{
cout << errmsg << endl;
}
catch(const exception & e)
{
cout << e.what() << endl;
}
catch(...)
{
cout << "未知异常" << endl;
}
return 0;
}
如果发生除 0 错误抛出异常,另外下面的array和array2没有得到释放。所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再重新抛出去。但是如果array2new的时候抛异常呢,就还需要套一层捕获释放逻辑,这里更好解决方案 是智能指针,否则代码太戳了
使用智能指针进行解决。比如:
cpp
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;
}
T& operator[](size_t i)
{
return _ptr[i];
}
private:
T* _ptr;
};
double Divide(int a,int b)
{
if (b == 0)
{
throw "Divide by zero condition";
}
else
{
return (double)a / (double)b;
}
}
void func()
{
// 使用 智能指针进行 管理 new 出来的数组,程序简单很多
SmartPtr<int> sp1 = new int[10];
SmartPtr<int> sp2 = new int[10];
for (size_t i = 0; i < 10; i++)
{
sp1[i] = sp2[i] = i;
}
int len, time;
cin >> len >> time;
cout << Divide(len,time) << endl;
}
int main()
{
try
{
func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
代码中将申请到的内存空间交给了一个SmartPtr对象进行管理。
- 在构造SmartPtr对象时,SmartPtr将传入的需要被管理的内存空间保存起来。
- 在SmartPtr对象析构时,SmartPtr的析构函数中会自动将管理的内存空间进行释放。
- 此外,为了让SmartPtr对象能够像原生指针一样使用,还需要对*和->运算符进行重载。
这样一来,无论程序是正常执行完毕返回了,还是因为某些原因中途返回了,或是因为抛异常返回了,只要SmartPtr对象的生命周期结束就会调用其对应的析构函数,进而完成内存资源的释放。
1.2. 智能指针的原理
RAII
RAII 是 Resource Acquisition Is Initializaion 的缩写,它是一种管理资源类的设计思想,本质是一种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等。RAII 在获取资源时资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免了资源泄露的问题。
实现智能指针时需要考虑以下三个方面的问题:
- 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
- 对 * 和 -> 运算符进行重载,使得该对象具有像指针一样的行为。
- 智能指针对象的拷贝问题。
为什么要解决智能指针对象的拷贝问题:
对于当前实现的 SmartPtr 类,如果用一个 SmartPtr 对象来 拷贝另一个 SmartPtr 对象,或是将一个 SmartPtr 对象赋值给另一个 SmartPtr 对象,都会导致 程序崩溃,例如:
cpp
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1); //拷贝构造
SmartPtr<int> sp3(new int);
SmartPtr<int> sp4(new int);
sp3 = sp4; //拷贝赋值
return 0;
}
原因如下:
- 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
- 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。
需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
二、C++ 中的智能指针
2.1. std::auto_ptr
介绍 auto_ptr
auto_ptr 是 C+98 设计出来的智能指针,他的特点是 拷贝时把被拷贝对象的资源的管理权转移给拷贝对象 ,但是这是一个非常糟糕的设计,因为会遇到 被拷贝对象悬空,访问报错的问题,强烈不建议使用 auto_ptr
cpp
#include <memory>
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);
// 此时 ap1 就消失了对 Date 的管理了
ap1->_month++; // error
return 0;
}
auto_ptr 的模拟实现
简易版的 auto_ptr 步骤如下:
- 在 构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
- 对 * 和 -> 运算符进行重载,使 auto_ptr 对象具有指针一样的行为。
- 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将掺入对象管理资源的指针置空。
- 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
代码如下:
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
namespace xxhh
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr):_ptr(ptr)
{}
// 拷贝构造函数
auto_ptr(auto_ptr<T>& ap):
_ptr(ap._ptr)
{
// 管理权转移
ap._ptr = nullptr;
}
// 赋值拷贝函数
auto_ptr& operator=(auto_ptr& ap)
{
if (this != &ap)
{
// 先释放 _ptr 的资源
if (_ptr)
delete[] _ptr;
// 把 ap 的资源 转给 _ptr
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete []" << _ptr << endl;
delete[] _ptr;
}
}
private:
T* _ptr; // 管理的资源
};
}
int main()
{
xxhh::auto_ptr<Date> ap1(new Date);
xxhh::auto_ptr<Date> ap2(ap1);
ap1->_day++;
return 0;
}
2.2. std::unique_ptr
介绍 unique_ptr
unique_ptr 是 C++11 设计出来的智能指针,他的特点是:不支持拷贝,支持移动。
cpp
int main()
{
unique_ptr<int> up1(new int);
//unique_ptr<int> up2(up1); // error
return 0;
}
实现unique_ptr
简易版的unique_ptr的实现步骤如下:
- 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
- 对*和->运算符进行重载,使unique_ptr对象具有指针一样的行为。
- 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上=delete,防止外部调用。
代码如下:
cpp
namespace xxhh
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr):_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;
unique_ptr<T>(unique_ptr<T>& up) = delete;
~unique_ptr()
{
if (_ptr)
{
cout << "delete " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
}
int main()
{
xxhh::unique_ptr<int> up1(new int);
//xxhh::unique_ptr<int> up2(up1); // error
return 0;
}
2.3. std::shared_ptr
介绍 shared_ptr
shared_ptr是C++11中引入的智能指针,shared_ptr通过引用计数的方式解决智能指针的拷贝问题。
- 每一个被管理的资源都有一个对应的引用计数,通过这个引用计数记录着当前有多少个对象在管理着这块资源。
- 当新增一个对象管理这块资源时则将该资源对应的引用计数进行++,当一个对象不再管理这块资源或该对象被析构时则将该资源对应的引用计数进行--。
- 当一个资源的引用计数减为0时说明已经没有对象在管理这块资源了,这时就可以将该资源进行释放了。
通过这种引用计数的方式就能支持多个对象一起管理某一个资源,也就是支持了智能指针的拷贝,并且只有当一个资源对应的引用计数减为0时才会释放资源,因此保证了同一个资源不会被释放多次。比如:
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
shared_ptr<Date> sp1(new Date);
// 支持拷贝
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl; // 3
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎
shared_ptr<Date> sp4(move(sp1));
cout << sp4.use_count() << endl; // 3
return 0;
}
说明一下: use_count成员函数,用于获取当前对象管理的资源对应的引用计数。
实现shared_ptr
简易版的shared_ptr的实现步骤如下:
- 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
- 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
- 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
- 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
- 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
- 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。
拷贝构造:

赋值拷贝:

代码如下:
cpp
namespace xxhh
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr) :_ptr(ptr), _pcount(new int(1))
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
if (_ptr != nullptr)
(*_pcount)++;
}
shared_ptr& operator=(shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
// 先减去 sp 的引用计数
if ((*_pcount--) == 0)
{
// 进行销毁
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count()
{
return *_pcount;
}
~shared_ptr()
{
// 先检查 -- 后 引用计数是否为 0
if (--(*_pcount) == 0)
{
if (_ptr != nullptr)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
_ptr = nullptr;
}
delete _pcount;
_pcount = nullptr;
}
}
private:
T* _ptr;
int* _pcount;
};
}
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
int main()
{
xxhh::shared_ptr<Date> sp1(new Date);
// 支持拷贝
xxhh::shared_ptr<Date> sp2(sp1);
xxhh::shared_ptr<Date> sp3 = sp2;
cout << sp1.use_count() << endl; // 3
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}
shared_ptr 的线程安全问题
shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进⾏shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的进行线程安全的控制。
最简单的方法,就是把上面的 int* _pcount 改成 atomic<int>* _pcount。
2.4. std::weak_ptr
weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr⽀持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用 lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
cpp
int main()
{
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
说明一下: shared_ptr还会提供一个get 函数,用于获取其管理的资源。
三、C++11和boost中智能指针的关系
- C++11和boost中智能指针的关系
- C++98中产生了第一个智能指针auto_ptr。
- C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
- C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
- C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。