《C++进阶之C++11》【智能指针】(上)
【智能指针】(上)目录
- 前言:
- ------------ 智能指针的使用 ------------
- 1. 什么是智能指针?
- 2. 常用智能指针的类型有哪些?
- 3. 怎么使用智能指针?
- 4. 为什么需要智能指针?
- 5. 手动new/delete VS 智能指针 谁更胜一筹?
- 6. 智能指针为什么这么吊?
- 一、智能指针的核心本质
- 二、智能指针的模拟实现
- 7. 使用智能指针的好处有哪些?
- 8. 关于智能指针有哪些注意事项?
- ------------ 删除器 ------------
- 1. 什么是删除器?
- 2. 为什么要引入删除器?
- 3. 删除器的本质是什么?
- 4. 删除器怎么使用?
- 一、与 unique_ptr 配合使用
- 二、与 shared_ptr 配合使用
- 5. unique_ptr与shared_ptr使用删除器的差异是什么?
- 6. 使用删除器的需要注意什么?
往期《C++初阶》回顾:
《C++初阶》目录导航
往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】
【unordered_set/unordered_map 模拟实现】
/------------ C++11 ------------/
【列表初始化 + 右值引用】
【移动语义 + 完美转发】
【可变参数模板 + emplace接口 + 新的类功能】
【lambda表达式 + 包装器】
【异常】
前言:
hi~ 小伙伴们大家好呀~,明天就是中秋节了啦🌕✧٩(ˊᗜˋ)و✧。
本来这篇博客鼠鼠是特意留到中秋当天单篇完结C++的,结果赶巧碰上 “不咕挑战赛” 要结束了 —— 估计大家早就忘了鼠鼠也报名参加了这个比赛了吧!(´• ω •`;)
毕竟这一个月里,鼠鼠发博客的频率实在不算高,别说日更了,连三天一次的打卡都偶尔放鸽了,完全没有正经参赛的样子,哈哈~~( ̄▽ ̄)ゞ
所以鼠鼠决定了,要在比赛结束前最后几个小时里,偷偷一口气连发两篇博客 “冲刺” 一下,来一个出奇不意~
嘘嘘,这个小秘密可别传出去哦,不然就没惊喜啦!(((┏(; ̄▽ ̄)┛
言归正传,这次要跟大家好好聊聊的是 C++ 进阶内容里的最后一章 ——【智能指针】。
这部分的知识点特别重要,内容也比较多,所以鼠鼠特意拆成了(上)、(下)两篇来写,每篇的字数大概在 1W 字左右,能把每个细节都讲清楚。
智能指针在 C++ 里绝对算是 “重头戏” (☉д⊙),不管是日常开发避坑,还是面试考点,它都是绕不开的内容,所以接下来的内容大家可得认真看呀~╰(▔∀▔)╯
------------ 智能指针的使用 ------------
1. 什么是智能指针?
智能指针
:是 C++ 标准库提供的一种封装了原始指针的类模板
,核心作用是自动管理动态内存,避免手动 new/delete 导致的内存泄漏(如:异常抛出时忘记释放内存)或重复释放等问题。
- 它的本质是利用RAII(资源获取即初始化)机制:将动态内存的生命周期与智能指针对象的生命周期绑定 —— 当智能指针对象离开作用域时,其析构函数会自动调用 delete 释放所管理的内存
2. 常用智能指针的类型有哪些?
三大常用智能指针的类型:
std::unique_ptr
基本特性:独占所有权,同一时间只能有一个 unique_ptr 管理某块内存,不允许拷贝,只能移动
适用场景:管理单个对象或数组,明确内存归属唯一的场景
#include <memory> using namespace std;int main() {//1.管理单个int对象unique_ptr<int> ptr1(new int(10));//2.或用更安全的make_unique(C++14)auto ptr2 = make_unique<int>(20);//3.不允许拷贝(编译报错)// unique_ptr<int> ptr3 = ptr1; //4.允许移动(所有权转移)unique_ptr<int> ptr4 = move(ptr1); } // 离开作用域时,ptr2、ptr4自动释放内存
std::shared_ptr
基本特性:共享所有权,通过引用计数记录有多少个 shared_ptr 管理同一块内存,当计数为 0 时自动释放
适用场景:需要多个指针共享同一资源(如:容器中存储的对象被多个地方引用)
#include <memory> using namespace std;int main() {auto ptr1 = make_shared<int>(30);shared_ptr<int> ptr2 = ptr1; // 引用计数变为2{ //使用大括号定义了一个 “临时代码块” ---> 定义了一个局部作用域shared_ptr<int> ptr3 = ptr1; // 引用计数变为3} // ptr3销毁,引用计数变回2} // ptr1、ptr2销毁,引用计数变为0,内存释放
std::weak_ptr
基本特性:弱引用,不增加引用计数,用于解决 shared_ptr 的循环引用问题(两个 shared_ptr 互相引用导致计数无法归零)
适用场景:作为观察者关联共享资源,不参与所有权管理
#include <memory> using namespace std;struct Node {weak_ptr<Node> next; // 用weak_ptr避免循环引用 };int main() {auto node1 = make_shared<Node>();auto node2 = make_shared<Node>();node1->next = node2; // weak_ptr不增加计数node2->next = node1; } // 计数正常归零,内存释放
总结:
- 独占资源 →
unique_ptr
—> 适合 “一对一” 的场景- 共享资源 →
shared_ptr
—> 适合 “多对一” 的场景- 观察资源 →
weak_ptr
—> 解决循环引用
3. 怎么使用智能指针?
使用 C++ 智能指针的核心是利用其自动管理内存的特性,避免手动
new
/delete
导致的问题。以下是三种常用智能指针的具体使用方法和场景:
一、std::unique_ptr(独占所有权)
#include <iostream>
#include <memory> // 需包含智能指针头文件
using namespace std;int main()
{/*--------------- 创建 unique_ptr(管理单个对象)---------------*///方式1:直接绑定 new 分配的内存(不推荐,存在异常安全风险)unique_ptr<int> ptr1(new int(10));//方式2:用 make_unique 创建(C++14 推荐,更安全)auto ptr2 = make_unique<int>(20); // 自动推导类型/*--------------- 访问所管理的对象(同普通指针)---------------*/*ptr2 = 30; // 修改值cout << *ptr2 << endl; // 输出:30/*--------------- 转移所有权(只能用 move,原指针会变为空)---------------*/unique_ptr<int> ptr3 = move(ptr2); //注意:ptr2 不再拥有内存if (ptr2 == nullptr) {cout << "ptr2 已为空" << endl;}/*--------------- 管理动态数组(需指定数组类型)---------------*/unique_ptr<int[]> arr_ptr = make_unique<int[]>(5); // 5个int的数组arr_ptr[0] = 1; // 数组访问
}
// 离开作用域时,所有 unique_ptr 自动释放内存(无需手动 delete)
二、std::shared_ptr(共享所有权)
#include <iostream>
#include <memory>
using namespace std;int main()
{//1.创建 shared_ptr(推荐用 make_shared)auto ptr1 = make_shared<int>(100); // 引用计数 = 1//2.共享所有权(拷贝指针时,引用计数增加)shared_ptr<int> ptr2 = ptr1; // 引用计数 = 2shared_ptr<int> ptr3 = ptr2; // 引用计数 = 3//3.访问对象(同普通指针)*ptr3 = 200;cout << *ptr1 << endl; // 输出:200(所有指针指向同一内存)//4.查看引用计数(use_count() 仅用于调试)cout << "计数:" << ptr1.use_count() << endl; // 输出:3//5.局部作用域演示计数变化{shared_ptr<int> ptr4 = ptr1; // 计数 = 4} // ptr4 销毁,计数 = 3//6.手动释放(重置指针,计数减少)ptr2.reset(); // ptr2 不再指向内存,计数 = 2
}
// ptr1、ptr3 销毁,计数 = 0 → 内存自动释放
三、std::weak_ptr(弱引用,解决循环引用)
#include <iostream>
#include <memory> // 包含智能指针所需的头文件
using namespace std;// 定义链表节点结构
// 场景:链表节点之间可能互相引用,容易引发shared_ptr的循环引用问题
struct Node
{int value; // 节点存储的值weak_ptr<Node> next; // 指向链表下一个节点的弱指针//关键:使用weak_ptr而非shared_ptr,避免循环引用
};int main()
{//1.创建两个共享指针,分别管理两个Node对象auto node1 = make_shared<Node>(); // node1的引用计数为1auto node2 = make_shared<Node>(); // node2的引用计数为1//注意: make_shared是创建shared_ptr的推荐方式,安全且高效//2.构建节点间的互相引用关系node1->next = node2; // weak_ptr接收shared_ptr时,不增加node2的引用计数(仍为1)node2->next = node1; // 同理,node1的引用计数仍为1//注意:若此处用shared_ptr,会导致引用计数循环增加,无法归零//3.1:temp是有效的shared_ptr,说明node2仍存在if (auto temp = node1->next.lock()) //注意:访问weak_ptr指向的对象:必须通过lock()方法转换为shared_ptr{cout << "node2 有效" << endl;}//3.2:若node2已被释放,进入此分支else{cout << "node2 已释放" << endl;}/* 说明:lock()的作用:检查被引用的对象是否还存在* 1. 若存在:返回一个指向该对象的shared_ptr(此时引用计数临时+1)* 2. 若已释放:返回空的shared_ptr */} // main函数结束,局部变量node1和node2开始销毁// 1. node2的引用计数从1减为0 → 其管理的Node对象被释放// 2. node1的引用计数从1减为0 → 其管理的Node对象被释放// 3. 由于使用weak_ptr,没有循环引用,所有内存正常释放(无内存泄漏)
4. 为什么需要智能指针?
在 C++ 中,智能指针的出现主要是为了:解决手动管理动态内存时容易出现的问题,其核心价值在于自动管理内存生命周期,避免内存相关的 bug。
具体来说,需要智能指针的原因可以从以下几个方面理解:
1. 避免内存泄漏
手动管理动态内存(使用
new
分配、delete
释放)时,若因逻辑疏漏导致delete
未执行,会造成内存泄漏(已分配的内存无法回收,直到程序结束)void func() {int* ptr = new int(10); // 分配内存if (someCondition) {return; // 提前返回,导致后续的delete未执行}delete ptr; // 若if条件成立,此行不会执行,内存泄漏 }
智能指针会在自身生命周期结束时(如:超出作用域、被销毁)自动调用delete,无论程序执行路径如何(即使有提前返回、异常抛出等),都能保证内存被释放
2. 防止重复释放
手动释放内存时,若对同一块内存多次调用
delete
,会导致未定义行为(程序崩溃、数据损坏等)void func() {int* ptr1 = new int(10);int* ptr2 = ptr; // 两个指针指向同一块内存delete ptr1;delete ptr2; // 重复释放,行为未定义 }
智能指针通过引用计数等机制追踪内存的引用情况,只有当最后一个引用它的智能指针被销毁时,才会真正释放内存,避免重复释放
3. 应对异常安全
当程序抛出异常时,手动管理的内存可能因
delete
语句被跳过而泄漏void func() {int* ptr = new int(10);try {someOperation(); // 若此函数抛出异常}catch (...) {// 如果发生了异常:且未在catch中手动释放ptr,内存泄漏throw;}delete ptr; // 如果未发生了异常:ptr指向的资源将在这里释放 }
智能指针的析构函数会在异常发生时被自动调用(C++ 的栈展开机制),确保内存释放,无需手动在异常处理中额外处理
4. 简化内存管理逻辑
复杂程序中,动态内存的所有权可能在多个函数、对象之间传递,手动追踪所有权并确定
delete
的时机非常困难智能指针通过明确的所有权语义(如:
unique_ptr
的独占所有权、shared_ptr
的共享所有权),简化了内存管理的逻辑,降低了人为出错的概率
总结:
C++ 没有垃圾回收机制,动态内存的分配与释放完全依赖程序员手动控制,这使得内存管理成为 C++ 开发中的常见痛点(
内存泄漏
、重复释放
、异常安全
等)智能指针通过封装原始指针,在编译期和运行期自动处理内存的释放时机,本质上是
用对象管理资源
:“资源获取即初始化”(RAII:Resource Acquisition Is Initialization)思想的体现,大幅提升了代码的安全性和可维护性
5. 手动new/delete VS 智能指针 谁更胜一筹?
手动内存管理的泄露痛点:
当代码中混合使用
new动态分配内存
和异常抛出
时,若异常触发导致 delete 未执行,会直接引发 内存泄漏。更复杂的是:
- 多个 new 操作可能各自抛异常(如:内存不足)
- 业务逻辑(如:Divide 函数)也可能抛异常
- 手动处理这些情况需要嵌套大量
try/catch
,代码会变得冗余、难维护
代码示例:手动内存管理的困境
#include <iostream>
#include <string>
using namespace std;//1. 业务函数:除法运算,除数为0时抛异常
double Divide(int a, int b)
{if (b == 0){// 抛C风格字符串异常throw "Divide by zero condition!";}return static_cast<double>(a) / b;
}//2. 演示函数:手动管理内存 + 异常处理的复杂逻辑
void Func()
{//2.1:动态分配两个数组int* array1 = new int[10];int* array2 = new int[10]; // 若内存不足,此行抛异常,导致array1泄漏//2.2:包裹可能出现异常的程序try{//1.定义并输入两个int类型的操作数int a, b;cin >> a >> b;//2.输出运算结果cout << Divide(a, b) << endl;}//2.3:捕获所有异常(但不处理,仅释放内存后重新抛出)catch (...){//1.输出报错信息cout << "delete [] " << array1 << endl;cout << "delete [] " << array2 << endl;//2.释放内存delete[] array1;delete[] array2;//3.重新抛出异常,交给上层处理throw;}//2.3:冗余的防御性释放(永远不会执行)cout << "delete [] " << array1 << endl;delete[] array1;cout << "delete [] " << array2 << endl;delete[] array2;/* 说明:* 1. 若try块内无异常,流程走到这里才会释放* 2. 但如果try内抛异常,会进入catch,导致此行被跳过 → 无意义*/
}//3. 主函数:调用Func并处理异常
int main()
{//3.1:包裹可能出现异常的程序try{Func();}//3.2:捕获C风格字符串异常catch (const char* errmsg){cout << errmsg << endl;}//3.3:捕获标准异常catch (const exception& e){cout << e.what() << endl;}//3.4:捕获未知异常catch (...){cout << "未知异常" << endl;}return 0;
}
手动new/delete的不足之处:
内存泄漏风险:
若
array2 = new int[10]
抛异常,array1
未被释放 → 泄漏若
try
块内抛异常,catch
外的delete
永远不会执行 → 泄漏异常处理冗余:
需要嵌套try/catch
确保每个new
都被释放,代码复杂度随new
数量指数级上升
智能指针的优雅处理:
通过
std::unique_ptr
管理动态内存,利用 RAII 自动释放,无需手动delete
代码可简化为:
#include <iostream>
#include <memory> // 包含智能指针所需的头文件(unique_ptr、make_unique等)
#include <stdexcept> // 包含标准异常类(如:runtime_error)
using namespace std;//1. 除法运算函数
double Divide(int a, int b)
{//情况一:除数是0抛出异常if (b == 0){// 抛标准异常:使用std::runtime_error(继承自std::exception)throw runtime_error("Divide by zero condition!"); //注:相比C风格字符串异常,标准异常支持多态捕获,信息更规范}//情况二:除数不为0返回计算结果return static_cast<double>(a) / b; //注:转换为double类型后再除法,保证结果精度
}//2. 演示智能指针自动管理内存的函数
void Func()
{//2.1:使用unique_ptr管理动态数组(int[]表示数组类型)unique_ptr<int[]> array1 = make_unique<int[]>(10);unique_ptr<int[]> array2 = make_unique<int[]>(10);/* 说明:make_unique<int[]>(10):创建包含10个int元素的动态数组,返回unique_ptr* 1. 优势1:无需手动调用new,避免忘记初始化的风险* 2. 优势2:离开作用域时自动调用delete[]释放内存(无论正常执行还是异常)*///2.2:包裹可能出现异常的程序try{//1.定义并输入两个int类型的操作数int len, time;cin >> len >> time;//2.调用Divide函数,可能抛出异常cout << Divide(len, time) << endl;}//2.3:捕获所有std::exception派生的异常(包括runtime_error)catch (const exception& e){//1.输出异常信息(e.what()返回异常描述字符串)cout << "捕获异常: " << e.what() << endl;//2.重新抛出异常,让上层函数(如:main)继续处理throw; //注意:此时array1和array2尚未释放,会在Func()函数结束时自动释放}
} // Func()函数结束,局部变量array1和array2离开作用域// 自动调用unique_ptr的析构函数,执行delete[]释放动态数组// 无论是否发生异常,此过程都会执行,彻底避免内存泄漏//3. 主函数
int main()
{//3.1:try{Func();}//3.2:捕获Func()中抛出的异常(包括重新抛出的异常)catch (const exception& e){// 输出最终的异常处理信息cout << "主函数捕获: " << e.what() << endl;}return 0;
}
总结对比:智能指针通过 RAII 机制 完美解决了手动内存管理与异常处理的冲突,是现代 C++ 开发的推荐实践。
方案 | 内存管理方式 | 异常处理复杂度 | 泄漏风险 | 代码简洁度 |
---|---|---|---|---|
手动 new/delete | 手动控制 | 高(需嵌套) | 高 | 低 |
智能指针 | RAII 自动管理 | 低(无需手动) | 无 | 高 |
6. 智能指针为什么这么吊?
一、智能指针的核心本质
智能指针(unique_ptr/shared_ptr)是 RALL思想 的典型实现,RALL 设计思想:用对象生命周期管理资源
- RALL 是 Resource Acquisition Is Initialization 的缩写,中文译为 “资源获取即初始化”
它是一种管理动态资源的设计思想,核心逻辑是:
资源获取:在对象构造时获取资源(如:内存、文件句柄、网络连接)
资源持有:资源在对象的生命周期内始终有效
资源释放:对象析构时自动释放资源(无需手动调用
delete
/close
等)通过绑定
“资源的生命周期”
与“对象的生命周期”
,避免因手动管理失误(如:忘记释放、异常导致跳过释放)引发的资源泄漏。
RALL解决问题:传统手动管理资源的痛点
void func() {// 1. 获取资源(动态内存)int* ptr = new int[10];// 2. 业务逻辑(可能抛异常、提前返回)if (someCondition){return; // 直接返回,导致 ptr 未释放 → 内存泄漏}// 3. 释放资源(若流程未被打断才会执行)delete[] ptr; }
RALL如何解决:用对象封装资源,析构时自动释放
class ArrayWrapper
{
private:int* ptr; // 管理的资源(动态内存)public://1.构造时获取资源(动态内存)ArrayWrapper() : ptr(new int[10]) {}//2.析构时释放资源(无论对象如何销毁,析构函数必执行)~ArrayWrapper() { delete[] ptr; }//3.禁用拷贝(避免资源重复释放)ArrayWrapper(const ArrayWrapper&) = delete;ArrayWrapper& operator=(const ArrayWrapper&) = delete;//3.提供资源访问接口int& operator[](int idx) { return ptr[idx]; }
};void func()
{// 构造对象 → 获取资源ArrayWrapper arr;// 业务逻辑(即使抛异常、提前返回,对象析构时会释放资源)if (someCondition){return; // 析构 arr → 自动释放内存}} // 离开作用域 → 析构 arr → 自动释放内存
二、智能指针的模拟实现
#include <iostream>
#include <string>
using namespace std;// ------------------------------
// 1. 模拟实现智能指针(RAII 思想)
// 目标:管理动态数组,自动释放内存
// ------------------------------
template<class T>
class SmartPtr
{
private:T* _ptr; // 管理的动态数组指针public:/*----------------------默认成员函数----------------------*///1.实现:“构造函数”SmartPtr(T* ptr) //参数:ptr 是 new 分配的数组指针: _ptr(ptr){// 构造时获取资源,无需额外操作(资源由外部 new 分配)}//2.实现:“析构函数”~SmartPtr(){//2.1:显示释放的地址cout << "delete[] " << _ptr << endl;//2.2:自动释放动态数组delete[] _ptr;}/*----------------------重载运算符----------------------*///1.重载 operator*:支持解引用(类似原始指针 *ptr)T& operator*(){return *_ptr; // 返回数组首元素的引用(注意:数组解引用无意义,仅演示语法)}//2.重载 operator->:支持成员访问(类似原始指针 ptr->)T* operator->(){return _ptr; // 返回原始指针}//3.重载 operator[]:支持数组访问(类似原始数组 ptr[i])T& operator[](size_t i){return _ptr[i]; // 返回数组第 i 个元素的引用}
};// ------------------------------
// 2. 业务函数:除法运算(可能抛异常)
// ------------------------------
double Divide(int a, int b)
{if (b == 0){throw "Divide by zero condition!"; //抛 C 风格字符串异常}return static_cast<double>(a) / b; //正常除法(转换为 double 避免整数截断)
}// ------------------------------
// 3. 演示函数:用模拟智能指针管理资源
// ------------------------------
void Func()
{//3.1:用 SmartPtr 管理动态数组(长度为 10)SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];//3.2:初始化数组元素for (size_t i = 0; i < 10; i++){sp1[i] = i; // 通过 operator[] 访问数组sp2[i] = i; // 通过 operator[] 访问数组}//3.3:业务逻辑:输入并调用 Divideint len, time;cin >> len >> time;cout << Divide(len, time) << endl;
}// ------------------------------
// 4. 主函数:调用 Func 并处理异常
// ------------------------------
int main()
{try{Func(); // 调用可能抛异常的函数}// 捕获 C 风格字符串异常catch (const char* errmsg){cout << errmsg << endl;}// 捕获标准异常(若有)catch (const exception& e){cout << e.what() << endl;}// 捕获未知异常catch (...){cout << "未知异常" << endl;}return 0;
}
7. 使用智能指针的好处有哪些?
智能指针的核心优势:
- 自动释放:无需手动调用
delete
,即使程序因异常跳转,也能保证内存被释放- 避免泄漏:解决了
“忘记释放”、“重复释放”、“释放后继续使用”
等常见问题- 清晰语义:通过
unique_ptr
(独占)和shared_ptr
(共享)清晰表达资源的管理方式
8. 关于智能指针有哪些注意事项?
智能指针的通用设计细节:
(1)RAII 与资源释放
智能指针的析构函数默认调用 delete 释放资源
若管理的资源不是new分配的(如:文件句柄、网络连接),需自定义删除器
- 注意:避免用智能指针管理非动态内存,否则会导致 delete 栈内存的未定义行为
/*------------- 自定义删除器:释放文件句柄 -------------*/ void deleteFile(FILE* fp) {fclose(fp); }shared_ptr<FILE> p(fopen("test.txt", "w"),deleteFile // 析构时调用 deleteFile 释放资源 );
(2)数组特化支持
unique_ptr和shared_ptr可通过模板特化支持动态数组
unique_ptr<int[]> arr1 = make_unique<int[]>(10); // 管理 10 个 int 的数组 shared_ptr<int[]> arr2 = make_shared<int[]>(10); // 同理arr1[0] = 100; // 支持数组下标访问
(3)推荐的创建方式 make_shared/make_unique
相比直接new(如:
shared_ptr<int>(new int(10))
),工厂函数更安全(避免内存泄漏风险)且高效auto p1 = make_unique<int>(10); // 推荐auto p2 = unique_ptr<int>(new int(10)); // 不推荐(异常安全风险)
(4)空指针判断与类型安全
支持operator bool:直接判断智能指针是否为空(管理资源)
if (p1) // 等价于 p1 != nullptr {cout << "p1 管理资源" << endl; }
构造函数用explicit修饰:禁止普通指针隐式转换为智能指针,避免误操作
// 编译报错(禁止隐式转换) shared_ptr<int> p = new int(10); // 正确写法 shared_ptr<int> p(new int(10));
代码案例:智能指针的使用细节
#include <iostream>
#include <memory>
using namespace std;/*-----------------定义:“日期类”-----------------*/
struct Date
{int _year; // 年int _month; // 月int _day; // 日//1.实现:“构造函数”Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){ }//2.实现:“析构函数”~Date(){//当对象被销毁时输出提示信息,便于追踪智能指针的释放行为cout << "~Date()" << endl; }
};int main()
{/*----------------------------------------- auto_ptr -----------------------------------------*///1.创建auto_ptr智能指针,管理一个Date对象auto_ptr<Date> ap1(new Date); /* 说明:auto_ptr(C++98 遗留,已废弃)* 1. 特性:管理权唯一,拷贝时会转移所有权* 2. 注意:C++11后已被弃用,不推荐使用,存在潜在风险*/auto_ptr<Date> ap2(ap1);/** 1. 拷贝auto_ptr会导致所有权转移:ap1失去管理权,ap2获得管理权* 2. 此时ap1变为悬空指针,指向已被转移的对象** -------------------------------------------------------------------** 3. 危险操作:ap1已空悬,访问其指向的对象会导致未定义行为(可能崩溃)* 4. 这里注释掉以避免运行时错误*/// ap1->_year++; /*----------------------------------------- unique_ptr -----------------------------------------*///2.创建unique_ptr智能指针,管理一个Date对象unique_ptr<Date> up1(new Date); /* 说明:unique_ptr(C++11 推荐,独占所有权)* 1. 特性:严格独占所管理的对象,禁止拷贝,只支持移动* 2. 适用场景:需要唯一管理资源,避免共享的情况*/// unique_ptr<Date> up2(up1); /** 1. 禁止拷贝:unique_ptr的拷贝构造函数被删除,上面的代码会编译报错* 2. 这是设计上的保护,防止多个unique_ptr管理同一个对象** -------------------------------------------------------------------** 3. 支持移动语义:通过std::move()转移所有权* 4. 转移后,up1变为悬空指针,up3获得对象的管理权*/unique_ptr<Date> up3(move(up1));/*----------------------------------------- shared_ptr -----------------------------------------*///3. 创建shared_ptr智能指针,管理一个Date对象shared_ptr<Date> sp1(new Date); /* 说明:shared_ptr(C++11 推荐,共享所有权)* 1. 特性:允许多个shared_ptr共享同一个对象的所有权* 2. 内部通过引用计数(reference count)跟踪对象被多少指针共享* 3. 当引用计数为0时,自动释放所管理的对象*///3.1:支持拷贝:新的shared_ptr会共享对象所有权,引用计数加1shared_ptr<Date> sp2(sp1); //此时引用计数变为2 //3.2:再次拷贝,引用计数变为3shared_ptr<Date> sp3(sp2);//3.3:输出当前引用计数 ---> 注:use_count()方法返回当前共享该对象的shared_ptr数量cout << "sp1的引用计数: " << sp1.use_count() << endl; //3.4:通过智能指针访问对象成员(与普通指针用法类似)sp1->_year++; //3.5:验证所有共享指针都指向同一个对象 --> 输出结果均为2,证明它们共享同一个Date对象cout << "sp1指向的年份: " << sp1->_year << endl;cout << "sp2指向的年份: " << sp2->_year << endl;cout << "sp3指向的年份: " << sp3->_year << endl;//3.6:支持移动语义:通过std::move()转移所有权shared_ptr<Date> sp4(move(sp1));/* 注意:* 1. 转移后,sp1变为悬空指针,sp4获得对象的管理权* 2. 注意:移动操作不会增加引用计数,只是转移管理权限*/// 程序结束时,所有智能指针会自动释放所管理的对象// 对于shared_ptr,当最后一个管理对象的指针销毁时,才会调用Date的析构函数// 此处会输出一次"~Date()",因为所有shared_ptr共享的是同一个对象// unique_ptr和auto_ptr管理的对象也会在此处自动释放return 0;
}
------------ 删除器 ------------
1. 什么是删除器?
删除器
:是智能指针(如:unique_ptr
、shared_ptr
)用于释放所管理资源的一个重要机制。
- 它的核心作用是定义资源的释放方式,确保智能指针在生命周期结束时,能正确、安全地回收所管理的资源
- 它允许自定义资源释放的方式,而不仅限于 delete 操作,这使得智能指针可以管理各种类型的资源,如:文件句柄、网络连接、锁等
2. 为什么要引入删除器?
智能指针默认使用
delete
运算符释放资源,但实际场景中存在很多特殊情况:
- 动态数组:需要用
delete[]
释放(而非delete
)- 非内存资源(如:文件句柄
FILE*
、网络套接字socket
):需要调用特定函数释放(如:fclose
、closesocket
)- 自定义的资源释放逻辑(如:日志记录、状态清理等)
此时,默认的
delete
无法满足需求,必须通过自定义删除器来指定资源的释放方式。总结:使用删除器可以管理非 new 分配的资源(如:malloc、fopen 分配的资源)
3. 删除器的本质是什么?
删除器本质是一个 “可调用对象”,它可以是:
普通函数
仿函数
(重载 operator() 的类对象)lambda 表达式
函数指针
std::function包装的函数
- …………
当智能指针需要释放其管理的资源时,会调用这个删除器而不是简单的 delete
4. 删除器怎么使用?
一、与 unique_ptr 配合使用
unique_ptr 是独占所有权的智能指针,其模板参数必须显式指定删除器的类型(删除器类型是 unique_ptr 类型的一部分)
1. 函数指针作为删除器
#include <memory>
#include <iostream>
using namespace std; /*------------------------ 定义一个资源类 ------------------------*/
struct Resource
{int id; // 资源的唯一标识,用于区分不同的Resource对象Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }~Resource() { cout << "Resource " << id << " 被默认释放\n"; }
};/*------------------------ 定义“函数指针”删除器 ------------------------*/
template<class T>
void ArrayDeleter(T* ptr) //注意:数组中元素的类型(此处为Resource)
{if (ptr) //安全检查:避免对空指针执行释放操作{//1.显示使用自定义删除器释放数组的提示cout << "用 ArrayDeleter 释放数组\n"; //2.用delete[]释放动态数组(与new[]配对)delete[] ptr; }
}int main()
{// 创建unique_ptr智能指针,管理一个包含3个Resource对象的动态数组unique_ptr<Resource, void(*)(Resource*)> up(new Resource[3]{ 1, 2, 3 }, // 动态数组:用new[]创建3个Resource对象,初始化ID为1、2、3ArrayDeleter<Resource> // 绑定删除器:指定用ArrayDeleter<Resource>函数释放资源);//注意:unique_ptr的模板参数必须显式包含删除器类型,因为删除器类型是unique_ptr类型的一部分/* 模板参数说明:<资源类型, 删除器类型(函数指针)>* 1. 第一个参数 Resource:表示智能指针管理的资源类型(数组元素类型)* 2. 第二个参数 void(*)(Resource*):表示删除器的类型(函数指针类型,指向接收Resource*参数、返回void的函数)**/
}
/*
* main函数结束,up的生命周期结束
* 此时unique_ptr会自动调用绑定的删除器(ArrayDeleter<Resource>)释放管理的动态数组
*/
2. 仿函数作为删除器
- 仿函数是重载了
operator()
的类,适合需要复用或带状态的删除逻辑
# define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std; /*------------------------ 定义“仿函数”删除器 ------------------------*/
class FileDeleter
{
public:void operator()(FILE* fp) const //参数fp:指向FILE类型的指针(文件句柄),即需要释放的资源{if (fp) // 安全检查:避免对空指针执行操作(fclose(nullptr)可能导致崩溃){//1.显示当前关闭的文件句柄(便于追踪资源释放)cout << "用 FileDeleter 关闭文件\n";//2.调用标准库函数fclose关闭文件,释放系统资源fclose(fp); //替代手动调用fclose,由智能指针自动触发}}
};int main()
{// 创建unique_ptr智能指针,管理文件句柄(FILE*类型资源)unique_ptr<FILE, FileDeleter> up_file(fopen("test.txt", "r"), // 资源初始化:调用fopen打开文件,返回FILE*句柄FileDeleter() // 绑定删除器:传入FileDeleter的实例(可省略,因unique_ptr会默认构造)); //注:"r"表示只读模式,若文件不存在则fopen返回nullptr// 注意:若fopen失败(返回nullptr),删除器也会安全处理(因operator()内有if(fp)判断)/* 模板参数说明:<资源类型, 仿函数类型>* 1. 第一个参数 FILE:表示智能指针管理的资源类型(文件句柄类型)* 2. 第二个参数 FileDeleter:表示删除器的类型(仿函数类)*/// 当up_file生命周期结束时(main函数退出),发生以下过程:// 1. unique_ptr自动调用绑定的删除器(FileDeleter的operator())// 2. 删除器调用fclose关闭文件,确保资源不泄漏// 无需手动调用fclose,避免了“忘记关闭文件”导致的资源泄漏return 0;
}
3. lambda 表达式作为删除器
- lambda 表达式简洁灵活,适合临时的简单释放逻辑
#include <iostream>
#include <memory>
using namespace std; /*------------------------ 定义一个资源类 ------------------------*/struct Resource{int id; // 资源的唯一标识,用于区分不同的Resource对象Resource(int i) : id(i) { cout << "Resource " << id << " 创建\n"; }~Resource() { cout << "Resource " << id << " 被默认释放\n"; }};int main()
{/*------------------------ 定义“lambda表达式”删除器 ------------------------*/auto lambda_deleter = [](Resource* ptr) { //lambda捕获列表为空([]),参数为Resource*(指向数组的指针)if (ptr) // 安全检查:避免对空指针执行释放操作{//1.标识当前使用lambda删除器释放数组cout << "用 lambda 释放数组\n";//2.用delete[]释放动态数组(与new[]配对)delete[] ptr;}};// 创建unique_ptr智能指针,管理包含2个Resource对象的动态数组unique_ptr<Resource, decltype(lambda_deleter)> up(new Resource[2]{ 4, 5 }, // 动态数组:用new[]创建2个Resource对象,ID为4和5lambda_deleter // 绑定删除器:指定用上面定义的lambda表达式释放资源);//注意:因lambda类型是编译器自动生成的匿名类型,必须用decltype推导/* 模板参数说明:<资源类型, decltype(lambda)>(通过 decltype 获取 lambda 类型)* 1. 第一个参数 Resource:表示智能指针管理的资源类型(数组元素类型)* 2. 第二个参数 decltype(lambda_deleter):通过decltype获取lambda的类型*//* 当up的生命周期结束时(main函数退出):* 1. unique_ptr自动调用绑定的lambda_deleter删除器* 2. lambda删除器先打印"用 lambda 释放数组"* 3. 再调用delete[]释放数组(触发元素析构,打印"被默认释放")* 整个过程无需手动释放,避免内存泄漏*/return 0;
}
二、与 shared_ptr 配合使用
shared_ptr 是共享所有权的智能指针,其模板参数不需要包含删除器类型(删除器类型不影响 shared_ptr 本身的类型),只需在构造时传入删除器即可,这使得 shared_ptr 对删除器的使用更灵活
1. 函数指针作为删除器
/*---------------------注意:定义“资源类 + 函数指针删除器”同上(这里省略了)---------------------*/int main()
{// 创建shared_ptr智能指针,管理包含3个Resource对象的动态数组shared_ptr<Resource> sp(new Resource[3]{ 1, 2, 3 }, // 动态数组:用new[]创建3个Resource对象,ID为1、2、3ArrayDeleter<Resource> // 绑定删除器:传入ArrayDeleter<Resource>函数作为释放逻辑);/* 注意:* 1. shared_ptr模板参数仅需指定资源类型(Resource),无需包含删除器类型* 2. shared_ptr的删除器在构造时传入,不影响智能指针类型*/// shared_ptr的核心特性:支持共享所有权,可以创建多个shared_ptr共享同一资源(引用计数会自动增加)// 例如:// shared_ptr<Resource> sp2 = sp; // 此时引用计数为2/* 当最后一个持有该资源的shared_ptr(如sp、sp2等)生命周期结束时:* 1. 引用计数减为0,触发资源释放* 2. 自动调用绑定的ArrayDeleter<Resource>删除器* 3. 删除器执行:先打印释放提示,再用delete[]释放数组(调用元素析构函数)* 整个过程无需手动管理内存,彻底避免内存泄漏*/return 0;
}
2. 仿函数作为删除器
/*---------------------注意:定义“仿函数删除器”同上(这里省略了)---------------------*/int main()
{// 创建shared_ptr智能指针,管理文件句柄(FILE*类型的资源)shared_ptr<FILE> sp_file(fopen("test.txt", "r"), // 资源初始化:调用fopen打开文件FileDeleter() // 绑定删除器:传入FileDeleter仿函数的实例); /* 注意事项:* 1. "r"表示只读模式,成功则返回FILE*指针,失败返回nullptr* 2. shared_ptr模板参数仅需指定资源类型(FILE),无需包含删除器类型* 2. shared_ptr构造时直接传入删除器,不影响智能指针类型* 3. 若fopen失败(返回nullptr),删除器会通过if(fp)判断跳过操作,避免错误*/// shared_ptr支持共享所有权:多个shared_ptr可共享同一文件句柄// 例如:// shared_ptr<FILE> sp_file2 = sp_file; // 引用计数+1/* 当最后一个持有该文件句柄的shared_ptr(如:sp_file、sp_file2)生命周期结束时:* 1. 引用计数减为0,触发资源释放* 2. 自动调用FileDeleter仿函数的operator()方法* 3. 仿函数内部调用fclose关闭文件,确保资源不泄漏* 无需手动调用fclose,避免“忘记关闭文件”导致的系统资源泄漏*/return 0;
}
3. lambda 表达式作为删除器
/*---------------------注意:定义“资源类”同上(这里省略了)---------------------*/int main()
{// 创建shared_ptr智能指针,管理包含2个Resource对象的动态数组shared_ptr<Resource> sp(new Resource[2]{ 6, 7 }, // 动态数组:用new[]创建2个Resource对象,ID为6和7[](Resource* ptr) // 直接传入lambda表达式作为删除器{ if (ptr) // 安全检查:避免对空指针执行释放操作{ //1.标识当前使用lambda删除器释放数组cout << "用 lambda 释放数组\n";//2.用delete[]释放动态数组(与new[]配对)delete[] ptr;}});//注意:shared_ptr模板参数仅需指定资源类型(Resource),无需包含删除器类型// shared_ptr的特性:支持共享所有权:可以创建多个shared_ptr共享同一资源,引用计数会自动维护// 例如:// shared_ptr<Resource> sp2 = sp; // 此时引用计数增加到2/* 当最后一个持有该资源的shared_ptr(如:sp、sp2等)生命周期结束时:* 1. 引用计数减少到0,触发资源释放机制* 2. 自动调用绑定的lambda表达式(删除器)* 3. lambda删除器执行流程:* - 先打印"用 lambda 释放数组"* - 再调用delete[]释放数组,此时会依次调用数组中每个元素的析构函数* - 最终完成数组内存的释放,避免内存泄漏*/// 优势:lambda表达式作为删除器无需提前定义,适合简单的释放逻辑,代码更紧凑return 0;
}
代码案例:删除器使用的总结
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory> // 智能指针头文件
#include <cstdio> // 文件操作相关函数
using namespace std;/*---------------------------定义:“日期类”---------------------------*/
struct Date
{int _year; // 年int _month; // 月int _day; // 日//1.实现:“构造函数”Date(int year = 1, int month = 1, int day = 1): _year(year), _month(month), _day(day){}//2.实现:“析构函数”~Date(){//当对象被销毁时输出提示信息,便于追踪智能指针的释放行为cout << "~Date()" << endl;}
};/*---------------------------定义“函数指针”删除器:删除动态数组---------------------------*/
template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr; // 使用delete[]释放动态数组,与new[]配对
}/*---------------------------定义“仿函数”删除器:删除动态数组---------------------------*/
template<class T>
class DeleteArray
{
public:// 重载()运算符,使类的对象可以像函数一样被调用void operator()(T* ptr){delete[] ptr; // 使用delete[]释放动态数组}
};/*---------------------------定义“仿函数”删除器:关闭文件句柄---------------------------*/
class Fclose
{
public:// 重载()运算符,接收FILE*类型的指针void operator()(FILE* ptr){//1.显示关闭的文件指针cout << "fclose:" << ptr << endl;//2.调用标准库函数,关闭文件fclose(ptr);}
};int main()
{/*----------------------------------- 案例一:智能指针管理“动态数组” -----------------------------------*/// unique_ptr<Date> up1(new Date[10]); // shared_ptr<Date> sp1(new Date[10]); /* 错误示例:直接用智能指针管理数组会导致内存泄漏* 1. 原因:智能指针默认使用delete释放资源,而动态数组需要用delete[]* 2. 所以:以上两行代码会编译通过,但运行时会产生未定义行为(内存泄漏或崩溃)*//*--------------- 解决方案1:利用智能指针的数组特化版本 --------------*///注:unique_ptr和shared_ptr都提供了数组版本的特化,会自动使用delete[]unique_ptr<Date[]> up1(new Date[5]); // 管理包含5个Date对象的动态数组shared_ptr<Date[]> sp1(new Date[5]); // 数组特化版本,自动调用delete[]/*--------------- 解决方案2.1:自定义删除器(函数指针版本)--------------*///1. unique_ptr需指定函数指针类型作为模板参数unique_ptr<Date, void(*)(Date*)> up2(new Date[5], DeleteArrayFunc<Date>);//2. shared_ptr对函数指针的支持更简洁,直接传入函数名即可shared_ptr<Date> sp2(new Date[5], DeleteArrayFunc<Date>);/*--------------- 解决方案2.2:自定义删除器(仿函数版本)--------------*///1. unique_ptr需要在模板参数中显式指定删除器类型unique_ptr<Date, DeleteArray<Date>> up3(new Date[5], DeleteArray<Date>());//2. shared_ptr不需要在模板参数中指定删除器类型,直接在构造函数中传入即可shared_ptr<Date> sp3(new Date[5], DeleteArray<Date>());/*--------------- 解决方案2.3:自定义删除器(lambda表达式版本)--------------*/// 定义一个lambda表达式,用于释放动态数组auto delArrOBJ = [](Date* ptr) { delete[] ptr; };//1. unique_ptr需要用decltype获取lambda的类型作为模板参数unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);//2. shared_ptr可以直接使用lambda作为删除器,无需指定类型shared_ptr<Date> sp4(new Date[5], delArrOBJ);/*----------------------------------- 案例二:智能指针管理“非内存资源” -----------------------------------*///1.使用Fclose仿函数作为删除器shared_ptr<FILE> sp5(fopen("test.txt", "r"), Fclose());//2.使用lambda表达式作为删除器(功能与Fclose仿函数相同)shared_ptr<FILE> sp6(fopen("test.txt", "r"), [](FILE* ptr){cout << "fclose:" << ptr << endl; // 调试输出fclose(ptr); // 关闭文件});/* 程序结束时,所有智能指针会自动调用对应的删除器释放资源:* 1. 数组特化版本:调用delete[]* 2. 自定义删除器版本调用:对应的函数/仿函数/lambda* 3. 文件指针会被正确关闭,避免资源泄漏*/return 0;
}
5. unique_ptr与shared_ptr使用删除器的差异是什么?
总结:
unique_ptr
与shared_ptr
使用删除器的差异
特性 | unique_ptr | shared_ptr |
---|---|---|
模板参数是否包含删除器 | 是 (删除器类型是模板的一部分,如 unique_ptr<T, Deleter> ) | 否 (删除器类型不影响 shared_ptr 类型,仅在构造时传入) |
语法要求 | 必须显式指定删除器类型(或用 decltype 获取) | 无需指定删除器类型,直接传入删除器对象即可 |
灵活性 | 同一 unique_ptr 类型只能绑定固定类型的删除器 | 同一 shared_ptr 类型可绑定不同删除器(只要释放逻辑兼容) |
内存开销 | 若删除器是无状态的(如空仿函数),不增加额外开销 有状态删除器会增加大小 | 无论删除器类型,shared_ptr 本身大小固定(通常为两个指针:资源指针 + 控制块指针) |
6. 使用删除器的需要注意什么?
使用删除器的注意事项:
- 删除器的参数类型必须匹配:删除器接收的参数类型必须与智能指针管理的资源类型一致(如:管理
FILE*
资源,删除器必须接收FILE*
)- 避免删除空指针:删除器内部最好判断
ptr != nullptr
,避免对空指针执行释放操作(虽然delete nullptr
是安全的,但fclose(nullptr)
可能崩溃)- shared_ptr的删除器存储在控制块:
shared_ptr
的删除器与引用计数一起存在控制块中,因此即使删除器很大,也不会增加shared_ptr
本身的大小- 默认删除器的局限性:智能指针默认使用
delete
释放资源,因此管理动态数组
或非内存资源
时,必须显式指定删除器,否则会导致未定义行为
总结:
删除器的使用核心:根据资源类型,定义对应的释放逻辑,然后将其绑定到智能指针。
具体选择哪种形式的删除器(函数指针/仿函数 /lambda),取决于场景的复杂度和复用需求。