前言:上一篇说到异常会让程序跳过后面的一部分代码,这就带来了一个问题:如果后面的代码刚好是释放内存的,那这块内存该怎么办呢?智能指针就是用来帮我们管理这类资源的。不过它也不是把普通指针换一个名字就完事了,拷贝以后由谁释放资源、多个对象能不能一起管理同一块空间,这些问题都需要考虑。下面就从这些地方来看看几种智能指针的使用和实现。
1.为什么需要智能指针
先来看一个很简单的例子:
cpp
void Func()
{
int* p = new int[10];
cout << "申请内存成功" << endl;
// 如果中途发生异常
throw "发生异常";
// 异常发生后,这句代码不会执行找出内存泄漏
delete[] p;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
这里虽然写了delete[] p,但是执行到throw以后,程序就会跳到外层匹配的catch,不会再回来执行后面的释放语句。
函数退出时,局部变量p的生命周期结束了,但它只是一个保存地址的普通指针,**指针变量的销毁,不会自动释放它指向的堆内存。**所以申请出来的这块空间就没有被释放,出现了内存泄漏。
那在函数里面先捕获异常,把资源释放了,再重新抛出去不就可以了吗?
这个思路可以处理当前的问题,但资源多起来以后就比较麻烦了。比如连续申请两块空间,第一块申请成功,第二次new却抛出了异常,那么第一块仍然需要释放。正常退出、提前返回和不同位置抛异常,都要考虑哪些资源已经申请成功,哪些还没有申请,单靠在每条路径上补释放语句很容易遗漏。
我们之前学过,类对象离开作用域时会自动调用析构函数;异常向外传递、进行栈展开时,已经构造完成的局部对象也会被析构。那么可以把资源交给一个对象管理,等这个管理对象析构时,再由它完成资源的释放。
这就是RAII 的基本思路。RAII是Resource Acquisition Is Initialization的缩写,通常叫作"资源获取即初始化"。理解时可以抓住一点:把资源的管理和对象的生命周期联系起来。
- 构造管理对象时,接管资源。
- 对象存活期间,通过它使用资源。
- 对象析构时,按约定释放资源。
RAII不只适用于内存,文件等需要配对获取和释放的资源也可以这样管理。后面用智能指针管理文件时,就会把释放动作换成fclose。
智能指针在这个基础上还需要模拟指针的使用方式,比如重载operator*和operator->,让我们仍然能通过解引用和箭头访问对象;数组形式还可以提供operator[]。
所以它本质上是一个类对象,只不过内部保存了指针,并且替我们承担了相应的资源管理工作。这里还有一个很重要的问题:如果这个类对象被拷贝了,两个对象都保存同一个地址,析构时岂不是会释放两遍?不同智能指针的区别,很大一部分就在于如何处理这个问题。
2.标准库智能指针的使用
标准库智能指针放在<memory>中。下面这些例子共用的头文件是:
2.1 auto_ptr、unique_ptr与shared_ptr
先定义一个Date类,析构时打印信息,方便观察对象什么时候被释放:
cpp
struct Date
{
int _year;
int _month;
int _day;
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
~Date()
{
cout << "~Date()" << endl;
}
};
再把几种智能指针放在一起看:
cpp
int main()
{
auto_ptr<Date> ap1(new Date);
// 构造拷贝时,管理权转移到 ap2 上,导致了 ap1 悬空
auto_ptr<Date> ap2(ap1);
//ap1->_year++;
unique_ptr<Date> up1(new Date);
// 不支持拷贝,但是支持移动但是移动后里面的
// up1 会悬空所以要谨慎哦
unique_ptr<Date> up2(move(up1));
// 它们共同管理一块空间的资源
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
// 这里可以取到这个资源被几个指针管理
cout << sp1.use_count() << endl;
sp1->_year++;
// 和上面一样同样会有悬空的问题
shared_ptr<Date> sp3(move(sp1));
return 0;
}
先看auto_ptr。
auto_ptr是C++98中的智能指针。它处理拷贝的方式比较特殊:不是让两个对象共同管理资源,而是把原对象的管理权转移给新对象。
执行auto_ptr<Date> ap2(ap1)以后,原来的Date对象没有被拷贝,仍然是同一个对象,只是改由ap2管理,ap1内部的指针则被置空。

这时不能再通过ap1访问原对象。如果继续执行ap1->_year++,就会变成对空指针的解引用:

这里要分清楚,原来的资源并没有被提前释放,而是ap1已经不再指向它。上面的访问错误也不是前面通过throw主动抛出的C++异常。
这种"写的是拷贝,实际却把原对象的管理权拿走了"的行为不太符合我们平常对拷贝的理解,使用时容易出问题。auto_ptr在C++11中被弃用,在C++17中被移除,现在主要用来了解早期的设计思路。上面含有它的历史示例可以放在C++11环境中观察,不能要求所有C++17环境继续支持它。版本说明可以看auto_ptr文档。
再看unique_ptr。
unique_ptr强调独占所有权,同一份资源在同一时刻由一个unique_ptr负责管理。它直接禁止拷贝构造和拷贝赋值,不让"两个管理对象到底谁来释放"这个问题通过普通拷贝产生。
但是独占不代表管理权永远不能转交。C++11引入移动语义以后,可以通过移动把管理权交给另一个对象。
在上面的unique_ptr<Date> up2(move(up1))中,std::move使这里能够选择移动构造函数,真正转交资源的是移动构造函数。移动完成后,up2管理原来的Date,up1变为空。

所以移动以后仍然不能继续解引用up1。它和auto_ptr的区别在于,转交管理权需要明确使用移动,而不是一次普通拷贝就把原对象变空了。
最后看shared_ptr。
有些场景下,确实需要多个对象共同管理一份资源,这时就可以使用shared_ptr。它支持拷贝,采用引用计数来记录同一组共享所有者的数量。
sp1刚创建时,引用计数为1;用sp1拷贝构造sp2以后,两者共同管理同一个Date,引用计数变成2。因此sp1.use_count()输出2。

这里拷贝的是智能指针对象,不是重新创建一份Date。所以通过sp1->_year++修改对象后,再通过sp2访问,看到的也是修改后的结果。
shared_ptr也支持移动。执行shared_ptr<Date> sp3(move(sp1))时,sp1原来持有的那一份管理权转交给sp3,sp1变为空。这次移动没有多增加一位所有者,原对象仍由sp2和sp3共同管理。
等某个shared_ptr析构时,先减少一次引用计数;只有最后一位所有者离开、计数降到0时,才真正释放资源。
上面这段代码的输出为:
text
2
~Date()
~Date()
~Date()
虽然定义了多个智能指针对象,但总共只申请了三个Date,最终这三个对象各析构一次。
还有一个weak_ptr,它不拥有被观察的对象,不会增加这里的引用计数。它主要用来配合shared_ptr,后面讲循环引用时再具体看。
2.2数组管理与定制删除器
前面的资源都是通过new Date创建的单个对象,使用默认的delete释放就可以了。但资源的释放方式不只有这一种。
new[]申请的数组应该用delete[],malloc申请的空间应该用free,fopen打开的文件应该用fclose。智能指针不会根据一个普通地址自动猜出它是如何申请的,释放方式需要匹配。
数组可以使用数组形式的智能指针:

unique_ptr<Date[]>对应数组管理,默认释放时使用delete[]。unique_ptr<T[]>从C++11就可以使用,而下面的shared_ptr<Date[]>
除了使用数组形式,还可以提供一个定制删除器。删除器本质上就是一个可调用对象,把资源的释放动作写在里面,智能指针需要释放资源时,再调用它。
先准备仿函数、普通函数和关闭文件的仿函数:
cpp
template<class T>
class DeleteArray
{
public:
void operator()(T* ptr)
{
delete[] ptr;
}
};
template<class T>
void DeleteArrayFunc(T* ptr)
{
delete[] ptr;
}
class Fclose
{
public:
void operator()(FILE* ptr)
{
cout << "fclose:" << ptr << endl;
fclose(ptr);
}
};
然后把它们交给智能指针:
cpp
int main()
{
// 对于 new[] 这样会报错
/*unique_ptr<Date> up1(new Date[10]);
shared_ptr<Date> up1(new Date[10]);*/
// 但是库里提供了一个特化版本的,本质上
// 是在析构时调用delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
// 也可以使用仿函数对象做删除器
// unique_ptr是在类模板参数⽀持的
unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);
// shared_ptr是构造函数参数⽀持的
shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());
// 一些可调用对象作为删除器
// 函数指针做删除器
unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);
// lambda表达式做删除器
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
shared_ptr<Date> sp4(new Date[5], delArrOBJ);
// 实现其他资源管理的删除器
shared_ptr<FILE> sp5(fopen("Main.cpp", "r"), Fclose());
shared_ptr<FILE> sp6(fopen("Main.cpp", "r"), [](FILE* ptr) {
cout << "fclose:" << ptr << endl;
fclose(ptr);
});
return 0;
}
几种写法虽然不同,但最终做的都是同一件事:让数组通过delete[]释放。
使用删除器时,unique_ptr和shared_ptr的接口有一点区别:
unique_ptr把删除器类型作为类模板的第二个参数。shared_ptr通过构造函数接收删除器,删除器的类型不需要写进shared_ptr<Date>这个类型名称中。
DeleteArray<Date>是一个仿函数类型,可以默认构造出对象,因此up2只传资源指针也可以使用默认构造出来的删除器。而up3用函数指针作为删除器,就同时传入具体的函数地址,也不知道为什么要设置成两个不同的方式,总感觉有些混乱的感觉
lambda表达式的类型没有一个方便直接写出来的名字,所以先用delArrOBJ保存lambda对象,再用decltype(delArrOBJ)取得类型,放到unique_ptr的第二个模板参数中:

再看最后两个文件对象,就能发现智能指针管理的并不一定是new出来的内存。sp5和sp6负责管理FILE*,资源释放时分别调用仿函数和lambda中的fclose。
2.3make_shared与判空
shared_ptr除了接收裸指针,还可以通过make_shared创建:
cpp
int main()
{
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11);
shared_ptr<Date> sp4;
// 本质上是调用这个
// if (sp1.operator bool())
if (sp1)
cout << "sp1 is not nullptr" << endl;
if (!sp4)
cout << "sp1 is nullptr" << endl;
// 不能这样,因为本质上是不同的类也没有支持它们
// 之间的转化
//shared_ptr<Date> sp5 = new Date(2024, 9, 11);
//unique_ptr<Date> sp6 = new Date(2024, 9, 11);
return 0;
}
make_shared<Date>(2024, 9, 11)会创建一个Date,并把后面的参数传给它的构造函数,最后返回管理这个对象的shared_ptr<Date>。
所以这里sp1、sp2和sp3分别管理三个独立创建的对象。构造参数相同不代表它们共享同一份资源,只有建立了共享所有权关系,才会共用同一组引用计数。
shared_ptr和unique_ptr都提供了operator bool,因此可以直接放进if判断所保存的指针是否非空。上面sp1保存着有效指针,会进入第一个分支;sp4默认构造为空,会进入if (!sp4)分支。
资源交给智能指针后,也不要再手动释放它。对于shared_ptr,共享所有权应该通过已有的shared_ptr建立,不能把同一个裸指针分别交给两个独立构造的shared_ptr,否则它们各自计数,最后可能重复释放同一个对象。
3.智能指针的模拟实现
3.1auto_ptr
最简单的资源管理方式,就是内部保存一个T*,构造时接管它,析构时释放它,再通过运算符重载模拟指针的行为:
cpp
template<typename T>
class auto_ptr
{
public:
// explicit 用来防止隐式类型转化
explicit auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{ }
auto_ptr(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
~auto_ptr()
{
delete _ptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 防止给自己赋值
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
// 模拟指针行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这里普通构造和析构比较直接。构造函数把传进来的地址保存到_ptr,析构函数执行delete _ptr。delete空指针不会释放任何对象,因此转移管理权后,原来的智能指针也可以正常析构。
operator*返回所指对象的引用,operator->返回内部指针,这样外面就可以像使用普通指针一样访问对象。
重点是拷贝构造。它先把ap._ptr交给当前对象,再把ap._ptr置空,这就完成了管理权转移。注意参数不是const auto_ptr<T>&,因为这里需要修改被拷贝的对象。
赋值比拷贝构造多一步:赋值对象原来可能已经管理着一份资源,需要先处理旧资源,再接管新资源。
所以先判断是不是自己给自己赋值;不是的话,释放当前的_ptr,接收ap._ptr,最后把ap置空。整个过程中,同一份资源仍然只有一个负责释放它的对象。
3.2unique_ptr
unique_ptr的基础部分相同,变化主要在拷贝和移动上:
cpp
template<typename T>
class unique_ptr
{
public:
explicit unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{ }
// 禁用左值拷贝构造
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr(unique_ptr<T>&& up)
{
_ptr = up._ptr;
up._ptr = nullptr;
}
// 禁用左值赋值重载
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(unique_ptr<T>&& up)
{
if (this != &up)
{
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
return *this;
}
~unique_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
这里通过= delete明确禁用了拷贝构造和拷贝赋值。注意它是告诉编译器"这个函数不能调用",和释放内存的delete _ptr不是一个意思。
移动构造接收unique_ptr<T>&&,先接管原指针,再把原对象置空。移动赋值还要考虑当前对象已有资源,所以先释放自己的旧资源,再接管对方的资源。
3.3shared_ptr
shared_ptr不能在每个对象析构时直接delete _ptr,因为别的共享所有者可能还要继续使用这份资源。它需要知道:当前到底还有几个智能指针在共同管理它。
这里的引用计数要跟着"这一份被共同管理的资源"走,而不是跟着某个独立的智能指针对象走。
如果每个对象都保存一份独立的整数,拷贝后修改的就只是各自的计数;如果使用类的静态成员,同类型的不同资源又会被混到同一个计数里。因此这里单独申请一个计数器,让同一组共享对象都保存指向它的指针。
完整的类定义如下:
cpp
template<typename T>
class shared_ptr
{
public:
explicit shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(ptr ? new int(1) : nullptr)
{ }
template<typename D>
explicit shared_ptr(T* ptr, D del)
:_ptr(ptr)
,_pcount(ptr ? new int(1) : nullptr)
,_del(del)
{ }
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _del(sp._del)
{
if (_pcount)
++(*_pcount);
}
void Cheke_release()
{
// 计数器为0时释放资源
if (_pcount && --(*_pcount) == 0)
{
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
Cheke_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
if (_pcount)
++(*_pcount);
_del = sp._del;
}
return *this;
}
~shared_ptr()
{
Cheke_release();
}
T* Get() const
{
return _ptr;
}
int use_count() const
{
return _pcount ? *_pcount : 0;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount; // 计数器
function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
普通构造时,如果传入的指针非空,就创建一个值为1的计数器;如果没有管理资源,_pcount也为空。use_count()根据这一点返回实际计数或0。
拷贝构造时,_ptr和_pcount都从原对象中复制过来,所以两个智能指针不只指向相同资源,也指向同一个计数器。再对这个计数器加1,共享关系就建立起来了。
删除器也要一起保存。因为最后释放资源的是哪一个shared_ptr,取决于它们的生命周期,不能假定一定由最开始创建的对象负责释放。这里使用前面学过的function<void(T*)>来保存可调用对象,通过_del(_ptr)执行最终的释放动作。
接着看Cheke_release()。这个函数先减少一次引用计数,表示当前智能指针不再管理这份资源:
- 减完以后仍然大于0,说明还有其他所有者,资源和计数器都不能释放。
- 减完以后等于0,说明自己是最后一位所有者,先通过删除器释放资源,再释放计数器。
析构函数直接调用它,而赋值运算符要把"离开原来的共享关系"和"加入新的共享关系"连起来做。
假设当前对象原来和另外两个智能指针共享资源A,现在改成共享资源B,那么赋值时先把A的计数减1,不能直接删除A,因为另外两个对象还在使用它;然后接收B的资源地址、计数器地址和删除器,并把B的计数加1。
this != &sp处理自己给自己赋值的情况。如果是两个不同的智能指针对象,但本来就在同一组共享关系中,这份实现会先减1再加1,最终计数不变。
观察效果:
cpp
// 测试
int main()
{
jy::auto_ptr<Date> ap1(new Date);
jy::auto_ptr<Date> ap2(ap1);
jy::unique_ptr<Date> up1(new Date);
jy::unique_ptr<Date> up3(move(up1));
jy::shared_ptr<Date> sp1(new Date);
jy::shared_ptr<Date> sp2(sp1);
jy::shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
return 0;
}
运行结果为:
text
3
2
2
2
~Date()
~Date()
~Date()
sp1、sp2和sp3共同管理一个Date,所以引用计数为3。Date默认年份是1,通过sp1加1以后,再从三个智能指针访问,得到的都是2。
最后仍然只析构了三个Date对象:一个由auto_ptr管理,一个由unique_ptr管理,一个由这三个shared_ptr共同管理。共享的是资源和计数,不是每拷贝一个智能指针就多创建一个资源对象。
4.shared_ptr与weak_ptr
4.1循环引用为什么会造成内存泄漏
引用计数能解决共享资源的释放问题,但如果共享关系形成了环,就可能出现所有对象都已经不再需要,资源却没有释放的情况。
用两个链表节点就能说明这个问题。假设节点A的_next和节点B的_prev都是shared_ptr,A通过_next拥有B,B又通过_prev拥有A。
刚创建节点时,外部的n1和n2分别管理一个节点,各自的引用计数都是1。把节点连起来以后,A除了被n1管理,还被B中的_prev管理;B除了被n2管理,还被A中的_next管理,因此两个计数都变成2。

等n1和n2离开作用域,各自的计数确实会减1,但减完以后仍然是1:
- A要被释放,需要B里面的
_prev先放弃对它的管理。 _prev是B的成员,B析构时才会跟着析构。- B要被释放,又需要A里面的
_next先放弃对它的管理。 _next是A的成员,只有A析构时才会跟着析构。
这样就形成了循环,谁也无法成为第一个被释放的节点。这里的问题是循环强引用让计数无法归零,造成内存泄漏。
既然只是想记录另一个节点的位置,能不能不把这条连接也算成一份所有权呢?这就是weak_ptr的作用。
weak_ptr可以观察shared_ptr管理的对象,但不会增加强引用计数,也不会因为自己还存在,就要求对象继续存活。
下面把节点里的连接改成weak_ptr:
cpp
struct ListNode
{
int _data;
//std::shared_ptr<ListNode> _next;
//std::shared_ptr<ListNode> _prev;
// 这里改成weak_ptr时
// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了
std::weak_ptr<ListNode> _next;
std::weak_ptr<ListNode> _prev;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
// 循环引用造成内存泄露
std::shared_ptr<ListNode> n1(new ListNode);
std::shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
n1->_next = n2;
n2->_prev = n1;
cout << n1.use_count() << endl;
cout << n2.use_count() << endl;
return 0;
}
这里实际启用的两个成员都是weak_ptr,所以n1->_next = n2和n2->_prev = n1只是建立观察关系,没有增加节点的强引用计数。
对应的输出是:
text
1
1
1
1
~ListNode()
~ListNode()
前后四次计数都是1,最后两个节点也正常析构了。对于这个例子,节点生命周期由外部的n1、n2负责,节点间的两个成员只负责记录连接关系,不负责延长对方的生命周期。
4.2weak_ptr如何访问资源
weak_ptr不保证被观察的对象一直活着,因此它没有提供像shared_ptr那样直接访问对象的operator*和operator->。如果直接保存一个地址就去解引用,那么真正的所有者已经释放资源时,就有可能访问到失效内存。
标准库的weak_ptr主要通过下面三个接口配合使用:
| 接口 | 作用 |
|---|---|
use_count() |
返回所观察对象的强引用计数,不是weak_ptr自身的数量 |
expired() |
判断所观察的对象是否已经没有共享所有者 |
lock() |
对象仍存活时,取得一个共享所有权的shared_ptr;已经过期时,返回空shared_ptr |
具体行为可以对照weak_ptr接口文档。真正访问对象时,先取得lock()返回的shared_ptr,再判断它是否为空。
cpp
int main()
{
shared_ptr<string> sp = make_shared<string>("hello");
// weak_ptr观察sp管理的资源,不增加引用计数
weak_ptr<string> wp = sp;
cout << "引用计数:" << wp.use_count() << endl;
cout << "是否过期:" << wp.expired() << endl;
// 通过lock获取shared_ptr,安全访问资源
shared_ptr<string> tmp = wp.lock();
if (tmp)
{
cout << *tmp << endl;
}
// 释放所有shared_ptr
sp.reset();
tmp.reset();
cout << "是否过期:" << wp.expired() << endl;
return 0;
}
开始时只有sp拥有这个字符串对象,因此引用计数为1。wp绑定它以后没有增加计数,此时对象仍然存在,expired()返回false,输出为0。
随后wp.lock()成功,返回一个与sp共享所有权的shared_ptr,保存在tmp中。这时才新增了一份共享所有权,强引用计数变成2。通过tmp就可以访问字符串,输出hello。
注意lock()不会把wp本身变成一个shared_ptr,它返回的是另一个对象,wp仍然只是观察者。
后面先执行sp.reset(),sp放弃所有权,但是tmp还在,因此字符串还不会销毁。再执行tmp.reset(),最后一个共享所有者也放弃了所有权,字符串对象才被销毁。
此时wp虽然还在,但它不能把字符串留住,expired()返回true。完整输出为:
text
引用计数:1
是否过期:0
hello
是否过期:1
写一个非常简化的版本:
cpp
template<typename T>
class weak_ptr
{
public:
weak_ptr()
{ }
weak_ptr(shared_ptr<T>& ptr)
:_ptr(ptr.Get())
{ }
weak_ptr<T>& operator=(shared_ptr<T>& ptr)
{
_ptr = ptr.Get();
return *this;
}
private:
T* _ptr;
};
构造和赋值时,它通过Get()取得地址并保存下来,没有增加计数,也没有负责释放目标资源的析构逻辑。
实际上想要实现一个完整可用的weak_ptr比较复杂前面的两个指针也要大改,我这里就只是像体现一下这个指针的思想
5.智能指针的发展与内存泄漏
5.1C++11与Boost中的智能指针
智能指针并不是到C++11才突然出现的。C++98就有了auto_ptr,而在C++11之前,Boost也提供了多种智能指针,用来处理不同的资源管理需求。
Boost可以理解成一组为C++提供扩展功能的库,其中一些成熟设计也为标准库的发展提供了参考。智能指针这部分的关系大致是:
- C++98提供
auto_ptr,通过拷贝转移管理权。 - Boost提供了
scoped_ptr、scoped_array、shared_ptr、shared_array和weak_ptr等类型。 shared_ptr、weak_ptr经过TR1这一库技术报告阶段,后来进入C++11标准。TR1不是一个名叫"C++TR1"的语言标准版本。- C++11标准库中有了
unique_ptr、shared_ptr和weak_ptr,独占、共享和非拥有观察几种需求有了相应的工具。
unique_ptr和boost::scoped_ptr都可以用于独占管理,但两者不能简单理解成完全相同的类。scoped_ptr强调在当前作用域内管理资源,不提供所有权转移语义;unique_ptr则支持通过移动转移所有权。相关背景可以看Boost.SmartPtr的介绍与历史。
5.2内存泄漏的危害、检测与避免
内存泄漏不是内存在物理上消失了,而是程序申请的内存已经不再需要,却没有按预期释放,继续占着资源。原因可能是忘记释放,也可能是异常跳过释放语句,或者像前面的循环引用一样,资源管理关系出了问题。
普通的小程序运行一会儿就退出,进程结束后,操作系统通常会回收它占用的进程内存,所以问题有时不容易被注意到。但是长期运行的程序不能依靠"等退出再回收":每次泄漏一点,累积起来就会占用越来越多的资源,影响运行,严重时会导致内存分配失败。
检测内存泄漏需要借助工具。例如Linux环境中可以使用Valgrind的Memcheck,Windows下也有Visual Leak Detector这类工具。工具可以帮助定位未释放内存及相关分配位置,使用时还要确认它与编译器、运行环境的兼容情况。
不过检测是发现问题,设计和编码时仍然要尽量避免产生问题,想要达到这点就多练习吧
完