1. RAII(资源获取即初始化)是什么?
核心理念:
RAII 将资源的管理与对象的生命周期紧密绑定。通过这种方式,确保资源在不再需要时能够被自动释放。
资源类型包括:堆内存、文件句柄、网络连接、互斥锁等。
对象角色:通常为栈上的局部变量。由于栈对象在离开作用域时会自动调用析构函数,因此非常适合用于管理动态分配的资源。
new
为何使用 RAII?
它利用了 C++ 中栈对象自动析构的机制,来安全地管理堆资源。具体流程如下:
- 构造函数:负责申请资源(如分配一块内存)。
- 析构函数:负责释放资源(如释放该内存)。
只要对象超出作用域,无论是否发生异常,析构函数都会被调用,从而避免了内存泄漏和异常路径下资源未释放的问题。
delete
2. C++ 标准库中的智能指针
C++11 引入了 <memory> 头文件,提供了三种主要的智能指针类型。它们本质上是类模板,封装了原始指针,并在析构时自动执行资源清理操作。
<memory>
A. std::unique_ptr(独占式智能指针)
在大多数场景中应优先选用此类型。
特性说明:
- 所有权模式:独占控制,同一时间仅允许一个 unique_ptr 指向特定资源。
- 拷贝行为:禁止复制构造,防止所有权分裂。
- 移动语义:支持移动操作,可通过 std::move 转让资源所有权。
- 性能表现:零开销设计,运行效率与原始指针相当,且占用空间相同。
C++ 实现示例:
#include <memory>
void usage() {
// 创建:推荐使用 make_unique (C++14)
std::unique_ptr<int> ptr1 = std::make_unique<int>(100);
// std::unique_ptr<int> ptr2 = ptr1; // 编译错误!禁止拷贝
// 转移所有权:ptr1 变为空,ptr2 接管内存
std::unique_ptr<int> ptr2 = std::move(ptr1);
// 访问数据
*ptr2 = 200;
} // 函数结束,ptr2 离开作用域,自动 delete 内存。ptr1 是空的,不做操作。
std::unique_ptr
unique_ptr
std::move
B. std::shared_ptr(共享式智能指针)
特性说明:
- 所有权模式:多个 shared_ptr 可共享同一资源。
- 实现原理:内部维护引用计数。每当有新指针指向资源时,计数加一;当某个指针销毁时,计数减一。当计数归零时,资源被自动释放。
- 性能影响:存在额外开销,因需维护原子性引用计数以支持线程安全。
std::shared_ptr
void usage() {
// 创建:推荐 make_shared,效率更高
std::shared_ptr<int> p1 = std::make_shared<int>(100); // 计数 = 1
{
std::shared_ptr<int> p2 = p1; // 拷贝成功!计数 = 2
std::cout << *p2 << std::endl;
} // p2 离开作用域,计数 - 1。当前计数 = 1。内存未释放。
} // p1 离开作用域,计数 - 1。当前计数 = 0。内存释放。
C. std::weak_ptr(弱引用智能指针)
作为 shared_ptr 的辅助工具,weak_ptr 不持有资源的所有权,也不会增加引用计数。
主要用途:解决由 shared_ptr 引发的循环引用问题。
使用方式:weak_ptr 可指向一个由 shared_ptr 管理的对象,但无法确定对象是否仍存活。在访问前必须调用 lock() 方法尝试提升为 shared_ptr,若成功则说明对象仍在。
std::weak_ptr
shared_ptr
shared_ptr
lock()
3. 经典问题:循环引用导致的内存泄漏
这是面试高频考点。当两个对象互相持有对方的 shared_ptr 时,引用计数永远无法降为零,造成资源无法释放。
典型错误代码示例:
class B; // 前置声明
class A {
public:
std::shared_ptr<B> ptrB; // A 持有 B
~A() { std::cout << "A 销毁" << std::endl; }
};
class B {
public:
std::shared_ptr<A> ptrA; // B 持有 A
~B() { std::cout << "B 销毁" << std::endl; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->ptrB = b; // A -> B (B 计数=2)
b->ptrA = a; // B -> A (A 计数=2)
}
// main 结束:
// a 销毁,A 计数变成 1。
// b 销毁,B 计数变成 1。
// 即使程序结束,A 和 B 的析构函数永远不会执行!内存泄漏。
解决方案:将其中一方的 shared_ptr 改为 weak_ptr。常见于“子节点引用父节点”或“观察者模式”中,使用 weak_ptr 避免强引用环。
weak_ptr
class B {
public:
std::weak_ptr<A> ptrA; // 只观测,不拥有。不增加 A 的引用计数。
// ...
};
4. 智能指针的自定义删除器机制
默认情况下,智能指针(如 unique_ptr 和 shared_ptr)在析构时会对内部指针执行 delete 操作。
然而,并非所有资源都由 new 分配,例如:
- 通过 fopen 打开的文件流 —— 需要 fclose 关闭。
- 通过 socket 创建的网络连接 —— 需 close 或 closesocket。
- 使用 malloc 分配的内存 —— 应调用 free。
- 第三方 C 库创建的对象 —— 通常提供专用的销毁函数。
fopen
fclose
socket
close
malloc
free
destroy_object
若对非 new 分配的资源直接使用 delete,会导致未定义行为甚至程序崩溃。此时必须使用自定义删除器(Custom Deleter)。
unique_ptr
delete
FILE*
1. unique_ptr 与 shared_ptr 在删除器处理上的关键差异
这是常被考察的知识点,两者机制截然不同:
| 特性 | std::unique_ptr | std::shared_ptr |
|---|---|---|
| 删除器类型地位 | 删除器类型是智能指针类型的一部分 | 删除器不是类型的一部分,仅为构造参数 |
| 代码灵活性 | 较低。不同类型删除器之间不可赋值 | 极高。只要托管对象类型一致,即使删除器不同也可相互赋值 |
| 性能表现 | 极高(静态绑定)。编译期可内联优化,无额外内存开销 | 稍低(动态绑定)。删除器存储于控制块中,存在间接调用成本 |
unique_ptr<int, MyDeleter> p;
shared_ptr<int> p;
2. std::unique_ptr 的删除器用法
为了追求极致性能(零开销抽象),unique_ptr 要求删除器类型在编译期就已确定。
推荐写法一:仿函数(Functor)
高效且紧凑,利用空基类优化(EBO),不会增加 unique_ptr 对象的大小。
#include <iostream>
#include <memory>
// 假设这是一个遗留的 C 风格 API
struct C_Socket { int id; };
void close_socket(C_Socket* s) {
std::cout << "Closing socket " << s->id << std::endl;
delete s; // 模拟释放资源
}
// 1. 定义一个仿函数类
struct SocketDeleter {
void operator()(C_Socket* s) const {
close_socket(s);
}
};
int main() {
// 2. 将类型作为模板参数传入
std::unique_ptr<C_Socket, SocketDeleter> ptr(new C_Socket{42});
// ptr 离开作用域时,会自动调用 SocketDeleter()(ptr.get())
return 0;
}
写法二:函数指针
更直观易懂,但会使 unique_ptr 占用更多空间(因其内部需保存函数指针)。
// using 定义一个函数指针类型
using DeleteFuncType = void(*)(C_Socket*);
int main() {
// 传入类型 和 具体函数
std::unique_ptr<C_Socket, DeleteFuncType> ptr(new C_Socket{88}, close_socket);
return 0;
}
3. std::shared_ptr 的删除器机制
shared_ptr 更注重灵活性,采用类型擦除(Type Erasure)技术,将删除器隐藏在其内部的控制块中。
这意味着:
- 无需修改 shared_ptr 的类型声明。
- 可在运行时传入任意删除逻辑。
常用写法:Lambda 表达式
最便捷的方式,无需额外定义结构体即可实现定制化释放逻辑。
#include <iostream>
#include <memory>
#include <cstdio> // for fopen, fclose
int main() {
// 打开一个文件
FILE* file = std::fopen("test.txt", "w");
// 创建 shared_ptr,并在构造函数中直接传入 Lambda 作为删除器
// 注意:类型依然是 shared_ptr<FILE>
std::shared_ptr<FILE> filePtr(file, [](FILE* fp) {
std::cout << "文件被自动关闭了" << std::endl;
if (fp) std::fclose(fp);
});
std::fprintf(filePtr.get(), "Hello RAII\n");
// filePtr 离开作用域 -> 引用计数归零 -> Lambda 被调用 -> fclose 执行
return 0;
}
为什么 shared_ptr 如此设计?
因为 shared_ptr 更强调通用性和运行时灵活性,牺牲少量性能换取更高的适配能力,适合复杂场景下的资源管理。
如果使用unique_ptr
上述情况便无法被放入同一个
vector
中,原因是它们的类型不同,尤其是删除器部分的类型存在差异。
而实际上,系统本身就需要分配一个堆内存块(即控制块)来管理引用计数。若顺带将删除器对象也存储在此块中,额外开销非常有限,并不会带来显著性能负担。
这一设计带来了显著的优势:
std::shared_ptr<int> p1(new int(1), [](int* p){ delete p; }); // 删除器 A
std::shared_ptr<int> p2(new int(2), [](int* p){ free(p); }); // 删除器 B
// 虽然 p1 和 p2 的回收逻辑完全不同,但它们是同一种类型!
// 所以可以放入同一个容器:
std::vector<std::shared_ptr<int>> vec;
vec.push_back(p1);
vec.push_back(p2); // 合法!

雷达卡


京公网安备 11010802022788号







