引、内存泄漏
在编写C++程序时,内存资源方面的问题一直是不可疏忽的,其中,内存泄漏就是一个典型。
内存泄漏并不是指内存资源在物理层面上的消失,而是程序按需分配某段内存资源后,失去了对该资源的控制,白白浪费了内存空间,导致系统“负重前行”。这种情况一旦发生,隐患极大,例如在长期运行的程序(如操作系统、后台服务等)中出现内存泄漏,会使系统响应得越来越慢,以至于卡顿、死机。
内存泄漏分为系统资源泄漏和堆内存泄漏。
系统资源泄漏指的是,系统分配的资源没有正确释放而导致的资源浪费。程序使用系统分配的资源,例如套接字、文件描述符、管道等,用完后没有通过相应的函数释放掉,可能导致系统效能减少,系统执行不稳定。
堆内存泄漏指的是,在堆上申请的资源没有正确释放导致的资源浪费。程序执行中通过malloc / calloc / realloc / new等,会按需从堆中分配一份内存资源,这份资源用完后必须通过相应的free或者delete释放。如果这份资源没有被释放,那么之后它将无法继续使用。
// 1.内存申请了忘记释放
void MemoryLeaks()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
}
// 2.异常安全问题(delete未执行到位)
void MemoryLeaks()
{
pair<string, string>* p1 = new pair<string, string>;
Func(); // 这里Func()抛异常会导致 delete p1未执行,p1没被释放.
delete p1;
}
要避免内存泄漏,一般有事后查错和事前预防两种方案。
事后查错如:
- 使用公司自带内存泄漏检测功能的私有内存管理库(这是部分公司的内部规范);
- 使用内存泄漏工具检测,如Valgrind和Sanitizer等(但许多工具都不够靠谱,有些还收费昂贵)。
【补】内存泄漏的检测工具
在linux下内存泄漏检测:linux下几款内存泄漏检测工具
在windows下使用第三方工具:VLD工具说明
其他工具:内存泄漏工具比较
事前预防如:
- 工程前期良好的设计规范、编码规范,申请的内存资源都有匹配的释放(但这比较理想,如果碰上异常,就算注意释放了,可能还会出问题);
- 采用RAII思想或者智能指针来管理资源。
本篇博客主要整理了RAII思想和智能指针原理,结合对库中四种智能指针的模拟实现,旨在让读者能更深入地认识C++的内存管理方案。
目录
引、内存泄漏
一、RAII与智能指针
1.什么是RAII
2.智能指针的原理
二、C++98的auto_ptr
1.基本原理和用法
2.模拟实现
三、C++11的unique_ptr
1.基本原理和用法
2.模拟实现
1.基本原理和用法
2.模拟实现
3.定制删除器
4.线程安全问题
5.循环引用问题
五、C++11的weak_ptr
1.基本原理和用法
2.模拟实现
补、智能指针的发展历史
补、智能指针与boost库
补、C++与Java关于内存管理方案的比较
一、RAII与智能指针
1.什么是RAII
对于上文中“内存申请了忘记释放”,在编写代码时将申请的资源都正确释放即可。
// 1.内存申请了忘记释放
void MemoryLeaks()
{
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
//delete p1;
//delete p2;
}
对于上文中的“异常安全问题”,一般可以通过try - catch来解决。
// 2.异常安全问题(delete未执行到位)
void MemoryLeaks()
{
pair<string, string>* p1 = new pair<string, string>;
Func(); // 这里Func()抛异常会导致 delete p1未执行,p1没被释放.
delete p1;
// 抛异常会影响执行流,
// 可能导致执行流一连跳跃了好几个函数栈,
// 使在堆区申请的许多资源没有及时回收,
// 造成内存泄露
}
//抛异常会影响执行流,delete不一定会执行到位
//一般可以通过try - catch解决这个问题
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void MemoryLeaks()
{
pair<string, string>* p1 = new pair<string, string>;
try
{
div();
}
catch (...)
{
delete p1;
cout << "delete:" << p1 << endl;
throw;
}
delete p1;
cout << "delete:" << p1 << endl;
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
//但当new的空间多了,try - catch就有些捉襟见肘
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void MemoryLeaks()
{
pair<string, string>* p1 = new pair<string, string>;
pair<string, string>* p2 = new pair<string, string>;
pair<string, string>* p3 = new pair<string, string>;
pair<string, string>* p4 = new pair<string, string>;
//p1可能会抛异常,p2、p3、p4也可能抛异常,为了使程序正常运行
//在p2抛异常之前应先释放p1,在p2抛异常之前应先释放p3...甚至还有div()也可能抛异常
//这样下去不知道要套多少层try - catch
//...
}
int main()
{
//...
return 0;
}
//于是乎,前人想到了一种更巧妙的方式:
//利用构造和析构的特性(对象初始化自动调用构造,对象出作用域(生命周期结束)自动调用析构)
//用一个对象来管理new返回的指针
//具体的做法是将指针用一个类封装起来,在类的析构中delete
//这样一来,那怕抛异常也不会影响delete执行了
template<class T>
class SmartPtr
{
public:
// 资源交给对象管理
// 对象生命周期内,资源有效;
// 对象生命周期到了,借助析构释放资源
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl; //这句是用于验证抛异常的时候析构是否也释放了资源
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
pair<string, string>* p1 = new pair<string, string>;
SmartPtr sp1(p1); // 资源交给对象管理
//无论是f()的生命周期正常结束,还是div()抛异常
//都不影响sp1的析构会释放p1
div();
//此时不必再手动释放资源
//delete p1;
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
int div()
{
int a, b;
cin >> a >> b;
if (b == 0)
throw invalid_argument("除0错误");
return a / b;
}
void f()
{
//实际运用中,可以传匿名对象给SmartPtr对象
SmartPtr<pair<string, string>> sp1(new pair<string, string>);
//如果sp1的new本身抛异常了,就不会为sp1调用构造
div();
//如果执行到这里,div()抛异常了,sp1的析构也会正常调用
SmartPtr<pair<string, string>> sp2(new pair<string, string>);
//如果sp2的new抛异常了,就就不会为sp2调用构造,sp1的析构会正常调用
SmartPtr<pair<string, string>> sp3(new pair<string, string>);
//如果sp3的new抛异常了,就就不会为sp3调用构造,sp1、sp2的析构会正常调用
div();
//如果执行到这里,div()抛异常了,sp1、sp2、sp3的析构都会正常调用
//无论new申请了多少资源,都是可以正确地释放的
}
int main()
{
try
{
f();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
把new申请资源交给具有生命周期的对象来管理,这种方式就叫做RAII。
RAII(Resource Acquisition Is Initialization,申请资源随即初始化)是一种利用对象生命周期来管理资源的简单技术(这些资源可以是内存、文件句柄、网络连接、互斥量等),可以使资源在对象构造时被获取且通过对象控制访问,资源在对象的生命周期内始终保持有效,最终在对象析构的时候被释放。
2.智能指针的原理
上文代码中的SmartPtr就是智能指针吗?实际并不是。
指针可以通过*解引用来得到所指的值,还可以通过->访问所指空间中的一些内容,SmartPtr并不具备指针的这些行为,所以只是一个可以生成管理资源对象的类,还远远不算上智能指针。
//以下代码中,SmartPtr加入了operator*()、 operator->(),
//基本具备了指针的行为
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<string> sp1(new string("xxxxx"));
cout << *sp1 << endl;
SmartPtr<pair<string, string>> sp2(new pair<string, string>("1111", "22222"));
cout << sp2->first << endl;
cout << sp2->second << endl;
return 0;
}
尽管在上文中,SmartPtr加入了operator*()、 operator->(),基本具备了指针的行为,但它想要成为智能指针,仍需面临一个拷贝问题:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<string> sp1(new string("xxxxx"));
SmartPtr<string> sp2(new string("yyyyy"));
sp1 = sp2;
//SmartPtr类中没有显示的赋值重载,编译器会为其生成默认的赋值重载,
//默认的赋值重载,是直接将一个对象中的值赋给另一个对象(浅拷贝)
//在这里,sp2中的指针赋给了sp1,会导致sp1中的指针丢失,sp1中指针管理的资源可能失控
return 0;
}
从以上代码中,sp1、sp2调用析构的情况来看,经过赋值后,sp1和sp2共享了同一份资源,它们分别去调用了一次析构,导致对同一份资源进行了两次delete,引发了异常。而在赋值前,sp1、sp2各有一份资源,对一份资源进行了两次delete,也意味着有一份资源没有被delete释放,存在内存泄漏的隐患。
关于这个拷贝问题的方案,前人主要探索出了四种,也对应了下文即将出现的四种智能指针,它们出现的顺序也是与智能指针的发展历史有关。
【Tips】智能指针的原理:
- 通过RAII思想管理资源;
- 具备像指针一样的行为(*解引用,->访问);
- 能够解决拷贝问题(不同智能指针的解决方案不同)。
二、C++98的auto_ptr
1.基本原理和用法
auto_ptr是解决上文提到的拷贝问题的第一个方案,但它在解决旧问题的同时又留下了新的问题。
对于上文提到的拷贝问题,auto_ptr给出的方案是管理权转移:如果发生拷贝,就将原先的资源转移给新的智能指针管理(还是通过拷贝),然后将原先的智能指针置空。具体的实现相当于在上文中的SmartPtr类中又加入一个显示的拷贝构造,在拷贝构造中将被拷贝的对象置空。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl; //验证调用了构造
}
~A()
{
cout << this;
cout << "~A()" << endl; //验证调用了析构
}
//private:
int _a;
};
int main()
{
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(new A(1));
//能正常调用构造和析构
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
int main()
{
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(ap1);
//如果发生拷贝,
//就将原先的资源转移给新的智能指针管理,
//然后将原先的智能指针置空
return 0;
}
但管理权转移发生后,拷贝的对象会被置空,此时再访问它就会导致程序崩溃。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
//private:
int _a;
};
int main()
{
auto_ptr<A> ap1(new A(1));
auto_ptr<A> ap2(ap1);
//拷贝时发生管理权转移
//用ap1拷贝ap3,把ap1资源的管理权给了ap3,使ap1自身置空了
ap3->_a++;//这句不会引发任何问题
cout << ap2->_a << endl;
ap1->_a++;//这句虽然编译可通过,但运行后程序会崩溃
//管理权转移:拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
//转移的隐患:导致被拷贝对象悬空,访问就会出问题
//所以使用auto_ptr,不应再使用被拷贝的对象
return 0;
}
//所以一般实践中,很多公司明确规定不要用auto_ptr
auto_ptr解决了浅拷贝带来的指针丢失、同一份资源被多次释放的旧问题,但遗留了被拷贝对象引起程序崩溃的新隐患,于是上了很多地方的黑名单,被明确规定禁用。
2.模拟实现
//c++98的auto_ptr
namespace CVE
{
template<class T>
class auto_ptr
{
public:
// 1、RAII思想管理资源
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
// 2、具备指针的行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//3、拷贝方案:管理权转移(通过显示的拷贝构造实现)
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
//被拷贝的对象必须置空,否则会造成资源被多次释放
//使用auto_ptr,不应再使用被拷贝的对象
}
private:
T* _ptr;
};
}
三、C++11的unique_ptr
1.基本原理和用法
对于上文提到的拷贝问题,unique_ptr给出的方案是:禁止拷贝。既然这个拷贝问题很难解决,那直接把问题干掉,不就不用解决了吗?具体实现是使用delete关键字在类中直接禁止编译器生成默认的拷贝构造和赋值重载。它虽然没有真正解决拷贝问题,但因为直接干掉了拷贝问题,所以还是相对安全的,可以运用在不需要拷贝的场景。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl; //验证调用了构造
}
~A()
{
cout << this;
cout << "~A()" << endl; //验证调用了析构
}
//private:
int _a;
};
int main()
{
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(new A(2));
//能正常调用构造和析构
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
int main()
{
unique_ptr<A> up1(new A(1));
unique_ptr<A> up2(up1);//unique_ptr简单粗暴,直接不让拷贝
return 0;
}
2.模拟实现
//C++11的unique_ptr
namespace CVE
{
template<class T>
class unique_ptr
{
public:
//1.RAII思想管理资源
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
//2.具备指针的行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//3.拷贝方案:禁止拷贝(delete关键字 - 默认成员函数只声明不实现)
unique_ptr(unique_ptr<T>& ap) = delete; //禁用拷贝构造
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;//禁用赋值重载
private:
T* _ptr;
};
}
四、C++11的shared_ptr
1.基本原理和用法
shared_ptr也是C++11中的智能指针,不同于unique_ptr禁止拷贝,shared_ptr既支持拷贝,又彻底解决了auto_ptr的毛病。
shared_ptr给出的拷贝方案是:引用计数。通过引用计数,来记录管理某一份资源的智能指针对象的具体数量,当计数为0时(意味着这份资源目前没有任何智能指针来管理),就允许释放资源。
具体的实现方式是,在类中增加一个指向堆上空间(这个空间是动态申请的)的指针作成员变量,既使每个对象拥有属于自己的独立的计数,同时又使拷贝对象和被拷贝对象共同管理着同一个计数指针(不同的对象指向同一份资源,这些对象应该有相同的引用计数。要实现这一点,不能使用整型变量,因为整型变量只能做到让每个对象有属于自己的引用计数;也不能使用静态变量,因为static只能做到让不同的对象共享同一个引用计数;只有指向堆空间的指针能满足需求。图解见下文)。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
int main()
{
shared_ptr<A> sp1(new A(1));
shared_ptr<A> sp2(new A(2));
//能正常调用构造和析构
shared_ptr<A> sp3(sp1);
sp3->_a++;
cout << sp3->_a << endl;
sp1->_a++;
cout << sp1->_a << endl;
//解决了auto_ptr的毛病
//拷贝的对象能正常管理和访问资源了
return 0;
}
2.模拟实现
//C++11的shared_ptr
namespace CVE
{
template<class T>
class shared_ptr
{
public:
//1.RAII思想管理资源
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
~shared_ptr()
{
if (--(*_pcount) == 0)//计数减到0时才释放
{
delete _ptr;
delete _pcount;
}
}
//2.具备指针的行为
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//3.拷贝方案:引用计数
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);//每拷贝构造一次,就计数一次
}
//赋值重载稍复杂
//要分情况讨论:
//1)参与赋值双方指向不同资源:
// 例如:sp1 = sp2
// 需在sp1指向别的空间/sp2的空间之前,要把sp1自己原本的计数减掉,否则会造成内存泄漏
//2)参与赋值双方指向相同资源(相当于自己赋值给自己):
// 不必赋值
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr) //这里是在处理情况2
return *this;
//左右操作数管理同一块资源(自己赋值自己),就没必要继续赋值了
//如果继续走下面的代码,
//可能在自己赋值自己的过程中把自己释放掉了,留下一堆随机值(野指针风险)
//先为左操作数减计数,
//(在这之后如果计数为0,就释放左操作数原有的资源)
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
//再让左操作数指向右操作数的资源,且这份资源的计数应+1
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
//最终返回左操作数
return *this;
}
//4.其他功能
int use_count() const//获取引用计数的指针
{
return *_pcount;
}
T* get() const//获取指向资源的指针
{
return _ptr;
}
private:
//两个指针,一个指针指向资源,一个指针指向计数
T* _ptr;
int* _pcount;
//用一个指向动态空间的指针来实现独立的计数,使每个对象都拥有自己的计数
//同时,使拷贝对象和被拷贝对象共同管理同一个计数指针
};
}
3.定制删除器
当shared_ptr对象管理的是一个new申请的数组时,应该匹配地使用delete []释放,而非delete。所以为了使不同类型的shared_ptr对象能匹配不同的释放方式,C++库中为shared_ptr提供了定制删除器(它在shared_ptr的构造函数中;其中“D del”接收就是一个删除器,删除器可以是函数指针、仿函数、lambda表达式等),通过仿函数来为不同的shared_ptr对象匹配它们合适的释放方式。
//测试shared_ptr的定制删除器
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
template<class T>
struct FreeFunc {
void operator()(T* ptr)
{
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr)
{
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
int main()
{
FreeFunc<int> freeFunc;
std::shared_ptr<int> sp1((int*)malloc(2), freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
std::shared_ptr<int> sp2((int*)malloc(2), deleteArrayFunc);
std::shared_ptr<A> sp3(new A[2], [](A* p) {delete[] p; });
return 0;
}
//将定制删除器写入上文模拟实现shared_ptr的代码中
namespace CVE
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
//1.要在构造内控制析构的删除方式,首先想到借助类的成员变量,
// 使指定的删除方式可以被析构接收
// 但成员变量的类型不确定就无法定义成员变量
//2.“function<void(T*)> _del;”
// void(T*)意味着,无论作为内部参数的指针是什么类型,函数的返回值始终都是确定的void
// 这样就可以用一个包装器来定义接收删除方式的成员变量
template<class D>
shared_ptr(T* ptr,D del)
: _ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
~shared_ptr()
{
if (--(*_pcount) == 0)//计数减到0时才释放shared_ptr的类对象
{
_del(_ptr);
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);//每拷贝构造一次,就计数一次
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr == sp._ptr)
return *this;
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;
_pcount = sp._pcount;
(*_pcount)++;
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
//用一个包装器实现构造控制析构的删除方式
//设置一个lambda表达式作为缺省值
};
}
4.线程安全问题
shared_ptr涉及的线程安全分为两方面:
- 同一份资源的引用计数是由多个shared_ptr对象同时管理的,如果在两个线程中shared_ptr的引用计数同时++或--,这个操作将不是原子性的。例如某一份资源的引用计数原来是1,被不同的对象一共++了两次,可能还是1。这样一来引用计数就错乱了,会有导致内存泄漏或者程序崩溃的隐患。所以shared_ptr中对引用计数++和--是需要加锁的,也就是说引用计数的操作必须是线程安全的。
- shared_ptr管理的资源是从堆空间申请的,如果有两个线程中同时去访问这份资源,也会有线程安全的隐患。
总得来说,为了保证线程安全,必须在引用计数的++和--时就保证原子性,具体的实现方式是shared_ptr内部带有互斥锁。
//将互斥量写入上文模拟实现shared_ptr的代码中
#include<mutex>
namespace CVE
{
template<class T>
class shared_ptr
{
public:
//引用计数++
void addCount()
{
//必须先上锁再++
_pmtx->lock();
(*_pcount)++;
//完事了要解锁
_pmtx->unlock();
}
//“引用计数--” + “控制资源释放”
void subCount()
{
_pmtx->lock();
int flag = 0;//管理锁的释放
//引用计数减为0时就释放资源
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
flag = 1;//同时将flag置为1
}
//只有当flag为1时才释放锁
_pmtx->unlock();//必须先解锁,再释放锁
if (flag)
delete _pmtx;
}
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmtx(new mutex)
{}
template<class D>
shared_ptr(T* ptr, D del)
: _ptr(ptr)
, _pcount(new int(1))
,_del(del)
{}
~shared_ptr()
{
subCount();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
addCount();//引用计数++
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp._ptr)
{
subCount();//旧资源引用计数--
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
addCount();//新资源引用计数++
}
return *this;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr) {delete ptr; };
std::mutex* _pmtx;//互斥锁
};
}
5.循环引用问题
shared_ptr虽然支持拷贝且解决了auto_ptr,但并非十全十美,它仍存在一个名为“循环引用”的问题,例如发生在下面这样一个情景中:
//用shared_ptr管理双向链表的节点
//要将节点首尾相连(因为是双向链表),只需对节点的前驱指针和后继指针赋值
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
//双向链表节点
struct Node
{
A _val;
Node* _next;
Node* _prev;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
//此处类型不匹配,前驱指针和后继指针是内置类型,sp1和sp2是自定义类型
//自定义类型无法赋给内置类型
}
//将链表节点的类型改为shared_ptr就支持赋值了
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
struct Node
{
A _val;
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
sp1->_next = sp2;
sp2->_prev = sp1;
// 但此时无法正常调用析构
// 释放在逻辑上发生了死循环,会引起内存泄漏
// 这种情况就是循环引用
}
Node内部使用了shared_ptr作为前驱指针和后继指针,当“sp1->_next = sp2;”被执行时,sp2中的引用计数会+1;同理,当“sp2->_next = sp1;”被执行时,sp1中的引用计数也会+1。
这就会导致析构无法正常调用。按照析构的顺序,sp2应该先析构,sp2中的引用计数理应-1,但sp1->_prev还指向sp2,会使sp2的引用计数始终为1,而无法正常为sp2调用析构;然后sp1再析构,也会因为sp2->_next还指向sp1,使sp1的引用计数始终为1,无法正常为sp1调用析构。
前人把这种因“套娃”而无法正常调用析构来释放资源的情况称之为“循环引用”。
循环引用一直是shared_ptr的死穴,仅凭shared_ptr本身难以解决,而作为解决问题的尝试,C++11又提供了weak_ptr。
五、C++11的weak_ptr
1.基本原理和用法
weak_ptr是不具有RAII思想的智能指针,专门用来解决shared_ptr的循环引用问题,具体的实现方式是weak_ptr不增加引用计数。weak_ptr的构造和赋值都要由shared_ptr支持,所以它实际不参与资源的释放,一般仅通过它访问资源。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << "~A()" << endl;
}
//private:
int _a;
};
struct Node
{
A _val;
weak_ptr<Node> _next;
weak_ptr<Node> _prev;
// weak_ptr是不具有RAII思想的智能指针,专门用来解决shared_ptr循环引用问题
// weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl;//use_count()是shared_ptr的一个成员函数,
cout << sp2.use_count() << endl;//可以查看引用计数
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
2.模拟实现
//C++11的weak_ptr
namespace CVE
{
template<class T>
class shared_ptr
{
//shared_ptr模拟实现的代码见上文
};
template<class T>
class weak_ptr
{
public:
//weak_ptr不支持RAII
//它无法独立进行管理,它的构造和赋值都要由shared_ptr支持
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
补、智能指针的发展历史
C++98中的auto_ptr,但在拷贝构造或者赋值的时候,原本的auto_ptr会被置空,存在非常大的缺陷,于是上了很多地方的黑名单。
C++11中的unique_ptr,禁止了拷贝和赋值,直接避免了auto_ptr可能存在的缺陷,但它不支持拷贝和赋值。
后续,C++11又提供了shared_ptr,通过引用计数的方式解决了unique_ptr不能拷贝和赋值的缺陷,且通过互斥锁保证了shared_ptr本身的线程安全,但它存在循环引用的问题。
为了解决shared_ptr的循环引用问题,C++11又通过“仅指向不管理”的方式提供了weak_ptr智能指针。
智能指针们各有各的特色,在使用的时候,根据具体情景选择合适的智能指针即可(但最好别使用auto_ptr,最好别)。
补、智能指针与boost库
C++委员会曾发行过一个名为boost的库(相当于是C++标准库的先行版,其中是很多实验性质的内容。它作为标准库的后备,进一步完善了标准库),其中优质的内容基本被C++标准库收录了。标准库中的智能指针就是参照boost库中的智能指针,再加以修改而来的。Boost库中有三个智能指针,scoped_ptr、shared_ptr、weak_ptr,使c++11后来有了unique_ptr、shared_ptr、weak_ptr。
C++的智能指针和boost的智能指针的关系:
- C++ 98 中产生了第一个智能指针auto_ptr;
- C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr;
- C++ TR1,引入了shared_ptr等(TR1并不是标准版);
- C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。其中,unique_ptr对应boost中的scoped_ptr,且这些智能指针的实现原理参考了boost。
补、C++与Java关于内存管理方案的比较
Java虽然没有指针,但有引用。用Java语言new了一个空间之后,并不会返回这个空间的地址,而返回的是这个空间的引用。但Java引用跟C++引用不一样的地方在于,Java引用跟C++指针类似,可以改变它本身的指向。这也导致Java中也存在类似C++中指针的问题。
但Java没有智能指针,而有垃圾回收器。垃圾回收指的是,new了以后不需要手动delete。在Java程序的后台,都有一个垃圾回收器,它会把申请的空间都记录下来,等这个空间不用了就自动释放掉。
C++没有垃圾回收器,而有智能指针。这既是因为垃圾回收的成本很高,也是因为C++程序的运行机制跟Java程序是不一样的。
C++编译好的程序就是一个一个的进程,它们采用CPU调度的机制,直接在操作系统之上运行。而Java不是这样的,要运行Java程序得先安装Java的虚拟机。Java的虚拟机可以看作是跑在操作系统上面的一个进程(这也是为什么,一些对性能极致要求的程序一般不会考虑用Java来写,例如游戏的一些服务器、物联网设备上的程序)。