C++ 智能指针_详细解释

前置说明

智能指针基于RAII(资源获取即初始化)机制 ,自动管理堆内存,核心解决裸指针的内存泄漏问题:把堆内存的生命周期绑定到栈上的智能指针对象,当智能指针对象超出作用域时,析构函数会自动释放绑定的堆内存,无需手动delete

C++ 中的裸指针(int* p = new int;)需要手动管理内存:

  • 忘记delete会导致内存泄漏
  • 多次delete会导致未定义行为
  • 异常场景下(比如delete前抛出异常),内存也无法释放。
    C++ 标准库在<memory>头文件中提供了三种核心智能指针:unique_ptrshared_ptrweak_ptr,其中auto_ptr已被废弃(设计缺陷)。
  1. unique_ptr是独占式智能指针(轻量、高效),shared_ptr是共享式(引用计数),weak_ptr辅助shared_ptr解决循环引用;
  2. 使用智能指针的核心原则:优先选unique_ptr,避免裸指针与智能指针混用,警惕shared_ptr的循环引用。

智能指针的使用原则:

  1. 优先使用unique_ptr(高效、无额外开销),仅在需要共享所有权时使用shared_ptr
  2. 避免用同一个裸指针创建多个智能指针(会导致重复释放);
  3. 不要手动delete智能指针管理的裸指针(智能指针析构时会再次delete);
  4. shared_ptr的循环引用必须用weak_ptr解决;
  5. 优先使用make_unique/make_shared创建智能指针(异常安全、更高效)。

1. std::unique_ptr(独占式智能指针)

特性与使用场景

  • 独占所有权 :同一时间只有一个 unique_ptr 指向资源,拷贝构造 / 赋值被禁用,仅支持移动(std::move);
  • 轻量级:无额外内存开销(仅封装裸指针 + 删除器),效率接近裸指针;
  • 适用场景 :函数返回值、容器元素(如 std::vector<unique_ptr<Foo>>)、独占资源的管理(如文件句柄、网络连接)。

核心签名(类模板声明)

cpp 复制代码
// 基础版本(针对单个对象)
template <class T, class Deleter = std::default_delete<T>>
class unique_ptr;

// 数组特化版本(针对动态数组 new T[])
template <class T, class Deleter>
class unique_ptr<T[], Deleter>;
部分 含义
template <class T, class Deleter = std::default_delete<T>> 模板参数:- T:智能指针指向的对象类型 (如 intstd::string、自定义类);- Deleter删除器类型 (负责释放资源的函数 / 仿函数),默认是 std::default_delete<T>(调用 delete 释放单个对象);
unique_ptr<T[], Deleter> 数组特化版本:专门处理 new T[] 分配的动态数组,默认删除器会调用 delete[],而非普通版本的 delete

关键成员函数签名(常用)

cpp 复制代码
// 1. 移动构造(独占所有权,仅支持移动,禁止拷贝)
template <class U, class E>
unique_ptr(unique_ptr<U, E>&& u) noexcept;

// 2. 重置(释放当前资源,接管新资源)
void reset(pointer p = pointer()) noexcept;

// 3. 释放所有权(返回裸指针,智能指针不再管理)
pointer release() noexcept;

// 4. C++14 辅助创建函数(更安全,避免裸指针)
template <class T, class... Args>
unique_ptr<T> make_unique(Args&&... args);

// 数组版本的make_unique
template <class T>
unique_ptr<T> make_unique(size_t n);

示例代码

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int val) : value(val) {
        std::cout << "MyClass 构造:" << value << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 析构:" << value << std::endl;
    }
    void show() {
        std::cout << "值:" << value << std::endl;
    }
private:
    int value;
};

int main() {
    // 1. 创建shared_ptr(推荐用make_shared,更高效)
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10);
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1

    // 2. 拷贝,引用计数+1
    std::shared_ptr<MyClass> ptr2 = ptr1;
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2

    // 3. 多个指针共享资源
    ptr1->show(); // 输出:值:10
    ptr2->show(); // 输出:值:10

    // 4. 重置指针,引用计数-1
    ptr1.reset();
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1

    // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new)
    std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
    arr_ptr[1] = 200;
    std::cout << arr_ptr[1] << std::endl; // 输出:200

    return 0; // ptr2析构,引用计数变为0,资源释放
}

2. std::shared_ptr(共享式智能指针)

特性与使用场景

  • 共享所有权 :多个 shared_ptr 指向同一资源,每新增一个shared_ptr指向该资源,引用计数 + 1;每销毁一个shared_ptr,引用计数 - 1;引用计数为 0 时自动释放资源;
  • 线程安全:引用计数的增减是原子操作(但访问 / 修改指向的对象需手动加锁);
  • 适用场景:需要多个对象共享同一资源的场景(如多线程访问同一数据、对象树的交叉引用)。

核心签名(类模板声明)

cpp 复制代码
template <class T>
class shared_ptr;

关键辅助函数 / 转换函数签名

cpp 复制代码
// 1. 高效创建(一次内存分配:对象+控制块,比直接new更优)
template <class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);

// 2. 类型转换(对应普通指针的cast,保证引用计数正确)
// static_cast 等价版本
template <class T, class U>
shared_ptr<T> static_pointer_cast(const shared_ptr<U>& r) noexcept;

// dynamic_cast 等价版本(运行时类型检查)
template <class T, class U>
shared_ptr<T> dynamic_pointer_cast(const shared_ptr<U>& r) noexcept;

// const_cast 等价版本
template <class T, class U>
shared_ptr<T> const_pointer_cast(const shared_ptr<U>& r) noexcept;

核心成员函数签名

cpp 复制代码
// 1. 拷贝构造(增加引用计数)
template <class U>
shared_ptr(const shared_ptr<U>& r) noexcept;

// 2. 赋值重载(引用计数增减)
template <class U>
shared_ptr& operator=(const shared_ptr<U>& r) noexcept;

// 3. 获取引用计数
long use_count() const noexcept;

// 4. 检查是否是唯一持有者
bool unique() const noexcept;

// 5. 重置(释放当前引用,引用计数-1)
void reset() noexcept;
部分 含义
template <class T> 仅需指定指向的对象类型 T,控制块(存储引用计数、删除器等)是内部隐式创建的;
make_shared<Args&&... args> 完美转发参数给 T 的构造函数,在堆上创建 T 对象并绑定到 shared_ptr
static/dynamic/const_pointer_cast 避免直接对 shared_ptr 的裸指针做 cast(会导致多个控制块),保证类型转换后引用计数统一;

示例代码

cpp 复制代码
#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int val) : value(val) {
        std::cout << "MyClass 构造:" << value << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 析构:" << value << std::endl;
    }
    void show() {
        std::cout << "值:" << value << std::endl;
    }
private:
    int value;
};

int main() {
    // 1. 创建shared_ptr(推荐用make_shared,更高效)
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(10);
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:1

    // 2. 拷贝,引用计数+1
    std::shared_ptr<MyClass> ptr2 = ptr1;
    std::cout << "引用计数:" << ptr1.use_count() << std::endl; // 输出:2
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:2

    // 3. 多个指针共享资源
    ptr1->show(); // 输出:值:10
    ptr2->show(); // 输出:值:10

    // 4. 重置指针,引用计数-1
    ptr1.reset();
    std::cout << "引用计数:" << ptr2.use_count() << std::endl; // 输出:1

    // 5. 管理数组(C++17+支持make_shared数组,C++11/14需手动new)
    std::shared_ptr<int[]> arr_ptr(new int[5]{1,2,3,4,5});
    arr_ptr[1] = 200;
    std::cout << arr_ptr[1] << std::endl; // 输出:200

    return 0; // ptr2析构,引用计数变为0,资源释放
}

关键问题:循环引用

shared_ptr的最大陷阱是循环引用 :两个shared_ptr互相指向对方,导致引用计数永远无法变为 0,最终内存泄漏。

循环引用示例(错误)

cpp 复制代码
#include <iostream>
#include <memory>

class B; // 前向声明

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

class B {
public:
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // A引用B
    b->a_ptr = a; // B引用A
    // 循环引用:a和b的引用计数都是2,析构时各减1,变为1,永远不会释放
    return 0; // 不会输出"A 析构"和"B 析构",内存泄漏
}

3. std::weak_ptr(弱引用智能指针)

特性与使用场景

  • 弱引用 :不拥有资源,不增加 shared_ptr 的引用计数,不影响资源释放;
  • 解决循环引用shared_ptr 循环引用会导致引用计数无法归零,weak_ptr 可打破循环;
  • 不能直接解引用 :必须通过 lock() 转为 shared_ptr 后才能访问资源。

签名(类模板声明)

cpp 复制代码
template <class T>
class weak_ptr;

成员函数签名

cpp 复制代码
// 1. 从shared_ptr构造(不增加引用计数)
template <class U>
weak_ptr(const shared_ptr<U>& r) noexcept;

// 2. 锁定为shared_ptr(安全访问资源)
shared_ptr<T> lock() const noexcept;

// 3. 检查资源是否已释放(过期)
bool expired() const noexcept;

// 4. 获取对应的shared_ptr引用计数(仅参考,可能瞬时变化)
long use_count() const noexcept;

// 5. 重置(清空弱引用)
void reset() noexcept;
部分 含义
template <class T> 指向的对象类型与 shared_ptr 一致;
lock() 核心函数:返回一个 shared_ptr(若资源未释放则引用计数 + 1,否则返回空 shared_ptr),是访问弱引用资源的唯一安全方式;
expired() 等价于 use_count() == 0,但更高效(无需获取精确的引用计数值);

解决循环引用的示例(正确):

cpp 复制代码
#include <iostream>
#include <memory>

class B;

class A {
public:
    std::weak_ptr<B> b_ptr; // 改为weak_ptr
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 改为weak_ptr
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b_ptr = b; // weak_ptr不增加引用计数
    b->a_ptr = a; // weak_ptr不增加引用计数

    // 检查资源是否存在
    if (auto temp = a->b_ptr.lock()) { // lock()返回shared_ptr,若资源存在则有效
        std::cout << "B资源存在" << std::endl;
    }

    return 0; // a和b析构,引用计数变为0,资源释放(输出"A 析构"和"B 析构")
}

关键说明

  • weak_ptr不能直接访问资源(没有->*运算符),必须通过lock()获取shared_ptr后才能访问;
  • expired()方法可以判断weak_ptr指向的资源是否已释放(返回true表示已释放);
  • weak_ptr的大小和shared_ptr相同(因为要存储引用计数的指针)。

内存泄漏案例以及改良方案

内存泄漏的核心本质是:堆内存被分配后,失去了对它的所有引用,导致程序无法再释放这块内存,直到程序退出(系统会回收,但长期运行的程序如服务器会持续占用内存)

常见的内存泄漏场景

1. 最基础:忘记释放手动分配的内存

这是新手最易犯的错误 ------ 用new/malloc分配堆内存后,未调用delete/free释放,尤其是在分支、循环等复杂逻辑中更容易遗漏。

示例代码(错误)

cpp 复制代码
#include <iostream>
using namespace std;

void func() {
    // 分配堆内存
    int* p = new int(10);
    string name = "test";
    // 分支逻辑导致忘记释放
    if (name == "test") {
        cout << "分支返回,遗漏delete" << endl;
        return; // 直接返回,p指向的内存永远无法释放
    }
    // 只有走else才会释放(本例不会执行)
    delete p;
}

int main() {
    func(); // 执行后内存泄漏(4字节int)
    return 0;
}

避免方法

  • 优先使用智能指针(unique_ptr/shared_ptr)替代裸指针;
  • 若必须用裸指针,遵循 "分配即规划释放" 原则,在分配内存时就确定释放的位置。

修复后的完整代码

cpp 复制代码
#include <iostream>
#include <memory>  // 必须包含智能指针的头文件
using namespace std;

void func() {
    // 用unique_ptr替代裸指针,make_unique是创建unique_ptr的推荐方式
    unique_ptr<int> p = make_unique<int>(10);
    string name = "test";
    
    if (name == "test") {
        cout << "分支返回,智能指针自动释放内存" << endl;
        return; // 即使提前返回,p也会析构并释放内存
    }
    
    // 无需手动delete!智能指针超出作用域时会自动释放
    // 原来的delete p 可以完全删除
}

int main() {
    func(); // 执行后无内存泄漏
    return 0;
}

2. 异常导致的内存泄漏

new分配内存后,delete执行前抛出异常,导致delete语句无法执行,进而泄漏内存。这是比 "忘记释放" 更隐蔽的问题。

示例代码(错误)

cpp 复制代码
#include <iostream>
#include <stdexcept>
using namespace std;

void riskyFunc() {
    throw runtime_error("突发异常"); // 抛出异常
}

void func() {
    int* p = new int(20); // 分配内存
    riskyFunc(); // 抛出异常,后续代码全部跳过
    delete p; // 永远执行不到,内存泄漏
}

int main() {
    try {
        func();
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    return 0;
}

避免方法

  • 核心方案:使用智能指针(RAII 机制),即使抛出异常,智能指针对象析构时仍会自动释放内存;
  • 兜底方案:用try-catch包裹,但代码冗余且易遗漏,不如智能指针可靠。

修复后的代码

cpp 复制代码
#include <iostream>
#include <stdexcept>
#include <memory> // 智能指针头文件
using namespace std;

void riskyFunc() {
    throw runtime_error("突发异常");
}

void func() {
    unique_ptr<int> p = make_unique<int>(20); // 智能指针
    riskyFunc(); // 抛异常也不影响,p析构时自动释放
}

int main() {
    try {
        func();
    } catch (const exception& e) {
        cout << "捕获异常:" << e.what() << endl;
    }
    return 0; // 无内存泄漏
}

3. shared_ptr 的循环引用(进阶陷阱)

这是使用智能指针时的高频错误 ------ 两个或多个shared_ptr互相持有对方的引用,导致引用计数永远无法归 0,内存无法释放。

示例代码(错误)

cpp 复制代码
#include <iostream>
#include <memory>
using namespace std;

class B; // 前向声明

class A {
public:
    shared_ptr<B> b_ptr; // A持有B的shared_ptr
    ~A() { cout << "A 析构" << endl; } // 不会执行
};

class B {
public:
    shared_ptr<A> a_ptr; // B持有A的shared_ptr
    ~B() { cout << "B 析构" << endl; } // 不会执行
};

int main() {
    shared_ptr<A> a = make_shared<A>();
    shared_ptr<B> b = make_shared<B>();
    a->b_ptr = b; // 循环引用开始
    b->a_ptr = a;
    // a和b的引用计数都是2,析构时各减1变为1,永远不会释放
    return 0; // 无析构输出,内存泄漏
}

避免方法

  • 将循环引用中的一方或双方的shared_ptr替换为weak_ptr(弱引用,不增加引用计数);
  • 修复后的代码可参考上一轮讲解智能指针时的weak_ptr示例。

4. 容器存储裸指针未清理

vector/list/map等容器存储裸指针时,清空容器(如clear())仅会删除指针本身(容器内的元素),但不会释放指针指向的堆内存。

示例代码(错误)

cpp 复制代码
#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int*> vec;
    // 向容器添加堆内存指针
    vec.push_back(new int(1));
    vec.push_back(new int(2));
    vec.push_back(new int(3));

    vec.clear(); // 仅清空容器,3个int的堆内存未释放,泄漏
    return 0;
}

避免方法

  • 容器中存储智能指针(如vector<unique_ptr<int>>),清空时自动释放内存;
  • 若必须存裸指针,清空容器前遍历 delete 每个元素。

修复后的代码

cpp 复制代码
#include <iostream>
#include <vector>
#include <memory>
using namespace std;

int main() {
    vector<unique_ptr<int>> vec;
    vec.push_back(make_unique<int>(1));
    vec.push_back(make_unique<int>(2));
    vec.push_back(make_unique<int>(3));

    vec.clear(); // 自动释放所有堆内存,无泄漏
    return 0;
}

5. 动态数组释放错误(delete vs delete [])

new[]分配的数组,若误用delete(而非delete[])释放:

  • 对于类对象数组:仅调用第一个元素的析构函数,其余元素的析构函数不执行,导致内存泄漏;
  • 对于内置类型数组(int/char 等):看似无泄漏,但属于 "未定义行为",可能引发其他问题。

示例代码(错误)

cpp 复制代码
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() { cout << "MyClass 构造" << endl; }
    ~MyClass() { cout << "MyClass 析构" << endl; }
};

int main() {
    // 分配对象数组
    MyClass* arr = new MyClass[3]; // 输出3次构造
    delete arr; // 错误!仅调用第一个对象的析构,后2个泄漏
    // 正确写法:delete[] arr;
    return 0;
}

避免方法

  • 严格遵循 "newdeletenew[]delete[]" 的规则;
  • 优先使用vectorunique_ptr<T[]>管理动态数组(无需手动释放)。

6. 全局 / 静态指针的内存泄漏

全局或静态指针指向堆内存时,若程序结束前未释放:

  • 虽然程序退出后操作系统会回收内存,但长期运行的程序(如服务器、后台服务)会持续占用内存,最终导致内存耗尽;
  • 不符合 "资源用完即释放" 的编程规范。

示例代码(错误)

cpp 复制代码
#include <iostream>
using namespace std;

// 全局指针
int* g_ptr = new int(100);

int main() {
    // 程序运行期间未释放g_ptr,直到退出才被系统回收
    cout << *g_ptr << endl;
    // 遗漏:delete g_ptr;
    return 0;
}

避免方法

  • 用全局智能指针(如static unique_ptr<int> g_ptr = make_unique<int>(100));
  • 在程序退出前(如 main 结束前)显式释放全局 / 静态裸指针。

7. 第三方库资源未释放

使用第三方库的 API 分配资源(如自定义句柄、内存、句柄)时,未调用库提供的 "释放函数",导致泄漏(这类泄漏常被忽略)。

示例场景(伪代码)

cpp 复制代码
// 第三方库API示例
void* create_obj(); // 分配资源,返回指针
void destroy_obj(void* p); // 释放资源

int main() {
    void* obj = create_obj(); // 分配资源
    // 业务逻辑...
    destroy_obj(obj); // 忘记调用,资源泄漏
    return 0;
}

避免方法

  • 封装成 RAII 类,析构函数中调用释放函数;
  • 记录所有 "分配 - 释放" API 对,确保成对调用。
相关推荐
编程大师哥2 小时前
JavaScript DOM
开发语言·javascript·ecmascript
dazzle2 小时前
Python数据结构(四):栈详解
开发语言·数据结构·python
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于java的办公自动化系统设计为例,包含答辩的问题和答案
java·开发语言
json{shen:"jing"}2 小时前
10_自定义事件组件交互
开发语言·前端·javascript
一位搞嵌入式的 genius2 小时前
深入理解 JavaScript 异步编程:从 Event Loop 到 Promise
开发语言·前端·javascript
brevity_souls2 小时前
SQL Server 窗口函数简介
开发语言·javascript·数据库
火云洞红孩儿2 小时前
零基础:100个小案例玩转Python软件开发!第六节:英语教学软件
开发语言·python
AI殉道师3 小时前
FastScheduler:让 Python 定时任务变得优雅简单
开发语言·python
花间相见3 小时前
【JAVA开发】—— HTTP常见请求方法
java·开发语言·http