跳到主要内容C++ RAII 与智能指针详解 | 极客日志C++算法
C++ RAII 与智能指针详解
介绍 C++ 中用于自动管理动态内存的智能指针技术。通过 RAII(资源获取即初始化)机制,智能指针在对象生命周期结束时自动释放资源,避免内存泄漏和悬空指针。文章对比了 auto_ptr、unique_ptr、shared_ptr 和 weak_ptr 的原理与区别,重点讲解了引用计数循环问题及 weak_ptr 的解决方案,并介绍了自定义删除器的用法。
静心1 浏览 智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new/delete 操作,从而显著减少内存泄漏和悬空指针的风险。
1. 为什么需要智能指针?
#include <iostream>
#include <stdexcept>
using namespace std;
int div() {
int a, b;
cin >> a >> b;
if (b == 0) throw invalid_argument("除 0 错误");
return a / b;
}
void Func() {
int* p1 = new int;
int* p2 = new int;
cout << div() << endl;
delete p1;
delete p2;
}
int main() {
try {
Func();
} catch (exception& e) {
cout << e.what() << endl;
}
return 0;
}
如果 p1 这里 new 抛异常会如何?
p1 未成功分配,值为 nullptr。函数直接跳转到 catch 块,p2 未分配,无内存泄漏。
如果 p2 这里 new 抛异常会如何?
已分配但未释放,导致内存泄漏。函数跳转到 块, 未分配, 和 均未执行。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如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
p1
catch
p2
delete p1
delete p2
p1 和 p2 均已分配但未释放,导致双重内存泄漏。函数跳转到 catch 块,打印错误信息(如'除 0 错误')。
C++ 不像 Java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存。
2. 智能指针原理
2.1 RAII
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr) : _ptr(ptr) {}
~SmartPtr() {
if (_ptr) delete _ptr;
}
private:
T* _ptr;
};
int main() {
SmartPtr<int> sp1(new int(1));
SmartPtr<string> sp2(new string("xxx"));
return 0;
}
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放。
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
2.2 像指针一样使用
都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可以通过 -> 去访问所指空间中的内容,因此类中还得需要将 *、-> 重载下,才可让其像指针一样去使用。
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;
};
* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文。
3. C++11 的智能指针
智能指针一般放在 <memory> 文件里,C++11 也参考了第三方库 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 中的实现的
3.1 auto_ptr
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr) : _ptr(ptr) {}
~auto_ptr() {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
auto_ptr(auto_ptr<T>& ap) : _ptr(ap._ptr) {
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
if (this != &ap) {
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
private:
T* _ptr;
};
auto_ptr 在 C++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似。
因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr。
3.2 unique_ptr
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr) : _ptr(ptr) {}
~unique_ptr() {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:
T* _ptr;
};
unique_ptr 很简单粗暴,直接禁止了拷贝机制。
3.3 shared_ptr
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}
~shared_ptr() {
if (--(*_pcount) == 0) {
cout << "delete:" << _ptr << endl;
delete _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;
};
C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题。
类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏。
🔥值得注意的是:shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充。
3.4 weak_ptr
看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环。
struct ListNode {
int _data;
shared_ptr<ListNode> _next;
shared_ptr<ListNode> _prev;
};
int main() {
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
当执行 node1->next = node2 和 node2->prev = node1 时,node1 内部的 _next 指针指向 node2,node2 内部的 _prev 指针指向 node1。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用。
当 node1 和 node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1。此时,每个节点的引用计数变为 1,而不是预期的 0。这是因为 node1 的 _next 还指向 node2,node2 的 _prev 还指向 node1,使得它们的引用计数无法归零。
对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2 的 _prev 引用着 node1),而 node2 又等待 node1 先释放(因为 node1 的 _next 引用着 node2),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏。
class ListNode {
public:
weak_ptr<ListNode> _next;
weak_ptr<ListNode> _prev;
};
为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptr。weak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev 或 _next 其中之一)改为 weak_ptr 类型,就可以打破循环引用。
因此,weak_ptr 是一种专门解决循环引用问题的指针。
4. 删除器
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class A {
public:
~A() {
cout << "A::~A()" << endl;
}
};
template<class T>
struct FreeFunc {
void operator()(T* ptr) const {
cout << "FreeFunc: free memory at " << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr) const {
cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl;
delete[] ptr;
}
};
int main() {
shared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>());
*sp1 = 100;
cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;
shared_ptr<int> sp2(new int[5], DeleteArrayFunc<int>());
for (int i = 0; i < 5; ++i) {
sp2.get()[i] = i;
}
cout << "sp2 array:";
for (int i = 0; i < 5; ++i) {
cout << " " << sp2.get()[i];
}
cout << endl;
shared_ptr<A> sp4(new A[3], [](A* p) {
cout << "Lambda: deleting array at " << p << endl;
delete[] p;
});
cout << "sp4 array of A objects created" << endl;
shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {
if (p) {
cout << "Lambda: closing file" << endl;
fclose(p);
}
});
if (sp5) {
fprintf(sp5.get(), "Hello, shared_ptr with deleter!\n");
cout << "File written" << endl;
}
return 0;
}
对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除。