跳到主要内容C++ 智能指针:使用场景、实现原理与内存泄漏防治 | 极客日志C++算法
C++ 智能指针:使用场景、实现原理与内存泄漏防治
本文深入解析 C++ 智能指针的核心机制与应用。涵盖 RAII 设计理念,对比 auto_ptr、unique_ptr、shared_ptr 及 weak_ptr 的特性差异。重点阐述 shared_ptr 引用计数原理、循环引用问题的成因与 weak_ptr 解决方案,以及定制删除器的使用技巧。通过代码示例演示如何有效防治内存泄漏,提升 C++ 程序的安全性与稳定性。
C++ 智能指针:使用场景、实现原理与内存泄漏防治
一、智能指针的使用场景分析
C++ 以高效著称,手动内存管理(new/delete)避免了高级语言自动垃圾回收的开销。但这把双刃剑对程序员要求极高,极易出现内存泄漏。特别是在异常处理场景中,一旦抛出异常,后续的 delete 语句可能无法执行,导致资源泄露。
例如在以下代码中,如果 Divide 函数抛出异常,或者第二个 new 失败,第一个 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;
}
delete[] array1;
delete[] array2;
}
{
{
();
} ( * errmsg) {
cout << errmsg << endl;
} ( exception& e) {
cout << e.() << endl;
} (...) {
cout << << endl;
}
;
}
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;
};
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;
}
三、C++ 标准库智能指针的使用
C++ 标准库中的智能指针都在 <memory> 头文件中。主要有以下几种:
1. auto_ptr(已废弃)
auto_ptr 是 C++98 时代的产物。它采用破坏性拷贝策略:拷贝时转移所有权,原指针变为空。这容易导致悬空指针问题,C++11 后强烈建议不再使用。
2. unique_ptr
unique_ptr 意为唯一指针,C++11 引入。它不支持拷贝,只支持移动语义(右值)。适用于不需要共享资源的场景,性能优于 shared_ptr。
3. shared_ptr
shared_ptr 意为共享指针,支持拷贝和移动。底层通过引用计数实现,多个指针可安全共享同一份资源。当引用计数归零时,自动释放资源。缺点是有一定的性能开销。
4. weak_ptr
weak_ptr 意为弱指针,不管理资源所有权,不增加引用计数。主要用于解决 shared_ptr 的循环引用问题。
struct Date {
int _year, _month, _day;
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {}
~Date() { cout << "~Date()" << endl; }
};
int main() {
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;
}
其他重要用法说明
make_shared
相比直接 new,make_shared 效率更高,因为它将计数器与资源分配在同一块内存中,优化了内存碎片。它是独立模板函数,定义在 <memory> 中。
shared_ptr<Date> sp1(new Date(2024, 9, 11));
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
operator bool
shared_ptr 和 unique_ptr 支持转换为 bool 类型,用于判断是否为空。
shared_ptr<Date> sp1(new Date(2024, 9, 11));
if (sp1) {
cout << "sp1 is not nullptr" << endl;
}
if (!sp1) {
cout << "sp1 is nullptr" << endl;
}
构造函数 explicit 修饰
标准库智能指针构造函数通常使用 explicit,防止隐式类型转换,强制开发者显式管理内存所有权。
shared_ptr<Date> sp5(new Date(2024, 9, 11));
定制删除器
默认情况下,智能指针析构时调用 delete。如果资源是通过 new[]、malloc 等方式申请的,需要匹配对应的释放方式(delete[]、free)。智能指针支持在构造时传入自定义删除器(Deleter)。
对于 new[] 申请的资源,unique_ptr 和 shared_ptr 有特化版本:
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
对于更复杂的删除器(如 Lambda 表达式),推荐使用 shared_ptr,因为 unique_ptr 的删除器作为模板参数传递时限制较多。
auto delArrOBJ = [](Date* ptr) { delete[] ptr; };
unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);
四、智能指针的原理
1. auto_ptr 和 unique_ptr 核心功能模拟
这两个指针实现较简单。unique_ptr 需禁用拷贝构造和赋值,仅支持移动。
namespace wusaqi {
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;
return *this;
}
private:
T* _ptr;
};
}
2. shared_ptr 设计与实现
shared_ptr 的核心在于引用计数。不能简单使用静态变量,因为每份资源需要独立的计数器。最佳方案是为每个资源动态开辟一个整型变量作为计数器。
引用计数实现
- 构造:初始化
_ptr 和 _pcount(指向新分配的计数器,初始值为 1)。
- 拷贝构造:复制指针和计数器地址,并将计数器加一。
- 析构:将计数器减一,若为 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;
};
}
赋值运算符重载
赋值前需先释放当前对象持有的资源(减少引用计数,若为 0 则释放)。同时需处理自赋值情况(判断 _ptr != sp._ptr)。
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;
}
定制删除器的实现
为了支持非 new 分配的资源(如 malloc),需引入删除器。使用 std::function<void(T*)> 存储删除器,默认为 delete。
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 = nullptr;
int* _pcount;
std::function<void(T*)> _del = [](T* ptr) { delete ptr; };
};
}
五、shared_ptr 和 weak_ptr
shared_ptr 循环引用问题
当两个 shared_ptr 互相持有对方时,引用计数永远无法归零,导致内存泄漏。常见于树形结构或双向链表节点。
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);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
weak_ptr 解决方案
weak_ptr 不管理资源,不增加引用计数。将其用于循环引用的一方,可以打破循环。
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);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
weak_ptr 本身不能直接访问资源,需调用 lock() 方法返回一个临时的 shared_ptr。若资源已释放,lock() 返回空指针,从而避免悬垂指针问题。
std::shared_ptr<string> sp1(new string("111111"));
std::shared_ptr<string> sp2(sp1);
std::weak_ptr<string> wp = sp1;
if (!wp.expired()) {
auto sp3 = wp.lock();
if (sp3) {
*sp3 += "###";
}
}
六、内存泄漏
什么是内存泄漏
内存泄漏指程序未能释放已经不再使用的内存。通常是因为忘记释放或异常导致释放路径未执行。物理内存并未消失,只是应用程序失去了控制权。
危害与预防
- 危害:短期运行影响不大,但长期运行的服务(如操作系统、后台进程)会因可用内存减少而变慢甚至崩溃。
- 预防:
- 事前预防:优先使用智能指针或遵循 RAII 思想封装资源。
- 事后检测:定期使用工具(如 Valgrind)检测内存泄漏。
总结来说,内存泄漏的解决方案分为两类:一是通过智能指针等机制进行事前预防;二是通过泄漏检测工具进行事后查错。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,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
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online