🌹 作者:云小逸
🤟 个人主页: 云小逸的主页
🤟 motto: 要敢于一个人默默的面对自己,强大自己才是核心。不要等到什么都没有了,才下定决心去做。种一颗树,最好的时间是十年前,其次就是现在!学会自己和解,与过去和解,努力爱自己。希望春天来之前,我们一起面朝大海,春暖花开!

🥇 专栏:

文章目录

📚 前言

在 C++ 编程中,动态内存管理是开发者绕不开的难题。手动使用new分配内存与delete释放内存,极易出现内存泄漏、重复释放、野指针等问题。为解决这些痛点,C++ 标准库引入了智能指针 —— 一种遵循 “资源获取即初始化(RAII)” 思想的类模板,能自动管理动态内存生命周期,大幅提升代码安全性与可靠性。本文将系统讲解智能指针的核心原理、三类标准智能指针(std::shared_ptr/std::unique_ptr/std::weak_ptr)的用法,以及实际开发中的关键避坑点。

一、智能指针的基础:为什么需要它?

1. 裸指针的痛点

普通指针(裸指针)的手动管理存在三大风险:

  • 内存泄漏:忘记调用delete,导致动态内存无法回收,长期占用系统资源;
  • 重复释放:对同一内存地址多次调用delete,触发未定义行为(程序崩溃);
  • 野指针访问:内存释放后仍使用指针,访问无效内存区域,导致程序异常。

2. 智能指针的核心价值

智能指针通过 “将资源生命周期与对象生命周期绑定”,实现自动内存管理:

  • 当智能指针对象离开作用域(如函数执行结束、对象被销毁)时,其析构函数会自动调用delete,释放所管理的动态内存;
  • 开发者无需关注内存释放时机,只需专注业务逻辑,从根源上减少内存管理错误。

3. 简易智能指针:自定义Auto_ptr

为理解智能指针的底层逻辑,实现简化版Auto_ptr(含核心要素):
template // 模板类:支持管理任意类型指针
class Auto_ptr
{
public:
// 构造函数:接收裸指针,用explicit防止隐式类型转换
explicit Auto_ptr(Ty* ptr) : _ptr(ptr) {}

// 析构函数:自动释放内存(智能指针的核心)
~Auto_ptr()
{
    if (_ptr != nullptr)  // 避免空指针重复释放
    {
        delete _ptr;      // 释放管理的动态内存
        _ptr = nullptr;   // 避免野指针
    }
}

private:
Ty* _ptr; // 存储管理的裸指针
};
使用与原理:void testAutoPtr() {
// 创建Auto_ptr,管理new分配的动态内存(int类型,值为20)
Auto_ptr age(new int(20));
// 函数结束时,age离开作用域,析构函数自动调用delete释放内存
}

  • 模板设计:template<typename Ty>支持任意类型指针,通用性强;
  • explicit关键字:防止意外隐式类型转换;
  • 注意:Auto_ptr为简化版,不支持拷贝/赋值,实际需用标准库智能指针。

二、标准库智能指针详解

1. std::unique_ptr:独占所有权的轻量之选

(1)核心特性:如何实现独占性?

通过删除拷贝构造与赋值运算符禁止拷贝,仅支持移动语义转移所有权:template
class unique_ptr {
public:
// 显式删除拷贝构造函数:不允许用一个unique_ptr初始化另一个
unique_ptr(const unique_ptr&) = delete;
// 显式删除拷贝赋值运算符:不允许将一个unique_ptr赋值给另一个
unique_ptr& operator=(const unique_ptr&) = delete;

// 移动构造与移动赋值(支持所有权转移,C++11默认生成)
unique_ptr(unique_ptr&& other) noexcept;
unique_ptr& operator=(unique_ptr&& other) noexcept;
// ... 其他成员

};

(2)基本用法#include // 包含unique_ptr与make_unique

void testUniquePtr() {
// 1. 创建unique_ptr:推荐用make_unique(C++14及以后)
auto up1 = std::make_unique(10); // 管理int类型,值为10
std::unique_ptrstd::string up2(new std::string(“hello”)); // 直接用new(不推荐)

// 2. 访问对象:支持解引用(*)、成员访问(->)
std::cout << *up1 << std::endl;       // 输出:10
std::cout << up2->size() << std::endl;// 输出:5(string长度)

// 3. 转移所有权:用std::move(),原指针变为空
auto up3 = std::move(up1);  // up3接管内存,up1变为nullptr
if (up1 == nullptr) {
    std::cout << "up1 is null" << std::endl;  // 输出:up1 is null
}

// 4. 主动释放/重置:reset()
up2.reset();  // 释放"hello"内存,up2变为nullptr
up3.reset(new int(20));  // 释放原内存,重新管理新内存(值为20)

// 5. 释放所有权(不推荐):release()返回裸指针,需手动释放
int* rawPtr = up3.release();
delete rawPtr;  // 必须手动delete,否则内存泄漏

}

(3)适用场景
  • 资源仅需一个所有者(如函数内局部动态内存、单个对象管理的资源);
  • 作为函数返回值(自动触发移动语义,无需手动std::move);
  • 存储在容器中(需通过std::move转移所有权,如vector.push_back(std::move(up)))。

2. std::shared_ptr:共享所有权的协作之选

(1)核心原理:引用计数

shared_ptr内部维护两个关键指针:

  • 数据指针:指向管理的动态内存;
  • 控制块指针:指向堆上 “控制块”,包含:
    • 引用计数(use_count):记录共享该内存的shared_ptr数量;
    • 弱引用计数(weak_count):记录weak_ptr数量;
    • 自定义删除器、分配器等。

引用计数工作流程

  1. 创建shared_ptr时,引用计数初始化为 1;
  2. 拷贝shared_ptr(如sp2 = sp1)时,引用计数 + 1;
  3. shared_ptr销毁(离开作用域)或reset()时,引用计数 - 1;
  4. 引用计数变为 0 时,释放管理的动态内存与控制块。
(2)基本用法#include

void testSharedPtr() {
// 1. 创建shared_ptr:推荐用make_shared(高效安全)
auto sp1 = std::make_shared(20); // 管理int,值为20,引用计数=1
std::shared_ptr sp2(new int(30)); // 直接用new(不推荐,效率低)

// 2. 共享所有权:拷贝操作,引用计数+1
auto sp3 = sp1;  // sp1与sp3共享内存,引用计数=2
std::cout << sp1.use_count() << std::endl;  // 输出:2(查看引用计数)

// 3. 访问对象:支持*、->
std::cout << *sp3 << std::endl;  // 输出:20

// 4. 重置指针:sp1.reset()后,引用计数-1(变为1)
sp1.reset();
if (sp1 == nullptr) {
    std::cout << "sp1 is null" << std::endl;  // 输出:sp1 is null
}
std::cout << sp3.use_count() << std::endl;  // 输出:1(仅sp3共享)

// 5. 自定义删除器(如管理数组)
// shared_ptr默认用delete释放,管理数组需指定delete[]
auto sp4 = std::shared_ptr<int>(new int[5], [](int* p) {
    delete[] p;  // 数组释放必须用delete[]
});

}

(3)make_shared的优势
  • 效率更高:一次性分配 “数据内存” 与 “控制块内存”,减少 1 次内存分配;
  • 更安全:避免异常导致内存泄漏,例如:
    // 危险:若someFunction()抛出异常,new int(10)的内存未被shared_ptr接管
    func(std::shared_ptr<int>(new int(10)), someFunction());
    
    // 安全:make_shared先分配内存并绑定到shared_ptr,无泄漏风险
    func(std::make_shared<int>(10), someFunction());
    
(4)适用场景
  • 资源需多对象共享(如多个模块访问同一数据库连接、图像资源);
  • 无法确定哪个对象最后释放资源(如多线程场景下的共享数据);
  • 资源创建成本高(如网络连接、大文件句柄),共享可减少重复创建开销。

3. std::weak_ptr:解决循环引用的辅助之选

(1)循环引用:shared_ptr的致命隐患

当两个shared_ptr互相引用时,引用计数无法归零,导致内存泄漏:class B; // 前置声明
class A {
public:
std::shared_ptr b_ptr; // A持有B的shared_ptr
~A() { std::cout << “A destroyed” << std::endl; }
};
class B {
public:
std::shared_ptr a_ptr; // B持有A的shared_ptr
~B() { std::cout << “B destroyed” << std::endl; }
};

void testCycle() {
auto a = std::make_shared();
auto b = std::make_shared();
a->b_ptr = b; // A引用B,B的引用计数=2
b->a_ptr = a; // B引用A,A的引用计数=2

// 函数结束时,a、b销毁,引用计数均变为1(而非0)
// A、B的内存无法释放,导致内存泄漏(析构函数不会执行)

}

(2)weak_ptr的解决方案

将其中一方的shared_ptr改为weak_ptr(不增加引用计数),打破循环:class A {
public:
std::weak_ptr b_ptr; // 改为weak_ptr,不增加引用计数
~A() { std::cout << “A destroyed” << std::endl; }
};
class B {
public:
std::shared_ptr a_ptr;
~B() { std::cout << “B destroyed” << std::endl; }
};

void testWeakPtr() {
auto a = std::make_shared();
auto b = std::make_shared();
a->b_ptr = b; // weak_ptr不增加B的引用计数(仍为1)
b->a_ptr = a; // A的引用计数=2

// 函数结束时:
// 1. b销毁,A的引用计数=1;
// 2. a销毁,A的引用计数=0,A释放,B的引用计数=0,B释放;
// 3. 析构函数正常执行,无内存泄漏

}

(3)weak_ptr的基本用法

weak_ptr无法直接访问对象,需通过lock()转换为shared_ptr:void testWeakPtrBasic() {
auto sp = std::make_shared(22);
std::weak_ptr wp = sp; // weak_ptr绑定到shared_ptr,不增加引用计数

// 1. 检查对象是否存在:expired()
if (!wp.expired()) {
    // 2. 转换为shared_ptr:lock()(成功则引用计数+1)
    auto sp2 = wp.lock();
    if (sp2) {  // 必须判断sp2是否有效(避免线程安全问题)
        std::cout << *sp2 << std::endl;  // 输出:22
    }
}

// 3. 查看引用计数:use_count()
std::cout << wp.use_count() << std::endl;  // 输出:1(仅sp共享)

// 4. 释放观测:reset()
wp.reset();  // wp不再观测任何内存

}

4. 三类智能指针的共有操作

操作 描述
*p 解引用指针,获取管理的对象(如*sp获取int值)
p->mem 成员访问,等价于(*p).mem(如sp->size()调用string方法)
p.get() 返回内部管理的裸指针(谨慎使用,避免与智能指针混用)
p.reset() 释放管理的内存,将指针置为nullptr
p.reset(q) 释放原内存,重新管理裸指针q指向的内存
p.swap(q) 交换pq管理的内存
p == nullptr 判断指针是否为空(如if (sp == nullptr)

三、实际开发避坑指南

1. shared_ptr的线程安全:计数安全≠数据安全

  • 误区:认为 “shared_ptr是线程安全的,多线程操作无需加锁”;
  • 真相shared_ptr的引用计数操作是线程安全的(原子操作),但管理的数据不是;
  • 风险场景
    auto sp = std::make_shared<int>(0);
    // 线程1:修改数据
    void thread1() {
        for (int i = 0; i < 10000; i++) {
            (*sp)++;  // 数据竞争!多个线程同时修改同一int
        }
    }
    // 线程2:修改数据(与线程1竞争)
    void thread2() {
        for (int i = 0; i < 10000; i++) {
            (*sp)++;  // 未定义行为,结果可能错误或程序崩溃
        }
    }
    
  • 解决方案:对数据访问加锁(如std::mutex),而非依赖shared_ptr的计数安全:
    std::mutex mtx;
    void threadSafeFunc() {
        std::lock_guard<std::mutex> lock(mtx);  // 加锁,确保线程安全
        (*sp)++;  // 安全修改共享数据
    }
    

2. unique_ptr自定义删除器:注意大小与类型

unique_ptr支持自定义删除器(如释放数组、文件句柄),但需关注两点:

(1)删除器状态影响unique_ptr大小
  • 无状态删除器:如普通函数、无捕获lambda(不捕获外部变量),unique_ptr大小与普通指针一致(64 位系统 8 字节):

    // 无捕获lambda作为删除器(无状态)
    auto delNoState = [](FILE* fp) { fclose(fp); };
    std::unique_ptr<FILE, decltype(delNoState)> upFile(
        fopen("test.txt", "r"), delNoState
    );
    // 64位系统中,upFile大小为8字节(仅存FILE*指针)
    
  • 有状态删除器:如捕获外部变量的lambda、带成员的结构体删除器,unique_ptr需额外存储删除器状态,大小增加:

    // 捕获外部变量的lambda(有状态)
    int logLevel = 1;
    auto delWithState = [logLevel](int* p) {
        if (logLevel > 0) {
            std::cout << "Deleting int: " << *p << std::endl;
        }
        delete p;
    };
    std::unique_ptr<int, decltype(delWithState)> upWithState(
        new int(100), delWithState
    );
    // 64位系统中,upWithState大小为16字节(8字节int* + 4字节logLevel + 4字节对齐)
    
(2)不同删除器的unique_ptr是不同类型

即使删除器功能相同,类型不同的unique_ptr无法直接赋值:// 两个功能相同但类型不同的删除器
auto del1 = [](int* p) { delete p; };
auto del2 = [](int* p) { delete p; };

std::unique_ptr<int, decltype(del1)> up1(new int(10), del1);
std::unique_ptr<int, decltype(del2)> up2(new int(20), del2);

// 错误:up1与up2类型不同,无法直接赋值
// up1 = up2;

// 正确:通过std::move转移,且删除器类型需匹配
up1 = std::unique_ptr<int, decltype(del1)>(new int(30), del1);

3. 严禁智能指针管理栈内存:必触发双重释放

智能指针仅能管理动态内存new/new[]分配),若误将栈内存地址传入,会导致 “双重释放”—— 栈内存会在函数结束时自动释放,智能指针析构时又会调用delete,触发程序崩溃。

风险示例(致命错误):void testStackMemory() {
int stackVar = 5; // 栈内存:函数结束后自动销毁
// 错误:用智能指针管理栈内存
std::shared_ptr spBad(&stackVar);
// 函数结束时:
// 1. stackVar随栈帧销毁;
// 2. spBad析构,调用delete &stackVar,释放已销毁的栈内存 → 程序崩溃
}
核心原则
智能指针的构造参数,必须是new/new[]分配的动态内存地址,或通过make_shared/make_unique自动分配的内存(本质仍是动态内存)。

4. weak_ptr访问对象:以lock()返回值为准,而非expired()

weak_ptr需通过lock()转换为shared_ptr才能访问对象,但易陷入 “先判断expired()lock()” 的误区 —— 即使expired()返回false(对象存在),在expired()lock()之间,对象仍可能被其他线程释放,导致lock()返回空指针。

错误用法(线程不安全):void accessWeakPtrWrong(std::weak_ptr wp) {
if (!wp.expired()) { // 步骤1:判断对象存在
// 风险窗口:其他线程可能在此处释放对象
std::shared_ptr sp = wp.lock(); // 步骤2:转换为shared_ptr
std::cout << *sp << std::endl; // 若sp为空,解引用崩溃
}
}
正确用法(线程安全)
直接调用lock(),通过返回的shared_ptr是否非空判断对象是否有效 ——lock()内部会原子性地检查对象状态,避免线程安全问题:void accessWeakPtrRight(std::weak_ptr wp) {
// 直接lock(),原子性获取对象状态
std::shared_ptr sp = wp.lock();
if (sp) { // 以sp是否非空为准,安全判断
std::cout << *sp << std::endl; // 安全访问
} else {
std::cout << “Object has been destroyed” << std::endl;
}
}

5. 智能指针作为函数参数:避免不必要的拷贝与所有权转移

根据 “是否转移所有权” 选择传递方式,避免性能损耗或意外丢失所有权:

(1)shared_ptr:优先传const引用,避免拷贝

按值传递shared_ptr会触发拷贝构造,导致引用计数 + 1;函数结束后引用计数 - 1,增加不必要的原子操作开销。若仅需访问对象(不转移所有权),传const std::shared_ptr<T>&更高效:// 低效:按值传递,触发引用计数增减
void funcValue(std::shared_ptr sp) {
std::cout << *sp << std::endl;
}

// 高效:传const引用,无拷贝开销
void funcRef(const std::shared_ptr& sp) {
std::cout << *sp << std::endl;
}

// 调用示例
auto sp = std::make_shared(100);
funcValue(sp); // 引用计数先+1(进入函数)后-1(离开函数)
funcRef(sp); // 无引用计数操作,性能更优

(2)unique_ptr:按值传递需std::move,仅传引用不转移所有权

unique_ptr不支持拷贝,若需将所有权转移给函数,需通过std::move按值传递;若仅需临时访问对象(不转移所有权),传非const引用(std::unique_ptr<T>&):// 转移所有权:按值传递,需std::move
void takeOwnership(std::unique_ptr up) {
std::cout << *up << std::endl;
// up离开函数时自动释放内存
}

// 临时访问:传非const引用,不转移所有权
void accessWithoutOwnership(std::unique_ptr& up) {
*up = 200; // 可修改对象内容,不影响所有权
}

// 调用示例
auto up = std::make_unique(100);
accessWithoutOwnership(up); // 不转移所有权,up仍有效
takeOwnership(std::move(up)); // 转移所有权,up变为nullptr
// 此时up已为空,不可再访问

6. shared_ptr管理数组:必须自定义删除器

std::shared_ptr默认使用delete释放资源,而new[]分配的数组需用delete[]释放(匹配内存分配方式)。若直接用shared_ptr管理数组,会因 “释放方式不匹配” 触发未定义行为(内存泄漏或崩溃)。

错误用法:// 错误:shared_ptr默认用delete释放,不匹配new[]
std::shared_ptr spArr(new int[5]);
// 析构时调用delete spArr.get(),而非delete[] → 内存泄漏(数组元素未正确释放)
正确用法
通过自定义删除器指定delete[],或使用std::default_delete<T[]>(标准库提供的数组删除器):// 方式1:lambda作为删除器,显式调用delete[]
std::shared_ptr spArr1(
new int[5], [](int* p) { delete[] p; }
);

// 方式2:使用std::default_delete<T[]>(更简洁)
std::shared_ptr spArr2(
new int[5], std::default_delete<int[]>()
);

// 方式3:C++20及以后,用std::make_shared_for_overwrite(需配合删除器)
#ifdef __cpp_lib_make_shared_for_overwrite
auto spArr3 = std::shared_ptr(
std::make_shared_for_overwrite<int[]>(5),
std::default_delete<int[]>()
);
#endif

注意std::unique_ptr有数组特化版本(std::unique_ptr<T[]>),默认用delete[]释放,无需自定义删除器,因此管理数组优先选择unique_ptr

四、总结:智能指针的选择与使用原则

1. 智能指针选择优先级

  • 优先用std::unique_ptr
    资源仅需单个所有者、无共享需求时,unique_ptr轻量(无引用计数开销)、高效,且能强制独占性,避免意外共享。

  • 次选用std::shared_ptr
    资源需多对象共享、无法确定释放顺序时,使用shared_ptr,但需注意避免循环引用(配合weak_ptr解决)。

  • 辅助用std::weak_ptr
    不单独使用,仅作为shared_ptr的补充 —— 解决循环引用、观测对象状态(不影响生命周期)。

2. 核心使用原则

  • 拒绝裸指针混用:不将智能指针管理的裸指针(get()返回值)暴露给外部代码,避免双重释放或野指针访问;
  • 优先用工厂函数创建make_shared/make_unique比直接new更高效、安全,减少内存分配次数与泄漏风险;
  • 明确所有权转移unique_ptr转移所有权需显式用std::move,避免意外丢失所有权;shared_ptr拷贝时清楚引用计数变化;
  • 多线程操作加锁shared_ptr的引用计数安全不代表数据安全,多线程修改共享数据需额外加锁(如std::mutex)。

通过掌握智能指针的原理、用法与避坑点,开发者可彻底摆脱手动内存管理的困扰,写出更安全、高效、易维护的 C++ 代码。智能指针不仅是内存管理工具,更是 C++“资源安全” 思想的核心体现 —— 让资源的生命周期与对象绑定,从根源上减少人为错误。

📣 结语

感谢你耐心看完,恭喜你比昨天的你进步了一点点哦

如果你觉得我写的不错,记得给我点赞,收藏 和 关注哦(。・ω・。)

让我们一起加油,向美好的未来奔去。让我们从一无所知的新手逐渐成为专家。为自己点赞吧!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐