什么是智能指针
C++没有像java那样的自动回收机制,手动创建的堆内存使用完,需要手动进行释放,否则会造成内存泄漏(也就是这块内存被无效占用着)。为了解决这个问题,发明了智能指针(smart point)。智能指针是存储只想动态分配对象指针的类,用于控制分配对象的生存周期,在离开分配对象的作用域时,进行自动的销毁对象,释放内存,从而边内存泄漏。智能指针的通过引用计数来实现控制策略,每使用一次,内部引用记数加1,每析构一次内部的引用计数减1,计数为0时,将删除所指向的对象,并释放空间
堆内存
堆内存指的是程序执行中程序员通过手动使用malloc / calloc / realloc / new等从堆(heap)中分配的一块内存,用完后必须通过调用相应的 free或者delete释放
智能指针的简单实现
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 pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
运行机制:
构造SmartPtr对象时,调用构造函数,在构造函数中将传入的对象内存保存起来
析构SmartPtr对象时,调用析构函数,在析构函数中将保存的对象内存空进行释放
重载原对象指针的 *和 ->运算符等进行重载,使得使用智能指针时的用法和原对象指针一致
如果在使用中抛出异常,申请的空间会随着 SmartPtr 对象的生命周期结束而释放,避免造成了内存泄漏
智能指针的原理
RALL(Resource Acquisition Is Initialization):资源获取即初始化,是一种利用对象生命周期来控制程序资源。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
采用这种方式不需要显示的释放资源,实现自动内存管理,避免内存泄漏
智能指针的拷贝问题
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;
}
由于智能指针自动释放所管理的内存空间的特殊机制,如果使用拷贝或者赋值将导致异常
原因:
编译器默认生成的拷贝构造函数对内置类型进行浅拷贝,浅拷贝就是拷贝对象和被拷贝对象使用同一块内存,也就是sp2和sp1指向管理同一块内存,这样会导致悬空指针,也就是当sp1生命周期结束时,将管理的内存释放,sp2指向管理的内存被释放了,指向该内存的指针就是悬空指针,再对这块内存进行操作,将发生无法预料的错误,但是sp2生命周期结束也需要释放这块内存,也就是这块内存将被释放两次,显然会造成异常
赋值函数与拷贝构造函数同理,也是浅拷贝
什么是野指针
野指针:没有被初始化且没有被置为NULL或nullptr的指针
int * p;
什么是悬空指针
悬空指针:指向的内存已经被释放的指针
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
解决办法
野指针:定义指针变量及时初始化,要么置空。
悬空指针:释放操作后立即置空。
什么是浅拷贝和深拷贝
浅拷贝:简单的赋值拷贝操作,将原对象或原数组的引用(地址)直接赋给新对象,新数组,新对象/新数组只是原对象的一个引用(地址)
深拷贝:创建一个新的对象和数组,重新申请空间,将原对象的各项属性的“值”(数组的所有元素)拷贝到新申请的空间
指针和引用本质都地址,通过地址访问值
C++库中的智能指针
auto_ptr
auto_ptr是C++98的,通过管理权转移的方式解决智能指针拷贝问题,保证了一个资源只有一个对象对其进行管理,这时候一个资源就不会被多个释放
int main()
{
std::auto_ptr<int> ap1(new int(1));
std::auto_ptr<int> ap2(ap1);
*ap2 = 10;
//*ap1 = 10;错误的写法
return 0;
}
在执行ap2(sp1),sp1已经把管理权给了sp2,所以再进行*ap1 = 10;等操作,就会产生异常
auto_ptr的简单实现
拷贝构造函数,用传入的对象的资源来构造当前对象,并将传入对象管理资源指针置空
template<class T>
class auto_ptr
{
public:
// RAII
// 保存资源
auto_ptr(T* ptr)
:_ptr(ptr)
{}// 释放资源
~auto_ptr()
{
//delete[] _ptr;
delete _ptr;
cout << _ptr << endl;
}auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
sp._ptr = nullptr;
}// 像指针一样
T& operator*()
{
return *_ptr;
}T* operator->()
{
return return _ptr;
}T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
auto_ptr是一个失败设计,很多公司也都明确规定了禁止使用auto_ptr
C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件<memory>
std::shared_ptr:共享的智能指针
std::unique_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
unique_ptr
unique_ptr是C++11中的智能指针,unique_ptr直接防止拷贝的方式解决智能指针的拷贝问题,简单而又粗暴,防止智能指针对象拷贝,保证资源不会被多次释放
以下将会报错
std::unique_ptr<int> up1(new int(0));
std::unique_ptr<int> up2(up1);
unique_ptr的简单实现
template<class T>
class unique_ptr
{
public:
// RAII
// 保存资源
unique_ptr(T* ptr)
:_ptr(ptr)
{}
// 释放资源
~unique_ptr()
{
//delete[] _ptr;
delete _ptr;
cout << _ptr << endl;
}unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;// 像指针一样
T& operator*()
{
return *_ptr;
}T* operator->()
{
return return _ptr;
}T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
shared_ptr
通过引用计数的方式解决智能指针的拷贝问题,每一个被管理的内存或资源对应一个引用计数,引用计数记录当前有多少个对象在管理这块资源,从而支持了拷贝构造,当引用计数为0时,说明没有对象在管理这块内存或者资源,则对它进行释放
用法:
int main()
{
std::shared_ptr<int> sp1(new int(1));
std::shared_ptr<int> sp2(sp1);
*sp1 = 10;
*sp2 = 20;
cout << sp1.use_count() << endl; //2
//use_count:用于获取当前对象管理的资源对应的引用计数。
std::shared_ptr<int> sp3(new int(1));
std::shared_ptr<int> sp4(new int(2));
sp3 = sp4;
cout << sp3.use_count() << endl; //2
return 0;
}
shared_ptr的简单实现
template<class T>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
{}// 释放资源
~shared_ptr()
{
Release();
}shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);
}void Release()
{
if (--(*_pcount) == 0)
{
delete _pcount;
delete _ptr;
}
}
//sp1 = sp1;
//sp1 = sp2;//sp2如果是sp1的拷贝呢?
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//资源地址不一样
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
++(*_pcount);
}return *this;
}
int use_count()
{
return *_pcount;
}// 像指针一样
T& operator*()
{
return *_ptr;
}T* operator->()
{
return _ptr;
}T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pcount;
};
注意拷贝构造需要将原来管理的资源引用计数减1,再将拷贝的资源引用计数加1
对引用计数可能存在的疑惑
对于类型int:shared_ptr的引用计数不能直接定义成int类型的成员变量,否则每一个shared_ptr对象在栈区都有自己的pcount成员变量,而当多个对象管理同一块资源时,这几个对象应该是用同一个引用计数!所以由第一个管理资源shared_ptr开辟一个堆区,存放引用计数,并且由最后一个结束生命周期的shared_ptr对管理资源释放的同时对引用计数的内存进行释放
对于静态:shared_ptr的引用计数不能定义成静态的成员变量,否则所有类型对象使用相同的引用计数,这会导致管理不同资源的shared_ptr对象用同一个引用计数
shared_ptr线程安全问题
多个线程可能同时会对引用计数进行操作,而引用计数的自增自减操作并不是原子操作,所以存在线程安全问题,需要通过加锁或者原子类atomic对引用计数及进行保护
template<class T>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr)
:_ptr(ptr)
, _pcount(new int(1))
,_pmtx(new mutex)
{}// 释放资源
~shared_ptr()
{
Release();
}shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
_pmtx->lock();//t1,t2
++(*_pcount);
_pmtx->unlock();
}void Release()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
delete _pcount;
delete _ptr;
flag = true;
}
_pmtx->unlock();
if (flag == true) delete _pmtx;
}//sp1 = sp1;
//sp1 = sp2;//sp2如果是sp1的拷贝呢?
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//资源地址不一样
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
_pmtx = sp->_pmtx;_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}return *this;
}// 像指针一样
T& operator*()
{
return *_ptr;
}T* operator->()
{
return _ptr;
}T& operator[](size_t pos)
{
return _ptr[pos];
}int use_count()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;
};
由于需要进行unlock操作,所以不能在此之前将锁进行释放,所以设置一个flag,用于unlock之后的判断
但是此时引用计数是线程安全的但是管理的资源并不一定是线程安全的,需要根据需求自行加锁解决
shared_ptr定制删除器
智能指针对象的生命周期结束时,默认是以
delete
的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以new
方式申请到的内存空间,也可能是以new[]
的方式申请到的空间,或是一个文件指针,需要通过fclose进行释放
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _val;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
std::shared_ptr<ListNode> sp1(new ListNode[10]);
std::shared_ptr<FILE> sp2(fopen("test.cpp", "r"));
return 0;
}
ptr:需要让智能指针管理的资源。
del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。
当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放
//定制删除器
template <class T>
struct DeleteArray
{
void operator()(const T* ptr=nullptr)
{
delete[] ptr;
cout << "delete []" << ptr << endl;
}
};
int main()
{
hwc::shared_ptr<ListNode> n1(new ListNode);
hwc::shared_ptr<ListNode, DeleteArray<ListNode>> sp1(new ListNode[10]);
return 0;
}
删除器的简单实现
template <class T>
class default_delete
{
public:
void operator()(T* ptr)
{
delete ptr;
}
};template<class T,class D =default_delete<T>>
class shared_ptr
{
public:
// RAII
// 保存资源
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
,_pmtx(new mutex)
{}
// 释放资源
~shared_ptr()
{
Release();
}shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
,_pmtx(sp._pmtx)
{
_pmtx->lock();//t1,t2
++(*_pcount);
_pmtx->unlock();
}void Release()
{
bool flag = false;
_pmtx->lock();
if (--(*_pcount) == 0)
{
delete _pcount;
//delete _ptr;
_del(_ptr);
flag = true;
}
_pmtx->unlock();
if (flag == true) delete _pmtx;
}//sp1 = sp1;
//sp1 = sp2;//sp2如果是sp1的拷贝呢?
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)//资源地址不一样
{
Release();
_pcount = sp._pcount;
_ptr = sp._ptr;
_pmtx = sp->_pmtx;_pmtx->lock();
++(*_pcount);
_pmtx->unlock();
}return *this;
}// 像指针一样
T& operator*()
{
return *_ptr;
}T* operator->()
{
return _ptr;
}T& operator[](size_t pos)
{
return _ptr[pos];
}int use_count() const
{
return *_pcount;
}T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
mutex* _pmtx;D _del;
};
shared_ptr循环引用的引用问题
struct ListNode
{
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr2()
{
std::shared_ptr<ListNode> n1 (new ListNode);
std::shared_ptr<ListNode> n2 (new ListNode);n1->_next = n2;
n2->_prev = n1;
}
两块内存将不会在智能智能指针生命周期结束的时候进行释放
由于_next与_prev也进行了引用,引用计数将会变成2,此时若要释放n1,则需要释放n2中的_prev,而释放n2中的_prev就得释放n2;但是释放n2,又需要释放n1中的_next,而释放n1中的_next,就得释放n1。如此进入了一个死循环。因此n1与n2彼此相互只应该链接一次,防止出现循环引用的问题
weak_ptr
主要用来解决shared_ptr循环引用的问题,支持用shared_ptr对象来构造weak_ptr对象,管理的是同一个资源但不会增加这块资源的引用计数
struct ListNode
{
std::weak_ptr<ListNode> _prev;
std::weak_ptr<ListNode> _next;~ListNode()
{
cout << "~ListNode()" << endl;
}
};void test_shared_ptr2()
{
std::shared_ptr<ListNode> n1 (new ListNode);
std::shared_ptr<ListNode> n2 (new ListNode);n1->_next = n2;
n2->_prev = n1;cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
}
weak_ptr的简单实现
支持用shared_ptr对象拷贝构造给weak_ptr对象,构造时获取shared_ptr对象管理的资源;支持shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取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<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
public:
T* _ptr;
};