智能指针
2 C++11特性--智能指针
2.1 为什么会出现智能指针
对学C++的朋友而言,要数最令人头疼的问题莫过于指针了。
当你在堆上创建了一个对象时,系统就把这个对象的生命期完全交给了你,当用完之后,系统并不会回收资源,而是需要你来释放它。
那么,既然要负责对象的释放问题,就要知道什么时候释放和在哪里释放。如果你没有处理好这两个问题,就会造成内存泄漏或程序崩溃的问题。
智能指针(smart pointer)其实不是一个指针。它就是用来帮助我们管理指针,维护其生命周期的类。c++引入 智能指针 ,智能指针即是C++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略。 智能指针在 头文件的 std 命名空间中定义。头文件:#include<memory>
2.1.1 传统指针存在的问题
- 需要手动管理内存
- 容易发生内存泄露(忘记释放、出现异常等)
- 释放之后产生野指针
2.1.2 内存泄露
内存泄露在维基百科中的解释如下:
在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。
在C++中出现内存泄露的主要原因就是程序猿在申请了内存后(malloc(), new),没有及时释放没用的内存空间,甚至消灭了指针导致该区域内存空间根本无法释放。
知道了出现内存泄露的原因就能知道如何应对内存泄露,即:不用了的内存空间记得释放!
内存泄漏可能会导致严重的后果:
- 程序运行后,随着时间占用了更多的内存,最后无内存可用而崩溃;
- 程序消耗了大量的内存,导致其他程序无法正常使用;
- 程序消耗了大量内存,导致消费者选用了别人的程序而不是你的;
- 经常做出内存泄露bug的程序猿被公司开除而贫困潦倒。
2.1.3 如何定位到泄露点呢?
- 根据原理,我们可以先review自己的代码,利用"查找"功能,查询new与delete,看看内存的申请与释放是不是成对释放的,这使你迅速发现一些逻辑较为简单的内存泄露情况。
- 如果依旧发生内存泄露,可以通过记录申请与释放的对象数目是否一致来判断。在类中追加一个静态变量 static int count;在构造函数中执行count++;在析构函数中执行count--;,通过在程序结束前将所有类析构,之后输出静态变量,看count的值是否为0,如果为0,则问题并非出现在该处,如果不为0,则是该类型对象没有完全释放。
- 检查类中申请的空间是否完全释放,尤其是存在继承父类的情况,看看子类中是否调用了父类的析构函数,有可能会因为子类析构时没有释放父类中申请的内存空间。
- 对于函数中申请的临时空间,认真检查,是否存在提前跳出函数的地方没有释放内存。
2.2 智能指针的分类
为了减少出现内存泄露的情况,STL中使用智能指针来减少泄露。STL中一般有四种智能指针:
指针类别 | 支持 | 备注 |
unique_ptr | C++ 11 | 拥有独有对象所有权语义的智能指针 |
shared_ptr | C++ 11 | 拥有共享对象所有权语义的智能指针 |
weak_ptr | C++ 11 | 到 std::shared_ptr 所管理对象的弱引用 |
auto_ptr | C++ 17中移除 | 拥有严格对象所有权语义的智能指针 |
因为 auto_ptr 已经在 C++ 17 中移除,对于面向未来的程序员来说,最好减少在代码中出现该使用的频次吧,这里我便不再研究该类型。又因为weak_ptr是shared_ptr的弱引用,所以,主要的智能指针分为两个unique_ptr和shared_ptr。
std::unique_ptr 是通过指针占有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象的智能指针。在下列两者之一发生时用关联的删除器释放对象:
- 销毁了管理的 unique_ptr 对象
- 通过 operator= 或 reset() 赋值另一指针给管理的 unique_ptr 对象。
std::shared_ptr 是通过指针保持对象共享所有权的智能指针。多个 shared_ptr 对象可占有同一对象。下列情况之一出现时销毁对象并解分配其内存:
- 最后剩下的占有对象的 shared_ptr 被销毁;
- 最后剩下的占有对象的 shared_ptr 被通过 operator= 或 reset() 赋值为另一指针。
2.2.1 基于引用计数的智能指针原理分析
下面的这张图,解释了智能指针的原理。
- 当从堆上申请了一个资源时,我们就创建一个智能指针对象,使它指向这个资源,同时,在堆上申请一个用于计数的资源,让后来所有的指向该资源的对象都共享这个计数资源,这样,引用计数的个数就只有一份。
- 当将ptr1对象赋值给对象ptr2时,其共享的引用计数变为2。
- 删除ptr2对象时,其对应的引用计数减为1。
- 删除ptr1对象时,引用计数变为0,则释放资源。
2.2.2 unique_ptr:独占的智能指针
这是个独占式的指针对象,在任何时间、资源只能被一个指针占有,当unique_ptr离开作用域,指针所包含的内容会被释放。
初始化
std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr。
创建
unique_ptr<int> uptr( new int );
unique_ptr<int[ ]> uptr( new int[5] );
//声明,可以用一个指针显示的初始化,或者声明成一个空指针,可以指向一个类型为T的对象
shared_ptr<T> sp;
unique_ptr<T> up;
//赋值,返回相对应类型的智能指针,指向一个动态分配的T类型对象,并且用args来初始化这个对象
make_shared<T>(args);
make_unique<T>(args); //注意make_unique是C++14之后才有的
//用来做条件判断,如果其指向一个对象,则返回true否则返回false
p;
//解引用
*p;
//获得其保存的指针,一般不要用
p.get();
//交换指针
swap(p,q);
p.swap(q);
//release()用法
//release()返回原来智能指针指向的指针,只负责转移控制权,不负责释放内存,常见的用法
unique_ptr<int> q(p.release()) // 此时p失去了原来的的控制权交由q,同时p指向nullptr
//所以如果单独用:
p.release()
//则会导致p丢了控制权的同时,原来的内存得不到释放
//则会导致//reset()用法
p.reset() // 释放p原来的对象,并将其置为nullptr,
p = nullptr // 等同于上面一步
p.reset(q) // 注意此处q为一个内置指针,令p释放原来的内存,p新指向这个对象
使用方法及注意事项
unique_ptr不允许被复制,但是可以通过函数返回给其他的unique_ptr,还可以通过std::move()转移给其他的unique_ptr。还是一个unique_ptr独占一个地址。
如果想要获取独占智能指针管理的原始地址,可以调用 get () 方法
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = move(ptr1);
ptr2.reset(new int(250));
cout << *ptr2.get() << endl; // 得到内存地址中存储的实际数值 250
return 0;
}
250
类满足可移动构造 (MoveConstructible) 和可移动赋值 (MoveAssignable) 的要求,但不满足可复制构造 (CopyConstructible) 或可复制赋值 (CopyAssignable) 的要求。 因此不可以使用 = 操作和拷贝构造函数,仅能使用移动操作。
2.2.3 shared_ptr:共享智能指针
有两种方式创建 shared_ptr:使用make_shared宏来加速创建的过程。因为shared_ptr主动分配内存并且保存引用计数(reference count),make_shared 以一种更有效率的方法来实现创建工作。
void main( )
{
shared_ptr<int> sptr1( new int );
shared_ptr<int> sptr2 = make_shared<int>(100);
}
拷贝和移动构造函数初始化
当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被调用了。
//构造函数
shared_ptr<int> ptr1(new int(520));
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
//拷贝构造函数
shared_ptr<int> ptr2(ptr1);
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<int> ptr3 = ptr1;
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
//移动构造函数
shared_ptr<int> ptr4(std::move(ptr1));
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
std::shared_ptr<int> ptr5 = std::move(ptr2);
cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;
打印结果如下:
ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3
make_shared初始化
通过c++11提供的std::make_shared()就可以完成内存对象的创建并将其初始化给智能指针
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Test
{
public:
Test()
{
cout << "无参构造函数" << endl;
}
Test(int x)
{
cout << "int类型构造函数 " << x << endl;
}
Test(string str)
{
cout << "string类型的构造函数" << str << endl;
}
~Test()
{
cout << "析构函数" << endl;
}
};
int main()
{
// 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
shared_ptr<int> ptr1 = make_shared<int>(520);
cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
shared_ptr<Test> ptr2 = make_shared<Test>();
cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
shared_ptr<Test> ptr3 = make_shared<Test>(520);
cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
shared_ptr<Test> ptr4 = make_shared<Test>("QQQQ");
cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
return 0;
}
打印结果如下:
ptr1管理的内存引用计数: 1
无参构造函数
ptr2管理的内存引用计数: 1
int类型构造函数 520
ptr3管理的内存引用计数: 1
string类型的构造函数QQQQ
ptr4管理的内存引用计数: 1
析构函数
析构函数
析构函数
如果使用拷贝的方式初始化共享智能指针,这两个对象会同时管理同一块内存,堆内存对应的引用技术也会增加。如果使用移动构造的方式初始化智能指针对象,只是转让了内存的所有权,管理内存的对象不会增加,因此内存引用技术不会增加。
析构
shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。在此场景下,shared_ptr指向一组对象,但是当离开作用域时,默认的析构函数调用delete释放资源。实际上,我们应该调用delete[]来销毁这个数组。用户可以通过调用一个函数,例如一个lamda表达式,来指定一个通用的释放步骤。
void main( )
{
shared_ptr<Test> sptr1( new Test[5],
[ ](Test* p) { delete[ ] p; } );
}
注意 尽量不要用裸指针创建 shared_ptr,以免出现分组不同导致错误
void main( )
{
// 错误
int* p = new int;
shared_ptr<int> sptr1( p); // count 1
shared_ptr<int> sptr2( p ); // count 1
// 正确
shared_ptr<int> sptr1( new int ); // count 1
shared_ptr<int> sptr2 = sptr1; // count 2
shared_ptr<int> sptr3;
sptr3 =sptr1 // count 3
}
循环引用
因为 Shared_ptr 是多个指向的指针,可能出现循环引用,导致超出了作用域后仍有内存未能释放。循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A
{
public:
shared_ptr<B> bptr;
~A()
{
cout << "class TA is disstruct ..." << endl;
}
};
class B
{
public:
shared_ptr<A> aptr;
~B()
{
cout << "class TB is disstruct ..." << endl;
}
};
void testPtr()
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
ap->bptr = bp;
cout << "B 的 引用计数: " << bp.use_count() << endl;
bp->aptr = ap;
cout << "A 的 引用计数: " << ap.use_count() << endl;
}
int main()
{
testPtr();
return 0;
}
A 的 引用计数: 1
B 的 引用计数: 1
B 的 引用计数: 2
A 的 引用计数: 2
共享智能指针 ap、bp 对 A、B 实例对象的引用计数变为 2,在共享智能指针离开作用域之后引用计数只能减为1,这种情况下不会去删除智能指针管理的内存,导致类 A、B 的实例对象不能被析构,最终造成内存泄露。通过使用 weak_ptr 可以解决这个问题,只需要将类 A 或者 B 的任意一个成员改为 weak_ptr.
2.2.4 weak_ptr:弱引用智能指针
什么是弱引用
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针。std::weak_ptr 没有重载操作符 * 和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在。
初始化
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int);
//weak_ptr<int> wp1; 构造了一个空 weak_ptr 对象
weak_ptr<int> wp1;
// weak_ptr<int> wp2(wp1); 通过一个空 weak_ptr 对象构造了另一个空 weak_ptr 对象
weak_ptr<int> wp2(wp1);
//weak_ptr<int> wp3(sp); 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象
weak_ptr<int> wp3(sp);
//wp4 = sp; 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象(这是一个隐式类型转换)
weak_ptr<int> wp4;
wp4 = sp;
//wp5 = wp3; 通过一个 weak_ptr 对象构造了一个可用的 weak_ptr 实例对象
weak_ptr<int> wp5;
wp5 = wp3;
return 0;
}
use_count()
通过调用 std::weak_ptr 类提供的 use_count() 方法可以获得当前所观测资源的引用计数
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int);
weak_ptr<int> wp1;
weak_ptr<int> wp2(wp1);
weak_ptr<int> wp3(sp);
weak_ptr<int> wp4;
wp4 = sp;
weak_ptr<int> wp5;
wp5 = wp3;
cout << "use_count: " << endl;
cout << "wp1: " << wp1.use_count() << endl;
cout << "wp2: " << wp2.use_count() << endl;
cout << "wp3: " << wp3.use_count() << endl;
cout << "wp4: " << wp4.use_count() << endl;
cout << "wp5: " << wp5.use_count() << endl;
return 0;
}
use_count:
wp1: 0
wp2: 0
wp3: 1
wp4: 1
wp5: 1
通过打印的结果可以知道,虽然弱引用智能指针 wp3、wp4、wp5 监测的资源是同一个,但是它的引用计数并没有发生任何的变化,也进一步证明了 weak_ptr只是监测资源,并不管理资源。
expired()
通过调用 std::weak_ptr 类提供的 expired() 方法来判断观测的资源是否已经被释放
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> shared(new int(10));
weak_ptr<int> weak(shared);
cout << "1. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;
shared.reset();
cout << "2. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;
return 0;
}
1. weak is not expired
2. weak is expired
weak_ptr 监测的就是 shared_ptr 管理的资源,当共享智能指针调用 shared.reset(); 之后管理的资源被释放,因此 weak.expired() 函数的结果返回 true,表示监测的资源已经不存在了。
lock()
通过调用 std::weak_ptr 类提供的 lock() 方法来获取管理所监测资源的 shared_ptr 对象
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1, sp2;
weak_ptr<int> wp;
sp1 = std::make_shared<int>(520);
wp = sp1;
sp2 = wp.lock();
cout << "use_count: " << wp.use_count() << endl;
sp1.reset();
cout << "use_count: " << wp.use_count() << endl;
sp1 = wp.lock();
cout << "use_count: " << wp.use_count() << endl;
cout << "*sp1: " << *sp1 << endl;
cout << "*sp2: " << *sp2 << endl;
return 0;
}
use_count: 2
use_count: 1
use_count: 2
*sp1: 520
*sp2: 520
sp2 = wp.lock(); 通过调用 lock() 方法得到一个用于管理 weak_ptr 对象所监测的资源的共享智能指针对象,使用这个对象初始化 sp2,此时所监测资源的引用计数为 2.
sp1.reset(); 共享智能指针 sp1 被重置,weak_ptr 对象所监测的资源的引用计数减 1.
sp1 = wp.lock(); sp1 重新被初始化,并且管理的还是 weak_ptr 对象所监测的资源,因此引用计数加 1.
共享智能指针对象 sp1 和 sp2 管理的是同一块内存,因此最终打印的内存中的结果是相同的,都是 520.
reset()
通过调用 std::weak_ptr 类提供的 reset() 方法来清空对象,使其不监测任何资源。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << "1. wp " << (wp.expired() ? "is" : "is not") << " expired" << endl;
wp.reset();
cout << "2. wp " << (wp.expired() ? "is" : "is not") << " expired" << endl;
return 0;
}
1. wp is not expired
2. wp is expired
返回管理this的 share_ptr
#include <iostream>
#include <memory>
using namespace std;
struct Test
{
shared_ptr<Test> getSharedPtr()
{
return shared_ptr<Test>(this);
}
~Test()
{
cout << "析构函数" << endl;
}
};
int main()
{
shared_ptr<Test> sp1(new Test);
cout << "引用个数 " << sp1.use_count() << endl;
shared_ptr<Test> sp2 = sp1->getSharedPtr();
cout << "引用个数: " << sp1.use_count() << endl;
return 0;
}
引用计数: 1
引用计数: 1
析构函数
析构函数
通过输出的结果可以看到一个对象被析构了两次,其原因是这样的:在这个例子中使用同一个指针 this 构造了两个智能指针对象 sp1 和 sp2,这二者之间是没有任何关系的,因为 sp2 并不是通过 sp1 初始化得到的实例对象。在离开作用域之后 this 将被构造的两个智能指针各自析构,导致重复析构的错误。
#include <iostream>
#include <memory>
using namespace std;
struct Test : public enable_shared_from_this<Test>
{
shared_ptr<Test> getSharedPtr()
{
return shared_from_this();
}
~Test()
{
cout << "析构函数" << endl;
}
};
int main()
{
shared_ptr<Test> sp1(new Test);
cout << "引用个数: " << sp1.use_count() << endl;
shared_ptr<Test> sp2 = sp1;
cout << "引用个数: " << sp1.use_count() << endl;
return 0;
}
引用个数: 1
引用个数: 2
析构函数
解决 shared_ptr 循环引用问题
class B;
class A
{
public:
A( ) : m_a(5) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
void PrintSpB( );
weak_ptr<B> m_sptrB;
int m_a;
};
class B
{
public:
B( ) : m_b(10) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
weak_ptr<A> m_sptrA;
int m_b;
};
void A::PrintSpB( )
{
if( !m_sptrB.expired() )
{
cout<< m_sptrB.lock( )->m_b<<endl;
}
}
void main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
sptrA->PrintSpB( );
}
2.3 STL 智能指针的陷阱/不够智能的地方
- 尽量用make_shared/make_unique,少用new
std::shared_ptr在实现的时候使用的refcount技术,因此内部会有一个计数器(控制块,用来管理数据)和一个指针,指向数据。因此在执行std::shared_ptr<A> p2(new A) 的时候,首先会申请数据的内存,然后申请内控制块,因此是两次内存申请,而std::make_shared<A>()则是只执行一次内存申请,将数据和控制块的申请放到一起。
- 不要使用相同的内置指针来初始化(或者reset)多个智能指针
- 不要delete get()返回的指针
- 不要用get()初始化/reset另一个智能指针
- 智能指针管理的资源它只会默认删除new分配的内存,如果不是new分配的则要传递给其一个删除器
- 不要把this指针交给智能指针管理
以下代码发生了什么事情呢?还是同样的错误。把原生指针 this 同时交付给了 m_sp 和 p 管理,这样会导致 this 指针被 delete 两次。 这里值得注意的是:以上所说的交付给m_sp 和 p 管理不对,并不是指不能多个shared_ptr同时占有同一类资源。shared_ptr之间的资源共享是通过shared_ptr智能指针拷贝、赋值实现的,因为这样可以引起计数器的更新;而如果直接通过原生指针来初始化,就会导致m_sp和p都根本不知道对方的存在,然而却两者都管理同一块地方。相当于”一间庙里请了两尊神”。
class Test{
public:
void Do(){ m_sp = shared_ptr<Test>(this); }
private:
shared_ptr<Test> m_sp;
};
int main()
{
Test* t = new Test;
shared_ptr<Test> p(t);
p->Do();
return 0;
}
- 不要把一个原生指针给多个shared_ptr或者unique_ptr管理
我们知道,在使用原生指针对智能指针初始化的时候,智能指针对象都视原生指针为自己管理的资源。换句话意思就说:初始化多个智能指针之后,这些智能指针都担负起释放内存的作用。那么就会导致该原生指针会被释放多次!!
int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
//p1,p2析构的时候都会释放ptr,同一内存被释放多次!
- 不是使用new出来的空间要自定义删除器
以下代码试图将malloc产生的动态内存交给shared_ptr管理;显然是有问题的,delete 和 malloc 牛头不对马嘴!!! 所以我们需要自定义删除器[](int* p){ free(p); }传递给shared_ptr。
int main()
{
int* pi = (int*)malloc(4 * sizeof(int));
shared_ptr<int> sp(pi);
return 0;
}
- 尽量不要使用 get()
智能指针设计者之处提供get()接口是为了使得智能指针也能够适配原生指针使用的相关函数。这个设计可以说是个好的设计,也可以说是个失败的设计。因为根据封装的封闭原则,我们将原生指针交付给智能指针管理,我们就不应该也不能得到原生指针了;因为原生指针唯一的管理者就应该是智能指针。而不是客户逻辑区的其他什么代码。 所以我们在使用get()的时候要额外小心,禁止使用get()返回的原生指针再去初始化其他智能指针或者释放。(只能够被使用,不能够被管理)。而下面这段代码就违反了这个规定:
int main()
{
shared_ptr<int> sp(new int(4));
shared_ptr<int> pp(sp.get());
return 0;
}
2.4 智能指针在面试中常考问题总结
2.4.1 智能指针是如何释放内存的?
智能指针的类对象是栈上的,所以当函数(或程序)结束时会自动被释放
#include <iostream>
using namespace std;
template<class T>
class SmartPointer {
public:
SmartPointer(T* p) {
ptr = p;
}
// 随析构函数一同释放
~SmartPointer() {
delete ptr;
}
private:
T* ptr;
};
2.4.2 智能指针是何时释放内存的
观察如下代码:
#include <iostream>
using namespace std;
template<class T>
class SmartPointer {
public:
SmartPointer(T* p) {
ptr = p;
}
~SmartPointer() {
delete ptr;
}
private:
T* ptr;
};
class Object1 {
public:
int a;
int b;
};
void Process1(SmartPointer<Object1> p) {
// do something with p
}
class Object2 {
public:
int a;
int b;
};
void Process2(SmartPointer<Object2>& p) {
// do something with p
}
int main() {
while (true) {
SmartPointer<Object1> p1(new Object1());
SmartPointer<Object2> p2(new Object2());
Process1(p1);
Process2(p2);
}
}
很容易发现如下区别:
而对于Process1 函数的执行会出现两次析构的异常:
Process(p1) 传参的时候会进行默认的赋值构造函数,形参的p1和实参的p指向同一块内存,Process1函数运行结束回到main函数后会释放函数内部的p, while单层循环结束后仍然会释放p,p会进行两次的释放,造成异常。
引用计数类
// 引用计数类
class Counter {
friend class SmartPointerPro;
public:
Counter() {
ptr = NULL;
cnt = 0;
}
~Counter() {
delete ptr;
}
private:
Object2* ptr;
int cnt;
};
如何改进SmartPointer类使得它能够动态维护引用计数
改进后的智能指针类
template<class T>
// 引用计数类
class Counter {
friend class SmartPointerPro;
public:
Counter() {
ptr = NULL;
cnt = 0;
}
Counter(T* p) {
ptr = p;
cnt = 1;
}
~Counter() {
delete ptr;
}
private:
T* ptr;
int cnt;
};
template<class T>
// 智能指针类
class SmartPointerPro {
public:
SmartPointerPro(T* p) {
ptr_counter = new Counter(p);
}
SmartPointerPro(const SmartPointerPro& sp) {
ptr_counter = sp.ptr_counter;
++ptr_counter->cnt;
}
SmartPointerPro& operator=(const SmartPointerPro& sp) {
++sp.ptr_counter->cnt;
--ptr_counter->cnt;
if (ptr_counter->cnt == 0) {
delete ptr_counter;
}
ptr_counter = sp.ptr_counter;
}
~SmartPointerPro() {
--ptr_counter->cnt;
if (ptr_counter->cnt == 0) {
delete ptr_counter;
}
}
private:
Counter* ptr_counter;
};
完整的自定义智能指针代码
template<class T>
class SmartPointerPro {
public:
SmartPointerPro(T* p) {
ptr_counter = new Counter(p);
}
SmartPointerPro(const SmartPointerPro& sp) {
ptr_counter = sp.ptr_counter;
++ptr_counter->cnt;
}
SmartPointerPro& operator=(const SmartPointerPro& sp) {
++sp.ptr_counter->cnt;
--ptr_counter->cnt;
if (ptr_counter->cnt == 0) {
delete ptr_counter;
}
ptr_counter = sp.ptr_counter;
}
T* operator->() {
return ptr_counter->ptr;
}
T& operator*() {
return *(ptr_counter->ptr);
}
~SmartPointerPro() {
--ptr_counter->cnt;
if (ptr_counter->cnt == 0) {
delete ptr_counter;
}
}
private:
Counter* ptr_counter;
};
引用计数所带来的问题:循环引用
2.4.3 如何解决循环引用的问题
一般来讲,解除这种循环引用有下面有三种可行的方法( 参考 ):
1 . 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
2 . 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
3 . 使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。使用weak_ptr时,并不知道引用计数是多少,一般会有一个强指针来帮助其判断内存是否已经被释放掉或者是否合法。weak_ptr不会去修改引用计数的大小,相当于把循环打破了,所以会用weak_ptr去解决循环引用问题。
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A
{
public:
weak_ptr<B> bptr;
~A()
{
cout << "A 的 析构函数" << endl;
}
};
class B
{
public:
shared_ptr<A> aptr;
~B()
{
cout << "B 的 析构函数" << endl;
}
};
void testPtr()
{
shared_ptr<A> ap(new A);
shared_ptr<B> bp(new B);
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
ap->bptr = bp;
bp->aptr = ap;
cout << "A 的 引用计数: " << ap.use_count() << endl;
cout << "B 的 引用计数: " << bp.use_count() << endl;
}
int main()
{
testPtr();
return 0;
}
A 的 引用计数: 1
B 的 引用计数: 1
A 的 引用计数: 2
B 的 引用计数: 1
B 的 析构函数
A 的 析构函数
上面程序中,在对类 A 成员赋值时 ap->bptr = bp; 由于 bptr 是 weak_ptr 类型,这个赋值操作并不会增加引用计数,所以 bp 的引用计数仍然为 1,在离开作用域之后 bp 的引用计数减为 0,类 B 的实例对象被析构。
在类 B 的实例对象被析构的时候,内部的 aptr 也被析构,其对 A 对象的管理解除,内存的引用计数减为 1,当共享智能指针 ap 离开作用域之后,对 A 对象的管理也解除了,内存的引用计数减为 0,类 A 的实例对象被析构。