Skip to content

智能指针

RAII 是什么?

一句话理解: RAII 是 C++ 里用“对象生命周期”管理资源的思想:构造函数里获取资源,析构函数里释放资源。raii-explained

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:

构造时:拿到堆内存所有权
析构时:自动 delete

RAII 管理锁,非常重要

没有 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_ptrshared_ptr
文件ifstreamofstreamfstream
lock_guardunique_lock
容器内存vectorstring
线程jthread
自定义资源自己写析构函数封装

RAII 和析构函数的关系: RAII 非常依赖析构函数。

因为 C++ 有一个重要规则:

局部对象离开作用域时,会自动调用析构函数。

所以 RAII 把释放资源的逻辑放进析构函数里。

c
class Resource {
public:
    Resource() {
        // 获取资源
    }

    ~Resource() {
        // 释放资源
    }
};

面试容易问:RAII 和 GC 有什么区别?

RAII 是确定性释放。

对象离开作用域,立刻析构,资源立刻释放

GC 是垃圾回收。

对象没人引用后,将来某个时刻由 GC 回收

所以 RAII 特别适合管理文件、锁、网络连接这些需要“及时释放”的资源。

CAUTION

面试高分回答: RAII 是 C++ 里非常核心的资源管理思想,全称是 Resource Acquisition Is Initialization。它的核心是把资源和对象生命周期绑定起来,在构造函数中获取资源,在析构函数中释放资源。这样资源释放就不依赖程序员手动调用 deletecloseunlock,而是依靠对象离开作用域时自动析构完成。RAII 的好处是可以避免内存泄漏、文件句柄泄漏、锁未释放等问题,并且天然具备异常安全性。典型例子有 std::unique_ptr 管理堆内存,std::lock_guard 管理互斥锁,std::fstream 管理文件句柄。现代 C++ 中应该优先使用 RAII 和智能指针,尽量避免裸 new/delete 手动管理资源。

unique_ptrshared_ptrweak_ptr 区别是什么?

一句话理解:unique_ptr 是独占所有权,shared_ptr 是共享所有权,weak_ptr 是不拥有对象的观察者,主要用来配合 shared_ptr 防止循环引用。

smart-pointers-difference

零基础理解: 普通指针的问题是:你 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

函数结束时,外面的 papb 销毁了,但 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_ptrshared_ptrweak_ptr
所有权独占共享不拥有
能否拷贝不能
能否移动
是否有引用计数没有有强引用计数有弱引用计数
是否影响对象生命周期影响影响不影响
访问对象直接访问直接访问lock()
主要用途单一所有者多个所有者观察、防循环引用

怎么选择?

默认优先用:

c
unique_ptr

因为它简单、清晰、开销小。

只有当多个地方确实都需要拥有对象时,才用:

c
shared_ptr

当你只是想观察 shared_ptr 管理的对象,或者要打破循环引用时,用:

c
weak_ptr

NOTE

面试高分回答:unique_ptrshared_ptrweak_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-cycle-solution

为什么会循环引用?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;
}

这段代码的问题是:AB 互相强引用。 程序结束时,外面的 papb 没了,但对象内部还互相持有,所以可能看不到:

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_ptrweak_ptr 不增加强引用计数,只是观察对象是否还存在,使用前通过 lock() 临时转成 shared_ptr,如果对象已经释放则返回空指针。常见设计是父对象用 shared_ptr 拥有子对象,子对象用 weak_ptr 指回父对象。核心原则是:拥有关系用 shared_ptr,非拥有关系用 weak_ptr

make_shared 有什么好处?

一句话理解:make_shared 是创建 shared_ptr 的推荐写法:更简洁,通常更高效,也更安全。

make-shared-benefits

普通写法:

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”导致的泄漏,但如果所有权设计错了,照样会泄漏。

can-smart-pointers-prevent-all-leaks

智能指针能解决什么? 它能解决最常见的手动内存管理问题:

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

外部的 papb 销毁后,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

when-not-to-use-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;
}

问题是:p1p2 会各自创建一个控制块。 它们都以为自己是这个对象的主人,最后可能对同一个对象 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-ref-count-thread-safe

准确结论: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;
}

这里没问题。 因为 t1t2 拿到的是不同的 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 指向的对象是线程安全的,多个线程同时读写对象内部数据仍然需要 mutexatomic。另外,如果多个线程访问的是同一个 shared_ptr 变量,并且至少一个线程在赋值、reset 等修改它,也需要加锁,或者在 C++20 使用 std::atomic<std::shared_ptr<T>>。一句话:shared_ptr 只保证引用计数层面的线程安全,不保证对象访问层面的线程安全。

自定义 deleter 有什么用?

一句话理解: 自定义 deleter 的作用是:让智能指针在销毁资源时,不走默认的 delete,而是执行你指定的释放方式,比如 fclosefreecloseDestroy、归还对象池。

image-20260714231926936

为什么需要它? 智能指针默认以为资源是这样来的:

c
T* p = new T; // 用 new 创建对象,所以默认应该用 delete 释放

但很多资源不是 new 出来的:

资源来源正确释放方式
newdelete
new[]delete[]
mallocfree
fopenfclose
socket / fdclose
第三方库资源对应的 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_ptrunique_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 作为 deleter

IMPORTANT

面试高分回答: 自定义 deleter 用来告诉智能指针资源该如何释放。默认智能指针会用 delete,但很多资源不能用 delete,比如 FILE* 要用 fclosemalloc 的内存要用 free,socket 要用 close,第三方库资源要调用对应的销毁函数,对象池资源要归还对象池。自定义 deleter 的本质是把“释放资源的方式”也纳入 RAII 管理,避免忘记释放或释放方式不匹配。shared_ptr 的 deleter 存在控制块中,不影响 shared_ptr<T> 类型;unique_ptr 的 deleter 是类型的一部分,所以要写成 unique_ptr<T, Deleter>。最重要的原则是:资源怎么来的,就要用匹配的方式释放。

C++ 中如何管理资源生命周期?

一句话理解: C++ 管资源生命周期,核心不是“记得释放”,而是用 RAII:让资源跟着对象走,对象创建时拿资源,对象销毁时自动释放资源。

cpp-resource-lifetime-management

资源包括什么? 资源不只是内存。凡是“用了之后必须归还”的东西,都算资源。

资源获取方式释放方式
堆内存newdelete
数组内存new[]delete[]
C 内存mallocfree
文件fopenfclose
lockunlock
socketsocketclose
第三方库资源CreateXXXDestroyXXX

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 自动调用自定义 deleter

NOTE

面试高分回答: C++ 管理资源生命周期的核心是 RAII,也就是把资源绑定到对象生命周期上。构造函数获取资源,析构函数释放资源,这样即使函数提前 return 或抛异常,局部对象也会自动析构。堆内存优先用 unique_ptrshared_ptr,只观察对象用 weak_ptr、引用或裸指针;数组和动态数据优先用 vectorstring;文件、socket、第三方资源可以用智能指针加自定义 deleter;锁用 lock_guardunique_lock。核心原则是:先明确所有权,再让拥有者用 RAII 自动释放资源。

游戏引擎为什么常用对象池/内存池?

一句话理解: 游戏引擎常用对象池/内存池,是为了减少运行时频繁 new/delete 或创建/销毁对象带来的卡顿、内存碎片和帧耗时抖动。

game-engine-object-memory-pool

先分清:对象池和内存池不是完全一回事

对象池关心的是:

对象能不能复用

比如子弹、特效、怪物、飘字、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 等对象,会带来堆分配开销、内存碎片、缓存不友好和帧耗时尖峰。对象池通过提前创建对象、运行时复用对象,把高频的创建销毁变成取出、重置、归还;内存池通过提前申请大块内存并切成固定块,减少频繁向系统分配内存的成本。这样可以降低卡顿、减少碎片、提高缓存局部性,并让内存预算更可控。但池化也有代价,比如需要正确重置对象状态、处理池容量不足、防止旧引用误用,以及注意多线程安全。总体上,池化是用更高的内存占用和更复杂的管理,换取更稳定、更可控的运行时性能。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0