什么是智能指针,智能指针相比于传统的裸指针而言,其智能之处在哪里?关于这个问题的解释,首先我们来看一下传统的裸指针在使用的时候常常会 出现的几种问题:
1、通过malloc/new开辟的内存,在函数执行到某处的时候return掉了,以至于没有执行相应的free/delete语句,造成了内存泄漏;
2、通过malloc/new开辟的内存,忘记写free/delete语句,导致内存泄漏;
3、对同一个资源多次释放,导致释放野指针,程序发生崩溃错误;
智能指针的智能之处就在于写程序的人不用关心资源的释放,利用了栈上的对象出作用域自动析构的特性,自动释放指针所指向的资源,避免内存泄漏。关于他的底层实现其实就是对裸指针的封装,提供了运算符重载函数而言。下面我们将实现一个智能指针。
#include<iostream>
using namespace std;
template<typename T>
class Smart {
public:
Smart(T* ptr = T()) :_ptr(ptr) {
}
T& operator*(){return *_ptr; }
T* operator->(){return _ptr; }
~Smart() { delete _ptr; }
private:
T* _ptr;
};
int main() {
Smart<int> p1(new int);
Smart<int> p2(p1);
return 0;
}
可以看到,正如上述所说,智能指针实际上就是将裸指针进行了一次面向对象的封装,利用了栈上对象出作用域自动析构的特点实现其智能之处。分析一下main函数的执行结果是什么,没错程序最终会崩溃,原因是p1和p2都指向同一块资源,在对象出作用域析构的时候,一块资源被释放两次,发生了浅拷贝问题。下面我们来看看C++库里所提供的智能指针是如何解决这个问题的?
不带引用计数的智能指针主要有auto_ptr,scoped_ptr,unique_ptr关于上述问题,我们将一个一个进行分析:
<1>auto_ptr,首先我们看一下auto_ptr的底层实现:
auto_ptr(auto_ptr<_Other>& _Right) noexcept
: _Myptr(_Right.release()) { // construct by assuming pointer from _Right
在其构造函数下,调用了release方法,再来看一下release方法的具体实现是什么?
_Ty* release() noexcept { // return wrapped pointer and give up ownership
_Ty* _Tmp = _Myptr;
_Myptr = nullptr;
return _Tmp;
}
看到这里我们明白了auto_ptr是如何解决浅拷贝问题了,它实际上是进行了资源的转移,简单来说,就是只有最后一个对象拥有该资源,前面的对象都不在拥有(将自己置nullptr)。所以一定不能对进行资源转移过的对象进行操作。
说到这里,试问auto_ptr能在容器中使用吗?答案肯定是不行的,当容器之间进行拷贝构造时,相当于容器的元素进行拷贝构造,那么原来容器中的元素都被置nullptr,再次访问会出现程序崩溃错误,所以切记容器中不可使用auto_ptr。
<2>scoped_ptr,一样我们来看一下其底层实现:
template<class T> class scoped_ptr // noncopyable
{
private:
T * px;
/*
私有化拷贝构造函数和赋值函数,这样scoped_ptr的智能指针
对象就不支持这两种操作,从根本上杜绝浅拷贝的发生
*/
scoped_ptr(scoped_ptr const &);
scoped_ptr & operator=(scoped_ptr const &);
};
scoped_ptr直接将拷贝构造函数和赋值运算符重载函数私有化,从根本上杜绝了浅拷贝的发生,对于容器而言,scoped_ptr也是不能使用在其中的,原因就在于当容器之间进行拷贝构造时,其元素之间之间也会进行拷贝构造,由于将拷贝构造函数私有化,因此会发生编译错误。
<3>unique_ptr,首先观察一下其底层实现
template <class _Ty,class _Dx> // = default_delete<_Ty>
class unique_ptr {
public:
//提供右值引用的拷贝构造函数
unique_ptr(unique_ptr&& _Right) noexcept
: _Mypair(_One_then_variadic_args_t(), _STD forward<_Dx>(_Right.get_deleter()), _Right.release()) {}
//提供右值引用的赋值运算符重载
unique_ptr& operator=(unique_ptr&& _Right) noexcept {
if (this != _STD addressof(_Right)) {
reset(_Right.release());
_Mypair._Get_first() = _STD forward<_Dx>(_Right._Mypair._Get_first());
}
return *this;
}
//删除左值引用的拷贝构造函数和赋值运算符重载函数
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
private:
template <class, class>
friend class unique_ptr;
_Compressed_pair<_Dx, pointer> _Mypair;
};
从上面看到,unique_ptr有一点和scoped_ptr做的一样,就是去掉了拷贝构造函数和operator=赋值重载函数,禁止用户对unique_ptr进行显示的拷贝构造和赋值,防止智能指针浅拷贝问题的发生。
但是unique_ptr提供了带右值引用参数的拷贝构造和赋值,也就是说,unique_ptr智能指针可以通过右值引用进行拷贝构造和赋值操作,或者在产生unique_ptr临时对象的地方,如把unique_ptr作为函数的返回值时进行拷贝构造和赋值操作。
什么是带引用计数的智能指针,通俗来说当允许多个智能指针指向同一个资源的时候,每一个智能指针都会给资源的引用计数加1,当一个智能指针析构时,同样会使资源的引用计数减1,这样最后一个智能指针把资源的引用计数从1减到0时,就说明该资源可以释放了,由最后一个智能指针的析构函数来处理资源的释放问题,这就是引用计数的概念。
要对资源的引用个数进行计数,我们知道i++,i--操作在多线程环境下不是线程安全的操作,带引用计数的的智能指针底层的引用计数已经通过了CAS操作保证了其原子性,因此shared_ptr和weak_ptr本身就是线程安全的带引用计数的智能指针。
template<typename T>
class Share_ptr {
public:
//构造函数
Share_ptr(T *ptr = T()) :_ptr(ptr) {
if (_ptr != nullptr) {
_cnt = new int(1);
}
else {
_cnt = new int(0);
}
}
//*号运算符重载函数
T& operator*() {
if (this->_ptr) {
return *this->_ptr;
}
}
T* operator->() {
if (this->_ptr) {
return this->_ptr;
}
}
const T& operator*()const {
if (this->_ptr) {
return *this->_ptr;
}
}
const T* operator->() const{
if (this->_ptr) {
return this->_ptr;
}
}
//拷贝构造函数
Share_ptr(const Share_ptr& src) {
if (this != &src) {
this->_ptr = src._ptr;
this->_cnt = src._cnt;
(*this->_cnt)++;
}
}
//赋值运算符重载函数
Share_ptr& operator=(const Share_ptr& src) {
if (this->_ptr == src._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_cnt)--;
if ((*this->_cnt) == 0) {
delete this->_ptr;
this->_ptr = nullptr;
delete this->_cnt;
this->_cnt = nullptr;
}
}
this->_ptr = src._ptr;
this->_cnt = src._cnt;
(*this->_cnt)++;
return *this;
}
//析构函数
~Share_ptr() {
(*this->_cnt)--;
if ((*this->_cnt) == 0) {
delete _ptr;
_ptr = nullptr;
delete _cnt;
_cnt = nullptr;
}
}
//获得引用计数
int getCnt() {
return (*this->_cnt);
}
private:
T* _ptr;
int* _cnt;
};
int main() {
Share_ptr<int> ptr(new int());
cout << ptr.getCnt() << endl;
Share_ptr<int> ptr1(ptr);
cout << ptr1.getCnt() << endl;
Share_ptr<int> ptr2;
ptr2 = ptr;
cout << ptr2.getCnt() << endl;
return 0;
什么是指针指针的交叉引用问题:首先列出一下代码:
#include<iostream>
#include<memory>
using namespace std;
class B;
class A {
public:
A() {cout << "constructA!" << endl;}
~A() {cout << "deleteA" << endl;}
shared_ptr<B> _ptrb;
};
class B{
public:
B() { cout << "constructB!" << endl; }
~B() { cout << "deleteB" << endl; }
shared_ptr<A> _ptra;
};
int main(){
shared_ptr<A> _ptr1(new A);
shared_ptr<B> _ptr2(new B);
_ptr1->_ptrb = _ptr2;
_ptr2->_ptra = _ptr1;
cout << _ptr1.use_count() << endl;
cout << _ptr2.use_count() << endl;
return 0;
}
观察其结果:
可以看到,A和B对象并没有进行析构,通过上面的代码示例,能够看出来“交叉引用”的问题所在,就是对象无法析构,资源无法释放,如图所示:
那怎么解决这个问题呢?请注意强弱智能指针的一个重要应用规则:定义对象时,用强智能指针shared_ptr,在其它地方引用对象时,使用弱智能指针weak_ptr。
弱智能指针weak_ptr区别于shared_ptr之处在于:
- weak_ptr不会改变资源的引用计数,只是一个观察者的角色,通过观察shared_ptr来判定资源是否存在
- weak_ptr持有的引用计数,不是资源的引用计数,而是同一个资源的观察者的计数
- weak_ptr没有提供常用的指针操作,无法直接访问资源,需要先通过lock方法提升为shared_ptr强智能指针,才能访问资源
对上述代码进行修改:
#include<iostream>
#include<memory>
using namespace std;
class B;
class A {
public:
A() {cout << "constructA!" << endl;}
~A() {cout << "deleteA" << endl;}
weak_ptr<B> _ptrb;
};
class B{
public:
B() { cout << "constructB!" << endl; }
~B() { cout << "deleteB" << endl; }
weak_ptr<A> _ptra;
};
int main(){
shared_ptr<A> _ptr1(new A);
shared_ptr<B> _ptr2(new B);
_ptr1->_ptrb = _ptr2;
_ptr2->_ptra = _ptr1;
cout << _ptr1.use_count() << endl;
cout << _ptr2.use_count() << endl;
return 0;
}
观察其结果:
这下我们发现资源成功进行释放,其原理如下图所示:
什么是多线程访问共享对象问题呢?解释如下:线程A和线程B访问一个共享的对象,如果线程A正在析构这个对象的时候,线程B又要调用该共享对象的成员方法,此时可能线程A已经把对象析构完了,线程B再去访问该对象,就会发生不可预期的错误。
代码如下:
#include<iostream>
#include<memory>
#include<thread>
using namespace std;
class test {
public:
test() {}
~test() {}
void func() {cout << "this is good function!" << endl;}
};
void threadworker(test *p) {
this_thread::sleep_for(std::chrono::seconds(2));
p->func();
}
int main(){
test* ptr = new test();
thread t1(threadworker,ptr);
delete ptr; //析构这个对象
t1.join();
return 0;
}
程序运行结果如下:
按照正常逻辑而言,子线程睡眠两秒,那么主线程在析构对象后,子线程不应该能访问到该对象的资源,这可能会导致不可预期的错误。可以借助share_ptr和weak_ptr来解决这个问题,代码如下:
#include<iostream>
#include<memory>
#include<thread>
using namespace std;
class test {
public:
test() {}
~test() {}
void func() {cout << "this is good function!" << endl;}
};
void threadworker(weak_ptr<test> p) { //引用处用弱智能指针
this_thread::sleep_for(std::chrono::seconds(2));
/*
如果想访问对象的方法,先通过p的lock方法进行提升操作,把weak_ptr提升为
shared_ptr强智能指针,提升过程中,是通过检测它所观察的强智能指针保存
的Test对象的引用计数,来判定Test对象是否存活,ps如果为nullptr,说明Test对象
已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。
*/
if (shared_ptr<test> ptr = p.lock()) {
ptr->func();
}
}
int main(){
shared_ptr<test> ptr(new test()); //定义处用强智能指针
thread t1(threadworker,ptr);
t1.join();
return 0;
}
代码可以正常的输出结果,因为主线程调用join方法等待子线程结束,子线程中p调用lock方法提升成功。
那么思考一下如果将子线程处理为分离线程结果会是怎样呢?注意处理为分离线程,那么主线程先于子线程结束,析构了对象的资源,p调用lock方法提升失败,无法执行成员方法。
关于智能智能指针的删除器我自己认为他是一个极其强大的功能。对于正常情况下,我们常常管理的是堆资源,当智能指针出作用域的时候,会调用析构函数释放资源。那么删除一个文件资源,或者希望资源按照我们所希望的方式进行回收等等操作,该如何处理呢?自定义智能指针的删除器就可以实现。
我们来看一下unique_ptr的类声明:
template <class _Ty,
class _Dx> // = default_delete<_Ty>
//除了传入所开辟资源的类型_ty外,还能传入一个函数对象类型删除删除_ty类型的资源
再来看一下unique_ptr的析构函数:
~unique_ptr() noexcept {
if (_Mypair._Myval2 != pointer()) {
_Mypair._Get_first()(_Mypair._Myval2);
}
}
//返回用于析构被管理对象的删除器
_NODISCARD _Dx& get_deleter() noexcept {
return _Mypair._Get_first();
//返回指向被管理对象的指针
_NODISCARD pointer get() const noexcept {
return _Mypair._Myval2;
}
可以看出来实现智能指针的删除器需要定义一个函数对象:
下面看一个删除文件资源的例子:
class FileDeleter
{
public:
// 删除器负责删除资源的函数
void operator()(FILE *pf)
{
fclose(pf);
}
};
int main()
{
// 由于用智能指针管理文件资源,因此传入自定义的删除器类型FileDeleter
unique_ptr<FILE, FileDeleter> filePtr(fopen("data.txt", "w"));
return 0;
}
上述方法虽然可行,但是需要创建额外的类,我并不热衷于此做法。function和lambda表达式可以更好的实现了智能指针的删除器。
// 自定义智能指针删除器,关闭文件资源
unique_ptr<FILE, function<void(FILE*)>>
filePtr(fopen("data.txt", "w"), [](FILE *pf)->void{fclose(pf);});