引言:为什么需要智能指针?

C++作为一门系统级编程语言,允许开发者直接操作内存,但手动管理内存(new 分配 / delete 释放 )存在三大核心痛点:

  • 内存泄漏:忘记释放内存,导致资源长期占用;
  • 野指针:指针指向已释放的内存,后续访问会触发未定义行为;
  • 重复释放:同一内存被多次 delete ,导致程序崩溃。

智能指针的核心目标是自动管理内存生命周期,其设计基于C++的RAII(Resource Acquisition Is Initialization,资源获取即初始化)思想:通过类封装原始指针,在构造函数中获取资源(分配内存),在析构函数中自动释放资源(调用 delete),即使程序异常退出,析构函数也会被编译器自动调用,从而避免内存问题。

一、智能指针的发展历程

C++智能指针的演进是为了解决“所有权管理”和“使用安全性”问题:

阶段代表指针标准版本核心特点
第一代auto_ptrC++98引入,C++17弃用首个官方智能指针,独占所有权
第二代unique_ptr / shared_ptr / weak_ptrC++11引入,C++14/C++17优化解决 auto_ptr 缺陷,覆盖绝大多数场景

核心演进逻辑:从“简单解决释放问题”到“精准管理所有权”,从“单一功能”到“分工协作”( shared_ptr 负责共享, weak_ptr 负责解环)。

二、智能指针的核心原理:RAII

智能指针本质是封装原始指针的类模板,其底层逻辑完全依赖RAII思想,步骤如下:

  1. 资源获取:智能指针对象构造时,通过 new 分配内存,将原始指针存储在类内部;
  2. 资源使用:重载 * 和 -> 运算符,让智能指针像原始指针一样使用;
  3. 资源释放:智能指针对象生命周期结束(如出作用域、被销毁)时,析构函数自动调用 delete ,释放原始指针指向的内存。
// 简化版智能指针实现(理解原理用,非标准库实现)
template <typename T>
class SimpleSmartPtr {
private:
    T* ptr; // 封装原始指针
public:
    // 构造函数:获取资源(分配内存)
    explicit SimpleSmartPtr(T* p = nullptr) : ptr(p) {}
    
    // 析构函数:释放资源(自动调用)
    ~SimpleSmartPtr() { delete ptr; }
    
    // 重载*和->,模拟原始指针行为
    T& operator*() const { return *ptr; }
    T* operator->() const { return ptr; }
    
    // 禁止拷贝(避免重复释放)
    SimpleSmartPtr(const SimpleSmartPtr&) = delete;
    SimpleSmartPtr& operator=(const SimpleSmartPtr&) = delete;
};

// 使用示例:无需手动delete
void test() {
    SimpleSmartPtr<int> p(new int(10));
    cout << *p << endl; // 输出10,像原始指针一样使用
} // p出作用域,析构函数自动释放内存

三、C++标准库智能指针详解

C++11及后续标准提供了3个核心智能指针,定义在头文件中。

1.unique_ptr:独占式智能指针

定位

  • 独占所有权:同一时间,只有一个 unique_ptr 指向某块内存,禁止拷贝(拷贝构造和赋值运算符被 delete),只能通过“所有权转移”传递指针。
  • 零额外开销:底层仅封装原始指针,无引用计数器等额外数据结构,效率与原始指针一致。

用法

  1. 创建 unique_ptr :推荐用 make_unique (C++14引入,更安全,避免内存泄漏),而非直接 new (C++11需手动 new)。
#include <memory>
#include <iostream>
using namespace std;

int main() {
    // 1. 管理单个对象(推荐make_unique)
    unique_ptr<int> p1 = make_unique<int>(10); // C++14+
    unique_ptr<int> p2(new int(20)); // C++11兼容写法(不推荐,存在异常安全问题)
    
    // 2. 管理数组(必须指定[],析构时调用delete[])
    unique_ptr<int[]> p3 = make_unique<int[]>(5);
    p3[0] = 1; p3[1] = 2; // 支持数组下标访问
    
    // 3. 访问对象:重载*和->
    cout << *p1 << endl; // 输出10
    unique_ptr<string> p4 = make_unique<string>("hello");
    cout << p4->size() << endl; // 输出5
    
    return 0;
} // 所有unique_ptr出作用域,自动释放内存

2.所有权转移:通过 std::move实现,转移后原指针变为 nullptr(避免悬空)。

unique_ptr<int> p1 = make_unique<int>(10);
unique_ptr<int> p2 = move(p1); // 转移所有权,p1变为nullptr

if (p1 == nullptr) cout << "p1 is null" << endl; 
cout << *p2 << endl; // 输出10

// 错误:禁止直接拷贝
// unique_ptr<int> p3 = p2; // 编译报错(拷贝构造被delete)

3.常用成员函数:

  • get():获取原始指针(谨慎使用,避免手动释放);
  • reset():释放当前内存,可选重新指向新对象;
  • release():释放所有权,返回原始指针(需手动管理后续释放,慎用);
  • swap():交换两个 unique_ptr 的指向。
 unique_ptr<int> p = make_unique<int>(10);
int* raw_ptr = p.get(); // 获取原始指针,不能delete raw_ptr

p.reset(new int(20)); // 释放10,指向20
p.reset(); // 释放20,p变为nullptr

int* raw = p.release(); // p变为nullptr,raw指向20(需手动delete raw)
delete raw;

适用场景

  • 管理单个对象/数组,无需共享所有权(如函数返回局部堆对象、容器中存储独立对象);
  • 替代 auto_ptr (完全解决 auto_ptr 的缺陷);
  • 作为容器元素( vector<unique_ptr> 是安全的,而 vector<auto_ptr> 会崩溃)。

优点与缺陷

  • 优点:高效(零开销)、安全(禁止拷贝避免重复释放)、支持数组;
  • 缺陷:不能共享所有权,需要共享时只能切换为 shared_ptr 。

2.shared_ptr:共享式智能指针

定位

  • 共享所有权:多个 shared_ptr 可指向同一内存,通过引用计数跟踪当前指向该内存的指针数量;
  • 自动释放:当最后一个 shared_ptr 被销毁时,引用计数变为0,自动释放内存。

原理:引用计数

shared_ptr 内部维护两个指针:

  1. 指向目标对象的数据指针;
  2. 指向“控制块”的控制指针(控制块存储引用计数、弱引用计数、自定义删除器等)。
  • 拷贝 shared_ptr 时:引用计数+1;
  • shared_ptr 销毁时:引用计数-1;
  • 引用计数为0时:释放目标对象和控制块。

用法

  1. 创建 shared_ptr :推荐用 make_shared (C++11引入,更高效,控制块和对象内存一次性分配),避免直接 new 。
// 1. 管理单个对象(推荐make_shared)
shared_ptr<int> p1 = make_shared<int>(10);
shared_ptr<int> p2(new int(20)); // 不推荐,控制块和对象分开分配,效率低

// 2. 管理数组(C++17+支持直接make_shared<T[]>,C++11需自定义删除器)
shared_ptr<int[]> p3 = make_shared<int[]>(5); // C++17+
p3[0] = 1;

// 3. 共享所有权(拷贝时引用计数+1)
shared_ptr<int> p4 = p1; // p1和p4指向同一对象,引用计数=2
cout << p1.use_count() << endl; // 输出2(use_count()获取引用计数)
  1. 常用成员函数:
  • use_count() :返回当前引用计数(仅用于调试,不建议用于逻辑判断);
  • unique() :判断引用计数是否为1(C++20弃用,建议用 use_count() == 1 );
  • reset() / get() / swap() :用法与 unique_ptr 一致;
  • 支持拷贝和赋值(自动更新引用计数)。
shared_ptr<string> p = make_shared<string>("hello");
shared_ptr<string> p2 = p; // 引用计数=2

p.reset(); // 引用计数=1,p变为nullptr
if (p2.use_count() == 1) cout << "p2 is unique" << endl; // 输出

cout << *p2 << endl; // 输出hello
  1. 自定义删除器:当默认 delete 无法满足需求(如释放数组、文件句柄、动态库资源)时,可指定删除器(函数指针、lambda、函数对象)。
// 示例1:释放数组(C++17前需自定义删除器)
shared_ptr<int> p1(new int[5], [](int* ptr) {
    delete[] ptr; // 自定义删除器,调用delete[]
});

// 示例2:释放文件句柄
#include <cstdio>
shared_ptr<FILE> file(fopen("test.txt", "r"), [](FILE* f) {
    fclose(f); // 自动关闭文件
});

线程安全性

  • 引用计数是线程安全的:多个线程同时拷贝/销毁 shared_ptr 时,引用计数的增减是原子操作,不会出现数据竞争;
  • 指向的对象不是线程安全的:多个线程同时读写 shared_ptr 指向的对象时,需手动加锁(如 std::mutex )。

适用场景

  • 多个对象共享同一资源(如多线程访问的全局对象、容器中共享的元素);
  • 资源需要传递给多个使用者,且无法确定谁先释放。

优点与缺陷

  • 优点:支持共享所有权、使用灵活、异常安全;
  • 缺陷:
    1. 额外开销(维护引用计数和控制块);
    2. 循环引用(致命问题,需 weak_ptr 配套解决)。

3.weak_ptr:弱引用智能指针(解决循环引用)

定位

  • 弱引用:不拥有对象的所有权,仅能观察 shared_ptr 管理的对象;
  • 无引用计数影响: weak_ptr 的创建、拷贝、销毁不会改变 shared_ptr 的引用计数;
  • 作用:解决 shared_ptr 的循环引用问题。

循环引用问题

当两个 shared_ptr 互相指向对方时,引用计数永远无法变为0,导致内存泄漏:

// 循环引用示例(内存泄漏)
class B; // 前向声明
class A {
public:
    shared_ptr<B> b_ptr; // A持有B的shared_ptr
    ~A() { cout << "A destroyed" << endl; }
};

class B {
public:
    shared_ptr<A> a_ptr; // B持有A的shared_ptr
    ~B() { cout << "B destroyed" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    
    a->b_ptr = b; // a指向b,b的引用计数=2
    b->a_ptr = a; // b指向a,a的引用计数=2
    
    return 0;
} // 程序结束时,a和b的引用计数均为1,不会调用析构函数(内存泄漏)

弱引用解决循环引用
将其中一个 shared_ptr 改为 weak_ptr (不增加引用计数):

class B;
class A {
public:
    shared_ptr<B> b_ptr;
    ~A() { cout << "A destroyed" << endl; }
};

class B {
public:
    weak_ptr<A> a_ptr; // 改为weak_ptr,不增加A的引用计数
    ~B() { cout << "B destroyed" << endl; }
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    
    a->b_ptr = b; // b的引用计数=2
    b->a_ptr = a; // a的引用计数=1(weak_ptr不增加计数)
    
    return 0;
} 
// 输出:A destroyed → B destroyed(无内存泄漏)

用法

  1. 创建 weak_ptr :只能通过 shared_ptr 构造(不能直接 new ):
shared_ptr<int> p = make_shared<int>(10);
weak_ptr<int> wp = p; // 基于shared_ptr创建,p的引用计数仍为1
  1. 访问对象: weak_ptr 不能直接访问对象(无 * 和 -> 重载),需通过 lock() 获取 shared_ptr (成功则引用计数+1,失败返回空 shared_ptr )。
shared_ptr<int> p = make_shared<int>(10);
weak_ptr<int> wp = p;

// 方法1:lock()获取shared_ptr
if (shared_ptr<int> temp = wp.lock()) {
    cout << *temp << endl; // 输出10(temp存在时才访问)
}

// 方法2:expired()判断对象是否已释放
if (!wp.expired()) {
    cout << *wp.lock() << endl; // 输出10
}

// 释放p的所有权
p.reset();
if (wp.expired()) cout << "object destroyed" << endl; // 输出
  1. 常用成员函数:
  • lock() :获取 shared_ptr (对象未释放时有效);
  • expired() :判断对象是否已释放(引用计数为0);
  • use_count() :获取当前 shared_ptr 的引用计数(仅用于调试);
  • reset() :解除与 shared_ptr 的关联。

适用场景

  • 解决 shared_ptr 的循环引用(如链表节点、树形结构、双向依赖对象);
  • 观察对象是否存活(不影响对象生命周期)。

优点与缺陷

  • 优点:无额外开销、不影响引用计数、解决循环引用;
  • 缺陷:不能直接访问对象,需通过 lock() 获取 shared_ptr (增加少量操作)。

4.被弃用的auto_ptr

auto_ptr 是C++98的第一代智能指针,核心问题是所有权转移设计不合理,导致悬空指针和容器使用崩溃,C++11已被 unique_ptr 替代,C++17正式弃用。

缺陷示例

 #include <memory>
#include <iostream>
using namespace std;

int main() {
    auto_ptr<int> p1(new int(10));
    auto_ptr<int> p2 = p1; // 所有权转移,p1变为悬空指针
    
    cout << *p2 << endl; // 输出10
    cout << *p1 << endl; // 未定义行为(p1已悬空,可能崩溃)
    
    // 容器中使用崩溃(auto_ptr拷贝时转移所有权,导致容器元素悬空)
    vector<auto_ptr<int>> vec;
    vec.push_back(auto_ptr<int>(new int(10)));
    vec.push_back(auto_ptr<int>(new int(20))); // 拷贝时转移所有权,第一个元素悬空
    return 0;
}

auto_ptr 被弃用的原因?

  1. 所有权转移导致悬空指针;
  2. 不能用于容器;
  3. 不支持数组。

四、智能指针对比总结

智能指针所有权引用计数额外开销支持数组(C++17)适用场景核心缺陷
auto_ptr独占(转移)已弃用(C++17)悬空指针、不能用于容器
unique_ptr独占(不可转移)是(需指定[])单个对象/数组、无需共享不能共享所有权
shared_ptr共享是(需指定[])多对象共享资源循环引用、额外开销
weak_ptr无(弱引用)解决循环引用、观察对象存活不能直接访问对象

五、常见问题与考点

1.循环引用的成因与解决?

  • 成因:两个或多个 shared_ptr 互相指向对方,导致引用计数无法归零;
  • 解决:将其中一个指针改为 weak_ptr (不增加引用计数);
  • 面试要求:能写出循环引用的代码示例,并给出解决方案(如上述A/B类案例)。

2.shared_ptr 的控制块包含哪些内容?

  1. 强引用计数( use_count() 返回值,决定对象是否释放);
  2. 弱引用计数(跟踪 weak_ptr 的数量,决定控制块是否释放);
  3. 自定义删除器;
  4. 分配器。

3.shared_ptr 的线程安全性?

  • 引用计数的增减是线程安全的(原子操作);
  • 指向的对象本身不是线程安全的(多个线程读写需加锁);
  • 示例:
shared_ptr<int> p = make_shared<int>(10);
// 线程1:拷贝shared_ptr(线程安全)
// 线程2:修改对象(非线程安全,需加锁)
thread t2([p]() { *p = 20; }); // 数据竞争,未定义行为

4.智能指针与原始指针的转换?

  • 智能指针→原始指针:用 get() 方法(谨慎使用,避免手动 delete );
  • 原始指针→智能指针:通过 make_shared / make_unique 或直接 new (不建议直接用原始指针构造多个智能指针,会导致重复释放);
  • 禁止行为: delete p.get() (会导致智能指针再次释放,程序崩溃)。

5.自定义删除器的使用场景?

当默认 delete 无法释放资源时(如数组、文件句柄、网络连接、动态库资源),需自定义删除器:

// 示例:释放动态库(Windows)
#include <windows.h>
// 自定义删除器:释放动态库句柄
auto dll_deleter = [](HMODULE h) {
    FreeLibrary(h);
};
// 用shared_ptr管理动态库句柄
shared_ptr<HMODULE__> dll(LoadLibraryA("test.dll"), dll_deleter);

6.make_shared 与直接 new 的区别?

  • 效率: make_shared 一次性分配“对象+控制块”内存, new 需分别分配对象和控制块(两次内存分配);
  • 异常安全: make_shared 避免内存泄漏(如 shared_ptr(new int(10), deleter) 中, new 成功后 shared_ptr 构造前抛出异常,内存无法释放);
  • 缺陷: make_shared 无法使用自定义删除器(需用 new 构造)。

7.为什么 unique_ptr 比 shared_ptr 高效?

  • unique_ptr 无引用计数和控制块,仅封装原始指针,零额外开销;
  • shared_ptr 需维护引用计数和控制块,拷贝/销毁时需原子操作,有性能损耗。

8.智能指针不能管理哪些资源?

  • 栈上对象(如 unique_ptr p(&x) ,x是栈变量,析构时会重复释放);
  • 全局对象/静态对象(无需手动释放,智能指针析构时会重复释放);
  • 其他已被管理的资源(如同一原始指针构造多个智能指针)。
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐