C++-RAII
本文介绍C++基于RAII思想的智能指针,借对象生命周期管理资源,构造函数获取资源,析构函数释放,重载运算符方便访问,均在`<memory>`头文件。auto_ptr因拷贝转移所有权易出错已废弃。unique_ptr独占所有权,不支持拷贝仅支持移动,删除器为模板参数。shared_ptr通过引用计数共享所有权,最后一个销毁时释放,删除器构造时指定。weak_ptr无所有权,解决shared_ptr
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 once
namespace 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_ptr
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int[]> arr = std::make_unique<int[]>(5);
// 访问数据
std::cout << *ptr1 << std::endl; // 42
arr[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 once
namespace 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; // 2
std::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; // 0
cout << wp.use_count() << endl; // 2
// sp1和sp2都指向了其他资源,则weak_ptr就过期了
sp1 = std::make_shared<string>("222222");
cout << wp.expired() << endl; // 0
cout << wp.use_count() << endl; // 1
sp2 = std::make_shared<string>("333333");
cout << wp.expired() << endl; // 1
cout << wp.use_count() << endl; // 0
wp = sp1;
//std::shared_ptr<string> sp3 = wp.lock();
auto sp3 = wp.lock();
cout << wp.expired() << endl; // 0
cout << 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. 对于非数组类型 - 使用 delete
std::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:管理的内存和引用计数分配在一起(减少内存碎片)
-
-
若使用自定义删除器,必须使用直接构造。
更多推荐


所有评论(0)