std::auto_ptr
std::auto_ptr指针在C++11标准中被移除了,可以使用unique_ptr来代替,功能上是相同的,unique_ptr相比较auto_ptr而言,提升了安全性(没有浅拷贝),增加了特性(delete析构)和对数组的支持。
这个类模版提供了有限的垃圾回收机制,通过将一个指针保存在auto_ptr对象中,当auto_ptr对象析构时,这个对象所保存的指针也会被析构掉。
auto_ptr对象拥有其内部指针的所有权。这意味着auto_ptr对其内部指针的释放负责,既当自身被释放时,会在析构函数中自动的调用delete,从而释放内部指针的内存。
解释:
- 正因为如此,不能有两个auto_ptr对象拥有同一个内部指针的所有权,因为有可能在某个时机,两者均会尝试析构这个内部指针。
- 当两个auto_ptr对象之间发生赋值操作时,内部指针被拥有的所有权会发生转移,这意味着这个赋值的右者对象会丧失该所有权,不在指向这个内部指针(其会被设置成null指针)。
到这里,我们来看一下auto_ptr的提供的接口和使用方法:
- constructor
- destructor
- get
- operator*
- operator->
- operator=
- release
- reset
- conversion operators
其中构造只的说一下:
- std::auto_ptr::auto_ptr
解释:auto_ptr的构造的参数可以是一个指针,或者是另外一个auto_ptr对象。
- 当一个新的auto_ptr获取了内部指针的所有权后,之前的拥有者会释放其所有权
- auto_ptr的构造及所有权的转移
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
// 通过指针进行构造
std::auto_ptr<int> aptr1(new int(3));
std::printf("aptr1 %p : %d\r\n", aptr1.get(), *aptr1);
// 这样会编译出错,因为auto_ptr的构造有关键字explicit
// explicit 关键字表示调用构造函数是不能使用隐式赋值,而必须是显示调用
// std::auto_ptr<int> aptr2 = new int(3);
// 可以用其他的auto_ptr指针进行初始化
std::auto_ptr<int> aptr2(new int(44));
std::printf("aptr2 %p : %d\r\n", aptr2.get(), *aptr2);
std::printf("===========================\n");
// aptr2 会被释放
aptr1 = aptr2;
std::printf("aptr1 %p : %d\r\n", aptr1.get(), *aptr1);
// 内存访问出错,直接0xc05,因为aptr2已经释放了其所有权
// std::printf("aptr2 %p : %d\r\n", aptr2.get(), *aptr2);
return 0;
}
- auto_ptr析构及资源的自动释放
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int * pNew = new int(3);
{
std::printf("pNew:%d\n", *pNew);
//当释放aptr时,pNew 也会被释放了(注意,要出作用域才释放)
std::auto_ptr<int> aptr(pNew);
std::printf("aptr:%d\n", *aptr);
}
std::printf("pNew:%d\n", *pNew);
return 0;
}
- 这里显然,当出了块作用域之后,aptr对象会自动调用析构,然后在析构中会自动的delete其内部指针,也就是出了这个作用域后,其内部指针就被释放了。
- 当然上面这种写法是不推荐的,因为我们这里本质上就是希望不去管理指针的释放工作,上面的写法就又需要成员自己操心指针的问题,也就是使用智能指针要避免出现指针的直接使用!
在这里可以使用前调用release,从而放弃其内部指针的使用权,但是同样这么做违背了智能指针的初衷。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int * pNew = new int(3);
{
std::printf("pNew:%d\n", *pNew);
std::auto_ptr<int> aptr(pNew);
// aptr 放弃其内部指针的使用权
int* p = aptr.release();
// std::printf("aptr:%d\n", *aptr);
}
std::printf("pNew:%d\n", *pNew);
return 0;
}
- 分配新的指针所有权
- 可以调用reset来重新分配指针的所有权,reset中会先释放原来的内部指针的内存,然后分配新的内部指针。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int * pNew = new int(3);
{
std::printf("point:%p, pNew:%d\n", pNew, *pNew);
std::auto_ptr<int> aptr(pNew);
std::printf("point:%p, aptr:%d\n", aptr.get(), *aptr);
// aptr 重新分配指针的所有权, pNew的内容被清除
aptr.reset(new int(555));
std::printf("====================\n");
std::printf("point:%p, aptr:%d\n", aptr.get(), *aptr);
std::printf("point:%p, pNew:%d\n", pNew, *pNew);
}
std::printf("====================\n");
std::printf("point:%p, pNew:%d\n", pNew, *pNew);
return 0;
}
- = 运算符的使用
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
{
std::auto_ptr<int> p1;
std::auto_ptr<int> p2;
p1 = std::auto_ptr<int>(new int(33));
std::printf("point:%p, pNew:%d\n", p1.get(), *p1);
*p1 = 4;
std::printf("point:%p, pNew:%d\n", p1.get(), *p1);
// p1 会被释放
p2 = p1;
// std::printf("point:%p, pNew:%d\n", p1, *p1);
std::printf("point:%p, pNew:%d\n", p2, *p2);
}
return 0;
}
auto_ptr存在的问题
为什么C11标准会不让使用auto_ptr,原因是其使用有问题。
作为参数传递会存在问题
- 因为有拷贝构造函数和赋值的情况下,会释放原有的对象的内部指针,所有当有函数使用的是auto_ptr时,调用后会导致原来的内部指针释放。
#include <iostream>
#include <memory>
void func_test(std::auto_ptr<int> p){
std::printf("point:%p, p:%d \n", p.get(), *p);
}
int main(int argc, char const *argv[]){
std::auto_ptr<int> p1 = std::auto_ptr<int>(new int(44));
func_test(p1);
// 这里的调用会出错,因为拷贝构造函数的存在,p1实际上已经释放了其内部指针的所有权了
// std::printf("point:%p, p1:%d \n", p1.get(), *p1);
return 0;
}
不能使用vector数组
#include <iostream>
#include <memory>
#include <vector>
int main(int argc, char const *argv[]){
std::vector<std::auto_ptr<int>> ary;
std::auto_ptr<int> p(new int(33));
// 用不了
ary.push_back(p);
return 0;
}
unique_ptr
前面我们讲解了auto_ptr的使用及为什么会被C++11标准抛弃,接下来,我们学习unique_ptr的使用
unique_ptr提供一下操作:
- (constructor)
- (destructor)
- operator=
- get
- get_deleter
- operator bool
- release
- reset
- swap
- operator*
- operator->
- operator[]
- 析构函数
从根源杜绝了auto_ptr作为参数传递的写法
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
// 这样构造是可以的
std::unique_ptr<int> p1(new int(3));
// 空构造也是可以的
std::unique_ptr<int> p2;
// 下面三种写法会报错
// std::unique_ptr<int> p3 = p1; // 需要拷贝构造
// std::unique_ptr<int> p4(p1); // 需要拷贝构造
// p2 = p1; // 需要=运算符重载
return 0;
- reset
reset的用法和auto_ptr是一致的
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int *pNew = new int(4);
int *p = new int(1);
{
std::printf("pNew:%d, p:%d\n", *pNew, *p);
// pNew 会被释放
std::unique_ptr<int> uptr(pNew);
uptr.reset(p);
// *p = 444;
std::printf("pNew:%d, p:%d, uptr:%d \n", *pNew, *p, *uptr);
// uptr 出作用域会被释放,p也会被置空,但p指针没有被释放
}
//指针没有被释放
*p = 99;
std::printf("pNew:%d, p:%d\n", *pNew, *p);
return 0;
}
- release
release与reset一样,也不会释放原来的内部指针,只是简单的将自身置空。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int *pNew = new int(7);
int *p = NULL;
{
std::unique_ptr<int> uptr(pNew);
std::printf("pNew point:%p, v:%d\n", pNew, *pNew);
std::printf("uptr point:%p, v:%d\n", uptr.get(), *uptr);
p = uptr.release();
std::printf("pNew point:%p, v:%d\n", pNew, *pNew);
// 报错 uptr 被置空
// std::printf("pNew point:%p, v:%d\n", uptr.get(), *uptr);
std::printf("p point:%p, v:%d\n", p, *p);
}
std::printf("pNew point:%p, v:%d\n", pNew, *pNew);
std::printf("p point:%p, v:%d\n", p, *p);
return 0;
}
- move
但是多了个move的用法
因为unique_ptr不能将自身对象内部指针直接赋值给其他unique_ptr,所以这里可以使用std::move()函数,让unique_ptr交出其内部指针的所有权,而自身置空,内部指针不会释放。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int *p = new int(99);
std::unique_ptr<int> uptr(p);
std::printf("uptr point:%p, v:%d\n", uptr.get(), *uptr);
std::unique_ptr<int> uptr2 = std::move(uptr);
std::printf("p point:%p, v:%d\n", p, *p);
// 报错 uptr 被置空
// std::printf("uptr point:%p, v:%d\n", uptr.get(), *uptr);
std::printf("uptr2 point:%p, v:%d\n", uptr2.get(), *uptr2);
return 0;
}
- 数组
可以采用move的方式来使用数据。
直接使用仍然会报错:
#include <iostream>
#include <memory>
#include <vector>
int main(int argc, char const *argv[]){
std::vector<std::unique_ptr<int>> Ary;
std::unique_ptr<int> p(new int(3));
// 报错,不能直接赋值
// Ary.push_back(p);
Ary.push_back(std::move(p));
// 报错 p已被置空
// std::printf("p:%d\n", *p);
return 0;
}
shareed_ptr与weak_ptr
shared_ptr是带引用计数的智能指针
1、构造
其初始化多了一个写法:std::make_shared
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int *p = new int(11);
std::shared_ptr<int> sptr1(p);
std::shared_ptr<int> sptr2(new int(5));
std::shared_ptr<int> sptr3 = sptr2;
std::shared_ptr<int> sptr4 = std::make_shared<int>(4);
std::printf("sptr1 point:%p, v:%d\n", sptr1.get(), *sptr1);
std::printf("sptr2 point:%p, v:%d\n", sptr2.get(), *sptr2);
std::printf("sptr3 point:%p, v:%d\n", sptr3.get(), *sptr3);
std::printf("sptr4 point:%p, v:%d\n", sptr4.get(), *sptr4);
return 0;
}
这里显然可以看到有引用计数的存在
通过修改上面例子种的是sptr3的作用域之后,shared_ptr对应的引用计数的值减少了。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
std::shared_ptr<int> sptr2(new int(5));
{
std::shared_ptr<int> sptr3 = sptr2;
*sptr3 = 9;
std::printf("sptr2 point:%p, v:%d\n", sptr2.get(), *sptr2);
std::printf("sptr3 point:%p, v:%d\n", sptr3.get(), *sptr3);
}
std::printf("sptr2 point:%p, v:%d\n", sptr2.get(), *sptr2);
return 0;
}
注意事项
- 如果用同一个指针去初始化两个shared_ptr时,则引用计数仍然会出错:
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
int *p = new int(44);
{
std::shared_ptr<int> sptr1(p);
{
// 运行报错,指针p 会被释放
std::shared_ptr<int> sptr2(p);
}
// 出作用域,会释放指针p,会导致p被重复释放
}
return 0;
}
显然出了最里面的作用域之后,sptr2对象就已经释放了,此时,对于sptr2来说,p的引用计数为0,所以p被释放,但是实际上sptr1还存在,所以再释放sptr1时,就会报错。
#include <iostream>
#include <memory>
int main(int argc, char const *argv[]){
{
std::shared_ptr<int> sptr1(new int(5));
{
std::shared_ptr<int> sptr2(sptr1);
*sptr2 = 9;
std::printf("sptr2 point:%p, v:%d\n", sptr2.get(), *sptr2);
}
std::printf("sptr2 point:%p, v:%d\n", sptr1.get(), *sptr1);
}
return 0;
}
share_ptr最多的问题时存在循环引用的问题:
如果两个类的原始指针的循环使用,那么会出现重复释放的问题。
这里,delete pSon会出现循环调用父子类的析构函数,问题很大。
#include <iostream>
#include <memory>
class CSon;
class CPerson;
class CPerson{
public:
CPerson(){}
void Set(CSon *pSon){
m_pson = pSon;
}
~CPerson(){
if(m_pson != nullptr){
delete m_pson;
m_pson = nullptr;
}
}
public:
CSon *m_pson;
};
class CSon{
public:
CSon(){}
void Set(CPerson *pParent){
m_pParent = pParent;
}
~CSon(){
if(m_pParent != nullptr){
delete m_pParent;
m_pParent = nullptr;
}
}
public:
CPerson *m_pParent;
};
int main(int argc, char const *argv[]){
CPerson *pPer = new CPerson();
CSon *pSon = new CSon();
pPer->Set(pSon);
pSon->Set(pPer);
// delete pSon;
return 0;
}
因此,这里考虑使用引用计数的shared_ptr来实现。
#include <iostream>
#include <memory>
class CSon;
class CPerson;
class CPerson{
public:
CPerson(){}
void Set(std::shared_ptr<CSon> pSon){
m_pson = pSon;
}
~CPerson(){}
public:
std::shared_ptr<CSon> m_pson;
};
class CSon{
public:
CSon(){}
void Set(std::shared_ptr<CPerson> pParent){
m_pParent = pParent;
}
~CSon(){}
public:
std::shared_ptr<CPerson> m_pParent;
};
int main(int argc, char const *argv[]){
// CPerson *pPer = new CPerson();
// CSon *pSon = new CSon();
// 循环的引用,会出现析构异常
// weak_ptr 弱指针
{
// std::shared_ptr<CPerson> shared_parent(pPer);
// std::shared_ptr<CSon> shared_son(pSon);
std::shared_ptr<CPerson> shared_parent(new CPerson());
std::shared_ptr<CSon> shared_son(new CSon());
shared_parent->Set(shared_son);
shared_son->Set(shared_parent);
std::printf("pPer: %d\n", shared_parent.use_count());
std::printf("pSon: %d\n", shared_son.use_count());
}
return 0;
}
这里在出作用域后发现,实际上两个对象均为被销毁
最后两者的引用计数均为1,原因是除了块作用域之后,两个shared_parent和shared_son均会析构,在这两个智能指针的内部,均会先去判断对应的内部指针引用次数-1是否为0,显然,这里相互引用的情况下,引用次数初值为2,减1后值为1,所以两个指针均不会被释放。
这里,其实只需要一个释放了,另外一个也能跟着释放,可以采用弱指针,即认为的迫使其中一个引用计数为1,从而打破闭环。
这里只需要将上例子中的任意一个强指针改为弱指针即可。
#include <iostream>
#include <memory>
class CSon;
class CPerson;
class CPerson{
public:
CPerson(){}
void Set(std::shared_ptr<CSon> pSon){
m_pson = pSon;
}
~CPerson(){}
public:
std::shared_ptr<CSon> m_pson;
};
class CSon{
public:
CSon(){}
void Set(std::shared_ptr<CPerson> pParent){
m_pParent = pParent;
}
~CSon(){}
public:
std::weak_ptr<CPerson> m_pParent;
};
int main(int argc, char const *argv[]){
// CPerson *pPer = new CPerson();
// CSon *pSon = new CSon();
// 循环的引用,会出现析构异常
// weak_ptr 弱指针
{
// std::shared_ptr<CPerson> shared_parent(pPer);
// std::shared_ptr<CSon> shared_son(pSon);
std::shared_ptr<CPerson> shared_parent(new CPerson());
std::shared_ptr<CSon> shared_son(new CSon());
shared_parent->Set(shared_son);
shared_son->Set(shared_parent);
std::printf("pPer: %d\n", shared_parent.use_count());
std::printf("pSon: %d\n", shared_son.use_count());
}
return 0;
}
最后的结果,此时,两个内部指针均会得到释放。
强弱指针计数器增减分析
前4个字节是虚表指针
中间两个4字节分别是内部对象指针计数器和自身的计数其
最后4字节是内部对象指针。
到这里就shared_ptr与weak_ptr的代码分析的差不多了
最后说一下计数器增减的规则
初始化及增加的情形
- 当创建一个新的shared_ptr时,内部对象计数器和自身的计数器均置1
- 当将另外一个shared_ptr赋值给新的shared_ptr时,内部对象计数器+1,自身计数器不变。
- 当将另外一个shared_ptr赋值给新的weak_ptr时,内部对象计数器不变,自身计数器+1.
- 当从weak_ptr获取一个shared_ptr时,内部对象计数器+1,自身计数器不变。
减少的情形
- 当一个shared_ptr析构时,内部对象计数器-1,当内部对象计数器减为0时,则释放内部对象,并将自身计数器-1
- 当一个weak_ptr析构时,自身计数器-1,当自身计数器减为0时,则释放自身_Ref_count* 对象
那么就可以自己来模拟强弱指针,并修改成模版。
思考
- 强指针构造,析构,=赋值,拷贝构造等情况下,计数器的变化
- 弱指针构造,析构,=赋值,拷贝构造等情况下,计数器的变化
- 弱指针提升为强指针时,计数器的变化
强指针直接构造(拿原始指针构造)时
- 初始化_Ty *_Ptr
- 创建_Ref_count对象
- _Ref_count_base对象构造时,分别为_Uses = 1 并且 _Weaks = 1