本章代码Gitee仓库:智能指针
1. 为什么要有智能指针
C++引入了异常体系之后,对于普通的指针的内存释放是较困难的,例如:
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
{
throw invalid_argument("error: DIV_ZERO");
}
return a / b;
}
void func()
{
int* p = new int(10);
try
{
div();
}
catch (...)
{
delete p;
throw;
}
delete p;
}
int main()
{
try {
func();
}
catch (const exception&e)
{
cout << e.what() << endl;
}
return 0;
}
这里如果指针一多,那么对于每个异常的处理都需要考虑到指针的释放,这个操作还很繁琐的,而且代码很不优雅!
使用智能指针能较好的预防内存泄漏
2. RAII机制
为了解决上面的问题,设计了一个类来管理指针:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:ptr_(ptr)
{}
~SmartPtr()
{
if(ptr_)
delete ptr_;
}
private:
int* ptr_;
};
这里利用了构造和析构的特性,对象初始化自动调用构造函数,出了作用域自动调用析构函数,这就不需要我们手动的delete
。
这就是 RAII(Resource Acquisition Is Initialization)
机制:利用对象生命周期控制程序资源,将需要管理的资源交给一个对象,这样我们就无需显示释放资源且对象所需资源在生命周期内始终有效。
3. 智能指针原理
引入RAII机制之后,解决了申请释放的问题,但是还无法像指针一样操作,例如指针解引用和指向空间的内容,这里就需要重载一下*
和->
:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:ptr_(ptr)
{}
T& operator*()
{
return *ptr_;
}
T* operator->()
{
return ptr_;
}
~SmartPtr()
{
if (ptr_)
{
delete ptr_;
}
}
private:
T* ptr_;
};
以上就是智能指针的原理:
- 利用RAII机制管理
- 重载
*
、->
,让其能像指针一样操作 - 拷贝问题
但是这里还没完,这里还是有一些问题的,例如赋值操作:
这里赋值操作导致了指向同一块空间,最后释放了2次,而另一块空间内存泄漏。
为了解决这一系列的隐含问题,便出现了我们现在的智能指针,智能指针发展:
- C++98:
auto_ptr
(管理权转移)- C++11:
unique_ptr
(禁止拷贝)、shared_ptr
(支持拷贝,但有循环引用问题)、weak_ptr
(不支持RAII
机制,专门解决shared_ptr循环引用问题)
4. auto_ptr (c++98)
auto_ptr
是一个较为失败的设计:
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a_;
};
int main()
{
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(ap1);
return 0;
}
这里拷贝构造之后,将被拷贝的对象资源管理权直接转给了拷贝对象,导致被拷贝对象悬空,访问就会出问题
这里不能理解成移动构造,移动构造转移的是将亡值的资源,而
auto_ptr
是将正常的资源直接转移了
auto_ptr
底层简易模拟实现:
namespace myptr
{
template<class T>
class auto_ptr
{
//RAII
//像正常指针
public:
auto_ptr(T* ptr = nullptr)
:ptr_(ptr)
{}
//拷贝构造
auto_ptr(auto_ptr<T>& ap)
:ptr_(ap.ptr_) //管理器转让
{
ap.ptr_ = nullptr; //将自己置空
}
T& operator*()
{
return *ptr_;
}
T* operator->()
{
return ptr_;
}
~auto_ptr()
{
if (ptr_)
{
cout << "delete: " << ptr_ << endl;
delete ptr_;
}
}
private:
T* ptr_;
};
};
5. unique_ptr (c++11)
c++11引入新的智能指针,是“借鉴”boost
库的,unique_ptr
在boost
库中叫做scoped_ptr
,其他2个名字没变
auto_ptr
拷贝有问题,而unique_ptr
的解决方案十分简单粗暴,直接禁掉拷贝
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a_;
};
int main()
{
unique_ptr<A> up1(new A(10));
//error
//unique_ptr<A> up2(up1);
return 0;
}
unique_ptr
底层简易模拟实现:
template<class T>
class unique_ptr
{
//RAII
//像正常指针
public:
unique_ptr(T* ptr = nullptr)
:ptr_(ptr)
{}
//拷贝构造
unique_ptr(unique_ptr<T>& up) = delete; //禁拷贝构造
unique_ptr<T>& operator=(unique_ptr<T>& up) = delete; //禁赋值运算符重载
T& operator*()
{
return *ptr_;
}
T* operator->()
{
return ptr_;
}
~unique_ptr()
{
if (ptr_)
{
cout << "delete: " << ptr_ << endl;
delete ptr_;
}
}
private:
T* ptr_;
};
6. shared_ptr (c++11)
shared_ptr
是支持拷贝,通过引用计数的方式实现资源共享:
最后一个走的人锁门
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a_;
};
int main()
{
shared_ptr<A> sp1(new A(10));
shared_ptr<A> sp2(new A(20));
shared_ptr<A> sp3(sp1);
shared_ptr<A> sp4 = sp2;
return 0;
}
//输出:
// A()
// A()
// ~A()
// ~A()
6.1 shared_ptr线程安全问题
这个引用计数是一个临界资源,当多个线程同时访问的时候,会引发线程安全问题,这里就需要加锁
template<class T>
class shared_ptr
{
//RAII
//像正常指针
public:
shared_ptr(T* ptr = nullptr)
:ptr_(ptr), pcount_(new int(1)), pmtx_(new mutex)
{}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:ptr_(sp.ptr_), pcount_(sp.pcount_), pmtx_(sp.pmtx_)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止自己给自己赋值
if (ptr_ == sp.ptr_)
return *this;
ptr_ = sp.ptr_;
pcount_ = sp.pcount_;
pmtx_ = sp.pmtx_;
AddCount();
return *this;
}
T& operator*()
{
return *ptr_;
}
T* operator->()
{
return ptr_;
}
void Release()
{
pmtx_->lock();
bool deleteFlag = false;
if (--(*pcount_) == 0)
{
delete ptr_;
delete pcount_;
deleteFlag = true;
}
pmtx_->unlock();
if (deleteFlag) delete pmtx_;
}
void AddCount()
{
pmtx_->lock();
++(*pcount_);
pmtx_->unlock();
}
T* get()
{
return ptr_;
}
int use_count()
{
return *pcount_;
}
~shared_ptr()
{
Release();
}
private:
T* ptr_;
int* pcount_; //指针,多个智能指针访问同一个计数器
mutex* pmtx_; //指针,多个智能指针访问同一把锁
};
shared_ptr
本身是线程安全的,因为计数有加锁保护!
而shared_ptr
管理的对象,不一定是线程安全的!
struct Date
{
int year_ = 0;
int month_ = 0;
int day_ = 0;
};
void SharePtrFunc(myptr::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{
cout << sp.get() << endl;
int begin = clock();
for (size_t i = 0; i < n; i++)
{
//std::shared_ptr<Date> copy(sp);
myptr::shared_ptr<Date> copy(sp);
mtx.lock(); //加锁保护
{
copy->year_++;
copy->month_++;
copy->day_++;
}
//cout << i << endl;
mtx.unlock();
}
int end = clock();
//cout << end - begin << endl;
}
int main()
{
//std::shared_ptr<Date> p(new Date);
myptr::shared_ptr<Date> p(new Date);
cout << p.get() << endl;
const size_t sz = 500000;
mutex mtx;
thread t1(SharePtrFunc, ref(p), sz, ref(mtx));
thread t2(SharePtrFunc, ref(p), sz, ref(mtx));
t1.join();
t2.join();
cout << p.use_count() << endl;
cout << p->year_ << endl;
cout << p->month_ << endl;
cout << p->day_ << endl;
}
6.2 shared_ptr循环引用
来看这段代码:
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int a_;
};
struct Node
{
A val_;
shared_ptr<Node> next_;
shared_ptr<Node> prev_;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->next_ = sp2;
sp2->prev_ = sp1;
return 0;
}
//输出:
//A()
//A()
这里的sp1
和sp2
都没有析构,这就是shared_ptr
的循环引用问题:
这属于shared_ptr
的一个缺陷,要解决这个问题就得采用weak_ptr
6.3 定制删除器
shared_ptr
底层默认析构是delete ptr
,要是对于数组指针或者是malloc
出来的,就无法析构,所以shared_ptr
构造还是支持了定制删除器(仿函数)。
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << a_ << "~A()" << endl;
}
private:
int a_;
};
template<class T>
struct DeleteArr
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
shared_ptr<A> sp1(new A[5], DeleteArr<A>()); //返函数
cout << endl;
shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); }); //lambda表达式
return 0;
}
shared_ptr
底层简易模拟实现:
这个可以指向同一块空间,而且不会被多次释放,采用引用计数的方式。
这里的引用计数,每个申请资源都有一份
template<class T>
class shared_ptr
{
//RAII
//像正常指针
public:
shared_ptr(T* ptr = nullptr)
:ptr_(ptr), pcount_(new int(1)), pmtx_(new mutex)
{}
template<class D>
shared_ptr(T* ptr, D del)
: ptr_(ptr), pcount_(new int(1)), pmtx_(new mutex), del_(del)
{}
//拷贝构造
shared_ptr(const shared_ptr<T>& sp)
:ptr_(sp.ptr_), pcount_(sp.pcount_), pmtx_(sp.pmtx_)
{
AddCount();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
//防止自己给自己赋值
if (ptr_ == sp.ptr_)
return *this;
ptr_ = sp.ptr_;
pcount_ = sp.pcount_;
pmtx_ = sp.pmtx_;
AddCount();
return *this;
}
T& operator*()
{
return *ptr_;
}
T* operator->()
{
return ptr_;
}
void Release()
{
pmtx_->lock();
bool deleteFlag = false;
if (--(*pcount_) == 0)
{
//delete ptr_;
del_(ptr_);
delete pcount_;
deleteFlag = true;
}
pmtx_->unlock();
if (deleteFlag) delete pmtx_;
}
void AddCount()
{
pmtx_->lock();
++(*pcount_);
pmtx_->unlock();
}
T* get() const
{
return ptr_;
}
int use_count() const
{
return *pcount_;
}
~shared_ptr()
{
Release();
}
private:
T* ptr_;
int* pcount_; //指针,多个智能指针访问同一个计数器
mutex* pmtx_; //指针,多个智能指针访问同一把锁
function<void(T*)> del_ = [](T* ptr) {delete ptr; }; //定制删除器
};
7. weak_ptr (c++11)
weak_ptr
不是RAII
机制,它的出现是专门用来解决shared_ptr
的循环引用问题,不是单独使用的,是搭配着shared_ptr
使用:
class A
{
public:
A(int a = 0)
:a_(a)
{
cout << "A()" << endl;
}
~A()
{
cout << a_ << "~A()" << endl;
}
private:
int a_;
};
struct Node
{
A val_;
weak_ptr<Node> next_;
weak_ptr<Node> prev_;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->next_ = sp2;
sp2->prev_ = sp1;
return 0;
}
这里
shared_ptr
能赋值给weak_ptr
,是因为weak_ptr
重载了=
运算符:
这里解决循环的引用的原理就是不增加引用计数,不参与资源的释放管理
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_;
};