文章目录
-
- 为什么需要智能指针
- unique_ptr
-
- unique_ptr常用操作
- shared_ptr
-
- share_ptr常用的操作
- 引用计数变化的情况
- move语法
- 使用注意
- 弊端:循环引用
- weak_ptr
-
- weak_ptr常用的操作
- 如何选择智能指针
为什么需要智能指针
程序使用动态内存
出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
而在C++中动态内存
的管理是通过一对运算符来完成的,即new
和delete
,但是动态内存的使用很容易产生以下问题:
-
忘记delete内存,导致内存泄露问题,而且这种问题很难排查,往往出现在程序运行阶段而不是编译阶段。
-
使用已释放的对象。
通过释放内存后,将指针置为空,有时可以检测出这种错误
-
同一块内存被释放两次。当两个指针指向相同的动态分配对象时,如果其中一个指针进行了delete操作,对象的内存就被归还给自由空间了,如果我们随后又delete第二个指针,自由空间就可能被破坏。
所以为了使开发人员能够更容易更安全的使用动态内存,就需要智能指针,智能指针的使用类似于指针,最主要的区别是它负责自动释放所指向的对象,目前有3种智能指针,分别是: -
shared_ptr
:允许多个指针指向同一个对象 -
unique_ptr
:独占所指向的对象 -
weak_ptr
:弱引用,指向shared_ptr所管理的对象
STL一共提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr
auto_ptr功能类似于unique_ptr,但是其在C++11中已经废弃,被unique_ptr替代,原因是auto_ptr不够安全,且支持拷贝和赋值,如p2= p1,p2会接管p1原来的内存管理权,p1会变为空指针,如果p2原来不为空,则它会释放原来的资源,基于这个原因,应该避免将auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL。
在C++17中auto_ptr已被废除。
unique_ptr
一个unique_ptr拥有
它所指向的对象。某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它指向的对象也被销毁。
由于unique_ptr
独占
的特点,所以unique_ptr不支持普通的拷贝或者赋值操作(拷贝或赋值一个将要被销毁的unique_ptr例外,比如函数返回一个unique_ptr)
,定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且也必须使用直接初始化方式例如:unique_ptrp(new int(10))
unique_ptr常用操作
-
u.release()
:u放弃对指针的控制权,返回内置指针
(即new出来的指针),并将u置为空class A { public:~A() { cout << "~A()" << endl;} }; int main() { unique_ptr<A>p1(new A);//错误:release函数只是将p1置为空,并不会释放其指向的内存。而运行后并没有任何输出,说明没有调用A的析构函数,也说明release函数不会释放其指向的内存。p1.release();return 0; }
注意:该操作通常用来初始化另一个智能指针,如果我们不用另一个智能指针保存其返回的指针,我们的程序就要负责资源的释放
-
u.reset()
:释放u指向的对象 -
u.reset(q)
:令u指向q,q必须是内置指针;
通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr;
int main()
{
unique_ptr<int>p1(new int(10));unique_ptr<int>p2(p1.release()); //release将p1置空,并指针的控制权交给p2p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);p2.get() ? (cout << "p2的值为:" << *p2.get() << endl) : (cout << "p2为空指针" << endl);unique_ptr<int>p3(new int(20));p2.reset(p3.release());//将所有权从p3转交给p2,并释放原先p2指向的对象p2.get() ? (cout << "p2的值为:" << *p2.get() << endl) : (cout << "p2为空指针" << endl);p3.get() ? (cout << "p3的值为:" << *p3.get() << endl) : (cout << "p3为空指针" << endl);return 0;
}
输出结果为:
shared_ptr
与一个对象只能被一个unique_ptr拥有不同,对象可被多个shared_ptr共享,在每个shared_ptr对象中,都有一个计数器,我们通常称为引用计数
,该引用计数会记录有多少个其他shared_ptr指向相同的对象。
对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,即引用计数不为0,就不会被释放掉,相反,一旦一个shared_ptr对象的引用计数变为0,它就会自动销毁所管理的对象并自动释放相关联的内存。
因此通过该引用计数从而允许多个指针共享一个对象。
share_ptr常用的操作
make_shared<T>(args)
: 返回一个shared_ptr,指向一个动态分配的类型为T的对象。使用args初始化对象。该函数也是最安全的分配和使用动态内存的方法。shared_ptr<T>p(q)
:p是shared_ptr q的拷贝
。p.use_count()
:返回与p共享对象的智能指针数量;p.get()
:返回p中保存的指针。要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了,且返回的是内置指针。p.reset()
:若p是唯一指向其对象的shared_ptr,reset会释放此对象
注意,我们不能将一个内置指针隐式转换为一个智能制造,所以shared_ptr必须使用
直接初始化形式
,即shared_ptrp1(new int(1024)); 正确
!!!shared_ptrp1=new int(1024); 错误(拷贝初始化)
引用计数变化的情况
情况1
:用一个shared_ptr初始化另一个shared_ptr,引用计数增加
情况2
:shared_ptr作为参数传给一个函数,引用计数增加
情况3
:shared_ptr作为函数返回值,引用计数增加
情况4
:shared_ptr的赋值操作,赋值号右边的shared_ptr引用计数增加,左侧的shared_ptr的引用计数减小
情况5
:shared_ptr离开了其作用域,引用计数减小
void func1(shared_ptr<int>ptr) {
cout << "p1作为参数传递给函数func1后,p1的引用计数:" << ptr.use_count() << endl;
}shared_ptr<int>func2(shared_ptr<int>ptr) {
return ptr;
}
int main()
{
shared_ptr<int>p1 = make_shared<int>(10); //p1指向的int只有一个引用者cout << "p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况1" << endl;shared_ptr<int>p2(p1);//p1和p2都指向相同对象,cout << "p1的引用计数:" << p1.use_count() << endl;cout << "p2的引用计数:" << p2.use_count() << endl;cout << "=============================================" << endl;cout << "情况2" << endl;func1(p1);//拷贝p1会增加它的引用计数,在func1中引用计数为3cout << "程序执行完函数func1后,p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况3" << endl;{
shared_ptr<int>p3 = func2(p1);cout << "shared_ptr作为函数返回值给p3后,p1的引用计数:" << p1.use_count() << endl;}cout << "p3离开其作用域后,p1的引用计数:" << p1.use_count() << endl;cout << "=============================================" << endl;cout << "情况4" << endl;shared_ptr<int>p4 = make_shared<int>(20);shared_ptr<int>p5(p4);cout << "被赋值前p4的引用计数:" << p4.use_count() << endl;cout << "被赋值前p1的引用计数:" << p1.use_count() << endl;p5 = p1;cout << "被赋值后p4的引用计数:" << p4.use_count() << endl;cout << "被赋值后p1的引用计数::" << p1.use_count() << endl;cout << "=============================================" << endl;
}
输出结果:
从情况1中我们其实可以看到,当某个对象的引用计数增加时,所有指向该对象的shared_ptr的引用计数都会增加
从内存角度
在程序执行的过程中,p1、p2、ptr、p3、p5都指向过同一块内存单元,但是当执行到p4的时候,由于此时ptr和p3都已经不再其作用域,所以不再指向内存单元。从图中ptr和p3变灰可以看出。
上面的动态图中,在监视窗口的值中,
strong refs
表示当前有多少指针指向同一个对象,当该值增加或减少时,会同时更改与该shared_ptr指向相同对象的其他shared_ptr的strong refs
当引用计数为0时,即指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁次对象,并释放它所占的内存
int main()
{
shared_ptr<int>p1 = make_shared<int>(10); //p1指向的int只有一个引用者shared_ptr<int>p2 = make_shared<int>(20);p1 = p2;return 0;
}
可以看到执行完p1=p2后,原先p1所指向的对象被销毁
move语法
将一个shared_ptr所指向对象的所有权转交给另一个shared_ptr,并将原先的shared_ptr置空;
int main()
{
shared_ptr<int>p1=make_shared<int>(10);p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);shared_ptr<int>p2 = move(p1);p1.get() ? (cout << "p1的值为:" << *p1.get() << endl) : (cout << "p1为空指针" << endl);cout << "p1的引用计数:" << p1.use_count() << endl;cout << "p2的引用计数:" << p2.use_count() << endl;return 0;
}
可以看到执行完move操作后,p1的指针被置空,其所指向的对象所有权交付给了p2
输出结果为:
使用注意
- 不要混合使用普通指针和智能指针
void process(shared_ptr<int>ptr) {
}
int main()
{
int *x = new int(10);process(shared_ptr<int>(x));//虽然不会出错,但是执行完后,内存会被释放int val = *x; //此时x是一个空悬指针,结果未知return 0;
- 永远不要用get初始化另一个只能指针或者为另一个智能指针赋值
int main()
{
shared_ptr<int>p1 = make_shared<int>(10); {
shared_ptr<int>p2(p1.get());cout << "p1的引用计数:"<<p1.use_count() << endl;//输出为1cout << "p2的引用计数:"<<p2.use_count() << endl;//输出为1}return 0;
}
从上图可以看到虽然p1和p2指向相同的内存,但是它们是相互独立的,因为各自的引用计数都是1,当p2所在的程序块结束时,p2被销毁,这会导致p1所指向的内存被释放,从而p1变成空悬指针
弊端:循环引用
引用计数虽然允许多个shared_ptr指向同一个对象,但是会带来循环引用
的问题,循环引用会导致堆里的内存无法正常回收,造成内存泄露。
weak_ptr
weak_ptr是不控制所指向对象生命周期的智能指针,也就是说即使有weak_ptr指向对象,对象还是会被释放,且weak_ptr指向由shared_ptr管理的对象,将weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,当所有shared_ptr析构了之后,不管有没有weak_ptr引用该内存,内存还是会被释放,从而可以有效解决循环引用的问题。
class B;
class A
{
public:shared_ptr<B>pA;~A() {
cout << "~A()" << endl;}
};
class B
{
public:shared_ptr<A>pB;~B() {
cout << "~B()" << endl;}
};
class BW;
class AW
{
public:shared_ptr<BW>pA;~AW() {
cout << "~AW()" << endl;}
};
class BW
{
public:weak_ptr<AW>pB;~BW() {
cout << "~BW()" << endl;}
};
void Test1() {
shared_ptr<A>ta(new A());shared_ptr<B>tb(new B());cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;ta->pA = tb;tb->pB = ta;cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;
}
void Test2() {
shared_ptr<AW>ta(new AW());shared_ptr<BW>tb(new BW());cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;ta->pA = tb;tb->pB = ta;cout << "ta的引用计数:" << ta.use_count() << endl;cout << "tb的引用计数:" << tb.use_count() << endl;
}
int main()
{
Test1();cout << "=======================" << endl;Test2();return 0;
}
输出结果为:
可以看到在Test1()中,pA和pB存在循环引用的问题,在调用完Test1()后,pA和pB并没有正常释放,因为没有调用相应的析构函数;而当采用weak_ptr后,有效解决了这个问题,而从Test2()中ta的引用计数为1,tb的引用计数为,也验证了weak_ptr具有不会改变shared_ptr的引用计数的特点。
weak_ptr常用的操作
w.reset()
:将w置空w.use_count()
:与w共享对象的shared_ptr的数量w.expired()
:若w.use_count()为0,返回true,否则返回falsew.lock()
:如果expired为true,返回一个空shared_ptr;否则返回一个指向w的对象的shared_ptr
由于weak_ptr不会改变shared_ptr的引用计数,所以weak_ptr所指向的对象可能被释放掉,所以我们不能使用weak_ptr直接访问对象,而必须调用lock(),从而检查weak_ptr指向的对象是否仍然存在,如果存在,则返回一个指向共享对象的shared_ptr.
如何选择智能指针
- 如果程序要使用多个指向同一个对象的指针,应选择 shared_ptr。这样的情况包括:
- 将指针作为参数或者函数的返回值进行传递的话,应该使用 shared_ptr;
- 两个对象都包含指向第三个对象的指针,此时应该使用 shared_ptr 来管理第三个对象;
- STL 容器包含指针。很多 STL 算法都支持复制和赋值操作,这些操作可用于 shared_ptr,但不能用于 unique_ptr(编译器发出 warning)
- 如果程序不需要多个指向同一个对象的指针,则可使用 unique_ptr。
- 为了解决 shared_ptr 的循环引用问题,应该使用 weak_ptr。
参考:
《C++ Primer (第5版)中文版》
C++ STL 四种智能指针的用法详解
c++ 智能指针用法详解