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 对,确保成对调用。
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区10 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜11 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa11 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio