跳到主要内容C++ 智能指针:使用场景、实现原理与内存泄漏防治 | 极客日志C++算法
C++ 智能指针:使用场景、实现原理与内存泄漏防治
C++ 智能指针基于 RAII 机制自动管理动态资源,有效防止内存泄漏。涵盖智能指针的使用场景、RAII 设计思路、标准库智能指针(auto_ptr、unique_ptr、shared_ptr、weak_ptr)的区别与用法。详细解析 shared_ptr 引用计数原理、自定义删除器实现及循环引用问题的弱指针解决方案。最后总结内存泄漏的危害与预防策略,提供工程化最佳实践建议。
暗影行者16 浏览 一、智能指针的使用场景分析
C++ 是公认的高效编程语言,手动内存管理(new/delete)避免了高级语言自动内存管理的开销,但这把双刃剑对程序员要求更高。手动管理容易出现内存泄漏,尤其在异常处理场景中。
下面程序中可以看到,new 以后虽然调用了 delete,但因为抛出异常导致后面的 delete 没有得到执行,造成内存泄漏。如果 array2 new 的时候抛异常,还需要套一层捕获释放逻辑,否则代码极其臃肿。智能指针放到这样的场景里就让问题简单多了。
double Divide(int a, int b) {
if (b == 0) throw "Divide by zero condition!";
else return (double)a / (double)b;
}
void Func() {
int* array1 = new int[10];
int* array2 = new int[10];
try {
int len, time; cin >> len >> time; cout << Divide(len, time) << endl;
} catch (...) {
cout << "delete []" << array1 << endl;
cout << "delete []" << array2 << endl;
delete[] array1;
delete[] array2;
throw;
}
cout << "delete []" << array1 << endl;
[] array1;
cout << << array2 << endl;
[] array2;
}
{
{
();
} ( * errmsg) { cout << errmsg << endl; }
( exception& e) { cout << e.() << endl; }
(...) { cout << << endl; }
;
}
delete
"delete []"
delete
int main()
try
Func
catch
const
char
catch
const
what
catch
"未知异常"
return
0
二、RAII 和智能指针的设计思路
RAII 是 Resource Acquisition Is Initialization(资源获取立即初始化) 的缩写,是一种管理资源的类的设计思想。本质是利用对象生命周期来管理获取到的动态资源,避免资源泄漏。这里的资源可以是内存、文件指针、网络连接、互斥锁等等。
RAII 在获取资源时把资源委托给一个对象 (构造一个对象),接着控制对资源的访问。资源在对象的周期内始终保持有效,最后不论是正常退出函数栈帧还是抛异常退出栈帧 (退出栈帧时析构函数一定会被调用),都会在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
• 智能指针类除了满足 RAII 的设计思路,还要方便资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/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; }
T& operator[](size_t i){ return _ptr[i]; }
private:
T* _ptr;
};
double Divide(int a, int b) {
if (b == 0) throw "Divide by zero condition!";
else return (double)a / (double)b;
}
void Func() {
SmartPtr<int> sp1(new int[10]);
SmartPtr<int> sp2(new int[10]);
for(size_t i = 0; i < 10; i++) {
sp1[i] = sp2[i] = i;
}
int len, time; cin >> len >> time; cout << Divide(len, time) << endl;
}
int main() {
try {
Func();
} catch (const char* errmsg) { cout << errmsg << endl; }
catch (const exception& e) { cout << e.what() << endl; }
catch (...) { cout << "未知异常" << endl; }
return 0;
}
三、C++ 标准库智能指针的使用
C++ 标准库中的智能指针都在< memory >这个头文件下面,我们包含< memory >就可以使用了。智能指针有好几种,除了 weak_ptr 他们都符合 RAII 和像指针一样访问的行为,原理上而言主要是解决智能指针拷贝时的思路不同。
auto_ptr 是 C++98 时设计出来的智能指针,因为指针不能像普通内置类型对象那样拷贝,这样会让两个指针指向同一空间,也不能深拷贝,这样同一指针指向两块不同的空间,和指针的特性相背离。所以这时 auto_ptr 的设计思路是破坏性拷贝:拷贝直接拿走资源,原指针会被强制置为 nullptr(失去所有权),只有新指针指向资源 (该行为和移动拷贝构造类似,但是移动拷贝构造事先知道原对象会被置为空)。这是一个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题。C++11 设计出新的智能指针后,强烈建议不要使用 auto_ptr。其他 C++11 出来之前很多公司也是明令禁止使用这个智能指针的。
unique_ptr 是 C++11 有了移动语义后设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点是不支持拷贝,只支持移动 (移动构造和移动赋值),移动的前提必须是右值。如果不需要拷贝的场景就非常建议使用他。
shared_ptr 也是 C++11 设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动,移动构造的前提也是只能移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的,所以它是有一定代价的,如果是需要移动的场景更推荐使用 unique_ptr。引用计数就像一个'使用计数器',shared_ptr 通过它实现了多个指针安全共享同一份资源:有新指针共享资源时,计数增加;有指针不再使用资源时,计数减少;计数为 0 时,自动释放资源。这种机制既允许资源被多个指针共享,又能保证资源只被释放一次,从根本上避免了'双重释放'和'内存泄漏'问题。
weak_ptr 也是 C++11 设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持 RAII,也就意味着不能用它直接管理资源,weak_ptr 的产生本质是要解决 shared_ptr 的一个循环引用导致内存泄漏的问题。具体细节下面我们再细讲。
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; }
};
int main() {
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1);
unique_ptr<Date> up1(new Date);
unique_ptr<Date> up3(move(up1));
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1);
shared_ptr<Date> sp3(sp2);
cout << sp1.use_count() << endl;
sp1->_year++;
cout << sp1->_year << endl;
cout << sp2->_year << endl;
cout << sp3->_year << endl;
shared_ptr<Date> sp4(move(sp1));
return 0;
}
1、make_shared
shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared。
这一可变参数模板用于初始化资源对象的值直接构造,make_shared 相比手动申请资源效率更高,因为它会将计数器和申请的资源放在一起,优化了内存碎片问题。
make_shared 不是 std::shared_ptr 的成员函数,而是一个独立的非成员函数模板,定义在 head 文件中。
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
2、operator bool
shared_ptr 和 unique_ptr 都支持了 operator bool 的类型转换,operator bool 可以进行类型显示转换,将 shared_ptr 和 unique_ptr 转换为 bool 类型。如果智能指针对象是一个空对象没有管理资源,则返回 false,否则返回 true,意味着我们可以直接把智能指针对象给 if 判断是否为空。
operator bool 是 shared_ptr 和 unique_ptr 的成员函数。
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)
cout << "sp1 is not nullptr" << endl;
if(!sp4) cout << "sp1 is nullptr" << endl;
3、构造函数用 explicit 修饰
标准库中 shared_ptr 和 unique_ptr 的构造函数都使用了 explicit 修饰,可以阻止普通指针隐式类型转换成智能指针对象,是 C++ 标准为了强制开发者显式管理内存所有权而设计的安全机制。
shared_ptr<Date> sp5 = new Date(2024, 9, 11);
unique_ptr<Date> sp6 = new Date(2024, 9, 11);
关于定制删除器:
智能指针析构时默认是进行 delete 释放资源,这也就意味着如果不是 new 出来的资源,交给智能指针管理,析构时就会崩溃。
我们知道 new 必须和 delete 匹配,new[] 必须和 delete[] 匹配,其他申请资源的方式也需要用和它匹配的方式释放资源,如果错配系统就会崩溃。所以为了解决这一问题,智能指针支持在构造时给一个删除器,所谓删除器本质上就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。因为 new[] 经常使用,所以为了简洁一点,unique_ptr 和 shared_ptr 都特化了一份 [] 的版本,使用如下方式就可以管理 new [] 的资源。
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
关于 C++ 标准库的定制删除器,unique_ptr 和 shared_ptr 支持删除器的方式有所不同,unique_ptr 是在类模板参数支持的,shared_ptr 是构造函数参数支持的:
删除器的本质就是可调用对象,所有可调用对象类型都可以只通过构造函数参数传递,但是类模板参数传递时只有仿函数可以只通过类模板参数,因为仿函数是自定义的类/结构体,类型可显式命名并且支持默认构造,所以仿函数定义的对象可以直接使用。函数指针和 lambda 表达式不仅要通过类模板参数传递,还需要通过构造函数参数传递。其中函数指针是因为'类型'与'值'分离,函数指针类型定义出的对象的空指针,需要在构造函数参数部分显示传递实例函数指针。lambda 表达式是因为类型无法显式命名,所以只能通过 decltype 推导对象类型再传递,并且 lambda 表达式没有默认构造能力,因此必须通过构造函数传递具体的 lambda 实例。
所以在需要使用定制删除器的场景更推荐使用 shared_ptr。
auto delArrOBJ = [](Date* ptr){ delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ)
四、智能指针的原理
下面我们模拟实现了 auto_ptr 和 unique_ptr 的核心功能,这两个智能指针的实现比较简单,大家了解一下原理即可。这里提一下 unique_ptr 禁用拷贝构造和拷贝赋值的实现,因为拷贝构造和拷贝赋值是默认成员函数,我们不写编译器会自动生成,所以需要显示写出拷贝构造和拷贝赋值并把它们=delete,这样才能彻底禁用它们。
namespace wusaqi {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr):_ptr(ptr){}
auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if(this != &ap) {
if(_ptr) delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr(){
if(_ptr) {
cout <<"delete:"<< _ptr << endl;
delete _ptr;
}
}
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
private:
T* _ptr;
};
template<class T>
class unique_ptr {
public:
explicit unique_ptr(T* ptr):_ptr(ptr){}
~unique_ptr(){
if(_ptr) {
cout <<"delete:"<< _ptr << endl;
delete _ptr;
}
}
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
unique_ptr(unique_ptr<T>&& sp):_ptr(sp._ptr){ sp._ptr = nullptr; }
unique_ptr<T>& operator=(unique_ptr<T>&& sp) {
delete _ptr;
_ptr = sp._ptr;
sp._ptr = nullptr;
}
private:
T* _ptr;
};
}
大家要重点关注 shared_ptr 是如何设计的,下面分步拆解:
1、首先实现出智能指针的基本功能
我们要明确智能指针通常是用来管理动态申请的资源的,所以析构需要把资源 delete 掉,并且析构前需要先判断资源是否为空,不为空再 delete。
namespace wusaqi {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr =nullptr):_ptr(ptr){}
~shared_ptr(){
if(_ptr) {
delete _ptr;
_ptr = nullptr;
}
}
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
private:
T* _ptr;
};
}
2、引用计数功能实现
首先我们能想到的是创建一个静态成员变量计数器来记录有多少指针指向同一资源,但其实是行不通的,因为这样的话所有同类型对象都共享同一计数器,而我们期望的是对每份资源各自分配一个独立的计数器来记录指向这份资源的引用对象。
所以实现计数器的最佳思路是创建一个 int* 类型的指针变量,用该指针指向一个动态开辟的整型变量,用该整型变量来充当计数器。
这里有个问题,这个整型变量为什么要动态开辟呢?原因是该整型变量的生命周期要和该整型变量所在的 shared_ptr 对象管理的资源生命周期匹配,交由程序员控制。
接着优化其他函数接口,构造函数增加初始化_pcount。
拷贝构造要把两个成员变量初始化为和源对象的成员变量一样,并且拷贝构造一次就需要把计数器加一,所以在拷贝构造体内还需要把计数器加一。
析构函数这里不用再关注_ptr 是否为空,因为 delete 空指针是安全的,这里主要关注计数器是否为 0,为 0 才调用析构函数。并且每次调用析构函数都需要把计数器减一。
namespace wusaqi {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}
shared_ptr(const shared_ptr<T>& ptr):_ptr(ptr._ptr),_pcount(ptr._pcount){
++(*_pcount);
}
~shared_ptr(){
if(--(*_pcount)==0) {
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
private:
T* _ptr;
int* _pcount;
};
}
int main() {
wusaqi::shared_ptr<Date> ptr1 = new Date;
wusaqi::shared_ptr<Date> ptr2 = ptr1;
wusaqi::shared_ptr<Date> ptr3 = new Date;
return 0;
}
3、赋值运算符重载的实现
首先要把两个成员变量给给赋值对象、计数器加一这是能想到的,但是仅这样就足够了吗?以上面图片为例,把 sp3 赋值给 sp1:sp1 = sp3,让 sp1 也指向左边的资源,那么 sp1 之前指向的左边资源怎么办呢?所以我们还需要处理 sp1 之前指向的资源,这里是示例我们不需要处理左边的资源,因为还有 sp2 在维护,如果 sp1 是最后一个指向左边资源的指针,我们就需要把它释放,所以赋值操作是有可能释放资源的。具体的做法就是赋值前把被赋值对象计数器减一,如果等于 0 就需要释放被赋值对象指向的资源,否则不需要做处理。
除此之外还需要处理自己给自己赋值的问题,智能指针这里自赋值有两种情况,两个相同对象 s1 = s1 是自赋值,两个不同对象 s1 = s2 也是自赋值,因为 s1、s2 指向同一份资源,所以判断自赋值不能只判断两个对象的 this 指针:this != &sp,而应该判断两个对象是否指向同一份资源:_ptr != sp._ptr。
赋值运算符重载和析构函数都要用同一份代码,所以我们把它封装成 release 函数。
void release(){
if(--(*_pcount)==0) {
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if(_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
4、定制删除器的实现
至此,shared_ptr 大部分功能我们已经实现完毕,还有最后一个问题现在 shared_ptr 释放资源逻辑被写死成 delete,所以只支持释放 new 出来的资源,如果要释放通过 new[]、malloc 等等其他方式申请的资源程序就会崩。前面介绍了标准库为了解决这一问题专门为 unique_ptr 和 shared_ptr 都特化了一份 [] 的版本,但是这只适用于 new[] 申请的资源,如果是 malloc 出来的资源还需要用 free,所以又引入自定义删除器,关于自定义删除器具体说明在前面介绍 C++ 标准库智能指针的使用部分,这里我们聚焦为 shared_ptr 实现一个定制删除器。
首先因为删除器有可能有多种类型,所以我们还需要再创建一个带模板参数 D 的专门用于接受传递删除器的构造函数,但是如果我们直接用 D 类型对象_del 接受传递的删除器就会导致_del 的作用域只在构造函数内部,无法在释放资源的时候使用,所以我们还需要用一个能作用于整个类的成员变量接受删除器参数,用 function 类型成员变量就是这里的首选,它可以用来接受各种删除器类型,然后将释放资源:delete_ptr 改为 使用删除器释放资源:_del(_ptr)。如果我们没有显示传递删除器就需要用到缺省值,这里我们在成员变量声明处给一个 lambda 表达式缺省值,默认 delete 释放资源。这里不能在构造函数:shared_ptr(T* ptr, D del) 给删除器缺省值,这样就会依发传递单参数时的二义性,普通构造和模板构造都能匹配,所以这里的最佳实践是在成员变量声明处给删除器缺省值,普通构造参数位置给 ptr 缺省值,这样传递 0 个或 1 个参数时匹配普通构造,使用默认删除器,传递 2 个参数时匹配模板构造,使用用户定制的删除器。
shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}
template<class D>
shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}
void release(){
if(--(*_pcount)==0) {
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
private:
T* _ptr = nullptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr){ delete ptr; };
}
shared_ptr源码
namespace wusaqi {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}
template<class D>
shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}
shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_del(sp._del){
++(*_pcount);
}
void release(){
if(--(*_pcount)==0) {
_del(_ptr);
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
if(_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
_del = sp._del;
++(*_pcount);
}
return *this;
}
~shared_ptr(){ release(); }
T* get() const { return _ptr; }
int use_count() const { return *_pcount; }
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
private:
T* _ptr;
int* _pcount;
function<void(T*)> _del = [](T* ptr){ delete ptr; };
};
}
五、shared_ptr 和 weak_ptr
shared_ptr 循环引用问题
shared_ptr 大多数情况下管理资源非常合适,支持 RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用 weak_ptr 解决这种问题。
如下图所述场景,n1 和 n2 析构后,管理两个节点的引用计数减到 1。
struct ListNode {
int _data;
std::shared_ptr<ListNode> _next;
std::shared_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;
}
上面代码中我们可以看到 ListNode 结构体的的_next 和_prev 指针类型并不是它自己的结构体类型,而是 shared_ptr 指针类型,原因是这个示例中两块资源是由智能指针维护的,而智能指针是不支持隐式类型转换的,所以只有当_next 和_prev 的类型也为智能指针类型时才能实现互相指向的操作。
右边的节点什么时候释放呢,左边节点中的_next 管着呢,_next 析构后,右边的节点就释放了。_next 什么时候析构呢,_next 是左边节点的成员,左边节点释放,_next 就析构了。左边节点什么时候释放呢,左边节点由右边节点中的_prev 管着呢,_prev 析构后,左边的节点就释放了。_prev 什么时候析构呢,_prev 是右边节点的成员,右边节点释放,_prev 就析构了。
至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。
循环引用的核心本质确实可以简化理解为:两块资源互相通过强引用(如 shared_ptr)持有对方,导致各自的引用计数无法减到 0,因此都不会被释放,最终造成内存泄漏。
把 ListNode 结构体中的_next 和_prev 改成 weak_ptr,weak_ptr 绑定到 shared_ptr 时不会增加它的引用计数,_next 和_prev 不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题。
weak_ptr
weak_ptr 不支持 RAII,也不支持访问资源,所以我们看文档发现 weak_ptr 构造时不支持绑定到资源,只支持绑定到 shared_ptr,绑定到 shared_ptr 时,不增加 shared_ptr 的引用计数,那么就可以解决上述的循环引用问题。
std::weak_ptr<ListNode> wp(new ListNode);
struct ListNode {
int _data;
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 也没有重载 operator* 和 operator-> 等,因为他不参与资源管理,那么如果他绑定的 shared_ptr 已经释放了资源,那么它通过其他方式绕开限制去访问资源就是很危险的。weak_ptr 支持 expired 检查指向的资源是否过期,use_count 也可获取 shared_ptr 的引用计数,weak_ptr 想访问资源时,可以调用 lock 返回一个管理同一资源的 shared_ptr,如果资源已经被释放,返回的 shared_ptr 是一个空对象,如果资源没有释放,则通过返回的 shared_ptr 访问资源是安全的。
以下是具体解释:避免访问已销毁的资源(解决悬垂指针问题)
weak_ptr 不持有资源的所有权,因此它指向的资源可能在任意时刻被最后一个 shared_ptr 释放(此时资源已销毁)。
如果允许 weak_ptr 直接访问资源(比如重载 operator* 或 operator->),就可能出现'悬垂指针'问题:访问已经被销毁的内存,导致程序崩溃或未定义行为。
而 lock() 方法的本质是'原子性检查并获取资源的临时所有权':
若资源未被释放,lock() 返回一个有效的 shared_ptr(此时引用计数 +1,保证资源在该 shared_ptr 生命周期内不会被销毁);
若资源已被释放,lock() 返回空的 shared_ptr,此时访问会被安全拦截(不会操作无效内存)。符合'所有权管理'的设计逻辑。
C++ 智能指针的核心是'所有权'管理:shared_ptr 通过引用计数明确资源的所有者,所有者存在则资源存活,所有者消失则资源销毁。weak_ptr 作为'观察者',其设计目的是不干扰所有权(因此不增加引用计数)。如果允许它直接访问资源,就绕过了 shared_ptr 的所有权机制,破坏了'只有所有者才能安全操作资源'的逻辑。通过 lock() 转为 shared_ptr 后,相当于临时获取了资源的所有权(引用计数 +1),此时访问资源才符合'所有权决定访问权限'的设计原则,确保了资源管理的一致性。
namespace wusaqi {
template<class T>
class weak_ptr {
public:
weak_ptr(){}
weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}
weak_ptr<T>& operator=(const shared_ptr<T>& sp) { _ptr = sp.get(); return *this; }
private:
T* _ptr = nullptr;
};
}
int main() {
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp1 = make_shared<string>("222222");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
sp2 = make_shared<string>("333333");
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
wp = sp1;
auto sp3 = wp.lock();
cout << wp.expired() << endl;
cout << wp.use_count() << endl;
*sp3 += "###";
cout << *sp1 << endl;
return 0;
}
六、内存泄漏
什么是内存泄漏,内存泄漏的危害
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常释放程序未能执行导致的。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。
如何避免内存泄漏
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。尽量使用智能指针来管理资源,如果自己场景比较特殊,采用 RAII 思想自己造个轮子管理。定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。总结一下:内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online