C++-RAII
C++智能指针
1. 概念
RAII是Resource Acquisition Is Initialization的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利⽤对象⽣命周期来管理获取到的动态资源,避免资源泄漏,这⾥的资源可以是内存、⽂件指针、⽹络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的⽣命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
- 资源获取在构造函数中完成。
- 资源释放在析构函数中完成。
- 智能指针类除了满⾜RAII的设计思路,还要⽅便资源的访问,所以智能指针类还会想迭代器类⼀样,重载
operator*
/operator->
/operator[]
等运算符,⽅便访问资源。
2. 标准库智能指针的使用
C++标准库中的智能指针都在<memory>
头文件。
2.1 auto_ptr(已废弃)
2.1.1 概念和示例
auto_ptr是C++98时设计出来的智能指针,他的特点是拷⻉时把被拷⻉对象的资源的管理权转移给拷⻉对象,这是⼀个⾮常糟糕的设计,因为他会到被拷⻉对象悬空,访问报错的问题。
#include <iostream>
#include <memory> // 在 C++98/11 中// 示例:auto_ptr 的问题
void autoPtrDemo() {std::auto_ptr<int> p1(new int(10));std::auto_ptr<int> p2 = p1; // 所有权转移,p1 变为空// std::cout << *p1 << std::endl; // 运行时错误!std::cout << *p2 << std::endl; // 正常输出 10
}// 常见问题:意外所有权转移
void problematicFunction(std::auto_ptr<int> param) {// 函数调用时所有权已经转移
}void testAutoPtr() {std::auto_ptr<int> ptr(new int(20));problematicFunction(ptr); // ptr 现在为空// *ptr = 30; // 运行时错误!
}
2.1.2 模拟实现
#pragma oncenamespace simulate_auto_ptr {template <typename T>class auto_ptr {public:auto_ptr(T* _ptr) :ptr(_ptr) {}auto_ptr(auto_ptr<T>& self) {if (ptr) delete ptr;ptr = self.ptr;self.ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& self) {if (this != &self) {if (ptr) delete ptr;ptr = self.ptr;self.ptr = nullptr;}return *this;}~auto_ptr() { if (ptr)delete ptr;}T& operator* () { return *ptr; }T* operator-> () { return ptr; }private:T* ptr;};
}
2.2 unique_ptr
2.2.1 概念和示例
unique_ptr是C++11设计出来的智能指针,特点是不支持拷贝,只支持移动,独占所有权的智能指针。
#include <iostream>
#include <memory>void uniquePtrDemo() {// 创建 unique_ptrstd::unique_ptr<int> ptr1 = std::make_unique<int>(42);std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);// 访问数据std::cout << *ptr1 << std::endl; // 42arr[0] = 10;std::cout << arr[0] << std::endl; // 10// 移动语义 - 显式所有权转移std::unique_ptr<int> ptr2 = std::move(ptr1);// std::cout << *ptr1 << std::endl; // 错误!ptr1 为空std::cout << *ptr2 << std::endl; // 42// 释放所有权int* rawPtr = ptr2.release();delete rawPtr; // 需要手动释放// 重置指针auto ptr3 = std::make_unique<int>(100);ptr3.reset(new int(200)); // 自动释放旧内存,管理新内存ptr3.reset(); // 释放内存,ptr3 为空// 自定义删除器auto fileDeleter = [](FILE* f) {if (f) {std::cout << "关闭文件" << std::endl;fclose(f);}};std::unique_ptr<FILE, decltype(fileDeleter)> filePtr(fopen("test.txt", "w"), fileDeleter);
}
2.2.2 模拟实现
#pragma oncenamespace simulate_unique_ptr {// unique_ptr 不允许拷贝template <typename T>class unique_ptr {public:explicit unique_ptr(T* _ptr) :ptr(_ptr) {}unique_ptr(const unique_ptr<T>& self) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& self) = delete;unique_ptr(unique_ptr<T>&& self) {ptr = self.ptr;self.ptr = nullptr;}~unique_ptr() { if (ptr)delete ptr;}T& operator* () { return *ptr; }T* operator-> () { return ptr; }private:T* ptr;};
}
2.3 shared_ptr
2.3.1 概念和示例
shared_ptr共享所有权,使用引用计数,支持拷贝和移动,当最后一个管理资源的shared_ptr被销毁时释放内存。
#include <iostream>
#include <memory>// 创建 shared_ptr
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // 引用计数增加// 获取引用计数
std::cout << "引用计数: " << ptr1.use_count() << std::endl;
2.3.2 模拟实现
#pragma once
#include <iostream>
#include <functional>namespace simulate_shared_ptr {template<typename T>struct default_delete {void operator()(T* ptr) const { delete ptr; }};template<typename T>struct default_delete<T[]> {void operator()(T* ptr) const { delete[] ptr; }};// unique_ptr的实现方式 template<typename T , typename Deleter = default_delete<T>> class unique_ptr {};template<typename T>class shared_ptr {public:template<typename D>shared_ptr<T>(T* _ptr , D del) :ptr(_ptr) ,pcount(new int(1)) , deleter(del) {}// 构造的时候初始化引用计数shared_ptr<T>(T* _ptr) :ptr(_ptr) , pcount(new int(1)) {}shared_ptr<T>(const shared_ptr<T>& sp) :ptr(sp.ptr) , pcount(sp.pcount) { ++(*pcount); }shared_ptr<T>& operator=(const shared_ptr<T>& sp) {// 1.相同对象之间的赋值 p1 = p1;// 2.不同对象,但管理的资源相同之间的赋值 p1 = p2;// 3.不同对象,管理的资源也不同之间的赋值 p3 = p4;// 判断两个对象管理的不是同一个资源再相互赋值if (ptr != sp.ptr) {if (--(*pcount) == 0) {deleter(ptr);delete pcount;}ptr = sp.ptr;pcount = sp.pcount;++(*pcount);}return *this;}~shared_ptr<T>() {// 引用计数减到0释放空间if (--(*pcount) == 0) {delete ptr;delete pcount;}}T& operator*() { return *ptr; }T* operator->() { return ptr; }private:T* ptr;int* pcount;std::function<void(T*)> deleter = [](T* ptr){ delete ptr; }; // 默认使用delete};template<typename T>class shared_ptr<T[]> {public:T& operator[](size_t pos) { return ptr[pos]; }private:T* ptr;int* pcount;std::function<void(T*)> deleter;};
}
2.4 weak_ptr
std::weak_ptr
是 C++11 引入的一种智能指针,主要用于解决 std::shared_ptr
可能导致的循环引用问题,它本身不拥有对象的所有权。
循环引用问题:
#include <memory>
#include <iostream>
#include <string>class Node {
public:std::shared_ptr<Node> next;std::shared_ptr<Node> prev;Node(const std::string& name) : name(name) {std::cout << name << " 创建\n";}~Node() {std::cout << name << " 销毁\n";}std::string name;
};void circularReferenceProblem() {auto n1 = std::make_shared<Node>("Node1");auto n2 = std::make_shared<Node>("Node2");// 创建循环引用n1->next = n2;n2->prev = n1;std::cout << "n1 引用计数: " << n1.use_count() << std::endl; // 2std::cout << "n2 引用计数: " << n2.use_count() << std::endl; // 2// 退出作用域时,由于循环引用,对象不会被销毁!// 内存泄漏!
}
weak_ptr的主要特性:
-
不控制对象生命周期
weak_ptr
指向一个由shared_ptr
管理的对象,但不会增加其引用计数。当最后一个关联的shared_ptr
被销毁时,无论是否有weak_ptr
指向该对象,该资源都会被释放。 -
解决循环引用
当两个对象相互持有shared_ptr
时会形成循环引用,导致引用计数无法减为 0,内存泄漏。使用weak_ptr
替代shared_ptr
可打破循环。 -
需要转换为 shared_ptr 使用
weak_ptr
不能直接访问对象,必须通过lock()
方法返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回shared_ptr
是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr
访问资源是安全的。 -
weak_ptr⽀持expired检查指向的资源是否过期
-
如果两个shared_ptr管理一个资源,再加一个 weak_ptr,当这两个shared_ptr都析构了,同时资源也会被释放,但是为了方便use_count的访问,资源释放了,但是引用计数不会释放,可能通过另一个引用计数来保证。
#include <memory>
#include <string>
using namespace std;void test() {std::shared_ptr<string> sp1(new string("111111"));std::shared_ptr<string> sp2(sp1);std::weak_ptr<string> wp = sp1;cout << wp.expired() << endl; // 0cout << wp.use_count() << endl; // 2// sp1和sp2都指向了其他资源,则weak_ptr就过期了sp1 = std::make_shared<string>("222222"); cout << wp.expired() << endl; // 0cout << wp.use_count() << endl; // 1sp2 = std::make_shared<string>("333333");cout << wp.expired() << endl; // 1cout << wp.use_count() << endl; // 0wp = sp1;//std::shared_ptr<string> sp3 = wp.lock();auto sp3 = wp.lock(); cout << wp.expired() << endl; // 0cout << wp.use_count() << endl; // 2
}
解决循环引用问题:
// 将 std::shared_ptr<Node> next;
// std::shared_ptr<Node> prev;
// 改为 weak_ptr<Node> next; weak_ptr<Node> prev;
2.5 shared_ptr和unique_ptr删除器的不同使用
unique_ptr
的删除器是模板参数的一部分,需要在声明时指定shared_ptr
的删除器不是模板参数,而是在构造时指定
// unique_ptr 使用删除器
auto deleter = [](int* p) { delete p;
};// 删除器是模板参数的一部分
std::unique_ptr<int, decltype(deleter)> uptr(new int, deleter);// shared_ptr 使用删除器
// 删除器在构造时指定,不影响类型
std::shared_ptr<int> sptr(new int, [](int* p) { delete p;
});
2.6 底层怎么区分delete还是delete[]
智能指针通过模板特化 和 删除器 来区分 delete
和 delete[]
。标准库为数组类型提供了特化版本。
2.6.1 示例
#include <memory>
#include <iostream>// unique_ptr 的两种特化形式
void uniquePtrSpecialization() {// 1. 对于非数组类型 - 使用 deletestd::unique_ptr<int> singlePtr = std::make_unique<int>(42);// 2. 对于数组类型 - 使用 delete[]std::unique_ptr<int[]> arrayPtr = std::make_unique<int[]>(5);
}
2.6.2 部分实现
// 简化版的 unique_ptr 实现
namespace detail {// 默认删除器(非数组)template<typename T>struct default_delete {void operator()(T* ptr) const {delete ptr;}};// 数组特化的删除器template<typename T>struct default_delete<T[]> {void operator()(T* ptr) const {delete[] ptr;}};
}// 主模板
template<typename T, typename Deleter = detail::default_delete<T>>
class unique_ptr {
private:T* ptr;Deleter deleter;public:// 构造函数、析构函数等...~unique_ptr() {deleter(ptr);}
};// 数组特化
template<typename T, typename Deleter>
class unique_ptr<T[], Deleter> {
private:T* ptr;Deleter deleter;public:// 针对数组的特殊接口T& operator[](size_t index) {return ptr[index];}~unique_ptr() {deleter(ptr);}
};
类模板部分特化的接口继承行为:在类的部分特化中,如果只提供特殊的接口,其他接口会自动使用主模板的版本。部分特化会继承主模板的所有接口。
在部分特化中,不能重复主模板的默认参数。这是 C++ 模板语法的一个规定。
2.7 make_unique、make_shared与直接构造的区别
-
make_unique、make_shared异常安全。
-
make_shared性能更好。
-
若直接构造,需要两次分配(容易产生内存碎片)
-
make_shared:管理的内存和引用计数分配在一起(减少内存碎片)
-
-
若使用自定义删除器,必须使用直接构造。