使用指针有时候是比较危险的,可能存在空指针,野指针问题,并造成内存泄漏的问题。可是指针又非常的高效,所以我们希望以更安全的方式来使用指针:
- 使用更安全的指针——智能指针
- 不使用指针,使用安全的方式——引用
C++中推出了四种常用的智能指针:unique_ptr、shared_ptr、weak_ptr和C++11中已经废弃的auto_ptr,auto_ptr在C++17中被正式删除。
要记住,智能指针和指针一样都是栈中的变量,它们指向堆区中的内容。
auto_ptr
由new expression获得对象,它假设我们的对象是在栈空间中创建,所以在auto_ptr对象销毁时(auto_ptr跳出作用域),他所管理的对象也会被自动delete掉。
所有权转移:当我们用auto_ptr指向一个对象,而不小心用另一个auto_ptr指向同一个对象,那么这个时候原来的auto_ptr就会失效,不再拥有这个对象,而是指向nullptr。这是在拷贝/赋值的过程中把指针对原对象的内存控制权剥夺的。所以这让我们很难管理程序,会造成混乱。
#include<string>
#include<iostream>
#include<memory> //智能指针头文件
using namespace std;
int main()
{
{ //使用大括号是为了确定auto_ptr的范围,出了大括号,auto_ptr将失效
//对基本类型int使用auto_ptr
auto_ptr<int> pl(new int(10));
cout<<*pl<<endl;
//对string数组使用auto_ptr
auto_ptr<string> language[5] = {
auto_ptr<string>(new string("C")),
auto_ptr<string>(new string("Java")),
auto_ptr<string>(new string("C++")),
auto_ptr<string>(new string("Python")),
auto_ptr<string>(new string("Rust")),
};
auto_ptr<string> pC;
pC = language[2];//这个时候language[2]所有权转移,language[2]不再引用该字符串而是变成空指针nullptr
} //出了作用域,智能指针被析构
return 0;
}
unique_ptr
auto_ptr是为了解决我们在堆空间中创建对象忘记释放的问题,它可以自动释放对象。但是问题在于auto_ptr与对象之间耦合性过于强壮,auto_ptr被销毁时它指向的对象也会被销毁,而且存在所有权转移的问题。所以我们基本不再使用了。
unique_ptr是专属所有权,所以unique_ptr管理的内存,只能被一个对象持有,不支持赋值和复制。
移动语义:unique_ptr禁止了拷贝语义,但是有时候我们也会需要能够转移所有权,于是提供了移动语义,即可以使用std::move()
进行控制所有权地方转移。
#include<string>
#include<iostream>
#include<memory> //智能指针头文件
using namespace std;
int main()
{
auto w = std::make_unique<int>(10); //使用make_unique方法获取unique_ptr
cout<< *(w.get()) <<endl; //get方法返回一个对象
//auto w2 = w; //编译错误,unique_ptr不支持赋值语句
//unqiue_ptr只支持移动语义
auto w2 = std::move(w); //w2获得内存所有权,此时w等于Nullptr
cout << (w.get() != nullptr) ? (*w.get() : -1) <<endl; //输出-1
cout << (w2.get() != nullptr) ? (*w2.get() : -1) <<endl; //输出10
return 0;
}
shared_ptr和weak_ptr
auto_ptr和unique_ptr有一个缺陷,同一时刻一个对象只能有一个智能指针指向它,这就带来了局限性。我们需要让对象可以共享访问。
shared_ptr通过一个引用计数共享一个对象。它是为了解决auto_ptr在对象所有权上的局限性。在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销。
当引用计数为0 时,该对象没有被使用,可以进行析构。
引用计数也会带来一些副作用,比如循环引用的问题,导致堆内的内存无法正常回收,造成内存泄漏。如下图,有A和B两个对象,各含有一个shared_ptr pA和pB。某一时刻pA指向的对象是pB,而pB指向的对象是pA。当A对象想要释放自己的时候,由于释放自己要释放自己内部的所有内容,因为内部智能指针指向B,那么就需要释放B,而B要释放就要释放pB指向的A对象,这样就形成了一个环结构,相互指向无法释放。
为了避免循环引用,weak_ptr被设计与shared_ptr共同工作,用一种观察者模式工作。它协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着weak_ptr只对shared_ptr进行引用,而不改变其引用计数,当被观察的shared_ptr失效后,相应的weak_ptr也失效。
由下图可知,A中有一个shared_ptr指向B,当B对象也需要指针指向A的时候,我们不需要声明shared_ptr,而是声明一个weak_ptr,它不保有引用计数。但是shared_ptr失效后,这个weak_ptr也失效。
#include<iostream>
#include<memory>
using namespace std;
int main()
{
{
//shared_ptr代表的是共享所有权,即多个shared_ptr可以共享同一块内存
auto wA = shared_ptr<int>(new int(20));
{
auto wA2 = wA;
cout<<((wA2.get() != nullptr) ? (*wA2.get()) : -1) <<endl; //20
cout<<((wA.get() != nullptr) ? (*wA.get()) : -1) <<endl; //20
cout<<wA2.use_count()<<endl; //引用计数,2
cout<<wA.use_count()<<endl; //2
}
cout<<wA.use_count()<<endl; //跳出作用域,wA2消亡,此时引用计数为1
}
//跳出作用域,引用计数为0,销毁指针
//move语法
auto wAA = std::make_shared<int>(30);
auto wAA2 = std::move(wAA); //此时wAA等于nullptr,wAA2.use_count()=1
return 0;
}
#include<string>
#include<iostream>
#include<memory>
using namespace std;
struct B;
struct A{
shared_ptr<B> pb;
~A()
{
cout<<"~A()"<<endl;
}
};
struct B{
shared_ptr<A> pa;
~B()
{
cout<<"~B()"<<endl;
}
};
//以上pa和pb存在循环引用,根据shared_ptr的引用计数原理,pa和pb都无法被正常释放
struct BW;
struct AW{
shared_ptr<BW> pb;
~AW()
{
cout<<"~AW()"<<endl;
}
};
struct BW{
weak_ptr<AW> pa;
~BW()
{
cout<<"~BW()"<<endl;
}
};
void Test()
{
cout<<"Test shared_ptr and shared_ptr: "<<endl;
shared_ptr<A> tA(new A());
shared_ptr<B> tB(new B());
cout<<tA.use_count()<<endl; //1
cout<<tB.use_count()<<endl; //1
tA->pb = tB;
tB->pa = tA;
cout<<tA.use_count()<<endl; //2
cout<<tB.use_count()<<endl; //2
} //函数结束,两个对象的堆空间无法被释放
void Test2()
{
cout<<"Test weak_ptr and shared_ptr: "<<endl;
shared_ptr<AW> tA(new AW());
shared_ptr<BW> tB(new BW());
cout<<tA.use_count()<<endl; //1
cout<<tB.use_count()<<endl; //1
tA->pb = tB;
tB->pa = tA; //不参与引用计数,tA引用计数不增加
cout<<tA.use_count()<<endl; //1
cout<<tB.use_count()<<endl; //2
} //函数结束,两个对象的堆空间被释放