内存泄漏和智能指针
约 4445 字大约 15 分钟
2025-06-21
我们在异常代码中留下了一个坑没填,就是下面这个代码关于 array 多次释放资源的问题。
#include <iostream>
using namespace std;
double Division(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition";
}
return (double)a / (double)b;
}
void Function()
{
int* array_1 = new int[10];//(6)new 本身也会抛出异常
int* array_2 = new int[10];//(7)new 本身也会抛出异常
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << '\n';//(1)假设抛出“除零异常”
}
catch (...)//(2)“除零异常”被代码拦截下来
{
//(3)先释放 array,防止内存泄露
cout << "delete_1[] " << array_1 << '\n';
delete[] array_1;
cout << "delete_2[] " << array_2 << '\n';
delete[] array_2;
throw;//(4)重新将异常抛出去
}
cout << "delete_1[] " << array_1 << '\n';
delete[] array_1;
cout << "delete_2[] " << array_2 << '\n';
delete[] array_2;
}
int main()
{
try
{
Function();
}
catch (const char* error)//(5)“除零错误”会在这里被接收
{
cout << error << endl;
}
catch (const exception& e)//(8)new 的异常可以在这里被接收
{
cout << e.what() << '\n';
}
return 0;
}这样捕捉异常就会十分困难,稍不注意就会导致内存泄漏,因此我们有必要学习智能指针。
1.内存泄漏分类
要解决这一痛病,就需要“对症下药”,那常见的内存泄露有哪一些呢?C/C++ 程序中一般我们关心两种方面的内存泄漏:
- 堆内存泄漏(
Heap leak) :堆内存是指程序执行中,通过malloc、calloc、realloc、new等函数或关键字,从堆空间中分配的某块内存。该资源被用完后就必须通过调用对应的free或者delete释放掉。假设由于程序的设计错误,导致这部分内存资源没有被正常释放,那么以后这部分空间将无法再被使用(持续占用),就会产生内存泄漏 - 系统资源泄漏(
system resource leak):指程序使用系统分配的资源,比如使用了:方套接字、文件描述符、管道等,没有使用对应的函数释放掉,导致系统资源的浪费,严重时会导致系统效能减少,系统执行不稳定,资源逐渐减少
2.内存泄漏应对
2.1.事前预防型
- 工程前期指定好良好的设计规范,编码过程中养成良好的编码规范,申请的内存空间需要匹配者释放(但如果碰上带有异常的代码,就算注意释放了,可能还会出现问题)
- 采用
RAII思想和智能指针来管理资源
2.2.事后查错型
- 有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项
- 出问题了使用内存泄漏工具检测(不过很多工具都不够靠谱,或者收费昂贵)
3.内存泄漏实践
3.1.智能指针模拟
实际上智能指针的原理没那么复杂,我们可以简单实现一个智能指针,让指针在是否抛出异常的情况下都可以正常释放,进而解决内存泄漏的问题。
#include <iostream>
using namespace std;
class SmartPtr
{
public:
SmartPtr(int* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "delete " << _ptr << '\n';
}
private:
int* _ptr;
};
int main()
{
int* p = new int;
SmartPtr sp(p);//交给智能指针管理
return 0;
}实际上这就是 RAII(Resource Acquisition Is Initialization) 技术,即:“资源请求立即初始化”。
这翻译什么意思?好像和上面用到的东西没什么太大关联呀?实际上,这里的资源指 new 一类的调用,初始化就是将获取到的资源放入到构造函数内。
因此 RAII 实际就是一种利用对象生命周期控制持续资源(例如:内存、文件句柄、网络连接、互斥量等等)的技术。
在 C++ 中体现为“在对象生命周期结束时使用析构函数释放”。实际上就是将管理资源的责任转移给了一个对象,这样做的好处很明显:
- 无需显式释放资源,将释放资源的责任交给编译器,而不是
C++程序员 - 对象在生命周期内始终保持有效
库中的智能指针就是类似的原理(RAII 是这类技术的统称,比如:智能指针、锁的守卫等具体实现)。
但是生成的智能指针对象需要像指针一样被用户使用,这就是库中智能指针和我们实现的简易智能指针的最大区别(因此重载 * 和 -> 和 () 等符号就尤为重要、以及关于智能指针对象拷贝、delete[] 实现的问题)。
不过在考虑拷贝问题之前,我们先来试着将我们自己的 SmartPtr{}; 改造为一个“指针”,也就是重载两个和指针脱不了干系的符号:* 和 ->,并且改为类模板。
#include <iostream>
using namespace std;
struct Data
{
Data()
: _data_1(1), _data_2(2), _data_3(2)
{}
int _data_1;
int _data_2;
int _data_3;
};
template <typename Type>
class SmartPtr
{
public:
SmartPtr(Type* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "delete " << _ptr << '\n';
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
private:
Type* _ptr;
};
int main()
{
SmartPtr<int> sp_1(new int[10]);//交给智能指针管理
(*sp_1) = 5;
SmartPtr<Data> sp_2(new Data);
sp_2->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1
sp_2->_data_2 = 10;
sp_2->_data_3 = 10;
return 0;
}行为果然很像指针,接下来我们尝试想指针一样赋值或初始化,可以发现代码会奔溃:
#include <iostream>
using namespace std;
struct Data
{
Data()
: _data_1(1), _data_2(2), _data_3(2)
{}
int _data_1;
int _data_2;
int _data_3;
};
template <typename Type>
class SmartPtr
{
public:
SmartPtr(Type* ptr)
: _ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "delete " << _ptr << '\n';
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
private:
Type* _ptr;
};
int main()
{
SmartPtr<int> sp_1(new int[10]);//交给智能指针管理
(*sp_1) = 5;
SmartPtr<Data> sp_2(new Data);
sp_2->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1
sp_2->_data_2 = 10;
sp_2->_data_3 = 10;
SmartPtr<Data> sp_3 = sp_2;//模仿指针拷贝,代码崩溃
return 0;
}原因是析构了两次资源,这怎么解决呢?深拷贝?不行,为什么?因为指针赋值是希望两个指针都可以拥有对同一块内存的管理权限。而我们以往实现的容器,在拷贝后都有属于各自容器对象的资源,而指针可以共享,这是智能指针拷贝的最大问题。
补充:除了
auto_ptr{};的探索失误,准标准C++库boost也做了很多智能指针的探索工作,这里给一个 Boost 官方网站 供您探索一二。
3.2.auto_ptr
那怎么办呢?使用引用计数即可解决这个问题,但 C++ 在最开始的时候并不是这么做的。在 C++ 98 中,第一次出现智能指针 auto_ptr{};,在处理这方面的问题时,使用了“管理权转移”,认为“两个对象管理一份资源”是存在问题的,因此在上面代码 sp_3 = sp_2 中就会表现为:sp_2 的资源转移给 sp_3。
这种实现非常简单,但是不符合我们对指针的理解,容易出现对象悬空的问题(资源被别的对象掠夺了)。
3.3.unique_ptr
因此 auto_ptr{}; 这种智能指针在后来也被委员会标记为弃用,改用智能指针 unique_ptr{};。
这个做得很极端,直接让智能指针对象禁用拷贝,这如何实现?两种方法:
- 使用自定义的拷贝构造和赋值重载(而不是默认拷贝构造和默认赋值重载),但是限定为私有,并且只声明不实现
- 使用
C++11语义重载后的关键字delete(”删除“成员函数)禁止编译器生成默认的拷贝构造和赋值重载
3.4.shared_ptr
但是 unique_ptr{}; 这种做法有点逃避现实问题的意思,因此后面又出现了新的智能指针 shared_ptr{};,支持引用计数后,智能指针就允许拷贝构造和赋值重载了,但是其具体实现有一些细节需要注意。
但是怎么计数呢?直接定义一个成员变量?不可以,这会导致多个成员计数不同步。那改成静态成员变量?依旧不行,会影响到同类型的指向其他资源的智能指针对象,这就更坑了。
那另辟蹊径,直接将引用计数存放到指向的资源处,但是更加麻烦了,还要考虑更多问题。
最后还有一种方案,每一个对象新增计数指针 _pcount,单独指向的空间存储计数数据,指向同一块空间资源的智能指针对象,其内部的 _pcount 指针也需要指向同一块计数空间。
#include <iostream>
using namespace std;
struct Data
{
Data()
: _data_1(1), _data_2(2), _data_3(2)
{}
int _data_1;
int _data_2;
int _data_3;
};
template <typename Type>
class SmartPtr
{
public:
SmartPtr(Type* ptr)
: _ptr(ptr), _pcount(new int(1))
{
cout << "deposit " << _ptr << '\n';
}
void _release()
{
if (--(*_pcount) == 0)
{
cout << "delete " << _ptr << '\n';
delete _ptr;
delete _pcount;
}
else
{
cout << "count-1 " << _ptr << '\n';
}
}
~SmartPtr()
{
_release();
}
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
cout << "count+1 " << _ptr << '\n';
}
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
return *this;
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
};
int main()
{
SmartPtr<int> sp(new int[10]);//交给智能指针管理
(*sp) = 5;
SmartPtr<Data> sp1(new Data);
sp1->_data_1 = 10;//完整写法为 sp_2.operator->()->_data_1
sp1->_data_2 = 10;
sp1->_data_3 = 10;
SmartPtr<Data> sp2 = sp1;
SmartPtr<Data> sp3(new Data);
sp2 = sp3;
return 0;
}3.5.weak_ptr
但是很遗憾,尽管 shared_ptr{}; 已经很完美了,但实践中依旧出现了一个关于”循环引用“的问题,我们简单介绍一下什么是”循环引用“:
#include <iostream>
using namespace std;
template <typename Type>
class SmartPtr
{
public:
SmartPtr(Type* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
void _release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
~SmartPtr()
{
_release();
}
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下
{
if (_ptr != sp._ptr)//防止给自己赋值
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
};
struct ListNode
{
int val;
SmartPtr<ListNode> next;
SmartPtr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << '\n';
}
};
int main()
{
SmartPtr<ListNode> n1 = new ListNode;
SmartPtr<ListNode> n2 = new ListNode;
//发生循环引用
n1->next = n2;
n2->prev = n1;
//析构顺序是先析构 n2,再析构 n1
//但是这里的析构是“引用析构”,不会真的调用 delete 释放资源,
//仅仅只是 (*pcount)--
//然后就导致:n2 的 prev 依旧指向 n1 的资源,n1 的 next 依旧指向 n2 的资源
//想要析构 n1 的 next,就需要 n1 先析构
//想要 n1 先析构,就需要 prev 先析构
//想要 prev 先析构,就需要 n2 先析构
//想要 n2 先析构,就需要 next 先析构
//想要 next 先析构,就需要 n1 先析构
//ok 想要 n1 析构?这不又回到开头了?
return 0;
}这就比较尴尬了,这种问题还是比较大的,我们经常会使用到类似链表的结构,一旦使用智能指针,那必然造成内存泄漏,并且是不可逆转的内存泄漏,用户即便知道这一循环引用,也无力释放......
由此诞生了 weak_ptr{};,严格来说这不是智能指针,而是专门为了解决“循环引用”而生的。因此在库中,上述代码就会类似写成:
#include <iostream>
#include <memory>
using namespace std;
struct ListNode
{
int val;
//shared_ptr<ListNode> next;
//shared_ptr<ListNode> prev;
weak_ptr<ListNode> next;
weak_ptr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << '\n';
}
};
int main()
{
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
cout << n1.use_count() << " " << n2.use_count() << '\n';//输出 1
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << " " << n2.use_count() << '\n';//使用了 weak_ptr 还是 1,这和之前不同
return 0;
}什么原理呢?实际上 weak_ptr{}; 在指向别的 shared_ptr{}; 时不会进行计数,我们可以尝试简单实现一下 weak_ptr{};。
#include <iostream>
using namespace std;
template <typename Type>
class SmartPtr
{
public:
SmartPtr(Type* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
void _release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
~SmartPtr()
{
_release();
}
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)//这个代码需要注意一下
{
if (_ptr != sp._ptr)//防止给自己赋值
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
Type* get() const
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
};
template <typename Type>
class WeakPtr
{
public:
WeakPtr()
: _ptr(nullptr)
{}
WeakPtr(SmartPtr<Type>& sp)
: _ptr(sp.get())
{}
WeakPtr<Type>& operator=(const SmartPtr<Type>& sp)
{
_ptr = sp.get();
return *this;
}
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
private:
Type* _ptr;
};
struct ListNode
{
int val;
WeakPtr<ListNode> next;
WeakPtr<ListNode> prev;
~ListNode()
{
cout << "~ListNode()" << '\n';
}
};
int main()
{
SmartPtr<ListNode> n1(new ListNode);
SmartPtr<ListNode> n2(new ListNode);
cout << n1.use_count() << " " << n2.use_count() << '\n';
n1->next = n2;
n2->prev = n1;
cout << n1.use_count() << " " << n2.use_count() << '\n';
return 0;
}补充:总算是讲完了智能指针的发展历程,但是我需要告诉您一个残酷的真相,还有一个严重问题没有解决,但是这就涉及到智能指针和多线程的结合了,如果您尚未学过多线程,还请转移到我的
Linux系列文章,详细了解线程后再回来这里继续思考(这个问题我在讲解C++线程库的时候,也会再次提及,并且更加详细)。这个严重问题就是,多线程环境下,引用计数本身是线程不安全的,其
++不是原子操作,我们需要将其转化为原子操作(这还涉及到原子库),这样的智能指针才基本是安全的。
3.6.定制删除器
但是我们还有一个问题,我们的实现只能 delete 一个指针资源,但是无法完全释放数组一类的资源,这个时候就需要一个定制删除器 Deleter,这在库的智能指针构造函数签名中就有删除器的出现。
//智能指针的某一重载版本的构造函数
template< class Y, class Deleter >
shared_ptr( Y* ptr, Deleter d );//d 就是一个定制删除器这里的 d 可以传递可调用对象,例如:仿函数和 lambda 表达式。
//使用库中的 shared_ptr
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <memory>
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
//使用仿函数
std::shared_ptr <std::string> sp1(new std::string[10], DelArray<std::string>());
//使用 lambda 表达式
std::shared_ptr <std::string> sp2(new std::string[10], [](std::string* ptr) { delete[] ptr; });
std::shared_ptr <FILE> sp3(fopen("limou.txt", "r"), [](FILE* ptr) { fclose(ptr); });
return 0;
}那我们怎么自己实现一个呢?实际上并不复杂,只需要向构造函数传递一个对象即可(类似以前改变 sort() 的升降序而传递仿函数、大堆小堆的改变)
//模拟实现定制删除器
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <functional>
template <typename Type>
class SmartPtr
{
public:
//构造
SmartPtr(Type* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
//重载构造
template <typename Deleter>
SmartPtr(Type* ptr, Deleter del)
: _ptr(ptr), _pcount(new int(1))
{}
//析构
void _release()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
del(_ptr);
delete _pcount;
}
}
~SmartPtr()
{
_release();
}
//拷贝构造
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
//赋值重载
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)
{
if (_ptr != sp._ptr)//防止给自己赋值
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
//指针行为
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
//引用个数
int use_count() const
{
return *_pcount;
}
//获取原生指针
Type* get_ptr() const
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
};
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
SmartPtr<std::string> sp1(new std::string[10], DelArray<std::string>());
SmartPtr<std::string> sp2(new std::string[10], [](std::string* ptr) { delete[] ptr; });
SmartPtr<FILE> sp3(fopen("limou.txt", "w+"), [](FILE* ptr) { fclose(ptr); });
SmartPtr<int> sp4(new int(1));
return 0;
}但是这个地方有一个困难没有解决,就是析构没有办法拿到调用函数,但是如果变成成员变量就拿不到类型,我们可以使用包装类来解决(也有其他的实现,这里我跟着库走)。
//模拟实现定制删除器
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <functional>
template <typename Type>
class SmartPtr
{
public:
//构造
SmartPtr(Type* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
//重载构造
template <typename Deleter>
SmartPtr(Type* ptr, Deleter del)
: _ptr(ptr), _pcount(new int(1)), _del(del)
{}
//析构
void _release()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
~SmartPtr()
{
_release();
}
//拷贝构造
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
//赋值重载
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)
{
if (_ptr != sp._ptr)//防止给自己赋值
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
//指针行为
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
//引用个数
int use_count() const
{
return *_pcount;
}
//获取原生指针
Type* get_ptr() const
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
std::function<void(Type*)> _del;
};
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
SmartPtr<std::string> sp1(new std::string[10], DelArray<std::string>());
SmartPtr<std::string> sp2(new std::string[10], [](std::string* ptr) { delete[] ptr; });
SmartPtr<FILE> sp3(fopen("limou.txt", "w+"), [](FILE* ptr) { fclose(ptr); });
SmartPtr<int> sp4(new int(1));
return 0;
}并且还需要给一个缺省值,防止用户没有传递可调用对象后没有调用析构,进而发生内存泄漏。
//模拟实现定制删除器
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
#include <functional>
template <typename Type>
class SmartPtr
{
public:
//构造
SmartPtr(Type* ptr = nullptr)
: _ptr(ptr), _pcount(new int(1))
{}
//重载构造
template <typename Deleter>
SmartPtr(Type* ptr, Deleter del)
: _ptr(ptr), _pcount(new int(1)), _del(del)
{}
//析构
void _release()
{
if (--(*_pcount) == 0)
{
//delete _ptr;
_del(_ptr);
delete _pcount;
}
}
~SmartPtr()
{
_release();
}
//拷贝构造
SmartPtr(const SmartPtr<Type>& sp)
: _ptr(sp._ptr), _pcount(sp._pcount)
{
++(*_pcount);
}
//赋值重载
SmartPtr<Type>& operator=(const SmartPtr<Type>& sp)
{
if (_ptr != sp._ptr)//防止给自己赋值
{
_release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
//指针行为
Type& operator*()
{
return *_ptr;
}
Type* operator->()
{
return _ptr;
}
//引用个数
int use_count() const
{
return *_pcount;
}
//获取原生指针
Type* get_ptr() const
{
return _ptr;
}
private:
Type* _ptr;
int* _pcount;
std::function<void(Type*)> _del = [](Type* ptr) { delete ptr; };
};
template<class T>
struct DelArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
int main()
{
SmartPtr<std::string> sp1(new std::string[10], DelArray<std::string>());
SmartPtr<std::string> sp2(new std::string[10], [](std::string* ptr) { delete[] ptr; });
SmartPtr<FILE> sp3(fopen("limou.txt", "w+"), [](FILE* ptr) { fclose(ptr); });
SmartPtr<int> sp4(new int(1));
return 0;
}