Appearance
智能指针
RAII 是什么?
一句话理解: RAII 是 C++ 里用“对象生命周期”管理资源的思想:构造函数里获取资源,析构函数里释放资源。
RAII 全称是什么? RAII 全称是:
c
Resource Acquisition Is Initialization中文常翻译为:
资源获取即初始化听起来有点绕,但真正要记的是这句话:
资源交给对象管理,对象创建时拿资源,对象销毁时释放资源。零基础理解: 比如你打开一个文件:
打开文件
使用文件
关闭文件如果你手动管理,就要自己记得关闭:
c
FILE* fp = fopen("data.txt", "r");
// 中间做很多事情
fclose(fp); // 必须记得写问题是:如果中途 return 了,或者抛异常了,fclose 可能就执行不到,资源就泄漏了。
RAII 的做法是:把文件包装成一个对象。
对象创建时:打开文件
对象析构时:自动关闭文件这样你不用到处手动写释放逻辑。
最经典例子:智能指针
没有 RAII 的写法:
c
#include <iostream>
using namespace std;
class Player {
public:
Player() {
cout << "Player 创建" << endl;
}
~Player() {
cout << "Player 销毁" << endl;
}
};
void test() {
Player* p = new Player();
// 如果这里中途 return,下面 delete 就执行不到
// return;
delete p; // 必须手动释放
}RAII 写法:
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
Player() {
cout << "Player 创建" << endl;
}
~Player() {
cout << "Player 销毁" << endl;
}
};
void test() {
// unique_ptr 是 RAII 对象
// 它构造时接管 Player*
// 它析构时自动 delete Player
unique_ptr<Player> p = make_unique<Player>();
// 即使这里 return,p 也会离开作用域
// p 的析构函数会自动释放 Player
return;
}unique_ptr 的本质就是 RAII:
构造时:拿到堆内存所有权
析构时:自动 deleteRAII 管理锁,非常重要
没有 RAII 的写法:
c
#include <mutex>
using namespace std;
mutex mtx;
void updateData() {
mtx.lock();
// 如果这里发生异常,unlock 就执行不到
// 其他线程可能永远等锁,造成死锁
mtx.unlock();
}RAII 写法:
c
#include <mutex>
using namespace std;
mutex mtx;
void updateData() {
// lock_guard 构造时自动加锁
lock_guard<mutex> lock(mtx);
// 这里写需要保护的数据逻辑
// 函数结束时,lock 离开作用域
// lock_guard 析构时自动解锁
}这个例子面试很加分,因为 RAII 不只是防内存泄漏,也能防死锁。
RAII 管理文件
c
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
void readFile() {
// ifstream 构造时打开文件
ifstream file("data.txt");
if (!file.is_open()) {
cout << "文件打开失败" << endl;
return;
}
string line;
while (getline(file, line)) {
cout << line << endl;
}
// 不需要手动 close
// file 离开作用域时,析构函数会自动关闭文件
}ifstream 也是 RAII 思想:
构造或 open:获取文件资源
析构:释放文件资源自己写一个 RAII 类
比如我们模拟管理一个资源:
c
#include <iostream>
using namespace std;
class FileGuard {
public:
FileGuard() {
// 构造函数中获取资源
cout << "打开文件" << endl;
}
~FileGuard() {
// 析构函数中释放资源
cout << "关闭文件" << endl;
}
};
void test() {
FileGuard file;
cout << "正在使用文件" << endl;
// 函数结束时,file 自动析构
// 析构函数自动执行关闭逻辑
}输出:
打开文件
正在使用文件
关闭文件这就是 RAII 的味道:资源释放不靠你记,而靠对象自动析构。
RAII 为什么异常安全?
看这个例子:
c
#include <iostream>
#include <stdexcept>
using namespace std;
class Guard {
public:
Guard() {
cout << "获取资源" << endl;
}
~Guard() {
cout << "释放资源" << endl;
}
};
void test() {
Guard g;
cout << "准备抛异常" << endl;
throw runtime_error("出错了");
// 这里不会执行
cout << "后续逻辑" << endl;
}
int main() {
try {
test();
} catch (...) {
cout << "捕获异常" << endl;
}
return 0;
}即使抛异常,局部对象 g 还是会析构,所以资源仍然能释放。
这就是 RAII 非常强的地方。
RAII 常见应用:
| 资源 | RAII 工具 |
|---|---|
| 堆内存 | unique_ptr、shared_ptr |
| 文件 | ifstream、ofstream、fstream |
| 锁 | lock_guard、unique_lock |
| 容器内存 | vector、string |
| 线程 | jthread |
| 自定义资源 | 自己写析构函数封装 |
RAII 和析构函数的关系: RAII 非常依赖析构函数。
因为 C++ 有一个重要规则:
局部对象离开作用域时,会自动调用析构函数。所以 RAII 把释放资源的逻辑放进析构函数里。
c
class Resource {
public:
Resource() {
// 获取资源
}
~Resource() {
// 释放资源
}
};面试容易问:RAII 和 GC 有什么区别?
RAII 是确定性释放。
对象离开作用域,立刻析构,资源立刻释放GC 是垃圾回收。
对象没人引用后,将来某个时刻由 GC 回收所以 RAII 特别适合管理文件、锁、网络连接这些需要“及时释放”的资源。
CAUTION
面试高分回答: RAII 是 C++ 里非常核心的资源管理思想,全称是 Resource Acquisition Is Initialization。它的核心是把资源和对象生命周期绑定起来,在构造函数中获取资源,在析构函数中释放资源。这样资源释放就不依赖程序员手动调用 delete、close、unlock,而是依靠对象离开作用域时自动析构完成。RAII 的好处是可以避免内存泄漏、文件句柄泄漏、锁未释放等问题,并且天然具备异常安全性。典型例子有 std::unique_ptr 管理堆内存,std::lock_guard 管理互斥锁,std::fstream 管理文件句柄。现代 C++ 中应该优先使用 RAII 和智能指针,尽量避免裸 new/delete 手动管理资源。
unique_ptr、shared_ptr、weak_ptr 区别是什么?
一句话理解:unique_ptr 是独占所有权,shared_ptr 是共享所有权,weak_ptr 是不拥有对象的观察者,主要用来配合 shared_ptr 防止循环引用。
零基础理解: 普通指针的问题是:你 new 了对象,就必须记得 delete。
c
Player* p = new Player();
// 中间如果 return 或异常,delete 可能执行不到
delete p;智能指针的作用是:把裸指针包装成对象,利用 RAII 自动释放资源。
对象创建时:接管资源
对象析构时:自动释放资源unique_ptr:独占所有权
unique_ptr 表示:这个对象只有一个主人。
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
Player() {
cout << "Player 创建" << endl;
}
~Player() {
cout << "Player 销毁" << endl;
}
};
int main() {
// make_unique 创建对象,并交给 unique_ptr 管理
unique_ptr<Player> p1 = make_unique<Player>();
// unique_ptr 不能拷贝,因为对象只能有一个主人
// unique_ptr<Player> p2 = p1; // 错误
// 可以转移所有权
unique_ptr<Player> p2 = move(p1);
// 此时 p1 不再拥有对象
if (p1 == nullptr) {
cout << "p1 已经没有对象了" << endl;
}
return 0;
}unique_ptr 的特点:
| 特点 | 说明 |
|---|---|
| 所有权 | 独占 |
| 能否拷贝 | 不能 |
| 能否移动 | 可以 |
| 引用计数 | 没有 |
| 开销 | 很小,接近裸指针 |
| 适合场景 | 对象只有一个明确拥有者 |
shared_ptr:共享所有权
shared_ptr 表示:多个指针可以共同拥有同一个对象。
它内部有引用计数:
c
有几个 shared_ptr 拥有对象,计数就是几
最后一个 shared_ptr 销毁时,对象才会释放
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
Player() {
cout << "Player 创建" << endl;
}
~Player() {
cout << "Player 销毁" << endl;
}
};
int main() {
shared_ptr<Player> p1 = make_shared<Player>();
cout << p1.use_count() << endl; // 1
{
// p2 和 p1 共同拥有同一个 Player
shared_ptr<Player> p2 = p1;
cout << p1.use_count() << endl; // 2
}
// p2 离开作用域,引用计数减 1
cout << p1.use_count() << endl; // 1
return 0;
}shared_ptr 的特点:
| 特点 | 说明 |
|---|---|
| 所有权 | 共享 |
| 能否拷贝 | 可以 |
| 引用计数 | 有 |
| 对象释放时机 | 强引用计数变成 0 |
| 开销 | 比 unique_ptr 大 |
| 适合场景 | 多个模块确实都需要拥有对象 |
weak_ptr:弱引用,不拥有对象
weak_ptr 是配合 shared_ptr 用的。
它可以观察一个由 shared_ptr 管理的对象,但不会增加强引用计数。
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
~Player() {
cout << "Player 销毁" << endl;
}
};
int main() {
weak_ptr<Player> weak;
{
shared_ptr<Player> sp = make_shared<Player>();
// weak 观察 sp 管理的对象
weak = sp;
cout << sp.use_count() << endl; // 1
}
// sp 已经销毁,对象也释放了
// weak 不拥有对象,所以不会阻止对象销毁
if (weak.expired()) {
cout << "对象已经不存在" << endl;
}
return 0;
}weak_ptr 不能直接使用对象,要先 lock():
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
void SayHello() {
cout << "Hello" << endl;
}
};
int main() {
shared_ptr<Player> sp = make_shared<Player>();
weak_ptr<Player> wp = sp;
// lock 会尝试生成一个 shared_ptr
// 如果对象还活着,返回有效 shared_ptr
// 如果对象已经销毁,返回空 shared_ptr
shared_ptr<Player> temp = wp.lock();
if (temp) {
temp->SayHello();
}
return 0;
}weak_ptr 的特点:
| 特点 | 说明 |
|---|---|
| 是否拥有对象 | 不拥有 |
| 是否增加强引用计数 | 不增加 |
| 能否直接访问对象 | 不能,要先 lock() |
| 主要作用 | 观察对象、防止循环引用 |
| 对象释放时机 | 不受 weak_ptr 影响 |
为什么需要 weak_ptr?循环引用问题
看一个危险例子:
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
~A() {
cout << "A 析构" << endl;
}
};
struct B {
shared_ptr<A> a;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<B> pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}问题是:
c
pa 拥有 A
pb 拥有 B
A 里面的 shared_ptr 拥有 B
B 里面的 shared_ptr 拥有 A函数结束时,外面的 pa 和 pb 销毁了,但 A 和 B 里面还互相持有对方。
所以引用计数不会归零,析构函数可能不会执行。
解决方法:把其中一边改成 weak_ptr。
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
~A() {
cout << "A 析构" << endl;
}
};
struct B {
// B 只观察 A,不拥有 A
weak_ptr<A> a;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
shared_ptr<A> pa = make_shared<A>();
shared_ptr<B> pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}这样 A 和 B 就不会互相强持有,对象能正常释放。
make_unique 和 make_shared 为什么推荐?
推荐写:
c
auto p1 = make_unique<Player>();
auto p2 = make_shared<Player>();少写:
c
unique_ptr<Player> p1(new Player());
shared_ptr<Player> p2(new Player());原因是:
更简洁
更安全
make_shared 通常还能减少一次内存分配make_shared 通常会把对象和控制块一起分配,效率更好。
shared_ptr 的控制块是什么?
shared_ptr 不只是保存一个裸指针,它还需要控制块。控制块里通常有:
强引用计数
弱引用计数
删除器
分配器信息简单理解:
c
shared_ptr 指向对象
shared_ptr 也共享一个控制块
控制块记录还有多少个 shared_ptr / weak_ptr 关联它当强引用计数变成 0:
对象被销毁当弱引用计数也变成 0:
控制块也被释放三者对比表:
| 对比点 | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| 所有权 | 独占 | 共享 | 不拥有 |
| 能否拷贝 | 不能 | 能 | 能 |
| 能否移动 | 能 | 能 | 能 |
| 是否有引用计数 | 没有 | 有强引用计数 | 有弱引用计数 |
| 是否影响对象生命周期 | 影响 | 影响 | 不影响 |
| 访问对象 | 直接访问 | 直接访问 | 先 lock() |
| 主要用途 | 单一所有者 | 多个所有者 | 观察、防循环引用 |
怎么选择?
默认优先用:
c
unique_ptr因为它简单、清晰、开销小。
只有当多个地方确实都需要拥有对象时,才用:
c
shared_ptr当你只是想观察 shared_ptr 管理的对象,或者要打破循环引用时,用:
c
weak_ptrNOTE
面试高分回答:unique_ptr、shared_ptr、weak_ptr 都是 C++ 的智能指针,用来通过 RAII 管理动态对象。unique_ptr 表示独占所有权,不能拷贝,只能移动,开销小,适合对象只有一个明确拥有者的场景。shared_ptr 表示共享所有权,内部通过控制块维护强引用计数,多个 shared_ptr 可以共同管理同一个对象,当最后一个 shared_ptr 销毁时对象才释放,但它有引用计数开销,并且可能产生循环引用。weak_ptr 是弱引用,不增加强引用计数,不拥有对象,不能直接访问对象,要通过 lock() 临时转成 shared_ptr,主要用于观察对象是否还活着,以及打破 shared_ptr 的循环引用。实际项目中一般优先用 unique_ptr,确实需要共享所有权才用 shared_ptr,需要观察或避免循环引用时用 weak_ptr。
shared_ptr 循环引用怎么解决?
一句话理解:shared_ptr 循环引用要用 weak_ptr 打断:真正拥有对象的一方用 shared_ptr,只是反向访问、观察的一方用 weak_ptr。
为什么会循环引用?shared_ptr 靠“强引用计数”决定对象什么时候释放。
强引用计数 == 0
对象才会析构如果两个对象互相用 shared_ptr 指向对方,就会这样:
A 拥有 B
B 又拥有 A即使外部的 shared_ptr 都销毁了,A 和 B 内部还互相把对方的引用计数撑住,导致计数永远不能归零,析构函数不执行。
错误示例:
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b; // A 强引用 B
~A() {
cout << "A 析构" << endl;
}
};
struct B {
shared_ptr<A> a; // B 强引用 A,形成环
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}这段代码的问题是:A 和 B 互相强引用。 程序结束时,外面的 pa、pb 没了,但对象内部还互相持有,所以可能看不到:
c
A 析构
B 析构正确做法:用 weak_ptr 打断环
通常保留一个方向为 shared_ptr,另一个方向改成 weak_ptr。
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b; // A 拥有 B
~A() {
cout << "A 析构" << endl;
}
};
struct B {
weak_ptr<A> a; // B 只观察 A,不拥有 A
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}这里 weak_ptr<A> 不会增加 A 的强引用计数,所以外部 pa 释放后,A 的强引用计数可以正常归零,对象就能析构。
weak_ptr 怎么访问对象?
weak_ptr 不能直接 -> 访问对象,因为它不保证对象还活着。 要先调用 lock() 临时转成 shared_ptr。
c
#include <iostream>
#include <memory>
using namespace std;
struct A {
void SayHello() {
cout << "Hello A" << endl;
}
};
int main() {
shared_ptr<A> pa = make_shared<A>();
weak_ptr<A> wa = pa;
// lock 会尝试把 weak_ptr 转成 shared_ptr
// 如果对象还活着,返回有效 shared_ptr
// 如果对象已经释放,返回空 shared_ptr
if (auto temp = wa.lock()) {
temp->SayHello();
} else {
cout << "A 已经被释放" << endl;
}
return 0;
}怎么判断哪边用 weak_ptr? 看“所有权”。
比如父子关系:
父对象拥有子对象
子对象只需要知道父对象是谁那通常:
c
父 -> 子:shared_ptr
子 -> 父:weak_ptr示例:
c
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
struct Parent;
struct Child {
weak_ptr<Parent> parent; // 子对象不拥有父对象,只观察父对象
~Child() {
cout << "Child 析构" << endl;
}
};
struct Parent {
vector<shared_ptr<Child>> children; // 父对象拥有多个子对象
~Parent() {
cout << "Parent 析构" << endl;
}
};
int main() {
auto parent = make_shared<Parent>();
auto child = make_shared<Child>();
parent->children.push_back(child);
child->parent = parent;
return 0;
}IMPORTANT
面试高分回答:shared_ptr 循环引用的根本原因是两个对象互相用 shared_ptr 强持有,导致引用计数无法归零,对象析构函数不会执行,从而造成内存泄漏。解决方法是梳理所有权关系,把不拥有对象的一方改成 weak_ptr。weak_ptr 不增加强引用计数,只是观察对象是否还存在,使用前通过 lock() 临时转成 shared_ptr,如果对象已经释放则返回空指针。常见设计是父对象用 shared_ptr 拥有子对象,子对象用 weak_ptr 指回父对象。核心原则是:拥有关系用 shared_ptr,非拥有关系用 weak_ptr。
make_shared 有什么好处?
一句话理解:make_shared 是创建 shared_ptr 的推荐写法:更简洁,通常更高效,也更安全。
普通写法:
c
shared_ptr<Player> p(new Player());推荐写法:
c
auto p = make_shared<Player>();好处 1:通常少一次内存分配
shared_ptr 不只是保存对象指针,它还需要一个“控制块”。
控制块里通常保存:
强引用计数
弱引用计数
删除器
分配器信息如果你这样写:
c
shared_ptr<Player> p(new Player());通常可能发生两次分配:
c
1. new Player() 分配 Player 对象
2. shared_ptr 内部分配控制块而:
c
auto p = make_shared<Player>();主流实现通常会把“对象”和“控制块”放在一块内存里,减少一次堆分配。
对象 T + 控制块这带来的好处是:
分配次数少
性能更好
内存局部性更好
代码更短好处 2:代码更简洁
c
shared_ptr<Player> p(new Player("Tom", 100));可以写成:
c
auto p = make_shared<Player>("Tom", 100);完整例子:
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
Player(string name, int hp) {
cout << "创建玩家: " << name << ", hp = " << hp << endl;
}
~Player() {
cout << "销毁玩家" << endl;
}
};
int main() {
// 推荐写法:不用手写 new
auto player = make_shared<Player>("Tom", 100);
return 0;
}好处 3:更异常安全
make_shared 避免你把裸指针暴露出来。
不推荐:
c
shared_ptr<Player> p(new Player());推荐:
c
auto p = make_shared<Player>();尤其在复杂表达式里,裸 new 更容易引入资源泄漏风险。make_shared 把“创建对象”和“交给 shared_ptr 管理”合成一步,更符合 RAII 思想。
好处 4:更符合现代 C++ 习惯
现代 C++ 一般推荐:
c
make_unique
make_shared而不是:
c
new
delete因为手动 new/delete 容易忘记释放,智能指针工厂函数更清晰。
make_shared 也不是永远最好
第一种情况:需要自定义删除器时,不适合用 make_shared。
c
#include <iostream>
#include <memory>
using namespace std;
struct FileCloser {
void operator()(FILE* fp) const {
if (fp) {
cout << "关闭文件" << endl;
fclose(fp);
}
}
};
int main() {
// 这里需要自定义删除器,所以不用 make_shared
shared_ptr<FILE> file(fopen("data.txt", "r"), FileCloser{});
return 0;
}第二种情况:对象很大,并且还有 weak_ptr 活很久。
make_shared 通常把对象和控制块放在同一块内存中。 当最后一个 shared_ptr 消失时,对象会析构;但如果还有 weak_ptr 存在,控制块还不能释放。
因为对象和控制块在同一整块内存里,所以这整块内存可能要等 weak_ptr 都消失后才真正释放。
所以:
c
大对象 + weak_ptr 很长寿这种场景要小心。
对比表:
| 写法 | 优点 | 缺点 |
|---|---|---|
make_shared<T>() | 简洁、通常一次分配、异常安全 | 不方便自定义删除器 |
shared_ptr<T>(new T) | 更灵活,可配自定义删除器 | 通常两次分配,裸 new 暴露 |
make_unique<T>() | 独占所有权,开销更小 | 不能共享所有权 |
IMPORTANT
面试高分回答:make_shared 是创建 shared_ptr 的推荐方式。它的主要好处是代码更简洁,不需要显式写 new,减少裸指针暴露;同时主流实现通常会把对象和 shared_ptr 的控制块放在同一块内存中,一次分配完成,所以比 shared_ptr<T>(new T) 通常更高效,内存局部性也更好。另外它把对象创建和智能指针接管合成一步,异常安全性更好。但它也不是所有场景都适合,比如需要自定义删除器时不能直接用 make_shared;如果对象很大且存在长生命周期的 weak_ptr,对象存储所在的大块内存可能要等控制块释放时才归还。默认优先用 make_shared,特殊需求再手动构造 shared_ptr。
智能指针能不能完全避免内存泄漏?
一句话理解: 智能指针不能完全避免内存泄漏。它能大幅减少“忘记 delete”导致的泄漏,但如果所有权设计错了,照样会泄漏。
智能指针能解决什么? 它能解决最常见的手动内存管理问题:
new 了忘记 delete
中途 return 导致 delete 没执行
抛异常导致释放逻辑没执行比如:
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
Player() {
cout << "Player 创建" << endl;
}
~Player() {
cout << "Player 销毁" << endl;
}
};
void Test() {
// unique_ptr 接管 Player 对象
auto p = make_unique<Player>();
// 即使这里提前 return
// p 离开作用域时也会自动析构
return;
}这就是 RAII 的好处:资源跟着对象走。
但为什么不能完全避免?
因为智能指针只是工具,它不能自动判断你的所有权设计对不对。
最典型的问题就是 shared_ptr 循环引用。
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
~A() {
cout << "A 析构" << endl;
}
};
struct B {
shared_ptr<A> a;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}这里 A 和 B 互相强引用:
c
A 强持有 B
B 强持有 A外部的 pa、pb 销毁后,A 和 B 内部还互相撑着对方的引用计数,所以强引用计数无法归零,对象不会析构。
解决方式:用 weak_ptr 打断循环
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b; // A 拥有 B
~A() {
cout << "A 析构" << endl;
}
};
struct B {
weak_ptr<A> a; // B 只观察 A,不拥有 A
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto pa = make_shared<A>();
auto pb = make_shared<B>();
pa->b = pb;
pb->a = pa;
return 0;
}weak_ptr 不增加强引用计数,所以不会阻止对象释放。
还有哪些情况智能指针也挡不住?
比如 unique_ptr::release() 用错:
c
#include <memory>
using namespace std;
void Test() {
auto p = make_unique<int>(10);
int* raw = p.release();
// release 的意思是:
// unique_ptr 放弃所有权,不再负责 delete
// 如果你后面忘记 delete raw,就泄漏了
// delete raw; // 必须手动释放,但不推荐这样写
}release() 很危险,因为它把智能指针重新变回裸指针问题。
还有一种错误:同一个裸指针交给多个 shared_ptr。
c
#include <memory>
using namespace std;
void Test() {
int* raw = new int(10);
shared_ptr<int> p1(raw);
shared_ptr<int> p2(raw); // 错误:两个 shared_ptr 控制块管理同一个 raw
// 这不是普通泄漏,而是更严重:
// 可能 double delete,程序崩溃
}正确写法是:
c
auto p1 = make_shared<int>(10);
auto p2 = p1; // 共享同一个控制块长期缓存也可能看起来像泄漏
c
#include <memory>
#include <vector>
using namespace std;
struct Texture {
// 模拟大资源
};
vector<shared_ptr<Texture>> cache;
void LoadTexture() {
auto tex = make_shared<Texture>();
// 放进全局缓存后,只要 cache 不清理
// 这个 Texture 就一直不会释放
cache.push_back(tex);
}这不一定是“内存泄漏 bug”,但如果缓存永远不清理,内存就会一直涨。
所以要有明确策略:
c
cache.clear();或者做 LRU 缓存、资源引用计数、资源卸载策略。
lambda 捕获 shared_ptr 也可能延长生命周期
c
#include <functional>
#include <memory>
using namespace std;
struct Player {
void Update() {}
};
function<void()> callback;
void RegisterCallback() {
auto player = make_shared<Player>();
callback = [player]() {
// lambda 捕获了 shared_ptr
// 只要 callback 还在,player 就不会释放
player->Update();
};
}如果这个回调被全局事件系统长期保存,player 也会一直活着。
更谨慎的写法:
c
void RegisterCallback() {
auto player = make_shared<Player>();
weak_ptr<Player> weakPlayer = player;
callback = [weakPlayer]() {
// 使用前先 lock,确认对象还活着
if (auto p = weakPlayer.lock()) {
p->Update();
}
};
}怎么减少泄漏风险?
优先用:
c
unique_ptr因为独占所有权最清晰。
只有确实需要多个地方共同拥有时,才用:
c
shared_ptr如果只是观察,不负责生命周期,用:
c
weak_ptr
引用
裸指针注意:裸指针不一定错,只要它不负责释放资源,它可以表示“非拥有观察”。
WARNING
面试高分回答: 智能指针不能完全避免内存泄漏。它解决的是资源释放自动化问题,比如避免忘记 delete、异常提前返回导致释放逻辑不执行等。但它不能替程序员设计正确的所有权关系。比如两个对象互相用 shared_ptr 强引用,会造成循环引用,引用计数无法归零,对象不会析构,需要用 weak_ptr 打断。再比如错误使用 release()、把同一个裸指针交给多个 shared_ptr、全局容器长期持有 shared_ptr、lambda 或回调长期捕获 shared_ptr,都可能造成泄漏或生命周期过长。所以智能指针是减少泄漏的工具,不是绝对保险;关键是明确所有权:独占用 unique_ptr,共享用 shared_ptr,观察用 weak_ptr。
什么情况下不能用 shared_ptr?
一句话理解:shared_ptr 只适合“共享所有权”。如果对象不是多个地方共同负责生命周期,就不要用 shared_ptr。
1. 对象只有一个主人时,不该用 shared_ptr 如果对象只有一个明确拥有者,用 unique_ptr 更好。
c
#include <memory>
using namespace std;
class Player {};
int main() {
// 推荐:对象只有一个主人
unique_ptr<Player> p = make_unique<Player>();
// 不推荐:明明不共享,却用 shared_ptr
// shared_ptr<Player> p = make_shared<Player>();
return 0;
}shared_ptr 会带引用计数开销,而且会让所有权变模糊:别人看到它会以为“这个对象可能被多个地方共享”。
2. 只是函数临时使用,不该传 shared_ptr
如果函数只是用一下对象,不负责延长对象生命周期,传引用或裸指针更合适。
c
#include <iostream>
#include <memory>
using namespace std;
class Player {
public:
void Attack() {
cout << "攻击" << endl;
}
};
// 推荐:这里只是使用 Player,不拥有 Player
void UsePlayer(Player& player) {
player.Attack();
}
int main() {
auto p = make_shared<Player>();
UsePlayer(*p);
return 0;
}不推荐这样写:
c
void UsePlayer(shared_ptr<Player> player) {
player->Attack();
}因为这样会复制 shared_ptr,强引用计数加一,函数结束再减一。 如果只是临时访问,这个开销和语义都没必要。
3. 栈对象不能交给 shared_ptr 管
非常危险:
c
#include <memory>
using namespace std;
class Player {};
int main() {
Player player; // 栈对象
// 错误:shared_ptr 会以为自己负责 delete 这个对象
shared_ptr<Player> p(&player);
return 0;
}player 是栈对象,函数结束会自动析构。 但 shared_ptr 析构时也会尝试 delete &player,这会造成未定义行为。
简单记:
shared_ptr 默认管理堆对象
不要让它接管栈对象、成员对象、全局对象的地址4. 同一个裸指针不能交给多个 shared_ptr
错误写法:
c
#include <memory>
using namespace std;
class Player {};
int main() {
Player* raw = new Player();
shared_ptr<Player> p1(raw);
shared_ptr<Player> p2(raw); // 错误
return 0;
}问题是:p1 和 p2 会各自创建一个控制块。 它们都以为自己是这个对象的主人,最后可能对同一个对象 delete 两次。
正确写法:
c
auto p1 = make_shared<Player>();
// 从已有 shared_ptr 拷贝
// 这样 p1 和 p2 共享同一个控制块
auto p2 = p1;5. 在对象内部直接用 shared_ptr(this) 很危险
错误写法:
c
#include <memory>
using namespace std;
class Player {
public:
shared_ptr<Player> GetPtr() {
// 错误:会创建新的控制块
return shared_ptr<Player>(this);
}
};如果这个对象本来已经由某个 shared_ptr 管理,再 shared_ptr<Player>(this),就会出现多个控制块管理同一个对象的问题。
正确做法是继承:
c
#include <memory>
using namespace std;
class Player : public enable_shared_from_this<Player> {
public:
shared_ptr<Player> GetPtr() {
// 正确:返回和原 shared_ptr 共享控制块的 shared_ptr
return shared_from_this();
}
};
int main() {
auto p = make_shared<Player>();
auto p2 = p->GetPtr();
return 0;
}注意:shared_from_this() 必须在对象已经被 shared_ptr 管理之后才能用。
6. 会形成循环引用时,不能两边都用 shared_ptr
错误写法:
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
~A() {
cout << "A 析构" << endl;
}
};
struct B {
shared_ptr<A> a;
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b = b;
b->a = a;
return 0;
}这里 A 和 B 互相强引用,强引用计数不能归零,析构函数可能不会执行。
正确做法:一边改成 weak_ptr。
c
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A {
shared_ptr<B> b;
~A() {
cout << "A 析构" << endl;
}
};
struct B {
weak_ptr<A> a; // 不拥有 A,只观察 A
~B() {
cout << "B 析构" << endl;
}
};
int main() {
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b = b;
b->a = a;
return 0;
}7. 性能敏感场景,不要滥用 shared_ptr
shared_ptr 有引用计数。拷贝时计数加一,析构时计数减一。 在多线程场景下,引用计数通常还涉及原子操作,成本比普通指针、引用、unique_ptr 更高。
比如频繁调用的函数里,如果只是借用对象,不要这样:
c
void Update(shared_ptr<Player> player) {
// 每次调用都会增加/减少引用计数
}更合适:
c
void Update(Player& player) {
// 只是使用,不改变所有权
}8. 对象生命周期由别人管理时,不要用 shared_ptr 接管
比如某些引擎、框架、对象池、C API 已经负责对象生命周期,你只是拿到一个地址。 这种情况下,你通常不应该随便写:
c
shared_ptr<T> p(raw);因为这表示:
c
我现在负责 delete raw如果实际不是你负责释放,就会出问题。
可以用:
c
T* // 非拥有观察
T& // 非空引用
weak_ptr // 观察 shared_ptr 管理的对象怎么选择?
| 场景 | 推荐 |
|---|---|
| 独占拥有 | unique_ptr |
| 多个地方共同拥有 | shared_ptr |
| 只观察,不拥有 | weak_ptr / 裸指针 / 引用 |
| 函数只是临时使用 | 引用或裸指针 |
| 可能循环引用 | 一边 shared_ptr,一边 weak_ptr |
| 栈对象、成员对象 | 不要交给 shared_ptr 接管 |
| 性能敏感热路径 | 尽量避免频繁拷贝 shared_ptr |
NOTE
面试高分回答:shared_ptr 不能当成默认指针使用,它只适合表达共享所有权。如果对象只有一个拥有者,应该用 unique_ptr;如果函数只是临时访问对象,应该传引用或裸指针,而不是复制 shared_ptr;如果对象是栈对象、成员对象或生命周期由别的系统管理,不能用 shared_ptr 接管它的地址;同一个裸指针也不能交给多个 shared_ptr,否则会产生多个控制块导致重复释放;对象内部也不能随便 shared_ptr(this),应使用 enable_shared_from_this。另外,如果两个对象互相用 shared_ptr 持有,会形成循环引用,需要用 weak_ptr 打断。核心原则是:shared_ptr 表示共享拥有,不是普通传参工具。
引用计数线程安全吗?
一句话理解:shared_ptr 的引用计数是线程安全的,但它指向的对象不是自动线程安全的。
准确结论:shared_ptr 有三层要分开看:
| 关注点 | 是否线程安全 |
|---|---|
| 控制块里的引用计数增减 | 是,线程安全 |
不同 shared_ptr 副本并发拷贝、析构 | 是,线程安全 |
多线程读写同一个 shared_ptr 变量 | 不一定,需要同步 |
多线程访问 shared_ptr 指向的对象 | 不自动安全,需要对象自己加锁 |
安全的情况:不同 shared_ptr 副本
c
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
struct Player {
~Player() {
cout << "Player 析构" << endl;
}
};
int main() {
auto p = make_shared<Player>();
thread t1([p]() {
// 这里捕获了一份 shared_ptr 副本
// 引用计数会安全地 +1 / -1
cout << "线程 1 使用 Player" << endl;
});
thread t2([p]() {
// 这里也是一份 shared_ptr 副本
// 多个副本共享同一个控制块,引用计数增减是安全的
cout << "线程 2 使用 Player" << endl;
});
t1.join();
t2.join();
return 0;
}这里没问题。 因为 t1 和 t2 拿到的是不同的 shared_ptr 副本,它们共享同一个控制块,控制块里的引用计数操作是线程安全的。
不安全的情况:对象本身被并发修改
c
#include <iostream>
#include <memory>
#include <thread>
using namespace std;
struct Player {
int hp = 100;
void Hurt() {
// hp++ / hp-- 这种操作不是原子操作
hp--;
}
};
int main() {
auto p = make_shared<Player>();
thread t1([p]() {
for (int i = 0; i < 10000; i++) {
p->Hurt();
}
});
thread t2([p]() {
for (int i = 0; i < 10000; i++) {
p->Hurt();
}
});
t1.join();
t2.join();
cout << p->hp << endl;
return 0;
}这段代码里,shared_ptr 的引用计数是安全的,但 Player::hp 不是安全的。 两个线程同时修改 hp,会发生数据竞争。
正确做法:
c
#include <iostream>
#include <memory>
#include <thread>
#include <mutex>
using namespace std;
struct Player {
int hp = 100;
mutex mtx;
void Hurt() {
// 保护对象内部数据
lock_guard<mutex> lock(mtx);
hp--;
}
};不安全的情况:多个线程读写同一个 shared_ptr 变量
c
#include <memory>
#include <thread>
using namespace std;
struct Player {};
shared_ptr<Player> globalPlayer;
void Writer() {
// 写 globalPlayer
globalPlayer = make_shared<Player>();
}
void Reader() {
// 读 globalPlayer
if (globalPlayer) {
// 使用 globalPlayer
}
}这里要小心: globalPlayer 是同一个 shared_ptr 变量。一个线程写它,另一个线程读它,没有同步就可能数据竞争。
可以用互斥锁:
c
#include <memory>
#include <mutex>
using namespace std;
struct Player {};
shared_ptr<Player> globalPlayer;
mutex globalMutex;
void SetPlayer(shared_ptr<Player> p) {
lock_guard<mutex> lock(globalMutex);
globalPlayer = p;
}
shared_ptr<Player> GetPlayer() {
lock_guard<mutex> lock(globalMutex);
// 返回一份副本
// 副本会增加引用计数,保证对象在使用期间还活着
return globalPlayer;
}使用时:
c
auto local = GetPlayer();
if (local) {
// local 是局部副本
// 只要 local 还活着,对象就不会被释放
}C++20 可以用 atomic<shared_ptr<T>>
c
#include <memory>
#include <atomic>
using namespace std;
struct Player {};
atomic<shared_ptr<Player>> globalPlayer;
void Writer() {
auto p = make_shared<Player>();
// 原子写入 shared_ptr
globalPlayer.store(p);
}
void Reader() {
// 原子读取 shared_ptr
auto p = globalPlayer.load();
if (p) {
// 安全拿到 shared_ptr 副本
}
}注意:atomic<shared_ptr<T>> 解决的是 shared_ptr 变量本身的并发读写问题。 它仍然不保证 Player 对象内部数据线程安全。
NOTE
面试高分回答:shared_ptr 的引用计数是线程安全的,因为多个 shared_ptr 副本共享同一个控制块,拷贝、析构时对强引用计数的增减可以在多线程下安全进行。所以不同线程各自持有 shared_ptr 副本,一般没问题。但这不代表 shared_ptr 指向的对象是线程安全的,多个线程同时读写对象内部数据仍然需要 mutex 或 atomic。另外,如果多个线程访问的是同一个 shared_ptr 变量,并且至少一个线程在赋值、reset 等修改它,也需要加锁,或者在 C++20 使用 std::atomic<std::shared_ptr<T>>。一句话:shared_ptr 只保证引用计数层面的线程安全,不保证对象访问层面的线程安全。
自定义 deleter 有什么用?
一句话理解: 自定义 deleter 的作用是:让智能指针在销毁资源时,不走默认的 delete,而是执行你指定的释放方式,比如 fclose、free、close、Destroy、归还对象池。

为什么需要它? 智能指针默认以为资源是这样来的:
c
T* p = new T; // 用 new 创建对象,所以默认应该用 delete 释放但很多资源不是 new 出来的:
| 资源来源 | 正确释放方式 |
|---|---|
new | delete |
new[] | delete[] |
malloc | free |
fopen | fclose |
| socket / fd | close |
| 第三方库资源 | 对应的 DestroyXXX |
| 对象池对象 | 归还对象池 |
如果释放方式不匹配,就可能内存泄漏、崩溃、未定义行为。
例子 1:用 shared_ptr 管理 FILE*
c
#include <cstdio> // 引入 C 文件操作函数,比如 fopen 和 fclose
#include <memory> // 引入智能指针 shared_ptr
#include <iostream> // 引入 cout 输出
using namespace std; // 使用标准命名空间,少写 std::
int main() { // 程序入口函数开始
shared_ptr<FILE> file( // 创建一个 shared_ptr,用来管理 FILE*
fopen("data.txt", "r"), // 打开文件,返回 FILE*
[](FILE* fp) { // 自定义 deleter,参数是要释放的 FILE*
if (fp != nullptr) { // 判断文件指针是否有效
cout << "关闭文件" << endl; // 输出提示信息
fclose(fp); // 正确释放 FILE*,不能用 delete
} // if 结束
} // lambda deleter 结束
); // shared_ptr 构造结束
if (!file) { // 判断文件是否打开失败
cout << "打开文件失败" << endl; // 输出失败信息
return 0; // 提前结束程序
} // if 结束
cout << "文件打开成功" << endl; // 输出成功信息
return 0; // main 函数结束,file 自动调用 fclose
} // 程序结束这里重点是:
c
fopen -> fclose
不能 -> delete例子 2:malloc 要配 free
c
#include <cstdlib> // 引入 malloc 和 free
#include <memory> // 引入 shared_ptr
#include <iostream> // 引入 cout
using namespace std; // 使用标准命名空间
int main() { // 程序入口函数开始
shared_ptr<int> p( // 创建 shared_ptr 管理 int*
static_cast<int*>(malloc(sizeof(int))), // 用 malloc 分配 int 大小的内存
[](int* ptr) { // 自定义 deleter,负责释放 malloc 的内存
if (ptr != nullptr) { // 判断指针是否为空
cout << "使用 free 释放内存" << endl; // 输出释放提示
free(ptr); // malloc 分配的内存必须用 free 释放
} // if 结束
} // lambda deleter 结束
); // shared_ptr 构造结束
if (p != nullptr) { // 判断内存是否分配成功
*p = 100; // 给这块内存写入一个整数
cout << *p << endl; // 输出这个整数
} // if 结束
return 0; // main 结束,p 自动调用 free
} // 程序结束错误写法是:
c
shared_ptr<int> p(static_cast<int*>(malloc(sizeof(int)))); // 错误:默认 deleter 会用 delete,而 malloc 应该配 free例子 3:unique_ptr 的自定义 deleter
unique_ptr 也能自定义 deleter,但它的写法更明显,因为 deleter 是类型的一部分。
c
#include <cstdio> // 引入 FILE、fopen、fclose
#include <memory> // 引入 unique_ptr
using namespace std; // 使用标准命名空间
int main() { // 程序入口函数开始
unique_ptr<FILE, decltype(&fclose)> file( // 创建 unique_ptr,并指定 deleter 类型是 fclose 函数指针
fopen("data.txt", "r"), // 打开文件,得到 FILE*
&fclose // 指定销毁时调用 fclose
); // unique_ptr 构造结束
return 0; // main 结束,file 自动调用 fclose
} // 程序结束shared_ptr 和 unique_ptr 的区别:
| 对比点 | shared_ptr | unique_ptr |
|---|---|---|
| deleter 存在哪里 | 控制块里 | 类型里 |
| 是否影响指针类型 | 不影响 shared_ptr<T> | 影响 unique_ptr<T, Deleter> |
| 常见写法 | shared_ptr<T>(ptr, deleter) | unique_ptr<T, Deleter>(ptr, deleter) |
| 是否有引用计数 | 有 | 没有 |
例子 4:对象池里不是 delete,而是归还池子
c
#include <iostream> // 引入 cout
#include <memory> // 引入 shared_ptr
using namespace std; // 使用标准命名空间
class Bullet { // 定义 Bullet 类
public: // public 成员开始
void Fire() { // 定义发射函数
cout << "子弹发射" << endl; // 输出发射信息
} // Fire 函数结束
}; // Bullet 类结束
class BulletPool { // 定义对象池类
public: // public 成员开始
Bullet* Get() { // 从对象池获取对象
cout << "从对象池取出 Bullet" << endl; // 输出获取信息
return new Bullet(); // 演示用 new,真实对象池可能复用旧对象
} // Get 函数结束
void Release(Bullet* bullet) { // 把对象归还对象池
cout << "归还 Bullet 到对象池" << endl; // 输出归还信息
delete bullet; // 演示用 delete,真实对象池可能只是标记为空闲
} // Release 函数结束
}; // BulletPool 类结束
int main() { // 程序入口函数开始
BulletPool pool; // 创建对象池
shared_ptr<Bullet> bullet( // 创建 shared_ptr 管理 Bullet
pool.Get(), // 从对象池取出一个 Bullet
[&pool](Bullet* b) { // 自定义 deleter,捕获对象池引用
pool.Release(b); // 销毁时不是直接 delete,而是归还对象池
} // lambda deleter 结束
); // shared_ptr 构造结束
bullet->Fire(); // 使用 Bullet 发射
return 0; // main 结束,bullet 自动归还对象池
} // 程序结束make_shared 能不能指定自定义 deleter? 不能直接指定。
c
auto p = make_shared<Player>(); // make_shared 使用默认释放方式,不能传自定义 deleter如果你需要自定义 deleter,就要这样写:
c
shared_ptr<FILE> file(fopen("data.txt", "r"), fclose); // 手动构造 shared_ptr,并指定 fclose 作为 deleterIMPORTANT
面试高分回答: 自定义 deleter 用来告诉智能指针资源该如何释放。默认智能指针会用 delete,但很多资源不能用 delete,比如 FILE* 要用 fclose,malloc 的内存要用 free,socket 要用 close,第三方库资源要调用对应的销毁函数,对象池资源要归还对象池。自定义 deleter 的本质是把“释放资源的方式”也纳入 RAII 管理,避免忘记释放或释放方式不匹配。shared_ptr 的 deleter 存在控制块中,不影响 shared_ptr<T> 类型;unique_ptr 的 deleter 是类型的一部分,所以要写成 unique_ptr<T, Deleter>。最重要的原则是:资源怎么来的,就要用匹配的方式释放。
C++ 中如何管理资源生命周期?
一句话理解: C++ 管资源生命周期,核心不是“记得释放”,而是用 RAII:让资源跟着对象走,对象创建时拿资源,对象销毁时自动释放资源。
资源包括什么? 资源不只是内存。凡是“用了之后必须归还”的东西,都算资源。
| 资源 | 获取方式 | 释放方式 |
|---|---|---|
| 堆内存 | new | delete |
| 数组内存 | new[] | delete[] |
| C 内存 | malloc | free |
| 文件 | fopen | fclose |
| 锁 | lock | unlock |
| socket | socket | close |
| 第三方库资源 | CreateXXX | DestroyXXX |
RAII 是核心思想 RAII 就是:构造函数里获取资源,析构函数里释放资源。
c
#include <iostream> // 引入标准输入输出库,用来使用 cout
using namespace std; // 使用标准命名空间,避免每次写 std::
class FileGuard { // 定义一个资源管理类,模拟管理文件资源
public: // public 区域开始,外部可以访问这里的成员
FileGuard() { // 构造函数,对象创建时自动执行
cout << "打开文件" << endl; // 模拟获取资源,比如打开文件
} // 构造函数结束
~FileGuard() { // 析构函数,对象销毁时自动执行
cout << "关闭文件" << endl; // 模拟释放资源,比如关闭文件
} // 析构函数结束
}; // FileGuard 类定义结束
void Test() { // 定义一个测试函数
FileGuard file; // 创建局部对象,构造时自动获取资源
cout << "正在使用文件" << endl; // 模拟使用资源
} // Test 函数结束,file 离开作用域,自动调用析构函数
int main() { // 程序入口函数
Test(); // 调用测试函数
return 0; // 程序正常结束
} // main 函数结束堆内存怎么管?优先用智能指针 不要到处手写 new/delete。现代 C++ 里,堆对象优先交给智能指针。
c
#include <memory> // 引入智能指针相关头文件
using namespace std; // 使用标准命名空间
class Player { }; // 定义一个 Player 类,模拟游戏角色
void CreatePlayer() { // 定义创建角色的函数
unique_ptr<Player> p1 = make_unique<Player>(); // 独占所有权,离开作用域自动释放
shared_ptr<Player> p2 = make_shared<Player>(); // 共享所有权,最后一个 shared_ptr 销毁时释放
weak_ptr<Player> p3 = p2; // 弱引用,只观察对象,不延长生命周期
} // 函数结束,p1 自动释放,p2 引用计数归零后释放对象选择口诀:
| 场景 | 推荐 |
|---|---|
| 只有一个主人 | unique_ptr |
| 多个地方共同拥有 | shared_ptr |
| 只观察,不拥有 | weak_ptr / 引用 / 裸指针 |
| 一组对象 | vector / string 等容器 |
释放方式不是 delete | 自定义 deleter |
文件、锁这类资源也用 RAII 管 比如锁资源,不推荐手动 lock/unlock,因为中途异常可能忘记解锁。
c
#include <mutex> // 引入互斥锁相关头文件
using namespace std; // 使用标准命名空间
mutex gMutex; // 定义一个全局互斥锁
int gValue = 0; // 定义一个共享数据
void AddValue() { // 定义修改共享数据的函数
lock_guard<mutex> lock(gMutex); // 构造时自动加锁,析构时自动解锁
gValue++; // 在锁保护下修改共享数据
} // 函数结束,lock 自动析构,gMutex 自动解锁非 new 资源,用自定义 deleter 比如 FILE* 不能用 delete,必须用 fclose。
c
#include <cstdio> // 引入 FILE、fopen、fclose
#include <memory> // 引入 shared_ptr
using namespace std; // 使用标准命名空间
void ReadFile() { // 定义读取文件函数
shared_ptr<FILE> file( // 创建 shared_ptr 管理 FILE*
fopen("data.txt", "r"), // 打开文件,返回 FILE*
[](FILE* fp) { // 自定义 deleter,指定如何释放 FILE*
if (fp != nullptr) { // 判断文件指针是否有效
fclose(fp); // FILE* 必须用 fclose 释放
} // if 结束
} // lambda deleter 结束
); // shared_ptr 构造结束
} // 函数结束,file 自动调用自定义 deleterNOTE
面试高分回答: C++ 管理资源生命周期的核心是 RAII,也就是把资源绑定到对象生命周期上。构造函数获取资源,析构函数释放资源,这样即使函数提前 return 或抛异常,局部对象也会自动析构。堆内存优先用 unique_ptr、shared_ptr,只观察对象用 weak_ptr、引用或裸指针;数组和动态数据优先用 vector、string;文件、socket、第三方资源可以用智能指针加自定义 deleter;锁用 lock_guard 或 unique_lock。核心原则是:先明确所有权,再让拥有者用 RAII 自动释放资源。
游戏引擎为什么常用对象池/内存池?
一句话理解: 游戏引擎常用对象池/内存池,是为了减少运行时频繁 new/delete 或创建/销毁对象带来的卡顿、内存碎片和帧耗时抖动。
先分清:对象池和内存池不是完全一回事
对象池关心的是:
对象能不能复用比如子弹、特效、怪物、飘字、UI Item。
内存池关心的是:
内存块能不能快速分配和回收比如引擎底层分配器、组件内存、固定大小对象分配。
实际游戏引擎里经常两个一起用:对象池负责对象复用,内存池负责底层内存高效管理。
为什么游戏里特别需要池? 因为游戏是实时程序,通常一秒要跑 30 帧、60 帧、120 帧。
如果 60 FPS,每帧预算大概是:
16.67 ms一次普通 new/delete 可能单独看不贵,但如果一帧里大量发生:
生成 500 个子弹
销毁 500 个特效
刷新 1000 个 UI Item
创建很多临时对象就可能让某一帧突然变慢,玩家看到的就是卡顿。
不用对象池的问题
c
#include <iostream> // 引入 cout 输出
using namespace std; // 使用标准命名空间
class Bullet { // 定义子弹类
public: // public 成员开始
Bullet() { // 构造函数
cout << "创建子弹" << endl; // 模拟创建子弹的开销
} // 构造函数结束
~Bullet() { // 析构函数
cout << "销毁子弹" << endl; // 模拟销毁子弹的开销
} // 析构函数结束
}; // Bullet 类结束
void FireBullet() { // 定义发射子弹函数
Bullet* bullet = new Bullet(); // 每次发射都 new 一个子弹
delete bullet; // 子弹用完后立刻 delete
} // FireBullet 函数结束
int main() { // 程序入口
FireBullet(); // 发射一次子弹
return 0; // 程序结束
} // main 函数结束这种写法的问题是:如果一秒发很多子弹,就会频繁分配和释放。
对象池的思路
创建时:先准备一批对象
使用时:从池里取一个空闲对象
不用时:不 delete,而是重置状态后放回池里对象池的核心不是“不销毁所有东西”,而是:
把高频创建销毁,变成低成本复用对象池代码示例:每一行都有注释
c
#include <iostream> // 引入标准输入输出库
#include <vector> // 引入 vector 容器
#include <memory> // 引入 unique_ptr 智能指针
using namespace std; // 使用标准命名空间
class Bullet { // 定义子弹类
public: // public 成员开始
bool active = false; // 标记子弹是否正在使用
int damage = 0; // 记录子弹伤害
void Init(int newDamage) { // 初始化子弹状态
active = true; // 标记子弹为激活状态
damage = newDamage; // 设置子弹伤害
cout << "子弹激活,伤害 = " << damage << endl; // 输出激活信息
} // Init 函数结束
void Reset() { // 重置子弹状态
active = false; // 标记子弹为空闲状态
damage = 0; // 清空伤害数据
cout << "子弹回收并重置" << endl; // 输出回收信息
} // Reset 函数结束
}; // Bullet 类结束
class BulletPool { // 定义子弹对象池类
private: // private 成员开始
vector<unique_ptr<Bullet>> bullets; // 保存所有子弹对象,由对象池统一拥有
public: // public 成员开始
BulletPool(int count) { // 构造函数,提前创建指定数量的子弹
for (int i = 0; i < count; i++) { // 循环创建 count 个子弹
bullets.push_back(make_unique<Bullet>()); // 创建子弹并放入对象池
} // for 循环结束
} // 构造函数结束
Bullet* GetBullet(int damage) { // 从对象池获取一个可用子弹
for (auto& bullet : bullets) { // 遍历池中的所有子弹
if (!bullet->active) { // 找到一个没有被使用的子弹
bullet->Init(damage); // 初始化这个子弹
return bullet.get(); // 返回子弹裸指针,调用者只使用,不拥有
} // if 结束
} // for 循环结束
cout << "对象池不够用,无法发射子弹" << endl; // 输出池容量不足提示
return nullptr; // 没有空闲对象,返回空指针
} // GetBullet 函数结束
void ReleaseBullet(Bullet* bullet) { // 把子弹归还对象池
if (bullet != nullptr) { // 判断传入指针是否有效
bullet->Reset(); // 重置子弹状态,让它变回空闲
} // if 结束
} // ReleaseBullet 函数结束
}; // BulletPool 类结束
int main() { // 程序入口函数
BulletPool pool(2); // 创建一个容量为 2 的子弹池
Bullet* b1 = pool.GetBullet(10); // 从池里取出第一颗子弹
Bullet* b2 = pool.GetBullet(20); // 从池里取出第二颗子弹
Bullet* b3 = pool.GetBullet(30); // 池里没有空闲子弹,获取失败
pool.ReleaseBullet(b1); // 归还第一颗子弹
Bullet* b4 = pool.GetBullet(40); // 复用刚刚归还的子弹
(void)b2; // 避免示例里 b2 未使用的警告
(void)b3; // 避免示例里 b3 未使用的警告
(void)b4; // 避免示例里 b4 未使用的警告
return 0; // 程序结束
} // main 函数结束对象池适合什么场景?
| 场景 | 为什么适合 |
|---|---|
| 子弹 | 高频生成、很快消失 |
| 粒子特效 | 大量短生命周期对象 |
| 飘字伤害 | UI 对象反复出现 |
| 怪物小兵 | 同类对象可复用 |
| UI 列表 Item | 滚动列表反复创建销毁很贵 |
| 网络消息对象 | 高频临时对象 |
| 音效播放对象 | 可重复使用播放通道 |
内存池为什么有用? 普通堆分配器要处理各种大小的内存申请:
8 字节
24 字节
128 字节
4096 字节频繁申请和释放不同大小的内存,容易产生内存碎片。
内存池常见做法是:
提前申请一大块内存
切成固定大小的小块
分配时拿一个空闲块
释放时把块放回空闲链表这样分配释放会更快,也更可控。
内存池的简化示例:每一行都有注释
c
#include <iostream> // 引入 cout 输出
#include <vector> // 引入 vector 容器
using namespace std; // 使用标准命名空间
class FixedBlockPool { // 定义固定块内存池
private: // private 成员开始
vector<char> memory; // 保存一整块连续内存
vector<void*> freeList; // 保存空闲内存块的地址
size_t blockSize = 0; // 每个内存块的大小
public: // public 成员开始
FixedBlockPool(size_t blockCount, size_t sizePerBlock) { // 构造函数,指定块数量和块大小
blockSize = sizePerBlock; // 记录每个块的大小
memory.resize(blockCount * blockSize); // 一次性申请整块内存
for (size_t i = 0; i < blockCount; i++) { // 遍历每一个内存块
void* block = memory.data() + i * blockSize; // 计算第 i 个块的起始地址
freeList.push_back(block); // 把这个块加入空闲列表
} // for 循环结束
} // 构造函数结束
void* Allocate() { // 从内存池分配一个块
if (freeList.empty()) { // 判断是否还有空闲块
return nullptr; // 没有空闲块,返回空指针
} // if 结束
void* block = freeList.back(); // 取出最后一个空闲块
freeList.pop_back(); // 从空闲列表移除这个块
return block; // 返回分配到的内存块
} // Allocate 函数结束
void Free(void* block) { // 释放一个内存块
if (block != nullptr) { // 判断指针是否有效
freeList.push_back(block); // 把块重新放回空闲列表
} // if 结束
} // Free 函数结束
}; // FixedBlockPool 类结束
int main() { // 程序入口函数
FixedBlockPool pool(4, sizeof(int)); // 创建 4 个 int 大小块的内存池
void* block = pool.Allocate(); // 从池里分配一个内存块
int* value = new (block) int(123); // 在这块内存上构造一个 int,这叫 placement new
cout << *value << endl; // 输出 int 的值
value->~int(); // 对 placement new 构造的对象手动调用析构,int 实际不需要但这里演示规则
pool.Free(block); // 把内存块归还给内存池
return 0; // 程序结束
} // main 函数结束对象池/内存池的主要好处
减少运行时分配释放。
把不可控的 `new/delete` 成本前移到加载阶段或初始化阶段。
减少内存碎片。
固定大小对象反复复用,不容易把堆切得很碎。
提高缓存命中率。
对象放得更集中,CPU 遍历时更容易命中缓存。
让帧时间更稳定。
游戏最怕平均不慢但偶尔一卡,池化可以减少这种尖峰。
方便做内存预算。
比如某类子弹最多 1000 个,提前分配好,运行时就知道上限。但对象池不是万能的
对象归还时必须重置状态。
比如位置、速度、血量、回调、父节点、可见性、碰撞状态都要清。
池太小会不够用。
池满时要决定:扩容、拒绝生成、复用最旧对象,还是报警。
池太大会浪费内存。
不用的对象也占着空间。
容易出现悬空引用。
对象被回收后,外部还拿着旧指针继续用,会出 bug。
多线程下要加锁或分线程池。
否则多个线程同时取还对象会数据竞争。NOTE
面试高分回答: 游戏引擎常用对象池和内存池,是因为游戏是实时程序,追求稳定帧率。频繁创建和销毁子弹、特效、怪物、UI Item 等对象,会带来堆分配开销、内存碎片、缓存不友好和帧耗时尖峰。对象池通过提前创建对象、运行时复用对象,把高频的创建销毁变成取出、重置、归还;内存池通过提前申请大块内存并切成固定块,减少频繁向系统分配内存的成本。这样可以降低卡顿、减少碎片、提高缓存局部性,并让内存预算更可控。但池化也有代价,比如需要正确重置对象状态、处理池容量不足、防止旧引用误用,以及注意多线程安全。总体上,池化是用更高的内存占用和更复杂的管理,换取更稳定、更可控的运行时性能。